Модуль dev связывает сценарий MicroPython с ресурсами контроллера: входами и выходами, пользовательскими регистрами, событиями, авариями и портом Modbus RTU. Также в нём есть ПИД-регулятор и планировщик периодических вызовов.

  import dev

dev.count(Reg=1)
r = dev.Reg(0).conf(name='Счётчик', type='int', access='rw')
r(0)
r.inc()
print(r())  # 1
  

Общие правила

  • Нумерация ресурсов начинается с 0. Например, при count(Reg=3) доступны Reg(0), Reg(1) и Reg(2).
  • Reg(n), Do(n), Di(n), Ao(n), Ai(n), Event(n) и Alarm(n) принимают один позиционный индекс. Именованный индекс и лишние аргументы не поддерживаются.
  • Объект связан с существующим ресурсом. Два объекта Reg(0) обращаются к одному регистру, а не создают два независимых значения.
  • Чтение obj() эквивалентно obj.val(), а запись obj(value) — obj.val(value) для классов, поддерживающих эти операции.
  • conf(...) возвращает тот же объект, поэтому допустима цепочка Reg(0).conf(...). Методы записи и управления обычно возвращают None.
  • conf() изменяет текущие настройки контроллера; он не является методом чтения конфигурации. Не считайте повторный conf() частичным обновлением: у ряда пропущенных параметров применяются значения по умолчанию. Задавайте связанные параметры вместе.
  • Настройка выполняется последовательно. Если один из аргументов неверен, уже внесённые этим вызовом изменения могут сохраниться.

Доступ через веб-интерфейс и внешние службы

У Reg, Do, Di, Ao и Ai параметр access — строка длиной один или два символа:

  • первый символ задаёт доступ через веб-интерфейс;
  • второй — доступ через внешние службы;
  • '-' — нет доступа, 'r' — чтение, 'w' — чтение и запись.

Например, access='rw' разрешает чтение в веб-интерфейсе и запись через внешние службы, а access='-r' скрывает ресурс в веб-интерфейсе и оставляет внешнее чтение. Для входов Ai и Di символ 'w' запрещён в обеих позициях.

Если второй символ не указан, внешний доступ становится 'r'. Значение по умолчанию при conf() — 'wr' для регистров/выходов и 'rr' для входов. Эти права не ограничивают доступ самого сценария к ресурсам.

Старые аргументы web= и ext= в conf() не поддерживаются. Для выбора элемента управления регистра используется отдельный аргумент ui=.

Отслеживание изменений

changed() доступен у Reg, Do, Di, Ao и Ai. Он сравнивает текущее значение с сохранённым при предыдущем опросе и возвращает bool. При обнаружении отличия сохранённое значение обновляется.

История опроса общая для одного ресурса: вызов Reg(0).changed() влияет на следующий такой вызов через другой объект Reg(0). Промежуточное изменение с возвратом к прежнему значению между опросами может остаться незамеченным.

  from dev import Reg
from time import sleep_ms

r = Reg(0).conf(type='int', access='wr')
r.changed()  # Зафиксировать исходное значение
while True:
    if r.changed():
        print('Новое значение:', r())
    sleep_ms(100)
  

Классы

Reg

Reg(n) — пользовательский регистр. Его индекс должен быть меньше текущего dev.count('Reg').

conf()

Поддерживаются следующие параметры:

  • type: 'bool', 'int', 'uint', 'float' или 'enum'. По умолчанию при каждом вызове — 'int'.
  • name: имя в веб-интерфейсе, до 127 байт UTF-8; русские символы могут занимать несколько байт.
  • access: права доступа, описанные выше.
  • ui: 'default', 'input', 'select', 'radio' или 'button'. По умолчанию — 'default'; вид и применимость элемента зависят от веб-интерфейса и типа регистра.
  • dec: целое от 0 до 7, только для int, uint и float. По умолчанию — 0 для целочисленных типов и 2 для float.
  • limits: список или кортеж из двух значений [minimum, maximum], только для int, uint и float. Вместо одной из границ можно передать None; limits=None отключает обе границы. Если параметр пропущен, ограничения также отключаются.
  • step: неотрицательный числовой шаг для int, uint и float. Используется inc()/dec() без аргумента; для целочисленных типов шаг хранится с учётом dec и должен помещаться в 16 бит.
  • items: список из 2–256 строк для enum. Значением регистра служит индекс от 0 до len(items)-1. Символ ';' в строках запрещён; подписи используют общее ограниченное хранилище имён. Кортеж вместо списка не принимается.
  • default: значение, которое записывается, только если текущее значение не проходит проверку типа/границ после конфигурации. Это не безусловная инициализация и не отдельное сохранённое значение для будущих сбросов.

