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

Модуль FlashDB

Модуль flashdb предоставляет FlashDB 2.2.0 — компактную встраиваемую базу ключ-значение (KVDB) и временных рядов (TSDB). FlashDB распространяется по лицензии Apache-2.0.

Модуль включён в профиль ODNFC-LAN-PRO и выключен по умолчанию в остальных профилях. Он работает в файловом режиме: отдельный raw-раздел flash не создаётся, а каталог базы можно разместить в произвольном месте любой доступной для записи файловой системы.

local flashdb = require("flashdb")
-- В прошивке тот же модуль доступен как ROM-глобал flashdb.

local db, message, code = flashdb.open_kv(
    "config",            -- имя базы, до 8 символов
    "/public/config",    -- произвольный каталог
    64 * 1024             -- размер, необязательно
)
assert(db, string.format("FlashDB %s: %s", code, message))

assert(db:set("device-name", "controller-01"))
print(db:get("device-name"))
assert(db:close())

Как хранятся базы

FlashDB создаёт не один монолитный файл, а обычные секторные файлы внутри указанного каталога:

/public/config/config.fdb.0
/public/config/config.fdb.1
/public/config/config.fdb.2
...

Имя базы входит в имя каждого файла. Оно должно содержать от 1 до 8 латинских букв, цифр, _ или -. Абсолютные и относительные пути разрешены; относительный путь вычисляется от текущего рабочего каталога. Если каталога ещё нет, модуль создаёт его вместе с отсутствующими родительскими каталогами.

Размер сектора фиксирован и равен flashdb.SECTOR_SIZE (4096 байт). Размер базы должен быть кратен 4096 и содержать не менее двух секторов. По умолчанию создаётся 64 КиБ, а профиль ODNFC-LAN-PRO ограничивает одну базу 1 МиБ. Размер можно изменить через Kconfig:

  • CONFIG_LUA_RTOS_FLASHDB_DEFAULT_SIZE — размер по умолчанию;
  • CONFIG_LUA_RTOS_FLASHDB_MAX_SIZE — максимальный размер одной базы;
  • CONFIG_LUA_RTOS_FLASHDB_MAX_OPEN — максимальное число одновременно открытых баз.

При повторном открытии передавайте тот же размер. Перед копированием, переименованием, перезаписью или удалением файлов вызовите close() и работайте со всем набором <name>.fdb.N. Изменение одного секторного файла открытой базы может повредить данные. Модуль не позволяет одновременно открыть одну пару каталог + имя дважды через Lua, но прямой доступ через io, os, веб-интерфейс или другой нативный код может обойти эту защиту.

Для постоянных данных рекомендуется LittleFS: её fsync доводит синхронные записи FlashDB до носителя, а сама файловая система рассчитана на внезапное отключение питания. На RAMFS база исчезнет после перезагрузки; свойства FAT/SD и других точек монтирования зависят от их драйвера и носителя.

Возвращаемые ошибки

Открытие и изменяющие операции при успехе возвращают объект или true. При ожидаемой ошибке они возвращают:

nil, message, code

code — код FlashDB для операций базы либо положительный errno для ошибок пути, лимита открытых баз и блокировки. Неверные аргументы и обращение к закрытому объекту создают Lua-исключение.

Полезные коды FlashDB:

КодЗначение
1ошибка стирания сектора
2ошибка чтения
3ошибка записи или fsync
5неверное имя ключа
7база заполнена
8ошибка инициализации

KVDB

flashdb.open_kv(name, path [, size])

Открывает или создаёт KVDB. Значения передаются как Lua-строки и поэтому могут содержать UTF-8, нулевые байты и произвольные бинарные данные. Ключ должен содержать от 1 до 63 байт без NUL; значение — не более 3968 байт.

local kv = assert(flashdb.open_kv("state", "/data/device-state", 128 * 1024))

assert(kv:set("counter", "42"))
assert(kv:set("packet", "\x01\x00\xff"))

print(kv:get("counter"))                 -- "42"
print(kv:get("missing"))                 -- nil
print(kv:get("missing", "default"))      -- "default"
print(kv:exists("packet"))               -- true

assert(kv:delete("counter"))
assert(kv:check())
assert(kv:close())

KVDB methods

