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