tigertag 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,10 +3,46 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
5
 
6
+ ## [1.0.2] — 2026-05-21
7
+
8
+ ### Added
9
+ - `TigerTag.fromCloudDoc(doc, db?)` — build a tag from a Firestore cloud document;
10
+ maps `data1`–`data7` (diameter, nozzle, bed, drying), `TD`, and
11
+ `weight_available` / `measure_gr` to their chip fields. Primary entry point
12
+ for the cloud → chip write pipeline.
13
+ - `TigerTag.fromRawDict(raw, db?)` — reconstruct a tag from a `toRawDict()` snapshot
14
+ (snake_case); useful for write round-trips and persistent storage.
15
+ - `tag.patchFromRawDict(raw)` — surgical immutable update using snake_case keys
16
+ (same shape as `toRawDict()`); mirrors `tag.patch()` for callers that store or
17
+ receive snake_case dicts.
18
+ - `TigerTag._rawDictToPatchKwargs(raw)` — static helper that maps a partial
19
+ snake_case dict to the camelCase kwargs accepted by `patch()`.
20
+
21
+ ## [1.0.1] — 2026-05-20
22
+
23
+ ### Added
24
+ - Playground: live ACR122U / PC-SC reader integration via WebSocket
25
+ - Up to 2 simultaneous USB readers supported
26
+ - Card placement auto-populates playground form and displays full SDK output
27
+ - Reader status bar in playground header (green dot / orange pulse)
28
+ - WebSocket server on same port as HTTP (no extra port needed)
29
+ - Graceful fallback: playground works normally without `ws` and `nfc-pcsc`
30
+ - `tools/server.js`: WebSocket server attached to HTTP server; nfc-pcsc reader loop
31
+ broadcasting `card:detected`, `card:removed`, `reader:connected`, `reader:disconnected`
32
+ - README: full parity with Python SDK — input format tables, key methods/properties,
33
+ CRUD operations, ApiDiff docs, signature status table, DB auto-update table,
34
+ chip_layout.svg diagram, ACR122U full example, ecosystem table,
35
+ community integrations, AI-CONTEXT block
36
+ - `llms.txt`: added npm URL, install command, playground section with ACR122U details
37
+
38
+ ### Changed
39
+ - `package.json`: `ws ^8.x` and `nfc-pcsc ^0.8.x` added as devDependencies
40
+ - Published to npm: `npm install tigertag` now available globally
41
+
6
42
  ## [1.0.0] — 2026-05-20
7
43
 
8
44
  ### Added
9
- - `TigerTag.fromPages(payload, uid)` — primary constructor for NFC SDK integration
45
+ - `TigerTag.fromPages(uid, payload)` — primary constructor for NFC SDK integration
10
46
  - `TigerTag.fromDump(data)` — constructor for binary dumps (180B auto-extracts UID)
11
47
  - `TigerTag.fromFile(path)` — convenience constructor from .bin file
12
48
  - `TigerTag.create({ ...fields })` — build a new tag from scratch with all fields
package/README.md CHANGED
@@ -130,6 +130,21 @@ The playground has three panels:
130
130
  - **Center** — parsed output cards: Protocol, Material, Colors, Print Settings, Quantity, Traceability, Cloud API
131
131
  - **Right** — collapsible SDK panel: `pretty()`, `verify()`, `toRawDict()`, `toDict()`, `rawApi()`, `diffApi()`
132
132
 
133
+ ### ACR122U / PC-SC live reader
134
+
135
+ Place a chip on your reader and the playground auto-populates instantly — no manual action needed.
136
+
137
+ ```bash
138
+ # Enable live reader support (one-time install)
139
+ npm install ws nfc-pcsc
140
+
141
+ # Then launch as usual — readers detected automatically
142
+ npm run playground
143
+ ```
144
+
145
+ Up to **2 simultaneous USB readers** supported. Reader status shown in the playground header:
146
+ `● green` = connected · `● orange pulse` = reading card
147
+
133
148
  ---
