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-hopper

v1.0.0

Published

A strongly-typed, event-driven USB hopper driver for the Web Serial API, built on top of webserial-core.

Readme

@danidoble/webserial-hopper

A strongly-typed, event-driven coin hopper driver built on top of webserial-core.

Supports up to 4 independent coin hoppers, a coin validator, balance tracking, and change dispensing — all through a clean event API. The binary serial protocol, handshake, frame parsing, auto-reconnect, and multi-frame splitting are handled automatically.

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-hopper webserial-core

# pnpm
pnpm add @danidoble/webserial-hopper webserial-core

# yarn
yarn add @danidoble/webserial-hopper webserial-core

# bun
bun add @danidoble/webserial-hopper webserial-core

webserial-core is a peer dependency — it must be installed alongside this package.


Quick start

import { Hopper } from '@danidoble/webserial-hopper';

const hopper = new Hopper({ filters: [{ usbVendorId: 0x2341 }] });

// ── Configure hoppers (optional — these are the defaults) ──────────────
hopper
  .setHopperName({ hopper: 1, name: '10 Pesos' })
  .setHopperCurrency({ hopper: 1, currency: 10 })
  .setMaxCapacity({ hopper: 1, capacity: 1000 });

// ── Listen for events ──────────────────────────────────────────────────
hopper.on('serial:connecting',      () => console.log('Opening port…'));
hopper.on('serial:connected',       () => console.log('Port open, running handshake…'));
hopper.on('serial:disconnected',    () => console.log('Disconnected'));

hopper.on('hopper:levels',          (levels) => console.log('All levels:', levels));
hopper.on('hopper:updated',         (level)  => console.log(`Hopper ${level.id} → ${level.amount}`));
hopper.on('hopper:coin:inserted',   ({ coinValue }) => console.log('Coin inserted, value:', coinValue));
hopper.on('hopper:balance:updated', ({ balance })   => console.log('Balance:', balance));
hopper.on('hopper:dispense-change', ({ amount })    => console.log('Change dispensed:', amount));
hopper.on('hopper:message',         (msg)  => console.log(`[${msg.no_code}] ${msg.name}`));

// ── Open the port (requires a user gesture) ────────────────────────────
await hopper.connect();

// ── Read all hopper levels ─────────────────────────────────────────────
await hopper.sendRequestStatus();

// ── Read / write a single hopper ──────────────────────────────────────
await hopper.sendReadHopper({ hopper: 1 });
await hopper.sendWriteHopper({ hopper: 1, quantity: 500 });

// ── Dispense from a specific hopper ───────────────────────────────────
await hopper.sendDispenseHopper({ hopper: 1 });

// ── Dispense exact change (sum across hoppers) ─────────────────────────
await hopper.sendDispenseChange({ change: 15 }); // dispenses 15 units

// ── Balance management ─────────────────────────────────────────────────
await hopper.sendReadBalance();
await hopper.sendClearBalance();

// ── Validator (coin acceptor) ──────────────────────────────────────────
await hopper.sendEnableValidator();
await hopper.sendDisableValidator();

Serial settings

The constructor pre-configures the following defaults — no extra setup needed:

| Setting | Value | | ------------------ | ------------------------ | | Baud rate | 9 600 | | Data bits | 8 | | Stop bits | 1 | | Parity | none | | Flow control | none | | Buffer size | 255 B | | Parser | interByteTimeout (40 ms) | | Command timeout | 3 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 { Hopper } from '@danidoble/webserial-hopper';

const hopper = new Hopper({ filters: [{ usbVendorId: 0x2341 }] });
await hopper.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 { Hopper, WebUsbProvider } from '@danidoble/webserial-hopper';

const hopper = new Hopper({
  filters: [{ usbVendorId: 0x2341 }],
  provider: new WebUsbProvider()
});

await hopper.connect();

Web Bluetooth (createBluetoothProvider)

Communicate over Bluetooth Low Energy using the Nordic UART Service (NUS). The device must expose NUS characteristics.

import { Hopper, createBluetoothProvider } from '@danidoble/webserial-hopper';

const hopper = new Hopper({
  provider: createBluetoothProvider()
});

await hopper.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 { Hopper, createWebSocketProvider } from '@danidoble/webserial-hopper';

const hopper = new Hopper({
  filters: [{ usbVendorId: 0x2341 }],
  provider: createWebSocketProvider('ws://localhost:8080')
});

await hopper.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 { Hopper } from '@danidoble/webserial-hopper';

AbstractSerialDevice.setProvider(new WebUsbProvider());

const hopper = new Hopper({ filters: [{ usbVendorId: 0x2341 }] });
await hopper.connect();

Custom provider

Implement the SerialProvider interface to target any platform:

