Skip to content

feat(web): Basicmicro MCP console (UART/CAN) + generic slcan CAN console - #731

Open
finger563 wants to merge 3 commits into
mainfrom
feat/can-web-tools
Open

feat(web): Basicmicro MCP console (UART/CAN) + generic slcan CAN console#731
finger563 wants to merge 3 commits into
mainfrom
feat/can-web-tools

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Description

Two new self-contained browser tools (auto-hosted + indexed at esp-cpp.github.io/espp/apps/ by the docs pipeline):

mcp_console.html — Basicmicro MCP Console

Test/configure MCP236/MCP266 (RoboClaw-family) controllers through either kind of USB converter:

  • USB↔UART (packet serial): CRC16-CCITT with a load-time golden self-test, 0xFF-ACK write validation, reply-CRC verification seeded with sent addr+cmd, and the ≥15 ms quiet-resync recovery on any failure. Device info (firmware version / battery / temperature / signed currents / decoded status bits with a 32→16-bit auto-retry for older firmware), spring-to-zero duty sliders (keyboard-accessible zero buttons), strict-i32 speed commands, 5 Hz encoder polling with status-bit decode, encoder reset.
  • USB↔CAN (slcan → CANopen/DS402): slcan bring-up with CR/BEL ack handling, NMT, expedited SDO client (index/sub-matched waiter, 24 decoded abort codes), heartbeat badge with staleness, CiA 402 statusword state machine + verified enable sequence, fault-reset/quick-stop, profile velocity + position (with new-setpoint pulse), identity/device-type/error-register, and an SDO object browser (15 preloaded standard objects + arbitrary typed access) — the tool for discovering the MCP's actual object dictionary, which Basicmicro doesn't publish.
  • Always-visible STOP (duty 0/0 in UART mode, quick-stop controlword in CAN mode). Note: STOP runs through the serialized transaction chain (~worst-case a few hundred ms behind an in-flight poll) to keep request/reply pairing sound — the reasoning is documented in-file; hardware E-stop remains the real safety path.

can_console.html — CAN Bus Console (slcan)

Generic console for any slcan (LAWICEL) USB-CAN adapter: bitrate + open/close, strict TX composer (std/ext/RTR with 11/29-bit range checks, DLC/data consistency), multiple periodic-send slots, chronological log (pause keeps capturing for export), per-ID latest table with changed-byte highlighting + period estimates, include/exclude ID filters (values + ranges), adapter info panel.

Both follow the established console conventions: single-file/offline (no CDN), light/dark theme-aware, responsive, aria-labeled, promise-chain-serialized wire I/O, e.port-based unplug teardown, <title> + <meta name="description"> for the apps index.

Testing

🤖 Generated with Claude Code

Add two self-contained, offline, dependency-free Web Serial browser apps,
following the structure and quality bar of the odrive_ascii web consoles
(theme-aware CSS with prefers-color-scheme + data-theme overrides,
responsive grid layout, accessible labels, <title> + meta description for
the auto-generated docs apps index, e.port-based unplug handling, and
promise-chain transaction serialization with strict typed codecs).

components/basicmicro/web/mcp_console.html — "Basicmicro MCP Console"
  Test/config console for Basicmicro MCP236/MCP266 (RoboClaw-family)
  motor controllers over either kind of USB converter:

  * Transport A — USB-UART (packet serial): CRC16-CCITT (poly 0x1021,
    init 0, non-reflected, big-endian) framing; write commands carry
    data+CRC and expect the single 0xFF ACK; read commands send
    [addr][cmd] and verify the reply CRC over addr+cmd+data; >=15 ms
    quiet-period resync after any timeout/CRC/ACK failure (the device
    clears its packet buffer after >10 ms of silence). Implements
    GETVERSION(21), GETMBATT(24), GETTEMP(82), GETCURRENTS(49),
    GETSTATUS(90, 32-bit with 16-bit retry fallback + status-bit decode),
    duty cmds 32/33/34 (spring-to-zero sliders with paced updates),
    speed cmds 35/36/37 (strict i32 qpps entry), encoder readback
    16/17 (u32 + status-bit decode, 5 Hz poll toggle), RESETENC(20).

  * Transport B — USB-CAN adapter (slcan/LAWICEL) carrying
    CANopen/DS402: slcan bring-up (C, Sn, O; CR ok / BEL error),
    client-side CANopen — NMT (start/stop/pre-op/reset), expedited SDO
    upload/download on 0x600/0x580+node with abort-code decoding and a
    single-waiter transaction chain, heartbeat monitor badge on
    0x700+node — and the CiA 402 profile: statusword state machine
    decode with standard masks, verified enable sequence
    (0x0006→0x0007→0x000F with per-step statusword confirmation),
    fault reset edge (0x0080→0x0000), quick stop (0x0002), mode 0x6060,
    target/actual velocity 0x60FF/0x606C, target/actual position
    0x607A/0x6064, identity 0x1018:1-4, device type 0x1000, error
    register 0x1001, plus an SDO object browser (preloaded standard
    objects + arbitrary index/sub/type read/write).

  A big always-visible STOP control sends duty 0/0 (cmd 34) in UART
  mode or the DS402 quick-stop controlword in CAN mode. Raw traffic log
  (hex packets / slcan lines) with pause/clear/export. Every transmitted
  value passes a strict full-string parse + range check; CRC16 golden
  vector + SDO codec round-trip self-tests run at load.

