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-boardroid-v3

v1.0.0

Published

Typed Web Serial client for the Boardroid MDB firmware v3 binary protocol.

Downloads

186

Readme

webserial-boardroid

Cliente TypeScript para Boardroid MDB firmware v3, construido sobre webserial-core v2. Implementa framing 7E, escaping 7D, CRC-16/CCITT-FALSE, correlación por sequence, comandos avanzados y helpers autónomos.

Instalación y uso

pnpm install
pnpm test
pnpm typecheck
pnpm build

Laboratorio Vite en el navegador

El directorio demo/ contiene una página local para probar la API completa sin crear otra aplicación. Incluye conexión/desconexión, estado del puerto, todos los helpers tipados y un registro de tramas TX/RX, eventos y resultados.

pnpm install
pnpm demo

Abre http://127.0.0.1:5173, pulsa Conectar y selecciona el puerto USB de Boardroid. Web Serial requiere Chrome o Edge y un contexto seguro; localhost y 127.0.0.1 se consideran seguros. La selección del puerto debe originarse en el clic del usuario, por eso el demo no intenta conectarse automáticamente.

Para validar o servir el bundle de producción:

pnpm demo:build
pnpm demo:preview

Los importes del formulario se capturan como enteros en la unidad mínima de la moneda. Por ejemplo, con decimalPlaces = 2, el valor 5000 equivale a $50.00. Los campos de máscaras y moneda aceptan decimal o hexadecimal con prefijo 0x.

import { Boardroid } from '@danidoble/webserial-boardroid-v3';

const board = new Boardroid(); // Web Serial nativo, 115200 8N1
board.on('boardroid:event', frame => console.log('evento', frame));

await board.connect(); // debe ejecutarse desde un click del usuario
console.log(await board.getInfo());

const change = await board.autoChange({
  currencyCode: 0x1484,
  decimalPlaces: 2,
  amount: 20_000 // MXN $200.00
});
console.log(change.delivered, change.undelivered);

const sale = await board.autoCashlessVend({
  reader: 0xff, // autodetectar reader 1/2
  item: 1,
  price: 5_000 // $50.00 en unidades negociadas
});
console.log(sale.approved, sale.dispensedReported);

await board.disconnect();

API tipada de comandos

La librería tiene un método tipado para cada comando del firmware. request() permanece como escape de bajo nivel para diagnóstico o extensiones futuras; una aplicación normal no necesita construir payloads manualmente.

Sistema e I/O

| Método | Comando | Resultado | | --------------------------- | ------: | -------------------------------------- | | getInfo() | 0001 | versión y protocolo interpretados | | getHealth() | 0002 | contadores, entradas y estado coin | | resetMdbBus() | 0003 | ejecuta el reset global MDB | | getDeviceInfo(device) | 0004 | unión tipada coin/bill/cashless | | getMdbDiagnostics(device) | 0005 | último intercambio MDB crudo | | getDeviceDenominations() | 0006 | valores monetarios detectados | | enterBootloader() | 0007 | entra al bootloader y libera puerto | | softwareReset() | 0008 | reinicia la aplicación y libera puerto | | getIoState() | 2000 | puerta y botón PROGRAM | | pulseBuzzer(durationMs) | 2001 | pulso de 0 a 5000 ms |

import { BoardroidDevice } from '@danidoble/webserial-boardroid-v3';

const health = await board.getHealth();
const coin = await board.getDeviceInfo(BoardroidDevice.Coin);
const inputs = await board.getIoState();
await board.pulseBuzzer(200);

enterBootloader() envía la autorización exacta 42 4F 4F 54 01, exige una respuesta OK y desconecta Web Serial —lo que también detiene la reconexión— para que el actualizador obtenga propiedad exclusiva del puerto. Después del reset la placa ya no habla Host v3: usa el protocolo boot v1. La librería no mezcla ambos parsers ni escribe páginas de firmware automáticamente; para actualizar, ejecuta desde la raíz del repositorio del firmware la CLI probada:

tools/boardroid firmware update build/production/boardroid-mdb.hex

softwareReset() envía un request vacío y exige una respuesta OK. El firmware responde BUSY mientras haya una operación autónoma o un intercambio MDB en curso, y BAD_REQUEST si llega con payload. Tras OK, vacía la USART1 y provoca un reset por watchdog: la placa arranca otra vez en la aplicación (no en el bootloader), de modo que el descubrimiento MDB se reinicia desde cero. La librería libera el puerto tras la respuesta y deja que autoReconnect re-handshake contra la misma placa cuando vuelva a publicar Host v3 —útil para reaplicar configuración sin desconectar el cable USB.

Coin changer

La API simple trabaja con los tipos detectados y devuelve dinero, no arreglos MDB de 16 posiciones:

await board.enableCoin(); // acepta todas las monedas anunciadas
await board.enableCoin({ types: [0, 2] }); // sólo tipos detectados seleccionados

const coin = await board.getCoinInventory();
console.log(coin.totalValue);
for (const item of coin.denominations) {
  console.log(`${item.value} × ${item.count} = ${item.totalValue}`);
}

await board.disableCoin();

configureCoin() y getCoinTubeStatus() continúan disponibles como API avanzada para máscaras y respuestas crudas.

| Método | Parámetros principales | Comando | | --------------------- | ----------------------------------- | ------: | | configureCoin() | acceptMask, manualDispenseMask? | 1000 | | getCoinTubeStatus() | ninguno | 1001 | | dispenseCoinType() | type, count (1..15) | 1002 | | payoutCoinValue() | valor escalado (1..255) | 1003 |

await board.configureCoin({ acceptMask: 0xffff, manualDispenseMask: 0 });
const tubes = await board.getCoinTubeStatus();
console.log(tubes.fullMask, tubes.counts); // 16 conteos ya interpretados
await board.dispenseCoinType({ type: 2, count: 3 });

Bill validator y recycler

enableBill() deja escrow apagado por defecto: todo billete aceptado sigue directamente al stacker/recycler. Puede activarse para todos los tipos o para una lista concreta. La lectura de inventario identifica automáticamente el tipo de dispositivo; el stacker de un validador no se cuenta como cambio.

await board.enableBill();
await board.enableBill({ escrow: true });
await board.acceptBillEscrow(); // aceptar el retenido
await board.rejectBillEscrow(); // rechazar/devolver el retenido

const bill = await board.getBillInventory();
console.log(bill.kind); // 'bill-validator' o 'bill-recycler'
await board.disableBill();

| Método | Parámetros principales | Comando | | -------------------------- | ------------------------------------- | ------: | | configureBill() | acceptMask, escrowMask? | 1100 | | getBillStackerStatus() | ninguno | 1101 | | resolveBillEscrow(stack) | boolean | 1102 | | configureRecycler() | manualDispenseMask?, 16 typeModes | 1103 | | getRecyclerStatus() | ninguno | 1104 | | dispenseRecyclerType() | type, count | 1105 | | payoutRecyclerValue() | valor escalado | 1106 | | cancelRecyclerPayout() | ninguno | 1107 | | setBillSecurity() | máscara de alta seguridad | 1108 |

await board.configureBill({ acceptMask: 0xffff, escrowMask: 0 });
const stacker = await board.getBillStackerStatus();
const recycler = await board.getRecyclerStatus();
console.log(stacker.count, recycler.counts);

await board.configureRecycler({
  manualDispenseMask: 0,
  typeModes: Array(16).fill(3)
});
await board.dispenseRecyclerType({ type: 2, count: 3 });

Cashless avanzado

reader siempre es 0 para cashless #1 o 1 para cashless #2.

| Método | Parámetros principales | Comando | | ---------------------------- | ---------------------------------------- | ------: | | configureCashless() | reader, maximumPrice, minimumPrice | 1200 | | enableCashless() | reader | 1201 | | disableCashless() | reader | 1202 | | cancelCashless() | reader | 1203 | | requestCashlessVend() | reader, price, item | 1204 | | reportCashlessVendResult() | reader, success, item | 1205 | | completeCashlessSession() | reader | 1206 | | reportCashlessCashSale() | reader, price, item, mixedFlags? | 1207 | | requestCashlessRevalue() | reader, value | 1208 | | getCashlessRevalueLimit() | reader | 1209 |

