#include #include #include #include #include #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 0xB0220002u #define CFG_MAGIC_PRE_COOLDOWN_MS 0xB0220001u typedef struct { uint32_t magic; app_config_t cfg; } 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, "cooldown after alert (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)"), P(heartbeat_h, PT_UINT, 0, 720, "heartbeat period (h), 0 = off"), }; #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; c->heartbeat_h = CONFIG_BUZZ_HEARTBEAT_HOURS; 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) { cfg_blob_t blob; size_t len = sizeof(blob); if (nvs_get_blob(h, CFG_KEY, &blob, &len) == ESP_OK && len == sizeof(blob)) { if (blob.magic == CFG_MAGIC) { s_cfg = blob.cfg; ESP_LOGI(TAG, "loaded saved config from NVS"); } else if (blob.magic == CFG_MAGIC_PRE_COOLDOWN_MS) { s_cfg = blob.cfg; s_cfg.cooldown_ms = CONFIG_BUZZ_COOLDOWN_MS; ESP_LOGI(TAG, "migrating saved config to millisecond cooldown"); 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); } }