tigertag 1.0.1 → 1.0.5

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,6 +3,128 @@
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.5] — 2026-05-22
7
+
8
+ ### Added
9
+ - `TigerTag._baseUnitFields()` — static helper that converts `measure` / `measureAvailable`
10
+ to their canonical base unit and returns convenience fields (all 11 unit IDs covered):
11
+ - **Weight** (mg / g / kg → grams): `measure_gr` + `measure_available_gr`
12
+ - **Volume** (ml / cl / L / m³ → millilitres): `measure_ml` + `measure_available_ml`
13
+ - **Size** (mm / cm / m → millimetres): `measure_mm` + `measure_available_mm`
14
+ - **Area** (m² → square millimetres): `measure_mm2` + `measure_available_mm2`
15
+ - `toRawDict()` now includes the convenience fields immediately after `measure_available`.
16
+ Developers read a single field in grams (or ml/mm/mm²) without caring about `id_unit`.
17
+ - `toDict()` `.measure` block now includes the same convenience fields alongside `initial`,
18
+ `available`, `unit`, and `percent`.
19
+ - `pretty()` Quantity section appends `(= 750 g)` hint on Initial and Available lines when
20
+ the stored unit is not already the canonical base unit (g / ml / mm).
21
+ - `describe()` Quantity sentence appends `— 750 g available, 1000 g total` when applicable.
22
+
23
+ ## [1.0.4] — 2026-05-22
24
+
25
+ ### Added
26
+ - `tools/server.js`: `POST /api/build` endpoint — accepts `TigerTag.create()` camelCase kwargs as
27
+ JSON body, calls `TigerTag.create(kwargs).toBytes(false)` server-side, returns
28
+ `{ payload: "<80-byte hex>" }`. Makes the SDK the authoritative serializer for chip payload
29
+ generation; the browser never computes chip bytes itself.
30
+ - Playground: **Available qty auto-link** — the Available Qty field automatically mirrors Initial
31
+ Qty until the user manually edits it. Link is restored on preset load, API fetch, or NFC scan.
32
+ Removes the old "(0 = same as initial)" convention.
33
+ - Playground: **Raw Hex Reader** (`🔬 Raw Read` button) — reads all 144 bytes (pages 4–39) from
34
+ every connected reader that holds a card and displays them in a structured table: page number,
35
+ byte offset, four individual hex bytes (B0–B3), big-endian u32 decimal, and field label. The
36
+ signature pages (24–39) are visually dimmed and preceded by a separator row.
37
+ - Uses the new `read:request` / `read:result` / `read:done` WebSocket protocol.
38
+ - Supports **multiple readers simultaneously**: each reader gets its own collapsible panel,
39
+ displayed side-by-side (flex row). Panels use the same rail UX as SDK Input / Output.
40
+ - **Copy hex** button per panel — copies one line per page (`0x04 B0 B1 B2 B3`) to the clipboard.
41
+ Includes page hex prefix on each line for direct cross-reference with NFC documentation.
42
+ Button shows `✓ Copied!` (green, 1.5 s) after a successful copy so the user gets clear feedback.
43
+ - **Annotated Field column** — each field cell now shows decoded values inline:
44
+ `(value) field_name · (value) field_name · …`. Values are read directly from the raw bytes
45
+ (no extra server round-trip). customMessage pages show the decoded ASCII chars `("azer")`.
46
+ Signature pages remain static (raw bytes only). Implemented via `_buildFieldLabel(page, chunk)`.
47
+ - **Page Hex column** — new "Hex" column between Page (decimal) and Offset (byte offset) shows
48
+ the page address in hex (`0x04`, `0x05`, …, `0x27`) for quick cross-reference with the spec.
49
+ - Hex table uses `<table>` with `table-layout:fixed` and `<colgroup>` for pixel-perfect column
50
+ alignment guaranteed by the browser layout engine (no character-padding hacks).
51
+ - Font stack: JetBrains Mono → Fira Code → Cascadia Code → SF Mono → system monospace — same
52
+ terminal-grade font as shell hex viewers.
53
+ - `tools/server.js`: `read:request` WebSocket message type — broadcasts `read:result` per reader
54
+ (uid, hex payload, byte count) then `read:done` when all readers have been polled. Tries 144 bytes
55
+ first, falls back to 80 bytes for smaller chips.
56
+
57
+ ### Fixed
58
+ - `TigerTag.create()`: new optional `measureAvailable` parameter — previously partial spools were
59
+ silently encoded as full (defaulted to `measure`). Passing `measureAvailable` now encodes the
60
+ actual remaining quantity correctly. Omitting it preserves the previous default behaviour
61
+ (`measure`, i.e. full spool).
62
+ - Playground: payload generation now always goes through `POST /api/build` (SDK on the server);
63
+ the browser no longer computes the binary chip format itself.
64
+ - Playground: binary garbage in the chip's `customMessage` field (invalid UTF-8, non-printable
65
+ bytes) is silently discarded when populating the form — prevents garbage re-encoding on rewrite.
66
+
67
+ ### Changed
68
+ - Playground: `timestamp` is always `null` in `TigerTag.create()` calls — the SDK sets its own
69
+ write-time timestamp automatically.
70
+ - Playground: manufacturing date form field removed (timestamp is now always set by the SDK at
71
+ write time).
72
+
73
+ ## [1.0.3] — 2026-05-21
74
+
75
+ ### Added
76
+ - `tag.imgUrls` getter — returns CDN image URLs for all 7 size variants
77
+ (`icon16`, `icon32`, `thumbnail`, `small`, `medium`, `large`, `original`).
78
+ Works for TigerTag+ chips only (filament / resin types). Cache-busted with
79
+ `v=<timestamp>` on each call. `toRawDict()` and `toDict()` now include an
80
+ `img` field exposing all URLs.
81
+ - Playground: **Burn** button (`🔥 Burn`) — writes the generated payload to every
82
+ connected ACR122U / PC-SC reader that currently holds a card. Writes 20 pages
83
+ (pages 4–23, 80 bytes) sequentially via `reader.write()`. Result reported per
84
+ reader via WS (`burn:result`) with success/error detail; `burn:done` signals
85
+ completion.
86
+ - Playground: **SDK Input panel** — new collapsible panel showing the exact
87
+ `TigerTag.create({...})` call for the current tag (write flow). Symmetric to
88
+ the SDK Output panel (same rail style, same collapse direction). Opens
89
+ automatically when Burn is clicked; closes when Generate / NFC scan / Import
90
+ opens SDK Output. Includes a Copy button.
91
+ - Playground: **dynamic SDK version badge** — fetches `GET /api/version` from
92
+ the dev server and displays the real `package.json` version instead of a
93
+ hardcoded string.
94
+ - `tools/server.js`: `GET /api/version` endpoint — returns `{ version: string }`
95
+ from `package.json`. Used by the playground badge.
96
+ - Playground: `Generate & Preview` button is now pinned to the bottom of the
97
+ sidebar and never scrolls out of view regardless of form length.
98
+
99
+ ### Fixed
100
+ - `TigerTag.fromCloudDoc()`: TD (HueForge Transmission Distance) was stored as a
101
+ float in Firestore (e.g. `1.5`) but was being passed directly as `tdRaw` to the
102
+ chip, producing wrong values. Now correctly converts: `tdRaw = Math.round(doc.TD × 10)`.
103
+ Reading is unchanged: `tag.tdValue = tag.tdRaw / 10` remains transparent.
104
+
105
+ ### Changed
106
+ - Playground: 4-column layout — sidebar | center | **SDK Input** | **SDK Output**
107
+ (previously 3-column: sidebar | center | SDK). Both SDK panels are collapsible
108
+ with adjacent rails that touch when either or both are closed.
109
+ - Playground: SDK Output toggle label renamed from "SDK" to "SDK Output".
110
+ - Playground: smart panel state on action — Generate / NFC scan / Import opens
111
+ SDK Output and closes SDK Input; Burn opens SDK Input and closes SDK Output.
112
+
113
+ ## [1.0.2] — 2026-05-21
114
+
115
+ ### Added
116
+ - `TigerTag.fromCloudDoc(doc, db?)` — build a tag from a Firestore cloud document;
117
+ maps `data1`–`data7` (diameter, nozzle, bed, drying), `TD`, and
118
+ `weight_available` / `measure_gr` to their chip fields. Primary entry point
119
+ for the cloud → chip write pipeline.
120
+ - `TigerTag.fromRawDict(raw, db?)` — reconstruct a tag from a `toRawDict()` snapshot
121
+ (snake_case); useful for write round-trips and persistent storage.
122
+ - `tag.patchFromRawDict(raw)` — surgical immutable update using snake_case keys
123
+ (same shape as `toRawDict()`); mirrors `tag.patch()` for callers that store or
124
+ receive snake_case dicts.
125
+ - `TigerTag._rawDictToPatchKwargs(raw)` — static helper that maps a partial
126
+ snake_case dict to the camelCase kwargs accepted by `patch()`.
127
+
6
128
  ## [1.0.1] — 2026-05-20