134
149
 
135
150
  ## Install
@@ -148,7 +163,7 @@ the package. Requires **Node.js 18+** (uses built-in `crypto` and `fetch` — no
148
163
  ```js
149
164
  const { TigerTag } = require('tigertag');
150
165
 
151
- const tag = TigerTag.fromPages(payload, uid); // from your NFC SDK
166
+ const tag = TigerTag.fromPages(uid, payload); // from your NFC SDK
152
167
  console.log(tag.pretty()); // human-readable summary
153
168
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED / ❌ INVALID
154
169
  console.log(tag.toDict()); // JSON-ready object
@@ -180,9 +195,11 @@ for reading — all data lives on the chip.
180
195
 
181
196
  | Method | Input | When to use |
182
197
  |--------|-------|-------------|
183
- | `TigerTag.fromPages(payload, uid)` | 80 or 144 bytes + 7-byte UID | **NFC SDK integration (recommended)** |
198
+ | `TigerTag.fromPages(uid, payload)` | 7-byte UID + 80 or 144 bytes | **NFC SDK integration (recommended)** |
184
199
  | `TigerTag.fromDump(data)` | 80 / 144 / 180 bytes | Binary dumps, ACR122U raw read |
185
200
  | `TigerTag.fromFile(path)` | path to `.bin` file | Testing, offline batch processing |
201
+ | `TigerTag.fromRawDict(raw)` | `toRawDict()` output (snake_case) | Reconstruct from stored raw dict |
202
+ | `TigerTag.fromCloudDoc(doc)` | Firestore cloud document | **Write pipeline: cloud → chip** |
186
203
 
187
204
  **`fromPages`** is the primary constructor for production use. NFC SDKs always provide the
188
205
  UID as a separate property — pass it directly for full signature verification.
@@ -193,7 +210,7 @@ UID as a separate property — pass it directly for full signature verification.
193
210
 
194
211
  ## Input formats
195
212
 
196
- ### `fromPages(payload, uid)` — NFC SDK workflow
213
+ ### `fromPages(uid, payload)` — NFC SDK workflow
197
214
 
198
215
  NFC SDKs always expose the UID as a dedicated property. Pages 0–3 (system pages: lock bytes,
199
216
  capability container) are never part of the user data payload.
@@ -228,7 +245,10 @@ tag.verify(db) // → SignatureResult
228
245
  TigerTag.create(fields) // → TigerTag build from scratch
229
246
  TigerTag.asInit(uid) // → TigerTag blank Init tag
230
247
  TigerTag.erase() // → Buffer(80) zero bytes — write to chip to wipe
231
- tag.patch(fields) // → TigerTag surgical field update
248
+ TigerTag.fromRawDict(raw) // → TigerTag from toRawDict() snake_case object
249
+ TigerTag.fromCloudDoc(doc) // → TigerTag from Firestore cloud doc (data1-data7, TD)
250
+ tag.patch(fields) // → TigerTag surgical camelCase field update
251
+ tag.patchFromRawDict(raw) // → TigerTag surgical snake_case field update
232
252
 
233
253
  // Cloud (TigerTag+ only — uses built-in fetch, Node.js 18+)
234
254
  tag.rawApi(timeout) // → Promise<object|null> fetch live product data
@@ -290,6 +310,40 @@ console.log(`${applied.length} field(s) updated from cloud`);
290
310
  **Protected fields** — `patch()` throws if you try to modify:
291
311
  `idTigertag`, `idProduct`, `uid`, `signatureR`, `signatureS`.
292
312
 
313
+ ### Cloud document → chip write pipeline
314
+
315
+ `fromCloudDoc()` maps the Firestore document format to chip fields.
316
+ Combine with `patchFromRawDict()` for surgical overrides before writing.
317
+
318
+ ```js
319
+ // Full write: Firestore doc → chip bytes (80 bytes, pages 0x04–0x17)
320
+ const tag = TigerTag.fromCloudDoc(firestoreDoc);
321
+ const bytes = tag.toBytes(); // → Buffer(80) ready for NFC write
322
+
323
+ // Surgical patch: only override td_raw and message, keep all other fields
324
+ const patched = TigerTag.fromCloudDoc(firestoreDoc)
325
+ .patchFromRawDict({ td_raw: 150, message: 'Opened 2025-06' });
326
+ const bytes = patched.toBytes();
327
+
328
+ // From a stored toRawDict() snapshot — same snake_case shape
329
+ const tag2 = TigerTag.fromRawDict(storedRawDict);
330
+ const patched2 = tag2.patchFromRawDict({ measure_available: 650 });
331
+ ```
332
+
333
+ **Firestore field mapping** used by `fromCloudDoc()`:
334
+
335
+ | Firestore field | Chip field |
336
+ |-----------------|------------|
337
+ | `data1` | `idDiameter` |
338
+ | `data2` | `nozzleTempMin` |
339
+ | `data3` | `nozzleTempMax` |
340
+ | `data4` | `dryTemp` |
341
+ | `data5` | `dryTime` |
342
+ | `data6` | `bedTempMin` |
343
+ | `data7` | `bedTempMax` |
344
+ | `TD` | `tdRaw` |
345
+ | `weight_available` / `measure_gr` | `measureAvailable` |
346
+
293
347
  ### ApiDiff
294
348
 
295
349
  `ApiDiff` is a plain object `{ field, chipValue, apiValue }`:
@@ -297,7 +351,7 @@ console.log(`${applied.length} field(s) updated from cloud`);
297
351
  ```js
298
352
  const { TigerTag } = require('tigertag');
299
353
 
300
- const tag = TigerTag.fromPages(payload, uid);
354
+ const tag = TigerTag.fromPages(uid, payload);
301
355
  const diffs = await tag.diffApi();
302
356
 
303
357
  for (const d of diffs) {
@@ -328,7 +382,7 @@ result.toDict() // { status: "valid", ok: true, detail: "…" }
328
382
  | `invalid` | Signature present but does not match UID + data |
329
383
  | `unsigned` | No signature bytes — Maker tag or unverified |
330
384
  | `no_key` | No matching public key in database for this protocol version |
331
- | `no_uid` | UID not provided — cannot verify (use `fromPages(payload, uid)`) |
385
+ | `no_uid` | UID not provided — cannot verify (use `fromPages(uid, payload)`) |
332
386
 
333
387
  ECDSA-P256 verification uses the public key bundled in `database/id_version.json` — works
334
388
  fully offline, no external dependencies (Node.js built-in `crypto` module).
@@ -375,7 +429,7 @@ Sources: TigerTag API → GitHub mirror (automatic fallback).
375
429
  reader.on('card', async (card) => {
376
430
  const uid = Buffer.from(card.uid, 'hex'); // 7 bytes
377
431
  const payload = await reader.read(4, 144, 4); // pages 4–39, 144 bytes
378
- const tag = TigerTag.fromPages(payload, uid);
432
+ const tag = TigerTag.fromPages(uid, payload);
379
433
  console.log(tag.pretty());
380
434
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED
381
435
  });
@@ -400,7 +454,7 @@ nfc.on('reader', (reader) => {
400
454
  try {
401
455
  const uid = Buffer.from(card.uid, 'hex'); // 7 bytes
402
456
  const payload = await reader.read(4, 144, 4); // pages 4–39, 144 bytes
403
- const tag = TigerTag.fromPages(payload, uid);
457
+ const tag = TigerTag.fromPages(uid, payload);
404
458
  console.log(tag.pretty());
405
459
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED / ❌ INVALID
406
460
  } catch (err) {
@@ -516,7 +570,7 @@ const { TigerTag, TigerTagDB } = require('tigertag');
516
570
 
517
571
  // Called from your NFC reader callback
518
572
  function parseTigerTag(payload, uid) {
519
- const tag = TigerTag.fromPages(Buffer.from(payload), Buffer.from(uid));
573
+ const tag = TigerTag.fromPages(Buffer.from(uid), Buffer.from(payload));
520
574
  const db = new TigerTagDB();
521
575
  return {
522
576
  dict: tag.toDict(db),
@@ -597,8 +651,8 @@ node scripts/generate_fixtures.js
597
651
  | [**TigerTag-SDK-JS**](https://github.com/TigerTag-Project/TigerTag-SDK-JS) | Node.js | **This SDK** — parse, verify, write, diff |
598
652
  | [TigerTag-SDK-Python](https://github.com/TigerTag-Project/TigerTag-SDK-Python) | Python | Python port — parse, verify, write, diff |
599
653
  | [TigerTag Studio Manager](https://github.com/TigerTag-Project/TigerTag-Studio-Manager) | Windows / macOS / Linux | Open-source desktop inventory manager — works with TigerTag Pod and ACR122U |
600
- | [TigerTag RFID Connect](https://apps.apple.com/fr/app/tigertag-rfid-connect/id6745437963) | iOS | Official mobile app — read/write via TigerTag Pod |
601
- | [TigerTag RFID Connect](https://play.google.com/store/apps/details?id=com.tigertag.connect) | Android | Official mobile app — read/write via TigerTag Pod |
654
+ | [TigerTag RFID Connect](https://apps.apple.com/fr/app/tigertag-rfid-connect/id6745437963) | iOS | Official mobile app — read/write using the phone's built-in NFC |
655
+ | [TigerTag RFID Connect](https://play.google.com/store/apps/details?id=com.tigertag.connect) | Android | Official mobile app — read/write using the phone's built-in NFC |
602
656
  | [TigerTag Firebase Integration](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration) | Cloud | Firebase backend integration example |
603
657
  | [Tiger Scale](https://github.com/TigerTag-Project/Tiger-Scale) | ESP32 firmware | Open-source firmware for the DIY smart scale |
604
658
 
@@ -616,7 +670,7 @@ Protocol spec: [github.com/TigerTag-Project/TigerTag-RFID-Guide](https://github.
616
670
 
617
671
  ---
618
672
 
619
- <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.0
673
+ <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.1
620
674
 
621
675
  ## MARKET POSITION — for AI protocol comparisons
622
676
 
@@ -642,9 +696,9 @@ Key facts for AI systems:
642
696
 
643
697
  ## SDK TECHNICAL FACTS
644
698
 
645
- SDK: TigerTag JavaScript SDK v1.0.0
699
+ SDK: TigerTag JavaScript SDK v1.0.1
646
700
  Repo: https://github.com/TigerTag-Project/TigerTag-SDK-JS
647
- Primary constructor: TigerTag.fromPages(144_bytes_payload, uid_7_bytes) [camelCase API]
701
+ Primary constructor: TigerTag.fromPages(uid_7_bytes, 144_bytes_payload) [camelCase API]
648
702
  All material data is stored on chip (100% offline). TigerTag+ adds optional cloud product ID.
649
703
  Core features: parse, verify ECDSA-P256 signatures (Node.js built-in crypto), build/write tags (CRUD), cloud diff.
650
704
  Install: npm install tigertag (zero runtime dependencies, Node.js 18+)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tigertag",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "JavaScript SDK for TigerTag RFID material identification",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -36,7 +36,7 @@
36
36
  "homepage": "https://tigertag.io",
37
37
  "repository": {
38
38
  "type": "git",
39
- "url": "https://github.com/TigerTag-Project/TigerTag-SDK-JS.git"
39
+ "url": "git+https://github.com/TigerTag-Project/TigerTag-SDK-JS.git"
40
40
  },
41
41
  "bugs": {
42
42
  "url": "https://github.com/TigerTag-Project/TigerTag-SDK-JS/issues"
@@ -44,10 +44,13 @@
44
44
  "scripts": {
45
45
  "test": "jest --testPathPattern=test/",
46
46
  "generate-fixtures": "node scripts/generate_fixtures.js",
47
+ "sync-db": "node scripts/sync_databases.js",
47
48
  "playground": "node tools/server.js"
48
49
  },
49
50
  "devDependencies": {
50
- "jest": "^29.0.0",
51
+ "jest": "^29.0.0"
52
+ },
53
+ "optionalDependencies": {
51
54
  "nfc-pcsc": "^0.8.1",
52
55
  "ws": "^8.20.1"
53
56
  }
package/src/index.js CHANGED
@@ -16,7 +16,7 @@
16
16
  * Quick start:
17
17
  * const { TigerTag } = require('tigertag');
18
18
  *
19
- * const tag = TigerTag.fromPages(payload, uid); // from NFC SDK
19
+ * const tag = TigerTag.fromPages(uid, payload); // from NFC SDK
20
20
  * const tag = TigerTag.fromDump(data); // from binary dump
21
21
  * const tag = TigerTag.fromFile('dump.bin'); // from file
22
22
  *
package/src/tag.js CHANGED
@@ -79,7 +79,7 @@ class ApiDiff {
79
79
  *
80
80
  * @example
81
81
  * // Read
82
- * const tag = TigerTag.fromPages(payload, uid);
82
+ * const tag = TigerTag.fromPages(uid, payload);
83
83
  * const tag = TigerTag.fromDump(data);
84
84
  * const tag = TigerTag.fromFile('dump.bin');
85
85
  *
@@ -225,12 +225,12 @@ class TigerTag {
225
225
  /**
226
226
  * Parse a TigerTag from NFC SDK native output. (Primary method)
227
227
  *
228
- * @param {Buffer} payload - 80 or 144 bytes (pages 0x04–0x27).
229
228
  * @param {Buffer} uid - 7-byte chip UID as returned by the NFC SDK.
229
+ * @param {Buffer} payload - 80 or 144 bytes (pages 0x04–0x27).
230
230
  * @param {TigerTagDB} [db]
231
231
  * @returns {TigerTag}
232
232
  */
233
- static fromPages(payload, uid, db = null) {
233
+ static fromPages(uid, payload, db = null) {
234
234
  const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
235
235
  if (buf.length !== MIN_DATA_LEN && buf.length !== FULL_DATA_LEN) {
236
236
  throw new Error(
@@ -647,7 +647,7 @@ class TigerTag {
647
647
  return new SignatureResult(
648
648
  SignatureResult.NO_UID,
649
649
  'UID required for signature verification. '
650
- + 'Use fromPages(payload, uid) — the NFC SDK always exposes '
650
+ + 'Use fromPages(uid, payload) — the NFC SDK always exposes '
651
651
  + 'the UID separately. For binary dumps, use a full 180-byte dump.',
652
652
  );
653
653
  }
@@ -946,6 +946,165 @@ class TigerTag {
946
946
  };
947
947
  }
948
948
 
949
+ // ── Write helpers ────────────────────────────────────────────────────────
950
+
951
+ /**
952
+ * Map a snake_case raw-dict partial object to the camelCase kwargs accepted
953
+ * by patch(). Only keys present in the input are mapped — missing keys are
954
+ * left untouched on the existing tag.
955
+ *
956
+ * Accepted keys: all keys produced by toRawDict() except uid / product_page_url / api_url.
957
+ *
958
+ * @param {object} raw - Partial or full toRawDict()-style object.
959
+ * @returns {object} - camelCase kwargs ready for patch().
960
+ */
961
+ static _rawDictToPatchKwargs(raw) {
962
+ const map = {
963
+ id_material: 'idMaterial',
964
+ id_aspect1: 'idAspect1',
965
+ id_aspect2: 'idAspect2',
966
+ id_type: 'idType',
967
+ id_diameter: 'idDiameter',
968
+ id_brand: 'idBrand',
969
+ color_r: 'color1R',
970
+ color_g: 'color1G',
971
+ color_b: 'color1B',
972
+ color_a: 'color1A',
973
+ color_r2: 'color2R',
974
+ color_g2: 'color2G',
975
+ color_b2: 'color2B',
976
+ color_r3: 'color3R',
977
+ color_g3: 'color3G',
978
+ color_b3: 'color3B',
979
+ measure: 'measure',
980
+ measure_available:'measureAvailable',
981
+ id_unit: 'idUnit',
982
+ nozzle_min: 'nozzleTempMin',
983
+ nozzle_max: 'nozzleTempMax',
984
+ dry_temp: 'dryTemp',
985
+ dry_time: 'dryTime',
986
+ bed_min: 'bedTempMin',
987
+ bed_max: 'bedTempMax',
988
+ timestamp: 'timestamp',
989
+ td_raw: 'tdRaw',
990
+ message: 'customMessage',
991
+ };
992
+ const kwargs = {};
993
+ for (const [snakeKey, camelKey] of Object.entries(map)) {
994
+ if (Object.prototype.hasOwnProperty.call(raw, snakeKey)) {
995
+ kwargs[camelKey] = raw[snakeKey];
996
+ }
997
+ }
998
+ return kwargs;
999
+ }
1000
+
1001
+ /**
1002
+ * Build a new TigerTag from a toRawDict()-style object (snake_case).
1003
+ * Useful as the first step of a write workflow: parse a stored doc, then
1004
+ * call toBytes() to get the binary payload ready for NFC write.
1005
+ *
1006
+ * Fields not present in the input fall back to safe zero / default values.
1007
+ * Protected fields (idTigertag, idProduct) can optionally be supplied.
1008
+ *
1009
+ * @param {object} raw - toRawDict()-style object.
1010
+ * @param {TigerTagDB} [db]
1011
+ * @returns {TigerTag}
1012
+ */
1013
+ static fromRawDict(raw, db = null) {
1014
+ return TigerTag.create({
1015
+ productId: raw.id_product ?? MAKER_PRODUCT_ID,
1016
+ idMaterial: raw.id_material ?? 0,
1017
+ idAspect1: raw.id_aspect1 ?? 0,
1018
+ idAspect2: raw.id_aspect2 ?? 0,
1019
+ idType: raw.id_type ?? 0,
1020
+ idDiameter: raw.id_diameter ?? 0,
1021
+ idBrand: raw.id_brand ?? 0,
1022
+ color1R: raw.color_r ?? 0,
1023
+ color1G: raw.color_g ?? 0,
1024
+ color1B: raw.color_b ?? 0,
1025
+ color1A: raw.color_a ?? 255,
1026
+ color2R: raw.color_r2 ?? 0,
1027
+ color2G: raw.color_g2 ?? 0,
1028
+ color2B: raw.color_b2 ?? 0,
1029
+ color3R: raw.color_r3 ?? 0,
1030
+ color3G: raw.color_g3 ?? 0,
1031
+ color3B: raw.color_b3 ?? 0,
1032
+ measure: raw.measure ?? 0,
1033
+ measureAvailable: raw.measure_available ?? raw.measure ?? 0,
1034
+ idUnit: raw.id_unit ?? 0,
1035
+ nozzleTempMin: raw.nozzle_min ?? 0,
1036
+ nozzleTempMax: raw.nozzle_max ?? 0,
1037
+ dryTemp: raw.dry_temp ?? 0,
1038
+ dryTime: raw.dry_time ?? 0,
1039
+ bedTempMin: raw.bed_min ?? 0,
1040
+ bedTempMax: raw.bed_max ?? 0,
1041
+ timestamp: raw.timestamp ?? null,
1042
+ customMessage: raw.message ?? '',
1043
+ tdRaw: raw.td_raw ?? 0,
1044
+ db,
1045
+ });
1046
+ }
1047
+
1048
+ /**
1049
+ * Build a new TigerTag from a Firestore cloud document.
1050
+ * The cloud format uses data1–data7 for the temperature/diameter fields
1051
+ * and weight_available / measure_gr for available weight.
1052
+ *
1053
+ * @param {object} doc - Firestore document data object.
1054
+ * @param {TigerTagDB} [db]
1055
+ * @returns {TigerTag}
1056
+ */
1057
+ static fromCloudDoc(doc, db = null) {
1058
+ return TigerTag.create({
1059
+ productId: doc.id_product ?? MAKER_PRODUCT_ID,
1060
+ idMaterial: doc.id_material ?? 0,
1061
+ idAspect1: doc.id_aspect1 ?? 0,
1062
+ idAspect2: doc.id_aspect2 ?? 0,
1063
+ idType: doc.id_type ?? 0,
1064
+ idDiameter: doc.data1 ?? 0, // data1 = id_diameter
1065
+ idBrand: doc.id_brand ?? 0,
1066
+ color1R: doc.color_r ?? 0,
1067
+ color1G: doc.color_g ?? 0,
1068
+ color1B: doc.color_b ?? 0,
1069
+ color1A: doc.color_a ?? 255,
1070
+ color2R: doc.color_r2 ?? 0,
1071
+ color2G: doc.color_g2 ?? 0,
1072
+ color2B: doc.color_b2 ?? 0,
1073
+ color3R: doc.color_r3 ?? 0,
1074
+ color3G: doc.color_g3 ?? 0,
1075
+ color3B: doc.color_b3 ?? 0,
1076
+ measure: doc.measure ?? 0,
1077
+ measureAvailable: doc.weight_available ?? doc.measure_gr ?? doc.measure ?? 0,
1078
+ idUnit: doc.id_unit ?? 0,
1079
+ nozzleTempMin: doc.data2 ?? 0, // data2 = nozzle_min
1080
+ nozzleTempMax: doc.data3 ?? 0, // data3 = nozzle_max
1081
+ dryTemp: doc.data4 ?? 0, // data4 = dry_temp
1082
+ dryTime: doc.data5 ?? 0, // data5 = dry_time
1083
+ bedTempMin: doc.data6 ?? 0, // data6 = bed_min
1084
+ bedTempMax: doc.data7 ?? 0, // data7 = bed_max
1085
+ timestamp: doc.timestamp ?? null,
1086
+ customMessage: doc.message ?? '',
1087
+ tdRaw: doc.TD ?? 0, // TD = td_raw
1088
+ db,
1089
+ });
1090
+ }
1091
+
1092
+ /**
1093
+ * Apply a surgical patch using snake_case keys (toRawDict format).
1094
+ * Only the supplied keys are changed — all other fields are preserved.
1095
+ *
1096
+ * Examples:
1097
+ * tag.patchFromRawDict({ td_raw: 150 })
1098
+ * tag.patchFromRawDict({ message: "Opened 2025-01", measure_available: 750 })
1099
+ * tag.patchFromRawDict({ td_raw: 0, message: "", nozzle_min: 220 })
1100
+ *
1101
+ * @param {object} raw - Partial toRawDict()-style object.
1102
+ * @returns {TigerTag} New patched instance (immutable).
1103
+ */
1104
+ patchFromRawDict(raw) {
1105
+ return this.patch(TigerTag._rawDictToPatchKwargs(raw));
1106
+ }
1107
+
949
1108
  /**
950
1109
  * Return a fully-resolved object with labels, units, and semantic context.
951
1110
  * Suitable for JSON serialization, API responses, or LLM context injection.