Сценарий управляет приточной системой вентиляции и нагрева с использованием заслонки, вентиляторов и нагревателя.

Скриншот панели управления Скриншот панели управления

Основные функции

  • Управление заслонкой: Открытие и закрытие заслонки регулируется в зависимости от текущего статуса системы и состояния питания.
  • Контроль вентиляторов: Управление скоростью приточного и вытяжного вентиляторов на основе заданных параметров и состояния системы.
  • ПИД-регулирование температуры: Поддержание заданной температуры в канале с помощью нагревателя, используя ПИД-регулятор.
  • Обработка аварийных ситуаций: Мониторинг различных сенсоров и входов для обнаружения аварий, таких как сбой вентиляторов, перегрев ТЭНов или пожарная тревога, с последующей активацией тревожных сигналов и отключением системы.
  • Визуализация состояния: Отображение текущего состояния системы на виртуальном дисплее с использованием графических элементов и значков.
  • Логирование событий: Запись важных событий и изменений состояния системы с отметкой времени для последующего анализа.

Сфера применения

Этот сценарий подходит для использования в различных системах автоматизации, требующих управления вентиляцией и нагревом с учетом безопасности и эффективности. Примеры применения:

  • Климатические системы зданий: Управление вентиляцией и отоплением в жилых или коммерческих зданиях для поддержания комфортных условий.
  • Промышленные установки: Контроль температурных режимов и вентиляции в производственных процессах, где требуется точное регулирование условий.
  • Серверные комнаты и дата-центры: Обеспечение эффективной системы охлаждения и вентиляции для предотвращения перегрева оборудования.
  • Системы умного дома: Интеграция в системы управления домашними климатическими условиями для автоматизации комфорта и энергосбережения.
  • Медицинское оборудование: Контроль температурных режимов и вентиляции в установках, требующих строгих климатических условий.

Производительность

Замеры осуществлялись на тестовом образце

Время компиляции 238 мс
Среднее время выполнения тела цикла 3-4 мс
Использование стека 520 байт (6.3%)
Использование кучи 10448 байт (17.1%)

Сценарий

  from dev import Ai, Ao, Do, Di, Reg, Alarm, Event, count, PID, run, conv, webconf
from display import clear, update, image, lines, text, WIDTH, HEIGHT
from time import localtime

# Параметры алгоритма
STEP        = 0.2  # шаг алгоритма в секундах
TIME_CAP    = const(10)   # время открытия/закрытия заслонки (сек)
TIME_FAN    = const(10)   # время разгона вентиляторов (сек)
TIME_BLOW   = const(20)   # время продува ТЭНов (сек)
FAN_MIN     = const(2.0)  # минимальное значение для вентилятора
FAN_MAX     = const(10.0) # максимальное значение для вентилятора
TEMP_MIN    = const(15)   # минимально допустимая температура (°С)
TEMP_MAX    = const(45)   # максимально допустимая температура (°С) 

# Статусы работы установки
STATUS_NONE      = const(-1)
STATUS_OFF       = const(0)    # Установка отключена
STATUS_CAP_OPEN  = const(1)    # Открытие заслонки
STATUS_WORK      = const(2)    # Работа установки
STATUS_BLOW      = const(3)    # Продув ТЭНов
STATUS_FAN_STOP  = const(4)    # Остановка вентиляторов
STATUS_CAP_CLOSE = const(5)    # Закрытие заслонки

# Режимы работы установки
MODE_VENT   = const(0)    # "Вентиляция"
MODE_HEATER = const(1)    # "Нагрев"

# Параметры ПИД-регулятора (значения подобраны согласно вашим предыдущим расчетам)
PID_MIN = const(15)
PID_MAX = const(5)
pid     = PID(kP=5, kI=0.1, limits=[PID_MIN, PID_MAX])

# Таблица переходов состояний
# Для каждого состояния задаются пара [время (в шагах алгоритма), следующий статус]
STATUSES = [
    {'name': 'Выкл',                   'on':  [int(TIME_CAP/STEP), STATUS_CAP_OPEN],     'off': [0, STATUS_NONE]},   
    {'name': 'Открытие заслонки',      'on':  [0, STATUS_WORK],     'off': [int(TIME_CAP/STEP), STATUS_CAP_CLOSE]},
    {'name': 'Работа',                 'on':  [0, STATUS_NONE],     'off': [0, STATUS_BLOW]},    
    {'name': 'Продув ТЭНов',           'on':  [0, STATUS_WORK],     'off': [int(TIME_FAN/STEP), STATUS_FAN_STOP]},
    {'name': 'Остановка вентиляторов', 'on':  [0, STATUS_WORK],     'off': [int(TIME_CAP/STEP), STATUS_CAP_CLOSE]},    
     {'name': 'Закрытие заслонки',     'on':  [0, STATUS_CAP_OPEN],     'off': [0, STATUS_OFF]},
]

