Перейти к содержанию
Материалы раздела

Cutils

Модуль cutils предоставляет набор утилит для работы с кэшами, логами, бинарными данными, таблицами и путями файловой системы.

Модуль также доступен под именем cacheutils для обратной совместимости.

Кэш целых чисел

cutils.newi(size)

Создать кэш для хранения целых чисел.

Аргументы:

  • size: число; размер кэша (1-5000)

Возвращает: userdata кэша

local cache = cutils.newi(100)

cutils.addi(cache, value)

Добавить целое число в кэш.

Аргументы:

  • cache: userdata; кэш, созданный через newi()
  • value: число; значение для добавления

Возвращает true, если значение добавлено или уже было в кэше. Возвращает false, если заполненный кэш не может принять новое значение.

local cache = cutils.newi(100)
cutils.addi(cache, 12345)
cutils.addi(cache, 67890)

cutils.findi(cache, value)

Найти целое число в кэше.

Аргументы:

  • cache: userdata; кэш
  • value: число; искомое значение

Возвращает: boolean; true если найдено, false если нет

local cache = cutils.newi(100)
cutils.addi(cache, 12345)
print(cutils.findi(cache, 12345))  -- true
print(cutils.findi(cache, 99999))  -- false

Кэш строк

cutils.news(size, maxStrLen)

Создать кэш для хранения строк.

Аргументы:

  • size: число; количество строк (1-5000)
  • maxStrLen: число; максимальная длина строки (1-20)

Возвращает: userdata кэша

local cache = cutils.news(100, 10)

cutils.adds(cache, str)

Добавить строку в кэш.

Аргументы:

  • cache: userdata; кэш, созданный через news()
  • str: строка; значение для добавления

Возвращает true, если строка добавлена или уже была в кэше. Возвращает false, если кэш заполнен, строка длиннее maxStrLen или содержит нулевой байт.

local cache = cutils.news(100, 10)
cutils.adds(cache, "ABC123")

cutils.finds(cache, str)

Найти строку в кэше.

Аргументы:

  • cache: userdata; кэш
  • str: строка; искомое значение

Возвращает: boolean; true если найдено

local cache = cutils.news(100, 10)
cutils.adds(cache, "ABC123")
print(cutils.finds(cache, "ABC123"))  -- true

Логирование

cutils.newLog(size)

Создать буфер для логов.

Аргументы:

  • size: число; размер буфера в байтах (1–65536)

Возвращает: userdata лога

local log = cutils.newLog(1024)

cutils.addLog(log, message, level)

Добавить сообщение в лог.

Аргументы:

  • log: userdata; лог, созданный через newLog()
  • message: строка; текст сообщения
  • level: число; уровень лога (cutils.Info, cutils.Warn, cutils.Err)

Функция ничего не возвращает. Когда буфер заполнен, она удаляет самые старые записи. Сообщение, которое вместе со служебными данными не помещается в буфер, не добавляется. Неизвестный уровень логирования также игнорируется.

local log = cutils.newLog(1024)
cutils.addLog(log, "System started", cutils.Info)
cutils.addLog(log, "Low memory", cutils.Warn)
cutils.addLog(log, "Connection failed", cutils.Err)

cutils.printLog(log)

Получить содержимое лога.

Аргументы:

  • log: userdata; лог

Возвращает: строка; содержимое лога

local log = cutils.newLog(1024)
cutils.addLog(log, "Hello", cutils.Info)
print(cutils.printLog(log))

Константы уровней логирования

  • cutils.Info — информационное сообщение (@)
  • cutils.Warn — предупреждение ($)
  • cutils.Err — ошибка (^)

Бинарные функции

cutils.hex(num [, big])

Преобразовать младший байт числа в двухсимвольную шестнадцатеричную строку.

Аргументы:

  • num: целое число; используется младший байт
  • big: boolean (опционально); true для верхнего регистра

Возвращает: строка; hex-представление

print(cutils.hex(255))        -- "ff"
print(cutils.hex(255, true))  -- "FF"

cutils.hexlify(table [, big])

Преобразовать таблицу байтов в таблицу hex-строк.

Аргументы:

  • table: таблица; массив целых чисел от 0 до 255
  • big: boolean (опционально); true для верхнего регистра

Возвращает: таблица; массив hex-строк

