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