fix(firmware): event_log thread safety and NVS wear
- Remove monotonic counter writes to NVS (stop burning flash on every event). Derive head and cnt by scanning slots on boot. - Widen seq to uint32 so slot scan works across multi-year lifetimes. - Add FreeRTOS mutex around write/read so WiFi event handlers can safely call event_log_write from another task. - Check Preferences.begin() return; disable logging if NVS unavailable. - Extract NTP_SYNC_THRESHOLD constant; drop misleading native uptime. - Add tests for empty read, max_entries truncation, real-path hash.
This commit is contained in:
@@ -7,10 +7,15 @@
|
||||
#include <Arduino.h>
|
||||
#include <Preferences.h>
|
||||
#include <time.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
static Preferences s_prefs;
|
||||
static const char* NVS_NS = "evlog";
|
||||
static const char* NVS_HEAD = "head"; // next write slot (0..31)
|
||||
static const char* NVS_CNT = "cnt"; // total writes (for seq)
|
||||
static bool s_ok = false;
|
||||
static SemaphoreHandle_t s_mutex = nullptr;
|
||||
static uint32_t g_head = 0; // next write slot (0..31), RAM-only
|
||||
static uint32_t g_cnt = 0; // total writes since boot scan, RAM-only
|
||||
static constexpr time_t NTP_SYNC_THRESHOLD = 1700000000; // 2023-11-14
|
||||
#else
|
||||
// Native build: in-memory stub
|
||||
#include <cstdint>
|
||||
@@ -56,48 +61,78 @@ static bool slot_read(size_t idx, EventLogEntry& e) {
|
||||
|
||||
void event_log_init() {
|
||||
#ifdef ARDUINO
|
||||
s_prefs.begin(NVS_NS, /*readOnly=*/false);
|
||||
if (s_mutex == nullptr) {
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
}
|
||||
s_ok = s_prefs.begin(NVS_NS, /*readOnly=*/false);
|
||||
if (!s_ok) {
|
||||
Serial.println("[evlog] NVS begin failed");
|
||||
return;
|
||||
}
|
||||
// Scan all 32 slots; locate the one with the largest seq.
|
||||
// Empty log: every slot tag == 0 (not a valid EventLogTag, which starts at 1).
|
||||
uint32_t max_seq = 0;
|
||||
int max_idx = -1;
|
||||
bool any_valid = false;
|
||||
for (size_t i = 0; i < SLOTS; i++) {
|
||||
EventLogEntry e = {};
|
||||
if (!slot_read(i, e)) continue;
|
||||
if (e.tag == 0) continue;
|
||||
any_valid = true;
|
||||
if (max_idx < 0 || e.seq >= max_seq) {
|
||||
max_seq = e.seq;
|
||||
max_idx = (int)i;
|
||||
}
|
||||
}
|
||||
if (any_valid) {
|
||||
g_head = (uint32_t)((max_idx + 1) % SLOTS);
|
||||
g_cnt = max_seq + 1;
|
||||
} else {
|
||||
g_head = 0;
|
||||
g_cnt = 0;
|
||||
}
|
||||
#else
|
||||
// nothing
|
||||
#endif
|
||||
}
|
||||
|
||||
void event_log_write(EventLogTag tag, uint16_t data0, uint16_t data1) {
|
||||
EventLogEntry e = {};
|
||||
#ifdef ARDUINO
|
||||
if (!s_ok) return;
|
||||
if (s_mutex) xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||
EventLogEntry e = {};
|
||||
time_t now = time(nullptr);
|
||||
e.ts_unix = (now > 1700000000) ? (uint32_t)now : 0;
|
||||
e.ts_unix = (now > NTP_SYNC_THRESHOLD) ? (uint32_t)now : 0;
|
||||
e.uptime_s = (uint32_t)(millis() / 1000);
|
||||
uint32_t head = s_prefs.getUInt(NVS_HEAD, 0);
|
||||
uint32_t cnt = s_prefs.getUInt(NVS_CNT, 0);
|
||||
#else
|
||||
e.ts_unix = 0;
|
||||
e.uptime_s = g_cnt; // stand-in for uptime in native tests
|
||||
uint32_t head = g_head;
|
||||
uint32_t cnt = g_cnt;
|
||||
#endif
|
||||
e.tag = (uint8_t)tag;
|
||||
e.data0 = data0;
|
||||
e.data1 = data1;
|
||||
e.seq = (uint8_t)(cnt & 0xFF);
|
||||
slot_write(head % SLOTS, e);
|
||||
#ifdef ARDUINO
|
||||
s_prefs.putUInt(NVS_HEAD, (head + 1) % SLOTS);
|
||||
s_prefs.putUInt(NVS_CNT, cnt + 1);
|
||||
e.seq = g_cnt;
|
||||
slot_write(g_head % SLOTS, e);
|
||||
g_head = (g_head + 1) % SLOTS;
|
||||
g_cnt = g_cnt + 1;
|
||||
if (s_mutex) xSemaphoreGive(s_mutex);
|
||||
#else
|
||||
g_head = (head + 1) % SLOTS;
|
||||
g_cnt = cnt + 1;
|
||||
EventLogEntry e = {};
|
||||
e.ts_unix = 0;
|
||||
e.uptime_s = 0;
|
||||
e.tag = (uint8_t)tag;
|
||||
e.data0 = data0;
|
||||
e.data1 = data1;
|
||||
e.seq = g_cnt;
|
||||
slot_write(g_head % SLOTS, e);
|
||||
g_head = (g_head + 1) % SLOTS;
|
||||
g_cnt = g_cnt + 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
size_t event_log_read_recent(EventLogEntry* out, size_t max_entries) {
|
||||
#ifdef ARDUINO
|
||||
uint32_t head = s_prefs.getUInt(NVS_HEAD, 0);
|
||||
uint32_t cnt = s_prefs.getUInt(NVS_CNT, 0);
|
||||
#else
|
||||
if (!s_ok) return 0;
|
||||
if (s_mutex) xSemaphoreTake(s_mutex, portMAX_DELAY);
|
||||
#endif
|
||||
uint32_t head = g_head;
|
||||
uint32_t cnt = g_cnt;
|
||||
#endif
|
||||
size_t available = (cnt < SLOTS) ? (size_t)cnt : SLOTS;
|
||||
size_t n = (max_entries < available) ? max_entries : available;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
@@ -105,5 +140,8 @@ size_t event_log_read_recent(EventLogEntry* out, size_t max_entries) {
|
||||
size_t idx = (head + SLOTS - 1 - i) % SLOTS;
|
||||
slot_read(idx, out[i]);
|
||||
}
|
||||
#ifdef ARDUINO
|
||||
if (s_mutex) xSemaphoreGive(s_mutex);
|
||||
#endif
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -27,8 +27,8 @@ struct EventLogEntry {
|
||||
uint16_t data0;
|
||||
uint16_t data1;
|
||||
uint8_t tag; // EventLogTag
|
||||
uint8_t seq; // rolling sequence, wraps
|
||||
uint8_t _pad[18]; // pad to 32 bytes for fixed slot size
|
||||
uint32_t seq; // widened; survives multi-year event rates
|
||||
uint8_t _pad[15]; // pad to 32 bytes for fixed slot size
|
||||
} __attribute__((packed));
|
||||
static_assert(sizeof(EventLogEntry) == 32, "EventLogEntry must be 32 bytes");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user