1399 lines
47 KiB
C
1399 lines
47 KiB
C
#include <math.h>
|
|
#include <string.h>
|
|
#include <sys/param.h>
|
|
#include "esp_partition.h"
|
|
#include "esp_err.h"
|
|
#include "esp_log.h"
|
|
#include "esp_crc.h"
|
|
#include "esp_task_wdt.h"
|
|
#include "storage.h"
|
|
#include "esp_timer.h"
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "nvs_flash.h"
|
|
#include "nvs.h"
|
|
#include "version.h"
|
|
|
|
#define TAG "STORAGE"
|
|
|
|
// Add these includes at the top
|
|
#include "freertos/queue.h"
|
|
#include "freertos/task.h"
|
|
|
|
// Add these defines near the top
|
|
#define LOG_QUEUE_SIZE 8 // Number of log entries that can be queued
|
|
#define LOG_TASK_STACK_SIZE 4096
|
|
#define LOG_TASK_PRIORITY 5
|
|
|
|
// Add these static variables after the existing log variables (around line 100)
|
|
static QueueHandle_t log_queue = NULL;
|
|
static TaskHandle_t log_task_handle = NULL;
|
|
static bool log_task_running = false;
|
|
|
|
// Log queue entry structure
|
|
typedef struct {
|
|
uint8_t data[LOG_MAX_PAYLOAD]; // +1 for length byte
|
|
uint8_t len;
|
|
uint8_t type;
|
|
} log_queue_entry_t;
|
|
|
|
/* LOG_TYPE_* + IS_VALID_LOG_TYPE moved to storage.h as the single source
|
|
* of truth. The duplicate set previously here disagreed with the header
|
|
* (different LOG_TYPE_CUSTOM* names). */
|
|
|
|
// ============================================================================
|
|
// PARAMETER TABLE GENERATION
|
|
// ============================================================================
|
|
|
|
// Helper macros to construct initializers
|
|
#define PARAM_VALUE_INIT(type, val) {.type = val}
|
|
#define PARAM_TYPE_ENUM(type) PARAM_TYPE_##type
|
|
#define PARAM_NAME_STR(name) #name
|
|
|
|
// Generate parameter table with live values (initialized to defaults)
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, default_val),
|
|
param_value_t parameter_table[NUM_PARAMS] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
// Generate default values array
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, default_val),
|
|
const param_value_t parameter_defaults[NUM_PARAMS] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
// Generate parameter types array
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_TYPE_ENUM(type),
|
|
const param_type_e parameter_types[NUM_PARAMS] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
// Generate parameter names array
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_NAME_STR(name),
|
|
const char* parameter_names[NUM_PARAMS] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
// Generate parameter units array (8 chars max per unit)
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) unit,
|
|
const char parameter_units[NUM_PARAMS][8] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
// Generate parameter min bounds array
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, min),
|
|
static const param_value_t parameter_mins[NUM_PARAMS] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
// Generate parameter max bounds array
|
|
#define PARAM_DEF(name, type, default_val, unit, min, max) PARAM_VALUE_INIT(type, max),
|
|
static const param_value_t parameter_maxs[NUM_PARAMS] = {
|
|
PARAM_LIST
|
|
};
|
|
#undef PARAM_DEF
|
|
|
|
size_t param_type_size(param_type_e x) {
|
|
switch(x) {
|
|
case PARAM_TYPE_u16: return 2;
|
|
case PARAM_TYPE_i16: return 2;
|
|
case PARAM_TYPE_u32: return 4;
|
|
case PARAM_TYPE_i32: return 4;
|
|
case PARAM_TYPE_f32: return 4;
|
|
case PARAM_TYPE_f64: return 8;
|
|
case PARAM_TYPE_str: return PARAM_STR_SIZE;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PARAMETER VALIDATION
|
|
// ============================================================================
|
|
// Returns true if the value was modified (clamped or reset to default).
|
|
// - Strings: always skipped
|
|
// - Float NaN/Inf: reset to default
|
|
// - min == max (same raw bytes): skip bounds check (sentinel for "no bounds")
|
|
// - Otherwise: clamp to [min, max]
|
|
static bool validate_param(param_idx_t id) {
|
|
param_type_e type = parameter_types[id];
|
|
|
|
// String params: no numeric validation
|
|
if (type == PARAM_TYPE_str) return false;
|
|
|
|
// Float types: NaN/Inf → reset to default
|
|
if (type == PARAM_TYPE_f32) {
|
|
if (isnanf(parameter_table[id].f32) || isinff(parameter_table[id].f32)) {
|
|
ESP_LOGW(TAG, "Param %s: NaN/Inf, reset to default", parameter_names[id]);
|
|
parameter_table[id] = parameter_defaults[id];
|
|
return true;
|
|
}
|
|
}
|
|
if (type == PARAM_TYPE_f64) {
|
|
if (isnan(parameter_table[id].f64) || isinf(parameter_table[id].f64)) {
|
|
ESP_LOGW(TAG, "Param %s: NaN/Inf, reset to default", parameter_names[id]);
|
|
parameter_table[id] = parameter_defaults[id];
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Skip bounds check if min == max (sentinel)
|
|
size_t sz = param_type_size(type);
|
|
if (memcmp(¶meter_mins[id], ¶meter_maxs[id], sz) == 0)
|
|
return false;
|
|
|
|
// Clamp to [min, max] per type
|
|
bool clamped = false;
|
|
switch (type) {
|
|
case PARAM_TYPE_u16:
|
|
if (parameter_table[id].u16 < parameter_mins[id].u16) {
|
|
parameter_table[id].u16 = parameter_mins[id].u16; clamped = true;
|
|
} else if (parameter_table[id].u16 > parameter_maxs[id].u16) {
|
|
parameter_table[id].u16 = parameter_maxs[id].u16; clamped = true;
|
|
}
|
|
break;
|
|
case PARAM_TYPE_i16:
|
|
if (parameter_table[id].i16 < parameter_mins[id].i16) {
|
|
parameter_table[id].i16 = parameter_mins[id].i16; clamped = true;
|
|
} else if (parameter_table[id].i16 > parameter_maxs[id].i16) {
|
|
parameter_table[id].i16 = parameter_maxs[id].i16; clamped = true;
|
|
}
|
|
break;
|
|
case PARAM_TYPE_u32:
|
|
if (parameter_table[id].u32 < parameter_mins[id].u32) {
|
|
parameter_table[id].u32 = parameter_mins[id].u32; clamped = true;
|
|
} else if (parameter_table[id].u32 > parameter_maxs[id].u32) {
|
|
parameter_table[id].u32 = parameter_maxs[id].u32; clamped = true;
|
|
}
|
|
break;
|
|
case PARAM_TYPE_i32:
|
|
if (parameter_table[id].i32 < parameter_mins[id].i32) {
|
|
parameter_table[id].i32 = parameter_mins[id].i32; clamped = true;
|
|
} else if (parameter_table[id].i32 > parameter_maxs[id].i32) {
|
|
parameter_table[id].i32 = parameter_maxs[id].i32; clamped = true;
|
|
}
|
|
break;
|
|
case PARAM_TYPE_f32:
|
|
if (parameter_table[id].f32 < parameter_mins[id].f32) {
|
|
parameter_table[id].f32 = parameter_mins[id].f32; clamped = true;
|
|
} else if (parameter_table[id].f32 > parameter_maxs[id].f32) {
|
|
parameter_table[id].f32 = parameter_maxs[id].f32; clamped = true;
|
|
}
|
|
break;
|
|
case PARAM_TYPE_f64:
|
|
if (parameter_table[id].f64 < parameter_mins[id].f64) {
|
|
parameter_table[id].f64 = parameter_mins[id].f64; clamped = true;
|
|
} else if (parameter_table[id].f64 > parameter_maxs[id].f64) {
|
|
parameter_table[id].f64 = parameter_maxs[id].f64; clamped = true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (clamped) {
|
|
ESP_LOGW(TAG, "Param %s: out of range, clamped", parameter_names[id]);
|
|
}
|
|
return clamped;
|
|
}
|
|
|
|
// Partition pointers (separate partitions for params, log, and POST test)
|
|
static const esp_partition_t *params_partition = NULL;
|
|
static const esp_partition_t *log_partition = NULL;
|
|
static const esp_partition_t *post_partition = NULL;
|
|
|
|
// Log head/tail tracking with mutex protection.
|
|
// These track byte offsets within the log partition (0-based).
|
|
// RTC_DATA_ATTR is historical — log_init() always recovers these from a flash scan,
|
|
// so the RTC values are overwritten on every boot. No partial-write risk.
|
|
RTC_DATA_ATTR static uint32_t log_head_offset = 0;
|
|
RTC_DATA_ATTR static uint32_t log_tail_offset = 0;
|
|
RTC_DATA_ATTR static bool log_initialized = false;
|
|
|
|
static SemaphoreHandle_t log_mutex = NULL;
|
|
|
|
uint32_t log_get_head(void) {
|
|
uint32_t head;
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
head = log_head_offset;
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return head;
|
|
}
|
|
|
|
uint32_t log_get_tail(void) {
|
|
uint32_t tail;
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
tail = log_tail_offset;
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return tail;
|
|
}
|
|
|
|
uint32_t log_get_offset(void) {
|
|
return 0;
|
|
}
|
|
|
|
uint32_t log_get_size(void) {
|
|
if (log_partition == NULL) return 0;
|
|
return log_partition->size;
|
|
}
|
|
|
|
// ============================================================================
|
|
// PARAMETER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
param_value_t get_param_value_t(param_idx_t id) {
|
|
if (id >= NUM_PARAMS) {
|
|
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
|
param_value_t err = {0};
|
|
return err;
|
|
}
|
|
return parameter_table[id];
|
|
}
|
|
|
|
esp_err_t set_param_value_t(param_idx_t id, param_value_t val) {
|
|
if (id >= NUM_PARAMS) {
|
|
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
parameter_table[id] = val;
|
|
/* Run bounds/NaN validation immediately so callers that read between
|
|
* set and the next commit_params() see a clamped value rather than a
|
|
* raw out-of-range one. */
|
|
validate_param(id);
|
|
ESP_LOGI(TAG, "Parameter %d (%s) set (not committed)", id, parameter_names[id]);
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t set_param_string(param_idx_t id, const char* str) {
|
|
if (id >= NUM_PARAMS) {
|
|
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (parameter_types[id] != PARAM_TYPE_str) {
|
|
ESP_LOGE(TAG, "Parameter %d (%s) is not a string type", id, parameter_names[id]);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (str == NULL) {
|
|
parameter_table[id].str[0] = '\0';
|
|
} else {
|
|
strncpy(parameter_table[id].str, str, 15);
|
|
parameter_table[id].str[15] = '\0'; // Ensure null termination
|
|
}
|
|
|
|
ESP_LOGI(TAG, "String parameter %d (%s) set to '%s' (not committed)",
|
|
id, parameter_names[id], parameter_table[id].str);
|
|
return ESP_OK;
|
|
}
|
|
|
|
char* get_param_string(param_idx_t id) {
|
|
if (id >= NUM_PARAMS) {
|
|
ESP_LOGE(TAG, "Invalid parameter ID: %d", id);
|
|
return "";
|
|
}
|
|
|
|
if (parameter_types[id] != PARAM_TYPE_str) {
|
|
ESP_LOGE(TAG, "Parameter %d (%s) is not a string type", id, parameter_names[id]);
|
|
return "";
|
|
}
|
|
|
|
return parameter_table[id].str;
|
|
}
|
|
|
|
param_type_e get_param_type(param_idx_t id) {
|
|
if (id >= NUM_PARAMS) {
|
|
return PARAM_TYPE_f64; // Default fallback
|
|
}
|
|
return parameter_types[id];
|
|
}
|
|
|
|
// ============================================================================
|
|
// JSON-FRIENDLY STRING CONVERSION
|
|
// ============================================================================
|
|
const char* get_param_json_string(param_idx_t id, char* buffer, size_t buf_size) {
|
|
if (id >= NUM_PARAMS || buffer == NULL || buf_size == 0) {
|
|
if (buffer && buf_size > 0) buffer[0] = '\0';
|
|
return "";
|
|
}
|
|
|
|
param_type_e type = parameter_types[id];
|
|
param_value_t val = parameter_table[id];
|
|
|
|
switch(type) {
|
|
case PARAM_TYPE_u16:
|
|
snprintf(buffer, buf_size, "%u", val.u16);
|
|
break;
|
|
case PARAM_TYPE_i16:
|
|
snprintf(buffer, buf_size, "%d", val.i16);
|
|
break;
|
|
case PARAM_TYPE_u32:
|
|
snprintf(buffer, buf_size, "%lu", (unsigned long)val.u32);
|
|
break;
|
|
case PARAM_TYPE_i32:
|
|
snprintf(buffer, buf_size, "%ld", (long)val.i32);
|
|
break;
|
|
case PARAM_TYPE_f32:
|
|
if (isnan(val.f32) || isinf(val.f32)) {
|
|
snprintf(buffer, buf_size, "null");
|
|
} else {
|
|
snprintf(buffer, buf_size, "%.6g", val.f32);
|
|
}
|
|
break;
|
|
case PARAM_TYPE_f64:
|
|
if (isnan(val.f64) || isinf(val.f64)) {
|
|
snprintf(buffer, buf_size, "null");
|
|
} else {
|
|
snprintf(buffer, buf_size, "%.15g", val.f64);
|
|
}
|
|
break;
|
|
case PARAM_TYPE_str:
|
|
// Escape quotes and backslashes for JSON string
|
|
snprintf(buffer, buf_size, "\"%s\"", val.str);
|
|
break;
|
|
default:
|
|
snprintf(buffer, buf_size, "null");
|
|
break;
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
const char* get_param_name(param_idx_t id) {
|
|
if (id >= NUM_PARAMS) {
|
|
return "INVALID";
|
|
}
|
|
return parameter_names[id];
|
|
}
|
|
|
|
param_value_t get_param_default(param_idx_t id) {
|
|
if (id >= NUM_PARAMS) {
|
|
param_value_t err = {0};
|
|
return err;
|
|
}
|
|
return parameter_defaults[id];
|
|
}
|
|
|
|
const char* get_param_unit(param_idx_t id) {
|
|
if (id >= NUM_PARAMS) {
|
|
return "";
|
|
}
|
|
return parameter_units[id];
|
|
}
|
|
|
|
// ============================================================================
|
|
// STORAGE HELPER: Pack parameter value into buffer
|
|
// ============================================================================
|
|
static void pack_param(uint8_t *dest, param_idx_t id) {
|
|
param_type_e type = parameter_types[id];
|
|
|
|
switch(type) {
|
|
case PARAM_TYPE_u16:
|
|
memcpy(dest, ¶meter_table[id].u16, 2);
|
|
break;
|
|
case PARAM_TYPE_i16:
|
|
memcpy(dest, ¶meter_table[id].i16, 2);
|
|
break;
|
|
case PARAM_TYPE_u32:
|
|
memcpy(dest, ¶meter_table[id].u32, 4);
|
|
break;
|
|
case PARAM_TYPE_i32:
|
|
memcpy(dest, ¶meter_table[id].i32, 4);
|
|
break;
|
|
case PARAM_TYPE_f32:
|
|
memcpy(dest, ¶meter_table[id].f32, 4);
|
|
break;
|
|
case PARAM_TYPE_f64:
|
|
memcpy(dest, ¶meter_table[id].f64, 8);
|
|
break;
|
|
case PARAM_TYPE_str:
|
|
memcpy(dest, parameter_table[id].str, 16);
|
|
break;
|
|
default:
|
|
memset(dest, 0, 16);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// STORAGE HELPER: Unpack parameter value from buffer
|
|
// ============================================================================
|
|
static void unpack_param(const uint8_t *src, param_idx_t id) {
|
|
param_type_e type = parameter_types[id];
|
|
|
|
switch(type) {
|
|
case PARAM_TYPE_u16:
|
|
memcpy(¶meter_table[id].u16, src, 2);
|
|
break;
|
|
case PARAM_TYPE_i16:
|
|
memcpy(¶meter_table[id].i16, src, 2);
|
|
break;
|
|
case PARAM_TYPE_u32:
|
|
memcpy(¶meter_table[id].u32, src, 4);
|
|
break;
|
|
case PARAM_TYPE_i32:
|
|
memcpy(¶meter_table[id].i32, src, 4);
|
|
break;
|
|
case PARAM_TYPE_f32:
|
|
memcpy(¶meter_table[id].f32, src, 4);
|
|
break;
|
|
case PARAM_TYPE_f64:
|
|
memcpy(¶meter_table[id].f64, src, 8);
|
|
break;
|
|
case PARAM_TYPE_str:
|
|
memcpy(parameter_table[id].str, src, 16);
|
|
parameter_table[id].str[15] = '\0'; // Ensure null termination
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// COMMIT PARAMETERS TO FLASH
|
|
// ============================================================================
|
|
esp_err_t commit_params(void) {
|
|
if (params_partition == NULL) {
|
|
ESP_LOGE(TAG, "Params partition not initialized");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Committing %d parameters to flash...", NUM_PARAMS);
|
|
|
|
// Erase entire params partition
|
|
esp_err_t err = esp_partition_erase_range(params_partition, 0, params_partition->size);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to erase params partition: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
// Write each parameter with CRC
|
|
uint32_t flash_offset = 0;
|
|
for (int i = 0; i < NUM_PARAMS; i++) {
|
|
param_stored_t stored;
|
|
memset(&stored, 0, sizeof(param_stored_t));
|
|
|
|
// Validate before writing — clamp out-of-range, reset NaN/Inf
|
|
validate_param(i);
|
|
|
|
// Pack parameter data
|
|
pack_param(stored.data, i);
|
|
|
|
// Calculate CRC over actual data size
|
|
uint8_t size = param_type_size(parameter_types[i]);
|
|
uint32_t crc_input = PARAM_CRC_SALT;
|
|
stored.crc = esp_crc32_le(crc_input, stored.data, size);
|
|
|
|
// Write to flash
|
|
err = esp_partition_write(params_partition, flash_offset,
|
|
&stored, sizeof(param_stored_t));
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to write parameter %d (%s): %s",
|
|
i, parameter_names[i], esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
flash_offset += sizeof(param_stored_t);
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Successfully committed all parameters to flash");
|
|
return ESP_OK;
|
|
}
|
|
|
|
// ============================================================================
|
|
// FACTORY RESET
|
|
// ============================================================================
|
|
esp_err_t factory_reset(void) {
|
|
ESP_LOGI(TAG, "Performing factory reset...");
|
|
|
|
/* Stop the log writer task before erasing the log partition so an
|
|
* in-flight write doesn't race against esp_partition_erase_range. We
|
|
* also take log_mutex for the rest of this function so any pre-existing
|
|
* writer that's mid-write completes before we erase, and any new writer
|
|
* (if log_task_running observation lagged) blocks. The caller is about
|
|
* to esp_restart, so we never give the mutex back. */
|
|
if (log_task_running) {
|
|
log_task_running = false;
|
|
/* Give the writer task time to observe the flag (its queue receive
|
|
* has a 100 ms timeout) and finish any in-flight write. */
|
|
vTaskDelay(pdMS_TO_TICKS(150));
|
|
}
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
|
|
// Reset all parameters to defaults
|
|
for (int i = 0; i < NUM_PARAMS; i++) {
|
|
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t));
|
|
}
|
|
|
|
// Commit defaults to params partition
|
|
esp_err_t err = commit_params();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to commit defaults during factory reset");
|
|
return err;
|
|
}
|
|
|
|
// Erase the log partition
|
|
const esp_partition_t *log_part = esp_partition_find_first(
|
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "log");
|
|
if (log_part != NULL) {
|
|
ESP_LOGI(TAG, "Erasing log partition (%lu bytes)...", (unsigned long)log_part->size);
|
|
err = esp_partition_erase_range(log_part, 0, log_part->size);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to erase log partition: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
}
|
|
|
|
// Erase the POST test partition
|
|
const esp_partition_t *post_part = esp_partition_find_first(
|
|
ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, "post_test");
|
|
if (post_part != NULL) {
|
|
err = esp_partition_erase_range(post_part, 0, post_part->size);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG, "Failed to erase post_test partition: %s", esp_err_to_name(err));
|
|
}
|
|
}
|
|
|
|
// Reset log state so next boot starts fresh
|
|
log_head_offset = 0;
|
|
log_tail_offset = 0;
|
|
log_initialized = false;
|
|
|
|
ESP_LOGI(TAG, "Factory reset complete");
|
|
return ESP_OK;
|
|
}
|
|
|
|
// ============================================================================
|
|
// HARDWARE IDENTITY (separate NVS namespace, survives factory reset)
|
|
// ============================================================================
|
|
#define HW_NVS_NAMESPACE "hw"
|
|
#define HW_NVS_BOARD_REV "board_rev"
|
|
|
|
static uint16_t cached_board_rev = 0;
|
|
static bool board_rev_loaded = false;
|
|
|
|
uint16_t hw_get_board_rev(void) {
|
|
if (board_rev_loaded) return cached_board_rev;
|
|
nvs_handle_t h;
|
|
if (nvs_open(HW_NVS_NAMESPACE, NVS_READONLY, &h) == ESP_OK) {
|
|
nvs_get_u16(h, HW_NVS_BOARD_REV, &cached_board_rev);
|
|
nvs_close(h);
|
|
}
|
|
board_rev_loaded = true;
|
|
return cached_board_rev;
|
|
}
|
|
|
|
esp_err_t hw_set_board_rev(uint16_t rev) {
|
|
nvs_handle_t h;
|
|
esp_err_t err = nvs_open(HW_NVS_NAMESPACE, NVS_READWRITE, &h);
|
|
if (err != ESP_OK) return err;
|
|
err = nvs_set_u16(h, HW_NVS_BOARD_REV, rev);
|
|
if (err == ESP_OK) err = nvs_commit(h);
|
|
nvs_close(h);
|
|
if (err == ESP_OK) {
|
|
cached_board_rev = rev;
|
|
board_rev_loaded = true;
|
|
}
|
|
return err;
|
|
}
|
|
|
|
// ============================================================================
|
|
// FLASH POST (Power-On Self-Test)
|
|
// ============================================================================
|
|
esp_err_t storage_post(void) {
|
|
if (post_partition == NULL) {
|
|
// Find post_test partition if not already found
|
|
post_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
|
ESP_PARTITION_SUBTYPE_ANY,
|
|
"post_test");
|
|
if (post_partition == NULL) {
|
|
ESP_LOGE(TAG, "POST: post_test partition not found");
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
}
|
|
|
|
uint8_t write_buf[16];
|
|
uint8_t read_buf[16];
|
|
|
|
// Fill with a pattern based on boot time so we don't pass on stale data
|
|
uint32_t seed = (uint32_t)esp_timer_get_time();
|
|
for (int i = 0; i < 16; i++) write_buf[i] = (uint8_t)(seed + i * 37);
|
|
|
|
esp_err_t err = esp_partition_erase_range(post_partition, 0, FLASH_SECTOR_SIZE);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "POST: flash erase failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
err = esp_partition_write(post_partition, 0, write_buf, sizeof(write_buf));
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "POST: flash write failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
err = esp_partition_read(post_partition, 0, read_buf, sizeof(read_buf));
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "POST: flash read failed: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
if (memcmp(write_buf, read_buf, sizeof(write_buf)) != 0) {
|
|
ESP_LOGE(TAG, "POST: flash verify MISMATCH");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
// Erase the test sector so it's clean for next boot
|
|
esp_partition_erase_range(post_partition, 0, FLASH_SECTOR_SIZE);
|
|
|
|
ESP_LOGI(TAG, "POST: flash OK");
|
|
return ESP_OK;
|
|
}
|
|
|
|
// ============================================================================
|
|
// STORAGE INITIALIZATION
|
|
// ============================================================================
|
|
esp_err_t storage_init(void) {
|
|
ESP_LOGI(TAG, "Initializing storage system...");
|
|
|
|
// NVS must be initialized before WiFi and BT
|
|
esp_err_t nvs_err = nvs_flash_init();
|
|
if (nvs_err == ESP_ERR_NVS_NO_FREE_PAGES || nvs_err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
|
ESP_LOGW(TAG, "NVS partition needs erasing, performing erase...");
|
|
nvs_err = nvs_flash_erase();
|
|
if (nvs_err == ESP_OK) nvs_err = nvs_flash_init();
|
|
}
|
|
if (nvs_err != ESP_OK) {
|
|
ESP_LOGE(TAG, "nvs_flash_init failed: %s", esp_err_to_name(nvs_err));
|
|
return nvs_err;
|
|
}
|
|
|
|
log_mutex = xSemaphoreCreateMutex();
|
|
if (log_mutex == NULL) {
|
|
ESP_LOGE(TAG, "Failed to create log mutex");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
|
|
params_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
|
ESP_PARTITION_SUBTYPE_ANY,
|
|
"params");
|
|
if (params_partition == NULL) {
|
|
ESP_LOGE(TAG, "Params partition not found");
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Params partition found: size=%lu bytes",
|
|
(unsigned long)params_partition->size);
|
|
|
|
// Load parameters from flash
|
|
uint32_t flash_offset = 0;
|
|
//bool all_valid = true;
|
|
|
|
for (int i = 0; i < NUM_PARAMS; i++) {
|
|
param_stored_t stored;
|
|
|
|
esp_err_t err = esp_partition_read(params_partition, flash_offset,
|
|
&stored, sizeof(param_stored_t));
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGW(TAG, "Failed to read parameter %d (%s), using default",
|
|
i, parameter_names[i]);
|
|
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t));
|
|
//all_valid = false;
|
|
flash_offset += sizeof(param_stored_t);
|
|
continue;
|
|
}
|
|
|
|
// Validate CRC over actual data size
|
|
uint8_t size = param_type_size(parameter_types[i]);
|
|
uint32_t crc_input = PARAM_CRC_SALT;
|
|
uint32_t calculated_crc = esp_crc32_le(crc_input, stored.data, size);
|
|
|
|
if (calculated_crc == stored.crc) {
|
|
unpack_param(stored.data, i);
|
|
if (validate_param(i)) {
|
|
ESP_LOGW(TAG, "Param %d (%s) out of range after load, clamped", i, parameter_names[i]);
|
|
}
|
|
} else {
|
|
ESP_LOGW(TAG, "Parameter %d (%s) failed CRC check, using default",
|
|
i, parameter_names[i]);
|
|
memcpy(¶meter_table[i], ¶meter_defaults[i], sizeof(param_value_t));
|
|
//all_valid = false;
|
|
}
|
|
|
|
flash_offset += sizeof(param_stored_t);
|
|
}
|
|
|
|
/*if (all_valid) {
|
|
ESP_LOGI(TAG, "All parameters loaded successfully from flash");
|
|
} else {
|
|
ESP_LOGW(TAG, "Some parameters failed validation, using defaults");
|
|
}*/
|
|
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_OK;
|
|
}
|
|
|
|
static inline uint32_t log_sector_end(uint32_t x) {
|
|
return (x/FLASH_SECTOR_SIZE+1)*FLASH_SECTOR_SIZE;
|
|
}
|
|
|
|
|
|
// Helper function to check if a sector is erased (starts with 0xFF)
|
|
static bool is_sector_erased(uint32_t x) {
|
|
uint8_t buf;
|
|
esp_err_t err = esp_partition_read(log_partition, x * FLASH_SECTOR_SIZE, &buf, 1);
|
|
if (err != ESP_OK) return false;
|
|
return (buf == 0xFF);
|
|
}
|
|
|
|
static bool is_sector_full(uint32_t x) {
|
|
uint8_t buf;
|
|
esp_err_t err = esp_partition_read(log_partition, (x+1) * FLASH_SECTOR_SIZE - 1, &buf, 1);
|
|
if (err != ESP_OK) return false;
|
|
return (buf != 0xFF);
|
|
}
|
|
|
|
// Replace log_write with this non-blocking version:
|
|
esp_err_t log_write(const uint8_t* buf, uint8_t len, uint8_t type) {
|
|
if (!log_initialized || log_partition == NULL) {
|
|
ESP_LOGE(TAG, "Logging not initialized");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
if (len > LOG_MAX_PAYLOAD) {
|
|
ESP_LOGE(TAG, "Log payload too large: %d bytes (max %d)", len, LOG_MAX_PAYLOAD);
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
|
|
if (log_queue == NULL) {
|
|
ESP_LOGE(TAG, "Log queue not initialized");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
if (type == 0xFF) {
|
|
ESP_LOGE(TAG, "Attempt to log with type=0xFF; not allowed");
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
// Create queue entry
|
|
log_queue_entry_t entry;
|
|
entry.len = len;
|
|
entry.type = type;
|
|
memcpy(entry.data, buf, len);
|
|
|
|
// Try to send to queue (non-blocking)
|
|
if (xQueueSend(log_queue, &entry, 0) != pdTRUE) {
|
|
ESP_LOGW(TAG, "Log queue full, dropping entry");
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
// The actual blocking write function (called by the task)
|
|
static esp_err_t log_write_blocking(const uint8_t* buf, uint8_t len, uint8_t type) {
|
|
if (!log_initialized || log_partition == NULL) {
|
|
ESP_LOGE(TAG, "Logging not initialized");
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
if (len > LOG_MAX_PAYLOAD) {
|
|
ESP_LOGE(TAG, "Log payload too large: %d bytes (max %d)", len, LOG_MAX_PAYLOAD);
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
|
|
// check if we will overrun the sector
|
|
if (log_head_offset + len+2 >= log_sector_end(log_head_offset)) {
|
|
/* Zero the remainder of the current sector. The gap can be up to
|
|
* one full sector (FLASH_SECTOR_SIZE = 4096); a single 256-byte
|
|
* stack buffer would have caused the partition driver to read past
|
|
* the buffer and write arbitrary stack contents to flash. Use a
|
|
* static-zero buffer (`.bss` is implicitly zero-initialized) and
|
|
* chunk the write so an oversize buffer is never required. */
|
|
static const uint8_t zeros[256] = {0};
|
|
size_t gap = log_sector_end(log_head_offset) - log_head_offset;
|
|
size_t written = 0;
|
|
while (written < gap) {
|
|
size_t chunk = MIN(sizeof(zeros), gap - written);
|
|
esp_err_t we = esp_partition_write(log_partition,
|
|
log_head_offset + written,
|
|
zeros, chunk);
|
|
if (we != ESP_OK) {
|
|
ESP_LOGW(TAG, "Failed zero-pad write: %s", esp_err_to_name(we));
|
|
/* Bump head to the next sector regardless — readers tolerate
|
|
* 0xFF or 0x00 padding between entries. */
|
|
break;
|
|
}
|
|
written += chunk;
|
|
}
|
|
|
|
// set head to next sector, and check for wrap
|
|
log_head_offset = log_sector_end(log_head_offset);
|
|
if (log_head_offset >= log_partition->size)
|
|
log_head_offset = 0;
|
|
|
|
// Next write will be in a new sector - check if it needs erasing
|
|
uint8_t check_byte;
|
|
esp_err_t err = esp_partition_read(log_partition, log_head_offset,
|
|
&check_byte, 1);
|
|
|
|
// Erase the next sector
|
|
if (err == ESP_OK && check_byte != 0xFF) {
|
|
err = esp_partition_erase_range(log_partition,
|
|
log_head_offset,
|
|
FLASH_SECTOR_SIZE);
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to erase sector: %s", esp_err_to_name(err));
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_FAIL;
|
|
}
|
|
|
|
}
|
|
|
|
// update the tail, if needed
|
|
if (log_tail_offset >= log_head_offset + FLASH_SECTOR_SIZE) log_tail_offset = log_head_offset + FLASH_SECTOR_SIZE;
|
|
if (log_tail_offset >= log_partition->size)
|
|
log_tail_offset = 0;
|
|
|
|
ESP_LOGI(TAG, "Erased; Tail/Head are now %lu/%lu",
|
|
(unsigned long)log_tail_offset, (unsigned long)log_head_offset);
|
|
}
|
|
len++; // account for type bit
|
|
|
|
/* Check each write — a partial failure here leaves a corrupt entry that
|
|
* the recovery scan has to skip past. On any failure, bump head to the
|
|
* next sector so the next write starts clean. */
|
|
esp_err_t we;
|
|
we = esp_partition_write(log_partition, log_head_offset, &len, 1);
|
|
if (we == ESP_OK) {
|
|
we = esp_partition_write(log_partition, log_head_offset+1, buf, len-1);
|
|
}
|
|
if (we == ESP_OK) {
|
|
we = esp_partition_write(log_partition, log_head_offset+len, &type, 1);
|
|
}
|
|
if (we != ESP_OK) {
|
|
ESP_LOGE(TAG, "log_write_blocking partial-write fail: %s",
|
|
esp_err_to_name(we));
|
|
log_head_offset = log_sector_end(log_head_offset);
|
|
if (log_head_offset >= log_partition->size) log_head_offset = 0;
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return we;
|
|
}
|
|
|
|
log_head_offset+=len+1;
|
|
ESP_LOGI(TAG, "Wrote; Tail/Head are now %lu/%lu",
|
|
(unsigned long)log_tail_offset, (unsigned long)log_head_offset);
|
|
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Log writer task
|
|
static void log_writer_task(void *pvParameters) {
|
|
log_queue_entry_t entry;
|
|
|
|
ESP_LOGI(TAG, "Log writer task started");
|
|
|
|
while (log_task_running) {
|
|
// Feed watchdog
|
|
//esp_task_wdt_reset();
|
|
// Wait for log entry (with timeout to check running flag)
|
|
if (xQueueReceive(log_queue, &entry, pdMS_TO_TICKS(100)) == pdTRUE) {
|
|
// Write the log entry (blocking)
|
|
esp_err_t err = log_write_blocking(entry.data, entry.len, entry.type);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to write log entry: %s", esp_err_to_name(err));
|
|
}
|
|
|
|
//esp_task_wdt_reset();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Log writer task stopping");
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
// Modified log_init to create queue and task
|
|
esp_err_t log_init() {
|
|
// Find the log partition
|
|
log_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA,
|
|
ESP_PARTITION_SUBTYPE_ANY,
|
|
"log");
|
|
if (log_partition == NULL) {
|
|
ESP_LOGE(TAG, "Log partition not found");
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
if (log_mutex == NULL) {
|
|
log_mutex = xSemaphoreCreateMutex();
|
|
if (log_mutex == NULL) return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
|
|
uint32_t num_sectors = log_partition->size / FLASH_SECTOR_SIZE;
|
|
|
|
ESP_LOGI(TAG, "Log init: scanning %lu sectors with bisection", (unsigned long)num_sectors);
|
|
|
|
// Default to empty log
|
|
log_head_offset = 0;
|
|
log_tail_offset = 0;
|
|
|
|
// Binary search for the first non-full sector
|
|
int32_t l = 0;
|
|
int32_t r = num_sectors - 1;
|
|
int32_t head_sector = 0;
|
|
int32_t tail_sector = 0;
|
|
|
|
while (l < r) {
|
|
int32_t m = (l + r) / 2;
|
|
if (is_sector_full(m)) {
|
|
l = m + 1;
|
|
} else {
|
|
r = m;
|
|
}
|
|
}
|
|
|
|
int32_t first_non_full = l;
|
|
|
|
// Determine head based on what the first non-full sector is
|
|
if (first_non_full >= num_sectors) {
|
|
// All sectors are full (wraparound case)
|
|
head_sector = 0;
|
|
} else if (is_sector_erased(first_non_full)) {
|
|
// First non-full is erased -> head is this erased sector
|
|
head_sector = first_non_full;
|
|
} else {
|
|
// First non-full is partial -> head is this partial sector
|
|
head_sector = first_non_full;
|
|
}
|
|
|
|
// Binary search for tail: first full sector after head (with wraparound)
|
|
// Search from head+1 to head+num_sectors-1 (wrapping around)
|
|
l = 1;
|
|
r = num_sectors - 1;
|
|
|
|
while (l <= r) {
|
|
int32_t m = (l + r) / 2;
|
|
int32_t sector = (head_sector + m) % num_sectors;
|
|
|
|
if (is_sector_full(sector)) {
|
|
// Found a full sector, but need the FIRST one
|
|
tail_sector = sector;
|
|
r = m - 1;
|
|
} else {
|
|
l = m + 1;
|
|
}
|
|
}
|
|
|
|
if (head_sector == -1) {
|
|
if (is_sector_erased(0)) {
|
|
ESP_LOGI(TAG, "Log is empty");
|
|
log_head_offset = 0;
|
|
log_tail_offset = 0;
|
|
} else {
|
|
head_sector = 0;
|
|
ESP_LOGW(TAG, "Log appears full, searching from start");
|
|
}
|
|
}
|
|
|
|
// Walk the data structure to find exact head
|
|
uint32_t cursor;
|
|
if (head_sector > 0) {
|
|
cursor = (head_sector - 1) * FLASH_SECTOR_SIZE;
|
|
} else {
|
|
cursor = 0;
|
|
}
|
|
|
|
uint32_t head_sector_start = head_sector * FLASH_SECTOR_SIZE;
|
|
|
|
bool found_head = false;
|
|
while (cursor < head_sector_start + FLASH_SECTOR_SIZE && cursor < log_partition->size) {
|
|
uint8_t buf;
|
|
esp_err_t err = esp_partition_read(log_partition, cursor, &buf, 1);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to read during log init at offset %lu", (unsigned long)cursor);
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return err;
|
|
}
|
|
|
|
if (buf == 0xFF) {
|
|
log_head_offset = cursor;
|
|
found_head = true;
|
|
break;
|
|
} else if (buf == 0x00) {
|
|
cursor = log_sector_end(cursor);
|
|
} else {
|
|
cursor += buf + 1;
|
|
}
|
|
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
if (!found_head) {
|
|
log_head_offset = cursor;
|
|
}
|
|
|
|
if (tail_sector >= 0) {
|
|
log_tail_offset = tail_sector * FLASH_SECTOR_SIZE;
|
|
} else {
|
|
log_tail_offset = 0;
|
|
}
|
|
|
|
log_initialized = true;
|
|
ESP_LOGI(TAG, "Log system initialized (bisection). Head: %lu, Tail: %lu",
|
|
(unsigned long)log_head_offset, (unsigned long)log_tail_offset);
|
|
|
|
// Create the log queue
|
|
log_queue = xQueueCreate(LOG_QUEUE_SIZE, sizeof(log_queue_entry_t));
|
|
if (log_queue == NULL) {
|
|
ESP_LOGE(TAG, "Failed to create log queue");
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// Create the log writer task
|
|
log_task_running = true;
|
|
BaseType_t task_created = xTaskCreate(
|
|
log_writer_task,
|
|
"log_writer",
|
|
LOG_TASK_STACK_SIZE,
|
|
NULL,
|
|
LOG_TASK_PRIORITY,
|
|
&log_task_handle
|
|
);
|
|
|
|
if (task_created != pdPASS) {
|
|
ESP_LOGE(TAG, "Failed to create log writer task");
|
|
vQueueDelete(log_queue);
|
|
log_queue = NULL;
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_ERR_NO_MEM;
|
|
}
|
|
|
|
// Add the task to watchdog
|
|
//if (esp_task_wdt_add(log_task_handle) != ESP_OK) {
|
|
// ESP_LOGW(TAG, "Log task already subscribed to watchdog or failed to add");
|
|
//}
|
|
|
|
ESP_LOGI(TAG, "Log writer task created successfully");
|
|
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_OK;
|
|
}
|
|
|
|
// Update storage_deinit to stop the task
|
|
void storage_deinit(void) {
|
|
// Stop the log writer task
|
|
if (log_task_running) {
|
|
log_task_running = false;
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
|
|
if (log_task_handle != NULL) {
|
|
// Don't try to delete from watchdog - task was never added
|
|
// esp_task_wdt_delete(log_task_handle); // <-- REMOVE THIS LINE
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
log_task_handle = NULL;
|
|
}
|
|
}
|
|
|
|
// Delete the queue
|
|
if (log_queue != NULL) {
|
|
vQueueDelete(log_queue);
|
|
log_queue = NULL;
|
|
}
|
|
|
|
params_partition = NULL;
|
|
log_partition = NULL;
|
|
post_partition = NULL;
|
|
log_initialized = false;
|
|
if (log_mutex) {
|
|
vSemaphoreDelete(log_mutex);
|
|
log_mutex = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
// LOG READ / TEST SUPPORT FUNCTIONS
|
|
// ============================================================================
|
|
|
|
static uint32_t log_read_cursor = 0;
|
|
|
|
/**
|
|
* @brief Read a log entry from the current read cursor position
|
|
*
|
|
* Reads a log entry from flash starting at the current read cursor and advances
|
|
* the cursor to the next entry. The read cursor is independent of tail - reading
|
|
* does NOT affect the tail pointer (which marks the boundary of valid data).
|
|
*
|
|
* The tail is only moved by the log write system when sectors need to be erased
|
|
* during wraparound. Reading is completely non-destructive.
|
|
*
|
|
* This is a test/debug function - the production logging system is write-only.
|
|
*
|
|
* Log entry format in flash:
|
|
* [1 byte: length] [length-1 bytes: data] [1 byte: type]
|
|
*
|
|
* @param len Pointer to store the entry length (data length, not including type byte)
|
|
* @param buf Buffer to store the entry data (must be at least LOG_MAX_PAYLOAD bytes)
|
|
* @param type Pointer to store the entry type
|
|
* @return ESP_OK on success, ESP_ERR_NOT_FOUND if no more entries, error code otherwise
|
|
*/
|
|
esp_err_t log_read(uint8_t* len, uint8_t* buf, uint8_t* type) {
|
|
// NOTE: log_read_cursor must be declared as a file-scope static variable
|
|
// Add this declaration near the other log static variables in storage.c:
|
|
// static uint32_t log_read_cursor = 0;
|
|
|
|
if (!log_initialized || log_partition == NULL) {
|
|
ESP_LOGE(TAG, "Logging not initialized");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
|
|
if (len == NULL || buf == NULL || type == NULL) {
|
|
ESP_LOGE(TAG, "NULL pointer passed to log_read");
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
|
|
// Initialize read cursor to tail on first read (when cursor is 0)
|
|
if (log_read_cursor == 0) {
|
|
log_read_cursor = log_tail_offset;
|
|
}
|
|
|
|
// Check if we've caught up to head (no more entries to read)
|
|
if (log_read_cursor == log_head_offset) {
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
// Read the length byte
|
|
uint8_t entry_len;
|
|
esp_err_t err = esp_partition_read(log_partition, log_read_cursor, &entry_len, 1);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to read entry length at offset %lu", (unsigned long)log_read_cursor);
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return err;
|
|
}
|
|
|
|
// Check for erased flash (0xFF) - means we've reached the end
|
|
if (entry_len == 0xFF) {
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_ERR_NOT_FOUND;
|
|
}
|
|
|
|
// Check for sector padding (0x00) - skip to next sector
|
|
if (entry_len == 0x00) {
|
|
// Move to next sector
|
|
uint32_t next_sector = ((log_read_cursor / FLASH_SECTOR_SIZE) + 1) * FLASH_SECTOR_SIZE;
|
|
|
|
// Handle wraparound
|
|
if (next_sector >= log_partition->size) {
|
|
log_read_cursor = 0;
|
|
} else {
|
|
log_read_cursor = next_sector;
|
|
}
|
|
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return log_read(len, buf, type); // Recursive call to read from next sector
|
|
}
|
|
|
|
// Validate length
|
|
if (entry_len > LOG_MAX_PAYLOAD + 1) { // +1 for type byte included in length
|
|
ESP_LOGE(TAG, "Invalid entry length %d at offset %lu", entry_len, (unsigned long)log_read_cursor);
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_ERR_INVALID_SIZE;
|
|
}
|
|
|
|
// Read the data (length-1 bytes, since length includes the type byte)
|
|
uint8_t data_len = entry_len - 1;
|
|
err = esp_partition_read(log_partition, log_read_cursor + 1, buf, data_len);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to read entry data at offset %lu", (unsigned long)(log_read_cursor + 1));
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return err;
|
|
}
|
|
|
|
// Read the type byte
|
|
uint8_t entry_type;
|
|
err = esp_partition_read(log_partition, log_read_cursor + entry_len, &entry_type, 1);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to read entry type at offset %lu", (unsigned long)(log_read_cursor + entry_len));
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return err;
|
|
}
|
|
|
|
// Validate type
|
|
if (!IS_VALID_LOG_TYPE(entry_type)) {
|
|
ESP_LOGW(TAG, "Invalid log type 0x%02X at offset %lu", entry_type, (unsigned long)(log_read_cursor + entry_len));
|
|
// Continue anyway - might be valid data with unknown type
|
|
}
|
|
|
|
// Set output parameters
|
|
*len = data_len;
|
|
*type = entry_type;
|
|
|
|
// Advance read cursor (NOT tail - tail is only moved by writes during wraparound)
|
|
log_read_cursor += entry_len + 1; // +1 for the type byte after data
|
|
|
|
// Handle wraparound
|
|
if (log_read_cursor >= log_partition->size) {
|
|
log_read_cursor = 0;
|
|
}
|
|
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Reset the log read cursor to start reading from the beginning
|
|
*
|
|
* Resets the internal read cursor so that the next call to log_read()
|
|
* will start reading from the tail (oldest valid entry).
|
|
* This is useful for tests that need to re-read the log.
|
|
*
|
|
* IMPORTANT: This does NOT affect the tail pointer. The tail marks the
|
|
* boundary of valid data and is only moved by the write system.
|
|
*/
|
|
void log_read_reset(void) {
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
log_read_cursor = 0; // Reset to 0 so next read starts from tail
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
}
|
|
|
|
/**
|
|
* @brief Erase all sectors in the log area
|
|
*
|
|
* This function erases the entire log area of the flash partition,
|
|
* resetting it to a clean state. Useful for testing.
|
|
*
|
|
* @return ESP_OK on success, error code otherwise
|
|
*/
|
|
esp_err_t log_erase_all_sectors(void) {
|
|
if (log_partition == NULL) {
|
|
ESP_LOGE(TAG, "Log partition not initialized");
|
|
return ESP_ERR_INVALID_STATE;
|
|
}
|
|
// Stop the log writer task
|
|
if (log_task_running) {
|
|
log_task_running = false;
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
|
|
if (log_task_handle != NULL) {
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
log_task_handle = NULL;
|
|
}
|
|
}
|
|
|
|
// Clear the queue
|
|
if (log_queue != NULL) {
|
|
xQueueReset(log_queue);
|
|
}
|
|
|
|
if (log_mutex) xSemaphoreTake(log_mutex, portMAX_DELAY);
|
|
|
|
ESP_LOGI(TAG, "Erasing all log sectors (%lu bytes)...", (unsigned long)log_partition->size);
|
|
|
|
esp_err_t err = esp_partition_erase_range(log_partition, 0, log_partition->size);
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to erase log area: %s", esp_err_to_name(err));
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
return err;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "All log sectors erased successfully");
|
|
|
|
if (log_mutex) xSemaphoreGive(log_mutex);
|
|
|
|
// Reinitialize
|
|
log_initialized = false;
|
|
log_read_cursor = 0;
|
|
|
|
err = log_init();
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to reinitialize log after erase");
|
|
return err;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
/**
|
|
* @brief Simulate a power cycle by re-running log initialization
|
|
*
|
|
* This function simulates what happens when the device powers off and back on:
|
|
* - Clears the in-memory head/tail tracking (simulated by marking as uninitialized)
|
|
* - Re-runs log_init() to scan flash and recover head/tail positions
|
|
*
|
|
* This is crucial for testing that the log system can correctly recover its
|
|
* state from flash after a power loss.
|
|
*
|
|
* @return ESP_OK on success, error code otherwise
|
|
*/
|
|
esp_err_t log_simulate_power_cycle(void) {
|
|
ESP_LOGI(TAG, "Simulating power cycle...");
|
|
// Stop the log writer task
|
|
if (log_task_running) {
|
|
log_task_running = false;
|
|
vTaskDelay(pdMS_TO_TICKS(200));
|
|
|
|
if (log_task_handle != NULL) {
|
|
// Don't try to delete from watchdog - task was never added
|
|
// esp_task_wdt_delete(log_task_handle); // <-- REMOVE THIS LINE
|
|
vTaskDelay(pdMS_TO_TICKS(100));
|
|
log_task_handle = NULL;
|
|
}
|
|
}
|
|
|
|
// Clear the queue
|
|
if (log_queue != NULL) {
|
|
xQueueReset(log_queue);
|
|
}
|
|
|
|
// Mark log as uninitialized (simulates losing RAM state)
|
|
log_initialized = false;
|
|
|
|
// Reset read cursor (simulates losing RAM state)
|
|
log_read_cursor = 0;
|
|
|
|
// Re-run log initialization to scan flash and recover state
|
|
esp_err_t err = log_init();
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to reinitialize log after simulated power cycle");
|
|
return err;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Power cycle simulation complete. Head=%lu, Tail=%lu",
|
|
(unsigned long)log_head_offset, (unsigned long)log_tail_offset);
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t log_write_blocking_test(const uint8_t* buf, uint8_t len, uint8_t type) {
|
|
// Check queue space - wait if nearly full
|
|
while (uxQueueSpacesAvailable(log_queue) < 2) {
|
|
vTaskDelay(pdMS_TO_TICKS(10));
|
|
esp_task_wdt_reset();
|
|
}
|
|
|
|
return log_write(buf, len, type);
|
|
} |