Repository navigation
Conversation
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
Decodes/encodes the TigerTag Maker/Pro binary format on NTAG213 tags (144 bytes user memory, pages 4-39): product/material/brand IDs, color, weight, temperatures, timestamp, emoji, and a 28-byte user message. Same scope discipline as codec/openprinttag (Donkie#880's PR1): codec and tests only, no wiring into the tag scan/link endpoints from Donkie#1096, no hardware dependency in core, no external dependency at all here (pure stdlib: struct/dataclasses/logging). Split out as its own PR per Donkie's request to keep Donkie#880 small -- TigerTag already ships on retail spools from several brands, so there's a case for it following the OpenPrintTag PR reasonably soon rather than much later. 21 tests covering the binary round trip, color/diameter helpers, and edge cases (truncated user message, 24-bit weight masking, header-only data, invalid UTF-8 in the message field).
Same pattern as OpenPrintTag (codec/openprinttag): tag_decode.decode() gains a "tigertag" branch that maps TigerTagData onto the shared DecodedTag shape, reusing the existing /tag/scan `decoded` field and `create: true` opt-in create/link path -- no changes needed in api/v1/tag.py, models.py, or database/tag.py, since those were already format-agnostic. TigerTag's id_material/id_brand are the format's own catalog IDs, not names, and resolving them needs the optional external-DB addon (a separate, off-by-default PR not merged yet) -- so material_type/material_name/ brand_name stay unset for now rather than surfacing a bare number. color_hex, diameter_mm, and net_weight_g decode directly from the tag. id_product is a catalog SKU, not a per-roll identifier, so it's deliberately not used as external_id (same instance-uniqueness caution as OpenPrintTag's UID-derived id). A TIGERTAG_INIT (blank/uninitialized) tag decodes to None, same as any other unparseable payload. 9 new dispatch/unit tests in tests/test_tag_decode.py, 4 new integration tests across test_scan_decode.py and test_scan_create.py, and a stdlib-only _tigertag_fixtures.py (no new tester dependency, unlike OpenPrintTag's cbor2) mirroring the existing fixture-duplication pattern.
Cross-checked spoolman/tags/tigertag.py against the official spec (TigerTag-Project/TigerTag-RFID-Guide: the README's normative byte table and the offsets its own Sample code/parse_tigertag.py reference parser reads, which agree exactly) and found several places this codec never matched real hardware: - Diameter IDs were invented. The spec's database/id_diameter.json defines only 56 (0x38) = 1.75mm and 221 (0xDD) = 2.85mm. This code instead accepted ids 1/2, plus an "alt" 57 that appears nowhere in the spec, and never recognized the real 221 -- so a genuine 2.85mm retail TigerTag spool decoded as diameter=None and silently defaulted to 1.75mm on auto-create. - The weight field's unit byte was packed on encode but never read on decode. The real layout at offset +20 is a 3-byte "Measure" plus a 1-byte "ID Unit" (database/id_measure_unit.json: 21=g, 35=kg, plus mg/ml/cl/L/m^3/mm/cm/m/m^2) -- the spec's own worked example for a Polymaker TigerTag+ tag reports its weight in kilograms, not grams. Treating the raw measure as always-grams would read 1000kg as 1000g. TigerTagData now exposes `weight_grams`, which is the measure only when id_unit says grams, and None otherwise -- decode.py's _decode_tigertag uses it instead of the raw field. - bed_temp/bed_temp_max were read from offset 36/37; the spec puts them at 30/31 (what this code read as 2 bytes of "reserved"). Fixed offset, not removed -- nothing was wrong with the fields themselves. - user_message was read from offset 58; the spec's Custom Message region starts at 48. Fixed offset. - `emoji` wasn't a real field at all -- offset 54, where it was read from, falls inside the real 28-byte Custom Message region. Removed; nothing downstream read it anyway. - Color 2/3, tag index/count, HueForge TD, Measure Available, and the ECDSA signature pages are real spec fields this codec still doesn't decode -- deliberately out of scope here, same discipline as the OpenPrintTag codec (decode what's used, land more fields later). Also drops encode_ntag213 from production: it has no caller outside tests, the same situation Donkie flagged for OpenPrintTag's own encode_nfcv_memory on Donkie#1112, which was dropped from production in response. Moved into tests/test_tigertag_codec.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JuqDTmgHEWXFA6xJ6UmDsS
Adds three tests built from TigerTag-Project/TigerTag-RFID-Guide's own worked examples (README sections 4, 4.1, 4.2: a Rosa3D red PLA Maker tag, a Polymaker PolyTerra+ tag whose weight is in kilograms, and a blank Init tag) -- real reference field values, the same role the Prusa spec repo's sample dumps played for the OpenPrintTag codec's tests, rather than only synthetic ones. Also updates the existing unit tests for the previous commit's offset/id fixes (diameter 56/221 instead of 1/2/56/57, the weight_grams unit guard, corrected bed_temp, dropped emoji), and moves encode_ntag213 into this file as a test-only helper now that production has no caller for it. test_tag_decode.py's dispatch tests import it from here instead of the production module. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JuqDTmgHEWXFA6xJ6UmDsS
Fixes the integration fixture/tests for the previous commits' id and unit corrections (id_diameter defaults to the real 56/1.75mm id, not the invented 1; build_tigertag takes an id_unit knob). New tests_integration/tests/tag/test_scan_tigertag_hostile.py covers what's actually hostile for a fixed-width binary format: TigerTag has no self-describing types, so the CBOR-style cases test_scan_hostile.py covers (a number field holding a list, a bignum, NaN, Infinity) cannot arrive here at all, and the file's docstring says so instead of silently omitting them. What it does cover, each with the row-count-before/after pattern from test_scan_hostile.py: weight at the 24-bit max (over the shared range check), a non-gram unit (must come through as no weight, not a scaled one), a too-short payload, an unmapped magic number, the blank Init tag (now also checked for create:true + row counts, not just decoded absence), and an unmapped diameter id as the positive-path case (still creates, using the 1.75mm default). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JuqDTmgHEWXFA6xJ6UmDsS
goeland86
force-pushed
the
codec/tigertag
branch
from
November 10, 2026 08:17
0d23e19 to
1866708
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Decodes the TigerTag Maker/+ binary format (NTAG213, 144 bytes user memory) and wires it
into
/tag/scan, reusing the samedecoded/create: truecontract #1112 (OpenPrintTag)established. Same split as that PR: a format-specific codec (
spoolman/tags/tigertag.py)mapping onto the shared
DecodedTag, with every write going through the already-validated,already-transactional
create_spool_from_decoded_tagpath inspoolman/database/tag.py.What's in this PR
spoolman/tags/tigertag.py: decodes the fixed-width binary header (id_tigertag/id_product/id_material/id_aspect/id_type/id_diameter/id_brand, color RGBA, weight +
measure unit, nozzle/bed/drying temperatures, timestamp, a 28-byte custom message).
spoolman/tags/decode.py: atigertagbranch mappingTigerTagDataonto the sharedDecodedTag-- no changes needed inapi/v1/tag.py,models.py, ordatabase/tag.py,since those are already format-agnostic.
id_material/id_brandare the format's own catalog IDs, not names --resolving them to real names needs an external DB lookup, which is deliberately a
separate PR, feat(nfc): optional TigerTag external-DB add-on for name/density resolution #1325 (same reasoning as OpenPrintTag's
gtin).material/name/vendor_namestay unset here rather than surfacing a bare catalog number.Validated against the real spec, not guesses
Before opening this, I went through every category of problem raised on #1112's three
review rounds and checked whether it still needed addressing here, plus cross-checked this
codec against the official TigerTag spec (
TigerTag-Project/TigerTag-RFID-GuideonGitHub) the same way #1112 was checked against OpenPrintTag's own spec repo.
Inherited for free, since this branch is rebased on top of #1112's fixes: one
transaction for the whole auto-create (vendor/filament/spool/tag-link flush together,
commit once), range-checked density/weight/diameter before any write, tolerant
diameter/weight matching (plus color) when reusing an existing filament, the decoded tag
never being echoed back when create refuses it, non-ASCII
payload_b64handling, anodd-length UID answering 400, and the
TagDecodedInfo/instance_uuidfield naming.Structurally doesn't apply to this format: TigerTag is a fixed-width, big-endian
struct-packed binary layout, not a self-describing one like OpenPrintTag's CBOR. Adensity/weight that's a list, a bignum, NaN, or Infinity can't arrive here -- every field
is a plain unsigned int of a fixed byte width, and there's no way to encode a different
Python type into any of them. No new dependency, no NDEF wrapping either.
Found and fixed by checking the real spec (not assumed from a guess at some point --
database/id_diameter.json,database/id_measure_unit.json, and the README's three fullyworked examples, cross-confirmed against the offsets its own
Sample code/ parse_tigertag.pyreference parser reads):1/2, plus an "alt"57with no spec backing) instead ofthe real
56(0x38, 1.75mm) /221(0xDD, 2.85mm) -- a real 2.85mm spool would'vesilently defaulted to 1.75mm.
Polymaker worked example reports its weight in kilograms, not grams -- trusting the raw
value as always-grams would read 1000kg as 1000g.
weight_gramsis nowNoneunless thetag says grams.
bed_temp/bed_temp_maxwere read from the wrong offset (fixed);user_messagewasread from the wrong offset (fixed);
emojiwasn't a real field at all (removed -- theoffset it was read from falls inside the real Custom Message region).
encode_ntag213had no caller outside tests -- same situation as OpenPrintTag'sencode_nfcv_memory, dropped from production there in response to review. Same fix here.Color 2/3, tag index/count, HueForge TD, Measure Available, and the ECDSA signature pages
are real spec fields this PR still doesn't decode -- deliberately out of scope, same
discipline as OpenPrintTag's own PR1 (decode what's used, land more fields later).
Known limitation
Without the external-DB add-on (#1325, not part of this PR and mergeable independently of
it), a bare TigerTag tag has no product name --
create: truestill makes a usable spool(diameter/weight/color all come straight off the tag), just with an unnamed filament and no
vendor. Intentional, and covered by
test_create_true_on_an_unmatched_tigertag_creates_and_links_a_spool_without_names.Test plan
python tests_integration/run.py sqlite), 413 unit testspass (
pytest tests),ruff check/ruff format --checkclean,uv lock --checkclean(no dependency change).
(
tests/test_tigertag_codec.py), including the kilograms case.tests_integration/tests/tag/test_scan_tigertag_hostile.py: the hostile cases thatactually apply to a fixed-width format (over-range weight, a non-gram unit, a too-short
payload, an unmapped magic number, the blank Init tag), each with before/after row-count
checks, plus the positive-path case (an unmapped diameter id still creates, using the
default).
🤖 Generated with Claude Code