diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 30d371ca32..bfae3c30ac 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -119,6 +119,8 @@ jobs: target: esp32s3 - path: 'components/csv/example' target: esp32 + - path: 'components/dispatcher/example' + target: esp32 - path: 'components/display_drivers/example' target: esp32 - path: 'components/dns_server/example' @@ -285,6 +287,8 @@ jobs: target: esp32s3 - path: 'components/state_machine/example' target: esp32 + - path: 'components/stream_frame/example' + target: esp32 - path: 'components/sx126x/example' target: esp32s3 - path: 'components/t-deck/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index b984fcb2fd..8092b6f79b 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -72,9 +72,15 @@ jobs: components/codec components/color components/controller + # stream_frame is intentionally listed here (out of alphabetical + # order) ahead of its first-time dependents coredump / dispatcher / + # ota, so it is uploaded before they try to resolve it from the + # registry (see the note above). + components/stream_frame components/coredump components/cst816 components/csv + components/dispatcher components/display components/display_drivers components/dns_server diff --git a/components/bldc_haptics/example/CMakeLists.txt b/components/bldc_haptics/example/CMakeLists.txt index 595b7f0e51..fc31b660ef 100644 --- a/components/bldc_haptics/example/CMakeLists.txt +++ b/components/bldc_haptics/example/CMakeLists.txt @@ -43,6 +43,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/ota" "../../../components/pid" "../../../components/spi" + "../../../components/stream_frame" "../../../components/task" "../../../components/timer" "../../../components/usb_device" @@ -50,7 +51,7 @@ set(EXTRA_COMPONENT_DIRS set( COMPONENTS - "main esptool_py bldc_driver bldc_haptics bldc_motor i2c motorgo-axis motorgo-mini mt6701 ota task usb_device esp_tinyusb" + "main esptool_py bldc_driver bldc_haptics bldc_motor i2c motorgo-axis motorgo-mini mt6701 ota stream_frame task usb_device esp_tinyusb" CACHE STRING "List of components to include" ) diff --git a/components/bldc_haptics/example/PROTOCOL.md b/components/bldc_haptics/example/PROTOCOL.md index 21948954a9..f18d9b7126 100644 --- a/components/bldc_haptics/example/PROTOCOL.md +++ b/components/bldc_haptics/example/PROTOCOL.md @@ -12,20 +12,26 @@ for the reference host implementation. ## Framing -Identical to the espp `ota` component's stream framing -(`components/ota/include/detail/ota_stream_protocol.hpp` is the authoritative -spec). All multi-byte fields are **little-endian**: +Uses the espp `stream_frame` v2 codec +(`components/stream_frame/include/stream_frame.hpp` is the authoritative spec). +The whole haptics protocol is dispatcher **module 2**. All multi-byte fields are +**little-endian**: ``` -[magic u16 = 0x4F54 "OT"] [type u8] [len u32] [payload: len bytes] [crc32 u32] +[magic u16 = 0x4F54 "OT"] [flags u8] [module u8] [type u8] [len u32] [payload: len bytes] [crc32 u32] ``` - `magic`: u16 `0x4F54`; on the wire the bytes are `0x54 'T'` then `0x4F 'O'`. +- `flags`: `bit0` = reply (`0` = host→device request, `1` = device→host + reply/event); `bits 4-7` = protocol version = `1`. So a request byte is + `0x10` and a reply/telemetry byte is `0x11`. Request types (`0x0_`/`0x1_`) + clear the reply bit; reply/telemetry types (`0x8_`/`0x9_`) set it. +- `module`: `u8` dispatcher module — **2** for the entire haptics protocol. - `type`: message type (tables below). - `len`: payload length, capped at **4096** bytes per frame; receivers reject and resynchronize past any frame whose length field exceeds the cap. - `crc32`: standard zlib CRC-32 (poly `0xEDB88320` reflected, init/final xor - `0xFFFFFFFF`) over `magic..payload` (i.e. the 7 header bytes + payload). + `0xFFFFFFFF`) over `magic..payload` (i.e. the 9 header bytes + payload). Golden check value: `crc32("123456789") == 0xCBF43926`. Receivers parse incrementally and resynchronize on bad magic / oversized @@ -69,7 +75,9 @@ The device suspends telemetry while an OTA session is active. Notes: -- **OTA** semantics are identical to the espp `ota` example: `OTA_BEGIN` erases +- **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 diff --git a/components/bldc_haptics/example/README.md b/components/bldc_haptics/example/README.md index 063029fea9..13e10af44c 100644 --- a/components/bldc_haptics/example/README.md +++ b/components/bldc_haptics/example/README.md @@ -100,9 +100,9 @@ otadata. image crashes before that, the bootloader automatically rolls back to the previous slot on the next reset. -The same OTA transfer can also be driven from the generic espp OTA console -(`components/ota/web/ota_console.html`), since the OTA subset of the protocol -is byte-compatible with the espp `ota` example. +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. ## Example Behaviors @@ -159,8 +159,8 @@ components: * `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 `ota_stream` framing (`components/ota/include/detail/ota_stream_protocol.hpp`) - reused as the framing layer for the haptics 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)) You combine the `Mt6701` and `BldcDriver` together when creating the `BldcMotor` diff --git a/components/bldc_haptics/example/main/CMakeLists.txt b/components/bldc_haptics/example/main/CMakeLists.txt index 5efbbb3ce8..59ee5a5d07 100644 --- a/components/bldc_haptics/example/main/CMakeLists.txt +++ b/components/bldc_haptics/example/main/CMakeLists.txt @@ -1,4 +1,4 @@ idf_component_register(SRC_DIRS "." INCLUDE_DIRS "." REQUIRES bldc_driver bldc_haptics bldc_motor i2c motorgo-axis motorgo-mini - mt6701 ota task usb_device esp_tinyusb esp_timer espcoredump) + 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 37d61f8c43..116a16b6d6 100644 --- a/components/bldc_haptics/example/main/bldc_haptics_example.cpp +++ b/components/bldc_haptics/example/main/bldc_haptics_example.cpp @@ -454,10 +454,19 @@ extern "C" void app_main(void) { proto::stream::StreamParser parser; bool restart_pending = false; - auto reply_ok = [&](uint32_t value) { usb_send(proto::stream::make_ok(value)); }; + // Build replies via proto::build so they carry the haptics module (2) + reply + // flag — NOT the OTA make_ok/make_error (those are OTA module 0). + auto reply_ok = [&](uint32_t value) { + std::vector payload; + proto::put_u32(payload, value); + usb_send(proto::build(proto::Msg::Ok, payload)); + }; auto reply_error = [&](const std::error_code &err, const std::string &context) { - usb_send(proto::stream::make_error(static_cast(err.value()), - context + ": " + err.message())); + std::vector payload; + proto::put_u32(payload, static_cast(err.value())); + const std::string message = context + ": " + err.message(); + payload.insert(payload.end(), message.begin(), message.end()); + usb_send(proto::build(proto::Msg::Error, payload)); }; auto reply_errc = [&](std::errc errc, const std::string &context) { reply_error(std::make_error_code(errc), context); @@ -688,14 +697,17 @@ extern "C" void app_main(void) { std::error_code abort_ec; ota.abort(abort_ec); parser.reset(); - usb_send(proto::stream::make_error( - static_cast(std::make_error_code(std::errc::no_buffer_space).value()), - "RX overflow: frames dropped -- wait for OK replies between frames")); + 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)) - handle_frame(frame); + // 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); 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 24549e2e28..28640c81d8 100644 --- a/components/bldc_haptics/example/main/haptics_usb_protocol.hpp +++ b/components/bldc_haptics/example/main/haptics_usb_protocol.hpp @@ -1,13 +1,16 @@ #pragma once // espp BLDC haptics USB protocol — message ids + payload helpers layered on the -// espp `ota_stream` framing (magic "OT" + type u8 + len u32 + payload + CRC-32, -// all little-endian; see components/ota/include/detail/ota_stream_protocol.hpp -// for the authoritative framing spec and ../PROTOCOL.md next to this example -// for the full haptics wire protocol). +// espp `stream_frame` codec (magic "OT" + flags u8 + module u8 + type u8 + len +// u32 + payload + CRC-32, all little-endian; see +// components/stream_frame/include/stream_frame.hpp for the authoritative framing +// spec and ../PROTOCOL.md next to this example for the full haptics wire +// protocol). // -// The message-type space is partitioned so the OTA subset stays byte-compatible -// with the espp `ota` example / ota_console.html web app: +// 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) // 0x10..0x2F host -> device haptics commands // 0x81..0x8F device -> host generic + OTA replies (OK / ERROR / PROGRESS) @@ -22,15 +25,23 @@ #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; +/// Dispatcher module id owned by the haptics protocol (the frame `module` byte). +static constexpr uint8_t kModule = 2; + /// Protocol version reported in the INFO reply. static constexpr uint8_t kProtocolVersion = 1; -/// Message types carried in the ota_stream frame `type` byte. +/// 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) @@ -100,9 +111,11 @@ inline std::optional get_f32_at(std::span bytes, size_t of return std::bit_cast(get_u32(bytes.subspan(offset))); } -/// Build a frame for any haptics-protocol message type. +/// Build a frame for any haptics-protocol message type (module 2; the reply flag +/// is set for reply/telemetry types, whose ids have the high bit set). inline std::vector build(Msg type, std::span payload = {}) { - return stream::build_frame(static_cast(type), payload); + const bool reply = (static_cast(type) & 0x80) != 0; + return espp::stream_frame::build_frame(reply, kModule, static_cast(type), payload); } /// Status flag bits (Status + Telemetry `flags` byte). diff --git a/components/bldc_haptics/example/webapp/index.html b/components/bldc_haptics/example/webapp/index.html index 8a7ab33e9d..2d9b862b6a 100644 --- a/components/bldc_haptics/example/webapp/index.html +++ b/components/bldc_haptics/example/webapp/index.html @@ -10,13 +10,18 @@ ================================== Single-file, fully offline, dependency-free browser console for the espp bldc_haptics USB example. Speaks the framed vendor-interface protocol - documented in ../PROTOCOL.md: + documented in ../PROTOCOL.md (dispatcher framing v2): - [magic u16 = 0x4F54 "OT"][type u8][len u32][payload...][crc32 u32] + [magic u16 = 0x4F54 "OT"][flags u8][module u8][type u8][len u32][payload...][crc32 u32] + - flags u8: bit0 = reply (0 host->device request, 1 device->host + reply/event); bits 4-7 = protocol version = 1. Request byte = 0x10, + reply = 0x11. + - module u8: the whole haptics protocol is dispatcher module 2. - CRC-32 is the standard zlib CRC (poly 0xEDB88320 reflected, init/final - 0xFFFFFFFF) over magic..payload; check value crc32("123456789") - == 0xCBF43926. Payloads are capped at 4096 bytes per frame. + 0xFFFFFFFF) over the 9-byte header + payload; check value + crc32("123456789") == 0xCBF43926. Payloads are capped at 4096 bytes + per frame. - Commands are serialized (one in flight, wait for the reply); TELEMETRY (0x93) and OTA_PROGRESS (0x83) frames arrive unsolicited and are dispatched out-of-band by the RX pump. @@ -288,7 +293,8 @@

Log