#include "pod_settings.h" #include "bosch456.h" #include "esp_log.h" #include "nvs.h" #include "nvs_flash.h" #include static const char *TAG = "[SETTINGS]"; static const char *NS = "powerpod"; static const char *KEY_ACCEL_DZ = "accel_dz"; static const char *KEY_UV_LATCH = "uv_latch"; static const char *KEY_TAP_CFG = "tap_cfg"; static const char *KEY_ID_MAP = "id_map"; static const char *KEY_ID_BIND = "id_bind"; #define ACCEL_DEADZONE_MAX 4095u #define TAP_CFG_NVS_VERSION 1u #define ID_MAP_NVS_VERSION 1u #define ID_BIND_NVS_VERSION 1u typedef struct __attribute__((packed)) { uint8_t mac[POD_ID_MAC_LEN]; uint8_t id; } id_map_entry_nvs_t; typedef struct __attribute__((packed)) { uint8_t version; uint8_t next_id; uint8_t count; id_map_entry_nvs_t entries[POD_ID_MAP_MAX]; } id_map_nvs_t; typedef struct __attribute__((packed)) { uint8_t version; uint8_t master_mac[POD_ID_MAC_LEN]; uint8_t assigned_id; } id_bind_nvs_t; /** Packed layout for NVS blob (stable across rebuilds). */ typedef struct __attribute__((packed)) { uint8_t version; uint16_t tap_sens_thres; uint16_t max_gest_dur; uint16_t tap_shock_dur; uint16_t quite_time_after_gest; uint16_t wait_for_timeout; uint16_t axis_sel; uint8_t enable_flags; /* bit0=single, bit1=double, bit2=triple */ } tap_config_nvs_t; static bool s_nvs_ready; static esp_err_t ensure_nvs(void) { if (s_nvs_ready) { return ESP_OK; } return pod_settings_init(); } esp_err_t pod_settings_init(void) { if (s_nvs_ready) { return ESP_OK; } 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, "NVS erase and re-init"); ESP_ERROR_CHECK(nvs_flash_erase()); err = nvs_flash_init(); } if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_flash_init failed: %s", esp_err_to_name(err)); return err; } s_nvs_ready = true; ESP_LOGI(TAG, "NVS ready, accel deadzone %lu LSB (stored)", (unsigned long)pod_settings_load_accel_deadzone()); return ESP_OK; } uint32_t pod_settings_load_accel_deadzone(void) { if (!s_nvs_ready) { return BMA456_DEFAULT_ACCEL_DEADZONE; } nvs_handle_t handle; esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); if (err != ESP_OK) { return BMA456_DEFAULT_ACCEL_DEADZONE; } uint32_t value = BMA456_DEFAULT_ACCEL_DEADZONE; err = nvs_get_u32(handle, KEY_ACCEL_DZ, &value); nvs_close(handle); if (err == ESP_ERR_NVS_NOT_FOUND) { return BMA456_DEFAULT_ACCEL_DEADZONE; } if (err != ESP_OK) { ESP_LOGW(TAG, "read accel_dz failed: %s", esp_err_to_name(err)); return BMA456_DEFAULT_ACCEL_DEADZONE; } if (value > ACCEL_DEADZONE_MAX) { return BMA456_DEFAULT_ACCEL_DEADZONE; } return value; } esp_err_t pod_settings_save_accel_deadzone(uint32_t deadzone_lsb) { if (deadzone_lsb > ACCEL_DEADZONE_MAX) { return ESP_ERR_INVALID_ARG; } esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } err = nvs_set_u32(handle, KEY_ACCEL_DZ, deadzone_lsb); if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "save accel_dz failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "saved accel deadzone %lu LSB", (unsigned long)deadzone_lsb); return ESP_OK; } void pod_settings_apply_accel_deadzone(void) { uint32_t deadzone = pod_settings_load_accel_deadzone(); if (bma456_is_ready()) { bma456_set_accel_deadzone(deadzone); } } static void tap_config_to_nvs(const bma456_tap_config_t *cfg, tap_config_nvs_t *out) { memset(out, 0, sizeof(*out)); out->version = TAP_CFG_NVS_VERSION; out->tap_sens_thres = cfg->tap_sens_thres; out->max_gest_dur = cfg->max_gest_dur; out->tap_shock_dur = cfg->tap_shock_dur; out->quite_time_after_gest = cfg->quite_time_after_gest; out->wait_for_timeout = cfg->wait_for_timeout; out->axis_sel = cfg->axis_sel; if (cfg->enable_single) { out->enable_flags |= 0x01u; } if (cfg->enable_double) { out->enable_flags |= 0x02u; } if (cfg->enable_triple) { out->enable_flags |= 0x04u; } } static bool tap_config_from_nvs(const tap_config_nvs_t *in, bma456_tap_config_t *cfg) { if (in->version != TAP_CFG_NVS_VERSION) { return false; } cfg->tap_sens_thres = in->tap_sens_thres; cfg->max_gest_dur = in->max_gest_dur; cfg->tap_shock_dur = in->tap_shock_dur; cfg->quite_time_after_gest = in->quite_time_after_gest; cfg->wait_for_timeout = in->wait_for_timeout; cfg->axis_sel = in->axis_sel; cfg->enable_single = (in->enable_flags & 0x01u) != 0; cfg->enable_double = (in->enable_flags & 0x02u) != 0; cfg->enable_triple = (in->enable_flags & 0x04u) != 0; return bma456_tap_config_valid(cfg); } bool pod_settings_load_tap_config(bma456_tap_config_t *cfg_out) { static const bma456_tap_config_t k_default = BMA456_TAP_CONFIG_DEFAULT; if (cfg_out == NULL) { return false; } *cfg_out = k_default; if (!s_nvs_ready) { return false; } nvs_handle_t handle; esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); if (err != ESP_OK) { return false; } tap_config_nvs_t blob = {0}; size_t len = sizeof(blob); err = nvs_get_blob(handle, KEY_TAP_CFG, &blob, &len); nvs_close(handle); if (err == ESP_ERR_NVS_NOT_FOUND) { return false; } if (err != ESP_OK || len != sizeof(blob)) { ESP_LOGW(TAG, "read tap_cfg failed: %s (len=%u)", esp_err_to_name(err), (unsigned)len); return false; } if (!tap_config_from_nvs(&blob, cfg_out)) { ESP_LOGW(TAG, "tap_cfg blob invalid; using defaults"); *cfg_out = k_default; return false; } return true; } esp_err_t pod_settings_save_tap_config(const bma456_tap_config_t *cfg) { if (!bma456_tap_config_valid(cfg)) { return ESP_ERR_INVALID_ARG; } esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } tap_config_nvs_t blob; tap_config_to_nvs(cfg, &blob); err = nvs_set_blob(handle, KEY_TAP_CFG, &blob, sizeof(blob)); if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "save tap_cfg failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "saved tap config sens=%u max_gest=%u shock=%u quiet=%u wait=%u " "axis=%u (S=%d D=%d T=%d)", (unsigned)cfg->tap_sens_thres, (unsigned)cfg->max_gest_dur, (unsigned)cfg->tap_shock_dur, (unsigned)cfg->quite_time_after_gest, (unsigned)cfg->wait_for_timeout, (unsigned)cfg->axis_sel, cfg->enable_single, cfg->enable_double, cfg->enable_triple); return ESP_OK; } void pod_settings_apply_tap_config(void) { bma456_tap_config_t cfg; if (!pod_settings_load_tap_config(&cfg)) { return; } if (!bma456_is_ready()) { return; } if (bma456_set_tap_config(&cfg) != ESP_OK) { ESP_LOGW(TAG, "failed to apply NVS tap config"); } } bool pod_settings_is_uv_latched(void) { if (!s_nvs_ready) { return false; } nvs_handle_t handle; esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); if (err != ESP_OK) { return false; } uint8_t value = 0; err = nvs_get_u8(handle, KEY_UV_LATCH, &value); nvs_close(handle); return err == ESP_OK && value != 0; } esp_err_t pod_settings_set_uv_latched(bool latched) { esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } err = nvs_set_u8(handle, KEY_UV_LATCH, latched ? 1u : 0u); if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "save uv_latch failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "UV latch %s", latched ? "set" : "cleared"); return ESP_OK; } esp_err_t pod_settings_clear_uv_latched(void) { return pod_settings_set_uv_latched(false); } esp_err_t pod_settings_load_id_map(pod_id_map_t *out) { if (out == NULL) { return ESP_ERR_INVALID_ARG; } memset(out, 0, sizeof(*out)); out->next_id = (uint8_t)POD_CLIENT_ID_MIN; if (!s_nvs_ready) { return ESP_OK; } nvs_handle_t handle; esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); if (err != ESP_OK) { return ESP_OK; } id_map_nvs_t blob; memset(&blob, 0, sizeof(blob)); size_t len = sizeof(blob); err = nvs_get_blob(handle, KEY_ID_MAP, &blob, &len); nvs_close(handle); if (err == ESP_ERR_NVS_NOT_FOUND) { return ESP_OK; } if (err != ESP_OK || len < 3 || blob.version != ID_MAP_NVS_VERSION) { ESP_LOGW(TAG, "read id_map failed: %s (len=%u)", esp_err_to_name(err), (unsigned)len); return ESP_OK; } if (blob.next_id < POD_CLIENT_ID_MIN) { blob.next_id = (uint8_t)POD_CLIENT_ID_MIN; } out->next_id = blob.next_id; out->count = blob.count; for (uint8_t i = 0; i < blob.count; i++) { memcpy(out->entries[i].mac, blob.entries[i].mac, POD_ID_MAC_LEN); out->entries[i].id = blob.entries[i].id; } ESP_LOGI(TAG, "loaded id_map count=%u next_id=%u", (unsigned)out->count, (unsigned)out->next_id); return ESP_OK; } esp_err_t pod_settings_save_id_map(const pod_id_map_t *map) { if (map == NULL) { return ESP_ERR_INVALID_ARG; } if (map->next_id < POD_CLIENT_ID_MIN) { return ESP_ERR_INVALID_ARG; } esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } id_map_nvs_t blob; memset(&blob, 0, sizeof(blob)); blob.version = ID_MAP_NVS_VERSION; blob.next_id = map->next_id; blob.count = map->count; for (uint8_t i = 0; i < map->count; i++) { memcpy(blob.entries[i].mac, map->entries[i].mac, POD_ID_MAC_LEN); blob.entries[i].id = map->entries[i].id; } nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } err = nvs_set_blob(handle, KEY_ID_MAP, &blob, sizeof(blob)); if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "save id_map failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "saved id_map count=%u next_id=%u", (unsigned)map->count, (unsigned)map->next_id); return ESP_OK; } esp_err_t pod_settings_clear_id_map(void) { esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } err = nvs_erase_key(handle, KEY_ID_MAP); if (err == ESP_ERR_NVS_NOT_FOUND) { err = ESP_OK; } if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "clear id_map failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "cleared id_map"); return ESP_OK; } bool pod_settings_load_id_binding(uint8_t master_mac[POD_ID_MAC_LEN], uint32_t *assigned_id_out) { if (master_mac == NULL || assigned_id_out == NULL) { return false; } memset(master_mac, 0, POD_ID_MAC_LEN); *assigned_id_out = 0; if (!s_nvs_ready) { return false; } nvs_handle_t handle; esp_err_t err = nvs_open(NS, NVS_READONLY, &handle); if (err != ESP_OK) { return false; } id_bind_nvs_t blob = {0}; size_t len = sizeof(blob); err = nvs_get_blob(handle, KEY_ID_BIND, &blob, &len); nvs_close(handle); if (err != ESP_OK || len != sizeof(blob) || blob.version != ID_BIND_NVS_VERSION) { return false; } if (blob.assigned_id < POD_CLIENT_ID_MIN) { return false; } memcpy(master_mac, blob.master_mac, POD_ID_MAC_LEN); *assigned_id_out = blob.assigned_id; return true; } esp_err_t pod_settings_save_id_binding(const uint8_t master_mac[POD_ID_MAC_LEN], uint32_t assigned_id) { if (master_mac == NULL || assigned_id < POD_CLIENT_ID_MIN || assigned_id > POD_CLIENT_ID_MAX) { return ESP_ERR_INVALID_ARG; } esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } id_bind_nvs_t blob = { .version = ID_BIND_NVS_VERSION, .assigned_id = (uint8_t)assigned_id, }; memcpy(blob.master_mac, master_mac, POD_ID_MAC_LEN); nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } err = nvs_set_blob(handle, KEY_ID_BIND, &blob, sizeof(blob)); if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "save id_bind failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "saved id_bind id=%lu master=%02x:…:%02x", (unsigned long)assigned_id, master_mac[0], master_mac[5]); return ESP_OK; } esp_err_t pod_settings_clear_id_binding(void) { esp_err_t err = ensure_nvs(); if (err != ESP_OK) { return err; } nvs_handle_t handle; err = nvs_open(NS, NVS_READWRITE, &handle); if (err != ESP_OK) { ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err)); return err; } err = nvs_erase_key(handle, KEY_ID_BIND); if (err == ESP_ERR_NVS_NOT_FOUND) { err = ESP_OK; } if (err == ESP_OK) { err = nvs_commit(handle); } nvs_close(handle); if (err != ESP_OK) { ESP_LOGE(TAG, "clear id_bind failed: %s", esp_err_to_name(err)); return err; } ESP_LOGI(TAG, "cleared id_bind"); return ESP_OK; }