# Инициализация регистров и устройств
count(Reg=7, Ai=1, Do=2, Ao=2, Di=4, Alarm=5)

rPower    = Reg(0).conf(name='Питание', type='bool', access='w')
rMode     = Reg(1).conf(name='Режим', type='enum', items=['Вентиляция', 'Нагрев'], default=0, access='w')
rSetPoint = Reg(2).conf(name='Уставка', type='int', limits=[15, 45], default=25, access='w', web='select')
rFanSpeed = Reg(3).conf(name='Вентилятор', type='int', limits=[1, 5], step=1, web='select', default=5, access='w')
rStatus   = Reg(4).conf(name='Статус', type='enum', items=[s['name'] for s in STATUSES], access='r')
rTimer    = Reg(5).conf(name='Таймер', type='int', access='r')
rSignal   = Reg(6).conf(name='Сигнал', type='float', access='r')

ntcTemp   = Ai(0).conf(name='Температура в канале', type='ntc')
doHeater  = Do(0).conf(name='Нагреватель', type='pwm', period=30, access='r')
doCap     = Do(1).conf(name='Заслонка', access='r')
aoFanIn   = Ao(0).conf(name='Приточный вентилятор', access='r')
aoFanOut  = Ao(1).conf(name='Вытяжной вентилятор', access='r')

ePower = Event(0).conf(name='Питание')
eMode  = Event(1).conf(name='Режим')

# Конфигурация аварий – каждый элемент содержит функцию проверки,
# время истечения (в шагах алгоритма) и имя аварии.
ALARMS = [
    {'name':'Авария приточного вент-ра',     'timeout': int(10/STEP), 'fn': lambda: not Di(0).val()},
    {'name':'Авария вытяжного вент-ра',      'timeout': int(10/STEP), 'fn': lambda: not Di(1).val()},
    {'name':'Авария ТЭНа',                   'timeout': int(10/STEP), 'fn': lambda: not Di(2).val()},
    {'name':'Пожарная тревога',              'timeout': int(1/STEP),  'fn': lambda: not Di(3).val()},
    {'name':'Недопустимая темп-ра в канале', 'timeout': int(30/STEP), 'fn': lambda: ntcTemp.val() < TEMP_MIN or ntcTemp.val() > TEMP_MAX},
]

# Инициализация аварий и диалоговых входов, если применимо
for i, alarm in enumerate(ALARMS):
    alarm['time'] = 0
    _ = Alarm(i).conf(name=alarm['name'])
    if i < 4:  # для первых четырёх аварий используем цифровые входы
        _ = Di(i).conf(name=alarm['name'], type='no')

# Вывод сообщения с отметкой времени
def log(msg: str) -> None:
    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 alarmsTest() -> bool:
    """
    Проверяет наличие аварий. Если таймер срабатывания превышен, активирует тревогу.
    Возвращает True, если обнаружена хотя бы одна активная авария.
    """
    any_alarm = False
    for i, alarm in enumerate(ALARMS):
        # Если авария уже активирована, сразу возвращаем True
        if Alarm(i).val():
            return True
        # Если функция аварии срабатывает, увеличиваем счётчик времени аварии
        if alarm['fn']():
            alarm['time'] += 1
            if alarm['time'] > alarm['timeout']:
                Alarm(i).on()
                log("АВАРИЯ " + alarm['name'])
                any_alarm = True
        else:
            alarm['time'] = 0
    return any_alarm

# Глобальные переменные
status   = STATUS_NONE   # текущий статус работы установки
timer    = 0             # счётчик оставшегося времени для текущего статуса (в шагах)
blowTime = 0             # время для продува ТЭНов (в шагах)

def switchStatus() -> None:
    """Переключает статус установки согласно таблице переходов"""
    global status, timer, blowTime
    # Выбор следующего состояния в зависимости от состояния питания rPower
    transition = STATUSES[status]['on'] if rPower.val() else STATUSES[status]['off']
    new_time, new_status = transition
    # Если следующий статус не равен STATUS_NONE, применяем его
    if new_status != STATUS_NONE:
        status = new_status
        # Если переходим в состояние продува, используем накопленное время, иначе новое время перехода
        timer = blowTime if status == STATUS_BLOW else new_time
        rStatus.val(status)
        # Отображаем время в секундах – перевод из шагов
        rTimer.val(int(timer * STEP))
        log('Статус ' + STATUSES[status]['name'])

