Skip to content

Простая база данных для Arduino

License

Notifications You must be signed in to change notification settings

GyverLibs/GyverDB

Repository files navigation

latest PIO Foo Foo Foo

Foo

GyverDB

Простая база данных для Arduino:

  • Хранение данных в парах ключ-значение
  • Поддерживает все целочисленные типы, float, строки и бинарные данные
  • Быстрая автоматическая конвертация данных между разными типами
  • Быстрый доступ благодаря хэш ключам и бинарному поиску - в 10 раз быстрее библиотеки Pairs и в 11 раз быстрее Preferences (ESP32)
  • Компактная реализация - 8 байт на одну ячейку
  • Встроенный механизм автоматической записи на флешку ESP8266/ESP32

Совместимость

Совместима со всеми Arduino платформами (используются Arduino-функции)

Зависимости

Содержание

Документация

Настройки компиляции перед подключением библиотеки

#define DB_NO_UPDATES  // убрать стек обновлений
#define DB_NO_FLOAT    // убрать поддержку float
#define DB_NO_INT64    // убрать поддержку int64
#define DB_NO_CONVERT  // не конвертировать данные (принудительно менять тип ячейки, keepTypes не работает)

GyverDB

// конструктор
// можно зарезервировать ячейки
GyverDB(uint16_t reserve = 0);

// не изменять тип ячейки (конвертировать данные если тип отличается) (умолч. true)
void keepTypes(bool keep);

// было изменение бд
bool changed();

// сбросить флаг изменения бд
void clearChanged();

// вывести всё содержимое БД
void dump(Print& p);

// полный вес БД
size_t size();

// экспортный размер БД (для writeTo)
size_t writeSize();

// экспортировать БД в Stream (напр. файл)
bool writeTo(Stream& stream);

// экспортировать БД в буфер размера writeSize()
bool writeTo(uint8_t* buffer);

// импортировать БД из Stream (напр. файл)
bool readFrom(Stream& stream, size_t len);

// импортировать БД из буфера
bool readFrom(const uint8_t* buffer, size_t len);

// создать ячейку. Если существует - перезаписать пустой с новым типом
bool create(size_t hash, gdb::Type type, uint16_t reserve = 0);

// полностью освободить память
void reset();

// стереть все ячейки (не освобождает зарезервированное место)
void clear();

// удалить из БД ячейки, ключей которых нет в переданном списке
void cleanup(size_t* hashes, size_t len);

// вывести все ключи в массив длиной length()
void getKeys(size_t* hashes);

// получить ячейку
gdb::Entry get(size_t hash);
gdb::Entry get(const Text& key);

// получить ячейку по порядку
gdb::Entry getN(int idx);

// удалить ячейку
void remove(size_t hash);
void remove(const Text& key);

// БД содержит ячейку с именем
bool has(size_t hash);
bool has(const Text& key);

// записать данные (создать ячейку, если не существует). DATA - любой тип данных
bool set(size_t hash, DATA data);
bool set(const Text& key, DATA data);

// инициализировать данные (создать ячейку и записать, если ячейка не существует). DATA - любой тип данных
bool init(size_t hash, DATA data);
bool init(const Text& key, DATA data);

// обновить данные (если ячейка существует). DATA - любой тип данных
bool update(size_t hash, DATA data);
bool update(const Text& key, DATA data);

// использовать стек обновлений (умолч. false)
void useUpdates(bool use);

// есть непрочитанные изменения
bool updatesAvailable();

// пропустить необработанные обновления
void skipUpdates();

// получить хеш обновления из стека
size_t updateNext();

GyverDBFile

Данный класс наследует GyverDB, но умеет самостоятельно записываться в файл на флешку ESP при любом изменении и по истечении таймаута.

GyverDBFile(fs::FS* nfs = nullptr, const char* path = nullptr, uint32_t tout = 10000);

// установить файловую систему и имя файла
void setFS(fs::FS* nfs, const char* path);

// установить таймаут записи, мс (умолч. 10000)
void setTimeout(uint32_t tout = 10000);

// прочитать данные
bool begin();

// обновить данные в файле, если было изменение БД. Вернёт true при успешной записи
bool update();

// тикер, вызывать в loop. Сам обновит данные при изменении и выходе таймаута, вернёт true
bool tick();

Для использования нужно запустить FS и вызывать тикер в loop:

#include <LittleFS.h>
#include <GyverDBFile.h>
GyverDBFile db(&LittleFS, "data.db");

void setup() {
    LittleFS.begin();
    db.begin(); // прочитать данные из файла

    // для работы в таком режиме пригодится метод init():
    // создаёт ячейку соответствующего типа и записывает "начальные" данные,
    // если такой ячейки ещё нет в БД
    db.init("key", 123);    // int
    db.init("fl", 3.14);    // float
    db.init("str", "init"); // строка
}
void loop() {
    db.tick();
}
  • При любом изменении в БД она сама запишется в файл после выхода таймаута
  • БД находится в оперативной памяти для быстрого доступа, она читается из файла только при вызове begin
  • Расширение файла не важно - это больше подсказка для пользователя, что данный файл хранит БД. Файл содержит БД в бинарном виде - её нельзя редактировать через блокнот!

