Skip to content

Commit e5c2eb1

Browse files
committed
Add interfaces. Classes for UIDs. AFI filter
Also adds properties for AFI, DSFID and IC reference for ISO 15639 cards
1 parent 27feaa3 commit e5c2eb1

15 files changed

+549
-176
lines changed

examples/basic_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def main() -> int:
4141

4242
return 0
4343

44-
except Exception as e:
44+
except Exception as e: # pylint: disable=broad-exception-caught
4545
print(f"Error: {e}", file=sys.stderr)
4646
return 1
4747

examples/iso_14443-get-uid.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def main() -> int:
4545
# Connect to a card
4646
try:
4747
card = session.connect_one_iso14443a()
48-
print(f"UID: {card.uid.hex(':')}")
48+
print(card.id)
4949
except TimeoutError as e:
5050
print(f"Error: {e}")
5151
return 1

examples/iso_14443-read-memory.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import argparse
1515
import sys
1616

17-
from pn5180_tagomatic import PN5180
17+
from pn5180_tagomatic import PN5180, RxProtocol, TxProtocol
1818

1919

2020
def main() -> int:
@@ -41,21 +41,15 @@ def main() -> int:
4141
# Connect to a card
4242
try:
4343
card = session.connect_one_iso14443a()
44-
print(f"UID: {card.uid.hex(':')}")
44+
print(card.id)
4545
except TimeoutError as e:
4646
print(f"Error: {e}")
4747
return 1
4848
except ValueError as e:
4949
print(f"Error: {e}")
5050
return 1
5151

52-
# Read memory based on card type
53-
if len(card.uid) == 4:
54-
# MIFARE Classic card
55-
memory = card.read_mifare_memory()
56-
else:
57-
# Other ISO 14443-A card (e.g., NTAG)
58-
memory = card.read_memory()
52+
memory = card.read_memory()
5953

6054
# Display memory content
6155
for offset in range(0, len(memory), 16):

examples/iso_14443-reqa.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@
1515
import argparse
1616
import sys
1717

18-
from pn5180_tagomatic import PN5180, Registers
19-
from pn5180_tagomatic.constants import (
20-
RxProtocol,
21-
TxProtocol,
22-
)
18+
from pn5180_tagomatic import PN5180, Registers, RxProtocol, TxProtocol
2319

2420

2521
def main() -> int:

examples/iso_14443-write-memory.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def main() -> int:
4343
# Connect to a card
4444
try:
4545
card = session.connect_one_iso14443a()
46-
print(f"UID: {card.uid.hex(':')}")
46+
print(card.id)
4747
except TimeoutError as e:
4848
print(f"Error: {e}")
4949
return 1
@@ -52,20 +52,18 @@ def main() -> int:
5252
return 1
5353

