-
Notifications
You must be signed in to change notification settings - Fork 236
nfc_driver_matrix
Derived from the code on private_rfid2_poll, not from datasheets. Everything below
is what the drivers in extras/mmu/unit/nfc/ actually implement; several of these
chips speak more transports and protocols in silicon than we drive
(reader_factory.SUPPORTED_INTERFACES says as much in its comment).
Key files:
-
extras/mmu/unit/nfc/reader_factory.py— transport selection, wiring validation -
extras/mmu/unit/nfc/mmu_nfc_reader.py— per-instance facade, probe contract + shim -
extras/mmu/unit/mmu_nfc_manager.py— poll orchestration, homing poll cadence -
extras/mmu/unit/nfc/mmu_nfc_endstop.py— reader-as-endstop -
extras/mmu/mmu_filament_movement.py— the homing moves that use it
| Chip | Transports implemented | Default | Required config | Extra MCU GPIO per reader | Multiple readers | Maturity |
|---|---|---|---|---|---|---|
| PN532 |
i2c, uart, spi
|
i2c |
i2c_address (0x24) |
0 on a hardware bus | One per bus. Address is fixed at 0x24, so a 2nd PN532 needs its own bus | Default chip (DEFAULT_READER_TYPE); homing path still wants a bench check — see §C |
| PN532 (uart / HSU) | — | — |
serial: (host device path), baud
|
0 (not MCU-attached) | One reader per tty, enforced (_validate_serial_port) |
Supported |
| PN532 (spi) | — | — | cs_pin |
1 (cs_pin) on a shared SPI bus |
Fine — one CS per reader |
UNTESTED — warns at startup in create_reader()
|
| PN7160 | i2c |
i2c |
i2c_address (0x28–0x2B) |
0 base; +1 for irq_pin (needed for probe homing); optional +1 ven_pin
|
Up to 4 per bus (four addresses); _software_i2c_buses explicitly allows several on one bit-banged bus |
Working; probe path is IRQ-gated |
| PN5180 | spi |
spi |
cs_pin, busy_pin, reset_pin (both required, no defaults) |
3 | Fine on shared SPI (one CS each) | Working reads; no presence probe |
| RC522 | spi |
spi |
cs_pin |
1 | Fine on shared SPI | Working; "Confirmed viable" for homing per the endstop header |
Software (bit-banged) I2C, if you need more of the same chip than the addressing
allows: 2 GPIO per bus plus its own pull-ups. reader_factory validates the pins
and catches bus/address collisions, because Klipper does not — two readers handed the
same pin pair silently share one bus.
Per-gate GPIO cost, cheapest first: RC522 (1) → PN532/SPI (1, untested) → PN7160 (0–1 shared bus, +1 for IRQ) → PN5180 (3) → PN532/I2C (2 per gate, software bus). PN532/UART can't scale per-gate at all without one USB adapter per gate.
| Capability | PN532 (i2c/uart/spi) | PN7160 | PN5180 | RC522 |
|---|---|---|---|---|
| Presence probe (non-blocking, for homing) | Yes, all three transports | Only with irq_pin |
No (shim) | Yes |
UID read (read_tag) |
Yes | Yes | Yes | Yes |
Target info (read_target: SAK/ATQA/UID len) |
Yes | Yes | Yes | Yes |
| ISO14443A (MIFARE / NTAG) | Yes | Yes | Yes | Yes |
| ISO15693 / NFC Type-5 | No | Yes | Yes | No |
| NTAG/Type-2 deep read | Yes, NDEF-aware (ntag_read_ndef_user_memory — stops at the TLV end) |
Yes, NDEF-aware | Fixed page span only (no NDEF-aware helper, so _capture_ntag falls back to tag_max_pages pages) |
Yes, NDEF-aware |
| MIFARE Classic authenticated read | Yes | Yes | Yes | Yes |
Vendor formats reachable (via tag_parser) |
Bambu, Creality, QIDI, Elegoo, Anycubic, NDEF-based (OpenTag3D, OpenSpool, TigerTag, …) | same, plus anything on a Type-5 carrier | same, plus Type-5 | same as PN532 |
Notes:
- The PN532 UART/SPI drivers look sparse if you grep them —
read_target,ntag_*andmifare_*all live in_PN532Base(pn532_driver.py:213), abovePN532Driver/PN532SPIDriver. All three transports share one capability surface. Verified by position, not assumed. - Deep read is only performed when Spoolman auto-create needs it, and never during
a homing move —
read_gate_after_home()does it aftermovequeue_wait(), with the machine stationary. Deep-read cost is therefore irrelevant to homing accuracy. - ISO15693 only matters if you actually hold Type-5 tags.
tag_parseris carrier-agnostic: the NDEF-based formats parse identically whether the bytes came off an NTAG or off a Type-5 tag (_type5_parser_memorystrips the CC first). - PN5180 with
pn5180_tag_format: autowalks both protocols on every read — pin it tontagoriso15693if you know which you have; it halves the miss cost.
Cadence, from mmu_nfc_manager.py:55: NFC_HOMING_POLL_INTERVAL = 0.020 for drivers
implementing the probe contract, NFC_HOMING_POLL_INTERVAL_SHIM = 0.050 for the
blocking shim, whose per-tick read is bounded at PROBE_SHIM_TIMEOUT = 0.050.
| Chip / mode | Detector | Pins needed for full-rate probe | Cost of a "no tag" tick | Worst-case detection latency | ≈ overshoot @100 mm/s |
|---|---|---|---|---|---|
| PN532 (i2c/uart/spi) | Continuous — InListPassiveTarget stays in flight and retries activation forever, so the chip itself is the detector |
none | one 1-byte status read | ~1 tick ≈ 20 ms | ~2 mm |
PN7160 + irq_pin |
Continuous — NCI RF discovery running; IRQ line signals a pending frame |
irq_pin (real wired GPIO) |
zero bus I/O — just reads the cached irq_state
|
~20 ms + ≤2 ms button sampling | ~2 mm |
| RC522 | Discrete — one WUPA exchange per cycle, started on one tick and collected on the next | none (it polls _ComIrqReg over SPI — no physical IRQ pin) |
a few register reads | ~2 ticks ≈ 40 ms | ~4 mm |
PN7160, no irq_pin |
Shim — probe_supported() declines |
— | a bounded blocking read_target()
|
50 ms interval + ≤50 ms read ≈ 100 ms | ~10 mm |
| PN5180 | Shim — probe deliberately not implemented (pn5180_driver.py, "deliberately NOT implemented") |
— | bounded blocking read_target(); auto format walks 2 protocols |
≈ 100–150 ms | ~10–15 mm |
Two different things get called "IRQ" in the comments and they are not the same: RC522 reads an interrupt register over the SPI bus it already has, costing no pins; PN7160 needs a real wired interrupt line, and without it there is no way to ask "is a frame waiting?" without blocking, so it falls back to the shim. RC522 is the only chip that gets full-rate probing at one pin per gate.
The endstop module still carries a PROTOTYPE header: "Confirmed viable on RC522 (SPI); PN532/PN7160 (I2C) need a bench check of per-poll blocking time against the drip budget (~50 ms safe)." So RC522 is the only one of these measured in practice — the PN532 and PN7160 numbers above are what the code should cost per tick, not what anyone has scoped.
Latency figures are code-derived upper bounds on detection only. They exclude the deceleration ramp (speed/accel dependent) and, more importantly, RF coupling range — see the caveat at the end.
The contract is probe_start() / probe_poll() / probe_stop(), driven by
MmuNfcManager._homing_poll on a reactor timer. probe_poll() returns True (tag
present), False (this scan finished with nothing there — the manager restarts it) or
None (still in flight — ask again). It reports presence only, never a UID: the
endstop needs a boolean, and the UID comes from the stationary post-move read.
-
PN532 —
InListPassiveTargetas an explicit 3-stage state machine (send→ collect ACK → collect response), one bus transaction per tick. The in-flight scan is the detector, which is why detection is one tick rather than one scan. The 2 s watchdog exists to recover a wedged chip, not to bound a healthy scan. -
PN7160 —
start_discovery()once, then each tick collects at most one pending NCI frame ifirq_statesays one is waiting. Presence =RF_DISCOVER_NTForRF_INTF_ACTIVATED_NTF; it deliberately does not select/activate, becauseselect_discovered_endpoint()carries a 1 s NCI timeout. One caveat: the tick that does read a frame ends in_wait_for_irq_release(), bounded at 50 ms — right at the ~50 ms drip budget the endstop header flags for a bench check. It only happens on the detection tick, when the move is stopping anyway. -
RC522 —
_transceive()split at its tag-wait sleep:probe_startloads WUPA with 7-bit framing and hits StartSend;probe_pollreads_ComIrqRegand decides. A single WUPA, no anticollision, no SELECT. -
PN5180 / PN7160-without-IRQ —
MmuNfcReader.probe_poll()'s shim: one boundedread_target()per tick. Still blocking, so the manager polls it at the slower cadence rather than making things worse by ticking a blocking read faster.
What is not a problem:
-
Overshoot does not corrupt the positional datum.
_home_gate_with_nfc()already treats the NFC stop as imprecise: after an NFC-first trigger it continues to the gate switch, and after a jog-scan hit it re-homes backward to the gate switch ("Virtual-NFC stop overshoots more than the MCU gate switch"). The reference always comes from the real MCU endstop. So the poll-interval overshoot costs filament travel and time, not accuracy of the gate datum. - Reactor stalls during the move. The probe split is exactly what removed them. The old path issued a blocking read whose most expensive answer was "nothing there" — ~50 pause/resume cycles per poll, and it abandoned a command still running on the chip, whose late reply the next command's ACK wait then read as garbage.
Real limitations:
-
Presence only — no identity during the move. You cannot verify which tag
stopped the move. The UID arrives afterwards from
read_gate_after_home(), and that read can legitimately come back empty: the tag keeps travelling through the deceleration ramp and may no longer be coupled to the antenna. The code logs it and moves on — it deliberately does not jog to chase it. So "homed to tag" and "read the tag" are separate outcomes, and_home_gate_with_nfcreturns them as separate flags (homed, tag_read). -
It is a host software endstop. Trigger time is
estimated_print_time(eventtime)from a reactor timer, so host scheduling jitter adds to the poll interval.mmu_filament_movementalso has to forcegear_homing_speedfor the NFC-only chase move, because a virtual endstop otherwise defaults to the much slowervirtual_sensor_homing_speed. -
Compounding requires a real MCU gate switch.
_build_nfc_compound_endstop()refuses to compound the reader with a virtual gate endstop — that would host-poll two virtuals in one drip move and double the stall risk. On such a machine you fall back to plain homing and the tag is not read during the move. -
The drain after a hit can block.
probe_stop()is real work: PN532'sInReleaseis bounded at 200 ms (it ignores the caller's timeout), PN7160 honours a 25 msRF_DEACTIVATEguard, RC522 drops and restores the RF field (~4 ms)._homing_polldeliberately does not do this at the moment of detection —_drain_probe()runs at the next stationary point. Correct, but it means the chip is briefly in an undefined-ish state while the move decelerates. -
RC522's handoff is subtle. Its probe wakes tags with WUPA, leaving them in
READY, and the following
read_target()starts with REQA, which READY tags ignore.probe_stop()drops the field to reset PICCs back to IDLE. If that field-drop fails, the post-move read fails on the very tag the probe just found. -
One homing move at a time. A single timer and a single
_homing_endstop. Fine as Happy Hare homes one gate at a time — but note a shared reader listed for several gates yields distinct endstop objects over one reader object, so those homes are strictly serialized. -
A disabled reader refuses to arm (
start_homing_polllogs and returns) rather than letting the move run its full length and fail with an opaque "no trigger". - RF range dominates repeatability. Detection happens over a volume, not a plane; where in that volume the tag first answers depends on tag size, antenna geometry, orientation and what's behind the tag (metal, wet filament, spool core). None of that is in the code, and it very plausibly exceeds the 2–4 mm of poll-interval overshoot on the good chips. Assume the poll interval stops being the limiting factor once you're on a probe-capable driver — which is another way of saying the difference between PN532 (2 mm) and RC522 (4 mm) is probably noise, while the difference to the shim (10–15 mm) is not.
The user-visible answer: not currently possible, and the reason is three specific things, not general immaturity.
-
The endstop cannot express trigger-on-absence.
MmuNfcEndstop.home_start()pinstriggered=Trueunconditionally — "A tag detection is ALWAYS the trigger, independent of move direction" — and pre-resets state withnote_filament_present(print_time, False). Both bake in trigger-on-present. -
The poll loop has no absence path.
_homing_polltriggers on truthyfound;found is Falsemeans "that scan completed empty — start the next one". Absence is currently the loop's idle state, so it cannot also be its trigger. Making it one needs a distinct return value (e.g.'absent') plus hysteresis — N consecutive confirmed absences — because a single missed exchange is not the same as a tag having left the field. - Most drivers don't produce a trustworthy absence signal at all. Per chip:
| Chip | Absence signal available? |
|---|---|
| PN532 |
No. The in-flight InListPassiveTarget retries activation forever (MxRtyPassiveActivation = 0xFF), so a healthy no-tag scan returns None indefinitely. The only False is an error or the 2 s watchdog — which the code treats as a wedge. Getting absence means running activation with a finite retry count, or deliberately using the abort path as a bounded scan. |
| RC522 |
Best of the probe drivers. An unanswered WUPA is positive evidence of absence, ~every 20 ms, already distinguished in probe_poll (TimerIRq set, RxIRq/IdleIRq clear). Caveat: within a run WUPA keeps working from READY, so a woken tag still answers — good, that's what makes "it stopped answering" meaningful. |
| PN7160 |
Tractable but not implemented. is_tag_present_ntf() covers only RF_DISCOVER_NTF (oid 0x03) and RF_INTF_ACTIVATED_NTF (oid 0x05). NCI does signal deactivation — the driver already knows gid RF/oid 0x06 as RF_DEACTIVATE and sends NCI_RF_DEACTIVATE_IDLE_CMD — so a sibling is_tag_removed_ntf() predicate is the missing piece, plus deciding whether to run a presence check while activated. |
| PN5180 |
Ironically already correct, just slow. The shim's False means "a bounded read_target() found nothing" — a genuine absence assertion, unlike the probe drivers' False. At ~100–150 ms per confirmation it's too coarse for accurate homing, but it's the semantically right signal. |
So the cheapest real path to homing-off-a-tag is RC522 (or PN7160 once a removal predicate exists), with the endstop and poll loop taught a three-state present/absent/unknown vocabulary.
Best shared reader (one reader, user presents tags by hand — the manager polls it
at NFC_CHECK_INTERVAL = 1.0 s with a 0.1 s read, and homing accuracy is irrelevant):
-
PN532 over UART/HSU. Zero MCU pins, plugs into the Pi, exclusive tty enforced,
and a missing adapter doesn't stop klippy — just reports not-alive until
MMU_RFID_INIT. Doesn't scale per-gate, which doesn't matter here. - PN5180 if you need ISO15693/Type-5 and don't need homing. Its 3 pins are paid once, and the shim's latency is a non-issue for a hand-presented reader.
- PN7160 if you want Type-5 and the option of reusing the same chip per-gate.
Best per-gate reader (reader per lane, used as a homing endstop):
- RC522 — 1 pin per gate on a shared SPI bus, full-rate probing with no extra wiring, ~4 mm detection window, and the only driver the endstop header calls "confirmed viable". The clear default for a reader-per-gate build.
-
PN7160 — pick this when you need ISO15693 per-gate or want to run up to 4
readers on one I2C bus. Budget
irq_pinper gate; without it you drop to the shim and lose 5× on detection latency. Bench-check the 50 ms_wait_for_irq_release()on the detection tick. - PN532 over software I2C — works, but 2 pins per gate plus pull-ups per bus, because 0x24 is fixed. Best detection latency of the lot (~2 mm) if pin count isn't your constraint. PN532/SPI would be 1 pin per gate and is fully written — but it is untested against hardware and warns as such at startup.
- PN5180 — avoid per-gate for homing. 3 required pins per gate and no probe. Its place is as a shared Type-5 reader.
If you need ISO15693/Type-5 at all: PN7160 or PN5180 only. PN532 and RC522 cannot read those tags in this codebase.
(\_/)
( *,*)
(")_(") Happy Hare ReadyCopyright (C) 2022-2026 Paul Morgan
*** NEW V4 DOC IS HERE ***
1. Introduction
2. Installation
3. Essential Configuration
5. Operation
-- YOUR MMU IS READY TO PLAY WITH --
6. Slicer-MMU Setup
-- NOW YOU ARE READY TO PRINT! --
7. Tuning
8. Optional Feature Setup
9. Advanced Configuration
10. Advanced Concepts
- State Persistence
- Statistics and Counters
- Tool and Gate Maps
- Filament Bypass
- Runout/Clog Detection
- Consumption Counters
- Synchronized Gear/Extruder
11. Quick References
12. Troubleshooting
13. FAQ
14. MCU Board Reference 🆕
15. Change Log
Happy Hare Discord