- 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,无需硬件)
143 lines
6.8 KiB
Markdown
143 lines
6.8 KiB
Markdown
# ch340-webusb
|
||
|
||
浏览器直连 **CH340 / CH341**(以及 CDC-ACM、CP210x、FTDI、PL2303)USB 转串口芯片的纯前端驱动。
|
||
|
||
- **不用装 App、不用 root、不用装驱动**:安卓 Chrome/Edge 走 **WebUSB**,桌面 Chrome/Edge 走 **Web Serial**。
|
||
- **安卓浏览器终于能连 CH340 了**:安卓版 Chrome 有 WebUSB,但至今没有 Web Serial;本项目在 JS 里补上了 Web Serial 缺失的"芯片驱动层"(波特率寄存器、流控、LCR、DTR/RTS),并提供一层 `navigator.serial` 垫片,**让已有的串口业务代码零改动跑在安卓浏览器里**。
|
||
|
||
```
|
||
┌──────────────┐ navigator.usb (WebUSB) ┌──────────────┐
|
||
│ 你的网页 │ ─────────────────────────▶ │ CH340 芯片 │──── UART ────▶ 设备
|
||
│ ch340-webusb │ vendor 控制传输 + bulk │ (无需驱动) │
|
||
└──────────────┘ └──────────────┘
|
||
```
|
||
|
||
## 快速开始
|
||
|
||
```html
|
||
<script src="/ch340-webusb.js?v=1"></script>
|
||
<script src="/ch340-serial-shim.js?v=1"></script> <!-- 需要免改业务代码时加载 -->
|
||
```
|
||
|
||
### 方式一:直接用(WebUSB,安卓 + 桌面通用)
|
||
|
||
```js
|
||
const port = await TamaCh340.requestPort({ baudRate: 460800 }); // 弹系统选设备框
|
||
await port.open({ baudRate: 460800 }); // 内部完成 CH340 初始化
|
||
const reader = port.readable.getReader();
|
||
const writer = port.writable.getWriter();
|
||
|
||
await writer.write(new TextEncoder().encode('hello\r\n'));
|
||
const { value } = await reader.read(); // Uint8Array
|
||
await port.close();
|
||
```
|
||
|
||
### 方式二:当它是 Web Serial(零改动接入老站)
|
||
|
||
垫片会在**安卓**上(有 WebUSB、没有 Web Serial 时)自动补出 `navigator.serial`,
|
||
之后原来写给 Web Serial 的代码/打包好的 SPA 直接就能用:
|
||
|
||
```js
|
||
const port = await navigator.serial.requestPort(); // 安卓上返回的是 WebUSB 的 CH340
|
||
await port.open({ baudRate: 460800 });
|
||
```
|
||
|
||
> 桌面浏览器本来就有真 Web Serial,垫片**不会**安装,避免改变原有行为。
|
||
|
||
### 方式三:测试台
|
||
|
||
打开 `ch340-test.html`(需要 HTTPS 或 localhost):连接、收发、HEX、DTR/RTS、回环自测、诊断计数一屏搞定。
|
||
|
||
```bash
|
||
npx serve . # 或 python -m http.server 8080
|
||
# 手机访问 https 地址;安卓上建议用 cloudflared/ngrok 暴露本地端口
|
||
```
|
||
|
||
## API
|
||
|
||
| 成员 | 说明 |
|
||
|---|---|
|
||
| `TamaCh340.requestPort(opts)` | 弹设备选择框,返回**未打开**的端口对象 |
|
||
| `TamaCh340.connect(opts)` | 选择 + 打开一步到位 |
|
||
| `TamaCh340.fromDevice(device, opts)` | 用已授权的 `USBDevice` 建端口 |
|
||
| `TamaCh340.getAuthorizedDevices()` | 之前授权过的设备(拔插后可免弹窗重连) |
|
||
| `TamaCh340.chipName(vid, pid)` | `'ch340' / 'cp210x' / 'ftdi' / 'pl2303' / ...` |
|
||
| `TamaCh340.calcCh34xBaud(baud)` | `{factor, divisor}`,与 Linux `ch341.c` 一致 |
|
||
| `TamaCh340.onLog = fn` / `debug = true` | 日志钩子 / 控制台输出 |
|
||
|
||
端口对象(与 Web Serial 的 `SerialPort` 对齐):
|
||
`open({baudRate,dataBits,stopBits,parity,flowControl,readTransferSize,readQueueDepth})` ·
|
||
`close()` · `getInfo()` · `readable` · `writable` · `read(ms)` · `write(bytes)` ·
|
||
`setSignals({dataTerminalReady,requestToSend})` · `forget()` · `addEventListener('disconnect', fn)` ·
|
||
`getDiagnostics()` · `device`(原始 `USBDevice`)· `chip`
|
||
|
||
## 支持的芯片
|
||
|
||
| 驱动 | VID:PID | 说明 |
|
||
|---|---|---|
|
||
| **CH340/CH341** | `1a86:7523`, `1a86:5523`(+ `5512/7522` 克隆) | 主目标;原厂拓麻歌子 USB 线就是 CH340C |
|
||
| CDC-ACM | `1a86:55d2…55e7` 等 | CH343 / CH9102 / CH347 的 CDC 模式 |
|
||
| CP210x | `10c4:ea60/ea70/ea71` | Silicon Labs |
|
||
| FTDI | `0403:6001/6010/6011/6014/6015` | 含 modem 状态字节剥离 |
|
||
| PL2303 | `067b:2303…23f3` | 含 HXN/HX/01/T 变种识别 |
|
||
|
||
## 各平台要不要装驱动?
|
||
|
||
| 平台 | 浏览器通道 | 需要额外驱动吗 |
|
||
|---|---|---|
|
||
| **安卓**(Chrome/Edge/Brave/Kiwi) | WebUSB | **完全不用**。CH340 是 vendor-specific 类,内核不绑定,浏览器直接抢占接口 |
|
||
| **Linux** | Web Serial(桌面首选) | 不用,内核自带 `ch341` |
|
||
| **macOS** | Web Serial(桌面首选) | 不用,系统自带 Apple USB 串口驱动 |
|
||
| **Windows** | Web Serial(桌面首选) | 装一次 WCH 官方 CH341SER 即可;**WebUSB 反而需要把设备换成 WinUSB 驱动(Zadig)才能用**——所以桌面端本库优先走 Web Serial |
|
||
|
||
也就是说:**安卓端零驱动**(这正是本项目的重点),桌面端用系统/官方驱动 + Web Serial 即可。
|
||
垫片只在"有 WebUSB 且没有 Web Serial"时安装,桌面端不会被改变行为。
|
||
|
||
## 安卓注意事项
|
||
|
||
1. **必须 HTTPS**(`localhost` 除外),且要用**用户手势**(点击)触发连接。
|
||
2. 只有 **Chromium 系**浏览器可以:Chrome / Edge / Brave / Kiwi。Firefox 安卓没有 WebUSB,iOS 全线没有。
|
||
3. 需要 **OTG**:手机要支持 USB Host;部分机型 OTG 与充电互斥、供电不足会表现为反复掉线。
|
||
4. **CH340 是 vendor-specific 类(0xFF)**,安卓内核不会去绑定它,所以能被浏览器抢占;
|
||
CDC-ACM 类设备在个别 ROM 上会被内核 `cdc_acm` 先占,`claimInterface` 会失败(报错已给出提示)。
|
||
5. 连接后页面切后台 / 锁屏会被节流断流,长任务建议申请 Wake Lock:
|
||
`await navigator.wakeLock.request('screen')`。
|
||
6. 拔插后 `getDevices()` 能拿回已授权设备,可直接重连,不必再弹窗。
|
||
|
||
## 实现要点(为什么必须自己写驱动)
|
||
|
||
浏览器的 Web Serial 在桌面端内置了芯片驱动,而 WebUSB 只给"裸 USB"。
|
||
所以本驱动用控制传输复刻了内核里的那套寄存器操作,CH340 的初始化序列:
|
||
|
||
```
|
||
0x5F 读版本自检
|
||
0xA1 0000 0000 清 modem 控制
|
||
0x9A 1312 <factor> 波特率(先按 9600 配置)
|
||
0x9A 0F2C <divisor>
|
||
0x95 2518 (读 LCR) 0x9A 2518 00C3 → 8 数据位 / 1 停止位 / 无校验
|
||
0x95 0706 (读)
|
||
0xA1 501F D90A 关硬件流控
|
||
0x9A 1312/0F2C <目标波特率>
|
||
0x9A 2518 00C3
|
||
0xA4 FFFF DTR/RTS 复位
|
||
0xA4 FF9F DTR+RTS 有效(位取反写入)
|
||
```
|
||
|
||
波特率分频:`factor = floor(1532620800 / baud)`,>65520 则右移 3 位并叠加预分频,
|
||
再取 `0x10000 - factor`,高位写 `0x1312`、低位写 `0x0F2C`。
|
||
详见 `docs/ch34x-registers.md`。
|
||
|
||
## 测试
|
||
|
||
```bash
|
||
node test/driver.test.mjs
|
||
```
|
||
|
||
14 项断言全部离线跑(mock `USBDevice`):波特率算法、完整控制传输序列、8N1 校验、
|
||
4096 分片写、ReadableStream 读、DTR/RTS、关闭清理、CDC 布局与 SET_LINE_CODING、
|
||
FTDI modem 字节剥离、垫片自动安装。
|
||
|
||
## 许可
|
||
|
||
MIT
|