prefs.h : прототипы функций из библиотеки
#ifndef prefs_h
#define prefs_h
//
// Мини-библиотека реализующая простенький функционал "Файл настроек"
//
// Терминология:
//
// Файл настроек (или файл параметров) - это не обязательно физический файл в файловой системе.
// Библиотека работает с буфером, с областью памяти, в которой и лежит этот файл настроек.
// Фактически, это просто строка, которая, как и положено в Си, должна заканчиваться нулём.
//
//
// Файл настроек это текстовая строка, выглядит, например, так:
//
// esp32 {
// core {
// clocks {
// pll = 240
// xtal=40
// apb = -80
// features=
// extra_flags=777
// }
// pwm {
// enabled = yes
// freq = 666
// duty = 0.78
// }
// }
//}
//esp32s3 {
// core {
// clocks
// {
// pll = 480
// xtal=80
// apb = 160
// features=
// }
// pwm {
// enabled = yes
// freq = 6666
// duty = 0.87
// }
// }
// }
//
// Эта строка подается на вход функции pref_reload(), которая создает **список параметров** или **список настроек**,
// после чего сам файл настроек становится больше не нужным и память, которую он занимал, можно освободить.
// Таких файлов настроек можно загрузить несколько: функция pref_append() ДОБАВЛЯЕТ новый фал настроек к предыдующему,
// pref_reload() - затирает старые настройки новыми.
//
// Окей, данные загружены (вызовом pref_reload() или pref_append()). Как считывать значения?
// -----------------------------------------------------------------------------------------
//
// Используя функции pref_string(), pref_uint(), pref_int() и pref_float(), чтобы получить значение в виде
// строчки, беззнакового и знакового целых или числа с плавающей точкой. В качестве имени параметра указывается
// полный путь до этого параметра. Так, например, в файл-примере выше, в качестве имени параметра extra_flags
// следует указать "esp32.core.clocks.extra_flags"
//
// Пример получения значения параметра:
// const char *value = pref_string("esp32.core.clocks.apb", "80");
// ^------ название параметра ^-- значение по умолчанию
//
// Результат выполнения perf_string() - строка, со значением (см. пример файла выше) "-80". Если бы параметр
// "esp32.core.clocks.apb" в файле отсутствовал, тогда результатом была бы строка "80" - второй аргумент функции
// pref_string(). Втрой аргумент этой функции может быть чем угодно, и это значение в неизменненном виде будет
// возвращено, если указанная настройка в файле не будет найдена.
//
// Если нужна не строчка, а число, то можно использовать одну из функций ниже:
//
// uint32_t pref_uint (const char *name, uint32_t default)
// int32_t pref_int (const char *name, int32_t default)
// uint32_t pref_float(const char *name, float default)
//
// Проверить, есть ли настройка в файле можно так:
//
// if (pref_string("some.pref.value",NULL)) {
// // Настройка в файле имеется
// ...
// }
//
// ПАМЯТЬ
// ------
// После разбора файла настроек, они сохраняются в памяти, в относительно компактном виде: имя настройки сжимается
// до двух байт (вне зависимости от длины имени :) привет BABOSик!)
//
// Значение хранится в виде строки, и дополнительная память, необъодимая библиотеке составляет 8 байт на каждый
// параметр + сам параметр, в виде строчки
//
#include <stdint.h>
#include <stdbool.h>
#ifdef __cplusplus
extern "C" {
#endif
// Прочитать файл с настройками, (лучше использовать макросы ниже)
//
// Аргументы:
// /p/ - указатель на память, где находится файл с настройками.
// Функция ожидает, что файл должен заканчиваться нулем!
//
//
// /append/ - удалять уже существующие параметры или добавить новые к существуюшим?
// true - если нужно объеденить содержимое нескольких файлов
// false - если нужно "перечитать" файл настроек
//
// ЕСЛИ оба параметра функции равны нулю, то такой вызов высвобождает память,
// занятую под хранение параметров. Параметры становятся недоступны.
//
// Возвращает:
//
// Возвращает /false/ если была вызвана с обоими аргументами равными нулю. В остальных
// случаях возвращает /true/
//
bool pref_parse(const char *p, bool append);
#define pref_append(_P) pref_parse(_P,true) // прочитать новые настройки в дополнение к старым
#define pref_reload(_P) pref_parse(_P,false) // перезаписать старые настройки новыми
#define pref_purge() pref_parse(0,0) // освободить всю память используемую библиотекой
// Возвращает значение-строчку, по имени настройки /name/.
// Если запрошенного параметра (с именем /name/) в файле настроек не было, то функция
// вернет свой второй аргумент, "значение по умолчанию". Функция не проверяет
// и не использует значение /def/, поэтому там может быть что угодно, включая
// 0 и указатели на несуществющую память. С помощью второго параметра, в частности, можно
// проверять, есть ли нужный параметр в файле или нет.
//
// Аргументы:
// /name/ - указатель на строчку-название параметра (например "firmware.spi_settings.bus_freq") или NULL
// /def/ - значение по умолчанию: значение, которое вернет функция, если параметр в файле отсутствует
// Возвращает:
//
// Указатель на строчку, содержащую *значение* параметра /name/
// Возвращает /def/, если имя /name/ не найдено
// Может вернуть NULL только если /def/ == NULL
//
const char *pref_string(const char *name, const char *def);
// То же самое, что и pref_string, но результат сконвертирован к 32-битному значению
//
// Аргументы:
// /name/ - указатель на строчку-название параметра (например "firmware.spi_settings.bus_freq") или NULL
// /def/ - значение по умолчанию: значение, которое вернет функция, если параметр в файле отсутствует
// Возвращает:
// Беззнаковое 32-битное целое число, значение параметра /name/
// Возвращает /def/, если имя /name/ не найдено
//
uint32_t pref_uint(const char *name, uint32_t def);
// То же самое, что и pref_uint(), но для чисел со знаком
//
int32_t pref_int(const char *name, int32_t def);
// И еще разок, но теперь уже для чисел с плавающей точкой
float pref_float(const char *name, float def);
#ifdef __cplusplus
};
#endif
#endif // prefs_h
pref.c : сама библиотека
//#define TEST 1
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "prefs.h"
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
// Список настроек, который строит парсер (pref_parse()) на основании файла настроек. После того,
// как этот список создан (т.е. функция pref_parse() была вызвана), нужды в файле настроек больше нет,
// память, которую он занимал можно освобождать.
//
// Список настроек не содержит названий настроек. Вместо этого используется 16-ти битовый хэш (MCU friendly code yay!)
//
static struct prefentry {
struct prefentry *next; //
uint16_t lvhash; // хэш имени параметра
char rval[0]; // значение параметра в виде С-строчки (можно отдавать atoi(), atol() или atof())
} *prefs = NULL;
// Проверить, является ли символ /_X/ значимым
#define is_space(_X) ((_X) <= ' ')
// Проверить, является ли символ /c/ концом строки.
static inline __attribute__((const)) bool is_eol(const char c) {
return c == '\n' || c == '\r' || c == '\0';
}
// Вычислить 16-битный хэш для строки /s/.
//
static uint16_t lval_hash(const char *s) {
uint32_t hash = 0;
char ch;
while ((ch = *s++) != '\0')
hash = hash + ((hash) << 5) + ch + (ch << 7);
return (uint16_t)(((hash) ^ (hash >> 16)) & 0xffff);
}
// Добавить очередной символ к имени параметра (т.е. к lvalue), добавить терминатор
//
static int lval_add_sym(char *lval, char c, uint8_t wp) {
if (wp == 255) {
#if TEST
printf("lval_add_sym() : scratch buffer overflow (c=%u)\r\n", c);
#endif
}
else {
lval[wp++] = c;
lval[wp] = '\0';
}
return wp;
}
// Отрезать от lvalue последний компонент, оставить точку
//
// "kitty.poo.r." превращается в "kitty.poo."
// "cat.whiskers.amount" превращается в "cat.whiskers."
//
static int lval_strip_last(char *lval, uint8_t wp) {
if (wp > 0) {
if (lval[wp - 1] == '.')
wp--;
while(wp > 0 && lval[wp - 1] != '.')
wp--;
}
lval[wp] = '\0';
return wp;
}
// Найти начало (первые осмысленные символы) значения параметра (т.е. rvalue).
// " some random text here " -- значение параметра
// ^-начало -- то, что найдет функция
static const char *rval_begin(const char *p) {
while (!is_eol(*p) && is_space(*p))
p++;
return p;
}
// Ищем конец текста, возвращает указатель на символ следующий за последним символом текста.
// " some random text here "
// ^--end
static const char *rval_end(const char *p) {
char const * candidate = 0;
for (; !is_eol(*p); p++) {
if (is_space(*p)) {
if (!candidate)
candidate = p;
} else
candidate = 0;
}
return candidate ? candidate : p;
}
// Распознали очередную опцию - сохраним ее в виде элемента списка prefs
// Память выделяем одним блоком, имя настройки (lvalue) не сохраняется. Вместо него сохраняется 16-битный хэш, для экономии памяти.
// Параметр /rval/ содержит "сырой" rval - с пробелами
// Параметр /lval/ не содержит пробелов (символов с кодом <= ' ').
//
static const char *pref_add_value(char *lval, const char *rval) {
struct prefentry *pe;
int siz;
const char *begin, *end;
end = begin = rval_begin(rval); // end - begin == 0
if (!is_space(*begin))
end = rval_end(begin + 1);
siz = end - begin;
if ((pe = (struct prefentry *)malloc(sizeof(struct prefentry) + siz + 1)) != NULL) {
pe->next = prefs;
prefs = pe;
if (siz)
memcpy(pe->rval, begin, siz);
pe->rval[siz] = '\0';
pe->lvhash = lval_hash(lval);
#if TEST
printf("pref_add_value() : \"%s\" (hash: %04x) = \"%s\"\r\n", lval, pe->lvhash, pe->rval);
#endif
} else {
// нету памяти. скорее всего и printf отсюда не выполнится.
// ничего не поделать, молча продолжаем пытаться читать другие параметры
}
return end;
}
// Прочитать файл с настройками.
//
bool pref_parse(const char *p, bool append) {
char c, // очередная буковка
cp = 0; // прежняя буковка (или 0)
char lval[256];
uint8_t wp = 0;
lval[0] = '\0';
// /append/ == true : добавляем новые настройки к существующим
// /append/ == false : читаем новые настройки, старые - удаляем
if (!append && prefs) {
struct prefentry *pe = prefs;
while (pe) {
struct prefentry *pn = pe->next;
free(pe);
pe = pn;
}
prefs = NULL;
}
// специальный случай: pref_parse(0,0) - высвобождает всю память занятую
// настройками.
if (p == NULL)
return false;
// пробегаемся по всему файлу, до конца
// /c/ - это очередной прочитанный символ, а /cp/ - тот, что был перед ним
//
for( ;(c = *p) != '\0'; cp = c, p++) {
// пропускаем пустое место (т.н. "пробелы", символы с кодами меньше чем 33)
if (is_space(c))
continue;
// Если прошлый символ был "\", то текущий символ никак не обрабатывается (т.е. является обычной буквой)
// Если же прошлый символ был другой, то текущий проверяется на специальные значения; {,},=
// Если хочется использовать специальные символы в качестве имени (lvalue), то следует вставить "\"
// перед специальным символом (например так; value\{1\}=666).
if ( cp != '\\' ) {
switch( c ) {
case '=' : p = pref_add_value(lval,p + 1); // добрались до конца lvalue, создаем и добавляем новый элемент в список настроек.
case '}' : wp = lval_strip_last(lval, wp); // в аккумуляторе lvalue отрезаем последний элемент с точкой ("sample.pref." --> "sample.")
case '\\': continue; // к следующему символу
case '{' : c = '.';
default : // остальные символы идут без обработки
}
}
// добавить очередной символ к lvalue
wp = lval_add_sym(lval, c, wp);
}
return true;
}
// Возвращает значение-строчку, по имени настройки /name/.
// Если запрошенного параметра (с именем /name/) в файле настроек не было, то функция
// вернет свой второй аргумент, "значение по умолчанию".
//
const char *pref_string(const char *name, const char *def) {
struct prefentry *pe;
uint16_t hash;
if (name)
for (hash = lval_hash(name), pe = prefs; pe; pe = pe->next)
if (pe->lvhash == hash)
return pe->rval;
return def;
}
// То же самое, что и pref_string, но результат сконвертирован к 32-битному значению
//
uint32_t pref_uint(const char *name, uint32_t def) {
const char *s = pref_string(name, NULL);
return s ? (uint32_t )atol(s) : def;
}
// То же самое, что и pref_uint(), но для чисел со знаком
//
int32_t pref_int(const char *name, int32_t def) {
const char *s = pref_string(name, NULL);
return s ? (int32_t )atoi(s) : def;
}
// И еще разок, но теперь уже для чисел с плавающей точкой
float pref_float(const char *name, float def) {
const char *s = pref_string(name, NULL);
return s ? (float )atof(s) : def;
}
Пример использования (загрузка параметров, добавление параметров к загруженным, удаление параметров, чтение значений параметров, альтернативный синтаксис для файла настроек):
#include <Arduino.h>
#include "prefs.h"
// файл с параметрами
//
static const char *config1 =
"esp32 {\n"
"\n"
" core {\n"
" clocks {\n"
" pll = 240\n"
" xtal=40\n"
" apb = -80\n"
" features=\n"
" }\n"
" pwm {\n"
" enabled = yes\n"
" freq = 666 \n"
" duty = 0.78 \n"
" }\n"
" }\n"
"}\n";
// еще один
//
static const char *config2 =
"esp32s3{\n"
" \n"
" core { \n"
" \n"
" clocks \n"
" {pll = 480\n"
" xtal=80\n"
" apb = 160\n"
" features=\n"
" extraa_flags=10\n"
" }\n"
"\n"
" pwm {\n"
" enabled = yes\n"
" freq = 6666\n"
" duty = 0.87\n"
" }\n"
" }\n"
"}";
// да, так тоже можно. просто имя = значение, по одному параметру на строчку, точки в именах запрещены!
//
static const char *config3 =
"esp32s3_core_clocks_pll = 480\n"
"esp32s3_core_clocks_xtal = 80\n"
"esp32s3_core_clocks_apb=160\n"
"esp32s3_core_pwm_duty = 0.87\n";
void setup() {
Serial.begin(115200);
}
void loop() {
// загружаем первый файл
pref_reload(config1);
// добавляем еше один файл
pref_append(config2);
// печатаем три значения в виде unsigned, signed и float
Serial.printf("PLL=%u APB=:%d DUTY=%f\r\n",
pref_uint("esp32.core.clocks.pll",0),
pref_int("esp32.core.clocks.apb",0),
pref_float("esp32.core.pwm.duty",0));
// два значения наугад, а одно - несуществующее. Несуществующее (duty1) будет прочитано как 0.666 (см. ниже)
Serial.printf("PLL=:%u APB=%d DUTY1:%f\r\n",
pref_uint("esp32s3.core.clocks.pll",0),
pref_int("esp32s3.core.clocks.apb",0),
pref_float("esp32s3.core.pwm.duty1",0.666));
// удаляем старые настройки и загружаем новые.
// в этот раз - еще и в другом формате, без скобочек и иерархии.
pref_reload(config3);
Serial.printf("PLL=%u APB=%d DUTY=%f\r\n",
pref_uint("esp32s3_core_clocks_pll",0),
pref_int("esp32s3_core_clocks_apb",0),
pref_float("esp32s3_core_pwm_duty",0));
pref_purge();
delay(10000);
}