Типы ячеек gdb::Type

None
Int
Uint
Int64
Uint64
Float
String
Bin

Entry

Наследует класс Text для более удобного чтения строк

// тип ячейки
gdb::Type type();

// вывести данные в буфер размера size(). Не добавляет 0-терминатор, если это строка
void writeBytes(void* buf);

// вывести в переменную
bool writeTo(T& dest);

Value toText();
String toString();
bool toBool();
int32_t toInt();
int64_t toInt64();
double toFloat();

Использование

GyverDB - динамическая база данных (БД), которая хранит данные в формате ключ-значение. По ключу можно записать данные в ячейку и прочитать их из неё:

  • Ключ - 29 бит число, по сути БД это массив на 2^29 ячеек
  • Значение - данные любого типа: числа, строки, любые бинарные данные
db[0] = 123;
db[2] = 3.14;
db[100] = "hello";

Пока в ячейку ничего не записано - она не существует и не занимает память. К ключу следует относиться как к уникальному идентификатору ячейки, а не как к порядковому номеру в массиве - индексу.

Ключи

Для повышения читаемости кода вместо номеров ячеек удобнее использовать константы, например enum. Это очень удобно, потому что IDE подскажет список имеющихся ключей при вводе keys::, а значения подставит компилятор:

enum keys : size_t {
    key1,
    key2,
    mykey,
    lolkek,
};

db[keys::key1] = 123;
db[keys::key2] = 3.14;
db[keys::mykey] = "hello";

При активной разработке и хранении БД в энергонезависимой памяти (в файле, чтение при загрузке МК) данный подход неудобен, т.к. удаление или добавление ключа в середине списка приведёт к смещению нумерации и под старыми ключами окажутся новые данные. Для сохранения читаемости и уникальности каждого ключа можно использовать хэш-строки - строка при помощи специальной функции преобразуется в число, которое соответствует только этой строке. Данная возможность встроена в GyverDB - можно обращаться к ячейкам по строковому ключу:

db["key1"] = 123;

Для ускорения и облегчения кода можно использовать внешнюю хэш-функцию, которая выполняется на этапе компиляции и сразу превращается в число. Вместе с GyverDB идёт несколько вариантов, они равноценны:

db[SH("key1")] = 123;
db["key2"_h] = 3.14;
db[H(mykey)] = "hello";

В этом случае enum тоже можно использовать для подсказок IDE, но чуть в другом виде:

// enum с хэшами
enum keys : size_t {
    key1 = "key1"_h,
    key2 = SH("key2"),
    mykey = H(mykey),
};

db[keys::key1] = 123;
db[keys::key2] = 3.14;
db[keys::mykey] = "hello";

Теперь enum хранит хэши и не боится удаления или добавления ключей - они уникальны. Для более короткой записи в библиотеке есть удобный макрос:

DB_KEYS(keys,
    key1,
    key2,
    mykey   // последняя запятая не ставится
);

Он развернётся в такой же хэш-enum как в примере выше. Рекомендуется использовать этот вариант как самый удобный и оптимальный.

Запись и чтение

GyverDB db;

// ЗАПИСЬ
// напрямую. При создании ячейка получит тип Int
db["key1"] = 123;

// эта ячейка у нас int, текст сконвертируется в число
db["key1"] = "123456";

// чуть эффективнее записывать через set
db.set("key1", 123321);
db.set("key2", "3.14");
// ЧТЕНИЕ
// ячейка сама конвертируется в тип, который стоит слева от знака =
int i = db["key1"];
float f = db.get("key2");   // чуть эффективнее читать через get

// любые данные "печатаются" в Print, даже бинарные
Serial.println(db["key3"]);

// можно сконвертировать в конкретный тип
i = db["key1"].toInt();
i = db["key2"].toBool();
f = db["key3"].toFloat();

// можно сравнивать с числами
db["key1"] == 123;
db["key1"] >= 123;

// для чисел работают составные операторы и инкремент/декремент
db["key1"]++;
db["key1"] += 10;
db["key1"] &= 0x12;

// записи типа String можно сравнивать со строками
db["key2"] == "str";

// но можно и вот так, для любых типов ячеек
// toText() конвертирует все типы ячеек БД во временную строку
db["key1"].toText() == "12345";
// БИНАРНЫЕ
// GyverDB может записать данные любого типа, даже составные (массивы, структуры)
uint8_t arr[5] = {1, 2, 3, 4, 5};
db["arr"] = arr;

// вывод обратно. Тип должен иметь такой же размер!
uint8_t arr2[5];
db["arr"].writeTo(arr2);

// вывод всей БД в Print
db.dump(Serial);
// СТРУКТУРЫ
struct Foo {
    int a;
    float b;
};

Foo foo{123, 3.14};
db["struct"] = foo;

// чтение в копию
Foo foo2;
db["struct"].writeTo(foo2);
Serial.println(foo2.b);

