/*! * 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)零改动即可在安卓浏览器里跑。 * * 加载顺序(重要): * * * * * * 可选配置:加载前设置 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)); } })();