При повторной настройке указывайте type, access, ui, dec, limits и нужный step явно: они не обязательно сохраняют прежние значения. Для начального значения используйте отдельный вызов r(value).

  from dev import count, Reg

count(Reg=3)

power = Reg(0).conf(name='Питание', type='bool', access='wr')
power(False)

mode = Reg(1).conf(name='Режим', type='enum',
                   items=['Вентиляция', 'Нагрев'], ui='select', access='wr')
mode(0)  # Вентиляция

target = Reg(2).conf(name='Уставка', type='int', dec=1,
                     limits=[10.0, 45.0], step=0.5, default=25.0,
                     access='wr')
target(25.0)  # Явно задать начальную уставку
  

val() и десятичная шкала

  • bool: запись True/False или целых 0/1, чтение возвращает bool.
  • int: хранение в знаковом 32-битном целом.
  • uint: хранение в беззнаковом 32-битном целом.
  • float: хранение в 32-битном числе с плавающей точкой, чтение возвращает float.
  • enum: запись и чтение целого индекса варианта; строка подписи значением не является.

У int и uint параметр dec задаёт фиксированную десятичную шкалу: внутреннее целое делится на 10 ** dec при чтении. При записи и int, и float масштабируются. При dec=0 чтение возвращает int, при dec>0 — float. Диапазон пользовательских значений соответственно уменьшается: например, для знакового регистра с dec=2 он составляет примерно −21474836.48…21474836.47. Дробная часть при преобразовании во внутреннее целое отбрасывается; учитывайте точность float.

У регистра типа float параметр dec относится к форматированию, а не к переводу в целое. Ограничения limits проверяются при записи; выход за границы вызывает исключение, автоматического ограничения значения нет.

  from dev import Reg

r = Reg(0).conf(type='int', dec=2, step=0.25)
r(1)         # Значение 1.00; внутри хранится 100
r.inc()      # 1.25
r.dec(0.5)   # 0.75
print(r())
  

inc(), dec(), on(), off(), toggle()

  • inc() увеличивает значение, dec() уменьшает.
  • inc(delta) / dec(delta) используют явно указанный шаг.
  • Без аргумента используется настроенный step. Для enum шаг равен одному индексу, переход через край списка вызывает исключение.
  • Для bool методы inc()/dec() запрещены.
  • Для int/uint без настроенного шага меняется одна единица внутреннего хранения: при dec=2 это 0.01. Для float исходный шаг — 1.0.
  • on(), off(), toggle() удобно использовать с bool: включить, выключить, инвертировать. На числовых регистрах эти методы работают с внутренними 1 и 0; они не устанавливают максимум диапазона и не применяют десятичную шкалу.
  from dev import Reg

r = Reg(0).conf(type='int')
r(1)
r.inc()
print(r())  # 2
r.dec()
print(r())  # 1
  

Do

Do(n) — цифровой выход. val() / вызов объекта возвращают целое 0 или 1. Для записи допустимы 0, 1, False, True.

Методы: conf(), val(), on(), off(), toggle(), pwm(), changed(). Методов inc() и dec() у Do нет.

Параметры conf():

  • name, access — имя и права доступа;
  • type='do' или 'nopwm' — обычный цифровой выход; это режим по умолчанию при вызове conf();
  • type='pwm' — ШИМ, только на поддерживающих его выходах;
  • period — целое число секунд для ШИМ. В текущем формате хранится в uint16, поэтому используйте положительное значение 1..65535. На выходах без поддержки ШИМ параметр запрещён.

Не все выводы поддерживают ШИМ; их количество зависит от модели. period не является параметром с плавающей точкой. Повторный conf(name=...) без type='pwm' вернёт обычный режим.

  from dev import count, Do
from time import sleep

count(Do=2)
out = Do(1).conf(name='Заслонка', type='do', access='rr')
out.on()
sleep(1)
out.toggle()
  

