commit f9789412dab93cfb1bac8205435cc67df8f4f576 Author: Татьяна Фарбер Date: Thu Aug 20 14:01:08 2026 +0400 init diff --git a/DS1820.lua b/DS1820.lua new file mode 100644 index 0000000..7e40a84 --- /dev/null +++ b/DS1820.lua @@ -0,0 +1,209 @@ +-------------------------------------------------------------------------------- +-- DS18B20 one wire module for NODEMCU +-- NODEMCU TEAM +-- LICENCE: http://opensource.org/licenses/MIT +-- @voborsky, @devsaurus, TerryE 26 Mar 2017 +-------------------------------------------------------------------------------- +local modname = ... + +-- Used modules and functions +local type, tostring, pcall, ipairs = + type, tostring, pcall, ipairs +-- Local functions +local ow_setup, ow_search, ow_select, ow_read, ow_read_bytes, ow_write, ow_crc8, + ow_reset, ow_reset_search, ow_skip, ow_depower = + ow.setup, ow.search, ow.select, ow.read, ow.read_bytes, ow.write, ow.crc8, + ow.reset, ow.reset_search, ow.skip, ow.depower + +local node_task_post, node_task_LOW_PRIORITY = node.task.post, node.task.LOW_PRIORITY +local string_char, string_dump = string.char, string.dump +local now, tmr_create, tmr_ALARM_SINGLE = tmr.now, tmr.create, tmr.ALARM_SINGLE +local table_sort, table_concat = table.sort, table.concat +local file_open = file.open +local conversion + +local DS18B20FAMILY = 0x28 +local DS1920FAMILY = 0x10 -- and DS18S20 series +local CONVERT_T = 0x44 +local READ_SCRATCHPAD = 0xBE +local READ_POWERSUPPLY= 0xB4 +local MODE = 1 + +local pin, cb, unit = 3 +local status = {} + +local debugPrint = function() return end + +-------------------------------------------------------------------------------- +-- Implementation +-------------------------------------------------------------------------------- +local function enable_debug() + debugPrint = function (...) print(now(),' ', ...) end +end + +local function to_string(addr, esc) + if type(addr) == 'string' and #addr == 8 then + return ( esc == true and + '"\\%u\\%u\\%u\\%u\\%u\\%u\\%u\\%u"' or + '%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X '):format(addr:byte(1,8)) + else + return tostring(addr) + end +end + +local function readout(self) + local next = false + local sens = self.sens + local temp = self.temp + for i, s in ipairs(sens) do + if status[i] == 1 then + ow_reset(pin) + local addr = s:sub(1,8) + ow_select(pin, addr) -- select the sensor + ow_write(pin, READ_SCRATCHPAD, MODE) + local data = ow_read_bytes(pin, 9) + + local t=(data:byte(1)+data:byte(2)*256) + -- t is actually signed so process the sign bit and adjust for fractional bits + -- the DS18B20 family has 4 fractional bits and the DS18S20s, 1 fractional bit + t = ((t <= 32767) and t or t - 65536) * + ((addr:byte(1) == DS18B20FAMILY) and 625 or 5000) + local crc, b9 = ow_crc8(string.sub(data,1,8)), data:byte(9) + + if unit == 'F' then + t = (t * 18)/10 + 320000 + elseif unit == 'K' then + t = t + 2731500 + end + local sgn = t<0 and -1 or 1 + local tA = sgn*t + local tH=tA/10000 + local tL=(tA%10000)/1000 + ((tA%1000)/100 >= 5 and 1 or 0) + + if tH and (t~=850000) then + debugPrint(to_string(addr),(sgn<0 and "-" or "")..tH.."."..tL, crc, b9) + if crc==b9 then temp[addr]=t end + status[i] = 2 + end + end + next = next or status[i] == 0 + end + if next then + node_task_post(node_task_LOW_PRIORITY, function() return conversion(self) end) + else + --sens = {} + if cb then + node_task_post(node_task_LOW_PRIORITY, function() return cb(temp) end) + end + end +end + +conversion = (function (self) + local sens = self.sens + local powered_only = true + for _, s in ipairs(sens) do powered_only = powered_only and s:byte(9) ~= 1 end + if powered_only then + debugPrint("starting conversion: all sensors") + ow_reset(pin) + ow_skip(pin) -- skip ROM selection, talk to all sensors + ow_write(pin, CONVERT_T, MODE) -- and start conversion + for i, _ in ipairs(sens) do status[i] = 1 end + else + local started = false + for i, s in ipairs(sens) do + if status[i] == 0 then + local addr, parasite = s:sub(1,8), s:byte(9) == 1 + if parasite and started then break end -- do not start concurrent conversion of powered and parasite + debugPrint("starting conversion:", to_string(addr), parasite and "parasite" or "") + ow_reset(pin) + ow_select(pin, addr) -- select the sensor + ow_write(pin, CONVERT_T, MODE) -- and start conversion + status[i] = 1 + if parasite then break end -- parasite sensor blocks bus during conversion + started = true + end + end + end + tmr_create():alarm(750, tmr_ALARM_SINGLE, function() return readout(self) end) +end) + +local function _search(self, lcb, lpin, search, save) + self.temp = {} + if search then self.sens = {}; status = {} end + local sens = self.sens + pin = lpin or pin + + local addr + if not search and #sens == 0 then + -- load addreses if available + debugPrint ("geting addreses from flash") + local s,check,a = pcall(dofile, "ds18b20_save.lc") + if s and check == "ds18b20" then + for i = 1, #a do sens[i] = a[i] end + end + debugPrint (#sens, "addreses found") + end + + ow_setup(pin) + if search or #sens == 0 then + ow_reset_search(pin) + -- ow_target_search(pin,0x28) + -- search the first device + addr = ow_search(pin) + else + for i, _ in ipairs(sens) do status[i] = 0 end + end + local function cycle() + if addr then + local crc=ow_crc8(addr:sub(1,7)) + if (crc==addr:byte(8)) and ((addr:byte(1)==DS1920FAMILY) or (addr:byte(1)==DS18B20FAMILY)) then + ow_reset(pin) + ow_select(pin, addr) + ow_write(pin, READ_POWERSUPPLY, MODE) + local parasite = (ow_read(pin)==0 and 1 or 0) + sens[#sens+1]= addr..string_char(parasite) + status[#sens] = 0 + debugPrint("contact: ", to_string(addr), parasite == 1 and "parasite" or "") + end + addr = ow_search(pin) + node_task_post(node_task_LOW_PRIORITY, cycle) + else + ow_depower(pin) + -- place powered sensors first + table_sort(sens, function(a, b) return a:byte(9) num_readings then read_index = 1 end + return total / num_readings +end + +function control_logic(avg_temp) + local time_now = tmr.time() + local temp_ok = (avg_temp >= (current_t - hysteresis)) and (avg_temp <= (current_t + hysteresis)) + + if temp_ok then + gpio.write(pin_led_g, gpio.HIGH) + gpio.write(pin_led_r, gpio.LOW) + else + gpio.write(pin_led_g, gpio.LOW) + gpio.write(pin_led_r, gpio.HIGH) + end + + if avg_temp < (current_t - hysteresis) then + if gpio.read(pin_ten) == gpio.HIGH then + gpio.write(pin_ten, gpio.LOW) + ten_on_time = time_now + ten_was_active = true + end + if ten_was_active and ten_on_time > 0 and (time_now - ten_on_time >= 300) then + if gpio.read(pin_fan) == gpio.HIGH then + gpio.write(pin_fan, gpio.LOW) + end + else + gpio.write(pin_fan, gpio.HIGH) + end + elseif avg_temp > (current_t + hysteresis) then + gpio.write(pin_ten, gpio.HIGH) + ten_was_active = false + ten_on_time = 0 + if gpio.read(pin_fan) == gpio.HIGH then + gpio.write(pin_fan, gpio.LOW) + end + else + if gpio.read(pin_ten) == gpio.LOW then + gpio.write(pin_ten, gpio.HIGH) + ten_off_time = time_now + end + if ten_was_active and ten_off_time > 0 then + if (time_now - ten_off_time >= 300) then + gpio.write(pin_fan, gpio.HIGH) + ten_was_active = false + ten_on_time = 0 + ten_off_time = 0 + end + else + gpio.write(pin_fan, gpio.HIGH) + end + end + + local ten_state = gpio.read(pin_ten) == gpio.LOW and 1 or 0 + local fan_state = gpio.read(pin_fan) == gpio.LOW and 1 or 0 + print(string.format("T_cur: %2d C | T_set: %2d C | TEN: %d | FAN: %d | Act: %s", + avg_temp, current_t, ten_state, fan_state, tostring(ten_was_active))) +end + +function readout(temp) + local count = 0 + local sum = 0 + for addr, t_val in pairs(temp) do + local short_id = string.format('%02X', addr:byte(8)) + local t_ceil = t_val + if t_val > 1000 then t_ceil = t_val / 10000 end + + results[short_id] = t_ceil + print("Sensor " .. short_id .. ": " .. t_ceil .. " C") + + sum = sum + t_ceil + count = count + 1 + end + if count > 0 then + control_logic(sum / count) + else + print("No sensors found") + end +end + +function read_sensors() + t:read_temp(readout, ds_pin, t.C) +end + +function start_web_server() + wifi.setmode(wifi.SOFTAP) + local cfg = {} + cfg.ssid = "Thermostat-WiFi" + wifi.ap.config(cfg) + + srv = net.createServer(net.TCP) + srv:listen(80, function(conn) + conn:on("receive", function(sck, payload) + local response = {"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\n\r\n"} + table.insert(response, "") + table.insert(response, "

