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.");