Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the OS, Python version, and other tools you might need
build:
os: ubuntu-24.04
tools:
python: "3.13"

mkdocs:
configuration: mkdocs.yml
fail_on_warning: false

python:
install:
- method: pip
path: .
extra_requirements:
- docs
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,25 @@ $(VENV)/.timestamp-dev: $(VENV)/bin/activate pyproject.toml
$(PIP) install -e ".[dev]"
touch $@

# Install package with docs dependencies
$(VENV)/.timestamp-docs: $(VENV)/bin/activate pyproject.toml
$(PIP) install -e ".[docs]"
touch $@

.PHONY: install-dev
install-dev: $(VENV)/.timestamp-dev

.PHONY: install-docs
install-dev: $(VENV)/.timestamp-docs

.PHONY: test
test: install-dev
$(PYTEST)

.PHONY: docs
docs: install-docs
mkdocs build

.PHONY: lint
lint: install-dev
$(RUFF) check src/ tests/
Expand All @@ -66,6 +78,7 @@ clean:
rm -rf $(VENV)
rm -rf build/
rm -rf dist/
rm -rf site/
rm -rf *.egg-info
rm -rf .pytest_cache
rm -rf .ruff_cache
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ Frontend module and a Raspberry Pi Pico (Zero).
- Python library for easy integration.
- Uses USB serial communication to the reader.
- Cross-platform support (Linux, Windows, macOS).
- Finds and selects single ISO/IEC 14443 cards.
- Uses NFC FORUM commands to read/write 14443-A cards' memories.
- Finds and selects ISO/IEC 14443A and ISO/IEC 15693 cards.
- Can read/write the cards' memories.
- Can authenticate against Mifare classic cards to read their memories.
- Finds ISO/IEC 15693 cards, uses 15693-3 commands to read/write their memories.
- Supports multiple cards within the field.

Multiple cards can be detected within the field.
## API Documentation

