feat: v1.0.0 收尾 — 五芯片 DTR/RTS、类型声明、示例、CI
- CP210x / FTDI / PL2303 补齐 setSignals()(编码与各自初始化序列一致) - 新增 index.d.ts(TypeScript 类型声明)、examples/minimal.html(最小接入示例) - 新增 .gitea/workflows/ci.yml(push 时跑离线单测)、.gitattributes(强制 LF)、CHANGELOG.md - package.json 补 repository / homepage / bugs / types / engines - 单测 16 项全部通过:node test/driver.test.mjs
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
* text=auto eol=lf
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
name: test
|
||||||
|
on: [push, pull_request]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
- name: 运行驱动单测(无需硬件)
|
||||||
|
run: node test/driver.test.mjs
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
|
||||||
|
首个公开版本。
|
||||||
|
|
||||||
|
- ch340-webusb.js:WebUSB 上的 USB-Serial 栈
|
||||||
|
- CH340/CH341(1a86:7523 / 1a86:5523,兼容 5512/7522 克隆)完整初始化序列,对齐 Linux ch341.c
|
||||||
|
- 波特率分频 calcCh34xBaud();32 深度预排队读 + ReadableStream;4096 分片写 + WritableStream
|
||||||
|
- DTR/RTS(CH340 用 0xA4 取反写入)、拔插事件、读诊断计数
|
||||||
|
- 附带 CDC-ACM(CH343/CH9102/CH347)、CP210x、FTDI(含 modem 状态字节剥离)、PL2303(HXN/HX/01/T)
|
||||||
|
- 五种芯片都实现了 setSignals()(DTR/RTS)
|
||||||
|
- ch340-serial-shim.js:安卓上补出 navigator.serial,Web Serial 业务代码零改动可用;支持 chain 模式让位给蓝牙垫片
|
||||||
|
- ch340-test.html:连接 / 收发 / HEX / DTR-RTS / 回环自测 测试台
|
||||||
|
- examples/minimal.html:最小接入示例
|
||||||
|
- index.d.ts:TypeScript 类型声明
|
||||||
|
- docs/ch34x-registers.md:寄存器与控制传输说明
|
||||||
|
- test/driver.test.mjs:16 项离线断言(mock USBDevice,无需硬件)
|
||||||
@@ -53,6 +53,18 @@ npx serve . # 或 python -m http.server 8080
|
|||||||
# 手机访问 https 地址;安卓上建议用 cloudflared/ngrok 暴露本地端口
|
# 手机访问 https 地址;安卓上建议用 cloudflared/ngrok 暴露本地端口
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 仓库结构
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| ch340-webusb.js | 核心驱动(UMD:script 标签直接引,也可 require / import) |
|
||||||
|
| ch340-serial-shim.js | navigator.serial 垫片(安卓零改动接入) |
|
||||||
|
| ch340-test.html | 测试台(连接 / 收发 / HEX / DTR-RTS / 回环自测) |
|
||||||
|
| examples/minimal.html | 最小接入示例 |
|
||||||
|
| index.d.ts | TypeScript 类型声明 |
|
||||||
|
| docs/ch34x-registers.md | 寄存器与控制传输说明 |
|
||||||
|
| test/driver.test.mjs | 离线单测(mock USBDevice,无需硬件) |
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
| 成员 | 说明 |
|
| 成员 | 说明 |
|
||||||
|
|||||||
@@ -268,6 +268,11 @@
|
|||||||
await req(0x07, 0x0302);
|
await req(0x07, 0x0302);
|
||||||
log('driver', 'CP210x 初始化完成 @' + opts.baudRate + ' bps');
|
log('driver', 'CP210x 初始化完成 @' + opts.baudRate + ' bps');
|
||||||
},
|
},
|
||||||
|
/** SET_MHS(0x07):高字节=掩码,低字节=电平;0x03 掩码表示"两条线都由本调用决定" */
|
||||||
|
setSignals: async function (ctx, sig) {
|
||||||
|
var bits = (sig.dataTerminalReady ? 0x01 : 0) | (sig.requestToSend ? 0x02 : 0);
|
||||||
|
await ctx.controlOut('vendor', 'interface', 0x07, 0x0300 | bits, ctx.layout.controlInterface);
|
||||||
|
},
|
||||||
shutdown: async function (ctx) {
|
shutdown: async function (ctx) {
|
||||||
var ci = ctx.layout.controlInterface;
|
var ci = ctx.layout.controlInterface;
|
||||||
await ctx.controlOut('vendor', 'interface', 0x12, 0x000f, ci);
|
await ctx.controlOut('vendor', 'interface', 0x12, 0x000f, ci);
|
||||||
@@ -342,6 +347,12 @@
|
|||||||
await req(0x01, 0x0202);
|
await req(0x01, 0x0202);
|
||||||
log('driver', 'FTDI 初始化完成 @' + opts.baudRate + ' bps');
|
log('driver', 'FTDI 初始化完成 @' + opts.baudRate + ' bps');
|
||||||
},
|
},
|
||||||
|
/** SET_MODEM_CTRL(0x01):高字节=掩码,低字节=电平(0x01 DTR / 0x02 RTS) */
|
||||||
|
setSignals: async function (ctx, sig) {
|
||||||
|
var idx = ctx.layout.controlInterface + 1;
|
||||||
|
var bits = (sig.dataTerminalReady ? 0x01 : 0) | (sig.requestToSend ? 0x02 : 0);
|
||||||
|
await ctx.controlOut('vendor', 'device', 0x01, 0x0300 | bits, idx);
|
||||||
|
},
|
||||||
shutdown: async function (ctx) {
|
shutdown: async function (ctx) {
|
||||||
var idx = ctx.layout.controlInterface + 1;
|
var idx = ctx.layout.controlInterface + 1;
|
||||||
await ctx.controlOut('vendor', 'device', 0x00, 0x0001, idx);
|
await ctx.controlOut('vendor', 'device', 0x00, 0x0001, idx);
|
||||||
@@ -411,6 +422,11 @@
|
|||||||
await ctx.controlOut('class', 'interface', 0x22, 0x0003, 0x0000);
|
await ctx.controlOut('class', 'interface', 0x22, 0x0003, 0x0000);
|
||||||
log('driver', 'PL2303 初始化完成 (' + type + ') @' + opts.baudRate + ' bps');
|
log('driver', 'PL2303 初始化完成 (' + type + ') @' + opts.baudRate + ' bps');
|
||||||
},
|
},
|
||||||
|
/** 标准 CDC 请求 SET_CONTROL_LINE_STATE(0x22):bit0=DTR, bit1=RTS */
|
||||||
|
setSignals: async function (ctx, sig) {
|
||||||
|
var bits = (sig.dataTerminalReady ? 0x01 : 0) | (sig.requestToSend ? 0x02 : 0);
|
||||||
|
await ctx.controlOut('class', 'interface', 0x22, bits, 0x0000);
|
||||||
|
},
|
||||||
shutdown: async function (ctx) {
|
shutdown: async function (ctx) {
|
||||||
var type = ctx.pl2303Type;
|
var type = ctx.pl2303Type;
|
||||||
if (!type) return;
|
if (!type) return;
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>ch340-webusb 最小示例</title>
|
||||||
|
<style>
|
||||||
|
body { font:14px/1.6 system-ui,sans-serif; margin:0 auto; max-width:640px; padding:16px; }
|
||||||
|
button { padding:10px 14px; font-size:14px; }
|
||||||
|
pre { background:#0b0d12; color:#d8dee9; padding:10px; border-radius:8px; height:220px; overflow:auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h3>ch340-webusb 最小示例</h3>
|
||||||
|
<p>
|
||||||
|
<button id="connect">连接 CH340</button>
|
||||||
|
<button id="close" disabled>断开</button>
|
||||||
|
<input id="text" value="hello" size="12">
|
||||||
|
<button id="send" disabled>发送</button>
|
||||||
|
</p>
|
||||||
|
<pre id="log"></pre>
|
||||||
|
|
||||||
|
<script src="../ch340-webusb.js"></script>
|
||||||
|
<script>
|
||||||
|
// 1. 选设备(必须由用户点击触发)
|
||||||
|
document.getElementById('connect').onclick = async () => {
|
||||||
|
const port = await TamaCh340.requestPort({ baudRate: 115200 });
|
||||||
|
// 2. 打开:内部完成 CH340 芯片初始化(波特率 / LCR / DTR-RTS)
|
||||||
|
await port.open({ baudRate: 115200 });
|
||||||
|
const writer = port.writable.getWriter();
|
||||||
|
const reader = port.readable.getReader();
|
||||||
|
|
||||||
|
(async () => { // 3. 收
|
||||||
|
for (;;) {
|
||||||
|
const { value, done } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
log('RX ' + Array.from(value).map(b => b.toString(16).padStart(2, '0')).join(' '));
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
document.getElementById('send').onclick = () => // 4. 发
|
||||||
|
writer.write(new TextEncoder().encode(document.getElementById('text').value + '\r\n'));
|
||||||
|
document.getElementById('send').disabled = false;
|
||||||
|
document.getElementById('connect').disabled = true;
|
||||||
|
document.getElementById('close').disabled = false;
|
||||||
|
log('已连接 ' + JSON.stringify(port.getInfo()) + ' chip=' + port.chip);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('close').onclick = async () => { location.reload(); };
|
||||||
|
function log(s) { const el = document.getElementById('log'); el.textContent += s + '\n'; el.scrollTop = el.scrollHeight; }
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Vendored
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* ch340-webusb 类型声明(UMD:<script> 全局 / ESM / CJS 均可)
|
||||||
|
*/
|
||||||
|
export = TamaCh340;
|
||||||
|
export as namespace TamaCh340;
|
||||||
|
|
||||||
|
declare const TamaCh340: TamaCh340.Api;
|
||||||
|
|
||||||
|
declare namespace TamaCh340 {
|
||||||
|
interface OpenOptions {
|
||||||
|
/** 波特率,默认 460800 */
|
||||||
|
baudRate?: number;
|
||||||
|
dataBits?: 8;
|
||||||
|
stopBits?: 1;
|
||||||
|
parity?: 'none';
|
||||||
|
flowControl?: 'none';
|
||||||
|
/** 单次 bulk 读的字节数,默认 = 端点 wMaxPacketSize(CH340 是 32) */
|
||||||
|
readTransferSize?: number;
|
||||||
|
/** 预排队读请求数,默认取驱动的 readQueueDepth(CH340 是 32) */
|
||||||
|
readQueueDepth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Signals {
|
||||||
|
dataTerminalReady?: boolean;
|
||||||
|
requestToSend?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Info {
|
||||||
|
usbVendorId: number;
|
||||||
|
usbProductId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Endpoint {
|
||||||
|
endpointNumber: number;
|
||||||
|
direction: 'in' | 'out';
|
||||||
|
type: string;
|
||||||
|
packetSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Layout {
|
||||||
|
claimedInterfaces: number[];
|
||||||
|
controlInterface: number;
|
||||||
|
input: Endpoint;
|
||||||
|
output: Endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Diagnostics {
|
||||||
|
transfers: number;
|
||||||
|
bytes: number;
|
||||||
|
transferSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 与 Web Serial 的 SerialPort 对齐,另加驱动相关扩展 */
|
||||||
|
interface BrowserSerialPort {
|
||||||
|
open(options?: OpenOptions): Promise<BrowserSerialPort>;
|
||||||
|
close(): Promise<void>;
|
||||||
|
getInfo(): Info;
|
||||||
|
/** open() 之后才有值 */
|
||||||
|
readonly readable: ReadableStream<Uint8Array> | null;
|
||||||
|
readonly writable: WritableStream<Uint8Array> | null;
|
||||||
|
/** 便捷读:timeoutMs 内无数据返回 null(会持有内部 reader) */
|
||||||
|
read(timeoutMs?: number): Promise<Uint8Array | null>;
|
||||||
|
/** 便捷写(会持有内部 writer) */
|
||||||
|
write(data: Uint8Array | ArrayBuffer | number[]): Promise<void>;
|
||||||
|
setSignals(signals: Signals): Promise<void>;
|
||||||
|
forget(): Promise<void>;
|
||||||
|
getDiagnostics(): Diagnostics;
|
||||||
|
addEventListener(type: 'disconnect', fn: (ev: { type: string; target: BrowserSerialPort }) => void): void;
|
||||||
|
removeEventListener(type: 'disconnect', fn: (ev: unknown) => void): void;
|
||||||
|
readonly device: USBDevice;
|
||||||
|
readonly chip: string;
|
||||||
|
readonly opened: boolean;
|
||||||
|
readonly layout?: Layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Driver {
|
||||||
|
name: string;
|
||||||
|
readQueueDepth?: number;
|
||||||
|
filters: USBDeviceFilter[];
|
||||||
|
supports(device: USBDevice): boolean;
|
||||||
|
layout(configuration: USBConfiguration): Layout;
|
||||||
|
initialize(ctx: unknown, options: OpenOptions & { baudRate: number }): Promise<void>;
|
||||||
|
shutdown?(ctx: unknown): Promise<void>;
|
||||||
|
setSignals?(ctx: unknown, signals: Signals): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Api {
|
||||||
|
version: string;
|
||||||
|
/** 调试日志钩子 / 打开 console 输出 */
|
||||||
|
onLog: ((...args: unknown[]) => void) | null;
|
||||||
|
debug: boolean;
|
||||||
|
drivers: Driver[];
|
||||||
|
UsbSerialPort: new (device: USBDevice, driver: Driver, defaults?: OpenOptions) => BrowserSerialPort;
|
||||||
|
|
||||||
|
hasWebUSB(): boolean;
|
||||||
|
hasWebSerial(): boolean;
|
||||||
|
isAndroid(): boolean;
|
||||||
|
/** 全部驱动的 filters(去重) */
|
||||||
|
supportedFilters(): USBDeviceFilter[];
|
||||||
|
/** 只筛 CH340/CH341 */
|
||||||
|
ch34xFilters(): USBDeviceFilter[];
|
||||||
|
chipName(vendorId: number, productId: number): string;
|
||||||
|
pickDriver(device: USBDevice): Driver | null;
|
||||||
|
/** CH34x 波特率分频,与 Linux ch341.c 一致 */
|
||||||
|
calcCh34xBaud(baudRate: number): { factor: number; divisor: number };
|
||||||
|
calcFtdiBaud(baudRate: number): { value: number; fractionalIndex: number };
|
||||||
|
|
||||||
|
/** 弹设备选择框 → 返回未打开的端口 */
|
||||||
|
requestPort(options?: OpenOptions & { filters?: USBDeviceFilter[]; allDrivers?: boolean }): Promise<BrowserSerialPort>;
|
||||||
|
/** 选择 + 打开 */
|
||||||
|
connect(options?: OpenOptions & { allDrivers?: boolean }): Promise<BrowserSerialPort>;
|
||||||
|
fromDevice(device: USBDevice, options?: OpenOptions): BrowserSerialPort;
|
||||||
|
getAuthorizedDevices(options?: { allDrivers?: boolean }): Promise<USBDevice[]>;
|
||||||
|
getAuthorizedPorts(options?: { allDrivers?: boolean }): Promise<BrowserSerialPort[]>;
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-10
@@ -1,27 +1,26 @@
|
|||||||
{
|
{
|
||||||
"name": "ch340-webusb",
|
"name": "ch340-webusb",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Browser driver for CH340/CH341 (and CDC-ACM/CP210x/FTDI/PL2303) USB-serial chips over WebUSB — works on Android Chrome, plus a navigator.serial shim.",
|
"description": "纯前端 CH340/CH341 USB 串口驱动:安卓 Chrome 走 WebUSB 零驱动,桌面走 Web Serial。Browser-only WebUSB driver for CH340/CH341 (plus CDC-ACM/CP210x/FTDI/PL2303) with a navigator.serial shim.",
|
||||||
"main": "ch340-webusb.js",
|
"main": "ch340-webusb.js",
|
||||||
|
"types": "index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"ch340-webusb.js",
|
"ch340-webusb.js",
|
||||||
"ch340-serial-shim.js",
|
"ch340-serial-shim.js",
|
||||||
"ch340-test.html",
|
"ch340-test.html",
|
||||||
|
"index.d.ts",
|
||||||
"docs",
|
"docs",
|
||||||
|
"examples",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node test/driver.test.mjs"
|
"test": "node test/driver.test.mjs"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": ["webusb", "web-serial", "ch340", "ch341", "usb-serial", "android", "serial", "otg", "cp210x", "ftdi", "pl2303"],
|
||||||
"webusb",
|
"repository": { "type": "git", "url": "git+https://gitea.truestudio.cn/jiangchen/ch340-webusb.git" },
|
||||||
"ch340",
|
"homepage": "https://gitea.truestudio.cn/jiangchen/ch340-webusb",
|
||||||
"ch341",
|
"bugs": { "url": "https://gitea.truestudio.cn/jiangchen/ch340-webusb/issues" },
|
||||||
"serial",
|
"engines": { "node": ">=18" },
|
||||||
"android",
|
|
||||||
"web-serial",
|
|
||||||
"usb-serial"
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,38 @@ await t('DTR/RTS 控制(CH340 走 0xA4 取反写入)', async () => {
|
|||||||
await port.close();
|
await port.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await t('CP210x / FTDI / PL2303 的 DTR/RTS 编码', async () => {
|
||||||
|
// CP210x: vendor/interface req 0x07, value = 0x0300 | (DTR|RTS)
|
||||||
|
const cp = new MockDevice({ vendorId: 0x10c4, productId: 0xea60 });
|
||||||
|
const cport = api.fromDevice(cp, {});
|
||||||
|
await cport.open({ baudRate: 115200 });
|
||||||
|
cp.control.length = 0;
|
||||||
|
await cport.setSignals({ dataTerminalReady: true, requestToSend: true });
|
||||||
|
eq(cp.control, [['out', 'vendor', 'interface', 0x07, 0x0303, 0, null]]);
|
||||||
|
cp.control.length = 0;
|
||||||
|
await cport.setSignals({ dataTerminalReady: false, requestToSend: false });
|
||||||
|
eq(cp.control, [['out', 'vendor', 'interface', 0x07, 0x0300, 0, null]]);
|
||||||
|
await cport.close();
|
||||||
|
|
||||||
|
// FTDI: vendor/device req 0x01, index = controlInterface+1, value = 0x0300 | (DTR|RTS)
|
||||||
|
const ft = new MockDevice({ vendorId: 0x0403, productId: 0x6001 });
|
||||||
|
const fport = api.fromDevice(ft, {});
|
||||||
|
await fport.open({ baudRate: 115200 });
|
||||||
|
ft.control.length = 0;
|
||||||
|
await fport.setSignals({ dataTerminalReady: true, requestToSend: false });
|
||||||
|
eq(ft.control, [['out', 'vendor', 'device', 0x01, 0x0301, 1, null]]);
|
||||||
|
await fport.close();
|
||||||
|
|
||||||
|
// PL2303: class/interface req 0x22, value = bit0 DTR | bit1 RTS
|
||||||
|
const pl = new MockDevice({ vendorId: 0x067b, productId: 0x2303, deviceClass: 0x02 });
|
||||||
|
const pport = api.fromDevice(pl, {});
|
||||||
|
await pport.open({ baudRate: 115200 });
|
||||||
|
pl.control.length = 0;
|
||||||
|
await pport.setSignals({ requestToSend: true, dataTerminalReady: false });
|
||||||
|
eq(pl.control, [['out', 'class', 'interface', 0x22, 0x0002, 0, null]]);
|
||||||
|
await pport.close();
|
||||||
|
});
|
||||||
|
|
||||||
await t('关闭:释放接口 + 关闭设备(CDC 会先 shutdown)', async () => {
|
await t('关闭:释放接口 + 关闭设备(CDC 会先 shutdown)', async () => {
|
||||||
const dev = new MockDevice();
|
const dev = new MockDevice();
|
||||||
const port = api.fromDevice(dev, {});
|
const port = api.fromDevice(dev, {});
|
||||||
|
|||||||
Reference in New Issue
Block a user