Skip to content

API Reference

PN5180-tagomatic: USB connected RFID reader with Python interface.

Card

Bases: Protocol

Protocol for Cards

Source code in src/pn5180_tagomatic/cards.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
class Card(Protocol):
    """Protocol for Cards"""

    @property
    def id(self) -> UniqueId:
        """Returns the card's unique id"""

    @property
    def memory_block_size(self) -> int:
        """How big the memory blocks are.
        This is the smallest unit that can be written
        and read from the card.

        Raises:
            PN5180Error: If communication with the card fails.
            TimeoutError: If card does not respond.
        """

    def read_memory(self, offset: int = 0, length: int = 128) -> bytes:
        """Read memory from the card.

        Cards normally can only read blocks of fixed sizes so
        the returned memory region may be larger than the requested length.
        At the end of the cards memory, some cards wrap around, others
        stop responding. So the returned memory may be shorter as well.

        Args:
            offset: Starting offset (default: 0).
            length: Number of bytes to read (default: 128).

        Returns:
            All read memory as a single bytes object.

        Raises:
            PN5180Error: If communication with the card fails.
            TimeoutError: If card does not respond.
        """

    def write_memory(self, offset: int, data: bytes) -> None:
        """Write memory to a card.

        Cards can only write blocks of a fixed size.
        The offset needs to start at an even such multiple
        and the data needs to be of the right length as well.

        Args:
            offset: Starting page number (default: 0).
            data: 32-bit data to write to that page

        Raises:
            PN5180Error: If communication with the card fails.
            TimeoutError: If card does not respond.
            MemoryWriteError: Other memory write failures.
        """

    def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
        """Find the NDEF memory.

        If found, the start index in the input memory and
        its bytes are returned.

        Args:
            memory: The card's memory, starting from 0

        Returns:
            (start, ndef_bytes),
            or None if it wasn't found.
        """

id property

Returns the card's unique id

memory_block_size property

How big the memory blocks are. This is the smallest unit that can be written and read from the card.

Raises:

Type Description
PN5180Error

If communication with the card fails.

TimeoutError

If card does not respond.

get_ndef(memory)

Find the NDEF memory.

If found, the start index in the input memory and its bytes are returned.

Parameters:

Name Type Description Default
memory bytes

The card's memory, starting from 0

required

Returns:

Type Description
tuple[int, bytes] | None

(start, ndef_bytes),

tuple[int, bytes] | None

or None if it wasn't found.

Source code in src/pn5180_tagomatic/cards.py
125
126
127
128
129
130
131
132
133
134
135
136
137
def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
    """Find the NDEF memory.

    If found, the start index in the input memory and
    its bytes are returned.

    Args:
        memory: The card's memory, starting from 0

    Returns:
        (start, ndef_bytes),
        or None if it wasn't found.
    """

read_memory(offset=0, length=128)

Read memory from the card.

Cards normally can only read blocks of fixed sizes so the returned memory region may be larger than the requested length. At the end of the cards memory, some cards wrap around, others stop responding. So the returned memory may be shorter as well.

Parameters:

Name Type Description Default
offset int

Starting offset (default: 0).

0
length int

Number of bytes to read (default: 128).

128

Returns:

Type Description
bytes

All read memory as a single bytes object.

Raises:

Type Description
PN5180Error

If communication with the card fails.

TimeoutError

If card does not respond.

Source code in src/pn5180_tagomatic/cards.py
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def read_memory(self, offset: int = 0, length: int = 128) -> bytes:
    """Read memory from the card.

    Cards normally can only read blocks of fixed sizes so
    the returned memory region may be larger than the requested length.
    At the end of the cards memory, some cards wrap around, others
    stop responding. So the returned memory may be shorter as well.

    Args:
        offset: Starting offset (default: 0).
        length: Number of bytes to read (default: 128).

    Returns:
        All read memory as a single bytes object.

    Raises:
        PN5180Error: If communication with the card fails.
        TimeoutError: If card does not respond.
    """

write_memory(offset, data)

Write memory to a card.

Cards can only write blocks of a fixed size. The offset needs to start at an even such multiple and the data needs to be of the right length as well.

Parameters:

Name Type Description Default
offset int

Starting page number (default: 0).

required
data bytes

32-bit data to write to that page

required

Raises:

Type Description
PN5180Error

If communication with the card fails.

TimeoutError

If card does not respond.

MemoryWriteError

Other memory write failures.

Source code in src/pn5180_tagomatic/cards.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def write_memory(self, offset: int, data: bytes) -> None:
    """Write memory to a card.

    Cards can only write blocks of a fixed size.
    The offset needs to start at an even such multiple
    and the data needs to be of the right length as well.

    Args:
        offset: Starting page number (default: 0).
        data: 32-bit data to write to that page

    Raises:
        PN5180Error: If communication with the card fails.
        TimeoutError: If card does not respond.
        MemoryWriteError: Other memory write failures.
    """

ISO14443ACard

Bases: Card

Represents a connected ISO 14443-A card.

This class provides methods to interact with a card that has been successfully connected via the ISO 14443-A anticollision protocol.