pwm([percent])

После conf(type='pwm', period=...) метод pwm() читает коэффициент заполнения в процентах как float, а pwm(value) задаёт его. Диапазон — 0..100, разрешение — 0.1 процентного пункта; лишние разряды отбрасываются.

  from dev import Do

heater = Do(0).conf(name='Нагреватель', type='pwm', period=30, access='rr')
heater.pwm(30.0)
print(heater.pwm())  # 30.0
  

val() отражает бинарное состояние выхода, а pwm() — заданное заполнение. Для регулирования в режиме ШИМ используйте pwm(): on()/off() не заменяют установку заполнения 100/0 %, а текущее бинарное состояние обновляется обработчиком ШИМ.

Di

Di(n) — цифровой вход, доступный только для чтения. Методы: conf(), val(), changed(). Чтение di() или di.val() возвращает целое 0 или 1.

Параметры conf():

  • name — имя входа;
  • access — только '-' и 'r' в каждой позиции;
  • type='nz' — нормально закрытый, режим по умолчанию;
  • type='no' — нормально открытый.

Режим меняет полярность логического состояния входа. Соответствие физическому состоянию контакта учитывайте по схеме подключения контроллера.

  from dev import Di
from time import sleep_ms

contact = Di(0).conf(name='Контакт', type='no', access='rr')
while True:
    print(contact())
    sleep_ms(500)
  

Ao

Ao(n) — аналоговый выход 0–10 В. Чтение ao() / ao.val() возвращает float в вольтах.

Параметры conf(): name и access. Аргументы type, web и ext для Ao не используются.

При записи тип аргумента имеет значение:

  • ao(2.5) — float задаёт вольты; внутреннее значение получается умножением на 100;
  • ao(250) — int задаёт внутреннее значение в сотых долях вольта, то есть тоже 2.50 В.

Используйте float, если значение задано в вольтах. Допустимый диапазон — 0.0..10.0 В, внутренний целочисленный диапазон — 0..1000.

  from dev import count, Ao

count(Ao=1)
ao = Ao(0).conf(name='Клапан', access='wr')
ao(2.5)
ao.inc(0.5)
print(ao())  # 3.0
ao.dec()    # Уменьшить на 0.1 В
  

Методы inc(delta) / dec(delta) принимают шаг в вольтах; целый аргумент здесь тоже масштабируется, например inc(1) добавляет 1 В. Без аргумента шаг — 0.1 В. Выход за диапазон вызывает исключение.

off() устанавливает 0 В. В текущей реализации on() записывает внутреннюю единицу, то есть 0.01 В, а не 10 В. Чтобы открыть выход полностью, вызывайте ao(10.0). Метода toggle() у Ao нет. Для наблюдения доступен changed().

Ai

Ai(n) — аналоговый вход. Методы: conf(), val(), changed(). Запись значения не поддерживается.

Параметры conf():

  • name — имя входа;
  • access — только '-' и 'r' в каждой позиции;
  • type='ntc' — температура NTC10K в °C. Это значение по умолчанию при вызове conf();
  • type='raw' — сопротивление в омах, без преобразования в температуру. Это не исходный отсчёт АЦП и не напряжение.

val() и вызов объекта возвращают 32-битный float в физических единицах. Например, 23.5 в режиме ntc означает 23,5 °C, а 10000.0 в режиме raw — 10 кОм. После чтения не нужно делить значение на 10 или 100.

При ошибке измерения возвращается NaN, в том числе при недопустимом сопротивлении датчика. Это значение не равно самому себе; его можно проверить через math.isnan(value) или value != value. Перед вычислениями удобно использовать math.isfinite(value), чтобы исключить и NaN, и бесконечность. 32767 / 0x7FFF не является признаком ошибки Python Ai — этот маркер применяется в целочисленных внешних протоколах.

Температура в веб-интерфейсе округляется до двух знаков после запятой для отображения. Сам сценарий получает float без этого округления; небольшие погрешности представления дробей нормальны.

На X1 вход AI2 аппаратно предназначен для 0–10 В, однако в этой версии Python conf() принимает только ntc и raw. Эти варианты не выбирают режим напряжения. Не используйте Ai(2).conf(type='raw') как способ получить вольты и не передавайте неподдерживаемый type='volt'; перед применением входа 0–10 В убедитесь, что ваша прошивка предоставляет настройку соответствующего режима.

  from dev import count, Ai, run
