diff --git a/components/bldc_haptics/example/CMakeLists.txt b/components/bldc_haptics/example/CMakeLists.txt index 09f3a7efbf..53c1e9fa7a 100644 --- a/components/bldc_haptics/example/CMakeLists.txt +++ b/components/bldc_haptics/example/CMakeLists.txt @@ -27,6 +27,8 @@ set(EXTRA_COMPONENT_DIRS "../../../components/bldc_motor" "../../../components/bldc_types" "../../../components/cli" + "../../../components/coredump" + "../../../components/dispatcher" "../../../components/esp-dsp" "../../../components/filters" "../../../components/format" 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/main/CMakeLists.txt b/components/bldc_haptics/example/main/CMakeLists.txt index 59ee5a5d07..9c005466e4 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 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 116a16b6d6..8fcb6e7729 100644 --- a/components/bldc_haptics/example/main/bldc_haptics_example.cpp +++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp @@ -9,9 +9,9 @@ #include #include #include +#include #include -#include "esp_core_dump.h" #include "esp_system.h" #include "esp_timer.h" #include "tusb_cdc_acm.h" @@ -22,6 +22,10 @@ #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" #include "ota.hpp" @@ -111,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 can + // download and explicitly erase it 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()); } // -------------------------------------------------------------------------- @@ -383,7 +357,10 @@ extern "C" void app_main(void) { // The vendor TX path is written to from two tasks (protocol worker replies + // telemetry), so serialize the writes. std::mutex usb_tx_mutex; - auto usb_send = [&](const std::vector &frame) { + // 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) { if (frame.empty()) return; std::lock_guard lk(usb_tx_mutex); @@ -451,7 +428,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 @@ -516,57 +496,7 @@ 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(); @@ -577,11 +507,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"); @@ -677,6 +602,114 @@ extern "C" void app_main(void) { } }; + // 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(f); }, + .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()) + handle_frame(frame); + }, + {.name = "BLDC Haptics", + .app = "haptics_console.html", + .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. + dispatcher.serve_discovery([&](std::span f) { usb_send(f); }); + espp::Task usb_task( {.callback = [&](std::mutex &, std::condition_variable &) -> bool { std::vector> chunks; @@ -694,20 +727,25 @@ extern "C" void app_main(void) { } if (overflowed) { // Bytes were dropped: any in-flight frame / OTA image is unusable. + // Report the drop on the module whose transfer was in flight so the + // driving host console sees it immediately: an active OTA session + // means ota_console (module 0) is uploading, otherwise it is a + // haptics command (module 2). Sending the wrong module's error would + // be ignored by the host, which would then wait for its own timeout. + const bool ota_was_active = ota.session_active(); std::error_code abort_ec; ota.abort(abort_ec); - parser.reset(); - reply_errc(std::errc::no_buffer_space, - "RX overflow: frames dropped -- wait for OK replies between frames"); + dispatcher.reset(); + if (ota_was_active) + ota_error(std::make_error_code(std::errc::no_buffer_space), + "RX overflow: frames dropped, update aborted"); + else + 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/bldc_haptics/example/main/haptics_usb_protocol.hpp b/components/bldc_haptics/example/main/haptics_usb_protocol.hpp index 28640c81d8..fa186ea013 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 (0x8_/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) }; // --------------------------------------------------------------------------- diff --git a/components/bldc_haptics/example/webapp/index.html b/components/bldc_haptics/example/webapp/index.html index 2d9b862b6a..3929da9b97 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) - +