See [API Reference](https://pn5180-tagomatic.readthedocs.io/en/latest/).

## Installation

Expand Down
7 changes: 6 additions & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ SPDX-FileCopyrightText = "2026 PN5180-tagomatic contributors"
SPDX-License-Identifier = "GPL-3.0-or-later"

[[annotations]]
path = [".gitignore", ".clang-format"]
path = [
".gitignore",
".clang-format",
".readthedocs.yaml",
"docs/reference.md"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 PN5180-tagomatic contributors"
SPDX-License-Identifier = "CC0-1.0"
Expand Down
74 changes: 74 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<!--
SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
SPDX-License-Identifier: GPL-3.0-or-later
-->

# PN5180 Tagomatic
A USB connected RFID Reader/Writer with a python interface.

---

A PN5180 card is connected to a Raspberry Pi Pico Zero via SPI.
The Pico Zero runs an arduino firmware which exposes a SimpleRPC
interface which this python module uses via USB to communicate
with RFID tags.

## Features

- Python library for easy integration.
- Uses USB serial communication to the reader.
- Cross-platform support (Linux, Windows, macOS).
- Finds and selects ISO/IEC 14443A and ISO/IEC 15693 cards.
- Can read/write the cards' memories.
- Can authenticate against Mifare classic cards to read their memories.
- Supports multiple cards within the field.

## API Documentation

See [API Reference](reference.md).

## Installation

### Python Package

Install from PyPI:

```bash
pip install pn5180-tagomatic
```

### Building the hardware

See
[here](https://github.com/bofh69/pn5180-tagomatic/tree/main/sketch/README.md)
for instructions on the hardware and the firmware.

## Usage

```python
from pn5180_tagomatic import PN5180, RxProtocol, TxProtocol

# Create reader instance and use it
with PN5180("/dev/ttyACM0") as reader:
versions = reader.ll.read_eeprom(0x10, 6)
with reader.start_session(
TxProtocol.ISO_14443_A_106, RxProtocol.ISO_14443_A_106
) as session:
card = session.connect_one_iso14443a()
print(f"Reading from card {card.id}")
memory = card.read_memory()
```

## License

This project is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later).

## Contributing

Contributions are welcome!

## Acknowledgments

This project uses FastLED by Daniel Garcia et al.

SimpleRPC by Jeroen F.J. Laros, Chris Flesher et al is also used.
1 change: 1 addition & 0 deletions docs/reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: src.pn5180_tagomatic
18 changes: 18 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
# SPDX-License-Identifier: CC0-1.0

site_name: PN5180 Tagomatic
repo_url: https://github.com/bofh69/pn5180-tagomatic

theme:
name: "material"
palette:
scheme: slate

plugins:
- search
- mkdocstrings

nav:
- Home: 'README.md'
- API Reference: 'reference.md'
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ dev = [
"types-pyserial>=3.5.0",
]

docs = [
"mkdocs>=1.6.1",
"mkdocs-material>=9.7.1",
"mkdocs-material-extensions>=1.3.1",
"mkdocstrings-python>=2.0.1",
]

[project.urls]
Homepage = "https://github.com/bofh69/PN5180-tagomatic"
Repository = "https://github.com/bofh69/PN5180-tagomatic"
Expand Down
2 changes: 1 addition & 1 deletion src/pn5180_tagomatic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2026 PN5180-tagomatic contributors
# SPDX-License-Identifier: GPL-3.0-or-later

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

from .cards import (
Card,
Expand Down
25 changes: 15 additions & 10 deletions src/pn5180_tagomatic/pn5180.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,21 @@ class PN5180:
Attributes:
ll: Low-level PN5180 interface for direct hardware access.

Example:
>>> from pn5180_tagomatic import PN5180
Examples:
>>> from pn5180_tagomatic import *
>>> with PN5180("/dev/ttyACM0") as reader:
... # High-level API (recommended)
... with reader.start_session(0x00, 0x80) as comm:
... card = comm.connect_one_iso14443a()
... memory = card.read_memory()
... # 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 if needed
... reader.ll.write_register(addr, value)
... # 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:
Expand Down Expand Up @@ -66,11 +71,11 @@ def start_session(
Raises:
PN5180Error: If the operation fails.

Example:
Examples:
>>> reader = PN5180("/dev/ttyACM0")
>>> with reader.start_session(0x00, 0x80) as comm:
... card = comm.connect_one_iso14443a()
... uid = card.uid
... uid = card.id.uid_as_bytes()
... memory = card.read_memory()
"""
self.ll.load_rf_config(tx_config, rx_config)
Expand Down
7 changes: 5 additions & 2 deletions src/pn5180_tagomatic/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class PN5180Proxy: # pylint: disable=too-many-public-methods
Args:
tty: The tty device path to communicate via.

Example:
Examples:
>>> from pn5180_tagomatic import PN5180Proxy
>>> reader = PN5180Proxy("/dev/ttyACM0")
>>> reader.reset()
Expand Down Expand Up @@ -681,8 +681,10 @@ def send_and_receive(self, bits: int, data: bytes) -> bytes:

return self.read_received_data()

# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments
def send_15693_request(
self, # pylint: disable=too-many-arguments
self,
command: int,
parameters: bytes,
is_inventory: bool = False,
Expand Down Expand Up @@ -711,6 +713,7 @@ def send_15693_request(
PN5180Error: If communication fails.
ValueError: Incorrect parameters to function.
"""
# pylint: disable=too-many-branches

self._validate_uint8(command, "command")

Expand Down
10 changes: 4 additions & 6 deletions src/pn5180_tagomatic/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def get_all_iso14443a_uids(
(0, b"", 0, [], True),
]
while len(discovery_stack) > 0:
(cl, mask, coll_bit, uid, restart) = discovery_stack.pop()
cl, mask, coll_bit, uid, restart = discovery_stack.pop()

if restart:
self._reader.turn_off_crc()
Expand Down Expand Up @@ -246,9 +246,7 @@ def get_all_iso14443a_uids(
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
)
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)
Expand Down Expand Up @@ -353,11 +351,11 @@ def iso15693_inventory(
Raises:
PN5180Error: If communication fails.

Example:
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.uid_to_string()}")
... print(f"Found UID: {card_id}")
"""
if not self._active:
raise RuntimeError("Communication session is no longer active")
Expand Down