Skip to content

Release v3.2.1 (MQTT crash fixes + diagnostics)#28

Merged
botts7 merged 7 commits into
mainfrom
feature/3.2
Jul 23, 2026
Merged

Release v3.2.1 (MQTT crash fixes + diagnostics)#28
botts7 merged 7 commits into
mainfrom
feature/3.2

Conversation

@botts7

@botts7 botts7 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Stability release on top of v3.2.0. Fixes the recurring MQTT-related crash/reboot (root-caused from on-device coredumps), the /logs Download hang, and ships the crash-diagnostics tooling that made the fixes possible.

Fixed

  • MQTT-related crash/reboot — two independent faults, both fixed:
    • BLE task was driving MQTT.loop() concurrently with the main loop (cross-task access to non-thread-safe PubSubClient/lwip → pbuf corruption). Yield callback removed.
    • MQTT reconnect could deref a NULL/torn-down socket after a WiFi blip. Now gated on WL_CONNECTED + fresh socket.
  • /logs Download hang — now builds the file client-side (no second server request racing the poll).

Added

  • firmware.elf attached to releases (coredump symbolisation).
  • BLE op timings on /api/diag/runtime + crash-breadcrumb phase.

Known issues

  • A rare interrupt-watchdog reboot (~once every few days) remains under investigation; no coredump captured. The frequent MQTT crashes fixed here were the dominant cause.

Full detail in the [3.2.1] section of CHANGELOG.md.

🤖 Generated with Claude Code

botts7 and others added 7 commits July 18, 2026 18:46
A coredump is raw addresses; /api/coredump/summary's backtrace only becomes
function names + line numbers with the EXACT ELF that built the running image.
Releases attached firmware.bin but not the ELF, so a crash on a released build
couldn't be fully symbolised without rebuilding and hoping the layout matched.

Now every release attaches wallbox-gateway-<tag>-esp32s3.elf, and every build
retains firmware.elf as a 90-day artifact. Debug symbols only — never flashed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The /logs Download button linked to /api/logs, firing a second ~16 KB request
that raced the page's 3s auto-refresh poll for the async server's connection
slots. Under the heap pressure of overlapping 16 KB String responses some came
back empty and the page hung — the 'stuck on export' report.

Download now builds the file client-side (a Blob from the log the page already
displays), so it makes no server request, can't contend, and is instant.
/api/logs is unchanged for external monitors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings the v3.2.0 release tag into feature/3.2's ancestry so git-describe on
the dev branch anchors to v3.2.0 (not the older rc.4), keeping dev-build
version strings monotonic — a build off feature/3.2 now reports
'v3.2.0-N-g<hash>' (> 3.2.0) instead of 'v3.2.0-rc.4-N-...' (< 3.2.0), so the
HA update entity never mistakes newer dev code for a downgrade target. Content
is unchanged (main == the PR #27 merge of this branch); this only records the
ancestry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The core dump pins #168 to a core-0 task-watchdog with our wb_ble task
priority-boosted (holding a BT-stack lock during writeValue), but a single
partial dump isn't enough to fix the carefully-built mutex code confidently.
Add lightweight instrumentation to catch the stalling op on the next crash:

- Time each ATT write and the full write+response round-trip; log a warning
  when a write exceeds 300ms (normal < 50ms) or a round-trip exceeds 2s.
- Track worst-case ble_write_max_ms / ble_rt_max_ms on /api/diag/runtime, so
  the peak survives the in-RAM log ring wrapping.
- Mark the RTC-NOINIT crash breadcrumb 'w:<met>' during the write and 'q:<met>'
  during the response-wait. The log ring is wiped on reboot and the running
  BLE task's coredump backtrace is often unwind-corrupt, but the breadcrumb
  survives — so a watchdog reset mid-write is identifiable post-reboot via
  /api/boot/history.

Instrumentation only — no change to the write/mutex flow, so it can't
reintroduce the write-race panics the mutexes were added to prevent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coredump from the box (task=loopTask, LoadProhibited read @0x14, clean
backtrace) pinned a #168 crash exactly:

  loop -> WallboxMQTT::loop -> _connect -> PubSubClient::connect
       -> readPacket -> readByte -> WiFiClient::read()   <- NULL deref

MQTT reconnect had no WiFi-up guard, and PubSubClient::connect() skips its own
TCP connect when wifiClient.connected() returns true. After a WiFi blip that
read is STALE (connected() true while the socket handle is NULL), so connect()
jumps straight to reading the CONNACK and WiFiClient::read() dereferences the
NULL handle -> LoadProhibited panic in loopTask.

Two guards:
- loop() skips _connect() unless WiFi.status()==WL_CONNECTED (don't even try
  while WiFi is down).
- _connect() calls wifiClient.stop() first, forcing connect() to establish a
  fresh socket instead of trusting a stale connected()==true.

Found via the coredump + breadcrumb instrumentation added this cycle (the
surviving breadcrumb was 'q:g_psh'). Note: an earlier #168 dump had a different
signature (task-watchdog / IDLE0 with the BLE task priority-boosted) — that may
be a separate issue; keep the instrumentation and watch for it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second coredump (task=wb_ble, breadcrumb q:r_lse, clean bt) symbolized to:
  WallboxMQTT::loop -> PubSubClient::loop -> readPacket -> WiFiClient::read
  -> lwip_recv_tcp -> pbuf_free()  <- lwip assertion (abort)

The BLE yield callback (set in setupServices) pumped wallboxMQTT.loop() +
ArduinoOTA.handle() during BLE response-waits. That callback was written when
sendCommand() ran on the web/main task, but BLE now runs on its own pinned
task (wb_ble) — so those calls fired CONCURRENTLY with the main loop's own
wallboxMQTT.loop() (main.cpp:512) and ArduinoOTA.handle() (main.cpp:493).
PubSubClient / WiFiClient / lwip are not thread-safe; two tasks reading the
same socket corrupted an lwip pbuf -> pbuf_free assert -> crash.

This is the same root as the earlier loopTask MQTT-connect NULL-deref (1a3e1f5):
MQTT touched from two contexts. That commit's WiFi-up guard + wifiClient.stop()
stay as complementary defense for the main loop's own reconnect; this removes
the cross-task access entirely.

Fix: drop the yield callback. Its keepalive-starvation rationale is moot now —
BLE waits run off the main loop, so the main loop services MQTT (60s keepalive)
+ OTA continuously on its own. The BLE wait just delay(1)-yields to the
scheduler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@botts7
botts7 merged commit 5f848cc into main Jul 23, 2026
1 check 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.

1 participant