diff --git a/apps/web/src/core/connections/transports.test.ts b/apps/web/src/core/connections/transports.test.ts new file mode 100644 index 000000000..68c83b72d --- /dev/null +++ b/apps/web/src/core/connections/transports.test.ts @@ -0,0 +1,59 @@ +import { TransportWebSerial } from "@meshtastic/transport-web-serial"; +import { Result } from "better-result"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { openTransport } from "./transports.ts"; + +vi.mock("@meshtastic/transport-web-serial", () => ({ + SerialConnectError: class SerialConnectError extends Error {}, + TransportWebSerial: { createFromPort: vi.fn() }, +})); + +describe("openTransport serial selection", () => { + const info = { usbVendorId: 0x303a, usbProductId: 0x1001 }; + const portA = { getInfo: () => info } as SerialPort; + const portB = { getInfo: () => info } as SerialPort; + const selectedPort = { getInfo: () => info } as SerialPort; + const requestPort = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + requestPort.mockResolvedValue(selectedPort); + Object.defineProperty(navigator, "serial", { + configurable: true, + value: { + getPorts: vi.fn().mockResolvedValue([portA, portB]), + requestPort, + }, + }); + vi.mocked(TransportWebSerial.createFromPort).mockResolvedValue( + Result.ok({} as never), + ); + }); + + it("prompts when multiple permitted ports share the saved VID/PID", async () => { + await openTransport( + { + id: 1, + name: "Serial: 303a:1001", + type: "serial", + status: "disconnected", + usbVendorId: info.usbVendorId, + usbProductId: info.usbProductId, + }, + { allowPrompt: true }, + ); + + expect(requestPort).toHaveBeenCalledOnce(); + expect(requestPort).toHaveBeenCalledWith({ + filters: [ + { + usbVendorId: info.usbVendorId, + usbProductId: info.usbProductId, + }, + ], + }); + expect(TransportWebSerial.createFromPort).toHaveBeenCalledWith( + selectedPort, + ); + }); +}); diff --git a/apps/web/src/core/connections/transports.ts b/apps/web/src/core/connections/transports.ts index 9dfc81594..595f6ff4b 100644 --- a/apps/web/src/core/connections/transports.ts +++ b/apps/web/src/core/connections/transports.ts @@ -154,13 +154,24 @@ async function openSerial( }; } ).serial; + const requestPortOptions = + conn.usbVendorId !== undefined && conn.usbProductId !== undefined + ? { + filters: [ + { + usbVendorId: conn.usbVendorId, + usbProductId: conn.usbProductId, + }, + ], + } + : {}; let port = opts.cachedSerialPort; if (!port) { const ports = await serial.getPorts(); log.debug("openSerial: getPorts", { count: ports.length }); if (ports && conn.usbVendorId && conn.usbProductId) { - port = ports.find((p: SerialPort) => { + const matchingPorts = ports.filter((p: SerialPort) => { const info = ( p as SerialPort & { @@ -172,11 +183,22 @@ async function openSerial( info.usbProductId === conn.usbProductId ); }); + if (matchingPorts.length === 1) { + port = matchingPorts[0]; + } else if (matchingPorts.length > 1) { + log.info( + "openSerial: multiple permitted ports share VID/PID; user selection required", + { count: matchingPorts.length }, + ); + if (opts.allowPrompt) { + port = await serial.requestPort(requestPortOptions); + } + } } } if (!port && opts.allowPrompt) { log.debug("openSerial: requesting port via picker"); - port = await serial.requestPort({}); + port = await serial.requestPort(requestPortOptions); } if (!port) { log.warn("openSerial: no port resolved");