135 lines
4.1 KiB
TypeScript
135 lines
4.1 KiB
TypeScript
import { SerialPort, NavigatorSerial } from '../types';
|
|
|
|
export class SerialManager {
|
|
port: SerialPort | null = null;
|
|
private reader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
|
private writer: WritableStreamDefaultWriter<Uint8Array> | 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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.
|
|
}
|
|
} |