Skip to content

EPIC-09: device-side visibility and recovery - #9

Merged
diegoparrilla merged 6 commits into
release/v1.1.0from
epic/09-device-visibility-and-recovery
Sep 15, 2026
Merged

diegoparrilla merged 6 commits into
release/v1.1.0from
epic/09-device-visibility-and-recovery

Conversation

@diegoparrilla

Copy link
Copy Markdown
Contributor

Second epic of v1.1.0: the device reports its own health, and a crash or hang reboots it with the reason kept instead of freezing silently.

What changes

  • Crash reboots (D-03). panic() goes to health_panic (PICO_PANIC_FUNCTION), and a HardFault handler is installed in the RAM vector table at boot. fatfs-sdk's own handler stopped at a breakpoint, which locks up an RP2040 with no debugger. Both write the reason, PC, LR and SP to watchdog scratch registers 0-3 and reboot after 100 ms.
  • Watchdog (D-04). 8 s, in both build types, paused while a debugger halts the CPU. It is fed from the main loop, emul_pollTick, the Wi-Fi connect loop, the HTTP spin-waits and two slow boot steps. A phase ID says where a hang happened.
  • A hang is told apart from a probe reset. A debug probe resets with SYSRESETREQ, which leaves every watchdog and reset register exactly as a real expiry does. A once-a-second timer interrupt writes a stall mark after 5 s without a feed, and only a reset with that mark counts as a hang.
  • Reset reason on the menu. Row 2 shows Recovered: panic @…, Recovered: fault @… or Recovered: hang in <phase>. After 3 crash reboots within 60 s the boot countdown stays stopped until a SELECT reset or a power cycle.
  • Health report without a console. GET /api/v1/system/health and sidecart.py health report heap free and minimum, sbrk and stack high-water marks, code in RAM, the last reset with its phase or PC, crash count, ROM3 overruns and debug-byte drops. Debug builds print a summary every 30 s.
  • lwIP statistics opt-in. They were keyed on NDEBUG, which both build types define. A debug build with DEVOPS_LWIP_STATS=1 now adds the pool counters to the health report. Release lwIP objects are unchanged.
  • ROM3 ring overruns are counted, and the DMA capture is re-armed before its transfer count runs out. The protocol parser rejects a payload size larger than its buffer, which a corrupted frame could overflow.
  • fatfs-sdk pinned to 6c644cf, the commit Booster uses. After a card pull, sd_read_blocks returned without releasing its locks, so the next SD call deadlocked the RP. 6c644cf is the upstream fix plus a README change.
  • sd_timeouts.c overrides fatfs-sdk's weak timeout table. Debug-only fault-injection routes under POST /api/v1/debug/test/. docs/api.md, README and CLAUDE.md updated.

Verified on hardware

On release and debug, with the ST in the setup menu, [G] and [U]:

Check Result
Panic, HardFault Reboot in 6 to 12 s; the reason and PC on the menu, the console and health; the PC resolves to the faulting line
Hang in the main loop, hang inside an HTTP handler Watchdog reboot, hang in main_loop / hang in http_request
Three crashes within 60 s Countdown stopped with x3; a SELECT tap clears it
Debug-probe reset and probe flash reboot, crash count 0
30 s summary line, lwIP counters, ROM3 overruns under a forced stall All present; overruns count exactly the stalls that hit a debug burst
Boot with no SD card, no reachable network, SELECT held 8 s, long debugger halt No reboot
SD card pulled Before the fatfs-sdk pin: deadlock and watchdog reboot. After: every request fails in under a second, no reboot
RP crash while the ST is in [G] or [U] The ST keeps running; GEMDRIVE answers again, Runner reconnects after the countdown
cli/test_sidecart.py 115 tests pass
Build Heap total Heap free, idle Code in RAM
release, before this PR 36,132 — 47,224
release 35,508 26,640 47,896
debug 27,284 18,416 51,964

Found and filed, not fixed here

  • The HTTP idle sweeper closes uploads and slow downloads after a few seconds, so a 4 MB upload fails (EPIC-13).
  • gemdrive mkdir panics in malloc on a 29 GB FAT32 card (EPIC-11).
  • A reinserted SD card is not mounted until the RP restarts; the CLI prints a traceback when the device resets a connection (EPIC-15).
  • The core-0 stack reaches about 6.2 KB against its 2 KB reservation (EPIC-12).

The 4 MB upload and 10 s runner load false-reboot checks wait for the idle-sweeper fix.

