@danidoble/webserial-boardroid
v1.0.0
Published
A strongly-typed, event-driven USB boardroid driver for the Web Serial API, built on top of webserial-core.
Readme
@danidoble/webserial-boardroid
A strongly-typed, event-driven vending machine controller driver built on top of webserial-core.
Manages the serial connection, binary handshake, auto-reconnect, and message routing for a full vending machine main board: coin purse (MDB), banknote purse (recycler / ICT), card reader, cooling relay, temperature sensor, door monitor, and product dispensing.
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-boardroid webserial-core
# pnpm
pnpm add @danidoble/webserial-boardroid webserial-core
# yarn
yarn add @danidoble/webserial-boardroid webserial-core
# bun
bun add @danidoble/webserial-boardroid webserial-core
webserial-coreis a peer dependency — it must be installed alongside this package.
Quick start
import { Boardroid } from '@danidoble/webserial-boardroid';
const boardroid = new Boardroid({ channel: 1, filters: [{ usbVendorId: 0x2341 }] });
// Serial lifecycle
boardroid.on('serial:connecting', () => console.log('Opening port…'));
boardroid.on('serial:connected', () => console.log('Port open'));
boardroid.on('serial:disconnected', () => console.log('Disconnected'));
// Boardroid ready
boardroid.on('boardroid:connected', ({ channel }) => console.log(`Boardroid on channel ${channel} ready`));
// Every device message
boardroid.on('boardroid:message', (msg) => console.log(`[${msg.no_code}] ${msg.name}`));
// Payment events
boardroid.on('money:inserted', ({ type, money }) => console.log(`${type}: ${money.name}`));
boardroid.on('session:money-request', () => console.log('Money session updated'));
// Product dispense result
boardroid.on('dispensed', () => console.log('Dispense cycle finished'));
// Door / temperature / relay monitoring
boardroid.on('event:door', ({ open }) => console.log('Door', open ? 'open' : 'closed'));
boardroid.on('status:temperature',({ temperature }) => console.log('Temp:', temperature));
boardroid.on('status:relay', ({ enabled }) => console.log('Relay', enabled ? 'on' : 'off'));
// Opens a port picker dialog (requires a user gesture)
await boardroid.connect();
// Enable payment purses (coin + banknote)
await boardroid.sendPaymentPursesEnable({ coin: true, banknote: true });
// Dispense product on selection 5
await boardroid.sendDispense({ selection: 5 });
// Disable payment purses at end of sale
await boardroid.sendPaymentPursesDisable();
// Dispense change
await boardroid.sendCoinPurseDispense({ $5: 1, $1: 2 });
// Read temperature
await boardroid.sendReadTemperature();
// Test all 80 motors sequentially
const results = await boardroid.sendTestEngines({ limit: 80 });
console.log(results.filter(r => r.dispensed).length, 'motors OK');Serial settings
The constructor pre-configures the following defaults — no extra setup needed:
| Setting | Value | | ------------------ | ------------------------ | | Baud rate | 115 200 | | Data bits | 8 | | Stop bits | 1 | | Parity | none | | Flow control | none | | Buffer size | 255 B | | Parser | fixedLength (14 bytes) | | Command timeout | 5 000 ms | | Auto-reconnect | ✓ | | Reconnect interval | 1 500 ms | | Handshake timeout | 3 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 { Boardroid } from '@danidoble/webserial-boardroid';
const boardroid = new Boardroid({ filters: [{ usbVendorId: 0x2341 }] });
await boardroid.connect();WebUSB (WebUsbProvider)
import { Boardroid, WebUsbProvider } from '@danidoble/webserial-boardroid';
const boardroid = new Boardroid({
filters: [{ usbVendorId: 0x2341 }],
provider: new WebUsbProvider()
});
await boardroid.connect();Web Bluetooth (createBluetoothProvider)
import { Boardroid, createBluetoothProvider } from '@danidoble/webserial-boardroid';
const boardroid = new Boardroid({ provider: createBluetoothProvider() });
await boardroid.connect();WebSocket (createWebSocketProvider)
import { Boardroid, createWebSocketProvider } from '@danidoble/webserial-boardroid';
const boardroid = new Boardroid({
filters: [{ usbVendorId: 0x2341 }],
provider: createWebSocketProvider('ws://localhost:8080')
});
await boardroid.connect();Global provider (AbstractSerialDevice.setProvider)
import { AbstractSerialDevice, WebUsbProvider } from 'webserial-core';
import { Boardroid } from '@danidoble/webserial-boardroid';
AbstractSerialDevice.setProvider(new WebUsbProvider());
const boardroid = new Boardroid({ filters: [{ usbVendorId: 0x2341 }] });
await boardroid.connect();Custom provider
import type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-boardroid';
import { Boardroid } from '@danidoble/webserial-boardroid';
const myProvider: SerialProvider = {
async requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort> {
// return a SerialPort-compatible object
},
async getPorts(): Promise<SerialPort[]> {
// return previously authorised ports
}
};
const boardroid = new Boardroid({ provider: myProvider });API
new Boardroid(options?)
| Option | Type | Default | Description |
| ----------------- | ----------------------------- | ------- | ------------------------------------------------------------ |
| channel | number | 1 | Channel used in the binary handshake. |
| filters | SerialPortFilter[] | [] | USB vendor/product filters for port matching. |
| provider | SerialProvider | — | Per-instance transport provider. |
| polyfillOptions | SerialDeviceOptions<Uint8Array> | — | Extra options forwarded to the underlying device. |
Connection
boardroid.connect()
Opens the serial port and performs the binary handshake. Shows a browser port-picker on first connection; subsequent calls reuse the last authorised port.
await boardroid.connect();boardroid.disconnect()
Gracefully closes the port and stops auto-reconnect.
await boardroid.disconnect();boardroid.isConnected()
Returns true when the port is open and the handshake has completed.
boardroid.softReload()
Resets the in-memory sale session (price, change, inserted/retired counters) without touching the physical device.
boardroid.softReload();Properties
| Property | Type | R/W | Description |
| ------------------------ | --------- | --- | --------------------------------------------------------------------------- |
| totalInTubes | number | R | Total monetary value (in currency units) currently in the coin tubes. |
| totalInRecycler | number | R | Total monetary value currently in the banknote recycler. |
| hasRecycler | boolean | R/W | Whether the banknote purse has a recycler unit. |
| hasICT | boolean | R/W | Whether the recycler uses ICT (Innovative Technology) hardware. |
| banknoteICT | number | R/W | Active ICT banknote denomination (20 \| 50 \| 100 \| 200 \| 500). |
| hasCoinPurse | boolean | R/W | Whether a coin purse is physically connected. |
| price | number | R/W | Current sale price. Negative or NaN values are normalised to 0. |
| change | number | R | Calculated change due (inserted − price). Returns 0 if no debt. |
| coins | CoinsInfo | R | Live snapshot of all coin counters (tubes, box, totals). |
| banknotes | BanknotesInfo | R | Live snapshot of all banknote counters (stacker, recycler, out, totals). |
| cardReaderAvailable | boolean | R/W | Whether a card reader is physically connected. |
| cardReaderMaxPreCredit | number | R/W | Maximum pre-credit amount allowed for the card reader. |
Coin purse
boardroid.sendCoinPurseConfigure(options?)
Sends a full configuration frame to the coin purse.
| Option | Type | Default | Description |
| -------- | --------- | -------- | ------------------------------ |
| enable | boolean | false | Enable (true) or disable. |
| high | number | 0xff | High-byte tube limit. |
| low | number | 0xff | Low-byte tube limit. |
boardroid.sendCoinPurseEnable()
Shorthand for sendCoinPurseConfigure({ enable: true }).
boardroid.sendCoinPurseDisable()
Shorthand for sendCoinPurseConfigure({ enable: false }).
boardroid.sendCoinPurseReadTubes()
Requests the current coin count in each tube. Triggers a coin-purse:tubes event.
boardroid.sendCoinPurseDispense(options?)
Dispenses coins from the coin purse.
| Option | Type | Default | Description |
| ------- | -------- | ------- | ------------------------------ |
| $50c | number | 0 | Number of 50-centavo coins. |
| $1 | number | 0 | Number of $1 coins. |
| $2 | number | 0 | Number of $2 coins. |
| $5 | number | 0 | Number of $5 coins. |
| $10 | number | 0 | Number of $10 coins. |
await boardroid.sendCoinPurseDispense({ $5: 1, $1: 2 }); // dispense $7 in coinsBanknote purse
boardroid.sendBanknotePurseConfigure(options?)
Sends a configuration frame to the banknote purse. Automatically selects ICT or standard protocol based on hasICT.
| Option | Type | Default | Description |
| -------- | --------- | ------- | ----------------------------------------- |
| enable | boolean | false | Enable (true) or disable. |
| scrow | boolean | false | Enable scrow mode (manual accept/reject). |
boardroid.sendBanknotePurseEnable(options?)
Shorthand for sendBanknotePurseConfigure({ enable: true, ... }).
| Option | Type | Default | Description |
| ------- | --------- | ------- | -------------------- |
| scrow | boolean | false | Enable scrow mode. |
boardroid.sendBanknotePurseDisable()
Shorthand for sendBanknotePurseConfigure({ enable: false }).
boardroid.sendBanknotePurseDispense(options?)
Dispenses banknotes from the recycler. Requires hasRecycler = true.
| Option | Type | Default | Description |
| -------- | -------- | ------- | ---------------------------- |
| $20 | number | 0 | Number of $20 banknotes. |
| $50 | number | 0 | Number of $50 banknotes. |
| $100 | number | 0 | Number of $100 banknotes. |
| $200 | number | 0 | Number of $200 banknotes. |
| $500 | number | 0 | Number of $500 banknotes. |
| $1000 | number | 0 | Number of $1000 banknotes. |
boardroid.sendBanknotePurseAcceptInScrow()
Accepts (stacks/recycles) a banknote currently held in scrow.
boardroid.sendBanknotePurseRejectInScrow()
Rejects and ejects a banknote currently held in scrow.
boardroid.sendBanknotePurseReadRecycler()
Requests the current banknote count in the recycler. Triggers a banknote-purse:recycler event.
boardroid.sendBanknotePurseSaveMemory(options)
Saves denomination counts to the banknote purse NVRAM. All fields are required.
| Option | Type | Description |
| --------- | -------- | ----------------------------- |
| channel | number | Channel number. |
| $20 | number | $20 count to save. |
| $50 | number | $50 count to save. |
| $100 | number | $100 count to save. |
| $200 | number | $200 count to save. |
| $500 | number | $500 count to save. |
| $1000 | number | $1000 count to save. |
Payment purses (combined)
boardroid.sendPaymentPursesEnable(options?)
Enables the specified payment peripherals simultaneously.
| Option | Type | Default | Description |
| --------------- | --------- | ------- | ---------------------------------------- |
| coin | boolean | true | Enable coin purse. |
| banknote | boolean | true | Enable banknote purse. |
| scrowBanknote | boolean | false | Enable scrow mode on the banknote purse. |
boardroid.sendPaymentPursesDisable(options?)
Disables the specified payment peripherals simultaneously.
| Option | Type | Default | Description |
| ------------ | --------- | ------- | ----------------------------- |
| coin | boolean | true | Disable coin purse. |
| banknote | boolean | true | Disable banknote purse. |
| cardReader | boolean | false | Disable card reader. |
Card reader
boardroid.sendCardReaderDispense(options?)
Initiates a card reader transaction and dispenses the selected product.
| Option | Type | Default | Description |
| ------------------ | ------------------ | ------- | ---------------------------------------------------- |
| selection | number | 1 | Primary product selection (1-based). |
| second_selection | number \| null | null | Secondary selection for combo dispensing. |
| sensor | boolean | true | Use optical sensor to detect delivery. |
| seconds | number \| null | null | Motor run time in seconds (0.1–40.0) if no sensor. |
| price | number | 0 | Transaction amount. Must be > 0. |
boardroid.sendCardReaderDisable()
Disables the card reader.
Dispense
boardroid.sendDispense(options?)
Triggers the vending motor for the given product selection.
| Option | Type | Default | Description |
| ------------------ | ---------------- | ------- | ---------------------------------------------------- |
| selection | number | 1 | Product selection (1–80). |
| second_selection | number \| null | null | Secondary selection (1–80, must differ from first). |
| sensor | boolean | true | Use optical sensor to confirm delivery. |
| seconds | number \| null | null | Motor run time in seconds (0.1–40.0) if no sensor. |
await boardroid.sendDispense({ selection: 5 });
// without sensor — run motor for 2 s
await boardroid.sendDispense({ selection: 5, sensor: false, seconds: 2 });boardroid.sendTestEngines(options?)
Sequentially fires every motor from 1 to limit, collects the response for each, and returns an array of DispenserDispenseResponse. Emits percentage:test after each motor.
| Option | Type | Default | Description |
| ------- | -------- | ------- | ------------------------------------- |
| limit | number | 80 | Last motor to test (1-based). |
boardroid.on('percentage:test', ({ percentage }) => console.log(`${percentage}%`));
const results = await boardroid.sendTestEngines({ limit: 80 });Cooling relay
boardroid.sendCoolingRelayConfigure(options?)
| Option | Type | Default | Description |
| -------- | --------- | ------- | -------------------- |
| enable | boolean | false | Turn relay on/off. |
boardroid.sendCoolingRelayEnable()
Shorthand for sendCoolingRelayConfigure({ enable: true }).
boardroid.sendCoolingRelayDisable()
Shorthand for sendCoolingRelayConfigure({ enable: false }).
Temperature
boardroid.sendReadTemperature()
Requests the current cabinet temperature. Triggers a status:temperature event.
Custom frame
boardroid.sendCustomCode(options)
Sends a raw byte array directly to the device.
| Option | Type | Description |
| ------ | ---------- | ------------------------------ |
| code | number[] | Raw bytes to send (12 bytes). |
await boardroid.sendCustomCode({ code: [0xf1, 0xcb, 0, 0, 0, 0, 0, 0, 0, 0, 0xf2, 0x00] });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 14-byte 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. |
Boardroid events
| Event | Payload | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------------------ | --------------------------------------------------------- |
| boardroid:connected | { channel: number } | Handshake succeeded; device is ready. |
| boardroid:message | BoardroidMessage | Fired for every recognised device response. |
| boardroid:unknown | BoardroidMessage | Fired for unrecognised response bytes. |
| run:default-load | {} | Fires right after boardroid:connected. Load defaults here. |
| money:inserted | { type: 'coin' \| 'banknote'; money: MoneyInfo; where: string } | A coin or banknote was inserted. |
| session:money-request | {} | Money session totals changed. |
| session:money-dispensed | { type_money: string \| null; retired: number \| null; finish: boolean; type: string; data?: BoardroidMessage } | Change or banknote was dispensed. |
| dispensed | {} | Product dispense cycle completed. |
| coin-purse:config | { enabled: boolean } | Coin purse enable/disable confirmed. |
| coin-purse:tubes | CoinTubes | Current coin count per tube denomination. |
| coin-purse:coin-event | CoinsInfo | Full coin counter snapshot. |
| coin-purse:reject-lever | {} | Reject lever was pressed. |
| coin-purse:reset | {} | Coin purse was reset. |
| banknote-purse:config | { enabled: boolean; scrow: boolean } | Banknote purse enable/scrow confirmed. |
| banknote-purse:event-banknote | BanknotesInfo | Full banknote counter snapshot. |
| banknote-purse:recycler | BanknoteStacker | Current count per recycler denomination. |
| banknote-purse:banknote-scrow-status | { status: boolean } | Scrow accept/reject result. |
| banknote-purse:save-memory | { message: BoardroidMessage } | NVRAM save operation result. |
| banknote-purse:read-memory | { message: BoardroidMessage } | NVRAM read operation result. |
| card-reader:event | BoardroidMessage | Card reader state change (disable, pre-auth, sell, etc.). |
| event:door | { open: boolean } | Cabinet door opened or closed. |
| door:event | { open: boolean } | Alias for event:door. |
| status:temperature | { high: number; low: number; temperature: number } | Current cabinet temperature (°C). |
| status:relay | { enabled: boolean } | Cooling relay state. |
| percentage:test | { percentage: number; dispensed: DispenserDispenseResponse[] \| null } | Progress during sendTestEngines(). |
BoardroidMessage — selected no_code values
| no_code | Meaning |
| ------------ | ----------------------------------------------------- |
| 1 | Connection handshake completed. |
| 3 | Coin purse enabled. |
| 4 | Coin purse disabled. |
| 5 | Banknote purse configured. |
| 6 | Coin tubes read. |
| 7 | Banknote recycler read. |
| 8 | Banknote scrow status. |
| 9 | Banknotes dispensed. |
| 10 | Coins dispensed. |
| 11 | Product not delivered. |
| 12 | Product delivered. |
| 13 | Door closed. |
| 14 | Door open. |
| 15 | Temperature status. |
| 16 | Relay on. |
| 17 | Relay off. |
| 18 | Banknote NVRAM saved. |
| 19 | Banknote NVRAM read. |
| 20–31 | Card reader events (code 20 + event_byte). |
| 100 | Coin reject lever pressed. |
| 101 | Coin purse reset. |
| 200 | Banknote dispensed by ICT recycler. |
| 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 {
Boardroid,
WebUsbProvider,
createBluetoothProvider,
createWebSocketProvider
} from '@danidoble/webserial-boardroid';
import type {
BoardroidOptions,
BoardroidMessage,
DispenserDispenseResponse,
CommandOptions,
CoinPurseConfigureOptions,
CoinPurseDispenseOptions,
BanknotePurseConfigureOptions,
BanknotePurseEnableOptions,
BanknotePurseDispenseOptions,
BanknotePurseICTConfigureOptions,
BanknotePurseICTDispenseOptions,
BanknotePurseOtherConfigureOptions,
BanknotePurseOtherDispenseOptions,
BanknotePurseSaveMemoryOptions,
CardReaderDispenseOptions,
BoardroidDispenseOptions,
CoolingRelayConfigureOptions,
PaymentPursesOptions,
PaymentPursesEnableOptions,
SendCustomCodeOptions,
MoneyInfo,
CoinTubes,
CoinsInfo,
BanknoteStacker,
BanknotesInfo,
DenominationBanknote,
SerialPortFilter,
SerialDeviceOptions,
SerialEventMap,
SerialParser,
SerialProvider,
SerialPolyfillOptions
} from '@danidoble/webserial-boardroid';