Перейти к содержанию

Программирование устройств UnitX на Lua

Руководство по программированию устройств UnitX на Lua: различия сред исполнения, структура проекта, многозадачность, память, ошибки, watchdog, безопасность и примеры ODNFC.

Это общее руководство по программированию устройств UnitX на Lua. Оно охватывает общие инженерные правила для всех моделей, но не объединяет разные среды исполнения в одну:

  • в ODNFC и LuaTerm используется Lua 5.3 в составе Lua RTOS и FreeRTOS;
  • контроллер С3 использует другую реализацию Lua — не переносите в него модули, примеры и средства разработки Lua RTOS.

Руководство также не относится к uLua: это другие среды с собственными ограничениями и API.

Сначала откройте инструкцию конкретного устройства, затем используйте это руководство и справочник Lua:

Набор модулей, выводы, настройки и способы подключения зависят от модели, исполнения и версии прошивки. При расхождении между этой страницей и инструкцией устройства следуйте инструкции устройства.

Если вы будете разрабатывать с помощью ИИ, скопируйте промпт для Lua.

Ключевые правила разработки

Lua-программа при выполнении вашей задачи делит процессор, оперативную память и периферию с сетью, веб-интерфейсом и системными задачами прошивки. Поэтому код, который нормально запускается на компьютере, ещё не готов к работе в контроллере. Принципы ниже общие, а конкретные имена thread, cpu, Settings и usercode.lua применимы только к тем моделям, где их подтверждает руководство устройства.

Учитывайте модель и среду исполнения

  • Зафиксируйте модель, исполнение и версию прошивки. Не переносите API из NodeMCU, настольной Lua или другой прошивки только из-за совпадения имени модуля. Проверяйте каждую функцию и константу по инструкции устройства и справочнику этой версии.
  • Уточните точку входа и структуру проекта. В ODNFC и LuaTerm пользовательская точка входа — usercode.lua. Небольшую программу можно оставить в одном файле, а крупную — разделить на модули и подключить через require. В этих прошивках нет менеджера пакетов: загрузите все дополнительные файлы вместе с проектом.
  • Не дублируйте системную инициализацию. Сеть, веб-интерфейс и часть периферии запускает прошивка. В ODNFC и LuaTerm не перенастраивайте их из usercode.lua, если вы не делаете это осознанно.
  • Не полагайтесь на поведение настольной или другой встроенной Lua. Состав стандартных библиотек, числовые типы, встроенные модули и обработка ошибок зависят от реализации и сборки. Например, широкие идентификаторы безопаснее хранить строкой или бинарными данными, если они не помещаются в Lua integer.

Не занимайте планировщик надолго

  • Исключите активное ожидание. Каждый бесконечный или долгий цикл должен регулярно отдавать управление планировщику. В Lua RTOS используйте thread.sleep, thread.sleepms или документированный блокирующий ввод-вывод. Интервал выбирайте по задаче.
  • Делайте обработчики событий короткими. В обработчике прочитайте и сохраните событие, а долгую сетевую операцию или вычисление выполните отдельно. Иначе один медленный обработчик задержит чтение следующего события и системные задачи.
  • Не создавайте поток для каждого события. Запускайте только необходимые долгоживущие потоки. В Lua RTOS проверяйте их состояние и запас стека через thread.list(). По возможности закрепите периферийный интерфейс за одним потоком; если ресурс общий и среда поддерживает мьютексы, защищайте совместный доступ.
  • Делите тяжёлую работу на части. Большой разбор данных, перебор таблицы или запись файла выполняйте порциями и между ними возвращайте управление планировщику.

