From 01ae4614e346eee28e38d06051333c39e731f4ab Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 4 Sep 2026 22:58:12 -0500 Subject: [PATCH] fix(bldc_haptics): auto-pause telemetry when the host stops draining USB 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) --- .../example/main/bldc_haptics_example.cpp | 28 +++++++++++++++++-- .../bldc_haptics/example/webapp/index.html | 16 +++++++++++ 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/components/bldc_haptics/example/main/bldc_haptics_example.cpp b/components/bldc_haptics/example/main/bldc_haptics_example.cpp index 8fcb6e772..6576fa19c 100644 --- a/components/bldc_haptics/example/main/bldc_haptics_example.cpp +++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp @@ -360,9 +360,12 @@ extern "C" void app_main(void) { // Takes a span so callers can pass either an owning std::vector (built by // proto::build / ota_stream make_*, converted implicitly, no copy) or a // borrowed buffer (the discovery / coredump reply spans) without allocating. - auto usb_send = [&](std::span frame) { + // Returns true if the frame was queued, false if it was dropped (FIFO full / + // host not draining). Callers that don't care (command replies) ignore it; + // the telemetry task uses it to auto-pause a stream the host has abandoned. + auto usb_send = [&](std::span frame) -> bool { if (frame.empty()) - return; + return true; std::lock_guard lk(usb_tx_mutex); std::error_code tx_ec; if (!usb.write_vendor(frame, tx_ec)) { @@ -372,7 +375,9 @@ extern "C" void app_main(void) { // disconnected host streaming telemetry cannot flood the console. logger.warn_rate_limited("USB vendor TX failed, dropped {}-byte frame: {}", frame.size(), tx_ec.message()); + return false; } + return true; }; // RX bytes arrive in the TinyUSB task context: queue them and dispatch from @@ -760,6 +765,14 @@ extern "C" void app_main(void) { // -------------------------------------------------------------------------- // Telemetry streaming task // -------------------------------------------------------------------------- + // Auto-pause guard: if telemetry sends fail continuously for this long, the + // host has stopped draining the vendor IN endpoint (tab closed / backgrounded + // / frozen). Streaming into a full FIFO just spams drops and, worse, buries a + // reconnecting host's GET_INFO reply so it can never connect -- so pause the + // stream until a host re-enables it (SET_STREAMING on connect). + constexpr auto kTelemetryStallTimeout = 2s; + auto telemetry_stall_start = std::chrono::steady_clock::time_point{}; + espp::Task telemetry_task( {.callback = [&](std::mutex &m, std::condition_variable &cv) -> bool { const auto start = std::chrono::steady_clock::now(); @@ -774,7 +787,16 @@ extern "C" void app_main(void) { proto::put_f32(payload, continuous_value()); proto::put_f32(payload, motor->get_shaft_angle()); proto::put_f32(payload, motor->get_shaft_velocity()); - usb_send(proto::build(proto::Msg::Telemetry, payload)); + 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)"); + } } { std::unique_lock lk(m); diff --git a/components/bldc_haptics/example/webapp/index.html b/components/bldc_haptics/example/webapp/index.html index 3929da9b9..048761e33 100644 --- a/components/bldc_haptics/example/webapp/index.html +++ b/components/bldc_haptics/example/webapp/index.html @@ -1169,6 +1169,22 @@

Log

els.streamToggle.addEventListener("change", applyStreaming); els.rateSelect.addEventListener("change", () => { if (els.streamToggle.checked) applyStreaming(); }); + // Pause telemetry while the tab is hidden. A backgrounded tab is throttled + // and stops draining the USB IN endpoint, so the device's TX FIFO fills and + // it drops frames (the firmware also auto-pauses after a couple seconds of + // this). Tell the device to stop now, and resume when the tab is shown again + // (the streamToggle keeps the user's intent). Best-effort: if the tab is + // frozen the send may not land, but the firmware auto-pause covers that. + document.addEventListener("visibilitychange", () => { + if (!device) return; + if (document.hidden) { + transact(TYPE.SET_STREAMING, streamingPayload(false, parseInt(els.rateSelect.value, 10)), + [TYPE.OK], CMD_TIMEOUT_MS).catch(() => {}); + } else if (els.streamToggle.checked) { + applyStreaming(); + } + }); + window.addEventListener("resize", drawDial); logLine("sys", "espp BLDC Haptics Console ready. Connect a device running the bldc_haptics USB example.");