webUSB api 工作但接收到的数据未正确解码
webUSB api working but the data received aren't decoded properly
更新代码
我正在使用此代码声明对串行 2-USB 设备的使用。它出现了,我可以查询有关它的信息,即。 “connected to USB2.0-Serial VID: 6790 PID: 29987”(秦恒的CH34x)。说明一下,我用的是winUSB驱动(用zadig-2下载的。5.exe,Windows 10),已经卸载了原来的驱动。
我将接收到的数据作为数据视图获取,但是当我进行解码时,它会出现乱码。我看到数组长度几乎与我知道我的 ESP8266 通过串行端口发布的内容相对应。
解码数据示例:�(#���D"D�T�b�!A#7mP�R�N����#�m93aw9 ½�d-K��b��BF+3ѡ��kag1�R�#��#!!r����g�!d��a��谛oa��399�}��1D�#��'99�����9�����'99���'99����@@譛
我得到的数据是 Uint8Arrays,但即使我尝试制作自己的解析器,最终结果也是一样的乱码。我是否需要关心 USB 的内容,如起始位、ack 位等?那里的所有示例代码只是做一个文本解码器,仅此而已。
serial.js + index.html
//https://zadig.akeo.ie/ --- for windows, get supported driver "winusb"
//https://www.cypress.com/file/134171/download --- read up on the USB standard
//https://cscott.net/usb_dev/data/devclass/usbcdc11.pdf --- more here
//http://www.linux-usb.org/usb.ids --- list of ids
//https://developer.mozilla.org/en-US/docs/Web/API/USBDevice --- more info
// host --- data pipe ---> vendor/device TX (host) transmitting data
// host <-- control pipe --> vendor/device Endpoint 0 (this is the one we us to control data up (from device to host) and data down (from host to device)
// host <-- data pipe ---- vendor/device RX (host) receiving data
//It's possible to send interrupt, bulk ant isochronus data packages.... this flasher only use bulk (and control transfers on the control pipe)
//Typically the usb cable work/look as follows:
//red 5V, black 0V/GND, green TX(host), white RX(host)
//USB standard transaction:
// [TOKEN][DATA][HANDSHAKE]
//... the data package ....
//1: Packet ID (PID): 8 bits (4 type bits + 4 error check bits) this is where we till if the transfer is "in / out / setup / SOF (start of frame)
//2: Optional device address: 7 bits (max 127 devices in the bus)
//3: Optional endpoint address: 4 bits (max 16 endpoints in a device)
//4: Optional payload data (0 ... 1023 bytes)
//5: Optional CRC (cyclic redundancy checksum)
//this flasher uses 8-N-1 for the serial parameters (8 data bits, No parity and 1 stop bit)
//when we control the "device" we control the USB chip directly, when we control the "interface" we control the serial functionality (class: 0xFF, vendor specific driver)
window.addEventListener("load", initiate, false);
//The different hardware we support + their specific data/configs
const table = {
0x0403: {"FTDI": {
0x6001: "FT232R",
0x6010: "FT2232H",
0x6011: "FT4232H",
0x6014: "FT232H",
0x6015: "FT231X", // same ID for FT230X, FT231X, FT234XD
}},
0x1a86: {"Quinheng": {
0x7523: "CH340",
0x5523: "CH341A",
}},
0x10c4: {"Silicon Labs": {
0xea60: "CP210x", // same ID for CP2101, CP2103, CP2104, CP2109
0xea70: "CP2105",
0xea71: "CP2108",
}},
0x067b: {"Prolific": {
0x2303: "PL2303"
}}
}
const config = {
"DEBUG" : true,
"DEFAULT_BAUD_RATE" : 115200,
//CH34x --> https://github.com/torvalds/linux/blob/master/drivers/usb/serial/ch341.c <-- we have used the linux driver and made into a webUSB driver
"CH340": {
"BAUD_RATES" : [600,1200,2400,4800,9600,14400,19200,38400,57600,76800,115200,230400], // highest is 300 0000 limited by the BAUD_RATE_MAX_BPS
"REQUEST_READ_VERSION" : 0x5F,
"REQUEST_READ_REGISTRY" : 0x95,
"REQUEST_WRITE_REGISTRY" : 0x9A,
"REQUEST_SERIAL_INITIATION" : 0xA1,
"REG_DIVISOR" : 0x13,
"REG_PRESCALER" : 0x12,
"REG_LCR_1" : 0x18,
"REG_LCR_2" : 0x25,
"REG_MODEM_CTRL" : 0xA4,
"REG_MODEM_VALUE_OFF" : 0xFF,
"REG_MODEM_VALUE_ON" : 0xDF,
"REG_MODEM_VALUE_CALL" : 0x9F, // what does it do??
//"BAUD_RATE_CHIP_CLOCK_FREQ" : 48000000, // which one is it? 12MHz is the official value for the 340...
"BAUD_RATE_CHIP_CLOCK_FREQ" : 12000000,
"BAUD_RATE_MAX_DIVISOR" : function (ps, fact) {
return (1 << (12 - 3 * (ps) - (fact)))
},
"BAUD_RATE_MIN" : function (ps) {
return (config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (config.CH340.BAUD_RATE_MAX_DIVISOR(ps, 1) * 512))
},
"BAUD_RATE_MIN_BPS" : function () { // 47 bps
return Math.ceil((config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ + (config.CH340.BAUD_RATE_MAX_DIVISOR(0, 0) * 256) - 1) / (config.CH340.BAUD_RATE_MAX_DIVISOR(0, 0) * 256)); //Linux DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
},
"BAUD_RATE_MAX_BPS" : function () { // 3000000 bps
return Math.floor(config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (config.CH340.BAUD_RATE_MAX_DIVISOR(3, 0) * 2));
},
"QUIRK_LIMITED_PRESCALER" : 0b0, //binary 0
"QUIRK_SIMULATE_BREAK" : 0b1, //binary 1
"QUIRKS" : 0,
"LCR_ENABLE_RX" : 0x80,
"LCR_ENABLE_TX" : 0x40,
"LCR_DATA_BITS_8N1" : 0x03,
"LCR" : function () {
return config.CH340.LCR_ENABLE_RX | config.CH340.LCR_ENABLE_TX | config.CH340.LCR_DATA_BITS_8N1;
},
"FLAG_DTR" : false,
"FLAG_RTS" : false,
"REG_MCR_DTR" : 0x20,
"REG_MCR_RTS" : 0x40,
"MCR" : function () {
return (config.CH340.FLAG_RTS ? config.CH340.REG_MCR_RTS : 0) | (config.CH340.FLAG_DTR ? config.CH340.REG_MCR_DTR : 0); // if false, set to 0, else set to value of the REG_MCR_<...>
},
}
}
const serial = {};
let device = {};
let port;
(function() {
'use strict';
serial.getPorts = function() {
return navigator.usb.getDevices().then(devices => {
return devices.map(device => new serial.Port(device));
});
};
serial.requestPort = function() {
let supportedHardware = [];
//This one create the filter of hardware based on the hardware table
Object.keys(table).map(vendorId => {
Object.keys(table[vendorId]).map(vendorName => {
Object.keys(table[vendorId][vendorName]).map(productId => {
supportedHardware.push({
"vendorId": vendorId,
"productId": productId
})
})
})});
//device contains the "device descriptor" (see USB standard), add as a new device to be able to control
return navigator.usb.requestDevice({ 'filters': supportedHardware }).then(
device => new serial.Port(device)
);
}
//set it to the active device..
serial.Port = function(device) {
this.device_ = device;
};
//here's the config + read loop is taking place....
serial.Port.prototype.connect = function() {
//this is the read loop on whatever port is currently used... it will repeat itself
let readLoop = () => {
this.device_.transferIn(this.endpointIn_, 64).then(result => {
this.onReceive(result.data);
readLoop();
}, error => {
this.onReceiveError(error);
});
};
return this.device_.open()
.then(() => {
//first we get some GUI stuff populated, we use "device" for that... serial and port are used for the configuration elsewhere
device.hostName = port.device_.productName;
device.vendorName = Object.keys(table[port.device_.vendorId])[0];
device.chip = table[port.device_.vendorId][device.vendorName][port.device_.productId];
device.serialNumber = port.device_.serialNumber;
device.manufacturerName = port.device_.manufacturerName;
//1: we set an configuration (configuration descriptor in the USB standard)
if (this.device_.configuration === null) {
return this.device_.selectConfiguration(1);
}
})
.then(() => {
//2: we set what endpoints for data we will use, we use only "bulk" transfer and thus we parse their addresses
let configInterfaces = this.device_.configuration.interfaces;
configInterfaces.forEach(element => {
element.alternates.forEach(elementalt => {
if (elementalt.interfaceClass === 0xff) {
this.interfaceNumber_ = element.interfaceNumber;
elementalt.endpoints.forEach(elementendpoint => {
//This part here get the bulk in and out endpoints programmatically
if (elementendpoint.direction === "out" && elementendpoint.type === "bulk") {
this.endpointOut_ = elementendpoint.endpointNumber;
this.endpointOutPacketSize_ = elementendpoint.packetSize;
}
if (elementendpoint.direction === "in" && elementendpoint.type === "bulk") {
this.endpointIn_ = elementendpoint.endpointNumber;
this.endpointInPacketSize_ = elementendpoint.packetSize;
}
})
}
})
})
})
//3: we claim this interface and select the alternative interface
.then(() => this.device_.claimInterface(this.interfaceNumber_))
.then(() => this.device_.selectAlternateInterface(this.interfaceNumber_, 0))
//4: we configure in and out transmissions, based on detected hardware
.then(() => serial[device.chip](this))
//5: we start the loop
.then(() => {
//console.log(this);
readLoop();
})
};
//upon disconnect, what to do
serial.Port.prototype.disconnect = async function() {
await serial[device.chip](this).DISCONNECT;
};
//send data, what to do
serial.Port.prototype.send = function(data) {
return this.device_.transferOut(this.endpointOut_, data);
};
serial.controlledTransfer = async function (object, direction, type, recipient, request, value = 0, data = new DataView(new ArrayBuffer(0)), index = object.interfaceNumber_) {
direction = direction.charAt(0).toUpperCase() + direction.slice(1);
type = type.toLowerCase();
recipient = recipient.toLowerCase();
if (data.byteLength === 0 && direction === "In") {
// we set how many bits we want back for an "in"
// so set data = 0....N in the call otherwise it will default to 0
data = 0;
}
return await object.device_["controlTransfer" + direction]({
'requestType': type,
'recipient': recipient,
'request': request,
'value': value,
'index': index
}, data)
.then(res => {
if (config.DEBUG) {
//debugger; // remove comment for extra debugging tools
console.log(res);
}
if (res.status !== "ok") {
let errorRequest = `
controlTransfer` + direction + `
'requestType': ` + type + `,
'recipient': ` + recipient + `,
'request': 0x` + request.toString(16) + `,
'value': 0x` + value.toString(16) + `,
'index': 0x` + index.toString(16) + `
}`;
console.warn("error!", errorRequest, data) // add more here
}
if (res.data !== undefined && res.data.buffer !== undefined) {
return res.data.buffer;
}
return null;
});
};
// you can really use any numerical value since JS treat them the same:
// dec = 15 // dec will be set to 15
// bin = 0b1111; // bin will be set to 15
// oct = 0o17; // oct will be set to 15
// oxx = 017; // oxx will be set to 15
// hex = 0xF; // hex will be set to 15
// note: bB oO xX are all valid
serial.hexToDataView = function (number) {
if (number === 0) {
let array = new Uint8Array([0]);
return new DataView(array.buffer)
}
let hexString = number.toString(16);
// split the string into pairs of octets
let pairs = hexString.match(/[\dA-F]{2}/gi);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
// you can give this method a string like "00 AA F2 01 23" and it will turn it into a DataView for the webUSB API transfer data
serial.hexStringArrayToDataView = function (hexString) {
// remove the leading 0x (if any)
hexString = hexString.replace(/^0x/, '');
// split the string into pairs of octets
let pairs = hexString.split(/ /);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
// these are the hardware specific initialization procedures...
serial["CH340"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
/*
direction request value
in 0xA1 0xC39C -
in 0x9A 0xF2C -
in 0xA4 0xDF - modem on? 0xFF modem off?
in 0xA4 0x9F - modem?
in 0x95 0x706 - got 2 bytes
in 0x9A 0x2727 - LCR wrong??
in 0x9A 0x1312 - baud rate seems ok
in 0x95 0x706 - got 2 bytes
in 0x9A 0x2727 -
*/
/* await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x706); //test
return;
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_SERIAL_INITIATION, 0xC39C); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0xF2C); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_ON); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_CALL); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x706); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0x2727); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0x1312); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x706); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0x2727); //test
*/
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_VERSION); // we expect to get a OK with the response of [48, 00] 0x30 0x00 or 0x27 0x00.... you can look at the response by adding a "let r = await serial.controlledTrans....." and then console log the "r"
//return; // if I uncomment this and thus not gonna continue the script I get gibbrish.... but as far as I can tell I have followed the linux initialization ?
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_SERIAL_INITIATION);
await serial["CH340"].setBaudRate(obj, baudRate);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.MCR()); // handshake
// now what? all the control transfers came back "ok"?
}
serial["CH340"].setBaudRate = async function (obj, baudRate) {
let data = serial["CH340"].getDivisor(baudRate);
// CH34x buffers data until a full endpoint size packet (32 bytes) has been reached unless bit 7 is set
data |= (1 << 7); // data |= (1 << 6); //seems correct for the 7th bit??
data = serial.hexToDataView(data);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, (config.CH340.REG_DIVISOR << 8 | config.CH340.REG_PRESCALER), data);
data = serial.hexToDataView(config.CH340.LCR());
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, (config.CH340.REG_LCR_2 << 8 | config.CH340.REG_LCR_1), data);
}
serial["CH340"].getDivisor = function (baudRate) {
let forceFactor0 = false;
// make sure our baud rate is with the min max
let baudRateChecked = Math.min(Math.max(parseInt(baudRate), config.CH340.BAUD_RATE_MIN_BPS()), config.CH340.BAUD_RATE_MAX_BPS());
// start with highest possible base clock (factor = 1) that will give a divisor strictly less than 512.
let factor = 1;
let ps = 3;
for (ps; ps >= 0; ps--) {
if (baudRateChecked > config.CH340.BAUD_RATE_MIN(ps)) {
break;
}
}
// determine corresponding divisor, rounding down
let clockDivisor = Math.floor(config.CH340.BAUD_RATE_MAX_DIVISOR(ps, factor));
let divisor = config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (clockDivisor * baudRateChecked);
// some devices require a lower base clock if ps < 3
if (ps < 3 && (config.CH340.QUIRKS & config.CH340.QUIRK_LIMITED_PRESCALER)) {
forceFactor0 = true;
}
// if we force a factor = 0 or have divisors outside range, split the base clock divisor by 2 and make factor=0
if (divisor < 9 || divisor > 255 || forceFactor0) {
divisor /= 2;
clockDivisor *= 2;
factor = 0;
}
divisor = Math.ceil(divisor);
// pick next divisor if resulting rate is closer to the requested one, scale up (16x) to avoid rounding errors on low rates.
let compare1 = 16 * config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (clockDivisor * divisor) - 16 * baudRateChecked;
let compare2 = 16 * baudRateChecked - 16 * (config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (clockDivisor * (divisor + 1)));
if (compare1 >= compare2) {
divisor++;
}
// prefer lower base clock (factor = 0) if even divisor "divisor % 2"... this makes the receiver more tolerant to errors
if (factor === 1 && (divisor % 2 === 0) ) {
divisor /= 2;
factor = 0;
}
return (0x100 - divisor) << 8 | factor << 2 | ps;
}
serial["CH340"].DISCONNECT = async function (obj) {
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_OFF);
}
serial["CP210x"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2105"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2108"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["PL2303"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT2232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT4232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT231X"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
})();
//GUI function "connect"
function connect() {
port.connect().then(() => {
document.getElementById('editor').value = "connected to: " + device.hostName + "\nvendor name: " + device.vendorName + "\nchip type: " + device.chip;
port.onReceive = data => {
console.log(data);
document.getElementById('output').value += new TextDecoder().decode(data);
}
port.onReceiveError = error => {
//console.error(error);
port.disconnect();
};
});
}
//GUI function "disconnect"
function disconnect() {
port.disconnect();
}
//GUI function "send"
function send(string) {
console.log("sending to serial:" + string.length);
if (string.length === 0)
return;
console.log("sending to serial: [" + string +"]\n");
let data = new TextEncoder('utf-8').encode(string);
console.log(data);
if (port) {
port.send(data);
}
}
//the init function which we have an event listener connected to
function initiate(){
serial.getPorts()
.then(ports => {
//these are devices already paired, let's try the first one...
if (ports.length > 0) {
port = ports[0];
connect();
}
});
document.querySelector("#connect").onclick = async function () {
await serial.requestPort().then(selectedPort => {
if (port === undefined || port.device_ !== selectedPort.device_) {
port = selectedPort;
connect();
} else {
// port already selected...
}
});
}
document.querySelector("#disconnect").onclick = function() {
disconnect()
}
document.querySelector("#submit").onclick = () => {
let source = document.querySelector("#editor").value;
send(source);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="serial.js"></script>
<title>Grovkillen test webusb</title>
</head>
<body>
<button id="connect">Connect</button>
<button id="disconnect">disConnect</button>
<label for="editor">TX</label><textarea id="editor">Flash Easy</textarea>
<button id="submit">Send</button>
<label for="output">RX</label><textarea id="output"></textarea>
</body>
POC 运行 Chrome Android 10
POC 运行 Chrome Windows 10
作为测试,我为 Android 使用了名为 Serial USB Terminal 的出色软件。而且那个应用程序可以很好地连接,因为我可以在其中设置波特率等......但是看看当我分割屏幕并关闭终端应用程序上的连接并在我的网络应用程序上打开它时发生了什么......显然,错误在于 Chrome 是如何启动连接的。
这不是 WebUSB 中的缺陷,但您的脚本是 运行。 Arduino 示例存储库中包含的 serial.js 脚本旨在与 Arduino 设备一起使用,因为端口是虚拟的,因此无需设置波特率。为了设置 USB 转串口适配器的波特率,您需要发送 SET_LINE_CODING 控制传输。这应该在现有代码中的 SET_CONTROL_LINE_STATE 命令之前。这里有控制传输结构的文档:
感谢这个库,我设法让我的 CH340 理解了 webUSB :)
Javascript 下方有效!
window.addEventListener("load", initiate, false);
//The different hardware we support + their specific data/configs
const table = {
0x0403: {"FTDI": {
0x6001: "FT232R",
0x6010: "FT2232H",
0x6011: "FT4232H",
0x6014: "FT232H",
0x6015: "FT231X", // same ID for FT230X, FT231X, FT234XD
}},
0x1a86: {"Quinheng": {
0x7523: "CH340",
0x5523: "CH341A",
}},
0x10c4: {"Silicon Labs": {
0xea60: "CP210x", // same ID for CP2101, CP2103, CP2104, CP2109
0xea70: "CP2105",
0xea71: "CP2108",
}},
0x067b: {"Prolific": {
0x2303: "PL2303"
}}
}
const config = {
"DEBUG" : true,
"DEFAULT_BAUD_RATE" : 115200,
"BAUD_RATES" : [600,1200,2400,4800,9600,14400,19200,38400,57600,115200,230400], // highest is 300 0000 limited by the BAUD_RATE_MAX_BPS
//CH34x --> https://github.com/torvalds/linux/blob/master/drivers/usb/serial/ch341.c <-- we have used the linux driver and made into a webUSB driver
// plus --> https://github.com/felHR85/UsbSerial/tree/master/usbserial/src/main/java/com/felhr/usbserial <--
"CH340": {
"REQUEST_READ_VERSION": 0x5F,
"REQUEST_READ_REGISTRY": 0x95,
"REQUEST_WRITE_REGISTRY": 0x9A,
"REQUEST_SERIAL_INITIATION": 0xA1,
"REG_SERIAL": 0xC29C,
"REG_MODEM_CTRL": 0xA4,
"REG_MODEM_VALUE_OFF": 0xFF,
"REG_MODEM_VALUE_ON": 0xDF,
"REG_MODEM_VALUE_CALL": 0x9F,
"REG_BAUD_FACTOR": 0x1312,
"REG_BAUD_OFFSET": 0x0F2C,
"REG_BAUD_LOW": 0x2518,
"REG_CONTROL_STATUS": 0x2727,
"BAUD_RATE": {
600: {"FACTOR": 0x6481, "OFFSET": 0x76},
1200: {"FACTOR": 0xB281, "OFFSET": 0x3B},
2400: {"FACTOR": 0xD981, "OFFSET": 0x1E},
4800: {"FACTOR": 0x6482, "OFFSET": 0x0F},
9600: {"FACTOR": 0xB282, "OFFSET": 0x08},
14400: {"FACTOR": 0xd980, "OFFSET": 0xEB},
19200: {"FACTOR": 0xD982, "OFFSET": 0x07},
38400: {"FACTOR": 0x6483, "OFFSET": null},
57600: {"FACTOR": 0x9883, "OFFSET": null},
115200: {"FACTOR": 0xCC83, "OFFSET": null},
230500: {"FACTOR": 0xE683, "OFFSET": null},
}
}
}
const serial = {};
let device = {};
let port;
(function() {
'use strict';
serial.getPorts = function() {
return navigator.usb.getDevices().then(devices => {
return devices.map(device => new serial.Port(device));
});
};
serial.requestPort = function() {
let supportedHardware = [];
//This one create the filter of hardware based on the hardware table
Object.keys(table).map(vendorId => {
Object.keys(table[vendorId]).map(vendorName => {
Object.keys(table[vendorId][vendorName]).map(productId => {
supportedHardware.push({
"vendorId": vendorId,
"productId": productId
})
})
})});
//device contains the "device descriptor" (see USB standard), add as a new device to be able to control
return navigator.usb.requestDevice({ 'filters': supportedHardware }).then(
device => new serial.Port(device)
);
}
//set it to the active device..
serial.Port = function(device) {
this.device_ = device;
};
//here's the config + read loop is taking place....
serial.Port.prototype.connect = function() {
//this is the read loop on whatever port is currently used... it will repeat itself
let readLoop = () => {
this.device_.transferIn(this.endpointIn_, 64).then(result => {
this.onReceive(result.data);
readLoop();
}, error => {
this.onReceiveError(error);
});
};
return this.device_.open()
.then(() => {
//first we get some GUI stuff populated, we use "device" for that... serial and port are used for the configuration elsewhere
device.hostName = port.device_.productName;
device.vendorName = Object.keys(table[port.device_.vendorId])[0];
device.chip = table[port.device_.vendorId][device.vendorName][port.device_.productId];
device.serialNumber = port.device_.serialNumber;
device.manufacturerName = port.device_.manufacturerName;
//1: we set an configuration (configuration descriptor in the USB standard)
if (this.device_.configuration === null) {
return this.device_.selectConfiguration(1);
}
})
.then(() => {
//2: we set what endpoints for data we will use, we use only "bulk" transfer and thus we parse their addresses
let configInterfaces = this.device_.configuration.interfaces;
configInterfaces.forEach(element => {
element.alternates.forEach(elementalt => {
if (elementalt.interfaceClass === 0xff) {
this.interfaceNumber_ = element.interfaceNumber;
elementalt.endpoints.forEach(elementendpoint => {
//This part here get the bulk in and out endpoints programmatically
if (elementendpoint.direction === "out" && elementendpoint.type === "bulk") {
this.endpointOut_ = elementendpoint.endpointNumber;
this.endpointOutPacketSize_ = elementendpoint.packetSize;
}
if (elementendpoint.direction === "in" && elementendpoint.type === "bulk") {
this.endpointIn_ = elementendpoint.endpointNumber;
this.endpointInPacketSize_ = elementendpoint.packetSize;
}
})
}
})
})
})
//3: we claim this interface and select the alternative interface
.then(() => this.device_.claimInterface(this.interfaceNumber_))
.then(() => this.device_.selectAlternateInterface(this.interfaceNumber_, 0))
//4: we configure in and out transmissions, based on detected hardware
.then(() => serial[device.chip](this))
//5: we start the loop
.then(() => {
//console.log(this);
readLoop();
})
};
//upon disconnect, what to do
serial.Port.prototype.disconnect = async function() {
await serial[device.chip](this).DISCONNECT;
};
//send data, what to do
serial.Port.prototype.send = function(data) {
return this.device_.transferOut(this.endpointOut_, data);
};
serial.controlledTransfer = async function (object, direction, type, recipient, request, value = 0, data = new DataView(new ArrayBuffer(0)), index = object.interfaceNumber_) {
direction = direction.charAt(0).toUpperCase() + direction.slice(1);
type = type.toLowerCase();
recipient = recipient.toLowerCase();
if (data.byteLength === 0 && direction === "In") {
// we set how many bits we want back for an "in"
// so set data = 0....N in the call otherwise it will default to 0
data = 0;
}
return await object.device_["controlTransfer" + direction]({
'requestType': type,
'recipient': recipient,
'request': request,
'value': value,
'index': index
}, data)
.then(res => {
if (config.DEBUG) {
//debugger; // remove comment for extra debugging tools
console.log(res);
}
if (res.status !== "ok") {
let errorRequest = `
controlTransfer` + direction + `
'requestType': ` + type + `,
'recipient': ` + recipient + `,
'request': 0x` + request.toString(16) + `,
'value': 0x` + value.toString(16) + `,
'index': 0x` + index.toString(16) + `
}`;
console.warn("error!", errorRequest, data) // add more here
}
if (res.data !== undefined && res.data.buffer !== undefined) {
return res.data.buffer;
}
return null;
});
};
// you can really use any numerical value since JS treat them the same:
// dec = 15 // dec will be set to 15
// bin = 0b1111; // bin will be set to 15
// oct = 0o17; // oct will be set to 15
// oxx = 017; // oxx will be set to 15
// hex = 0xF; // hex will be set to 15
// note: bB oO xX are all valid
serial.hexToDataView = function (number) {
if (number === 0) {
let array = new Uint8Array([0]);
return new DataView(array.buffer)
}
let hexString = number.toString(16);
// split the string into pairs of octets
let pairs = hexString.match(/[\dA-F]{2}/gi);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
// you can give this method a string like "00 AA F2 01 23" or "0x00 0xAA 0xF2 0x01 0x23" and it will turn it into a DataView for the webUSB API transfer data
serial.hexStringArrayToDataView = function (hexString) {
// remove the leading 0x (if any)
hexString = hexString.replace(/^0x/, '');
// split the string into pairs of octets
let pairs = hexString.split(/ /);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
serial.arrayBufferToHex = function (arrayBuffer) {
let hex = "0x0" + Array.prototype.map.call(new Uint8Array(arrayBuffer), x => ('00' + x.toString(16)).slice(-2)).join('');
return parseInt(hex);
}
// these are the hardware specific initialization procedures...
serial["CH340"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
let data = serial.hexToDataView(0); // null data
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_SERIAL_INITIATION, config.CH340.REG_SERIAL, data, 0xB2B9) // first request...
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_ON);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_CALL);
let r = await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x0706, 2);
r = serial.arrayBufferToHex(r);
if (r < 0) {
// we have an error
return;
}
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_CONTROL_STATUS, data);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_FACTOR, data, 0xB282);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_OFFSET, data, 0x0008);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_LOW, data, 0x00C3);
r = await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x0706, 2);
r = serial.arrayBufferToHex(r);
if (r < 0) {
// we have an error
return;
}
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_CONTROL_STATUS, data);
await serial["CH340"].setBaudRate(obj, baudRate);
// now what? all the control transfers came back "ok"?
}
serial["CH340"].setBaudRate = async function (obj, baudRate) {
let data = serial.hexToDataView(0);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_FACTOR, data, config.CH340.BAUD_RATE[baudRate].FACTOR);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_OFFSET, data, config.CH340.BAUD_RATE[baudRate].OFFSET);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_CONTROL_STATUS, data);
}
serial["CH340"].DISCONNECT = async function (obj) {
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_OFF);
}
serial["CP210x"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2105"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2108"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["PL2303"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT2232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT4232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT231X"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
})();
//GUI function "connect"
function connect() {
port.connect().then(() => {
document.getElementById('editor').value = "connected to: " + device.hostName + "\nvendor name: " + device.vendorName + "\nchip type: " + device.chip;
port.onReceive = data => {
console.log(data);
document.getElementById('output').value += new TextDecoder().decode(data);
}
port.onReceiveError = error => {
//console.error(error);
port.disconnect();
};
});
}
//GUI function "disconnect"
function disconnect() {
port.disconnect();
}
//GUI function "send"
function send(string) {
console.log("sending to serial:" + string.length);
if (string.length === 0)
return;
console.log("sending to serial: [" + string +"]\n");
let data = new TextEncoder('utf-8').encode(string);
console.log(data);
if (port) {
port.send(data);
}
}
//the init function which we have an event listener connected to
function initiate(){
serial.getPorts()
.then(ports => {
//these are devices already paired, let's try the first one...
if (ports.length > 0) {
port = ports[0];
connect();
}
});
document.querySelector("#connect").onclick = async function () {
await serial.requestPort().then(selectedPort => {
if (port === undefined || port.device_ !== selectedPort.device_) {
port = selectedPort;
connect();
} else {
// port already selected...
}
});
}
document.querySelector("#disconnect").onclick = function() {
disconnect()
}
document.querySelector("#submit").onclick = () => {
let source = document.querySelector("#editor").value;
send(source);
}
}
请注意我还没有实现奇偶校验位和停止位等
更新代码
我正在使用此代码声明对串行 2-USB 设备的使用。它出现了,我可以查询有关它的信息,即。 “connected to USB2.0-Serial VID: 6790 PID: 29987”(秦恒的CH34x)。说明一下,我用的是winUSB驱动(用zadig-2下载的。5.exe,Windows 10),已经卸载了原来的驱动。
我将接收到的数据作为数据视图获取,但是当我进行解码时,它会出现乱码。我看到数组长度几乎与我知道我的 ESP8266 通过串行端口发布的内容相对应。
解码数据示例:�(#���D"D�T�b�!A#7mP�R�N����#�m93aw9 ½�d-K��b��BF+3ѡ��kag1�R�#��#!!r����g�!d��a��谛oa��399�}��1D�#��'99�����9�����'99���'99����@@譛
我得到的数据是 Uint8Arrays,但即使我尝试制作自己的解析器,最终结果也是一样的乱码。我是否需要关心 USB 的内容,如起始位、ack 位等?那里的所有示例代码只是做一个文本解码器,仅此而已。
serial.js + index.html
//https://zadig.akeo.ie/ --- for windows, get supported driver "winusb"
//https://www.cypress.com/file/134171/download --- read up on the USB standard
//https://cscott.net/usb_dev/data/devclass/usbcdc11.pdf --- more here
//http://www.linux-usb.org/usb.ids --- list of ids
//https://developer.mozilla.org/en-US/docs/Web/API/USBDevice --- more info
// host --- data pipe ---> vendor/device TX (host) transmitting data
// host <-- control pipe --> vendor/device Endpoint 0 (this is the one we us to control data up (from device to host) and data down (from host to device)
// host <-- data pipe ---- vendor/device RX (host) receiving data
//It's possible to send interrupt, bulk ant isochronus data packages.... this flasher only use bulk (and control transfers on the control pipe)
//Typically the usb cable work/look as follows:
//red 5V, black 0V/GND, green TX(host), white RX(host)
//USB standard transaction:
// [TOKEN][DATA][HANDSHAKE]
//... the data package ....
//1: Packet ID (PID): 8 bits (4 type bits + 4 error check bits) this is where we till if the transfer is "in / out / setup / SOF (start of frame)
//2: Optional device address: 7 bits (max 127 devices in the bus)
//3: Optional endpoint address: 4 bits (max 16 endpoints in a device)
//4: Optional payload data (0 ... 1023 bytes)
//5: Optional CRC (cyclic redundancy checksum)
//this flasher uses 8-N-1 for the serial parameters (8 data bits, No parity and 1 stop bit)
//when we control the "device" we control the USB chip directly, when we control the "interface" we control the serial functionality (class: 0xFF, vendor specific driver)
window.addEventListener("load", initiate, false);
//The different hardware we support + their specific data/configs
const table = {
0x0403: {"FTDI": {
0x6001: "FT232R",
0x6010: "FT2232H",
0x6011: "FT4232H",
0x6014: "FT232H",
0x6015: "FT231X", // same ID for FT230X, FT231X, FT234XD
}},
0x1a86: {"Quinheng": {
0x7523: "CH340",
0x5523: "CH341A",
}},
0x10c4: {"Silicon Labs": {
0xea60: "CP210x", // same ID for CP2101, CP2103, CP2104, CP2109
0xea70: "CP2105",
0xea71: "CP2108",
}},
0x067b: {"Prolific": {
0x2303: "PL2303"
}}
}
const config = {
"DEBUG" : true,
"DEFAULT_BAUD_RATE" : 115200,
//CH34x --> https://github.com/torvalds/linux/blob/master/drivers/usb/serial/ch341.c <-- we have used the linux driver and made into a webUSB driver
"CH340": {
"BAUD_RATES" : [600,1200,2400,4800,9600,14400,19200,38400,57600,76800,115200,230400], // highest is 300 0000 limited by the BAUD_RATE_MAX_BPS
"REQUEST_READ_VERSION" : 0x5F,
"REQUEST_READ_REGISTRY" : 0x95,
"REQUEST_WRITE_REGISTRY" : 0x9A,
"REQUEST_SERIAL_INITIATION" : 0xA1,
"REG_DIVISOR" : 0x13,
"REG_PRESCALER" : 0x12,
"REG_LCR_1" : 0x18,
"REG_LCR_2" : 0x25,
"REG_MODEM_CTRL" : 0xA4,
"REG_MODEM_VALUE_OFF" : 0xFF,
"REG_MODEM_VALUE_ON" : 0xDF,
"REG_MODEM_VALUE_CALL" : 0x9F, // what does it do??
//"BAUD_RATE_CHIP_CLOCK_FREQ" : 48000000, // which one is it? 12MHz is the official value for the 340...
"BAUD_RATE_CHIP_CLOCK_FREQ" : 12000000,
"BAUD_RATE_MAX_DIVISOR" : function (ps, fact) {
return (1 << (12 - 3 * (ps) - (fact)))
},
"BAUD_RATE_MIN" : function (ps) {
return (config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (config.CH340.BAUD_RATE_MAX_DIVISOR(ps, 1) * 512))
},
"BAUD_RATE_MIN_BPS" : function () { // 47 bps
return Math.ceil((config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ + (config.CH340.BAUD_RATE_MAX_DIVISOR(0, 0) * 256) - 1) / (config.CH340.BAUD_RATE_MAX_DIVISOR(0, 0) * 256)); //Linux DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
},
"BAUD_RATE_MAX_BPS" : function () { // 3000000 bps
return Math.floor(config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (config.CH340.BAUD_RATE_MAX_DIVISOR(3, 0) * 2));
},
"QUIRK_LIMITED_PRESCALER" : 0b0, //binary 0
"QUIRK_SIMULATE_BREAK" : 0b1, //binary 1
"QUIRKS" : 0,
"LCR_ENABLE_RX" : 0x80,
"LCR_ENABLE_TX" : 0x40,
"LCR_DATA_BITS_8N1" : 0x03,
"LCR" : function () {
return config.CH340.LCR_ENABLE_RX | config.CH340.LCR_ENABLE_TX | config.CH340.LCR_DATA_BITS_8N1;
},
"FLAG_DTR" : false,
"FLAG_RTS" : false,
"REG_MCR_DTR" : 0x20,
"REG_MCR_RTS" : 0x40,
"MCR" : function () {
return (config.CH340.FLAG_RTS ? config.CH340.REG_MCR_RTS : 0) | (config.CH340.FLAG_DTR ? config.CH340.REG_MCR_DTR : 0); // if false, set to 0, else set to value of the REG_MCR_<...>
},
}
}
const serial = {};
let device = {};
let port;
(function() {
'use strict';
serial.getPorts = function() {
return navigator.usb.getDevices().then(devices => {
return devices.map(device => new serial.Port(device));
});
};
serial.requestPort = function() {
let supportedHardware = [];
//This one create the filter of hardware based on the hardware table
Object.keys(table).map(vendorId => {
Object.keys(table[vendorId]).map(vendorName => {
Object.keys(table[vendorId][vendorName]).map(productId => {
supportedHardware.push({
"vendorId": vendorId,
"productId": productId
})
})
})});
//device contains the "device descriptor" (see USB standard), add as a new device to be able to control
return navigator.usb.requestDevice({ 'filters': supportedHardware }).then(
device => new serial.Port(device)
);
}
//set it to the active device..
serial.Port = function(device) {
this.device_ = device;
};
//here's the config + read loop is taking place....
serial.Port.prototype.connect = function() {
//this is the read loop on whatever port is currently used... it will repeat itself
let readLoop = () => {
this.device_.transferIn(this.endpointIn_, 64).then(result => {
this.onReceive(result.data);
readLoop();
}, error => {
this.onReceiveError(error);
});
};
return this.device_.open()
.then(() => {
//first we get some GUI stuff populated, we use "device" for that... serial and port are used for the configuration elsewhere
device.hostName = port.device_.productName;
device.vendorName = Object.keys(table[port.device_.vendorId])[0];
device.chip = table[port.device_.vendorId][device.vendorName][port.device_.productId];
device.serialNumber = port.device_.serialNumber;
device.manufacturerName = port.device_.manufacturerName;
//1: we set an configuration (configuration descriptor in the USB standard)
if (this.device_.configuration === null) {
return this.device_.selectConfiguration(1);
}
})
.then(() => {
//2: we set what endpoints for data we will use, we use only "bulk" transfer and thus we parse their addresses
let configInterfaces = this.device_.configuration.interfaces;
configInterfaces.forEach(element => {
element.alternates.forEach(elementalt => {
if (elementalt.interfaceClass === 0xff) {
this.interfaceNumber_ = element.interfaceNumber;
elementalt.endpoints.forEach(elementendpoint => {
//This part here get the bulk in and out endpoints programmatically
if (elementendpoint.direction === "out" && elementendpoint.type === "bulk") {
this.endpointOut_ = elementendpoint.endpointNumber;
this.endpointOutPacketSize_ = elementendpoint.packetSize;
}
if (elementendpoint.direction === "in" && elementendpoint.type === "bulk") {
this.endpointIn_ = elementendpoint.endpointNumber;
this.endpointInPacketSize_ = elementendpoint.packetSize;
}
})
}
})
})
})
//3: we claim this interface and select the alternative interface
.then(() => this.device_.claimInterface(this.interfaceNumber_))
.then(() => this.device_.selectAlternateInterface(this.interfaceNumber_, 0))
//4: we configure in and out transmissions, based on detected hardware
.then(() => serial[device.chip](this))
//5: we start the loop
.then(() => {
//console.log(this);
readLoop();
})
};
//upon disconnect, what to do
serial.Port.prototype.disconnect = async function() {
await serial[device.chip](this).DISCONNECT;
};
//send data, what to do
serial.Port.prototype.send = function(data) {
return this.device_.transferOut(this.endpointOut_, data);
};
serial.controlledTransfer = async function (object, direction, type, recipient, request, value = 0, data = new DataView(new ArrayBuffer(0)), index = object.interfaceNumber_) {
direction = direction.charAt(0).toUpperCase() + direction.slice(1);
type = type.toLowerCase();
recipient = recipient.toLowerCase();
if (data.byteLength === 0 && direction === "In") {
// we set how many bits we want back for an "in"
// so set data = 0....N in the call otherwise it will default to 0
data = 0;
}
return await object.device_["controlTransfer" + direction]({
'requestType': type,
'recipient': recipient,
'request': request,
'value': value,
'index': index
}, data)
.then(res => {
if (config.DEBUG) {
//debugger; // remove comment for extra debugging tools
console.log(res);
}
if (res.status !== "ok") {
let errorRequest = `
controlTransfer` + direction + `
'requestType': ` + type + `,
'recipient': ` + recipient + `,
'request': 0x` + request.toString(16) + `,
'value': 0x` + value.toString(16) + `,
'index': 0x` + index.toString(16) + `
}`;
console.warn("error!", errorRequest, data) // add more here
}
if (res.data !== undefined && res.data.buffer !== undefined) {
return res.data.buffer;
}
return null;
});
};
// you can really use any numerical value since JS treat them the same:
// dec = 15 // dec will be set to 15
// bin = 0b1111; // bin will be set to 15
// oct = 0o17; // oct will be set to 15
// oxx = 017; // oxx will be set to 15
// hex = 0xF; // hex will be set to 15
// note: bB oO xX are all valid
serial.hexToDataView = function (number) {
if (number === 0) {
let array = new Uint8Array([0]);
return new DataView(array.buffer)
}
let hexString = number.toString(16);
// split the string into pairs of octets
let pairs = hexString.match(/[\dA-F]{2}/gi);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
// you can give this method a string like "00 AA F2 01 23" and it will turn it into a DataView for the webUSB API transfer data
serial.hexStringArrayToDataView = function (hexString) {
// remove the leading 0x (if any)
hexString = hexString.replace(/^0x/, '');
// split the string into pairs of octets
let pairs = hexString.split(/ /);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
// these are the hardware specific initialization procedures...
serial["CH340"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
/*
direction request value
in 0xA1 0xC39C -
in 0x9A 0xF2C -
in 0xA4 0xDF - modem on? 0xFF modem off?
in 0xA4 0x9F - modem?
in 0x95 0x706 - got 2 bytes
in 0x9A 0x2727 - LCR wrong??
in 0x9A 0x1312 - baud rate seems ok
in 0x95 0x706 - got 2 bytes
in 0x9A 0x2727 -
*/
/* await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x706); //test
return;
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_SERIAL_INITIATION, 0xC39C); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0xF2C); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_ON); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_CALL); // test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x706); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0x2727); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0x1312); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x706); //test
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, 0x2727); //test
*/
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_VERSION); // we expect to get a OK with the response of [48, 00] 0x30 0x00 or 0x27 0x00.... you can look at the response by adding a "let r = await serial.controlledTrans....." and then console log the "r"
//return; // if I uncomment this and thus not gonna continue the script I get gibbrish.... but as far as I can tell I have followed the linux initialization ?
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_SERIAL_INITIATION);
await serial["CH340"].setBaudRate(obj, baudRate);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.MCR()); // handshake
// now what? all the control transfers came back "ok"?
}
serial["CH340"].setBaudRate = async function (obj, baudRate) {
let data = serial["CH340"].getDivisor(baudRate);
// CH34x buffers data until a full endpoint size packet (32 bytes) has been reached unless bit 7 is set
data |= (1 << 7); // data |= (1 << 6); //seems correct for the 7th bit??
data = serial.hexToDataView(data);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, (config.CH340.REG_DIVISOR << 8 | config.CH340.REG_PRESCALER), data);
data = serial.hexToDataView(config.CH340.LCR());
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, (config.CH340.REG_LCR_2 << 8 | config.CH340.REG_LCR_1), data);
}
serial["CH340"].getDivisor = function (baudRate) {
let forceFactor0 = false;
// make sure our baud rate is with the min max
let baudRateChecked = Math.min(Math.max(parseInt(baudRate), config.CH340.BAUD_RATE_MIN_BPS()), config.CH340.BAUD_RATE_MAX_BPS());
// start with highest possible base clock (factor = 1) that will give a divisor strictly less than 512.
let factor = 1;
let ps = 3;
for (ps; ps >= 0; ps--) {
if (baudRateChecked > config.CH340.BAUD_RATE_MIN(ps)) {
break;
}
}
// determine corresponding divisor, rounding down
let clockDivisor = Math.floor(config.CH340.BAUD_RATE_MAX_DIVISOR(ps, factor));
let divisor = config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (clockDivisor * baudRateChecked);
// some devices require a lower base clock if ps < 3
if (ps < 3 && (config.CH340.QUIRKS & config.CH340.QUIRK_LIMITED_PRESCALER)) {
forceFactor0 = true;
}
// if we force a factor = 0 or have divisors outside range, split the base clock divisor by 2 and make factor=0
if (divisor < 9 || divisor > 255 || forceFactor0) {
divisor /= 2;
clockDivisor *= 2;
factor = 0;
}
divisor = Math.ceil(divisor);
// pick next divisor if resulting rate is closer to the requested one, scale up (16x) to avoid rounding errors on low rates.
let compare1 = 16 * config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (clockDivisor * divisor) - 16 * baudRateChecked;
let compare2 = 16 * baudRateChecked - 16 * (config.CH340.BAUD_RATE_CHIP_CLOCK_FREQ / (clockDivisor * (divisor + 1)));
if (compare1 >= compare2) {
divisor++;
}
// prefer lower base clock (factor = 0) if even divisor "divisor % 2"... this makes the receiver more tolerant to errors
if (factor === 1 && (divisor % 2 === 0) ) {
divisor /= 2;
factor = 0;
}
return (0x100 - divisor) << 8 | factor << 2 | ps;
}
serial["CH340"].DISCONNECT = async function (obj) {
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_OFF);
}
serial["CP210x"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2105"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2108"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["PL2303"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT2232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT4232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT231X"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
})();
//GUI function "connect"
function connect() {
port.connect().then(() => {
document.getElementById('editor').value = "connected to: " + device.hostName + "\nvendor name: " + device.vendorName + "\nchip type: " + device.chip;
port.onReceive = data => {
console.log(data);
document.getElementById('output').value += new TextDecoder().decode(data);
}
port.onReceiveError = error => {
//console.error(error);
port.disconnect();
};
});
}
//GUI function "disconnect"
function disconnect() {
port.disconnect();
}
//GUI function "send"
function send(string) {
console.log("sending to serial:" + string.length);
if (string.length === 0)
return;
console.log("sending to serial: [" + string +"]\n");
let data = new TextEncoder('utf-8').encode(string);
console.log(data);
if (port) {
port.send(data);
}
}
//the init function which we have an event listener connected to
function initiate(){
serial.getPorts()
.then(ports => {
//these are devices already paired, let's try the first one...
if (ports.length > 0) {
port = ports[0];
connect();
}
});
document.querySelector("#connect").onclick = async function () {
await serial.requestPort().then(selectedPort => {
if (port === undefined || port.device_ !== selectedPort.device_) {
port = selectedPort;
connect();
} else {
// port already selected...
}
});
}
document.querySelector("#disconnect").onclick = function() {
disconnect()
}
document.querySelector("#submit").onclick = () => {
let source = document.querySelector("#editor").value;
send(source);
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="serial.js"></script>
<title>Grovkillen test webusb</title>
</head>
<body>
<button id="connect">Connect</button>
<button id="disconnect">disConnect</button>
<label for="editor">TX</label><textarea id="editor">Flash Easy</textarea>
<button id="submit">Send</button>
<label for="output">RX</label><textarea id="output"></textarea>
</body>
POC 运行 Chrome Android 10
POC 运行 Chrome Windows 10
作为测试,我为 Android 使用了名为 Serial USB Terminal 的出色软件。而且那个应用程序可以很好地连接,因为我可以在其中设置波特率等......但是看看当我分割屏幕并关闭终端应用程序上的连接并在我的网络应用程序上打开它时发生了什么......显然,错误在于 Chrome 是如何启动连接的。
这不是 WebUSB 中的缺陷,但您的脚本是 运行。 Arduino 示例存储库中包含的 serial.js 脚本旨在与 Arduino 设备一起使用,因为端口是虚拟的,因此无需设置波特率。为了设置 USB 转串口适配器的波特率,您需要发送 SET_LINE_CODING 控制传输。这应该在现有代码中的 SET_CONTROL_LINE_STATE 命令之前。这里有控制传输结构的文档:
感谢这个库,我设法让我的 CH340 理解了 webUSB :)
Javascript 下方有效!
window.addEventListener("load", initiate, false);
//The different hardware we support + their specific data/configs
const table = {
0x0403: {"FTDI": {
0x6001: "FT232R",
0x6010: "FT2232H",
0x6011: "FT4232H",
0x6014: "FT232H",
0x6015: "FT231X", // same ID for FT230X, FT231X, FT234XD
}},
0x1a86: {"Quinheng": {
0x7523: "CH340",
0x5523: "CH341A",
}},
0x10c4: {"Silicon Labs": {
0xea60: "CP210x", // same ID for CP2101, CP2103, CP2104, CP2109
0xea70: "CP2105",
0xea71: "CP2108",
}},
0x067b: {"Prolific": {
0x2303: "PL2303"
}}
}
const config = {
"DEBUG" : true,
"DEFAULT_BAUD_RATE" : 115200,
"BAUD_RATES" : [600,1200,2400,4800,9600,14400,19200,38400,57600,115200,230400], // highest is 300 0000 limited by the BAUD_RATE_MAX_BPS
//CH34x --> https://github.com/torvalds/linux/blob/master/drivers/usb/serial/ch341.c <-- we have used the linux driver and made into a webUSB driver
// plus --> https://github.com/felHR85/UsbSerial/tree/master/usbserial/src/main/java/com/felhr/usbserial <--
"CH340": {
"REQUEST_READ_VERSION": 0x5F,
"REQUEST_READ_REGISTRY": 0x95,
"REQUEST_WRITE_REGISTRY": 0x9A,
"REQUEST_SERIAL_INITIATION": 0xA1,
"REG_SERIAL": 0xC29C,
"REG_MODEM_CTRL": 0xA4,
"REG_MODEM_VALUE_OFF": 0xFF,
"REG_MODEM_VALUE_ON": 0xDF,
"REG_MODEM_VALUE_CALL": 0x9F,
"REG_BAUD_FACTOR": 0x1312,
"REG_BAUD_OFFSET": 0x0F2C,
"REG_BAUD_LOW": 0x2518,
"REG_CONTROL_STATUS": 0x2727,
"BAUD_RATE": {
600: {"FACTOR": 0x6481, "OFFSET": 0x76},
1200: {"FACTOR": 0xB281, "OFFSET": 0x3B},
2400: {"FACTOR": 0xD981, "OFFSET": 0x1E},
4800: {"FACTOR": 0x6482, "OFFSET": 0x0F},
9600: {"FACTOR": 0xB282, "OFFSET": 0x08},
14400: {"FACTOR": 0xd980, "OFFSET": 0xEB},
19200: {"FACTOR": 0xD982, "OFFSET": 0x07},
38400: {"FACTOR": 0x6483, "OFFSET": null},
57600: {"FACTOR": 0x9883, "OFFSET": null},
115200: {"FACTOR": 0xCC83, "OFFSET": null},
230500: {"FACTOR": 0xE683, "OFFSET": null},
}
}
}
const serial = {};
let device = {};
let port;
(function() {
'use strict';
serial.getPorts = function() {
return navigator.usb.getDevices().then(devices => {
return devices.map(device => new serial.Port(device));
});
};
serial.requestPort = function() {
let supportedHardware = [];
//This one create the filter of hardware based on the hardware table
Object.keys(table).map(vendorId => {
Object.keys(table[vendorId]).map(vendorName => {
Object.keys(table[vendorId][vendorName]).map(productId => {
supportedHardware.push({
"vendorId": vendorId,
"productId": productId
})
})
})});
//device contains the "device descriptor" (see USB standard), add as a new device to be able to control
return navigator.usb.requestDevice({ 'filters': supportedHardware }).then(
device => new serial.Port(device)
);
}
//set it to the active device..
serial.Port = function(device) {
this.device_ = device;
};
//here's the config + read loop is taking place....
serial.Port.prototype.connect = function() {
//this is the read loop on whatever port is currently used... it will repeat itself
let readLoop = () => {
this.device_.transferIn(this.endpointIn_, 64).then(result => {
this.onReceive(result.data);
readLoop();
}, error => {
this.onReceiveError(error);
});
};
return this.device_.open()
.then(() => {
//first we get some GUI stuff populated, we use "device" for that... serial and port are used for the configuration elsewhere
device.hostName = port.device_.productName;
device.vendorName = Object.keys(table[port.device_.vendorId])[0];
device.chip = table[port.device_.vendorId][device.vendorName][port.device_.productId];
device.serialNumber = port.device_.serialNumber;
device.manufacturerName = port.device_.manufacturerName;
//1: we set an configuration (configuration descriptor in the USB standard)
if (this.device_.configuration === null) {
return this.device_.selectConfiguration(1);
}
})
.then(() => {
//2: we set what endpoints for data we will use, we use only "bulk" transfer and thus we parse their addresses
let configInterfaces = this.device_.configuration.interfaces;
configInterfaces.forEach(element => {
element.alternates.forEach(elementalt => {
if (elementalt.interfaceClass === 0xff) {
this.interfaceNumber_ = element.interfaceNumber;
elementalt.endpoints.forEach(elementendpoint => {
//This part here get the bulk in and out endpoints programmatically
if (elementendpoint.direction === "out" && elementendpoint.type === "bulk") {
this.endpointOut_ = elementendpoint.endpointNumber;
this.endpointOutPacketSize_ = elementendpoint.packetSize;
}
if (elementendpoint.direction === "in" && elementendpoint.type === "bulk") {
this.endpointIn_ = elementendpoint.endpointNumber;
this.endpointInPacketSize_ = elementendpoint.packetSize;
}
})
}
})
})
})
//3: we claim this interface and select the alternative interface
.then(() => this.device_.claimInterface(this.interfaceNumber_))
.then(() => this.device_.selectAlternateInterface(this.interfaceNumber_, 0))
//4: we configure in and out transmissions, based on detected hardware
.then(() => serial[device.chip](this))
//5: we start the loop
.then(() => {
//console.log(this);
readLoop();
})
};
//upon disconnect, what to do
serial.Port.prototype.disconnect = async function() {
await serial[device.chip](this).DISCONNECT;
};
//send data, what to do
serial.Port.prototype.send = function(data) {
return this.device_.transferOut(this.endpointOut_, data);
};
serial.controlledTransfer = async function (object, direction, type, recipient, request, value = 0, data = new DataView(new ArrayBuffer(0)), index = object.interfaceNumber_) {
direction = direction.charAt(0).toUpperCase() + direction.slice(1);
type = type.toLowerCase();
recipient = recipient.toLowerCase();
if (data.byteLength === 0 && direction === "In") {
// we set how many bits we want back for an "in"
// so set data = 0....N in the call otherwise it will default to 0
data = 0;
}
return await object.device_["controlTransfer" + direction]({
'requestType': type,
'recipient': recipient,
'request': request,
'value': value,
'index': index
}, data)
.then(res => {
if (config.DEBUG) {
//debugger; // remove comment for extra debugging tools
console.log(res);
}
if (res.status !== "ok") {
let errorRequest = `
controlTransfer` + direction + `
'requestType': ` + type + `,
'recipient': ` + recipient + `,
'request': 0x` + request.toString(16) + `,
'value': 0x` + value.toString(16) + `,
'index': 0x` + index.toString(16) + `
}`;
console.warn("error!", errorRequest, data) // add more here
}
if (res.data !== undefined && res.data.buffer !== undefined) {
return res.data.buffer;
}
return null;
});
};
// you can really use any numerical value since JS treat them the same:
// dec = 15 // dec will be set to 15
// bin = 0b1111; // bin will be set to 15
// oct = 0o17; // oct will be set to 15
// oxx = 017; // oxx will be set to 15
// hex = 0xF; // hex will be set to 15
// note: bB oO xX are all valid
serial.hexToDataView = function (number) {
if (number === 0) {
let array = new Uint8Array([0]);
return new DataView(array.buffer)
}
let hexString = number.toString(16);
// split the string into pairs of octets
let pairs = hexString.match(/[\dA-F]{2}/gi);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
// you can give this method a string like "00 AA F2 01 23" or "0x00 0xAA 0xF2 0x01 0x23" and it will turn it into a DataView for the webUSB API transfer data
serial.hexStringArrayToDataView = function (hexString) {
// remove the leading 0x (if any)
hexString = hexString.replace(/^0x/, '');
// split the string into pairs of octets
let pairs = hexString.split(/ /);
// convert the octets to integers
let integers = pairs.map(function(s) {
return parseInt(s, 16);
});
let array = new Uint8Array(integers);
return new DataView(array.buffer);
}
serial.arrayBufferToHex = function (arrayBuffer) {
let hex = "0x0" + Array.prototype.map.call(new Uint8Array(arrayBuffer), x => ('00' + x.toString(16)).slice(-2)).join('');
return parseInt(hex);
}
// these are the hardware specific initialization procedures...
serial["CH340"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
let data = serial.hexToDataView(0); // null data
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_SERIAL_INITIATION, config.CH340.REG_SERIAL, data, 0xB2B9) // first request...
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_ON);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_CALL);
let r = await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x0706, 2);
r = serial.arrayBufferToHex(r);
if (r < 0) {
// we have an error
return;
}
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_CONTROL_STATUS, data);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_FACTOR, data, 0xB282);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_OFFSET, data, 0x0008);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_LOW, data, 0x00C3);
r = await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REQUEST_READ_REGISTRY, 0x0706, 2);
r = serial.arrayBufferToHex(r);
if (r < 0) {
// we have an error
return;
}
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_CONTROL_STATUS, data);
await serial["CH340"].setBaudRate(obj, baudRate);
// now what? all the control transfers came back "ok"?
}
serial["CH340"].setBaudRate = async function (obj, baudRate) {
let data = serial.hexToDataView(0);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_FACTOR, data, config.CH340.BAUD_RATE[baudRate].FACTOR);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_BAUD_OFFSET, data, config.CH340.BAUD_RATE[baudRate].OFFSET);
await serial.controlledTransfer(obj,"out", "vendor", "device", config.CH340.REQUEST_WRITE_REGISTRY, config.CH340.REG_CONTROL_STATUS, data);
}
serial["CH340"].DISCONNECT = async function (obj) {
await serial.controlledTransfer(obj,"in", "vendor", "device", config.CH340.REG_MODEM_CTRL, config.CH340.REG_MODEM_VALUE_OFF);
}
serial["CP210x"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2105"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["CP2108"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["PL2303"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT2232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT4232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT232H"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
serial["FT231X"] = async function (obj, baudRate = config.DEFAULT_BAUD_RATE) {
}
})();
//GUI function "connect"
function connect() {
port.connect().then(() => {
document.getElementById('editor').value = "connected to: " + device.hostName + "\nvendor name: " + device.vendorName + "\nchip type: " + device.chip;
port.onReceive = data => {
console.log(data);
document.getElementById('output').value += new TextDecoder().decode(data);
}
port.onReceiveError = error => {
//console.error(error);
port.disconnect();
};
});
}
//GUI function "disconnect"
function disconnect() {
port.disconnect();
}
//GUI function "send"
function send(string) {
console.log("sending to serial:" + string.length);
if (string.length === 0)
return;
console.log("sending to serial: [" + string +"]\n");
let data = new TextEncoder('utf-8').encode(string);
console.log(data);
if (port) {
port.send(data);
}
}
//the init function which we have an event listener connected to
function initiate(){
serial.getPorts()
.then(ports => {
//these are devices already paired, let's try the first one...
if (ports.length > 0) {
port = ports[0];
connect();
}
});
document.querySelector("#connect").onclick = async function () {
await serial.requestPort().then(selectedPort => {
if (port === undefined || port.device_ !== selectedPort.device_) {
port = selectedPort;
connect();
} else {
// port already selected...
}
});
}
document.querySelector("#disconnect").onclick = function() {
disconnect()
}
document.querySelector("#submit").onclick = () => {
let source = document.querySelector("#editor").value;
send(source);
}
}
请注意我还没有实现奇偶校验位和停止位等