Skip to content

feat: Add NFC spool identification: TigerTag and OpenPrintTag support - #880

Closed
goeland86 wants to merge 26 commits into
Donkie:masterfrom
goeland86:pr/nfc-support
Closed

feat: Add NFC spool identification: TigerTag and OpenPrintTag support#880
goeland86 wants to merge 26 commits into
Donkie:masterfrom
goeland86:pr/nfc-support

Conversation

@goeland86

@goeland86 goeland86 commented Mar 10, 2026

Copy link
Copy Markdown

Summary

Adds NFC tag support to Spoolman, enabling automatic spool identification by scanning NFC-equipped filament spools. Two open tag formats are supported:

Usage

There are three ways to scan NFC tags with Spoolman:

Browser-based scanning — The Spoolman web client includes an NFC scanner modal that uses the Web NFC
API
to read and write tags directly from your phone or NFC-equipped computer. No
additional hardware or software needed — just open the Spoolman UI in Chrome on Android and tap a tag.

Server-side USB reader — Attach a USB NFC reader (e.g. ACR1552U) to the machine running Spoolman. Enable with SPOOLMAN_NFC_ENABLED=TRUE. The
/api/v1/nfc/read and /api/v1/nfc/write endpoints control the reader directly.

External reader with API integration — For setups where the NFC reader is physically attached to the printer (not the Spoolman server), the POST /api/v1/nfc/lookup endpoint accepts raw tag memory from any client and handles all decoding and spool matching server-side. An example implementation
is klipper-nfc-daemon, a lightweight daemon that runs on a Klipper host, polls an NFC reader, and
automatically sets the active spool in Moonraker. It supports PN532 (UART), PN5180 (SPI), and ACR1552U (USB) readers.

Backend

  • POST /api/v1/nfc/lookup — unified endpoint that auto-detects tag format from raw bytes, matches to a spool, and optionally auto-creates
    spool/filament/vendor records from tag data
  • POST /api/v1/nfc/read / write / encode — server-side NFC reader endpoints
  • POST /api/v1/nfc/create-from-tag — create spool from decoded TigerTag data
  • TigerTag binary codec (big-endian, NTAG213 144-byte format) with external product DB sync
  • OpenPrintTag NDEF TLV parser + CBOR decoder with UUID derivation per spec
  • Spool matching by external_id (tigertag_{id} or opt_{instance_uuid}) — no database migrations needed

Frontend

  • NFC scanner modal with browser Web NFC API support
  • NFC tag write modal for encoding spools onto tags
  • Client-side TigerTag codec for browser-based tag reading/writing

Infrastructure

  • Dockerfile updated with libusb and uv --extra nfc for optional NFC dependencies
  • New optional dependencies: cbor2, ndeflib, nfcpy (in [nfc] extra)
  • Environment flags: SPOOLMAN_NFC_ENABLED, SPOOLMAN_TIGERTAG_ENABLED

Design decisions

  • NFC support is fully optional — gated behind environment variables, no impact on existing installations
  • No database migrations — uses existing external_id field on Filament for tag-to-spool mapping
  • Tag format auto-detection: 0xE1 first byte = OpenPrintTag (NFC-V capability container), 0x5C15E2E4 magic = TigerTag

Test plan

  • Existing tests pass (no schema changes)
  • TigerTag: scan NTAG213 tag → spool matched by external_id
  • TigerTag: scan unknown tag → auto-create spool from TigerTag product DB
  • OpenPrintTag: decode CBOR payload with all field types
  • OpenPrintTag: match spool by instance_uuid derived from tag UID
  • OpenPrintTag: auto-create spool with vendor/filament from tag data
  • NFC endpoints return 200 with enabled: false when NFC is not configured
  • Frontend NFC scanner modal opens and reads tags via Web NFC API

--- UPDATE ---

Rebased onto current master, which in the meantime picked up the client_v2 Svelte rewrite (now the default frontend), several security fixes, and general backend work. This closes the gap mentioned above — NFC support now exists on both frontends.

Rebase

  • Merged current upstream master (client_v2, security hardening, misc backend fixes). No conflicts in NFC-owned files; the few touching shared files (entrypoint.sh, main.py, router.py, externaldb.py) were straightforward import/wiring merges.
  • uv.lock regenerated against the merged pyproject.toml — that diff is dependency-version churn, not NFC-related.

client_v2 (new)

