diff --git a/components/canopen/web/can_bridge_console.html b/components/canopen/web/can_bridge_console.html index b93917d114..e22672d157 100644 --- a/components/canopen/web/can_bridge_console.html +++ b/components/canopen/web/can_bridge_console.html @@ -221,6 +221,7 @@ .log .tx { color: var(--tx-color); } .log .rx { color: var(--rx-color); } .log .err { color: var(--err-color); } + .log .warn { color: #d08770; } .log .sys { color: var(--sys-color); } footer { color: var(--text-muted); font-size: 12px; margin-top: 10px; } #unsupported { @@ -250,7 +251,7 @@

Device

-

Not connected. WebUSB default filter: VID 0x1209 / PID 0x0d32 (espp default); the vendor (0xFF) interface is discovered from the descriptors at runtime.

+

Not connected. WebUSB default filter: any espp device (VID 0x1209); the vendor (0xFF) interface is discovered from the descriptors at runtime.

@@ -380,6 +381,11 @@

Log

const MAX_PAYLOAD = 4096; // resync cap; interleaved protocols may use up to this const MAX_FRAME = HEADER_SIZE + CORRELATION_SIZE + MAX_PAYLOAD + CRC_SIZE; const MODULE_CAN = 5; // the CAN bridge is module 5 + // Reserved dispatcher discovery module: a device running an espp::Dispatcher + // answers a ListModules query on 0xFF with the modules it serves, so we can + // confirm the CAN bridge module is present before driving it. + const MODULE_DISCOVERY = 0xFF, T_LIST_MODULES = 0x00; + const DISCOVERY_TIMEOUT_MS = 800; // module-presence probe (0xFF ListModules) timeout const FLAGS_VERSION = 1; // protocol version lives in flags bits 4-7 const FLAGS_REQUEST = (FLAGS_VERSION << 4) | 0; // host->device request: 0x10 (reply bit 0) const FLAGS_REPLY_BIT = 0x01; // bit0 set on device->host replies/events @@ -612,15 +618,31 @@

Log

// =================================================================== let transport = null; // active transport or null let connectTime = 0; // performance.now() at connect, for relative timestamps + // A pending checkModulePresent() parks its resolver here; onIncoming calls it + // when the 0xFF ListModules reply arrives. + let discoveryResolve = null; + + // WebUSB requires a `filters` member (there is no acceptAllDevices). The + // "show all" path uses one empty filter `{}` (matches everything); a few + // WebUSB implementations reject an empty filter object, so fall back to a + // VID-only filter in that case. + async function requestUsbDevice(anyDevice) { + const showAll = { filters: [{}] }; + const vidOnly = { filters: [{ vendorId: DEFAULT_VID }] }; + try { + return await navigator.usb.requestDevice(anyDevice ? showAll : vidOnly); + } catch (e) { + if (anyDevice && e && e.name === "TypeError") + return await navigator.usb.requestDevice(vidOnly); + throw e; + } + } // ---- WebUSB transport ------------------------------------------------- class UsbTransport { constructor() { this.device = null; this.iface = null; this.epIn = null; this.epOut = null; this.inSize = 64; this.reading = false; } async open(anyDevice) { - const options = anyDevice - ? { acceptAllDevices: true } - : { filters: [{ vendorId: DEFAULT_VID, productId: DEFAULT_PID }] }; - this.device = await navigator.usb.requestDevice(options); + this.device = await requestUsbDevice(anyDevice); await this.device.open(); if (this.device.configuration === null) await this.device.selectConfiguration(1); const config = this.device.configuration; @@ -790,6 +812,9 @@

Log

if (transport === t) { logLine("err", "Read loop error: " + (e && e.message ? e.message : e)); onLinkLost(); } }); + // Best-effort: warn if this device does not advertise the CAN bridge module. + checkModulePresent().catch(() => {}); + // Poll STATUS and request an immediate one. requestStatus(); statusTimer = setInterval(requestStatus, STATUS_POLL_MS); @@ -837,7 +862,7 @@

Log

els.stopBtn.disabled = !connected; els.sendBtn.disabled = !connected; if (!connected) { - els.devInfo.textContent = "Not connected. WebUSB default filter: VID 0x1209 / PID 0x0d32 (espp default); the vendor (0xFF) interface is discovered from the descriptors at runtime."; + els.devInfo.textContent = "Not connected. WebUSB default filter: any espp device (VID 0x1209); the vendor (0xFF) interface is discovered from the descriptors at runtime."; setStatusLine(null); } } @@ -859,6 +884,46 @@