Контролируйте память и флеш-память

  • Контроллируйте потребление памяти. Смотрите свободную кучу через cpu.memfree() на устройствах с Lua RTOS или через диагностический API своей среды до запуска задачи, под рабочей нагрузкой и после повторяющихся операций.
  • Сокращайте число временных объектов. Не собирайте большой ответ или файл целиком, если данные можно обработать порциями. В частом цикле переиспользуйте таблицы и буферы, избегайте цепочек конкатенации строк и не храните историю без ограничения размера. Не создавайте большого количества локальных переменных.
  • Учитывайте сборщик мусора. Частые выделения памяти увеличивают работу GC и могут добавить паузы. Если среда поддерживает collectgarbage(), вызывайте его вручную только после измерений и в месте, где пауза допустима, а не в каждом цикле.
  • Не записывайте флеш-память при каждом опросе. Сохраняйте настройку только при изменении, а события и телеметрию буферизуйте или объединяйте. Ограничивайте размер и число файлов журнала.

Проектируйте восстановление после ошибок

  • Проверяйте результат функций. Одни API возвращают nil, err или false, err, другие при ошибке создают исключение, а при успехе могут ничего не вернуть. Не считайте любое пустое значение ошибкой; используйте описание конкретной функции и pcall для ожидаемых исключений.
  • Ограничивайте ожидание и повторные попытки. Для сети и периферии задавайте тайм-ауты, обрабатывайте обрыв соединения и увеличивайте паузу между повторными подключениями. Цикл мгновенных повторов одновременно загружает процессор и скрывает исходную ошибку.
  • Освобождайте ресурсы на всех ветках. Закрывайте файлы и соединения после успеха и ошибки. Не оставляйте заблокированный мьютекс, если защищённая операция завершилась исключением.
  • Проверяйте внешние данные. До обращения к GPIO, файлу или протоколу ограничьте длину входной строки, проверьте формат, диапазоны и допустимые команды. Не исполняйте полученный по сети текст через load.
  • Оставьте путь к восстановлению. Перед экспериментом сохраните рабочие файлы на компьютере и проверьте доступ по последовательной консоли.

Сторожевой таймер должен контролировать основную логику

Сторожевой таймер (watchdog) перезагружает устройство, если пользовательская программа перестала подтверждать, что работает по ожидаемому сценарию. Это запасной способ восстановления на случай, если программа завершилась из-за необработанной ошибки, зависла или попала в состояние, которое программист не предусмотрел. В текущей конфигурации Lua RTOS для ODNFC тайм-аут составляет 30 секунд. Если за это время программа не сбросит включённый таймер, устройство перезагрузится.

Вызов cpu.watchdog.reset() означает: «Основная логика завершила очередной исправный цикл». Поставьте этот вызов в одну осмысленную контрольную точку основного или наиболее ответственного цикла и выполняйте только после того, как критические операции завершились, а программа вернулась в ожидаемое состояние. Для считывателя штатным результатом может быть и обработанная карта, и завершённый опрос без карты. Не сбрасывайте watchdog безусловно в отдельном потоке или таймере: такой код продолжит работать, даже если основная логика зависла, и устройство не сможет восстановиться.

  • Во время разработки выключите watchdog в окне «Настройки» (sys_wdt = off) и перезагрузите устройство. Таймер мешает делать намеренные паузы, проверять незавершённый код и разбирать ошибки: через 30 секунд устройство перезагрузится и может скрыть исходную причину сбоя.
  • Перед установкой на объект включите watchdog (sys_wdt = on), перезагрузите устройство и проверьте восстановление. Для проверки временно не сбрасывайте таймер дольше 30 секунд и убедитесь, что устройство перезагрузилось. После проверки удалите тестовую задержку.

Отделяйте конфигурацию от программы

  • Не зашивайте изменяемые адреса и режимы в логику. Храните их в настройках устройства или отдельном конфигурационном файле. Исходники и резервные копии не должны содержать пароли, токены и закрытые ключи.
  • Не выводите секреты и персональные данные в журнал. Для диагностики записывайте состояние операции, код ошибки и безопасный идентификатор. У повторяющегося сообщения ограничьте частоту, иначе журнал вытеснит полезную историю.
  • Закройте отладочный доступ перед установкой “набело”. Отключите неиспользуемые WebSocket REPL, Telnet, HTTP, SSH и другие службы. Оставшиеся интерфейсы защитите паролями и не публикуйте напрямую в интернете.

