Skip to content

Wait for real firmware config-write acks (0x41/0x42)#19

Open
balloob wants to merge 1 commit into
OpenDisplay:mainfrom
balloob:fix/config-write-ack
Open

Wait for real firmware config-write acks (0x41/0x42)#19
balloob wants to merge 1 commit into
OpenDisplay:mainfrom
balloob:fix/config-write-ack

Conversation

@balloob

@balloob balloob commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

The config-write completion path in httpdocs/js/ble-common.js only matched 0x00 0xCE (success) / 0x00 0xCF (failure), which the firmware never sends — so it was dead code. The real acks (0x41 for the single/first chunk, 0x42 for subsequent/final chunks) fell through to the generic command handler and were merely logged. And writeConfig resolved as soon as the bytes were written to GATT, so callers like the firmware toolbox logged "Config written successfully" even when the device rejected the write or failed to save it, and the onComplete callback never fired.

What changed

handleConfigWriteNotification is rewritten to match the actual firmware protocol (verified against communication.cpp): 0x00/0xFF 0x41, 0x00/0xFF 0x42, and the 3-byte 0x00 0x41/0x42 0xFE auth-required replies. It settles the pending per-chunk ack waiter instead of firing a callback directly. I dropped the never-sent 0xCE/0xCF cases rather than keeping them as fallback, since the handler was fully rewritten and they can never match real firmware.

writeConfig / writeConfigChunked now pace on and settle from those acks:

  • Arm the ack waiter before sending each chunk, so a fast notification arriving while sendHexCommands own await is still in flight is not dropped.
  • Send the first chunk (0x0041 + 2-byte LE total size + data) and await its 0x41 ack, then send each subsequent chunk (0x0042 + data) and await its 0x42 ack. The final 0x42 ack is the overall save result. Chunk size stays 200.
  • The blind await delay(150) between chunks is replaced by this ack pacing (the ack is the flow control now).
  • Each wait has an 8s timeout so a dead connection rejects instead of hanging forever, and the write-state active flag is always cleared on completion / failure / timeout so stale state cannot swallow later notifications.

The returned promise now resolves only on the device final ack and rejects with a descriptive Error on 0xFF failure or 0xFE auth-required. The auth-required error message contains "Authentication required (0xFE)", which the toolbox isAuthError() helper recognizes to trigger its re-auth-and-retry path (mirroring the existing config-READ 0x40 0xFE handling). The optional onComplete(err) callback still works — called with null on success and an Error on failure, matching the previous convention.

The existing await bleLib.writeConfig(...) callers in firmware/toolbox/index.html (config write and the SD-card write path) are unchanged in shape — they await and then log success, which is exactly what this fix makes truthful.

Verification

  • node --check httpdocs/js/ble-common.js passes.
  • A throwaway Node harness stubs the transport (sendHexCommand, log, connection) and drives the new code with simulated firmware notifications. 16/16 assertions pass: single-write success resolves and calls onComplete(null); 0xFF 0x42 on the final chunk rejects with "Config write failed" and calls onComplete(Error); 0x00 0x41 0xFE rejects with an error that the toolboxs isAuthError recognizes; a silent firmware times out and rejects (fast); and for a 4-chunk config the sends and acks strictly interleave (send 0041, ack 0041, send 0042, ack 0042, ...), proving chunk N+1 is only sent after chunk Ns ack. All state-teardown assertions confirm configWriteState.active is cleared in every path.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TEs4tsAWupf2DXFM4faECC

The config-write completion handler only matched 0x00 0xCE / 0x00 0xCF, which
the firmware never sends, so it was dead code. The real acks (0x41 for the
single/first chunk, 0x42 for subsequent/final chunks) fell through to the
generic handler and were merely logged, and writeConfig resolved as soon as the
bytes hit GATT -- callers logged "Config written successfully" even when the
device rejected or failed to save.

Rewrite handleConfigWriteNotification to match the firmware protocol
(communication.cpp): 0x00/0xFF 0x41, 0x00/0xFF 0x42, and the 3-byte
0x00 0x41/0x42 0xFE auth-required replies. Drop the never-sent 0xCE/0xCF cases.

writeConfig / writeConfigChunked now pace on and settle from these acks: arm an
ack waiter before sending each chunk (avoiding a dropped-ack race), send the
first chunk (0x0041 + LE total size + data) and await its 0x41 ack, then send
each subsequent chunk (0x0042 + data) and await its 0x42 ack -- the final 0x42
ack is the save result. The blind delay(150) is replaced by this ack pacing.
Each wait has an 8s timeout so a dead connection rejects instead of hanging, and
write-state is always cleared on completion/failure/timeout. The promise
resolves on success and rejects with a descriptive Error on failure or
auth-required (message contains "Authentication required (0xFE)", matched by the
toolbox isAuthError re-auth path); the optional onComplete callback still fires
with null on success / Error on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEs4tsAWupf2DXFM4faECC
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