Модуль dev
Доступ к входам, выходам, регистрам, событиям и Modbus из MicroPython
Модуль 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())