Skip to content

Fix direct-write upload hanging on stall or error frame (MJ-19)#28

Merged
jonasniesner merged 1 commit into
OpenDisplay:mainfrom
balloob:fix/direct-write-ack-timeout
Jul 6, 2026
Merged

Fix direct-write upload hanging on stall or error frame (MJ-19)#28
jonasniesner merged 1 commit into
OpenDisplay:mainfrom
balloob:fix/direct-write-ack-timeout

Conversation

@balloob

@balloob balloob commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Problem

The BLE direct-write flow in httpdocs/js/ble-common.js had no ACK timeout and ignored mid-stream error frames. As a result a stalled transfer or a firmware error frame left the UI stuck at "Uploading N%" forever, with onComplete/onError never firing. Because directWriteState.active stayed true, a subsequent upload was also blocked by "Direct write already in progress".

Two concrete gaps:

  • The chunk-sending path (sendNextDirectWriteChunk / _activateFullDirectWrite) had no timer, so silence from the device hung indefinitely.
  • handleDirectWriteNotification only recognized a NACK while awaitingPartialStart, so a mid-stream {0xFF,0x71,err,0x00} NACK or the 2-byte {0xFF,0xFF} compressed-error frame matched no pattern → pendingAcks never drained.

Fix

  • Add a per-ACK timeout (DIRECT_WRITE_ACK_TIMEOUT_MS = 8000, matching the existing config-write ack timeout) armed when sending the start command, chunks, and the end command, and re-armed on each recognized ack. On expiry the upload aborts via the existing _abortDirectWrite path, firing onComplete(false, err) and clearing directWriteState. The timer is cleared once the end command acks (0x72), so the display-refresh phase remains bounded by the firmware's own refresh timeout (0x74) rather than the short ack timer.
  • Make handleDirectWriteNotification recognize mid-stream error frames at any point in the flow: a 4-byte {0xFF,cmd,err,0x00} NACK for any direct-write opcode, and the 2-byte {0xFF,0xFF} frame. Both fail the operation cleanly with a descriptive message. The existing partial-start NACK fallback-to-full behavior is preserved.

The change is scoped to the direct-write send/notification path; encoding/image code is untouched.

Verification

  • node --check httpdocs/js/ble-common.js passes.
  • Drove handleDirectWriteNotification in a small Node/VM harness: a {0xFF,0xFF} frame and a mid-stream {0xFF,0x71,err,0x00} NACK each return true, fire onComplete(false, err) once with a descriptive message, and reset directWriteState to null. Confirmed the partial-start NACK still falls back to full (no hard abort), and that the ack-timeout path aborts a stalled transfer and cleans up state.

The direct-write flow had no ACK timeout and ignored mid-stream error
frames, so a stalled transfer or a firmware error left the UI stuck at
"Uploading N%" forever with onComplete/onError never firing.

- Add a per-ACK timeout (DIRECT_WRITE_ACK_TIMEOUT_MS, 8s, matching the
  config-write path) armed when sending the start command, chunks, and
  the end command, and re-armed on each recognized ack. On expiry the
  upload aborts via _abortDirectWrite, firing onComplete(false, err) and
  clearing directWriteState so a later upload isn't blocked by a stuck
  active flag. The timer is cleared once the end command acks (0x72), so
  the display-refresh phase stays bounded by the firmware's own refresh
  timeout (0x74) rather than the ack timer.
- handleDirectWriteNotification now recognizes mid-stream error frames at
  any point in the flow: a 4-byte {0xFF,cmd,err,0x00} NACK for any
  direct-write opcode, and the 2-byte {0xFF,0xFF} compressed-error frame.
  Both abort the operation cleanly with a descriptive message. The
  existing partial-start NACK fallback-to-full behavior is preserved.

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

2 participants