Skip to content

Commit f27132e

Browse files
committed
Add NDEF decoding
Also fix formatting for black-26.1.0
1 parent f8793e2 commit f27132e

File tree

6 files changed

+190
-5
lines changed

6 files changed

+190
-5
lines changed

examples/iso_14443-read-memory.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def main() -> int:
4949
print(f"Error: {e}")
5050
return 1
5151

52-
memory = card.read_memory()
52+
memory = card.read_memory(0, 512)
5353

5454
# Display memory content
5555
for offset in range(0, len(memory), 16):
@@ -60,6 +60,13 @@ def main() -> int:
6060
)
6161
print(f"({offset:03x}): {chunk.hex(' ')} {ascii_values}")
6262

63+
ndef_result = card.get_ndef(memory)
64+
if ndef_result is not None:
65+
(start, mem) = ndef_result
66+
print(f"NDEF found, it starts at {start}, len={len(mem)}")
67+
print(f"Content:\n{mem.hex(' ')}")
68+
print(f"Next TLV type: {memory[start + len(mem)]:02x}")
69+
6370
return 0
6471

6572
except Exception as e:

examples/iso_15693-read-memory.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ def main() -> int:
5858
card = session.connect_iso15693(uids[0])
5959

6060
try:
61+
memory = b""
6162
for offset in range(0, 512, 16):
6263
chunk = card.read_memory(offset, 16)
64+
memory += chunk
6365
ascii_values = "".join(
6466
chr(byte) if 32 <= byte <= 126 else "."
6567
for byte in chunk
@@ -70,6 +72,14 @@ def main() -> int:
7072
except TimeoutError:
7173
# Done
7274
pass
75+
ndef_result = card.get_ndef(memory)
76+
if ndef_result is not None:
77+
(start, mem) = ndef_result
78+
print(
79+
f"NDEF found, it starts at {start}, len={len(mem)}"
80+
)
81+
print(f"Content:\n{mem.hex(' ')}")
82+
print(f"Next TLV type: {memory[start + len(mem)]:02x}")
7383
else:
7484
print("\nNo tags found")
7585

src/pn5180_tagomatic/cards.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,17 @@ def write_memory(self, offset: int, data: bytes) -> None:
121121
TimeoutError: If card does not respond.
122122
MemoryWriteError: Other memory write failures.
123123
"""
124+
125+
def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
126+
"""Find the NDEF memory.
127+
128+
If found, the start index in the input memory and
129+
its bytes are returned.
130+
131+
Args:
132+
memory: The card's memory, starting from 0
133+
134+
Returns:
135+
(start, ndef_bytes),
136+
or None if it wasn't found.
137+
"""

src/pn5180_tagomatic/iso14443a.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,3 +262,79 @@ def authenticate_for_page(
262262
if len(key_b) != 6:
263263
raise ValueError("key_b must be exactly 6 bytes")
264264
self._keys_b[page_num] = bytes(key_b)
265+
266+
def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
267+
"""Decode the CC memory block (block 0)
268+
269+
Args:
270+
cc(bytes): The memory from block 0.
271+
272+
Returns:
273+
(major_version, minor_version, memory size, is readonly)
274+
or None if CC isn't valid.
275+
276+
Raises:
277+
PN5180Error: If communication with the card fails.
278+
ValueError: If cc is less than 4 bytes.
279+
"""
280+
if len(cc) < 4:
281+
raise ValueError("cc should be at least 4 bytes")
282+
283+
if cc[0] != 0xE1:
284+
return None
285+
286+
major = cc[1] >> 4
287+
minor = cc[1] & 0xF
288+
289+
mlen = (cc[2]) * 4
290+
291+
is_readonly = bool((cc[3] & 0xF0) == 0xF)
292+
293+
return (major, minor, mlen, is_readonly)
294+
295+
def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
296+
"""Find the NDEF memory.
297+
298+
Args:
299+
memory: The card's memory, starting from offset 0
300+
301+
Returns:
302+
(start, ndef_bytes),
303+
or None if NDEF couldn't be found.
304+
"""
305+
306+
cc = self.decode_cc(memory[12:16])
307+
if cc is None:
308+
return None
309+
310+
major, _minor, mlen, _ = cc
311+
312+
if major > 1:
313+
return None
314+
315+
if mlen > len(memory):
316+
return None
317+
318+
pos = 16
319+
320+
def read_val(memory: bytes, pos: int) -> tuple[int, int]:
321+
if memory[pos] < 255:
322+
return memory[pos], pos + 1
323+
else:
324+
return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3
325+
326+
while pos < mlen:
327+
typ, pos = read_val(memory, pos)
328+
if typ == 0:
329+
continue
330+
if typ == 0xFE:
331+
# End of TLV
332+
return None
333+
field_len, pos = read_val(memory, pos)
334+
if typ == 0x03:
335+
if pos + field_len > mlen:
336+
return None
337+
return (pos, memory[pos:pos + field_len])
338+
pos += field_len
339+
340+
return None

src/pn5180_tagomatic/iso15693.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,89 @@ def memory_block_size(self) -> int:
7373

7474
@property
7575
def memory_number_of_blocks(self) -> int:
76+
"""Gets the number of blocks the card contains"""
7677
self._ensure_sys_info_loaded()
7778
return self._num_blocks
7879

80+
def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
81+
"""Decode the CC memory block (block 0)
82+
83+
Args:
84+
cc(bytes): The memory from block 0.
85+
86+
Returns:
87+
(major_version, minor_version, memory size, is readonly)
88+
or None if CC isn't valid.
89+
90+
Raises:
91+
PN5180Error: If communication with the card fails.
92+
ValueError: If cc is less than 4 bytes.
93+
"""
94+
if len(cc) < 4:
95+
raise ValueError("cc should be at least 4 bytes")
96+
97+
if cc[0] != 0xE1:
98+
return None
99+
100+
major = cc[1] >> 4
101+
minor = cc[1] & 0xF
102+
103+
mlen = (cc[2] + 1) * 8
104+
105+
is_readonly = bool(cc[3] & 1)
106+
107+
return (major, minor, mlen, is_readonly)
108+
109+
def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
110+
"""Find the NDEF memory.
111+
112+
If found, the start index in the input memory and
113+
its bytes are returned.
114+
115+
Args:
116+
memory: The card's memory, starting from 0
117+
118+
Returns:
119+
(start, ndef_bytes),
120+
or None if it wasn't found.
121+
"""
122+
123+
cc = self.decode_cc(memory)
124+
if cc is None:
125+
return None
126+
127+
major, _minor, mlen, _ = cc
128+
129+
if major > 4:
130+
return None
131+
132+
if mlen > len(memory):
133+
return None
134+
135+
pos = 4
136+
137+
def read_val(memory: bytes, pos: int) -> tuple[int, int]:
138+
if memory[pos] < 255:
139+
return memory[pos], pos + 1
140+
else:
141+
return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3
142+
143+
while pos < mlen:
144+
typ, pos = read_val(memory, pos)
145+
if typ == 0:
146+
continue
147+
if typ == 0xFE:
148+
# End of TLV
149+
return None
150+
field_len, pos = read_val(memory, pos)
151+
if typ == 0x03:
152+
if pos + field_len > mlen:
153+
return None
154+
return (pos, memory[pos:pos+field_len])
155+
pos += field_len
156+
157+
return None
158+
79159
def read_memory(self, offset: int = 0, length: int | None = None) -> bytes:
80160
"""Read memory from card.
81161

src/pn5180_tagomatic/session.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ def get_all_iso14443a_uids(
204204
(0, b"", 0, [], True),
205205
]
206206
while len(discovery_stack) > 0:
207-
(cl, mask, coll_bit, uid, restart) = discovery_stack.pop()
207+
cl, mask, coll_bit, uid, restart = discovery_stack.pop()
208208

209209
if restart:
210210
self._reader.turn_off_crc()
@@ -246,9 +246,7 @@ def get_all_iso14443a_uids(
246246
self._reader.set_rx_crc_and_first_bit(False, 0)
247247
self._reader.turn_off_tx_crc()
248248
cmd = self._get_cmd_for_level(cl)
249-
(nvb, final_bits) = self._get_nvb_and_final_bits(
250-
len(mask), coll_bit
251-
)
249+
nvb, final_bits = self._get_nvb_and_final_bits(len(mask), coll_bit)
252250

253251
try:
254252
self._reader.set_rx_crc_and_first_bit(False, final_bits)

0 commit comments

Comments
 (0)