local bytes = {0xDE, 0xAD, 0xBE, 0xEF}
local hex = cutils.hexlify(bytes, true)
-- hex = {"DE", "AD", "BE", "EF"}
print(table.concat(hex))  -- "DEADBEEF"

cutils.unhexlify(str [, asString])

Преобразовать hex-строку в таблицу байтов.

Аргументы:

  • str: строка; hex-строка (чётное количество символов)
  • asString: boolean (опционально); true для возврата строки, а не таблицы

Возвращает: таблица или строка (массив байтов)

local bytes = cutils.unhexlify("DEADBEEF")
-- bytes = {222, 173, 190, 239}

cutils.bin(num)

Преобразовать неотрицательное целое число в двоичную строку.

Аргументы:

  • num: неотрицательное целое число

Возвращает: строка; двоичное представление

print(cutils.bin(255))  -- "11111111"
print(cutils.bin(10))   -- "1010"

Работа с таблицами

cutils.toBits(num [, bits])

Преобразовать число в таблицу битов (старший бит первый).

Аргументы:

  • num: неотрицательное целое число
  • bits: число (опционально); количество возвращаемых битов. Если ширина меньше необходимой, старшие биты отбрасываются

Возвращает: таблица; массив битов (0 или 1)

local bits = cutils.toBits(5)
-- bits = {1, 0, 1}

bits = cutils.toBits(5, 8)
-- bits = {0, 0, 0, 0, 0, 1, 0, 1}

cutils.sliceTable(tbl [, first [, last]])

Извлечь срез из таблицы.

