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:
2026-04-23 13:06:38 -07:00
parent a37207b6ff
commit 9232766e60
3 changed files with 204 additions and 0 deletions

View 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;
}

View 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