Проверяйте программу по слоям

  1. Отладьте на компьютере чистые функции, которые не зависят от железа и модулей прошивки. Версия настольного интерпретатора должна совпадать с версией языка на устройстве; для С3 уточните её отдельно.
  2. На устройстве сначала проверьте запуск, журнал, модель и свободную память.
  3. Подключайте по одному входу, датчику или сетевому действию. Выходы и силовую нагрузку проверяйте отдельно, начиная с безопасного состояния.
  4. Проверьте длительную работу, повторные подключения, перезагрузку, потерю сети и питания, заполнение файлов и срабатывание watchdog.
  5. Перед установкой сохраните исходники, версию прошивки и рабочие настройки. После записи перезагрузите устройство и убедитесь по журналу, что запустилась новая версия программы.

Средства разработки

Доступные способы подключения зависят от устройства и реализации Lua. Для небольшой программы обычно достаточно встроенного редактора. Проект из нескольких файлов удобнее вести на компьютере и загружать поддерживаемым устройством способом. Очень сильно облегчит разработку/поддержку отладочный USB-донгл.

Встроенный редактор

В ODNFC откройте окно «Редактор», нажмите «Прочитать», внесите изменения и нажмите «Записать». Затем перезагрузите устройство. В LuaTerm названия действий и порядок запуска описаны в руководстве устройства. У С3 есть собственный редактор в веб-интерфейсе; используйте инструкцию С3, а не инструкции для usercode.lua ниже.

Встроенный редактор usercode.lua в веб-интерфейсе ODNFC
Встроенный редактор файла usercode.lua.

Онлайн-среда Lua RTOS (веб-IDE)

На странице lua.unitx.pro находится веб-среда разработки для совместимых устройств на Lua RTOS. Её можно открыть в браузере или установить на компьютер. К С3 эта инструкция не относится.

  • По WebSocket. Если этот транспорт поддерживается, включите в устройстве HTTPS и WebSocket REPL. Возможно, потребуется один раз вручную подтвердить в браузере самоподписанный сертификат. После подключения можно ходить по файловой системе, редактировать файлы и пользоваться терминалом.
  • По последовательному порту. Нужен сервисный разъём и подходящий USB–UART-адаптер. Браузер запросит доступ к порту. Этот режим не зависит от сетевого соединения.
Онлайн-среда разработки Lua для ODNFC
Онлайн-среда Lua: работа в браузере или отдельном окне PWA.
Подключение к ODNFC по WebSocket в онлайн-среде Lua
Подключение по WebSocket: включите HTTPS и WebSocket REPL.
Файловая система и редактор в онлайн-среде Lua для ODNFC
После подключения доступны файлы, редактор и терминал.
Последовательное подключение к ODNFC в онлайн-среде Lua
Вариант подключения через отладочный USB-донгл.
Запрос доступа к USB-порту в онлайн-среде Lua
Браузер запросит разрешение на доступ к USB-порту.
Терминал последовательного подключения в онлайн-среде Lua
Работа через USB не зависит от сетевого соединения.

Расширение Lua RTOS для VS Code

Расширение для совместимых устройств на Lua RTOS позволяет разрабатывать в привычной IDE. К С3 эта инструкция не относится. Установка: Extensions → … → Install from VSIX. Для подключения по WebSocket включите в устройстве WebSocket REPL. Расширение может подключаться без HTTPS; используйте такой режим только в доверенной локальной сети и не публикуйте порт в интернет. Нажмите шестерёнку в плагине, выберите тип подключения и задайте сетевой адрес, затем нажмите Connect. Конкретные транспорты зависят от модели и прошивки.

Установка плагина ODNFC Lua для VS Code из файла VSIX
Установка: Extensions → … → Install from VSIX.
Настройки подключения к ODNFC в плагине VS Code
В настройках выберите транспорт и сетевой адрес.
Кнопка Connect в плагине VS Code для ODNFC
После настройки нажмите Connect.
Файлы устройства ODNFC в плагине VS Code
После подключения доступны файлы устройства и терминал.

