211 lines
7.3 KiB
C
211 lines
7.3 KiB
C
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stddef.h>
|
|
#include "esp_log.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "nvs.h"
|
|
#include "nvs_flash.h"
|
|
#include "config_store.h"
|
|
|
|
static const char *TAG = "config";
|
|
#define NVS_NS "buzz"
|
|
#define CFG_KEY "cfg"
|
|
#define CFG_MAGIC 0xB0220004u
|
|
#define CFG_MAGIC_PRE_EVENT_POST 0xB0220003u
|
|
#define CFG_MAGIC_PRE_500MS 0xB0220002u
|
|
#define CFG_MAGIC_PRE_COOLDOWN_MS 0xB0220001u
|
|
|
|
typedef struct { uint32_t magic; app_config_t cfg; } cfg_blob_t;
|
|
typedef struct {
|
|
uint32_t magic;
|
|
float threshold, min_rms;
|
|
uint32_t win_n, win_m, cooldown_ms;
|
|
float noise_mult, adapt_weight;
|
|
uint32_t monitor_ms, heartbeat_h;
|
|
} legacy_cfg_blob_t;
|
|
|
|
typedef enum { PT_FLOAT, PT_UINT } ptype_t;
|
|
typedef struct {
|
|
const char *name;
|
|
ptype_t type;
|
|
size_t off;
|
|
double min, max;
|
|
const char *help;
|
|
} param_t;
|
|
|
|
#define P(n, t, mn, mx, h) { #n, t, offsetof(app_config_t, n), mn, mx, h }
|
|
static const param_t s_params[] = {
|
|
P(threshold, PT_FLOAT, 0.10, 0.999, "cosine threshold for a matching frame"),
|
|
P(min_rms, PT_FLOAT, 0.0, 32767.0, "loudness gate (int16 RMS)"),
|
|
P(win_n, PT_UINT, 1, 32, "sliding window length N (frames)"),
|
|
P(win_m, PT_UINT, 1, 32, "matches M needed in window (<= N)"),
|
|
P(cooldown_ms, PT_UINT, 0, 86400000, "quiet tail used to group buzzer frames (ms)"),
|
|
P(noise_mult, PT_FLOAT, 1.0, 100.0, "learn/adapt gate = noise floor x this"),
|
|
P(adapt_weight, PT_FLOAT, 0.0, 1.0, "weight of new template in adapt"),
|
|
P(monitor_ms, PT_UINT, 10, 60000, "min interval between monitor lines (ms)"),
|
|
};
|
|
#define NPARAMS (sizeof(s_params) / sizeof(s_params[0]))
|
|
|
|
static app_config_t s_cfg;
|
|
static SemaphoreHandle_t s_mx;
|
|
|
|
static void load_defaults(app_config_t *c)
|
|
{
|
|
c->threshold = CONFIG_BUZZ_THRESHOLD_MILLI / 1000.0f;
|
|
c->min_rms = (float)CONFIG_BUZZ_MIN_RMS;
|
|
c->win_n = CONFIG_BUZZ_WIN_N;
|
|
c->win_m = CONFIG_BUZZ_WIN_M;
|
|
c->cooldown_ms = CONFIG_BUZZ_COOLDOWN_MS;
|
|
c->noise_mult = CONFIG_BUZZ_NOISE_MULT_X10 / 10.0f;
|
|
c->adapt_weight = CONFIG_BUZZ_ADAPT_WEIGHT_PCT / 100.0f;
|
|
c->monitor_ms = CONFIG_BUZZ_MONITOR_MS;
|
|
if (c->win_m > c->win_n) c->win_m = c->win_n;
|
|
}
|
|
|
|
static esp_err_t save_locked(void)
|
|
{
|
|
nvs_handle_t h;
|
|
esp_err_t err = nvs_open(NVS_NS, NVS_READWRITE, &h);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "nvs_open: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
cfg_blob_t blob = { .magic = CFG_MAGIC, .cfg = s_cfg };
|
|
err = nvs_set_blob(h, CFG_KEY, &blob, sizeof(blob));
|
|
if (err == ESP_OK) err = nvs_commit(h);
|
|
if (err != ESP_OK) ESP_LOGE(TAG, "save failed: %s", esp_err_to_name(err));
|
|
nvs_close(h);
|
|
return err;
|
|
}
|
|
|
|
esp_err_t config_init(void)
|
|
{
|
|
s_mx = xSemaphoreCreateMutex();
|
|
if (!s_mx) return ESP_ERR_NO_MEM;
|
|
|
|
esp_err_t err = nvs_flash_init();
|
|
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
ESP_LOGW(TAG, "erasing NVS (%s)", esp_err_to_name(err));
|
|
ESP_ERROR_CHECK(nvs_flash_erase());
|
|
err = nvs_flash_init();
|
|
}
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "nvs_flash_init: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
load_defaults(&s_cfg);
|
|
nvs_handle_t h;
|
|
if (nvs_open(NVS_NS, NVS_READONLY, &h) == ESP_OK) {
|
|
union {
|
|
cfg_blob_t current;
|
|
legacy_cfg_blob_t legacy;
|
|
} blob;
|
|
size_t len = sizeof(blob);
|
|
if (nvs_get_blob(h, CFG_KEY, &blob, &len) == ESP_OK) {
|
|
if (len == sizeof(cfg_blob_t) && blob.current.magic == CFG_MAGIC) {
|
|
s_cfg = blob.current.cfg;
|
|
ESP_LOGI(TAG, "loaded saved config from NVS");
|
|
} else if (len == sizeof(legacy_cfg_blob_t) &&
|
|
(blob.legacy.magic == CFG_MAGIC_PRE_EVENT_POST ||
|
|
blob.legacy.magic == CFG_MAGIC_PRE_500MS ||
|
|
blob.legacy.magic == CFG_MAGIC_PRE_COOLDOWN_MS)) {
|
|
s_cfg.threshold = blob.legacy.threshold;
|
|
s_cfg.min_rms = blob.legacy.min_rms;
|
|
s_cfg.win_n = blob.legacy.win_n;
|
|
s_cfg.win_m = blob.legacy.win_m;
|
|
s_cfg.cooldown_ms = blob.legacy.magic == CFG_MAGIC_PRE_EVENT_POST
|
|
? blob.legacy.cooldown_ms : CONFIG_BUZZ_COOLDOWN_MS;
|
|
s_cfg.noise_mult = blob.legacy.noise_mult;
|
|
s_cfg.adapt_weight = blob.legacy.adapt_weight;
|
|
s_cfg.monitor_ms = blob.legacy.monitor_ms;
|
|
ESP_LOGI(TAG, "migrating saved detector settings; heartbeat setting removed");
|
|
err = save_locked();
|
|
if (err != ESP_OK) {
|
|
nvs_close(h);
|
|
return err;
|
|
}
|
|
} else {
|
|
ESP_LOGI(TAG, "no compatible saved config, using Kconfig defaults");
|
|
}
|
|
} else {
|
|
ESP_LOGI(TAG, "no compatible saved config, using Kconfig defaults");
|
|
}
|
|
nvs_close(h);
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
void config_get(app_config_t *out)
|
|
{
|
|
xSemaphoreTake(s_mx, portMAX_DELAY);
|
|
*out = s_cfg;
|
|
xSemaphoreGive(s_mx);
|
|
}
|
|
|
|
esp_err_t config_set_param(const char *name, const char *value, char *err, size_t err_len)
|
|
{
|
|
for (size_t i = 0; i < NPARAMS; i++) {
|
|
const param_t *p = &s_params[i];
|
|
if (strcmp(p->name, name) != 0) continue;
|
|
|
|
char *end = NULL;
|
|
errno = 0;
|
|
double v = strtod(value, &end);
|
|
if (end == value || *end != '\0' || errno != 0) {
|
|
snprintf(err, err_len, "'%s' is not a number", value);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
if (v < p->min || v > p->max) {
|
|
snprintf(err, err_len, "%s must be in [%g, %g]", p->name, p->min, p->max);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
xSemaphoreTake(s_mx, portMAX_DELAY);
|
|
app_config_t next = s_cfg;
|
|
void *dst = (uint8_t *)&next + p->off;
|
|
if (p->type == PT_FLOAT) *(float *)dst = (float)v;
|
|
else *(uint32_t *)dst = (uint32_t)(v + 0.5);
|
|
if (next.win_m > next.win_n) {
|
|
xSemaphoreGive(s_mx);
|
|
snprintf(err, err_len, "win_m (%lu) must be <= win_n (%lu)",
|
|
(unsigned long)next.win_m, (unsigned long)next.win_n);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
s_cfg = next;
|
|
esp_err_t e = save_locked();
|
|
xSemaphoreGive(s_mx);
|
|
if (e != ESP_OK) snprintf(err, err_len, "applied but NVS save failed");
|
|
return e;
|
|
}
|
|
snprintf(err, err_len, "unknown parameter '%s' (see `get`)", name);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
esp_err_t config_reset_defaults(void)
|
|
{
|
|
xSemaphoreTake(s_mx, portMAX_DELAY);
|
|
load_defaults(&s_cfg);
|
|
esp_err_t e = save_locked();
|
|
xSemaphoreGive(s_mx);
|
|
return e;
|
|
}
|
|
|
|
int config_param_count(void) { return (int)NPARAMS; }
|
|
const char *config_param_name(int i) { return s_params[i].name; }
|
|
|
|
void config_param_format(int i, char *buf, size_t len)
|
|
{
|
|
const param_t *p = &s_params[i];
|
|
app_config_t c;
|
|
config_get(&c);
|
|
const void *src = (const uint8_t *)&c + p->off;
|
|
if (p->type == PT_FLOAT) {
|
|
snprintf(buf, len, "%-13s = %-9.4g (%g..%g) %s", p->name, *(const float *)src, p->min, p->max, p->help);
|
|
} else {
|
|
snprintf(buf, len, "%-13s = %-9lu (%g..%g) %s", p->name, (unsigned long)*(const uint32_t *)src, p->min, p->max, p->help);
|
|
}
|
|
}
|