Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ is ISO 14443-3 compatible (NTAG21x family).
| `0x0B` | `3` | `+31` | 1 byte | Bed Temp Max | u8 | Maximum bed temperature (°C) |
| `0x0C` | `0-3` | `+32` | 4 bytes | Twin Tag ID & Timestamp | u32 BE | Seconds since 2000-01-01 GMT + twin tag pairing ID (see section 2.9) |
| `0x0D` | `0-2` | `+36` | 3 bytes | Color 2 (RGB) | bytes | Secondary color R/G/B |
| `0x0D` | `3` | `+39` | 1 byte | Chip count / index | u8 | High nibble = chips on the spool, low nibble = this chip; `0x00` = unknown (see section 2.11) |
| `0x0D` | `3` | `+39` | 1 byte | Tag index / count | u8 | High nibble = this tag, low nibble = tags on the spool (`0x12` = tag 1 of 2); `0x00` = unknown (see section 2.11) |
| `0x0E` | `0-2` | `+40` | 3 bytes | Color 3 (RGB) | bytes | Tertiary color R/G/B |
| `0x0E` | `3` | `+43` | 1 byte | Reserved | u8 | Must be `0x00` |
| `0x0F` | `0-1` | `+44` | 2 bytes | TD (HueForge) | u16 BE | HueForge Transmission Distance × 10 (see section 2.10) |
Expand Down Expand Up @@ -813,8 +813,8 @@ This shared value enables:
🧠 Think of the `Time Stamp` as a **"twin tag ID"** in addition to being a clock — a clever way to bind two tags using time as the key.