NFC never existed on client_v2 before this update — it only had the legacy React client's support. Added, following client_v2's own component/store conventions (not a port of the React JSX):

  • TopBar scan button (Nfc icon), shown only when a server reader or Web NFC is detected
  • Scan modal: server-reader and Web NFC paths, unmatched-tag → create-spool-from-tag flow
  • Spool inspector: Bind tag / Write tag actions, mirroring the legacy client's modals
  • TigerTag NTAG213 binary codec ported to the Spool/Filament shapes client_v2 uses (colors[] instead of a single color_hex, camelCase fields)

The legacy React client's NFC support (SPOOLMAN_LEGACY_CLIENT=true) is unchanged.

Qidi tag support (MIFARE Classic 1K)

Previously only announced in the comments below — now part of the branch and this description. Alongside TigerTag and OpenPrintTag:

  • Auto-detection of NTAG213 (TigerTag) vs NFC-V (OpenPrintTag) vs MIFARE Classic (Qidi) on server-side reads
  • UID-based binding, since Qidi tags carry material + color but no unique spool ID; fuzzy-matches by material/color when unbound
  • Auto-create from unrecognized tags; nearest-color snap to the 24-entry Qidi palette when writing
  • Dual-key MIFARE auth (Qidi factory key, falling back to the default blank-tag key)

Known limitation: Web NFC can't read/write MIFARE Classic at all (browser API restriction), so Qidi tags only work through the server-attached reader or the /api/v1/nfc/lookup endpoint — not the browser scan/write path. Still untested against real Qidi hardware by anyone but the reporting user; further test reports welcome.

Test plan (additions)

  • client_v2: scan → matched spool opens via /?sel=spool:<id>

  • client_v2: unmatched tag → create-from-tag flow

  • client_v2: bind/write modals against a connected TigerTag reader

  • Qidi: real-hardware round trip (read/write/auto-create) — reported working by @turw41th via server reader, not yet verified by me directly

  • OpenPrintTag: still no hardware access; unchanged from the original test plan above

    🤖 Generated with the help of Claude Code

goeland86 and others added 10 commits March 9, 2026 11:17
Fix two TS errors introduced in the TigerTag implementation:
- header/index.tsx: use correct RefineThemedLayoutHeaderProps export
- spools/show.tsx: use `query` instead of `queryResult` from useShow()

Update implementation log with verification results (223 tests pass,
frontend builds clean).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix TigerTag API base URL to use correct Xano path (/api:tigertag/)
- Rewrite TigerTag sync to use paginated POST /product/get/all endpoint
- Update TigerTagProduct model to match actual API response schema
- Replace hishel with httpx for TigerTag HTTP calls (POST not cacheable)
- Fix Optional[str] -> str | None for Python 3.10+ compatibility
- Remove duplicate [project.license] section from pyproject.toml
- Regenerate uv.lock with nfcpy optional dependency

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ensures the frontend is always built with the correct API URL
without needing to pass it manually.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…odec

- Add POST /api/v1/nfc/create-from-tag endpoint to create filament+spool
  from decoded TigerTag data (with TigerTag external DB lookup)
- Add browser-side NFC scan support with create-from-tag UI in scanner modal
- Fix TigerTag binary codec to use big-endian encoding (matching the actual
  TigerTag RFID Guide spec; code examples on doc.tigertag.io were misleading)
- Fix weight field decoding: upper 24 bits = weight, lower 8 = unit ID
- Fix NFC read service page stepping (range 4-40 step 4, not step 1)
- Add TigerTag diameter ID mapping (56->1.75mm, 57->2.85mm)
- Add client-side TigerTag codec (tigertagCodec.ts) with matching BE format
- Add Dockerfile NFC support (libusb, uv --extra nfc)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add openprinttag_codec.py: NDEF TLV parser + CBOR decoder for NFC-V
  (ISO 15693) tags using the OpenPrintTag spec (Prusa). Decodes all main
  section fields (UUIDs, material, brand, color, temps, weights) and aux
  section (consumed_weight). Includes UUID derivation and aux write-back.
- Add openprinttag_lookup.py: spool matching by instance_uuid or
  package_uuid, with auto-create (vendor + filament + spool from tag data).
- Update /api/v1/nfc/lookup: auto-detect tag format from raw bytes
  (0xE1 = OpenPrintTag, 0x5C15E2E4 = TigerTag). Add tag_type, nfc_tag_uid,
  and auto_create request fields. Return tag_format in response.
- Add cbor2 and ndeflib to [nfc] optional dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add NFC tag support for automatic spool identification using the TigerTag
format (ISO 14443A / NTAG213). Spools are matched by their TigerTag
product ID against the filament external_id field — no schema changes.

Backend:
- POST /api/v1/nfc/lookup — accepts raw tag binary or id_product,
  returns matched spool_id
