Files
ch340-webusb/ch340-webusb.js
T
jiangchen 6c5e647c8a
test / test (push) Canceled after 0s
feat: v1.0.0 收尾 — 五芯片 DTR/RTS、类型声明、示例、CI
- CP210x / FTDI / PL2303 补齐 setSignals()(编码与各自初始化序列一致)
- 新增 index.d.ts(TypeScript 类型声明)、examples/minimal.html(最小接入示例)
- 新增 .gitea/workflows/ci.yml(push 时跑离线单测)、.gitattributes(强制 LF)、CHANGELOG.md
- package.json 补 repository / homepage / bugs / types / engines
- 单测 16 项全部通过:node test/driver.test.mjs
2026-09-23 00:41:29 +08:00

769 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*!
* 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');
},
/** SET_MHS(0x07):高字节=掩码,低字节=电平;0x03 掩码表示"两条线都由本调用决定" */
setSignals: async function (ctx, sig) {
var bits = (sig.dataTerminalReady ? 0x01 : 0) | (sig.requestToSend ? 0x02 : 0);
await ctx.controlOut('vendor', 'interface', 0x07, 0x0300 | bits, ctx.layout.controlInterface);
},
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');
},
/** SET_MODEM_CTRL(0x01):高字节=掩码,低字节=电平(0x01 DTR / 0x02 RTS) */
setSignals: async function (ctx, sig) {
var idx = ctx.layout.controlInterface + 1;
var bits = (sig.dataTerminalReady ? 0x01 : 0) | (sig.requestToSend ? 0x02 : 0);
await ctx.controlOut('vendor', 'device', 0x01, 0x0300 | bits, idx);
},
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');
},
/** 标准 CDC 请求 SET_CONTROL_LINE_STATE(0x22):bit0=DTR, bit1=RTS */
setSignals: async function (ctx, sig) {
var bits = (sig.dataTerminalReady ? 0x01 : 0) | (sig.requestToSend ? 0x02 : 0);
await ctx.controlOut('class', 'interface', 0x22, bits, 0x0000);
},
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;
});