feat: 浏览器端 CH340/CH341 WebUSB 驱动 + navigator.serial 垫片

- ch340-webusb.js: WebUSB 上完整的 USB-Serial 栈
  · CH340/CH341 vendor 控制传输初始化(对齐 Linux ch341.c)
  · 波特率分频算法 calcCh34xBaud(1532620800 基准)
  · bulk IN 32 深读队列 + ReadableStream / 4096 分片写 + WritableStream
  · DTR/RTS(0xA4 取反写入)、拔插事件、诊断计数
  · 附带 CDC-ACM(CH343/CH9102/CH347)、CP210x、FTDI(含 modem 字节剥离)、PL2303(HXN/HX/01/T)
- ch340-serial-shim.js: 安卓上补出 navigator.serial,老代码零改动可用
- ch340-test.html: 连接/收发/HEX/DTR-RTS/回环自测 测试台
- docs/ch34x-registers.md: 寄存器与控制传输逆向说明
- test/driver.test.mjs: 15 项 mock 断言(node,无需硬件)
This commit is contained in:
2026-09-23 00:35:13 +08:00
commit 7af53efaa9
9 changed files with 1854 additions and 0 deletions
+752
View File
@@ -0,0 +1,752 @@
/*!
* 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;
});