feat(firmware): add NVS-backed event log ring buffer
Persistent 32-slot ring buffer of tagged diagnostic events (boot, wifi up/down, http ok/fail, heartbeat miss, reboot). Used to diagnose field failures post-hoc via the heartbeat payload, without needing serial access. Native-native stub lets policy be unit-tested.
This commit is contained in:
109
firmware/lib/event_log/event_log.cpp
Normal file
109
firmware/lib/event_log/event_log.cpp
Normal file
@@ -0,0 +1,109 @@
|
||||
// firmware/lib/event_log/event_log.cpp
|
||||
#include "event_log.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef ARDUINO
|
||||
#include <Arduino.h>
|
||||
#include <Preferences.h>
|
||||
#include <time.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)
|
||||
#else
|
||||
// Native build: in-memory stub
|
||||
#include <cstdint>
|
||||
static uint8_t g_slots[32 * 32];
|
||||
static uint32_t g_head = 0;
|
||||
static uint32_t g_cnt = 0;
|
||||
extern "C" void event_log_test_reset() {
|
||||
memset(g_slots, 0, sizeof(g_slots));
|
||||
g_head = 0;
|
||||
g_cnt = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
static const size_t SLOTS = 32;
|
||||
static const size_t SLOT_SIZE = sizeof(EventLogEntry);
|
||||
|
||||
uint16_t event_log_path_hash(const char* path) {
|
||||
// fnv1a-16 (fold 32-bit fnv1a down to 16 bits)
|
||||
uint32_t h = 0x811c9dc5u;
|
||||
while (*path) { h ^= (uint8_t)*path++; h *= 0x01000193u; }
|
||||
return (uint16_t)((h >> 16) ^ (h & 0xFFFF));
|
||||
}
|
||||
|
||||
static void slot_write(size_t idx, const EventLogEntry& e) {
|
||||
#ifdef ARDUINO
|
||||
char key[8]; snprintf(key, sizeof(key), "s%u", (unsigned)idx);
|
||||
s_prefs.putBytes(key, &e, SLOT_SIZE);
|
||||
#else
|
||||
memcpy(&g_slots[idx * SLOT_SIZE], &e, SLOT_SIZE);
|
||||
#endif
|
||||
}
|
||||
|
||||
static bool slot_read(size_t idx, EventLogEntry& e) {
|
||||
#ifdef ARDUINO
|
||||
char key[8]; snprintf(key, sizeof(key), "s%u", (unsigned)idx);
|
||||
size_t n = s_prefs.getBytes(key, &e, SLOT_SIZE);
|
||||
return n == SLOT_SIZE;
|
||||
#else
|
||||
memcpy(&e, &g_slots[idx * SLOT_SIZE], SLOT_SIZE);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
void event_log_init() {
|
||||
#ifdef ARDUINO
|
||||
s_prefs.begin(NVS_NS, /*readOnly=*/false);
|
||||
#else
|
||||
// nothing
|
||||
#endif
|
||||
}
|
||||
|
||||
void event_log_write(EventLogTag tag, uint16_t data0, uint16_t data1) {
|
||||
EventLogEntry e = {};
|
||||
#ifdef ARDUINO
|
||||
time_t now = time(nullptr);
|
||||
e.ts_unix = (now > 1700000000) ? (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);
|
||||
#else
|
||||
g_head = (head + 1) % SLOTS;
|
||||
g_cnt = 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
|
||||
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++) {
|
||||
// newest is at (head - 1), then (head - 2), ... modulo SLOTS
|
||||
size_t idx = (head + SLOTS - 1 - i) % SLOTS;
|
||||
slot_read(idx, out[i]);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
39
firmware/lib/event_log/event_log.h
Normal file
39
firmware/lib/event_log/event_log.h
Normal file
@@ -0,0 +1,39 @@
|
||||
// firmware/lib/event_log/event_log.h
|
||||
#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
enum EventLogTag : uint8_t {
|
||||
EVT_BOOT = 1, // data0 = esp_reset_reason() value
|
||||
EVT_WIFI_UP = 2, // data0 = rssi (signed, cast)
|
||||
EVT_WIFI_DOWN = 3, // data0 = disconnect reason code
|
||||
EVT_HTTP_OK = 4, // data0 = path hash (fnv1a16), data1 = elapsed_ms
|
||||
EVT_HTTP_FAIL = 5, // data0 = path hash, data1 = (http_code or negative errno)
|
||||
EVT_HEARTBEAT_MISS = 6, // data0 = consecutive miss count
|
||||
EVT_NTP_SYNC = 7, // data0 = seconds since boot
|
||||
EVT_REBOOT = 8, // data0 = reason enum (defined below)
|
||||
};
|
||||
|
||||
enum RebootReason : uint8_t {
|
||||
REBOOT_HEARTBEAT_MISS = 1,
|
||||
REBOOT_FACTORY_RESET = 2,
|
||||
REBOOT_OTA = 3,
|
||||
REBOOT_WIFI_REPROV = 4,
|
||||
};
|
||||
|
||||
struct EventLogEntry {
|
||||
uint32_t ts_unix; // 0 if NTP not synced yet; fall back to millis/1000
|
||||
uint32_t uptime_s; // millis()/1000 at log time
|
||||
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
|
||||
} __attribute__((packed));
|
||||
static_assert(sizeof(EventLogEntry) == 32, "EventLogEntry must be 32 bytes");
|
||||
|
||||
// NVS-backed 32-slot ring buffer. Safe to call before NTP sync.
|
||||
void event_log_init();
|
||||
void event_log_write(EventLogTag tag, uint16_t data0 = 0, uint16_t data1 = 0);
|
||||
size_t event_log_read_recent(EventLogEntry* out, size_t max_entries);
|
||||
uint16_t event_log_path_hash(const char* path); // fnv1a16 — exposed for tests
|
||||
Reference in New Issue
Block a user