Skip to content

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

Open
goeland86 wants to merge 20 commits intoDonkie:masterfrom
goeland86:pr/nfc-support
Open

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

Conversation

@goeland86
Copy link
Copy Markdown

@goeland86 goeland86 commented Mar 10, 2026

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

🤖 Generated with 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
Copy link
Copy Markdown

akira69 commented Mar 11, 2026

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
Copy link
Copy Markdown

turw41th commented Apr 12, 2026

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

Qidi NFC Tag Support

This update adds support for Qidi RFID tags (MIFARE Classic 1K / FM11RF08S) alongside the existing TigerTag and OpenPrintTag formats.

What's new

Qidi tags are ISO 14443A MIFARE Classic 1K tags used by Qidi 3D printers and the QIDIBOX filament management system. They store a simple 3-byte payload: material code, color code, and manufacturer ID. Spoolman now reads, writes, and auto-detects these tags through the same NFC infrastructure used for TigerTag and OpenPrintTag.

How it works

  • Auto-detection: The server-side NFC reader automatically detects whether a scanned tag is NTAG213 (TigerTag), NFC-V (OpenPrintTag), or MIFARE Classic (Qidi) and routes to the correct decoder.
  • UID-based binding: Since Qidi tags only store material + color (no unique spool ID), binding uses the tag's hardware UID. Scan once to bind, and all future scans of that physical tag resolve to the same spool.
  • Fuzzy matching: If no UID binding exists, Spoolman fuzzy-matches by material type and color hex against existing spools.
  • Auto-create: Unrecognized Qidi tags can auto-create a spool with the correct Qidi vendor, material name (e.g. "Qidi PLA Silk"), and color.
  • Writing: Spools can be written to blank MIFARE Classic 1K tags in Qidi format from the web UI.
  • Lookup endpoint: The /api/v1/nfc/lookup endpoint accepts tag_type: "qidi" or auto-detects from 16-byte block data, so Klipper integrations work out of the box.

Supported Qidi materials & colors

  • 35 materials: PLA, PLA Matte, PLA Silk, PLA-CF, ABS, ABS-GF, ASA, PA-CF, PAHT-CF, PETG, PETG-CF, PPS-CF, TPU, PVA, and more
  • 24 colors: White, Black, Red, Blue, Yellow, Orange, Green, Purple, Pink, and more — each mapped to an RGB hex value

Authentication

Qidi tags use MIFARE Classic Crypto-1 authentication with Key A. Two keys are tried in sequence:

  1. Qidi custom key: D3:F7:D3:F7:D3:F7 (factory Qidi tags)
  2. Default key: FF:FF:FF:FF:FF:FF (blank MIFARE Classic tags)

Files changed

File Description
spoolman/qidi_codec.py New — Material/color lookup tables, 3-byte encode/decode, auto-detection
spoolman/qidi_lookup.py New — UID-based spool binding, fuzzy matching, auto-create
spoolman/nfc_service.py Added read_tag_auto() with MIFARE Classic detection, dual-key auth read/write
spoolman/api/v1/nfc.py Added QidiTagDataResponse, Qidi handling in all NFC endpoints
client/src/utils/nfc.ts Added QidiTagData interface, updated request/response types
client/src/components/nfcScannerModal.tsx Shows Qidi tag summary, creates spools from Qidi data
client/src/components/nfcBindModal.tsx Supports Qidi UID-based tag binding
client/src/components/nfcWriteModal.tsx Tag format selector (TigerTag / Qidi)
client/public/locales/en/common.json New translation keys for Qidi UI
README.md Updated NFC feature list

API examples

Look up a Qidi tag (auto-detect):

curl -X POST /api/v1/nfc/lookup \
  -d '{"raw_data_b64": "ARIBAAAAAAAAAAAAAAAAAA==", "nfc_tag_uid": "A1B2C3D4"}'

Auto-create spool from Qidi tag:

curl -X POST /api/v1/nfc/lookup \
  -d '{"raw_data_b64": "ARIBAAAAAAAAAAAAAAAAAA==", "tag_type": "qidi", "nfc_tag_uid": "DEADBEEF", "auto_create": true}'

Write spool as Qidi tag:

curl -X POST /api/v1/nfc/write \
  -d '{"spool_id": 1, "tag_format": "qidi"}'

Reference

  • Qidi RFID Wiki
  • Tag chip: FM11RF08S (MIFARE Classic 1K compatible)
  • Data location: Sector 1, Block 0 (absolute block 4)
  • Protocol: ISO/IEC 14443-A, 13.56 MHz

@turw41th
Copy link
Copy Markdown

turw41th commented Apr 15, 2026

@goeland86 Very cool, I didn't expect you to be this quick! Let me order some matching NFC tags real quick and I'll test it out with your fork as soon as I have them.
How do you handle the supported colors? 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?

@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
Copy link
Copy Markdown
Author

goeland86 commented Apr 29, 2026

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.

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.

3 participants