@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 buildLaboratorio 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 demoAbre 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:previewLos 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.hexsoftwareReset() 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 combinadosautoChange()implementa3000y devuelve solicitado, entregado y faltante.autoCashlessVend()implementa3001y 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.
