test_nfc_reader.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. """Tests for daemon.nfc_reader — tray UUID extraction and raw blocks from Bambu tag blocks."""
  2. from unittest.mock import MagicMock
  3. from daemon.nfc_reader import NFCReader, NFCState, _extract_tray_uuid, _raw_blocks, capacity_refusal, choose_ndef
  4. # Measured on a real spool: block 9 equals the tray_uuid the AMS reports (#984).
  5. BLOCK9 = bytes.fromhex("9E0B0717BEE94D7887EB1D8DFD1A14F3")
  6. # Block 4 of a "PLA Matte" spool -- what the old blocks 4+5 read sent as tray_uuid.
  7. BLOCK4_PLA_MATTE = b"PLA Matte" + b"\x00" * 7
  8. class TestExtractTrayUuid:
  9. def test_reads_block_9(self):
  10. blocks = {1: b"\x01" * 16, 2: b"\x02" * 16, 4: BLOCK4_PLA_MATTE, 5: b"\x00" * 16, 9: BLOCK9}
  11. assert _extract_tray_uuid(blocks) == "9E0B0717BEE94D7887EB1D8DFD1A14F3"
  12. def test_ignores_blocks_4_and_5(self):
  13. """Without block 9 there is no tray UUID, never the filament type from block 4."""
  14. blocks = {4: BLOCK4_PLA_MATTE, 5: b"\x00" * 16}
  15. assert _extract_tray_uuid(blocks) is None
  16. def test_all_zero_block_9_is_no_uuid(self):
  17. assert _extract_tray_uuid({9: b"\x00" * 16}) is None
  18. def test_short_block_9_is_no_uuid(self):
  19. assert _extract_tray_uuid({9: b"\x9e\x0b"}) is None
  20. def test_empty_blocks(self):
  21. assert _extract_tray_uuid({}) is None
  22. class TestRawBlocks:
  23. def test_blocks_as_uppercase_hex_keyed_by_block_number(self):
  24. assert _raw_blocks({9: BLOCK9, 4: BLOCK4_PLA_MATTE}) == {
  25. "4": "504C41204D6174746500000000000000",
  26. "9": "9E0B0717BEE94D7887EB1D8DFD1A14F3",
  27. }
  28. def test_empty_blocks(self):
  29. assert _raw_blocks({}) == {}
  30. class TestChooseNdef:
  31. """#3500: OpenTag3D v2 only where the tag has room for it."""
  32. V1 = b"\x03" + bytes(128) # 129 bytes, fits every NTAG
  33. V2 = b"\x03" + bytes(188) # 189 bytes, more than an NTAG213 holds
  34. def test_ntag215_gets_v2(self):
  35. assert choose_ndef(bytes([0xE1, 0x10, 0x3E, 0x00]), self.V1, self.V2) == (self.V2, "v2")
  36. def test_ntag216_gets_v2(self):
  37. assert choose_ndef(bytes([0xE1, 0x10, 0x6D, 0x00]), self.V1, self.V2) == (self.V2, "v2")
  38. def test_ntag213_gets_v1(self):
  39. assert choose_ndef(bytes([0xE1, 0x10, 0x12, 0x00]), self.V1, self.V2) == (self.V1, "v1")
  40. def test_a_cc_that_is_not_ndef_gets_v1(self):
  41. assert choose_ndef(bytes([0x00, 0x00, 0x3E, 0x00]), self.V1, self.V2) == (self.V1, "v1")
  42. def test_an_unread_cc_gets_v1(self):
  43. assert choose_ndef(None, self.V1, self.V2) == (self.V1, "v1")
  44. assert choose_ndef(b"\xe1", self.V1, self.V2) == (self.V1, "v1")
  45. class TestWriteNtag:
  46. UID = bytes.fromhex("04A1B2C3D4E5F6")
  47. NTAG215_CC = bytes([0xE1, 0x10, 0x3E, 0x00])
  48. def _reader(
  49. self,
  50. cc: bytes | None = NTAG215_CC,
  51. read_error: Exception | None = None,
  52. read_back=None,
  53. ):
  54. """A reader over a fake PN5180 whose tag keeps what is written to it.
  55. *read_back* replaces what reading the written pages returns: a
  56. callable taking the written bytes, or an exception to raise.
  57. """
  58. reader = NFCReader.__new__(NFCReader)
  59. reader._state = NFCState.TAG_PRESENT
  60. reader._current_uid = self.UID.hex().upper()
  61. reader._current_sak = 0x00
  62. nfc = MagicMock()
  63. nfc.reactivate_card.return_value = (self.UID, 0x00)
  64. tag = bytearray()
  65. def write_pages(start_page, data):
  66. tag[:] = data + bytes(-len(data) % 4)
  67. return True
  68. def read_pages(start_page, num_pages):
  69. if start_page == 3:
  70. if read_error is not None:
  71. raise read_error
  72. return cc
  73. if isinstance(read_back, Exception):
  74. raise read_back
  75. if read_back is not None:
  76. return read_back(bytes(tag))
  77. return bytes(tag[: num_pages * 4])
  78. nfc.ntag_read_pages.side_effect = read_pages
  79. nfc.ntag_write_pages.side_effect = write_pages
  80. reader._nfc = nfc
  81. return reader, nfc
  82. def _written(self, nfc) -> bytes:
  83. return nfc.ntag_write_pages.call_args.kwargs["data"]
  84. def test_an_ntag215_is_written_v2(self):
  85. reader, nfc = self._reader(cc=bytes([0xE1, 0x10, 0x3E, 0x00]))
  86. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  87. assert ok
  88. assert "v2" in msg
  89. assert nfc.ntag_read_pages.call_args_list[0].kwargs == {"start_page": 3, "num_pages": 1}
  90. assert self._written(nfc) == TestChooseNdef.V2
  91. assert nfc.ntag_write_pages.call_args.kwargs["start_page"] == 4
  92. def test_an_ntag213_is_written_v1(self):
  93. reader, nfc = self._reader(cc=bytes([0xE1, 0x10, 0x12, 0x00]))
  94. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  95. assert ok
  96. assert "v1" in msg
  97. assert self._written(nfc) == TestChooseNdef.V1
  98. def test_a_failed_cc_read_writes_v1(self):
  99. reader, nfc = self._reader(read_error=RuntimeError("BUSY timeout"))
  100. ok, _ = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  101. assert ok
  102. assert self._written(nfc) == TestChooseNdef.V1
  103. def test_without_v2_the_capacity_is_still_checked(self):
  104. # #3076: v1 alone is checked against the tag's size too.
  105. reader, nfc = self._reader(cc=bytes([0xE1, 0x10, 0x06, 0x00])) # 48 bytes
  106. ok, msg = reader.write_ntag(TestChooseNdef.V1)
  107. assert not ok
  108. assert msg == "Tag too small: 129 bytes to write, 48 bytes of space"
  109. nfc.ntag_write_pages.assert_not_called()
  110. def test_a_tag_without_an_ndef_cc_is_refused(self):
  111. # A blank MIFARE Ultralight reports SAK 0x00 like an NTAG, but has no
  112. # NDEF Capability Container (#3076).
  113. reader, nfc = self._reader(cc=bytes(4))
  114. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  115. assert not ok
  116. assert "Not an NDEF-formatted NTAG" in msg
  117. nfc.ntag_write_pages.assert_not_called()
  118. def test_v1_that_fits_an_ntag213_is_written(self):
  119. reader, nfc = self._reader(cc=bytes([0xE1, 0x10, 0x12, 0x00])) # 144 bytes
  120. ok, _ = reader.write_ntag(TestChooseNdef.V1)
  121. assert ok
  122. assert self._written(nfc) == TestChooseNdef.V1
  123. class TestWriteNtagReadBack:
  124. """#3076: the PN5180 can't see the tag's ACK, so the pages are read back."""
  125. def test_matching_data_is_a_verified_write(self):
  126. reader, nfc = TestWriteNtag()._reader()
  127. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  128. assert (ok, msg) == (True, "Write successful (OpenTag3D v2)")
  129. # Read from page 4 over the whole payload, after a fresh select.
  130. assert nfc.ntag_read_pages.call_args_list[-1].kwargs == {"start_page": 4, "num_pages": 48}
  131. assert nfc.reactivate_card.call_count == 3
  132. def test_other_data_fails_the_write_and_names_the_page(self):
  133. def corrupt_page_9(written: bytes) -> bytes:
  134. data = bytearray(written)
  135. data[20] ^= 0xFF # page 4 + 20 // 4
  136. return bytes(data)
  137. reader, _ = TestWriteNtag()._reader(read_back=corrupt_page_9)
  138. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  139. assert not ok
  140. assert msg == "Verification failed at page 9: the tag holds other data"
  141. def test_no_answer_to_the_read_back_leaves_it_unverified(self):
  142. reader, _ = TestWriteNtag()._reader(read_back=lambda written: None)
  143. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  144. assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
  145. def test_a_short_answer_leaves_it_unverified(self):
  146. reader, _ = TestWriteNtag()._reader(read_back=lambda written: written[:16])
  147. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  148. assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
  149. def test_a_read_error_leaves_it_unverified(self):
  150. reader, _ = TestWriteNtag()._reader(read_back=RuntimeError("BUSY timeout"))
  151. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  152. assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
  153. def test_no_answer_to_the_reselect_leaves_it_unverified(self):
  154. reader, nfc = TestWriteNtag()._reader()
  155. nfc.reactivate_card.side_effect = [(TestWriteNtag.UID, 0x00), (TestWriteNtag.UID, 0x00), None]
  156. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  157. assert (ok, msg) == (True, "Write successful (OpenTag3D v2, not verified)")
  158. def test_a_failed_write_is_reported_as_such(self):
  159. reader, nfc = TestWriteNtag()._reader()
  160. nfc.ntag_write_pages.side_effect = None
  161. nfc.ntag_write_pages.return_value = False
  162. ok, msg = reader.write_ntag(TestChooseNdef.V1, TestChooseNdef.V2)
  163. assert (ok, msg) == (False, "Write failed")
  164. class TestCapacityRefusal:
  165. def test_an_ntag213_holds_144_bytes(self):
  166. cc = bytes([0xE1, 0x10, 0x12, 0x00])
  167. assert capacity_refusal(cc, 144) is None
  168. assert capacity_refusal(cc, 145) == "Tag too small: 145 bytes to write, 144 bytes of space"
  169. def test_a_cc_that_is_not_ndef_is_refused(self):
  170. assert capacity_refusal(bytes([0x00, 0x00, 0x3E, 0x00]), 10) is not None
  171. def test_an_unread_cc_proves_nothing(self):
  172. assert capacity_refusal(None, 500) is None
  173. assert capacity_refusal(b"\xe1", 500) is None