@danidoble/webserial-jsd
v1.0.0
Published
A strongly-typed, event-driven JSD Jofemar vending machine serial device driver built on top of webserial-core.
Readme
@danidoble/webserial-jsd
TypeScript driver for Jofemar JSD vending machines over serial port, built on top of webserial-core v2.
Handles serial connection, binary handshake, automatic reconnection and message routing — exposing only clean, typed events.
Not tied to a single transport: use the WebUSB, Web Bluetooth or WebSocket provider from webserial-core, or implement your own SerialProvider.
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-jsd webserial-core
# pnpm
pnpm add @danidoble/webserial-jsd webserial-core
# yarn
yarn add @danidoble/webserial-jsd webserial-core
# bun
bun add @danidoble/webserial-jsd webserial-core
webserial-coreis a peer dependency — it must be installed alongside this package.
Quick start
import { JSD } from '@danidoble/webserial-jsd';
const jsd = new JSD({ transport: 'rs232' });
jsd.on('vision:machine-status', ({ machine, status }) => {
console.log(`Machine ${machine}: ${status}`);
});
await jsd.connect();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 { jsd } from '@danidoble/webserial-jsd';
const jsd = new jsd({ filters: [{ usbVendorId: 0x2341 }] });
await jsd.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 { jsd, WebUsbProvider } from '@danidoble/webserial-jsd';
const jsd = new jsd({
filters: [{ usbVendorId: 0x2341 }],
provider: new WebUsbProvider()
});
await jsd.connect();Web Bluetooth (createBluetoothProvider)
Communicate over Bluetooth Low Energy using the Nordic UART Service (NUS). The device must expose NUS characteristics.
import { jsd, createBluetoothProvider } from '@danidoble/webserial-jsd';
const jsd = new jsd({
provider: createBluetoothProvider()
});
await jsd.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 { jsd, createWebSocketProvider } from '@danidoble/webserial-jsd';
const jsd = new jsd({
filters: [{ usbVendorId: 0x2341 }],
provider: createWebSocketProvider('ws://localhost:8080')
});
await jsd.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 { jsd } from '@danidoble/webserial-jsd';
AbstractSerialDevice.setProvider(new WebUsbProvider());
const jsd = new jsd({ filters: [{ usbVendorId: 0x2341 }] });
await jsd.connect();Custom provider
Implement the SerialProvider interface to target any platform:
import type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-jsd';
import { jsd } from '@danidoble/webserial-jsd';
const myProvider: SerialProvider = {
async requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort> {
// return a SerialPort-compatible object
},
async getPorts(): Promise<SerialPort[]> {
// return previously authorised ports
}
};
const jsd = new jsd({
filters: [{ usbVendorId: 0x2341 }],
provider: myProvider
});Constructor
new JSD(options?: JSDOptions)JSDOptions
| Property | Type | Default | Description |
| ----------------- | --------------------------------- | --------- | ------------------------------------------------------- |
| filters | SerialPortFilter[] | [] | Serial port filters to narrow the port picker. |
| transport | 'rs232' \| 'tcpip' | 'rs232' | Transport strategy. |
| provider | SerialProvider | — | Alternative provider (WebUSB, WebSocket, Bluetooth). |
| polyfillOptions | SerialDeviceOptions<Uint8Array> | — | Extra options that override the internal configuration. |
Connection API
Inherited from AbstractSerialDevice in webserial-core.
await jsd.connect(); // Opens the port and runs the handshake.
await jsd.disconnect(); // Closes the port.
await jsd.forget(); // Releases the port permission.
jsd.isConnected(); // boolean — true if the port is open.
jsd.isDisconnected(); // boolean — true if the port is closed.Sub-modules
jsd.vision
Vision machine management. Handles statuses, dispensing, channels and configuration.
Machine status
jsd.vision.getMachineStatus(machine: 1|2|3|4): MachineStatus | nullReturns the last known status of the machine.
jsd.vision.isAvailableToDispense({ machine?: 1|2|3|4 }): booleantrue if the machine is connected, in service, door closed and available to dispense.
await jsd.vision.requestMachineStatus({ machine?: 1|2|3|4 }): Promise<void>Requests the current machine status. Emits vision:machine-status.
Channels and selections
await jsd.vision.requestStatusChannel({ machine, selection }): Promise<void>Requests the status of a channel (selection 1-80 is mapped to tray/channel internally). Emits vision:channel-status.
await jsd.vision.assignChannels({ machine }): Promise<SelectionStatus[]>Requests the status of all 80 channels of a machine sequentially. Returns an array with each selection's status when complete. Blocks concurrent re-assignment.
await jsd.vision.requestStatusSelection({ selection }): Promise<void>Requests the status of a selection (1-240). Emits vision:status-selection.
await jsd.vision.requestChannelsLinkedToSelection({ selection }): Promise<void>Requests the channels linked to a selection. Emits vision:channels-linked-to-selection.
Dispensing
await jsd.vision.dispense({ machine, selection, speed?, timePostRun?, cart? }): Promise<boolean>Dispenses from a selection (1-81). cart groups the result with other cart commands. Resolves true on success, false on failure.
await jsd.vision.dispenseCart(data: { machine, selection }[]): Promise<{ selection, dispensed }[]>Dispenses multiple selections in batches of 10. Waits for machines to become available between batches. Emits vision:waiting-collection if a machine is waiting for collection.
await jsd.vision.dispenseFromChannel({ machine, tray, channel, speed, timePostRun, cart? }): Promise<boolean>Dispenses directly by tray and channel. Emits vision:dispense-status.
await jsd.vision.dispenseFromChannelExtended({
machine, tray, channel, speed, timePostRun,
fragileOrHeavy, typeAdjustElevator, timeAdjustElevator, cart?
}): Promise<boolean>Extended version with elevator adjustment and fragile/heavy product options. Emits vision:extended-dispense-status-data.
await jsd.vision.dispenseFromSelection({ selection, cart? }): Promise<boolean>Dispenses directly by JSD system selection number. Emits vision:dispense-status.
await jsd.vision.requestDispenseStatusFromChannel({ token }): Promise<void>
await jsd.vision.requestDispenseStatusFromSelection({ token }): Promise<void>
await jsd.vision.requestDispenseStatusFromChannelExtended({ token }): Promise<void>Query the status of an ongoing dispense by token.
Selection configuration
await jsd.vision.configureSelectionDispense({ selection, speed, timePostRun }): Promise<void>Configures the dispense parameters for a selection. Emits vision:selection-dispense-config.
await jsd.vision.requestSelectionDispenseConfig({ machine }): Promise<void>Requests the current dispense configuration of a machine's selections. Emits vision:selection-dispense-config.
await jsd.vision.configureAddChannelToSelection({ selection, machine, tray, channel }): Promise<void>Adds a channel to a selection.
Identification and version
await jsd.vision.requestMachineIdentification({ machine }): Promise<void>Requests the machine identifier. Emits vision:machine-id.
await jsd.vision.requestJSDVersion(): Promise<void>Requests the JSD firmware version. Emits vision:jsd-version.
Temperature
await jsd.vision.configureWorkingTemperature({ machine, temperature, enable }): Promise<void>
await jsd.vision.requestWorkingTemperature({ machine }): Promise<void>Configures or requests the working temperature. Emits vision:current-temperature.
Timings
await jsd.vision.configureWaitingTimings({ collectPosition, dispenseManoeuvres, afterPickup }): Promise<void>
await jsd.vision.requestWaitingTimings(): Promise<void>Configures or requests wait timings. Emits vision:time-waiting-for-product-collection.
await jsd.vision.configureTimeWaitingAfterPickup({ time }): Promise<void>
await jsd.vision.requestTimeWaitingAfterPickup(): Promise<void>Configures or requests the post-pickup wait time. Emits vision:time-waiting-after-product-collection.
Lights
await jsd.vision.lightsOn({ machine }): Promise<void>
await jsd.vision.lightsOff({ machine }): Promise<void>Faults and reset
await jsd.vision.requestReportActiveFaults({ machine }): Promise<void>Requests the active faults report. Emits vision:active-faults.
await jsd.vision.requestReportInactiveFaults({ machine }): Promise<void>Requests the inactive faults report. Emits vision:alarm-faults-events.
await jsd.vision.clearInactiveFaults({ machine }): Promise<void>
await jsd.vision.resetSoldOutChannels({ machine }): Promise<void>
await jsd.vision.resetFaultsAndSelfTest({ machine }): Promise<void>Clears inactive faults, resets sold-out channels or runs a self-test.
await jsd.vision.resetAllErrors({ machine }): Promise<void[]>Runs clearInactiveFaults, resetSoldOutChannels and resetFaultsAndSelfTest in parallel.
await jsd.vision.restartJSD(): Promise<void>⚠️ DANGEROUS — Fully restarts the JSD device. Emits vision:jsd-status-reset.
Collection
await jsd.vision.collect({ machine }): Promise<void>Starts the product collection cycle. Emits vision:collect.
jsd.manifest
Device manifest and log management.
await jsd.manifest.requestLogsEvent({ previous: boolean }): Promise<void>Requests log events. previous: true retrieves earlier logs.
await jsd.manifest.requestLogs(): Promise<string[]>Requests and returns all logs as a string array. Handles batches automatically. Throws if a request is already in progress.
await jsd.manifest.requestLogsByDate({ since: Date, until: Date }): Promise<void>Requests logs in a date range. Emits manifest:log.
await jsd.manifest.requestForSendingManifest({ fileSizeBytes: number, crc: string }): Promise<void>Prepares the device to receive a manifest file.
await jsd.manifest.sendManifestDataBlock({ prevBlockId: number, dataBlock: Uint8Array }): Promise<void>Sends a manifest data block. Emits manifest:block and manifest:completed when finished.
jsd.licensing
Device license management.
await jsd.licensing.requestFeatureStatus({ feature: number }): Promise<void>Queries the status of a licensed feature. Emits licensing:feature-status.
await jsd.licensing.requestTemporaryLicenseStatus(): Promise<void>Queries the temporary license status. Emits licensing:temporary-license-status.
await jsd.licensing.requestSeedData(): Promise<void>Requests the seed data needed to generate a license. Emits licensing:seed.
await jsd.licensing.requestLicenseActivation({ license: string }): Promise<void>Activates a license by passing the base64 string. Emits licensing:feature-status.
Additional getter
jsd.transport: string // 'rs232' | 'tcpip'Events
All events are listened to with jsd.on(event, handler).
Connection events (inherited from webserial-core)
| Event | Payload | When |
| ------------------------ | ---------------------- | --------------------------------- |
| serial:connecting | instance | connect() started |
| serial:connected | instance | Port open and handshake completed |
| serial:disconnected | instance | Port closed |
| serial:reconnecting | instance | Automatic reconnect cycle started |
| serial:data | Uint8Array, instance | Frame received from device |
| serial:sent | Uint8Array, instance | Bytes sent to device |
| serial:error | Error, instance | Read/write error |
| serial:need-permission | instance | User cancelled the port picker |
| serial:queue-empty | instance | Command queue empty |
| serial:timeout | Uint8Array, instance | Command timeout |
JSD protocol events
| Event | Payload | Description |
| ------------------ | ---------------------- | ------------------------------ |
| serial:jsd-data | VisionResponse | Parsed frame received |
| serial:jsd-ack | { packetId: number } | ACK received |
| serial:jsd-nack | { packetId: number } | NACK received |
| serial:jsd-error | { message: string } | Protocol error |
| serial:message | { message: string } | Informational protocol message |
Vision events
| Event | Payload | Description |
| -------------------------------------------------- | ------------------------------------------ | ----------------------------------- |
| vision:machine-status | MachineStatus & { machine } | Machine status updated |
| vision:channels | { machine, channels: SelectionStatus[] } | Status of all channels |
| vision:channels-progress | { machine, progress: number } | Channel assignment progress (0-100) |
| vision:channel-status | SelectionStatus & { machine } | Status of an individual channel |
| vision:status-selection | { selection, status } | Status of a selection |
| vision:dispense-status | DispenseToken & { dispensed: boolean } | Dispense result |
| vision:selection-dispense-config | { machine, config } | Selection dispense configuration |
| vision:channels-linked-to-selection | StatusChannelsLinkedToSelection | Channels linked to a selection |
| vision:machine-id | { machine, id: string } | Machine identifier |
| vision:current-temperature | { machine, temperature: string } | Current temperature |
| vision:alarm-faults-events | { machine, faults } | Alarms and faults (inactive) |
| vision:active-faults | { machine, faults } | Active faults |
| vision:active-faults-list | { machine, faults } | Active faults list |
| vision:time-waiting-for-product-collection | { collectPosition, dispenseManoeuvres } | Wait timings |
| vision:time-waiting-after-product-collection | { time } | Post-pickup wait time |
| vision:collect | { machine } | Collection cycle completed |
| vision:reset-sold-out-channels | { machine } | Sold-out channels reset |
| vision:jsd-version | { version: string } | Firmware version |
| vision:jsd-dispensing-queue | { queue } | JSD dispense queue |
| vision:special-characteristics-selection | { selection, characteristics } | Special selection characteristics |
| vision:perishable-products-config | { config } | Perishable products configuration |
| vision:jsd-status-reset | JsdResetStatus | JSD reset status |
| vision:extended-dispense-status-data | { token, data } | Extended dispense data |
| vision:trays-positioning-phototransistors-status | { machine, status } | Phototransistors status |
| vision:jsd-license-error | { error } | JSD license error |
| vision:dispensing | { machine, token } | Dispense in progress |
| vision:waiting-collection | { machine } | Machine waiting for collection |
| vision:door | { machine, open: boolean } | Door status |
| vision:connection | { machine, connected: boolean } | Connection with machine |
| vision:wrong-cmd | { opcode, params } | Wrong command received |
Manifest events
| Event | Payload | Description |
| -------------------- | --------------------- | ---------------------------- |
| manifest:log | { log: string } | Log entry received |
| manifest:block | { blockId: number } | Manifest block sent/received |
| manifest:completed | { logs: string[] } | Manifest transfer completed |
| manifest:wrong-cmd | { opcode, params } | Wrong command received |
Licensing events
| Event | Payload | Description |
| ------------------------------------ | -------------------------------------- | ------------------------------- |
| licensing:feature-status | { feature: number, active: boolean } | Licensed feature status |
| licensing:seed | { seed: string } | Seed data to generate a license |
| licensing:temporary-license-status | { active: boolean, remaining } | Temporary license status |
| licensing:wrong-cmd | { opcode, params } | Wrong command received |
Full example
import { JSD } from '@danidoble/webserial-jsd';
const jsd = new JSD({
transport: 'rs232',
filters: [{ usbVendorId: 0x0403 }] // FTDI
});
jsd.on('vision:machine-status', ({ machine, status, availabilityToDispense }) => {
console.log(`Machine ${machine}: ${status} / ${availabilityToDispense}`);
});
jsd.on('vision:dispense-status', ({ dispensed }) => {
console.log(dispensed ? 'Dispensed OK' : 'Dispense failed');
});
jsd.on('serial:error', err => console.error(err));
await jsd.connect();
if (jsd.vision.isAvailableToDispense({ machine: 1 })) {
const ok = await jsd.vision.dispense({ machine: 1, selection: 5 });
console.log('Result:', ok);
}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:
// Main class
import { JSD, WebUsbProvider, createBluetoothProvider, createWebSocketProvider } from '@danidoble/webserial-jsd';
export type {
JSDOptions,
SerialPortFilter,
SerialDeviceOptions,
SerialEventMap,
SerialParser,
SerialProvider,
SerialPolyfillOptions
} from '@danidoble/webserial-jsd';