def step(c) -> None:
    """Основной шаг алгоритма, выполняемый с периодичностью STEP секунд."""
    global status, timer, blowTime

    # Проверка аварий – если обнаружена авария, отключаем установку (rPower = False)
    if alarmsTest():
        if rPower.val(): 
            log("Отключение установки из-за аварии")
        rPower.val(False)

    # Если изменён режим работы, добавляем событие
    if rMode.changed():
        mode_str = "Нагрев" if rMode.val() == MODE_HEATER else "Вентиляция"
        log("Режим " + mode_str)
        eMode.add(rMode.val())
    
    if rSetPoint.changed():
        log("Уставка " + str(rSetPoint.val()))

    if rFanSpeed.changed():
        log("Скорость вентилятора " + str(rFanSpeed.val()))

    # Если изменилось питание – сразу переключаем статус
    if rPower.changed():
        switchStatus()
        ePower.add(rPower.val())
    else:
        # Если таймер ещё не истёк, уменьшаем его
        if timer > 0:
            timer -= 1
            rTimer.val(int(timer * STEP))
        else:
            switchStatus()

    # Управление заслонкой: заслонка открыта, если статус не равен выключенным состояниям
    doCap.val(not (status in [STATUS_OFF, STATUS_CAP_CLOSE, STATUS_NONE]))

    # Управление вентиляторами
    fanValue = 0.0
    if status == STATUS_BLOW:
        fanValue = FAN_MAX
    elif status == STATUS_WORK:
        # Преобразование значения вентиляторного регистра в диапазон [FAN_MIN, FAN_MAX]
        fanValue = conv(rFanSpeed.val(), 1, 5, FAN_MIN, FAN_MAX)
    aoFanIn.val(fanValue)
    aoFanOut.val(fanValue)

    # Расчёт управляющего сигнала для нагревателя
    signal = 0
    if status == STATUS_WORK and rMode.val() == MODE_HEATER:
        pidValue = pid(ntcTemp.val(), rSetPoint.val())
        # Преобразуем выход ПИД-регулятора в проценты (0-100)
        signal = conv(pidValue, PID_MIN, PID_MAX, 0.0, 100.0)
        # Корректируем blowTime: если сигнал > 0 – увеличиваем (до TIME_BLOW в шагах), иначе уменьшаем
        if signal > 0:
            if blowTime < int(TIME_BLOW/STEP):
                blowTime += 1
        elif blowTime > 0:
            blowTime -= 1

    doHeater.pwm(signal)
    rSignal.val(signal)


# Значки для обозначения элементов установки в бинарном виде
ICON_15x15_ALARM=b'\xfc\x1f\x02 9@\xc5@\x05C\x05L\x05P\xd5W\x05P\x05L\x05C\xc5@9@\x02 \xfc\x1f'
ICON_15x15_VALVE_CLOSE=b'\xfc\x1f\x02 \x01@\x01@\x01@\x01@\xc1A\xfd_\xc1A\x01@\x01@\x01@\x01@\x02 \xfc\x1f'
ICON_15x15_VALVE_OPEN=b'\xfc\x1f\x02 \x81@\x81@\x81@\x81@\xc1A\xc1A\xc1A\x81@\x81@\x81@\x81@\x02 \xfc\x1f'
ICON_15x15_VALVE_CLOSE2=b'\xfc\x1f\x02 \x01@\x01@\x01@\x01@\xc1A\xfd_\xc1A\x01P\x01J\x01F\x01N\x02 \xfc\x1f'
ICON_15x15_VALVE_OPEN2=b'\xfc\x1f\x02 \x81@\x81@\x81@\x81@\xc1A\xc1A\xc1A\x81\\\x81X\x81T\x81B\x02 \xfc\x1f'
ICON_15x15_HEATER_WORK=b'\xfc\x1f\x02 \x01@\x01@\x01@\x81A\x9dF\x8dX\xb5@\xc1@\x01@\x01@\x01@\x02 \xfc\x1f'
ICON_15x15_HEATER_STOP=b'\xfc\x1f\x02 \x01@\x81A\x9dF\x8dX\xb5@\xc1@\x1c@"@\x01@}@\x01@" \x9c\x1f'
ICON_15x15_FAN_STOP=b'\xfc\x1f\x02 \x19O\xbd_\xbd_\xbdO\xfdAAO\x9c_\xa2^\x01^}^\x01L" \x9c\x1f'
ICON_15x15_FAN_WORK1=b'\xfc\x1f\x02 \x01@\x01@\x01@\x01@\xc1AAO\xc1_\xf9^\xfd^\xfd^yL\x02 \xfc\x1f'
ICON_15x15_FAN_WORK2=b'\xfc\x1f\x02 \x19@=@=@=@\xfdAyA\xc1A\xf9@\xfd@\xfd@y@\x02 \xfc\x1f'
ICON_15x15_FAN_WORK3=b'\xfc\x1f\x02 \x19O\xbd_\xbd_\xbdO\xfdAyA\xc1A\x01@\x01@\x01@\x01@\x02 \xfc\x1f'
ICON_15x15_FAN_WORK4=b'\xfc\x1f\x02 \x01O\x81_\x81_\x81O\xc1AAO\xc1_\x01^\x01^\x01^\x01L\x02 \xfc\x1f'
ICON_15x15_FAN_WORK = (ICON_15x15_FAN_WORK1, ICON_15x15_FAN_WORK2, ICON_15x15_FAN_WORK3, ICON_15x15_FAN_WORK4)
ICON_8x6_SENSOR=b'\x04\x0e\xff\xff\x0e\x04'

