Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
840d719
Claude-based initial implementation of tigertag in spoolman.
goeland86 Feb 13, 2026
3674057
Added claude's implementation plan.
goeland86 Feb 13, 2026
0b48be7
Fix TypeScript build errors and update implementation log
goeland86 Feb 13, 2026
022fa95
Fix TigerTag API integration and Pydantic compatibility
goeland86 Mar 9, 2026
7d0eb5f
Add client .env.production with VITE_APIURL for builds
goeland86 Mar 9, 2026
6276715
Add auto-create spool from unrecognized NFC TigerTag and fix binary c…
goeland86 Mar 9, 2026
fe8f83b
Add OpenPrintTag NFC-V support alongside TigerTag
goeland86 Mar 10, 2026
a6a97c6
Add TigerTag NFC spool identification
goeland86 Mar 10, 2026
766a58e
Add OpenPrintTag NFC-V support alongside TigerTag
goeland86 Mar 10, 2026
3978c0d
Merge pull request #1 from goeland86/pr/nfc-support
goeland86 Mar 10, 2026
0dee4fc
Improve TigerTag spool matching and product resolution
goeland86 Mar 10, 2026
e4efe1b
Improve TigerTag spool matching and product resolution
goeland86 Mar 10, 2026
b16f5e1
Add NFC tag binding to link existing tags to existing spools
goeland86 Mar 10, 2026
4e313f0
Add NFC tag binding to link existing tags to existing spools
goeland86 Mar 10, 2026
0587514
Add TigerTag+ (Pro V1.0) support alongside TigerTag Maker V1.0
goeland86 Mar 11, 2026
7c8e547
Merge branch 'pr/tigertag-plus'
goeland86 Mar 11, 2026
ab0a71b
Fix TigerTag magic numbers to match real hardware tags
goeland86 Mar 11, 2026
9ccab08
Merge branch 'pr/nfc-support'
goeland86 Mar 11, 2026
100dffd
Add NFC USB hot-plug support with auto-reconnect
goeland86 Apr 10, 2026
f625a33
Add Qidi NFC tag support (MIFARE Classic 1K)
goeland86 Apr 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"permissions": {
"allow": [
"WebSearch",
"WebFetch(domain:doc.tigertag.io)",
"WebFetch(domain:github.com)"
]
}
}
109 changes: 109 additions & 0 deletions .claude/tigertag-implementation-log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# TigerTag Integration - Implementation Log

**Date:** 2026-02-13
**Branch:** master

---

## Status: All 4 Phases Implemented and Verified

- All code written and committed.
- Frontend build passes (TypeScript clean).
- All 223 integration tests pass (SQLite).
- Manual and NFC hardware testing still pending.

---

## Bugs Fixed During Verification

| File | Issue | Fix |
|------|-------|-----|
| `client/src/components/header/index.tsx` | Used non-existent `RefineThemedLayoutV2HeaderProps` export | Reverted to `RefineThemedLayoutHeaderProps` |
| `client/src/pages/spools/show.tsx` | Used `queryResult` instead of `query` from `useShow()` | Changed to `const { query } = useShow()` |
| `client/src/pages/spools/show.tsx` | Added unnecessary `IResourceComponentsProps` and `React` import | Removed; matches upstream component signature |

---

## Files Changed

### New Files Created (8)

| File | Phase | Description |
|------|-------|-------------|
| `spoolman/tigertagdb.py` | 1 | TigerTag API client, Pydantic models, sync scheduler, cache |
| `spoolman/tigertag_codec.py` | 3 | NTAG213 binary encoder/decoder (144 bytes, struct-based) |
| `spoolman/nfc_service.py` | 3 | NFC reader singleton wrapping nfcpy |
| `spoolman/tigertag_lookup.py` | 3 | Spool-to-TigerTag matching and reverse mapping |
| `spoolman/api/v1/nfc.py` | 3 | NFC API endpoints (status/read/write) |
| `client/src/utils/nfc.ts` | 4 | TS types, React Query hooks, Web NFC declarations |
| `client/src/components/nfcScannerModal.tsx` | 4 | NFC tag scanner modal (Browser + Server modes) |
| `client/src/components/nfcWriteModal.tsx` | 4 | NFC tag writer modal with data preview |

### Modified Files (11)

