Files
RS232Monitor/services/serialService.ts
2026-01-16 14:35:56 +09:00

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.
}
}