Skip to content

fix(bldc_haptics): auto-pause telemetry when the USB host stops draining - #773

Merged
finger563 merged 1 commit into
mainfrom
fix/haptics-telemetry-backpressure
Sep 5, 2026
Merged

fix(bldc_haptics): auto-pause telemetry when the USB host stops draining#773
finger563 merged 1 commit into
mainfrom
fix/haptics-telemetry-backpressure

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Symptom

After some time the haptics example spams the CDC console with:

[UsbDevice/W] Vendor TX FIFO full, dropping a 35-byte frame
[BLDC Haptics Example/W] USB vendor TX failed, dropped 35-byte frame: No buffer space available

(the 35-byte frame is a haptics telemetry frame), and once this starts, the web page can no longer connect to the device.

Cause

The telemetry task only gated on usb.is_vendor_connected() — which is just tud_mounted() (enumerated) — and never on whether the host is actually reading the vendor IN endpoint. streaming also stays true from the previous session. So when the browser tab is backgrounded / frozen / closed without a clean disconnect, it stops draining the endpoint, the TX FIFO fills, and the device streams into a full FIFO forever:

  1. every telemetry period logs a dropped frame (the spam), and
  2. a reconnecting host's GET_INFO reply is dropped because the FIFO is full of stale telemetry, so initializeDevice() times out → "unable to connect."

Fix

Deviceusb_send() now returns whether the frame was queued. The telemetry task tracks continuous send failures and, after kTelemetryStallTimeout (2 s) of the host not draining, sets streaming = false and logs once. That stops the flood and lets the FIFO drain, so a new host connects cleanly and re-enables streaming (the web console sends SET_STREAMING on connect). Command/reply sends are unaffected (they ignore the return value).

Web — pause streaming on visibilitychange when the tab is hidden (a throttled/backgrounded tab can't drain the endpoint) and resume when it's shown again; the streamToggle keeps the user's intent. The firmware auto-pause remains the safety net for a frozen or closed tab where the JS can't run.

Notes

  • cppcheck clean with the CI flags; the web console passes a JS parse check.
  • Follow-up option (not done here): the per-drop UsbDevice "Vendor TX FIFO full" log in usb_device.cpp could be rate-limited so the ~2 s window before auto-pause is quieter — but that's a shared-component change affecting every user, so left out of this example-scoped fix.

🤖 Generated with Claude Code

Symptom: after a while the CDC console spammed "Vendor TX FIFO full, dropping a
35-byte frame" (that 35-byte frame is haptics telemetry), and afterwards the web
page could no longer connect.

Cause: the telemetry task only gated on tud_mounted() (enumerated), never on
whether the host was actually reading the vendor IN endpoint, and `streaming`
stayed true from the previous session. When the browser tab is backgrounded /
frozen / closed-without-disconnect it stops draining, the TX FIFO fills, and the
device streams into it forever -- spamming drops and, crucially, dropping a
reconnecting host's GET_INFO reply so initializeDevice() times out.

Fix (device): usb_send() now returns whether the frame was queued; the telemetry
task tracks continuous send failures and, after kTelemetryStallTimeout (2s), sets
streaming=false and logs once. That stops the flood and lets the FIFO drain, so a
new host connects cleanly and re-enables streaming (SET_STREAMING on connect).

Fix (web): pause streaming on `visibilitychange` when the tab is hidden (a
throttled tab can't drain the endpoint) and resume when shown again; the firmware
auto-pause remains the safety net for a frozen/closed tab.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 5, 2026 03:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

This PR prevents the BLDC haptics example from flooding the USB vendor IN TX FIFO when the host stops reading, by auto-pausing telemetry on both the firmware and web console sides.

Changes:

  • Web console pauses telemetry when the tab is hidden and resumes when visible again.
  • Firmware usb_send() now reports enqueue success/failure and telemetry auto-pauses after sustained send failures.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
components/bldc_haptics/example/webapp/index.html Pause/resume streaming based on tab visibility to avoid host-side endpoint starvation.
components/bldc_haptics/example/main/bldc_haptics_example.cpp Track vendor TX failures and auto-pause telemetry after a stall timeout to prevent FIFO saturation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +790 to +799
if (usb_send(proto::build(proto::Msg::Telemetry, payload))) {
telemetry_stall_start = {}; // queued OK -> the host is draining
} else if (telemetry_stall_start == std::chrono::steady_clock::time_point{}) {
telemetry_stall_start = start; // first drop -> start the stall clock
} else if (start - telemetry_stall_start > kTelemetryStallTimeout) {
streaming = false; // host abandoned the stream: stop flooding a full FIFO
telemetry_stall_start = {};
logger.warn("Telemetry auto-paused: the host stopped draining the USB vendor "
"endpoint (re-enable streaming from the web console)");
}
Comment on lines +774 to +775
auto telemetry_stall_start = std::chrono::steady_clock::time_point{};

Comment on lines +792 to +793
} else if (telemetry_stall_start == std::chrono::steady_clock::time_point{}) {
telemetry_stall_start = start; // first drop -> start the stall clock
@finger563
finger563 merged commit f59f12c into main Sep 5, 2026
155 checks passed
@finger563
finger563 deleted the fix/haptics-telemetry-backpressure branch September 5, 2026 04:17
finger563 added a commit that referenced this pull request Sep 5, 2026
…on auto-pause (#775)

* fix(bldc_haptics web): drain stale RX before the connect handshake

Re-applies the RX-drain that missed the #773 squash-merge. On an abrupt tab close
the device's vendor TX FIFO is not cleared, so a reconnecting page could read
leftover telemetry / reply frames and mis-handle them ("first Connect returns
immediately, second works"). initializeDevice() now flushes the pipe (set a
`draining` flag, reset the parser, let the RX pump discard for ~150 ms) before the
GET_INFO handshake; dispatchFrame drops frames while draining.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bldc_haptics): clear the vendor TX backlog when telemetry auto-pauses

When the telemetry stall guard fires (host stopped draining), also drop the
queued-but-unsent telemetry via usb.vendor_write_clear() (added in #774). An
abrupt tab close does not unmount the device, so the FIFO is not cleared for us;
clearing it here means a reconnecting host reads a clean stream instead of a
stale backlog it might mis-parse as the reply to its first command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

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