| File | Phase | What Changed |
|------|-------|-------------|
| `spoolman/env.py` | 1,3 | Added 6 env config functions (tigertag + nfc) |
| `spoolman/externaldb.py` | 1 | Added `source` field to `ExternalFilament` model |
| `spoolman/api/v1/externaldb.py` | 1 | Changed to JSONResponse merging SpoolmanDB + TigerTag |
| `spoolman/main.py` | 1,3 | Added tigertagdb sync + NFC service init on startup |
| `spoolman/api/v1/router.py` | 3 | Registered nfc router |
| `pyproject.toml` | 3 | Added optional `nfc = ["nfcpy>=1.0"]` dependency |
| `client/src/utils/queryExternalDB.ts` | 2 | Added `source` field to ExternalFilament interface |
| `client/src/components/filamentImportModal.tsx` | 2 | Added colored source Tag badges, searchText filtering |
| `client/public/locales/en/common.json` | 2,4 | Added `external.*` and `nfc.*` translation keys |
| `client/src/components/header/index.tsx` | 4 | Added NfcScannerModal alongside QRCodeScannerModal |
| `client/src/pages/spools/show.tsx` | 4 | Added "Encode to NFC" button + NfcWriteModal |

---

## Environment Variables Added

| Variable | Default | Description |
|----------|---------|-------------|
| `SPOOLMAN_TIGERTAG_ENABLED` | `FALSE` | Enable TigerTag external DB |
| `SPOOLMAN_TIGERTAG_API_URL` | `https://api.tigertag.io/` | TigerTag API base URL |
| `SPOOLMAN_TIGERTAG_SYNC_INTERVAL` | `3600` | Sync interval in seconds |
| `SPOOLMAN_NFC_ENABLED` | `FALSE` | Enable server-side NFC reader |
| `SPOOLMAN_NFC_READER_TYPE` | `nfcpy` | NFC reader library |
| `SPOOLMAN_NFC_DEVICE` | `None` (auto) | NFC device path |

---

## API Endpoints Added

| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/nfc/status` | Reader status + enabled flag |
| `POST` | `/api/v1/nfc/read` | Read tag, decode TigerTag, match spool |
| `POST` | `/api/v1/nfc/write` | Encode spool as TigerTag, write to tag |

---

## Remaining TODO

1. **Manual testing with `SPOOLMAN_TIGERTAG_ENABLED=TRUE`**:
- Check logs for "Syncing TigerTag DB"
- `GET /api/v1/external/filament` should return entries with both sources
- Filament import modal should show colored source badges
2. **NFC testing** (requires hardware):
- `SPOOLMAN_NFC_ENABLED=TRUE` with PN532/ACR122U reader
- `GET /api/v1/nfc/status` returns `connected`
- Write/read cycle via spool detail page
- Browser Web NFC on Chrome Android
3. **Edge cases to verify**:
- TigerTag disabled: no tigertag entries in `/external/filament`, no errors
- NFC disabled: NFC button hidden in UI, `/nfc/status` returns `disabled`
- TigerTag API unreachable: graceful fallback, SpoolmanDB still works
- nfcpy not installed: startup logs warning, NFC endpoints return error

---

## Architecture Notes

- TigerTag filaments get `id` prefixed as `"tigertag_{id_product}"` to avoid collisions with SpoolmanDB IDs
- TigerTag codec uses Python `struct` module (big-endian) - no external dependencies
- NFC service is a singleton initialized once at startup, thread-safe with lock
- Frontend NFC scanner FloatButton only renders if server NFC or Web NFC is available
- Web NFC (browser) mode writes NDEF URI records (`web+spoolman:s-{id}`)
- Server mode writes raw TigerTag Maker format (144 bytes to NTAG213 pages 4-39)
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ instance/

# Sphinx documentation
docs/_build/
docs/
!docs/RELEASE_NOTES.md
!docs/SESSION_LOG.md

# PyBuilder
.pybuilder/
Expand Down
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,25 @@ WORKDIR /home/app/spoolman
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project
uv sync --locked --no-install-project --extra nfc

# Copy and install app
COPY --chown=app:app migrations /home/app/spoolman/migrations
COPY --chown=app:app spoolman /home/app/spoolman/spoolman
COPY --chown=app:app alembic.ini README.md uv.lock pyproject.toml /home/app/spoolman/
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked
uv sync --locked --extra nfc

FROM python:3.14-slim-bookworm AS python-runner

LABEL org.opencontainers.image.source=https://github.com/Donkie/Spoolman
LABEL org.opencontainers.image.description="Keep track of your inventory of 3D-printer filament spools."
LABEL org.opencontainers.image.licenses=MIT

# Install gosu for privilege dropping
# Install gosu for privilege dropping and libusb for NFC reader support
RUN apt-get update && apt-get install -y \
gosu \
libusb-1.0-0 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

Expand Down
69 changes: 69 additions & 0 deletions PR_880_UPDATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
## 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):**
```bash
curl -X POST /api/v1/nfc/lookup \
-d '{"raw_data_b64": "ARIBAAAAAAAAAAAAAAAAAA==", "nfc_tag_uid": "A1B2C3D4"}'
```

**Auto-create spool from Qidi tag:**
```bash
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:**
```bash
curl -X POST /api/v1/nfc/write \
-d '{"spool_id": 1, "tag_format": "qidi"}'
```