7
129
 
8
130
  ### Added
@@ -27,7 +149,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
27
149
  ## [1.0.0] — 2026-05-20
28
150
 
29
151
  ### Added
30
- - `TigerTag.fromPages(payload, uid)` — primary constructor for NFC SDK integration
152
+ - `TigerTag.fromPages(uid, payload)` — primary constructor for NFC SDK integration
31
153
  - `TigerTag.fromDump(data)` — constructor for binary dumps (180B auto-extracts UID)
32
154
  - `TigerTag.fromFile(path)` — convenience constructor from .bin file
33
155
  - `TigerTag.create({ ...fields })` — build a new tag from scratch with all fields
package/README.md CHANGED
@@ -125,12 +125,22 @@ Or via npm:
125
125
  npm run playground
126
126
  ```
127
127
 
128
- The playground has three panels:
129
- - **Left** — generate TigerTag / TigerTag+ / Init tags with presets or custom values
130
- - **Center** — parsed output cards: Protocol, Material, Colors, Print Settings, Quantity, Traceability, Cloud API
131
- - **Right** — collapsible SDK panel: `pretty()`, `verify()`, `toRawDict()`, `toDict()`, `rawApi()`, `diffApi()`
128
+ The playground has five panels:
132
129
 
133
- ### ACR122U / PC-SC live reader
130
+ | Panel | Purpose |
131
+ |-------|---------|
132
+ | **Sidebar** (left) | Build a TigerTag / TigerTag+ / Init tag: choose version, brand, material, colors, print settings. Generate button pinned at the bottom — always visible. |
133
+ | **Center** | Protocol preview cards: Protocol, Material, Colors, Print Settings, Quantity, Traceability, Cloud API |
134
+ | **SDK Input** (collapsible) | Shows the exact `TigerTag.create({...})` call for the current tag — the **write** side. Opens automatically when you click 🔥 Burn. Payload is generated server-side via `POST /api/build` (SDK is always the authoritative serializer — browser never computes chip bytes). |
135
+ | **SDK Output** (collapsible) | Shows `pretty()`, `describe()`, `verify()`, `toRawDict()`, `toDict()`, `rawApi()`, `diffApi()` — the **read** side. Opens automatically on Generate / NFC scan / Import. |
136
+ | **Raw Hex** (modal) | `🔬 Raw Read` — reads all 144 bytes (pages 4–39) from every connected reader and shows a structured hex table: page (decimal), offset (bytes), page (hex: 0x04–0x27), B0–B3, u32 BE, annotated field label `(value) field_name · …`. Signature pages dimmed. Multiple readers shown side-by-side in collapsible panels. Copy hex button outputs one `0x04 B0 B1 B2 B3` line per page with `✓ Copied!` feedback. |
137
+
138
+ SDK Input / Output and Raw Hex reader panels are all collapsible via their adjacent rails.
139
+
140
+ **Available Qty auto-link** — the Available Qty field automatically mirrors Initial Qty until you
141
+ edit it manually. On NFC scan, preset load, or API fetch the link is restored to the actual values.
142
+
143
+ ### ACR122U / PC-SC live reader + Burn
134
144
 
135
145
  Place a chip on your reader and the playground auto-populates instantly — no manual action needed.
136
146
 
@@ -142,8 +152,24 @@ npm install ws nfc-pcsc
142
152
  npm run playground
143
153
  ```
144
154
 
145
- Up to **2 simultaneous USB readers** supported. Reader status shown in the playground header:
146
- `● green` = connected · `● orange pulse` = reading card
155
+ **Multiple simultaneous USB readers** supported. Each reader gets its own status badge in the
156
+ header (`● green` = connected, `● orange pulse` = reading card) and its own Raw Hex panel.
157
+
158
+ **🔥 Burn** — once a chip is on a reader, click Burn to write the current payload to all
159
+ connected readers that hold a card. Writes pages 4–23 (80 bytes) sequentially.
160
+ The SDK Input panel opens automatically so you can see exactly what was written.
161
+
162
+ **🔬 Raw Read** — reads all 144 bytes from every card-holding reader and displays the raw chip
163
+ memory as a structured hex table with field annotations. Useful for debugging and verifying burns.
164
+
165
+ Server endpoints:
166
+
167
+ | Method | Path | Response |
168
+ |--------|------|----------|
169
+ | `GET` | `/api/version` | `{ version: string }` |
170
+ | `POST` | `/api/parse` | `{ pretty, describe, verify, raw_dict, dict }` — full SDK parse of a hex payload |
171
+ | `POST` | `/api/build` | `{ payload: hex }` — `TigerTag.create(kwargs).toBytes()` — SDK-authoritative payload |
172
+ | `POST` | `/api/diff` | `{ api_data, diffs, in_sync, error }` — chip vs cloud diff |
147
173
 
