This commit is contained in:
Татьяна Фарбер 2026-09-01 12:43:55 +04:00
parent 1f62887fad
commit ea1dae539a
5 changed files with 323 additions and 2 deletions

146
Autoclave Scale 60mm.html Normal file
View File

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html><head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<title>Autoclave Scale 60mm</title>
<style>
body { font-family: 'Arial', sans-serif; margin: 40px; background: #fff; text-align: center; }
.info { font-size: 14px; color: #333; margin-bottom: 20px; text-align: left; max-width: 600px; margin: 0 auto 20px auto; }
/* Printable 1:1 container in mm */
.scale-container {
width: 100mm;
height: 60mm;
border: 1px dashed #bbb;
margin: 0 auto;
padding: 10mm;
box-sizing: content-box;
background: #fafafa;
}
svg { width: 100mm; height: 60mm; display: block; }
@media print {
body { margin: 0; background: #fff; }
.info { display: none; }
.scale-container { border: none; padding: 0; background: #fff; }
}
</style>
</head>
<body>
<div class="info">
<h2>Horizontal Scale for 60mm Fader (1:1)</h2>
<p><strong>Instructions:</strong> Press <strong>Ctrl + P</strong> to print. In the print dialog, set <strong>Scale to 100%</strong> (or "Actual Size") and disable margins/headers. Cut along the outer solid box if needed.</p>
</div>
<div class="scale-container">
<svg viewBox="0 0 100 60" xmlns="http://www.w3.org/2000/svg">
<!-- Background panel for the scale label -->
<rect x="5" y="5" width="90" height="50" fill="#ffffff" stroke="#333333" stroke-width="0.5" rx="3"></rect>
<!-- Fader Slot (60mm travel from X=20 to X=80, at Y=30) -->
<line x1="20" y1="30" x2="80" y2="30" stroke="#000000" stroke-width="1" stroke-linecap="round"></line>
<line x1="20" y1="30" x2="80" y2="30" stroke="#cccccc" stroke-width="0.3" stroke-dasharray="1 1"></line>
<!-- Outer limits anchors -->
<circle cx="20" cy="30" r="1" fill="#ff3333"></circle>
<circle cx="80" cy="30" r="1" fill="#ff3333"></circle>
<!-- Product Sectors (TOP SIDE, Y < 30) -->
<!-- Sector 1: Meat / Мясо (1h20m - 2h00m -> 80-120 min -> indices 14 to 18) -->
<!-- X calculation: tbl_index goes from 0 to 24.
Index 0 = 5m (X=20)
Index 24 = 180m (X=80)
Step size in X = 60 / 24 = 2.5 mm per index.
Let's map the minutes precisely to X coordinates:
5m (idx 0) -> X = 20
15m (idx 2) -> X = 25
30m (idx 5) -> X = 32.5
45m (idx 8) -> X = 40
60m (idx 11) -> X = 47.5
90m (idx 14) -> X = 55
120m(idx 17) -> X = 62.5
150m(idx 20) -> X = 70
180m(idx 24) -> X = 80
-->
<!-- Sector: Vegetables / Mushrooms (15m - 40m -> X=25 to X=37.5) -->
<path d="M 25 25 L 37.5 25" stroke="#4CAF50" stroke-width="3" stroke-linecap="square" fill="none"></path>
<text x="31.25" y="21" font-size="3" text-anchor="middle" fill="#2E7D32" font-weight="bold">VEG / MUSH</text>
<!-- Sector: Fish (30m - 60m -> X=32.5 to X=47.5) -->
<path d="M 32.5 15 L 47.5 15" stroke="#2196F3" stroke-width="3" stroke-linecap="square" fill="none"></path>
<text x="40" y="11" font-size="3" text-anchor="middle" fill="#1565C0" font-weight="bold">FISH</text>
<!-- Sector: Poultry / Птица (60m - 90m -> X=47.5 to X=55) -->
<path d="M 47.5 25 L 55 25" stroke="#FF9800" stroke-width="3" stroke-linecap="square" fill="none"></path>
<text x="51.25" y="21" font-size="3" text-anchor="middle" fill="#E65100" font-weight="bold">POULTRY</text>
<!-- Sector: Meat / Мясо (80m - 120m -> idx 13 to 17 -> X=52.5 to X=62.5) -->
<path d="M 52.5 15 L 62.5 15" stroke="#F44336" stroke-width="3" stroke-linecap="square" fill="none"></path>
<text x="57.5" y="11" font-size="3" text-anchor="middle" fill="#C62828" font-weight="bold">MEAT</text>
<!-- Ticks and Time Labels (BOTTOM SIDE, Y > 30) -->
<!-- 5m (idx 0, X=20) -->
<line x1="20" y1="30" x2="20" y2="35" stroke="#000" stroke-width="0.5"></line>
<text x="20" y="39" font-size="3" text-anchor="middle" font-weight="bold">5m</text>
<!-- 15m (idx 2, X=25) -->
<line x1="25" y1="30" x2="25" y2="34" stroke="#000" stroke-width="0.3"></line>
<text x="25" y="38" font-size="2.5" text-anchor="middle">15m</text>
<!-- 30m (idx 5, X=32.5) -->
<line x1="32.5" y1="30" x2="32.5" y2="34" stroke="#000" stroke-width="0.3"></line>
<text x="32.5" y="38" font-size="2.5" text-anchor="middle">30m</text>
<!-- 45m (idx 8, X=40) -->
<line x1="40" y1="30" x2="40" y2="34" stroke="#000" stroke-width="0.3"></line>
<text x="40" y="38" font-size="2.5" text-anchor="middle">45m</text>
<!-- 1h 00m (idx 11, X=47.5) -->
<line x1="47.5" y1="30" x2="47.5" y2="35" stroke="#000" stroke-width="0.5"></line>
<text x="47.5" y="39" font-size="3" text-anchor="middle" font-weight="bold">1h </text>
<!-- 1h 30m (idx 14, X=55) -->
<line x1="55" y1="30" x2="55" y2="34" stroke="#000" stroke-width="0.3"></line>
<text x="55" y="38" font-size="2.5" text-anchor="middle">1:30</text>
<!-- 2h 00m (idx 17, X=62.5) -->
<line x1="62.5" y1="30" x2="62.5" y2="35" stroke="#000" stroke-width="0.5"></line>
<text x="62.5" y="39" font-size="3" text-anchor="middle" font-weight="bold">2h</text>
<!-- 2h 30m (idx 20, X=70) -->
<line x1="70" y1="30" x2="70" y2="34" stroke="#000" stroke-width="0.3"></line>
<text x="70" y="38" font-size="2.5" text-anchor="middle">2:30</text>
<!-- 3h 00m (idx 24, X=80) -->
<line x1="80" y1="30" x2="80" y2="35" stroke="#000" stroke-width="0.5"></line>
<text x="80" y="39" font-size="3" text-anchor="middle" font-weight="bold">3h</text>
<!-- Minor ticks for every single step (2.5mm each) -->
<!-- We can iterate dynamically or just script it, but since this is inside HTML string, let's just add them via SVG elements for simplicity. -->
<!-- Let's map all 24 intervals visually to ensure exact alignment -->
<line x1="22.5" y1="30" x2="22.5" y2="32" stroke="#666" stroke-width="0.2"></line>
<line x1="27.5" y1="30" x2="27.5" y2="32" stroke="#666" stroke-width="0.2"></line>
<line x1="30.0" y1="30" x2="30.0" y2="32" stroke="#666" stroke-width="0.2"></line>
<line x1="35.0" y1="30" x2="35.0" y2="32" stroke="#666" stroke-width="0.2"></line>
<line x1="37.5" y1="30" x2="37.5" y2="32" stroke="#666" stroke-width="0.2"></line>
<line x1="42.5" y1="30" x2="42.5" y2="32" stroke="#666" stroke-width="0.2"></line>
<line x1="45.0" y1="30" x2="45.0" y2="32" stroke="#666" stroke-width="0.2"></line>
<!-- Post 1h ticks (every 2.5mm represents 10 mins) -->
<line x1="50.0" y1="30" x2="50.0" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 1h 10m -->
<line x1="52.5" y1="30" x2="52.5" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 1h 20m -->
<line x1="57.5" y1="30" x2="57.5" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 1h 40m -->
<line x1="60.0" y1="30" x2="60.0" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 1h 50m -->
<line x1="65.0" y1="30" x2="65.0" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 2h 10m -->
<line x1="67.5" y1="30" x2="67.5" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 2h 20m -->
<line x1="72.5" y1="30" x2="72.5" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 2h 40m -->
<line x1="75.0" y1="30" x2="75.0" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 2h 50m -->
<line x1="77.5" y1="30" x2="77.5" y2="32" stroke="#666" stroke-width="0.2"></line> <!-- 3h 00m limit pad -->
</svg>
</div>
</body></html>

View File

@ -1,3 +1,25 @@
# autoclav # Таймер автоклава для работы с терморегулятором
Автоматика таймера для автоклава Легковесное решение для автоматизации процессов стерилизации в автоклаве на базе микроконтроллера Wemos D1 Mini (ESP8266) под управлением прошивки NodeMCU Lua (версия integer-only, без плавающей точки).
Система управляет циклом нагрева и выдержки времени по сигналу от внешнего термостата. Настройка времени работы осуществляется локально с помощью ползункового фейдера, а удаленный мониторинг доступен через автономный веб-интерфейс по WiFi.
## Особенности системы
- **Защита при старте:** Встроенная программная пауза 5 секунд при включении устройства в розетку для предотвращения ложных срабатываний до стабилизации питания.
- **Аппаратная фиксация таймера:** Уставка времени жестко блокируется в памяти в момент первого достижения рабочей температуры. Случайное смещение ползунка во время стерилизации не повлияет на обратный отсчет.
- **Фильтрация шумов АЦП:** Сглаживание показаний потенциометра методом скользящего среднего по 10 замерам для устранения дребезга значений.
- **Нелинейная шкала времени:** Дискретность шага составляет 5 минут в диапазоне до 1 часа и 10 минут в диапазоне от 1 до 3 часов для удобной физической разметки панели.
- **Автономный Веб-интерфейс:** Точка доступа WiFi (`192.168.4.1`) работает без пароля и выводит адаптивную страницу со статусом системы и поминутным округлением оставшегося времени (с автообновлением каждые 5 секунд).
- **Ориентированность на отказоустойчивость:** Код разработан с учетом совместной работы с независимой механической защитой по питанию.
## Коммутация и распиновка (Pinout)
| Компонент | Пин Wemos D1 | ID пина в NodeMCU | Логический уровень / Питание | Описание |
| :--- | :--- | :--- | :--- | :--- |
| **Реле ТЭНа** | `D1` (GPIO5) | `1` | Выход 3.3V | Управляет силовым реле или контактором ТЭНов. |
| **Сигнал термостата** | `D2` (GPIO4) | `2` | Вход 3.3V (Pull-up) | Подключается к сухим контактам внешнего термостата. Пока идет нагрев, контакты замкнуты на GND. При достижении уставки размыкаются (переход в HIGH). |
| **Фейдер времени** | `A0` (ADC0) | `0` | Аналоговый 0V - 3.3V | Ползунковый потенциометр 10 кОм. Подключается строго к линиям `3V3` и `GND`. |
| **Аварийный термостат** | *Внешний* | *Отсутствует* | Силовая линия ~220В | Биметаллический выключатель KSD301 (130°C/140°C NC с кнопкой ручного сброса) на корпусе. Включается последовательно в разрыв питания ТЭНа. |
### ⚠️ Предупреждение по питанию потенциометра
Несмотря на то, что периферийные датчики могут питаться от 5V, ползунковый резистор времени **ОБЯЗАТЕЛЬНО** должен быть запитан от пина **`3V3`** платы Wemos. Это гарантирует, что аналоговый сигнал не превысит допустимый предел встроенного делителя напряжения ESP8266 (3.2V).

2
flash.sh Normal file
View File

@ -0,0 +1,2 @@
python3 -m esptool --port /dev/ttyUSB0 erase_flash
python3 -m esptool --port /dev/ttyUSB0 write_flash -fm dio 0x00000 nodemcu-release-10-modules-2026-08-18-07-13-33-integer.bin

151
init.lua Normal file
View File

@ -0,0 +1,151 @@
main_timer = tmr.create()
cycle_timer = tmr.create()
adc_timer = tmr.create()
srv = net.createServer(net.TCP)
ap_config = {}
local pin_relay = 1
local pin_thermostat = 2
local START_DELAY_MS = 5000
local time_table = {}
local table_size = 0
local function init_time_table()
local index = 0
for m = 5, 60, 5 do
time_table[index] = m * 60
index = index + 1
end
for m = 70, 180, 10 do
time_table[index] = m * 60
index = index + 1
end
table_size = index - 1
end
init_time_table()
local adc_history = {}
local adc_index = 1
local max_samples = 10
local adc_average = 0
local set_time_sec = 300
local remaining_time_sec = 300
local process_started = false
local process_finished = false
gpio.mode(pin_relay, gpio.OUTPUT)
gpio.write(pin_relay, gpio.LOW)
gpio.mode(pin_thermostat, gpio.INPUT, gpio.PULLUP)
print("System init delay...")
local function read_averaged_adc()
local raw = adc.read(0)
adc_history[adc_index] = raw
adc_index = adc_index + 1
if adc_index > max_samples then adc_index = 1 end
local sum = 0
local count = 0
for i = 1, max_samples do
if adc_history[i] then
sum = sum + adc_history[i]
count = count + 1
end
end
adc_average = sum / count
local tbl_index = (adc_average * table_size) / 1023
if tbl_index < 0 then tbl_index = 0 end
if tbl_index > table_size then tbl_index = table_size end
set_time_sec = time_table[tbl_index]
if not process_started then
remaining_time_sec = set_time_sec
end
end
adc_timer:register(200, tmr.ALARM_AUTO, read_averaged_adc)
main_timer:register(START_DELAY_MS, tmr.ALARM_SINGLE, function()
print("Heater ON. Waiting thermostat...")
gpio.write(pin_relay, gpio.HIGH)
adc_timer:start()
cycle_timer:register(1000, tmr.ALARM_AUTO, function()
local thermo_state = gpio.read(pin_thermostat)
local left_min = (remaining_time_sec + 59) / 60
local set_min = set_time_sec / 60
print("TMR: Left=" .. left_min .. "m | Set=" .. set_min .. "m | Input D2=" .. thermo_state)
if not process_started and not process_finished then
if thermo_state == gpio.HIGH then
process_started = true
adc_timer:stop()
print("Target reached! Timer active: " .. set_min .. "m")
end
elseif process_started and not process_finished then
if remaining_time_sec > 0 then
remaining_time_sec = remaining_time_sec - 1
else
process_started = false
process_finished = true
gpio.write(pin_relay, gpio.LOW)
print("Process finished. Heater OFF.")
cycle_timer:stop()
end
end
end)
cycle_timer:start()
end)
main_timer:start()
wifi.setmode(wifi.SOFTAP)
ap_config.ssid = "Autoclave_Control"
ap_config.auth = wifi.OPEN
wifi.ap.config(ap_config)
print("AP Ready. IP: " .. wifi.ap.getip())
srv:listen(80, function(conn)
conn:on("receive", function(client, request)
local function format_time_min(sec, is_set)
local total_min = 0
if is_set then
total_min = sec / 60
else
total_min = (sec + 59) / 60
end
local h = total_min / 60
local m = total_min % 60
return string.format("%02d:%02d", h, m)
end
local status_text = "Heating"
if process_started then status_text = "Sterilization" end
if process_finished then status_text = "Done" end
local html = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n"
html = html .. "<!DOCTYPE html><html><head>"
html = html .. "<meta name='viewport' content='width=device-width, initial-scale=1.0'>"
html = html .. "<meta http-equiv='refresh' content='5'>"
html = html .. "<title>Autoclave</title>"
html = html .. "<style>body{font-family:sans-serif;text-align:center;background:#f4f4f4;padding:20px;}"
html = html .. ".card{background:white;padding:20px;border-radius:10px;box-shadow:0 4px 8px rgba(0,0,0,0.1);max-width:400px;margin:0 auto;}"
html = html .. "h1{color:#333;} .status{font-size:24px;font-weight:bold;color:#d9534f;}</style>"
html = html .. "</head><body><div class='card'>"
html = html .. "<h1>Autoclave Control</h1>"
html = html .. "<p>Status: <span class='status'>" .. status_text .. "</span></p>"
html = html .. "<p>Set Time: <b>" .. format_time_min(set_time_sec, true) .. "</b></p>"
html = html .. "<p>Remaining: <b style='font-size:30px;color:#5cb85c;'>" .. format_time_min(remaining_time_sec, false) .. "</b></p>"
html = html .. "</div></body></html>"
client:send(html)
client:close()
collectgarbage()
end)
end)