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 +123 -1
- package/README.md +94 -21
- package/database/id_aspect.json +122 -1
- package/database/last_update.json +1 -1
- package/package.json +1 -1
- package/src/index.js +1 -1
- package/src/tag.js +309 -12
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
`● green` = connected
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
616
|
-
| [TigerTag RFID Connect](https://play.google.com/store/apps/details?id=com.tigertag.connect) | Android | Official mobile app — read/write
|
|
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.
|
|
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.
|
|
733
|
+
SDK: TigerTag JavaScript SDK v1.0.4
|
|
661
734
|
Repo: https://github.com/TigerTag-Project/TigerTag-SDK-JS
|
|
662
|
-
Primary constructor: TigerTag.fromPages(
|
|
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+)
|
package/database/id_aspect.json
CHANGED
|
@@ -1 +1,122 @@
|
|
|
1
|
-
[
|
|
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":
|
|
1
|
+
{"versions":1763073059935,"types":1777884684291,"brands":1777885837902,"filament_diameters":1777895560487,"filament_materials":1778798861002,"aspects":1779219283884,"measure_units":1777896731691}
|
package/package.json
CHANGED
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(
|
|
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(
|
|
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(
|
|
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
|
|
416
|
-
customMessage
|
|
417
|
-
tdRaw
|
|
418
|
-
|
|
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(
|
|
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`
|