{"_id":"@93bx/aclas-scale-native-js","_rev":"3-584c86829157f80e7a61f2bbf222ab9f","name":"@93bx/aclas-scale-native-js","dist-tags":{"latest":"0.2.1"},"versions":{"0.1.0":{"name":"@93bx/aclas-scale-native-js","version":"0.1.0","keywords":["aclas","link69","link66","scale","label scale","barcode scale","weighing scale","udp","iot"],"author":{"name":"93bx"},"license":"MIT","_id":"@93bx/aclas-scale-native-js@0.1.0","maintainers":[{"name":"93bx","email":"mmo93b@gmail.com"}],"homepage":"https://github.com/93bx/aclas-scale-native-js#readme","bugs":{"url":"https://github.com/93bx/aclas-scale-native-js/issues"},"dist":{"shasum":"0c3eed2117ce1eb103c763afddba1daefea3ced2","tarball":"https://registry.npmjs.org/@93bx/aclas-scale-native-js/-/aclas-scale-native-js-0.1.0.tgz","fileCount":71,"integrity":"sha512-ooOeUaKud0VgmXUGfcAQL+VtZNgaIogXNxITh9uqS/JimJx6pizDtqPi/V8ZUZYU7nxH19hSUp0X/obIdLCIxQ==","signatures":[{"sig":"MEUCIG7FiG8yyeKgnPGFYfZ4R+qxaXrOlOrzL6Xv5QbYzRBqAiEAg3PV2tRP/BPbu0IvknIuSwn+J6LmFqOR6mhGFXENrSc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":165844},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"1fe7ebf7be53a7f2f4db0850af6644da79152665","scripts":{"test":"npm run build && NODE_OPTIONS= node tests/smoke.mjs","build":"tsc","verify:crc":"npm run build && node scripts/verify-crc.mjs","prepublishOnly":"npm run build"},"_npmUser":{"name":"93bx","email":"mmo93b@gmail.com"},"repository":{"url":"git+https://github.com/93bx/aclas-scale-native-js.git","type":"git"},"_npmVersion":"11.12.1","description":"Node.js/TypeScript client for AClas Link69 / Link66 label-weighing scales over UDP","directories":{},"_nodeVersion":"24.15.0","dependencies":{"adm-zip":"^0.5.17"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.8.3","@types/node":"^22.15.21","@types/adm-zip":"^0.5.8"},"_npmOperationalInternal":{"tmp":"tmp/aclas-scale-native-js_0.1.0_1779195228048_0.1848561516152465","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@93bx/aclas-scale-native-js","version":"0.2.0","keywords":["aclas","link69","link66","scale","label scale","barcode scale","weighing scale","udp","iot"],"author":{"name":"93bx"},"license":"MIT","_id":"@93bx/aclas-scale-native-js@0.2.0","maintainers":[{"name":"93bx","email":"mmo93b@gmail.com"}],"homepage":"https://github.com/93bx/aclas-scale-native-js#readme","bugs":{"url":"https://github.com/93bx/aclas-scale-native-js/issues"},"dist":{"shasum":"c5d1265c28bfd39f0109c73aeb843e9d27097d3b","tarball":"https://registry.npmjs.org/@93bx/aclas-scale-native-js/-/aclas-scale-native-js-0.2.0.tgz","fileCount":75,"integrity":"sha512-pkgz98iaLoLx/6Xbua6H5xqS6KX4QMMg5Zx/8kXh+Fqo7YZSQkCHPWlVNIQ/11u0GdqN7+RvjHr38QChGBAIEA==","signatures":[{"sig":"MEUCIQDtJVhwZO8GmYa/+RULaCxgpv2BkTWSZFvPkPce4sLV+wIgI2MdxXlZ5nansfy8UzS4INdUrEUU4bPO4ZrN32fZDfg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":188009},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"b8f8f8d3d965566173ecc3e2aa24bf036cd043af","scripts":{"test":"npm run build && node tests/plu-encoding.mjs && NODE_OPTIONS= node tests/smoke.mjs","build":"tsc","verify:crc":"npm run build && node scripts/verify-crc.mjs","test:offline":"npm run build && node tests/plu-encoding.mjs","prepublishOnly":"npm run build"},"_npmUser":{"name":"93bx","email":"mmo93b@gmail.com"},"repository":{"url":"git+https://github.com/93bx/aclas-scale-native-js.git","type":"git"},"_npmVersion":"11.12.1","description":"Node.js/TypeScript client for AClas Link69 / Link66 label-weighing scales over UDP","directories":{},"_nodeVersion":"24.15.0","dependencies":{"adm-zip":"^0.5.17"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.8.3","@types/node":"^22.15.21","@types/adm-zip":"^0.5.8"},"_npmOperationalInternal":{"tmp":"tmp/aclas-scale-native-js_0.2.0_1780235795834_0.7445931047073193","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@93bx/aclas-scale-native-js","version":"0.2.1","description":"Node.js/TypeScript client for AClas Link69 / Link66 label-weighing scales over UDP","keywords":["aclas","link69","link66","scale","label scale","barcode scale","weighing scale","udp","iot"],"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsc","test":"npm run build && node tests/plu-encoding.mjs && NODE_OPTIONS= node tests/smoke.mjs","test:offline":"npm run build && node tests/plu-encoding.mjs","verify:crc":"npm run build && node scripts/verify-crc.mjs","prepublishOnly":"npm run build"},"engines":{"node":">=18"},"license":"MIT","author":{"name":"93bx"},"repository":{"type":"git","url":"git+https://github.com/93bx/aclas-scale-native-js.git"},"homepage":"https://github.com/93bx/aclas-scale-native-js#readme","bugs":{"url":"https://github.com/93bx/aclas-scale-native-js/issues"},"devDependencies":{"@types/adm-zip":"^0.5.8","@types/node":"^22.15.21","typescript":"^5.8.3"},"dependencies":{"adm-zip":"^0.5.17"},"gitHead":"b8f8f8d3d965566173ecc3e2aa24bf036cd043af","_id":"@93bx/aclas-scale-native-js@0.2.1","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-AzjgrXkGIzAsc1CGhIhiN6Cr/Oa6hIDZXowZXN3LgV04Bs1LwluOVHtFaD1oYfjlkUNNegbMOjfsfgOxu120LQ==","shasum":"48059470746f6adaeb23977d823d6b62dc96aacb","tarball":"https://registry.npmjs.org/@93bx/aclas-scale-native-js/-/aclas-scale-native-js-0.2.1.tgz","fileCount":75,"unpackedSize":191712,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIH8VUskwIcG+I6koypVzqtq2mBw24V7hYs2AiSYvgkyqAiBbQJVnorZHtmpsqb9BxiU81lNvAr5SqK8JDmYkNXlBAA=="}]},"_npmUser":{"name":"93bx","email":"mmo93b@gmail.com"},"directories":{},"maintainers":[{"name":"93bx","email":"mmo93b@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/aclas-scale-native-js_0.2.1_1780236492905_0.507620341071015"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T12:53:47.974Z","modified":"2026-05-31T14:08:13.163Z","0.1.0":"2026-05-19T12:53:48.219Z","0.2.0":"2026-05-31T13:56:36.002Z","0.2.1":"2026-05-31T14:08:13.055Z"},"bugs":{"url":"https://github.com/93bx/aclas-scale-native-js/issues"},"author":{"name":"93bx"},"license":"MIT","homepage":"https://github.com/93bx/aclas-scale-native-js#readme","keywords":["aclas","link69","link66","scale","label scale","barcode scale","weighing scale","udp","iot"],"repository":{"type":"git","url":"git+https://github.com/93bx/aclas-scale-native-js.git"},"description":"Node.js/TypeScript client for AClas Link69 / Link66 label-weighing scales over UDP","maintainers":[{"name":"93bx","email":"mmo93b@gmail.com"}],"readme":"# @93bx/aclas-scale-native-js\r\n\r\nNode.js/TypeScript client for **AClas Link69 / Link66** label-weighing scales over a LAN.\r\n\r\nCommunicates via UDP port 5002 — the same binary protocol used by the official\r\nAClas Link69 PC software. All major operations are supported: PLU management,\r\ndepartment/category/unit/hotkey tables, label upload/download, clock sync,\r\nparameter read/write, and device discovery.\r\n\r\nProtocol reverse-engineered from Wireshark captures of an AClas LS5Z7 scale.\r\n\r\n---\r\n\r\n## What's new in 0.2.0\r\n\r\nThis is a **breaking** release that fixes two protocol issues uncovered by live\r\ntesting on 2026-05-31. Upgrading from `0.1.x` requires changes if you set PLU\r\nprices in your application.\r\n\r\n### Fixes\r\n\r\n- **PLU download silently dropped by the scale for `LFCode ≥ 100`** — the\r\n  `98 2e` packet CRC range was wrong. The CRC now covers `buf[13..525]` (which\r\n  includes the LFCode HIGH byte at `buf[14]`), matching every captured Link69\r\n  packet (5/5 in capture 14, 30/30 in capture 14B). Previously a CRC that\r\n  excluded `buf[14]` produced wrong checksums whenever `buf[14] != 0`, so the\r\n  scale dropped those packets without an ACK and the client saw a 5-second UDP\r\n  receive timeout. `LFCode < 100` packets happened to verify because a leading\r\n  zero byte does not change a CCITT-FALSE CRC. Symptom: `setScalePLUs` worked\r\n  for `LFCode 1–99` and timed out for `LFCode 100–9999`.\r\n\r\n### Breaking API changes\r\n\r\n- **`unitPrice` and `memberPrice` are now integers 0–9999**, stored as-is in\r\n  BCD at `rec[139-140]` and `rec[250-251]`. There is no implicit `×100` (cents)\r\n  conversion on the wire. Decimal placement is governed by the scale's own\r\n  display setting.\r\n\r\n  | Before (0.1.x) | After (0.2.0) | Wire bytes |\r\n  | --- | --- | --- |\r\n  | `unitPrice: 15.00`   | `unitPrice: 1500` | `[0x15, 0x00]` |\r\n  | `unitPrice: 0.15`    | `unitPrice: 15`   | `[0x00, 0x15]` |\r\n  | `unitPrice: 99.99`   | `unitPrice: 9999` | `[0x99, 0x99]` |\r\n\r\n  The decoder side (`decodePluRecord` / `getScalePLUs`) is symmetric: it now\r\n  returns the raw integer instead of dividing by 100. The previous \"cents\"\r\n  interpretation was a misread of the same evidence — Link69 PC also reads back\r\n  the raw value, which is why setting `unitPrice: 10` in 0.1.x and reading via\r\n  Link69 showed `1001` (storing 1000 cents) instead of `10`.\r\n\r\n  **Migration:** if your old code did `unitPrice: dollars`, change it to\r\n  `unitPrice: Math.round(dollars * 100)` (or just type the integer you want the\r\n  scale to display).\r\n\r\n- **`lfCode` now accepts 0–9999** (was effectively 0–99 in 0.1.x — the high byte\r\n  was missing from the wire). 2-byte BCD split across `buf[14]` (header) and\r\n  `rec[0]` (record body). Calling `buildDownloadPluPacket` / `setScalePLUs`\r\n  with `lfCode > 9999` throws `RangeError`.\r\n\r\n- **`code` now accepts 0–999999** (3-byte BCD at `rec[8-10]`; was effectively\r\n  0–99 in 0.1.x).\r\n\r\n- **`name1` / `name2` are encoded as Windows-1256 (CP-1256)**, supporting\r\n  Arabic characters. ASCII-only text is byte-for-byte identical to the previous\r\n  ASCII encoding, so existing English-only PLUs continue to work unchanged.\r\n\r\n### Internal\r\n\r\n- New regression tests lock in the CRC range against captures 14/14B and the\r\n  new `unitPrice` integer semantics (`tests/plu-encoding.mjs`, 23/23 pass).\r\n- `scripts/verify_crc.py` now exercises the `buf[14] != 0` cases too.\r\n\r\n---\r\n\r\n## Requirements\r\n\r\n- **Node.js ≥ 18**\r\n- Scale reachable over UDP on port 5002 (default)\r\n- No extra OS drivers needed; pure UDP sockets\r\n\r\n---\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install @93bx/aclas-scale-native-js\r\n```\r\n\r\n---\r\n\r\n## Quick Start\r\n\r\n```typescript\r\nimport { AclasScale } from '@93bx/aclas-scale-native-js';\r\n\r\n// One-liner: discover → handshake → ready\r\nconst scale = await AclasScale.connect('192.168.1.100');\r\n\r\n// Write a PLU\r\nawait scale.setScalePLUs([{\r\n  lfCode:           1,\r\n  code:             100,\r\n  barcodeStartCode: 99,\r\n  name1:            'Fresh Apples',\r\n  unitPrice:        350,    // integer 0–9999, stored as-is; scale's decimal setting governs display\r\n  unitId:           4,      // 4 = kg\r\n  barcodeType1:     7,      // 7 = EAN-13 weight (Link69 default)\r\n}]);\r\n\r\n// Read PLUs back from the scale\r\nconst plus = await scale.getScalePLUs();\r\nconsole.log(plus);\r\n\r\nscale.close();\r\n```\r\n\r\n---\r\n\r\n## API Reference\r\n\r\n### `AclasScale.connect(host, options?)` → `Promise<AclasScale>`\r\n\r\nStatic factory: creates an instance, runs discovery + handshake, and returns the\r\nconnected object. Equivalent to `new AclasScale({ host }) + scale.connect()`.\r\n\r\n```typescript\r\nconst scale = await AclasScale.connect('192.168.1.100', { timeoutMs: 8000 });\r\n```\r\n\r\n### `new AclasScale(options)`\r\n\r\n```typescript\r\ninterface AclasScaleOptions {\r\n  host:             string;   // Scale IP address\r\n  port?:            number;   // Default: 5002\r\n  localAddress?:    string;   // Bind to a specific local interface\r\n  timeoutMs?:       number;   // UDP timeout per operation (default: 5000 ms)\r\n  handshakeReplay?: Buffer;   // Optional: 116-byte captured handshake packet (legacy)\r\n  handshakeNonce?:  number;   // Optional: override nonce in handshakeReplay\r\n}\r\n```\r\n\r\n> **Zero-body handshake (default):** The scale does not validate the session body.\r\n> No Wireshark capture is required — `buildZeroBodyHandshake()` is used automatically.\r\n\r\n### `scale.connect()` → `Promise<void>`\r\n\r\nRun discovery (op `71 03`) then handshake (op `65 88`). Must be called before\r\nany data operation. The static `AclasScale.connect()` factory calls this for you.\r\n\r\n### `scale.close()`\r\n\r\nStop the heartbeat timer and close all UDP sockets. Always call this when done.\r\n\r\n---\r\n\r\n### PLU\r\n\r\n#### `scale.setScalePLUs(plus, options?)` → `Promise<void>`\r\n\r\nDownload PLU records to the scale (op `98 2e`). Sends a clock sync first unless\r\n`{ syncClock: false }` is passed.\r\n\r\n```typescript\r\nawait scale.setScalePLUs([\r\n  { lfCode: 1, code: 100, barcodeStartCode: 99, name1: 'Banana', unitPrice: 120, unitId: 4, barcodeType1: 7 },\r\n  { lfCode: 2, code: 200, barcodeStartCode: 99, name1: 'Apple',  unitPrice: 350, unitId: 4, barcodeType1: 7 },\r\n]);\r\n```\r\n\r\n#### `scale.getScalePLUs(options?)` → `Promise<PluRecord[]>`\r\n\r\nUpload all PLU records from the scale (ops `60 33` + `60 2e` × N).\r\n\r\n#### `scale.clearScalePLUs(options?)` → `Promise<void>`\r\n\r\nClear all PLU Info on the scale (op `98 30` — \"Clear Device Data → PLU Info\").\r\n\r\n#### `PluRecord`\r\n\r\n```typescript\r\ninterface PluRecord {\r\n  lfCode:            number;   // 0–9999. 2-byte BCD split across packet header buf[14] and rec[0]\r\n  code:              number;   // 0–999999. 3-byte BCD at rec[8-10]\r\n  barcodeStartCode:  number;   // BCD, e.g. 99 (typical EAN-13 weight prefix)\r\n  name1:             string;   // Up to 20 chars, Windows-1256 (CP-1256) — supports Arabic\r\n  unitPrice:         number;   // Integer 0–9999, stored as-is (no implicit ×100). 1500 → \"1500\"; 15 → \"15\"\r\n  unitId:            number;   // 1=g, 4=kg (see UnitPrintRecord for custom names)\r\n  barcodeType1:      number;   // Binary type ID (e.g. 1=EAN13, 7=EAN13 weight, 11=Code128)\r\n  // optional\r\n  categoryId?:       number;\r\n  name2?:            string;   // Up to 20 chars, Windows-1256 (CP-1256)\r\n  memberPrice?:      number;   // Integer 0–9999, same semantics as unitPrice\r\n  shelfDate?:        number;   // Shelf life in days\r\n  mainLabelId?:      number;\r\n  tare?:             number;   // Grams (BCD, 0–99)\r\n  tax1Percent?:      number;   // Percentage (BCD, 0–99)\r\n  message1Id?:       number;\r\n}\r\n```\r\n\r\n---\r\n\r\n### Department / Category / Unit / Hotkey\r\n\r\n#### `scale.setScaleDepartments(depts)` → `Promise<void>`\r\n\r\nDownload the full 99-slot department table (op `98 14`).\r\nOnly provided entries are set active; all other slots are cleared.\r\n\r\n```typescript\r\nawait scale.setScaleDepartments([\r\n  { id: 1, name: 'Produce', active: true },\r\n  { id: 2, name: 'Dairy' },\r\n]);\r\n```\r\n\r\n#### `scale.setScaleCategories(cats)` → `Promise<void>`\r\n\r\nDownload the full 99-slot category name table (op `98 15`).\r\nDepartment/parent assignment is managed by the PC — not sent on the wire.\r\n\r\n#### `scale.setScaleUnits(units)` → `Promise<void>`\r\n\r\nDownload custom display/print names for unit slots (op `98 23`).\r\nFirmware preset names (g, kg, …) are not overwritten by this command.\r\n\r\n```typescript\r\nawait scale.setScaleUnits([{ id: 0, printName: 'grams' }]);\r\n```\r\n\r\n#### `scale.setScaleHotkeys(hotkeys)` → `Promise<void>`\r\n\r\nDownload the full 160-slot hotkey assignment table (op `98 12`).\r\n\r\n```typescript\r\nawait scale.setScaleHotkeys([{ slot: 0, lfCode: 1 }, { slot: 1, lfCode: 2 }]);\r\n```\r\n\r\n#### `scale.getScaleHotkeys()` → `Promise<HotkeyRecord[]>`\r\n\r\nUpload the hotkey table from the scale (ops `60 13` + `60 12` × 3).\r\nReturns only the assigned (non-empty) slots.\r\n\r\n---\r\n\r\n### Label\r\n\r\n#### `scale.setScaleLabel(data)` → `Promise<void>`\r\n\r\nDownload a label template to the scale (ops `98 1e` + `98 1f` × N + `98 20`).\r\n\r\nBecause the element-descriptor (`98 1e`) and LabelMap (`98 1f`) binary formats\r\nare not fully decoded, supply the raw bodies from a capture or use `parseLabelTbz()`\r\nwith an exported `.tbz` label file.\r\n\r\n```typescript\r\nimport { parseLabelTbz } from '@93bx/aclas-scale-native-js';\r\nimport { readFileSync } from 'node:fs';\r\n\r\nconst tbz = readFileSync('my-label.tbz');\r\nawait scale.setScaleLabel(parseLabelTbz(tbz));\r\n```\r\n\r\n#### `scale.getScaleLabel()` → `Promise<LabelUploadResult>`\r\n\r\nUpload the active label template gzip stream from the scale (ops `60 31` + `60 20` × N).\r\n\r\n---\r\n\r\n### Parameters\r\n\r\n#### `scale.readParams()` → `Promise<ReadParamsResult>`\r\n\r\nRead the scale's parameter blob (op `60 28`). Returns the raw 251-byte blob.\r\nUse `scale.writeParams(blob)` (future) to write changes back.\r\n\r\n---\r\n\r\n### Discovery\r\n\r\n#### `AclasScale.discover(broadcastHost?, options?)` → `Promise<DiscoveredScale[]>`\r\n\r\nBroadcast a `71 03` discovery packet and collect all responding scales.\r\n\r\n```typescript\r\nconst scales = await AclasScale.discover('192.168.1.255', { timeoutMs: 3000 });\r\nfor (const s of scales) {\r\n  console.log(`${s.model} ${s.serialNumber} @ ${s.host} — ${s.firmware}`);\r\n}\r\n```\r\n\r\n#### `DiscoveredScale`\r\n\r\n```typescript\r\ninterface DiscoveredScale {\r\n  host:          string;\r\n  port:          number;\r\n  model:         string;   // e.g. \"LS5Z7\"\r\n  serialNumber:  string;   // e.g. \"24210002\"\r\n  firmware:      string;   // e.g. \"V7.429\"\r\n  firmwareSub:   number;\r\n  firmwareBuild: string;   // e.g. \"F-HF527\"\r\n  firmwareRev:   string;   // e.g. \"R299\"\r\n  internalModel: string;   // e.g. \"LS5RZX\"\r\n  zoneName:      string;   // \"TRACE NULL\" = not configured\r\n  requesterMac:  string;   // PC MAC echoed back by the scale\r\n  rawResponse:   Buffer;\r\n}\r\n```\r\n\r\n---\r\n\r\n### Heartbeat\r\n\r\n```typescript\r\nscale.startHeartbeat();        // fires every 5 s (matches Link69 cadence)\r\nscale.startHeartbeat(10_000);  // custom interval\r\nscale.stopHeartbeat();\r\nconst info = await scale.heartbeat();  // one-shot unicast 71 03\r\n```\r\n\r\n---\r\n\r\n### Clock Sync\r\n\r\n#### `scale.syncClock(date?)` → `Promise<void>`\r\n\r\nSend a `70 03` BCD timestamp to the scale. Called automatically before PLU downloads.\r\n\r\n---\r\n\r\n### Error Handling\r\n\r\nAll operations throw `ScaleError` on protocol failures:\r\n\r\n```typescript\r\nimport { ScaleError } from '@93bx/aclas-scale-native-js';\r\n\r\ntry {\r\n  await scale.setScalePLUs(plus);\r\n} catch (err) {\r\n  if (err instanceof ScaleError) {\r\n    console.error(`Scale op 0x${err.op} failed at seq ${err.seqIndex}: ${err.message}`);\r\n  }\r\n}\r\n```\r\n\r\nNetwork/timeout errors throw plain `Error` with a descriptive message.\r\n\r\n---\r\n\r\n### Low-level helpers\r\n\r\nThe package also exports protocol primitives for advanced use:\r\n\r\n```typescript\r\nimport {\r\n  crc16CcittFalse,\r\n  appendPacketCrcBe,\r\n  verifyPacketCrcBe,\r\n  buildZeroBodyHandshake,\r\n  buildDiscoveryRequest,\r\n  encodePluRecord,\r\n  decodePluRecord,\r\n  buildDownloadPluPacket,\r\n  encodeDeptRecord,\r\n  decodeDeptRecord,\r\n  encodeCategoryRecord,\r\n  encodeUnitRecord,\r\n  encodeHotkeyRecord,\r\n  parseLabelTbz,\r\n  extractGzipFromBodies,\r\n} from '@93bx/aclas-scale-native-js';\r\n```\r\n\r\n---\r\n\r\n## Protocol Notes\r\n\r\n- All communication is **UDP port 5002** — there is no TCP.\r\n- Tested against an **AClas LS5Z7** scale running firmware V7.429.\r\n- **CRC:** CRC-16/CCITT-FALSE (poly 0x1021, init 0x0000), big-endian on wire.\r\n  PLU packets (`98 2e` / `60 2e`) compute CRC over `buf[13..525]`, which\r\n  **includes `buf[14]` (LFCode HIGH byte)**.\r\n- **BCD encoding:** integers are packed BCD, two decimal digits per byte.\r\n  `unitPrice: 1500 → [0x15, 0x00]`; `unitPrice: 15 → [0x00, 0x15]`; single-byte\r\n  fields like `tare: 15 → 0x15`. Prices are stored as-is — the scale's own\r\n  display setting (firmware-side) determines whether `1500` renders as `1500`\r\n  or `15.00`.\r\n- **Department↔PLU mapping:** departments are assigned to *categories*, not\r\n  directly to PLUs. Set `categoryId` on a PLU and download the category/dept\r\n  tables separately.\r\n- **Label metadata:** the `98 1e` element-descriptor and `98 1f` LabelMap binary\r\n  formats are partially decoded. Use `parseLabelTbz()` with files exported from\r\n  the official Link69 software as the practical path.\r\n\r\n---\r\n\r\n## License\r\n\r\nMIT\r\n","readmeFilename":"README.md"}