- POST /api/v1/nfc/read, /write, /encode — server-side USB reader
- POST /api/v1/nfc/create-from-tag — auto-create spool from tag data
  with TigerTag external product DB lookup
- TigerTag binary codec (big-endian NTAG213 144-byte format)
- TigerTag product DB sync (paginated Xano API)

Frontend:
- NFC scanner modal with browser Web NFC API
- NFC write modal for encoding spools onto NTAG213 tags
- Client-side TigerTag codec

Infrastructure:
- Dockerfile: add libusb, uv --extra nfc
- Environment flags: SPOOLMAN_NFC_ENABLED, SPOOLMAN_TIGERTAG_ENABLED
- Optional dependency: nfcpy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add support for the OpenPrintTag standard (Prusa's open NFC spec) using
ISO 15693 / NFC-V tags with NDEF/CBOR-encoded filament data.

- NDEF TLV parser + CBOR decoder for NFC-V tag memory (ICODE SLIX2)
- Decodes all main section fields: UUIDs, material type/class, brand,
  color, temperatures, weights, density, diameter
- Decodes aux section: consumed_weight for usage tracking
- UUID derivation per spec (UUIDv5 from tag UID, brand name, etc.)
- Spool matching by instance_uuid (per-spool) or package_uuid (per-product)
- Auto-create vendor + filament + spool from tag data on first scan
- /api/v1/nfc/lookup auto-detects format: 0xE1 = OpenPrintTag,
  0x5C15E2E4 = TigerTag
- New request fields: tag_type, nfc_tag_uid, auto_create
- New optional dependencies: cbor2, ndeflib

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@goeland86 goeland86 changed the title Add NFC spool identification: TigerTag and OpenPrintTag support feat: Add NFC spool identification: TigerTag and OpenPrintTag support Mar 10, 2026
goeland86 and others added 4 commits March 10, 2026 16:56
- Add per-spool matching using (id_product, timestamp) as a composite key
  stored in SpoolField "nfc_tag_id". Both sides of paired tags share the
  same timestamp, so they resolve to the same spool.
- Add real-time TigerTag API product lookup using tag UID + product_id,
  since the tag's product_id differs from the API's internal database IDs.
- Sync and cache TigerTag brand and material lookup tables from the API
  (GET /brand/get/all, GET /material/get/all) for name resolution.
- Fall back to brand/material name resolution when product lookup fails,
  creating filaments named e.g. "Rosa3D PLA" instead of "TigerTag tigertag_N".
- Add auto_create support for TigerTag in the /lookup endpoint.
- Fix .unique() call on filament external_id query (joined eager loads).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add per-spool matching using (id_product, timestamp) as a composite key
  stored in SpoolField "nfc_tag_id". Both sides of paired tags share the
  same timestamp, so they resolve to the same spool.
- Add real-time TigerTag API product lookup using tag UID + product_id,
  since the tag's product_id differs from the API's internal database IDs.
- Sync and cache TigerTag brand and material lookup tables from the API
  (GET /brand/get/all, GET /material/get/all) for name resolution.
- Fall back to brand/material name resolution when product lookup fails,
  creating filaments named e.g. "Rosa3D PLA" instead of "TigerTag tigertag_N".
- Add auto_create support for TigerTag in the /lookup endpoint.
- Fix .unique() call on filament external_id query (joined eager loads).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New POST /api/v1/nfc/bind endpoint and "Link NFC Tag" button on the
spool detail page. Allows scanning an NFC tag and binding it to an
existing spool via SpoolField nfc_tag_id, so future scans resolve
to that specific spool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New POST /api/v1/nfc/bind endpoint and "Link NFC Tag" button on the
spool detail page. Allows scanning an NFC tag and binding it to an
existing spool via SpoolField nfc_tag_id, so future scans resolve
to that specific spool.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@goeland86

Copy link
Copy Markdown
Author

I unfortunately don't have OpenPrintTag tested because I lack the physical hardware at present. If anyone else wants to test this, please do share the test results.

@akira69

akira69 commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

Without digging into this or thinking too much, I wonder if you have considered to cross support with spoolease too - though the NFC info would be coming via a spoolease api instead of direct from the scanner. anyway, cool! more features :)

@goeland86

Copy link
Copy Markdown
Author

I had not, but I can easily try to get that implemented? I don't pretend to know every format or effort to support nfc for 3D printing 😉.

I would need help getting the tests done for anything I don't have access to, but I'm happy to do it.