PIPE_UP_Y = const(36)
PIPE_DOWN_Y = const(6)
PIPE_UP_WIDTH = const(120)
PIPE_DOWN_WIDTH = const(70)

webconf(Reg=True, Pin=True, Display=True, Console=True)

def drawItem(pos, pipeY, iconH, icon, title="") -> None:
    x = 5 + pos*10 + (pos-1)*15
    image(x, pipeY-2, iconH, icon)
    if title != "":
        text(x+8, pipeY+14, title, 2) # TODO align

def drawStep(c) -> None:
    """Визуализация алгоритма на виртуальном дисплее."""
    clear()
    # Приточный и вытяжной каналы
    lines((0, PIPE_UP_Y), (PIPE_UP_WIDTH, 0), (5, 5), (-5, 5), (-PIPE_UP_WIDTH, 0), (5, -5), (-5, -5))
    lines((5, PIPE_DOWN_Y), (PIPE_DOWN_WIDTH, 0), (-5, 5), (5, 5), (-PIPE_DOWN_WIDTH, 0), (-5, -5), (5, -5))

    # Заслонка
    if status in [STATUS_WORK, STATUS_BLOW, STATUS_FAN_STOP, STATUS_CAP_CLOSE]:
        icon = ICON_15x15_VALVE_OPEN2 if status == STATUS_CAP_CLOSE and c % 2 == 0 else ICON_15x15_VALVE_OPEN
    else:
        icon = ICON_15x15_VALVE_CLOSE2 if status == STATUS_CAP_OPEN and c % 2 == 0 else ICON_15x15_VALVE_CLOSE
    drawItem(1, PIPE_UP_Y, 15, icon)
    drawItem(1, PIPE_DOWN_Y, 15, icon)

    # Электрический нагреватель
    icon = ICON_15x15_HEATER_WORK if status == STATUS_WORK else ICON_15x15_HEATER_STOP
    if Alarm(2).val() and c % 4 > 1:
        icon = ICON_15x15_ALARM
    drawItem(2, PIPE_UP_Y, 15, icon, str(int(rSignal.val())))

    # Вентиляторы
    icon = ICON_15x15_FAN_STOP
    if status in [STATUS_WORK, STATUS_BLOW, STATUS_FAN_STOP]:
        icon = ICON_15x15_FAN_WORK[c % 4]
    iconFan = icon
    if Alarm(0).val():
        iconFan = ICON_15x15_ALARM if c % 4 > 1 else ICON_15x15_FAN_STOP
    drawItem(3, PIPE_UP_Y, 15, iconFan, str(aoFanIn.val()))
    iconFan = icon
    if Alarm(1).val():
        iconFan = ICON_15x15_ALARM if c % 4 > 1 else ICON_15x15_FAN_STOP
    drawItem(2, PIPE_DOWN_Y, 15, iconFan, str(aoFanOut.val()))

    # Датчик температуры в канале
    drawItem(4, PIPE_UP_Y, 8, ICON_8x6_SENSOR, str(ntcTemp.val()))

    # Уставка
    text(105, 14, "Уставка\n"+str(rSetPoint.val()), 2) # align

    update()



# Запуск основного цикла с шагом STEP
run((step, STEP), (drawStep, 0.25))