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


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