Skip to content

GyverLibs/GyverDB

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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);

// подключить обработчик создания и изменения значения записи вида void f(size_t hash)
void onChange(ChangeCallback cb);

// использовать стек обновлений (умолч. 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

// тип ячейки
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 как в примере выше. Рекомендуется использовать этот вариант как самый удобный и оптимальный.

Есть ещё DB_KEYS_CLASS - он создаёт enum class. Но такие константы нужно будет вручную кастовать к size_t

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

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, а лучше сразу пишите на почту alex@alexgyver.ru
Библиотека открыта для доработки и ваших Pull Request'ов!

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

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

About

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

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages