Ejemplo completo de conexión y simulación
Este recorrido se ejecuta desde tu propia aplicación con el paquete público instalado. No necesitas el repositorio privado, herramientas de compilación ni permisos para publicar. Si hace falta, empieza por instalación.
El comando instalado jsd-example ejecuta una aplicación completa: conexión, disponibilidad, listeners, comandos, errores y limpieza. Después puedes copiar el script y adaptarlo a tu aplicación.
1. Ejecutar sin hardware
El simulador requiere Node.js 20.6+. Elige tu gestor:
npx --no-install jsd-example simulatepnpm exec jsd-example simulatebun run jsd-example simulateyarn jsd-example simulateLos cuatro comandos ejecutan Node. El simulador usa un puerto virtual y nunca abre equipos físicos. Consulta estado, versión, identificación, temperatura, licencia, semilla, estado MDB y registros paginados; después simula una entrega. Cierra y sale automáticamente.
La salida incluye:
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] ManualLas consultas automáticas de las máquinas 1–4 pueden intercalarse con las respuestas. Un shutdown:notice puede indicar que cerrar canceló el ACK final; un fallo de cierre físico se informa aparte y establece un código de salida de error.
2. Explorar comandos
Abre una sesión virtual interactiva:
npx --no-install jsd-example simulate --interactiveCon otros gestores, cambia su comando simulate del paso 1 por simulate --interactive.
Escribe help, status, version, logs, channels, dispense 1 y quit.
| Comando | Qué hace |
|---|---|
| help | Lista comandos admitidos |
| status | Consulta la máquina seleccionada y filtra por su número |
| version | Solicita la versión del software JSD |
| id | Solicita identificación de máquina |
| temperature | Solicita lecturas de temperatura |
| license | Consulta licencia temporal o permanente |
| seed | Imprime bytes y Base64 de semilla; no activa licencias |
| mdb | Consulta estado del lector 1; no ejecuta pagos |
| logs | Lee páginas hasta END_LOG |
| channels | Verifica las 80 selecciones y muestra progreso |
| dispense 1 | Dispensa la selección 1 si se habilitó explícitamente |
| quit | Espera la desconexión y libera el cliente |
La consola acepta una lista fija de comandos. Nunca evalúa JavaScript arbitrario.
3. Observar un dispensado fallido
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_SCENARIOEmite FAILED-DISPENSE y devuelve { delivered: false }. Un fallo informado por el equipo es distinto de una excepción o una desconexión.
4. Conectar hardware real
Enumera puertos sin abrirlos:
npx --no-install jsd-example portsDespués configura ruta y velocidad:
JSD_PORT=/dev/ttyUSB0 JSD_BAUD_RATE=9600 npx --no-install jsd-example connectJSD_PORT=/dev/cu.usbserial-TU_EQUIPO JSD_BAUD_RATE=9600 npx --no-install jsd-example connect$env:JSD_PORT = "COM3"
$env:JSD_BAUD_RATE = "9600"
npx --no-install jsd-example connectCon pnpm usa pnpm exec jsd-example connect; con Bun, bun run jsd-example connect; con Yarn, yarn jsd-example connect. Configura las variables como en los bloques anteriores.
El hardware real empieza solo en lectura. Prueba status, version, id y temperature. La disponibilidad serial y la de la máquina son distintas.
Para descubrimiento USB, elimina JSD_PORT y proporciona selectores:
JSD_VENDOR_ID=0403 JSD_PRODUCT_ID=6001 npx --no-install jsd-example connectVariables de entorno
| Variable | Predeterminado | Significado |
|---|---|---|
| JSD_PORT | ninguno | Ruta explícita; si falta, proporciona un selector USB |
| JSD_VENDOR_ID | ninguno | Fabricante USB exacto |
| JSD_PRODUCT_ID | ninguno | Producto USB exacto |
| JSD_SERIAL_NUMBER | ninguno | Serie exacta de la unidad |
| JSD_BAUD_RATE | 9600 | Velocidad serial entera positiva |
| JSD_MACHINE | 1 | Máquina 1–4 |
| JSD_DEBUG | 0 | Usa 1 para imprimir tramas analizadas y alias |
| JSD_ALLOW_DISPENSE | 0 en hardware | Usa 1 para habilitar dispensado deliberado |
| JSD_SIM_SCENARIO | success | Solo simulación: success o failure |
Para dispensado deliberado en Linux/macOS:
JSD_PORT=/dev/ttyUSB0 JSD_ALLOW_DISPENSE=1 npx --no-install jsd-example connectEn PowerShell configura $env:JSD_ALLOW_DISPENSE = "1" antes de conectar. Todavía debes escribir dispense <1-80>. El script vuelve a consultar el estado y verifica disponibilidad. El simulador lo habilita automáticamente; usa 0 para practicar con dispensado deshabilitado.
5. Adaptar el ejemplo a tu aplicación
El script siguiente es autónomo: guárdalo como connect.mjs en tu aplicación y ejecuta node connect.mjs con las mismas variables. Solo importa el paquete instalado y módulos estándar de Node.
El paquete incluye una copia en node_modules/@danidoble/serial-node-jsd/examples/connect.mjs. Puedes leerla o copiarla sin acceso al repositorio privado. Los archivos del equipo virtual también están incluidos; el modo físico no los carga.
La secuencia importante es:
- Crear
JSDconautoConnect: falsey registrar listeners primero. - Escuchar
serial:connectedantes destart(), que devuelve void. - Recibir datos directamente; no existe
event.detaildel navegador. - Suscribirse antes de enviar. Una promesa de encolado resuelta no equivale a la respuesta del equipo.
- Limitar esperas: el ejemplo da 5 segundos a consultas y 60 a operaciones agregadas. Un plazo no deshace una entrega; el script cierra la sesión después de un comando interactivo que supera el límite.
- Cerrar con
quit, Ctrl+C, SIGTERM o fin de entrada. Esperar siemprestop()y retirar los listeners que pertenecen a la aplicación.
Script completo de aplicación
Los tipos JSDoc permiten autocompletado del IDE y el archivo se ejecuta directamente como 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.
}
}Alcance de la simulación
El proceso del ejemplo usa la API pública SerialPortMock de serialport mediante un cargador Node. Siguen ejecutándose cliente JSD, serial-core, parser, CRC, colas y enrutamiento reales. El equipo virtual responde con ACKs y tramas válidas en lugar de emitir eventos del cliente manualmente.
Implementa los comandos indicados con datos inventados. No es un firmware completo, un servicio de activación de licencias ni un simulador de pagos. Los comandos no admitidos producen una respuesta de comando incorrecto. Valida el comportamiento físico con la máquina objetivo.
Continúa con operaciones Vision, todos los eventos o manejo de errores.