goog.loadJs('web', () => { goog.require('Mixly.Web'); goog.provide('Mixly.Web.SerialTransport'); const { Web } = Mixly; class Transport { slipReaderEnabled = false; baudrate = 0; traceLog = ""; lastTraceTime = Date.now(); buffer = new Uint8Array(0); serial = null; tracing = false; constructor(serial, tracing = false, enableSlipReader = true) { this.serial = serial; this.tracing = tracing; this.slipReaderEnabled = enableSlipReader; this.serial.bind('onBuffer', (data) => { if (!(data instanceof Uint8Array)) data = new Uint8Array(data); this.buffer = this.appendArray(this.buffer, data); if (this.tracing) this.trace(`Received ${data.length} bytes: ${this.hexConvert(data)}`); }); } getInfo() { const VID = this.serial.getVID(); const PID = this.serial.getPID(); return `WebDevice VendorID 0x${VID.toString(16)} ProductID 0x${PID.toString(16)}`; } getPid() { return this.serial.getPID(); } trace(message) { const delta = Date.now() - this.lastTraceTime; const prefix = `TRACE +${delta}ms`; const traceMessage = `${prefix} ${message}`; this.lastTraceTime = Date.now(); console.log(traceMessage); this.traceLog += traceMessage + "\n"; } async returnTrace() { try { await navigator.clipboard.writeText(this.traceLog); console.log("Trace log copied to clipboard!"); } catch (err) { console.error("Failed to copy trace log:", 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); } } appendArray(arr1, arr2) { const combined = new Uint8Array(arr1.length + arr2.length); combined.set(arr1); combined.set(arr2, arr1.length); return combined; } async sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async connect(baud = 115200) { await this.serial.open(baud); this.baudrate = baud; if (this.tracing) this.trace(`Serial opened at ${baud} baud`); } async disconnect() { await this.serial.close(); if (this.tracing) this.trace("Serial connection closed"); } async setRTS(state) { await this.serial.setRTS(state); await this.serial.setDTR(this.serial.getDTR()); if (this.tracing) { this.trace(`Set RTS = ${state}`); this.trace(`Set DTR = ${this.serial.getDTR()}`); } } async setDTR(state) { await this.serial.setDTR(state); if (this.tracing) this.trace(`Set DTR = ${state}`); } inWaiting() { return this.buffer.length; } async flushInput() { this.buffer = new Uint8Array(0); } 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); } async write(data) { const outData = this.slipWriter(data); if (this.tracing) { this.trace(`Write ${outData.length} bytes: ${this.hexConvert(outData)}`); } await this.serial.sendBuffer(outData); } async newRead(numBytes, timeout) { const start = Date.now(); while (this.buffer.length < numBytes && Date.now() - start < timeout) { if (!this.serial.isOpened()) { return null; } await this.sleep(10); } const out = this.buffer.slice(0, numBytes); this.buffer = this.buffer.slice(numBytes); return out; } async *read(timeout = 1000) { let partialPacket = null; let isEscaping = false; const SLIP_END = 0xc0; const SLIP_ESC = 0xdb; const SLIP_ESC_END = 0xdc; const SLIP_ESC_ESC = 0xdd; while (true) { const data = await this.newRead(1, timeout); if (!data || data.length === 0) break; const byte = data[0]; if (partialPacket === null) { if (byte === SLIP_END) partialPacket = new Uint8Array(0); } else if (isEscaping) { isEscaping = false; if (byte === SLIP_ESC_END) partialPacket = this.appendArray(partialPacket, new Uint8Array([SLIP_END])); else if (byte === SLIP_ESC_ESC) partialPacket = this.appendArray(partialPacket, new Uint8Array([SLIP_ESC])); else throw new Error(`Invalid SLIP escape: 0x${byte.toString(16)}`); } else if (byte === SLIP_ESC) { isEscaping = true; } else if (byte === SLIP_END) { if (partialPacket.length > 0) { if (this.tracing) this.trace(`Yield packet: ${this.hexConvert(partialPacket)}`); yield partialPacket; partialPacket = null; } } else { partialPacket = this.appendArray(partialPacket, new Uint8Array([byte])); } } } 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); } } } Web.SerialTransport = Transport; });