The statistics were keyed on NDEBUG, which both build types define, so
MEM_STATS, MEMP_STATS and stats_display were compiled out everywhere.
Port Booster's block: a debug build with DEVOPS_LWIP_STATS=1 in the
environment turns them on. Release is unchanged; its lwIP objects are
byte-identical to before.
…ORY-05)

commemul_poll derived the write index from the DMA transfer count with
no lap detection, so a main loop stalled for more than 4,096 samples
silently mixed old and new data into the protocol parser. Count a lap
when that many samples arrived since the last poll, and drop the unread
ones. The channel stopped for good after 2^32 samples; re-arm it at
2^31 from its live write address.

A corrupted frame could also carry a payload size larger than the
payload buffer, and the parser wrote past it. Reject such a frame.
…(EPIC-09 STORY-01 STORY-04 STORY-06)

A panic or HardFault used to freeze the RP silently: the SDK's panic
stops at a breakpoint and fatfs-sdk's HardFault handler does the same,
which locks up an RP2040 with no debugger. A hang had no recovery at all.

health.c adds:
- panic() through PICO_PANIC_FUNCTION and a HardFault handler installed
  in the RAM vector table at boot. Both write the reason, PC, LR and SP
  to watchdog scratch registers 0-3 and reboot after 100 ms.
- An 8 s watchdog, paused under a debug probe, fed from the main loop,
  emul_pollTick, the Wi-Fi connect loop, the HTTP spin-waits and after
  the two slow boot steps. A phase ID in scratch[0] says where a hang
  happened.
- Boot decoding into power-on, reset, panic, HardFault, hang or reboot,
  shown on row 2 of the setup menu and on the debug console.
- A crash-loop guard: 3 crash reboots within 60 s stop the countdown
  until a SELECT reset or a power cycle.
- A painted core-0 stack for a high-water mark, heap free and minimum
  free from mallinfo and sbrk, and code copied to RAM.
- Debug-only fault injection for hardware verification.

sd_timeouts.c overrides fatfs-sdk's weak timeout table so a failing card
gives up well within the watchdog. The jump to Booster stops the
watchdog first, since it does not reset the chip.
A release build has no console, so nothing on the device could be read
without a debugger. GET /api/v1/system/health returns uptime, heap total,
free, minimum free and sbrk high-water, stack high-water and reservation,
code in RAM, the last reset reason with its phase or PC, the crash count
and crash-loop state, ROM3 overruns and debug-byte drops. A debug build
with DEVOPS_LWIP_STATS adds lwIP's pool counters. It is a new endpoint
rather than more ping fields, so ping stays a minimal liveness check.

`sidecart.py health` prints it, with tests. Debug builds also expose
POST /api/v1/debug/test/<fault> to trigger each recovery path on
hardware. Both are documented in docs/api.md.
…d (EPIC-09 STORY-06)

With a mounted SD card pulled, sd_read_blocks returned early from its
error path without releasing the card and SPI mutexes. Those mutexes
block without a timeout, so the next disk call waited forever: the
first GEMDRIVE or HTTP request after the pull failed, and the second
froze the RP until the watchdog rebooted it (reported as a hang in
disk_read by a diagnostic build).

Upstream fixed it in f5f31b3. 6c644cf is that fix plus a README change,
and is the commit Booster already pins. Verified on hardware: after an
idle pull, listings and downloads fail in under a second and the RP
stays up.
Flashing or resetting through a debug probe uses SYSRESETREQ, which
leaves the watchdog reason and scratch registers exactly as a real
watchdog expiry does, so every probe flash showed "Recovered: hang"
and counted as a crash. Measured on hardware: reason, watchdog control,
chip reset and the SDK's marker all match, and DHCSR reads as zero from
the firmware.

A once-a-second timer interrupt now writes a stall mark into scratch[1]
after 5 s without a watchdog feed, and clears it when feeding resumes.
A watchdog reset counts as a hang only if the mark is there. A hang with
interrupts disabled cannot set the mark and is reported as a reboot.

Verified on hardware: main-loop hang and HTTP handler hang still report
their phase; a probe reset and a probe flash now report a reboot with
crash count 0.
@diegoparrilla
diegoparrilla merged commit 5cc61bf into release/v1.1.0 Sep 15, 2026
2 checks passed
@diegoparrilla
diegoparrilla deleted the epic/09-device-visibility-and-recovery branch September 15, 2026 17:11
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.

1 participant