Thermostat Status

") + table.insert(response, "

Target Temp: " .. current_t .. " C


") + + for id, temp in pairs(results) do + table.insert(response, "

Tank [ID: " .. id .. "]: " .. temp .. " C

") + end + + table.insert(response, "") + + local function send_chunk(sk) + if #response > 0 then + sk:send(table.remove(response, 1)) + else + sk:close() + end + end + sck:on("sent", send_chunk) + send_chunk(sck) + end) + end) + print("HTTP Server started on 192.168.4.1") +end + +tmr.create():alarm(2000, tmr.ALARM_SINGLE, function() + collectgarbage() + start_web_server() + + sensortimer = tmr.create() + sensortimer:register(timing, tmr.ALARM_AUTO, read_sensors) + sensortimer:start() + + adctimer = tmr.create() + adctimer:register(300, tmr.ALARM_AUTO, function() + local avg_raw = read_smoothed_adc() + if avg_raw < adc_min then avg_raw = adc_min end + if avg_raw > adc_max then avg_raw = adc_max end + local step_index = 1 + ((avg_raw - adc_min) * (#steps - 1)) / (adc_max - adc_min) + if step_index > #steps then step_index = #steps end + if step_index < 1 then step_index = 1 end + current_t = steps[step_index] + end) + adctimer:start() +end) diff --git a/nodemcu-release-10-modules-2026-08-18-07-13-33-integer.bin b/nodemcu-release-10-modules-2026-08-18-07-13-33-integer.bin new file mode 100644 index 0000000..83d6118 Binary files /dev/null and b/nodemcu-release-10-modules-2026-08-18-07-13-33-integer.bin differ diff --git a/potentiometer_scale.png b/potentiometer_scale.png new file mode 100644 index 0000000..b814cf6 Binary files /dev/null and b/potentiometer_scale.png differ diff --git a/schematic.md b/schematic.md new file mode 100644 index 0000000..9164e68 --- /dev/null +++ b/schematic.md @@ -0,0 +1,37 @@ +# ТЕХНИЧЕСКАЯ СХЕМА ПОДКЛЮЧЕНИЯ ЭЛЕМЕНТОВ + +### 1. Аналоговый узел задания температуры (Защитный барьер) +В качестве регулятора уставки используется потенциометр с высоким номиналом, формирующий нелинейную «полку» насыщения АЦП. +* **Переменный резистор (500 кОм):** + * Левый вывод ➡️ Пин **`3V3`** платы Wemos D1 + * Правый вывод ➡️ Пин **`GND`** платы Wemos D1 + * Средний вывод (бегунок) ➡️ Аналоговый пин **`A0`** платы Wemos D1 + * *Примечание:* Полученный сигнал программно фильтруется в прошивке с помощью 8-кратного скользящего среднего для устранения наводок. + +### 2. Измерительный контур сусла (Шина 1-Wire) +Все 10 герметичных датчиков, погруженных в кеги, подключаются параллельно по топологии «шина». +* **10 датчиков DS18B20:** + * Все красные провода (`VCC` / Питание) ➡️ Пин **`3V3`** платы Wemos D1 + * Все черные провода (`GND` / Земля) ➡️ Пин **`GND`** платы Wemos D1 + * Все желтые/белые провода (`DATA` / Данные) ➡️ Цифровой пин **`D1`** платы Wemos D1 + * **Аппаратное требование:** Между линией `DATA` (пин `D1`) и линией `VCC` (пин `3V3`) необходимо впаять один жесткий подтягивающий резистор номиналом **4.7 кОм**. Без него из-за суммарной емкости длинных проводов от 10 кег шина 1-Wire перестанет читаться. + +### 3. Силовая исполнительная часть (Модуль реле Low Level Trigger) +Блок реле управляется активным низким уровнем (`LOW` / Земля). При подаче логического нуля со стороны Wemos реле щелкает, и на модуле загорается соответствующий светодиод канала. +* **Реле ТЭНа (залит в бетон):** + * Вывод `IN1` (Вход управления 1) ➡️ Цифровой пин **`D2`** платы Wemos D1 +* **Реле Вентиляторов (промес воздуха):** + * Вывод `IN2` (Вход управления 2) ➡️ Цифровой пин **`D6`** платы Wemos D1 +* **Питание катушек модуля реле:** + * Вывод `VCC` модуля ➡️ Положительный вывод (**`+5В`**) внешнего блока питания + * Вывод `GND` модуля ➡️ Минусовой вывод (**`GND`**) внешнего блока питания *(общая земля с Wemos)* + +### 4. Панель визуального мониторинга камеры (Светодиоды) +Светодиоды подключаются через токоограничивающие резисторы и управляются активным высоким уровнем (`HIGH` / 3.3В). +* **Зеленый светодиод (Температура сусла в норме):** + * Анод (длинная ножка) ➡️ Через резистор **220 Ом** на цифровой пин **`D7`** платы Wemos D1 + * Катод (короткая ножка с плоским срезом) ➡️ Пин **`GND`** платы Wemos D1 +* **Красный светодиод (Идет нагрев / Охлаждение камеры):** + * Анод (длинная ножка) ➡️ Через резистор **220 Ом** на цифровой пин **`D5`** платы Wemos D1 + * Катод (короткая ножка с плоским срезом) ➡️ Пин **`GND`** платы Wemos D1 + * *Внимание:* Пин `D5` выбран намеренно вместо дефолтного `D8`. Притягивание пина `D8` к высокому уровню во время старта намертво блокирует загрузку процессора ESP8266, переводя его в режим ожидания прошивки. С пином `D5` старт устройства всегда безопасен.