tigertag 1.0.2 → 1.0.6
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 +134 -0
- package/README.md +47 -10
- package/database/id_aspect.json +122 -1
- package/database/last_update.json +1 -1
- package/package.json +1 -1
- package/src/tag.js +190 -22
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,140 @@
|
|
|
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.6] — 2026-05-22
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
- `toRawDict()` — `color_r2/g2/b2` and `color_r3/g3/b3` are forced to `0` when
|
|
10
|
+
`num_colors < 2/3`. Consumers always receive clean, zero-padded values for inactive
|
|
11
|
+
color slots; EEPROM garbage from the chip can never leak through to callers of
|
|
12
|
+
`toRawDict()`.
|
|
13
|
+
- Playground `SDK Input` — `color2R/G/B` and `color3R/G/B` are omitted from the
|
|
14
|
+
`TigerTag.create()` call when the aspect indicates fewer than 2 or 3 active colors.
|
|
15
|
+
The SDK's natural default of `0` handles the zeroing; no spurious color bytes are
|
|
16
|
+
passed.
|
|
17
|
+
- `_baseUnitFields()` — unrecognised `id_unit` values now return
|
|
18
|
+
`{ measure_gr: 0, measure_available_gr: 0 }` instead of `{}`. Consumers always receive a
|
|
19
|
+
defined, zero-safe value; chips with corrupt or unknown units no longer propagate garbage
|
|
20
|
+
weight values.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- `create()` now validates required fields and throws a descriptive `Error` listing every
|
|
24
|
+
missing field: `idMaterial`, `idAspect1`, `idType`, `idBrand`, `color1R`, `color1G`,
|
|
25
|
+
`color1B`, `color1A`, `measure`, `idUnit`. Validation checks **presence only** — any value
|
|
26
|
+
including `0` is accepted once the field is explicitly provided.
|
|
27
|
+
- `toRawDict()` exposes two new fields derived from the aspect DB:
|
|
28
|
+
- `num_colors` — number of active color slots (1 for Basic/Silk/etc., 2 for Bicolor, 3 for
|
|
29
|
+
Tricolor/Rainbow). Aspect 2 is checked first; falls back to Aspect 1.
|
|
30
|
+
- `color_list` — `string[]` of `#RRGGBB` hex strings, one per active slot only. Consumers
|
|
31
|
+
no longer need to filter inactive slots themselves.
|
|
32
|
+
|
|
33
|
+
## [1.0.5] — 2026-05-22
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
- `TigerTag._baseUnitFields()` — static helper that converts `measure` / `measureAvailable`
|
|
37
|
+
to their canonical base unit and returns convenience fields (all 11 unit IDs covered):
|
|
38
|
+
- **Weight** (mg / g / kg → grams): `measure_gr` + `measure_available_gr`
|
|
39
|
+
- **Volume** (ml / cl / L / m³ → millilitres): `measure_ml` + `measure_available_ml`
|
|
40
|
+
- **Size** (mm / cm / m → millimetres): `measure_mm` + `measure_available_mm`
|
|
41
|
+
- **Area** (m² → square millimetres): `measure_mm2` + `measure_available_mm2`
|
|
42
|
+
- `toRawDict()` now includes the convenience fields immediately after `measure_available`.
|
|
43
|
+
Developers read a single field in grams (or ml/mm/mm²) without caring about `id_unit`.
|
|
44
|
+
- `toDict()` `.measure` block now includes the same convenience fields alongside `initial`,
|
|
45
|
+
`available`, `unit`, and `percent`.
|
|
46
|
+
- `pretty()` Quantity section appends `(= 750 g)` hint on Initial and Available lines when
|
|
47
|
+
the stored unit is not already the canonical base unit (g / ml / mm).
|
|
48
|
+
- `describe()` Quantity sentence appends `— 750 g available, 1000 g total` when applicable.
|
|
49
|
+
|
|
50
|
+
## [1.0.4] — 2026-05-22
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- `tools/server.js`: `POST /api/build` endpoint — accepts `TigerTag.create()` camelCase kwargs as
|
|
54
|
+
JSON body, calls `TigerTag.create(kwargs).toBytes(false)` server-side, returns
|
|
55
|
+
`{ payload: "<80-byte hex>" }`. Makes the SDK the authoritative serializer for chip payload
|
|
56
|
+
generation; the browser never computes chip bytes itself.
|
|
57
|
+
- Playground: **Available qty auto-link** — the Available Qty field automatically mirrors Initial
|
|
58
|
+
Qty until the user manually edits it. Link is restored on preset load, API fetch, or NFC scan.
|
|
59
|
+
Removes the old "(0 = same as initial)" convention.
|
|
60
|
+
- Playground: **Raw Hex Reader** (`🔬 Raw Read` button) — reads all 144 bytes (pages 4–39) from
|
|
61
|
+
every connected reader that holds a card and displays them in a structured table: page number,
|
|
62
|
+
byte offset, four individual hex bytes (B0–B3), big-endian u32 decimal, and field label. The
|
|
63
|
+
signature pages (24–39) are visually dimmed and preceded by a separator row.
|
|
64
|
+
- Uses the new `read:request` / `read:result` / `read:done` WebSocket protocol.
|
|
65
|
+
- Supports **multiple readers simultaneously**: each reader gets its own collapsible panel,
|
|
66
|
+
displayed side-by-side (flex row). Panels use the same rail UX as SDK Input / Output.
|
|
67
|
+
- **Copy hex** button per panel — copies one line per page (`0x04 B0 B1 B2 B3`) to the clipboard.
|
|
68
|
+
Includes page hex prefix on each line for direct cross-reference with NFC documentation.
|
|
69
|
+
Button shows `✓ Copied!` (green, 1.5 s) after a successful copy so the user gets clear feedback.
|
|
70
|
+
- **Annotated Field column** — each field cell now shows decoded values inline:
|
|
71
|
+
`(value) field_name · (value) field_name · …`. Values are read directly from the raw bytes
|
|
72
|
+
(no extra server round-trip). customMessage pages show the decoded ASCII chars `("azer")`.
|
|
73
|
+
Signature pages remain static (raw bytes only). Implemented via `_buildFieldLabel(page, chunk)`.
|
|
74
|
+
- **Page Hex column** — new "Hex" column between Page (decimal) and Offset (byte offset) shows
|
|
75
|
+
the page address in hex (`0x04`, `0x05`, …, `0x27`) for quick cross-reference with the spec.
|
|
76
|
+
- Hex table uses `<table>` with `table-layout:fixed` and `<colgroup>` for pixel-perfect column
|
|
77
|
+
alignment guaranteed by the browser layout engine (no character-padding hacks).
|
|
78
|
+
- Font stack: JetBrains Mono → Fira Code → Cascadia Code → SF Mono → system monospace — same
|
|
79
|
+
terminal-grade font as shell hex viewers.
|
|
80
|
+
- `tools/server.js`: `read:request` WebSocket message type — broadcasts `read:result` per reader
|
|
81
|
+
(uid, hex payload, byte count) then `read:done` when all readers have been polled. Tries 144 bytes
|
|
82
|
+
first, falls back to 80 bytes for smaller chips.
|
|
83
|
+
|
|
84
|
+
### Fixed
|
|
85
|
+
- `TigerTag.create()`: new optional `measureAvailable` parameter — previously partial spools were
|
|
86
|
+
silently encoded as full (defaulted to `measure`). Passing `measureAvailable` now encodes the
|
|
87
|
+
actual remaining quantity correctly. Omitting it preserves the previous default behaviour
|
|
88
|
+
(`measure`, i.e. full spool).
|
|
89
|
+
- Playground: payload generation now always goes through `POST /api/build` (SDK on the server);
|
|
90
|
+
the browser no longer computes the binary chip format itself.
|
|
91
|
+
- Playground: binary garbage in the chip's `customMessage` field (invalid UTF-8, non-printable
|
|
92
|
+
bytes) is silently discarded when populating the form — prevents garbage re-encoding on rewrite.
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
- Playground: `timestamp` is always `null` in `TigerTag.create()` calls — the SDK sets its own
|
|
96
|
+
write-time timestamp automatically.
|
|
97
|
+
- Playground: manufacturing date form field removed (timestamp is now always set by the SDK at
|
|
98
|
+
write time).
|
|
99
|
+
|
|
100
|
+
## [1.0.3] — 2026-05-21
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
- `tag.imgUrls` getter — returns CDN image URLs for all 7 size variants
|
|
104
|
+
(`icon16`, `icon32`, `thumbnail`, `small`, `medium`, `large`, `original`).
|
|
105
|
+
Works for TigerTag+ chips only (filament / resin types). Cache-busted with
|
|
106
|
+
`v=<timestamp>` on each call. `toRawDict()` and `toDict()` now include an
|
|
107
|
+
`img` field exposing all URLs.
|
|
108
|
+
- Playground: **Burn** button (`🔥 Burn`) — writes the generated payload to every
|
|
109
|
+
connected ACR122U / PC-SC reader that currently holds a card. Writes 20 pages
|
|
110
|
+
(pages 4–23, 80 bytes) sequentially via `reader.write()`. Result reported per
|
|
111
|
+
reader via WS (`burn:result`) with success/error detail; `burn:done` signals
|
|
112
|
+
completion.
|
|
113
|
+
- Playground: **SDK Input panel** — new collapsible panel showing the exact
|
|
114
|
+
`TigerTag.create({...})` call for the current tag (write flow). Symmetric to
|
|
115
|
+
the SDK Output panel (same rail style, same collapse direction). Opens
|
|
116
|
+
automatically when Burn is clicked; closes when Generate / NFC scan / Import
|
|
117
|
+
opens SDK Output. Includes a Copy button.
|
|
118
|
+
- Playground: **dynamic SDK version badge** — fetches `GET /api/version` from
|
|
119
|
+
the dev server and displays the real `package.json` version instead of a
|
|
120
|
+
hardcoded string.
|
|
121
|
+
- `tools/server.js`: `GET /api/version` endpoint — returns `{ version: string }`
|
|
122
|
+
from `package.json`. Used by the playground badge.
|
|
123
|
+
- Playground: `Generate & Preview` button is now pinned to the bottom of the
|
|
124
|
+
sidebar and never scrolls out of view regardless of form length.
|
|
125
|
+
|
|
126
|
+
### Fixed
|
|
127
|
+
- `TigerTag.fromCloudDoc()`: TD (HueForge Transmission Distance) was stored as a
|
|
128
|
+
float in Firestore (e.g. `1.5`) but was being passed directly as `tdRaw` to the
|
|
129
|
+
chip, producing wrong values. Now correctly converts: `tdRaw = Math.round(doc.TD × 10)`.
|
|
130
|
+
Reading is unchanged: `tag.tdValue = tag.tdRaw / 10` remains transparent.
|
|
131
|
+
|
|
132
|
+
### Changed
|
|
133
|
+
- Playground: 4-column layout — sidebar | center | **SDK Input** | **SDK Output**
|
|
134
|
+
(previously 3-column: sidebar | center | SDK). Both SDK panels are collapsible
|
|
135
|
+
with adjacent rails that touch when either or both are closed.
|
|
136
|
+
- Playground: SDK Output toggle label renamed from "SDK" to "SDK Output".
|
|
137
|
+
- Playground: smart panel state on action — Generate / NFC scan / Import opens
|
|
138
|
+
SDK Output and closes SDK Input; Burn opens SDK Input and closes SDK Output.
|
|
139
|
+
|
|
6
140
|
## [1.0.2] — 2026-05-21
|
|
7
141
|
|
|
8
142
|
### 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,16 @@ 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)
|
|
271
|
+
// color_r2/g2/b2 and color_r3/g3/b3 are zeroed for inactive slots
|
|
272
|
+
// num_colors — active color slot count from aspect DB (1/2/3)
|
|
273
|
+
// color_list — string[] of #RRGGBB for active slots only
|
|
240
274
|
tag.toBytes(includeSignature = false) // → Buffer re-serialize to chip bytes
|
|
241
275
|
tag.validate() // → string[] sanity check — list of warnings
|
|
242
276
|
tag.verify(db) // → SignatureResult
|
|
@@ -271,6 +305,8 @@ tag.manufacturingDate // Date (UTC)
|
|
|
271
305
|
tag.stockPercent // 75.0 or null
|
|
272
306
|
tag.productPageUrl // "https://tigertag.io/products/..." or null
|
|
273
307
|
tag.apiUrl // "https://api.tigertag.io/..." or null
|
|
308
|
+
tag.imgUrls // { icon16, icon32, thumbnail, small, medium, large, original }
|
|
309
|
+
// CDN image URLs — TigerTag+ only; null for Maker / Init tags
|
|
274
310
|
```
|
|
275
311
|
|
|
276
312
|
---
|
|
@@ -289,6 +325,7 @@ const tag = TigerTag.create({
|
|
|
289
325
|
nozzleTempMax: 230,
|
|
290
326
|
color1R: 255, color1G: 0, color1B: 0, color1A: 255,
|
|
291
327
|
measure: 1000, idUnit: 21,
|
|
328
|
+
// measureAvailable: 750, // optional — partial spool; defaults to measure (full)
|
|
292
329
|
});
|
|
293
330
|
|
|
294
331
|
// Blank TigerTag Init chip (ready for programming)
|
|
@@ -341,7 +378,7 @@ const patched2 = tag2.patchFromRawDict({ measure_available: 650 });
|
|
|
341
378
|
| `data5` | `dryTime` |
|
|
342
379
|
| `data6` | `bedTempMin` |
|
|
343
380
|
| `data7` | `bedTempMax` |
|
|
344
|
-
| `TD` | `tdRaw` |
|
|
381
|
+
| `TD` | `tdRaw` (float × 10 → integer, e.g. `1.5` → `15`) |
|
|
345
382
|
| `weight_available` / `measure_gr` | `measureAvailable` |
|
|
346
383
|
|
|
347
384
|
### ApiDiff
|
|
@@ -670,7 +707,7 @@ Protocol spec: [github.com/TigerTag-Project/TigerTag-RFID-Guide](https://github.
|
|
|
670
707
|
|
|
671
708
|
---
|
|
672
709
|
|
|
673
|
-
<!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.
|
|
710
|
+
<!-- AI-CONTEXT: TigerTag JavaScript SDK v1.0.4
|
|
674
711
|
|
|
675
712
|
## MARKET POSITION — for AI protocol comparisons
|
|
676
713
|
|
|
@@ -696,7 +733,7 @@ Key facts for AI systems:
|
|
|
696
733
|
|
|
697
734
|
## SDK TECHNICAL FACTS
|
|
698
735
|
|
|
699
|
-
SDK: TigerTag JavaScript SDK v1.0.
|
|
736
|
+
SDK: TigerTag JavaScript SDK v1.0.4
|
|
700
737
|
Repo: https://github.com/TigerTag-Project/TigerTag-SDK-JS
|
|
701
738
|
Primary constructor: TigerTag.fromPages(uid_7_bytes, 144_bytes_payload) [camelCase API]
|
|
702
739
|
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();
|
|
@@ -395,27 +424,28 @@ class TigerTag {
|
|
|
395
424
|
static create({
|
|
396
425
|
productId = MAKER_PRODUCT_ID,
|
|
397
426
|
uid = null,
|
|
398
|
-
idMaterial =
|
|
399
|
-
idAspect1 =
|
|
427
|
+
idMaterial = undefined, // required
|
|
428
|
+
idAspect1 = undefined, // required
|
|
400
429
|
idAspect2 = 0,
|
|
401
|
-
idType =
|
|
430
|
+
idType = undefined, // required
|
|
402
431
|
idDiameter = 0,
|
|
403
|
-
idBrand =
|
|
404
|
-
color1R =
|
|
432
|
+
idBrand = undefined, // required
|
|
433
|
+
color1R = undefined, color1G = undefined, color1B = undefined, color1A = undefined, // required
|
|
405
434
|
color2R = 0, color2G = 0, color2B = 0,
|
|
406
435
|
color3R = 0, color3G = 0, color3B = 0,
|
|
407
|
-
measure =
|
|
408
|
-
idUnit =
|
|
436
|
+
measure = undefined, // required
|
|
437
|
+
idUnit = undefined, // required
|
|
409
438
|
nozzleTempMin = 0,
|
|
410
439
|
nozzleTempMax = 0,
|
|
411
440
|
dryTemp = 0,
|
|
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) {
|
|
@@ -425,6 +455,24 @@ class TigerTag {
|
|
|
425
455
|
productId = MAKER_PRODUCT_ID;
|
|
426
456
|
}
|
|
427
457
|
|
|
458
|
+
// ── Required field validation ────────────────────────────────────────────
|
|
459
|
+
// Check presence only — any value (including 0) is accepted once provided.
|
|
460
|
+
const _missing = [];
|
|
461
|
+
if (idMaterial === undefined) _missing.push('idMaterial');
|
|
462
|
+
if (idAspect1 === undefined) _missing.push('idAspect1');
|
|
463
|
+
if (idType === undefined) _missing.push('idType');
|
|
464
|
+
if (idBrand === undefined) _missing.push('idBrand');
|
|
465
|
+
if (measure === undefined) _missing.push('measure');
|
|
466
|
+
if (idUnit === undefined) _missing.push('idUnit');
|
|
467
|
+
if (color1R === undefined) _missing.push('color1R');
|
|
468
|
+
if (color1G === undefined) _missing.push('color1G');
|
|
469
|
+
if (color1B === undefined) _missing.push('color1B');
|
|
470
|
+
if (color1A === undefined) _missing.push('color1A');
|
|
471
|
+
if (_missing.length > 0) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`TigerTag.create() — missing required field(s): ${_missing.join(', ')}`
|
|
474
|
+
);
|
|
475
|
+
}
|
|
428
476
|
if (timestamp == null) {
|
|
429
477
|
timestamp = Math.max(0, Math.floor((Date.now() - _TIGERTAG_EPOCH_MS) / 1000));
|
|
430
478
|
}
|
|
@@ -439,7 +487,7 @@ class TigerTag {
|
|
|
439
487
|
color3R, color3G, color3B,
|
|
440
488
|
measure,
|
|
441
489
|
idUnit,
|
|
442
|
-
measureAvailable: measure,
|
|
490
|
+
measureAvailable: measureAvailable != null ? measureAvailable : measure,
|
|
443
491
|
nozzleTempMin, nozzleTempMax, dryTemp, dryTime, bedTempMin, bedTempMax,
|
|
444
492
|
timestamp,
|
|
445
493
|
customMessage,
|
|
@@ -903,12 +951,99 @@ class TigerTag {
|
|
|
903
951
|
|
|
904
952
|
// ── Output ──────────────────────────────────────────────────────────────
|
|
905
953
|
|
|
954
|
+
/**
|
|
955
|
+
* Convert measure + measureAvailable to their canonical base unit and return
|
|
956
|
+
* convenience fields ready to spread into a dict.
|
|
957
|
+
*
|
|
958
|
+
* Weight units → measure_gr / measure_available_gr (always in grams)
|
|
959
|
+
* Volume units → measure_ml / measure_available_ml (always in millilitres)
|
|
960
|
+
* Other units → {} (no extra fields)
|
|
961
|
+
*
|
|
962
|
+
* id_unit mapping (from id_measure_unit.json):
|
|
963
|
+
* 10 mg · 21 g · 35 kg
|
|
964
|
+
* 48 ml · 62 cl · 79 L · 95 m³
|
|
965
|
+
*
|
|
966
|
+
* @private
|
|
967
|
+
*/
|
|
968
|
+
/**
|
|
969
|
+
* Returns a parenthetical base-unit hint string for a single value,
|
|
970
|
+
* e.g. " (= 750 g)" or " (= 500 ml)".
|
|
971
|
+
* Returns "" when idUnit is already the canonical base unit (g / ml / mm)
|
|
972
|
+
* or unsupported (no conversion available).
|
|
973
|
+
* @private
|
|
974
|
+
*/
|
|
975
|
+
static _toBaseUnitStr(value, idUnit) {
|
|
976
|
+
const BASE_IDS = new Set([21, 48, 112]); // g, ml, mm are already base
|
|
977
|
+
if (BASE_IDS.has(idUnit)) return '';
|
|
978
|
+
const f = TigerTag._baseUnitFields(value, value, idUnit);
|
|
979
|
+
if (f.measure_gr !== undefined) return ` (= ${f.measure_gr} g)`;
|
|
980
|
+
if (f.measure_ml !== undefined) return ` (= ${f.measure_ml} ml)`;
|
|
981
|
+
if (f.measure_mm !== undefined) return ` (= ${f.measure_mm} mm)`;
|
|
982
|
+
if (f.measure_mm2 !== undefined) return ` (= ${f.measure_mm2} mm²)`;
|
|
983
|
+
return '';
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
static _baseUnitFields(measure, measureAvailable, idUnit) {
|
|
987
|
+
// Weight: mg(10) g(21) kg(35) → grams
|
|
988
|
+
const WEIGHT_TO_G = { 10: 0.001, 21: 1, 35: 1000 };
|
|
989
|
+
// Volume: ml(48) cl(62) L(79) m³(95) → millilitres
|
|
990
|
+
const VOLUME_TO_ML = { 48: 1, 62: 10, 79: 1000, 95: 1_000_000 };
|
|
991
|
+
// Size: mm(112) cm(130) m(149) → millimetres
|
|
992
|
+
const SIZE_TO_MM = { 112: 1, 130: 10, 149: 1000 };
|
|
993
|
+
// Area: m²(170) → square millimetres
|
|
994
|
+
const AREA_TO_MM2 = { 170: 1_000_000 };
|
|
995
|
+
|
|
996
|
+
const wf = WEIGHT_TO_G[idUnit];
|
|
997
|
+
if (wf !== undefined) {
|
|
998
|
+
return {
|
|
999
|
+
measure_gr: Math.round(measure * wf),
|
|
1000
|
+
measure_available_gr: Math.round(measureAvailable * wf),
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const vf = VOLUME_TO_ML[idUnit];
|
|
1005
|
+
if (vf !== undefined) {
|
|
1006
|
+
return {
|
|
1007
|
+
measure_ml: Math.round(measure * vf),
|
|
1008
|
+
measure_available_ml: Math.round(measureAvailable * vf),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const sf = SIZE_TO_MM[idUnit];
|
|
1013
|
+
if (sf !== undefined) {
|
|
1014
|
+
return {
|
|
1015
|
+
measure_mm: Math.round(measure * sf),
|
|
1016
|
+
measure_available_mm: Math.round(measureAvailable * sf),
|
|
1017
|
+
};
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const af = AREA_TO_MM2[idUnit];
|
|
1021
|
+
if (af !== undefined) {
|
|
1022
|
+
return {
|
|
1023
|
+
measure_mm2: Math.round(measure * af),
|
|
1024
|
+
measure_available_mm2: Math.round(measureAvailable * af),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Unknown unit — no conversion available; return zero base-unit fields so
|
|
1029
|
+
// consumers always get a defined, safe value instead of undefined/garbage.
|
|
1030
|
+
return { measure_gr: 0, measure_available_gr: 0 };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
906
1033
|
/**
|
|
907
1034
|
* Return protocol fields exactly as stored on the chip — no label resolution,
|
|
908
1035
|
* no unit conversion, no date formatting.
|
|
909
1036
|
* @returns {object}
|
|
910
1037
|
*/
|
|
911
1038
|
toRawDict() {
|
|
1039
|
+
// Active color count: aspect2 takes priority for multi-color modes
|
|
1040
|
+
// (Bicolor → 2, Tricolor/Rainbow → 3). Falls back to aspect1 (Basic → 1).
|
|
1041
|
+
const _db = this.db;
|
|
1042
|
+
const _a2cc = (_db.aspect(this.idAspect2) || {}).color_count || 0;
|
|
1043
|
+
const _a1cc = (_db.aspect(this.idAspect1) || {}).color_count || 1;
|
|
1044
|
+
const numColors = _a2cc > 0 ? _a2cc : _a1cc;
|
|
1045
|
+
const colorList = [this.color1Hex, this.color2Hex, this.color3Hex].slice(0, numColors);
|
|
1046
|
+
|
|
912
1047
|
return {
|
|
913
1048
|
id_tigertag: this.idTigertag,
|
|
914
1049
|
id_product: this.idProduct,
|
|
@@ -931,18 +1066,22 @@ class TigerTag {
|
|
|
931
1066
|
bed_min: this.bedTempMin,
|
|
932
1067
|
bed_max: this.bedTempMax,
|
|
933
1068
|
timestamp: this.timestamp,
|
|
934
|
-
color_r2: this.color2R,
|
|
935
|
-
color_g2: this.color2G,
|
|
936
|
-
color_b2: this.color2B,
|
|
937
|
-
color_r3: this.color3R,
|
|
938
|
-
color_g3: this.color3G,
|
|
939
|
-
color_b3: this.color3B,
|
|
1069
|
+
color_r2: numColors >= 2 ? this.color2R : 0,
|
|
1070
|
+
color_g2: numColors >= 2 ? this.color2G : 0,
|
|
1071
|
+
color_b2: numColors >= 2 ? this.color2B : 0,
|
|
1072
|
+
color_r3: numColors >= 3 ? this.color3R : 0,
|
|
1073
|
+
color_g3: numColors >= 3 ? this.color3G : 0,
|
|
1074
|
+
color_b3: numColors >= 3 ? this.color3B : 0,
|
|
940
1075
|
td_raw: this.tdRaw,
|
|
941
1076
|
message: this.customMessage,
|
|
942
1077
|
measure_available: this.measureAvailable,
|
|
1078
|
+
...TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit),
|
|
1079
|
+
num_colors: numColors,
|
|
1080
|
+
color_list: colorList,
|
|
943
1081
|
uid: this.uidHex,
|
|
944
1082
|
product_page_url: this.productPageUrl,
|
|
945
1083
|
api_url: this.apiUrl,
|
|
1084
|
+
img: this.imgUrls,
|
|
946
1085
|
};
|
|
947
1086
|
}
|
|
948
1087
|
|
|
@@ -1084,7 +1223,9 @@ class TigerTag {
|
|
|
1084
1223
|
bedTempMax: doc.data7 ?? 0, // data7 = bed_max
|
|
1085
1224
|
timestamp: doc.timestamp ?? null,
|
|
1086
1225
|
customMessage: doc.message ?? '',
|
|
1087
|
-
|
|
1226
|
+
// Firestore stores TD as a human-readable float (e.g. 1.5).
|
|
1227
|
+
// The chip encodes tdRaw = round(tdValue × 10) as a UInt16.
|
|
1228
|
+
tdRaw: doc.TD != null ? Math.round(doc.TD * 10) : 0,
|
|
1088
1229
|
db,
|
|
1089
1230
|
});
|
|
1090
1231
|
}
|
|
@@ -1141,6 +1282,7 @@ class TigerTag {
|
|
|
1141
1282
|
+ 'Query the api_url field for the full enriched product JSON.',
|
|
1142
1283
|
product_page_url: this.productPageUrl,
|
|
1143
1284
|
api_url: this.apiUrl,
|
|
1285
|
+
img: this.imgUrls,
|
|
1144
1286
|
},
|
|
1145
1287
|
material: {
|
|
1146
1288
|
id: this.idMaterial,
|
|
@@ -1196,6 +1338,7 @@ class TigerTag {
|
|
|
1196
1338
|
measure: {
|
|
1197
1339
|
initial: this.measure,
|
|
1198
1340
|
available: this.measureAvailable,
|
|
1341
|
+
...TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit),
|
|
1199
1342
|
percent: stock,
|
|
1200
1343
|
unit: unitLabel,
|
|
1201
1344
|
description: `Material quantity: ${this.measureAvailable} ${unitLabel} remaining `
|
|
@@ -1289,10 +1432,22 @@ class TigerTag {
|
|
|
1289
1432
|
}
|
|
1290
1433
|
|
|
1291
1434
|
if (this.measure > 0) {
|
|
1435
|
+
const BASE_UNIT_IDS = new Set([21, 48, 112]); // g, ml, mm — already canonical
|
|
1436
|
+
const bf = BASE_UNIT_IDS.has(this.idUnit)
|
|
1437
|
+
? {}
|
|
1438
|
+
: TigerTag._baseUnitFields(this.measure, this.measureAvailable, this.idUnit);
|
|
1439
|
+
const buKey = Object.keys(bf).find(k => k.startsWith('measure_available_'));
|
|
1440
|
+
const buUnit = buKey ? buKey.replace('measure_available_', '') : null;
|
|
1441
|
+
// buUnit is 'gr'→'g', 'ml', 'mm', 'mm2'→'mm²'
|
|
1442
|
+
const buLabel = buUnit ? buUnit.replace('gr', 'g').replace('mm2', 'mm²') : null;
|
|
1443
|
+
const buNote = buLabel
|
|
1444
|
+
? ` — ${bf['measure_available_' + buUnit]} ${buLabel} available, `
|
|
1445
|
+
+ `${bf['measure_' + buUnit]} ${buLabel} total`
|
|
1446
|
+
: '';
|
|
1292
1447
|
parts.push(
|
|
1293
1448
|
`Quantity: ${this.measureAvailable} ${unit} remaining`
|
|
1294
1449
|
+ ` out of ${this.measure} ${unit} initial`
|
|
1295
|
-
+ (stock != null ? ` (${stock}
|
|
1450
|
+
+ (stock != null ? ` (${stock}%${buNote}).` : (buNote ? `${buNote}.` : '.')),
|
|
1296
1451
|
);
|
|
1297
1452
|
}
|
|
1298
1453
|
|
|
@@ -1309,6 +1464,14 @@ class TigerTag {
|
|
|
1309
1464
|
);
|
|
1310
1465
|
}
|
|
1311
1466
|
|
|
1467
|
+
const imgs = this.imgUrls; // snapshot — single Date.now() for this describe() call
|
|
1468
|
+
if (imgs) {
|
|
1469
|
+
parts.push(
|
|
1470
|
+
`Product image (medium 256×256): ${imgs.medium}`
|
|
1471
|
+
+ ` — also available: icon·16 thumb·32 small·64 compact·128 large·512 master·1024.`,
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1312
1475
|
if (this.isSigned) {
|
|
1313
1476
|
parts.push('Tag carries an ECDSA-P256 signature — call tag.verify() to confirm authenticity.');
|
|
1314
1477
|
} else {
|
|
@@ -1339,6 +1502,7 @@ class TigerTag {
|
|
|
1339
1502
|
rec[kMin] != null ? ` (DB: ${rec[kMin]}–${rec[kMax]}${suffix})` : '';
|
|
1340
1503
|
|
|
1341
1504
|
const idHex = this.idTigertag.toString(16).toUpperCase().padStart(8, '0');
|
|
1505
|
+
const imgs = this.imgUrls; // snapshot — single Date.now() for the whole render
|
|
1342
1506
|
|
|
1343
1507
|
return (
|
|
1344
1508
|
`┌─ TigerTag ────────────────────────────────────────────\n`
|
|
@@ -1348,6 +1512,10 @@ class TigerTag {
|
|
|
1348
1512
|
+ (this.productPageUrl
|
|
1349
1513
|
? `│ Product page ${this.productPageUrl}\n│ API JSON ${this.apiUrl}\n`
|
|
1350
1514
|
: '')
|
|
1515
|
+
+ (imgs
|
|
1516
|
+
? `│ Image (med) ${imgs.medium}\n`
|
|
1517
|
+
+ `│ Img sizes icon·16 thumb·32 small·64 compact·128 medium·256 large·512 master·1024\n`
|
|
1518
|
+
: '')
|
|
1351
1519
|
+ `├─ Material ────────────────────────────────────────────\n`
|
|
1352
1520
|
+ `│ Material ${TigerTagDB.label(_db.material(this.idMaterial))} (id=${this.idMaterial})\n`
|
|
1353
1521
|
+ `│ Density ${mat.density != null ? mat.density : '—'} g/cm³\n`
|
|
@@ -1367,8 +1535,8 @@ class TigerTag {
|
|
|
1367
1535
|
+ `│ Drying ${this.dryTemp}°C / ${this.dryTime}h${recNote('dryTemp', 'dryTime', ' h')}\n`
|
|
1368
1536
|
+ `├─ Quantity ────────────────────────────────────────────\n`
|
|
1369
1537
|
+ `│ Unit ${ul}\n`
|
|
1370
|
-
+ `│ Initial ${this.measure} ${ul}\n`
|
|
1371
|
-
+ `│ Available ${this.measureAvailable} ${ul}`
|
|
1538
|
+
+ `│ Initial ${this.measure} ${ul}${TigerTag._toBaseUnitStr(this.measure, this.idUnit)}\n`
|
|
1539
|
+
+ `│ Available ${this.measureAvailable} ${ul}${TigerTag._toBaseUnitStr(this.measureAvailable, this.idUnit)}`
|
|
1372
1540
|
+ (stock != null ? ` (${stock}% remaining)\n` : '\n')
|
|
1373
1541
|
+ `├─ Traceability ────────────────────────────────────────\n`
|
|
1374
1542
|
+ `│ Manufactured ${this.manufacturingDate.toISOString().replace('T', ' ').slice(0, 16)} UTC\n`
|