Аргументы:

  • tbl: таблица; исходная таблица
  • first: число (опционально); начальный индекс (по умолчанию 1)
  • last: число (опционально); конечный индекс (по умолчанию #tbl)

Возвращает: таблица; новая таблица со срезом

local t = {1, 2, 3, 4, 5}
local slice = cutils.sliceTable(t, 2, 4)
-- slice = {2, 3, 4}

cutils.compareSlices(slice1, slice2)

Сравнивает длину последовательной части таблиц и соответствующие элементы оператором Lua ==. Вложенные таблицы рекурсивно не сравниваются.

Аргументы:

  • slice1: таблица
  • slice2: таблица

Возвращает: boolean; true если таблицы равны

local a = {1, 2, 3}
local b = {1, 2, 3}
local c = {1, 2, 4}
print(cutils.compareSlices(a, b))  -- true
print(cutils.compareSlices(a, c))  -- false

Работа с путями

cutils.splitPath(path)

Разбить путь на компоненты.

Аргументы:

  • path: строка; путь к файлу

Возвращает: таблица; массив компонентов пути

local parts = cutils.splitPath("/lib/lua/mylib.lua")
-- parts = {"lib", "lua", "mylib.lua"}

cutils.dirname(path)

Получить директорию из пути.

Аргументы:

  • path: строка; путь к файлу

Возвращает: строка; путь к директории

print(cutils.dirname("/lib/lua/mylib.lua"))  -- "/lib/lua"
print(cutils.dirname("file.txt"))            -- "."
print(cutils.dirname("/file.txt"))           -- "/"

cutils.basename(path)

Получить имя файла из пути.

Аргументы:

  • path: строка; путь к файлу

Возвращает: строка; имя файла

print(cutils.basename("/lib/lua/mylib.lua"))  -- "mylib.lua"
print(cutils.basename("file.txt"))            -- "file.txt"

cutils.mkdirp(path)

Создать все директории в пути (аналог mkdir -p). Последний компонент пути считается именем файла и не создаётся как директория.

Аргументы:

  • path: строка; путь к файлу

Возвращает true при успехе или false, err при системной ошибке. Путь длиной 256 байт и больше вызывает исключение.

-- Создаст директории /lib/lua/mylib/
local ok, err = cutils.mkdirp("/lib/lua/mylib/test.lua")
if not ok then
    print("Error: " .. err)
end

Base64

cutils.b64enc(data)

Закодировать данные в Base64.

Аргументы:

  • data: строка; бинарные данные для кодирования

Возвращает: строка; Base64 представление

print(cutils.b64enc("Hello World"))
-- "SGVsbG8gV29ybGQ="

-- Бинарные данные
local bin = string.char(0x00, 0x01, 0xFF)
print(cutils.b64enc(bin))
-- "AAH/"

cutils.b64dec(base64)

Декодировать данные из Base64. Декодер принимает строки как с завершающими знаками =, так и без них, а пробельные и другие посторонние символы пропускает.

Аргументы:

  • base64: строка; Base64 строка

Возвращает: строка; декодированные бинарные данные

print(cutils.b64dec("SGVsbG8gV29ybGQ="))
-- "Hello World"

-- Работает с заполнением и без него
print(cutils.b64dec("SGVsbG8"))  -- без =
print(cutils.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 cutils.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(cutils.b64dec(b64))
    f:close()
    return true
end

-- Использование
local encoded = file_to_b64("/test.lua")
b64_to_file("/backup.lua", encoded)

Время

cutils.adjustTz(timeFormat, timeOffset)

Добавляет к текущему системному времени заданное смещение и форматирует результат через strftime. Это числовое смещение, а не база часовых поясов: функция не применяет правила перехода на летнее время.

Аргументы:

  • timeFormat: строка; формат времени (например, “%Y-%m-%d %H:%M:%S”)
  • timeOffset: число; смещение в часах, а не в секундах, относительно системного времени

Возвращает: строка; отформатированное время с учётом смещения

-- Москва (UTC+3), если системное время настроено на UTC
local moscowTime = cutils.adjustTz("%Y-%m-%d %H:%M:%S", 3)
print(moscowTime)

-- Нью-Йорк (UTC-5)
local nyTime = cutils.adjustTz("%Y-%m-%d %H:%M:%S", -5)
print(nyTime)

Антидребезг (debounce)

Бит-параллельный антидребезг входного слова. Каждый бит входа фильтруется независимо «вертикальным» (bit-sliced) счётчиком-интегратором: чтобы смена была принята, бит должен продержаться отличным от текущего стабильного состояния в течение 2^depth выборок подряд; любая выборка, совпавшая со стабильным состоянием, сбрасывает счётчик этого бита, поэтому дребезг и одиночные глитчи отсекаются.

cutils.newDebounce([depth [, initial]])

Создать дебаунсер.

Аргументы:

  • depth: число (опционально); глубина счётчика 1..16, смена принимается после 2^depth подряд отличающихся выборок (по умолчанию 5, то есть 32 выборки)
  • initial: число (опционально); начальное стабильное состояние, маска битов (по умолчанию 0)

Возвращает: userdata

-- при цикле опроса 2 мс: 2^5 = 32 выборки ≈ 64 мс дебаунса
local db = cutils.newDebounce(5)

cutils.debounce(db, sample)

Подать одну «сырую» выборку входного слова и получить отфильтрованное состояние.

Аргументы:

  • db: userdata; дебаунсер, созданный через newDebounce()
  • sample: число; текущее сырое состояние входов (маска битов)

Возвращает три значения:

  • stable: число; стабильное (отфильтрованное) состояние всех битов
  • rising: число; маска битов, которые на этом вызове перешли 0 → 1 (фронт)
  • falling: число; маска битов, которые на этом вызове перешли 1 → 0 (спад)
local db = cutils.newDebounce(5)

while true do
    local stable, rising, falling = cutils.debounce(db, pca:read() & 0x3ff)

    if (rising >> 0) & 1 == 1 then
        print("кнопка 1 нажата")
    end

    thread.sleepms(2)
end

cutils.debounceReset(db [, state])

Сбросить счётчики дебаунсера и задать стабильное состояние.

Аргументы:

  • db: userdata; дебаунсер
  • state: число (опционально); новое стабильное состояние (по умолчанию 0)
-- считать дебаунсер «прогретым»: все входы стабильно высокие
cutils.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 создаёт гистерезис и не даёт состоянию переключаться туда-обратно, когда шум удерживает балл около одного порога.

cutils.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 = cutils.newIntegrator(30, 20, 10, 0, 0x3ff)

cutils.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 = cutils.newIntegrator(30, 20, 10, initial, input_mask)

while true do
    local stable, rising, falling = cutils.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 мс непрерывных единичных выборок. При наличии накопленного балла время может быть меньше.

cutils.integratorReset(filter [, state])

Сбросить накопленные баллы и задать новое стабильное состояние. Баллы битов, установленных в state, становятся равны max_score; остальные становятся равны нулю.

cutils.integratorReset(filter, pca:read() & 0x3ff)