nfc_reader.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. """NFC reader wrapper with state machine for tag presence detection."""
  2. import logging
  3. import time
  4. from enum import Enum, auto
  5. logger = logging.getLogger(__name__)
  6. MISS_THRESHOLD = 3 # Consecutive misses before declaring tag removed
  7. class NFCState(Enum):
  8. IDLE = auto()
  9. TAG_PRESENT = auto()
  10. class NFCReader:
  11. def __init__(self):
  12. self._nfc = None
  13. self._state = NFCState.IDLE
  14. self._current_uid: str | None = None
  15. self._current_sak: int | None = None
  16. self._miss_count = 0
  17. self._ok = False
  18. try:
  19. from read_tag import PN5180
  20. self._nfc = PN5180()
  21. self._nfc.reset()
  22. self._nfc.load_rf_config(0x00, 0x80)
  23. time.sleep(0.010)
  24. self._nfc.rf_on()
  25. time.sleep(0.030)
  26. self._nfc.set_transceive_mode()
  27. self._ok = True
  28. logger.info("NFC reader initialized")
  29. except Exception as e:
  30. logger.info("NFC not available: %s", e)
  31. @property
  32. def ok(self) -> bool:
  33. return self._ok
  34. @property
  35. def state(self) -> NFCState:
  36. return self._state
  37. @property
  38. def current_uid(self) -> str | None:
  39. return self._current_uid
  40. def close(self):
  41. try:
  42. self._nfc.rf_off()
  43. self._nfc.close()
  44. except Exception:
  45. pass
  46. def poll(self) -> tuple[str, dict | None]:
  47. """Poll for tag. Returns (event_type, event_data).
  48. event_type: "none", "tag_detected", "tag_removed"
  49. """
  50. try:
  51. result = self._nfc.activate_type_a()
  52. except Exception as e:
  53. logger.debug("NFC poll error: %s", e)
  54. self._ok = False
  55. return "none", None
  56. self._ok = True
  57. if result is not None:
  58. uid_bytes, sak = result
  59. uid_hex = uid_bytes.hex().upper()
  60. self._miss_count = 0
  61. if self._state == NFCState.IDLE:
  62. self._state = NFCState.TAG_PRESENT
  63. self._current_uid = uid_hex
  64. self._current_sak = sak
  65. # Try reading Bambu tag data
  66. tray_uuid = None
  67. tag_type = "mifare_classic" if sak in (0x08, 0x18) else "ntag" if sak == 0x00 else "unknown"
  68. if sak in (0x08, 0x18):
  69. blocks = self._nfc.read_bambu_tag(uid_bytes)
  70. if blocks:
  71. tray_uuid = _extract_tray_uuid(blocks)
  72. logger.info("Tag detected: %s (SAK=0x%02X)", uid_hex, sak)
  73. return "tag_detected", {
  74. "tag_uid": uid_hex,
  75. "sak": sak,
  76. "tag_type": tag_type,
  77. "tray_uuid": tray_uuid,
  78. }
  79. # Tag still present — no event
  80. return "none", None
  81. # No tag found
  82. if self._state == NFCState.TAG_PRESENT:
  83. self._miss_count += 1
  84. if self._miss_count >= MISS_THRESHOLD:
  85. old_uid = self._current_uid
  86. self._state = NFCState.IDLE
  87. self._current_uid = None
  88. self._current_sak = None
  89. self._miss_count = 0
  90. logger.info("Tag removed: %s", old_uid)
  91. return "tag_removed", {"tag_uid": old_uid}
  92. return "none", None
  93. def _extract_tray_uuid(blocks: dict[int, bytes]) -> str | None:
  94. """Extract tray_uuid from Bambu MIFARE Classic data blocks."""
  95. # Block 4-5 contain the 32-char tray UUID (first 16 bytes from block 4 + 5)
  96. if 4 in blocks and 5 in blocks:
  97. raw = blocks[4] + blocks[5]
  98. # UUID is stored as ASCII hex in the first 16 bytes of blocks 4-5
  99. uuid_bytes = raw[:16]
  100. try:
  101. uuid_str = uuid_bytes.hex().upper()
  102. if uuid_str and uuid_str != "0" * 32:
  103. return uuid_str
  104. except Exception:
  105. pass
  106. return None