Сценарий 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)