{"_id":"@danidoble/webserial-boardroid-v3","name":"@danidoble/webserial-boardroid-v3","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@danidoble/webserial-boardroid-v3","type":"module","version":"1.0.0","description":"Typed Web Serial client for the Boardroid MDB firmware v3 binary protocol.","author":"Danidoble <ddanidoble@gmail.com>","license":"GPL-3.0-only","homepage":"https://github.com/danidoble/boardroid#readme","repository":{"type":"git","url":"git+https://github.com/danidoble/boardroid.git","directory":"tools/webserial-boardroid"},"bugs":{"url":"https://github.com/danidoble/boardroid/issues"},"exports":{".":{"import":"./dist/index.mjs","require":"./dist/index.cjs"},"./package.json":"./package.json"},"devDependencies":{"@eslint/js":"^10.0.1","@typescript/native-preview":"7.0.0-dev.20260328.1","bumpp":"^11.1.0","eslint":"^10.10.0","globals":"^17.12.0","prettier":"3.8.3","tsdown":"^0.21.10","typescript":"^6.0.3","typescript-eslint":"^8.70.0","vite":"^8.2.2","vitest":"^4.1.11","webserial-core":"^2.1.0"},"peerDependencies":{"webserial-core":"^2.0.3"},"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.cts","scripts":{"build":"tsdown","dev":"prettier --write ./src/**/*.ts && tsdown --watch","demo":"vite --config vite.demo.config.ts --host 127.0.0.1","demo:build":"vite build --config vite.demo.config.ts","demo:preview":"vite preview --config vite.demo.config.ts --host 127.0.0.1","test":"vitest","typecheck":"tsc --noEmit","release":"bumpp","lint":"eslint ./src/**/*.ts ./tests/**/*.ts ./demo/**/*.ts ./vite.demo.config.ts","format":"prettier --write ./src/ ./tests/ ./demo/ ./vite.demo.config.ts ./README.md ./package.json"},"_nodeVersion":"24.19.0","_id":"@danidoble/webserial-boardroid-v3@1.0.0","dist":{"integrity":"sha512-3oDbc7YMun965sojlXjMq/HQaXstfS1yjG2YrlJkjKiHTG271nNvwsW6IR5rtdqrzqMi3Dsym9XOn3vEMCPBrQ==","shasum":"06b34061f80465e2a3fe67773732fc90a31b3d09","tarball":"https://registry.npmjs.org/@danidoble/webserial-boardroid-v3/-/webserial-boardroid-v3-1.0.0.tgz","fileCount":7,"unpackedSize":140411,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQC+g+66YMNhozKMZ8MmXiGB5XFksHEjxrKJ/3VREXh2vwIgbJoMyaw6QXmARFQCoBPCZAqGJ3FesFgZ7B2RJ4xsiYc="}]},"_npmUser":{"name":"danidoble","email":"ddanidoble@gmail.com"},"directories":{},"maintainers":[{"name":"danidoble","email":"ddanidoble@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/webserial-boardroid-v3_1.0.0_1789056046583_0.256760776464106"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-10T16:00:46.384Z","1.0.0":"2026-09-10T16:00:46.713Z","modified":"2026-09-10T16:00:46.968Z"},"maintainers":[{"name":"danidoble","email":"ddanidoble@gmail.com"}],"description":"Typed Web Serial client for the Boardroid MDB firmware v3 binary protocol.","homepage":"https://github.com/danidoble/boardroid#readme","repository":{"type":"git","url":"git+https://github.com/danidoble/boardroid.git","directory":"tools/webserial-boardroid"},"author":"Danidoble <ddanidoble@gmail.com>","bugs":{"url":"https://github.com/danidoble/boardroid/issues"},"license":"GPL-3.0-only","readme":"# webserial-boardroid\n\nCliente TypeScript para Boardroid MDB firmware v3, construido sobre\n`webserial-core` v2. Implementa framing `7E`, escaping `7D`, CRC-16/CCITT-FALSE,\ncorrelación por `sequence`, comandos avanzados y helpers autónomos.\n\n## Instalación y uso\n\n```sh\npnpm install\npnpm test\npnpm typecheck\npnpm build\n```\n\n## Laboratorio Vite en el navegador\n\nEl directorio `demo/` contiene una página local para probar la API completa sin\ncrear otra aplicación. Incluye conexión/desconexión, estado del puerto, todos\nlos helpers tipados y un registro de tramas TX/RX, eventos y resultados.\n\n```sh\npnpm install\npnpm demo\n```\n\nAbre `http://127.0.0.1:5173`, pulsa **Conectar** y selecciona el puerto USB de\nBoardroid. Web Serial requiere Chrome o Edge y un contexto seguro; `localhost`\ny `127.0.0.1` se consideran seguros. La selección del puerto debe originarse en\nel clic del usuario, por eso el demo no intenta conectarse automáticamente.\n\nPara validar o servir el bundle de producción:\n\n```sh\npnpm demo:build\npnpm demo:preview\n```\n\nLos importes del formulario se capturan como enteros en la unidad mínima de la\nmoneda. Por ejemplo, con `decimalPlaces = 2`, el valor `5000` equivale a\n`$50.00`. Los campos de máscaras y moneda aceptan decimal o hexadecimal con\nprefijo `0x`.\n\n```ts\nimport { Boardroid } from '@danidoble/webserial-boardroid-v3';\n\nconst board = new Boardroid(); // Web Serial nativo, 115200 8N1\nboard.on('boardroid:event', frame => console.log('evento', frame));\n\nawait board.connect(); // debe ejecutarse desde un click del usuario\nconsole.log(await board.getInfo());\n\nconst change = await board.autoChange({\n  currencyCode: 0x1484,\n  decimalPlaces: 2,\n  amount: 20_000 // MXN $200.00\n});\nconsole.log(change.delivered, change.undelivered);\n\nconst sale = await board.autoCashlessVend({\n  reader: 0xff, // autodetectar reader 1/2\n  item: 1,\n  price: 5_000 // $50.00 en unidades negociadas\n});\nconsole.log(sale.approved, sale.dispensedReported);\n\nawait board.disconnect();\n```\n\n## API tipada de comandos\n\nLa librería tiene un método tipado para cada comando del firmware. `request()`\npermanece como escape de bajo nivel para diagnóstico o extensiones futuras; una\naplicación normal no necesita construir payloads manualmente.\n\n### Sistema e I/O\n\n| Método                      | Comando | Resultado                              |\n| --------------------------- | ------: | -------------------------------------- |\n| `getInfo()`                 |  `0001` | versión y protocolo interpretados      |\n| `getHealth()`               |  `0002` | contadores, entradas y estado coin     |\n| `resetMdbBus()`             |  `0003` | ejecuta el reset global MDB            |\n| `getDeviceInfo(device)`     |  `0004` | unión tipada coin/bill/cashless        |\n| `getMdbDiagnostics(device)` |  `0005` | último intercambio MDB crudo           |\n| `getDeviceDenominations()`  |  `0006` | valores monetarios detectados          |\n| `enterBootloader()`         |  `0007` | entra al bootloader y libera puerto    |\n| `softwareReset()`           |  `0008` | reinicia la aplicación y libera puerto |\n| `getIoState()`              |  `2000` | puerta y botón PROGRAM                 |\n| `pulseBuzzer(durationMs)`   |  `2001` | pulso de 0 a 5000 ms                   |\n\n```ts\nimport { BoardroidDevice } from '@danidoble/webserial-boardroid-v3';\n\nconst health = await board.getHealth();\nconst coin = await board.getDeviceInfo(BoardroidDevice.Coin);\nconst inputs = await board.getIoState();\nawait board.pulseBuzzer(200);\n```\n\n`enterBootloader()` envía la autorización exacta `42 4F 4F 54 01`, exige una\nrespuesta `OK` y desconecta Web Serial —lo que también detiene la reconexión—\npara que el actualizador obtenga propiedad exclusiva del puerto. Después del reset\nla placa ya no habla Host v3: usa el protocolo boot v1. La librería no mezcla\nambos parsers ni escribe páginas de firmware automáticamente; para actualizar,\nejecuta desde la raíz del repositorio del firmware la CLI probada:\n\n```sh\ntools/boardroid firmware update build/production/boardroid-mdb.hex\n```\n\n`softwareReset()` envía un request vacío y exige una respuesta `OK`. El firmware\nresponde `BUSY` mientras haya una operación autónoma o un intercambio MDB en\ncurso, y `BAD_REQUEST` si llega con payload. Tras `OK`, vacía la USART1 y\nprovoca un reset por watchdog: la placa arranca otra vez en la **aplicación**\n(no en el bootloader), de modo que el descubrimiento MDB se reinicia desde\ncero. La librería libera el puerto tras la respuesta y deja que\n`autoReconnect` re-handshake contra la misma placa cuando vuelva a publicar\nHost v3 —útil para reaplicar configuración sin desconectar el cable USB.\n\n### Coin changer\n\nLa API simple trabaja con los tipos detectados y devuelve dinero, no arreglos\nMDB de 16 posiciones:\n\n```ts\nawait board.enableCoin(); // acepta todas las monedas anunciadas\nawait board.enableCoin({ types: [0, 2] }); // sólo tipos detectados seleccionados\n\nconst coin = await board.getCoinInventory();\nconsole.log(coin.totalValue);\nfor (const item of coin.denominations) {\n  console.log(`${item.value} × ${item.count} = ${item.totalValue}`);\n}\n\nawait board.disableCoin();\n```\n\n`configureCoin()` y `getCoinTubeStatus()` continúan disponibles como API\navanzada para máscaras y respuestas crudas.\n\n| Método                | Parámetros principales              | Comando |\n| --------------------- | ----------------------------------- | ------: |\n| `configureCoin()`     | `acceptMask`, `manualDispenseMask?` |  `1000` |\n| `getCoinTubeStatus()` | ninguno                             |  `1001` |\n| `dispenseCoinType()`  | `type`, `count` (1..15)             |  `1002` |\n| `payoutCoinValue()`   | valor escalado (1..255)             |  `1003` |\n\n```ts\nawait board.configureCoin({ acceptMask: 0xffff, manualDispenseMask: 0 });\nconst tubes = await board.getCoinTubeStatus();\nconsole.log(tubes.fullMask, tubes.counts); // 16 conteos ya interpretados\nawait board.dispenseCoinType({ type: 2, count: 3 });\n```\n\n### Bill validator y recycler\n\n`enableBill()` deja escrow apagado por defecto: todo billete aceptado sigue\ndirectamente al stacker/recycler. Puede activarse para todos los tipos o para\nuna lista concreta. La lectura de inventario identifica automáticamente el\ntipo de dispositivo; el stacker de un validador no se cuenta como cambio.\n\n```ts\nawait board.enableBill();\nawait board.enableBill({ escrow: true });\nawait board.acceptBillEscrow(); // aceptar el retenido\nawait board.rejectBillEscrow(); // rechazar/devolver el retenido\n\nconst bill = await board.getBillInventory();\nconsole.log(bill.kind); // 'bill-validator' o 'bill-recycler'\nawait board.disableBill();\n```\n\n| Método                     | Parámetros principales                | Comando |\n| -------------------------- | ------------------------------------- | ------: |\n| `configureBill()`          | `acceptMask`, `escrowMask?`           |  `1100` |\n| `getBillStackerStatus()`   | ninguno                               |  `1101` |\n| `resolveBillEscrow(stack)` | boolean                               |  `1102` |\n| `configureRecycler()`      | `manualDispenseMask?`, 16 `typeModes` |  `1103` |\n| `getRecyclerStatus()`      | ninguno                               |  `1104` |\n| `dispenseRecyclerType()`   | `type`, `count`                       |  `1105` |\n| `payoutRecyclerValue()`    | valor escalado                        |  `1106` |\n| `cancelRecyclerPayout()`   | ninguno                               |  `1107` |\n| `setBillSecurity()`        | máscara de alta seguridad             |  `1108` |\n\n```ts\nawait board.configureBill({ acceptMask: 0xffff, escrowMask: 0 });\nconst stacker = await board.getBillStackerStatus();\nconst recycler = await board.getRecyclerStatus();\nconsole.log(stacker.count, recycler.counts);\n\nawait board.configureRecycler({\n  manualDispenseMask: 0,\n  typeModes: Array(16).fill(3)\n});\nawait board.dispenseRecyclerType({ type: 2, count: 3 });\n```\n\n### Cashless avanzado\n\n`reader` siempre es `0` para cashless #1 o `1` para cashless #2.\n\n| Método                       | Parámetros principales                   | Comando |\n| ---------------------------- | ---------------------------------------- | ------: |\n| `configureCashless()`        | `reader`, `maximumPrice`, `minimumPrice` |  `1200` |\n| `enableCashless()`           | reader                                   |  `1201` |\n| `disableCashless()`          | reader                                   |  `1202` |\n| `cancelCashless()`           | reader                                   |  `1203` |\n| `requestCashlessVend()`      | `reader`, `price`, `item`                |  `1204` |\n| `reportCashlessVendResult()` | `reader`, `success`, `item`              |  `1205` |\n| `completeCashlessSession()`  | reader                                   |  `1206` |\n| `reportCashlessCashSale()`   | `reader`, `price`, `item`, `mixedFlags?` |  `1207` |\n| `requestCashlessRevalue()`   | `reader`, `value`                        |  `1208` |\n| `getCashlessRevalueLimit()`  | reader                                   |  `1209` |\n\n```ts\nawait board.configureCashless({\n  reader: 0,\n  maximumPrice: 10_000,\n  minimumPrice: 100\n});\nawait board.enableCashless(0);\n\nconst decision = await board.requestCashlessVend({\n  reader: 0,\n  price: 5_000,\n  item: 42\n});\nif (decision.approved) {\n  const productWasDispensed = true; // resultado del mecanismo externo\n  await board.reportCashlessVendResult({\n    reader: 0,\n    success: productWasDispensed,\n    item: 42\n  });\n}\nawait board.completeCashlessSession(0);\n```\n\n### Operaciones autónomas\n\nPara consultar todo el cambio entregable, combinando tubos y recycler por\ndenominación:\n\n```ts\nconst available = await board.getAvailableChange();\nconsole.log(available.totalValue, available.coinMinorValue, available.billMinorValue);\nconsole.table(available.denominations); // valor, cantidad y subtotal combinados\n```\n\n- `autoChange()` implementa `3000` y devuelve solicitado, entregado y faltante.\n- `autoCashlessVend()` implementa `3001` y devuelve aprobación y cierre del flujo.\n\nTodos los métodos MDB avanzados esperan tanto el `ACCEPTED` inmediato como el\nevento terminal `8001` correlacionado. Devuelven `MdbOperationResult` cuando no\nhay una respuesta más específica. Un ACK de payout significa que el periférico\naceptó la orden; para confirmar entrega física usa `getCoinTubeStatus()`,\n`getRecyclerStatus()` o el comando autónomo de cambio.\n\nEl timeout terminal predeterminado es 60 segundos y puede configurarse con\n`new Boardroid({ operationTimeout: 90_000 })` o sobrescribirse en el último\nargumento de cada método avanzado.\n\n### Acceso de bajo nivel\n\nLa respuesta de `request(command, payload?)` sólo se resuelve cuando coinciden\n`sequence` y `command`; los eventos espontáneos no satisfacen una petición.\n\n```ts\nimport { Boardroid, BoardroidCommand, BoardroidStatus } from '@danidoble/webserial-boardroid-v3';\n\nconst frame = await board.request(BoardroidCommand.DeviceInfo, Uint8Array.of(0));\nif (frame.payload[0] !== BoardroidStatus.Ok) throw new Error('coin no disponible');\n```\n\nEventos específicos:\n\n- `boardroid:frame`: toda trama v3 válida;\n- `boardroid:response`: respuestas correlacionables;\n- `boardroid:event`: actividad MDB y resultados terminales.\n\nAdemás se emiten eventos semánticos, conservando los crudos para diagnóstico:\n\n```ts\nboard.on('door:status', ({ open }) => console.log(open ? 'abierta' : 'cerrada'));\nboard.on('program-button:status', ({ pressed }) => console.log({ pressed }));\nboard.on('coin:deposit', event => console.log(event.routing, event.denomination));\nboard.on('coin:dispensed', event => console.log(event.count, event.remaining));\nboard.on('coin:status', event => console.log(event.name, event.severity));\nboard.on('bill:routed', event => console.log(event.routing, event.denomination));\nboard.on('bill:status', event => console.log(event.name, event.severity));\nboard.on('cashless:status', event => console.log(event.name, event.approvedAmount));\n```\n\nTambién se exportan `encodeFrame`, `decodeFrame`, `boardroidParser`, enums y\ntipos. El parser conserva fragmentos entre chunks, acepta varias tramas por\nchunk, valida tamaño/CRC y se resincroniza en el siguiente `7E`.\n\n## Providers\n\nSin `provider` se usa Web Serial nativo. Para los adapters públicos de\n`webserial-core`:\n\n```ts\nimport { Boardroid, WebUsbProvider, createWebSocketProvider } from '@danidoble/webserial-boardroid-v3';\n\nconst usb = new Boardroid({ provider: new WebUsbProvider() });\nconst remote = new Boardroid({\n  provider: createWebSocketProvider('wss://bridge.example')\n});\n```\n\nWebUSB depende del convertidor USB/UART de la placa y de sus interfaces; debe\nprobarse con el hardware real. Un bridge WebSocket de producción requiere TLS,\nautenticación, validación de origen y control exclusivo del puerto.\n\nLa documentación completa del wire protocol está en\n[`../../docs/02-framing-and-crc.md`](../../docs/02-framing-and-crc.md) y los\ncomandos autónomos en\n[`../../docs/07-autonomous-commands.md`](../../docs/07-autonomous-commands.md).\n","readmeFilename":"","_rev":"1-3cc340f0d8a83353adb1ee120e9fc500"}