import { SerialPort, NavigatorSerial } from '../types'; export class SerialManager { port: SerialPort | null = null; private reader: ReadableStreamDefaultReader | null = null; private writer: WritableStreamDefaultWriter | null = null; private isReading = false; // Callback for receiving data private onDataCallback: ((data: Uint8Array) => void) | null = null; // Optional bridge target to automatically forward read data to private bridgeTarget: SerialManager | null = null; constructor(public id: string) {} async connect(baudRate: number): Promise { const nav = navigator as unknown as { serial: NavigatorSerial }; if (!nav.serial) { console.error("Web Serial API is not supported in this environment."); throw new Error("Web Serial API not supported in this browser."); } try { console.log(`[${this.id}] Requesting port...`); // Explicitly passing filters: [] allows the user to see all available ports this.port = await nav.serial.requestPort({ filters: [] }); console.log(`[${this.id}] Port selected. Opening with baudRate ${baudRate}...`); await this.port.open({ baudRate }); console.log(`[${this.id}] Port opened successfully.`); } catch (err) { console.error(`[${this.id}] Failed to connect:`, err); throw err; } } async disconnect(): Promise { this.isReading = false; if (this.reader) { try { await this.reader.cancel(); // The loop will catch the cancel and release the lock } catch (e) { console.warn("Error cancelling reader", e); } } if (this.writer) { try { this.writer.releaseLock(); } catch (e) { console.warn("Error releasing writer lock", e); } this.writer = null; } if (this.port) { // Wait a tick for lock release setTimeout(async () => { try { await this.port?.close(); console.log(`[${this.id}] Port closed.`); } catch(e) { console.error("Error closing port", e); } this.port = null; }, 100); } } setBridgeTarget(target: SerialManager | null) { this.bridgeTarget = target; } startReading(onData: (data: Uint8Array) => void) { if (!this.port || !this.port.readable) return; this.onDataCallback = onData; this.isReading = true; this.readLoop(); } private async readLoop() { while (this.port && this.port.readable && this.isReading) { try { this.reader = this.port.readable.getReader(); while (true) { const { value, done } = await this.reader.read(); if (done) { break; } if (value) { // 1. Notify UI if (this.onDataCallback) this.onDataCallback(value); // 2. Bypass/Bridge: If a target is set, write data immediately to it if (this.bridgeTarget) { this.bridgeTarget.send(value).catch(err => console.error("Bridge write error:", err)); } } } } catch (error) { console.error(`Read error on ${this.id}:`, error); break; } finally { if (this.reader) { this.reader.releaseLock(); this.reader = null; } } } } async send(data: Uint8Array): Promise { if (!this.port || !this.port.writable) { console.warn(`[${this.id}] Cannot send: Port not writable or disconnected.`); return; } if (!this.writer) { this.writer = this.port.writable.getWriter(); } try { await this.writer.write(data); } catch (e) { console.error("Write error:", e); // If writer is dead, release and retry next time this.writer.releaseLock(); this.writer = null; throw e; } // We intentionally keep the writer locked for performance in high-throughput // scenarios, but for a general app, you might releaseLock here if you expect // other things to grab it. For this app, SerialManager owns the writer. } }