From 9fe723af15861d681deaaa88a92739cbf5d68269 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 27 Sep 2023 23:27:25 +0200 Subject: [PATCH] created script with lib that can output to console --- web/js-ts/serialv2.js | 17 + web/js-ts/webseriallib.js | 1248 +++++++++++++++++++++++++++++++++++++ 2 files changed, 1265 insertions(+) create mode 100644 web/js-ts/serialv2.js create mode 100644 web/js-ts/webseriallib.js diff --git a/web/js-ts/serialv2.js b/web/js-ts/serialv2.js new file mode 100644 index 0000000..325bb24 --- /dev/null +++ b/web/js-ts/serialv2.js @@ -0,0 +1,17 @@ +let port; + +async function setup() { + port = createSerial(); + port.open(9600); + + + if (port.opened()) { + console.log("adsnij") + } +} + +async function readloop() { + + console.log(port.read()); + +} diff --git a/web/js-ts/webseriallib.js b/web/js-ts/webseriallib.js new file mode 100644 index 0000000..0ac380c --- /dev/null +++ b/web/js-ts/webseriallib.js @@ -0,0 +1,1248 @@ +/** + * p5.webserial + * (c) Gottfried Haider 2021-2023 + * LGPL + * https://github.com/gohai/p5.webserial + * Based on documentation: https://web.dev/serial/ + */ + +'use strict'; + +(function() { + + + // Can be called with ArrayBuffers or views on them + function memcpy(dst, dstOffset, src, srcOffset, len) { + if (!(dst instanceof ArrayBuffer)) { + dstOffset += dst.byteOffset; + dst = dst.buffer; + } + if (!(src instanceof ArrayBuffer)) { + srcOffset += src.byteOffset; + src = src.buffer; + } + const dstView = new Uint8Array(dst, dstOffset, len); + const srcView = new Uint8Array(src, srcOffset, len); + dstView.set(srcView); + } + + + let ports = []; + async function getPorts() { + try { + if ('serial' in navigator) { + ports = await navigator.serial.getPorts(); + } else if ('usb' in navigator) { + // unsure if this returns "Cannot access 'serial' before initialization" + // even with known devices present + ports = await serial.getPorts(); + } + } catch (error) { + console.warn('Unable to get previously used serial ports:', error.message); + } + }; + getPorts(); + + /** + * Get all available serial ports used previously on this page, + * which can be used without additional user interaction. + * This is useful for automatically connecting to serial devices + * on page load. Pass one of the SerialPort objects this function + * returns to WebSerial() to do so. + * @method usedSerialPorts + * @return {Array of SerialPort} + */ + p5.prototype.usedSerialPorts = function() { + return ports; + } + + + /** + * Create a and return a WebSerial instance. + */ + p5.prototype.createSerial = function() { + return new p5.prototype.WebSerial(this); + } + + + p5.prototype.WebSerial = class { + + constructor(p5inst) { + this.options = { baudRate: 9600 }; // for port.open() + this.port = null; // SerialPort object + this.reader = null; // ReadableStream object + this.keepReading = true; // set to false by close() + this.inBuf = new ArrayBuffer(1024 * 1024); // 1M + this.inLen = 0; // bytes in inBuf + this.textEncoder = new TextEncoder(); // to convert to UTF-8 + this.textDecoder = new TextDecoder(); // to convert from UTF-8 + this.p5 = null; // optional p5 instance + + if ('serial' in navigator) { + // using WebSerial API + } else if ('usb' in navigator) { + console.log('Using WebUSB polyfill for WebSerial'); + } else { + throw new Error('WebSerial is not supported in your browser (try Chrome or Edge)'); + } + + if (p5inst instanceof p5) { // this ony argument might be a p5 instance + this.p5 = p5inst; // might be used for callbacks in the future + } + } + + /** + * Returns the number of characters available for reading. + * Note: use availableBytes() to get the number of bytes instead. + * @method available + * @return {Number} number of Unicode characters + */ + available() { + const view = new Uint8Array(this.inBuf, 0, this.inLen); + + // count the number of codepoint start bytes, excluding + // incomplete trailing characters + let characters = 0; + for (let i=0; i < view.length; i++) { + const byte = view[i]; + if (byte >> 7 == 0b0) { + characters++; + } else if (byte >> 5 == 0b110 && i < view.length-1) { + characters++; + } else if (byte >> 4 == 0b1110 && i < view.length-2) { + characters++; + } else if (byte >> 3 == 0b11110 && i < view.length-3) { + characters++; + } + } + return characters; + } + + /** + * Returns the number of bytes available for reading. + * Note: use available() to get the number of characters instead, + * as a Unicode character can take more than a byte. + * @method availableBytes + * @return {Number} number of bytes + */ + availableBytes() { + return this.inLen; + } + + /** + * Change the size of the input buffer. + * By default, the input buffer is one megabyte in size. Use this + * function to request a larger buffer if needed. + * @method bufferSize + * @param {Number} size buffer size in bytes + */ + bufferSize(size) { + if (size != this.inBuf.byteLength) { + const newBuf = new ArrayBuffer(size); + const newLen = Math.min(this.inLen, size); + memcpy(newBuf, 0, this.inBuf, this.inLen-newLen, newLen); + this.inBuf = newBuf; + this.inLen = newLen; + } + } + + /** + * Empty the input buffer and remove all data stored there. + * @method clear + */ + clear() { + this.inLen = 0; + } + + /** + * Closes the serial port. + * @method close + */ + close() { + if (this.reader) { + this.keepReading = false; + this.reader.cancel(); + } else { + console.log('Serial port is already closed'); + } + } + + /** + * Returns whether or not the argument is a SerialPort (either native + * or polyfill) + * @param {object} port + * @return {Boolean} + */ + isSerialPort(port) { + const nativeSerialPort = window.SerialPort; + return (port instanceof nativeSerialPort || port instanceof SerialPort); + } + + /** + * Returns the last character received. + * This method clears the input buffer afterwards, discarding its data. + * @method last + * @return {String} last character received + */ + last() { + if (!this.inLen) { + return ''; + } + + const view = new Uint8Array(this.inBuf, 0, this.inLen); + + let startByteOffset = null; + let byteLength = null; + + for (let i=view.length-1; 0 <= i; i--) { + const byte = view[i]; + if (byte >> 7 == 0b0) { + startByteOffset = i; + byteLength = 1; + break; + } else if (byte >> 5 == 0b110 && i < view.length-1) { + startByteOffset = i; + byteLength = 2; + break; + } else if (byte >> 4 == 0b1110 && i < view.length-2) { + startByteOffset = i; + byteLength = 3; + break; + } else if (byte >> 3 == 0b11110 && i < view.length-3) { + startByteOffset = i; + byteLength = 4; + break; + } + } + + if (startByteOffset !== null) { + const out = new Uint8Array(this.inBuf, startByteOffset, byteLength); + const str = this.textDecoder.decode(out); + + // shift input buffer + if (startByteOffset+byteLength < this.inLen) { + memcpy(this.inBuf, 0, this.inBuf, startByteOffset+byteLength, this.inLen-byteLength-startByteOffset); + } + this.inLen -= startByteOffset+byteLength; + + return str; + } else { + return ''; + } + } + + /** + * Returns the last byte received as a number from 0 to 255. + * Note: For the oldest byte in the input buffer, use readByte() instead. + * This method clears the input buffer afterwards, discarding its data. + * @method lastByte + * @return {Number} value of the byte (0 to 255), or null if none available + */ + lastByte() { + if (this.inLen) { + const view = new Uint8Array(this.inBuf, this.inLen-1, 1); + this.inLen = 0; // Serial library in Processing does similar + return view[0]; + } else { + return null; + } + } + + + /** + * Opens a port based on arguments + * e.g. + * - open(); + * - open(57600); + * - open('Arduino'); + * - open(usedSerialPorts()[0]); + * - open('Arduino', 57600); + * - open(usedSerialPorts()[0], 57600); + */ + open() { + (async () => { + await this.selectPort(...arguments); // sets options and port + await this.start(); // opens the port and starts the read-loop + })(); + } + + /** + * Returns whether the serial port is open and available for + * reading and writing. + * @method opened + * @return {Boolean} true if the port is open, false if not + */ + opened() { + return (this.isSerialPort(this.port) && this.port.readable !== null); + } + + presets = { + 'Adafruit': [ // various Adafruit products + { usbVendorId: 0x1b4f }, + ], + 'Arduino': [ // from Arduino's board.txt files as of 9/13/21 + { usbVendorId: 0x03eb, usbProductId: 0x2111, usbProductId: 0x1A86 }, // Arduino M0 Pro (Atmel Corporation) + { usbVendorId: 0x03eb, usbProductId: 0x2157 }, // Arduino Zero (Atmel Corporation) + { usbVendorId: 0x10c4, usbProductId: 0xea70 }, // Arduino Tian (Silicon Laboratories) + { usbVendorId: 0x1b4f }, // Spark Fun Electronics + { usbVendorId: 0x2341 }, // Arduino SA + { usbVendorId: 0x239a }, // Adafruit + { usbVendorId: 0x2a03 }, // dog hunter AG + { usbVendorId: 0x3343, usbProductId: 0x0043 }, // DFRobot UNO R3 + ], + 'MicroPython': [ // from mu-editor as of 9/4/22 + { usbVendorId: 0x0403, usbProductId: 0x6001 }, // M5Stack & FT232/FT245 (XinaBox CW01, CW02) + { usbVendorId: 0x0403, usbProductId: 0x6010 }, // FT2232C/D/L/HL/Q (ESP-WROVER-KIT) + { usbVendorId: 0x0403, usbProductId: 0x6011 }, // FT4232 + { usbVendorId: 0x0403, usbProductId: 0x6014 }, // FT232H + { usbVendorId: 0x0403, usbProductId: 0x6015 }, // FT X-Series (Sparkfun ESP32) + { usbVendorId: 0x0403, usbProductId: 0x601c }, // FT4222H + { usbVendorId: 0x0694, usbProductId: 0x0009 }, // Lego Spike + { usbVendorId: 0x0d28, usbProductId: 0x0204 }, // BBC micro:bit + { usbVendorId: 0x10c4, usbProductId: 0xea60 }, // CP210x + { usbVendorId: 0x1a86, usbProductId: 0x7523 }, // HL-340 + { usbVendorId: 0x2e8A, usbProductId: 0x0005 }, // Raspberry Pi Pico + { usbVendorId: 0xf055, usbProductId: 0x9800 }, // Pyboard + ], + 'esp32s3': [ + { usbVendorId: 0x1b4f }, // various Raspberry Pi products + ] + }; + + /** + * Reads characters from the serial port and returns them as a string. + * The data received over serial are expected to be UTF-8 encoded. + * @method read + * @param {Number} length number of characters to read (default: all available) + * @return {String} + */ + read(length = this.inLen) { + if (!this.inLen || !length) { + return ''; + } + + const view = new Uint8Array(this.inBuf, 0, this.inLen); + + // This consumes UTF-8, ignoring invalid byte sequences at the + // beginning (we might have connected mid-sequence), and the + // end (we might still missing bytes). + + // 0xxxxxxx + // 110xxxxx 10xxxxxx + // 1110xxxx 10xxxxxx 10xxxxxx + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + + let bytesToConsume = 0; + let startByteOffset = null; + let byteLength = null; + let charLength = 0; + + for (let i=0; i < view.length; i++) { + const byte = view[i]; + //console.log('Byte', byte); + + let codepointStart; + if (byte >> 7 == 0b0) { + codepointStart = true; + bytesToConsume = 0; + //console.log('ASCII character'); + } else if (byte >> 5 == 0b110) { + codepointStart = true; + bytesToConsume = 1; + //console.log('Begin 2-byte codepoint'); + } else if (byte >> 4 == 0b1110) { + codepointStart = true; + bytesToConsume = 2; + //console.log('Begin 3-byte codepoint'); + } else if (byte >> 3 == 0b11110) { + codepointStart = true; + bytesToConsume = 3; + //console.log('Begin 4-byte codepoint'); + } else { + codepointStart = false; + bytesToConsume--; + //console.log('Continuation codepoint'); + } + + if (startByteOffset === null && codepointStart) { + startByteOffset = i; + //console.log('String starts at', i); + } + if (startByteOffset !== null && bytesToConsume <= 0) { + charLength++; + byteLength = i-startByteOffset+1; + //console.log('Added character', charLength, 'characters', byteLength, 'bytes'); + } + if (length <= charLength) { + //console.log('Enough characters'); + break; + } + } + + if (startByteOffset !== null && byteLength !== null) { + const out = new Uint8Array(this.inBuf, startByteOffset, byteLength); + const str = this.textDecoder.decode(out); + //console.log('String is', str); + + // shift input buffer + if (startByteOffset+byteLength < this.inLen) { + memcpy(this.inBuf, 0, this.inBuf, startByteOffset+byteLength, this.inLen-byteLength-startByteOffset); + } + this.inLen -= startByteOffset+byteLength; + + return str; + } else { + return ''; + } + } + + /** + * Reads characters from the serial port up to (and including) a given + * string to look for. + * The data received over serial are expected to be UTF-8 encoded. + * @method readUntil + * @param {String} needle sequence of characters to look for + * @return {String} + */ + readUntil(needle) { + let out = this.readArrayBufferUntil(needle); + + // trim leading invalid bytes, as does read() + let startByteOffset = null; + + for (let i=0; i < out.length; i++) { + const byte = out[i]; + if (byte >> 7 == 0b0) { + startByteOffset = i; + break; + } else if (byte >> 5 == 0b110) { + startByteOffset = i; + break; + } else if (byte >> 4 == 0b1110) { + startByteOffset = i; + break; + } else if (byte >> 3 == 0b11110) { + startByteOffset = i; + break; + } + } + + if (startByteOffset !== null) { + if (0 < startByteOffset) { + out = new Uint8Array(out.buffer, out.byteOffset+startByteOffset, out.length-startByteOffset); + } + return this.textDecoder.decode(out); + } else { + return ''; + } + } + + /** + * Reads bytes from the serial port and returns them as Uint8Array. + * @method readArrayBuffer + * @param {Number} length number of bytes to read (default: all available) + * @return {Uint8Array} data + */ + readArrayBuffer(length = this.inLen) { + if (this.inLen && length) { + length = Math.min(length, this.inLen); + const view = new Uint8Array(this.inBuf, 0, length); + + // this makes a copy of the underlying ArrayBuffer + const out = new Uint8Array(view); + + // shift input buffer + if (length < this.inLen) { + memcpy(this.inBuf, 0, this.inBuf, length, this.inLen-length); + } + this.inLen -= length; + + return out; + } else { + return new Uint8Array([]); + } + } + + /** + * Reads bytes from the serial port up until (and including) a given sequence + * of bytes, and returns them as Uint8Array. + * @method readArrayBufferUntil + * @param {String|Number|Array of number|Uint8Array} needle sequence of bytes to look for + * @return {Uint8Array} data + */ + readArrayBufferUntil(needle) { + // check argument + if (typeof needle === 'string') { + needle = this.textEncoder.encode(needle); + } else if (typeof needle === 'number' && Number.isInteger(needle)) { + if (needle < 0 || 255 < needle) { + throw new TypeError('readArrayBufferUntil expects as an argument an integer between 0 to 255'); + } + needle = new Uint8Array([ needle ]); + } else if (Array.isArray(needle)) { + for (let i=0; i < needle.length; i++) { + if (typeof needle[i] !== 'number' || !Number.isInteger(needle[i]) || + needle[i] < 0 || 255 < needle[i]) { + throw new TypeError('Array contained a value that wasn\'t an integer, or outside of 0 to 255'); + } + } + needle = new Uint8Array(needle); + } else if (needle instanceof Uint8Array) { + // nothing to do + } else { + throw new TypeError('Supported types are: String, Integer number (0 to 255), Array of integer numbers (0 to 255), Uint8Array'); + } + + if (!this.inLen || !needle.length) { + return new Uint8Array([]); + } + + const view = new Uint8Array(this.inBuf, 0, this.inLen); + + let needleMatchLen = 0; + + for (let i=0; i < view.length; i++) { + if (view[i] === needle[needleMatchLen]) { + needleMatchLen++; + } else { + needleMatchLen = 0; + } + + if (needleMatchLen == needle.length) { + const src = new Uint8Array(this.inBuf, 0, i+1); + + // this makes a copy of the underlying ArrayBuffer + const out = new Uint8Array(src); + + // shift input buffer + if (i+1 < this.inLen) { + memcpy(this.inBuf, 0, this.inBuf, i+1, this.inLen-i-1); + } + this.inLen -= i+1; + + return out; + } + } + + return new Uint8Array([]); + } + + /** + * Reads a byte from the serial port and returns it as a number + * from 0 to 255. + * Note: this returns the oldest byte in the input buffer. For + * the most recent one, use lastByte() instead. + * @method readByte + * @return {Number} value of the byte (0 to 255), or null if none available + */ + readByte() { + const out = this.readArrayBuffer(1); + + if (out.length) { + return out[0]; + } else { + return null; + } + } + + /** + * Reads bytes from the serial port and returns them as an + * array of numbers from 0 to 255. + * @method readBytes + * @param {Number} length number of bytes to read (default: all available) + * @return {Array of number} + */ + readBytes(length = this.inLen) { + const out = this.readArrayBuffer(length); + + const bytes = []; + for (let i=0; i < out.length; i++) { + bytes.push(out[i]); + } + return bytes; + } + + /** + * Reads bytes from the serial port up until (and including) a given sequence + * of bytes, and returns them as an array of numbers. + * @method readBytesUntil + * @param {String|Number|Array of number|Uint8Array} needle sequence of bytes to look for + * @return {Array of number} + */ + readBytesUntil(needle) { + const out = this.readArrayBufferUntil(needle); + + const bytes = []; + for (let i=0; i < out.length; i++) { + bytes.push(out[i]); + } + return bytes; + } + + /** + * Sets this.port and this.options based on arguments passed + * to the constructor. + */ + async selectPort() { + let filters = []; + + if (1 <= arguments.length) { + if (Array.isArray(arguments[0])) { // for requestPort(), verbatim + filters = arguments[0]; + } else if (this.isSerialPort(arguments[0])) { // use SerialPort as-is, skip requestPort() + this.port = arguments[0]; + filters = null; + } else if (typeof arguments[0] === 'object') { // single vid/pid-containing object + filters = [arguments[0]]; + } else if (typeof arguments[0] === 'string') { // preset + const preset = arguments[0]; + if (preset in this.presets) { + filters = this.presets[preset]; + } else { + throw new TypeError('Unrecognized preset "' + preset + '", available: ' + Object.keys(this.presets).join(', ')); + } + } else if (typeof arguments[0] === 'number') { + this.options.baudRate = arguments[0]; + } else { + throw new TypeError('Unexpected first argument "' + arguments[0] + '"'); + } + } + + if (2 <= arguments.length) { + if (typeof arguments[1] === 'object') { // for port.open(), verbatim + this.options = arguments[1]; + } else if (typeof arguments[1] === 'number') { + this.options.baudRate = arguments[1]; + } else { + throw new TypeError('Unexpected second argument "' + arguments[1] + '"'); + } + } + + try { + if (filters) { + if ('serial' in navigator) { + this.port = await navigator.serial.requestPort({ filters: filters }); + } else if ('usb' in navigator) { + this.port = await serial.requestPort({ filters: filters }); + } + } else { + // nothing to do if we got passed a SerialPort instance + } + } catch (error) { + console.warn(error.message); + this.port = null; + } + } + + /** + * Opens this.port and read from it indefinitely. + */ + async start() { + if (!this.port) { + console.error('No serial port selected.'); + return; + } + + try { + await this.port.open(this.options); + console.log('Connected to serial port'); + this.keepReading = true; + } catch (error) { + let msg = error.message; + if (msg === 'Failed to open serial port.') { + msg += ' (The port might already be open in another tab or program, e.g. the Arduino Serial Monitor.)'; + } + console.error(msg); + return; + } + + while (this.port.readable && this.keepReading) { + this.reader = this.port.readable.getReader(); + + try { + while (true) { + let { value, done } = await this.reader.read(); + + if (done) { + this.reader.releaseLock(); // allow the serial port to be closed later + break; + } + + if (value) { + // take the most recent bytes if the newly-read buffer was + // to instantly overflow the input buffer (unlikely) + if (this.inBuf.byteLength < value.length) { + value = new Uint8Array(value.buffer, value.byteOffset+value.length-this.inBuf.byteLength, this.inBuf.byteLength); + } + + // discard the oldest parts of the input buffer on overflow + if (this.inBuf.byteLength < this.inLen + value.length) { + memcpy(this.inBuf, 0, this.inBuf, this.inLen+value.length-this.inBuf.byteLength, this.inBuf.byteLength-value.length); + console.warn('Discarding the oldest ' + (this.inLen+value.length-this.inBuf.byteLength) + ' bytes of serial input data (you might want to read more frequently or increase the buffer via bufferSize())'); + this.inLen -= this.inLen+value.length-this.inBuf.byteLength; + } + + // copy to the input buffer + memcpy(this.inBuf, this.inLen, value, 0, value.length); + this.inLen += value.length; + } + } + } catch (error) { + // if a non-fatal (e.g. framing) error occurs, continue w/ new Reader + this.reader.releaseLock(); + console.warn(error.message); + } + } + + this.port.close(); + this.reader = null; + console.log('Disconnected from serial port'); + } + + /** + * Writes data to the serial port. + * Note: when passing a number or an array of numbers, those need to be integers + * and between 0 to 255. + * @method write + * @param {String|Number|Array of number|ArrayBuffer|TypedArray|DataView} out data to send + * @return {Boolean} true if the port was open, false if not + */ + async write(out) { + let buffer; + + // check argument + if (typeof out === 'string') { + buffer = this.textEncoder.encode(out); + } else if (typeof out === 'number' && Number.isInteger(out)) { + if (out < 0 || 255 < out) { + throw new TypeError('Write expects a number between 0 and 255 for sending it as a byte. To send any number as a sequence of digits instead, first convert it to a string before passing it to write().'); + } + buffer = new Uint8Array([ out ]); + } else if (Array.isArray(out)) { + for (let i=0; i < out.length; i++) { + if (typeof out[i] !== 'number' || !Number.isInteger(out[i]) || + out[i] < 0 || 255 < out[i]) { + throw new TypeError('Array contained a value that wasn\'t an integer, or outside of 0 to 255'); + } + } + buffer = new Uint8Array(out); + } else if (out instanceof ArrayBuffer || ArrayBuffer.isView(out)) { + buffer = out; + } else { + throw new TypeError('Supported types are: String, Integer number (0 to 255), Array of integer numbers (0 to 255), ArrayBuffer, TypedArray or DataView'); + } + + if (!this.port || !this.port.writable) { + console.warn('Serial port is not open, ignoring write'); + return false; + } + + const writer = this.port.writable.getWriter(); + await writer.write(buffer); + writer.releaseLock(); // allow the serial port to be closed later + return true; + } + } + + + /* + * web-serial-polyfill, version 1.0.14 + * (c) Google LLC 2019 + * This section is covered by the Apache License, 2.0 + * https://github.com/google/web-serial-polyfill/ + */ + + const kSetLineCoding = 0x20; + const kSetControlLineState = 0x22; + const kSendBreak = 0x23; + const kDefaultBufferSize = 255; + const kDefaultDataBits = 8; + const kDefaultParity = 'none'; + const kDefaultStopBits = 1; + const kAcceptableDataBits = [16, 8, 7, 6, 5]; + const kAcceptableStopBits = [1, 2]; + const kAcceptableParity = ['none', 'even', 'odd']; + const kParityIndexMapping = ['none', 'odd', 'even']; + const kStopBitsIndexMapping = [1, 1.5, 2]; + const kDefaultPolyfillOptions = { + protocol: 'UsbCdcAcm', + usbControlInterfaceClass: 2, + usbTransferInterfaceClass: 10, + }; + + /** + * Utility function to get the interface implementing a desired class. + * @param {USBDevice} device The USB device. + * @param {number} classCode The desired interface class. + * @return {USBInterface} The first interface found that implements the desired + * class. + * @throws TypeError if no interface is found. + */ + function findInterface(device, classCode) { + const configuration = device.configurations[0]; + for (const iface of configuration.interfaces) { + const alternate = iface.alternates[0]; + if (alternate.interfaceClass === classCode) { + return iface; + } + } + throw new TypeError(`Unable to find interface with class ${classCode}.`); + } + + /** + * Utility function to get an endpoint with a particular direction. + * @param {USBInterface} iface The interface to search. + * @param {USBDirection} direction The desired transfer direction. + * @return {USBEndpoint} The first endpoint with the desired transfer direction. + * @throws TypeError if no endpoint is found. + */ + function findEndpoint(iface, direction) { + const alternate = iface.alternates[0]; + for (const endpoint of alternate.endpoints) { + if (endpoint.direction == direction) { + return endpoint; + } + } + throw new TypeError(`Interface ${iface.interfaceNumber} does not have an ` + + `${direction} endpoint.`); + } + + /** + * Implementation of the underlying source API[1] which reads data from a USB + * endpoint. This can be used to construct a ReadableStream. + * + * [1]: https://streams.spec.whatwg.org/#underlying-source-api + */ + class UsbEndpointUnderlyingSource { + /** + * Constructs a new UnderlyingSource that will pull data from the specified + * endpoint on the given USB device. + * + * @param {USBDevice} device + * @param {USBEndpoint} endpoint + * @param {function} onError function to be called on error + */ + constructor(device, endpoint, onError) { + this.type = 'bytes'; + this.device_ = device; + this.endpoint_ = endpoint; + this.onError_ = onError; + } + + /** + * Reads a chunk of data from the device. + * + * @param {ReadableByteStreamController} controller + */ + pull(controller) { + (async () => { + var _a; + let chunkSize; + if (controller.desiredSize) { + const d = controller.desiredSize / this.endpoint_.packetSize; + chunkSize = Math.ceil(d) * this.endpoint_.packetSize; + } + else { + chunkSize = this.endpoint_.packetSize; + } + try { + const result = await this.device_.transferIn(this.endpoint_.endpointNumber, chunkSize); + if (result.status != 'ok') { + controller.error(`USB error: ${result.status}`); + this.onError_(); + } + if ((_a = result.data) === null || _a === void 0 ? void 0 : _a.buffer) { + const chunk = new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength); + controller.enqueue(chunk); + } + } + catch (error) { + controller.error(error.toString()); + this.onError_(); + } + })(); + } + } + + /** + * Implementation of the underlying sink API[2] which writes data to a USB + * endpoint. This can be used to construct a WritableStream. + * + * [2]: https://streams.spec.whatwg.org/#underlying-sink-api + */ + class UsbEndpointUnderlyingSink { + /** + * Constructs a new UnderlyingSink that will write data to the specified + * endpoint on the given USB device. + * + * @param {USBDevice} device + * @param {USBEndpoint} endpoint + * @param {function} onError function to be called on error + */ + constructor(device, endpoint, onError) { + this.device_ = device; + this.endpoint_ = endpoint; + this.onError_ = onError; + } + + /** + * Writes a chunk to the device. + * + * @param {Uint8Array} chunk + * @param {WritableStreamDefaultController} controller + */ + async write(chunk, controller) { + try { + const result = await this.device_.transferOut(this.endpoint_.endpointNumber, chunk); + if (result.status != 'ok') { + controller.error(result.status); + this.onError_(); + } + } + catch (error) { + controller.error(error.toString()); + this.onError_(); + } + } + } + + /** a class used to control serial devices over WebUSB */ + class SerialPort { + /** + * constructor taking a WebUSB device that creates a SerialPort instance. + * @param {USBDevice} device A device acquired from the WebUSB API + * @param {SerialPolyfillOptions} polyfillOptions Optional options to + * configure the polyfill. + */ + constructor(device, polyfillOptions) { + this.polyfillOptions_ = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions); + this.outputSignals_ = { + dataTerminalReady: false, + requestToSend: false, + break: false, + }; + this.device_ = device; + this.controlInterface_ = findInterface(this.device_, this.polyfillOptions_.usbControlInterfaceClass); + this.transferInterface_ = findInterface(this.device_, this.polyfillOptions_.usbTransferInterfaceClass); + this.inEndpoint_ = findEndpoint(this.transferInterface_, 'in'); + this.outEndpoint_ = findEndpoint(this.transferInterface_, 'out'); + } + + /** + * Getter for the readable attribute. Constructs a new ReadableStream as + * necessary. + * @return {ReadableStream} the current readable stream + */ + get readable() { + var _a; + if (!this.readable_ && this.device_.opened) { + this.readable_ = new ReadableStream(new UsbEndpointUnderlyingSource(this.device_, this.inEndpoint_, () => { + this.readable_ = null; + }), { + highWaterMark: (_a = this.serialOptions_.bufferSize) !== null && _a !== void 0 ? _a : kDefaultBufferSize, + }); + } + return this.readable_; + } + + /** + * Getter for the writable attribute. Constructs a new WritableStream as + * necessary. + * @return {WritableStream} the current writable stream + */ + get writable() { + var _a; + if (!this.writable_ && this.device_.opened) { + this.writable_ = new WritableStream(new UsbEndpointUnderlyingSink(this.device_, this.outEndpoint_, () => { + this.writable_ = null; + }), new ByteLengthQueuingStrategy({ + highWaterMark: (_a = this.serialOptions_.bufferSize) !== null && _a !== void 0 ? _a : kDefaultBufferSize, + })); + } + return this.writable_; + } + + /** + * a function that opens the device and claims all interfaces needed to + * control and communicate to and from the serial device + * @param {SerialOptions} options Object containing serial options + * @return {Promise} A promise that will resolve when device is ready + * for communication + */ + async open(options) { + this.serialOptions_ = options; + this.validateOptions(); + try { + await this.device_.open(); + if (this.device_.configuration === null) { + await this.device_.selectConfiguration(1); + } + await this.device_.claimInterface(this.controlInterface_.interfaceNumber); + if (this.controlInterface_ !== this.transferInterface_) { + await this.device_.claimInterface(this.transferInterface_.interfaceNumber); + } + await this.setLineCoding(); + await this.setSignals({ dataTerminalReady: true }); + } + catch (error) { + if (this.device_.opened) { + await this.device_.close(); + } + throw new Error('Error setting up device: ' + error.toString()); + } + } + + /** + * Closes the port. + * + * @return {Promise} A promise that will resolve when the port is + * closed. + */ + async close() { + const promises = []; + if (this.readable_) { + promises.push(this.readable_.cancel()); + } + if (this.writable_) { + promises.push(this.writable_.abort()); + } + await Promise.all(promises); + this.readable_ = null; + this.writable_ = null; + if (this.device_.opened) { + await this.setSignals({ dataTerminalReady: false, requestToSend: false }); + await this.device_.close(); + } + } + + /** + * Forgets the port. + * + * @return {Promise} A promise that will resolve when the port is + * forgotten. + */ + async forget() { + return this.device_.forget(); + } + + /** + * A function that returns properties of the device. + * @return {SerialPortInfo} Device properties. + */ + getInfo() { + return { + usbVendorId: this.device_.vendorId, + usbProductId: this.device_.productId, + }; + } + + /** + * A function used to change the serial settings of the device + * @param {object} options the object which carries serial settings data + * @return {Promise} A promise that will resolve when the options are + * set + */ + reconfigure(options) { + this.serialOptions_ = Object.assign(Object.assign({}, this.serialOptions_), options); + this.validateOptions(); + return this.setLineCoding(); + } + + /** + * Sets control signal state for the port. + * @param {SerialOutputSignals} signals The signals to enable or disable. + * @return {Promise} a promise that is resolved when the signal state + * has been changed. + */ + async setSignals(signals) { + this.outputSignals_ = Object.assign(Object.assign({}, this.outputSignals_), signals); + if (signals.dataTerminalReady !== undefined || + signals.requestToSend !== undefined) { + // The Set_Control_Line_State command expects a bitmap containing the + // values of all output signals that should be enabled or disabled. + // + // Ref: USB CDC specification version 1.1 §6.2.14. + const value = (this.outputSignals_.dataTerminalReady ? 1 << 0 : 0) | + (this.outputSignals_.requestToSend ? 1 << 1 : 0); + await this.device_.controlTransferOut({ + 'requestType': 'class', + 'recipient': 'interface', + 'request': kSetControlLineState, + 'value': value, + 'index': this.controlInterface_.interfaceNumber, + }); + } + if (signals.break !== undefined) { + // The SendBreak command expects to be given a duration for how long the + // break signal should be asserted. Passing 0xFFFF enables the signal + // until 0x0000 is send. + // + // Ref: USB CDC specification version 1.1 §6.2.15. + const value = this.outputSignals_.break ? 0xFFFF : 0x0000; + await this.device_.controlTransferOut({ + 'requestType': 'class', + 'recipient': 'interface', + 'request': kSendBreak, + 'value': value, + 'index': this.controlInterface_.interfaceNumber, + }); + } + } + + /** + * Checks the serial options for validity and throws an error if it is + * not valid + */ + validateOptions() { + if (!this.isValidBaudRate(this.serialOptions_.baudRate)) { + throw new RangeError('invalid Baud Rate ' + this.serialOptions_.baudRate); + } + if (!this.isValidDataBits(this.serialOptions_.dataBits)) { + throw new RangeError('invalid dataBits ' + this.serialOptions_.dataBits); + } + if (!this.isValidStopBits(this.serialOptions_.stopBits)) { + throw new RangeError('invalid stopBits ' + this.serialOptions_.stopBits); + } + if (!this.isValidParity(this.serialOptions_.parity)) { + throw new RangeError('invalid parity ' + this.serialOptions_.parity); + } + } + + /** + * Checks the baud rate for validity + * @param {number} baudRate the baud rate to check + * @return {boolean} A boolean that reflects whether the baud rate is valid + */ + isValidBaudRate(baudRate) { + return baudRate % 1 === 0; + } + + /** + * Checks the data bits for validity + * @param {number} dataBits the data bits to check + * @return {boolean} A boolean that reflects whether the data bits setting is + * valid + */ + isValidDataBits(dataBits) { + if (typeof dataBits === 'undefined') { + return true; + } + return kAcceptableDataBits.includes(dataBits); + } + + /** + * Checks the stop bits for validity + * @param {number} stopBits the stop bits to check + * @return {boolean} A boolean that reflects whether the stop bits setting is + * valid + */ + isValidStopBits(stopBits) { + if (typeof stopBits === 'undefined') { + return true; + } + return kAcceptableStopBits.includes(stopBits); + } + + /** + * Checks the parity for validity + * @param {string} parity the parity to check + * @return {boolean} A boolean that reflects whether the parity is valid + */ + isValidParity(parity) { + if (typeof parity === 'undefined') { + return true; + } + return kAcceptableParity.includes(parity); + } + + /** + * sends the options alog the control interface to set them on the device + * @return {Promise} a promise that will resolve when the options are set + */ + async setLineCoding() { + var _a, _b, _c; + // Ref: USB CDC specification version 1.1 §6.2.12. + const buffer = new ArrayBuffer(7); + const view = new DataView(buffer); + view.setUint32(0, this.serialOptions_.baudRate, true); + view.setUint8(4, kStopBitsIndexMapping.indexOf((_a = this.serialOptions_.stopBits) !== null && _a !== void 0 ? _a : kDefaultStopBits)); + view.setUint8(5, kParityIndexMapping.indexOf((_b = this.serialOptions_.parity) !== null && _b !== void 0 ? _b : kDefaultParity)); + view.setUint8(6, (_c = this.serialOptions_.dataBits) !== null && _c !== void 0 ? _c : kDefaultDataBits); + const result = await this.device_.controlTransferOut({ + 'requestType': 'class', + 'recipient': 'interface', + 'request': kSetLineCoding, + 'value': 0x00, + 'index': this.controlInterface_.interfaceNumber, + }, buffer); + if (result.status != 'ok') { + throw new DOMException('NetworkError', 'Failed to set line coding.'); + } + } + } + + /** implementation of the global navigator.serial object */ + class Serial { + /** + * Requests permission to access a new port. + * + * @param {SerialPortRequestOptions} options + * @param {SerialPolyfillOptions} polyfillOptions + * @return {Promise} + */ + async requestPort(options, polyfillOptions) { + polyfillOptions = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions); + const usbFilters = []; + if (options && options.filters) { + for (const filter of options.filters) { + const usbFilter = { + classCode: polyfillOptions.usbControlInterfaceClass, + }; + if (filter.usbVendorId !== undefined) { + usbFilter.vendorId = filter.usbVendorId; + } + if (filter.usbProductId !== undefined) { + usbFilter.productId = filter.usbProductId; + } + usbFilters.push(usbFilter); + } + } + if (usbFilters.length === 0) { + usbFilters.push({ + classCode: polyfillOptions.usbControlInterfaceClass, + }); + } + const device = await navigator.usb.requestDevice({ 'filters': usbFilters }); + const port = new SerialPort(device, polyfillOptions); + return port; + } + + /** + * Get the set of currently available ports. + * + * @param {SerialPolyfillOptions} polyfillOptions Polyfill configuration that + * should be applied to these ports. + * @return {Promise} a promise that is resolved with a list of + * ports. + */ + async getPorts(polyfillOptions) { + polyfillOptions = Object.assign(Object.assign({}, kDefaultPolyfillOptions), polyfillOptions); + const devices = await navigator.usb.getDevices(); + const ports = []; + devices.forEach((device) => { + try { + const port = new SerialPort(device, polyfillOptions); + ports.push(port); + } + catch (e) { + // Skip unrecognized port. + } + }); + return ports; + } + } + + let serial = new Serial(); + + +})(); \ No newline at end of file