goeland86 and others added 4 commits March 11, 2026 10:00
TigerTag+ uses magic number 0x12C4C408 and enables cloud-synced product
IDs (0x00000001–0xFFFFFFFE) while sharing the same 144-byte binary format.
Both variants are now recognized for reading/lookup; writing still uses
Maker V1 format since we lack the ECDSA signing key for TigerTag+.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	client/src/components/nfcBindModal.tsx
The TigerTag RFID Guide README has incorrect hex values for magic
numbers. The actual values from the id_version.json API database
(confirmed by reading real tags written by the TigerTag mobile app):

- Maker V1: 0x5BF59264 (was 0x5C15E2E4 from README)
- TigerTag+: 0xBC0FCB97 (was 0x12C4C408 from README)
- Init: 0x6C41A2E1 (was 0x6C46A3C1 from README)

Also adds TigerTag+ detection, isTigerTag() helpers, and improves
the browser NFC write warning about NDEF incompatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	client/src/utils/tigertagCodec.ts
#	spoolman/tigertag_codec.py
The ACR122U NFC reader would disappear from the UI after being
unplugged/replugged because Docker's devices: directive snapshots
device nodes at container start. Switched docker-compose to a live
volumes: bind mount of /dev/bus/usb and added auto-reconnect logic
to nfc_service.py so the backend recovers without a container restart.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@turw41th

turw41th commented Apr 12, 2026

Copy link
Copy Markdown

How easy would it be to also add support for Qidi NFC tags for usage with their Qidi Box system? They have it documented here: https://wiki.qidi3d.com/en/QIDIBOX/RFID
I would be happy to help you test this as I have a Qidi Box.

@goeland86

Copy link
Copy Markdown
Author

Let me look. The question is whether I have the hardware to test it. I'm mostly using the tiger tag format right now for cost reasons.

@goeland86

Copy link
Copy Markdown
Author

@turw41th so looking at it closer, it looks like we can add it easily. But we'd be using the tag's hardware UID to bind it to an entry in Spoolman. It's exactly what it does with Tigertags, so that's not an issue architecturally. But they're MIFARE tags as opposed to NTAG213 which is what I have on hand, so I won't be able to test it. If you're willing to test it out, I'll have my fork updated with it shortly.

Add support for Qidi RFID tags alongside TigerTag and OpenPrintTag.
Qidi tags use MIFARE Classic 1K (FM11RF08S) with a 3-byte payload
encoding material code, color code, and manufacturer ID.

- Auto-detect Qidi tags from MIFARE Classic product string or block data
- Dual-key authentication (Qidi custom + factory default)
- UID-based spool binding since tags lack unique spool identifiers
- Fuzzy matching by material type + color as fallback
- Auto-create spools with Qidi vendor, material name, and mapped color
- Read/write Qidi format from web UI with tag format selector
- 35 material codes and 24 color codes with RGB hex values
- All existing TigerTag/OpenPrintTag endpoints remain backwards-compatible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@goeland86

Copy link
Copy Markdown
Author

As Qidi tags only support a very specific set of colors, but spoolman allows to use all hex colors? How is that managed if I have never scanned an already written qidi specific tag, but only want to write my own tags?

