From 87bc08b06240b85294ffda3ab1262e1ab0e232b2 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 4 Sep 2026 12:57:27 -0500 Subject: [PATCH 01/10] feat(bldc_haptics): advertise for dispatcher capability discovery The bldc_haptics example was the one framed-USB example still driving a raw stream_frame::StreamParser (with a manual `frame.module == kModule` filter) instead of an espp::Dispatcher, so it could not advertise itself to the browser Device Hub. Migrate it and advertise. - Replace the raw parser + module filter with an espp::Dispatcher: register the haptics protocol on module 2 (proto::kModule) with a handler that gates on !is_reply() (unchanged semantics), and feed()/reset() the dispatcher from the USB RX worker. The example's OTA subset + haptics commands all ride module 2's message types, so this stays a single-module protocol. - Advertise it: register_module(..., ModuleInfo{"BLDC Haptics", "haptics_console.html", ...}), set_device_info(usb_cfg.product), and serve_discovery() routed through the existing tx_mutex-guarded usb_send. - Add the dispatcher component to the example's EXTRA_COMPONENT_DIRS and to the main component's REQUIRES. Also complete the dispatcher README module-id table (add 2 = BLDC haptics, 6 = MCP266). This finishes the set: every framed-USB example (ota, coredump, can_bridge, mcp266, haptics) now advertises over the reserved discovery module 0xFF. Verified: builds clean, manager-off, on IDF v6.0.1 (esp32s3). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../bldc_haptics/example/CMakeLists.txt | 1 + .../bldc_haptics/example/main/CMakeLists.txt | 5 +-- .../example/main/bldc_haptics_example.cpp | 34 ++++++++++++++----- components/dispatcher/README.md | 2 ++ 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/components/bldc_haptics/example/CMakeLists.txt b/components/bldc_haptics/example/CMakeLists.txt index 09f3a7efbf..0e3133fdd7 100644 --- a/components/bldc_haptics/example/CMakeLists.txt +++ b/components/bldc_haptics/example/CMakeLists.txt @@ -27,6 +27,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/bldc_motor" "../../../components/bldc_types" "../../../components/cli" + "../../../components/dispatcher" "../../../components/esp-dsp" "../../../components/filters" "../../../components/format" diff --git a/components/bldc_haptics/example/main/CMakeLists.txt b/components/bldc_haptics/example/main/CMakeLists.txt index 59ee5a5d07..1fe9442105 100644 --- a/components/bldc_haptics/example/main/CMakeLists.txt +++ b/components/bldc_haptics/example/main/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register(SRC_DIRS "." INCLUDE_DIRS "." - REQUIRES bldc_driver bldc_haptics bldc_motor i2c motorgo-axis motorgo-mini - mt6701 ota stream_frame task usb_device esp_tinyusb esp_timer espcoredump) + REQUIRES bldc_driver bldc_haptics bldc_motor dispatcher i2c motorgo-axis + motorgo-mini mt6701 ota stream_frame task usb_device esp_tinyusb + esp_timer espcoredump) diff --git a/components/bldc_haptics/example/main/bldc_haptics_example.cpp b/components/bldc_haptics/example/main/bldc_haptics_example.cpp index 116a16b6d6..aa93dafb94 100644 --- a/components/bldc_haptics/example/main/bldc_haptics_example.cpp +++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include "esp_core_dump.h" @@ -22,6 +23,7 @@ #include "bldc_driver.hpp" #include "bldc_haptics.hpp" #include "bldc_motor.hpp" +#include "dispatcher.hpp" #include "i2c.hpp" #include "mt6701.hpp" #include "ota.hpp" @@ -451,7 +453,10 @@ extern "C" void app_main(void) { // -------------------------------------------------------------------------- // Protocol frame handling (runs in the worker task) // -------------------------------------------------------------------------- - proto::stream::StreamParser parser; + // Route the vendor stream through a Dispatcher on the haptics module (2); the + // module is registered (and advertised for capability discovery) once + // handle_frame is defined, below. + espp::Dispatcher dispatcher; bool restart_pending = false; // Build replies via proto::build so they carry the haptics module (2) + reply @@ -677,6 +682,24 @@ extern "C" void app_main(void) { } }; + // Register the haptics protocol on the dispatcher (module 2) and advertise it + // for capability discovery, so the browser Device Hub can list and link it. + // The Dispatcher routes by module and hands us the whole frame; we still gate + // on !is_reply() so a reply-typed echo cannot re-enter the request handler. + dispatcher.register_module(proto::kModule, + [&](const proto::stream::Frame &frame) { + if (!frame.is_reply()) + handle_frame(frame); + }, + {.name = "BLDC Haptics", + .app = "haptics_console.html", + .description = "Haptic feedback modes + firmware update"}); + dispatcher.set_device_info(usb_cfg.product); + // serve_discovery answers the reserved 0xFF module; route its reply through the + // same tx_mutex-guarded usb_send as every other frame. + dispatcher.serve_discovery( + [&](std::span f) { usb_send(std::vector(f.begin(), f.end())); }); + espp::Task usb_task( {.callback = [&](std::mutex &, std::condition_variable &) -> bool { std::vector> chunks; @@ -696,18 +719,13 @@ extern "C" void app_main(void) { // Bytes were dropped: any in-flight frame / OTA image is unusable. std::error_code abort_ec; ota.abort(abort_ec); - parser.reset(); + dispatcher.reset(); reply_errc(std::errc::no_buffer_space, "RX overflow: frames dropped -- wait for OK replies between frames"); return false; // dropped chunks are gone; skip parse } for (const auto &chunk : chunks) - for (const auto &frame : parser.feed(chunk)) - // this protocol's REQUESTS only: ignore other modules and - // reply-flagged frames (the device answers requests; a reply-typed - // echo must not re-enter the request handler) - if (frame.module == proto::kModule && !frame.is_reply()) - handle_frame(frame); + dispatcher.feed(chunk); if (restart_pending) { // give the final OK reply time to reach the host std::this_thread::sleep_for(750ms); diff --git a/components/dispatcher/README.md b/components/dispatcher/README.md index 5e6ab439b9..7ba39ccc5b 100644 --- a/components/dispatcher/README.md +++ b/components/dispatcher/README.md @@ -23,8 +23,10 @@ built-in protocols use, for example: | Module | Protocol | |-----------|----------------------| | 0 | OTA | +| 2 | BLDC haptics | | 4 | crash dump | | 5 | CAN bridge | +| 6 | MCP266 | | 0xF0–0xFF | reserved (meta) | | 0xFF | capability discovery | From d7ecaa1f901f1dbe232fbc519ae1db53542ace2b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 4 Sep 2026 13:29:25 -0500 Subject: [PATCH 02/10] refactor(bldc_haptics): split OTA -> module 0, core dump -> module 4 Bring the haptics example in line with the other framed-USB examples: instead of multiplexing an OTA subset + a crash-report command into the haptics protocol (module 2), run the STANDARD protocols on their own dispatcher modules so the device is discovered as three modules and the plain ota / coredump web consoles work against it directly. - OTA -> module 0: a handler speaking the espp ota_stream protocol (Begin/Data/ Finish/Abort -> make_ok/make_error), reusing the existing espp::Ota. Removed the Ota* message types from haptics_usb_protocol.hpp + the cases from the haptics handler. - Core dump -> module 4: an espp::CoreDump + espp::CoreDumpService (full download/ erase protocol). The dump is no longer erased at boot (the coredump console downloads + erases it); the boot-time summary log stays. Removed GetCrash/Crash from the haptics protocol + the raw esp_core_dump usage. - Haptics stays module 2 (commands + telemetry only). - Register + advertise all three (OTA / Core Dump / BLDC Haptics); add the coredump component to the example deps. Web console cleanup (drop the now-moved OTA + crash UI) follows in a separate commit. Verified: builds clean, manager-off, IDF v6.0.1 (esp32s3). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../bldc_haptics/example/CMakeLists.txt | 1 + .../bldc_haptics/example/main/CMakeLists.txt | 6 +- .../example/main/bldc_haptics_example.cpp | 223 +++++++++--------- .../example/main/haptics_usb_protocol.hpp | 38 ++- 4 files changed, 136 insertions(+), 132 deletions(-) diff --git a/components/bldc_haptics/example/CMakeLists.txt b/components/bldc_haptics/example/CMakeLists.txt index 0e3133fdd7..53c1e9fa7a 100644 --- a/components/bldc_haptics/example/CMakeLists.txt +++ b/components/bldc_haptics/example/CMakeLists.txt @@ -27,6 +27,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/bldc_motor" "../../../components/bldc_types" "../../../components/cli" + "../../../components/coredump" "../../../components/dispatcher" "../../../components/esp-dsp" "../../../components/filters" diff --git a/components/bldc_haptics/example/main/CMakeLists.txt b/components/bldc_haptics/example/main/CMakeLists.txt index 1fe9442105..9c005466e4 100644 --- a/components/bldc_haptics/example/main/CMakeLists.txt +++ b/components/bldc_haptics/example/main/CMakeLists.txt @@ -1,5 +1,5 @@ idf_component_register(SRC_DIRS "." INCLUDE_DIRS "." - REQUIRES bldc_driver bldc_haptics bldc_motor dispatcher i2c motorgo-axis - motorgo-mini mt6701 ota stream_frame task usb_device esp_tinyusb - esp_timer espcoredump) + REQUIRES bldc_driver bldc_haptics bldc_motor coredump dispatcher i2c + motorgo-axis motorgo-mini mt6701 ota stream_frame task usb_device + esp_tinyusb esp_timer espcoredump) diff --git a/components/bldc_haptics/example/main/bldc_haptics_example.cpp b/components/bldc_haptics/example/main/bldc_haptics_example.cpp index aa93dafb94..6b511e91f6 100644 --- a/components/bldc_haptics/example/main/bldc_haptics_example.cpp +++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp @@ -12,7 +12,6 @@ #include #include -#include "esp_core_dump.h" #include "esp_system.h" #include "esp_timer.h" #include "tusb_cdc_acm.h" @@ -23,6 +22,9 @@ #include "bldc_driver.hpp" #include "bldc_haptics.hpp" #include "bldc_motor.hpp" +#include "coredump.hpp" +#include "coredump_service.hpp" +#include "detail/ota_stream_protocol.hpp" #include "dispatcher.hpp" #include "i2c.hpp" #include "mt6701.hpp" @@ -113,53 +115,23 @@ extern "C" void app_main(void) { namespace proto = haptics_proto; // -------------------------------------------------------------------------- - // Last-crash report. TinyUSB owns the S3's only USB PHY, so there is no live - // USB-Serial-JTAG console and a panic backtrace cannot be watched directly; - // instead panics core-dump to flash (see sdkconfig/partitions) and THIS boot - // summarizes the previous crash - over the CDC banner below, the console, - // and the GET_CRASH protocol command (shown in the web console's log). + // Core dump: the last-crash summary (logged at boot + shown on the CDC banner + // below) plus the module-4 download service further down. TinyUSB owns the + // S3's only USB PHY, so there is no live USB-Serial-JTAG console to watch a + // panic backtrace on; panics core-dump to flash (see sdkconfig/partitions). + // The dump is intentionally NOT erased here -- the coredump web console + // downloads it (and erases on success) over the module-4 protocol. // -------------------------------------------------------------------------- - std::string crash_report; - { - const esp_reset_reason_t reset_reason = esp_reset_reason(); - const char *reset_names[] = {"UNKNOWN", "POWERON", "EXT", "SW", "PANIC", - "INT_WDT", "TASK_WDT", "WDT", "DEEPSLEEP", "BROWNOUT", - "SDIO", "USB", "JTAG"}; - const auto reason_index = static_cast(reset_reason); - const char *reason_name = - reason_index < std::size(reset_names) ? reset_names[reason_index] : "?"; - logger.info("Reset reason: {} ({})", reason_name, static_cast(reset_reason)); - if (esp_core_dump_image_check() == ESP_OK) { - esp_core_dump_summary_t summary = {}; - if (esp_core_dump_get_summary(&summary) == ESP_OK) { - crash_report = fmt::format("last reset: {} | crashed task '{}' PC=0x{:08x}", reason_name, - summary.exc_task, summary.exc_pc); - crash_report += " | backtrace:"; - const auto depth = - std::min(summary.exc_bt_info.depth, std::size(summary.exc_bt_info.bt)); - for (uint32_t i = 0; i < depth; i++) - crash_report += fmt::format(" 0x{:08x}", summary.exc_bt_info.bt[i]); - if (summary.exc_bt_info.corrupted) - crash_report += " (corrupted)"; - crash_report += - "\ndecode with: xtensa-esp32s3-elf-addr2line -pfiaC -e build/bldc_haptics_example.elf " - ""; - logger.error("Previous crash detected: {}", crash_report); - // The flash dump persists until erased; without this every subsequent - // clean boot would keep re-reporting the same old crash (mislabeled - // with the CURRENT reset reason). The summary above is the advertised - // decode path, so consume the dump now that it is cached in RAM. - if (esp_core_dump_image_erase() != ESP_OK) - logger.warn("Failed to erase the consumed core dump image"); - } - } else if (reset_reason == ESP_RST_BROWNOUT || reset_reason == ESP_RST_INT_WDT || - reset_reason == ESP_RST_TASK_WDT) { - // no core dump is written for these, but the reason itself is the story - crash_report = fmt::format( - "last reset: {} (no core dump: {})", reason_name, - reset_reason == ESP_RST_BROWNOUT ? "brownout - check motor/USB power" : "watchdog reset"); - logger.error("Previous abnormal reset: {}", crash_report); - } + espp::CoreDump core_dump({.log_level = espp::Logger::Verbosity::INFO}); + const std::string crash_report = core_dump.format_report(); + if (crash_report.empty()) { + logger.info("Clean boot (reset reason: {})", + espp::CoreDump::reset_reason_name(espp::CoreDump::reset_reason())); + } else { + logger.error("Previous abnormal reset:\n{}", crash_report); + if (core_dump.has_core_dump()) + logger.info("Core dump in flash ({} bytes) -- download it with the coredump web console", + core_dump.image_size()); } // -------------------------------------------------------------------------- @@ -523,55 +495,6 @@ extern "C" void app_main(void) { auto handle_frame = [&](const proto::stream::Frame &frame) { std::error_code ec; switch (static_cast(frame.type)) { - // --- OTA subset ---------------------------------------------------------- - case proto::Msg::OtaBegin: { - const auto image_size = proto::stream::parse_u32_payload(frame); - if (!image_size.has_value()) { - reply_errc(std::errc::invalid_argument, "malformed OTA BEGIN"); - break; - } - if (ota.begin(*image_size, ec)) - reply_ok(0); - else - reply_error(ec, "OTA begin failed"); - break; - } - case proto::Msg::OtaData: - if (!ota.session_active()) { - reply_errc(std::errc::operation_not_permitted, "no update session (send BEGIN first)"); - break; - } - if (ota.write(frame.payload, ec)) - reply_ok(static_cast(ota.bytes_written())); - else - reply_error(ec, "OTA write failed"); // write() aborted the session on failure - break; - case proto::Msg::OtaFinish: { - if (!ota.session_active()) { - reply_errc(std::errc::operation_not_permitted, "no update session (send BEGIN first)"); - break; - } - const auto written = static_cast(ota.bytes_written()); - if (ota.finish(ec)) { - reply_ok(written); - restart_pending = true; // reply first; the worker restarts shortly - } else { - reply_error(ec, "OTA finish (validate/activate) failed"); - } - break; - } - case proto::Msg::OtaAbort: { - if (!ota.session_active()) { - reply_errc(std::errc::operation_not_permitted, "no update session to abort"); - break; - } - const auto written = static_cast(ota.bytes_written()); - if (ota.abort(ec)) - reply_ok(written); - else - reply_error(ec, "OTA abort failed"); - break; - } // --- Haptics commands ---------------------------------------------------- case proto::Msg::GetInfo: send_info(); @@ -582,11 +505,6 @@ extern "C" void app_main(void) { case proto::Msg::GetModes: send_modes(); break; - case proto::Msg::GetCrash: { - std::vector payload(crash_report.begin(), crash_report.end()); - usb_send(proto::build(proto::Msg::Crash, payload)); - break; - } case proto::Msg::SetMode: { if (frame.payload.size() != 1 || frame.payload[0] >= kPresets.size()) { reply_errc(std::errc::invalid_argument, "SET_MODE needs a valid u8 mode index"); @@ -682,10 +600,103 @@ extern "C" void app_main(void) { } }; - // Register the haptics protocol on the dispatcher (module 2) and advertise it - // for capability discovery, so the browser Device Hub can list and link it. - // The Dispatcher routes by module and hands us the whole frame; we still gate - // on !is_reply() so a reply-typed echo cannot re-enter the request handler. + // This example carries THREE protocols over the one vendor stream, each on its + // own dispatcher module and each advertised for capability discovery so the + // browser Device Hub lists and links them: + // module 0 -> OTA (standard espp ota_stream protocol -> ota_console) + // module 2 -> BLDC haptics (this example's protocol -> haptics_console) + // module 4 -> core dump (espp::CoreDumpService -> coredump_console) + // Every handler gates on !is_reply() so a reply-typed echo cannot re-enter it. + namespace otap = espp::detail::ota_stream; + + // --- OTA (module 0): same handling as the espp ota example, over this one + // (USB-vendor) transport, so a plain ota_console can update this device. + auto ota_error = [&](const std::error_code &err, const std::string &ctx) { + usb_send(otap::make_error(static_cast(err.value()), ctx + ": " + err.message())); + }; + auto handle_ota_frame = [&](const espp::stream_frame::Frame &frame) { + std::error_code ec; + switch (static_cast(frame.type)) { + case otap::MessageType::Begin: { + const auto image_size = otap::parse_u32_payload(frame); + if (!image_size.has_value()) { + ota_error(std::make_error_code(std::errc::invalid_argument), "malformed BEGIN"); + break; + } + if (ota.begin(*image_size, ec)) + usb_send(otap::make_ok(0)); + else + ota_error(ec, "OTA begin failed"); + break; + } + case otap::MessageType::Data: + if (!ota.session_active()) { + ota_error(std::make_error_code(std::errc::operation_not_permitted), + "no update session (send BEGIN first)"); + break; + } + if (ota.write(frame.payload, ec)) + usb_send(otap::make_ok(static_cast(ota.bytes_written()))); + else + ota_error(ec, "OTA write failed"); // write() aborted the session on failure + break; + case otap::MessageType::Finish: { + if (!ota.session_active()) { + ota_error(std::make_error_code(std::errc::operation_not_permitted), + "no update session (send BEGIN first)"); + break; + } + const auto written = static_cast(ota.bytes_written()); + if (ota.finish(ec)) { + usb_send(otap::make_ok(written)); + restart_pending = true; // reply first; the worker restarts shortly + } else { + ota_error(ec, "OTA finish (validate/activate) failed"); + } + break; + } + case otap::MessageType::Abort: { + if (!ota.session_active()) { + ota_error(std::make_error_code(std::errc::operation_not_permitted), + "no update session to abort"); + break; + } + const auto written = static_cast(ota.bytes_written()); + if (ota.abort(ec)) + usb_send(otap::make_ok(written)); + else + ota_error(ec, "OTA abort failed"); + break; + } + default: + ota_error(std::make_error_code(std::errc::not_supported), "unknown OTA message"); + break; + } + }; + + // --- Core dump (module 4): the CoreDumpService serves the flash dump over the + // standard protocol, so a plain coredump_console can download / erase it. + espp::CoreDumpService coredump_service( + core_dump, + {.send = + [&](std::span f) { usb_send(std::vector(f.begin(), f.end())); }, + .log_level = espp::Logger::Verbosity::INFO}); + + dispatcher.register_module( + otap::kModule, + [&](const espp::stream_frame::Frame &frame) { + if (!frame.is_reply()) + handle_ota_frame(frame); + }, + {.name = "OTA", .app = "ota_console.html", .description = "Firmware update over USB"}); + dispatcher.register_module(espp::CoreDumpService::kModule, + [&](const espp::stream_frame::Frame &frame) { + if (!frame.is_reply()) + coredump_service.handle_frame(frame.type, frame.payload); + }, + {.name = "Core Dump", + .app = "coredump_console.html", + .description = "Inspect the last crash core dump"}); dispatcher.register_module(proto::kModule, [&](const proto::stream::Frame &frame) { if (!frame.is_reply()) @@ -693,7 +704,7 @@ extern "C" void app_main(void) { }, {.name = "BLDC Haptics", .app = "haptics_console.html", - .description = "Haptic feedback modes + firmware update"}); + .description = "Haptic detent / feedback modes"}); dispatcher.set_device_info(usb_cfg.product); // serve_discovery answers the reserved 0xFF module; route its reply through the // same tx_mutex-guarded usb_send as every other frame. diff --git a/components/bldc_haptics/example/main/haptics_usb_protocol.hpp b/components/bldc_haptics/example/main/haptics_usb_protocol.hpp index 28640c81d8..8e7250792a 100644 --- a/components/bldc_haptics/example/main/haptics_usb_protocol.hpp +++ b/components/bldc_haptics/example/main/haptics_usb_protocol.hpp @@ -7,13 +7,15 @@ // spec and ../PROTOCOL.md next to this example for the full haptics wire // protocol). // -// The whole protocol occupies dispatcher MODULE 2. The `type` byte carries the -// message id below; request types (host->device) clear the frame reply flag and -// reply/telemetry types (0x8_/0x9_, host<-device) set it (build() derives it -// from the type's high bit): -// 0x01..0x04 host -> device OTA (BEGIN / DATA / FINISH / ABORT) +// The haptics protocol occupies dispatcher MODULE 2 (haptics commands only). +// Firmware update and crash-dump inspection are NOT part of it: the example runs +// the standard espp OTA protocol on module 0 and the coredump service on module +// 4 (routed by the same espp::Dispatcher), handled by the ota / coredump web +// consoles. The `type` byte carries the message id below; request types +// (host->device) clear the frame reply flag and reply/telemetry types (0x9_, +// host<-device) set it (build() derives it from the type's high bit): // 0x10..0x2F host -> device haptics commands -// 0x81..0x8F device -> host generic + OTA replies (OK / ERROR / PROGRESS) +// 0x81..0x8F device -> host generic replies (OK / ERROR) // 0x90..0xAF device -> host haptics replies + telemetry #include @@ -24,16 +26,14 @@ #include #include -#include "detail/ota_stream_protocol.hpp" #include "stream_frame.hpp" namespace haptics_proto { -// The ota_stream facade re-exports the stream_frame codec (StreamParser / Frame -// / put_* / get_* / parse_u32_payload); build() below uses the generic -// stream_frame builder directly so it can set this protocol's module + reply -// flag. -namespace stream = espp::detail::ota_stream; +// The little-endian payload helpers (put_* / get_*) come straight from the +// stream_frame codec; build() below uses its frame builder so it can set this +// protocol's module + reply flag. +namespace stream = espp::stream_frame; /// Dispatcher module id owned by the haptics protocol (the frame `module` byte). static constexpr uint8_t kModule = 2; @@ -43,11 +43,6 @@ static constexpr uint8_t kProtocolVersion = 1; /// Message types carried in the frame `type` byte (within module 2). enum class Msg : uint8_t { - // --- OTA subset (identical semantics to the espp ota example) ------------- - OtaBegin = 0x01, ///< host->dev: u32 image_size (0 = unknown / streaming) - OtaData = 0x02, ///< host->dev: raw image bytes (<= 4096 per frame) - OtaFinish = 0x03, ///< host->dev: validate + activate the received image - OtaAbort = 0x04, ///< host->dev: discard the in-progress session // --- Haptics commands ------------------------------------------------------ GetInfo = 0x10, ///< host->dev: no payload -> Info reply GetStatus = 0x11, ///< host->dev: no payload -> Status reply @@ -57,17 +52,14 @@ enum class Msg : uint8_t { SetEnabled = 0x15, ///< host->dev: u8 0/1 -> Ok(0/1) PlayHaptic = 0x16, ///< host->dev: f32 strength -> Ok(0) SetStreaming = 0x17, ///< host->dev: u8 0/1 + u16 period_ms -> Ok(period_ms) - GetCrash = 0x18, ///< host->dev: no payload -> Crash reply - // --- Generic / OTA replies ------------------------------------------------- - Ok = 0x81, ///< dev->host: u32 context-dependent value - Error = 0x82, ///< dev->host: u32 code (std::errc) + utf8 message - OtaProgress = 0x83, ///< dev->host: u32 written + u32 total (informational) + // --- Generic replies ------------------------------------------------------- + Ok = 0x81, ///< dev->host: u32 context-dependent value + Error = 0x82, ///< dev->host: u32 code (std::errc) + utf8 message // --- Haptics replies / telemetry ------------------------------------------- Info = 0x90, ///< dev->host: protocol version + firmware description Status = 0x91, ///< dev->host: full status snapshot Modes = 0x92, ///< dev->host: enumeration of the detent presets Telemetry = 0x93, ///< dev->host: periodic position/detent frame (streaming) - Crash = 0x94, ///< dev->host: utf8 crash report text (empty = clean boot history) }; // --------------------------------------------------------------------------- From 6feaae69c23e9fb9e1a295e8689dcd1c2bae70b8 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Fri, 4 Sep 2026 13:37:34 -0500 Subject: [PATCH 03/10] refactor(bldc_haptics): drop inline OTA/crash from the haptics console Now that OTA is dispatcher module 0 and core dump is module 4 (not multiplexed into the haptics protocol), remove their UI from the haptics console: the device hub discovers all three modules and links to the standard ota_console / coredump_console, which speak those modules directly. - webapp/index.html: remove the Firmware-update (OTA) panel + upload flow, the GET_CRASH fetch, the OTA/crash message types + progress plumbing, and the now- unused constants/CSS. Haptics controls + telemetry are unchanged. - PROTOCOL.md: module 2 now documents haptics only; note OTA=module 0 / coredump=module 4 as separate standard protocols. - README.md: point firmware update / crash inspection at the ota / coredump consoles (and the device hub) instead of an inline panel. Web app passes node --check; no dangling references remain. Co-Authored-By: Claude Opus 4.8 (1M context) --- components/bldc_haptics/example/PROTOCOL.md | 38 +-- components/bldc_haptics/example/README.md | 48 ++-- .../bldc_haptics/example/webapp/index.html | 221 ++---------------- 3 files changed, 45 insertions(+), 262 deletions(-) diff --git a/components/bldc_haptics/example/PROTOCOL.md b/components/bldc_haptics/example/PROTOCOL.md index f18d9b7126..4cbe02fc8f 100644 --- a/components/bldc_haptics/example/PROTOCOL.md +++ b/components/bldc_haptics/example/PROTOCOL.md @@ -44,10 +44,12 @@ reply (`OK` / `ERROR`, or the type-specific reply for the getters) before sending the next. Two device-to-host frame kinds may arrive *unsolicited* and must be tolerated at any time: -- `TELEMETRY (0x93)` — when streaming is enabled; -- `OTA_PROGRESS (0x83)` — informational during an OTA transfer. +- `TELEMETRY (0x93)` — when streaming is enabled. -The device suspends telemetry while an OTA session is active. +> Firmware update and crash-dump inspection are **not** part of this protocol: +> the example runs the standard espp OTA protocol on dispatcher **module 0** and +> the coredump service on **module 4** (use the `ota` / `coredump` web consoles, +> or the device hub, which discovers all three modules). ### Primitive types @@ -59,10 +61,6 @@ The device suspends telemetry while an OTA session is active. | Type | Name | Payload | Reply | |------|---------------|-------------------------------------------|-------| -| 0x01 | OTA_BEGIN | `u32 image_size` (0 = unknown/streaming) | OK(0) / ERROR | -| 0x02 | OTA_DATA | raw image bytes (1..4096) | OK(total bytes written) / ERROR | -| 0x03 | OTA_FINISH | — | OK(total bytes written) / ERROR | -| 0x04 | OTA_ABORT | — | OK(bytes written) / ERROR | | 0x10 | GET_INFO | — | INFO | | 0x11 | GET_STATUS | — | STATUS | | 0x12 | GET_MODES | — | MODES | @@ -71,20 +69,9 @@ The device suspends telemetry while an OTA session is active. | 0x15 | SET_ENABLED | `u8` 0 = disable, 1 = enable | OK(0/1) / ERROR | | 0x16 | PLAY_HAPTIC | `f32 strength` (clamped to 0..10) | OK(0) / ERROR | | 0x17 | SET_STREAMING | `u8 enable` + `u16 period_ms` (5..1000; 0 = default 20) | OK(period_ms) / ERROR | -| 0x18 | GET_CRASH | none | CRASH | Notes: -- **OTA** semantics match the espp `ota` example, but the frames are **not** - byte-compatible: the haptics OTA subset rides dispatcher module 2, whereas the - `ota` example / `ota_console.html` use module 0. `OTA_BEGIN` erases - the next OTA app partition (can take several seconds — use a generous - timeout), `OTA_DATA` streams image bytes, `OTA_FINISH` validates the complete - image (structure + appended SHA-256) and sets it as the boot partition, then - the device **reboots ~750 ms after replying OK** (expect a USB disconnect). - With bootloader rollback enabled the new app must mark itself valid on first - boot or the bootloader rolls back. `OTA_DATA`/`OTA_FINISH`/`OTA_ABORT` - without an active session yield `ERROR(operation_not_permitted)`. - `SET_POSITION` re-labels the detent the knob is currently resting in: it sets the *logical* detent index (clamped to the active config's `[min_position, max_position]`) that position/telemetry values count from. @@ -106,10 +93,6 @@ Notes: `u32 code` (a `std::errc` value) followed by a UTF-8 message. -### OTA_PROGRESS (0x83) - -`u32 written` + `u32 total` (0 if unknown). Informational; may be ignored. - ### INFO (0x90) Reply to GET_INFO: @@ -194,14 +177,3 @@ detent index plus the fractional progress toward the neighboring detent, and it **decreases as the shaft angle increases** (the firmware's snap convention). `value` spans `[min_position, max_position]` for bounded modes. This is what the web app's dial renders. - -### CRASH (0x94) - -Reply to `GET_CRASH`. The payload is a UTF-8 text report of the previous -abnormal reset, or EMPTY when the boot history is clean. When the previous -reset was a panic with a flash core dump, the report includes the crashed -task, PC, and raw backtrace addresses (decode with -`xtensa-esp32s3-elf-addr2line -pfiaC -e build/bldc_haptics_example.elf `); -brownout / watchdog resets are reported by reason (no core dump exists for -those). The web console requests this automatically after connecting and -prints the report in its log pane. diff --git a/components/bldc_haptics/example/README.md b/components/bldc_haptics/example/README.md index 13e10af44c..0747502dfd 100644 --- a/components/bldc_haptics/example/README.md +++ b/components/bldc_haptics/example/README.md @@ -18,9 +18,10 @@ install: * **Mode switching** — select any of the built-in `espp::detail` detent presets (unbounded, bounded, multi-rev, on/off, coarse/fine, magnetic detents, return-to-center) from a dropdown. -* **Firmware update (OTA)** — upload a new `.bin` over the same USB interface - (via the espp `ota` component), with progress, image validation (SHA-256) and - bootloader rollback support. +* **Firmware update + crash inspection** — the same USB link also serves the + standard espp OTA protocol (dispatcher module 0) and the coredump service + (module 4), so `ota_console.html` and `coredump_console.html` — or the device + hub, which discovers all three modules — work against this device directly. The wire protocol is documented in [PROTOCOL.md](./PROTOCOL.md); the browser console lives in [webapp/index.html](./webapp/index.html). @@ -70,7 +71,7 @@ otadata. > routes the system console to it, so attach any serial terminal (e.g. > `screen /dev/tty.usbmodem*`) for live logs. Flashing also still works over > the same connector via the ROM bootloader (hold BOOT while resetting, or -> just use `webapp/index.html` for OTA updates after the first flash). +> just use `ota_console.html` for OTA updates after the first flash). ### Web console @@ -86,23 +87,23 @@ otadata. switch detent presets, enable/disable the motor, move to a detent, or play a haptic click. -### Firmware update (OTA) flow +### Firmware update + crash inspection -1. Make a change and `idf.py build` (do not flash). -2. In the web console's **Firmware update** panel, pick - `build/bldc_haptics_example.bin` (the app image — NOT the merged / - bootloader image) and click **Upload**. -3. The device streams the image into the inactive OTA slot (progress + rate are - shown), validates it (structure + SHA-256), switches the boot partition and - reboots. Expect a USB disconnect; reconnect after the device re-enumerates. -4. Rollback: the freshly-booted image starts in `PENDING_VERIFY`; this example - marks itself valid after its self-check (motor + haptics up). If the new - image crashes before that, the bootloader automatically rolls back to the - previous slot on the next reset. +OTA and crash-dump download are the **standard** espp protocols on their own +dispatcher modules (not part of the haptics protocol), so the plain consoles +work against this device: -The OTA subset is part of the haptics protocol on **dispatcher module 2**, so it -is *not* interchangeable with the generic espp `ota` example (which is module 0) -— use this example's own web console for OTA here. +- **OTA (module 0)**: build (do not flash), then open `ota_console.html`, + connect, and upload `build/bldc_haptics_example.bin` (the app image — NOT the + merged / bootloader image). The device streams it into the inactive OTA slot, + validates it (structure + SHA-256), switches the boot partition and reboots + (expect a USB disconnect). The freshly-booted image starts in `PENDING_VERIFY` + and marks itself valid after its self-check; a crash before that rolls back. +- **Core dump (module 4)**: after an abnormal reset, open `coredump_console.html` + to download / erase the flash core dump (the boot log also prints a summary). + +The **device hub** (`dispatcher_hub.html`) discovers all three modules on this +one device and links to each console. ## Example Behaviors @@ -158,10 +159,11 @@ components: * `espp::BldcHaptics` * `espp::UsbDevice` — native USB vendor interface with WebUSB + MS OS 2.0 descriptors (driverless browser access) -* `espp::Ota` — transport-agnostic OTA engine fed from the USB protocol -* The `stream_frame` codec (`components/stream_frame/include/stream_frame.hpp`) - as the framing layer for the haptics protocol (module 2) - (see [PROTOCOL.md](./PROTOCOL.md)) +* `espp::Ota` — transport-agnostic OTA engine (served on module 0) +* `espp::CoreDump` / `espp::CoreDumpService` — crash core-dump access (module 4) +* `espp::Dispatcher` + the `stream_frame` codec — route the vendor stream to the + OTA (0), haptics (2) and coredump (4) modules, each advertised for capability + discovery (see [PROTOCOL.md](./PROTOCOL.md)) You combine the `Mt6701` and `BldcDriver` together when creating the `BldcMotor` and then simply pass the `BldcMotor` to the `BldcHaptics` component. At that diff --git a/components/bldc_haptics/example/webapp/index.html b/components/bldc_haptics/example/webapp/index.html index 2d9b862b6a..991c91edaa 100644 --- a/components/bldc_haptics/example/webapp/index.html +++ b/components/bldc_haptics/example/webapp/index.html @@ -4,7 +4,7 @@ espp BLDC Haptics Console (WebUSB) - +