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 +107 -0
- package/README.md +44 -10
- package/database/id_aspect.json +122 -1
- package/database/last_update.json +1 -1
- package/package.json +1 -1
- package/src/tag.js +147 -9
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
|
|
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
|
|
|
@@ -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.
|
|
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.
|
|
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.
|
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/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
|
|
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,
|
|
@@ -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
|
-
|
|
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`
|