Complete connection and simulation example
This walkthrough runs from your own application with the public package installed. You do not need the private source repository, build tools or publishing permissions. If needed, follow installation first.
The installed jsd-example command runs a complete application: connection, readiness, event listeners, commands, errors and cleanup. You can then copy its full script and adapt it to your application.
1. Run without hardware
Use Node.js 20.6+ for the simulator. Choose your package manager:
npx --no-install jsd-example simulatepnpm exec jsd-example simulatebun run jsd-example simulateyarn jsd-example simulateAll four commands execute Node. The simulator uses a virtual port and never opens a physical device. It queries status, version, identification, temperature, license, seed, MDB state and paginated logs, then performs a simulated dispense. It closes and exits automatically.
Expected output includes:
SIMULATION: no physical port will be opened.
[serial:status] SCANNING
[serial:status] CONNECTING
[serial:status] CONNECTED
[serial:connected] { path: 'JSD-SIMULATOR', baudRate: 9600 }
...
[vision:dispense-status] ... status: 'COLLECTED' ...
[result] { delivered: true }
Simulation finished.
[serial:disconnected] ManualAutomatic status queries for machines 1–4 can interleave with command responses. A shutdown:notice can indicate a final ACK canceled by closing; a physical close failure is reported separately and sets a failure exit code.
2. Explore commands
Start an interactive virtual session:
npx --no-install jsd-example simulate --interactiveFor other package managers, replace their simulate command from step 1 with simulate --interactive.
Type help, status, version, logs, channels, dispense 1, then quit.
| Command | What happens |
|---|---|
| help | List supported commands |
| status | Query the selected machine and filter its response by number |
| version | Request the JSD software version |
| id | Request machine identity |
| temperature | Request temperature readings |
| license | Request temporary/permanent license status |
| seed | Print seed bytes and Base64; does not activate a license |
| mdb | Query card-holder 1 status; does not execute a payment |
| logs | Read pages until END_LOG |
| channels | Verify all 80 selections and show progress |
| dispense 1 | Dispense selection 1 when explicitly enabled |
| quit | Await disconnection and release the client |
The console accepts a fixed command list. It never evaluates arbitrary JavaScript.
3. Observe a dispense failure
JSD_SIM_SCENARIO=failure npx --no-install jsd-example simulate$env:JSD_SIM_SCENARIO = "failure"
npx --no-install jsd-example simulate
Remove-Item Env:JSD_SIM_SCENARIOThis emits FAILED-DISPENSE and returns { delivered: false }. A device-reported failure is different from an exception or a lost connection.
4. Connect real hardware
List ports without opening them:
npx --no-install jsd-example portsThen configure the path and baud rate for your device:
JSD_PORT=/dev/ttyUSB0 JSD_BAUD_RATE=9600 npx --no-install jsd-example connectJSD_PORT=/dev/cu.usbserial-YOUR_DEVICE JSD_BAUD_RATE=9600 npx --no-install jsd-example connect$env:JSD_PORT = "COM3"
$env:JSD_BAUD_RATE = "9600"
npx --no-install jsd-example connectFor pnpm, use pnpm exec jsd-example connect; for Bun, bun run jsd-example connect; for Yarn, yarn jsd-example connect. Set environment variables as shown above.
Real hardware starts read-only. Try status, version, id and temperature. Serial readiness is separate from machine availability.
To use USB discovery, unset JSD_PORT and supply selectors instead:
JSD_VENDOR_ID=0403 JSD_PRODUCT_ID=6001 npx --no-install jsd-example connectEnvironment settings
| Variable | Default | Meaning |
|---|---|---|
| JSD_PORT | none | Explicit path; otherwise supply a USB selector |
| JSD_VENDOR_ID | none | Exact USB vendor selector |
| JSD_PRODUCT_ID | none | Exact USB product selector |
| JSD_SERIAL_NUMBER | none | Exact unit serial number |
| JSD_BAUD_RATE | 9600 | Positive integer serial speed |
| JSD_MACHINE | 1 | Machine 1–4 |
| JSD_DEBUG | 0 | Set 1 to print parsed frames and aliases |
| JSD_ALLOW_DISPENSE | 0 for hardware | Set 1 to enable deliberate dispensing |
| JSD_SIM_SCENARIO | success | Simulation only: success or failure |
For deliberate hardware dispensing on Linux/macOS:
JSD_PORT=/dev/ttyUSB0 JSD_ALLOW_DISPENSE=1 npx --no-install jsd-example connectIn PowerShell set $env:JSD_ALLOW_DISPENSE = "1" before connecting. You must still type dispense <1-80>. The script refreshes machine status and checks availability first. The simulator opts in automatically; use 0 to practice with dispensing disabled.
5. Adapt the example to your application
The script below is standalone: save it as connect.mjs in your application and execute node connect.mjs with the same environment variables. It imports only the installed package and Node's standard library.
The package contains a copy at node_modules/@danidoble/serial-node-jsd/examples/connect.mjs. You can read or copy that file without access to the private library repository. The virtual-device files are included too; hardware mode does not load them.
The important sequence is:
- Create
JSDwithautoConnect: falseand register listeners first. - Subscribe to
serial:connectedbeforestart(), which returns void. - Read callback payloads directly; there is no browser
event.detail. - Subscribe before submitting a command. A resolved submission promise is not its device response.
- Bound waits: the example allows 5 seconds for queries and 60 for aggregate operations. A timeout does not undo a physical operation; the script retires the session after a timed-out interactive command.
- Close with
quit, Ctrl+C, SIGTERM or EOF. Always awaitstop()and remove listeners your application owns.
Complete application script
The JSDoc types give IDE completion while keeping the script directly runnable as JavaScript.
/**
* Complete Node.js starter. Uses only the public package API.
* Copy to your application as connect.mjs, then set JSD_PORT and run node connect.mjs.
* Installed simulation: npx --no-install jsd-example simulate
*/
import { once } from 'node:events';
import { createInterface } from 'node:readline/promises';
import { stdin, stdout } from 'node:process';
import { JSD, SerialStatus } from '@danidoble/serial-node-jsd';
/** @typedef {import('@danidoble/serial-node-jsd').JSDEvents} JSDEvents */
/** @typedef {import('@danidoble/serial-node-jsd').MachineValid} MachineValid */
const demo = process.argv.includes('--demo');
const simulation = process.env.JSD_SIMULATION === '1' && process.env.JSD_PORT === 'JSD-SIMULATOR';
const allowDispense = process.env.JSD_ALLOW_DISPENSE === '1';
const timeout = 5_000;
const operationTimeout = 60_000;
/** @param {string} label @param {string | undefined} value @param {number} fallback */
function positiveInteger(label, value, fallback) {
const number = value === undefined ? fallback : Number(value);
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`${label} must be a positive integer`);
return number;
}
// Listing never opens a connection, and works without setting JSD_PORT.
if (process.argv.includes('--list')) {
try {
console.table(await JSD.listPorts());
} catch (error) {
console.error(error);
process.exitCode = 1;
}
} else {
try {
await main();
} catch (error) {
console.error('[fatal]', error instanceof Error ? error.message : error);
process.exitCode = 1;
}
}
async function main() {
if (demo && !simulation) throw new Error('--demo is reserved for the virtual-port example. Use jsd-example simulate.');
if (!process.env.JSD_PORT && !process.env.JSD_VENDOR_ID && !process.env.JSD_PRODUCT_ID && !process.env.JSD_SERIAL_NUMBER) {
throw new Error('Set JSD_PORT (e.g. /dev/ttyUSB0 or COM3), or a USB selector. Use --list to discover ports.');
}
const machineNumber = positiveInteger('JSD_MACHINE', process.env.JSD_MACHINE, 1);
if (machineNumber > 4) throw new Error('JSD_MACHINE must be between 1 and 4');
const machine = /** @type {MachineValid} */ (machineNumber);
/** @type {import('@danidoble/serial-node-jsd').JSDConfig} */
const config = {
path: process.env.JSD_PORT || undefined,
vendorId: process.env.JSD_VENDOR_ID || undefined,
productId: process.env.JSD_PRODUCT_ID || undefined,
serialNumber: process.env.JSD_SERIAL_NUMBER || undefined,
baudRate: positiveInteger('JSD_BAUD_RATE', process.env.JSD_BAUD_RATE, 9600),
autoConnect: false, // Attach listeners first. start() returns void, not readiness.
autoReconnect: true,
reconnectInterval: 1_000,
reconnectBackoffFactor: 2,
maxReconnectInterval: 5_000,
maxReconnectAttempts: 3,
writeTimeout: 3_000
};
const jsd = new JSD(config, { transport: 'rs232' });
/** @type {ReturnType<typeof createInterface> | undefined} */
let terminal;
let stopping = false;
const lifetime = new AbortController();
/** @type {Promise<void> | undefined} */
let closePromise;
// Event callbacks receive payloads directly. There is no browser event.detail.
jsd.on('serial:status', status => console.log('[serial:status]', status));
jsd.on('serial:connected', info => console.log('[serial:connected]', info));
jsd.on('serial:disconnected', reason => console.log('[serial:disconnected]', reason));
jsd.on('serial:reconnecting', info => console.warn('[serial:reconnecting]', info));
jsd.on('serial:reconnectFailed', info => {
console.error('[serial:reconnectFailed]', info);
process.exitCode = 1;
void shutdown();
});
jsd.on('serial:error', error => {
// stop() may cancel an ACK still draining. Closure failures are handled below.
if (stopping) console.log('[shutdown:notice]', error.message);
else console.error('[serial:error]', error.message);
});
jsd.on('serial:jsd-error', message => console.error('[serial:jsd-error]', message));
jsd.on('serial:jsd-nack', frame => console.warn('[serial:jsd-nack]', frame));
if (process.env.JSD_DEBUG === '1') {
jsd.on('serial:message', message => console.log('[serial:message]', message.alias, message.parsed));
}
jsd.on('vision:machine-status', data => console.log('[vision:machine-status]', data));
jsd.on('vision:door', data => console.log('[vision:door]', data));
jsd.on('vision:connection', data => console.log('[vision:connection]', data));
jsd.on('vision:machine-id', data => console.log('[vision:machine-id]', data));
jsd.on('vision:jsd-version', data => console.log('[vision:jsd-version]', data));
jsd.on('vision:current-temperature', data => console.log('[vision:current-temperature]', data));
jsd.on('vision:dispensing', data => console.log('[vision:dispensing]', data));
jsd.on('vision:dispense-status', data => console.log('[vision:dispense-status]', data));
jsd.on('vision:waiting-collection', data => console.log('[vision:waiting-collection]', data));
jsd.on('vision:channels-progress', data => console.log('[vision:channels-progress]', data));
jsd.on('vision:wrong-cmd', data => console.error('[vision:wrong-cmd]', data));
jsd.on('vision:jsd-license-error', data => console.error('[vision:jsd-license-error]', data));
jsd.on('licensing:temporary-license-status', data => console.log('[licensing:temporary-license-status]', data));
jsd.on('licensing:identification', data => console.log('[licensing:identification]', data));
jsd.on('licensing:seed', data => console.log('[licensing:seed]', { base64: data.base64, bytes: data.bytes.length }));
jsd.on('manifest:log', data => console.log('[manifest:log]', data));
jsd.on('mdbCardHolder:condition-status', data => console.log('[mdbCardHolder:condition-status]', data));
// An application deadline stops this example's wait. It does not undo a vend.
// If a command result is ambiguous, reconcile the device before trying again.
/** @template T @param {Promise<T>} operation @returns {Promise<T>} */
function bounded(operation) {
/** @type {ReturnType<typeof setTimeout> | undefined} */
let timer;
/** @type {Promise<never>} */
const deadline = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error('Application deadline exceeded; outcome may be unknown')), operationTimeout);
});
return Promise.race([operation, deadline]).finally(() => clearTimeout(timer));
}
/**
* Subscribe BEFORE sending. A resolved Promise<void> only confirms submission.
* @template {keyof JSDEvents} K
* @param {K} event
* @param {() => Promise<void> | void} send
* @returns {Promise<JSDEvents[K][0]>}
*/
async function request(event, send) {
const abort = new AbortController();
const onStop = () => abort.abort(lifetime.signal.reason);
lifetime.signal.addEventListener('abort', onStop, { once: true });
if (lifetime.signal.aborted) onStop();
const timer = setTimeout(() => abort.abort(new Error(`Timed out waiting for ${event}`)), timeout);
const response = once(jsd, event, { signal: abort.signal });
try {
const [args] = await Promise.all([response, Promise.resolve().then(send)]);
return args[0];
} finally {
clearTimeout(timer);
lifetime.signal.removeEventListener('abort', onStop);
abort.abort();
}
}
// Filter by machine: automatic startup queries can produce events for 1–4.
function status() {
/** @type {Promise<JSDEvents['vision:machine-status'][0]>} */
const result = new Promise((resolve, reject) => {
/** @param {JSDEvents['vision:machine-status'][0]} data */
const onStatus = data => {
if (data.machine === machine) {
cleanup();
resolve(data);
}
};
const onLost = () => {
cleanup();
reject(new Error('Session lost while waiting for machine status'));
};
const timer = setTimeout(() => {
cleanup();
reject(new Error('Machine status deadline exceeded'));
}, timeout);
function cleanup() {
clearTimeout(timer);
jsd.off('vision:machine-status', onStatus);
jsd.off('serial:disconnected', onLost);
lifetime.signal.removeEventListener('abort', onLost);
}
jsd.on('vision:machine-status', onStatus);
jsd.once('serial:disconnected', onLost);
lifetime.signal.addEventListener('abort', onLost, { once: true });
void jsd.vision.requestMachineStatus({ machine }).catch(error => {
cleanup();
reject(error);
});
});
return result;
}
async function shutdown() {
if (closePromise) return closePromise;
stopping = true;
lifetime.abort(new Error('Example stopped'));
terminal?.close();
closePromise = jsd.stop().catch(error => {
console.error('[shutdown]', error);
process.exitCode = 1;
});
return closePromise;
}
const onSignal = () => {
void shutdown();
};
process.once('SIGINT', onSignal);
process.once('SIGTERM', onSignal);
try {
console.log(simulation ? 'SIMULATION: no physical port will be opened.' : 'REAL HARDWARE: read-only commands are enabled by default.');
await request('serial:connected', () => jsd.start());
if (stopping) return;
console.log('Serial session ready. Machine readiness is checked separately.');
/** @param {string} line */
async function execute(line) {
const [command = 'help', argument] = line.trim().split(/\s+/);
if (jsd.devicePortStatus() !== SerialStatus.CONNECTED && !['help', 'quit'].includes(command)) {
throw new Error('Serial session is not ready; wait for serial:connected');
}
switch (command) {
case 'status':
return status();
case 'version':
return request('vision:jsd-version', () => jsd.vision.requestJSDVersion());
case 'id':
return request('vision:machine-id', () => jsd.vision.requestMachineIdentification({ machine }));
case 'temperature':
return request('vision:current-temperature', () => jsd.vision.requestWorkingTemperature({ machine }));
case 'license':
return request('licensing:temporary-license-status', () => jsd.licensing.requestTemporaryLicenseStatus());
case 'seed':
return request('licensing:seed', () => jsd.licensing.requestSeedData());
case 'mdb':
return request('mdbCardHolder:condition-status', () => jsd.mdbCardHolder.requestStatus({ deviceNumber: 1 }));
case 'logs':
return bounded(jsd.manifest.requestLogs());
case 'channels':
return bounded(jsd.vision.assignChannels({ machine }));
case 'dispense': {
if (!allowDispense) throw new Error('Dispensing is disabled. Set JSD_ALLOW_DISPENSE=1 explicitly to enable it.');
if (argument === undefined) throw new Error('Usage: dispense <selection 1-80>');
const selection = positiveInteger('selection', argument, 1);
if (selection > 80) throw new Error('Selection must be between 1 and 80');
await status();
if (!jsd.vision.isAvailableToDispense({ machine })) throw new Error('Machine is not available to dispense');
const delivered = await bounded(
jsd.vision.dispense({ machine, selection: /** @type {import('@danidoble/serial-node-jsd').Range0To81} */ (selection) })
);
return { delivered };
}
case 'quit':
await shutdown();
return 'Stopped';
case 'help':
return 'status | version | id | temperature | license | seed | mdb | logs | channels | dispense <1-80> | quit';
default:
throw new Error(`Unknown command: ${command}. Type help.`);
}
}
if (demo) {
for (const command of ['status', 'version', 'id', 'temperature', 'license', 'seed', 'mdb', 'logs', 'dispense 1']) {
if (stopping) break;
console.log(`\n> ${command}`);
console.log('[result]', await execute(command));
}
console.log('Simulation finished.');
} else {
console.log(await execute('help'));
terminal = createInterface({ input: stdin, output: stdout, terminal: Boolean(stdin.isTTY) });
terminal.setPrompt('jsd> ');
terminal.prompt();
for await (const line of terminal) {
if (stopping) break;
try {
console.log('[result]', await execute(line));
} catch (error) {
console.error('[command]', error instanceof Error ? error.message : error);
// Abandoning a timed-out operation requires retiring its session.
if (error instanceof Error && /deadline|Timed out/.test(error.message)) {
await shutdown();
break;
}
}
if (stopping) break;
terminal.prompt();
}
}
} catch (error) {
if (!stopping) throw error;
} finally {
await shutdown();
process.off('SIGINT', onSignal);
process.off('SIGTERM', onSignal);
jsd.removeAllListeners(); // This script owns this client and all its listeners.
}
}Simulation scope
The example's process uses serialport's public SerialPortMock through a Node module loader. The actual JSD client, serial-core, delimiter parser, CRC, queues and event routing still execute. The virtual device answers with valid ACKs and application frames, rather than manually emitting client events.
It implements the commands listed above with synthetic data. It is not an entire firmware, a license-activation service or a payment simulator. Unsupported commands produce a wrong-command response. Physical behavior still needs validation with the target machine.
Continue with Vision operations, all events or error handling.