Программирование ODNFC

Следующие настройки и примеры относятся к ODNFC с Lua-прошивкой. Примеры чтения карт требуют встроенного модуля mfrc522; они не подходят для UHF-модели и других устройств без этого модуля. Для LuaTerm и С3 используйте отдельные руководства: у них другой набор периферии, настроек и готовых сценариев.

Настройки пользовательской программы

Параметры из окна «Настройки» доступны в usercode.lua через глобальный объект Settings. Метод Settings.get() читает значение по имени:

local destination = Settings.get("net_dest")
local wdt_on = Settings.get("sys_wdt") == "on"

iprint("destination:", destination)

Здесь net_dest — получатель из сетевых настроек, а sys_wdt — состояние сторожевого таймера. Сеть запускает системный код, поэтому вручную вызывать net.wf.setup() в usercode.lua не нужно. Функция iprint() выводит сообщения в окно «Информация» веб-интерфейса.

Быстрый старт: от запуска до чтения карты

Начинайте с одного действия и добавляйте следующее только после проверки предыдущего. Каждый фрагмент ниже — самостоятельный usercode.lua.

1. Убедиться, что программа запустилась

local board, subtype, brand = cpu.board()

iprint("usercode.lua started")
iprint("device:", board, subtype, brand)
iprint("free memory:", cpu.memfree())

После сохранения и перезапуска эти строки должны появиться в окне «Информация» веб-интерфейса.

2. Прочитать UID в окне «Информация»

local wdt_on = Settings.get("sys_wdt") == "on"

while true do
    local callback_ok, err = mfrc522.scan(0, function(uid)
        iprint("UID:", uid)
    end)
    if callback_ok == false then
        iprint("scan callback error:", err)
    elseif wdt_on then
        cpu.watchdog.reset()
    end
    thread.sleepms(50)
end

0 выбирает UID как шестнадцатеричную строку в верхнем регистре. Функция-обработчик вызывается только после чтения карты; отсутствие карты — нормальное состояние, а не ошибка.

3. Добавить свет и звук

local wdt_on = Settings.get("sys_wdt") == "on"

local leds = indication.Leds.new()
local snd = indication.Sound.new()

leds:start()
snd:start()

while true do
    local callback_ok, err = mfrc522.scan(0, function(uid)
        iprint("UID:", uid)
        leds:ok()
        snd:ok()
    end)
    if callback_ok == false then
        eprint("MFRC522: " .. tostring(err))
        leds:err()
        snd:err()
    elseif wdt_on then
        cpu.watchdog.reset()
    end
    thread.sleepms(50)
end

Конструкторы без явных пинов используют настройки конкретной платы, поэтому этот фрагмент подходит и для ODNFC-LAN, и для ODNFC-LAN-C.

Примеры сценариев

Все семь вариантов собраны в каталоге шаблонов программ ODNFC: там можно сравнить сценарии, посмотреть совместимость и скачать отдельный .lua-файл. Ниже подключены те же файлы, поэтому код в документации и скачиваемый шаблон не расходятся. Основной вариант — передача UID по UDP.

Передача по UDP — основной шаблон.

local get, sleepms = Settings.get, thread.sleepms

-- Сеть запускается системным кодом по Web-настройкам.
local host, _, _, port = net.parseUrl(get("net_dest"))
local wdt_on = get("sys_wdt") == "on"

local leds = indication.Leds.new()
local snd = indication.Sound.new()

leds:start()
snd:start()

local function on_uid(uid)
    net.udp.sendto(host, port, uid)
    iprint("UDP sent:", uid)
    leds:ok()
    snd:ok()
end

local function iteration()
    local ok, err = mfrc522.scan(0, on_uid)
    if ok == false then error(err, 0) end
end

while true do
    if wdt_on then cpu.watchdog.reset() end

    local ok, err = pcall(iteration)
    if not ok then
        local message = tostring(err)
        if message:find("interrupted!", 1, true) then
            error(err, 0)
        end
        eprint("Main loop: " .. message)
        leds:err()
        snd:err()
        sleepms(250)
    else
        sleepms(50)
    end