await board.configureCashless({
  reader: 0,
  maximumPrice: 10_000,
  minimumPrice: 100
});
await board.enableCashless(0);

const decision = await board.requestCashlessVend({
  reader: 0,
  price: 5_000,
  item: 42
});
if (decision.approved) {
  const productWasDispensed = true; // resultado del mecanismo externo
  await board.reportCashlessVendResult({
    reader: 0,
    success: productWasDispensed,
    item: 42
  });
}
await board.completeCashlessSession(0);

Operaciones autónomas

Para consultar todo el cambio entregable, combinando tubos y recycler por denominación:

const available = await board.getAvailableChange();
console.log(available.totalValue, available.coinMinorValue, available.billMinorValue);
console.table(available.denominations); // valor, cantidad y subtotal combinados
  • autoChange() implementa 3000 y devuelve solicitado, entregado y faltante.
  • autoCashlessVend() implementa 3001 y devuelve aprobación y cierre del flujo.

Todos los métodos MDB avanzados esperan tanto el ACCEPTED inmediato como el evento terminal 8001 correlacionado. Devuelven MdbOperationResult cuando no hay una respuesta más específica. Un ACK de payout significa que el periférico aceptó la orden; para confirmar entrega física usa getCoinTubeStatus(), getRecyclerStatus() o el comando autónomo de cambio.

El timeout terminal predeterminado es 60 segundos y puede configurarse con new Boardroid({ operationTimeout: 90_000 }) o sobrescribirse en el último argumento de cada método avanzado.

Acceso de bajo nivel

La respuesta de request(command, payload?) sólo se resuelve cuando coinciden sequence y command; los eventos espontáneos no satisfacen una petición.

import { Boardroid, BoardroidCommand, BoardroidStatus } from '@danidoble/webserial-boardroid-v3';

const frame = await board.request(BoardroidCommand.DeviceInfo, Uint8Array.of(0));
if (frame.payload[0] !== BoardroidStatus.Ok) throw new Error('coin no disponible');

Eventos específicos:

  • boardroid:frame: toda trama v3 válida;
  • boardroid:response: respuestas correlacionables;
  • boardroid:event: actividad MDB y resultados terminales.

Además se emiten eventos semánticos, conservando los crudos para diagnóstico:

board.on('door:status', ({ open }) => console.log(open ? 'abierta' : 'cerrada'));
board.on('program-button:status', ({ pressed }) => console.log({ pressed }));
board.on('coin:deposit', event => console.log(event.routing, event.denomination));
board.on('coin:dispensed', event => console.log(event.count, event.remaining));
board.on('coin:status', event => console.log(event.name, event.severity));
board.on('bill:routed', event => console.log(event.routing, event.denomination));
board.on('bill:status', event => console.log(event.name, event.severity));
board.on('cashless:status', event => console.log(event.name, event.approvedAmount));

También se exportan encodeFrame, decodeFrame, boardroidParser, enums y tipos. El parser conserva fragmentos entre chunks, acepta varias tramas por chunk, valida tamaño/CRC y se resincroniza en el siguiente 7E.

Providers

Sin provider se usa Web Serial nativo. Para los adapters públicos de webserial-core:

import { Boardroid, WebUsbProvider, createWebSocketProvider } from '@danidoble/webserial-boardroid-v3';

const usb = new Boardroid({ provider: new WebUsbProvider() });
const remote = new Boardroid({
  provider: createWebSocketProvider('wss://bridge.example')
});

WebUSB depende del convertidor USB/UART de la placa y de sus interfaces; debe probarse con el hardware real. Un bridge WebSocket de producción requiere TLS, autenticación, validación de origen y control exclusivo del puerto.

La documentación completa del wire protocol está en ../../docs/02-framing-and-crc.md y los comandos autónomos en ../../docs/07-autonomous-commands.md.