/*!
* 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));
}
})();