Reading a Qidi tag → The color index (1-24) is mapped to a known RGB hex value via a lookup table (e.g. color code 18 = Red = #FF362D). That hex value gets stored on the Spoolman spool.

Writing a Qidi tag from Spoolman → Since Spoolman allows arbitrary hex colors but Qidi only supports 24 predefined colors, I use nearest-color matching: the code computes the Euclidean distance in RGB space between your spool's color and all 24 Qidi palette entries, then picks the closest one. If it's an exact match it returns immediately, otherwise it snaps to the nearest color. I'll admit I had Claude figure that distance-matching for me.

For example, a spool with color #FF0000 (pure red) would map to Qidi color code 18 ("Red", #FF362D). The Spoolman spool retains its original hex color — only the Qidi tag gets the quantized palette index.

TL;DR: You don't need to scan an existing Qidi tag first. You can write brand new tags from any Spoolman spool and the color is automatically snapped to the nearest of the 24 Qidi colors.

The relevant code is in qidi_codec.py — color_code_from_hex() handles the mapping, and COLOR_CODE_MAP defines all 24 palette entries.

@turw41th

Copy link
Copy Markdown

@goeland86 I received the tags, but I did not check the WebNFC api specs beforehand and did not notice that it cannot write or read Mifare classic tags. I also don't have an ncf reader at home. I might vibe code a native android app that acts as a front end for spoolman and uses android.nfc and test it this way. Other wise I'd have to wait for another ali express order with an nfc reader.

@goeland86

Copy link
Copy Markdown
Author

So, I think my branch has a webNFC fork that basically transmits the byte arrays to and from Spoolman, so you should be able to use your mobile directly with spoolman in the browser? I haven't really looked into it, my use-case was aimed at PN532's hooked up to the printers, so I put a spool on the printer and it automatically sets the right Spoolman ID on klipper.

@turw41th

Copy link
Copy Markdown

I can use the tags and write the tags, as my mifare tags have an NDEF layer. But the tag will always be recognized as an NDEF tag by spoolman and it will use the tigertag format which is not being recognized by my multi material system.

@goeland86

goeland86 commented Apr 29, 2026

Copy link
Copy Markdown
Author

Ah, bummer. Let me see if I can tweak the UI to set a preferred format for the tags.

Update: re-read what you said about WebNFC not working with Mifare in general. That's annoying.

@turw41th

Copy link
Copy Markdown

Are you going to try some hack or should I look how I could test it on other ways?

@goeland86

Copy link
Copy Markdown
Author

Are you going to try some hack or should I look how I could test it on other ways?

I'm not sure that there's much I could hack, the WebNFC is a capability built into the browser. I'd have to build a custom browser to get the hack working, and I'm not that good, even with an LLM assistant. 😓

@turw41th

Copy link
Copy Markdown

Alright no worries, I'll check if I can whip up an android native frontend app. That might take me a day or two though.

sherrmann added a commit to sherrmann/Spoolman-NG that referenced this pull request Jun 29, 2026
* Add NFC spool identification (TigerTag, OpenPrintTag, QIDI)

Ports upstream Donkie#880: scan NFC-equipped filament spools to identify or
auto-create them. Three open tag formats are supported — TigerTag
(NTAG213), Prusa OpenPrintTag (ISO 15693 NDEF/CBOR), and QIDI (MIFARE
Classic) — read three ways: browser Web NFC, a server-side USB reader
(SPOOLMAN_NFC_ENABLED=TRUE), or an external reader posting raw tag bytes
to /api/v1/nfc/lookup. Spool matching is by the existing external_id
field (tigertag_{id} / opt_{uuid}), so there is no database migration.

Backend
- spoolman/api/v1/nfc.py: read/write/encode/lookup/bind/create-from-tag
  endpoints, registered on the v1 router.
- Binary codecs + external-DB lookups: tigertag*, qidi*, openprinttag*,
  tigertagdb; nfc_service.py wraps nfcpy for USB/UART readers.
- env.py: SPOOLMAN_NFC_* settings. Dockerfile/entrypoint: libusb +
  optional `nfc` extra + USB device permissions.
- The nfcpy/cbor2/ndeflib deps are an optional `[project.optional-
  dependencies] nfc` extra; all uses are lazily imported so the base app
  still imports and runs without them.

Frontend
- NFC scanner / bind / write modals using the Web NFC API, client-side
  TigerTag codec, and English locale strings.

Conformance to the fork's lint/format (the PR did not target select=ALL):
- Fixed real issues: a latent NameError (Spool used unimported in nfc.py),
  dead locals, commented-out code, missing type hints/return types, and
  redundant FastAPI response_model args.
- Scoped a per-file-ignores block for the binary-protocol/hardware modules
  (magic byte values, branchy decoders, defensive broad-except, lazy
  optional-dep imports) mirroring the existing tests* relaxation.
- Dropped the PR's dev-log artifacts (PR_880_UPDATE.md, docs/SESSION_LOG,
  RELEASE_NOTES, .claude/*, client/.env.production) and regenerated
  uv.lock locally rather than taking the PR's lock delta.

Verified: ruff format + ruff check, uv lock --check, base import without
the extra and full NFC-module import with it, eslint, prettier, and the
client build. Single alembic head; no migration.

Ported from upstream Donkie#880 by @goeland86.

Co-authored-by: goeland86 <goeland86@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqPtZvXQkraNt9ttEhLsBt

* Fix NFC review findings (event-loop blocking, false bind, portability)

- nfc read/write endpoints: run the blocking nfcpy hardware calls
  (read_tag_auto / write_tag / write_mifare_classic_block) via asyncio.to_thread so a pending
  read/write no longer freezes the whole async server for up to the 10s timeout. (HIGH)
- tigertag_lookup: stop auto-binding on the "Spool.id == id_product" heuristic. id_product is a
  TigerTag catalog identifier from a different number space, so a genuine third-party tag whose
  id_product collides with a spool PK was matching the wrong spool and writing a permanent
  nfc_tag_id binding to it. Now a read-only, logged, best-effort match only. (HIGH)
- qidi_lookup: compare colour case-insensitively (func.lower) — Qidi-created filaments store an
  uppercase hex while the fuzzy lookup lowercased the value, so it never matched on case-sensitive
  backends and created duplicate spools on every rescan. (MEDIUM)
- /external/filament: return validated list[ExternalFilament] (dropping malformed entries) so the
  declared response_model / exclude_none actually applies, instead of emitting raw merged dicts
  from a JSONResponse. (MEDIUM)
- nfcScannerModal / nfcBindModal: abort the in-flight Web NFC scan on modal close/unmount so the
  NDEFReader scan and onreading handler don't keep running / setState later. (MEDIUM)
- nfc.py bind duplicate-check: use scalars().first() instead of scalar_one_or_none() so a tag
  erroneously bound to multiple spools doesn't raise MultipleResultsFound. (LOW)
- client nfc fetch hooks: check response.ok before .json() so server errors surface as real
  errors instead of JSON-parse failures / false successes. (LOW)

Note: storing nfc_tag_id as an unregistered raw-string extra field (vs the registered/JSON-encoded
convention) is left as-is — changing the storage format touches every lookup site and the
field-management UI and is a design decision for the owner.

Verified: ruff, eslint, tsc, prettier, build, and sqlite+postgres integration tests (223 pass) all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: goeland86 <goeland86@users.noreply.github.com>
@mathiasbk

Copy link
Copy Markdown

Anything new on this? This is the main missing feature for my use.

@goeland86

Copy link
Copy Markdown
Author

As far as I know there is a major architecture refactor going on, and until it's complete this will be waiting.

goeland86 and others added 6 commits August 20, 2026 11:11
# Conflicts:
#	entrypoint.sh
#	spoolman/api/v1/externaldb.py
#	spoolman/api/v1/router.py
#	spoolman/main.py
#	uv.lock
The upstream merge (e473e6d) brought in client_v2 as the default
frontend, but it never had NFC support — that only existed in the
legacy React client this fork built for TigerTag/Qidi hardware.

Ports the read/bind/write flows (server-reader and Web NFC paths),
the TigerTag NTAG213 binary codec, and the create-spool-from-tag flow
onto client_v2's component/store conventions: a TopBar scan button
(shown only when a reader or Web NFC is available), and Bind/Write
actions on the spool inspector.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was scratch content for updating PR Donkie#880's description, not meant to
live in the repo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Local Claude Code settings and session/debugging logs from this fork's
deployment history — kept on master, not carried into pr/nfc-support.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… code

Upstream's rebase brought in select = ["ALL"] for ruff, which the
NFC/TigerTag/OpenPrintTag/Qidi backend code (written under the old
lenient default ruleset) never had to pass. Brings it into compliance:

- Backend: type annotations, narrowed exception handling (hardware I/O
  boundaries keep a justified blind except with a comment; everything
  else narrows to the real exception types), named constants for magic
  values, keyword-only boolean args, and complexity reduction via
  extraction (nfc.py's TigerTag product resolution, nfc_service.py's
  per-key MIFARE auth loops, openprinttag_codec.py's ~20-branch field
  populator turned into a data-driven loop, tigertag_lookup.py's brand/
  material/diameter mapping). Also wires read/write hardware timeouts
  that were previously accepted as a parameter and silently ignored.
- Frontend (legacy client): drop an unused React import, run prettier
  on the NFC modal files (never formatted since the original PR).

Verified: full pytest suite (210 passed), a manual check of the two
non-trivial codec refactors (openprinttag_codec field population and
NDEF short/standard record parsing) against hand-built fixtures, ruff
check/format, eslint/prettier on both clients, and client_v2 svelte-check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
npm run build's strip-empty-locales prebuild script rewrote every
non-English locale file when I built client_v2 earlier (removing
untranslated placeholder keys), and that got committed along with the
real changes. Those files are Weblate-managed and CI's guard-translations
check rejects any manual edit to them for exactly this reason — the
next Weblate sync would silently overwrite it anyway. Only en/common.json
(the source of truth) carries real changes now: the new nfc.* keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@goeland86

Copy link
Copy Markdown
Author

Pushed two follow-up commits to get CI green after the rebase:

  1. fix(ci): revert non-English client_v2 locale files to upstream — running npm run build locally had triggered client_v2's strip-empty-locales prebuild script, which rewrote every non-English client_v2/locales/*/common.json (stripping untranslated placeholder keys) and I'd committed that by accident. Those files are Weblate-managed, so guard-translations correctly rejected the manual edit — a real sync would've clobbered it anyway. Reverted all of them to upstream's version; only en/common.json still carries real changes (the new nfc.* keys for the client_v2 scan/bind/write UI).

  2. fix(ci): satisfy the ALL-rules ruff config and eslint on NFC/TigerTag code — upstream's rebase brought in select = ["ALL"] for ruff, which the NFC/TigerTag/OpenPrintTag/Qidi backend never had to pass under the old default ruleset. Brought it into compliance: type annotations, narrowed exception handling (hardware I/O boundaries keep a justified blind except with a comment; everything else narrows to real exception types), named constants for magic values, keyword-only boolean args, and complexity reduction via extraction — notably openprinttag_codec.py's ~20-branch field populator became a data-driven loop, and the per-key MIFARE auth loops in nfc_service.py got pulled into helpers. Also wires up the read/write hardware timeout parameter that was previously accepted and silently ignored. On the frontend, dropped an unused React import and ran prettier on the NFC modal files (never formatted since the original PR).

Both fixes verified against the full backend test suite, ruff check/format, eslint/prettier on both clients, and client_v2's svelte-check — all 14 CI checks are now passing.

@goeland86

Copy link
Copy Markdown
Author

@akira69 following up on your comment above:

Without digging into this or thinking too much, I wonder if you have considered to cross support with spoolease too - though the NFC info would be coming via a spoolease api instead of direct from the scanner. anyway, cool! more features :)

