/*! * ch340-webusb.js — 浏览器直连 USB-Serial 芯片(CH340 为主,兼容 CH341/CDC-ACM/CP210x/FTDI/PL2303) * --------------------------------------------------------------------------------------------- * 不用装 App、不用 root、不用驱动:安卓 Chrome 走 WebUSB,桌面 Chrome/Edge 走 Web Serial。 * 浏览器本身没有"串口"语义,波特率/流控这些寄存器配置由本文件用控制传输(vendor request)完成。 * * CH340 初始化序列对齐 Linux 内核 drivers/usb/serial/ch341.c 与 bbamorachi 站点实现: * 0x5F 读版本 → 0xA1 清 modem → 0x9A 写 0x1312/0x0F2C 设波特率 → 0x95 读 0x2518 LCR * → 0x9A 写 0x2518=0xC3(8N1) → 0x95 读 0x0706 → 0xA1 0x501F/0xD90A 关流控 * → 0x9A 再设波特率 → 0xA4 0xFFFF(复位 DTR/RTS) → 0x9A 0x2518=0xC3 → 0xA4 0xFF9F(DTR+RTS 有效) * * 用法(WebUSB,安卓/桌面通用): * const port = await TamaCh340.requestPort({ baudRate: 460800 }); * await port.open({ baudRate: 460800 }); // 内部完成芯片初始化 * const reader = port.readable.getReader(); // 和 Web Serial 的用法完全一样 * const writer = port.writable.getWriter(); * await writer.write(new Uint8Array([1,2,3])); * * 许可:MIT */ (function (root, factory) { var api = factory(); if (typeof module === 'object' && module.exports) module.exports = api; if (root) root.TamaCh340 = api; })(typeof globalThis !== 'undefined' ? globalThis : (typeof self !== 'undefined' ? self : this), function () { 'use strict'; var VERSION = '1.0.0'; var DEFAULT_BAUD = 460800; // 拓麻歌子原厂线/UART 速率;其它场景请按需传参 var WRITE_CHUNK = 4096; // bulk OUT 分片大小 // ───────────────────────────── 基础工具 ───────────────────────────── var api = { version: VERSION, onLog: null, debug: false }; function log() { if (api.onLog) { try { api.onLog.apply(null, arguments); } catch (e) { /* 用户回调异常不能影响驱动 */ } } if (api.debug && typeof console !== 'undefined') { var a = Array.prototype.slice.call(arguments); a.unshift('[ch340]'); console.log.apply(console, a); } } /** 统一转成独立 Uint8Array(DataView/TypedArray 都能吃,且避免 buffer 被 detach) */ function toU8(data) { if (data instanceof Uint8Array) return new Uint8Array(data); if (data && data.buffer) return new Uint8Array(data.buffer, data.byteOffset, data.byteLength).slice(); return new Uint8Array(data || 0); } function hasWebUSB() { return typeof navigator !== 'undefined' && !!navigator.usb; } function hasWebSerial() { return typeof navigator !== 'undefined' && !!navigator.serial; } function isAndroid() { return typeof navigator !== 'undefined' && /Android/i.test(navigator.userAgent || ''); } // ───────────────────────────── 布局解析(端点发现) ───────────────────────────── /** 通用 vendor 串口:第一个同时带 bulk IN/OUT 的接口(CH340 就是接口 0) */ function genericLayout(configuration) { var ifaces = configuration.interfaces; for (var i = 0; i < ifaces.length; i++) { var iface = ifaces[i]; for (var j = 0; j < iface.alternates.length; j++) { var alt = iface.alternates[j]; var epIn = null, epOut = null; for (var k = 0; k < alt.endpoints.length; k++) { var ep = alt.endpoints[k]; if (ep.type === 'bulk' && ep.direction === 'in' && !epIn) epIn = ep; if (ep.type === 'bulk' && ep.direction === 'out' && !epOut) epOut = ep; } if (epIn && epOut) { return { claimedInterfaces: [iface.interfaceNumber], interfaceAlternates: [{ interfaceNumber: iface.interfaceNumber, alternateSetting: alt.alternateSetting }], controlInterface: iface.interfaceNumber, input: epIn, output: epOut }; } } } throw new Error('这个 USB 设备没有可用的 bulk 输入/输出端点(不是串口芯片?)'); } function findInterfaceByClass(configuration, classCode) { var ifaces = configuration.interfaces; for (var i = 0; i < ifaces.length; i++) { for (var j = 0; j < ifaces[i].alternates.length; j++) { if (ifaces[i].alternates[j].interfaceClass === classCode) { return { usbInterface: ifaces[i], alternate: ifaces[i].alternates[j] }; } } } return null; } /** CDC-ACM:控制接口 class 2 + 数据接口 class 10(CH343/CH9102/CH347 的 CDC 模式等) */ function cdcLayout(configuration) { var comm = findInterfaceByClass(configuration, 0x02); var data = findInterfaceByClass(configuration, 0x0a); if (!comm || !data) throw new Error('CDC-ACM 控制/数据接口不完整'); var epIn = null, epOut = null; for (var k = 0; k < data.alternate.endpoints.length; k++) { var ep = data.alternate.endpoints[k]; if (ep.type === 'bulk' && ep.direction === 'in' && !epIn) epIn = ep; if (ep.type === 'bulk' && ep.direction === 'out' && !epOut) epOut = ep; } if (!epIn || !epOut) throw new Error('CDC-ACM 数据端点不可用'); return { claimedInterfaces: [comm.usbInterface.interfaceNumber, data.usbInterface.interfaceNumber], interfaceAlternates: [ { interfaceNumber: comm.usbInterface.interfaceNumber, alternateSetting: comm.alternate.alternateSetting }, { interfaceNumber: data.usbInterface.interfaceNumber, alternateSetting: data.alternate.alternateSetting } ], controlInterface: comm.usbInterface.interfaceNumber, input: epIn, output: epOut }; } // ───────────────────────────── 控制传输上下文 ───────────────────────────── function ControlContext(device, layout) { this.device = device; this.layout = layout; } ControlContext.prototype.controlIn = function (requestType, recipient, request, value, index, length) { var self = this; return this.device.controlTransferIn({ requestType: requestType, recipient: recipient, request: request, value: value, index: index }, length).then(function (r) { if (r.status !== 'ok' || !r.data || r.data.byteLength !== length) { throw new Error('USB 控制读 0x' + request.toString(16) + ' 失败 (' + r.status + ')'); } return new Uint8Array(r.data.buffer, r.data.byteOffset, r.data.byteLength).slice(); }); }; ControlContext.prototype.controlOut = function (requestType, recipient, request, value, index, data) { var payload = data ? toU8(data) : undefined; return this.device.controlTransferOut({ requestType: requestType, recipient: recipient, request: request, value: value, index: index }, payload).then(function (r) { if (r.status !== 'ok' || (payload && r.bytesWritten !== undefined && r.bytesWritten !== payload.byteLength)) { throw new Error('USB 控制写 0x' + request.toString(16) + ' 失败 (' + r.status + ')'); } return r; }); }; // 便捷包装:vendor 请求 function vOut(ctx) { return function (req, val, idx, data) { return ctx.controlOut('vendor', 'device', req, val, idx, data); }; } function vIn(ctx) { return function (req, val, idx, len) { return ctx.controlIn('vendor', 'device', req, val, idx, len); }; } // ───────────────────────────── CH340 / CH341 ───────────────────────────── var CH34X_VID = 0x1a86; var CH34X_PIDS = [0x7523, 0x5523]; // CH340 / CH341(最常见的两种) // 少数 CH341A 克隆/变种会报这两个 PID;默认也接受,如遇异常可注释掉 var CH34X_EXTRA_PIDS = [0x5512, 0x7522]; /** CH34x 波特率 → { factor, divisor },算法与 Linux ch341.c 完全一致 */ function calcCh34xBaud(baud) { if (!Number.isInteger(baud) || baud <= 0) throw new Error('波特率必须是正整数'); var factor = Math.floor(1532620800 / baud); var ps = 3; while (factor > 65520 && ps > 0) { factor >>= 3; ps -= 1; } if (factor > 65520) throw new Error('CH34x 不支持该波特率: ' + baud); factor = 65536 - factor; ps |= 0x80; return { factor: (factor & 0xff00) | ps, divisor: factor & 0xff }; } var ch34x = { name: 'CH340/CH341', readQueueDepth: 32, // 预排队 32 个 bulk 读,避免安卓上丢数据 filters: [{ vendorId: CH34X_VID, productId: 0x7523 }, { vendorId: CH34X_VID, productId: 0x5523 }], supports: function (d) { return d.vendorId === CH34X_VID && CH34X_PIDS.indexOf(d.productId) !== -1; }, layout: genericLayout, initialize: async function (ctx, opts) { var out = vOut(ctx); var read = vIn(ctx); var setBaud = async function (baud) { var d = calcCh34xBaud(baud); await out(0x9a, 0x1312, d.factor); // 波特率高位/分频 await out(0x9a, 0x0f2c, d.divisor); // 波特率低位 }; // 1. 版本自检:必须返回 [?, 0x00] var ver = await read(0x5f, 0x0000, 0x0000, 2); if (ver[1] !== 0) throw new Error('CH34x 版本应答异常: 0x' + ver[0].toString(16) + ' 0x' + ver[1].toString(16)); // 2. 关 modem 控制 → 先按 9600 配置(芯片上电默认值,所有初始化都从这里开始) await out(0xa1, 0x0000, 0x0000); await setBaud(9600); // 3. 读 LCR(0x2518) 后写 0xC3 = 8 数据位 / 1 停止位 / 无校验,bit7 允许写分频 await read(0x95, 0x2518, 0x0000, 2); await out(0x9a, 0x2518, 0x00c3); await read(0x95, 0x0706, 0x0000, 2); // 4. 关硬件流控(0x501F/0xD90A 是 Linux 内核里的固定字) await out(0xa1, 0x501f, 0xd90a); await setBaud(9600); await out(0xa4, 0xffff, 0x0000); // DTR/RTS 全复位 await read(0x95, 0x0706, 0x0000, 2); // 5. 切到目标波特率并重新写 LCR,最后拉高 DTR+RTS await setBaud(opts.baudRate); await out(0x9a, 0x2518, 0x00c3); await out(0xa4, 0xff9f, 0x0000); // ~0x0060 = RTS(bit6)+DTR(bit5) 有效 log('driver', 'CH34x 初始化完成 @' + opts.baudRate + ' bps'); }, /** DTR / RTS 控制(request 0xA4,位是取反写入的) */ setSignals: async function (ctx, sig) { var mask = 0; if (sig.requestToSend) mask |= 0x40; // RTS if (sig.dataTerminalReady) mask |= 0x20; // DTR await vOut(ctx)(0xa4, (~mask) & 0xffff, 0x0000); } }; // ───────────────────────────── CDC-ACM ───────────────────────────── var WCH_CDC_PIDS = [ 0x55d2, 0x55d3, 0x55d4, 0x55d5, 0x55d6, 0x55d7, 0x55d8, 0x55d9, 0x55da, 0x55db, 0x55dc, 0x55dd, 0x55de, 0x55df, 0x55e0, 0x55e1, 0x55e2, 0x55e3, 0x55e7 ]; function cdcLineCoding(baud) { var b = new Uint8Array(7); new DataView(b.buffer).setUint32(0, baud, true); b[4] = 0; // 1 stop bit b[5] = 0; // no parity b[6] = 8; // 8 data bits return b; } var cdcAcm = { name: 'CDC-ACM', filters: [{ classCode: 0x02 }].concat(WCH_CDC_PIDS.map(function (p) { return { vendorId: CH34X_VID, productId: p }; })), supports: function (d) { return (d.configurations || []).some(function (c) { return findInterfaceByClass(c, 0x02) !== null && findInterfaceByClass(c, 0x0a) !== null; }); }, layout: cdcLayout, initialize: async function (ctx, opts) { var ci = ctx.layout.controlInterface; await ctx.controlOut('class', 'interface', 0x20, 0x0000, ci, cdcLineCoding(opts.baudRate)); // SET_LINE_CODING await ctx.controlOut('class', 'interface', 0x22, 0x0003, ci); // DTR+RTS }, setSignals: async function (ctx, sig) { var v = (sig.dataTerminalReady ? 1 : 0) | (sig.requestToSend ? 2 : 0); await ctx.controlOut('class', 'interface', 0x22, v, ctx.layout.controlInterface); }, shutdown: function (ctx) { return ctx.controlOut('class', 'interface', 0x22, 0x0000, ctx.layout.controlInterface); } }; // ───────────────────────────── CP210x (Silicon Labs) ───────────────────────────── var CP210X_VID = 0x10c4; var CP210X_PIDS = [0xea60, 0xea70, 0xea71]; var cp210x = { name: 'CP210x', readQueueDepth: 32, filters: CP210X_PIDS.map(function (p) { return { vendorId: CP210X_VID, productId: p }; }), supports: function (d) { return d.vendorId === CP210X_VID && CP210X_PIDS.indexOf(d.productId) !== -1; }, layout: genericLayout, initialize: async function (ctx, opts) { var ci = ctx.layout.controlInterface; var req = function (r, v, data) { return ctx.controlOut('vendor', 'interface', r, v, ci, data); }; var baud = new Uint8Array(4); new DataView(baud.buffer).setUint32(0, opts.baudRate, true); await req(0x00, 0x0001); // IFC_ENABLE await req(0x07, 0x0300); // SET_MHS await req(0x13, 0x0000, new Uint8Array(16)); // SET_FLOW (关流控) await req(0x1e, 0x0000, baud); // SET_BAUDRATE await req(0x03, 0x0800); // SET_LINE_CTL 8N1 await req(0x07, 0x0301); await req(0x07, 0x0302); log('driver', 'CP210x 初始化完成 @' + opts.baudRate + ' bps'); }, shutdown: async function (ctx) { var ci = ctx.layout.controlInterface; await ctx.controlOut('vendor', 'interface', 0x12, 0x000f, ci); await ctx.controlOut('vendor', 'interface', 0x00, 0x0000, ci); } }; // ───────────────────────────── FTDI ───────────────────────────── var FTDI_VID = 0x0403; var FTDI_PIDS = [0x6001, 0x6010, 0x6011, 0x6014, 0x6015]; /** FTDI 的波特率是 48MHz/(n+frac),只能逼近 */ function calcFtdiBaud(baud) { if (!Number.isInteger(baud) || baud <= 0 || baud > 3500000) throw new Error('FTDI 不支持的波特率: ' + baud); var value, frac, actual; if (baud >= 2500000) { value = 0; frac = 0; actual = 3000000; } else if (baud >= 1750000) { value = 1; frac = 0; actual = 2000000; } else { var n = Math.floor(48000000 / baud); n = (n + 1) >> 1; frac = n & 7; value = n >> 3; if (value > 16383) throw new Error('FTDI 不支持的波特率: ' + baud); actual = Math.floor(48000000 / ((value << 3) + frac)); actual = (actual + 1) >> 1; } if (Math.abs(1 - actual / baud) >= 0.031) throw new Error('FTDI 无法精确表示波特率 ' + baud); var v = value, idx = 0; switch (frac) { case 0: break; case 4: v |= 16384; break; case 2: v |= 32768; break; case 1: v |= 49152; break; case 3: idx = 1; break; case 5: v |= 16384; idx = 1; break; case 6: v |= 32768; idx = 1; break; case 7: v |= 49152; idx = 1; break; } return { value: v, fractionalIndex: idx }; } /** FTDI 每 packetSize 字节会插入 2 字节 modem 状态,必须剥掉 */ function ftdiStripStatus(data, packetSize) { if (data.length < 2) return new Uint8Array(0); var out = []; for (var i = 0; i < data.length; i += packetSize) { var end = Math.min(i + packetSize, data.length); for (var j = Math.min(i + 2, end); j < end; j += 1) out.push(data[j] === undefined ? 0 : data[j]); } return Uint8Array.from(out); } var ftdi = { name: 'FTDI', filters: FTDI_PIDS.map(function (p) { return { vendorId: FTDI_VID, productId: p }; }), supports: function (d) { return d.vendorId === FTDI_VID && FTDI_PIDS.indexOf(d.productId) !== -1; }, layout: genericLayout, transformInput: ftdiStripStatus, initialize: async function (ctx, opts) { var idx = ctx.layout.controlInterface + 1; // FTDI 的控制请求打在下一个接口号上 var req = function (r, v, i) { return ctx.controlOut('vendor', 'device', r, v, i === undefined ? idx : i); }; await req(0x00, 0x0000); // reset await req(0x01, 0x0300); // set baud divisor 高位 await req(0x02, 0x0000); // 低位 var b = calcFtdiBaud(opts.baudRate); var major = ctx.device.deviceVersionMajor === undefined ? 0 : ctx.device.deviceVersionMajor; var needIdx = [7, 8, 9].indexOf(major) !== -1 || ctx.device.configuration.interfaces.length > 1; await req(0x03, b.value, needIdx ? ((b.fractionalIndex << 8) | idx) : b.fractionalIndex); await req(0x04, 0x0008); // 8N1 await req(0x09, 0x0006); // 关流控 await req(0x01, 0x0101); await req(0x01, 0x0202); log('driver', 'FTDI 初始化完成 @' + opts.baudRate + ' bps'); }, shutdown: async function (ctx) { var idx = ctx.layout.controlInterface + 1; await ctx.controlOut('vendor', 'device', 0x00, 0x0001, idx); await ctx.controlOut('vendor', 'device', 0x00, 0x0002, idx); } }; // ───────────────────────────── PL2303 (Prolific) ───────────────────────────── var PL2303_VID = 0x067b; var PL2303_PIDS = [0x2303, 0x23a3, 0x23b3, 0x23c3, 0x23d3, 0x23e3, 0x23f3]; function pl2303LineCoding(baud) { var b = new Uint8Array(7); new DataView(b.buffer).setUint32(0, baud, true); b[4] = 0; b[5] = 0; b[6] = 8; return b; } /** 判断 PL2303 变种:HXN / HX / HX 新片 / 01 */ function pl2303Detect(ctx) { var dev = ctx.device; if (dev.productId !== 0x2303) return Promise.resolve('HXN'); if (dev.deviceClass === 0x02) return Promise.resolve('01'); if (dev.usbVersionMajor === 2) { if (dev.deviceVersionMajor === 3 || dev.deviceVersionMajor === 5) { return ctx.controlIn('vendor', 'device', 0x01, 0x8080, 0x0000, 1).then(function () { return 'T'; })['catch'](function () { return 'HXN'; }); } return Promise.resolve('HXN'); } return Promise.resolve('HX'); } var pl2303 = { name: 'PL2303', filters: PL2303_PIDS.map(function (p) { return { vendorId: PL2303_VID, productId: p }; }), supports: function (d) { return d.vendorId === PL2303_VID && PL2303_PIDS.indexOf(d.productId) !== -1; }, layout: genericLayout, initialize: async function (ctx, opts) { var type = await pl2303Detect(ctx); ctx.pl2303Type = type; var write = function (req, val) { return ctx.controlOut('vendor', 'device', type === 'HXN' ? 0x80 : 0x01, val, req); }; var read = function (req, val) { return ctx.controlIn('vendor', 'device', type === 'HXN' ? 0x81 : 0x01, val, req, 1); }; var vendor = function (req, val) { return type === 'HXN' ? write(req, val) : null; }; var clearHalt = async function () { if (type === 'HXN') await write(0x0007, 0x0003); else { await write(0x0008, 0x0000); await write(0x0009, 0x0000); } }; await clearHalt(); if (type !== 'HXN') { await read(0x8484, 0x0000); await write(0x0404, 0x0000); await read(0x8484, 0x0000); await read(0x8383, 0x0000); await read(0x8484, 0x0000); await write(0x0404, 0x0001); await read(0x8484, 0x0000); await read(0x8383, 0x0000); await write(0x0000, 0x0001); await write(0x0001, 0x0000); await write(0x0002, type === '01' ? 0x0024 : 0x0044); } await ctx.controlOut('class', 'interface', 0x22, 0x0000, 0x0000); // 先复位控制线 if (type === 'HXN') await write(0x000a, 0x00ff); else await write(0x0000, 0x0000); await ctx.controlOut('class', 'interface', 0x20, 0x0000, 0x0000, pl2303LineCoding(opts.baudRate)); await clearHalt(); await ctx.controlOut('class', 'interface', 0x22, 0x0003, 0x0000); log('driver', 'PL2303 初始化完成 (' + type + ') @' + opts.baudRate + ' bps'); }, shutdown: async function (ctx) { var type = ctx.pl2303Type; if (!type) return; if (type === 'HXN') await ctx.controlOut('vendor', 'device', 0x80, 0x0003, 0x0007); else { await ctx.controlOut('vendor', 'device', 0x01, 0x0000, 0x0008); await ctx.controlOut('vendor', 'device', 0x01, 0x0000, 0x0009); } } }; // ───────────────────────────── 驱动注册表 ───────────────────────────── var DRIVERS = [ch34x, cdcAcm, cp210x, ftdi, pl2303]; api.drivers = DRIVERS; api.calcCh34xBaud = calcCh34xBaud; api.calcFtdiBaud = calcFtdiBaud; api.hasWebUSB = hasWebUSB; api.hasWebSerial = hasWebSerial; api.isAndroid = isAndroid; /** 合并所有驱动的 filters,用于 requestDevice(去重) */ function allFilters() { var seen = {}, out = []; DRIVERS.forEach(function (d) { (d.filters || []).forEach(function (f) { var k = JSON.stringify(f); if (!seen[k]) { seen[k] = 1; out.push(f); } }); }); return out; } api.supportedFilters = allFilters; /** 只按 CH34x 过滤(默认;安卓上弹窗列表更干净) */ function ch34xFilters() { return ch34x.filters.slice(); } api.ch34xFilters = ch34xFilters; api.chipName = function (vid, pid) { if (vid === CH34X_VID) { if (pid === 0x7523 || pid === 0x5523) return 'ch340'; if (WCH_CDC_PIDS.indexOf(pid) !== -1) return 'ch34x-cdc'; } if (vid === CP210X_VID) return 'cp210x'; if (vid === FTDI_VID) return 'ftdi'; if (vid === PL2303_VID) return 'pl2303'; return 'unknown'; }; function pickDriver(device) { for (var i = 0; i < DRIVERS.length; i++) { try { if (DRIVERS[i].supports(device)) return DRIVERS[i]; } catch (e) { /* 某些设备描述符残缺,跳过 */ } } return null; } api.pickDriver = pickDriver; // ───────────────────────────── 端口对象(Web Serial 兼容) ───────────────────────────── /** * 暴露给业务的端口对象,接口与 Web Serial 的 SerialPort 对齐: * open({baudRate,...}) / close() / getInfo() / readable / writable / setSignals() / forget() * 另加:device(原始 USBDevice)、chip、addEventListener('disconnect') */ function UsbSerialPort(device, driver, defaults) { this.device = device; this.driver = driver; this.options = defaults || {}; this.readable = null; this.writable = null; this.opened = false; this.closing = false; this.claimedInterfaces = []; this.context = null; this.reader = null; this.writer = null; this.chip = api.chipName(device.vendorId, device.productId); this.readDiagnostics = { transfers: 0, bytes: 0, transferSize: 0 }; this._readQueue = []; this._listeners = { disconnect: [] }; this._onUsbDisconnect = null; } UsbSerialPort.prototype.getInfo = function () { return { usbVendorId: this.device.vendorId, usbProductId: this.device.productId }; }; UsbSerialPort.prototype.getDiagnostics = function () { return { transfers: this.readDiagnostics.transfers, bytes: this.readDiagnostics.bytes, transferSize: this.readDiagnostics.transferSize }; }; UsbSerialPort.prototype.addEventListener = function (type, fn) { if (this._listeners[type]) this._listeners[type].push(fn); }; UsbSerialPort.prototype.removeEventListener = function (type, fn) { if (!this._listeners[type]) return; this._listeners[type] = this._listeners[type].filter(function (f) { return f !== fn; }); }; UsbSerialPort.prototype._emit = function (type, ev) { (this._listeners[type] || []).forEach(function (fn) { try { fn(ev); } catch (e) { log('warn', 'disconnect 回调异常: ' + e.message); } }); }; UsbSerialPort.prototype.open = async function (openOptions) { var opts = { baudRate: DEFAULT_BAUD, dataBits: 8, stopBits: 1, parity: 'none', flowControl: 'none' }; var k; for (k in this.options) if (Object.prototype.hasOwnProperty.call(this.options, k)) opts[k] = this.options[k]; for (k in (openOptions || {})) if (Object.prototype.hasOwnProperty.call(openOptions, k)) opts[k] = openOptions[k]; if (this.opened) throw new Error('端口已经打开'); if (opts.dataBits !== 8 || opts.stopBits !== 1 || opts.parity !== 'none') { throw new Error('浏览器 USB 通道目前只支持 8N1'); } log('open', this.driver.name + ' @' + opts.baudRate + ' bps'); this.closing = false; this.readDiagnostics = { transfers: 0, bytes: 0, transferSize: 0 }; var device = this.device; try { if (!device.opened) await device.open(); if (!device.configuration) { var cfg = device.configurations && device.configurations[0]; if (!cfg) throw new Error('USB 设备没有可用配置'); await device.selectConfiguration(cfg.configurationValue); } var configuration = device.configuration; if (!configuration) throw new Error('读取 USB 配置失败'); var layout = this.driver.layout(configuration); this.layout = layout; var uniq = layout.claimedInterfaces.filter(function (v, i, a) { return a.indexOf(v) === i; }); for (var i = 0; i < uniq.length; i++) { try { await device.claimInterface(uniq[i]); } catch (e) { throw new Error('无法占用 USB 接口 ' + uniq[i] + '(可能被系统驱动/其它程序占用):' + (e && e.message ? e.message : e)); } this.claimedInterfaces.push(uniq[i]); } var alts = layout.interfaceAlternates || []; for (var j = 0; j < alts.length; j++) { if (alts[j].alternateSetting !== 0) await device.selectAlternateInterface(alts[j].interfaceNumber, alts[j].alternateSetting); } this.context = new ControlContext(device, layout); this.context.device = device; await this.driver.initialize(this.context, opts); this._createStreams(opts); this.opened = true; this._watchUnplug(); log('open', '已连接 ' + this.driver.name + ' (VID 0x' + device.vendorId.toString(16) + ' PID 0x' + device.productId.toString(16) + ')'); return this; } catch (e) { await this._cleanup(false); throw e; } }; UsbSerialPort.prototype._watchUnplug = function () { if (!hasWebUSB() || typeof navigator.usb.addEventListener !== 'function') return; var self = this; this._onUsbDisconnect = function (ev) { if (ev && ev.device && ev.device !== self.device) return; log('warn', 'USB 设备已拔出'); self.closing = true; self.opened = false; self._emit('disconnect', { type: 'disconnect', target: self }); }; navigator.usb.addEventListener('disconnect', this._onUsbDisconnect); }; UsbSerialPort.prototype._createStreams = async function (opts) { var self = this; var layout = this.layout; var epIn = layout.input, epOut = layout.output; var packetSize = Math.max(1, epIn.packetSize || 64); var transferSize = Math.max(packetSize, opts.readTransferSize || this.driver.readTransferSize || packetSize); var queueDepth = Math.max(1, opts.readQueueDepth || this.driver.readQueueDepth || 4); this.readDiagnostics.transferSize = transferSize; var queue = []; this._readQueue = queue; var refill = function () { queue.push(self.device.transferIn(epIn.endpointNumber, transferSize).then( function (result) { return { result: result }; }, function (error) { return { error: error }; } )); }; this.readable = new ReadableStream({ start: function () { for (var i = 0; i < queueDepth; i++) refill(); }, pull: async function (controller) { if (self.closing) { try { controller.close(); } catch (e) {} return; } try { var pending = queue.shift(); if (!pending) throw new Error('USB 读队列为空'); var settled = await pending; if (self.closing) { try { controller.close(); } catch (e) {} return; } if (settled.error) throw settled.error; refill(); var res = settled.result; if (res.status !== 'ok') throw new Error('USB bulk 读失败 (' + res.status + ')'); self.readDiagnostics.transfers++; self.readDiagnostics.bytes += (res.data && res.data.byteLength) || 0; if (res.data && res.data.byteLength > 0) { var chunk = new Uint8Array(res.data.buffer, res.data.byteOffset, res.data.byteLength).slice(); if (self.driver.transformInput) chunk = self.driver.transformInput(chunk, packetSize); if (chunk.length > 0) controller.enqueue(chunk); } } catch (err) { if (self.closing) { try { controller.close(); } catch (e) {} } else { try { controller.error(err); } catch (e) {} } } }, cancel: function () { self.closing = true; } }, { highWaterMark: 0 }); this.writable = new WritableStream({ write: async function (chunk) { var data = toU8(chunk); var offset = 0; while (offset < data.length) { var slice = data.subarray(offset, Math.min(offset + WRITE_CHUNK, data.length)); var res = await self.device.transferOut(epOut.endpointNumber, slice); if (res.status !== 'ok') throw new Error('USB bulk 写失败 (' + res.status + ')'); var written = res.bytesWritten === undefined ? slice.length : res.bytesWritten; if (written <= 0 || written > slice.length) throw new Error('USB bulk 写没有进展'); offset += written; } } }); }; /** 便捷:写一段数据(首次调用会持有 writer,和直接玩 writable 二选一) */ UsbSerialPort.prototype.write = async function (data) { if (!this.opened) throw new Error('端口还没打开'); if (!this.writer) this.writer = this.writable.getWriter(); await this.writer.write(toU8(data)); }; /** 便捷:读一段数据,timeoutMs 内没数据返回 null */ UsbSerialPort.prototype.read = async function (timeoutMs) { if (!this.opened) throw new Error('端口还没打开'); if (!this.reader) this.reader = this.readable.getReader(); var reader = this.reader; if (!timeoutMs) { var r = await reader.read(); return r.done ? null : r.value; } var timer = null; var timeout = new Promise(function (resolve) { timer = setTimeout(function () { resolve({ timeout: true }); }, timeoutMs); }); var result = await Promise.race([reader.read().then(function (r) { return { read: r }; }), timeout]); if (timer) clearTimeout(timer); if (result.timeout) return null; return result.read.done ? null : result.read.value; }; UsbSerialPort.prototype.setSignals = async function (signals) { if (!this.opened || !this.context) throw new Error('端口还没打开'); if (!this.driver.setSignals) throw new Error(this.driver.name + ' 不支持 DTR/RTS 控制'); await this.driver.setSignals(this.context, signals || {}); }; UsbSerialPort.prototype.forget = async function () { if (this.device.forget) await this.device.forget(); }; UsbSerialPort.prototype.close = async function () { if (!this.opened && this.claimedInterfaces.length === 0) return; this.closing = true; this.readable = null; this.writable = null; await this._cleanup(true); this.opened = false; }; UsbSerialPort.prototype._cleanup = async function (withShutdown) { if (this._onUsbDisconnect && hasWebUSB() && typeof navigator.usb.removeEventListener === 'function') { try { navigator.usb.removeEventListener('disconnect', this._onUsbDisconnect); } catch (e) {} } this._onUsbDisconnect = null; try { if (this.reader) { try { this.reader.releaseLock(); } catch (e) {} this.reader = null; } } catch (e) {} try { if (this.writer) { try { this.writer.releaseLock(); } catch (e) {} this.writer = null; } } catch (e) {} if (withShutdown && this.context && this.driver.shutdown) { try { await this.driver.shutdown(this.context); } catch (e) { log('warn', 'shutdown 失败: ' + (e && e.message)); } } this.context = null; var claimed = this.claimedInterfaces.slice().reverse(); this.claimedInterfaces = []; for (var i = 0; i < claimed.length; i++) { try { await this.device.releaseInterface(claimed[i]); } catch (e) {} } this.opened = false; try { await this.device.close(); } catch (e) {} log('close', '端口已关闭'); }; api.UsbSerialPort = UsbSerialPort; // ───────────────────────────── 连接入口 ───────────────────────────── /** 弹系统选设备框,返回未打开的端口对象(Web Serial 同款语义) */ api.requestPort = async function (options) { if (!hasWebUSB()) throw new Error('这个浏览器没有 WebUSB(iOS 不支持,安卓/桌面请用 Chrome/Edge)'); options = options || {}; var filters = options.filters || (options.allDrivers ? allFilters() : ch34xFilters()); log('chooser', '等待用户选择 USB 设备… filters=' + JSON.stringify(filters)); var device = await navigator.usb.requestDevice({ filters: filters }); return api.fromDevice(device, options); }; /** 用已授权(getDevices)的设备直接建端口 */ api.fromDevice = function (device, options) { var driver = pickDriver(device); if (!driver) { throw new Error('没有匹配的浏览器端驱动: VID 0x' + device.vendorId.toString(16) + ' PID 0x' + device.productId.toString(16)); } return new UsbSerialPort(device, driver, options || {}); }; /** 之前授权过的设备(拔插后免再次弹窗) */ api.getAuthorizedDevices = async function (options) { if (!hasWebUSB()) return []; var filters = (options && options.filters) || (options && options.allDrivers ? allFilters() : ch34xFilters()); var devices = await navigator.usb.getDevices(); return devices.filter(function (d) { if (pickDriver(d)) return true; return filters.some(function (f) { if (f.vendorId !== undefined && f.vendorId !== d.vendorId) return false; if (f.productId !== undefined && f.productId !== d.productId) return false; return f.vendorId !== undefined; }); }); }; api.getAuthorizedPorts = async function (options) { var devices = await api.getAuthorizedDevices(options); return devices.map(function (d) { return api.fromDevice(d, options); }); }; /** 一键连接:选设备 + 打开(安卓上就是这两步) */ api.connect = async function (options) { var port = await api.requestPort(options); await port.open(options || {}); return port; }; return api; });