components/twai/web/can_console.html — "CAN Bus Console (slcan)"
  Generic raw-CAN console for any slcan USB-CAN adapter: bitrate select
  (S0-S8) with open/close; TX composer for standard/extended data and
  RTR frames with strict full-string validation (hex ID with 11/29-bit
  range check, DLC auto/0-8 consistency, hex data bytes); multiple
  periodic-transmit slots with individual stop and stop-all; live RX as
  both a chronological log (pause/autoscroll/clear/export, TX echoed
  inline) and a sorted per-ID latest table (dlc, data with
  changed-byte delta highlighting, count, smoothed period estimate);
  include/exclude ID filter with hex ranges; adapter command panel
  (V/N/F + guarded raw command entry).

Both files were verified with node --check on the extracted inline
scripts, contain no external resource references, and initialize
cleanly (self-tests pass) in headless Chromium.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 18, 2026 21:46

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.

Pull request overview

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds two new offline, single-file browser consoles for interacting with CAN/slcan adapters and Basicmicro MCP motor controllers via the Web Serial API.

Changes:

  • Introduces a generic slcan CAN bus console with TX composer, periodic sends, traffic log export, per-ID “latest” table, and RX filtering.
  • Introduces a Basicmicro MCP console supporting both packet-serial (UART) and CANopen/DS402-over-slcan workflows, including safety STOP and SDO object browsing.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
components/twai/web/can_console.html New generic slcan/Web Serial CAN console UI + protocol handling.
components/basicmicro/web/mcp_console.html New MCP controller console supporting both UART packet-serial and CANopen/DS402-over-slcan.

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

Comment thread components/twai/web/can_console.html Outdated
Comment thread components/twai/web/can_console.html
Comment thread components/twai/web/can_console.html Outdated
Comment thread components/basicmicro/web/mcp_console.html Outdated
Comment thread components/basicmicro/web/mcp_console.html
- Both consoles: 'z'/'Z' transmit acks are unsolicited tokens and no
  longer consume the pending-command FIFO, so an autopolling adapter can
  no longer desynchronize command/response pairing.
- Both consoles: the open-channel result now distinguishes CR ack (open),
  BEL (refused, stays closed), and TIMEOUT (silent adapter): the silent
  case proceeds tentatively (some bridges never ack) but surfaces
  'Open (no ack)' status + a warning instead of reporting a confirmed
  open channel.
- can_console: periodic transmit slots use a self-scheduling setTimeout
  loop armed only after the previous send completes — a slow/stalled
  send can no longer overlap itself with concurrent writer.write() calls.

node --check passes on both extracted scripts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

components/twai/web/can_console.html:508

  • String.prototype.substr is legacy/deprecated in JavaScript. Prefer slice(start, start + length) (or substring) to avoid relying on deprecated APIs.
      for (let i = 0; i < n; i++) out[i] = parseInt(compact.substr(i * 2, 2), 16);

components/twai/web/can_console.html:843

  • String.prototype.substr is legacy/deprecated in JavaScript. Prefer slice(start, start + length) (or substring) to avoid relying on deprecated APIs.
        for (let i = 0; i < dlc; i++) data[i] = parseInt(dataStr.substr(i * 2, 2), 16);

components/twai/web/can_console.html:309

  • The traffic log is expected to update frequently; aria-live=\"polite\" on a high-churn region can overwhelm screen readers and make the page difficult to use. Consider using aria-live=\"off\" (or role=\"log\" with carefully scoped aria-relevant=\"additions\") and relying on explicit user actions (Pause/Export) for assistive-tech consumption.
        <div id="log" aria-live="polite" aria-label="Chronological CAN traffic log"></div>

components/basicmicro/web/mcp_console.html:607

  • Same concern as the CAN console: this log can update at high frequency (polling + traffic), and aria-live=\"polite\" may spam assistive technologies. Consider disabling live announcements (aria-live=\"off\") or using a more suitable pattern for logs (e.g., role=\"log\" + limited announcements) so screen reader users aren’t flooded.
        <div id="log" aria-live="polite" aria-label="Raw traffic log"></div>

Comment thread components/twai/web/can_console.html
Comment thread components/basicmicro/web/mcp_console.html
…iew)