end

net.udp.sendto при успехе ничего не возвращает, поэтому результат здесь проверяется через защищённый вызов общего шага цикла. Адрес и порт берутся из Web-настройки net_dest.

Дополнительные рецепты

Отправка почты

Рассчитано на версию PRO — в остальных может не заработать (зависит от нагрузки и сертификатов почтового сервера).

-- имя и порт почтового сервера
net.curl.mailserver("smtp.адрес.сервера", 465)

options = {
    user = "логин",
    pass = "пароль",
    to = {"получатель1", "получатель2"},
    subj = "Тема",
    msg = "Тестовая посылка",
    secure = true,
    attach = {"picture.jpg"}
}

ret, msg = net.curl.sendmail(options)

Добавление UID с сервера по HTTP GET

-- host, path, content-type, headers, ssl_en
local code, chunks = net.http.get(
    "192.168.0.160",
    "/userlist.txt",
    "text/plain",
    nil,
    false
)

if code == 200 and chunks then
    local file = assert(io.open("userlist.txt", "a"))
    -- "a" — append для текстового списка;
    -- большой список экономнее хранить как uint32 UID в бинарном виде
    file:write(table.concat(chunks))
    file:close()
end

Отправка UDP и TCP

--- udp ---
net.udp.sendto("192.168.68.110", 5555, "hello")

--- tcp ---
net.tcp.sendto("192.168.68.110", 5555, "hello")

Добавление UID с сервера по UDP

Аналогично можно пополнять список через MQTT или HTTP.

local socket = net.udp.bind(5555)

while true do
    local from_ip, from_port, data = socket:readfrom(1024)
    if from_ip then
        iprint(from_ip, from_port, data)
        local file = assert(io.open("userlist.txt", "a"))
        file:write(data, "\n")
        file:close()
    end
    thread.sleepms(100)
end

TCP-сервер

Доступен с прошивки 1.5.0. Данные могут приходить дроблёными — буферизируйте их и собирайте в обработчике. Передаются «как есть», могут включать непечатаемые символы и 0 (для Lua-строк это нормально).

local SERVER_PORT = 8080

local function handle_client(client_ip, data)
    -- убираем перевод строки
    local clean_data = data:gsub("\r", ""):gsub("\n", "")
    iprint("client: " .. client_ip)
    iprint("вы ввели: " .. clean_data)
end

iprint("Запуск TCP сервера на порту", SERVER_PORT)
-- false оставляет вывод iprint в окне «Информация»
net.tcpserv.start(SERVER_PORT, handle_client, 60, false)

Дополнительные материалы

  • Строка форматирования — как читать данные с карты, а не только UID.
  • mfrc522.scan — форматы результата, функции-обработчики и режимы обслуживания считывателя.
  • PIO — прямое управление входами и выходами.
  • Сеть — HTTP, UDP, TCP и сетевые интерфейсы.
  • Консоль — работа с файловой системой, редактором и дополнительными библиотеками.
  • Пример: Modbus TCP.

История обновлений ODNFC на Lua

Если установлена версия ниже 1.7.0, сначала обновите её до 1.7.0, а затем до более новой версии. Перед обновлением сотрите пользовательскую программу и запустите устройство без сторожевого таймера.

1.8.2
  • Повышена стабильность Lua: исправлены ошибки выполнения кода, управления памятью, работы с большими строками и обработки ошибок.
  • Исправлен tcpserv; снижено потребление памяти при длительных HTTP-соединениях.
  • Улучшено восстановление Ethernet/DHCP и стабильность WebSocket/REPL.
  • Расширена поддержка RFID.
  • Расширен функционал BLE.
1.8.1

Улучшения:

  • Новый веб-интерфейс.
  • Улучшена работа RFID-модулей MFRC и EM-Marine.
  • Улучшена работа Modbus.
  • Оптимизирована работа устройства.
1.7.0

Улучшения:

  • улучшена работа MFRC и EM-Marine;
  • в раздел «Редактор» добавлен простой файловый менеджер;
  • улучшена работа сетевых модулей;
  • оптимизирована работа устройства.