from math import isfinite

count(Ai=1)
temperature = Ai(0).conf(name='Температура', type='ntc', access='rr')

def show_temperature():
    value = temperature()
    if not isfinite(value):
        print('Ошибка датчика')
    else:
        print('Температура: %.2f °C' % value)

run(show_temperature, 1.0)
  

Для чтения сопротивления настройте нужный вход через Ai(n).conf(type='raw', access='rr'); числовой результат уже выражен в омах. При смене режима новое измерение появляется после очередного опроса входа, а не непосредственно внутри conf().

Event

Event(n) — пользовательское событие. Перед использованием задайте количество событий через count(Event=...). Методы: conf(name=...) и add(...). Событие не является регистром со значением: используйте add(), а не вызов e().

add(par=..., par2=..., reason=...) добавляет запись в журнал. Передача на сервер зависит от настройки служб контроллера.

  • par — целое значение в диапазоне signed int32.
  • par2 — целое значение в диапазоне signed int16. Передавайте его вместе с par: отдельно второй параметр в журнал не записывается.
  • reason — строка 'error', 'auto', 'user', 'ok', 'algo', 'time', 'web', 'ext' или 'pu'. Если параметр пропущен, используется причина «не задана».

Для параметров используйте целые в указанных диапазонах: float приводится к целому, а par2 сужается до 16 бит. Старое значение reason='regular' не поддерживается; для успешного завершения используйте 'ok'.

  from dev import count, Event

count(Event=1)
event = Event(0).conf(name='Цикл завершён')
event.add(par=42, par2=3, reason='ok')
  

Alarm

Alarm(n) — пользовательская авария. Перед использованием задайте count(Alarm=...).

  • conf(name=...) задаёт имя и возвращает объект.
  • on(par=..., par2=..., reason=...) активирует аварию. Аргументы такие же, как у Event.add().
  • Запись об активации добавляется только при переходе из неактивного состояния в активное. Повторный on() уже активной аварии не добавляет новую запись.
  • off() снимает аварию.
  • val() или вызов объекта без аргументов возвращает bool активности.

Для активации с записью в журнал используйте on(), а не прямую запись alarm(True). Методов changed() и toggle() у Alarm нет.

  from dev import count, Alarm

count(Alarm=1)
alarm = Alarm(0).conf(name='Перегрев')
alarm.on(par=80, reason='error')
print(alarm())  # True
alarm.off()
  

PID

PID(kP=1.0, kI=0.0, kD=0.0, limits=...) создаёт независимый ПИД-регулятор. Коэффициенты можно передать позиционно или по имени. Если limits пропущен, ограничение выхода не применяется; иначе нужен список/кортеж из двух чисел, где минимум строго меньше максимума.

  • step(invalue, setpoint) выполняет расчёт и возвращает float — управляющий сигнал.
  • pid(invalue, setpoint) эквивалентен pid.step(...). Оба значения передавайте позиционно.
  • reset() очищает интеграл, предыдущее время, ошибку и выход; коэффициенты и ограничения сохраняются.

Ошибка вычисляется как setpoint - invalue. Заданные limits ограничивают как выход, так и накопленный интеграл. Интервал между вызовами учитывается автоматически, в секундах. Первый шаг после создания/сброса рассчитывает только пропорциональную составляющую. При интервале меньше 50 мс возвращается предыдущий выход. При паузе больше 300 секунд регулятор сбрасывается и возвращает 0.0; следующий вызов снова будет первым шагом.

  from dev import PID
from time import sleep_ms

pid = PID(kP=1.0, kI=0.5, kD=0.1, limits=(0.0, 10.0))
print(pid(20.0, 25.0))
sleep_ms(100)
print(pid(21.0, 25.0))
pid.reset()
  

Modbus

Modbus(...) использует общий порт Modbus RTU контроллера в режиме мастера. Создание объекта настраивает этот порт, поэтому объекты с разными настройками линии не являются независимыми соединениями. Для нескольких устройств одной шины используйте общий объект и addr= в отдельных запросах.

