Fix BLE connection-robustness: abort in-flight state machines and dedupe notification listener#32
Merged
jonasniesner merged 1 commit intoJul 6, 2026
Conversation
Abort all in-flight state machines on disconnect. resetState() previously cleared only configReadState, leaving directWriteState, dfuState, and firmwareVersionState active with their pending callbacks never invoked. A disconnect mid-transfer left "Read Toolbox"/firmware upload hung silently, and a lingering directWriteState.active made the next sendCanvasToDisplay throw 'Direct write already in progress' until page reload. A new abortInFlightOperations() settles each pending callback with a "Disconnected" error and clears the state so a later operation starts fresh, mirroring _abortDirectWrite for the direct-write path. Attach the characteristicvaluechanged listener exactly once. connectToGATT() added a fresh anonymous listener every call; auto-reconnect re-ran it against Chrome's cached characteristic object, so after one reconnect every notification was processed twice (double-counting config-read length and double-driving the chunk pump). The handler is now a bound reference removed before it is re-added, mirroring the existing _onDisconnect pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two connection-robustness fixes in
httpdocs/js/ble-common.js.1. State machines don't abort on disconnect
resetState()(called on both user disconnect and GATTgattserverdisconnected) cleared onlyconfigReadState.directWriteState,dfuState, andfirmwareVersionStatewere leftactivewith their pending callbacks never invoked and no timeout. Consequences:onComplete/onErrornever fired.directWriteState.activestayingtruemade the nextsendCanvasToDisplaythrow'Direct write already in progress'until a page reload.Fix: a new
abortInFlightOperations(err)helper, invoked fromresetState(), settles each pending callback with a clear "Disconnected" error and clearsconfigReadState/directWriteState/dfuState/firmwareVersionStateso a later operation starts fresh. The direct-write path reuses the existing_abortDirectWrite(err)so cleanup stays consistent with that state machine.2. Duplicate notification listeners on auto-reconnect
connectToGATT()added a fresh anonymouscharacteristicvaluechangedlistener on every call. Auto-reconnect re-runsconnectToGATT()against Chrome's cached characteristic object, so after one reconnect every notification was processed twice — double-counting the config-read length (→ truncated config) and double-driving the direct-write chunk pump.Fix: the handler is now a single bound reference (
this._onNotification, created once in the constructor) that isremoveEventListener'd before being re-added, mirroring the existingthis._onDisconnectpattern already in the file. Exactly one listener per characteristic, so notifications are processed once after a reconnect.Verification
node --check httpdocs/js/ble-common.jspasses.vmharness (14 assertions, all passing):configRead/directWrite/dfu/firmwareVersioncallbacks are each settled with the error and their state cleared (directWriteState === null);resetState()with nothing in flight is a safe no-op.Overlap with #28 (
fix/direct-write-ack-timeout)This branches off
mainand touches the same direct-write /resetStatearea as #28. Per the team this textual overlap is expected and accepted ("fix later") — #28 adds a direct-write ack timer and expands_abortDirectWrite(clearing that timer); this PR calls_abortDirectWritefrom the disconnect path. The two are semantically compatible (if #28's ack-timer field exists, its_abortDirectWritealready clears it), and any textual conflict at merge time will be reconciled then. This PR deliberately does not touch the CRC (#27) or encoding (#26/#30) areas.🤖 Generated with Claude Code
https://claude.ai/code/session_012g2e8mr132vcizx92WsgiR