Skip to content

Stop failed DFU sessions wedging the radio or failing silently - #2467

Open
fluffyspace wants to merge 2 commits into
InfiniTimeOrg:mainfrom
fluffyspace:fix-dfu-silent-wedge
Open

Stop failed DFU sessions wedging the radio or failing silently#2467
fluffyspace wants to merge 2 commits into
InfiniTimeOrg:mainfrom
fluffyspace:fix-dfu-silent-wedge

Conversation

@fluffyspace

@fluffyspace fluffyspace commented Aug 6, 2026

Copy link
Copy Markdown

Two independent DFU robustness fixes, one per commit, so either can be taken alone. Both came out of debugging repeated OTA failures on real hardware; the underlying data-corruption bug is #2466, and these are about the surrounding failure modes being invisible or unrecoverable.

1. Bound the wait for the sleep wake lock

WritePacketHandler spins until SystemTask disables sleeping, with no upper bound:

while (!systemTask.IsSleepDisabled()) {
  vTaskDelay(pdMS_TO_TICKS(5));
}

This runs on the BLE host task. If the wake lock never arrives, the host task never returns: GATT operations stop being served, disconnect events are never processed, and the watch cannot advertise again. The status bar still shows Bluetooth as connected, but the watch is invisible to every scanner until it is rebooted — a failed update looks like the radio has died.

Capped at 1 s, then continue without the lock. Racing the sleep timer is a much better failure mode than a radio that needs a reboot to recover.

2. Answer commands that arrive in the wrong state

Every wrong-state branch in ControlPointHandler returns silently, and so do stray data packets. A host that reconnects to a watch left parked mid-transfer by an interrupted update gets no reply at all — Start DFU simply never answers, which is indistinguishable from a watch that is not listening. Every retry looks like an unexplained hang until the watch is rebooted or the 10 s inactivity timer happens to reset the state machine.

ErrorCodes::InvalidState is already defined in the protocol and was never used. These branches now reply [Response, opcode, InvalidState, state], the fourth byte naming the state the watch is actually in, which makes the failure diagnosable from the host without a debugger.

Stray data packets report once per session rather than per packet, so a host that keeps sending cannot flood the notification path. The flag is cleared in Reset().

Testing

Tested on a PineTime (bootloader 1.0.1, InfiniTime 1.16.0), alongside #2466: the firmware builds and runs with these changes, and OTA updates complete normally with them in place.

To be precise about what I did not verify: I did not stage a mid-transfer reconnect to observe an InvalidState response arrive at the host. The silent-return behaviour it replaces is plain in the code, and my host client decodes the response, but the new reply path itself is unexercised — worth a reviewer's eye.

The unbounded wake-lock wait, by contrast, was hit repeatedly in practice: a watch showing Bluetooth as connected while being invisible to every scanner, recoverable only by reboot.

Happy to split this into two PRs if you would prefer them reviewed separately.

WritePacketHandler spins until SystemTask disables sleeping, with no upper
bound. This runs on the BLE host task, so if the wake lock never arrives the
host task never returns: GATT operations stop being served, disconnect
events are never processed, and the watch cannot advertise again until it is
rebooted. A failed update then looks like a watch whose Bluetooth has died.

Cap the wait at 1s and continue without the lock. Racing the sleep timer is
a far better failure mode than a radio that needs a reboot to recover.
DfuService silently drops any control point command it cannot handle in its
current state, and silently drops data packets the same way. A host that
reconnects to a watch left parked mid-transfer by an interrupted update
therefore gets no reply at all: from its side, Start DFU simply never
answers, which is indistinguishable from a watch that is not listening.
Every subsequent attempt looks like an unexplained hang until the watch is
rebooted or the 10s inactivity timer happens to reset the state machine.

The protocol already defines ErrorCodes::InvalidState, and it was never
used. Reply with [Response, opcode, InvalidState, state] instead of
returning silently; the fourth byte names the state the watch is actually
in, which makes the failure diagnosable from the host without a debugger.

Stray data packets report once per session rather than per packet, so the
notification path cannot be flooded by a host that keeps sending.
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown

Build size and comparison to main:

Section Size Difference
text 385344B 112B
data 944B 0B
bss 22640B 0B

Run in InfiniEmu

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