feat: Add NFC spool identification: TigerTag and OpenPrintTag support - #880
feat: Add NFC spool identification: TigerTag and OpenPrintTag support#880goeland86 wants to merge 26 commits into
Conversation
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>
Pr/nfc support
- 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>
|
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. |
|
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 :) |
|
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. |
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>
|
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 |
|
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. |
|
@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>
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. |
|
@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. |
|
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. |
|
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. |
|
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. |
|
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. 😓 |
|
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. |
* 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>
|
Anything new on this? This is the main missing feature for my use. |
|
As far as I know there is a major architecture refactor going on, and until it's complete this will be waiting. |
# 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>
|
Pushed two follow-up commits to get CI green after the rebase:
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. |
|
@akira69 following up on your comment above:
Looked into this. A couple of things worth sharing:
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 |
|
@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 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 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 |
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.
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. |
|
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 Things to build around:
Creating from a scan should be opt in. A change of mind on my part. I am fine with Weight needs care, but scales are fine. A scale should identify the spool by tag and then call 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 |
|
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. |
|
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. |
|
Understood. |
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.
|
Closing this in favor of smaller, separately reviewable pieces, per the discussion First piece is up: #1112 (OpenPrintTag codec + tests). TigerTag, Qidi, and the Thanks for the detailed direction on this, @Donkie. |
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/readand/api/v1/nfc/writeendpoints 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/lookupendpoint accepts raw tag memory from any client and handles all decoding and spool matching server-side. An example implementationis 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-createsspool/filament/vendor records from tag data
POST /api/v1/nfc/read/write/encode— server-side NFC reader endpointsPOST /api/v1/nfc/create-from-tag— create spool from decoded TigerTag dataexternal_id(tigertag_{id}oropt_{instance_uuid}) — no database migrations neededFrontend
Infrastructure
libusbanduv --extra nfcfor optional NFC dependenciescbor2,ndeflib,nfcpy(in[nfc]extra)SPOOLMAN_NFC_ENABLED,SPOOLMAN_TIGERTAG_ENABLEDDesign decisions
external_idfield onFilamentfor tag-to-spool mapping0xE1first byte = OpenPrintTag (NFC-V capability container),0x5C15E2E4magic = TigerTagTest plan
external_idinstance_uuidderived from tag UIDenabled: falsewhen NFC is not configured--- UPDATE ---
Rebased onto current
master, which in the meantime picked up theclient_v2Svelte 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
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.lockregenerated against the mergedpyproject.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):
Nfcicon), shown only when a server reader or Web NFC is detectedSpool/Filamentshapes client_v2 uses (colors[]instead of a singlecolor_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:
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/lookupendpoint — 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