{"_id":"@danidoble/webserial-locker","name":"@danidoble/webserial-locker","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@danidoble/webserial-locker","type":"module","version":"1.0.0","description":"A strongly-typed, event-driven USB locker driver for the Web Serial API, built on top of webserial-core.","author":{"name":"Danidoble","email":"ddanidoble@gmail.com"},"license":"GPL-3.0-only","homepage":"https://github.com/danidoble/webserial-locker#readme","repository":{"type":"git","url":"git+https://github.com/danidoble/webserial-locker.git"},"bugs":{"url":"https://github.com/danidoble/webserial-locker/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.0.1","eslint":"^10.2.0","globals":"^17.5.0","prettier":"3.8.3","tsdown":"^0.21.9","typescript":"^6.0.3","typescript-eslint":"^8.58.2","vitest":"^4.1.4","webserial-core":"^2.1.0"},"peerDependencies":{"webserial-core":"^2.1.0"},"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.cts","scripts":{"build":"tsdown","dev":"prettier --write ./src/**/*.ts && tsdown --watch","test":"vitest","typecheck":"tsc --noEmit","release":"bumpp","lint":"eslint ./src/**/*.ts ./tests/**/*.ts","format":"prettier --write ./src/ ./tests/ ./README.md ./package.json"},"_id":"@danidoble/webserial-locker@1.0.0","_integrity":"sha512-lrK/GeihbBSm5BJcyF4n9AiV3mZZqOxcvYLatBvRRE8dmtRRYT279tmO1lrZrs0LdPyuy9zdXBJFq41sTTVyBw==","_resolved":"/tmp/b3ba3be3833a4a420d96051136c01015/danidoble-webserial-locker-1.0.0.tgz","_from":"file:danidoble-webserial-locker-1.0.0.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-lrK/GeihbBSm5BJcyF4n9AiV3mZZqOxcvYLatBvRRE8dmtRRYT279tmO1lrZrs0LdPyuy9zdXBJFq41sTTVyBw==","shasum":"18fcb4d606bff283d2fbb04e60f0178b26313d78","tarball":"https://registry.npmjs.org/@danidoble/webserial-locker/-/webserial-locker-1.0.0.tgz","fileCount":7,"unpackedSize":72258,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCO/xM9ndJ0nJebofd8G3wrxHdfA9Usy/mnunYCRdNaVwIgEp27DTMbeIkCK4Zrf7fr9whpJT9pZK5bk93FDHY9CP4="}]},"_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-locker_1.0.0_1776991089912_0.8183589508389184"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-24T00:38:09.804Z","1.0.0":"2026-04-24T00:38:10.080Z","modified":"2026-04-24T00:38:10.328Z"},"maintainers":[{"name":"danidoble","email":"ddanidoble@gmail.com"}],"description":"A strongly-typed, event-driven USB locker driver for the Web Serial API, built on top of webserial-core.","homepage":"https://github.com/danidoble/webserial-locker#readme","repository":{"type":"git","url":"git+https://github.com/danidoble/webserial-locker.git"},"author":{"name":"Danidoble","email":"ddanidoble@gmail.com"},"bugs":{"url":"https://github.com/danidoble/webserial-locker/issues"},"license":"GPL-3.0-only","readme":"# @danidoble/webserial-locker\n\nA strongly-typed, event-driven USB locker driver built on top of [`webserial-core`](https://github.com/danidoble/webserial-core).\n\nHandles the serial connection, binary handshake, auto-reconnect, and message routing — so you only deal with clean, typed events.\n\nNot tied to a single transport: swap in the **WebUSB**, **Web Bluetooth**, or **WebSocket** provider from `webserial-core`, or implement your own `SerialProvider` for any platform.\n\n[![npm version](https://img.shields.io/npm/v/@danidoble/webserial-locker)](https://www.npmjs.com/package/@danidoble/webserial-locker)\n[![license](https://img.shields.io/npm/l/@danidoble/webserial-locker)](./LICENSE.md)\n\n---\n\n## Requirements\n\n- [`webserial-core`](https://www.npmjs.com/package/webserial-core) `^2.1.0` (peer dependency)\n- A compatible transport (see [Providers](#providers)):\n  - **Web Serial API** — Chrome / Edge 89+ (default, no extra setup)\n  - **WebUSB** — Chrome / Edge (via `WebUsbProvider`)\n  - **Web Bluetooth** — Chrome / Edge (via `createBluetoothProvider`, Nordic UART Service)\n  - **WebSocket** — any environment (via `createWebSocketProvider` + a bridge server)\n  - **Custom** — any platform via your own `SerialProvider` implementation\n\n---\n\n## Installation\n\n```bash\n# npm\nnpm install @danidoble/webserial-locker webserial-core\n\n# pnpm\npnpm add @danidoble/webserial-locker webserial-core\n\n# yarn\nyarn add @danidoble/webserial-locker webserial-core\n\n# bun\nbun add @danidoble/webserial-locker webserial-core\n```\n\n> `webserial-core` is a **peer dependency** — it must be installed alongside this package.\n\n---\n\n## Quick start\n\n```ts\nimport { Locker } from '@danidoble/webserial-locker';\n\nconst locker = new Locker({ channel: 1, filters: [{ usbVendorId: 0x2341 }] });\n\nlocker.on('serial:connecting',   () => console.log('Opening port…'));\nlocker.on('serial:connected',    () => console.log('Port open'));\nlocker.on('serial:disconnected', () => console.log('Disconnected'));\n\nlocker.on('locker:connected',     ({ channel }) => console.log(`Locker on channel ${channel} ready`));\nlocker.on('locker:dispensed',     ({ cell_status }) => console.log('Cell opened, status:', cell_status));\nlocker.on('locker:not-dispensed', ({ cell_status }) => console.warn('Cell not opened, status:', cell_status));\nlocker.on('locker:message',       (msg) => console.log(`[${msg.no_code}] ${msg.name}`));\n\n// Opens a port picker dialog (requires a user gesture)\nawait locker.connect();\n\n// Open cell 5\nconst result = await locker.sendDispense({ cell: 5 });\nconsole.log(result.dispensed); // true | false\n\n// Check status of a cell\nawait locker.sendStatus({ cell: 5 });\n\n// Enable / disable individual cells\nawait locker.sendEnable({ cell: 3 });\nawait locker.sendDisable({ cell: 3 });\n\n// Light scan (columns 0–5)\nawait locker.sendLightScan({ since: 0, until: 5 });\n\n// Bulk operations\nawait locker.sendOpenAll();    // opens cells 1–80 sequentially\nawait locker.sendEnableAll();  // enables cells 1–80 sequentially\nawait locker.sendDisableAll(); // disables cells 1–80 sequentially\n```\n\n---\n\n## Serial settings\n\nThe constructor pre-configures the following defaults — no extra setup needed:\n\n| Setting            | Value                      |\n| ------------------ | -------------------------- |\n| Baud rate          | 9600                       |\n| Data bits          | 8                          |\n| Stop bits          | 1                          |\n| Parity             | none                       |\n| Flow control       | none                       |\n| Buffer size        | 255 B                      |\n| Parser             | interByteTimeout (40 ms)   |\n| Command timeout    | 1 000 ms                   |\n| Auto-reconnect     | ✓                          |\n| Reconnect interval | 1 500 ms                   |\n| Handshake timeout  | 2 000 ms                   |\n\n---\n\n## Providers\n\nBy default the library uses the browser's native **Web Serial API** (`navigator.serial`). You can replace this with any of the built-in providers from `webserial-core`, or write your own.\n\n### Web Serial API (default)\n\nNo setup required — works out of the box in Chrome / Edge 89+.\n\n```ts\nimport { Locker } from '@danidoble/webserial-locker';\n\nconst locker = new Locker({ filters: [{ usbVendorId: 0x2341 }] });\nawait locker.connect();\n```\n\n### WebUSB (`WebUsbProvider`)\n\nUse the **WebUSB API** as the transport. Useful for devices or platforms where the native Web Serial API is unavailable, or when targeting CP210x / vendor-specific USB chips.\n\n```ts\nimport { Locker, WebUsbProvider } from '@danidoble/webserial-locker';\n\nconst locker = new Locker({\n  filters: [{ usbVendorId: 0x2341 }],\n  provider: new WebUsbProvider()\n});\n\nawait locker.connect();\n```\n\n### Web Bluetooth (`createBluetoothProvider`)\n\nCommunicate over **Bluetooth Low Energy** using the Nordic UART Service (NUS). The device must expose NUS characteristics.\n\n```ts\nimport { Locker, createBluetoothProvider } from '@danidoble/webserial-locker';\n\nconst locker = new Locker({\n  provider: createBluetoothProvider()\n});\n\nawait locker.connect(); // shows the browser Bluetooth picker\n```\n\n### WebSocket (`createWebSocketProvider`)\n\nRoute serial communication through a **WebSocket bridge server** — ideal for Node.js environments or remote devices. A reference bridge implementation is available in the [`webserial-core` demos](https://github.com/danidoble/webserial-core).\n\n```ts\nimport { Locker, createWebSocketProvider } from '@danidoble/webserial-locker';\n\nconst locker = new Locker({\n  filters: [{ usbVendorId: 0x2341 }],\n  provider: createWebSocketProvider('ws://localhost:8080')\n});\n\nawait locker.connect();\n```\n\n### Global provider (`AbstractSerialDevice.setProvider`)\n\nSet a provider once for **all** device instances instead of per-instance. Import `AbstractSerialDevice` directly from `webserial-core`:\n\n```ts\nimport { AbstractSerialDevice, WebUsbProvider } from 'webserial-core';\nimport { Locker } from '@danidoble/webserial-locker';\n\nAbstractSerialDevice.setProvider(new WebUsbProvider());\n\nconst locker = new Locker({ filters: [{ usbVendorId: 0x2341 }] });\nawait locker.connect();\n```\n\n### Custom provider\n\nImplement the `SerialProvider` interface to target any platform:\n\n```ts\nimport type { SerialProvider, SerialPortFilter } from '@danidoble/webserial-locker';\nimport { Locker } from '@danidoble/webserial-locker';\n\nconst myProvider: SerialProvider = {\n  async requestPort(options?: { filters?: SerialPortFilter[] }): Promise<SerialPort> {\n    // return a SerialPort-compatible object\n  },\n  async getPorts(): Promise<SerialPort[]> {\n    // return previously authorised ports\n  }\n};\n\nconst locker = new Locker({\n  filters: [{ usbVendorId: 0x2341 }],\n  provider: myProvider\n});\n```\n\n---\n\n## API\n\n### `new Locker(options?)`\n\n| Option            | Type                  | Default | Description                                                        |\n| ----------------- | --------------------- | ------- | ------------------------------------------------------------------ |\n| `channel`         | `number`              | `1`     | Channel number used in the binary handshake and commands.          |\n| `filters`         | `SerialPortFilter[]`  | `[]`    | USB vendor/product filters for port matching.                      |\n| `provider`        | `SerialProvider`      | —       | Per-instance provider. Overrides the global static provider.       |\n| `polyfillOptions` | `SerialDeviceOptions` | —       | Extra options forwarded to the provider (e.g. baud rate override). |\n\n### `locker.connect()`\n\nOpens the serial port and performs the binary handshake. Shows a browser port-picker dialog on first connection; subsequent calls reuse the last authorised port.\n\n```ts\nawait locker.connect();\n```\n\n### `locker.disconnect()`\n\nGracefully closes the port and stops auto-reconnect.\n\n```ts\nawait locker.disconnect();\n```\n\n### `locker.sendDispense(options?)`\n\nOpens the specified cell and resolves with a `DispenserDispenseResponse` describing the outcome.\n\n| Option | Type     | Default | Description               |\n| ------ | -------- | ------- | ------------------------- |\n| `cell` | `number` | `1`     | Cell number to open (1-based, max 90). |\n\n```ts\nconst result = await locker.sendDispense();           // cell 1\nconst result = await locker.sendDispense({ cell: 5 }); // cell 5\n// result: { dispensed: boolean, error: boolean, reason: string | null }\n```\n\n### `locker.sendStatus(options?)`\n\nRequests the current status of a cell.\n\n| Option | Type               | Default | Description               |\n| ------ | ------------------ | ------- | ------------------------- |\n| `cell` | `number \\| string` | `1`     | Cell number (1-based, max 90). |\n\n```ts\nawait locker.sendStatus({ cell: 3 });\n```\n\n### `locker.sendLightScan(options?)`\n\nTriggers a light/proximity scan across a column range.\n\n| Option   | Type     | Default | Description               |\n| -------- | -------- | ------- | ------------------------- |\n| `since`  | `number` | `0`     | Starting column (0–10).   |\n| `until`  | `number` | `10`    | Ending column (0–10).     |\n\n```ts\nawait locker.sendLightScan({ since: 0, until: 5 });\n```\n\n### `locker.sendEnable(options?)`\n\nEnables the specified cell.\n\n| Option | Type               | Default | Description               |\n| ------ | ------------------ | ------- | ------------------------- |\n| `cell` | `number \\| string` | `1`     | Cell number (1-based, max 90). |\n\n```ts\nawait locker.sendEnable({ cell: 4 });\n```\n\n### `locker.sendDisable(options?)`\n\nDisables the specified cell.\n\n| Option | Type               | Default | Description               |\n| ------ | ------------------ | ------- | ------------------------- |\n| `cell` | `number \\| string` | `1`     | Cell number (1-based, max 90). |\n\n```ts\nawait locker.sendDisable({ cell: 4 });\n```\n\n### `locker.sendOpenAll()`\n\nOpens all cells 1–80 sequentially. Emits `locker:percentage:open` after each cell. Returns an array of `DispenserDispenseResponse`.\n\n```ts\nconst results = await locker.sendOpenAll();\n```\n\n### `locker.sendEnableAll()`\n\nEnables all cells 1–80 sequentially. Emits `locker:percentage:enable` after each cell.\n\n```ts\nawait locker.sendEnableAll();\n```\n\n### `locker.sendDisableAll()`\n\nDisables all cells 1–80 sequentially. Emits `locker:percentage:disable` after each cell.\n\n```ts\nawait locker.sendDisableAll();\n```\n\n### `locker.isConnected()`\n\nReturns `true` when the port is open and the handshake has completed.\n\n---\n\n## Events\n\n### Core events (from `webserial-core`)\n\n| Event                    | Payload                           | Description                                       |\n| ------------------------ | --------------------------------- | ------------------------------------------------- |\n| `serial:connecting`      | `instance`                        | Port is being opened.                             |\n| `serial:connected`       | `instance`                        | Port opened successfully.                         |\n| `serial:disconnected`    | `instance`                        | Port closed or device unplugged.                  |\n| `serial:reconnecting`    | `instance`                        | Auto-reconnect attempt in progress.               |\n| `serial:data`            | `data: Uint8Array`, `instance`    | Raw binary frame received from the device.        |\n| `serial:sent`            | `data: Uint8Array`, `instance`    | Raw bytes written to the port.                    |\n| `serial:error`           | `error: Error`, `instance`        | An error occurred during communication.           |\n| `serial:need-permission` | `instance`                        | No authorised port found; user must grant access. |\n| `serial:timeout`         | `command: Uint8Array`, `instance` | A queued command timed out.                       |\n\n### Locker events\n\n| Event                       | Payload                                                                      | Description                                             |\n| --------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |\n| `locker:connected`          | `{ channel: number }`                                                        | Handshake succeeded; locker is ready.                   |\n| `locker:dispensed`          | `{ cell_status: number }`                                                    | A cell was successfully opened.                         |\n| `locker:not-dispensed`      | `{ cell_status: number }`                                                    | A cell could not be opened (closed, disconnected, etc). |\n| `locker:message`            | `LockerMessage`                                                              | Detailed message for every device response.             |\n| `locker:percentage:open`    | `{ percentage: number; dispensed: DispenserDispenseResponse[] \\| null }`    | Progress during `sendOpenAll()`.                        |\n| `locker:percentage:enable`  | `{ percentage: number }`                                                     | Progress during `sendEnableAll()`.                      |\n| `locker:percentage:disable` | `{ percentage: number }`                                                     | Progress during `sendDisableAll()`.                     |\n\n#### `LockerMessage` codes\n\n| `no_code` | Meaning                                          |\n| --------- | ------------------------------------------------ |\n| `100`     | Connection handshake completed.                  |\n| `102`     | Cell opened successfully.                        |\n| `103`     | Cell configuration applied.                      |\n| `104`     | Cell is inactive or does not exist.              |\n| `105`    | Cell is closed.                                  |\n| `101`     | Cell is disconnected or does not exist.          |\n| `404`     | Cell status is unknown.                          |\n| `400`     | Response received but not recognised.            |\n\n---\n\n## TypeScript\n\nAll events and method signatures are fully typed. The package ships with `.d.mts` / `.d.cts` declaration files — no extra `@types` package required.\n\nCommonly used types and all built-in providers are re-exported so you do not need to import directly from `webserial-core`:\n\n```ts\nimport {\n  Locker,\n  WebUsbProvider,\n  createBluetoothProvider,\n  createWebSocketProvider\n} from '@danidoble/webserial-locker';\n\nimport type {\n  LockerOptions,\n  DispenserDispenseResponse,\n  LockerMessage,\n  OpenCellOptions,\n  LightScanOptions,\n  SerialPortFilter,\n  SerialDeviceOptions,\n  SerialEventMap,\n  SerialProvider,\n  SerialPolyfillOptions\n} from '@danidoble/webserial-locker';\n```\n\n---\n\n## License\n\n[GPL-3.0-only](./LICENSE.md) © [Danidoble](https://github.com/danidoble)\n","readmeFilename":"README.md","_rev":"1-8e023f6be90b17007c3a23206e9ebb2e"}