import type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-hopper';
import { Hopper } from '@danidoble/webserial-hopper';

const myProvider: SerialProvider = {
  async requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort> {
    // return a SerialPort-compatible object
  },
  async getPorts(): Promise<SerialPort[]> {
    // return previously authorised ports
  }
};

const hopper = new Hopper({
  filters: [{ usbVendorId: 0x2341 }],
  provider: myProvider
});

API

new Hopper(options?)

| Option | Type | Default | Description | | ----------------- | --------------------- | ------- | ------------------------------------------------------------------ | | 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). |

hopper.connect()

Opens the serial port and runs the binary handshake. Shows a browser port-picker dialog on first connection; subsequent calls reuse the last authorised port.

await hopper.connect();

hopper.disconnect()

Gracefully closes the port and stops auto-reconnect.

await hopper.disconnect();

hopper.isConnected()

Returns true when the port is open and the handshake has completed.


Commands

hopper.sendConnect()

Sends the connection handshake frame manually. Useful if you need to re-trigger the handshake.

hopper.sendRequestStatus()

Requests the live coin level of all 4 hoppers at once. Triggers a hopper:levels event.

await hopper.sendRequestStatus();

hopper.sendReadHopper(options?)

Reads the level of a single hopper. Triggers hopper:updated.

| Option | Type | Default | Description | | -------- | -------------- | ------- | ------------------------ | | hopper | HopperNumber | 1 | Hopper to read (1 – 4). |

await hopper.sendReadHopper({ hopper: 2 });

hopper.sendWriteHopper(options?)

Writes (overwrites) the coin count of a single hopper. Triggers hopper:updated.

| Option | Type | Default | Description | | ---------- | -------------- | ------- | ---------------------------------------- | | hopper | HopperNumber | 1 | Hopper to write (1 – 4). | | quantity | number | 0 | New coin count (−32 768 – 32 767, integer). |

await hopper.sendWriteHopper({ hopper: 1, quantity: 500 });

hopper.sendDispenseHopper(options?)

Dispenses one coin from the specified hopper. Triggers hopper:updated.

| Option | Type | Default | Description | | -------- | -------------- | ------- | ------------------------- | | hopper | HopperNumber | 1 | Hopper to dispense from. |

await hopper.sendDispenseHopper({ hopper: 3 });

hopper.sendDispenseChange(options?)

Dispenses the exact change amount using the available hoppers. Triggers hopper:dispense-change.

| Option | Type | Default | Description | | -------- | -------- | ------- | ------------------------------------ | | change | number | 0 | Amount to dispense (0 – 32 767, integer). |

await hopper.sendDispenseChange({ change: 15 });

hopper.sendReadBalance()

Reads the accumulated balance (total inserted value). Triggers hopper:balance:updated.

await hopper.sendReadBalance();

hopper.sendClearBalance()

Clears the accumulated balance. Triggers hopper:balance:updated.

await hopper.sendClearBalance();

hopper.sendEnableValidator() / hopper.sendDisableValidator()

Enables or disables the coin validator (acceptor). Shorthand for sendConfigValidator.

await hopper.sendEnableValidator();
await hopper.sendDisableValidator();

hopper.sendConfigValidator(options?)

Low-level validator toggle.

| Option | Type | Default | Description | | -------- | --------- | ------- | ----------------------------- | | enable | boolean | false | true to enable the validator. |

await hopper.sendConfigValidator({ enable: true });

hopper.sendChange1x1(options?)

Sends a 1-for-1 change command (platform-specific).

| Option | Type | Default | Description | | -------- | -------------- | ------- | ----------------- | | hopper | HopperNumber | 1 | Target hopper. |

hopper.sendCustomCode(options?)

Sends a raw byte array as a command frame. Use for vendor-specific extensions.

| Option | Type | Default | Description | | ------ | ---------- | ------- | ------------------------------------- | | code | number[] | [] | Bytes to send (each 0 – 255, max 12). |

await hopper.sendCustomCode({ code: [0x0a, 0xff, 0x01] });

Configuration (chainable)

These methods update the in-memory hopper metadata and return this for chaining. They do not send any serial command.

hopper.setMaxCapacity(options?)

| Option | Type | Default | Description | | ---------- | -------------- | -------- | ------------------------------------ | | hopper | HopperNumber | 1 | Hopper to configure (1 – 4). | | capacity | number | 1000 | Maximum coin capacity. |

hopper.setHopperName(options?)

| Option | Type | Default | Description | | -------- | -------------- | ------- | ---------------------------- | | hopper | HopperNumber | 1 | Hopper to configure (1 – 4). | | name | string | '' | Human-readable label. |

hopper.setHopperKey(options?)