Параметры конструктора:

  • slaveaddr=1 — адрес по умолчанию. Реализация допускает 0..248; для обычных запросов к устройствам используйте адреса 1..247. Приём адреса 0 не означает отдельной реализации обмена без ответа.
  • baudrate=9600 — скорость 300..230400 бит/с.
  • databits=8 — 8 или 9.
  • parity='none' — 'none', 'even' или 'odd'.
  • stopbits — 1 или 2. Если пропущен: 2 без чётности, 1 с чётностью.
  • timeout=0.2 — таймаут обмена в секундах, диапазон 0.0..1.0, преобразуется в целые миллисекунды.
  • except=True — включение исключений при ошибках обмена. Поскольку except — ключевое слово Python, передавайте параметр через словарь: **{'except': False}.

Указывайте параметры конструктора по имени: их много, позиционная форма ограничена первыми четырьмя.

read(start=0, count=1, addr=…, timeout=…)

Читает Holding Registers функцией 0x03:

  • start — нулевой адрес регистра 0..65535 в пакете, без добавления префикса 40001;
  • count — количество; в текущей конфигурации буфера 1..125;
  • последний адрес start + count - 1 не должен превышать 65535;
  • addr и timeout при пропуске берутся из объекта.

При count=1 результат — одно целое, при count>1 — кортеж целых. Значения регистров беззнаковые, 0..65535; сборку 32-битных значений и интерпретацию знака выполняет сценарий.

write(start=0, values=…, addr=…, timeout=…)

Записывает Holding Registers функцией 0x10, в том числе при записи одного регистра. values обязателен: одно число/bool или непустой список/кортеж. В текущей конфигурации допустимо 1..123 регистра; диапазон адресов также должен укладываться в 0..65535.

Для предсказуемой записи используйте целые 0..65535. bool преобразуется в 0/1. float допускается в диапазоне [0, 65536) и усекается до целого. Целые значения после преобразования в машинный int сужаются до 16 бит, поэтому отрицательные и слишком большие значения не следует использовать как проверку диапазона протокола.

Успешная запись возвращает None. Если исключения отключены, неуспешная запись тоже возвращает None: проверяйте err().

Ошибки и счётчики

  • При ошибке обмена по умолчанию вызывается исключение.
  • При **{'except': False} неуспешное чтение возвращает None; ошибка записи определяется через err().
  • Неверные аргументы всё равно вызывают исключения, даже если исключения обмена отключены.
  • err() — строка с результатом последнего обмена объекта; при успехе 'ok'.
  • reads() и writes() — счётчики операций, дошедших до возврата из соответствующего метода. Ошибки с отключёнными исключениями учитываются, а вызовы, завершившиеся исключением, не увеличивают счётчик.
  from dev import Modbus

mb = Modbus(slaveaddr=1, baudrate=38400, parity='none',
            stopbits=2, timeout=0.5, **{'except': False})

value = mb.read(start=0)
if value is None:
    print('Ошибка чтения:', mb.err())
else:
    print('Регистр 0:', value)

mb.write(start=20, values=[1, 2, 3], addr=2, timeout=0.5)
print('Результат записи:', mb.err())
print('Операций:', mb.reads(), mb.writes())
  

Функции

run

Поддерживаются две формы вызова:

  run(function, interval)
run((function1, interval1), (function2, interval2))
  

Пары можно задавать списками или кортежами; допускается до восьми обработчиков. Для одной пары допустимо run((function, interval)). Интервал обязателен: run(function) не подставляет одну секунду автоматически.

  • Обработчик — обычная функция Python, объявленная через def или lambda, с нулём или одним позиционным параметром. Встроенные функции, связанные методы и экземпляры с __call__ не принимаются; keyword-only параметры не поддерживаются.
  • Если функция объявлена с одним параметром, ей передаётся номер её вызова: 0, 1, 2, …, отдельно для каждого обработчика.
  • Интервал задаётся в секундах: строго больше 0.020, не больше 2147483.647. Он переводится в целые миллисекунды. Для небольших периодов используйте значения от 0.021 с учётом точности float.
  • run() занимает текущий поток сценария до остановки. Обработчики выполняются последовательно; долгий обработчик задерживает остальные, точное соблюдение периода не гарантируется.
  • Первый вызов обычно выполняется сразу; если сценарий запущен в самые первые миллисекунды после включения, возможна начальная задержка.
  • Возврат именно False из любого обработчика останавливает весь run(). None, в том числе неявный возврат, продолжает работу.
  • После обработчика вызывается сборщик мусора. Это не гарантирует, что автоматическая сборка не произойдёт внутри обработчика при выделении памяти.
  • Исключение обработчика выходит из run(). Повторный вызов создаёт новое расписание.
  from dev import run, Do