// чтение напрямую
Serial.println(static_cast<Foo*>(db["struct"].buffer())->a);  // 123

Foo& ref = *static_cast<Foo*>(db["struct"].buffer());  // 3.14
Serial.println(ref.b);

// массив структур
Foo arr[] = {{123, 3.14}, {456, 2.72}};
db["arr"] = arr;

Foo* p = (Foo*)db["arr"].buffer();
Serial.println(p[0].a);  // 123
Serial.println(p[1].b);  // 2.72

При разработке проекта может оказаться так, что некоторые ключи "устарели" или были переименованы в процессе разработки, и ячейки по ним уже не нужны. В библиотеке есть возможность провести очистку БД: удалить все лишние ячейки и оставить только заданный список ключей. Это делается так:

// список ключей, которые надо оставить. В формате size_t в любом виде
size_t hashes[] = {SH("key1"), "key2"_h, kesy::key3};

// очищаем
db.cleanup(hashes, 3);

// в БД останутся только ячейки, соответствующие указанным выше ключам

Есть 4 варианта записи в ячейку:

  • create(ключ, тип) - создать пустую ячейку указанного типа. Если ячейка с таким ключом существует - очистить и сменить тип
  • init(ключ, значение) - создать ячейку с указанным значением, если ячейки с таким ключом нет или она имеет другой тип данных. Удобно для задания начальных значений в GyverDBFile
  • update(ключ, значение) - обновить данные, если ячейка с таким ключом существует
  • set(ключ, значение) - записать данные, создав ячейку если она не существует. Аналог db[ключ] = значение

Для инициализации можно использовать более короткий макрос:

DB_INIT(
    db,
    (keys::key1, 123),
    (keys::key2, 3.14),
    ("key3", 123321ull),
    ("key4", "abc")
);

Примечания

  • GyverDB хранит целые до 32 бит и float числа в памяти самой ячейки. 64-битные числа, строки и бинарные данные выделяются динамически
  • Ради компактности используется 29-битное хэширование. Этого должно хватать более чем, шанс коллизий крайне мал
  • Библиотека автоматически выбирает тип при записи в ячейку. Приводите тип вручную, если это нужно (например db["key"] = 12345ull)
  • По умолчанию включен параметр keepTypes() - сохранять тип ячейки при перезаписи. Это означает, что если ячейка была int, то при записи в неё данных другого типа они будут автоматически конвертироваться в int, даже если это строка. И наоборот
  • При создании пустой ячейки можно указать тип и зарезервировать место (только для строк и бинарных данных) db.create("kek", gdb::Type::String, 100)
  • Entry имеет автоматический доступ к строке как оператор String, это означает что ячейки с текстовым типом (String) можно передавать в функции, которые принимают String, например WiFi.begin(db["wifi_ssid"], db["wifi_pass"]);
  • Если нужно передать ячейку в функцию, принимающую const char* - используйте на ней c_str(). Это не продублирует строку в памяти, а даст к ней прямой доступ. Например foo(db["str"].c_str())

Версии

  • v1.0
  • v1.0.1 упразднены целые типы 8 и 16 бит, увеличено разрешение хэша
  • v1.2.1

Установка

  • Библиотеку можно найти по названию GyverDB и установить через менеджер библиотек в:
    • Arduino IDE
    • Arduino IDE v2
    • PlatformIO
  • Скачать библиотеку .zip архивом для ручной установки:
    • Распаковать и положить в C:\Program Files (x86)\Arduino\libraries (Windows x64)
    • Распаковать и положить в C:\Program Files\Arduino\libraries (Windows x32)
    • Распаковать и положить в Документы/Arduino/libraries/
    • (Arduino IDE) автоматическая установка из .zip: Скетч/Подключить библиотеку/Добавить .ZIP библиотеку… и указать скачанный архив
  • Читай более подробную инструкцию по установке библиотек здесь

Обновление

  • Рекомендую всегда обновлять библиотеку: в новых версиях исправляются ошибки и баги, а также проводится оптимизация и добавляются новые фичи
  • Через менеджер библиотек IDE: найти библиотеку как при установке и нажать "Обновить"
  • Вручную: удалить папку со старой версией, а затем положить на её место новую. "Замену" делать нельзя: иногда в новых версиях удаляются файлы, которые останутся при замене и могут привести к ошибкам!

Баги и обратная связь

При нахождении багов создавайте Issue, а лучше сразу пишите на почту [email protected]
Библиотека открыта для доработки и ваших Pull Request'ов!

При сообщении о багах или некорректной работе библиотеки нужно обязательно указывать:

  • Версия библиотеки
  • Какой используется МК
  • Версия SDK (для ESP)
  • Версия Arduino IDE
  • Корректно ли работают ли встроенные примеры, в которых используются функции и конструкции, приводящие к багу в вашем коде
  • Какой код загружался, какая работа от него ожидалась и как он работает в реальности
  • В идеале приложить минимальный код, в котором наблюдается баг. Не полотно из тысячи строк, а минимальный код

About

Простая база данных для Arduino

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages