feat(web): Basicmicro MCP console (UART/CAN) + generic slcan CAN console - #731
feat(web): Basicmicro MCP console (UART/CAN) + generic slcan CAN console#731finger563 wants to merge 3 commits into
Conversation
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>
There was a problem hiding this comment.
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.
- 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>
|
✅Static analysis result - no issues found! ✅ |
There was a problem hiding this comment.
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.substris legacy/deprecated in JavaScript. Preferslice(start, start + length)(orsubstring) 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.substris legacy/deprecated in JavaScript. Preferslice(start, start + length)(orsubstring) 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 usingaria-live=\"off\"(orrole=\"log\"with carefully scopedaria-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>
…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>
There was a problem hiding this comment.
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; }
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 ConsoleTest/configure MCP236/MCP266 (RoboClaw-family) controllers through either kind of USB converter:
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
node --checkpasses on both extracted scripts; zero external resource loads; title+description present (verified independently).0x31C3, real STOP packet CRC0x9817).🤖 Generated with Claude Code