Source code in src/pn5180_tagomatic/iso14443a.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
class ISO14443ACard(Card):
    """Represents a connected ISO 14443-A card.

    This class provides methods to interact with a card that has been
    successfully connected via the ISO 14443-A anticollision protocol.
    """

    def __init__(
        self, reader: PN5180Helper, card_id: Iso14443AUniqueId
    ) -> None:
        """Initialize ISO14443ACard.

        Args:
            reader: The PN5180 reader instance.
            card_id: The card's UID + SAK
        """
        self._reader = reader
        self._card_id = card_id
        self._keys_a: dict[int, bytes] = {}
        self._keys_b: dict[int, bytes] = {}
        self._keys_a[-1] = bytes([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
        self._keys_b[-1] = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00])

    @property
    def id(self) -> UniqueId:
        """Get the card's UniqueId."""
        return self._card_id

    @property
    def sak(self) -> bytes:
        """Get the card's SAK response."""
        return self._card_id.sak_as_bytes()

    @property
    def memory_block_size(self) -> int:
        """How big the memory blocks are"""
        # This should be calculated from card
        return 4

    def read_memory(self, offset: int = 0, length: int = 255) -> bytes:
        start_page = offset // self.memory_block_size
        num_pages = (length + 3) // self.memory_block_size

        uid = self.id.uid_as_bytes()
        if len(uid) == 4:
            return self._read_mifare_memory(start_page, num_pages)
        return self._read_unauthenticated_memory(start_page, num_pages)

    def _read_unauthenticated_memory(
        self, start_page: int = 0, num_pages: int = 255
    ) -> bytes:
        """Read memory from a non-MIFARE Classic ISO 14443-A card.

        This method reads memory pages from ISO 14443-A cards like NTAG
        that don't require authentication.

        Args:
            start_page: Starting page to read
            num_pages: How many pages to read.

        Returns:
            All read memory as a single bytes object.

        Raises:
            PN5180Error: If communication with the card fails.
            TimeoutError: If card does not respond.
        """
        self._reader.turn_on_crc()

        memory_parts = []
        end_page = min(start_page + num_pages, 255)
        for page in range(start_page, end_page, 4):
            # Send READ command
            memory_content = self._reader.send_and_receive(
                0, bytes([ISO14443ACommand.READ, page])
            )

            if len(memory_content) < 1:
                # No more data available
                break

            memory_parts.append(memory_content)

        return b"".join(memory_parts)

    def write_memory(self, offset: int, data: bytes) -> None:
        """Write memory to a non-MIFARE Classic ISO 14443-A card.

        This method writes memory pages from ISO 14443-A cards like NTAG
        that don't require authentication.

        Args:
            offset: Starting page number (default: 0).
            data: data to write to the page(s).

        Raises:
            PN5180Error: If communication with the card fails.
            TimeoutError: If card does not respond.
            MemoryWriteError: If memory write fails.
            ValueError: The bytes are not of an even page length or offset not aligned.
        """

        if offset % self.memory_block_size != 0:
            raise ValueError(
                "Offset not an even multiple of memory_block_size"
            )

        if len(data) % self.memory_block_size != 0:
            raise ValueError(
                "data's length is not an even multiple of memory_block_size"
            )

        start_page = offset // self.memory_block_size

        for page in range(len(data) // self.memory_block_size):
            response = self._reader.send_and_wait_for_ack(
                0,
                bytes([ISO14443ACommand.WRITE, start_page + page])
                + data[
                    offset
                    + page * self.memory_block_size : offset
                    + (page + 1) * self.memory_block_size
                ],
            )

            if len(response) == 0:
                raise MemoryWriteError(
                    offset=offset + page * self.memory_block_size,
                    error_code=0xFF,
                    response_data=b"",
                )

            if (response[0] & 0xF) != 0xA:
                raise MemoryWriteError(
                    offset=offset + page * self.memory_block_size,
                    error_code=response[0],
                    response_data=response,
                )

    def _read_mifare_memory(
        self,
        start_page: int = 0,
        num_pages: int = 255,
    ) -> bytes:
        """Read memory from a MIFARE Classic card.

        This method reads memory from MIFARE Classic cards that require
        authentication. It tries authentication with both KEY_A and KEY_B.

        Args:
            key_a: 6-byte KEY_A (default: all 0xFF).
            key_b: 6-byte KEY_B (default: all 0xFF).
            start_page: Starting page number (default: 0).
            num_pages: Number of pages to read (default: 255).

        Returns:
            All read memory as a single bytes object.

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If UID is not 4 bytes (not MIFARE Classic).
            TimeoutError: If card does not respond.
        """
        uid = self.id.uid_as_bytes()
        if len(uid) != 4:
            raise ValueError(
                "This card does not have a 4-byte UID required for MIFARE Classic"
            )

        self._reader.turn_on_crc()

        # Convert UID to 32-bit integer for authentication
        mifare_uid = uid[3] << 24 | uid[2] << 16 | uid[1] << 8 | uid[0]

        memory_parts = []
        end_page = min(start_page + num_pages, 255)
        for page in range(start_page, end_page, 4):
            # Try KEY A
            key_a = (
                self._keys_a[page]
                if page in self._keys_a
                else self._keys_a.get(-1, None)
            )

            if key_a is not None:
                retval_a = self._reader.mifare_authenticate(
                    key_a, MifareKeyType.KEY_A, page, mifare_uid
                )
                if retval_a == 2:  # timeout
                    break
            else:
                retval_a = -1

            # Try KEY B if KEY A failed
            if retval_a != 0:
                key_b = (
                    self._keys_b[page]
                    if page in self._keys_b
                    else self._keys_b.get(-1, None)
                )
                if key_b is not None:
                    retval_b = self._reader.mifare_authenticate(
                        key_b, MifareKeyType.KEY_B, page, mifare_uid
                    )
                    if retval_b == 2:  # timeout
                        break
                    if retval_b != 0:
                        # Both keys failed, stop reading
                        break
                else:
                    break

            # Send READ command
            memory_content = self._reader.send_and_receive(
                0, bytes([ISO14443ACommand.READ, page])
            )

            if len(memory_content) < 1:
                # No more data available
                break

            memory_parts.append(memory_content)

        return b"".join(memory_parts)

    def authenticate_for_page(
        self, page_num: int, key_a: bytes | None, key_b: bytes | None
    ) -> None:
        """Set authenticate keys for page.

        For mifare cards, an authentication key is needed to read their pages.
        It can be different per page.
        When reading a page and they key is missing, the -1 page's keys are used.

        Args:
            page_num: what page this key should be used for, -1 for setting default key
            key_a: The new key, or None to remove the old one.
            key_b: The new key, or None to remove the old one.
        """
        if key_a is None:
            self._keys_a.pop(page_num, None)
        else:
            if len(key_a) != 6:
                raise ValueError("key_a must be exactly 6 bytes")
            self._keys_a[page_num] = bytes(key_a)

        if key_b is None:
            self._keys_b.pop(page_num, None)
        else:
            if len(key_b) != 6:
                raise ValueError("key_b must be exactly 6 bytes")
            self._keys_b[page_num] = bytes(key_b)

    def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
        """Decode the CC memory block (block 0)

        Args:
            cc(bytes): The memory from block 0.

        Returns:
            (major_version, minor_version, memory size, is readonly)
            or None if CC isn't valid.

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If cc is less than 4 bytes.
        """
        if len(cc) < 4:
            raise ValueError("cc should be at least 4 bytes")

        if cc[0] != 0xE1:
            return None

        major = cc[1] >> 4
        minor = cc[1] & 0xF

        mlen = (cc[2]) * 4

        is_readonly = bool((cc[3] & 0xF0) == 0xF0)

        return (major, minor, mlen, is_readonly)

    def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
        """Find the NDEF memory.

        Args:
            memory: The card's memory, starting from offset 0

        Returns:
            (start, ndef_bytes),
            or None if NDEF couldn't be found.
        """

        cc = self.decode_cc(memory[12:16])
        if cc is None:
            return None

        major, _minor, mlen, _ = cc

        if major > 1:
            return None

        if mlen > len(memory):
            return None

        pos = 16

        def read_val(memory: bytes, pos: int) -> tuple[int, int]:
            if memory[pos] < 255:
                return memory[pos], pos + 1
            else:
                return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3

        while pos < mlen:
            typ, pos = read_val(memory, pos)
            if typ == 0:
                continue
            if typ == 0xFE:
                # End of TLV
                return None
            field_len, pos = read_val(memory, pos)
            if typ == 0x03:
                if pos + field_len > mlen:
                    return None
                return (pos, memory[pos : pos + field_len])
            pos += field_len

        return None

id property

Get the card's UniqueId.

memory_block_size property

How big the memory blocks are

sak property

Get the card's SAK response.

__init__(reader, card_id)

Initialize ISO14443ACard.

Parameters:

Name Type Description Default
reader PN5180Helper

The PN5180 reader instance.

required
card_id Iso14443AUniqueId

The card's UID + SAK

required
Source code in src/pn5180_tagomatic/iso14443a.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(
    self, reader: PN5180Helper, card_id: Iso14443AUniqueId
) -> None:
    """Initialize ISO14443ACard.

    Args:
        reader: The PN5180 reader instance.
        card_id: The card's UID + SAK
    """
    self._reader = reader
    self._card_id = card_id
    self._keys_a: dict[int, bytes] = {}
    self._keys_b: dict[int, bytes] = {}
    self._keys_a[-1] = bytes([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
    self._keys_b[-1] = bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00])

authenticate_for_page(page_num, key_a, key_b)

Set authenticate keys for page.

For mifare cards, an authentication key is needed to read their pages. It can be different per page. When reading a page and they key is missing, the -1 page's keys are used.

Parameters:

Name Type Description Default
page_num int

what page this key should be used for, -1 for setting default key

required
key_a bytes | None

The new key, or None to remove the old one.

required
key_b bytes | None

The new key, or None to remove the old one.

required
Source code in src/pn5180_tagomatic/iso14443a.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
def authenticate_for_page(
    self, page_num: int, key_a: bytes | None, key_b: bytes | None
) -> None:
    """Set authenticate keys for page.

    For mifare cards, an authentication key is needed to read their pages.
    It can be different per page.
    When reading a page and they key is missing, the -1 page's keys are used.

    Args:
        page_num: what page this key should be used for, -1 for setting default key
        key_a: The new key, or None to remove the old one.
        key_b: The new key, or None to remove the old one.
    """
    if key_a is None:
        self._keys_a.pop(page_num, None)
    else:
        if len(key_a) != 6:
            raise ValueError("key_a must be exactly 6 bytes")
        self._keys_a[page_num] = bytes(key_a)

    if key_b is None:
        self._keys_b.pop(page_num, None)
    else:
        if len(key_b) != 6:
            raise ValueError("key_b must be exactly 6 bytes")
        self._keys_b[page_num] = bytes(key_b)

decode_cc(cc)

Decode the CC memory block (block 0)

Parameters:

Name Type Description Default
cc bytes

The memory from block 0.

required

Returns:

Type Description
tuple[int, int, int, bool] | None

(major_version, minor_version, memory size, is readonly)

tuple[int, int, int, bool] | None

or None if CC isn't valid.

Raises:

Type Description
PN5180Error

If communication with the card fails.

ValueError

If cc is less than 4 bytes.

Source code in src/pn5180_tagomatic/iso14443a.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
    """Decode the CC memory block (block 0)

    Args:
        cc(bytes): The memory from block 0.

    Returns:
        (major_version, minor_version, memory size, is readonly)
        or None if CC isn't valid.

    Raises:
        PN5180Error: If communication with the card fails.
        ValueError: If cc is less than 4 bytes.
    """
    if len(cc) < 4:
        raise ValueError("cc should be at least 4 bytes")

    if cc[0] != 0xE1:
        return None

    major = cc[1] >> 4
    minor = cc[1] & 0xF

    mlen = (cc[2]) * 4

    is_readonly = bool((cc[3] & 0xF0) == 0xF0)

    return (major, minor, mlen, is_readonly)

get_ndef(memory)

Find the NDEF memory.

Parameters:

Name Type Description Default
memory bytes

The card's memory, starting from offset 0

required

Returns:

Type Description
tuple[int, bytes] | None

(start, ndef_bytes),

tuple[int, bytes] | None

or None if NDEF couldn't be found.

Source code in src/pn5180_tagomatic/iso14443a.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
    """Find the NDEF memory.

    Args:
        memory: The card's memory, starting from offset 0

    Returns:
        (start, ndef_bytes),
        or None if NDEF couldn't be found.
    """

    cc = self.decode_cc(memory[12:16])
    if cc is None:
        return None

    major, _minor, mlen, _ = cc

    if major > 1:
        return None

    if mlen > len(memory):
        return None

    pos = 16

    def read_val(memory: bytes, pos: int) -> tuple[int, int]:
        if memory[pos] < 255:
            return memory[pos], pos + 1
        else:
            return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3

    while pos < mlen:
        typ, pos = read_val(memory, pos)
        if typ == 0:
            continue
        if typ == 0xFE:
            # End of TLV
            return None
        field_len, pos = read_val(memory, pos)
        if typ == 0x03:
            if pos + field_len > mlen:
                return None
            return (pos, memory[pos : pos + field_len])
        pos += field_len

    return None

write_memory(offset, data)

Write memory to a non-MIFARE Classic ISO 14443-A card.

This method writes memory pages from ISO 14443-A cards like NTAG that don't require authentication.

Parameters:

Name Type Description Default
offset int

Starting page number (default: 0).

required
data bytes

data to write to the page(s).

required

Raises:

Type Description
PN5180Error

If communication with the card fails.

TimeoutError

If card does not respond.

MemoryWriteError

If memory write fails.

ValueError

The bytes are not of an even page length or offset not aligned.

Source code in src/pn5180_tagomatic/iso14443a.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def write_memory(self, offset: int, data: bytes) -> None:
    """Write memory to a non-MIFARE Classic ISO 14443-A card.

    This method writes memory pages from ISO 14443-A cards like NTAG
    that don't require authentication.

    Args:
        offset: Starting page number (default: 0).
        data: data to write to the page(s).

    Raises:
        PN5180Error: If communication with the card fails.
        TimeoutError: If card does not respond.
        MemoryWriteError: If memory write fails.
        ValueError: The bytes are not of an even page length or offset not aligned.
    """

    if offset % self.memory_block_size != 0:
        raise ValueError(
            "Offset not an even multiple of memory_block_size"
        )

    if len(data) % self.memory_block_size != 0:
        raise ValueError(
            "data's length is not an even multiple of memory_block_size"
        )

    start_page = offset // self.memory_block_size

    for page in range(len(data) // self.memory_block_size):
        response = self._reader.send_and_wait_for_ack(
            0,
            bytes([ISO14443ACommand.WRITE, start_page + page])
            + data[
                offset
                + page * self.memory_block_size : offset
                + (page + 1) * self.memory_block_size
            ],
        )

        if len(response) == 0:
            raise MemoryWriteError(
                offset=offset + page * self.memory_block_size,
                error_code=0xFF,
                response_data=b"",
            )

        if (response[0] & 0xF) != 0xA:
            raise MemoryWriteError(
                offset=offset + page * self.memory_block_size,
                error_code=response[0],
                response_data=response,
            )

ISO14443ACommand

Bases: IntEnum

ISO 14443-A protocol command bytes.

Source code in src/pn5180_tagomatic/constants.py
238
239
240
241
242
243
244
245
246
247
248
249
250
class ISO14443ACommand(IntEnum):
    """ISO 14443-A protocol command bytes."""

    ANTICOLLISION_CL1 = 0x93  # Anticollision/Select Cascade Level 1
    ANTICOLLISION_CL2 = 0x95  # Anticollision/Select Cascade Level 2
    ANTICOLLISION_CL3 = 0x97  # Anticollision/Select Cascade Level 3
    ANTICOLLISION = 0x20  # Anticollision command parameter
    HLTA = 0x50  # HALT
    READ = 0x30  # Read command
    REQA = 0x26  # Request A
    SELECT = 0x70  # Select command parameter
    WRITE = 0xA2  # Write command
    WUPA = 0x52  # Wake Up A

ISO15693Card

Bases: Card

Represents a connected ISO 15693 card.

This class provides methods to interact with a card that has been successfully connected via the SELECT command.

Source code in src/pn5180_tagomatic/iso15693.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
class ISO15693Card(Card):
    """Represents a connected ISO 15693 card.

    This class provides methods to interact with a card that has been
    successfully connected via the SELECT command.
    """

    def __init__(
        self, reader: PN5180Helper, card_id: Iso15693UniqueId
    ) -> None:
        """Initialize ISO15693.

        Args:
            reader: The PN5180 reader instance.
            card_id: The card's UID
        """
        self._reader = reader
        self._card_id = card_id
        self._block_size = -1
        self._num_blocks = 32
        self._dsfid: int | None = None
        self._afi: int | None = None
        self._ic_reference: int | None = None

    @property
    def id(self) -> UniqueId:
        """Get the card's UID."""
        return self._card_id

    @property
    def dsfid(self) -> int | None:
        """Gets the DSFID value, if supported by card"""
        self._ensure_sys_info_loaded()
        return self._dsfid

    @property
    def afi(self) -> int | None:
        """Gets the AFI value, if supported by card"""
        self._ensure_sys_info_loaded()
        return self._afi

    @property
    def ic_reference(self) -> int | None:
        """Gets the IC reference value, if supported by card"""
        self._ensure_sys_info_loaded()
        return self._ic_reference

    def _ensure_sys_info_loaded(self) -> None:
        if self._block_size < 0:
            sys_info = self.get_system_information()
            self._block_size = sys_info.get("block_size", 4)
            self._num_blocks = sys_info.get("num_blocks", 256)
            self._dsfid = sys_info.get("dsfid", None)
            self._afi = sys_info.get("afi", None)
            self._ic_reference = sys_info.get("ic_reference", None)

    @property
    def memory_block_size(self) -> int:
        self._ensure_sys_info_loaded()
        return self._block_size

    @property
    def memory_number_of_blocks(self) -> int:
        """Gets the number of blocks the card contains"""
        self._ensure_sys_info_loaded()
        return self._num_blocks

    def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
        """Decode the CC memory block (block 0)

        Args:
            cc(bytes): The memory from block 0.

        Returns:
            (major_version, minor_version, memory size, is readonly)
            or None if CC isn't valid.

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If cc is less than 4 bytes.
        """
        if len(cc) < 4:
            raise ValueError("cc should be at least 4 bytes")

        if cc[0] != 0xE1:
            return None

        major = cc[1] >> 4
        minor = cc[1] & 0xF

        mlen = (cc[2] + 1) * 8

        is_readonly = bool(cc[3] & 1)

        return (major, minor, mlen, is_readonly)

    def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
        """Find the NDEF memory.

        If found, the start index in the input memory and
        its bytes are returned.

        Args:
            memory: The card's memory, starting from 0

        Returns:
            (start, ndef_bytes),
            or None if it wasn't found.
        """
        # pylint: disable=too-many-return-statements

        cc = self.decode_cc(memory)
        if cc is None:
            return None

        major, _minor, mlen, _ = cc

        if major > 4:
            return None

        if mlen > len(memory):
            return None

        pos = 4

        def read_val(memory: bytes, pos: int) -> tuple[int, int]:
            if memory[pos] < 255:
                return memory[pos], pos + 1
            return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3

        while pos < mlen:
            typ, pos = read_val(memory, pos)
            if typ == 0:
                continue
            if typ == 0xFE:
                # End of TLV
                return None
            field_len, pos = read_val(memory, pos)
            if typ == 0x03:
                if pos + field_len > mlen:
                    return None
                return (pos, memory[pos : pos + field_len])
            pos += field_len

        return None

    def read_memory(self, offset: int = 0, length: int | None = None) -> bytes:
        """Read memory from card.

        Args:
            offset: Starting offset, in bytes (default: 0)
            length: Number of bytes to read read (default: 128)

        Returns:
            All read memory as a single bytes object.

        Raises:
            PN5180Error: If communication with the card fails.
        """
        if offset % self.memory_block_size != 0:
            raise ValueError(
                "Offset not an even multiple of memory_block_size"
            )

        start_block = offset // self.memory_block_size

        if length is None:
            num_blocks = self._num_blocks
        else:
            if length % self.memory_block_size != 0:
                raise ValueError(
                    "length is not an even multiple of memory_block_size"
                )
            num_blocks = length // self.memory_block_size

        self._reader.turn_on_crc()
        self._reader.change_mode_to_transceiver()

        # TODO: If num_blocks * memory_block_size > 128, loop
        # TODO: If start_block > 255, use EXTENDED_READ_MULTIPLE_BLOCKS
        memory_content = self._reader.send_and_receive_15693(
            ISO15693Command.READ_MULTIPLE_BLOCKS,
            bytes([start_block, num_blocks - 1]),
        )

        if len(memory_content) > 0 and memory_content[0] & 1:
            memory_content += b"\0"
            raise PN5180Error(
                "Got error while reading memory", memory_content[1]
            )
        if len(memory_content) < 2:
            # No more data available
            return b""

        return memory_content[1:]

    def get_system_information(self) -> dict[str, int]:
        """Get System information from card.

        Returns:
            The system info as a single bytes object.

        Raises:
            PN5180Error: If communication with the card fails.
        """
        self._reader.turn_on_crc()
        self._reader.change_mode_to_transceiver()

        system_info = self._reader.send_and_receive_15693(
            ISO15693Command.GET_SYSTEM_INFORMATION,
            b"",
            to_selected=True,
        )

        if len(system_info) > 0 and system_info[0] & 1:
            system_info += b"\0"
            raise PN5180Error(
                "Error getting system information", system_info[1]
            )
        if len(system_info) < 1:
            raise PN5180Error("Error getting system information, no answer", 0)

        if len(system_info) < 10:
            raise PN5180Error(
                "Error getting system information, no complete answer",
                system_info[0],
            )

        pos = 10
        result = {}
        if system_info[1] & 1:
            result["dsfid"] = system_info[pos]
            pos += 1
        if system_info[1] & 2:
            result["afi"] = system_info[pos]
            pos += 1
        if system_info[1] & 4:
            result["num_blocks"] = system_info[pos] + 1
            pos += 1
            result["block_size"] = (system_info[pos] & 31) + 1
            pos += 1
        if system_info[1] & 8:
            result["ic_reference"] = system_info[pos]
            pos += 1

        return result

    def write_memory(self, offset: int, data: bytes) -> None:
        """Write to a card's memory

        Args:
            offset: Starting byte (default: 0).
            data: <even memory_block_size> bytes

        Raises:
            PN5180Error: If communication with the card fails.
        """
        if offset % self.memory_block_size != 0:
            raise ValueError(
                "Offset not an even multiple of memory_block_size"
            )

        if len(data) % self.memory_block_size != 0:
            raise ValueError(
                "data's length is not an even multiple of memory_block_size"
            )

        start_block = offset // self.memory_block_size

        num_blocks = len(data) // self.memory_block_size

        self._reader.turn_on_crc()
        self._reader.change_mode_to_transceiver()

        ##### This should work, but some cards are incompatible...
        # result = self._reader.send_and_receive_15693(
        #    ISO15693Command.WRITE_MULTIPLE_BLOCKS,
        #    bytes([
        #        start_block,
        #        num_blocks - 1,
        #        ]) + data)

        for block in range(num_blocks):
            result = self._reader.send_and_receive_15693(
                ISO15693Command.WRITE_SINGLE_BLOCK,
                bytes(
                    [
                        block + start_block,
                    ]
                )
                + data[
                    block
                    * self.memory_block_size : (block + 1)
                    * self.memory_block_size
                ],
            )
            if len(result) < 1 or result[0] & 1:
                result += b"\0\0"
                raise MemoryWriteError(
                    offset=block * self.memory_block_size,
                    error_code=result[1],
                    response_data=result,
                )

afi property

Gets the AFI value, if supported by card

dsfid property

Gets the DSFID value, if supported by card

ic_reference property

Gets the IC reference value, if supported by card

id property

Get the card's UID.

memory_number_of_blocks property

Gets the number of blocks the card contains

__init__(reader, card_id)

Initialize ISO15693.

Parameters:

Name Type Description Default
reader PN5180Helper

The PN5180 reader instance.

required
card_id Iso15693UniqueId

The card's UID

required
Source code in src/pn5180_tagomatic/iso15693.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
def __init__(
    self, reader: PN5180Helper, card_id: Iso15693UniqueId
) -> None:
    """Initialize ISO15693.

    Args:
        reader: The PN5180 reader instance.
        card_id: The card's UID
    """
    self._reader = reader
    self._card_id = card_id
    self._block_size = -1
    self._num_blocks = 32
    self._dsfid: int | None = None
    self._afi: int | None = None
    self._ic_reference: int | None = None

decode_cc(cc)

Decode the CC memory block (block 0)

Parameters:

Name Type Description Default
cc bytes

The memory from block 0.

required

Returns:

Type Description
tuple[int, int, int, bool] | None

(major_version, minor_version, memory size, is readonly)

tuple[int, int, int, bool] | None

or None if CC isn't valid.

Raises:

Type Description
PN5180Error

If communication with the card fails.

ValueError

If cc is less than 4 bytes.

Source code in src/pn5180_tagomatic/iso15693.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def decode_cc(self, cc: bytes) -> tuple[int, int, int, bool] | None:
    """Decode the CC memory block (block 0)

    Args:
        cc(bytes): The memory from block 0.

    Returns:
        (major_version, minor_version, memory size, is readonly)
        or None if CC isn't valid.

    Raises:
        PN5180Error: If communication with the card fails.
        ValueError: If cc is less than 4 bytes.
    """
    if len(cc) < 4:
        raise ValueError("cc should be at least 4 bytes")

    if cc[0] != 0xE1:
        return None

    major = cc[1] >> 4
    minor = cc[1] & 0xF

    mlen = (cc[2] + 1) * 8

    is_readonly = bool(cc[3] & 1)

    return (major, minor, mlen, is_readonly)

get_ndef(memory)

Find the NDEF memory.

If found, the start index in the input memory and its bytes are returned.

Parameters:

Name Type Description Default
memory bytes

The card's memory, starting from 0

required

Returns:

Type Description
tuple[int, bytes] | None

(start, ndef_bytes),

tuple[int, bytes] | None

or None if it wasn't found.

Source code in src/pn5180_tagomatic/iso15693.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def get_ndef(self, memory: bytes) -> tuple[int, bytes] | None:
    """Find the NDEF memory.

    If found, the start index in the input memory and
    its bytes are returned.

    Args:
        memory: The card's memory, starting from 0

    Returns:
        (start, ndef_bytes),
        or None if it wasn't found.
    """
    # pylint: disable=too-many-return-statements

    cc = self.decode_cc(memory)
    if cc is None:
        return None

    major, _minor, mlen, _ = cc

    if major > 4:
        return None

    if mlen > len(memory):
        return None

    pos = 4

    def read_val(memory: bytes, pos: int) -> tuple[int, int]:
        if memory[pos] < 255:
            return memory[pos], pos + 1
        return (memory[pos + 1] << 8) | memory[pos + 2], pos + 3

    while pos < mlen:
        typ, pos = read_val(memory, pos)
        if typ == 0:
            continue
        if typ == 0xFE:
            # End of TLV
            return None
        field_len, pos = read_val(memory, pos)
        if typ == 0x03:
            if pos + field_len > mlen:
                return None
            return (pos, memory[pos : pos + field_len])
        pos += field_len

    return None

get_system_information()

Get System information from card.

Returns:

Type Description
dict[str, int]

The system info as a single bytes object.

Raises:

Type Description
PN5180Error

If communication with the card fails.

Source code in src/pn5180_tagomatic/iso15693.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def get_system_information(self) -> dict[str, int]:
    """Get System information from card.

    Returns:
        The system info as a single bytes object.

    Raises:
        PN5180Error: If communication with the card fails.
    """
    self._reader.turn_on_crc()
    self._reader.change_mode_to_transceiver()

    system_info = self._reader.send_and_receive_15693(
        ISO15693Command.GET_SYSTEM_INFORMATION,
        b"",
        to_selected=True,
    )

    if len(system_info) > 0 and system_info[0] & 1:
        system_info += b"\0"
        raise PN5180Error(
            "Error getting system information", system_info[1]
        )
    if len(system_info) < 1:
        raise PN5180Error("Error getting system information, no answer", 0)

    if len(system_info) < 10:
        raise PN5180Error(
            "Error getting system information, no complete answer",
            system_info[0],
        )

    pos = 10
    result = {}
    if system_info[1] & 1:
        result["dsfid"] = system_info[pos]
        pos += 1
    if system_info[1] & 2:
        result["afi"] = system_info[pos]
        pos += 1
    if system_info[1] & 4:
        result["num_blocks"] = system_info[pos] + 1
        pos += 1
        result["block_size"] = (system_info[pos] & 31) + 1
        pos += 1
    if system_info[1] & 8:
        result["ic_reference"] = system_info[pos]
        pos += 1

    return result

read_memory(offset=0, length=None)

Read memory from card.

Parameters:

Name Type Description Default
offset int

Starting offset, in bytes (default: 0)

0
length int | None

Number of bytes to read read (default: 128)

None

Returns:

Type Description
bytes

All read memory as a single bytes object.

Raises:

Type Description
PN5180Error

If communication with the card fails.

Source code in src/pn5180_tagomatic/iso15693.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def read_memory(self, offset: int = 0, length: int | None = None) -> bytes:
    """Read memory from card.

    Args:
        offset: Starting offset, in bytes (default: 0)
        length: Number of bytes to read read (default: 128)

    Returns:
        All read memory as a single bytes object.

    Raises:
        PN5180Error: If communication with the card fails.
    """
    if offset % self.memory_block_size != 0:
        raise ValueError(
            "Offset not an even multiple of memory_block_size"
        )

    start_block = offset // self.memory_block_size

    if length is None:
        num_blocks = self._num_blocks
    else:
        if length % self.memory_block_size != 0:
            raise ValueError(
                "length is not an even multiple of memory_block_size"
            )
        num_blocks = length // self.memory_block_size

    self._reader.turn_on_crc()
    self._reader.change_mode_to_transceiver()

    # TODO: If num_blocks * memory_block_size > 128, loop
    # TODO: If start_block > 255, use EXTENDED_READ_MULTIPLE_BLOCKS
    memory_content = self._reader.send_and_receive_15693(
        ISO15693Command.READ_MULTIPLE_BLOCKS,
        bytes([start_block, num_blocks - 1]),
    )

    if len(memory_content) > 0 and memory_content[0] & 1:
        memory_content += b"\0"
        raise PN5180Error(
            "Got error while reading memory", memory_content[1]
        )
    if len(memory_content) < 2:
        # No more data available
        return b""

    return memory_content[1:]

write_memory(offset, data)

Write to a card's memory

Parameters:

Name Type Description Default
offset int

Starting byte (default: 0).

required
data bytes

bytes

required

Raises:

Type Description
PN5180Error

If communication with the card fails.

Source code in src/pn5180_tagomatic/iso15693.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def write_memory(self, offset: int, data: bytes) -> None:
    """Write to a card's memory

    Args:
        offset: Starting byte (default: 0).
        data: <even memory_block_size> bytes

    Raises:
        PN5180Error: If communication with the card fails.
    """
    if offset % self.memory_block_size != 0:
        raise ValueError(
            "Offset not an even multiple of memory_block_size"
        )

    if len(data) % self.memory_block_size != 0:
        raise ValueError(
            "data's length is not an even multiple of memory_block_size"
        )

    start_block = offset // self.memory_block_size

    num_blocks = len(data) // self.memory_block_size

    self._reader.turn_on_crc()
    self._reader.change_mode_to_transceiver()

    ##### This should work, but some cards are incompatible...
    # result = self._reader.send_and_receive_15693(
    #    ISO15693Command.WRITE_MULTIPLE_BLOCKS,
    #    bytes([
    #        start_block,
    #        num_blocks - 1,
    #        ]) + data)

    for block in range(num_blocks):
        result = self._reader.send_and_receive_15693(
            ISO15693Command.WRITE_SINGLE_BLOCK,
            bytes(
                [
                    block + start_block,
                ]
            )
            + data[
                block
                * self.memory_block_size : (block + 1)
                * self.memory_block_size
            ],
        )
        if len(result) < 1 or result[0] & 1:
            result += b"\0\0"
            raise MemoryWriteError(
                offset=block * self.memory_block_size,
                error_code=result[1],
                response_data=result,
            )

ISO15693Command

Bases: IntEnum

ISO 15693 protocol command bytes.

Source code in src/pn5180_tagomatic/constants.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
class ISO15693Command(IntEnum):
    """ISO 15693 protocol command bytes."""

    GET_SYSTEM_INFORMATION = 0x2B
    GET_MULTIPLE_BLOCK_SECURITY_STATUS = 0x2C
    INVENTORY = 0x01
    LOCK_BLOCK = 0x22
    READ_SINGLE_BLOCK = 0x20
    READ_MULTIPLE_BLOCKS = 0x23
    RESET_TO_READY = 0x26
    SELECT = 0x25
    STAY_QUIET = 0x02
    WRITE_SINGLE_BLOCK = 0x21
    WRITE_MULTIPLE_BLOCKS = 0x24

ISO15693Error

Bases: Exception

Exception raised when an ISO 15693 command returns an error response.

Source code in src/pn5180_tagomatic/constants.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class ISO15693Error(Exception):
    """Exception raised when an ISO 15693 command returns an error response."""

    def __init__(
        self, command: int, error_code: int, response_data: bytes
    ) -> None:
        """Initialize ISO15693Error.

        Args:
            command: The ISO 15693 command that triggered the error (8-bit value).
            error_code: The error code from the tag's error response.
            response_data: The full error response data from the tag.
        """
        self.command = command
        self.error_code = error_code
        self.response_data = response_data
        super().__init__(
            f"ISO 15693 command 0x{command:02X} failed "
            f"with error code 0x{error_code:02X}"
        )

__init__(command, error_code, response_data)

Initialize ISO15693Error.

Parameters:

Name Type Description Default
command int

The ISO 15693 command that triggered the error (8-bit value).

required
error_code int

The error code from the tag's error response.

required
response_data bytes

The full error response data from the tag.

required
Source code in src/pn5180_tagomatic/constants.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def __init__(
    self, command: int, error_code: int, response_data: bytes
) -> None:
    """Initialize ISO15693Error.

    Args:
        command: The ISO 15693 command that triggered the error (8-bit value).
        error_code: The error code from the tag's error response.
        response_data: The full error response data from the tag.
    """
    self.command = command
    self.error_code = error_code
    self.response_data = response_data
    super().__init__(
        f"ISO 15693 command 0x{command:02X} failed "
        f"with error code 0x{error_code:02X}"
    )

Iso14443AUniqueId

Bases: UniqueId

ISO/IEC 14443A card identifiers. It also includes the SAK response.

Source code in src/pn5180_tagomatic/cards.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Iso14443AUniqueId(UniqueId):
    """ISO/IEC 14443A card identifiers.
    It also includes the SAK response.
    """

    def __init__(self, uid: bytes, sak: bytes):
        self._uid = uid
        self._sak = sak

    def uid_as_bytes(self) -> bytes:
        return self._uid

    def uid_as_string(self) -> str:
        return self._uid.hex(":")

    def sak_as_bytes(self) -> bytes:
        """The SAK response as bytes"""
        return self._sak

    def sak_as_string(self) -> str:
        """The SAK response as a string"""
        return self._sak.hex(":")

    def __str__(self) -> str:
        return f"UID: {self.uid_as_string()}, SAK={self.sak_as_string()}"

sak_as_bytes()

The SAK response as bytes

Source code in src/pn5180_tagomatic/cards.py
39
40
41
def sak_as_bytes(self) -> bytes:
    """The SAK response as bytes"""
    return self._sak

sak_as_string()

The SAK response as a string

Source code in src/pn5180_tagomatic/cards.py
43
44
45
def sak_as_string(self) -> str:
    """The SAK response as a string"""
    return self._sak.hex(":")

Iso15693UniqueId

Bases: UniqueId

ISO/IEC 15693 card identifiers.

Source code in src/pn5180_tagomatic/cards.py
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class Iso15693UniqueId(UniqueId):
    """ISO/IEC 15693 card identifiers."""

    def __init__(self, uid: bytes) -> None:
        self._uid = uid

    def uid_as_bytes(self) -> bytes:
        return self._uid

    def uid_as_string(self) -> str:
        return self._uid.hex(":")

    def __str__(self) -> str:
        return f"UID: {self.uid_as_string()}"

MemoryWriteError

Bases: Exception

Exception raised when memory_write returns an error response from card.

Source code in src/pn5180_tagomatic/constants.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class MemoryWriteError(Exception):
    """Exception raised when memory_write returns an error response from card."""

    def __init__(
        self, offset: int, error_code: int, response_data: bytes
    ) -> None:
        """Initialize MemoryWriteError.

        Args:
            offset: The offset that was written to.
            error_code: The error code from the tag's error response.
            response_data: The full error response data from the tag.
        """
        self.offset = offset
        self.error_code = error_code
        self.response_data = response_data
        super().__init__(
            f"MemoryWrite command failed at offset {offset} "
            f"with error code 0x{error_code:02X}"
        )

__init__(offset, error_code, response_data)

Initialize MemoryWriteError.

Parameters:

Name Type Description Default
offset int

The offset that was written to.

required
error_code int

The error code from the tag's error response.

required
response_data bytes

The full error response data from the tag.

required
Source code in src/pn5180_tagomatic/constants.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def __init__(
    self, offset: int, error_code: int, response_data: bytes
) -> None:
    """Initialize MemoryWriteError.

    Args:
        offset: The offset that was written to.
        error_code: The error code from the tag's error response.
        response_data: The full error response data from the tag.
    """
    self.offset = offset
    self.error_code = error_code
    self.response_data = response_data
    super().__init__(
        f"MemoryWrite command failed at offset {offset} "
        f"with error code 0x{error_code:02X}"
    )

MifareKeyType

Bases: IntEnum

Mifare authentication key types.

Source code in src/pn5180_tagomatic/constants.py
68
69
70
71
72
class MifareKeyType(IntEnum):
    """Mifare authentication key types."""

    KEY_A = 0x60
    KEY_B = 0x61

PN5180

High-level PN5180 RFID reader interface.

This class provides a convenient, high-level API for common RFID operations. For direct hardware access, use the ll (low-level) attribute.

Parameters:

Name Type Description Default
tty str

The tty device path to communicate via.

required

Attributes:

Name Type Description
ll

Low-level PN5180 interface for direct hardware access.

Examples:

>>> from pn5180_tagomatic import *
>>> with PN5180("/dev/ttyACM0") as reader:
...     # High-level API
...     with reader.start_session(
...         TxProtocol.ISO_14443_A_106, RxProtocol.ISO_14443_A_106
...     ) as comm:
...         card: Card = comm.connect_one_iso14443a()
...         print(f"Found card: {card.id}")
...         memory: bytes = card.read_memory()
...
...     # Low-level access, when needed
...     data: bytes = reader.ll.read_eeprom(0x12, 2)
...     print("Read from EEPROM")
...     print(f"Firmware version: {data[1]}.{data[0]}")
Source code in src/pn5180_tagomatic/pn5180.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class PN5180:
    """High-level PN5180 RFID reader interface.

    This class provides a convenient, high-level API for common RFID operations.
    For direct hardware access, use the `ll` (low-level) attribute.

    Args:
        tty: The tty device path to communicate via.

    Attributes:
        ll: Low-level PN5180 interface for direct hardware access.

    Examples:
        >>> from pn5180_tagomatic import *
        >>> with PN5180("/dev/ttyACM0") as reader:
        ...     # High-level API
        ...     with reader.start_session(
        ...         TxProtocol.ISO_14443_A_106, RxProtocol.ISO_14443_A_106
        ...     ) as comm:
        ...         card: Card = comm.connect_one_iso14443a()
        ...         print(f"Found card: {card.id}")
        ...         memory: bytes = card.read_memory()
        ...
        ...     # Low-level access, when needed
        ...     data: bytes = reader.ll.read_eeprom(0x12, 2)
        ...     print("Read from EEPROM")
        ...     print(f"Firmware version: {data[1]}.{data[0]}")
    """

    def __init__(self, tty: str) -> None:
        """Initialize the PN5180 reader.

        Args:
            tty: The tty device path to communicate via.
        """
        self.ll = PN5180Helper(tty)

    def start_session(
        self, tx_config: TxProtocol, rx_config: RxProtocol
    ) -> PN5180RFSession:
        """Start an RF communication session.

        This method loads the RF configuration and turns on the RF field,
        then returns a PN5180RFSession object that manages the session.
        The RF field will be automatically turned off when the session ends.

        Args:
            tx_config: TX configuration index (byte: 0-255, see table 32).
            rx_config: RX configuration index (byte: 0-255, see table 32).

        Returns:
            PN5180RFSession object for managing the session.

        Raises:
            PN5180Error: If the operation fails.

        Examples:
            >>> reader = PN5180("/dev/ttyACM0")
            >>> with reader.start_session(0x00, 0x80) as comm:
            ...     card = comm.connect_one_iso14443a()
            ...     uid = card.id.uid_as_bytes()
            ...     memory = card.read_memory()
        """
        self.ll.load_rf_config(tx_config, rx_config)
        self.ll.rf_on()
        return PN5180RFSession(self.ll)

    def close(self) -> None:
        """Close the serial connection."""
        self.ll.close()

    def __enter__(self) -> PN5180:
        """Context manager entry."""
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit."""
        self.close()

__enter__()

Context manager entry.

Source code in src/pn5180_tagomatic/pn5180.py
89
90
91
def __enter__(self) -> PN5180:
    """Context manager entry."""
    return self

__exit__(exc_type, exc_val, exc_tb)

Context manager exit.

Source code in src/pn5180_tagomatic/pn5180.py
93
94
95
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """Context manager exit."""
    self.close()

__init__(tty)

Initialize the PN5180 reader.

Parameters:

Name Type Description Default
tty str

The tty device path to communicate via.

required
Source code in src/pn5180_tagomatic/pn5180.py
47
48
49
50
51
52
53
def __init__(self, tty: str) -> None:
    """Initialize the PN5180 reader.

    Args:
        tty: The tty device path to communicate via.
    """
    self.ll = PN5180Helper(tty)

close()

Close the serial connection.

Source code in src/pn5180_tagomatic/pn5180.py
85
86
87
def close(self) -> None:
    """Close the serial connection."""
    self.ll.close()

start_session(tx_config, rx_config)

Start an RF communication session.

This method loads the RF configuration and turns on the RF field, then returns a PN5180RFSession object that manages the session. The RF field will be automatically turned off when the session ends.

Parameters:

Name Type Description Default
tx_config TxProtocol

TX configuration index (byte: 0-255, see table 32).

required
rx_config RxProtocol

RX configuration index (byte: 0-255, see table 32).

required

Returns:

Type Description
PN5180RFSession

PN5180RFSession object for managing the session.

Raises:

Type Description
PN5180Error

If the operation fails.

Examples:

>>> reader = PN5180("/dev/ttyACM0")
>>> with reader.start_session(0x00, 0x80) as comm:
...     card = comm.connect_one_iso14443a()
...     uid = card.id.uid_as_bytes()
...     memory = card.read_memory()
Source code in src/pn5180_tagomatic/pn5180.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def start_session(
    self, tx_config: TxProtocol, rx_config: RxProtocol
) -> PN5180RFSession:
    """Start an RF communication session.

    This method loads the RF configuration and turns on the RF field,
    then returns a PN5180RFSession object that manages the session.
    The RF field will be automatically turned off when the session ends.

    Args:
        tx_config: TX configuration index (byte: 0-255, see table 32).
        rx_config: RX configuration index (byte: 0-255, see table 32).

    Returns:
        PN5180RFSession object for managing the session.

    Raises:
        PN5180Error: If the operation fails.

    Examples:
        >>> reader = PN5180("/dev/ttyACM0")
        >>> with reader.start_session(0x00, 0x80) as comm:
        ...     card = comm.connect_one_iso14443a()
        ...     uid = card.id.uid_as_bytes()
        ...     memory = card.read_memory()
    """
    self.ll.load_rf_config(tx_config, rx_config)
    self.ll.rf_on()
    return PN5180RFSession(self.ll)

PN5180Error

Bases: Exception

Exception raised when a PN5180 operation fails.

Source code in src/pn5180_tagomatic/constants.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
class PN5180Error(Exception):
    """Exception raised when a PN5180 operation fails."""

    def __init__(self, operation: str, error_code: int) -> None:
        """Initialize PN5180Error.

        Args:
            operation: Name of the operation that failed.
            error_code: The error code returned by the operation.
        """
        self.operation = operation
        self.error_code = error_code
        super().__init__(f"{operation} failed with error code {error_code}")

__init__(operation, error_code)

Initialize PN5180Error.

Parameters:

Name Type Description Default
operation str

Name of the operation that failed.

required
error_code int

The error code returned by the operation.

required
Source code in src/pn5180_tagomatic/constants.py
12
13
14
15
16
17
18
19
20
21
def __init__(self, operation: str, error_code: int) -> None:
    """Initialize PN5180Error.

    Args:
        operation: Name of the operation that failed.
        error_code: The error code returned by the operation.
    """
    self.operation = operation
    self.error_code = error_code
    super().__init__(f"{operation} failed with error code {error_code}")

PN5180Helper

Bases: PN5180Proxy

Helper methods for PN5180.

This class extends PN5180Proxy with convenience methods that build on the low-level RPC methods but are not direct RPC wrappers.

Source code in src/pn5180_tagomatic/proxy.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
class PN5180Helper(PN5180Proxy):
    """Helper methods for PN5180.

    This class extends PN5180Proxy with convenience methods that build on
    the low-level RPC methods but are not direct RPC wrappers.
    """

    def turn_off_rx_crc(self) -> None:
        """Turn off CRC for RX.

        Disables CRC verification for reception.
        """
        # Turn off CRC for RX
        self.write_register_and_mask(Registers.CRC_RX_CONFIG, 0xFFFFFFFE)

    def turn_off_tx_crc(self) -> None:
        """Turn off CRC for TX.

        Disables CRC calculation for transmission.
        """
        # Turn off CRC for TX
        self.write_register_and_mask(Registers.CRC_TX_CONFIG, 0xFFFFFFFE)

    def turn_off_crc(self) -> None:
        """Turn off CRC for TX and RX. Sets RX_BIT_ALIGN to 0

        Disables CRC calculation and verification for transmission and reception.
        """
        self.set_rx_crc_and_first_bit(False, 0)
        self.turn_off_tx_crc()

    def turn_on_rx_crc(self) -> None:
        """Turn on CRC for RX.

        Enables CRC verification for reception.
        """
        # Turn on CRC for RX
        self.write_register_or_mask(Registers.CRC_RX_CONFIG, 0x00000001)

    def turn_on_tx_crc(self) -> None:
        """Turn on CRC for TX.

        Enables CRC calculation for transmission.
        """
        # Turn on CRC for TX
        self.write_register_or_mask(Registers.CRC_TX_CONFIG, 0x00000001)

    def set_rx_crc_and_first_bit(self, on: bool, bit_start: int = 0) -> None:
        """Set RX_CRC_ENABLE and RX_BIT_ALIGN fields

        Enables/disables CRC verification for reception,
        sets the RX_BIT_ALIGN field as needed for the first
        received bits.
        """
        self.write_register_and_mask(Registers.CRC_RX_CONFIG, 0xFFFFFE3E)
        flags = bit_start << 6
        if on:
            flags |= 1
        self.write_register_or_mask(Registers.CRC_RX_CONFIG, flags)

    def turn_on_crc(self) -> None:
        """Turn on CRC for TX and RX. Sets RX_BIT_ALIGN to 0

        Enables CRC calculation and verification for transmission and reception.
        """
        self.set_rx_crc_and_first_bit(True, 0)
        self.turn_on_tx_crc()

    def change_mode_to_transceiver(self) -> None:
        """Change PN5180 mode to transceiver.

        Sets the device to Idle state first, then initiates Transceiver state.
        """
        # Set Idle state
        self.write_register_and_mask(Registers.SYSTEM_CONFIG, 0xFFFFFFF8)
        # Initiates Transceiver state
        self.write_register_or_mask(Registers.SYSTEM_CONFIG, 0x00000003)

    def clear_rx_irq(self) -> None:
        """Clear RX IRQ in IRQ_STATUS register."""
        self.write_register(Registers.IRQ_CLEAR, 1)

    def enable_only_rx_irq(self) -> None:
        """Enable only RX IRQ in IRQ_ENABLE register."""
        self.write_register(Registers.IRQ_ENABLE, 1)

    def disable_all_irqs(self) -> None:
        """Disable all IRQs in IRQ_ENABLE register."""
        self.write_register(Registers.IRQ_ENABLE, 0)

    def get_rx_data_len(self) -> int:
        """Read the RX_STATUS register and get the length bits."""
        # TODO Verify other bits?
        rx_status = self.read_register(Registers.RX_STATUS)
        data_len = rx_status & 511
        return data_len

    def read_received_data(self) -> bytes:
        """Returns received data, empty bytes, if none."""
        data_len = self.get_rx_data_len()
        if data_len == 0:
            return b""
        return self.read_data(data_len)

    def send_and_receive(self, bits: int, data: bytes) -> bytes:
        """Send data and receive response.

        Args:
            bits: Number of valid bits in final byte (byte: 0-255).
            data: Up to 260 bytes to send.

        Returns:
            Received data as bytes. Empty bytes() if no data was received.

        Raises:
            PN5180Error: If communication fails.
        """
        self.clear_rx_irq()
        self.enable_only_rx_irq()

        self.send_data(bits, data)

        if not self.wait_for_irq(MAX_TIMEOUT):
            raise TimeoutError(f"No answer for {data[0]:x} request.")

        self.disable_all_irqs()
        self.clear_rx_irq()

        return self.read_received_data()

    # pylint: disable=too-many-arguments
    # pylint: disable=too-many-positional-arguments
    def send_15693_request(
        self,
        command: int,
        parameters: bytes,
        is_inventory: bool = False,
        slow_rate: bool = False,
        dual_sub_carrier: bool = False,
        protocol_extension: bool = False,
        to_selected: bool = False,
        option_flag: bool = False,
        uid: Iso15693UniqueId | None = None,
        afi: int | None = None,
    ) -> None:
        """Send ISO/IEC 15693 Request

        Args:
            command: The command's 8-bit value.
            parameters: Up to 250 bytes to send.
            is_inventory: Only set for the INVENTORY command.
            slow_rate: Use low data rate.
            dual_sub_carrier: Use dual sub-carrier
            protocol_extension: Sets that bit in flags.
            to_selected: Sets Select flag bit in flags.
            option_flag: Sets that bit in flags.
            uid: Sets the UID flag bit and includes the uid after command.

        Raises:
            PN5180Error: If communication fails.
            ValueError: Incorrect parameters to function.
        """
        # pylint: disable=too-many-branches

        self._validate_uint8(command, "command")

        flags = 0
        if dual_sub_carrier:
            flags |= 1
        if not slow_rate:
            flags |= 2
        if is_inventory:
            if uid is not None:
                raise ValueError(
                    "is_inventory can't be combined with uid field"
                )
            flags |= 4
        if protocol_extension:
            flags |= 8
        if to_selected:
            if is_inventory:
                raise ValueError(
                    "is_inventory must not be set if to_selected is given"
                )
            if uid is not None:
                raise ValueError("Can't combine UID with to_selected")
            flags |= 16
        if afi is not None:
            if not is_inventory:
                raise ValueError("is_inventory must be set if afi is given")
            flags |= 16
        if uid is not None:
            flags |= 32
        if option_flag:
            flags |= 64

        frame = bytes([flags, command])
        if uid is not None:
            frame += uid.uid_as_bytes()[::-1]
        if afi is not None:
            frame += bytes([afi])

        frame += parameters

        # print(f"Sending frame {frame.hex(' ')}")

        self.send_data(0, frame)

    def send_and_receive_15693(
        self,  # pylint: disable=too-many-arguments
        command: int,
        parameters: bytes,
        is_inventory: bool = False,
        slow_rate: bool = False,
        dual_sub_carrier: bool = False,
        protocol_extension: bool = False,
        to_selected: bool = False,
        option_flag: bool = False,
        uid: Iso15693UniqueId | None = None,
        afi: int | None = None,
    ) -> bytes:
        """Send ISO/IEC 15693 Request

        Args:
            command: The command's 8-bit value.
            parameters: Up to 250 bytes to send.
            is_inventory: Only set for the INVENTORY command.
            slow_rate: Use low data rate.
            dual_sub_carrier: Use dual sub-carrier
            protocol_extension: Sets that bit in flags.
            to_selected: Sets Select flag bit in flags.
            option_flag: Sets that bit in flags.
            uid: Sets the UID flag bit and includes the uid after command.

        Returns:
            Received data as bytes. Empty bytes() if no data was received.

        Raises:
            ISO15693Error: If the tag returns an error response.
            TimeoutError: If no response is received within timeout.
            PN5180Error: If communication with the PN5180 fails.
        """
        self.clear_rx_irq()
        self.enable_only_rx_irq()

        self.send_15693_request(
            command,
            parameters,
            is_inventory=is_inventory,
            slow_rate=slow_rate,
            dual_sub_carrier=dual_sub_carrier,
            protocol_extension=protocol_extension,
            to_selected=to_selected,
            option_flag=option_flag,
            uid=uid,
            afi=afi,
        )
        if not self.wait_for_irq(MAX_TIMEOUT):
            raise TimeoutError(f"No answer for 0x{command:02x} request.")

        self.disable_all_irqs()
        self.clear_rx_irq()

        data = self.read_received_data()

        if len(data) and data[0] & 1:
            error_code = 0xFF
            if len(data) >= 2:
                error_code = data[1]
            raise ISO15693Error(
                command=command,
                error_code=error_code,
                response_data=data,
            )
        return data

    def send_and_wait_for_ack(self, bits: int, data: bytes) -> bytes:
        """Send a request and wait for an ACK/NACK response.

        Args:
            bits: Number of valid bits in the data to send.
            data: Payload bytes to transmit.

        Returns:
            The raw response bytes received from the PN5180.

        Raises:
            TimeoutError: If no response is received within the configured timeout.
        """
        self.turn_on_tx_crc()
        self.turn_off_rx_crc()
        self.change_mode_to_transceiver()

        self.clear_rx_irq()
        self.enable_only_rx_irq()

        self.send_data(bits, data)

        if not self.wait_for_irq(MAX_TIMEOUT):
            raise TimeoutError(f"No answer for 0x{data[0]:02x} request.")

        self.clear_rx_irq()
        self.disable_all_irqs()

        data = self.read_received_data()
        return data

change_mode_to_transceiver()

Change PN5180 mode to transceiver.

Sets the device to Idle state first, then initiates Transceiver state.

Source code in src/pn5180_tagomatic/proxy.py
622
623
624
625
626
627
628
629
630
def change_mode_to_transceiver(self) -> None:
    """Change PN5180 mode to transceiver.

    Sets the device to Idle state first, then initiates Transceiver state.
    """
    # Set Idle state
    self.write_register_and_mask(Registers.SYSTEM_CONFIG, 0xFFFFFFF8)
    # Initiates Transceiver state
    self.write_register_or_mask(Registers.SYSTEM_CONFIG, 0x00000003)

clear_rx_irq()

Clear RX IRQ in IRQ_STATUS register.

Source code in src/pn5180_tagomatic/proxy.py
632
633
634
def clear_rx_irq(self) -> None:
    """Clear RX IRQ in IRQ_STATUS register."""
    self.write_register(Registers.IRQ_CLEAR, 1)

disable_all_irqs()

Disable all IRQs in IRQ_ENABLE register.

Source code in src/pn5180_tagomatic/proxy.py
640
641
642
def disable_all_irqs(self) -> None:
    """Disable all IRQs in IRQ_ENABLE register."""
    self.write_register(Registers.IRQ_ENABLE, 0)

enable_only_rx_irq()

Enable only RX IRQ in IRQ_ENABLE register.

Source code in src/pn5180_tagomatic/proxy.py
636
637
638
def enable_only_rx_irq(self) -> None:
    """Enable only RX IRQ in IRQ_ENABLE register."""
    self.write_register(Registers.IRQ_ENABLE, 1)

get_rx_data_len()

Read the RX_STATUS register and get the length bits.

Source code in src/pn5180_tagomatic/proxy.py
644
645
646
647
648
649
def get_rx_data_len(self) -> int:
    """Read the RX_STATUS register and get the length bits."""
    # TODO Verify other bits?
    rx_status = self.read_register(Registers.RX_STATUS)
    data_len = rx_status & 511
    return data_len

read_received_data()

Returns received data, empty bytes, if none.

Source code in src/pn5180_tagomatic/proxy.py
651
652
653
654
655
656
def read_received_data(self) -> bytes:
    """Returns received data, empty bytes, if none."""
    data_len = self.get_rx_data_len()
    if data_len == 0:
        return b""
    return self.read_data(data_len)

send_15693_request(command, parameters, is_inventory=False, slow_rate=False, dual_sub_carrier=False, protocol_extension=False, to_selected=False, option_flag=False, uid=None, afi=None)

Send ISO/IEC 15693 Request

Parameters:

Name Type Description Default
command int

The command's 8-bit value.

required
parameters bytes

Up to 250 bytes to send.

required
is_inventory bool

Only set for the INVENTORY command.

False
slow_rate bool

Use low data rate.

False
dual_sub_carrier bool

Use dual sub-carrier

False
protocol_extension bool

Sets that bit in flags.

False
to_selected bool

Sets Select flag bit in flags.

False
option_flag bool

Sets that bit in flags.

False
uid Iso15693UniqueId | None

Sets the UID flag bit and includes the uid after command.

None

Raises:

Type Description
PN5180Error

If communication fails.

ValueError

Incorrect parameters to function.

Source code in src/pn5180_tagomatic/proxy.py
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
def send_15693_request(
    self,
    command: int,
    parameters: bytes,
    is_inventory: bool = False,
    slow_rate: bool = False,
    dual_sub_carrier: bool = False,
    protocol_extension: bool = False,
    to_selected: bool = False,
    option_flag: bool = False,
    uid: Iso15693UniqueId | None = None,
    afi: int | None = None,
) -> None:
    """Send ISO/IEC 15693 Request

    Args:
        command: The command's 8-bit value.
        parameters: Up to 250 bytes to send.
        is_inventory: Only set for the INVENTORY command.
        slow_rate: Use low data rate.
        dual_sub_carrier: Use dual sub-carrier
        protocol_extension: Sets that bit in flags.
        to_selected: Sets Select flag bit in flags.
        option_flag: Sets that bit in flags.
        uid: Sets the UID flag bit and includes the uid after command.

    Raises:
        PN5180Error: If communication fails.
        ValueError: Incorrect parameters to function.
    """
    # pylint: disable=too-many-branches

    self._validate_uint8(command, "command")

    flags = 0
    if dual_sub_carrier:
        flags |= 1
    if not slow_rate:
        flags |= 2
    if is_inventory:
        if uid is not None:
            raise ValueError(
                "is_inventory can't be combined with uid field"
            )
        flags |= 4
    if protocol_extension:
        flags |= 8
    if to_selected:
        if is_inventory:
            raise ValueError(
                "is_inventory must not be set if to_selected is given"
            )
        if uid is not None:
            raise ValueError("Can't combine UID with to_selected")
        flags |= 16
    if afi is not None:
        if not is_inventory:
            raise ValueError("is_inventory must be set if afi is given")
        flags |= 16
    if uid is not None:
        flags |= 32
    if option_flag:
        flags |= 64

    frame = bytes([flags, command])
    if uid is not None:
        frame += uid.uid_as_bytes()[::-1]
    if afi is not None:
        frame += bytes([afi])

    frame += parameters

    # print(f"Sending frame {frame.hex(' ')}")

    self.send_data(0, frame)

send_and_receive(bits, data)

Send data and receive response.

Parameters:

Name Type Description Default
bits int

Number of valid bits in final byte (byte: 0-255).

required
data bytes

Up to 260 bytes to send.

required

Returns:

Type Description
bytes

Received data as bytes. Empty bytes() if no data was received.

Raises:

Type Description
PN5180Error

If communication fails.

Source code in src/pn5180_tagomatic/proxy.py
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
def send_and_receive(self, bits: int, data: bytes) -> bytes:
    """Send data and receive response.

    Args:
        bits: Number of valid bits in final byte (byte: 0-255).
        data: Up to 260 bytes to send.

    Returns:
        Received data as bytes. Empty bytes() if no data was received.

    Raises:
        PN5180Error: If communication fails.
    """
    self.clear_rx_irq()
    self.enable_only_rx_irq()

    self.send_data(bits, data)

    if not self.wait_for_irq(MAX_TIMEOUT):
        raise TimeoutError(f"No answer for {data[0]:x} request.")

    self.disable_all_irqs()
    self.clear_rx_irq()

    return self.read_received_data()

send_and_receive_15693(command, parameters, is_inventory=False, slow_rate=False, dual_sub_carrier=False, protocol_extension=False, to_selected=False, option_flag=False, uid=None, afi=None)

Send ISO/IEC 15693 Request

Parameters:

Name Type Description Default
command int

The command's 8-bit value.

required
parameters bytes

Up to 250 bytes to send.

required
is_inventory bool

Only set for the INVENTORY command.

False
slow_rate bool

Use low data rate.

False
dual_sub_carrier bool

Use dual sub-carrier

False
protocol_extension bool

Sets that bit in flags.

False
to_selected bool

Sets Select flag bit in flags.

False
option_flag bool

Sets that bit in flags.

False
uid Iso15693UniqueId | None

Sets the UID flag bit and includes the uid after command.

None

Returns:

Type Description
bytes

Received data as bytes. Empty bytes() if no data was received.

Raises:

Type Description
ISO15693Error

If the tag returns an error response.

TimeoutError

If no response is received within timeout.

PN5180Error

If communication with the PN5180 fails.

Source code in src/pn5180_tagomatic/proxy.py
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
def send_and_receive_15693(
    self,  # pylint: disable=too-many-arguments
    command: int,
    parameters: bytes,
    is_inventory: bool = False,
    slow_rate: bool = False,
    dual_sub_carrier: bool = False,
    protocol_extension: bool = False,
    to_selected: bool = False,
    option_flag: bool = False,
    uid: Iso15693UniqueId | None = None,
    afi: int | None = None,
) -> bytes:
    """Send ISO/IEC 15693 Request

    Args:
        command: The command's 8-bit value.
        parameters: Up to 250 bytes to send.
        is_inventory: Only set for the INVENTORY command.
        slow_rate: Use low data rate.
        dual_sub_carrier: Use dual sub-carrier
        protocol_extension: Sets that bit in flags.
        to_selected: Sets Select flag bit in flags.
        option_flag: Sets that bit in flags.
        uid: Sets the UID flag bit and includes the uid after command.

    Returns:
        Received data as bytes. Empty bytes() if no data was received.

    Raises:
        ISO15693Error: If the tag returns an error response.
        TimeoutError: If no response is received within timeout.
        PN5180Error: If communication with the PN5180 fails.
    """
    self.clear_rx_irq()
    self.enable_only_rx_irq()

    self.send_15693_request(
        command,
        parameters,
        is_inventory=is_inventory,
        slow_rate=slow_rate,
        dual_sub_carrier=dual_sub_carrier,
        protocol_extension=protocol_extension,
        to_selected=to_selected,
        option_flag=option_flag,
        uid=uid,
        afi=afi,
    )
    if not self.wait_for_irq(MAX_TIMEOUT):
        raise TimeoutError(f"No answer for 0x{command:02x} request.")

    self.disable_all_irqs()
    self.clear_rx_irq()

    data = self.read_received_data()

    if len(data) and data[0] & 1:
        error_code = 0xFF
        if len(data) >= 2:
            error_code = data[1]
        raise ISO15693Error(
            command=command,
            error_code=error_code,
            response_data=data,
        )
    return data

send_and_wait_for_ack(bits, data)

Send a request and wait for an ACK/NACK response.

Parameters:

Name Type Description Default
bits int

Number of valid bits in the data to send.

required
data bytes

Payload bytes to transmit.

required

Returns:

Type Description
bytes

The raw response bytes received from the PN5180.

Raises:

Type Description
TimeoutError

If no response is received within the configured timeout.

Source code in src/pn5180_tagomatic/proxy.py
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
def send_and_wait_for_ack(self, bits: int, data: bytes) -> bytes:
    """Send a request and wait for an ACK/NACK response.

    Args:
        bits: Number of valid bits in the data to send.
        data: Payload bytes to transmit.

    Returns:
        The raw response bytes received from the PN5180.

    Raises:
        TimeoutError: If no response is received within the configured timeout.
    """
    self.turn_on_tx_crc()
    self.turn_off_rx_crc()
    self.change_mode_to_transceiver()

    self.clear_rx_irq()
    self.enable_only_rx_irq()

    self.send_data(bits, data)

    if not self.wait_for_irq(MAX_TIMEOUT):
        raise TimeoutError(f"No answer for 0x{data[0]:02x} request.")

    self.clear_rx_irq()
    self.disable_all_irqs()

    data = self.read_received_data()
    return data

set_rx_crc_and_first_bit(on, bit_start=0)

Set RX_CRC_ENABLE and RX_BIT_ALIGN fields

Enables/disables CRC verification for reception, sets the RX_BIT_ALIGN field as needed for the first received bits.

Source code in src/pn5180_tagomatic/proxy.py
601
602
603
604
605
606
607
608
609
610
611
612
def set_rx_crc_and_first_bit(self, on: bool, bit_start: int = 0) -> None:
    """Set RX_CRC_ENABLE and RX_BIT_ALIGN fields

    Enables/disables CRC verification for reception,
    sets the RX_BIT_ALIGN field as needed for the first
    received bits.
    """
    self.write_register_and_mask(Registers.CRC_RX_CONFIG, 0xFFFFFE3E)
    flags = bit_start << 6
    if on:
        flags |= 1
    self.write_register_or_mask(Registers.CRC_RX_CONFIG, flags)

turn_off_crc()

Turn off CRC for TX and RX. Sets RX_BIT_ALIGN to 0

Disables CRC calculation and verification for transmission and reception.

Source code in src/pn5180_tagomatic/proxy.py
577
578
579
580
581
582
583
def turn_off_crc(self) -> None:
    """Turn off CRC for TX and RX. Sets RX_BIT_ALIGN to 0

    Disables CRC calculation and verification for transmission and reception.
    """
    self.set_rx_crc_and_first_bit(False, 0)
    self.turn_off_tx_crc()

turn_off_rx_crc()

Turn off CRC for RX.

Disables CRC verification for reception.

Source code in src/pn5180_tagomatic/proxy.py
561
562
563
564
565
566
567
def turn_off_rx_crc(self) -> None:
    """Turn off CRC for RX.

    Disables CRC verification for reception.
    """
    # Turn off CRC for RX
    self.write_register_and_mask(Registers.CRC_RX_CONFIG, 0xFFFFFFFE)

turn_off_tx_crc()

Turn off CRC for TX.

Disables CRC calculation for transmission.

Source code in src/pn5180_tagomatic/proxy.py
569
570
571
572
573
574
575
def turn_off_tx_crc(self) -> None:
    """Turn off CRC for TX.

    Disables CRC calculation for transmission.
    """
    # Turn off CRC for TX
    self.write_register_and_mask(Registers.CRC_TX_CONFIG, 0xFFFFFFFE)

turn_on_crc()

Turn on CRC for TX and RX. Sets RX_BIT_ALIGN to 0

Enables CRC calculation and verification for transmission and reception.

Source code in src/pn5180_tagomatic/proxy.py
614
615
616
617
618
619
620
def turn_on_crc(self) -> None:
    """Turn on CRC for TX and RX. Sets RX_BIT_ALIGN to 0

    Enables CRC calculation and verification for transmission and reception.
    """
    self.set_rx_crc_and_first_bit(True, 0)
    self.turn_on_tx_crc()

turn_on_rx_crc()

Turn on CRC for RX.

Enables CRC verification for reception.

Source code in src/pn5180_tagomatic/proxy.py
585
586
587
588
589
590
591
def turn_on_rx_crc(self) -> None:
    """Turn on CRC for RX.

    Enables CRC verification for reception.
    """
    # Turn on CRC for RX
    self.write_register_or_mask(Registers.CRC_RX_CONFIG, 0x00000001)

turn_on_tx_crc()

Turn on CRC for TX.

Enables CRC calculation for transmission.

Source code in src/pn5180_tagomatic/proxy.py
593
594
595
596
597
598
599
def turn_on_tx_crc(self) -> None:
    """Turn on CRC for TX.

    Enables CRC calculation for transmission.
    """
    # Turn on CRC for TX
    self.write_register_or_mask(Registers.CRC_TX_CONFIG, 0x00000001)

PN5180Proxy

Low-level PN5180 RFID reader interface.

This class provides direct access to the PN5180 RFID reader's RPC methods via the SimpleRPC protocol. It contains only the low-level methods that directly communicate with the hardware.

Parameters:

Name Type Description Default
tty str

The tty device path to communicate via.

required

Examples:

>>> from pn5180_tagomatic import PN5180Proxy
>>> reader = PN5180Proxy("/dev/ttyACM0")
>>> reader.reset()
Source code in src/pn5180_tagomatic/proxy.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
class PN5180Proxy:  # pylint: disable=too-many-public-methods
    """Low-level PN5180 RFID reader interface.

    This class provides direct access to the PN5180 RFID reader's RPC methods
    via the SimpleRPC protocol. It contains only the low-level methods that
    directly communicate with the hardware.

    Args:
        tty: The tty device path to communicate via.

    Examples:
        >>> from pn5180_tagomatic import PN5180Proxy
        >>> reader = PN5180Proxy("/dev/ttyACM0")
        >>> reader.reset()
    """

    def __init__(self, tty: str) -> None:
        """Initialize the PN5180 low-level reader.

        Args:
            tty: The tty device path to communicate via.
        """
        self._interface = Interface(tty)

    @staticmethod
    def _validate_uint8(value: int, name: str) -> None:
        """Validate that a value is a valid uint8_t (0-255)."""
        if not isinstance(value, int) or value < 0 or value > 255:
            raise ValueError(f"{name} must be between 0 and 255")

    @staticmethod
    def _validate_uint16(value: int, name: str) -> None:
        """Validate that a value is a valid uint16_t (0-65535)."""
        if not isinstance(value, int) or value < 0 or value > 65535:
            raise ValueError(f"{name} must be between 0 and 65535")

    @staticmethod
    def _validate_uint32(value: int, name: str) -> None:
        """Validate that a value is a valid uint32_t (0-2^32-1)."""
        if not isinstance(value, int) or value < 0 or value > 4294967295:
            raise ValueError(f"{name} must be between 0 and 4294967295")

    # pylint: disable=no-member

    def reset(self) -> None:
        """Reset the PN5180 NFC frontend.

        This method calls the reset function on the Arduino device,
        which performs a hardware reset of the PN5180 module.
        """
        self._interface.reset()

    def test_it(self) -> int:
        """Run a basic self-test on the PN5180 NFC frontend.

        This method invokes the underlying Arduino ``test_it`` RPC to verify
        communication with the PN5180 and perform a simple hardware check.

        Returns:
            int: Status code from the Arduino implementation:
                * ``0`` indicates success.
                * A negative value indicates a failure, with the exact
                  meaning determined by the Arduino firmware.

        Raises:
            Exception: Any communication or transport-related exception
                raised by the underlying :class:`simple_rpc.Interface`.
        """
        return cast(int, self._interface.test_it())

    def write_register(self, addr: int, value: int) -> None:
        """Write to a PN5180 register.

        Args:
            addr: Register address (byte: 0-255).
            value: 32-bit value to write (0-2^32-1).

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(addr, "addr")
        self._validate_uint32(value, "value")
        result = cast(
            int,
            self._interface.write_register(addr, value),
        )
        if result < 0:
            raise PN5180Error("write_register", result)

    def write_register_or_mask(self, addr: int, value: int) -> None:
        """Write to a PN5180 register OR the old value.

        Args:
            addr: Register address (byte: 0-255).
            value: 32-bit mask to OR (0-2^32-1).

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(addr, "addr")
        self._validate_uint32(value, "value")
        result = cast(
            int,
            self._interface.write_register_or_mask(addr, value),
        )
        if result < 0:
            raise PN5180Error("write_register_or_mask", result)

    def write_register_and_mask(self, addr: int, value: int) -> None:
        """Write to a PN5180 register AND the old value.

        Args:
            addr: Register address (byte: 0-255).
            value: 32-bit mask to AND (0-2^32-1).

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(addr, "addr")
        self._validate_uint32(value, "value")
        result = cast(
            int,
            self._interface.write_register_and_mask(addr, value),
        )
        if result < 0:
            raise PN5180Error("write_register_and_mask", result)

    def write_register_multiple(
        self, elements: list[tuple[int, int, int]]
    ) -> None:
        """Write to multiple PN5180 registers.

        Args:
            elements: List of (address, op, value/mask) tuples.
                     address: byte (0-255)
                     op: RegisterOperation (1=SET, 2=OR, 3=AND)
                     value/mask: 32-bit value (0-2^32-1)

        Raises:
            PN5180Error: If the operation fails.
        """
        for i, (addr, op, value) in enumerate(elements):
            self._validate_uint8(addr, f"elements[{i}].address")
            if op not in (
                RegisterOperation.SET,
                RegisterOperation.OR,
                RegisterOperation.AND,
            ):
                raise ValueError(
                    f"elements[{i}].op must be RegisterOperation.SET (1), "
                    f"OR (2), or AND (3)"
                )
            self._validate_uint32(value, f"elements[{i}].value")
        result = cast(int, self._interface.write_register_multiple(elements))
        if result < 0:
            raise PN5180Error("write_register_multiple", result)

    def read_register(self, addr: int) -> int:
        """Read from a PN5180 register.

        Args:
            addr: Register address (byte: 0-255).

        Returns:
            32-bit register value.

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(addr, "addr")
        result = cast(tuple[int, int], self._interface.read_register(addr))
        if result[0] < 0:
            raise PN5180Error("read_register", result[0])
        return result[1]

    def read_register_multiple(self, addrs: list[int]) -> list[int]:
        """Read from multiple PN5180 registers.

        Args:
            addrs: List of up to 18 register addresses (each byte: 0-255).

        Returns:
            List of 32-bit register values.

        Raises:
            PN5180Error: If the operation fails.
        """
        if len(addrs) > 18:
            raise ValueError("addrs must contain at most 18 addresses")
        for i, addr in enumerate(addrs):
            self._validate_uint8(addr, f"addrs[{i}]")
        result = cast(
            tuple[int, list[int]],
            self._interface.read_register_multiple(addrs),
        )
        if result[0] < 0:
            raise PN5180Error("read_register_multiple", result[0])
        return result[1]

    def write_eeprom(self, addr: int, values: bytes) -> None:
        """Write to the EEPROM.

        Args:
            addr: EEPROM address (byte: 0-255).
            values: Up to 255 bytes to write.

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(addr, "addr")
        if len(values) > 255:
            raise ValueError("values must be at most 255 bytes")
        result = cast(int, self._interface.write_eeprom(addr, list(values)))
        if result < 0:
            raise PN5180Error("write_eeprom", result)

    def read_eeprom(self, addr: int, length: int) -> bytes:
        """Read from the EEPROM.

        Args:
            addr: EEPROM address (byte: 0-255).
            length: Number of bytes to read (byte: 0-255).

        Returns:
            Bytes read from EEPROM.

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(addr, "addr")
        self._validate_uint8(length, "length")
        result = self._interface.read_eeprom(addr, length)
        if result[0] < 0:
            raise PN5180Error("read_eeprom", result[0])
        return bytes(result[1])

    def write_tx_data(self, values: bytes) -> None:
        """Write to tx buffer.

        Args:
            values: Up to 260 bytes to write.

        Raises:
            PN5180Error: If the operation fails.
        """
        if len(values) > 260:
            raise ValueError("values must be at most 260 bytes")
        result = cast(int, self._interface.write_tx_data(list(values)))
        if result < 0:
            raise PN5180Error("write_tx_data", result)

    def send_data(self, bits: int, values: bytes) -> None:
        """Write to TX buffer and send it.

        Args:
            bits: Number of valid bits in final byte (byte: 0-255).
            values: Up to 260 bytes to send.

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(bits, "bits")
        if len(values) > 260:
            raise ValueError("values must be at most 260 bytes")
        result = cast(int, self._interface.send_data(bits, list(values)))
        if result < 0:
            raise PN5180Error("send_data", result)

    def read_data(self, length: int) -> bytes:
        """Read from RX buffer.

        Args:
            length: Number of bytes to read (max 508, 16-bit value: 0-65535).

        Returns:
            Bytes read from RX buffer.

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint16(length, "length")
        if length > 508:
            raise ValueError("length must be at most 508")
        result = self._interface.read_data(length)
        if result[0] < 0:
            raise PN5180Error("read_data", result[0])
        return bytes(result[1])

    def switch_mode(self, mode: int, params: list[int]) -> None:
        """Switch mode.

        Args:
            mode: Operating mode (SwitchMode.STANDBY, LPCD, or AUTOCOLL).
            params: List of mode-specific parameters (each byte: 0-255).

        Raises:
            PN5180Error: If the operation fails.
        """
        if mode not in (
            SwitchMode.STANDBY,
            SwitchMode.LPCD,
            SwitchMode.AUTOCOLL,
        ):
            raise ValueError(
                f"mode must be SwitchMode.STANDBY (0), LPCD (1), "
                f"or AUTOCOLL (2), got {mode}"
            )
        for i, param in enumerate(params):
            self._validate_uint8(param, f"params[{i}]")
        result = cast(int, self._interface.switch_mode(mode, params))
        if result < 0:
            raise PN5180Error("switch_mode", result)

    def mifare_authenticate(
        self, key: bytes, key_type: int, block_addr: int, mifare_uid: int
    ) -> int:
        """Authenticate to mifare card.

        Args:
            key: 6 byte key.
            key_type: MifareKeyType.KEY_A (0x60) or MifareKeyType.KEY_B (0x61).
            block_addr: Block address (byte: 0-255).
            mifare_uid: card's UID (32-bit)

        Returns:
            Authentication result: 0=authenticated, 1=permission denied, 2=timeout.

        Raises:
            PN5180Error: If the operation fails with error < 0.
        """
        self._validate_uint32(mifare_uid, "mifare_uid")

        if len(key) != 6:
            raise ValueError("key must be exactly 6 bytes")
        if key_type not in (MifareKeyType.KEY_A, MifareKeyType.KEY_B):
            raise ValueError(
                f"key_type must be MifareKeyType.KEY_A (0x60) or "
                f"MifareKeyType.KEY_B (0x61), got {key_type:#x}"
            )
        self._validate_uint8(block_addr, "block_addr")
        self._validate_uint32(mifare_uid, "mifare_uid")
        result = cast(
            int,
            self._interface.mifare_authenticate(
                list(key), key_type, block_addr, mifare_uid
            ),
        )
        if result < 0:
            raise PN5180Error("mifare_authenticate", result)
        return result

    def epc_inventory(
        self,
        select_command: bytes,
        select_command_final_bits: int,
        begin_round: bytes,
        timeslot_behavior: int,
    ) -> None:
        """Start EPC inventory algorithm.

        Args:
            select_command: Up to 39 bytes.
            select_command_final_bits: Number of valid bits in final byte (byte: 0-255).
            begin_round: Exactly 3 bytes.
            timeslot_behavior: Timeslot behavior (TimeslotBehavior enum):
                - MAX_TIMESLOTS (0): NextSlot issued until buffer full
                - SINGLE_TIMESLOT (1): Algorithm pauses after one timeslot
                - SINGLE_WITH_HANDLE (2): Req_Rn issued if valid tag response

        Raises:
            PN5180Error: If the operation fails.
        """
        if len(select_command) > 39:
            raise ValueError("select_command must be at most 39 bytes")
        self._validate_uint8(
            select_command_final_bits, "select_command_final_bits"
        )
        if len(begin_round) != 3:
            raise ValueError("begin_round must be exactly 3 bytes")
        if timeslot_behavior not in (
            TimeslotBehavior.MAX_TIMESLOTS,
            TimeslotBehavior.SINGLE_TIMESLOT,
            TimeslotBehavior.SINGLE_WITH_HANDLE,
        ):
            raise ValueError(
                f"timeslot_behavior must be TimeslotBehavior.MAX_TIMESLOTS (0), "
                f"SINGLE_TIMESLOT (1), or SINGLE_WITH_HANDLE (2), "
                f"got {timeslot_behavior}"
            )
        result = cast(
            int,
            self._interface.epc_inventory(
                list(select_command),
                select_command_final_bits,
                list(begin_round),
                timeslot_behavior,
            ),
        )
        if result < 0:
            raise PN5180Error("epc_inventory", result)

    def epc_resume_inventory(self) -> None:
        """Continue EPC inventory algorithm.

        Raises:
            PN5180Error: If the operation fails.
        """
        result = cast(int, self._interface.epc_resume_inventory())
        if result < 0:
            raise PN5180Error("epc_resume_inventory", result)

    def epc_retrieve_inventory_result_size(self) -> int:
        """Get result size from EPC algorithm.

        Returns:
            Result size in bytes.

        Raises:
            PN5180Error: If the operation fails.
        """
        result = cast(
            int,
            self._interface.epc_retrieve_inventory_result_size(),
        )
        if result < 0:
            raise PN5180Error("epc_retrieve_inventory_result_size", result)
        return result

    def load_rf_config(
        self, tx_config: TxProtocol, rx_config: RxProtocol
    ) -> None:
        """Load RF config settings for RX/TX.

        Args:
            tx_config: TX configuration index (byte: 0-255, see table 32).
            rx_config: RX configuration index (byte: 0-255, see table 32).

        Raises:
            PN5180Error: If the operation fails.
        """
        self._validate_uint8(tx_config, "tx_config")
        self._validate_uint8(rx_config, "rx_config")
        result = cast(
            int,
            self._interface.load_rf_config(tx_config, rx_config),
        )
        if result < 0:
            raise PN5180Error("load_rf_config", result)

    def rf_on(
        self,
        disable_collision_avoidance: bool = False,
        use_active_communication: bool = False,
    ) -> None:
        """Turn on RF field.

        Args:
            disable_collision_avoidance: Turn off collision avoidance for ISO/IEC 18092.
            use_active_communication: Use Active Communication mode.

        Raises:
            PN5180Error: If the operation fails.
        """
        flags = 0
        if disable_collision_avoidance:
            flags |= 0x01
        if use_active_communication:
            flags |= 0x02
        result = cast(int, self._interface.rf_on(flags))
        if result < 0:
            raise PN5180Error("rf_on", result)

    def rf_off(self) -> None:
        """Turn off RF field.

        Raises:
            PN5180Error: If the operation fails.
        """
        result = cast(int, self._interface.rf_off())
        if result < 0:
            raise PN5180Error("rf_off", result)

    def is_irq_set(self) -> bool:
        """Is the IRQ pin set.

        Returns:
            True if IRQ is set.
        """
        return cast(bool, self._interface.is_irq_set())

    def wait_for_irq(self, timeout_ms: int) -> bool:
        """Wait up to a timeout value for the IRQ to be set.

        Args:
            timeout_ms: Time in milliseconds to wait (16-bit value: 0-65535).

        Returns:
            True if IRQ is set.
        """
        self._validate_uint16(timeout_ms, "timeout_ms")
        return cast(
            bool,
            self._interface.wait_for_irq(timeout_ms),
        )

    def close(self) -> None:
        """Close the serial connection."""
        if self._interface:
            self._interface.close()

    def __enter__(self) -> PN5180Proxy:
        """Context manager entry."""
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit."""
        self.close()

__enter__()

Context manager entry.

Source code in src/pn5180_tagomatic/proxy.py
545
546
547
def __enter__(self) -> PN5180Proxy:
    """Context manager entry."""
    return self

__exit__(exc_type, exc_val, exc_tb)

Context manager exit.

Source code in src/pn5180_tagomatic/proxy.py
549
550
551
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """Context manager exit."""
    self.close()

__init__(tty)

Initialize the PN5180 low-level reader.

Parameters:

Name Type Description Default
tty str

The tty device path to communicate via.

required
Source code in src/pn5180_tagomatic/proxy.py
51
52
53
54
55
56
57
def __init__(self, tty: str) -> None:
    """Initialize the PN5180 low-level reader.

    Args:
        tty: The tty device path to communicate via.
    """
    self._interface = Interface(tty)

close()

Close the serial connection.

Source code in src/pn5180_tagomatic/proxy.py
540
541
542
543
def close(self) -> None:
    """Close the serial connection."""
    if self._interface:
        self._interface.close()

epc_inventory(select_command, select_command_final_bits, begin_round, timeslot_behavior)

Start EPC inventory algorithm.

Parameters:

Name Type Description Default
select_command bytes

Up to 39 bytes.

required
select_command_final_bits int

Number of valid bits in final byte (byte: 0-255).

required
begin_round bytes

Exactly 3 bytes.

required
timeslot_behavior int

Timeslot behavior (TimeslotBehavior enum): - MAX_TIMESLOTS (0): NextSlot issued until buffer full - SINGLE_TIMESLOT (1): Algorithm pauses after one timeslot - SINGLE_WITH_HANDLE (2): Req_Rn issued if valid tag response

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def epc_inventory(
    self,
    select_command: bytes,
    select_command_final_bits: int,
    begin_round: bytes,
    timeslot_behavior: int,
) -> None:
    """Start EPC inventory algorithm.

    Args:
        select_command: Up to 39 bytes.
        select_command_final_bits: Number of valid bits in final byte (byte: 0-255).
        begin_round: Exactly 3 bytes.
        timeslot_behavior: Timeslot behavior (TimeslotBehavior enum):
            - MAX_TIMESLOTS (0): NextSlot issued until buffer full
            - SINGLE_TIMESLOT (1): Algorithm pauses after one timeslot
            - SINGLE_WITH_HANDLE (2): Req_Rn issued if valid tag response

    Raises:
        PN5180Error: If the operation fails.
    """
    if len(select_command) > 39:
        raise ValueError("select_command must be at most 39 bytes")
    self._validate_uint8(
        select_command_final_bits, "select_command_final_bits"
    )
    if len(begin_round) != 3:
        raise ValueError("begin_round must be exactly 3 bytes")
    if timeslot_behavior not in (
        TimeslotBehavior.MAX_TIMESLOTS,
        TimeslotBehavior.SINGLE_TIMESLOT,
        TimeslotBehavior.SINGLE_WITH_HANDLE,
    ):
        raise ValueError(
            f"timeslot_behavior must be TimeslotBehavior.MAX_TIMESLOTS (0), "
            f"SINGLE_TIMESLOT (1), or SINGLE_WITH_HANDLE (2), "
            f"got {timeslot_behavior}"
        )
    result = cast(
        int,
        self._interface.epc_inventory(
            list(select_command),
            select_command_final_bits,
            list(begin_round),
            timeslot_behavior,
        ),
    )
    if result < 0:
        raise PN5180Error("epc_inventory", result)

epc_resume_inventory()

Continue EPC inventory algorithm.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
436
437
438
439
440
441
442
443
444
def epc_resume_inventory(self) -> None:
    """Continue EPC inventory algorithm.

    Raises:
        PN5180Error: If the operation fails.
    """
    result = cast(int, self._interface.epc_resume_inventory())
    if result < 0:
        raise PN5180Error("epc_resume_inventory", result)

epc_retrieve_inventory_result_size()

Get result size from EPC algorithm.

Returns:

Type Description
int

Result size in bytes.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
def epc_retrieve_inventory_result_size(self) -> int:
    """Get result size from EPC algorithm.

    Returns:
        Result size in bytes.

    Raises:
        PN5180Error: If the operation fails.
    """
    result = cast(
        int,
        self._interface.epc_retrieve_inventory_result_size(),
    )
    if result < 0:
        raise PN5180Error("epc_retrieve_inventory_result_size", result)
    return result

is_irq_set()

Is the IRQ pin set.

Returns:

Type Description
bool

True if IRQ is set.

Source code in src/pn5180_tagomatic/proxy.py
517
518
519
520
521
522
523
def is_irq_set(self) -> bool:
    """Is the IRQ pin set.

    Returns:
        True if IRQ is set.
    """
    return cast(bool, self._interface.is_irq_set())

load_rf_config(tx_config, rx_config)

Load RF config settings for RX/TX.

Parameters:

Name Type Description Default
tx_config TxProtocol

TX configuration index (byte: 0-255, see table 32).

required
rx_config RxProtocol

RX configuration index (byte: 0-255, see table 32).

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
def load_rf_config(
    self, tx_config: TxProtocol, rx_config: RxProtocol
) -> None:
    """Load RF config settings for RX/TX.

    Args:
        tx_config: TX configuration index (byte: 0-255, see table 32).
        rx_config: RX configuration index (byte: 0-255, see table 32).

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(tx_config, "tx_config")
    self._validate_uint8(rx_config, "rx_config")
    result = cast(
        int,
        self._interface.load_rf_config(tx_config, rx_config),
    )
    if result < 0:
        raise PN5180Error("load_rf_config", result)

mifare_authenticate(key, key_type, block_addr, mifare_uid)

Authenticate to mifare card.

Parameters:

Name Type Description Default
key bytes

6 byte key.

required
key_type int

MifareKeyType.KEY_A (0x60) or MifareKeyType.KEY_B (0x61).

required
block_addr int

Block address (byte: 0-255).

required
mifare_uid int

card's UID (32-bit)

required

Returns:

Type Description
int

Authentication result: 0=authenticated, 1=permission denied, 2=timeout.

Raises:

Type Description
PN5180Error

If the operation fails with error < 0.

Source code in src/pn5180_tagomatic/proxy.py
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
def mifare_authenticate(
    self, key: bytes, key_type: int, block_addr: int, mifare_uid: int
) -> int:
    """Authenticate to mifare card.

    Args:
        key: 6 byte key.
        key_type: MifareKeyType.KEY_A (0x60) or MifareKeyType.KEY_B (0x61).
        block_addr: Block address (byte: 0-255).
        mifare_uid: card's UID (32-bit)

    Returns:
        Authentication result: 0=authenticated, 1=permission denied, 2=timeout.

    Raises:
        PN5180Error: If the operation fails with error < 0.
    """
    self._validate_uint32(mifare_uid, "mifare_uid")

    if len(key) != 6:
        raise ValueError("key must be exactly 6 bytes")
    if key_type not in (MifareKeyType.KEY_A, MifareKeyType.KEY_B):
        raise ValueError(
            f"key_type must be MifareKeyType.KEY_A (0x60) or "
            f"MifareKeyType.KEY_B (0x61), got {key_type:#x}"
        )
    self._validate_uint8(block_addr, "block_addr")
    self._validate_uint32(mifare_uid, "mifare_uid")
    result = cast(
        int,
        self._interface.mifare_authenticate(
            list(key), key_type, block_addr, mifare_uid
        ),
    )
    if result < 0:
        raise PN5180Error("mifare_authenticate", result)
    return result

read_data(length)

Read from RX buffer.

Parameters:

Name Type Description Default
length int

Number of bytes to read (max 508, 16-bit value: 0-65535).

required

Returns:

Type Description
bytes

Bytes read from RX buffer.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def read_data(self, length: int) -> bytes:
    """Read from RX buffer.

    Args:
        length: Number of bytes to read (max 508, 16-bit value: 0-65535).

    Returns:
        Bytes read from RX buffer.

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint16(length, "length")
    if length > 508:
        raise ValueError("length must be at most 508")
    result = self._interface.read_data(length)
    if result[0] < 0:
        raise PN5180Error("read_data", result[0])
    return bytes(result[1])

read_eeprom(addr, length)

Read from the EEPROM.

Parameters:

Name Type Description Default
addr int

EEPROM address (byte: 0-255).

required
length int

Number of bytes to read (byte: 0-255).

required

Returns:

Type Description
bytes

Bytes read from EEPROM.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def read_eeprom(self, addr: int, length: int) -> bytes:
    """Read from the EEPROM.

    Args:
        addr: EEPROM address (byte: 0-255).
        length: Number of bytes to read (byte: 0-255).

    Returns:
        Bytes read from EEPROM.

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(addr, "addr")
    self._validate_uint8(length, "length")
    result = self._interface.read_eeprom(addr, length)
    if result[0] < 0:
        raise PN5180Error("read_eeprom", result[0])
    return bytes(result[1])

read_register(addr)

Read from a PN5180 register.

Parameters:

Name Type Description Default
addr int

Register address (byte: 0-255).

required

Returns:

Type Description
int

32-bit register value.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def read_register(self, addr: int) -> int:
    """Read from a PN5180 register.

    Args:
        addr: Register address (byte: 0-255).

    Returns:
        32-bit register value.

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(addr, "addr")
    result = cast(tuple[int, int], self._interface.read_register(addr))
    if result[0] < 0:
        raise PN5180Error("read_register", result[0])
    return result[1]

read_register_multiple(addrs)

Read from multiple PN5180 registers.

Parameters:

Name Type Description Default
addrs list[int]

List of up to 18 register addresses (each byte: 0-255).

required

Returns:

Type Description
list[int]

List of 32-bit register values.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
def read_register_multiple(self, addrs: list[int]) -> list[int]:
    """Read from multiple PN5180 registers.

    Args:
        addrs: List of up to 18 register addresses (each byte: 0-255).

    Returns:
        List of 32-bit register values.

    Raises:
        PN5180Error: If the operation fails.
    """
    if len(addrs) > 18:
        raise ValueError("addrs must contain at most 18 addresses")
    for i, addr in enumerate(addrs):
        self._validate_uint8(addr, f"addrs[{i}]")
    result = cast(
        tuple[int, list[int]],
        self._interface.read_register_multiple(addrs),
    )
    if result[0] < 0:
        raise PN5180Error("read_register_multiple", result[0])
    return result[1]

reset()

Reset the PN5180 NFC frontend.

This method calls the reset function on the Arduino device, which performs a hardware reset of the PN5180 module.

Source code in src/pn5180_tagomatic/proxy.py
79
80
81
82
83
84
85
def reset(self) -> None:
    """Reset the PN5180 NFC frontend.

    This method calls the reset function on the Arduino device,
    which performs a hardware reset of the PN5180 module.
    """
    self._interface.reset()

rf_off()

Turn off RF field.

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
507
508
509
510
511
512
513
514
515
def rf_off(self) -> None:
    """Turn off RF field.

    Raises:
        PN5180Error: If the operation fails.
    """
    result = cast(int, self._interface.rf_off())
    if result < 0:
        raise PN5180Error("rf_off", result)

rf_on(disable_collision_avoidance=False, use_active_communication=False)

Turn on RF field.

Parameters:

Name Type Description Default
disable_collision_avoidance bool

Turn off collision avoidance for ISO/IEC 18092.

False
use_active_communication bool

Use Active Communication mode.

False

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
def rf_on(
    self,
    disable_collision_avoidance: bool = False,
    use_active_communication: bool = False,
) -> None:
    """Turn on RF field.

    Args:
        disable_collision_avoidance: Turn off collision avoidance for ISO/IEC 18092.
        use_active_communication: Use Active Communication mode.

    Raises:
        PN5180Error: If the operation fails.
    """
    flags = 0
    if disable_collision_avoidance:
        flags |= 0x01
    if use_active_communication:
        flags |= 0x02
    result = cast(int, self._interface.rf_on(flags))
    if result < 0:
        raise PN5180Error("rf_on", result)

send_data(bits, values)

Write to TX buffer and send it.

Parameters:

Name Type Description Default
bits int

Number of valid bits in final byte (byte: 0-255).

required
values bytes

Up to 260 bytes to send.

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
def send_data(self, bits: int, values: bytes) -> None:
    """Write to TX buffer and send it.

    Args:
        bits: Number of valid bits in final byte (byte: 0-255).
        values: Up to 260 bytes to send.

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(bits, "bits")
    if len(values) > 260:
        raise ValueError("values must be at most 260 bytes")
    result = cast(int, self._interface.send_data(bits, list(values)))
    if result < 0:
        raise PN5180Error("send_data", result)

switch_mode(mode, params)

Switch mode.

Parameters:

Name Type Description Default
mode int

Operating mode (SwitchMode.STANDBY, LPCD, or AUTOCOLL).

required
params list[int]

List of mode-specific parameters (each byte: 0-255).

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
def switch_mode(self, mode: int, params: list[int]) -> None:
    """Switch mode.

    Args:
        mode: Operating mode (SwitchMode.STANDBY, LPCD, or AUTOCOLL).
        params: List of mode-specific parameters (each byte: 0-255).

    Raises:
        PN5180Error: If the operation fails.
    """
    if mode not in (
        SwitchMode.STANDBY,
        SwitchMode.LPCD,
        SwitchMode.AUTOCOLL,
    ):
        raise ValueError(
            f"mode must be SwitchMode.STANDBY (0), LPCD (1), "
            f"or AUTOCOLL (2), got {mode}"
        )
    for i, param in enumerate(params):
        self._validate_uint8(param, f"params[{i}]")
    result = cast(int, self._interface.switch_mode(mode, params))
    if result < 0:
        raise PN5180Error("switch_mode", result)

test_it()

Run a basic self-test on the PN5180 NFC frontend.

This method invokes the underlying Arduino test_it RPC to verify communication with the PN5180 and perform a simple hardware check.

Returns:

Name Type Description
int int

Status code from the Arduino implementation: * 0 indicates success. * A negative value indicates a failure, with the exact meaning determined by the Arduino firmware.

Raises:

Type Description
Exception

Any communication or transport-related exception raised by the underlying :class:simple_rpc.Interface.

Source code in src/pn5180_tagomatic/proxy.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def test_it(self) -> int:
    """Run a basic self-test on the PN5180 NFC frontend.

    This method invokes the underlying Arduino ``test_it`` RPC to verify
    communication with the PN5180 and perform a simple hardware check.

    Returns:
        int: Status code from the Arduino implementation:
            * ``0`` indicates success.
            * A negative value indicates a failure, with the exact
              meaning determined by the Arduino firmware.

    Raises:
        Exception: Any communication or transport-related exception
            raised by the underlying :class:`simple_rpc.Interface`.
    """
    return cast(int, self._interface.test_it())

wait_for_irq(timeout_ms)

Wait up to a timeout value for the IRQ to be set.

Parameters:

Name Type Description Default
timeout_ms int

Time in milliseconds to wait (16-bit value: 0-65535).

required

Returns:

Type Description
bool

True if IRQ is set.

Source code in src/pn5180_tagomatic/proxy.py
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def wait_for_irq(self, timeout_ms: int) -> bool:
    """Wait up to a timeout value for the IRQ to be set.

    Args:
        timeout_ms: Time in milliseconds to wait (16-bit value: 0-65535).

    Returns:
        True if IRQ is set.
    """
    self._validate_uint16(timeout_ms, "timeout_ms")
    return cast(
        bool,
        self._interface.wait_for_irq(timeout_ms),
    )

write_eeprom(addr, values)

Write to the EEPROM.

Parameters:

Name Type Description Default
addr int

EEPROM address (byte: 0-255).

required
values bytes

Up to 255 bytes to write.

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def write_eeprom(self, addr: int, values: bytes) -> None:
    """Write to the EEPROM.

    Args:
        addr: EEPROM address (byte: 0-255).
        values: Up to 255 bytes to write.

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(addr, "addr")
    if len(values) > 255:
        raise ValueError("values must be at most 255 bytes")
    result = cast(int, self._interface.write_eeprom(addr, list(values)))
    if result < 0:
        raise PN5180Error("write_eeprom", result)

write_register(addr, value)

Write to a PN5180 register.

Parameters:

Name Type Description Default
addr int

Register address (byte: 0-255).

required
value int

32-bit value to write (0-2^32-1).

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def write_register(self, addr: int, value: int) -> None:
    """Write to a PN5180 register.

    Args:
        addr: Register address (byte: 0-255).
        value: 32-bit value to write (0-2^32-1).

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(addr, "addr")
    self._validate_uint32(value, "value")
    result = cast(
        int,
        self._interface.write_register(addr, value),
    )
    if result < 0:
        raise PN5180Error("write_register", result)

write_register_and_mask(addr, value)

Write to a PN5180 register AND the old value.

Parameters:

Name Type Description Default
addr int

Register address (byte: 0-255).

required
value int

32-bit mask to AND (0-2^32-1).

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def write_register_and_mask(self, addr: int, value: int) -> None:
    """Write to a PN5180 register AND the old value.

    Args:
        addr: Register address (byte: 0-255).
        value: 32-bit mask to AND (0-2^32-1).

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(addr, "addr")
    self._validate_uint32(value, "value")
    result = cast(
        int,
        self._interface.write_register_and_mask(addr, value),
    )
    if result < 0:
        raise PN5180Error("write_register_and_mask", result)

write_register_multiple(elements)

Write to multiple PN5180 registers.

Parameters:

Name Type Description Default
elements list[tuple[int, int, int]]

List of (address, op, value/mask) tuples. address: byte (0-255) op: RegisterOperation (1=SET, 2=OR, 3=AND) value/mask: 32-bit value (0-2^32-1)

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def write_register_multiple(
    self, elements: list[tuple[int, int, int]]
) -> None:
    """Write to multiple PN5180 registers.

    Args:
        elements: List of (address, op, value/mask) tuples.
                 address: byte (0-255)
                 op: RegisterOperation (1=SET, 2=OR, 3=AND)
                 value/mask: 32-bit value (0-2^32-1)

    Raises:
        PN5180Error: If the operation fails.
    """
    for i, (addr, op, value) in enumerate(elements):
        self._validate_uint8(addr, f"elements[{i}].address")
        if op not in (
            RegisterOperation.SET,
            RegisterOperation.OR,
            RegisterOperation.AND,
        ):
            raise ValueError(
                f"elements[{i}].op must be RegisterOperation.SET (1), "
                f"OR (2), or AND (3)"
            )
        self._validate_uint32(value, f"elements[{i}].value")
    result = cast(int, self._interface.write_register_multiple(elements))
    if result < 0:
        raise PN5180Error("write_register_multiple", result)

write_register_or_mask(addr, value)

Write to a PN5180 register OR the old value.

Parameters:

Name Type Description Default
addr int

Register address (byte: 0-255).

required
value int

32-bit mask to OR (0-2^32-1).

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def write_register_or_mask(self, addr: int, value: int) -> None:
    """Write to a PN5180 register OR the old value.

    Args:
        addr: Register address (byte: 0-255).
        value: 32-bit mask to OR (0-2^32-1).

    Raises:
        PN5180Error: If the operation fails.
    """
    self._validate_uint8(addr, "addr")
    self._validate_uint32(value, "value")
    result = cast(
        int,
        self._interface.write_register_or_mask(addr, value),
    )
    if result < 0:
        raise PN5180Error("write_register_or_mask", result)

write_tx_data(values)

Write to tx buffer.

Parameters:

Name Type Description Default
values bytes

Up to 260 bytes to write.

required

Raises:

Type Description
PN5180Error

If the operation fails.

Source code in src/pn5180_tagomatic/proxy.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def write_tx_data(self, values: bytes) -> None:
    """Write to tx buffer.

    Args:
        values: Up to 260 bytes to write.

    Raises:
        PN5180Error: If the operation fails.
    """
    if len(values) > 260:
        raise ValueError("values must be at most 260 bytes")
    result = cast(int, self._interface.write_tx_data(list(values)))
    if result < 0:
        raise PN5180Error("write_tx_data", result)

PN5180RFSession

Manages RF communication session.

This class handles the lifecycle of an RF communication session, ensuring that the RF field is turned off when the session ends.

Source code in src/pn5180_tagomatic/session.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
class PN5180RFSession:
    """Manages RF communication session.

    This class handles the lifecycle of an RF communication session,
    ensuring that the RF field is turned off when the session ends.
    """

    def __init__(self, reader: PN5180Helper) -> None:
        """Initialize PN5180RFSession.

        Args:
            reader: The PN5180 reader instance.
        """
        self._reader = reader
        self._active = True

    def connect_one_iso14443a(self) -> ISO14443ACard:
        """Connect to an ISO 14443-A card.

        This method performs the ISO 14443-A anticollision protocol to
        retrieve the card's UID and returns a card object.

        Returns:
            ISO14443ACard object representing the connected card.

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If the card's response is invalid.
            TimeoutError: If no card responds.
        """
        if not self._active:
            raise RuntimeError("Communication session is no longer active")

        card_id = self._get_one_iso14443a_card_id()
        return ISO14443ACard(self._reader, card_id)

    def connect_iso14443a(
        self, card_id: Iso14443AUniqueId
    ) -> ISO14443ACard | None:
        """
        Select an ISO/IEC 14443A card and return an object representing it.
        """
        # Activate card first.

        # pylint: disable=too-many-return-statements

        self._reader.turn_off_crc()
        self._reader.change_mode_to_transceiver()
        atqa_data = self._send_atqa()
        if len(atqa_data) == 0:
            # No cards left in field.
            return None

        self._reader.turn_on_crc()

        uid = card_id.uid_as_bytes()
        uid_list = list(uid)
        if len(uid) == 4:
            sak = self._send_select_for_cl(0, uid_list)
            if len(sak) == 0:
                return None
        else:
            sak = self._send_select_for_part(0, uid_list[0:3])
            if len(sak) == 0:
                return None

            if len(uid) == 7:
                sak = self._send_select_for_cl(1, uid_list[3:7])
                if len(sak) == 0:
                    return None
            else:
                sak = self._send_select_for_part(1, uid_list[3:6])
                if len(sak) == 0:
                    return None

            if len(uid) == 10:
                sak = self._send_select_for_cl(2, uid_list[6:])
                if len(sak) == 0:
                    return None

        return ISO14443ACard(self._reader, card_id)

    @staticmethod
    def _is_valid_bcc(data: bytes) -> bool:
        """Verify BCC byte"""
        if len(data) != 5:
            return False
        bcc = data[0] ^ data[1] ^ data[2] ^ data[3]
        return bcc == data[4]

    def _get_coll_bit(self) -> None | int:
        """Get collision bit"""
        rx_status = self._reader.read_register(Registers.RX_STATUS)

        if rx_status & (1 << 18):
            coll_bit = (rx_status >> 19) & 63
            return coll_bit

        return None

    @staticmethod
    def _get_cmd_for_level(level: int) -> int:
        if level == 0:
            return ISO14443ACommand.ANTICOLLISION_CL1
        if level == 1:
            return ISO14443ACommand.ANTICOLLISION_CL2
        if level == 2:
            return ISO14443ACommand.ANTICOLLISION_CL3
        raise ValueError("level argument is out of range")

    def _get_one_iso14443a_card_id(self) -> Iso14443AUniqueId:
        """Get the UID of an ISO 14443-A card using anticollision protocol.

        Returns:
            The card's (UID as bytes, SAK as bytes).

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If the card's response is invalid.
            TimeoutError: If no card responds.
        """
        card_ids = self.get_all_iso14443a_uids(
            wake_up_first=True,
            halt_when_found=False,
            max_cards=1,
        )
        if len(card_ids) == 0:
            raise TimeoutError("No ISO 14443-A card responded")
        return card_ids[0]

    @staticmethod
    def _get_nvb_and_final_bits(
        data_len: int, coll_bit: int
    ) -> tuple[int, int]:
        final_bits = coll_bit % 8
        nvb = ((data_len + 2) << 4) | final_bits
        if final_bits != 0:
            nvb -= 0x10
        return (nvb, final_bits)

    def _send_atqa(self) -> bytes:
        return self._reader.send_and_receive(7, bytes([ISO14443ACommand.WUPA]))

    def _send_select_for_cl(self, cl: int, uid: list[int]) -> bytes:
        bcc = uid[0] ^ uid[1] ^ uid[2] ^ uid[3]
        sak = bytes([uid[0], uid[1], uid[2], uid[3], bcc])
        cmd = self._get_cmd_for_level(cl)
        request = bytes([cmd, ISO14443ACommand.SELECT]) + sak
        sak = self._reader.send_and_receive(0, request)
        return sak

    def _send_select_for_part(self, cl: int, uid_part: list[int]) -> bytes:
        return self._send_select_for_cl(cl, [0x88] + uid_part)

    def get_all_iso14443a_uids(
        self,
        wake_up_first: bool = True,
        halt_when_found: bool = True,
        max_cards: int = 32,
    ) -> list[Iso14443AUniqueId]:
        """Get the UIDs of ISO 14443-A cards using anticollision protocol.

        Cards may be halted after discovery.

        If called again without "wake_up_first", cards that have previously
        been halted might not be found again. It depends on the cards' UIDs
        relative to each other.

        Args:
            wake_up_first: Send WUPA first to wake up halted cards.
            halt_when_found: Send HLTA to found cards.
            max_cards: The maximum number of cards that can be found.

        Returns:
            A list of the card's IDs.

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If the card's response is invalid.
        """
        # pylint: disable=too-many-locals
        # pylint: disable=too-many-statements
        # pylint: disable=too-many-branches
        card_ids: list[Iso14443AUniqueId] = []
        discovery_stack: list[tuple[int, bytes, int, list[int], bool]] = [
            (0, b"", 0, [], True),
        ]
        while len(discovery_stack) > 0:
            cl, mask, coll_bit, uid, restart = discovery_stack.pop()

            if restart:
                self._reader.turn_off_crc()
                self._reader.change_mode_to_transceiver()
                try:
                    cmd: int = ISO14443ACommand.REQA
                    if wake_up_first:
                        cmd = ISO14443ACommand.WUPA
                    atqa_data = self._reader.send_and_receive(7, bytes([cmd]))
                    if len(atqa_data) == 0:
                        # No longer any more cards in the field.
                        return card_ids
                    if len(uid) >= 3:
                        # This isn't tested, I don't have cards that
                        # collide in the second part only
                        self._reader.turn_on_crc()
                        sak = self._send_select_for_part(0, uid)
                        if len(sak) == 0:
                            # It no longer is in the field
                            continue

                    if len(uid) >= 6:
                        # This isn't tested, I don't have cards that
                        # collide in the third part only
                        sak = self._send_select_for_part(1, uid[4:])
                        if len(sak) == 0:
                            # It no longer is in the field
                            continue
                except TimeoutError:
                    # It no longer is in the field
                    continue
                except ValueError as e:
                    print("Got unexpected error:", e)
                    continue

            # ATQA uid length bits: 0 == 4 bytes, 1 == 7 bytes, 2 == 10 bytes

            # Send Anticollision CL X
            self._reader.set_rx_crc_and_first_bit(False, 0)
            self._reader.turn_off_tx_crc()
            cmd = self._get_cmd_for_level(cl)
            nvb, final_bits = self._get_nvb_and_final_bits(len(mask), coll_bit)

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

                new_mask = self._reader.send_and_receive(
                    final_bits,
                    bytes([cmd, nvb]) + mask,
                )

                if len(mask) and len(new_mask):
                    # Combine new_mask and mask...
                    tmp_new_mask = bytearray(mask)
                    tmp_new_mask[-1] |= new_mask[0]
                    if len(new_mask) > 1:
                        tmp_new_mask += new_mask[1:]
                    new_mask = bytes(tmp_new_mask)
            except TimeoutError:
                # It is no longer in the field.
                continue
            except ValueError as e:
                print("Got unexpected error:", e)
                continue
            finally:
                self._reader.set_rx_crc_and_first_bit(True, 0)

            new_coll_bit = self._get_coll_bit()

            if new_coll_bit is None:
                # No collision
                if not self._is_valid_bcc(new_mask):
                    # TODO: Maybe have some maximum retry?
                    # Retry:
                    discovery_stack.append((cl, mask, coll_bit, uid, True))
                    continue

                self._reader.set_rx_crc_and_first_bit(True, 0)
                self._reader.turn_on_tx_crc()
                sak = self._send_select_for_cl(cl, list(new_mask)[0:4])
                if len(sak) == 0:
                    # TODO: Maybe have some maximum retry?
                    discovery_stack.append((cl, mask, coll_bit, uid, True))
                    continue

                # Build UID
                if sak[0] & (1 << 2) == 0:
                    uid.append(new_mask[0])
                uid.append(new_mask[1])
                uid.append(new_mask[2])
                uid.append(new_mask[3])
                if sak[0] & (1 << 2) == 0:
                    # All CL levels completed for this card
                    card_ids.append(Iso14443AUniqueId(bytes(uid), sak))
                    if halt_when_found:
                        self._reader.send_data(
                            0, bytes([ISO14443ACommand.HLTA, 0x00])
                        )
                    if len(card_ids) >= max_cards:
                        return card_ids
                else:
                    # Go to next CL
                    discovery_stack.append((cl + 1, b"", 0, uid, False))
            else:
                # There was a collision
                n_bytes = 1 + (new_coll_bit + 7) // 8
                bit = new_coll_bit % 8

                new_mask = bytearray(new_mask[:n_bytes])

                new_mask[new_coll_bit // 8] |= 1 << bit
                final_bit = (new_coll_bit + 1) % 8
                # Need to restart, another is handled next.
                discovery_stack.append(
                    (cl, bytes(new_mask[:n_bytes]), final_bit, list(uid), True)
                )

                new_mask[new_coll_bit // 8] &= 255 ^ (1 << bit)
                # No need to restart, this is handled next.
                discovery_stack.append(
                    (cl, bytes(new_mask[:n_bytes]), final_bit, uid, False)
                )
        return card_ids

    def iso15693_inventory(
        self,
        slots: int = 16,
        mask_length: int = 0,
        afi: int | None = None,
    ) -> list[Iso15693UniqueId]:
        """Perform ISO 15693 inventory to find tags.

        This method implements the ISO 15693 inventory protocol to discover
        tags in the RF field. It uses 16 slots by default for anticollision.

        Args:
            slots: Number of slots for anticollision (default: 16).
                Must be 16 for mask_length 0.
            mask_length: Length of mask (default: 0).

        Returns:
            List of UniqueIds found.

        Raises:
            PN5180Error: If communication fails.

        Examples:
            >>> with reader.start_session(0x0D, 0x8D) as session:
            ...     card_ids = session.iso15693_inventory()
            ...     for card_id in card_ids:
            ...         print(f"Found UID: {card_id}")
        """
        if not self._active:
            raise RuntimeError("Communication session is no longer active")

        if slots != 16:
            raise NotImplementedError("Slots must currently be 16")

        card_ids = []

        self._reader.turn_on_crc()

        # Set to transceiver mode
        self._reader.change_mode_to_transceiver()

        stored_tx_config = self._reader.read_register(Registers.TX_CONFIG)

        # TODO Set flag according to slots
        self._reader.send_15693_request(
            ISO15693Command.INVENTORY,
            bytes([mask_length]),
            is_inventory=True,
            afi=afi,
        )

        # Loop through all slots
        for _ in range(slots):
            # Read response if available
            rx_status = self._reader.read_register(Registers.RX_STATUS)
            if rx_status:
                how_many_bytes = rx_status & 511
                if how_many_bytes > 0:
                    data = self._reader.read_data(how_many_bytes)
                    # Check if no error flag (bit 0 clear)
                    if len(data) > 0 and (data[0] & 1) == 0:
                        # UID is in bytes 10:1:-1 (reversed)
                        if len(data) >= 10:
                            uid = bytes(data[9:1:-1])
                            card_ids.append(Iso15693UniqueId(uid))

            # Prepare for next slot
            # Clear bit 7, 8 and 11 - only send EOF for next command
            self._reader.write_register_and_mask(
                Registers.TX_CONFIG, 0xFFFFFB3F
            )

            # Set state to TRANSCEIVE
            self._reader.change_mode_to_transceiver()

            # Send EOF
            self._reader.send_data(0, b"")

        self._reader.write_register(Registers.TX_CONFIG, stored_tx_config)

        return card_ids

    def connect_iso15693(self, card_id: Iso15693UniqueId) -> ISO15693Card:
        """Connect to an ISO 15693 card.

        This method selects an ISO 15693 card and returns
        a card object.

        Args: card_id: A unique identifier for the card.

        Returns:
            ISO15693Card object representing the connected card.

        Raises:
            PN5180Error: If communication with the card fails.
            ValueError: If the card's response is invalid.
            TimeoutError: If no card responds.
        """
        if not self._active:
            raise RuntimeError("Communication session is no longer active")

        self._reader.turn_on_crc()
        self._reader.change_mode_to_transceiver()

        _answer = self._reader.send_and_receive_15693(
            ISO15693Command.SELECT, b"", uid=card_id
        )

        return ISO15693Card(self._reader, card_id)

    def close(self) -> None:
        """Close the communication session and turn off RF field."""
        if self._active:
            self._reader.rf_off()
            self._active = False

    def __enter__(self) -> PN5180RFSession:
        """Context manager entry."""
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit."""
        self.close()

    def __del__(self) -> None:
        """Cleanup when object is destroyed."""
        self.close()

__del__()

Cleanup when object is destroyed.

Source code in src/pn5180_tagomatic/session.py
456
457
458
def __del__(self) -> None:
    """Cleanup when object is destroyed."""
    self.close()

__enter__()

Context manager entry.

Source code in src/pn5180_tagomatic/session.py
448
449
450
def __enter__(self) -> PN5180RFSession:
    """Context manager entry."""
    return self

__exit__(exc_type, exc_val, exc_tb)

Context manager exit.

Source code in src/pn5180_tagomatic/session.py
452
453
454
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
    """Context manager exit."""
    self.close()

__init__(reader)

Initialize PN5180RFSession.

Parameters:

Name Type Description Default
reader PN5180Helper

The PN5180 reader instance.

required
Source code in src/pn5180_tagomatic/session.py
26
27
28
29
30
31
32
33
def __init__(self, reader: PN5180Helper) -> None:
    """Initialize PN5180RFSession.

    Args:
        reader: The PN5180 reader instance.
    """
    self._reader = reader
    self._active = True

close()

Close the communication session and turn off RF field.

Source code in src/pn5180_tagomatic/session.py
442
443
444
445
446
def close(self) -> None:
    """Close the communication session and turn off RF field."""
    if self._active:
        self._reader.rf_off()
        self._active = False

connect_iso14443a(card_id)

Select an ISO/IEC 14443A card and return an object representing it.

Source code in src/pn5180_tagomatic/session.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def connect_iso14443a(
    self, card_id: Iso14443AUniqueId
) -> ISO14443ACard | None:
    """
    Select an ISO/IEC 14443A card and return an object representing it.
    """
    # Activate card first.

    # pylint: disable=too-many-return-statements

    self._reader.turn_off_crc()
    self._reader.change_mode_to_transceiver()
    atqa_data = self._send_atqa()
    if len(atqa_data) == 0:
        # No cards left in field.
        return None

    self._reader.turn_on_crc()

    uid = card_id.uid_as_bytes()
    uid_list = list(uid)
    if len(uid) == 4:
        sak = self._send_select_for_cl(0, uid_list)
        if len(sak) == 0:
            return None
    else:
        sak = self._send_select_for_part(0, uid_list[0:3])
        if len(sak) == 0:
            return None

        if len(uid) == 7:
            sak = self._send_select_for_cl(1, uid_list[3:7])
            if len(sak) == 0:
                return None
        else:
            sak = self._send_select_for_part(1, uid_list[3:6])
            if len(sak) == 0:
                return None

        if len(uid) == 10:
            sak = self._send_select_for_cl(2, uid_list[6:])
            if len(sak) == 0:
                return None

    return ISO14443ACard(self._reader, card_id)

connect_iso15693(card_id)

Connect to an ISO 15693 card.

This method selects an ISO 15693 card and returns a card object.

Args: card_id: A unique identifier for the card.

Returns:

Type Description
ISO15693Card

ISO15693Card object representing the connected card.

Raises:

Type Description
PN5180Error

If communication with the card fails.

ValueError

If the card's response is invalid.

TimeoutError

If no card responds.

Source code in src/pn5180_tagomatic/session.py
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def connect_iso15693(self, card_id: Iso15693UniqueId) -> ISO15693Card:
    """Connect to an ISO 15693 card.

    This method selects an ISO 15693 card and returns
    a card object.

    Args: card_id: A unique identifier for the card.

    Returns:
        ISO15693Card object representing the connected card.

    Raises:
        PN5180Error: If communication with the card fails.
        ValueError: If the card's response is invalid.
        TimeoutError: If no card responds.
    """
    if not self._active:
        raise RuntimeError("Communication session is no longer active")

    self._reader.turn_on_crc()
    self._reader.change_mode_to_transceiver()

    _answer = self._reader.send_and_receive_15693(
        ISO15693Command.SELECT, b"", uid=card_id
    )

    return ISO15693Card(self._reader, card_id)

connect_one_iso14443a()

Connect to an ISO 14443-A card.

This method performs the ISO 14443-A anticollision protocol to retrieve the card's UID and returns a card object.

Returns:

Type Description
ISO14443ACard

ISO14443ACard object representing the connected card.

Raises:

Type Description
PN5180Error

If communication with the card fails.

ValueError

If the card's response is invalid.

TimeoutError

If no card responds.

Source code in src/pn5180_tagomatic/session.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
def connect_one_iso14443a(self) -> ISO14443ACard:
    """Connect to an ISO 14443-A card.

    This method performs the ISO 14443-A anticollision protocol to
    retrieve the card's UID and returns a card object.

    Returns:
        ISO14443ACard object representing the connected card.

    Raises:
        PN5180Error: If communication with the card fails.
        ValueError: If the card's response is invalid.
        TimeoutError: If no card responds.
    """
    if not self._active:
        raise RuntimeError("Communication session is no longer active")

    card_id = self._get_one_iso14443a_card_id()
    return ISO14443ACard(self._reader, card_id)

get_all_iso14443a_uids(wake_up_first=True, halt_when_found=True, max_cards=32)

Get the UIDs of ISO 14443-A cards using anticollision protocol.

Cards may be halted after discovery.

If called again without "wake_up_first", cards that have previously been halted might not be found again. It depends on the cards' UIDs relative to each other.

Parameters:

Name Type Description Default
wake_up_first bool

Send WUPA first to wake up halted cards.

True
halt_when_found bool

Send HLTA to found cards.

True
max_cards int

The maximum number of cards that can be found.

32

Returns:

Type Description
list[Iso14443AUniqueId]

A list of the card's IDs.

Raises:

Type Description
PN5180Error

If communication with the card fails.

ValueError

If the card's response is invalid.

Source code in src/pn5180_tagomatic/session.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def get_all_iso14443a_uids(
    self,
    wake_up_first: bool = True,
    halt_when_found: bool = True,
    max_cards: int = 32,
) -> list[Iso14443AUniqueId]:
    """Get the UIDs of ISO 14443-A cards using anticollision protocol.

    Cards may be halted after discovery.

    If called again without "wake_up_first", cards that have previously
    been halted might not be found again. It depends on the cards' UIDs
    relative to each other.

    Args:
        wake_up_first: Send WUPA first to wake up halted cards.
        halt_when_found: Send HLTA to found cards.
        max_cards: The maximum number of cards that can be found.

    Returns:
        A list of the card's IDs.

    Raises:
        PN5180Error: If communication with the card fails.
        ValueError: If the card's response is invalid.
    """
    # pylint: disable=too-many-locals
    # pylint: disable=too-many-statements
    # pylint: disable=too-many-branches
    card_ids: list[Iso14443AUniqueId] = []
    discovery_stack: list[tuple[int, bytes, int, list[int], bool]] = [
        (0, b"", 0, [], True),
    ]
    while len(discovery_stack) > 0:
        cl, mask, coll_bit, uid, restart = discovery_stack.pop()

        if restart:
            self._reader.turn_off_crc()
            self._reader.change_mode_to_transceiver()
            try:
                cmd: int = ISO14443ACommand.REQA
                if wake_up_first:
                    cmd = ISO14443ACommand.WUPA
                atqa_data = self._reader.send_and_receive(7, bytes([cmd]))
                if len(atqa_data) == 0:
                    # No longer any more cards in the field.
                    return card_ids
                if len(uid) >= 3:
                    # This isn't tested, I don't have cards that
                    # collide in the second part only
                    self._reader.turn_on_crc()
                    sak = self._send_select_for_part(0, uid)
                    if len(sak) == 0:
                        # It no longer is in the field
                        continue

                if len(uid) >= 6:
                    # This isn't tested, I don't have cards that
                    # collide in the third part only
                    sak = self._send_select_for_part(1, uid[4:])
                    if len(sak) == 0:
                        # It no longer is in the field
                        continue
            except TimeoutError:
                # It no longer is in the field
                continue
            except ValueError as e:
                print("Got unexpected error:", e)
                continue

        # ATQA uid length bits: 0 == 4 bytes, 1 == 7 bytes, 2 == 10 bytes

        # Send Anticollision CL X
        self._reader.set_rx_crc_and_first_bit(False, 0)
        self._reader.turn_off_tx_crc()
        cmd = self._get_cmd_for_level(cl)
        nvb, final_bits = self._get_nvb_and_final_bits(len(mask), coll_bit)

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

            new_mask = self._reader.send_and_receive(
                final_bits,
                bytes([cmd, nvb]) + mask,
            )

            if len(mask) and len(new_mask):
                # Combine new_mask and mask...
                tmp_new_mask = bytearray(mask)
                tmp_new_mask[-1] |= new_mask[0]
                if len(new_mask) > 1:
                    tmp_new_mask += new_mask[1:]
                new_mask = bytes(tmp_new_mask)
        except TimeoutError:
            # It is no longer in the field.
            continue
        except ValueError as e:
            print("Got unexpected error:", e)
            continue
        finally:
            self._reader.set_rx_crc_and_first_bit(True, 0)

        new_coll_bit = self._get_coll_bit()

        if new_coll_bit is None:
            # No collision
            if not self._is_valid_bcc(new_mask):
                # TODO: Maybe have some maximum retry?
                # Retry:
                discovery_stack.append((cl, mask, coll_bit, uid, True))
                continue

            self._reader.set_rx_crc_and_first_bit(True, 0)
            self._reader.turn_on_tx_crc()
            sak = self._send_select_for_cl(cl, list(new_mask)[0:4])
            if len(sak) == 0:
                # TODO: Maybe have some maximum retry?
                discovery_stack.append((cl, mask, coll_bit, uid, True))
                continue

            # Build UID
            if sak[0] & (1 << 2) == 0:
                uid.append(new_mask[0])
            uid.append(new_mask[1])
            uid.append(new_mask[2])
            uid.append(new_mask[3])
            if sak[0] & (1 << 2) == 0:
                # All CL levels completed for this card
                card_ids.append(Iso14443AUniqueId(bytes(uid), sak))
                if halt_when_found:
                    self._reader.send_data(
                        0, bytes([ISO14443ACommand.HLTA, 0x00])
                    )
                if len(card_ids) >= max_cards:
                    return card_ids
            else:
                # Go to next CL
                discovery_stack.append((cl + 1, b"", 0, uid, False))
        else:
            # There was a collision
            n_bytes = 1 + (new_coll_bit + 7) // 8
            bit = new_coll_bit % 8

            new_mask = bytearray(new_mask[:n_bytes])

            new_mask[new_coll_bit // 8] |= 1 << bit
            final_bit = (new_coll_bit + 1) % 8
            # Need to restart, another is handled next.
            discovery_stack.append(
                (cl, bytes(new_mask[:n_bytes]), final_bit, list(uid), True)
            )

            new_mask[new_coll_bit // 8] &= 255 ^ (1 << bit)
            # No need to restart, this is handled next.
            discovery_stack.append(
                (cl, bytes(new_mask[:n_bytes]), final_bit, uid, False)
            )
    return card_ids

iso15693_inventory(slots=16, mask_length=0, afi=None)

Perform ISO 15693 inventory to find tags.

This method implements the ISO 15693 inventory protocol to discover tags in the RF field. It uses 16 slots by default for anticollision.

Parameters:

Name Type Description Default
slots int

Number of slots for anticollision (default: 16). Must be 16 for mask_length 0.

16
mask_length int

Length of mask (default: 0).

0

Returns:

Type Description
list[Iso15693UniqueId]

List of UniqueIds found.

Raises:

Type Description
PN5180Error

If communication fails.

Examples:

>>> with reader.start_session(0x0D, 0x8D) as session:
...     card_ids = session.iso15693_inventory()
...     for card_id in card_ids:
...         print(f"Found UID: {card_id}")
Source code in src/pn5180_tagomatic/session.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def iso15693_inventory(
    self,
    slots: int = 16,
    mask_length: int = 0,
    afi: int | None = None,
) -> list[Iso15693UniqueId]:
    """Perform ISO 15693 inventory to find tags.

    This method implements the ISO 15693 inventory protocol to discover
    tags in the RF field. It uses 16 slots by default for anticollision.

    Args:
        slots: Number of slots for anticollision (default: 16).
            Must be 16 for mask_length 0.
        mask_length: Length of mask (default: 0).

    Returns:
        List of UniqueIds found.

    Raises:
        PN5180Error: If communication fails.

    Examples:
        >>> with reader.start_session(0x0D, 0x8D) as session:
        ...     card_ids = session.iso15693_inventory()
        ...     for card_id in card_ids:
        ...         print(f"Found UID: {card_id}")
    """
    if not self._active:
        raise RuntimeError("Communication session is no longer active")

    if slots != 16:
        raise NotImplementedError("Slots must currently be 16")

    card_ids = []

    self._reader.turn_on_crc()

    # Set to transceiver mode
    self._reader.change_mode_to_transceiver()

    stored_tx_config = self._reader.read_register(Registers.TX_CONFIG)

    # TODO Set flag according to slots
    self._reader.send_15693_request(
        ISO15693Command.INVENTORY,
        bytes([mask_length]),
        is_inventory=True,
        afi=afi,
    )

    # Loop through all slots
    for _ in range(slots):
        # Read response if available
        rx_status = self._reader.read_register(Registers.RX_STATUS)
        if rx_status:
            how_many_bytes = rx_status & 511
            if how_many_bytes > 0:
                data = self._reader.read_data(how_many_bytes)
                # Check if no error flag (bit 0 clear)
                if len(data) > 0 and (data[0] & 1) == 0:
                    # UID is in bytes 10:1:-1 (reversed)
                    if len(data) >= 10:
                        uid = bytes(data[9:1:-1])
                        card_ids.append(Iso15693UniqueId(uid))

        # Prepare for next slot
        # Clear bit 7, 8 and 11 - only send EOF for next command
        self._reader.write_register_and_mask(
            Registers.TX_CONFIG, 0xFFFFFB3F
        )

        # Set state to TRANSCEIVE
        self._reader.change_mode_to_transceiver()

        # Send EOF
        self._reader.send_data(0, b"")

    self._reader.write_register(Registers.TX_CONFIG, stored_tx_config)

    return card_ids

RegisterOperation

Bases: IntEnum

Register write operations for write_register_multiple.

Source code in src/pn5180_tagomatic/constants.py
75
76
77
78
79
80
class RegisterOperation(IntEnum):
    """Register write operations for write_register_multiple."""

    SET = 1
    OR = 2
    AND = 3

Registers

Bases: IntEnum

PN5180 register addresses.

Source code in src/pn5180_tagomatic/constants.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
class Registers(IntEnum):
    """PN5180 register addresses."""

    SYSTEM_CONFIG = 0
    IRQ_ENABLE = 1
    IRQ_STATUS = 2
    IRQ_CLEAR = 3
    TRANSCEIVER_CONFIG = 4
    PADCONFIG = 5
    PADOUT = 7
    TIMER0_STATUS = 8
    TIMER1_STATUS = 9
    TIMER2_STATUS = 10
    TIMER0_RELOAD = 11
    TIMER1_RELOAD = 12
    TIMER2_RELOAD = 13
    TIMER0_CONFIG = 14
    TIMER1_CONFIG = 15
    TIMER2_CONFIG = 16
    RX_WAIT_CONFIG = 17
    CRC_RX_CONFIG = 18
    RX_STATUS = 19
    TX_UNDERSHOOT_CONFIG = 20
    TX_OVERSHOOT_CONFIG = 21
    TX_DATA_MOD = 22
    TX_WAIT_CONFIG = 23
    TX_CONFIG = 24
    CRC_TX_CONFIG = 25
    SIGPRO_CONFIG = 26
    SIGPRO_CM_CONFIG = 27
    SIGPRO_RM_CONFIG = 28
    RF_STATUS = 29
    AGC_CONFIG = 30
    AGC_VALUE = 31
    RF_CONTROL_TX = 32
    RF_CONTROL_TX_CLK = 33
    RF_CONTROL_RX = 34
    LD_CONTROL = 35
    SYSTEM_STATUS = 36
    TEMP_CONTROL = 37
    CECK_CARD_RESULT = 38
    DPC_CONFIG = 39
    EMD_CONTROL = 40
    ANT_CONTROL = 41

SwitchMode

Bases: IntEnum

PN5180 operating modes.

Source code in src/pn5180_tagomatic/constants.py
83
84
85
86
87
88
class SwitchMode(IntEnum):
    """PN5180 operating modes."""

    STANDBY = 0
    LPCD = 1
    AUTOCOLL = 2

TimeslotBehavior

Bases: IntEnum

EPC inventory timeslot behavior options.

Source code in src/pn5180_tagomatic/constants.py
91
92
93
94
95
96
class TimeslotBehavior(IntEnum):
    """EPC inventory timeslot behavior options."""

    MAX_TIMESLOTS = 0  # Response contains max. number of time slots
    SINGLE_TIMESLOT = 1  # Response contains only one timeslot
    SINGLE_WITH_HANDLE = 2  # Single timeslot with card handle if valid

UniqueId

Bases: Protocol

Represents a cards unique identifier. Subtypes contain type specific extensions.

Source code in src/pn5180_tagomatic/cards.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
class UniqueId(Protocol):
    """Represents a cards unique identifier.
    Subtypes contain type specific extensions.
    """

    def uid_as_bytes(self) -> bytes:
        """Returns the UID as bytes"""

    def uid_as_string(self) -> str:
        """Returns the UID as a string"""

    def __str__(self) -> str:
        """Returns a printable representation"""

__str__()

Returns a printable representation

Source code in src/pn5180_tagomatic/cards.py
20
21
def __str__(self) -> str:
    """Returns a printable representation"""

uid_as_bytes()

Returns the UID as bytes

Source code in src/pn5180_tagomatic/cards.py
14
15
def uid_as_bytes(self) -> bytes:
    """Returns the UID as bytes"""

uid_as_string()

Returns the UID as a string

Source code in src/pn5180_tagomatic/cards.py
17
18
def uid_as_string(self) -> str:
    """Returns the UID as a string"""