Looked into this. A couple of things worth sharing:

  • SpoolEase doesn't expose a public API for third-party consumption — it's a self-hosted local device (ESP32 console + scale), not a cloud service, so there's no "spoolease api" to integrate against the way there is with TigerTag.
  • It does advertise OpenPrintTag support though, and this PR already implements the full OpenPrintTag spec (NDEF/CBOR over NFC-V) in openprinttag_codec.py. If SpoolEase writes spec-compliant OpenPrintTag tags, Spoolman should already be able to read them via /api/v1/nfc/lookup — no new code needed, just needs to be verified against real hardware, same as the OpenPrintTag test-plan item still unchecked above.

If you (or anyone else reading this) has a SpoolEase console and can write a tag, then scan its raw NFC-V memory dump through /api/v1/nfc/lookup (or the client_v2 NFC scan modal once this merges), that'd confirm compatibility one way or the other. Happy to help debug if it doesn't decode cleanly.

@goeland86

Copy link
Copy Markdown
Author

@Donkie — flagging an overlap now that I've spotted it, rather than after doing more work on the wrong foundation.

#1096 and this PR cover the same territory (spool↔tag identification) with incompatible shapes: #1096 is UID-only linking with content decoding explicitly out of scope for now ("Decoding what is written on a tag... is not in here"), while this PR bakes full TigerTag/Qidi/OpenPrintTag decoding, auto-create-from-tag, and bind/write endpoints straight into core.

