Skip to content

feat(mcp266): WebUSB / Web Serial console example + web app - #761

Merged
finger563 merged 3 commits into
mainfrom
feat/mcp266-webapp
Sep 3, 2026
Merged

feat(mcp266): WebUSB / Web Serial console example + web app#761
finger563 merged 3 commits into
mainfrom
feat/mcp266-webapp

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Adds a companion components/mcp266/webapp_example (the scripted example is unchanged) that turns an ESP32-S3 into a browser front-end for a Basicmicro MCP266: configure, command, and view live status of both motor channels over WebUSB / Web Serial.

Unlike the CAN bridge (raw CAN + in-browser CANopen), this runs the espp::Mcp266 driver on the device behind a small high-level protocol (stream_frame + Dispatcher, module id 6), so the web app needs zero CANopen/DS402 knowledge.

Firmware

  • Twai + CanopenClient + Mcp266 + UsbDevice (vendor WebUSB + CDC Web Serial). A per-transport Dispatcher routes module-6 requests to a worker task (so blocking SDO never runs in the TinyUSB callback), and all Mcp266 SDO access is serialized under one mutex (command handler + status streamer share the single SDO channel; the Twai RX task feeds process_frame from a different task, per the client contract).
  • Protocol: START / RESET_FAULTS / RESET_ESTOP / CONFIGURE_POSITION_LOOP / SET_POSITION_LIMITS / MOVE_TO_POSITION / DRIVE_SPEED / DRIVE_DUTY / GET_STATUS / SET_STATUS_STREAM / GET_DEVICE_INFO; replies OK / ERROR / STATUS / DEVICE_INFO. STATUS streams per-axis position/velocity/statusword + battery/temperature.

Web app

components/mcp266/web/mcp266_console.html — single-file, offline, WebUSB/Web Serial, reusing the DS402 panel’s transport + stream_frame framing. Per-axis (M1/M2) cards with DS402 state decode + target-reached, configure (position-loop clamp + fallback P, CiA 402 software limits, reset faults/e-stop, start node), command (profile-position move with vel/accel/decel; the inert speed/duty mirrors), device telemetry, and a device-side live-status stream toggle. Auto-listed in the apps index.

Build / CI

CMakeLists mirror the can_bridge_example dual-mode setup (manager-on for registry users; manager-off + vendored esp_tinyusb/tinyusb submodules for CI). build.yml runs it on esp32s3 with IDF_COMPONENT_MANAGER=0; manifest lists the new example.

Verified (IDF v6.0.1 / GCC 15.2)

  • Firmware builds clean (esp32s3, manager-off, no managed_components).
  • Web app node --check clean.
  • Firmware↔web-app protocol round-trip test (25-byte STATUS layout + move/configure command encode/decode, incl. negative positions) — 8/8.
  • Hardware test against a real MCP266 still pending (no device on hand).

Follow-up context: the separate design review flagged that surfacing is_target_reached/get_state on Mcp266 would let the firmware avoid decoding the raw statusword here — not needed for this PR (the web app decodes DS402 state itself, as the DS402 panel does).

🤖 Generated with Claude Code

Add components/mcp266/webapp_example (companion to the scripted example, which is
unchanged): an ESP32-S3 that runs the espp::Mcp266 driver and exposes it to a
browser over native USB. Unlike the CAN bridge (raw CAN + in-browser CANopen),
this runs the driver ON the device behind a small high-level protocol (stream_frame
+ Dispatcher, module id 6), so the web app needs no CANopen/DS402 knowledge.

- Firmware: Twai + CanopenClient + Mcp266 + UsbDevice (vendor WebUSB + CDC Web
  Serial). A per-transport Dispatcher routes module-6 requests to a worker task
  (so blocking SDO calls never run in the TinyUSB callback), and all Mcp266 SDO
  access is serialized under one mutex (command handler + status streamer share
  the single SDO channel). Protocol: START / RESET_FAULTS / RESET_ESTOP /
  CONFIGURE_POSITION_LOOP / SET_POSITION_LIMITS / MOVE_TO_POSITION / DRIVE_SPEED /
  DRIVE_DUTY / GET_STATUS / SET_STATUS_STREAM / GET_DEVICE_INFO, with OK / ERROR /
  STATUS / DEVICE_INFO replies. STATUS streams per-axis position/velocity/statusword
  + battery/temperature.
- Web app components/mcp266/web/mcp266_console.html: single-file, offline,
  WebUSB/Web Serial, reusing the DS402 panel's transport + stream_frame framing.
  Per-axis (M1/M2) status cards with DS402 state decode + target-reached, configure
  (position-loop clamp + fallback P, CiA 402 software limits, reset faults/e-stop),
  command (profile-position move with vel/accel/decel; the inert speed/duty
  mirrors), device telemetry, and a device-side live-status stream toggle.
