npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

npm version license


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 SerialProvider implementation

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-core is 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 sequentially

Serial 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 picker

WebSocket (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';

License

GPL-3.0-only © Danidoble