Architecture
Overview
┌──────────────────────────────────────────────────────────┐
│ Your device class │
│ (extends AbstractSerialDevice<T>) │
└───────────────────────┬──────────────────────────────────┘
│ inherits
▼
┌──────────────────────────────────────────────────────────┐
│ AbstractSerialDevice<T> │
│ ┌─────────────────┐ ┌──────────┐ ┌────────────────┐ │
│ │ SerialRegistry │ │ CmdQueue │ │ SerialEventEmit│ │
│ │ (port locks) │ │ (FIFO) │ │ ter<EventMap> │ │
│ └─────────────────┘ └──────────┘ └────────────────┘ │
│ │
│ connect() / disconnect() / send() / handshake() │
└───────────────────────┬──────────────────────────────────┘
│ delegates to
▼
┌──────────────────────────────────────────────────────────┐
│ SerialProvider │
│ (requestPort, getPorts — returns SerialPort objects) │
├──────────────────────────────────────────────────────────┤
│ ┌────────────────┐ ┌──────────────────┐ ┌───────────┐ │
│ │ Navigator │ │ WebUsbProvider │ │ WebBT/WS │ │
│ │ (native Web │ │ (USB polyfill) │ │ providers │ │
│ │ Serial API) │ │ │ │ │ │
│ └────────────────┘ └──────────────────┘ └───────────┘ │
└──────────────────────────────────────────────────────────┘Core components
AbstractSerialDevice<T>
The base class your device inherits from. It manages the full connection lifecycle:
connect()— callsprovider.requestPort(), opens the port, runshandshake(), then starts the read loop.- Read loop — continuously reads
Uint8Arraychunks from the port'sReadableStreamand feeds them through the configuredSerialParser<T>. Every time the parser emits a value, aserial:dataevent fires. send(data)— encodes strings/bytes, enqueues the write inCommandQueue, and flushes to the port'sWritableStream.disconnect()— cancels the read loop, drains the queue, closes the port, and firesserial:disconnected.- Auto-reconnect — if
autoReconnect: trueand the connection drops, the device waitsautoReconnectIntervalms and callsconnect()again.
SerialProvider
interface SerialProvider {
requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort>;
getPorts(): Promise<SerialPort[]>;
}The default provider uses navigator.serial (native Web Serial API). Inject a custom provider for WebUSB, Web Bluetooth, or WebSocket.
SerialParser<T>
interface SerialParser<T> {
transform(chunk: Uint8Array, push: (value: T) => void): void;
flush?(push: (value: T) => void): void;
}Receives raw bytes and calls push() for each complete message. Three built-in parsers are included:
| Parser | Factory | Description |
|---|---|---|
| Delimiter | delimiter(sep) | Splits on a byte sequence (e.g. "\n") |
| Fixed length | fixedLength(n) | Emits every N bytes |
| Raw | raw() | Passes each chunk unchanged as Uint8Array |
CommandQueue
A FIFO queue that serialises writes to the port. Each send() call enqueues a write. If a command does not complete within commandTimeout ms, a serial:timeout event fires and the next command proceeds.
SerialRegistry
A global singleton that tracks which ports are currently open. Prevents two AbstractSerialDevice instances from opening the same port simultaneously.
SerialEventEmitter<EventMap>
A minimal typed event emitter. on, off, and emit are all fully typed using the SerialEventMap<T> generic, so TypeScript knows the exact payload type for every event.
Data flow
SerialPort.readable (ReadableStream<Uint8Array>)
│
▼
Read loop (pump)
│ Uint8Array chunks
▼
SerialParser<T>.transform(chunk, push)
│ Parsed values of type T
▼
emit("serial:data", value)
│
▼
Your event handlerProvider injection
// Global — affects all AbstractSerialDevice instances
AbstractSerialDevice.setProvider(myProvider);
// Or pass it directly to the constructor (per-instance)
super({ ...options, provider: myProvider });