I also noticed models.Tag already reserves two columns for exactly this — format ("informational in phase 1") and instance_uuid ("Where phase 2 records a tag's decoded instance identity... Nothing writes it yet"). That instance_uuid concept lines up with OpenPrintTag's own effective_instance_uuid, which this PR's openprinttag_codec.py already computes the same way. Reads like the schema is deliberately shaped for a decoder layer to land on top of #1096 later.

If that's the plan, I'd be glad to do that rework myself — take this PR's three codecs (TigerTag, Qidi, OpenPrintTag) and the auto-create/bind logic, and rebuild them as the phase-2 decoder layer on top of #1096's tag table and scan relay, instead of the standalone /api/v1/nfc/* endpoints this PR currently ships. That'd save you writing the decoders yourself once #1096 lands.

Wanted to check with you first rather than guess at the shape, though — is that the direction you had in mind for phase 2, and is there anything about how it should plug in (e.g. how format/instance_uuid get populated, whether decoding lives in core or stays optional/pluggable) that I should build around? Happy to hold off entirely if you'd rather do this part yourself once #1096 is in.

Donkie added a commit that referenced this pull request Aug 20, 2026
This reverts 9aea271.

The column bought one thing, that phase 2 would need no migration, and
cost something worse: it commits the schema to a shape nobody has run a
decoder against. Width, and above all the choice not to make it unique,
were read off a design document rather than off working code, and
goeland86's codec in #880 computes an effective_instance_uuid that may
not mean the same thing. Getting that wrong leaves us altering a
populated column on four databases, which is the exact operation the
"settle the shape before release" rule exists to avoid.

That rule earns its keep for target_type, target_value and filament_id,
where retrofitting a discriminator or a nullability change is painful.
Appending one nullable column later is not painful, so phase 2 can add
it when there is something to write into it.
@akira69

akira69 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor
  • SpoolEase doesn't expose a public API for third-party consumption — it's a self-hosted local device (ESP32 console + scale), not a cloud service, so there's no "spoolease api" to integrate against the way there is with TigerTag.
  • It does advertise OpenPrintTag support though, and this PR already implements the full OpenPrintTag spec (NDEF/CBOR over NFC-V) in openprinttag_codec.py. If SpoolEase writes spec-compliant OpenPrintTag tags, Spoolman should already be able to read them via /api/v1/nfc/lookup — no new code needed, just needs to be verified against real hardware, same as the OpenPrintTag test-plan item still unchecked above.

If you (or anyone else reading this) has a SpoolEase console and can write a tag, then scan its raw NFC-V memory dump through /api/v1/nfc/lookup (or the client_v2 NFC scan modal once this merges), that'd confirm compatibility one way or the other. Happy to help debug if it doesn't decode cleanly.

I moved my work over to Filaman-System it's got built in NFC support that accomplishes all my goals. basically integrating spoolman/spoolease. The developer is really quick with improvements too.

though I do like the new spoolman layout - really nice.

@Donkie

Donkie commented Aug 23, 2026

Copy link
Copy Markdown
Owner

@goeland86

Thanks for asking first, and yes, you read the schema right.

The line I hold is that Spoolman owns the codec and never the reader. So the direction you describe is the one I want: decoders on top of #1096's tag table and scan relay, not a parallel /api/v1/nfc/*. Glad to have you do it.

Things to build around:

instance_uuid is gone. I added it, then took it back out. Adding it is phase 2's job and it is yours to shape. Think hard about uniqueness: a payload copied onto a second sticker puts one identity on two rows pointing at the same spool.

Creating from a scan should be opt in. A change of mind on my part. I am fine with /tag/scan creating records, since anything on the network can already POST to /spool, but a reader should not quietly manufacture spools. Put it behind create: true, default off, keep import idempotent on identity or UID, and have the response say whether it created or matched.

Weight needs care, but scales are fine. A scale should identify the spool by tag and then call PUT /spool/{id}/measure with the gross weight, which is what that endpoint is for. What I do not want is the tag's aux consumed_weight silently overwriting our tracking, since that is a printer's gcode estimate competing with our number with no ordering between them. Decode it and show it. If you want to act on it, make it something a person confirms, applied through use or measure.

OpenPrintTag only for now. It is what ships on retail spools. Qidi buys MIFARE key derivation for nothing writable, and TigerTag has only #710 behind it. Not never, just not first.

How do you plan to verify it without NFC-V hardware? Test vectors from the spec repo, or a dump from someone with a tagged spool, would do. Hardware only proves the reader works, and the reader is not our code.

Easiest for me to review would be a first PR with just the codec and its tests. The libusb in the default image and the SPOOLMAN_NFC_ENABLED loop are the parts I do not want in core.

@goeland86

Copy link
Copy Markdown
Author

Understood, thanks for the detailed explanation.

I will rework as needed.

One caveat though, it looks like tigertag is already shipping with vendors, especially in Europe: Sunlu, Rosa3D, R3D and esun are among the larger brands already shipping the tags on their spools here. So I think there's value in more than one tag support being baked in early. Especially since the format is simpler to use and test for a lot of reasons. Last news I read is they're in talks with Polymaker, so that might roll out soon too.

I can get mifare hardware to do an end to end test with a few prusament spools in the coming weeks.

@Donkie

Donkie commented Aug 23, 2026

Copy link
Copy Markdown
Owner

What I mainly want to limit is the size of the PRs, so if support for tigertags adds a bunch of code it's something I rather take separately.

@goeland86

Copy link
Copy Markdown
Author

Understood.

pull Bot pushed a commit to maxkreja/Spoolman that referenced this pull request Aug 23, 2026
Phase 1: format-agnostic tag identity (spool_tag table, indexed UID
lookup, link/unlink API) plus the scan relay and scanner-to-browser
pairing. Phase 2: OpenPrintTag codec, import and export.

Based on an investigation of 9 open issues, 3 discussions, PR Donkie#880 and
the surrounding tag-standard ecosystem.
@goeland86

Copy link
Copy Markdown
Author

Closing this in favor of smaller, separately reviewable pieces, per the discussion
above about overlap with #1096 and the direction to split OpenPrintTag out first.

First piece is up: #1112 (OpenPrintTag codec + tests). TigerTag, Qidi, and the
optional TigerTag DB add-on will follow as their own PRs once that one's shape is
settled — noted in #1112's description.

Thanks for the detailed direction on this, @Donkie.

@goeland86 goeland86 closed this Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants