From 3b7afbe980c1a181dd9e28456ad334cf8a477b08 Mon Sep 17 00:00:00 2001 From: BenGlut Date: Thu, 1 Oct 2026 00:18:26 +0200 Subject: [PATCH] spec: byte 0x0D[3] is tag index / tag count, index first (v2.2) Amends the v2.2 byte before any SDK ships it. The high nibble is now the index and the low nibble the count, so the hex reads the way a person says it: 0x12 is tag 1 of 2, 0x22 tag 2 of 2, 0x11 a single tag. The fields are named tag index / tag count: what is counted is the TigerTags on a spool (a twin tag carries two); "chip" stays for the NTAG itself. --- README.md | 49 +++++++++++++++++++---------------- Sample code/parse_tigertag.py | 28 ++++++++++---------- assets/chip_layout.svg | 8 +++--- llms.txt | 9 ++++--- 4 files changed, 50 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 7eb0abd..1bf2866 100644 --- a/README.md +++ b/README.md @@ -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) | @@ -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 @@ -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. --- @@ -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 | --- @@ -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) diff --git a/Sample code/parse_tigertag.py b/Sample code/parse_tigertag.py index af24c17..1c9d7bb 100644 --- a/Sample code/parse_tigertag.py +++ b/Sample code/parse_tigertag.py @@ -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 @@ -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)) @@ -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: @@ -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), @@ -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) @@ -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, } @@ -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" diff --git a/assets/chip_layout.svg b/assets/chip_layout.svg index 135baf7..d203c47 100644 --- a/assets/chip_layout.svg +++ b/assets/chip_layout.svg @@ -104,13 +104,13 @@ Timestamp + Twin Tag ID โ€” u32 BE Seconds since 2000-01-01 GMT, shared by paired left/right tags - + 0x0D Color 2 โ€” R ยท G ยท B - Chips - Secondary color ยท chip count / index (e.g. 0x21 = 1 of 2) + Tags + Secondary color ยท tag index / count (e.g. 0x12 = tag 1 of 2) @@ -187,7 +187,7 @@ Print settings (Nozzle, Dry, Bed) - Metadata (Timestamp, Chips, Message) + Metadata (Timestamp, Tags, Message) Quantity (Measure, Measure Available) diff --git a/llms.txt b/llms.txt index 7c8a57d..0743008 100644 --- a/llms.txt +++ b/llms.txt @@ -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) @@ -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