out = Do(0).conf(type='do')

def blink(number):
    out.toggle()
    return number < 9  # После десятого вызова вернуть False

run(blink, 1.0)
  

Несколько периодических задач:

  from dev import count, Reg, run

count(Reg=2)
a = Reg(0).conf(type='int')
b = Reg(1).conf(type='int')
a(0)
b(0)

def fast():
    a.inc()

def slow(number):
    b.inc()
    if number == 4:
        return False

run((fast, 0.25), (slow, 1.0))
  

count

count('Reg') возвращает текущее количество объектов. Имена 'Do', 'Di', 'Ao', 'Ai', 'Reg', 'Event', 'Alarm' нечувствительны к регистру; неизвестное имя вызывает исключение.

count(Reg=..., Alarm=..., Event=..., Ai=..., Ao=..., Di=..., Do=...) задаёт количества. Все параметры этой формы передаются по имени; пропущенные параметры не изменяются.

  • Reg — пользовательские регистры.
  • Alarm — пользовательские аварии.
  • Event — пользовательские события.
  • Ai / Ao — аналоговые входы / выходы.
  • Di / Do — цифровые входы / выходы.

Диапазон — от нуля до предела модели включительно. В текущей конфигурации X0: Reg/Alarm/Event — до 32, Ai/Ao — до 3, Di — до 5, Do — до 3. У других моделей пределы могут отличаться. Возвращаемое значение формы установки — None.

Для физических входов и выходов количество управляет включением первых N каналов и их отображением. Для записи Do/Ao канал должен входить в настроенное количество; успешное создание объекта ещё не гарантирует возможность записи. Конструкторы Ai/Ao/Di/Do проверяют аппаратный предел модели, а не это настроенное количество. Для Reg/Event/Alarm индекс проверяется относительно настроенного количества. Поэтому count() нельзя использовать как универсальную защиту от обращения сценария к физическому каналу.

  from dev import count, Reg

count(Reg=3, Event=1, Alarm=1)
print(count('reg'))  # 3
r = Reg(2)          # Последний из трёх регистров
  

webconf

webconf(Display=..., Reg=..., Pin=..., Console=...) управляет видимостью разделов панели управления; значения — bool.

  • Display — виртуальный дисплей.
  • Reg — пользовательские регистры.
  • Pin — входы и выходы.
  • Console — консоль.

Пропущенные параметры выключаются, прежнее состояние не сохраняется. Например, webconf(Reg=True) оставляет видимым только раздел регистров. Это настройка видимости, а права на конкретные значения задаются через access.

  from dev import webconf

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

conv

conv(value, in_min, in_max, out_min, out_max) линейно переводит значение из входного диапазона в выходной и возвращает float. Все пять аргументов передаются позиционно.

Для возрастающего входного диапазона:

  • при value <= in_min результат равен out_min;
  • при value >= in_max — out_max;
  • внутри диапазона применяется линейная интерполяция;
  • при in_min == in_max возвращается out_max.

Выходной диапазон может быть убывающим. Входной диапазон задавайте в порядке in_min < in_max.

  from dev import conv

print(conv(50, 0, 100, 2.0, 10.0))  # 6.0
print(conv(150, 0, 100, 2.0, 10.0)) # 10.0
  

name, idcpu, idusr, uptime

Все четыре функции вызываются без аргументов:

  • name() — системное имя из прошивки, в текущей реализации 'telematic'; это не пользовательское имя из настроек.
  • idcpu() — идентификатор процессора: строка из 24 шестнадцатеричных символов.
  • idusr() — идентификатор контроллера: строка из 8 шестнадцатеричных символов.
  • uptime() — целое число секунд работы контроллера после включения, а не время исполнения текущего сценария.
  import dev

print(dev.name(), dev.idcpu(), dev.idusr())
print('Время работы, с:', dev.uptime())