Files
powerpods/main/pod_settings.c
skrueckenandCursor 7f28a48a10 Add runtime UV protection toggle and treat near-zero sense as external power.
Per-pod NVS enable (default off) via UART/ESP-NOW/dashboard, and skip UV latch when LiPo ADC reads ~0 mV so USB/external supply still boots.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-01 22:51:17 +01:00

605 lines
15 KiB
C

#include "pod_settings.h"
#include "bosch456.h"
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
#include <string.h>
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_UV_EN = "uv_en";
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);
}
bool pod_settings_is_uv_enabled(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_EN, &value);
nvs_close(handle);
if (err == ESP_ERR_NVS_NOT_FOUND) {
return false;
}
if (err != ESP_OK) {
return false;
}
return value != 0;
}
esp_err_t pod_settings_set_uv_enabled(bool enabled) {
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_EN, enabled ? 1u : 0u);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "save uv_en failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "UV protection %s", enabled ? "enabled" : "disabled");
return ESP_OK;
}
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;
}