Utils
Нативный модуль utils предоставляет набор утилит для работы с кэшами, логами,
бинарными данными, таблицами, путями файловой системы и временными фильтрами.
Файл utils.lua больше не нужен.
Модуль доступен автоматически как utils. Явная загрузка также поддерживается:
local utils = require("utils")
Таймер опроса и временной фильтр
Функции printTable, profile, kvprint, perioder и time_filter
реализованы в C и доступны через utils; отдельной Lua-обёртки нет.
Контракты вывода и профилирования описаны на
странице utils.
utils.perioder(lifetime)возвращает userdata с методамиstart(callback, [now]),check([now]),stop()иltime(lifetime). Callback вызывается только при опросеcheck(), без фонового RTOS-таймера.utils.time_filter(lifetime, [capacity])возвращает userdata с методамиappend(key, [now]),find(key, [noclean], [now])иclean([now]). Ключи сохраняют семантику Lua-таблиц, кроме запрещённыхnilиNaN.find()проверяет одну запись за O(1) в среднем;clean()выполняет полный обход.append()периодически очищает фильтр и возвращаетfalse, если заданная ёмкость заполнена непросроченными ключами.
lifetime — конечное положительное число секунд. capacity — неотрицательное
целое число; 0 или отсутствие аргумента означает отсутствие явного лимита.
По умолчанию используется монотонное время в целых секундах вместо os.time().
Явные now могут быть дробными, но все вызовы одного объекта должны
использовать одинаковую временную шкалу. Первое чтение времени выбирает
режим объекта: явный now либо время по умолчанию. Смешение этих режимов
вызывает Lua-ошибку; stop() и clean() режим не сбрасывают.
Поля прежних Lua-таблиц напрямую
недоступны; используйте методы.
Подробные контракты, правила очистки, обработки callback и примеры приведены в документации utils.
Кэш целых чисел
Кэш целых чисел предназначен в том числе для экономичного белого списка
4-байтных RFID UID. Внутри каждый ключ хранится как uint32_t, без hex-строки и
полей пользователя. В сборках с MLIB используется хеш-множество с поиском O(1)
в среднем; в сборках без MLIB — компактный массив с линейным поиском O(n).
mfrc522.scan(2, callback) сразу возвращает 4-байтный UID в формате,
подходящем для addi() и findi(); преобразовывать hex-строку не нужно. В
32-битной сборке Lua значения с установленным старшим битом отображаются как
отрицательные целые, но addi() и findi() преобразуют их в uint32_t и
сохраняют все 32 бита. Формат result_type = 2 не подходит для UID длиной 7
или 10 байт.
utils.newi(size)
Создать кэш фиксированной ёмкости для хранения 32-битных целых значений.
Аргументы:
- size: число; размер кэша (1-5000)
Возвращает: userdata кэша
local cache = utils.newi(100)
Пример проверки 4-байтного UID без создания строк:
local allowed = utils.newi(2000)
local ok, err = mfrc522.scan(2, function(uid32)
if utils.findi(allowed, uid32) then
print("UID разрешён")
else
print("UID не найден")
end
end)
if ok == false then
error(err, 0)
end
utils.addi(cache, value)
Добавить целое число в кэш.
Аргументы:
- cache: userdata; кэш, созданный через newi()
- value: число; значение для добавления
Возвращает true, если значение добавлено или уже было в кэше. Возвращает
false, если заполненный кэш не может принять новое значение.
local cache = utils.newi(100)
utils.addi(cache, 12345)
utils.addi(cache, 67890)
utils.findi(cache, value)
Найти целое число в кэше.
Аргументы:
- cache: userdata; кэш
- value: число; искомое значение
Возвращает: boolean; true если найдено, false если нет
local cache = utils.newi(100)
utils.addi(cache, 12345)
print(utils.findi(cache, 12345)) -- true
print(utils.findi(cache, 99999)) -- false
Кэш строк
utils.news(size, maxStrLen)
Создать кэш для хранения строк.
Аргументы:
- size: число; количество строк (1-5000)
- maxStrLen: число; максимальная длина строки (1-20)
Возвращает: userdata кэша
local cache = utils.news(100, 10)
utils.adds(cache, str)
Добавить строку в кэш.
Аргументы:
- cache: userdata; кэш, созданный через news()
- str: строка; значение для добавления
Возвращает true, если строка добавлена или уже была в кэше. Возвращает
false, если кэш заполнен, строка длиннее maxStrLen или содержит нулевой
байт.
local cache = utils.news(100, 10)
utils.adds(cache, "ABC123")
utils.finds(cache, str)
Найти строку в кэше.
Аргументы:
- cache: userdata; кэш
- str: строка; искомое значение
Возвращает: boolean; true если найдено
local cache = utils.news(100, 10)
utils.adds(cache, "ABC123")
print(utils.finds(cache, "ABC123")) -- true
Логирование
utils.newLog(size)
Создать буфер для логов.
Аргументы:
- size: число; размер буфера в байтах (1–65536)
Возвращает: userdata лога
local log = utils.newLog(1024)
utils.addLog(log, message, level)
Добавить сообщение в лог.
Аргументы:
- log: userdata; лог, созданный через newLog()
- message: строка; текст сообщения
- level: число; уровень лога (utils.Info, utils.Warn, utils.Err)
Функция ничего не возвращает. Когда буфер заполнен, она удаляет самые старые записи. Сообщение, которое вместе со служебными данными не помещается в буфер, не добавляется. Неизвестный уровень логирования также игнорируется.
local log = utils.newLog(1024)
utils.addLog(log, "System started", utils.Info)
utils.addLog(log, "Low memory", utils.Warn)
utils.addLog(log, "Connection failed", utils.Err)
utils.printLog(log)
Получить содержимое лога.
Аргументы:
- log: userdata; лог
Возвращает: строка; содержимое лога
local log = utils.newLog(1024)
utils.addLog(log, "Hello", utils.Info)
print(utils.printLog(log))
Константы уровней логирования
utils.Info— информационное сообщение (@)utils.Warn— предупреждение ($)utils.Err— ошибка (^)
Бинарные функции
utils.hex(num [, big])
Преобразовать младший байт числа в двухсимвольную шестнадцатеричную строку.
Аргументы:
- num: целое число от 0 до 255; выход за диапазон вызывает Lua-ошибку
- big: boolean (опционально); true для верхнего регистра
Возвращает: строка; hex-представление
print(utils.hex(255)) -- "ff"
print(utils.hex(255, true)) -- "FF"
utils.hexlify(data [, big])
Преобразовать бинарную строку в hex-строку или таблицу байтов в таблицу hex-строк.
Аргументы:
- data: бинарная строка или массив целых чисел от 0 до 255
- big: boolean (опционально); true для верхнего регистра, по умолчанию false
Для строки возвращает одну hex-строку; нулевые байты и байты 128–255 сохраняются. Пустая строка возвращает пустую строку. Для таблицы возвращает массив двухсимвольных hex-строк, как и раньше.
print(utils.hexlify("A\0\255")) -- "4100ff"
print(utils.hexlify("A\0\255", true)) -- "4100FF"
local bytes = {0xDE, 0xAD, 0xBE, 0xEF}
local hex = utils.hexlify(bytes, true)
-- hex = {"DE", "AD", "BE", "EF"}
print(table.concat(hex)) -- "DEADBEEF"
utils.unhexlify(str [, asString])
Преобразовать hex-строку в таблицу байтов или бинарную строку.
При нечётной длине или символах вне 0–9, a–f, A–F вызывает Lua-ошибку.
Аргументы:
- str: строка; hex-строка (чётное количество символов)
- asString: boolean (опционально); true для возврата строки, а не таблицы
Возвращает: таблица или строка (массив байтов)
local bytes = utils.unhexlify("DEADBEEF")
-- bytes = {222, 173, 190, 239}
local data = utils.unhexlify("4100FF", true)
assert(data == "A\0\255")
assert(utils.unhexlify(utils.hexlify(data), true) == data)
utils.bin(num)
Преобразовать неотрицательное целое число в двоичную строку.
Аргументы:
- num: неотрицательное целое число
Возвращает: строка; двоичное представление
print(utils.bin(255)) -- "11111111"
print(utils.bin(10)) -- "1010"
Работа с таблицами
utils.toBits(num [, bits])
Преобразовать число в таблицу битов (старший бит первый).
Аргументы:
- num: неотрицательное целое число
- bits: число (опционально); количество возвращаемых битов. Если ширина меньше необходимой, старшие биты отбрасываются
Возвращает: таблица; массив битов (0 или 1)
local bits = utils.toBits(5)
-- bits = {1, 0, 1}
bits = utils.toBits(5, 8)
-- bits = {0, 0, 0, 0, 0, 1, 0, 1}
utils.sliceTable(tbl [, first [, last]])
Извлечь срез из таблицы.
Аргументы:
- tbl: таблица; исходная таблица
- first: число (опционально); начальный индекс (по умолчанию 1)
- last: число (опционально); конечный индекс (по умолчанию #tbl)
Возвращает: таблица; новая таблица со срезом
local t = {1, 2, 3, 4, 5}
local slice = utils.sliceTable(t, 2, 4)
-- slice = {2, 3, 4}
utils.compareSlices(slice1, slice2)
Сравнивает длину последовательной части таблиц и соответствующие элементы
оператором Lua ==. Вложенные таблицы рекурсивно не сравниваются.
Аргументы:
- slice1: таблица
- slice2: таблица
Возвращает: boolean; true если таблицы равны
local a = {1, 2, 3}
local b = {1, 2, 3}
local c = {1, 2, 4}
print(utils.compareSlices(a, b)) -- true
print(utils.compareSlices(a, c)) -- false
Работа с путями
utils.splitPath(path)
Разбить путь на компоненты.
Аргументы:
- path: строка; путь к файлу
Возвращает: таблица; массив компонентов пути
local parts = utils.splitPath("/lib/lua/mylib.lua")
-- parts = {"lib", "lua", "mylib.lua"}
utils.dirname(path)
Получить директорию из пути.
Аргументы:
- path: строка; путь к файлу
Возвращает: строка; путь к директории
print(utils.dirname("/lib/lua/mylib.lua")) -- "/lib/lua"
print(utils.dirname("file.txt")) -- "."
print(utils.dirname("/file.txt")) -- "/"
utils.basename(path)
Получить имя файла из пути.
Аргументы:
- path: строка; путь к файлу
Возвращает: строка; имя файла
print(utils.basename("/lib/lua/mylib.lua")) -- "mylib.lua"
print(utils.basename("file.txt")) -- "file.txt"
utils.mkdirp(path)
Создать все директории в пути (аналог mkdir -p). Последний компонент пути считается именем файла и не создаётся как директория.
Аргументы:
- path: строка; путь к файлу
Возвращает true при успехе или false, err при системной ошибке. Путь длиной
256 байт и больше вызывает исключение.
-- Создаст директории /lib/lua/mylib/
local ok, err = utils.mkdirp("/lib/lua/mylib/test.lua")
if not ok then
print("Error: " .. err)
end
Base64
utils.b64enc(data)
Закодировать данные в Base64.
Аргументы:
- data: строка; бинарные данные для кодирования
Возвращает: строка; Base64 представление
print(utils.b64enc("Hello World"))
-- "SGVsbG8gV29ybGQ="
-- Бинарные данные
local bin = string.char(0x00, 0x01, 0xFF)
print(utils.b64enc(bin))
-- "AAH/"
utils.b64dec(base64)
Декодировать данные из Base64. Декодер принимает строки как с завершающими
знаками =, так и без них, а пробельные и другие посторонние символы
пропускает.
Аргументы:
- base64: строка; Base64 строка
Возвращает: строка; декодированные бинарные данные
print(utils.b64dec("SGVsbG8gV29ybGQ="))
-- "Hello World"
-- Работает с заполнением и без него
print(utils.b64dec("SGVsbG8")) -- без =
print(utils.b64dec("SGVsbG8=")) -- с =
Пример: передача файлов
-- Отправка файла (кодирование)
local function file_to_b64(path)
local f = io.open(path, "rb")
if not f then return nil end
local data = f:read("*a")
f:close()
return utils.b64enc(data)
end
-- Приём файла (декодирование)
local function b64_to_file(path, b64)
local f = io.open(path, "wb")
if not f then return false end
f:write(utils.b64dec(b64))
f:close()
return true
end
-- Использование
local encoded = file_to_b64("/test.lua")
b64_to_file("/backup.lua", encoded)
Время
utils.adjustTz(timeFormat, timeOffset)
Добавляет к текущему системному времени заданное смещение и форматирует
результат через strftime. Это числовое смещение, а не база часовых поясов:
функция не применяет правила перехода на летнее время.
Аргументы:
- timeFormat: строка; формат времени (например, “%Y-%m-%d %H:%M:%S”)
- timeOffset: число; смещение в часах, а не в секундах, относительно системного времени
Возвращает: строка; отформатированное время с учётом смещения
-- Москва (UTC+3), если системное время настроено на UTC
local moscowTime = utils.adjustTz("%Y-%m-%d %H:%M:%S", 3)
print(moscowTime)
-- Нью-Йорк (UTC-5)
local nyTime = utils.adjustTz("%Y-%m-%d %H:%M:%S", -5)
print(nyTime)
Антидребезг (debounce)
Бит-параллельный антидребезг входного слова. Каждый бит входа фильтруется
независимо «вертикальным» (bit-sliced) счётчиком-интегратором: чтобы смена была
принята, значение бита должно отличаться от текущего стабильного состояния в
течение 2^depth выборок подряд; любая выборка, совпавшая со стабильным
состоянием, сбрасывает счётчик этого бита, поэтому дребезг и одиночные импульсные помехи
отсекаются.
utils.newDebounce([depth [, initial]])
Создать дебаунсер.
Аргументы:
- depth: число (опционально); глубина счётчика 1..16, смена принимается после
2^depthподряд отличающихся выборок (по умолчанию 5, то есть 32 выборки) - initial: число (опционально); начальное стабильное состояние, маска битов (по умолчанию 0)
Возвращает: userdata
-- при цикле опроса 2 мс: 2^5 = 32 выборки ≈ 64 мс дебаунса
local db = utils.newDebounce(5)
utils.debounce(db, sample)
Подать одну «сырую» выборку входного слова и получить отфильтрованное состояние.
Аргументы:
- db: userdata; дебаунсер, созданный через newDebounce()
- sample: число; текущее сырое состояние входов (маска битов)
Возвращает три значения:
- stable: число; стабильное (отфильтрованное) состояние всех битов
- rising: число; маска битов, которые на этом вызове перешли 0 → 1 (фронт)
- falling: число; маска битов, которые на этом вызове перешли 1 → 0 (спад)
local db = utils.newDebounce(5)
while true do
local stable, rising, falling = utils.debounce(db, pca:read() & 0x3ff)
if (rising >> 0) & 1 == 1 then
print("кнопка 1 нажата")
end
thread.sleepms(2)
end
utils.debounceReset(db [, state])
Сбросить счётчики дебаунсера и задать стабильное состояние.
Аргументы:
- db: userdata; дебаунсер
- state: число (опционально); новое стабильное состояние (по умолчанию 0)
-- считать дебаунсер «прогретым»: все входы стабильно высокие
utils.debounceReset(db, 0x3ff)
Замечания:
- поддерживается до 32 входных бит; выборки и возвращаемые маски — обычные целые Lua;
rising/fallingуже содержат только изменившиеся биты — не нужно хранить предыдущее состояние вручную;- глубина задаёт время фильтрации: смена в любую сторону требует
2^depthстабильных выборок подряд.
Интегратор входов с баллами
Интегратор предназначен для более шумных дискретных сигналов, где одиночная
противоположная выборка не должна полностью сбрасывать накопленную историю.
Каждый выбранный бит имеет независимый балл: при значении 1 балл увеличивается
до max_score, при значении 0 уменьшается до нуля.
Для изменения стабильного состояния используются два разных порога:
- переход 0 → 1 выполняется при
score >= rising_threshold; - переход 1 → 0 выполняется при
score <= falling_threshold.
Условие falling_threshold < rising_threshold создаёт гистерезис и не даёт
состоянию переключаться туда-обратно, когда шум удерживает балл около одного
порога.
utils.newIntegrator(max_score, rising_threshold, falling_threshold [, initial [, mask]])
Создать интегратор для независимой фильтрации до 32 бит.
Аргументы:
max_score: число 1..65535; максимальный балл каждого бита;rising_threshold: число 1..max_score; порог перехода 0 → 1;falling_threshold: число 0..rising_threshold - 1; порог перехода 1 → 0;initial: число (опционально); начальное стабильное состояние, по умолчанию 0;mask: число (опционально); маска обрабатываемых битов, по умолчанию все 32 бита.
Возвращает: userdata интегратора.
При создании баллы установленных битов из initial получают значение
max_score, баллы сброшенных битов — 0. Биты вне mask всегда остаются
сброшенными.
-- десять входов, баллы 0..30, пороги включения/выключения 20 и 10
local filter = utils.newIntegrator(30, 20, 10, 0, 0x3ff)
utils.integrate(filter, sample)
Подать одну сырую выборку и обновить баллы всех выбранных битов.
Аргументы:
filter: userdata; интегратор, созданный черезnewIntegrator();sample: число; текущее сырое состояние входов.
Возвращает три значения:
stable: стабильное состояние;rising: маска принятых на этом вызове переходов 0 → 1;falling: маска принятых на этом вызове переходов 1 → 0.
local input_mask = 0x3ff
-- PCA9555 уже инвертирует активные низкие входы через polarity(), поэтому
-- нажатой кнопке соответствует 1.
local initial = pca:read() & input_mask
local filter = utils.newIntegrator(30, 20, 10, initial, input_mask)
while true do
local stable, rising, falling = utils.integrate(filter, pca:read())
if rising ~= 0 then
print("нажаты входы", rising)
end
if falling ~= 0 then
print("отпущены входы", falling)
end
thread.sleepms(2)
end
Частота опроса определяет скорость изменения баллов. Например, при периоде
2 мс и rising_threshold = 20 переход из начального нулевого состояния требует
не менее 40 мс непрерывных единичных выборок. При наличии накопленного балла
время может быть меньше.
utils.integratorReset(filter [, state])
Сбросить накопленные баллы и задать новое стабильное состояние. Баллы битов,
установленных в state, становятся равны max_score; остальные становятся
равны нулю.
utils.integratorReset(filter, pca:read() & 0x3ff)