### Reference

- [Qidi RFID Wiki](https://wiki.qidi3d.com/en/QIDIBOX/RFID)
- Tag chip: FM11RF08S (MIFARE Classic 1K compatible)
- Data location: Sector 1, Block 0 (absolute block 4)
- Protocol: ISO/IEC 14443-A, 13.56 MHz
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ Spoolman is a self-hosted web service designed to help you efficiently manage yo
* Add custom fields to tailor information to your specific needs.
* Print labels with QR codes for easy spool identification and tracking.
* Contribute to its translation into 18 languages via [Weblate](https://hosted.weblate.org/projects/spoolman/).
* **NFC Spool Identification**: Scan NFC tags to instantly identify and select spools. Supports three tag standards:
* [TigerTag](https://tigertag.io/) (ISO 14443A / NTAG213) — binary format with external product database lookup.
* [OpenPrintTag](https://openprinttag.org/) (ISO 15693 / NFC-V) — Prusa's NDEF/CBOR standard with per-spool UUIDs.
* [Qidi](https://wiki.qidi3d.com/en/QIDIBOX/RFID) (ISO 14443A / MIFARE Classic 1K) — Qidi filament tags with material and color identification.
* Browser-based NFC scanning via the Web NFC API, or server-side with a USB reader.
* Automatic spool creation from tag data when scanning unrecognized tags.
* External integration endpoint (`POST /api/v1/nfc/lookup`) for Klipper NFC daemons and other clients.
* **Database Support**: SQLite, PostgreSQL, MySQL, and CockroachDB.
* **Multi-Printer Management**: Handles spool updates from several printers simultaneously.
* **Advanced Monitoring**: Integrate with [Prometheus](https://prometheus.io/) for detailed historical analysis of filament usage, helping you track and optimize your printing processes. See the [Wiki](https://github.com/Donkie/Spoolman/wiki/Filament-Usage-History) for instructions on how to set it up.
Expand Down
1 change: 1 addition & 0 deletions client/.env.production
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_APIURL=/api/v1
63 changes: 63 additions & 0 deletions client/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -395,5 +395,68 @@
"new_location": "New Location",
"no_location": "No Location",
"no_locations_help": "This page lets you organize your spools in locations, add some spools to get started!"
},
"external": {
"source_spoolmandb": "SpoolmanDB",
"source_tigertag": "TigerTag"
},
"nfc": {
"scan_title": "NFC Tag Scanner",
"scan_description": "Scan an NFC tag to view details about the spool.",
"encode_button": "Encode to NFC",
"encode_title": "Encode Spool to NFC Tag",
"write_success": "Tag written successfully!",
"write_error": "Failed to write tag.",
"no_match": "No matching spool found for this tag.",
"create_from_tag": "Create Spool from Tag",
"create_from_tag_description": "No matching spool found. You can create a new spool from the tag data below.",
"creating_spool": "Creating spool...",
"create_success": "Spool created successfully!",
"create_error": "Failed to create spool from tag data.",
"tag_color": "Color",
"tag_diameter": "Diameter",
"tag_weight": "Weight",
"tag_nozzle_temp": "Nozzle Temp",
"tag_bed_temp": "Bed Temp",
"mode_browser": "Browser",
"mode_server": "Server",
"unsupported": "NFC is not supported in this browser.",
"place_tag": "Place tag on reader...",
"reading": "Reading tag...",
"writing": "Writing tag...",
"user_message": "User Message",
"user_message_help": "Optional message (max 28 characters)",
"preview_title": "Data Preview",
"status": {
"connected": "Connected",
"disconnected": "Disconnected",
"disabled": "Disabled"
},
"browser_ndef_warning": "Warning: Browser NFC writes use NDEF format. Tags written this way will NOT be recognized by the TigerTag app, which expects raw binary on pages 4-39. Use Server mode for TigerTag-compatible writes, or Download Raw Binary for use with an external NFC tool.",
"download_raw_binary": "Download Raw Binary",
"download_success": "Binary file downloaded successfully.",
"browser_write_success": "TigerTag data written to NFC tag via NDEF.",
"error": {
"no_reader": "No NFC reader detected.",
"read_failed": "Failed to read tag.",
"write_failed": "Failed to write tag.",
"not_supported": "Web NFC is not supported in this browser. Use the Server mode with a connected reader.",
"permission_denied": "NFC permission denied.",
"encode_failed": "Failed to encode TigerTag data."
},
"bind_title": "Link NFC Tag to Spool",
"bind_button": "Link NFC Tag",
"bind_description": "Scan an existing NFC tag to link it to this spool. Future scans of this tag will automatically select this spool.",
"bind_confirm_description": "Tag scanned. Review the tag data below and confirm to link it to this spool.",
"bind_binding": "Linking...",
"bind_success": "Tag linked to spool successfully!",
"bind_error": "Failed to link tag to spool.",
"bind_scan_again": "Scan Again",
"bind_no_tigertag": "No TigerTag data found on this tag. Only TigerTag format is supported for linking.",
"tag_format": "Tag Format",
"tag_format_label": "Tag Format",
"tag_material": "Material",
"tag_material_type": "Material Type",
"qidi_write_info": "Qidi tags store only material and color. The tag will be written with the closest matching Qidi material and color codes."
}
}
35 changes: 25 additions & 10 deletions client/src/components/filamentImportModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useTranslate } from "@refinedev/core";
import { Form, Modal, Select } from "antd";
import { Form, Modal, Select, Tag } from "antd";
import React from "react";
import { Trans } from "react-i18next";
import { formatFilamentLabel } from "../pages/spools/functions";
import { searchMatches } from "../utils/filtering";
Expand All @@ -16,20 +17,32 @@ export function FilamentImportModal(props: {
const externalFilaments = useGetExternalDBFilaments();
const filamentOptions =
externalFilaments.data?.map((item) => {
const textLabel = formatFilamentLabel(
item.name,
item.diameter,
item.manufacturer,
item.material,
item.weight,
item.spool_type
);
const sourceTag =
item.source === "tigertag" ? (
<Tag color="orange">{t("external.source_tigertag")}</Tag>
) : (
<Tag color="blue">{t("external.source_spoolmandb")}</Tag>
);
return {
label: formatFilamentLabel(
item.name,
item.diameter,
item.manufacturer,
item.material,
item.weight,
item.spool_type,
label: (
<span>
{sourceTag} {textLabel}
</span>
),
searchText: textLabel,
value: item.id,
item: item,
};
}) ?? [];
filamentOptions.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
filamentOptions.sort((a, b) => a.searchText.localeCompare(b.searchText, undefined, { sensitivity: "base" }));

return (
<Modal
Expand Down Expand Up @@ -63,7 +76,9 @@ export function FilamentImportModal(props: {
<Select
options={filamentOptions}
showSearch
filterOption={(input, option) => typeof option?.label === "string" && searchMatches(input, option?.label)}
filterOption={(input, option) =>
typeof option?.searchText === "string" && searchMatches(input, option.searchText)
}
/>
</Form.Item>
</Form>
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, { useContext } from "react";
import { ColorModeContext } from "../../contexts/color-mode";

import { languages } from "../../i18n";
import NfcScannerModal from "../nfcScannerModal";
import QRCodeScannerModal from "../qrCodeScanner";

const { useToken } = theme;
Expand Down Expand Up @@ -62,6 +63,7 @@ export const Header = ({ sticky }: RefineThemedLayoutHeaderProps) => {
defaultChecked={mode === "dark"}
/>
<QRCodeScannerModal />
<NfcScannerModal />
</Space>
</AntdLayout.Header>
);
Expand Down
Loading