Исправлена ошибка в HTTP-заголовке Content-Length, из-за которой интерфейс не работал в некоторых операционных системах.

1.6.2
  • Улучшена работа MFRC и EM-Marine.
  • Улучшена библиотека Modbus.
  • Оптимизирована работа устройства.
  • В ODNFC-LAN-LUA добавлена регистрация обработчика успешного чтения.
1.6.1
  • Добавлена запись в защищённые блоки карт MIFARE Classic.
  • Упрощена инициализация RFID: используются значения по умолчанию для каждой платы, поэтому изменён вызов основного класса и больше не требуются многие Lua-обёртки из rfid.lua.
  • Добавлена функция mfrc522.scan() — упрощённая версия базового класса для типовых операций.
  • Исправлены библиотеки EM-Marine и HID.
  • Оптимизирована библиотека Modbus.
  • Исправлена работа TCP-сервера.
1.6.0

Улучшения:

  • обновлён редактор кода: устранена проблема с переводами строк в новых версиях Firefox;
  • в веб-сервер добавлена поддержка HTTPS и WebSocket;
  • добавлены новые классы для работы с RFID-метками;
  • добавлена работа с NDEF и счётчиками Ultralight;
  • добавлены встроенные библиотеки cbor, cobs, mlib, Heatshrink и microtar;
  • расширены библиотеки osc, net, cutils, utils, cpu и emmarine;
  • LuaSocket больше не входит в прошивку;
  • добавлена загрузка сжатых Heatshrink-библиотек Lua;
  • добавлен WebSocket REPL для отладки через веб-IDE или расширение VS Code;
  • улучшена консоль: Unicode, автодополнение и история;
  • расширена поддержка Modbus;
  • Paho MQTT заменён собственной реализацией uMqtt;
  • ускорена работа устройства и веб-интерфейса;
  • выполнены другие небольшие исправления и оптимизации.

Исправлены ошибки, из-за которых после мягкой перезагрузки Wi-Fi запускался через раз, а RFID-модуль мог не инициализироваться.

1.5.0

Улучшения:

  • новый интерфейс и редактор кода;
  • добавлена библиотека tinycobs;
  • добавлены функции net.tcp.*;
  • добавлены функции eprint, hprint и wprint для вывода в окно «Инфо»;
  • трассировка ошибки загрузки теперь показывается в окне «Инфо»;
  • расширены возможности Telnet, SSH отключён;
  • в режиме восстановления Wi-Fi работает без пароля;
  • добавлена поддержка BLE-маяков на устройствах с BLE;
  • NTP запускается с задержкой 15 секунд, чтобы сеть успела установиться.

Исправлены режим статического Ethernet-адреса и установка яркости NeoPixel.

1.4.0
  • Улучшен встроенный веб-сервер.
  • Выполнены небольшие изменения для повышения стабильности.
  • Исправлена утечка памяти при HTTP-запросах из Windows.

Известная проблема этой версии: режим статического Ethernet-адреса в настройках не работает. Для обхода проблемы в начало пользовательской программы добавляли:

net.en.setup(
  net.packip(Settings.get("net_ip")),
  net.packip(Settings.get("net_mask")),
  net.packip(Settings.get("net_gw")),
  net.packip(Settings.get("net_dns"))
)
net.en.start()
1.3.2
  • В os.df() добавлен дополнительный параметр.
  • Выполнены небольшие изменения для повышения стабильности.
  • Исправлена утечка памяти при запросе метрики в интерфейсе.

Известная проблема этой версии: HTTP-запросы из Windows приводят к утечке памяти. После изменения настроек рекомендовалось закрыть браузер, перезагрузить устройство и не выполнять периодические HTTP-запросы.

1.3.1
  • Серийный номер синхронизирован с DataMatrix на корпусе модуля, если он есть.
1.3
  • Библиотека Telegram.lua добавлена в прошивку.
  • Библиотека LuaSocket теперь распространяется отдельно.