When reader.read() reports done, the stream has ended — breaking only the
inner loop re-entered the outer while and re-acquired a reader on the dead
stream in a tight loop, potentially never reaching the cleanup path. Set
keepReading = false on done so both loops exit and safeClose()/UI cleanup
runs. Applied identically to both consoles; node --check passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

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.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (5)

components/twai/web/can_console.html:842

  • parseFrame()` accepts standard IDs up to 0xFFF (3 hex digits) and extended IDs up to 0xFFFFFFFF (8 hex digits), but classic CAN IDs are limited to 11-bit (<= 0x7FF) for standard and 29-bit (<= 0x1FFFFFFF) for extended. This can cause malformed/garbage tokens to be treated as legitimate frames and populate the log/table/filter incorrectly. Add an explicit range check after parsing (based on ext) and return null (or log an error) when the ID exceeds the spec limit.
    function parseFrame(tok) {
      const kind = tok[0];
      const ext = kind === "T" || kind === "R";
      const rtr = kind === "r" || kind === "R";
      const idLen = ext ? 8 : 3;
      if (tok.length < 1 + idLen + 1) return null;
      const idStr = tok.slice(1, 1 + idLen);
      const dlcCh = tok[1 + idLen];
      if (!/^[0-9a-fA-F]+$/.test(idStr) || !/^[0-8]$/.test(dlcCh)) return null;
      const id = parseInt(idStr, 16);
      const dlc = parseInt(dlcCh, 10);

components/twai/web/can_console.html:713

  • When the adapter explicitly rejects the open command (okO === false), the UI is still switched into the "connected" state (enabling TX composer, periodic slots, adapter command buttons). Since channelOpen is false, actions will fail later with "Not connected / channel not open.", but the enabled controls make it easy to start timers/flows that will immediately error and spam the log. Consider keeping conn-gated controls disabled unless channelOpen is true, or switching back to disconnected UI on an O رفض (adapter refused open) while leaving the serial port open only if you have a specific reason to keep it open.
      setConnectedUI(true);
      if (okO === true) {
        channelOpen = true;
        setStatus("connected", "Open · " + kbit);
        logLine("sys", "CAN channel open at " + kbit + ".");
      } else if (okO === false) {
        channelOpen = false;
        setStatus("error", "Adapter refused O");
        logLine("err", "Adapter rejected the open command" + (okS === false ? " (bitrate was rejected too)" : "") +
          "; check the bitrate or power-cycle the adapter.");
      } else {

components/twai/web/can_console.html:587

  • The on-screen counters (rxCount/txCount) appear to track only CAN frames (txCount increments in sendFrameLine; rxCount increments in onRxFrame), but logLine("tx"/"rx") is also used for slcan control commands/responses (e.g., sendCmd logs "tx" without updating txCount). This makes the displayed "rx/tx" stats inconsistent with what a user sees in the log. Either (a) move counting into logLine() (like mcp_console does) so all tx/rx log entries are counted, or (b) relabel the stats to explicitly say "frames" and keep command tokens out of the counters.
    function updateLogStats() {
      els.logStats.textContent = `rx ${rxCount} · tx ${txCount}`;
    }

components/twai/web/can_console.html:921

  • The on-screen counters (rxCount/txCount) appear to track only CAN frames (txCount increments in sendFrameLine; rxCount increments in onRxFrame), but logLine("tx"/"rx") is also used for slcan control commands/responses (e.g., sendCmd logs "tx" without updating txCount). This makes the displayed "rx/tx" stats inconsistent with what a user sees in the log. Either (a) move counting into logLine() (like mcp_console does) so all tx/rx log entries are counted, or (b) relabel the stats to explicitly say "frames" and keep command tokens out of the counters.
        writer.write(encoder.encode(cmd + "\r")).then(() => {
          logLine("tx", cmd);
        }, (e) => {

components/basicmicro/web/mcp_console.html:1950

  • When SDO form parsing fails, the UI displays the error text but does not mark the specific invalid field(s) with aria-invalid="true" (and readSdoForm always resets aria-invalid to false on success only). This reduces screen-reader feedback and misses the visual invalid styling you rely on elsewhere (e.g., transport node ID, motion fields). Set aria-invalid="true" on the specific field that failed parsing (index vs subindex vs type), and only clear it for that field once it validates.
    function readSdoForm() {
      const index = parseIndexStrict(els.sdoIndex.value);
      const sub = parseIntStrict(els.sdoSub.value, 0, 255, "subindex");
      const type = els.sdoType.value;
      if (!SDO_TYPES[type]) throw new Error("unknown type");
      els.sdoIndex.setAttribute("aria-invalid", "false");
      els.sdoSub.setAttribute("aria-invalid", "false");
      return { index, sub, type };
    }
    els.sdoReadBtn.addEventListener("click", async () => {
      els.sdoError.textContent = "";
      let form;
      try { form = readSdoForm(); }
      catch (e) { els.sdoError.textContent = e.message; return; }

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