article
Термокамера/сушилка с рецептом
Сценарий algo_dryer.py ведёт термокамеру по выбранному рецепту: нагрев до полки, выдержка, следующая ступень, остывание с вытяжкой.
Возможности
- Три рецепта (переключатель на экране): дерево, керамика, трава. Состав ступеней зашит в программе (имя, температура, выдержка в минутах, ТЭН / вентилятор / вытяжка).
- Ступень без выдержки (0 мин) — только дойти до температуры и сразу дальше. Со выдержкой — таймер минут идёт после достижения полки (±2 °C).
- ТЭН по ШИМ, период 30 с, ПИД 0–100%. На экране процент ТЭНа и температура камеры.
- Вентилятор и вытяжка включаются по флагу ступени: на остывании нагрев выкл, вытяжка открыта.
- Дверь открыта в цикле: нагрев стоп, вентилятор остаётся, статус «Пауза». Закрыли — цикл продолжается с той же ступени (таймер выдержки на паузе не тикает, пока дверь открыта).
- Рецепт фиксируется в момент пуска. Смена рецепта на ходу не ломает текущий цикл.
- Аварии: обрыв NTC, термореле ТЭНа или вентилятора, перегрев камеры выше 90 °C — стоп и сброс пуска. Дверь в цикле — отдельная авария.
- События пуска/стопа, смены ступени и «цикл завершён». После «Готово» повторный пуск начинает рецепт сначала.
Пуск и выбор рецепта — с Управления. Ступень, уставка, остаток минут, ТЭН % — только чтение.
Сферы применения
Одна камера, один ТЭН, один датчик в объёме — не линия сушки и не печь с зонами.
- сушильный шкаф или камера для пиломатериала, керамики, трав и семян;
- термокамера с выдержкой «нагреть — подержать — остудить»;
- небольшой инкубатор / ванна / сушка покрытия, если хватает ТЭНа, вентилятора и двери;
- пилотная технология: рецепт правят в тексте программы, оператор только выбирает имя.
Не закрывает: влажность и равномерность по нескольким датчикам, взрывоопасные растворители, промышленную печь с несколькими зонами, смену рецепта без правки кода (только три готовых набора).
Клеммы
| Клемма | Назначение |
|---|---|
| AI0 NTC | температура камеры |
| DO0 ШИМ | ТЭН |
| DO1 | вентилятор |
| DO2 | вытяжка / заслонка сброса |
| DI0 НО | дверь открыта |
| DI1, DI2 НЗ | термореле ТЭНа и вентилятора |
Сценарий
'''
Сушилка / термокамера с рецептом.
Ступени: нагрев до температуры, выдержка, следующая полка, остывание.
ТЭН по ШИМ, вентилятор, вытяжка. Дверь открыта — нагрев стоп.
Клеммы:
AI0 NTC температура камеры
DO0 ШИМ ТЭН (период 30 с)
DO1 вентилятор
DO2 вытяжка / заслонка сброса
DI0 НО дверь (True = открыта)
DI1 НЗ термореле ТЭНа (True = норма)
DI2 НЗ термореле вентилятора (True = норма)
'''
from dev import Ai, Do, Di, Reg, Alarm, Event, PID, count, webconf, run
from time import localtime
from math import isnan
from micropython import const
STEP = const(1)
TEMP_OK = const(2)
# name, temp °C, hold_min (0 = ждать только достижения температуры),
# heat, fan, exhaust
RECIPES = (
(
('Нагрев 40', 40, 0, True, True, False),
('Выдержка 40', 40, 60, True, True, False),
('Нагрев 55', 55, 0, True, True, False),
('Выдержка 55', 55, 90, True, True, False),
('Остывание', 30, 20, False, True, True),
),
(
('Нагрев 50', 50, 0, True, True, False),
('Выдержка 50', 50, 120, True, True, False),
('Остывание', 25, 30, False, True, True),
),
(
('Сушка 35', 35, 180, True, True, False),
('Остывание', 25, 15, False, True, True),
),
)
STATUSES = ['Стоп', 'Работа', 'Пауза (дверь)', 'Готово']
ST_STOP = const(0)
ST_RUN = const(1)
ST_PAUSE = const(2)
ST_DONE = const(3)
count(Reg=9, Ai=1, Do=3, Di=3, Ao=0, Alarm=5, Event=3)
webconf(Reg=True, Pin=True, Display=False, Console=True)
rPower = Reg(0).conf('bool', 'Пуск')
rRecipe = Reg(1).conf('enum', 'Рецепт', items=['Дерево', 'Керамика', 'Трава'], default=0)
rStatus = Reg(2).conf('enum', 'Статус', items=STATUSES, access='r')
rStage = Reg(3).conf('int', 'Ступень', access='r')
rStageN = Reg(4).conf('int', 'Ступеней', access='r')
rTset = Reg(5).conf('int', 'Уставка, °C', access='r')
rLeft = Reg(6).conf('uint', 'Осталось, мин', access='r')
rHeat = Reg(7).conf('int', 'ТЭН %', access='r')
rTemp = Reg(8).conf('float', 'Камера, °C', access='r', dec=1)
aiT = Ai(0).conf(name='Камера', type='ntc', access='r')
doHeat = Do(0).conf(name='ТЭН', type='pwm', period=30, access='r')
doFan = Do(1).conf(name='Вентилятор', access='r')
doExh = Do(2).conf(name='Вытяжка', access='r')
diDoor = Di(0).conf(name='Дверь открыта', type='no', access='r')
diHth = Di(1).conf(name='Термореле ТЭН', type='nz', access='r')
diFth = Di(2).conf(name='Термореле вент.', type='nz', access='r')
aSens = Alarm(0).conf('Ошибка датчика')
aHeat = Alarm(1).conf('Авария ТЭНа')
aFan = Alarm(2).conf('Авария вентилятора')
aDoor = Alarm(3).conf('Дверь открыта в цикле')
aTemp = Alarm(4).conf('Температура вне допуска')
ePower = Event(0).conf('Пуск / стоп')
eStage = Event(1).conf('Ступень')
eDone = Event(2).conf('Цикл завершён')
pid = PID(kP=4.0, kI=0.08, kD=0.0, limits=[0, 100])
status = ST_STOP
stage = 0
holdLeft = 0
atTemp = False
recipeIdx = 0
def log(msg):
t = localtime()
print('{0}/{1:02}/{2:02} {3:02}:{4:02}:{5:02} {6}'.format(
t[0], t[1], t[2], t[3], t[4], t[5], msg))
def recipe():
return RECIPES[recipeIdx]
def outputs(heat, fan, exhaust, pwm=0):
if not heat:
pwm = 0
pid.reset()
doHeat.pwm(pwm)
rHeat.val(int(pwm))
doFan.val(fan)
doExh.val(exhaust)
def idle():
outputs(False, False, False, 0)
rTset.val(0)
rLeft.val(0)
rStage.val(0)
def beginCycle():
global status, stage, holdLeft, atTemp, recipeIdx
recipeIdx = rRecipe.val()
stage = 0
holdLeft = 0
atTemp = False
status = ST_RUN
rStatus.val(ST_RUN)
rStageN.val(len(recipe()))
enterStage()
def enterStage():
global holdLeft, atTemp
rec = recipe()
name, tset, hold_min, heat, fan, exhaust = rec[stage]
rStage.val(stage + 1)
rTset.val(tset)
rStageN.val(len(rec))
holdLeft = hold_min * 60
atTemp = False
rLeft.val(hold_min)
eStage.add(stage + 1)
log('Ступень {}/{} {}'.format(stage + 1, len(rec), name))
def finish(reason):
global status
status = ST_DONE
rStatus.val(ST_DONE)
idle()
rPower.val(False)
eDone.add()
log('Готово ({})'.format(reason))
def abort(reason):
global status
status = ST_STOP
rStatus.val(ST_STOP)
idle()
rPower.val(False)
log('Стоп ({})'.format(reason))
def step():
global status, stage, holdLeft, atTemp
t = aiT.val()
if isnan(t):
aSens.on()
rTemp.val(0)
abort('датчик')
return
aSens.off()
rTemp.val(t)
if not diHth.val():
if not aHeat.val():
aHeat.on()
log('Термореле ТЭНа')
abort('ТЭН')
return
if not diFth.val():
if not aFan.val():
aFan.on()
log('Термореле вентилятора')
abort('вентилятор')
return
if rPower.changed():
ePower.add(1 if rPower.val() else 0)
if rPower.val():
if status == ST_DONE or status == ST_STOP:
beginCycle()
else:
if status == ST_RUN or status == ST_PAUSE:
abort('оператор')
if status == ST_STOP or status == ST_DONE:
idle()
return
rec = recipe()
name, tset, hold_min, heat, fan, exhaust = rec[stage]
rTset.val(tset)
if diDoor.val():
aDoor.on()
outputs(False, True, False, 0)
status = ST_PAUSE
rStatus.val(ST_PAUSE)
return
aDoor.off()
if status == ST_PAUSE:
status = ST_RUN
rStatus.val(ST_RUN)
log('Дверь закрыта, продолжаем')
if t > 90:
aTemp.on()
abort('перегрев')
return
aTemp.off()
pwm = 0
if heat:
pwm = pid(t, tset)
else:
pid.reset()
pwm = 0
outputs(heat, fan, exhaust, pwm)
reached = t <= tset + TEMP_OK if not heat else t >= tset - TEMP_OK
if reached:
atTemp = True
if hold_min == 0:
if atTemp:
stage += 1
if stage >= len(rec):
finish('рецепт')
else:
enterStage()
return
if atTemp:
if holdLeft > 0:
holdLeft -= 1
rLeft.val((holdLeft + 59) // 60)
if holdLeft <= 0:
stage += 1
if stage >= len(rec):
finish('рецепт')
else:
enterStage()
else:
rLeft.val(hold_min)
idle()
rStatus.val(ST_STOP)
log('Старт сушилки')
run(step, STEP)