- CMakeLists mirror the can_bridge dual-mode setup (manager-on for registry users;
  manager-off + vendored esp_tinyusb/tinyusb submodules for CI). build.yml runs it
  on esp32s3 with IDF_COMPONENT_MANAGER=0; manifest lists the new example.

Verified on IDF v6.0.1 (GCC 15.2): firmware builds clean (esp32s3, manager-off);
web app node --check clean; a firmware<->web-app protocol round-trip test (status
25-byte layout + move/configure command encode/decode, incl. negatives) 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 3, 2026 01:41
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are confirmed protocol robustness and transport-selection concurrency issues in the new firmware example (plus a small docs mismatch) that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an ESP32-S3 firmware example and a companion single-file web console to control and monitor a Basicmicro MCP266 from a browser over WebUSB or Web Serial, using an on-device espp::Mcp266 driver behind a small framed protocol (module id 6).

Changes:

  • New components/mcp266/webapp_example firmware example: TWAI + CANopen client + MCP266 driver + USB vendor/CDC framed protocol with command handling and optional status streaming.
  • New offline web app components/mcp266/web/mcp266_console.html implementing the same framed protocol over WebUSB/Web Serial with per-axis UI and DS402 statusword decoding.
  • CI/docs wiring updates: add the new example to the mcp266 manifest examples list and build it in GitHub Actions.
File summaries
File Description
components/mcp266/webapp_example/sdkconfig.defaults Pins target to esp32s3 and enables TinyUSB vendor+CDC configs sized for streaming bursts.
components/mcp266/webapp_example/README.md Documents wiring, protocol (module 6), and build/flash steps for the webapp example.
components/mcp266/webapp_example/main/mcp266_webapp_example.cpp Implements the USB-framed protocol, dispatch/worker tasking, and MCP266 command/status handling over CANopen.
components/mcp266/webapp_example/main/mcp266_protocol.hpp Defines protocol constants and STATUS payload layout for host/device interoperability.
components/mcp266/webapp_example/main/CMakeLists.txt Registers the example’s main component and its dependencies.
components/mcp266/webapp_example/CMakeLists.txt Project-level CMake supporting manager-on (registry) and manager-off (CI) builds with vendored TinyUSB paths.
components/mcp266/web/mcp266_console.html Adds the single-file browser UI and WebUSB/Web Serial transport + framing/parser implementation.
components/mcp266/idf_component.yml Registers webapp_example as an additional example for the mcp266 component.
.github/workflows/build.yml Adds CI build entry for the new webapp example (esp32s3, manager-off build command).
Review details

Suppressed comments (1)

components/mcp266/webapp_example/main/mcp266_webapp_example.cpp:380

  • To keep transport selection consistent for a given received chunk (and avoid races with the TinyUSB callbacks), set active_transport in the RX worker immediately before feeding that chunk into the corresponding dispatcher.
         for (const auto &[source, chunk] : chunks)
           (source == Transport::Vendor ? vendor_dispatcher : cdc_dispatcher).feed(chunk);
  • Files reviewed: 9/9 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread components/mcp266/webapp_example/main/mcp266_webapp_example.cpp
Comment thread components/mcp266/webapp_example/main/mcp266_webapp_example.cpp
Comment thread components/mcp266/web/mcp266_console.html Outdated
…sport, meta

- Validate the axis selector on axis-addressed requests (CONFIGURE / SET_LIMITS /
  MOVE / DRIVE_SPEED / DRIVE_DUTY): a new need_axis() rejects pl[0] > 1 with
  invalid_argument, so an unexpected value can no longer fall through axis_of()
  to M1 and command the wrong motor channel.
- active_transport is now written only by the RX worker, immediately before it
  feeds each chunk to that chunk's dispatcher (removed the store in the TinyUSB
  receive callback). A frame arriving on the other endpoint between enqueue and
  dispatch can no longer retarget the reply/status for the frame being handled.
- Web app meta description reworded: the device runs the CANopen driver and
  exposes a high-level protocol over USB (WebUSB / Web Serial) - not a
  "USB-to-CANopen bridge" (the text shows in the apps index).