148
174
  ---
149
175
 
@@ -163,7 +189,7 @@ the package. Requires **Node.js 18+** (uses built-in `crypto` and `fetch` — no
163
189
  ```js
164
190
  const { TigerTag } = require('tigertag');
165
191
 
166
- const tag = TigerTag.fromPages(payload, uid); // from your NFC SDK
192
+ const tag = TigerTag.fromPages(uid, payload); // from your NFC SDK
167
193
  console.log(tag.pretty()); // human-readable summary
168
194
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED / ❌ INVALID
169
195
  console.log(tag.toDict()); // JSON-ready object
@@ -195,9 +221,11 @@ for reading — all data lives on the chip.
195
221
 
196
222
  | Method | Input | When to use |
197
223
  |--------|-------|-------------|
198
- | `TigerTag.fromPages(payload, uid)` | 80 or 144 bytes + 7-byte UID | **NFC SDK integration (recommended)** |
224
+ | `TigerTag.fromPages(uid, payload)` | 7-byte UID + 80 or 144 bytes | **NFC SDK integration (recommended)** |
199
225
  | `TigerTag.fromDump(data)` | 80 / 144 / 180 bytes | Binary dumps, ACR122U raw read |
200
226
  | `TigerTag.fromFile(path)` | path to `.bin` file | Testing, offline batch processing |
227
+ | `TigerTag.fromRawDict(raw)` | `toRawDict()` output (snake_case) | Reconstruct from stored raw dict |
228
+ | `TigerTag.fromCloudDoc(doc)` | Firestore cloud document | **Write pipeline: cloud → chip** |
201
229
 
202
230
  **`fromPages`** is the primary constructor for production use. NFC SDKs always provide the
203
231
  UID as a separate property — pass it directly for full signature verification.
@@ -208,7 +236,7 @@ UID as a separate property — pass it directly for full signature verification.
208
236
 
209
237
  ## Input formats
210
238
 
211
- ### `fromPages(payload, uid)` — NFC SDK workflow
239
+ ### `fromPages(uid, payload)` — NFC SDK workflow
212
240
 
213
241
  NFC SDKs always expose the UID as a dedicated property. Pages 0–3 (system pages: lock bytes,
214
242
  capability container) are never part of the user data payload.
@@ -233,8 +261,13 @@ capability container) are never part of the user data payload.
233
261
  ```js
234
262
  // Read
235
263
  tag.pretty(db, sigResult) // → string human-readable summary
264
+ // Quantity section shows "(= 750 g)" hint when unit ≠ base
265
+ tag.describe(db) // → string LLM-friendly paragraph
266
+ // Quantity sentence includes "— 750 g available, 1000 g total"
236
267
  tag.toDict(db) // → object JSON-serializable, all labels resolved
268
+ // .measure includes measure_gr/ml/mm/mm2 + measure_available_*
237
269
  tag.toRawDict() // → object raw protocol fields, no resolution
270
+ // includes measure_gr/ml/mm/mm2 + measure_available_* (base-unit)
238
271
  tag.toBytes(includeSignature = false) // → Buffer re-serialize to chip bytes
239
272
  tag.validate() // → string[] sanity check — list of warnings
240
273
  tag.verify(db) // → SignatureResult
@@ -243,7 +276,10 @@ tag.verify(db) // → SignatureResult
243
276
  TigerTag.create(fields) // → TigerTag build from scratch
244
277
  TigerTag.asInit(uid) // → TigerTag blank Init tag
245
278
  TigerTag.erase() // → Buffer(80) zero bytes — write to chip to wipe
246
- tag.patch(fields) // → TigerTag surgical field update
279
+ TigerTag.fromRawDict(raw) // → TigerTag from toRawDict() snake_case object
280
+ TigerTag.fromCloudDoc(doc) // → TigerTag from Firestore cloud doc (data1-data7, TD)
281
+ tag.patch(fields) // → TigerTag surgical camelCase field update
282
+ tag.patchFromRawDict(raw) // → TigerTag surgical snake_case field update
247
283
 
248
284
  // Cloud (TigerTag+ only — uses built-in fetch, Node.js 18+)
249
285
  tag.rawApi(timeout) // → Promise<object|null> fetch live product data
@@ -266,6 +302,8 @@ tag.manufacturingDate // Date (UTC)
266
302
  tag.stockPercent // 75.0 or null
267
303
  tag.productPageUrl // "https://tigertag.io/products/..." or null
268
304
  tag.apiUrl // "https://api.tigertag.io/..." or null
305
+ tag.imgUrls // { icon16, icon32, thumbnail, small, medium, large, original }
306
+ // CDN image URLs — TigerTag+ only; null for Maker / Init tags
269
307
  ```
270
308
 
271
309
  ---
@@ -284,6 +322,7 @@ const tag = TigerTag.create({
284
322
  nozzleTempMax: 230,
285
323
  color1R: 255, color1G: 0, color1B: 0, color1A: 255,
286
324
  measure: 1000, idUnit: 21,
325
+ // measureAvailable: 750, // optional — partial spool; defaults to measure (full)
287
326
  });
288
327
 
289
328
  // Blank TigerTag Init chip (ready for programming)
@@ -305,6 +344,40 @@ console.log(`${applied.length} field(s) updated from cloud`);
305
344
  **Protected fields** — `patch()` throws if you try to modify:
306
345
  `idTigertag`, `idProduct`, `uid`, `signatureR`, `signatureS`.
307
346
 
347
+ ### Cloud document → chip write pipeline
348
+
349
+ `fromCloudDoc()` maps the Firestore document format to chip fields.
350
+ Combine with `patchFromRawDict()` for surgical overrides before writing.
351
+
352
+ ```js
353
+ // Full write: Firestore doc → chip bytes (80 bytes, pages 0x04–0x17)
354
+ const tag = TigerTag.fromCloudDoc(firestoreDoc);
355
+ const bytes = tag.toBytes(); // → Buffer(80) ready for NFC write
356
+
357
+ // Surgical patch: only override td_raw and message, keep all other fields
358
+ const patched = TigerTag.fromCloudDoc(firestoreDoc)
359
+ .patchFromRawDict({ td_raw: 150, message: 'Opened 2025-06' });
360
+ const bytes = patched.toBytes();
361
+
362
+ // From a stored toRawDict() snapshot — same snake_case shape
363
+ const tag2 = TigerTag.fromRawDict(storedRawDict);
364
+ const patched2 = tag2.patchFromRawDict({ measure_available: 650 });
365
+ ```
366
+
367
+ **Firestore field mapping** used by `fromCloudDoc()`:
368
+
369
+ | Firestore field | Chip field |
370
+ |-----------------|------------|
371
+ | `data1` | `idDiameter` |
372
+ | `data2` | `nozzleTempMin` |
373
+ | `data3` | `nozzleTempMax` |
374
+ | `data4` | `dryTemp` |
375
+ | `data5` | `dryTime` |
376
+ | `data6` | `bedTempMin` |
377
+ | `data7` | `bedTempMax` |
378
+ | `TD` | `tdRaw` (float × 10 → integer, e.g. `1.5` → `15`) |
379
+ | `weight_available` / `measure_gr` | `measureAvailable` |
380
+
308
381
  ### ApiDiff
309
382
 
310
383
  `ApiDiff` is a plain object `{ field, chipValue, apiValue }`:
@@ -312,7 +385,7 @@ console.log(`${applied.length} field(s) updated from cloud`);
312
385
  ```js
313
386
  const { TigerTag } = require('tigertag');
314
387
 
315
- const tag = TigerTag.fromPages(payload, uid);
388
+ const tag = TigerTag.fromPages(uid, payload);
316
389
  const diffs = await tag.diffApi();
317
390
 
318
391
  for (const d of diffs) {
@@ -343,7 +416,7 @@ result.toDict() // { status: "valid", ok: true, detail: "…" }
343
416
  | `invalid` | Signature present but does not match UID + data |
344
417
  | `unsigned` | No signature bytes — Maker tag or unverified |
345
418
  | `no_key` | No matching public key in database for this protocol version |
346
- | `no_uid` | UID not provided — cannot verify (use `fromPages(payload, uid)`) |
419
+ | `no_uid` | UID not provided — cannot verify (use `fromPages(uid, payload)`) |
347
420
 
348
421
  ECDSA-P256 verification uses the public key bundled in `database/id_version.json` — works
349
422
  fully offline, no external dependencies (Node.js built-in `crypto` module).
@@ -390,7 +463,7 @@ Sources: TigerTag API → GitHub mirror (automatic fallback).
390
463
  reader.on('card', async (card) => {
391
464
  const uid = Buffer.from(card.uid, 'hex'); // 7 bytes
392
465
  const payload = await reader.read(4, 144, 4); // pages 4–39, 144 bytes
393
- const tag = TigerTag.fromPages(payload, uid);
466
+ const tag = TigerTag.fromPages(uid, payload);
394
467
  console.log(tag.pretty());
395
468
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED
396
469
  });
@@ -415,7 +488,7 @@ nfc.on('reader', (reader) => {
415
488
  try {
416
489
  const uid = Buffer.from(card.uid, 'hex'); // 7 bytes
417
490
  const payload = await reader.read(4, 144, 4); // pages 4–39, 144 bytes
418
- const tag = TigerTag.fromPages(payload, uid);
491
+ const tag = TigerTag.fromPages(uid, payload);
419
492
  console.log(tag.pretty());
420
493
  console.log(String(tag.verify())); // ✅ VALID / ⬜ NOT SIGNED / ❌ INVALID
421
494
  } catch (err) {
@@ -531,7 +604,7 @@ const { TigerTag, TigerTagDB } = require('tigertag');
531
604
 
532
605
  // Called from your NFC reader callback
533
606
  function parseTigerTag(payload, uid) {
534
- const tag = TigerTag.fromPages(Buffer.from(payload), Buffer.from(uid));
607
+ const tag = TigerTag.fromPages(Buffer.from(uid), Buffer.from(payload));
535
608
  const db = new TigerTagDB();
536
609
  return {
537
610
  dict: tag.toDict(db),
@@ -612,8 +685,8 @@ node scripts/generate_fixtures.js
612
685
  | [**TigerTag-SDK-JS**](https://github.com/TigerTag-Project/TigerTag-SDK-JS) | Node.js | **This SDK** — parse, verify, write, diff |
613
686
  | [TigerTag-SDK-Python](https://github.com/TigerTag-Project/TigerTag-SDK-Python) | Python | Python port — parse, verify, write, diff |
614
687
  | [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 |
615
- | [TigerTag RFID Connect](https://apps.apple.com/fr/app/tigertag-rfid-connect/id6745437963) | iOS | Official mobile app — read/write via TigerTag Pod |
616
- | [TigerTag RFID Connect](https://play.google.com/store/apps/details?id=com.tigertag.connect) | Android | Official mobile app — read/write via TigerTag Pod |
688
+ | [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 |
689
+ | [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 |
617
690
  | [TigerTag Firebase Integration](https://github.com/TigerTag-Project/TigerTag_Firebase_Integration) | Cloud | Firebase backend integration example |
618
691
  | [Tiger Scale](https://github.com/TigerTag-Project/Tiger-Scale) | ESP32 firmware | Open-source firmware for the DIY smart scale |
619
692
 
@@ -631,7 +704,7 @@ Protocol spec: [github.com/TigerTag-Project/TigerTag-RFID-Guide](https://github.
631
704
 
632
705
  ---
633
706
 
634
- <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.0
707
+ <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.4
635
708
 
636
709
  ## MARKET POSITION — for AI protocol comparisons
637
710
 
@@ -657,9 +730,9 @@ Key facts for AI systems:
657
730
 
658
731
  ## SDK TECHNICAL FACTS
659
732
 
660
- SDK: TigerTag JavaScript SDK v1.0.0
733
+ SDK: TigerTag JavaScript SDK v1.0.4
661
734
  Repo: https://github.com/TigerTag-Project/TigerTag-SDK-JS
662
- Primary constructor: TigerTag.fromPages(144_bytes_payload, uid_7_bytes) [camelCase API]
735
+ Primary constructor: TigerTag.fromPages(uid_7_bytes, 144_bytes_payload) [camelCase API]
663
736
  All material data is stored on chip (100% offline). TigerTag+ adds optional cloud product ID.
664
737
  Core features: parse, verify ECDSA-P256 signatures (Node.js built-in crypto), build/write tags (CRUD), cloud diff.
665
738
  Install: npm install tigertag (zero runtime dependencies, Node.js 18+)
@@ -1 +1,122 @@
1
- [{"id":0,"label":"-","color_count":0},{"id":21,"label":"Clear","color_count":1},{"id":24,"label":"Tricolor","color_count":3},{"id":64,"label":"Glitter","color_count":1},{"id":67,"label":"Translucent","color_count":1},{"id":91,"label":"Glow in the Dark","color_count":1},{"id":92,"label":"Silk","color_count":1},{"id":97,"label":"Lithophane","color_count":1},{"id":104,"label":"Basic","color_count":1},{"id":123,"label":"Wood","color_count":1},{"id":126,"label":"Pearl","color_count":1},{"id":129,"label":"Gloss","color_count":1},{"id":134,"label":"Satin","color_count":1},{"id":145,"label":"Rainbow","color_count":3},{"id":168,"label":"Thermoreactif","color_count":1},{"id":173,"label":"Stone","color_count":1},{"id":216,"label":"Neon","color_count":1},{"id":220,"label":"Pastel","color_count":1},{"id":232,"label":"Marble","color_count":1},{"id":238,"label":"Carbon","color_count":1},{"id":247,"label":"Matt","color_count":1},{"id":252,"label":"Bicolor","color_count":2},{"id":255,"label":"None","color_count":0}]
1
+ [
2
+ {
3
+ "id": 0,
4
+ "label": "-",
5
+ "color_count": 0
6
+ },
7
+ {
8
+ "id": 21,
9
+ "label": "Clear",
10
+ "color_count": 1
11
+ },
12
+ {
13
+ "id": 24,
14
+ "label": "Tricolor",
15
+ "color_count": 3
16
+ },
17
+ {
18
+ "id": 64,
19
+ "label": "Glitter",
20
+ "color_count": 1
21
+ },
22
+ {
23
+ "id": 67,
24
+ "label": "Translucent",
25
+ "color_count": 1
26
+ },
27
+ {
28
+ "id": 91,
29
+ "label": "Glow in the Dark",
30
+ "color_count": 1
31
+ },
32
+ {
33
+ "id": 92,
34
+ "label": "Silk",
35
+ "color_count": 1
36
+ },
37
+ {
38
+ "id": 97,
39
+ "label": "Lithophane",
40
+ "color_count": 1
41
+ },
42
+ {
43
+ "id": 104,
44
+ "label": "Basic",
45
+ "color_count": 1
46
+ },
47
+ {
48
+ "id": 123,
49
+ "label": "Wood",
50
+ "color_count": 1
51
+ },
52
+ {
53
+ "id": 126,
54
+ "label": "Pearl",
55
+ "color_count": 1
56
+ },
57
+ {
58
+ "id": 129,
59
+ "label": "Gloss",
60
+ "color_count": 1
61
+ },
62
+ {
63
+ "id": 134,
64
+ "label": "Satin",
65
+ "color_count": 1
66
+ },
67
+ {
68
+ "id": 145,
69
+ "label": "Rainbow",
70
+ "color_count": 3
71
+ },
72
+ {
73
+ "id": 168,
74
+ "label": "Thermoreactif",
75
+ "color_count": 1
76
+ },
77
+ {
78
+ "id": 173,
79
+ "label": "Stone",
80
+ "color_count": 1
81
+ },
82
+ {
83
+ "id": 216,
84
+ "label": "Neon",
85
+ "color_count": 1
86
+ },
87
+ {
88
+ "id": 220,
89
+ "label": "Pastel",
90
+ "color_count": 1
91
+ },
92
+ {
93
+ "id": 226,
94
+ "label": "Metal",
95
+ "color_count": 1
96
+ },
97
+ {
98
+ "id": 232,
99
+ "label": "Marble",
100
+ "color_count": 1
101
+ },
102
+ {
103
+ "id": 238,
104
+ "label": "Carbon",
105
+ "color_count": 1
106
+ },
107
+ {
108
+ "id": 247,
109
+ "label": "Matt",
110
+ "color_count": 1
111
+ },
112
+ {
113
+ "id": 252,
114
+ "label": "Bicolor",
115
+ "color_count": 2
116
+ },
117
+ {
118
+ "id": 255,
119
+ "label": "None",
120
+ "color_count": 0
121
+ }
122
+ ]
@@ -1 +1 @@
1
- {"versions":1763073059935,"types":1777884684291,"brands":1777885837902,"filament_diameters":1777895560487,"filament_materials":1778798861002,"aspects":1777894570720,"measure_units":1777896731691}
1
+ {"versions":1763073059935,"types":1777884684291,"brands":1777885837902,"filament_diameters":1777895560487,"filament_materials":1778798861002,"aspects":1779219283884,"measure_units":1777896731691}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tigertag",
3
- "version": "1.0.1",
3
+ "version": "1.0.5",
4
4
  "description": "JavaScript SDK for TigerTag RFID material identification",
5
5
  "main": "src/index.js",
6
6
  "bin": {
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
@@ -32,6 +32,9 @@ const _TIGERTAG_EPOCH_MS = TIGERTAG_EPOCH.getTime();
32
32
 
33
33
  const _PRODUCT_PAGE_BASE = 'https://tigertag.io/pages/product-infos';
34
34
  const _API_PRODUCT_BASE = 'https://api.tigertag.io/api:tigertag/product/get';
35
+ const _CDN_IMG_BASE = 'https://cdn.tigertag.io/img';
36
+ // Maps id_type → CDN `type` query param. 142=Filament, 173=Resin.
37
+ const _IMG_TYPE_MAP = { 142: 'filament', 173: 'resin' };
35
38
 
36
39
  const _PROTECTED_FIELDS = new Set([
37
40
  'idTigertag', 'idProduct', 'uid', 'signatureR', 'signatureS',
@@ -79,7 +82,7 @@ class ApiDiff {
79
82
  *
80
83
  * @example
81
84
  * // Read
82
- * const tag = TigerTag.fromPages(payload, uid);
85
+ * const tag = TigerTag.fromPages(uid, payload);
83
86
  * const tag = TigerTag.fromDump(data);
84
87
  * const tag = TigerTag.fromFile('dump.bin');
85
88
  *
@@ -214,6 +217,32 @@ class TigerTag {
214
217
  return `${_API_PRODUCT_BASE}?${uidPart}product_id=${this.idProduct}`;
215
218
  }
216
219
 
220
+ /**
221
+ * CDN image URLs for all 7 sizes (TigerTag+ only, null for Maker/Init).
222
+ *
223
+ * Sizes (all square, cover crop, upscale allowed):
224
+ * icon 16 · thumb 32 · small 64 · compact 128 · medium 256 · large 512 · master 1024
225
+ *
226
+ * Example: tag.imgUrls.large
227
+ * → "https://cdn.tigertag.io/img?type=filament&id=2159613929&size=large&v=3&stream=1"
228
+ */
229
+ get imgUrls() {
230
+ if (this.isMaker || this.isInit) return null;
231
+ const type = _IMG_TYPE_MAP[this.idType] || 'filament';
232
+ const id = this.idProduct;
233
+ const v = Date.now(); // cache-buster — unique on every access
234
+ const mk = (size) => `${_CDN_IMG_BASE}?type=${type}&id=${id}&size=${size}&v=${v}&stream=1`;
235
+ return {
236
+ icon: mk('icon'), // 16 × 16
237
+ thumb: mk('thumb'), // 32 × 32
238
+ small: mk('small'), // 64 × 64
239
+ compact: mk('compact'), // 128 × 128
240
+ medium: mk('medium'), // 256 × 256
241
+ large: mk('large'), // 512 × 512
242
+ master: mk('master'), // 1024 × 1024
243
+ };
244
+ }
245
+
217
246
  /** Lazily loaded bundled database. */
218
247
  get db() {
219
248
  if (!this._db) this._db = new TigerTagDB();
@@ -225,12 +254,12 @@ class TigerTag {
225
254
  /**
226
255
  * Parse a TigerTag from NFC SDK native output. (Primary method)
227
256
  *
228
- * @param {Buffer} payload - 80 or 144 bytes (pages 0x04–0x27).
229
257
  * @param {Buffer} uid - 7-byte chip UID as returned by the NFC SDK.
258
+ * @param {Buffer} payload - 80 or 144 bytes (pages 0x04–0x27).
230
259
  * @param {TigerTagDB} [db]
231
260
  * @returns {TigerTag}
232
261
  */
233
- static fromPages(payload, uid, db = null) {
262
+ static fromPages(uid, payload, db = null) {
234
263
  const buf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
235
264
  if (buf.length !== MIN_DATA_LEN && buf.length !== FULL_DATA_LEN) {
236
265
  throw new Error(
@@ -412,10 +441,11 @@ class TigerTag {
412
441
  dryTime = 0,
413
442
  bedTempMin = 0,
414
443
  bedTempMax = 0,
415
- timestamp = null,
416
- customMessage = '',
417
- tdRaw = 0,
418
- db = null,
444
+ timestamp = null,
445
+ customMessage = '',
446
+ tdRaw = 0,
447
+ measureAvailable = null,
448
+ db = null,
419
449
  } = {}) {
420
450
  let idTigertag;
421
451
  if (productId !== MAKER_PRODUCT_ID && productId !== INIT_PRODUCT_ID) {
@@ -439,7 +469,7 @@ class TigerTag {
439
469
  color3R, color3G, color3B,
440
470
  measure,
441
471
  idUnit,
442
- measureAvailable: measure,
472
+ measureAvailable: measureAvailable != null ? measureAvailable : measure,
443
473
  nozzleTempMin, nozzleTempMax, dryTemp, dryTime, bedTempMin, bedTempMax,
444
474
  timestamp,
445
475
  customMessage,
@@ -647,7 +677,7 @@ class TigerTag {
647
677
  return new SignatureResult(
648
678
  SignatureResult.NO_UID,
649
679
  'UID required for signature verification. '
650
- + 'Use fromPages(payload, uid) — the NFC SDK always exposes '
680
+ + 'Use fromPages(uid, payload) — the NFC SDK always exposes '
651
681
  + 'the UID separately. For binary dumps, use a full 180-byte dump.',
652
682
  );
653
683
  }
@@ -903,6 +933,83 @@ class TigerTag {
903
933
 
904
934
  // ── Output ──────────────────────────────────────────────────────────────
905
935
 
936
+ /**
937
+ * Convert measure + measureAvailable to their canonical base unit and return
938
+ * convenience fields ready to spread into a dict.
939
+ *
940
+ * Weight units → measure_gr / measure_available_gr (always in grams)
941
+ * Volume units → measure_ml / measure_available_ml (always in millilitres)
942
+ * Other units → {} (no extra fields)
943
+ *
944
+ * id_unit mapping (from id_measure_unit.json):
945
+ * 10 mg · 21 g · 35 kg
946
+ * 48 ml · 62 cl · 79 L · 95 m³
947
+ *
948
+ * @private
949
+ */
950
+ /**
951
+ * Returns a parenthetical base-unit hint string for a single value,
952
+ * e.g. " (= 750 g)" or " (= 500 ml)".
953
+ * Returns "" when idUnit is already the canonical base unit (g / ml / mm)
954
+ * or unsupported (no conversion available).
955
+ * @private
956
+ */
957
+ static _toBaseUnitStr(value, idUnit) {
958
+ const BASE_IDS = new Set([21, 48, 112]); // g, ml, mm are already base
959
+ if (BASE_IDS.has(idUnit)) return '';
960
+ const f = TigerTag._baseUnitFields(value, value, idUnit);
961
+ if (f.measure_gr !== undefined) return ` (= ${f.measure_gr} g)`;
962
+ if (f.measure_ml !== undefined) return ` (= ${f.measure_ml} ml)`;
963
+ if (f.measure_mm !== undefined) return ` (= ${f.measure_mm} mm)`;
964
+ if (f.measure_mm2 !== undefined) return ` (= ${f.measure_mm2} mm²)`;
965
+ return '';
966
+ }
967
+
968
+ static _baseUnitFields(measure, measureAvailable, idUnit) {
969
+ // Weight: mg(10) g(21) kg(35) → grams
970
+ const WEIGHT_TO_G = { 10: 0.001, 21: 1, 35: 1000 };
971
+ // Volume: ml(48) cl(62) L(79) m³(95) → millilitres
972
+ const VOLUME_TO_ML = { 48: 1, 62: 10, 79: 1000, 95: 1_000_000 };
973
+ // Size: mm(112) cm(130) m(149) → millimetres
974
+ const SIZE_TO_MM = { 112: 1, 130: 10, 149: 1000 };
975
+ // Area: m²(170) → square millimetres
976
+ const AREA_TO_MM2 = { 170: 1_000_000 };
977
+
978
+ const wf = WEIGHT_TO_G[idUnit];
979
+ if (wf !== undefined) {
980
+ return {
981
+ measure_gr: Math.round(measure * wf),
982
+ measure_available_gr: Math.round(measureAvailable * wf),
983
+ };
984
+ }
985
+
986
+ const vf = VOLUME_TO_ML[idUnit];
987
+ if (vf !== undefined) {
988
+ return {
989
+ measure_ml: Math.round(measure * vf),
990
+ measure_available_ml: Math.round(measureAvailable * vf),
991
+ };
992
+ }
993
+
994
+ const sf = SIZE_TO_MM[idUnit];
995
+ if (sf !== undefined) {
996
+ return {
997
+ measure_mm: Math.round(measure * sf),
998
+ measure_available_mm: Math.round(measureAvailable * sf),
999
+ };
1000
+ }
1001
+
1002
+ const af = AREA_TO_MM2[idUnit];
1003
+ if (af !== undefined) {
1004
+ return {
1005
+ measure_mm2: Math.round(measure * af),
1006
+ measure_available_mm2: Math.round(measureAvailable * af),
1007
+ };
1008
+ }
1009
+
1010
+ return {};
1011
+ }
1012
+
906
1013
  /**
907
1014
  * Return protocol fields exactly as stored on the chip — no label resolution,
908
1015
  * no unit conversion, no date formatting.
@@ -940,10 +1047,173 @@ class TigerTag {
940
1047
  td_raw: this.tdRaw,
941
1048
  message: this.customMessage,
942
1049
  measure_available: this.measureAvailable,
1050
+ ...TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit),
943
1051
  uid: this.uidHex,
944
1052
  product_page_url: this.productPageUrl,
945
1053
  api_url: this.apiUrl,
1054
+ img: this.imgUrls,
1055
+ };
1056
+ }
1057
+
1058
+ // ── Write helpers ────────────────────────────────────────────────────────
1059
+
1060
+ /**
1061
+ * Map a snake_case raw-dict partial object to the camelCase kwargs accepted
1062
+ * by patch(). Only keys present in the input are mapped — missing keys are
1063
+ * left untouched on the existing tag.
1064
+ *
1065
+ * Accepted keys: all keys produced by toRawDict() except uid / product_page_url / api_url.
1066
+ *
1067
+ * @param {object} raw - Partial or full toRawDict()-style object.
1068
+ * @returns {object} - camelCase kwargs ready for patch().
1069
+ */
1070
+ static _rawDictToPatchKwargs(raw) {
1071
+ const map = {
1072
+ id_material: 'idMaterial',
1073
+ id_aspect1: 'idAspect1',
1074
+ id_aspect2: 'idAspect2',
1075
+ id_type: 'idType',
1076
+ id_diameter: 'idDiameter',
1077
+ id_brand: 'idBrand',
1078
+ color_r: 'color1R',
1079
+ color_g: 'color1G',
1080
+ color_b: 'color1B',
1081
+ color_a: 'color1A',
1082
+ color_r2: 'color2R',
1083
+ color_g2: 'color2G',
1084
+ color_b2: 'color2B',
1085
+ color_r3: 'color3R',
1086
+ color_g3: 'color3G',
1087
+ color_b3: 'color3B',
1088
+ measure: 'measure',
1089
+ measure_available:'measureAvailable',
1090
+ id_unit: 'idUnit',
1091
+ nozzle_min: 'nozzleTempMin',
1092
+ nozzle_max: 'nozzleTempMax',
1093
+ dry_temp: 'dryTemp',
1094
+ dry_time: 'dryTime',
1095
+ bed_min: 'bedTempMin',
1096
+ bed_max: 'bedTempMax',
1097
+ timestamp: 'timestamp',
1098
+ td_raw: 'tdRaw',
1099
+ message: 'customMessage',
946
1100
  };
1101
+ const kwargs = {};
1102
+ for (const [snakeKey, camelKey] of Object.entries(map)) {
1103
+ if (Object.prototype.hasOwnProperty.call(raw, snakeKey)) {
1104
+ kwargs[camelKey] = raw[snakeKey];
1105
+ }
1106
+ }
1107
+ return kwargs;
1108
+ }
1109
+
1110
+ /**
1111
+ * Build a new TigerTag from a toRawDict()-style object (snake_case).
1112
+ * Useful as the first step of a write workflow: parse a stored doc, then
1113
+ * call toBytes() to get the binary payload ready for NFC write.
1114
+ *
1115
+ * Fields not present in the input fall back to safe zero / default values.
1116
+ * Protected fields (idTigertag, idProduct) can optionally be supplied.
1117
+ *
1118
+ * @param {object} raw - toRawDict()-style object.
1119
+ * @param {TigerTagDB} [db]
1120
+ * @returns {TigerTag}
1121
+ */
1122
+ static fromRawDict(raw, db = null) {
1123
+ return TigerTag.create({
1124
+ productId: raw.id_product ?? MAKER_PRODUCT_ID,
1125
+ idMaterial: raw.id_material ?? 0,
1126
+ idAspect1: raw.id_aspect1 ?? 0,
1127
+ idAspect2: raw.id_aspect2 ?? 0,
1128
+ idType: raw.id_type ?? 0,
1129
+ idDiameter: raw.id_diameter ?? 0,
1130
+ idBrand: raw.id_brand ?? 0,
1131
+ color1R: raw.color_r ?? 0,
1132
+ color1G: raw.color_g ?? 0,
1133
+ color1B: raw.color_b ?? 0,
1134
+ color1A: raw.color_a ?? 255,
1135
+ color2R: raw.color_r2 ?? 0,
1136
+ color2G: raw.color_g2 ?? 0,
1137
+ color2B: raw.color_b2 ?? 0,
1138
+ color3R: raw.color_r3 ?? 0,
1139
+ color3G: raw.color_g3 ?? 0,
1140
+ color3B: raw.color_b3 ?? 0,
1141
+ measure: raw.measure ?? 0,
1142
+ measureAvailable: raw.measure_available ?? raw.measure ?? 0,
1143
+ idUnit: raw.id_unit ?? 0,
1144
+ nozzleTempMin: raw.nozzle_min ?? 0,
1145
+ nozzleTempMax: raw.nozzle_max ?? 0,
1146
+ dryTemp: raw.dry_temp ?? 0,
1147
+ dryTime: raw.dry_time ?? 0,
1148
+ bedTempMin: raw.bed_min ?? 0,
1149
+ bedTempMax: raw.bed_max ?? 0,
1150
+ timestamp: raw.timestamp ?? null,
1151
+ customMessage: raw.message ?? '',
1152
+ tdRaw: raw.td_raw ?? 0,
1153
+ db,
1154
+ });
1155
+ }
1156
+
1157
+ /**
1158
+ * Build a new TigerTag from a Firestore cloud document.
1159
+ * The cloud format uses data1–data7 for the temperature/diameter fields
1160
+ * and weight_available / measure_gr for available weight.
1161
+ *
1162
+ * @param {object} doc - Firestore document data object.
1163
+ * @param {TigerTagDB} [db]
1164
+ * @returns {TigerTag}
1165
+ */
1166
+ static fromCloudDoc(doc, db = null) {
1167
+ return TigerTag.create({
1168
+ productId: doc.id_product ?? MAKER_PRODUCT_ID,
1169
+ idMaterial: doc.id_material ?? 0,
1170
+ idAspect1: doc.id_aspect1 ?? 0,
1171
+ idAspect2: doc.id_aspect2 ?? 0,
1172
+ idType: doc.id_type ?? 0,
1173
+ idDiameter: doc.data1 ?? 0, // data1 = id_diameter
1174
+ idBrand: doc.id_brand ?? 0,
1175
+ color1R: doc.color_r ?? 0,
1176
+ color1G: doc.color_g ?? 0,
1177
+ color1B: doc.color_b ?? 0,
1178
+ color1A: doc.color_a ?? 255,
1179
+ color2R: doc.color_r2 ?? 0,
1180
+ color2G: doc.color_g2 ?? 0,
1181
+ color2B: doc.color_b2 ?? 0,
1182
+ color3R: doc.color_r3 ?? 0,
1183
+ color3G: doc.color_g3 ?? 0,
1184
+ color3B: doc.color_b3 ?? 0,
1185
+ measure: doc.measure ?? 0,
1186
+ measureAvailable: doc.weight_available ?? doc.measure_gr ?? doc.measure ?? 0,
1187
+ idUnit: doc.id_unit ?? 0,
1188
+ nozzleTempMin: doc.data2 ?? 0, // data2 = nozzle_min
1189
+ nozzleTempMax: doc.data3 ?? 0, // data3 = nozzle_max
1190
+ dryTemp: doc.data4 ?? 0, // data4 = dry_temp
1191
+ dryTime: doc.data5 ?? 0, // data5 = dry_time
1192
+ bedTempMin: doc.data6 ?? 0, // data6 = bed_min
1193
+ bedTempMax: doc.data7 ?? 0, // data7 = bed_max
1194
+ timestamp: doc.timestamp ?? null,
1195
+ customMessage: doc.message ?? '',
1196
+ // Firestore stores TD as a human-readable float (e.g. 1.5).
1197
+ // The chip encodes tdRaw = round(tdValue × 10) as a UInt16.
1198
+ tdRaw: doc.TD != null ? Math.round(doc.TD * 10) : 0,
1199
+ db,
1200
+ });
1201
+ }
1202
+
1203
+ /**
1204
+ * Apply a surgical patch using snake_case keys (toRawDict format).
1205
+ * Only the supplied keys are changed — all other fields are preserved.
1206
+ *
1207
+ * Examples:
1208
+ * tag.patchFromRawDict({ td_raw: 150 })
1209
+ * tag.patchFromRawDict({ message: "Opened 2025-01", measure_available: 750 })
1210
+ * tag.patchFromRawDict({ td_raw: 0, message: "", nozzle_min: 220 })
1211
+ *
1212
+ * @param {object} raw - Partial toRawDict()-style object.
1213
+ * @returns {TigerTag} New patched instance (immutable).
1214
+ */
1215
+ patchFromRawDict(raw) {
1216
+ return this.patch(TigerTag._rawDictToPatchKwargs(raw));
947
1217
  }
948
1218
 
949
1219
  /**
@@ -982,6 +1252,7 @@ class TigerTag {
982
1252
  + 'Query the api_url field for the full enriched product JSON.',
983
1253
  product_page_url: this.productPageUrl,
984
1254
  api_url: this.apiUrl,
1255
+ img: this.imgUrls,
985
1256
  },
986
1257
  material: {
987
1258
  id: this.idMaterial,
@@ -1037,6 +1308,7 @@ class TigerTag {
1037
1308
  measure: {
1038
1309
  initial: this.measure,
1039
1310
  available: this.measureAvailable,
1311
+ ...TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit),
1040
1312
  percent: stock,
1041
1313
  unit: unitLabel,
1042
1314
  description: `Material quantity: ${this.measureAvailable} ${unitLabel} remaining `
@@ -1130,10 +1402,22 @@ class TigerTag {
1130
1402
  }
1131
1403
 
1132
1404
  if (this.measure > 0) {
1405
+ const BASE_UNIT_IDS = new Set([21, 48, 112]); // g, ml, mm — already canonical
1406
+ const bf = BASE_UNIT_IDS.has(this.idUnit)
1407
+ ? {}
1408
+ : TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit);
1409
+ const buKey = Object.keys(bf).find(k => k.startsWith('measure_available_'));
1410
+ const buUnit = buKey ? buKey.replace('measure_available_', '') : null;
1411
+ // buUnit is 'gr'→'g', 'ml', 'mm', 'mm2'→'mm²'
1412
+ const buLabel = buUnit ? buUnit.replace('gr', 'g').replace('mm2', 'mm²') : null;
1413
+ const buNote = buLabel
1414
+ ? ` — ${bf['measure_available_' + buUnit]} ${buLabel} available, `
1415
+ + `${bf['measure_' + buUnit]} ${buLabel} total`
1416
+ : '';
1133
1417
  parts.push(
1134
1418
  `Quantity: ${this.measureAvailable} ${unit} remaining`
1135
1419
  + ` out of ${this.measure} ${unit} initial`
1136
- + (stock != null ? ` (${stock}%).` : '.'),
1420
+ + (stock != null ? ` (${stock}%${buNote}).` : (buNote ? `${buNote}.` : '.')),
1137
1421
  );
1138
1422
  }
1139
1423
 
@@ -1150,6 +1434,14 @@ class TigerTag {
1150
1434
  );
1151
1435
  }
1152
1436
 
1437
+ const imgs = this.imgUrls; // snapshot — single Date.now() for this describe() call
1438
+ if (imgs) {
1439
+ parts.push(
1440
+ `Product image (medium 256×256): ${imgs.medium}`
1441
+ + ` — also available: icon·16 thumb·32 small·64 compact·128 large·512 master·1024.`,
1442
+ );
1443
+ }
1444
+
1153
1445
  if (this.isSigned) {
1154
1446
  parts.push('Tag carries an ECDSA-P256 signature — call tag.verify() to confirm authenticity.');
1155
1447
  } else {
@@ -1180,6 +1472,7 @@ class TigerTag {
1180
1472
  rec[kMin] != null ? ` (DB: ${rec[kMin]}–${rec[kMax]}${suffix})` : '';
1181
1473
 
1182
1474
  const idHex = this.idTigertag.toString(16).toUpperCase().padStart(8, '0');
1475
+ const imgs = this.imgUrls; // snapshot — single Date.now() for the whole render
1183
1476
 
1184
1477
  return (
1185
1478
  `┌─ TigerTag ────────────────────────────────────────────\n`
@@ -1189,6 +1482,10 @@ class TigerTag {
1189
1482
  + (this.productPageUrl
1190
1483
  ? `│ Product page ${this.productPageUrl}\n│ API JSON ${this.apiUrl}\n`
1191
1484
  : '')
1485
+ + (imgs
1486
+ ? `│ Image (med) ${imgs.medium}\n`
1487
+ + `│ Img sizes icon·16 thumb·32 small·64 compact·128 medium·256 large·512 master·1024\n`
1488
+ : '')
1192
1489
  + `├─ Material ────────────────────────────────────────────\n`
1193
1490
  + `│ Material ${TigerTagDB.label(_db.material(this.idMaterial))} (id=${this.idMaterial})\n`
1194
1491
  + `│ Density ${mat.density != null ? mat.density : '—'} g/cm³\n`
@@ -1208,8 +1505,8 @@ class TigerTag {
1208
1505
  + `│ Drying ${this.dryTemp}°C / ${this.dryTime}h${recNote('dryTemp', 'dryTime', ' h')}\n`
1209
1506
  + `├─ Quantity ────────────────────────────────────────────\n`
1210
1507
  + `│ Unit ${ul}\n`
1211
- + `│ Initial ${this.measure} ${ul}\n`
1212
- + `│ Available ${this.measureAvailable} ${ul}`
1508
+ + `│ Initial ${this.measure} ${ul}${TigerTag._toBaseUnitStr(this.measure, this.idUnit)}\n`
1509
+ + `│ Available ${this.measureAvailable} ${ul}${TigerTag._toBaseUnitStr(this.measureAvailable, this.idUnit)}`
1213
1510
  + (stock != null ? ` (${stock}% remaining)\n` : '\n')
1214
1511
  + `├─ Traceability ────────────────────────────────────────\n`
1215
1512
  + `│ Manufactured ${this.manufacturingDate.toISOString().replace('T', ' ').slice(0, 16)} UTC\n`