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; _DTR_state = false; SLIP_END = 0xc0; SLIP_ESC = 0xdb; SLIP_ESC_END = 0xdc; SLIP_ESC_ESC = 0xdd; 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(`Read ${data.length} bytes: ${this.hexConvert(data)}`); } }); } getInfo() { const VID = this.serial.getVID?.(); const PID = this.serial.getPID?.(); return (VID && PID) ? `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 traceMessage = `TRACE +${delta}ms ${message}`; this.lastTraceTime = Date.now(); // console.log(traceMessage); this.traceLog += traceMessage + '\n'; } async returnTrace() { await navigator.clipboard.writeText(this.traceLog); } hexify(arr) { return Array.from(arr) .map(b => b.toString(16).padStart(2, '0')) .join(' ') .padEnd(16, ' '); } hexConvert(arr, autoSplit = true) { if (!autoSplit || arr.length <= 16) { return this.hexify(arr); } let out = ''; let s = arr; while (s.length) { const line = s.slice(0, 16); const ascii = String.fromCharCode(...line) .replace(/[^\x20-\x7E]/g, '.'); s = s.slice(16); out += `\n ${this.hexify(line.slice(0,8))} ${this.hexify(line.slice(8))} | ${ascii}`; } return out; } appendArray(a, b) { const out = new Uint8Array(a.length + b.length); out.set(a); out.set(b, a.length); return out; } inWaiting() { return this.buffer.length; } async flushInput() { this.buffer = new Uint8Array(0); } slipWriter(data) { const out = [this.SLIP_END]; for (const b of data) { if (b === this.SLIP_END) out.push(this.SLIP_ESC, this.SLIP_ESC_END); else if (b === this.SLIP_ESC) out.push(this.SLIP_ESC, this.SLIP_ESC_ESC); else out.push(b); } out.push(this.SLIP_END); return new Uint8Array(out); } async write(data) { const out = this.slipReaderEnabled ? this.slipWriter(data) : data; if (this.tracing) { this.trace(`Write ${out.length} bytes: ${this.hexConvert(out)}`); } await this.serial.sendBuffer(out); } async newRead(numBytes, timeout) { const start = Date.now(); while (this.buffer.length < numBytes) { if (!this.serial.isOpened()) return null; if (Date.now() - start > timeout) break; await this.sleep(5); } const out = this.buffer.slice(0, numBytes); this.buffer = this.buffer.slice(numBytes); return out; } async *read(timeout = 1000) { let partial = null; let escaping = false; let successfulSlip = false; while (true) { const chunk = await this.newRead(1, timeout); if (!chunk || chunk.length === 0) { const msg = partial ? 'Packet content transfer stopped' : successfulSlip ? 'Serial stream stopped' : 'No serial data received'; throw new Error(msg); } const byte = chunk[0]; if (partial === null) { if (byte === this.SLIP_END) { partial = new Uint8Array(0); } continue; } if (escaping) { escaping = false; if (byte === this.SLIP_ESC_END) partial = this.appendArray(partial, new Uint8Array([this.SLIP_END])); else if (byte === this.SLIP_ESC_ESC) partial = this.appendArray(partial, new Uint8Array([this.SLIP_ESC])); else throw new Error(`Invalid SLIP escape 0xdb 0x${byte.toString(16)}`); continue; } if (byte === this.SLIP_ESC) { escaping = true; } else if (byte === this.SLIP_END) { if (this.tracing) { this.trace(`Received packet: ${this.hexConvert(partial)}`); } successfulSlip = true; yield partial; partial = null; } else { partial = this.appendArray(partial, new Uint8Array([byte])); } } } detectPanicHandler(input) { const text = new TextDecoder().decode(input); const guru = /G?uru Meditation Error/; const fatal = /F?atal exception \(\d+\)/; if (guru.test(text) || fatal.test(text)) { throw new Error('Guru Meditation Error detected'); } } async setRTS(state) { await this.serial.setRTS(state); await this.setDTR(this._DTR_state); } async setDTR(state) { this._DTR_state = state; await this.serial.setDTR(state); } async connect(baud = 115200) { await this.serial.open(baud); this.baudrate = baud; if (this.tracing) this.trace(`Serial opened @${baud}`); } async disconnect() { await this.serial.close(); this.buffer = new Uint8Array(0); if (this.tracing) this.trace('Serial closed'); } sleep(ms) { return new Promise(r => setTimeout(r, ms)); } } Web.SerialTransport = Transport; });