Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions components/bldc_haptics/example/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ for the reference host implementation.

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
The whole haptics protocol is one dispatcher module, **2 by default** (`kHapticsModule` in `bldc_haptics_example.cpp`; the hosted console expects 2). All multi-byte fields are
**little-endian**:

```
Expand All @@ -26,7 +26,7 @@ The whole haptics protocol is dispatcher **module 2**. All multi-byte fields are
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.
- `module`: `u8` dispatcher module — the haptics module id (**2** by default) 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.
Expand Down
6 changes: 6 additions & 0 deletions components/bldc_haptics/example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ work against this device:
The **device hub** (`dispatcher_hub.html`) discovers all three modules on this
one device and links to each console.

The module ids (haptics 2, OTA 0, core dump 4) are only routing keys and are
each configurable in one place — `kHapticsModule` at the top of
`bldc_haptics_example.cpp`, and `.module` in the `OtaService` /
`CoreDumpService` `Config` — but the hosted consoles expect these defaults, so
change them only together with your own host tooling.

## Example Behaviors

The detent presets can be switched at runtime from the web console (or by
Expand Down
32 changes: 23 additions & 9 deletions components/bldc_haptics/example/main/bldc_haptics_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ static constexpr size_t example_motor_index = 1;
static constexpr size_t example_motor_index = 0;
#endif

// Dispatcher module id the haptics protocol is registered under (and stamped on
// every haptics reply / telemetry frame). It is only a routing key: change this
// one constant to move the protocol, but note the hosted haptics console looks
// for the default (haptics_proto::kModule = 2) until it is told otherwise. The
// OTA (0) and core-dump (4) services keep their own defaults; pass `.module` in
// their Config to move those.
static constexpr uint8_t kHapticsModule = haptics_proto::kModule;

// The USB telemetry / web dial needs the continuous knob value, i.e. the detent
// index PLUS the fractional progress towards the neighboring detents. The
// detent center and active config are protected in espp::BldcHaptics, so expose
Expand Down Expand Up @@ -382,19 +390,23 @@ extern "C" void app_main(void) {
// Haptics protocol (module 2) frame handling -- runs on the dispatcher
// worker task, never on the TinyUSB task.
// --------------------------------------------------------------------------
// Build replies via proto::build so they carry the haptics module (2) + reply
// flag — NOT the OTA reply builders (those are OTA module 0).
// Build replies via proto::build so they carry the haptics module
// (kHapticsModule) + reply flag — NOT the OTA reply builders (those are OTA
// module 0).
auto build = [](proto::Msg type, std::span<const uint8_t> payload = {}) {
return proto::build(type, payload, kHapticsModule);
};
auto reply_ok = [&](uint32_t value) {
std::vector<uint8_t> payload;
proto::put_u32(payload, value);
usb_send(proto::build(proto::Msg::Ok, payload));
usb_send(build(proto::Msg::Ok, payload));
};
auto reply_error = [&](const std::error_code &err, const std::string &context) {
std::vector<uint8_t> payload;
proto::put_u32(payload, static_cast<uint32_t>(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));
usb_send(build(proto::Msg::Error, payload));
};
auto reply_errc = [&](std::errc errc, const std::string &context) {
reply_error(std::make_error_code(errc), context);
Expand All @@ -408,7 +420,7 @@ extern "C" void app_main(void) {
proto::put_str(payload, app.version);
proto::put_str(payload, app.date + " " + app.time);
proto::put_str(payload, app.idf_version);
usb_send(proto::build(proto::Msg::Info, payload));
usb_send(build(proto::Msg::Info, payload));
};

auto send_status = [&]() {
Expand All @@ -420,7 +432,7 @@ extern "C" void app_main(void) {
proto::put_f32(payload, motor->get_shaft_angle());
proto::put_f32(payload, motor->get_shaft_velocity());
proto::put_u16(payload, stream_period_ms);
usb_send(proto::build(proto::Msg::Status, payload));
usb_send(build(proto::Msg::Status, payload));
};

auto send_modes = [&]() {
Expand All @@ -440,7 +452,7 @@ extern "C" void app_main(void) {
proto::put_i32(payload, detent);
proto::put_str(payload, kPresets[i].name);
}
usb_send(proto::build(proto::Msg::Modes, payload));
usb_send(build(proto::Msg::Modes, payload));
};

auto handle_frame = [&](const proto::stream::Frame &frame) {
Expand Down Expand Up @@ -558,6 +570,8 @@ extern "C" void app_main(void) {
// module 0 -> OTA (espp::OtaService -> ota_console)
// module 2 -> BLDC haptics (this example's protocol -> haptics_console)
// module 4 -> core dump (espp::CoreDumpService -> coredump_console)
// (the defaults the hosted consoles expect; each id is configurable --
// kHapticsModule above, and `.module` in the services' Config)
// All replies -- and the discovery reply -- go through the same
// tx_mutex-guarded usb_send as the telemetry frames.

Expand Down Expand Up @@ -601,7 +615,7 @@ extern "C" void app_main(void) {
usb_link.register_module(ota_service); // module 0 + its discovery metadata
usb_link.register_module(coredump_service); // module 4 + its discovery metadata
// The handler gates on !is_reply() so a reply-typed echo cannot re-enter it.
usb_link.register_module(proto::kModule,
usb_link.register_module(kHapticsModule,
[&](const proto::stream::Frame &frame) {
if (!frame.is_reply())
handle_frame(frame);
Expand Down Expand Up @@ -659,7 +673,7 @@ 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());
if (usb_send(proto::build(proto::Msg::Telemetry, payload))) {
if (usb_send(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
Expand Down
21 changes: 14 additions & 7 deletions components/bldc_haptics/example/main/haptics_usb_protocol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
// spec and ../PROTOCOL.md next to this example for the full haptics wire
// protocol).
//
// The haptics protocol occupies dispatcher MODULE 2 (haptics commands only).
// The haptics protocol occupies dispatcher MODULE 2 by default (haptics commands
// only; kHapticsModule in bldc_haptics_example.cpp picks the id).
// 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
Expand Down Expand Up @@ -35,13 +36,17 @@ namespace haptics_proto {
// protocol's module + reply flag.
namespace stream = espp::stream_frame;

/// Dispatcher module id owned by the haptics protocol (the frame `module` byte).
/// Default dispatcher module id of the haptics protocol (the frame `module`
/// byte): the id the hosted haptics console expects. The example registers the
/// protocol under `kHapticsModule` (bldc_haptics_example.cpp), which defaults
/// to this; build() takes the id to stamp so replies follow whatever the app
/// registered.
static constexpr uint8_t kModule = 2;

/// Protocol version reported in the INFO reply.
static constexpr uint8_t kProtocolVersion = 1;

/// Message types carried in the frame `type` byte (within module 2).
/// Message types carried in the frame `type` byte (within the haptics module).
enum class Msg : uint8_t {
// --- Haptics commands ------------------------------------------------------
GetInfo = 0x10, ///< host->dev: no payload -> Info reply
Expand Down Expand Up @@ -103,11 +108,13 @@ inline std::optional<float> get_f32_at(std::span<const uint8_t> bytes, size_t of
return std::bit_cast<float>(get_u32(bytes.subspan(offset)));
}

/// 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<uint8_t> build(Msg type, std::span<const uint8_t> payload = {}) {
/// Build a frame for any haptics-protocol message type on `module` (kModule, 2,
/// by default; the reply flag is set for reply/telemetry types, whose ids have
/// the high bit set).
inline std::vector<uint8_t> build(Msg type, std::span<const uint8_t> payload = {},
uint8_t module = kModule) {
const bool reply = (static_cast<uint8_t>(type) & 0x80) != 0;
return espp::stream_frame::build_frame(reply, kModule, static_cast<uint8_t>(type), payload);
return espp::stream_frame::build_frame(reply, module, static_cast<uint8_t>(type), payload);
}

/// Status flag bits (Status + Telemetry `flags` byte).
Expand Down
7 changes: 5 additions & 2 deletions components/canopen/can_bridge_example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ connects over the native USB and can
node is a passive sniffer that never ACKs or transmits.

It bridges the ESP32-S3 TWAI (CAN 2.0) controller to the host over USB using the
espp `stream_frame` framing and an `espp::Dispatcher` (this example owns
**module id 5**). The same framed protocol is exposed on both the USB **vendor**
espp `stream_frame` framing and an `espp::Dispatcher` (this example uses
**module id 5** by default — `kCanBridgeModule` at the top of
`can_bridge_example.cpp` is the one place to change it, though the hosted
console looks for 5 until told otherwise). The same framed protocol is exposed
on both the USB **vendor**
interface (WebUSB) and a **CDC** interface (Web Serial), so the web app can use
either transport. The system console/logs stay on the separate built-in
USB-Serial-JTAG.
Expand Down
14 changes: 10 additions & 4 deletions components/canopen/can_bridge_example/main/can_bridge_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ namespace sf = espp::stream_frame;
static constexpr int kCanTxGpio = 17;
static constexpr int kCanRxGpio = 16;

// Dispatcher module id the bridge protocol is registered under (and stamped on
// every reply / CAN_RX frame). It is only a routing key: change this one
// constant to move the protocol, but the hosted CAN console looks for the
// default (can_bridge::kModuleId = 5) until it is told otherwise.
static constexpr uint8_t kCanBridgeModule = can_bridge::kModuleId;

extern "C" void app_main(void) {
espp::Logger logger({.tag = "CAN Bridge", .level = espp::Logger::Verbosity::INFO});
logger.info("Starting USB<->CAN bridge example");
Expand Down Expand Up @@ -94,9 +100,9 @@ extern "C" void app_main(void) {
send_fn stream_send;
auto build_frame = [](uint8_t type, std::span<const uint8_t> payload = {}) {
// Reply/event types (kCanRx/kOk/kError/kStatus) carry the high bit; map it
// to the frame reply flag. All CAN-bridge frames are module kModuleId.
// to the frame reply flag. All CAN-bridge frames are module kCanBridgeModule.
const bool reply = (type & 0x80) != 0;
return sf::build_frame(reply, can_bridge::kModuleId, type, payload);
return sf::build_frame(reply, kCanBridgeModule, type, payload);
};
auto send_frame = [&](const send_fn &send, uint8_t type, std::span<const uint8_t> payload = {}) {
send(build_frame(type, payload));
Expand Down Expand Up @@ -178,7 +184,7 @@ extern "C" void app_main(void) {
}
};

// --- CAN bridge protocol handler (dispatcher module id 5) ------------------
// --- CAN bridge protocol handler (dispatcher module kCanBridgeModule) ------
// `send` transmits on the transport the frame arrived on (each worker
// registers the handler with its own sender), so replies never cross streams.
auto handle_can_frame = [&](const espp::stream_frame::Frame &frame, const send_fn &send) {
Expand Down Expand Up @@ -291,7 +297,7 @@ extern "C" void app_main(void) {
"Raw CAN 2.0 bridge (WebUSB / Web Serial)"};
for (auto *link : {&vendor_link, &cdc_link}) {
link->register_module(
can_bridge::kModuleId,
kCanBridgeModule,
[&, send = link->sender()](const sf::Frame &f) { handle_can_frame(f, send); }, can_info);
link->serve_discovery(usb_cfg.product);
}
Expand Down
19 changes: 11 additions & 8 deletions components/canopen/can_bridge_example/main/can_bridge_protocol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
// Wire protocol for the USB <-> CAN (TWAI) bridge example.
//
// Framed with the espp stream_frame v2 codec and routed by espp::Dispatcher on
// MODULE ID 5. The v2 frame has a DEDICATED module byte, so EVERY frame here —
// requests and replies alike — sets module = 5 and routes to dispatcher module
// 5 (this is NOT the retired v1 scheme where the module was derived from the
// type's high nibble). The 0x5X / 0xD_ values below are the `type` byte, not
// the module; the reply/event types (0xD_) additionally set the frame reply
// flag (build derives it from the type's high bit). The hosted CAN console web
// app speaks this exact protocol over WebUSB / Web Serial.
// one module id, 5 by default (kModuleId here; the example registers under
// kCanBridgeModule in can_bridge_example.cpp, and a host must use the same id).
// The v2 frame has a DEDICATED module byte, so EVERY frame here — requests and
// replies alike — carries that module id and routes to it (this is NOT the
// retired v1 scheme where the module was derived from the type's high nibble). The 0x5X / 0xD_
// values below are the `type` byte, not the module; the reply/event types (0xD_) additionally set
// the frame reply flag (build derives it from the type's high bit). The hosted CAN console web app
// speaks this exact protocol over WebUSB / Web Serial.
//
// A CAN frame is encoded as a compact payload:
// [id u32 LE][flags u8][dlc u8][data: dlc bytes]
Expand All @@ -25,7 +26,9 @@

namespace can_bridge {

/// Dispatcher module id owned by the CAN bridge protocol.
/// Default dispatcher module id of the CAN bridge protocol: the id the hosted
/// CAN console expects. The example registers under `kCanBridgeModule`
/// (can_bridge_example.cpp), which defaults to this.
static constexpr uint8_t kModuleId = 5;

/// Host -> device (requests, high nibble 5).
Expand Down
16 changes: 10 additions & 6 deletions components/coredump/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ any transport. Failures are reported via `std::error_code` (no exceptions).
`stream_frame` codec (magic `"OT"` + flags + module + type + len + payload +
CRC-32) so the core dump can be inspected over **any byte stream** — the USB
vendor (WebUSB) interface, a USB CDC (Web Serial) port, a socket, a UART. It
owns dispatcher **module 4** (requests `0x40..0x4F`, replies `0xC0..0xCF` with
the frame reply flag set), so the service coexists with other framed protocols
(OTA on module 0, an app protocol, ...) — and with free-form console text — on
the same stream, routed by `espp::Dispatcher`.
uses dispatcher **module 4** by default (requests `0x40..0x4F`, replies
`0xC0..0xCF` with the frame reply flag set), so the service coexists with other
framed protocols (OTA on module 0, an app protocol, ...) — and with free-form
console text — on the same stream, routed by `espp::Dispatcher`. The module id
is only a routing key: `CoreDumpService::Config::module` moves an instance to
another id (used for both the requests it accepts and the replies it sends),
but the hosted console looks for 4 until told otherwise.

The matching browser tool is
[`web/coredump_console.html`](web/coredump_console.html), hosted at
Expand Down Expand Up @@ -47,8 +50,9 @@ the same stream), view the crash summary, download the core dump as
(partition-backed chunked reads), `erase(ec)`
- **Stream service**: `espp::CoreDumpService` — transport-agnostic; construct
with a `send` function, then register it on a dispatcher
(`dispatcher.register_module(service)` — it carries its module id 4,
`handle(frame)` and discovery `module_info()`), or `feed(bytes)` (internal
(`dispatcher.register_module(service)` — it carries its module id
(`Config::module`, default 4), `handle(frame)` and discovery
`module_info()`), or `feed(bytes)` (internal
resynchronizing frame parser) / `handle_frame(type, payload)` (bring your own
parser); GET_SUMMARY / GET_SIZE / READ / ERASE requests, unknown frame types
ignored
Expand Down
6 changes: 4 additions & 2 deletions components/coredump/example/main/coredump_example.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ enum class CrashKind : uint8_t { None, NullPointer, Assert, DivideByZero, Hang }
// Example-specific "trigger a test crash" command: a stream_frame with
// module = kCrashModule, type = kMsgTriggerCrash, payload = [CrashKind]. Its
// own dispatcher module keeps it cleanly separate from the core-dump protocol
// (module 4); the CDC text console keeps working for Web Serial / terminal
// users too.
// (module 4 by default -- CoreDumpService::Config::module can move it); the
// CDC text console keeps working for Web Serial / terminal users too. The
// module id is only a routing key: this one constant is the place to change
// it (the hosted coredump console sends the trigger on 1 until told otherwise).
static constexpr uint8_t kCrashModule = 1;
static constexpr uint8_t kMsgTriggerCrash = 0x00;

Expand Down
Loading
Loading