mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-21 22:22:33 -05:00
* feat(firmware): add the 16MB flash profile the C6 fleet runs on The 4MB and display layouts have 1.875MB and 2MB OTA slots, sized for 4MB and 8MB parts. A 16MB C6 (DevKitC-1 N16) has no layout in the tree at all, so building for one meant hand-editing sdkconfig -- and a smaller table written over a deployed 16MB node relocates NVS and destroys its provisioning, which is why this belongs in the repo rather than in a workstation's untracked sdkconfig. Two 4MB OTA slots, plus a 64K coredump partition and an 8000K FAT volume for on-node storage. Rollback is enabled so an OTA'd image that cannot confirm itself is reverted by the bootloader. Layered, not inherited: sdkconfig.defaults.esp32c6 keeps its 4MB layout so the common 4MB devkit is never silently repartitioned. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ * feat(firmware): persistent on-node log and coredump-to-flash A remote fault currently leaves no post-mortem. main.c decodes the reset reason at boot and logs it to the CONSOLE -- which is exactly what a power cycle destroys, and a power cycle is how a wedged node gets recovered. The flash was already provisioned for this and the features were simply off: partitions_16mb.csv has reserved a 64K `coredump` partition and an 8000K `storage` FAT volume since the 16MB layout was written, neither had ever been mounted or written, and the build defaulted to CONFIG_ESP_COREDUMP_ENABLE_TO_NONE. - node_log.c/h: a 128 B-record ring on the storage volume. Boot records (reset reason + the previous session's final uptime + a health snapshot), periodic health samples, and event records. Ordering is boot_id + uptime_s because these boards have no RTC and nothing can be timestamped absolutely. Counters are stored RAW and differenced at read time, so a cadence change never invalidates old records. Retention, not wear, binds: 128 B every 300 s is ~208 days in 8 MB, and the write rate is bounded explicitly. - ap_rssi_dbm, the association link from esp_wifi_sta_get_ap_info. The adaptive controller's rssi_median_dbm is a CSI statistic, not the uplink, so a node with a bad uplink currently shows no signal for it anywhere. - CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH, complementary rather than a substitute: a coredump catches a CRASH, the boot record catches a HANG followed by a watchdog reset. node_log_init failing is non-fatal by design -- the module disables itself and every entry point becomes a no-op. A logging volume that will not mount must never take the fleet down with it. csi_collector gains two gate accessors. This build selects its frame gate at compile time, so both return CSI_GATE_NOT_CONFIGURABLE rather than a plausible number: the log keeps a field of fixed width and meaning either way, and an explicit "not applicable" is stored instead of something a reader would later reason from as a real setting. Built with ESP-IDF v5.4 for esp32c6 using defaults + .esp32c6 + .16mb. Verified from the resulting artifacts: 16MB table with coredump and storage present, COREDUMP_ENABLE_TO_FLASH set, rollback enabled, image 0x10abd0 bytes in a 4M slot. NOT flashed to hardware from this branch -- the equivalent code was exercised on a node-3 pilot, but that is not evidence for this build. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ * docs(adr-362): reading the on-node log is on-demand, never streamed The log engine lands with no consumer, so the access pattern was open. Records the decision to pull a node's log only when a human asks -- after a hang or a crash, or while troubleshooting that node -- and to keep nothing server-side. The reasoning that decided it: the ring already survives a power cycle on the node (~208 days of retention), so a server copy duplicates a durability property that is already met; and airtime is this fleet's binding constraint, measured at 91% consumed by two slow nodes, so a periodic drain across nine boards would spend exactly the resource whose exhaustion you would open the log to investigate. An instrument must not perturb what it measures. Leaves the decode-where question open on purpose, with the argument for a raw proxy stated: decoding server-side defines the record layout in both C and Rust, and the pilot already paid once for a host/firmware disagreement about a field's meaning. Co-Authored-By: claude-flow <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ --------- Co-authored-by: Joe <joe@GitDev.hedgehog> Co-authored-by: claude-flow <ruv@ruv.net>
115 lines
4.5 KiB
C
115 lines
4.5 KiB
C
/* Persistent on-node log -- survives a power cycle, readable without a cable.
|
|
*
|
|
* The problem this exists for (Joe, 2026-09-07): "a hung node ... doesnt do us
|
|
* any good for troubleshooting remotly because the unplug and mount results in
|
|
* a clearing of the condtion." Today the post-mortem for any remote fault is
|
|
* nothing: main.c decodes the reset reason at boot and logs it to the CONSOLE,
|
|
* which is exactly what a power cycle destroys.
|
|
*
|
|
* The flash was already partitioned for this and the features were off --
|
|
* partitions_16mb.csv reserves a 64K coredump partition and an 8000K storage
|
|
* FAT volume, neither of which had ever been mounted or written.
|
|
*
|
|
* # Retention, not wear, is the binding constraint
|
|
*
|
|
* 128 B records every 300 s fills 8 MB in ~222 days. Endurance at that rate is
|
|
* ~61,000 years, so the ring wraps 36 million times before the flash is at
|
|
* risk. The write rate is nonetheless bounded explicitly below, because flash
|
|
* wear is the one way this module can do harm.
|
|
*
|
|
* # Why uptime and boot_id rather than a timestamp
|
|
*
|
|
* These boards have no RTC. Nothing can be timestamped absolutely, so ordering
|
|
* comes from boot_id (incremented once per boot, persisted in NVS) plus
|
|
* uptime_s within that boot. That ordering survives a power cycle; a wall clock
|
|
* would not.
|
|
*
|
|
* # Counters are stored RAW
|
|
*
|
|
* Every counter here is cumulative and is written as-is. Differencing happens
|
|
* at read time. Storing rates instead would bake in the sampling interval and
|
|
* make every record useless the moment the cadence changed.
|
|
*/
|
|
#pragma once
|
|
|
|
#include <stdbool.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
|
|
#include "esp_err.h"
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
/** Every record is exactly this size, so the ring is index-addressable and a
|
|
* reader never has to parse forward to find a boundary. */
|
|
#define NODE_LOG_RECORD_SIZE 128
|
|
|
|
/** Record types. A reader dispatches on this byte alone. */
|
|
#define NODE_LOG_TYPE_PERIODIC 1
|
|
#define NODE_LOG_TYPE_BOOT 2
|
|
#define NODE_LOG_TYPE_EVENT 3
|
|
|
|
/** Event subtypes. Periodic samples give the trend; these give the moment. */
|
|
#define NODE_LOG_EV_WIFI_DISCONNECT 1
|
|
#define NODE_LOG_EV_WIFI_RECONNECT 2
|
|
#define NODE_LOG_EV_THERMAL_STATE 3
|
|
#define NODE_LOG_EV_SEND_FAIL_RATE 4
|
|
#define NODE_LOG_EV_WATCHDOG 5
|
|
|
|
/** Mount the FAT volume and open the ring. Call once at boot, after NVS is up
|
|
* (the boot counter lives there).
|
|
*
|
|
* Failure is NOT fatal: on any error this module disables itself and every
|
|
* other entry point becomes a no-op. A logging volume that will not mount must
|
|
* never take the fleet down with it. */
|
|
esp_err_t node_log_init(void);
|
|
|
|
/** True when the ring mounted and writes are being accepted. */
|
|
bool node_log_is_active(void);
|
|
|
|
/** Append the boot record: reset reason, the previous session's final uptime,
|
|
* and a health snapshot.
|
|
*
|
|
* The highest-value record here. It catches a HANG FOLLOWED BY A WATCHDOG
|
|
* RESET, which is the failure that bites this fleet and the one a coredump
|
|
* does not capture. */
|
|
void node_log_boot(uint32_t reset_reason, uint32_t prev_uptime_s);
|
|
|
|
/** Append a periodic health sample. Rate-limited internally, so a caller cannot
|
|
* wear the flash by calling it in a tight loop. */
|
|
void node_log_periodic(void);
|
|
|
|
/** Append an event record. a/b carry subtype-specific detail -- for a WiFi
|
|
* disconnect they are the reason code and the RSSI at that moment, the pair
|
|
* main.c currently logs only to the console. */
|
|
void node_log_event(uint8_t subtype, int32_t a, int32_t b);
|
|
|
|
/** Record a WiFi disconnect for the health snapshot's cumulative counters.
|
|
* Kept as a setter so this module has no include cycle with main.c, which is
|
|
* where the reason and RSSI are actually known. */
|
|
void node_log_note_disconnect(uint8_t reason, int8_t rssi);
|
|
|
|
/** Copy raw records into out, newest-first, for the HTTP reader.
|
|
*
|
|
* @param out destination, at least max_records * NODE_LOG_RECORD_SIZE
|
|
* @param max_records maximum to copy
|
|
* @param skip how many of the newest to skip, so a caller can page
|
|
* @return number of records written into out */
|
|
size_t node_log_read(uint8_t *out, size_t max_records, size_t skip);
|
|
|
|
/** Total records currently held in the ring. */
|
|
size_t node_log_count(void);
|
|
|
|
/** Boot id of the current session. Records from the same boot share it. */
|
|
uint16_t node_log_boot_id(void);
|
|
|
|
/** Erase every record. Deliberately explicit and never called automatically --
|
|
* the whole point of the module is that state survives. */
|
|
esp_err_t node_log_clear(void);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|