/* global SerialPort, ParityType, FlowControlType */ /** * Wrapper class around Webserial API to communicate with the serial device. * @param {typeof import("w3c-web-serial").SerialPort} device - Requested device prompted by the browser. * * ``` * const port = await navigator.serial.requestPort(); * ``` */ class Transport { constructor(device, tracing = false, enableSlipReader = true) { this.device = device; this.tracing = tracing; this.slipReaderEnabled = false; this.baudrate = 0; this.traceLog = ""; this.lastTraceTime = Date.now(); this.buffer = new Uint8Array(0); this.SLIP_END = 0xc0; this.SLIP_ESC = 0xdb; this.SLIP_ESC_END = 0xdc; this.SLIP_ESC_ESC = 0xdd; this._DTR_state = false; this.slipReaderEnabled = enableSlipReader; } /** * Request the serial device vendor ID and Product ID as string. * @returns {string} Return the device VendorID and ProductID from SerialPortInfo as formatted string. */ getInfo() { const info = this.device.getInfo(); return info.usbVendorId && info.usbProductId ? `WebSerial VendorID 0x${info.usbVendorId.toString(16)} ProductID 0x${info.usbProductId.toString(16)}` : ""; } /** * Request the serial device product id from SerialPortInfo. * @returns {number | undefined} Return the product ID. */ getPid() { return this.device.getInfo().usbProductId; } /** * Format received or sent data for tracing output. * @param {string} message Message to format as trace line. */ trace(message) { const delta = Date.now() - this.lastTraceTime; const prefix = `TRACE ${delta.toFixed(3)}`; const traceMessage = `${prefix} ${message}`; console.log(traceMessage); this.traceLog += traceMessage + "\n"; } async returnTrace() { try { await navigator.clipboard.writeText(this.traceLog); console.log("Text copied to clipboard!"); } catch (err) { console.error("Failed to copy text:", err); } } hexify(s) { return Array.from(s) .map((byte) => byte.toString(16).padStart(2, "0")) .join("") .padEnd(16, " "); } hexConvert(uint8Array, autoSplit = true) { if (autoSplit && uint8Array.length > 16) { let result = ""; let s = uint8Array; while (s.length > 0) { const line = s.slice(0, 16); const asciiLine = String.fromCharCode(...line) .split("") .map((c) => (c === " " || (c >= " " && c <= "~" && c !== " ") ? c : ".")) .join(""); s = s.slice(16); result += `\n ${this.hexify(line.slice(0, 8))} ${this.hexify(line.slice(8))} | ${asciiLine}`; } return result; } else { return this.hexify(uint8Array); } } /** * Format data packet using the Serial Line Internet Protocol (SLIP). * @param {Uint8Array} data Binary unsigned 8 bit array data to format. * @returns {Uint8Array} Formatted unsigned 8 bit data array. */ slipWriter(data) { const outData = []; outData.push(0xc0); for (let i = 0; i < data.length; i++) { if (data[i] === 0xdb) { outData.push(0xdb, 0xdd); } else if (data[i] === 0xc0) { outData.push(0xdb, 0xdc); } else { outData.push(data[i]); } } outData.push(0xc0); return new Uint8Array(outData); } /** * Write binary data to device using the WebSerial device writable stream. * @param {Uint8Array} data 8 bit unsigned data array to write to device. */ async write(data) { const outData = this.slipWriter(data); if (this.device.writable) { const writer = this.device.writable.getWriter(); if (this.tracing) { console.log("Write bytes"); this.trace(`Write ${outData.length} bytes: ${this.hexConvert(outData)}`); } await writer.write(outData); writer.releaseLock(); } } /** * Append a buffer array after another buffer array * @param {Uint8Array} arr1 - First array buffer. * @param {Uint8Array} arr2 - magic hex number to select ROM. * @returns {Uint8Array} Return a 8 bit unsigned array. */ appendArray(arr1, arr2) { const combined = new Uint8Array(arr1.length + arr2.length); combined.set(arr1); combined.set(arr2, arr1.length); return combined; } // Asynchronous generator to yield incoming data chunks async *readLoop(timeout) { if (!this.reader) return; try { while (true) { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Read timeout exceeded")), timeout)); // Await the race between the timeout and the reader read const result = await Promise.race([this.reader.read(), timeoutPromise]); // If a timeout occurs, result will be null; otherwise, it will have { value, done } if (result === null) break; const { value, done } = result; if (done || !value) break; yield value; // Yield each data chunk } } catch (error) { console.error("Error reading from serial port:", error); } finally { this.buffer = new Uint8Array(0); } } // Read a specific number of bytes async newRead(numBytes, timeout) { if (this.buffer.length >= numBytes) { const output = this.buffer.slice(0, numBytes); this.buffer = this.buffer.slice(numBytes); // Remove the returned data from buffer return output; } while (this.buffer.length < numBytes) { const readLoop = this.readLoop(timeout); const { value, done } = await readLoop.next(); if (done || !value) { break; } // Append the newly read data to the buffer this.buffer = this.appendArray(this.buffer, value); } // Return as much data as possible const output = this.buffer.slice(0, numBytes); this.buffer = this.buffer.slice(numBytes); return output; } async flushInput() { var _a; if (this.reader && !(await this.reader.closed)) { await this.reader.cancel(); this.reader.releaseLock(); this.reader = (_a = this.device.readable) === null || _a === void 0 ? void 0 : _a.getReader(); } } async flushOutput() { var _a, _b; this.buffer = new Uint8Array(0); await ((_a = this.device.writable) === null || _a === void 0 ? void 0 : _a.getWriter().close()); (_b = this.device.writable) === null || _b === void 0 ? void 0 : _b.getWriter().releaseLock(); } // `inWaiting` returns the count of bytes in the buffer inWaiting() { return this.buffer.length; } /** * Detect if the data read from device is a Fatal or Guru meditation error. * @param {Uint8Array} input Data read from device */ detectPanicHandler(input) { const guruMeditationRegex = /G?uru Meditation Error: (?:Core \d panic'ed \(([a-zA-Z ]*)\))?/; const fatalExceptionRegex = /F?atal exception \(\d+\): (?:([a-zA-Z ]*)?.*epc)?/; const inputString = new TextDecoder("utf-8").decode(input); const match = inputString.match(guruMeditationRegex) || inputString.match(fatalExceptionRegex); if (match) { const cause = match[1] || match[2]; const msg = `Guru Meditation Error detected${cause ? ` (${cause})` : ""}`; throw new Error(msg); } } /** * Take a data array and return the first well formed packet after * replacing the escape sequence. Reads at least 8 bytes. * @param {number} timeout Timeout read data. * @yields {Uint8Array} Formatted packet using SLIP escape sequences. */ async *read(timeout) { var _a; if (!this.reader) { this.reader = (_a = this.device.readable) === null || _a === void 0 ? void 0 : _a.getReader(); } let partialPacket = null; let isEscaping = false; let successfulSlip = false; while (true) { const waitingBytes = this.inWaiting(); const readBytes = await this.newRead(waitingBytes > 0 ? waitingBytes : 1, timeout); if (!readBytes || readBytes.length === 0) { const msg = partialPacket === null ? successfulSlip ? "Serial data stream stopped: Possible serial noise or corruption." : "No serial data received." : `Packet content transfer stopped`; this.trace(msg); throw new Error(msg); } this.trace(`Read ${readBytes.length} bytes: ${this.hexConvert(readBytes)}`); let i = 0; // Track position in readBytes while (i < readBytes.length) { const byte = readBytes[i++]; if (partialPacket === null) { if (byte === this.SLIP_END) { partialPacket = new Uint8Array(0); // Start of a new packet } else { this.trace(`Read invalid data: ${this.hexConvert(readBytes)}`); const remainingData = await this.newRead(this.inWaiting(), timeout); this.trace(`Remaining data in serial buffer: ${this.hexConvert(remainingData)}`); this.detectPanicHandler(new Uint8Array([...readBytes, ...(remainingData || [])])); throw new Error(`Invalid head of packet (0x${byte.toString(16)}): Possible serial noise or corruption.`); } } else if (isEscaping) { isEscaping = false; if (byte === this.SLIP_ESC_END) { partialPacket = this.appendArray(partialPacket, new Uint8Array([this.SLIP_END])); } else if (byte === this.SLIP_ESC_ESC) { partialPacket = this.appendArray(partialPacket, new Uint8Array([this.SLIP_ESC])); } else { this.trace(`Read invalid data: ${this.hexConvert(readBytes)}`); const remainingData = await this.newRead(this.inWaiting(), timeout); this.trace(`Remaining data in serial buffer: ${this.hexConvert(remainingData)}`); this.detectPanicHandler(new Uint8Array([...readBytes, ...(remainingData || [])])); throw new Error(`Invalid SLIP escape (0xdb, 0x${byte.toString(16)})`); } } else if (byte === this.SLIP_ESC) { isEscaping = true; } else if (byte === this.SLIP_END) { this.trace(`Received full packet: ${this.hexConvert(partialPacket)}`); this.buffer = this.appendArray(this.buffer, readBytes.slice(i)); yield partialPacket; partialPacket = null; successfulSlip = true; } else { partialPacket = this.appendArray(partialPacket, new Uint8Array([byte])); } } } } /** * Read from serial device without slip formatting. * @yields {Uint8Array} The next number in the Fibonacci sequence. */ async *rawRead() { if (!this.reader) return; try { while (true) { const { value, done } = await this.reader.read(); if (done || !value) break; if (this.tracing) { console.log("Raw Read bytes"); this.trace(`Read ${value.length} bytes: ${this.hexConvert(value)}`); } yield value; // Yield each data chunk } } catch (error) { console.error("Error reading from serial port:", error); } finally { this.buffer = new Uint8Array(0); } } /** * Send the RequestToSend (RTS) signal to given state * # True for EN=LOW, chip in reset and False EN=HIGH, chip out of reset * @param {boolean} state Boolean state to set the signal */ async setRTS(state) { await this.device.setSignals({ requestToSend: state }); // # Work-around for adapters on Windows using the usbser.sys driver: // # generate a dummy change to DTR so that the set-control-line-state // # request is sent with the updated RTS state and the same DTR state // Referenced to esptool.py await this.setDTR(this._DTR_state); } /** * Send the dataTerminalReady (DTS) signal to given state * # True for IO0=LOW, chip in reset and False IO0=HIGH * @param {boolean} state Boolean state to set the signal */ async setDTR(state) { this._DTR_state = state; await this.device.setSignals({ dataTerminalReady: state }); } /** * Connect to serial device using the Webserial open method. * @param {number} baud Number baud rate for serial connection. Default is 115200. * @param {typeof import("w3c-web-serial").SerialOptions} serialOptions Serial Options for WebUSB SerialPort class. */ async connect(baud = 115200, serialOptions = {}) { var _a; await this.device.open({ baudRate: baud, dataBits: serialOptions === null || serialOptions === void 0 ? void 0 : serialOptions.dataBits, stopBits: serialOptions === null || serialOptions === void 0 ? void 0 : serialOptions.stopBits, bufferSize: serialOptions === null || serialOptions === void 0 ? void 0 : serialOptions.bufferSize, parity: serialOptions === null || serialOptions === void 0 ? void 0 : serialOptions.parity, flowControl: serialOptions === null || serialOptions === void 0 ? void 0 : serialOptions.flowControl, }); this.baudrate = baud; this.reader = (_a = this.device.readable) === null || _a === void 0 ? void 0 : _a.getReader(); } async sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Wait for a given timeout ms for serial device unlock. * @param {number} timeout Timeout time in milliseconds (ms) to sleep */ async waitForUnlock(timeout) { while ((this.device.readable && this.device.readable.locked) || (this.device.writable && this.device.writable.locked)) { await this.sleep(timeout); } } /** * Disconnect from serial device by running SerialPort.close() after streams unlock. */ async disconnect() { var _a, _b; if ((_a = this.device.readable) === null || _a === void 0 ? void 0 : _a.locked) { await ((_b = this.reader) === null || _b === void 0 ? void 0 : _b.cancel()); } await this.waitForUnlock(400); await this.device.close(); this.reader = undefined; } } export { Transport };