| Option | Type | Default | Description | | -------- | -------------- | ------- | ------------------------------------------ | | hopper | HopperNumber | 1 | Hopper to configure (1 – 4). | | key | string | '' | Machine-readable identifier (e.g. "ten_pesos"). |

hopper.setHopperCurrency(options?)

| Option | Type | Default | Description | | ---------- | -------------- | ------- | -------------------------------------- | | hopper | HopperNumber | 1 | Hopper to configure (1 – 4). | | currency | number | 1 | Coin denomination (positive number). |

// Chainable example
hopper
  .setHopperName({ hopper: 1, name: '10 Pesos' })
  .setHopperCurrency({ hopper: 1, currency: 10 })
  .setMaxCapacity({ hopper: 1, capacity: 800 });

Getters

| Getter | Type | Description | | ------------------------ | ----------------------- | ---------------------------------------------------- | | hopper.balance | number | Last known accumulated balance from the validator. | | hopper.currentHopper | HopperNumber \| null | The hopper targeted by the most recent command. | | hopper.levels | HopperLevel[] | Array of 4 HopperLevel objects (one per hopper). |

HopperLevel shape:

interface HopperLevel {
  id: number;       // 1 – 4
  currency: number; // denomination value (e.g. 10)
  key: string;      // machine key (e.g. "Hopper 1")
  name: string;     // display label (e.g. "10 Pesos")
  amount: number;   // current coin count
  capacity: number; // maximum capacity
}

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

Hopper events

| Event | Payload | Description | | -------------------------- | ---------------------------- | ------------------------------------------------------- | | hopper:test | Uint8Array | Handshake / test frame received. | | hopper:levels | HopperLevel[] | All 4 hopper levels updated (from sendRequestStatus). | | hopper:updated | HopperLevel | Single hopper level updated (read / write / dispense). | | hopper:dispense-change | { amount: number } | Change dispensed; amount reflects what was paid out. | | hopper:balance:updated | { balance: number } | Accumulated balance changed (read or cleared). | | hopper:coin:inserted | { coinValue: number } | Coin(s) inserted by the customer (debounced 500 ms; coinValue is the pulse count). | | hopper:validator:message | HopperMessage | Message from the coin validator module. | | hopper:unknown | HopperMessage | Unrecognised but valid frame received. | | hopper:message | HopperMessage | Emitted for every device response. |

HopperMessage

interface HopperMessage {
  code: Uint8Array;         // raw frame bytes
  name: string | null;      // protocol name (e.g. "STATUS")
  description: string | null;
  request: string | null;
  no_code: number;          // see table below
  error: boolean;
  data: unknown;
  hopper?: HopperNumber | null;
}

no_code reference

| no_code | Name | Trigger | | --------- | ------------------ | ------------------------------------------------- | | 0 | TEST | Handshake / test response. | | 1 | STATUS | All hopper levels (sendRequestStatus). | | 2 | READ_HOPPER | Single hopper read (sendReadHopper). | | 3 | WRITE_HOPPER | Single hopper write (sendWriteHopper). | | 4 | DISPENSE_HOPPER | Dispense from hopper (sendDispenseHopper). | | 5 | DISPENSE_CHANGE | Change dispensed (sendDispenseChange). | | 6 | READ_BALANCE | Balance read (sendReadBalance). | | 7 | CLEAR_BALANCE | Balance cleared (sendClearBalance). | | 8 | VALIDATOR_MESSAGE| Message from coin validator. | | 9 | INSERTED_COIN | Coin inserted by customer. | | 400 | SYNTAX_ERROR | Device reported a syntax error (0xFF 0xFF 0xFF). | | 401 | LOW_LEVEL | Low coin level in hopper (0xFF 0xAA 0xAA). | | 402 | TIMEOUT_DISPENSE | Dispense timed out (0xFF 0xBB 0xBB). | | 999 | UNKNOWN | Unrecognised frame. |


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 {
  Hopper,
  WebUsbProvider,
  createBluetoothProvider,
  createWebSocketProvider,
} from '@danidoble/webserial-hopper';

import type {
  HopperOptions,
  HopperLevel,
  HopperMessage,
  HopperNumber,
  MaxCapacityOptions,
  HopperNameOptions,
  HopperKeyOptions,
  HopperCurrencyOptions,
  ReadHopperOptions,
  WriteHopperOptions,
  DispenseHopperOptions,
  DispenseChangeOptions,
  ConfigValidatorOptions,
  Change1x1Options,
  CustomCodeOptions,
  SerialPortFilter,
  SerialDeviceOptions,
  SerialEventMap,
  SerialProvider,
  SerialPolyfillOptions,
} from '@danidoble/webserial-hopper';

License

GPL-3.0-only © Danidoble