Skip to content

feat(firmware): persistent on-node log and coredump-to-flash - #1878

Merged
ruvnet merged 4 commits into
ruvnet:mainfrom
clonea1:contrib/node-log-engine
Sep 9, 2026
Merged

ruvnet merged 4 commits into
ruvnet:mainfrom
clonea1:contrib/node-log-engine

Conversation

@clonea1

@clonea1 clonea1 commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Stacked on #1877 (the 16MB flash profile). #1877 should merge first — until it does, this PR's diff also shows those two data files, and the storage/coredump partitions this code mounts come from it.

Summary

A remote fault currently leaves no post-mortem. main.c decodes the reset reason at boot and logs it to the console — 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, 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. Ordered by boot_id + uptime_s because these boards have no RTC. Counters are stored raw and differenced at read time, so a cadence change never invalidates old records. Retention, not wear, is the binding constraint: 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 failing uplink currently surfaces that nowhere.
  • CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH — complementary, not a substitute: a coredump catches a crash; the boot record catches a hang followed by a watchdog reset.

Failure behaviour

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 a node down with it.

The two gate accessors

node_log's health record carries gate_mode/gate_seq_period. This tree selects its frame gate at compile time, so both accessors return CSI_GATE_NOT_CONFIGURABLE rather than a plausible number. The record keeps a field of fixed width and meaning either way, and an explicit "not applicable" is stored instead of a value a reader would later reason from as a real setting. If runtime gate selection lands later, these get real implementations with no change to the record format.

Not included here

The GET /log and GET /coredump HTTP endpoints (and the host-side decoder) depend on the config API from #1827 and will follow once that merges. Until then the log is written but only readable over USB.

Test plan

  • ESP-IDF v5.4 container build for esp32c6, defaults + .esp32c6 + .16mb, 1434/1434
  • Verified from build artifacts, not from the command line: partitions_16mb.csv in force, coredump + storage present in the generated table, COREDUMP_ENABLE_TO_FLASH=y, WL_SECTOR_SIZE_4096=y, rollback enabled, image 0x10abd0 in a 4M slot (74% free)
  • Not flashed to hardware from this branch. Equivalent code was exercised on a node-3 pilot (records surviving reboot, boot_id/seq continuity across a boundary), but that is not evidence for this build and is not claimed as such.

🤖 Generated with claude-flow

https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ

Joe and others added 4 commits September 9, 2026 10:35
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
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
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
# Conflicts:
#	firmware/esp32-csi-node/sdkconfig.defaults.16mb
@ruvnet
ruvnet merged commit 500cfe4 into ruvnet:main Sep 9, 2026
39 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants