Automated release from CI pipeline
Changes:
feat(firmware): persistent on-node log and coredump-to-flash (#1878)
- 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
Docker Image:
ghcr.io/ruvnet/RuView:500cfe47875705b47e08e6d79cd4b4066a70e445
详细ChangeLog
node-diagnostics-and-fusion-fixes-2026-09-09
2026年09月10日
RuView diagnostics, node memory, and fusion fixes — 2026-09-09
Five changes landed today, split across two themes: giving field nodes a memory that survives a power cycle, and tightening up two more places where sensor data was quietly wrong without anyone noticing.
What actually changes
A hung or crashed field node can finally tell you what happened to it.
Until now, if a node in the field wedged or rebooted unexpectedly, the only diagnostic evidence — a line printed to its serial console — was destroyed the instant it lost power, which is exactly when you'd want to read it. Nodes now keep a persistent, crash-surviving health log on their own flash storage: a compact rolling history (about 208 days' worth) of temperature, WiFi signal, frame rates, and disconnect events, plus a dedicated boot record that captures the one failure mode a crash dump can't — a hang followed by a watchdog reset. The write rate is deliberately capped so the log can never wear out the flash faster than the hardware could anyway last for centuries. Reading it is on-demand only: nothing is streamed or polled automatically, so the diagnostic tool doesn't itself eat the airtime it's trying to diagnose.
A new flash layout for boards with more room to work with.
Some field boards ship with 16MB of flash instead of the usual 4MB, but the repo had no ready-made configuration for that size — building for one meant hand-editing settings on a workstation, which is exactly the kind of untracked, easy-to-lose configuration this project tries to avoid. There's now a proper 16MB profile with two full-size update slots, room for a crash dump, and 8MB of on-board storage for the new health log above.
A carefully-run experiment that says "no" — and that's the useful result.
One theory for why sensors sometimes fail to pair their readings was that a node could be listening to a different WiFi access point than its neighbor, so their clocks never lined up. Forcing the whole fleet onto one shared access point was tested directly on live hardware, and it does fix the pairing rate — but the total number of usable paired readings per second actually drops, because far less data comes in overall. The experiment is documented in full, including the raw measurements, specifically so this shortcut doesn't get quietly reattempted later without knowing why it doesn't help.
A stray signal reading is no longer allowed to poison the room-wide average.
One sensor's signal-strength report was occasionally sending a placeholder value — something a phone's WiFi radio sends when it hasn't taken a real reading yet — and the system was treating it as if it were a genuine measurement. For one reading out of many, that placeholder would drag the whole room's reported signal strength down to an impossible number. It's now recognized as "not a real reading" and quietly ignored, the same way the system already ignores an outright missing value.
macOS can now do its own signal-strength sensing.
The Mac-based sensor path previously only worked on Windows. It's now been ported to macOS's native WiFi framework, with two things fixed along the way that only show up in real-world testing: the helper process is now killed automatically if it hangs instead of blocking the sensor indefinitely, and two different networks that both hide their identity are no longer mistaken for the same network just because they're on the same channel.
Thank you
Three contributions from clonea1 (persistent node health logging, the 16MB flash profile, and the single-access-point experiment write-up) turn "the node went quiet, we'll never know why" into an actual diagnostic trail — exactly the kind of unglamorous infrastructure work that pays for itself the first time a field node misbehaves.
Full changes:
- #1878 — persistent on-node log and coredump-to-flash (clonea1)
- #1877 — 16MB flash profile for the C6 fleet (clonea1)
- #1876 — single-transmitter pairing experiment, documented negative result (clonea1)
- #1882 — fix a firmware sentinel value from poisoning room-wide signal averages
- #1880 — fix node positions being dropped from the streaming fusion engine
详细ChangeLog
Automated release from CI pipeline
Changes:
feat(firmware): add the 16MB flash profile the C6 fleet runs on (#1877)
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.
Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ
Co-authored-by: Joe joe@GitDev.hedgehog
Co-authored-by: claude-flow ruv@ruv.net
Docker Image:
ghcr.io/ruvnet/RuView:f7c2c6587e64209c70c7635c17b6715e9fa07eef
详细ChangeLog
Automated release from CI pipeline
Changes:
docs(adr-361): single-transmitter pairing -- hypothesis confirmed, fix not adopted (#1876)
A-B-A capture from the overnight run, ten 30 s windows per arm: pairing
fraction rises 0.5668 -> 0.7287 (Welch t=13.26, p<0.0001) when the fleet is
filtered to one transmitter, but paired observations/s falls 44% because
tx/s falls 56%. Fraction was the wrong figure of merit; not adopted.
Firmware changes (persistent on-node log, live filter_mac, and the node-3
pilot fixes) that produced this capture are held back pending a port against
main's restructured firmware/main/ tree (config_api.c and ota_update.c are
gone there).
Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ
Co-authored-by: Joe joe@GitDev.hedgehog
Co-authored-by: claude-flow ruv@ruv.net
Docker Image:
ghcr.io/ruvnet/RuView:30391bfbd26746a4b5c3182e2753acdf930cd3c6
详细ChangeLog
Automated release from CI pipeline
Changes:
fix(sensing-server): don't let edge-vitals sentinel RSSI poison mean_rssi (#1882)
The ESP32 edge-vitals packet (0xC511_0002) carries a raw i8 RSSI byte with
no "valid" flag. When the edge pipeline hasn't sampled a real reading yet,
it has been observed sending near-zero sentinel values (-1, -2 dBm) that
are physically implausible for a WiFi link. These were pushed straight
into the node's rssi_history and the room's fused mean_rssi, producing a
one-tick reading of ~-2 dBm on node 3 while its own CSI path simultaneously
showed a real ~-50 dBm signal.
Add an is_plausible_rssi() guard (mirroring the existing
update_csi_fps_ema() plausibility-filter pattern) so an implausible RSSI
falls back to the node's last known-good reading instead of overwriting it.
Docker Image:
ghcr.io/ruvnet/RuView:00a42d7cda32cb742206593d8fb2f854db41a19e
详细ChangeLog
Automated release from CI pipeline
Changes:
Merge pull request #1879 from ruvnet/fix/hf-csi-embedding-metrics
fix(training): export the real contrastive lossHistory/duration, not a no-op baseline pass
Docker Image:
ghcr.io/ruvnet/RuView:203bd66daa726a9b23d13c4dc455ae17fb5ad52b
详细ChangeLog
Automated release from CI pipeline
Changes:
Merge pull request #1858 from clonea1/contrib/gate-experiment-findings
docs: measured comparison of three CSI frame-selection gates, and the harness
Docker Image:
ghcr.io/ruvnet/RuView:e5422d52e4d26c8d0812febebc321fe6c43cc055
详细ChangeLog
multistatic-timing-fixes-2026-09-09
2026年09月09日
RuView multistatic timing & positioning fixes — 2026-09-09
Nine community contributions landed today, all focused on one theme: getting multiple sensor nodes to actually agree on the same physical moment and the same physical location. That sounds basic, but it turns out to be the hardest part of making several WiFi sensors work together as one system instead of three independent guesses.
What actually changes
Nodes now measure their own speed correctly, even when data arrives in bursts.
The counter that tracks how fast a sensor is really sending data was quietly wrong whenever packets clumped together (which happens constantly over WiFi/UDP) — a burst of several readings in quick succession got counted as just one, so the system underestimated how fast a node was actually running. Fixed by measuring time gaps the right way around, so the reported speed now matches reality.
Multiple sensors can finally look at the same instant in time.
Each sensor used to decide "have 20 milliseconds passed since MY last reading?" independently — which meant two perfectly healthy sensors, both running at the right speed, could end up sampling completely different moments, with nothing in common to compare. The fix synchronizes that decision to a shared clock across the mesh, so sensors that are supposed to be looking at the same moment now actually are — a prerequisite for combining several sensors' views into one accurate picture.
A safety check that was supposed to catch bad timing... never actually could.
A guard rail meant to reject data if sensors were too out-of-sync had a math error: it was checking the wrong quantity, so it would never trigger even when it should have. Now fixed and verified with a real end-to-end test.
Room positions are now correctly remembered, even when a sensor briefly drops out.
This is a fix to something we shipped five days ago — thank you to the contributor who caught it. The original fix (assigning each sensor's configured position by "the Nth sensor currently online") had a subtle flaw: if one sensor went quiet, every sensor after it in the lineup would silently inherit the wrong position, since they'd all shift up one slot. Positions are now tied permanently to each sensor's actual ID, so a sensor going offline no longer scrambles everyone else's coordinates.
New optional antenna-selection support for a specific hardware variant (Seeed XIAO ESP32-C6), letting that board choose between its internal and external antenna — useful for controlled signal-strength comparisons. Fully opt-in; does nothing on other boards.
Documentation additions: a from-the-field operational runbook for flashing and running the ESP32-C6 firmware (the kind of guide written after getting each step wrong at least once), an architecture record explaining why device-free position estimation kept failing and what was learned from measuring it three different ways, and a rigorously honest write-up of a real nine-sensor experiment comparing three different timing strategies — including a reported prediction that turned out wrong and a mid-experiment methodology hiccup, disclosed rather than hidden.
Thank you to this round's contributors
- clonea1 — eight of today's nine contributions: the burst-timing fix, mesh-time alignment, the timing-guard math fix, the node-position correction (including catching a real bug in code merged five days ago), the antenna-selection groundwork's supporting docs, the operational runbook, the architecture record, and the measured timing-gate comparison.
- proffesor-for-testing (Dragan Spiridonov) — the XIAO ESP32-C6 antenna-selection feature.
Every change here was independently verified against the actual running code before merging — not just taken on the strength of its description.
What this doesn't include
Two related, still-open gaps surfaced during review, not fixed by this batch:
- The governed fusion path (the one that actually feeds provenance and trust tracking) still has its own separate instance of the position-tracking system that was never wired up to use configured positions at all — a different code path from the one fixed here. Tracked in #1870.
- A large, well-documented field report (#1804) raising real architecture questions about occupancy counting, still-person detection, and automatic gain control — partially answered, with several questions still needing the maintainer's own call on intended design.
Merged pull requests
| PR |
Title |
Contributor |
| #1855 |
chore: ignore host-test binaries by shape, not by a list that goes stale |
clonea1 |
| #1857 |
fix(server): recover the true CSI frame rate through bursty UDP delivery |
clonea1 |
| #1858 |
docs: measured comparison of three CSI frame-selection gates, and the harness |
clonea1 |
| #1859 |
feat(firmware): align the CSI rate gate to mesh time |
clonea1 |
| #1860 |
fix(sensing-server): key node positions by node id, not by list position |
clonea1 |
| #1861 |
docs(firmware): add the ESP32-C6 operational runbook |
clonea1 |
| #1862 |
docs(adr): ADR-345 — per-link CSI attribution and node-to-node ranging |
clonea1 |
| #1867 |
feat(firmware): add XIAO ESP32-C6 antenna selection |
proffesor-for-testing |
| #1872 |
fix(signal): the derived soft guard flagged the schedule it was derived from |
clonea1 |
All nine were independently reviewed against the underlying code before merge, including one merge-conflict resolution (#1858 vs. an unrelated concurrently-merged docs PR) handled during integration.
Known open issues (not fixed by this batch)
- #1870 — the governed
StreamingEngine fusion path owns a separate MultistaticFuser instance that is never configured with node positions at all; distinct from the instance fixed in #1860.
- #1804 — detailed field report on occupancy-counting and still-person-detection gaps; partially addressed in comments, several architecture questions remain open for maintainer input.
详细ChangeLog
Automated release from CI pipeline
Changes:
Merge pull request #1872 from clonea1/contrib/tdm-soft-guard
fix(signal): the derived soft guard flagged the schedule it was derived from
Docker Image:
ghcr.io/ruvnet/RuView:c407e738a34d8c3af019885b5c19f6aab1211c45
详细ChangeLog