Log

} function requestStatus() { sendFrame(T_GET_STATUS, null); } + // ---- discovery-based module presence check -------------------------------- + // Decode the ListModules TLV (see espp::Dispatcher::describe). + function parseDiscovery(payload) { + let i = 0; + const need = (n) => { if (i + n > payload.length) throw new Error("truncated discovery payload"); }; + const u8 = () => { need(1); return payload[i++]; }; + const str = () => { const n = u8(); need(n); const s = new TextDecoder().decode(payload.subarray(i, i + n)); i += n; return s; }; + const version = u8(); u8(); // version, reserved + const device = str(), fw = str(); + const count = u8(); + const modules = []; + for (let m = 0; m < count; m++) modules.push({ id: u8(), name: str(), app: str(), desc: str() }); + return { version, device, fw, modules }; + } + // Ask the device which dispatcher modules it serves and warn (best-effort) if + // the CAN bridge module is not among them. No discovery reply (older firmware) + // -> stay quiet; normal STATUS polling still surfaces a real missing module. + async function checkModulePresent() { + if (!transport) return; + const payload = await new Promise((resolve) => { + let timer = null; + // settle guards the shared resolver: only clear it if it is still ours, + // so a prior probe's timeout can't cancel a newer probe's resolver. + const settle = (p) => { clearTimeout(timer); if (discoveryResolve === settle) discoveryResolve = null; resolve(p); }; + timer = setTimeout(() => settle(null), DISCOVERY_TIMEOUT_MS); + discoveryResolve = settle; + transport.send(buildFrame(MODULE_DISCOVERY, T_LIST_MODULES)).catch(() => settle(null)); + }); + if (!payload) return; + let info; + try { info = parseDiscovery(payload); } catch (_) { return; } + if (info.modules.some((m) => m.id === MODULE_CAN)) { + logLine("sys", "Device advertises " + info.modules.length + " module(s); CAN bridge module present."); + return; + } + const names = info.modules.map((m) => m.name + " (#" + m.id + ")").join(", ") || "none"; + logLine("warn", "This device does not advertise the CAN bridge module. Detected: " + names + + ". You may have selected the wrong device, or its firmware lacks CAN bridge support."); + } + els.applyBtn.addEventListener("click", () => { const baud = parseInt(els.baud.value, 10); const mode = parseInt(els.mode.value, 10); @@ -948,6 +1013,12 @@

Log