МетодРезультат
db:set(key, value)записывает бинарную строку
db:get(key [, default])возвращает строку, default или nil
db:exists(key)проверяет наличие ключа
db:delete(key)помечает ключ удалённым
db:clear()форматирует все секторы этой KVDB
db:check()проверяет заголовки и CRC записей
db:each(callback)перебирает пары callback(key, value)
db:name(), db:path(), db:size()возвращают параметры открытой базы
db:close()синхронно закрывает файловые дескрипторы

each() возвращает число вызовов callback. Если callback вернёт строго false, обход остановится. Ошибка callback безопасно освобождает внутреннюю рекурсивную блокировку и повторно выбрасывается в Lua. Менять или закрывать эту же базу из callback нельзя; такая попытка вернёт nil, message, EBUSY.

local count = kv:each(function(key, value)
    print(key, #value)
    if key == "stop-here" then return false end
end)

Обновление создаёт новую запись и только затем удаляет старую. Удалённые записи занимают место до автоматической сборки мусора. clear() уничтожает все значения без подтверждения.

TSDB

flashdb.open_ts(name, path, max_record_size [, size])

Открывает или создаёт временной ряд. max_record_size задаётся при создании и должен быть от 1 до 3968 байт. Каждая запись содержит 32-битную временную метку со знаком (timestamp), бинарную строку и состояние.

local log = assert(flashdb.open_ts(
    "sensor",
    "/logs/temperature",
    64,
    256 * 1024
))

assert(log:append("23.75", os.time()))
assert(log:append("24.10", os.time() + 1))

log:each(function(timestamp, value, status)
    print(timestamp, value, status)
end)

assert(log:close())

TSDB methods

МетодРезультат
db:append(value [, timestamp])добавляет запись; без timestamp используется time()
db:each(callback)обход от старых записей к новым
db:each(callback, from, to)обход включительного диапазона времени; from > to задаёт обратное направление
db:each_reverse(callback)полный обход от новых записей к старым
db:count(from, to [, status])число записей указанного состояния
db:last_time()timestamp последней принятой записи
db:rollover()возвращает режим циклической перезаписи
db:rollover(enabled)включает или выключает циклическую перезапись
db:clean()форматирует все секторы TSDB
db:name(), db:path(), db:size()параметры базы
db:close()закрывает базу

Timestamp не должен уменьшаться относительно последней сохранённой записи и должен находиться в диапазоне от -2147483648 до 2147483647. Для обычных Unix timestamp это означает ограничение 2038 года. Если нужно хранить время за пределами этого диапазона, используйте собственное относительное время или счётчик.

Callback получает timestamp, value, status. Для фильтрации доступны константы:

  • flashdb.TSL_UNUSED;
  • flashdb.TSL_PRE_WRITE;
  • flashdb.TSL_WRITE;
  • flashdb.TSL_USER_STATUS1;
  • flashdb.TSL_DELETED;
  • flashdb.TSL_USER_STATUS2.

По умолчанию rollover включён: заполненная TSDB начинает заменять самые старые секторы. Если его выключить, append() после заполнения вернёт ошибку database is full.

Надёжность и ресурсные ограничения

Все обращения к FlashDB и файловому backend защищены одним глобальным рекурсивным mutex. Это позволяет использовать разные базы из разных Lua-потоков без отдельной задачи базы данных. Одновременное открытие одного набора файлов запрещено.

FlashDB синхронизирует завершающую запись состояния через fsync. После сбоя питания отдельная успешно завершённая операция должна читаться целиком, а незавершённая — либо отсутствовать, либо восстановиться как предыдущее значение. Операции с несколькими ключами не образуют общую транзакцию: атомарность действует на отдельную операцию set, delete или append.

Файловый режим заранее создаёт по одному файлу на каждый сектор. Оставляйте свободное место как минимум под полный размер базы, метаданные файловой системы и её служебный запас. Частые обновления KV создают устаревшие записи и запускают сборку мусора; для часто меняющихся данных выбирайте размер с несколькими свободными секторами, а не минимальные 8 КиБ.

Всегда закрывайте базу явно. __gc закрывает забытый объект как страховка, но момент запуска сборщика мусора не определён. Диагностика:

print(flashdb.version())
print(db:name(), db:path(), db:size())
assert(db:check())                 -- только KVDB
print(cpu.memfree(), cpu.memlargest())

Перед выпуском прошивки обязательны аппаратные проверки на целевой плате: повторное заполнение/сборка мусора, сброс питания во время set и append, повторное открытие и проверка данных, а также контроль свободной heap и largest free block после прогрева и отложенной очистки FreeRTOS.