Skip to content

Commit d941f95

Browse files
committed
Add ISO 15693 cmds
1 parent a9b1aba commit d941f95

File tree

11 files changed

+494
-52
lines changed

11 files changed

+494
-52
lines changed

examples/iso_14443-write-memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def main() -> int:
4848
# Write memory based on card type
4949
if len(card.uid) == 4:
5050
# MIFARE Classic card
51-
raise NotImplemented("Not yet implemented")
51+
raise NotImplementedError("Not yet implemented")
5252
else:
5353
# Other ISO 14443-A card (e.g., NTAG)
5454
card.write_memory(16//4, 0xEFBEADDE)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
3+
# SPDX-FileCopyrightText: 2025 PN5180-tagomatic contributors
4+
# SPDX-License-Identifier: GPL-3.0-or-later
5+
6+
"""Example program demonstrating PN5180 ISO 15693 inventory.
7+
8+
This example program finds the UID of ISO-15963 cards
9+
10+
Usage:
11+
examples/iso_15693_inventory.py /dev/ttyACM0
12+
examples/iso_15693_inventory.py COM3
13+
"""
14+
15+
import argparse
16+
import sys
17+
18+
from pn5180_tagomatic import PN5180
19+
20+
21+
def main() -> int:
22+
"""Main entry point for the example program."""
23+
parser = argparse.ArgumentParser(
24+
description="PN5180 ISO 15693 Inventory Example",
25+
formatter_class=argparse.RawDescriptionHelpFormatter,
26+
)
27+
parser.add_argument(
28+
"tty",
29+
help="Serial port device (e.g., /dev/ttyACM0 or COM3)",
30+
)
31+
args = parser.parse_args()
32+
33+
try:
34+
# Create PN5180 reader instance
35+
with PN5180(args.tty) as reader:
36+
print("PN5180 reader initialized")
37+
38+
# Start ISO 15693 communication session
39+
# 0x0D = TX config for ISO 15693
40+
# 0x8D = RX config for ISO 15693
41+
with reader.start_session(0x0D, 0x8D) as session:
42+
print("Performing ISO 15693 inventory...")
43+
44+
# Perform inventory
45+
uids = session.iso15693_inventory()
46+
47+
# Display results
48+
if uids:
49+
print(f"\nFound {len(uids)} tag(s):")
50+
for i, uid in enumerate(uids, 1):
51+
print(f" {i}. UID: {uid.hex(':')}")
52+
53+
card = session.connect_iso15693(uids[0])
54+
card.write_memory(4, b' Hello! ')
55+
56+
memory = card.read_memory()
57+
for offset in range(0, len(memory), 16):
58+
chunk = memory[offset : offset + 16]
59+
ascii_values = "".join(
60+
chr(byte) if 32 <= byte <= 126 else "."
61+
for byte in chunk
62+
)
63+
print(f"({offset:03x}): {chunk.hex(' ')} {ascii_values}")
64+
else:
65+
print("\nNo tags found")
66+
67+
return 0
68+
69+
except Exception as e:
70+
print(f"Error: {e}", file=sys.stderr)
71+
return 1
72+
73+
74+
if __name__ == "__main__":
75+
sys.exit(main())

src/pn5180_tagomatic/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2025 PN5180-tagomatic contributors
1+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
22
# SPDX-License-Identifier: GPL-3.0-or-later
33

44
"""PN5180-tagomatic: USB based RFID reader with Python interface."""
@@ -14,6 +14,7 @@
1414
TimeslotBehavior,
1515
)
1616
from .iso14443a import ISO14443ACard
17+
from .iso15693 import ISO15693Card
1718
from .pn5180 import PN5180
1819
from .proxy import PN5180Helper, PN5180Proxy
1920
from .session import PN5180RFSession
@@ -22,6 +23,7 @@
2223
__all__ = [
2324
"ISO14443ACard",
2425
"ISO14443ACommand",
26+
"ISO15693Card",
2527
"ISO15693Command",
2628
"MifareKeyType",
2729
"PN5180",

src/pn5180_tagomatic/constants.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2025 PN5180-tagomatic contributors
1+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
22
# SPDX-License-Identifier: GPL-3.0-or-later
33

44
"""Constants and enumerations for PN5180 RFID reader."""
@@ -115,4 +115,14 @@ class ISO14443ACommand(IntEnum):
115115
class ISO15693Command(IntEnum):
116116
"""ISO 15693 protocol command bytes."""
117117

118-
INVENTORY = 0x01 # Inventory command
118+
GET_SYSTEM_INFORMATION = 0x2B
119+
GET_MULTIPLE_BLOCK_SECURITY_STATUS = 0x2C
120+
INVENTORY = 0x01
121+
LOCK_BLOCK = 0x22
122+
READ_SINGLE_BLOCK = 0x20
123+
READ_MULTIPLE_BLOCKS = 0x23
124+
RESET_TO_READY = 0x26
125+
SELECT = 0x25
126+
STAY_QUIET = 0x02
127+
WRITE_SINGLE_BLOCK = 0x21
128+
WRITE_MULTIPLE_BLOCKS = 0x24

src/pn5180_tagomatic/iso15693.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
2+
# SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
"""ISO 15693 card implementation."""
5+
6+
from __future__ import annotations
7+
8+
from .constants import ISO15693Command
9+
from .proxy import PN5180Error, PN5180Helper
10+
11+
12+
class ISO15693Card:
13+
"""Represents a connected ISO 15693 card.
14+
15+
This class provides methods to interact with a card that has been
16+
successfully connected via the SELECT command.
17+
"""
18+
19+
def __init__(self, reader: PN5180Helper, uid: bytes) -> None:
20+
"""Initialize ISO15693.
21+
22+
Args:
23+
reader: The PN5180 reader instance.
24+
uid: The card's UID.
25+
"""
26+
self._reader = reader
27+
self._uid = uid
28+
self._block_size = -1
29+
self._num_blocks = 32
30+
31+
@property
32+
def uid(self) -> bytes:
33+
"""Get the card's UID."""
34+
return self._uid
35+
36+
def read_memory(
37+
self, start_block: int = 0, num_blocks: int = 255
38+
) -> bytes:
39+
"""Read memory from card.
40+
41+
Args:
42+
start_block: Starting block number (default: 0).
43+
num_blocks: Number of pages to read (default: 255).
44+
45+
Returns:
46+
All read memory as a single bytes object.
47+
48+
Raises:
49+
PN5180Error: If communication with the card fails.
50+
"""
51+
self._reader.turn_on_crc()
52+
self._reader.change_mode_to_transceiver()
53+
54+
memory_content = self._reader.send_and_receive_15693(
55+
ISO15693Command.READ_MULTIPLE_BLOCKS,
56+
bytes([start_block, num_blocks - 1]),
57+
)
58+
59+
if len(memory_content) > 0 and memory_content[0] & 1:
60+
memory_content += b"\0"
61+
raise PN5180Error(
62+
"Got error while reading memory", memory_content[1]
63+
)
64+
if len(memory_content) < 2:
65+
# No more data available
66+
return b""
67+
68+
return memory_content[1:]
69+
70+
def get_system_information(self) -> dict[str, int]:
71+
"""Get System information from card.
72+
73+
Returns:
74+
The system info as a single bytes object.
75+
76+
Raises:
77+
PN5180Error: If communication with the card fails.
78+
"""
79+
self._reader.turn_on_crc()
80+
self._reader.change_mode_to_transceiver()
81+
82+
system_info = self._reader.send_and_receive_15693(
83+
ISO15693Command.GET_SYSTEM_INFORMATION,
84+
b"",
85+
)
86+
87+
if len(system_info) > 0 and system_info[0] & 1:
88+
system_info += b"\0"
89+
raise PN5180Error("Got error while reading memory", system_info[1])
90+
if len(system_info) < 1:
91+
system_info += b"\0"
92+
93+
pos = 9
94+
result = {}
95+
if system_info[0] & 1:
96+
result["dsfid"] = system_info[pos]
97+
pos += 1
98+
if system_info[0] & 2:
99+
result["afi"] = system_info[pos]
100+
pos += 1
101+
if system_info[0] & 4:
102+
result["num_blocks"] = system_info[pos]
103+
pos += 1
104+
result["block_size"] = system_info[pos] + 1
105+
pos += 1
106+
if system_info[0] & 8:
107+
result["num_blocks"] = system_info[pos]
108+
pos += 1
109+
result["ic_reference"] = system_info[pos] + 1
110+
pos += 1
111+
112+
return result
113+
114+
def write_memory(self, start_block: int, data: bytes) -> None:
115+
"""Write memory to a non-MIFARE Classic ISO 14443-A card.
116+
117+
Args:
118+
block: Starting block number (default: 0).
119+
data: <block size> bytes
120+
121+
Raises:
122+
PN5180Error: If communication with the card fails.
123+
"""
124+
125+
if self._block_size < 0:
126+
sys_info = self.get_system_information()
127+
self._block_size = sys_info.get("block_size", 4)
128+
self._num_blocks = sys_info.get("num_blocks", 32)
129+
130+
if len(data) % self._block_size != 0:
131+
raise ValueError(
132+
f"data isn't an even multiple of the block size ({self._block_size})"
133+
)
134+
135+
self._reader.turn_on_crc()
136+
self._reader.change_mode_to_transceiver()
137+
138+
num_blocks = len(data) // self._block_size
139+
140+
# result = self._reader.send_and_receive_15693(
141+
# ISO15693Command.WRITE_MULTIPLE_BLOCKS,
142+
# bytes([
143+
# start_block,
144+
# num_blocks - 1,
145+
# ]) + data)
146+
147+
for block in range(num_blocks):
148+
print(f"Writing block {block}")
149+
result = self._reader.send_and_receive_15693(
150+
ISO15693Command.WRITE_SINGLE_BLOCK,
151+
bytes(
152+
[
153+
block + start_block,
154+
]
155+
)
156+
+ data[
157+
block * self._block_size : (block + 1) * self._block_size
158+
],
159+
)
160+
if len(result) < 1 or result[0] & 1:
161+
result += b"\0\0"
162+
raise PN5180Error(
163+
f"Got error response when writing to block {start_block + block}",
164+
result[1],
165+
)

src/pn5180_tagomatic/pn5180.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SPDX-FileCopyrightText: 2025 PN5180-tagomatic contributors
1+
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
22
# SPDX-License-Identifier: GPL-3.0-or-later
33

44
"""High-level PN5180 RFID reader interface."""

0 commit comments

Comments
 (0)