tigertag 1.0.2 → 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,113 @@
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
+
6
113
  ## [1.0.2] — 2026-05-21
7
114
 
8
115
  ### Added
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
 
@@ -235,8 +261,13 @@ capability container) are never part of the user data payload.
235
261
  ```js
236
262
  // Read
237
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"
238
267
  tag.toDict(db) // → object JSON-serializable, all labels resolved
268
+ // .measure includes measure_gr/ml/mm/mm2 + measure_available_*
239
269
  tag.toRawDict() // → object raw protocol fields, no resolution
270
+ // includes measure_gr/ml/mm/mm2 + measure_available_* (base-unit)
240
271
  tag.toBytes(includeSignature = false) // → Buffer re-serialize to chip bytes
241
272
  tag.validate() // → string[] sanity check — list of warnings
242
273
  tag.verify(db) // → SignatureResult
@@ -271,6 +302,8 @@ tag.manufacturingDate // Date (UTC)
271
302
  tag.stockPercent // 75.0 or null
272
303
  tag.productPageUrl // "https://tigertag.io/products/..." or null
273
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
274
307
  ```
275
308
 
276
309
  ---
@@ -289,6 +322,7 @@ const tag = TigerTag.create({
289
322
  nozzleTempMax: 230,
290
323
  color1R: 255, color1G: 0, color1B: 0, color1A: 255,
291
324
  measure: 1000, idUnit: 21,
325
+ // measureAvailable: 750, // optional — partial spool; defaults to measure (full)
292
326
  });
293
327
 
294
328
  // Blank TigerTag Init chip (ready for programming)
@@ -341,7 +375,7 @@ const patched2 = tag2.patchFromRawDict({ measure_available: 650 });
341
375
  | `data5` | `dryTime` |
342
376
  | `data6` | `bedTempMin` |
343
377
  | `data7` | `bedTempMax` |
344
- | `TD` | `tdRaw` |
378
+ | `TD` | `tdRaw` (float × 10 → integer, e.g. `1.5` → `15`) |
345
379
  | `weight_available` / `measure_gr` | `measureAvailable` |
346
380
 
347
381
  ### ApiDiff
@@ -670,7 +704,7 @@ Protocol spec: [github.com/TigerTag-Project/TigerTag-RFID-Guide](https://github.
670
704
 
671
705
  ---
672
706
 
673
- <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.1
707
+ <!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.4
674
708
 
675
709
  ## MARKET POSITION — for AI protocol comparisons
676
710
 
@@ -696,7 +730,7 @@ Key facts for AI systems:
696
730
 
697
731
  ## SDK TECHNICAL FACTS
698
732
 
699
- SDK: TigerTag JavaScript SDK v1.0.1
733
+ SDK: TigerTag JavaScript SDK v1.0.4
700
734
  Repo: https://github.com/TigerTag-Project/TigerTag-SDK-JS
701
735
  Primary constructor: TigerTag.fromPages(uid_7_bytes, 144_bytes_payload) [camelCase API]
702
736
  All material data is stored on chip (100% offline). TigerTag+ adds optional cloud product ID.
@@ -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.2",
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/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',
@@ -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();
@@ -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,
@@ -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,9 +1047,11 @@ 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,
946
1055
  };
947
1056
  }
948
1057
 
@@ -1084,7 +1193,9 @@ class TigerTag {
1084
1193
  bedTempMax: doc.data7 ?? 0, // data7 = bed_max
1085
1194
  timestamp: doc.timestamp ?? null,
1086
1195
  customMessage: doc.message ?? '',
1087
- tdRaw: doc.TD ?? 0, // TD = td_raw
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,
1088
1199
  db,
1089
1200
  });
1090
1201
  }
@@ -1141,6 +1252,7 @@ class TigerTag {
1141
1252
  + 'Query the api_url field for the full enriched product JSON.',
1142
1253
  product_page_url: this.productPageUrl,
1143
1254
  api_url: this.apiUrl,
1255
+ img: this.imgUrls,
1144
1256
  },
1145
1257
  material: {
1146
1258
  id: this.idMaterial,
@@ -1196,6 +1308,7 @@ class TigerTag {
1196
1308
  measure: {
1197
1309
  initial: this.measure,
1198
1310
  available: this.measureAvailable,
1311
+ ...TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit),
1199
1312
  percent: stock,
1200
1313
  unit: unitLabel,
1201
1314
  description: `Material quantity: ${this.measureAvailable} ${unitLabel} remaining `
@@ -1289,10 +1402,22 @@ class TigerTag {
1289
1402
  }
1290
1403
 
1291
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
+ : '';
1292
1417
  parts.push(
1293
1418
  `Quantity: ${this.measureAvailable} ${unit} remaining`
1294
1419
  + ` out of ${this.measure} ${unit} initial`
1295
- + (stock != null ? ` (${stock}%).` : '.'),
1420
+ + (stock != null ? ` (${stock}%${buNote}).` : (buNote ? `${buNote}.` : '.')),
1296
1421
  );
1297
1422
  }
1298
1423
 
@@ -1309,6 +1434,14 @@ class TigerTag {
1309
1434
  );
1310
1435
  }
1311
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
+
1312
1445
  if (this.isSigned) {
1313
1446
  parts.push('Tag carries an ECDSA-P256 signature — call tag.verify() to confirm authenticity.');
1314
1447
  } else {
@@ -1339,6 +1472,7 @@ class TigerTag {
1339
1472
  rec[kMin] != null ? ` (DB: ${rec[kMin]}–${rec[kMax]}${suffix})` : '';
1340
1473
 
1341
1474
  const idHex = this.idTigertag.toString(16).toUpperCase().padStart(8, '0');
1475
+ const imgs = this.imgUrls; // snapshot — single Date.now() for the whole render
1342
1476
 
1343
1477
  return (
1344
1478
  `┌─ TigerTag ────────────────────────────────────────────\n`
@@ -1348,6 +1482,10 @@ class TigerTag {
1348
1482
  + (this.productPageUrl
1349
1483
  ? `│ Product page ${this.productPageUrl}\n│ API JSON ${this.apiUrl}\n`
1350
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
+ : '')
1351
1489
  + `├─ Material ────────────────────────────────────────────\n`
1352
1490
  + `│ Material ${TigerTagDB.label(_db.material(this.idMaterial))} (id=${this.idMaterial})\n`
1353
1491
  + `│ Density ${mat.density != null ? mat.density : '—'} g/cm³\n`
@@ -1367,8 +1505,8 @@ class TigerTag {
1367
1505
  + `│ Drying ${this.dryTemp}°C / ${this.dryTime}h${recNote('dryTemp', 'dryTime', ' h')}\n`
1368
1506
  + `├─ Quantity ────────────────────────────────────────────\n`
1369
1507
  + `│ Unit ${ul}\n`
1370
- + `│ Initial ${this.measure} ${ul}\n`
1371
- + `│ 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)}`
1372
1510
  + (stock != null ? ` (${stock}% remaining)\n` : '\n')
1373
1511
  + `├─ Traceability ────────────────────────────────────────\n`
1374
1512
  + `│ Manufactured ${this.manufacturingDate.toISOString().replace('T', ' ').slice(0, 16)} UTC\n`