webusb todo

This commit is contained in:
Silent YANG 2024-04-21 11:36:21 +08:00
parent 065e1f9deb
commit 828e21f05f
4 changed files with 412 additions and 1 deletions

View File

@ -22,6 +22,7 @@
background-color: var(--color-text-3);
}
</style>
<script src="serial.js"></script>
</head>
<body>
<div id="app"></div>

387
public/serial.js Normal file
View File

@ -0,0 +1,387 @@
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" : 38400,
"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_, this.endpointInPacketSize_).then(result => {
readLoop();
if(sessionStorage.getItem('webusb')){
sessionStorage.setItem('webusb', sessionStorage.getItem('webusb') + ',' + (new Uint8Array(result.data.buffer)).toString())
}else{
sessionStorage.setItem('webusb', (new Uint8Array(result.data.buffer)).toString())
}
}, 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);
}
}

View File

@ -146,7 +146,20 @@
try{
if(appStore.connectPort)await disconnect(appStore.connectPort);
}catch{}
const _connect = await connect();
let _connect;
if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
alert('手机写频开发中,无法正常使用,请使用电脑连接电台写频!!!')
await serial.requestPort().then(selectedPort => {
_connect = selectedPort;
});
if(_connect){
await _connect.connect().then().catch((e)=>{
alert('请尝试刷新网页重新插拔写频线')
})
}
}else{
_connect = await connect();
}
if(!_connect){
alert('连接失败');

View File

@ -853,6 +853,10 @@ function unpacketize(packet) {
* @returns {Promise<Uint8Array>} - A promise that resolves with the received packet or gets rejected on timeout.
*/
async function readPacket(port, expectedData, timeout = 1000) {
if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
await new Promise(resolve => setTimeout(resolve, timeout))
return unpacketize(new Uint8Array(sessionStorage.getItem('webusb').split(',')));
}
// Create a reader to read data from the serial port
const reader = port.readable.getReader();
let buffer = new Uint8Array();
@ -957,6 +961,10 @@ async function readPacket(port, expectedData, timeout = 1000) {
* @throws {Error} - If the packet could not be sent.
*/
async function sendPacket(port, data) {
if (/Mobi|Android|iPhone/i.test(navigator.userAgent)) {
await port.send(packetize(data));
return
}
try {
// create writer for port
const writer = port.writable.getWriter();
@ -1008,6 +1016,7 @@ function hexReverseStringToUint8Array(hexReverseString) {
}
async function eeprom_init(port) {
sessionStorage.removeItem('webusb')
const packet = new Uint8Array([0x14, 0x05, 0x04, 0x00, 0xff, 0xff, 0xff, 0xff]);
await sendPacket(port, packet);
const response = await readPacket(port, 0x15);
@ -1017,6 +1026,7 @@ async function eeprom_init(port) {
}
async function eeprom_read(port, address, size = 0x80, protocol = "official") {
sessionStorage.removeItem('webusb')
if (protocol == "official") {
// packet format: uint16 ID, uint16 length, uint16 address, uint8 size, uint8 padding, uint32 timestamp
// size can be up to 0x80 bytes