- 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,无需硬件)
175 lines
7.5 KiB
JavaScript
175 lines
7.5 KiB
JavaScript
/*!
|
||
* ch340-serial-shim.js — 让 WebUSB 的 CH340 变成 navigator.serial
|
||
* ------------------------------------------------------------------
|
||
* 桌面 Chrome/Edge 本来就有 Web Serial,一行 port.open({baudRate}) 就能用 CH340。
|
||
* 安卓 Chrome 有 WebUSB 但没有 Web Serial,于是所有 "Connect USB" 都会显示不支持。
|
||
* 本垫片在安卓上补出 navigator.serial:requestPort() 打开的是 WebUSB 的 CH340,
|
||
* 返回的对象与 SerialPort 语义一致(open/close/getInfo/readable/writable/setSignals),
|
||
* 于是已有串口业务代码(包括打包好的 SPA)零改动即可在安卓浏览器里跑。
|
||
*
|
||
* 加载顺序(重要):
|
||
* <script src="/ch340-webusb.js?v=1"></script>
|
||
* <script src="/ch340-serial-shim.js?v=1"></script> <!-- 放在其它 navigator.serial 垫片之前 -->
|
||
* <script src="/bluetooth-serial-shim.js?v=1"></script> <!-- 它仍会暴露 window.__btSerial -->
|
||
* <script src="/业务 bundle.js"></script>
|
||
*
|
||
* 可选配置:加载前设置 window.__CH340_SHIM
|
||
* { autoInstall:true, baudRate:460800, allDrivers:false, force:false, prefer:'usb' }
|
||
* window.__CH340_PREFER = 'bluetooth' → 在 chain 模式下让位给原有垫片(例如 BLE)
|
||
* 许可:MIT
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
var g = typeof globalThis !== 'undefined' ? globalThis : (typeof window !== 'undefined' ? window : this);
|
||
var api = g.TamaCh340;
|
||
var state = { installed: false, reason: null, previous: null };
|
||
var listeners = { connect: [], disconnect: [] };
|
||
|
||
function merge(a, b) {
|
||
var out = {}, k;
|
||
for (k in (a || {})) if (Object.prototype.hasOwnProperty.call(a, k)) out[k] = a[k];
|
||
for (k in (b || {})) if (Object.prototype.hasOwnProperty.call(b, k)) out[k] = b[k];
|
||
return out;
|
||
}
|
||
|
||
/** Web Serial 的 filters([{usbVendorId,usbProductId}]) → WebUSB filters */
|
||
function toUsbFilters(serialOptions, fallbackOptions) {
|
||
var f = serialOptions && serialOptions.filters;
|
||
if (Array.isArray(f) && f.length) {
|
||
var mapped = f
|
||
.filter(function (x) { return x && x.usbVendorId !== undefined; })
|
||
.map(function (x) { return { vendorId: x.usbVendorId, productId: x.usbProductId }; });
|
||
if (mapped.length) return mapped;
|
||
}
|
||
if (fallbackOptions && fallbackOptions.filters) return fallbackOptions.filters;
|
||
if (fallbackOptions && fallbackOptions.allDrivers) return api.supportedFilters();
|
||
return api.ch34xFilters();
|
||
}
|
||
|
||
function emit(type, port) {
|
||
var ev = { type: type, port: port, target: null };
|
||
var arr = listeners[type] || [];
|
||
for (var i = 0; i < arr.length; i++) { try { arr[i](ev); } catch (e) {} }
|
||
var serial = g.navigator && g.navigator.serial;
|
||
var handler = serial && (type === 'connect' ? serial.onconnect : serial.ondisconnect);
|
||
if (typeof handler === 'function') { try { handler(ev); } catch (e) {} }
|
||
}
|
||
|
||
/** 包装端口:补齐 Web Serial 的细节语义 */
|
||
function wrap(port) {
|
||
if (port.__ch340Wrapped) return port;
|
||
port.__ch340Wrapped = true;
|
||
port._signals = { dataTerminalReady: false, requestToSend: false };
|
||
var rawSet = port.setSignals.bind(port);
|
||
port.setSignals = async function (sig) {
|
||
sig = sig || {};
|
||
await rawSet(sig);
|
||
if (sig.dataTerminalReady !== undefined) port._signals.dataTerminalReady = !!sig.dataTerminalReady;
|
||
if (sig.requestToSend !== undefined) port._signals.requestToSend = !!sig.requestToSend;
|
||
};
|
||
port.getSignals = async function () {
|
||
return {
|
||
dataCarrierDetect: false, clearToSend: false, ringIndicator: false,
|
||
dataSetReady: !!port.opened,
|
||
dataTerminalReady: port._signals.dataTerminalReady,
|
||
requestToSend: port._signals.requestToSend
|
||
};
|
||
};
|
||
return port;
|
||
}
|
||
|
||
/**
|
||
* 安装垫片
|
||
* @returns {{ok:boolean, reason?:string}}
|
||
*/
|
||
function install(options) {
|
||
options = options || {};
|
||
var nav = g.navigator;
|
||
if (!nav) { state.reason = '没有 navigator'; return { ok: false, reason: state.reason }; }
|
||
if (!api) { state.reason = '请先加载 ch340-webusb.js'; return { ok: false, reason: state.reason }; }
|
||
if (!nav.usb) { state.reason = '这个浏览器没有 WebUSB(iOS 不支持)'; return { ok: false, reason: state.reason }; }
|
||
if (nav.serial && nav.serial.__ch340Shim) {
|
||
state.installed = true; state.reason = null; return { ok: true, reason: 'already' };
|
||
}
|
||
if (nav.serial && !options.force) {
|
||
state.reason = 'navigator.serial 已存在(桌面浏览器或其它垫片先装了);需要接管请传 {force:true}';
|
||
return { ok: false, reason: state.reason };
|
||
}
|
||
|
||
var previous = nav.serial || null;
|
||
state.previous = previous;
|
||
|
||
async function usbRequestPort(serialOptions) {
|
||
var filters = toUsbFilters(serialOptions, options);
|
||
var device = await nav.usb.requestDevice({ filters: filters });
|
||
return wrap(api.fromDevice(device, merge(options, serialOptions)));
|
||
}
|
||
|
||
nav.serial = {
|
||
__ch340Shim: true,
|
||
__previous: previous,
|
||
requestPort: async function (serialOptions) {
|
||
var prefer = g.__CH340_PREFER || options.prefer || 'usb';
|
||
if (prefer === 'bluetooth' && previous && previous.requestPort) {
|
||
return previous.requestPort(serialOptions);
|
||
}
|
||
try {
|
||
return await usbRequestPort(serialOptions);
|
||
} catch (e) {
|
||
// 用户取消 USB 选择框时,可选回退到原有垫片(例如 BLE)
|
||
if (e && e.name === 'NotFoundError' && options.fallbackOnCancel && previous && previous.requestPort) {
|
||
return previous.requestPort(serialOptions);
|
||
}
|
||
throw e;
|
||
}
|
||
},
|
||
getPorts: async function () {
|
||
var devices = await api.getAuthorizedDevices(options);
|
||
return devices.map(function (d) { return wrap(api.fromDevice(d, options)); });
|
||
},
|
||
addEventListener: function (type, fn) { if (listeners[type]) listeners[type].push(fn); },
|
||
removeEventListener: function (type, fn) {
|
||
if (!listeners[type]) return;
|
||
listeners[type] = listeners[type].filter(function (f) { return f !== fn; });
|
||
},
|
||
onconnect: null,
|
||
ondisconnect: null
|
||
};
|
||
|
||
// 拔插事件透传(业务代码常靠它做重连)
|
||
try {
|
||
nav.usb.addEventListener('connect', function (ev) {
|
||
var d = ev && ev.device;
|
||
if (!d) return;
|
||
try { emit('connect', wrap(api.fromDevice(d, options))); } catch (e) {}
|
||
});
|
||
nav.usb.addEventListener('disconnect', function (ev) {
|
||
var d = ev && ev.device;
|
||
if (!d) { emit('disconnect', null); return; }
|
||
try { emit('disconnect', wrap(api.fromDevice(d, options))); } catch (e) { emit('disconnect', null); }
|
||
});
|
||
} catch (e) { /* 个别实现没有 addEventListener,忽略 */ }
|
||
|
||
state.installed = true;
|
||
state.reason = null;
|
||
if (api.debug && typeof console !== 'undefined') console.log('[ch340] navigator.serial 垫片已安装');
|
||
return { ok: true };
|
||
}
|
||
|
||
g.TamaSerialShim = {
|
||
install: install,
|
||
get installed() { return state.installed; },
|
||
get reason() { return state.reason; },
|
||
get previous() { return state.previous; },
|
||
version: api ? api.version : null
|
||
};
|
||
|
||
// ── 自动安装:有 WebUSB 且没有 navigator.serial 时装上(安卓默认走这条;桌面不动) ──
|
||
var cfg = merge({ autoInstall: true, baudRate: 460800 }, g.__CH340_SHIM);
|
||
if (cfg.autoInstall && g.navigator && g.navigator.usb && !g.navigator.serial && api) {
|
||
var res = install(cfg);
|
||
if (api.debug && typeof console !== 'undefined') console.log('[ch340] 自动安装: ' + JSON.stringify(res));
|
||
}
|
||
})();
|