@danidoble/webserial-locker
v1.0.0
Published
A strongly-typed, event-driven USB locker driver for the Web Serial API, built on top of webserial-core.
Readme
@danidoble/webserial-locker
A strongly-typed, event-driven USB locker driver built on top of webserial-core.
Handles the serial connection, binary handshake, auto-reconnect, and message routing — so you only deal with clean, typed events.
Not tied to a single transport: swap in the WebUSB, Web Bluetooth, or WebSocket provider from webserial-core, or implement your own SerialProvider for any platform.
Requirements
webserial-core^2.1.0(peer dependency)- A compatible transport (see Providers):
- Web Serial API — Chrome / Edge 89+ (default, no extra setup)
- WebUSB — Chrome / Edge (via
WebUsbProvider) - Web Bluetooth — Chrome / Edge (via
createBluetoothProvider, Nordic UART Service) - WebSocket — any environment (via
createWebSocketProvider+ a bridge server) - Custom — any platform via your own
SerialProviderimplementation
Installation
# npm
npm install @danidoble/webserial-locker webserial-core
# pnpm
pnpm add @danidoble/webserial-locker webserial-core
# yarn
yarn add @danidoble/webserial-locker webserial-core
# bun
bun add @danidoble/webserial-locker webserial-core
webserial-coreis a peer dependency — it must be installed alongside this package.
Quick start
import { Locker } from '@danidoble/webserial-locker';
const locker = new Locker({ channel: 1, filters: [{ usbVendorId: 0x2341 }] });
locker.on('serial:connecting', () => console.log('Opening port…'));
locker.on('serial:connected', () => console.log('Port open'));
locker.on('serial:disconnected', () => console.log('Disconnected'));
locker.on('locker:connected', ({ channel }) => console.log(`Locker on channel ${channel} ready`));
locker.on('locker:dispensed', ({ cell_status }) => console.log('Cell opened, status:', cell_status));
locker.on('locker:not-dispensed', ({ cell_status }) => console.warn('Cell not opened, status:', cell_status));
locker.on('locker:message', (msg) => console.log(`[${msg.no_code}] ${msg.name}`));
// Opens a port picker dialog (requires a user gesture)
await locker.connect();
// Open cell 5
const result = await locker.sendDispense({ cell: 5 });
console.log(result.dispensed); // true | false
// Check status of a cell
await locker.sendStatus({ cell: 5 });
// Enable / disable individual cells
await locker.sendEnable({ cell: 3 });
await locker.sendDisable({ cell: 3 });
// Light scan (columns 0–5)
await locker.sendLightScan({ since: 0, until: 5 });
// Bulk operations
await locker.sendOpenAll(); // opens cells 1–80 sequentially
await locker.sendEnableAll(); // enables cells 1–80 sequentially
await locker.sendDisableAll(); // disables cells 1–80 sequentiallySerial settings
The constructor pre-configures the following defaults — no extra setup needed:
| Setting | Value | | ------------------ | -------------------------- | | Baud rate | 9600 | | Data bits | 8 | | Stop bits | 1 | | Parity | none | | Flow control | none | | Buffer size | 255 B | | Parser | interByteTimeout (40 ms) | | Command timeout | 1 000 ms | | Auto-reconnect | ✓ | | Reconnect interval | 1 500 ms | | Handshake timeout | 2 000 ms |
Providers
By default the library uses the browser's native Web Serial API (navigator.serial). You can replace this with any of the built-in providers from webserial-core, or write your own.
Web Serial API (default)
No setup required — works out of the box in Chrome / Edge 89+.
import { Locker } from '@danidoble/webserial-locker';
const locker = new Locker({ filters: [{ usbVendorId: 0x2341 }] });
await locker.connect();WebUSB (WebUsbProvider)
Use the WebUSB API as the transport. Useful for devices or platforms where the native Web Serial API is unavailable, or when targeting CP210x / vendor-specific USB chips.
import { Locker, WebUsbProvider } from '@danidoble/webserial-locker';
const locker = new Locker({
filters: [{ usbVendorId: 0x2341 }],
provider: new WebUsbProvider()
});
await locker.connect();Web Bluetooth (createBluetoothProvider)
Communicate over Bluetooth Low Energy using the Nordic UART Service (NUS). The device must expose NUS characteristics.
import { Locker, createBluetoothProvider } from '@danidoble/webserial-locker';
const locker = new Locker({
provider: createBluetoothProvider()
});
await locker.connect(); // shows the browser Bluetooth pickerWebSocket (createWebSocketProvider)
Route serial communication through a WebSocket bridge server — ideal for Node.js environments or remote devices. A reference bridge implementation is available in the webserial-core demos.
import { Locker, createWebSocketProvider } from '@danidoble/webserial-locker';
const locker = new Locker({
filters: [{ usbVendorId: 0x2341 }],
provider: createWebSocketProvider('ws://localhost:8080')
});
await locker.connect();Global provider (AbstractSerialDevice.setProvider)
Set a provider once for all device instances instead of per-instance. Import AbstractSerialDevice directly from webserial-core:
import { AbstractSerialDevice, WebUsbProvider } from 'webserial-core';
import { Locker } from '@danidoble/webserial-locker';
AbstractSerialDevice.setProvider(new WebUsbProvider());
const locker = new Locker({ filters: [{ usbVendorId: 0x2341 }] });
await locker.connect();Custom provider
Implement the SerialProvider interface to target any platform:
import type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-locker';
import { Locker } from '@danidoble/webserial-locker';
const myProvider: SerialProvider = {
async requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort> {
// return a SerialPort-compatible object
},
async getPorts(): Promise<SerialPort[]> {
// return previously authorised ports
}
};
const locker = new Locker({
filters: [{ usbVendorId: 0x2341 }],
provider: myProvider
});API
new Locker(options?)
| Option | Type | Default | Description |
| ----------------- | --------------------- | ------- | ------------------------------------------------------------------ |
| channel | number | 1 | Channel number used in the binary handshake and commands. |
| filters | SerialPortFilter[] | [] | USB vendor/product filters for port matching. |
| provider | SerialProvider | — | Per-instance provider. Overrides the global static provider. |
| polyfillOptions | SerialDeviceOptions | — | Extra options forwarded to the provider (e.g. baud rate override). |
locker.connect()
Opens the serial port and performs the binary handshake. Shows a browser port-picker dialog on first connection; subsequent calls reuse the last authorised port.
await locker.connect();locker.disconnect()
Gracefully closes the port and stops auto-reconnect.
await locker.disconnect();locker.sendDispense(options?)
Opens the specified cell and resolves with a DispenserDispenseResponse describing the outcome.
| Option | Type | Default | Description |
| ------ | -------- | ------- | ------------------------- |
| cell | number | 1 | Cell number to open (1-based, max 90). |
const result = await locker.sendDispense(); // cell 1
const result = await locker.sendDispense({ cell: 5 }); // cell 5
// result: { dispensed: boolean, error: boolean, reason: string | null }locker.sendStatus(options?)
Requests the current status of a cell.
| Option | Type | Default | Description |
| ------ | ------------------ | ------- | ------------------------- |
| cell | number \| string | 1 | Cell number (1-based, max 90). |
await locker.sendStatus({ cell: 3 });locker.sendLightScan(options?)
Triggers a light/proximity scan across a column range.
| Option | Type | Default | Description |
| -------- | -------- | ------- | ------------------------- |
| since | number | 0 | Starting column (0–10). |
| until | number | 10 | Ending column (0–10). |
await locker.sendLightScan({ since: 0, until: 5 });locker.sendEnable(options?)
Enables the specified cell.
| Option | Type | Default | Description |
| ------ | ------------------ | ------- | ------------------------- |
| cell | number \| string | 1 | Cell number (1-based, max 90). |
await locker.sendEnable({ cell: 4 });locker.sendDisable(options?)
Disables the specified cell.
| Option | Type | Default | Description |
| ------ | ------------------ | ------- | ------------------------- |
| cell | number \| string | 1 | Cell number (1-based, max 90). |
await locker.sendDisable({ cell: 4 });locker.sendOpenAll()
Opens all cells 1–80 sequentially. Emits locker:percentage:open after each cell. Returns an array of DispenserDispenseResponse.
const results = await locker.sendOpenAll();locker.sendEnableAll()
Enables all cells 1–80 sequentially. Emits locker:percentage:enable after each cell.
await locker.sendEnableAll();locker.sendDisableAll()
Disables all cells 1–80 sequentially. Emits locker:percentage:disable after each cell.
await locker.sendDisableAll();locker.isConnected()
Returns true when the port is open and the handshake has completed.
Events
Core events (from webserial-core)
| Event | Payload | Description |
| ------------------------ | --------------------------------- | ------------------------------------------------- |
| serial:connecting | instance | Port is being opened. |
| serial:connected | instance | Port opened successfully. |
| serial:disconnected | instance | Port closed or device unplugged. |
| serial:reconnecting | instance | Auto-reconnect attempt in progress. |
| serial:data | data: Uint8Array, instance | Raw binary frame received from the device. |
| serial:sent | data: Uint8Array, instance | Raw bytes written to the port. |
| serial:error | error: Error, instance | An error occurred during communication. |
| serial:need-permission | instance | No authorised port found; user must grant access. |
| serial:timeout | command: Uint8Array, instance | A queued command timed out. |
Locker events
| Event | Payload | Description |
| --------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
| locker:connected | { channel: number } | Handshake succeeded; locker is ready. |
| locker:dispensed | { cell_status: number } | A cell was successfully opened. |
| locker:not-dispensed | { cell_status: number } | A cell could not be opened (closed, disconnected, etc). |
| locker:message | LockerMessage | Detailed message for every device response. |
| locker:percentage:open | { percentage: number; dispensed: DispenserDispenseResponse[] \| null } | Progress during sendOpenAll(). |
| locker:percentage:enable | { percentage: number } | Progress during sendEnableAll(). |
| locker:percentage:disable | { percentage: number } | Progress during sendDisableAll(). |
LockerMessage codes
| no_code | Meaning |
| --------- | ------------------------------------------------ |
| 100 | Connection handshake completed. |
| 102 | Cell opened successfully. |
| 103 | Cell configuration applied. |
| 104 | Cell is inactive or does not exist. |
| 105 | Cell is closed. |
| 101 | Cell is disconnected or does not exist. |
| 404 | Cell status is unknown. |
| 400 | Response received but not recognised. |
TypeScript
All events and method signatures are fully typed. The package ships with .d.mts / .d.cts declaration files — no extra @types package required.
Commonly used types and all built-in providers are re-exported so you do not need to import directly from webserial-core:
import {
Locker,
WebUsbProvider,
createBluetoothProvider,
createWebSocketProvider
} from '@danidoble/webserial-locker';
import type {
LockerOptions,
DispenserDispenseResponse,
LockerMessage,
OpenCellOptions,
LightScanOptions,
SerialPortFilter,
SerialDeviceOptions,
SerialEventMap,
SerialProvider,
SerialPolyfillOptions
} from '@danidoble/webserial-locker';