function onIncoming(chunk) { const frames = parser.feed(chunk); for (const f of frames) { + // Discovery reply (reserved module 0xFF): hand the TLV to a pending module + // probe before the CAN-module matching below ignores it. + if (f.module === MODULE_DISCOVERY && f.reply && f.type === T_LIST_MODULES) { + if (discoveryResolve) { discoveryResolve(f.payload); discoveryResolve = null; } + continue; + } // Only CAN bridge (module 5) device->host reply/event frames we know. if (f.module !== MODULE_CAN || !f.reply || !KNOWN_RX.has(f.type)) { // Frame belonging to another module/protocol on the shared stream: ignore. diff --git a/components/canopen/web/ds402_panel.html b/components/canopen/web/ds402_panel.html index 47a98cb9b0..3f52fcebff 100644 --- a/components/canopen/web/ds402_panel.html +++ b/components/canopen/web/ds402_panel.html @@ -219,6 +219,7 @@ .log .tx { color: var(--tx-color); } .log .rx { color: var(--rx-color); } .log .err { color: var(--err-color); } + .log .warn { color: #d08770; } .log .sys { color: var(--sys-color); } footer { color: var(--text-muted); font-size: 12px; margin-top: 10px; } #unsupported { @@ -248,7 +249,7 @@

Device

-

Not connected. WebUSB default filter: VID 0x1209 / PID 0x0d32 (espp default); the vendor (0xFF) interface is discovered from the descriptors at runtime.

+

Not connected. WebUSB default filter: any espp device (VID 0x1209); the vendor (0xFF) interface is discovered from the descriptors at runtime.

@@ -456,6 +457,11 @@

Log const MAX_PAYLOAD = 4096; const MAX_FRAME = HEADER_SIZE + CORRELATION_SIZE + MAX_PAYLOAD + CRC_SIZE; const MODULE_CAN = 5; + // Reserved dispatcher discovery module: a device running an espp::Dispatcher + // answers a ListModules query on 0xFF with the modules it serves, so we can + // confirm the CAN bridge module is present before driving it. + const MODULE_DISCOVERY = 0xFF, T_LIST_MODULES = 0x00; + const DISCOVERY_TIMEOUT_MS = 800; // module-presence probe (0xFF ListModules) timeout const FLAGS_VERSION = 1; const FLAGS_REQUEST = (FLAGS_VERSION << 4) | 0; const FLAGS_REPLY_BIT = 0x01; @@ -653,11 +659,25 @@

Log // Transport: WebUSB + Web Serial (identical to can_bridge_console.html) // =================================================================== let transport = null; + // WebUSB requires a `filters` member (there is no acceptAllDevices). The + // "show all" path uses one empty filter `{}` (matches everything); a few + // WebUSB implementations reject an empty filter object, so fall back to a + // VID-only filter in that case. + async function requestUsbDevice(anyDevice) { + const showAll = { filters: [{}] }; + const vidOnly = { filters: [{ vendorId: DEFAULT_VID }] }; + try { + return await navigator.usb.requestDevice(anyDevice ? showAll : vidOnly); + } catch (e) { + if (anyDevice && e && e.name === "TypeError") + return await navigator.usb.requestDevice(vidOnly); + throw e; + } + } class UsbTransport { constructor() { this.device = null; this.iface = null; this.epIn = null; this.epOut = null; this.inSize = 64; this.reading = false; } async open(anyDevice) { - const options = anyDevice ? { acceptAllDevices: true } : { filters: [{ vendorId: DEFAULT_VID, productId: DEFAULT_PID }] }; - this.device = await navigator.usb.requestDevice(options); + this.device = await requestUsbDevice(anyDevice); await this.device.open(); if (this.device.configuration === null) await this.device.selectConfiguration(1); const config = this.device.configuration; @@ -1139,6 +1159,51 @@

Log } } + // =================================================================== + // Discovery-based module presence check + // =================================================================== + // A pending checkModulePresent() parks its resolver here; onIncoming calls it + // when the 0xFF ListModules reply arrives. + let discoveryResolve = null; + // Decode the ListModules TLV (see espp::Dispatcher::describe). + function parseDiscovery(payload) { + let i = 0; + const need = (n) => { if (i + n > payload.length) throw new Error("truncated discovery payload"); }; + const u8 = () => { need(1); return payload[i++]; }; + const str = () => { const n = u8(); need(n); const s = new TextDecoder().decode(payload.subarray(i, i + n)); i += n; return s; }; + const version = u8(); u8(); // version, reserved + const device = str(), fw = str(); + const count = u8(); + const modules = []; + for (let m = 0; m < count; m++) modules.push({ id: u8(), name: str(), app: str(), desc: str() }); + return { version, device, fw, modules }; + } + // Ask the device which dispatcher modules it serves and warn (best-effort) if + // the CAN bridge module is not among them. No discovery reply (older firmware) + // -> stay quiet; the normal bus-status poll still surfaces a real missing module. + async function checkModulePresent() { + if (!transport) return; + const payload = await new Promise((resolve) => { + let timer = null; + // settle guards the shared resolver: only clear it if it is still ours, + // so a prior probe's timeout can't cancel a newer probe's resolver. + const settle = (p) => { clearTimeout(timer); if (discoveryResolve === settle) discoveryResolve = null; resolve(p); }; + timer = setTimeout(() => settle(null), DISCOVERY_TIMEOUT_MS); + discoveryResolve = settle; + transport.send(buildFrame(MODULE_DISCOVERY, T_LIST_MODULES)).catch(() => settle(null)); + }); + if (!payload) return; + let info; + try { info = parseDiscovery(payload); } catch (_) { return; } + if (info.modules.some((m) => m.id === MODULE_CAN)) { + logLine("sys", "Device advertises " + info.modules.length + " module(s); CAN bridge module present."); + return; + } + const names = info.modules.map((m) => m.name + " (#" + m.id + ")").join(", ") || "none"; + logLine("warn", "This device does not advertise the CAN bridge module. Detected: " + names + + ". You may have selected the wrong device, or its firmware lacks CAN bridge support."); + } + // =================================================================== // Incoming frame handling // =================================================================== @@ -1147,6 +1212,12 @@

Log try { frames = parser.feed(chunk); } catch (e) { logLine("err", "parse error: " + (e && e.message ? e.message : e)); return; } for (const f of frames) { + // Discovery reply (reserved module 0xFF): hand the TLV to a pending module + // probe before the CAN-module matching below ignores it. + if (f.module === MODULE_DISCOVERY && f.reply && f.type === T_LIST_MODULES) { + if (discoveryResolve) { discoveryResolve(f.payload); discoveryResolve = null; } + continue; + } if (f.module !== MODULE_CAN) continue; // ignore interleaved other-module frames if (f.type === T_CAN_RX) { const cf = decodeCanFrame(f.payload); @@ -1198,6 +1269,7 @@

Log els.devInfo.textContent = name; logLine("sys", "Connected: " + name); t.readLoop(onIncoming).catch((e) => { if (transport === t) { logLine("err", "Read loop error: " + (e && e.message ? e.message : e)); onLinkLost(); } }); + checkModulePresent().catch(() => {}); // best-effort: warn if this device lacks the CAN bridge module requestStatus(); statusTimer = setInterval(requestStatus, STATUS_POLL_MS); if (els.livePoll.checked) startLivePoll(); // honor a pre-ticked Live poll @@ -1242,7 +1314,7 @@

Log els.anyDevice.disabled = connected; for (const id of CONNECTED_CONTROLS) els[id].disabled = !connected; if (!connected) { - els.devInfo.textContent = "Not connected. WebUSB default filter: VID 0x1209 / PID 0x0d32 (espp default); the vendor (0xFF) interface is discovered from the descriptors at runtime."; + els.devInfo.textContent = "Not connected. WebUSB default filter: any espp device (VID 0x1209); the vendor (0xFF) interface is discovered from the descriptors at runtime."; setStatusLine(null); setDs402State(null); els.livePoll.checked = false;