5454
# Write memory based on card type
55-
if len(card.uid) == 4:
55+
if len(card.id.uid_as_bytes()) == 4:
5656
# MIFARE Classic card
5757
raise NotImplementedError("Not yet implemented")
58-
else:
59-
# Other ISO 14443-A card (e.g., NTAG)
60-
card.write_memory(16 // 4, 0xEFBEADDE)
61-
memory = card.read_memory(16 // 4, 1)
62-
memory = memory[:4]
63-
# Display memory content
64-
ascii_values = "".join(
65-
chr(byte) if 32 <= byte <= 126 else "."
66-
for byte in memory
67-
)
68-
print(f"{memory.hex(' ')} {ascii_values}")
58+
# Other ISO 14443-A card (e.g., NTAG)
59+
card.write_memory(16, bytes([0xDE, 0xAD, 0xBE, 0xEF]))
60+
memory = card.read_memory(16, 4)
61+
memory = memory[:4]
62+
# Display memory content
63+
ascii_values = "".join(
64+
chr(byte) if 32 <= byte <= 126 else "." for byte in memory
65+
)
66+
print(f"{memory.hex(' ')} {ascii_values}")
6967

7068
return 0
7169

examples/iso_15693-inventory.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ def main() -> int:
5252
if uids:
5353
print(f"\nFound {len(uids)} tag(s):")
5454
for i, uid in enumerate(uids, 1):
55-
print(f" {i}. UID: {uid.hex(':')}")
55+
print(f" {i}. {uid}")
5656
else:
5757
print("\nNo tags found")
5858

5959
return 0
6060

61-
except Exception as e:
61+
except Exception as e: # pylint: disable=broad-exception-caught
6262
print(f"Error: {e}", file=sys.stderr)
6363
return 1
6464

examples/iso_15693-read-memory.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
"""Example program demonstrating PN5180 ISO 15693 operations.
7+
8+
This example program finds the UID of ISO-15693 cards
9+
10+
Usage:
11+
examples/iso_15693-read-memory.py /dev/ttyACM0
12+
examples/iso_15693-read-memory.py COM3
13+
"""
14+
15+
import argparse
16+
import sys
17+
18+
from pn5180_tagomatic import PN5180
19+
from pn5180_tagomatic.constants import (
20+
RxProtocol,
21+
TxProtocol,
22+
)
23+
24+
25+
def main() -> int:
26+
"""Main entry point for the example program."""
27+
parser = argparse.ArgumentParser(
28+
description="PN5180 ISO 15693 Inventory Example",
29+
formatter_class=argparse.RawDescriptionHelpFormatter,
30+
)
31+
parser.add_argument(
32+
"tty",
33+
help="Serial port device (e.g., /dev/ttyACM0 or COM3)",
34+
)
35+
args = parser.parse_args()
36+
37+
try:
38+
# Create PN5180 reader instance
39+
with PN5180(args.tty) as reader:
40+
print("PN5180 reader initialized")
41+
42+
# Start ISO 15693 communication session
43+
with reader.start_session(
44+
TxProtocol.ISO_15693_ASK100_26,
45+
RxProtocol.ISO_15693_26,
46+
) as session:
47+
print("Performing ISO 15693 inventory...")
48+
49+
# Perform inventory
50+
uids = session.iso15693_inventory(afi=0x00)
51+
52+
# Display results
53+
if uids:
54+
print(f"\nFound {len(uids)} tag(s):")
55+
for i, uid in enumerate(uids, 1):
56+
print(f" {i}. {uid}")
57+
58+
card = session.connect_iso15693(uids[0])
59+
60+
try:
61+
for offset in range(0, 512, 16):
62+
chunk = card.read_memory(offset, 16)
63+
ascii_values = "".join(
64+
chr(byte) if 32 <= byte <= 126 else "."
65+
for byte in chunk
66+
)
67+
print(
68+
f"({offset:03x}): {chunk.hex(' ')} {ascii_values}"
69+
)
70+
except TimeoutError:
71+
# Done
72+
pass
73+
else:
74+
print("\nNo tags found")
75+
76+
return 0
77+
78+
except Exception as e: # pylint: disable=broad-exception-caught
79+
print(f"Error: {e}", file=sys.stderr)
80+
return 1
81+
82+
83+
if __name__ == "__main__":
84+
sys.exit(main())

src/pn5180_tagomatic/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33

44
"""PN5180-tagomatic: USB based RFID reader with Python interface."""
55

6+
from .cards import (
7+
Card,
8+
Iso14443AUniqueId,
9+
Iso15693UniqueId,
10+
UniqueId,
11+
)
612
from .constants import (
713
ISO14443ACommand,
814
ISO15693Command,
@@ -25,11 +31,14 @@
2531

2632
__version__ = "0.1.0"
2733
__all__ = [
34+
"Card",
2835
"ISO14443ACard",
2936
"ISO14443ACommand",
37+
"Iso14443AUniqueId",
3038
"ISO15693Card",
3139
"ISO15693Command",
3240
"ISO15693Error",
41+
"Iso15693UniqueId",
3342
"MemoryWriteError",
3443
"MifareKeyType",
3544
"PN5180",
@@ -41,5 +50,6 @@
4150
"Registers",
4251
"SwitchMode",
4352
"TimeslotBehavior",
53+
"UniqueId",
4454
"__version__",
4555
]

src/pn5180_tagomatic/cards.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
"""Common types for different cards"""
5+
6+
from typing import Protocol
7+
8+
9+
class UniqueId(Protocol):
10+
"""Represents a cards unique identifier.
11+
Subtypes contain type specific extensions.
12+
"""
13+
14+
def uid_as_bytes(self) -> bytes:
15+
"""Returns the UID as bytes"""
16+
17+
def uid_as_string(self) -> str:
18+
"""Returns the UID as a string"""
19+
20+
def __str__(self) -> str:
21+
"""Returns a printable representation"""
22+
23+
24+
class Iso14443AUniqueId(UniqueId):
25+
"""ISO/IEC 14443A card identifiers.
26+
It also includes the SAK response.
27+
"""
28+
29+
def __init__(self, uid: bytes, sak: bytes):
30+
self._uid = uid
31+
self._sak = sak
32+
33+
def uid_as_bytes(self) -> bytes:
34+
return self._uid
35+
36+
def uid_as_string(self) -> str:
37+
return self._uid.hex(":")
38+
39+
def sak_as_bytes(self) -> bytes:
40+
"""The SAK response as bytes"""
41+
return self._sak
42+
43+
def sak_as_string(self) -> str:
44+
"""The SAK response as a string"""
45+
return self._sak.hex(":")
46+
47+
def __str__(self) -> str:
48+
return f"UID: {self.uid_as_string()}, SAK={self.sak_as_string()}"
49+
50+
51+
# Add a class for ISO-15693 UIDs too
52+
53+
54+
class Iso15693UniqueId(UniqueId):
55+
"""ISO/IEC 15693 card identifiers."""
56+
57+
def __init__(self, uid: bytes) -> None:
58+
self._uid = uid
59+
60+
def uid_as_bytes(self) -> bytes:
61+
return self._uid
62+
63+
def uid_as_string(self) -> str:
64+
return self._uid.hex(":")
65+
66+
def __str__(self) -> str:
67+
return f"UID: {self.uid_as_string()}"
68+
69+
70+
class Card(Protocol):
71+
"""Protocol for Cards"""
72+
73+
@property
74+
def id(self) -> UniqueId:
75+
"""Returns the card's unique id"""
76+
77+
@property
78+
def memory_block_size(self) -> int:
79+
"""How big the memory blocks are.
80+
This is the smallest unit that can be written
81+
and read from the card.
82+
83+
Raises:
84+
PN5180Error: If communication with the card fails.
85+
TimeoutError: If card does not respond.
86+
"""
87+
88+
def read_memory(self, offset: int = 0, length: int = 128) -> bytes:
89+
"""Read memory from the card.
90+
91+
Cards normally can only read blocks of fixed sizes so
92+
the returned memory region may be larger than the requested length.
93+
At the end of the cards memory, some cards wrap around, others
94+
stop responding. So the returned memory may be shorter as well.
95+
96+
Args:
97+
offset: Starting offset (default: 0).
98+
length: Number of bytes to read (default: 128).
99+
100+
Returns:
101+
All read memory as a single bytes object.
102+
103+
Raises:
104+
PN5180Error: If communication with the card fails.
105+
TimeoutError: If card does not respond.
106+
"""
107+
108+
def write_memory(self, offset: int, data: bytes) -> None:
109+
"""Write memory to a card.
110+
111+
Cards can only write blocks of a fixed size.
112+
The offset needs to start at an even such multiple
113+
and the data needs to be of the right length as well.
114+
115+
Args:
116+
offset: Starting page number (default: 0).
117+
data: 32-bit data to write to that page
118+
119+
Raises:
120+
PN5180Error: If communication with the card fails.
121+
TimeoutError: If card does not respond.
122+
MemoryWriteError: Other memory write failures.
123+
"""

src/pn5180_tagomatic/constants.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,21 @@ def __init__(
4646
class MemoryWriteError(Exception):
4747
"""Exception raised when memory_write returns an error response from card."""
4848

49-
def __init__(self, error_code: int, response_data: bytes) -> None:
49+
def __init__(
50+
self, offset: int, error_code: int, response_data: bytes
51+
) -> None:
5052
"""Initialize MemoryWriteError.
5153
5254
Args:
55+
offset: The offset that was written to.
5356
error_code: The error code from the tag's error response.
5457
response_data: The full error response data from the tag.
5558
"""
59+
self.offset = offset
5660
self.error_code = error_code
5761
self.response_data = response_data
5862
super().__init__(
59-
f"MemoryWrite command failed "
63+
f"MemoryWrite command failed at offset {offset} "
6064
f"with error code 0x{error_code:02X}"
6165
)
6266

0 commit comments

Comments
 (0)