node --check clean; firmware rebuilds clean (esp32s3, manager-off, IDF v6.0.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563

Copy link
Copy Markdown
Contributor Author

Addressed the review comments in 7c97526ab:

  • Axis not validated (:243) — added need_axis() used by all axis-addressed requests (CONFIGURE / SET_LIMITS / MOVE / DRIVE_SPEED / DRIVE_DUTY); it rejects pl[0] > 1 with invalid_argument, so an unexpected axis byte can no longer fall through axis_of() to M1 and command the wrong motor.
  • active_transport race (:339/:379) — removed the store in the TinyUSB receive callback; the RX worker is now the single writer, setting it immediately before feeding each chunk to that chunk’s dispatcher. A frame arriving on the other endpoint between enqueue and dispatch can no longer retarget the reply/status for the frame being handled.
  • Meta description (web:7) — reworded to match the architecture: the device runs the CANopen driver and exposes a high-level protocol over USB (WebUSB / Web Serial), not a "USB-to-CANopen bridge".

Verified: node --check clean, firmware rebuilds clean (esp32s3, manager-off, IDF v6.0.1).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Input coercion, stale streaming state, ignored offline status, and unbounded polling can produce unsafe or misleading behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

Previously missed (5) — in code that hasn't changed since the last review.

components/mcp266/web/mcp266_console.html:306

  • These encoders silently coerce out-of-range values: for example, a user-entered velocity of -1 becomes 4294967295, and malformed text is converted to 0 by num() before encoding. That can command materially different motor motion than the value shown in the UI. Validate exact integer syntax, signed/unsigned wire ranges, and min/max relationships before sending, following the established pattern in components/canopen/web/ds402_panel.html:1000-1009.
    components/mcp266/web/mcp266_console.html:399
  • The firmware clears status byte 24 bit 0 when neither axis responds and fills the measurements with zeroes, but this handler ignores that flag and renders those zeroes as valid readings/“Not ready.” Check the online flag before updating the fields and visibly mark telemetry unavailable when it is clear.
    components/mcp266/web/mcp266_console.html:435
  • A previous session can leave device-side streaming enabled after an unplug or read-loop failure. The disconnected UI clears this checkbox, but reconnecting only sends SET_STATUS_STREAM when it is checked, so the stale firmware setting remains active and unsolicited polling continues. Synchronize both states on every connection by explicitly sending the disabled state too.
    .github/workflows/build.yml:84
  • The CI matrix is required to stay alphabetically ordered, but this new mcp266 entry is inserted between basicmicro and bdc_driver. Move the MCP266 entries into the m... section so future additions and duplicate checks remain predictable.
    components/mcp266/web/mcp266_console.html:327
  • The generated motor-control inputs use sibling <span> elements rather than associated labels, and none has an aria-label, so screen readers expose unlabeled edit fields. Give every input in this template an accessible name by wrapping it in a <label> or adding an explicit label association.
  • Files reviewed: 9/9 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment on lines +305 to +306
stream_enabled.store(pl[0] != 0);
stream_period_ms.store(rd_u16(pl, 1) ? rd_u16(pl, 1) : 200);
A STATUS snapshot issues eight blocking SDO reads while holding the shared MCP
mutex, so the previously unbounded period_ms (down to 1 ms) let a client flood
the CAN bus and starve command handling. Clamp it to [50, 10000] ms (0 => the
200 ms default), mirroring the streaming guard in the bldc_haptics example, and
document the range in the protocol header.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563

Copy link
Copy Markdown
Contributor Author

Addressed the remaining review comment in 7ca036ea6:

  • period_ms unbounded (:306) — a STATUS snapshot performs eight blocking SDO reads while holding the shared MCP mutex, so a small period could flood the CAN bus and starve command handling. kSetStatusStream now clamps the host-requested period to a documented [50, 10000] ms window (0 ⇒ the 200 ms default) via std::clamp, mirroring the streaming guard in bldc_haptics_example.cpp. The accepted range is documented on kSetStatusStream in mcp266_protocol.hpp. (The web console sends a fixed 250 ms, already inside the window.)

Verified: firmware rebuilds clean (esp32s3, manager-off, IDF v6.0.1).

@finger563
finger563 merged commit 77cfebc into main Sep 3, 2026
154 checks passed
@finger563
finger563 deleted the feat/mcp266-webapp branch September 3, 2026 03:30
finger563 added a commit that referenced this pull request Sep 3, 2026
#761 (webapp) merged to main while this branch was in review; now that this
branch rebases onto main, update the webapp for the mcp266 API changes here:
- configure_position_loop: ec is now the last argument (fallback_p precedes it).
- set_position_limits -> set_software_position_limits.
- add the new motor_controller component (a transitive mcp266 dependency) to the
  manager-off component closure (EXTRA_COMPONENT_DIRS + COMPONENTS) so the
  firmware still builds with IDF_COMPONENT_MANAGER=0.

Builds clean: webapp firmware (esp32s3, manager-off) + basicmicro/mcp266
examples on IDF v6.0.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants