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

v1.0.0

Published

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

Readme

@danidoble/webserial-jofemar

A strongly-typed, event-driven driver for Jofemar vending machines (ES-Plus / Ice-Plus) 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-jofemar webserial-core

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

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

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

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


Quick start

import { Jofemar } from '@danidoble/webserial-jofemar';
import type { SerialPortFilter } from '@danidoble/webserial-jofemar';

const filters: SerialPortFilter[] = [{ usbVendorId: 0x0403 }]; // FTDI chip
const machine = new Jofemar({ channel: 1, type: 'esplus', filters });

machine.on('serial:connecting',   () => console.log('Opening port…'));
machine.on('serial:connected',    () => console.log('Port open'));
machine.on('serial:disconnected', () => console.log('Disconnected'));

machine.on('jofemar:connected',      ({ channel }) => console.log(`Jofemar on channel ${channel} ready`));
machine.on('jofemar:machine-status', (msg)         => console.log(`[${msg.no_code}] ${msg.name}`));
machine.on('jofemar:warning',        ({ type })     => console.warn('Warning:', type));
machine.on('jofemar:error',          ({ type })     => console.error('Error:', type));
machine.on('jofemar:message',        (msg)          => console.log(`[${msg.no_code}] ${msg.name}`));

// Opens a port picker dialog (requires a user gesture)
await machine.connect();

// Dispense selection 5 (returns DispenseResult)
const result = await machine.sendDispense({ selection: 5 });
console.log(result.status);  // true | false
console.log(result.error);   // null | 'not-dispensed' | 'elevator-locked' | 'no-response' | 'send-failed'

// Check machine status
await machine.sendStatus();

// Collect the dispensed product
await machine.sendCollect();

// Reset errors
await machine.sendResetAll();

// Scan channels 1–80 for presence
machine.startChannelVerification = 1;
machine.endChannelVerification   = 80;
const channels = await machine.sendAssignChannels();
console.log(channels); // [{ selection: 1, active: true }, ...]

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 | 4 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 { Jofemar } from '@danidoble/webserial-jofemar';

const machine = new Jofemar({ filters: [{ usbVendorId: 0x0403 }] });
await machine.connect();

WebUSB (WebUsbProvider)

import { Jofemar, WebUsbProvider } from '@danidoble/webserial-jofemar';

const machine = new Jofemar({
  filters: [{ usbVendorId: 0x0403 }],
  provider: new WebUsbProvider(),
});
await machine.connect();

Web Bluetooth (createBluetoothProvider)

import { Jofemar, createBluetoothProvider } from '@danidoble/webserial-jofemar';

const machine = new Jofemar({ provider: createBluetoothProvider() });
await machine.connect();

WebSocket (createWebSocketProvider)

import { Jofemar, createWebSocketProvider } from '@danidoble/webserial-jofemar';

const machine = new Jofemar({
  filters: [{ usbVendorId: 0x0403 }],
  provider: createWebSocketProvider('ws://localhost:8080'),
});
await machine.connect();

Global provider (AbstractSerialDevice.setProvider)

import { AbstractSerialDevice, WebUsbProvider } from 'webserial-core';
import { Jofemar } from '@danidoble/webserial-jofemar';

AbstractSerialDevice.setProvider(new WebUsbProvider());
const machine = new Jofemar({ filters: [{ usbVendorId: 0x0403 }] });
await machine.connect();

Custom provider

import type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-jofemar';
import { Jofemar } from '@danidoble/webserial-jofemar';

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

const machine = new Jofemar({ filters: [{ usbVendorId: 0x0403 }], provider: myProvider });

API

new Jofemar(options?)

| Option | Type | Default | Description | | ----------------- | --------------------- | ----------- | ------------------------------------------------------------------ | | channel | number | 1 | Machine address used in the handshake and all outbound commands. | | type | DeviceType | 'esplus' | Machine model. Affects temperature limits and reset duration. | | supportCart | boolean | true | Whether the machine has a cart (affects dispense command byte). | | 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 underlying core device. |

Getters and setters

| Name | Type | R/W | Notes | | --------------------------- | ---------------------- | --- | ----------------------------------------- | | isDispensing | boolean | R | true while a dispense is in flight. | | isDoorOpen | boolean | R | Updated from door-event frames. | | deviceType | DeviceType | R/W | 'esplus' | 'iceplus' | | supportCart | boolean | R/W | — | | listenOnChannel | number | R/W | Valid range: 1–31. Throws on invalid. | | startChannelVerification | number \| string | W | Valid range: 1–126. Throws on invalid. | | endChannelVerification | number \| string | W | Valid range: 1–126. Throws on invalid. |


Connection

jofemar.connect()

Opens the serial port and performs the handshake. Shows a browser port-picker dialog on first use.

jofemar.disconnect()

Gracefully closes the port and stops auto-reconnect.


Dispense

jofemar.sendDispense(options?)

Dispenses the product at the given selection. Automatically retries on elevator-locked or no-response.

| Option | Type | Default | Description | | ----------- | ------------------ | ------- | -------------------------------------------------------- | | selection | number \| string | 1 | Channel selection (1–130). | | cart | boolean | false | Use the cart-dispense command byte instead of standard. |

Returns Promise<DispenseResult>.

const result = await machine.sendDispense({ selection: 3, cart: false });
// { status: true, error: null }
// { status: false, error: 'not-dispensed' | 'elevator-locked' | 'no-response' | 'send-failed' }

jofemar.sendProductRemoved()

Signals that the product from the elevator has been physically removed, immediately ending the withdrawal wait.


Status and collect

| Method | Returns | Description | | --------------------- | -------------- | -------------------------------------------------- | | sendStatus() | Promise<void> | Request machine status. Triggers jofemar:machine-status. | | sendCollect() | Promise<void> | Signal that the dispensed product has been collected. | | sendEndDispense() | Promise<void> | End a cart-mode dispense cycle. |


Reset

| Method | Returns | Description | | ------------------------------- | -------------- | -------------------------------------------------------------------- | | sendResetSoldOut() | Promise<void> | Reset sold-out errors. | | sendResetWaitingProduct() | Promise<void> | Reset waiting-product errors. | | sendResetMachine() | Promise<void> | Full machine reset. Emits jofemar:reset-errors. 25 s (ES-Plus) / 40 s (Ice-Plus). | | sendResetAll() | Promise<void> | Runs sendResetWaitingProductsendResetSoldOutsendResetMachine in sequence. |


Lights

| Method | Returns | Description | | ------------------- | -------------- | -------------------- | | sendLightsOn() | Promise<void> | Turn interior lights on. | | sendLightsOff() | Promise<void> | Turn interior lights off. |


Program (write settings)

| Method | Parameters | Description | | -------------------------------------------------------- | --------------------------------------------- | ---------------------------------------- | | sendProgram(param1, param2) | param1: number, param2: number | Send a raw program command. | | sendProgramDisplayLanguage({ language? }) | 'spanish' \| 'english' \| 'french' | Set display language. | | sendProgramBeeper({ enable? }) | enable: boolean (default true) | Enable/disable beeper. | | sendProgramDisableWorkingTemperature() | — | Disable working temperature control (ES-Plus only). | | sendProgramDisableThermometer() | — | Alias for disable working temperature. | | sendProgramWorkingTemperature({ degrees? }) | degrees: number (multiple of 0.5) | Set working temperature. | | sendProgramIsolationTray({ tray? }) | tray: number (0–12) | Set isolation tray number (0 = none). | | sendProgramStandbyAfterCollect({ seconds? }) | seconds: number (15–120) | Time to standby after product collection. | | sendProgramStandbyWithoutCollect({ minutes? }) | minutes: number | Time to standby when product is not collected. | | sendProgramElevatorSpeed({ speed? }) | 'high' \| 'low' | Set elevator speed. | | sendProgramTemperatureExpiration({ enable? }) | enable: boolean (default false) | Enable/disable temperature expiration. | | sendProgramEnableTemperatureExpiration() | — | Enable temperature expiration. | | sendProgramDisableTemperatureExpiration() | — | Disable temperature expiration. | | sendProgramMachineAddress({ address? }) | address: number (default 1) | Assign machine channel address. | | sendProgramTemperatureBeforeExpiration({ degrees? }) | degrees: number | Set temperature threshold before expiration. | | sendProgramTimeBeforeExpiration({ minutes? }) | minutes: number (default 1) | Time before expiration by temperature. | | sendProgramTemperatureScale({ scale? }) | 'celsius' \| 'fahrenheit' | Set temperature display scale. | | sendProgramVoltageEngine({ selection?, voltage? }) | selection: number, voltage: number | Set engine voltage for a channel. | | sendProgramPushOverProducts({ selection?, enable? }) | selection: number, enable: boolean | Enable/disable push-over for a channel. | | sendProgramChannelRunningAfterDispense({ selection?, seconds? }) | selection: number, seconds: number | Set channel run time after dispense. | | sendProgramClock({ date? }) | date: Date (default new Date()) | Synchronise the machine clock. |


Get / Check (read settings)

All methods return Promise<void> and deliver their response via the corresponding jofemar:check-* event.

| Method | Response event | | --------------------------------------------- | ------------------------------------------ | | sendCheckData(type, aux?) | depends on type | | sendGetDisplayLanguage() | jofemar:check-language | | sendGetBeeper() | jofemar:check-beeper | | sendGetWorkingTemperature() | jofemar:temperature-working | | sendGetIsolationTray() | jofemar:check-isolation-tray | | sendGetProgramVersion() | jofemar:program-version | | sendGetFaults() | jofemar:machine-faults | | sendGetMachineId() | jofemar:check-machine-id | | sendGetCurrentTemperature() | jofemar:temperature-current | | sendGetStandbyAfterCollect() | jofemar:check-standby-after-collect | | sendGetStandbyWithoutCollect() | jofemar:check-standby-without-collect | | sendGetElevatorSpeed() | jofemar:check-elevator-speed | | sendGetTemperatureExpiration() | jofemar:check-expiration-by-temperature | | sendGetTemperatureBeforeExpiration() | jofemar:check-temperature-before-expiration | | sendGetTimeBeforeExpiration() | jofemar:check-expiration-after | | sendGetTemperatureScale() | jofemar:check-temperature-scale | | sendGetClockRegisters() | jofemar:clock-registers | | sendGetMachineActivity() | jofemar:machine-activity | | sendGetVoltageEngine({ selection? }) | jofemar:check-engine-voltage | | sendGetChannelPresence({ selection? }) | jofemar:channel-status | | sendGetPushOverProducts({ selection? }) | jofemar:check-push-over | | sendGetChannelRunningAfterDispense({ selection? }) | jofemar:check-extractor-after-dispense |


Display

| Method | Parameters | Description | | --------------------------------------------------------- | -------------------------------------------------- | ------------------------------------------- | | sendSetDisplayStandbyMessage({ message? }) | message: string (max 32 chars) | Set the idle/standby screen message. | | sendSetDisplayMessageTemporarily({ message?, seconds? }) | message: string, seconds: number (default 1) | Show a message for the given number of seconds. | | sendSetDisplayMessageUnlimited({ message? }) | message: string (max 32 chars) | Show a message indefinitely. |


Events config

| Method | Parameters | Description | | ----------------------------------------------- | ------------------------------------------------------- | ------------------------------- | | sendEventsConfig({ event?, enable? }) | event: number \| string \| null, enable: boolean | Configure an automatic event. | | sendEventEnable({ event? }) | event: number \| string \| null | Enable an automatic event. | | sendEventDisable({ event? }) | event: number \| string \| null | Disable an automatic event. |


Custom code

await machine.sendCustomCode({ code: [0x02, 0x30, 0x30, 0x81, 0x53, 0xff, 0xff] });

Channel verification

Scans a range of selections for product presence. Configure the range with the startChannelVerification and endChannelVerification setters before calling.

machine.startChannelVerification = 1;
machine.endChannelVerification   = 80;

machine.on('jofemar:channels-progress', ({ percentage }) => console.log(`${percentage}%`));
machine.on('jofemar:channels',          ({ channels })   => console.log(channels));

const channels = await machine.sendAssignChannels();
// [{ selection: 1, active: true }, { selection: 2, active: false }, ...]

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

Jofemar events

| Event | Payload | Description | | ---------------------------------------- | ------------------------------------ | ------------------------------------------------------- | | jofemar:connected | { channel: number } | Handshake succeeded; machine is ready. | | jofemar:message | MessageSerial | Every parsed frame emits this. | | jofemar:machine-status | MessageSerial | Status response (no_code 8–37). | | jofemar:command-executed | MessageSerial | The last sent command was acknowledged by the machine. | | jofemar:warning | JofemarWarningPayload | Non-fatal condition (empty channel, thermometer, etc.). | | jofemar:error | JofemarErrorPayload | Hardware error (jam, malfunction, EEPROM, etc.). | | jofemar:door-event | JofemarDoorEventPayload | Door opened or closed. | | jofemar:keyboard-pressed | MessageSerialAdditional | A key was pressed on the machine's keypad. | | jofemar:channel-status | JofemarChannelStatusPayload | Single-channel presence/availability response. | | jofemar:program-version | MessageSerialAdditional | Response to sendGetProgramVersion(). | | jofemar:machine-faults | MessageSerialAdditional | Response to sendGetFaults(). | | jofemar:clock-registers | MessageSerialAdditional | Response to sendGetClockRegisters(). | | jofemar:machine-activity | MessageSerialAdditional | Response to sendGetMachineActivity(). | | jofemar:temperature-working | MessageSerialAdditional | Response to sendGetWorkingTemperature(). | | jofemar:temperature-current | MessageSerialAdditional | Response to sendGetCurrentTemperature(). | | jofemar:check-language | MessageSerialAdditional | Response to sendGetDisplayLanguage(). | | jofemar:check-beeper | MessageSerialAdditional | Response to sendGetBeeper(). | | jofemar:check-isolation-tray | MessageSerialAdditional | Response to sendGetIsolationTray(). | | jofemar:check-engine-voltage | MessageSerialAdditional | Response to sendGetVoltageEngine(). | | jofemar:check-push-over | MessageSerialAdditional | Response to sendGetPushOverProducts(). | | jofemar:check-extractor-after-dispense | MessageSerialAdditional | Response to sendGetChannelRunningAfterDispense(). | | jofemar:check-standby-after-collect | MessageSerialAdditional | Response to sendGetStandbyAfterCollect(). | | jofemar:check-standby-without-collect | MessageSerialAdditional | Response to sendGetStandbyWithoutCollect(). | | jofemar:check-elevator-speed | MessageSerialAdditional | Response to sendGetElevatorSpeed(). | | jofemar:check-expiration-by-temperature| MessageSerialAdditional | Response to sendGetTemperatureExpiration(). | | jofemar:check-temperature-before-expiration | MessageSerialAdditional | Response to sendGetTemperatureBeforeExpiration(). | | jofemar:check-expiration-after | MessageSerialAdditional | Response to sendGetTimeBeforeExpiration(). | | jofemar:check-temperature-scale | MessageSerialAdditional | Response to sendGetTemperatureScale(). | | jofemar:check-machine-id | MessageSerialAdditional | Response to sendGetMachineId(). | | jofemar:reset-errors | JofemarResetErrorsPayload | Emitted immediately when sendResetMachine() is called. | | jofemar:channels | JofemarChannelsPayload | Final result of sendAssignChannels(). | | jofemar:channels-progress | JofemarChannelsProgressPayload | Progress updates during sendAssignChannels(). | | jofemar:dispensing-withdrawal | JofemarDispensingWithdrawalPayload | Emitted every second while waiting for elevator clearance. |


TypeScript

All events and method signatures are fully typed. The package ships with .d.mts / .d.cts declaration files — no extra @types package needed.

Providers and commonly used types are re-exported so you rarely need to import directly from webserial-core:

import { Jofemar, WebUsbProvider, createBluetoothProvider, createWebSocketProvider } from '@danidoble/webserial-jofemar';

import type {
  DeviceType,
  JofemarOptions,
  DispenseOptions,
  DispenseResult,
  MessageSerial,
  MessageSerialAdditional,
  JofemarConnectedPayload,
  JofemarWarningPayload,
  JofemarErrorPayload,
  JofemarDoorEventPayload,
  JofemarChannelStatusPayload,
  JofemarChannelsPayload,
  JofemarChannelsProgressPayload,
  JofemarDispensingWithdrawalPayload,
  JofemarResetErrorsPayload,
  SerialPortFilter,
  SerialDeviceOptions,
  SerialProvider,
} from '@danidoble/webserial-jofemar';

License

GPL-3.0-only © Danidoble