The shared timestamp says *which* chips belong together; it does not say
*how many* there are. That is the job of the chip count / index byte —
see [section 2.11](#211-chip-count--chip-index).
*how many* there are. That is the job of the tag index / count byte —
see [section 2.11](#211-tag-index--tag-count).

## 2.10 Transmission Distance (TD) — HueForge value

Expand Down Expand Up @@ -845,45 +845,50 @@ the chip — HueForge reads it without any manual entry.
- Atome3D.com — https://www.atome3d.com/products/biqu-ajax-td1s-v1-0
- Tigertag.io — https://shop.tigertag.io/products/biqu-ajax-td1s-v1-0

## 2.11 Chip count & chip index
## 2.11 Tag index & tag count

Page `0x0D`, byte 3 (offset `+39`) tells a reader **how many TigerTag
chips the spool carries** and **which one it has just read**. With it,
a reader that scans one chip of a twin-tagged spool knows another chip
is waiting — and can ask for it to collect the missing UID.
Page `0x0D`, byte 3 (offset `+39`) tells a reader **which TigerTag it has
just read** and **how many TigerTags the spool carries**. With it, a reader
that scans one tag of a twin-tagged spool knows another tag is waiting —
and can ask for it to collect the missing UID.

**Encoding:** one byte, split into two 4-bit halves (nibbles).
A *tag* here is one TigerTag on the spool; a twin-tagged spool carries two.
Each tag is its own NTAG chip, with its own UID.

**Encoding:** one byte, split into two 4-bit halves (nibbles), in the order
a person reads it — **index, then count** — so the hex value reads as-is:
`0x12` is tag **1** of **2**.

| Bits | Field | Values |
| ---- | ----- | ------ |
| 7–4 (high nibble) | Chip count | `0` = unknown, `1` = single chip, `2` = twin tag, … up to `15` |
| 3–0 (low nibble) | Chip index | `0` = unknown, `1` = first chip, `2` = second chip, … |
| 7–4 (high nibble) | Tag index | `0` = unknown, `1` = first tag, `2` = second tag, … |
| 3–0 (low nibble) | Tag count | `0` = unknown, `1` = single tag, `2` = twin tag, … up to `15` |

```c
uint8_t b = page0x0D[3];
uint8_t count = b >> 4; // chips on the spool — 0 = unknown
uint8_t index = b & 0x0F; // this chip — 0 = unknown
uint8_t index = b >> 4; // this tag — 0 = unknown
uint8_t count = b & 0x0F; // tags on the spool — 0 = unknown
```

**Examples:**
- `0x00` → Unknown (every chip written before protocol v2.2)
- `0x11` → Single chip — nothing else to scan
- `0x21` → Twin tag, chip 1 of 2
- `0x22` → Twin tag, chip 2 of 2
- `0x00` → Unknown (every tag written before protocol v2.2)
- `0x11` → Single tag (1/1) — nothing else to scan
- `0x12` → Twin tag, tag 1 of 2
- `0x22` → Twin tag, tag 2 of 2

**Rules:**
- All chips of the same spool MUST carry the same chip count and the
- All tags of the same spool MUST carry the same tag count and the
same `Timestamp` (section 2.9); each one carries its own index.
- When the count is set, the index MUST be between `1` and the count.
A writer that knows the count but not the index writes `0` as the
index (e.g. `0x20`).
index (e.g. `0x02`).
- Readers MUST treat `0x00` as "unknown" and fall back to the timestamp
alone, exactly as before v2.2.
- The byte is not covered by the ECDSA signature (section 3): setting it
never invalidates a signed TigerTag+ chip.

**Backward compatibility:** this byte was reserved and required to be
`0x00` until v2.2, so every chip already written reads as "unknown" and
`0x00` until v2.2, so every tag already written reads as "unknown" and
stays valid. Readers that predate v2.2 ignore it.

---
Expand Down Expand Up @@ -1473,7 +1478,7 @@ this page grants any right to those marks.
| 1.0 | 2025-06-09 | Initial public format | TigerTag Team |
| 2.0 | 2026-03-11 | Corrected binary memory layout and NTAG21x capacity alignment | TigerTag Team |
| 2.1 | 2026-05-18 | Add UID documentation, system pages layout, fix example hex values | TigerTag Team |
| 2.2 | 2026-09-30 | Add chip count / chip index at page `0x0D` byte 3 (previously reserved) | TigerTag Team |
| 2.2 | 2026-09-30 | Add tag index / tag count at page `0x0D` byte 3 (previously reserved) — `0x12` = tag 1 of 2 | TigerTag Team |

---

Expand Down Expand Up @@ -1734,7 +1739,7 @@ Binary layout (offsets from start of page 0x04):
+31 u8 Bed Temp Max (°C)
+32 u32 BE Timestamp + Twin Tag ID (secs since 2000-01-01 GMT)
+36 3 B Color 2 RGB
+39 u8 Chip count (high nibble) / chip index (low nibble), 0x00 = unknown
+39 u8 Tag index (high nibble) / tag count (low nibble), 0x12 = tag 1 of 2, 0x00 = unknown
+40 3 B Color 3 RGB
+43 u8 reserved 0x00
+44 u16 BE HueForge TD (value × 10, range 10..1000)
Expand Down
28 changes: 14 additions & 14 deletions Sample code/parse_tigertag.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
0x0B +31 1B Bed Temp Max u8 °C
0x0C +32 4B Twin Tag ID+Timestamp u32 BE sec since 2000-01-01 GMT
0x0D +36 3B Color 2 (RGB) bytes
0x0D +39 1B Chip count / index u8 high nibble = chips on spool, low = this chip (0=unknown)
0x0D +39 1B Tag index / count u8 high nibble = this tag, low = tags on spool (0x12 = 1 of 2)
0x0E +40 3B Color 3 (RGB) bytes
0x0E +43 1B Reserved u8 = 0x00
0x0F +44 2B TD HueForge u16 BE value / 10
Expand Down Expand Up @@ -617,8 +617,8 @@ class TigerTag:
# ── HueForge ──────────────────────────────────────────────────────────────
td_raw : int # u16 BE — actual TD = td_raw / 10 (0=undefined, 1-1000 valid)

# ── Chip count / index (page 0x0D byte 3) ─────────────────────────────────
chip_info : int = 0 # u8 — high nibble = chips on the spool, low nibble = this chip (0=unknown)
# ── Tag index / count (page 0x0D byte 3) ──────────────────────────────────
tag_info : int = 0 # u8 — high nibble = this tag, low nibble = tags on the spool (0=unknown)

# ── Signature (optional, pages 0x18-0x27) ─────────────────────────────────
signature_r : bytes = field(default_factory=lambda: bytes(32))
Expand Down Expand Up @@ -655,14 +655,14 @@ def td_value(self) -> float:
return self.td_raw / 10.0

@property
def chip_count(self) -> int:
"""Number of TigerTag chips on the spool. 0=unknown, 1=single, 2=twin tag."""
return self.chip_info >> 4
def tag_index(self) -> int:
"""Which of the spool's TigerTags this one is, from 1. 0=unknown."""
return self.tag_info >> 4

@property
def chip_index(self) -> int:
"""Which of those chips this one is, from 1. 0=unknown."""
return self.chip_info & 0x0F
def tag_count(self) -> int:
"""Number of TigerTags on the spool. 0=unknown, 1=single, 2=twin tag."""
return self.tag_info & 0x0F

@property
def manufacturing_date(self) -> datetime:
Expand Down Expand Up @@ -844,7 +844,7 @@ def u32(o: int) -> int: return struct.unpack_from(">I", data, o)[0]
color2_r = u8(36),
color2_g = u8(37),
color2_b = u8(38),
chip_info = u8(39),
tag_info = u8(39),
color3_r = u8(40),
color3_g = u8(41),
color3_b = u8(42),
Expand Down Expand Up @@ -902,7 +902,7 @@ def p32(v): return struct.pack(">I", v & 0xFFFFFFFF)
+ bytes([self.dry_temp, self.dry_time, self.bed_temp_min, self.bed_temp_max])
+ p32(self.timestamp)
+ bytes([self.color2_r, self.color2_g, self.color2_b])
+ bytes([self.chip_info & 0xFF])
+ bytes([self.tag_info & 0xFF])
+ bytes([self.color3_r, self.color3_g, self.color3_b])
+ b"\x00"
+ p16(self.td_raw)
Expand Down Expand Up @@ -1092,8 +1092,8 @@ def to_dict(self, db: TigerTagDB = None) -> Dict:
"timestamp": self.timestamp,
"manufacturing_date": self.manufacturing_date.isoformat(),
"twin_tag_pairing_id": self.timestamp,
"chip_count": self.chip_count or None,
"chip_index": self.chip_index or None,
"tag_index": self.tag_index or None,
"tag_count": self.tag_count or None,
"custom_message": self.custom_message,
"signed": self.is_signed,
}
Expand Down Expand Up @@ -1148,7 +1148,7 @@ def rec_note(kmin, kmax, suffix="°C"):
f"├─ Traceability ────────────────────────────────────────\n"
f"│ Manufactured {self.manufacturing_date.strftime('%Y-%m-%d %H:%M UTC')}\n"
f"│ Twin tag ID {self.timestamp}\n"
f"│ Chip {self.chip_index or '?'} of {self.chip_count or '?'}" + (" (unknown)\n" if self.chip_info == 0 else "\n") +
f"│ Tag {self.tag_index or '?'} of {self.tag_count or '?'}" + (" (unknown)\n" if self.tag_info == 0 else "\n") +
f"│ Message {self.custom_message!r}\n"
f"├─ Signature ───────────────────────────────────────────\n"
f"│ ECDSA {sig}\n"
Expand Down
8 changes: 4 additions & 4 deletions assets/chip_layout.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 5 additions & 4 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ User memory layout (offsets relative to start of page 0x04):
+31 u8 Bed Temp Max — °C
+32 u32 BE Timestamp + Twin Tag ID — secs since 2000-01-01 GMT
+36 3 bytes Color 2 RGB
+39 u8 Chip count / index — high nibble = chips on spool, low = this chip, 0x00 = unknown
+39 u8 Tag index / count — high nibble = this tag, low = tags on spool, 0x00 = unknown
+40 3 bytes Color 3 RGB
+43 u8 reserved (0x00)
+44 u16 BE HueForge TD — value × 10 (range 10–1000)
Expand All @@ -608,9 +608,10 @@ User memory layout (offsets relative to start of page 0x04):

Capacity: 80 bytes user data + 64 bytes signature = 144 bytes.

Chip count / index (+39, since v2.2): count = byte >> 4, index = byte & 0x0F.
0x11 = single chip; 0x21 / 0x22 = chip 1 / 2 of a twin tag (both share
the same Timestamp); 0x00 = unknown (all chips written before v2.2).
Tag index / count (+39, since v2.2): index = byte >> 4, count = byte & 0x0F,
so the hex reads as "index/count". 0x11 = single tag (1/1); 0x12 / 0x22 =
tag 1 / 2 of a twin tag (both share the same Timestamp); 0x00 = unknown
(all tags written before v2.2).

ECDSA signed message: `SHA-256( UID_bytes (7) + block4 (4) + block5 (4) )`.
The public key for each protocol variant is published in
Expand Down
Loading