NFC spool selection¶
The end-to-end story of "tap a spool, the printer knows what it is."
Single-tool mode¶
sequenceDiagram
autonumber
actor U as Operator
participant R as NFC reader
participant D as klipper-nfc-daemon
participant SM as Spoolman
participant H as Moonraker / Bridge
participant K as Klipper / Bridge internal
participant UI as Mainsail
U->>R: physically tap spool on reader
R-->>D: tag UID + raw bytes
D->>D: debounce check (skip if same tag within window)
D->>SM: POST /api/v1/nfc/lookup<br/>{uid, protocol, data}
SM-->>D: spool_id = 42
D->>SM: GET /api/v1/spool/42
SM-->>D: PolyTerra PLA Red, 210/60, PA 0.04
D->>H: POST /server/spoolman/spool_id {id: 42}
H->>K: persist active spool
D->>H: SAVE_VARIABLE nfc_material '"PLA"'
D->>H: SAVE_VARIABLE nfc_extruder_temp 210
D->>H: SAVE_VARIABLE nfc_bed_temp 60
Note over D,H: …rest of metadata
D->>H: update Mainsail preheat preset (optional)
H-->>UI: notify_status_update (active spool changed)
UI-->>U: spool name shown in Mainsail spool panel
Operator sees nothing other than the spool name updating in the Mainsail spool panel and (optionally) a new preheat preset.
Multi-tool mode¶
The daemon doesn't know which physical tool the just-scanned spool belongs to. It asks the operator via a Mainsail prompt dialog:
sequenceDiagram
autonumber
actor U as Operator
participant R as NFC reader
participant D as klipper-nfc-daemon
participant SM as Spoolman
participant H as Moonraker / Bridge
participant UI as Mainsail
U->>R: tap spool
R-->>D: tag UID + raw bytes
D->>SM: POST /api/v1/nfc/lookup
SM-->>D: spool_id = 42
D->>SM: GET /api/v1/spool/42
SM-->>D: spool details
Note over D,H: Stash pending data in _NFC_STATE macro vars
D->>H: SET_GCODE_VARIABLE MACRO=_NFC_STATE VARIABLE=pending_spool_id VALUE=42
D->>H: SET_GCODE_VARIABLE … pending_material '"PLA"'
D->>H: SET_GCODE_VARIABLE … pending_vendor '"PolyTerra"'
D->>H: SET_GCODE_VARIABLE … pending_extruder_temp 210
Note over D,H: …rest of metadata
Note over D,H: Build prompt
D->>H: RESPOND TYPE=command MSG="action:prompt_begin Assign spool"
D->>H: RESPOND TYPE=command MSG="action:prompt_text PolyTerra PLA Red"
D->>H: RESPOND TYPE=command MSG="action:prompt_button T0|NFC_ASSIGN_TOOL T=0"
D->>H: RESPOND TYPE=command MSG="action:prompt_button T1|NFC_ASSIGN_TOOL T=1"
D->>H: RESPOND TYPE=command MSG="action:prompt_button Cancel|NFC_CANCEL"
D->>H: RESPOND TYPE=command MSG="action:prompt_show"
H-->>UI: notify_gcode_response "// action:prompt_*"
UI->>U: modal: "Assign spool — PolyTerra PLA Red" [T0] [T1] [Cancel]
U->>UI: tap T1
UI->>H: printer.gcode.script {script: "NFC_ASSIGN_TOOL T=1"}
Note over H: Real Klipper: macro fires.<br/>Bridge: intercepted natively.
H->>SM: PATCH /api/v1/spool/42 (tool=1)
H->>H: SAVE_VARIABLE nfc_t1_material '"PLA"'
H->>H: SAVE_VARIABLE nfc_t1_extruder_temp 210
Note over H: …per-tool metadata
H-->>UI: notify_gcode_response "// action:prompt_end"
H-->>UI: notify_gcode_response "// Spool #42 assigned to T1"
UI->>U: dialog closes, console shows assignment
Why _NFC_STATE exists¶
The prompt dialog and the assignment command are decoupled — the
daemon broadcasts the prompt and then returns to its poll loop. When
the operator eventually clicks a tool button, the resulting
NFC_ASSIGN_TOOL macro/intercept needs to know which spool to assign.
The daemon stashes the pending data in Klipper variables under a fake
macro named _NFC_STATE:
SET_GCODE_VARIABLE MACRO=_NFC_STATE VARIABLE=pending_spool_id VALUE=42
SET_GCODE_VARIABLE MACRO=_NFC_STATE VARIABLE=pending_material VALUE='"PLA"'
…
On real Klipper this needs a stub macro:
[gcode_macro _NFC_STATE]
variable_pending_spool_id: 0
variable_pending_material: ""
# …etc, in nfc_macros.cfg
gcode:
# never executed; storage only
On the bridge there is no macro — the bridge intercepts every
SET_GCODE_VARIABLE MACRO=_NFC_STATE … and stores the field in its
own in-memory NFCState (see
Bridge intercepts).
Cancel flow¶
Clicking Cancel sends NFC_CANCEL which clears the pending state
and emits action:prompt_end:
Same flow on real Klipper (macro) and bridge (intercept).
Debounce¶
The daemon's debounce_time (default 5 s) suppresses re-trigger from
the same tag UID. Useful because PN532 readers will happily report the
same tag once per poll while a spool sits on the reader.
What happens if Spoolman is unreachable¶
The daemon logs an error, skips the rest of the flow, and goes back to
polling. No partial state written to the printer. The operator sees
nothing in Mainsail — they should check journalctl -u nfc-spoolman -f.
What happens if the tag isn't in Spoolman¶
- TigerTag (NTAG213): no match → logged, ignored. Use Spoolman's
UI to create a filament profile with the right
external_id. - OpenPrintTag (NFC-V): if
auto_create = true, the daemon auto-creates a placeholder spool linked to the tag's UUID and writes the lookup back to Spoolman; otherwise same as TigerTag.
Per-printer reader¶
Each printer in the fleet has its own PN532 NFC reader on a USB-UART
cable, each running its own nfc-spoolman daemon, each pointed at the
same shared Spoolman instance. That way assignments are always scoped
to the printer the operator is standing in front of.
The Voron 2.4 (StealthChanger) is the only printer running in
multi_tool mode because it's the only multi-tool machine. The
Trident, V0, CR-30, Alcheman, and J1S all run in single mode.