Files
ch340-webusb/ch340-serial-shim.js
T
jiangchen 7af53efaa9 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,无需硬件)
2026-09-23 00:35:13 +08:00

175 lines
7.5 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-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));
}
})();