From 48e800adc1ce544b52f2b55c7e36c9f4d5d96bce Mon Sep 17 00:00:00 2001 From: scheianu Date: Mon, 5 Oct 2026 11:46:46 +0300 Subject: [PATCH] Initializare dupa curatare folder --- .gitignore | 12 + LAUNCH.md | 375 ++++++++++++ LICENSE | 21 + README.md | 532 ++++++++++++++++++ deploy/99-rdm-reader.rules | 28 + deploy/install.sh | 261 +++++++++ deploy/rdm-card-reader.service | 56 ++ pyproject.toml | 38 ++ src/rdm_card_reader/__init__.py | 63 +++ src/rdm_card_reader/__main__.py | 8 + src/rdm_card_reader/bootstrap.py | 312 ++++++++++ src/rdm_card_reader/bridge.py | 503 +++++++++++++++++ src/rdm_card_reader/card.py | 135 +++++ src/rdm_card_reader/cli.py | 382 +++++++++++++ src/rdm_card_reader/demo.py | 49 ++ src/rdm_card_reader/led.py | 314 +++++++++++ src/rdm_card_reader/ports.py | 397 +++++++++++++ src/rdm_card_reader/py.typed | 0 src/rdm_card_reader/reader.py | 129 +++++ src/rdm_card_reader/wheels/SHA256SUMS | 4 + .../wheels/colorzero-2.0-py2.py3-none-any.whl | Bin 0 -> 26573 bytes .../gpiozero-2.0.1.post3-py3-none-any.whl | Bin 0 -> 151465 bytes .../wheels/pyserial-3.5-py2.py3-none-any.whl | Bin 0 -> 90585 bytes .../wheels/rdm6300-0.1.1-py3-none-any.whl | Bin 0 -> 7217 bytes tests/__init__.py | 0 tests/conftest.py | 8 + tests/test_bootstrap.py | 201 +++++++ tests/test_bridge.py | 501 +++++++++++++++++ tests/test_card.py | 111 ++++ tests/test_cli.py | 93 +++ tests/test_deploy.py | 199 +++++++ tests/test_e2e_bridge.py | 177 ++++++ tests/test_e2e_led.py | 209 +++++++ tests/test_e2e_pty.py | 165 ++++++ tests/test_led.py | 306 ++++++++++ tests/test_ports.py | 228 ++++++++ tests/test_protocol.py | 108 ++++ tests/test_reader.py | 149 +++++ tests/test_wheels.py | 229 ++++++++ 39 files changed, 6303 insertions(+) create mode 100644 .gitignore create mode 100644 LAUNCH.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 deploy/99-rdm-reader.rules create mode 100755 deploy/install.sh create mode 100644 deploy/rdm-card-reader.service create mode 100644 pyproject.toml create mode 100644 src/rdm_card_reader/__init__.py create mode 100644 src/rdm_card_reader/__main__.py create mode 100644 src/rdm_card_reader/bootstrap.py create mode 100644 src/rdm_card_reader/bridge.py create mode 100644 src/rdm_card_reader/card.py create mode 100644 src/rdm_card_reader/cli.py create mode 100644 src/rdm_card_reader/demo.py create mode 100644 src/rdm_card_reader/led.py create mode 100644 src/rdm_card_reader/ports.py create mode 100644 src/rdm_card_reader/py.typed create mode 100644 src/rdm_card_reader/reader.py create mode 100644 src/rdm_card_reader/wheels/SHA256SUMS create mode 100644 src/rdm_card_reader/wheels/colorzero-2.0-py2.py3-none-any.whl create mode 100644 src/rdm_card_reader/wheels/gpiozero-2.0.1.post3-py3-none-any.whl create mode 100644 src/rdm_card_reader/wheels/pyserial-3.5-py2.py3-none-any.whl create mode 100644 src/rdm_card_reader/wheels/rdm6300-0.1.1-py3-none-any.whl create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_bootstrap.py create mode 100644 tests/test_bridge.py create mode 100644 tests/test_card.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_deploy.py create mode 100644 tests/test_e2e_bridge.py create mode 100644 tests/test_e2e_led.py create mode 100644 tests/test_e2e_pty.py create mode 100644 tests/test_led.py create mode 100644 tests/test_ports.py create mode 100644 tests/test_protocol.py create mode 100644 tests/test_reader.py create mode 100644 tests/test_wheels.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..218f8d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.venv/ +venv/ +.pytest_cache/ +.coverage +htmlcov/ +.idea/ +.vscode/ diff --git a/LAUNCH.md b/LAUNCH.md new file mode 100644 index 0000000..71c84f6 --- /dev/null +++ b/LAUNCH.md @@ -0,0 +1,375 @@ +# LAUNCH.md — starting the app, and why it fails on this host + +## TL;DR + +**This host (`development`, x86_64) has no card reader attached. Failure is +expected and correct.** To see the app work without hardware, use `--demo`: + +```bash +.venv/bin/python -m rdm_card_reader --demo +``` + +To see the reader path end-to-end without hardware, drive a pseudo-terminal +pretending to be an RDM6300 — see **Testing the reader path without a reader** +below. + +--- + +## Quick start + +Build the virtualenv once, from the wheels bundled in the source tree — no +network needed: + +```bash +python3 -m venv .venv +.venv/bin/pip install --no-index \ + --find-links src/rdm_card_reader/wheels rdm6300 pyserial +echo "$PWD/src" > "$(.venv/bin/python -c \ + 'import sysconfig; print(sysconfig.get_paths()["purelib"])')/rdm_card_reader_src.pth" +``` + +For the status LED, add `gpiozero colorzero` to the same `pip install` line. + +Then, from the project root: + +```bash +.venv/bin/python -m rdm_card_reader --demo # simulate a card, no hardware +.venv/bin/python -m rdm_card_reader --list-ports # what serial ports exist +.venv/bin/python -m rdm_card_reader # real reader (fails here) +.venv/bin/python -m rdm_card_reader --serve # HTTP bridge for a web page +``` + +That is the whole install: the runtime libraries come from the bundled wheels +and `src/` is on the path via the `.pth` file. `pip install .` also works, but +only with network — it builds the project. See **Troubleshooting** below. + +--- + +## Troubleshooting install + +### "Could not find a version that satisfies the requirement setuptools>=68" + +Seen when running `deploy/install.sh` on a Pi with no internet, at the +"Building the virtual environment" step: + +``` +ERROR: Could not find a version that satisfies the requirement setuptools>=68 + (from versions: none) +ERROR: No matching distribution found for setuptools>=68 +``` + +**Cause:** `pip install .` builds the project. pip creates an isolated build +environment and downloads the *build* backend (`setuptools`) into it — even +though the *runtime* libraries are already sitting in the wheel directory. With +no network that download cannot happen. + +**It is not** a problem with the bundled wheels, and not a Python version +mismatch. Two separate confusions produce a similar-looking error: + +| Situation | Error | +| --- | --- | +| Build isolation, no network | `Could not find a version ... setuptools>=68` | +| `--no-build-isolation`, and setuptools absent | `Cannot import 'setuptools.build_meta'` | + +Note that Python 3.12+ venvs no longer include `setuptools`, so the second case +is increasingly common. + +**Fix:** the current `install.sh` avoids the build entirely — it installs the +bundled runtime wheels and puts `src/` on the path with a `.pth` file. If you +are on an older copy, update it (`git pull`) or run these by hand on the Pi: + +```bash +python3 -m venv .venv +.venv/bin/pip install --no-index \ + --find-links src/rdm_card_reader/wheels rdm6300 pyserial +SITE=$(.venv/bin/python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])') +echo "$PWD/src" > "$SITE/rdm_card_reader_src.pth" +.venv/bin/python -m rdm_card_reader --demo # verify, no hardware needed +``` + +No build step, no network, and the same result. + +### "Could not create the virtualenv" + +On Debian and Raspberry Pi OS, `python3 -m venv` needs the `python3-venv` +package: + +```bash +sudo apt-get install python3-venv +``` + +### apt-get failing on the first step + +`apt-get update` and `apt-get install` need the network. The installer now +treats both as best-effort and continues, because a device typically already +has `python3`. If they fail and something really is missing, install it while +online and re-run. + +--- + +## What happens on this host, and why + +### `--list-ports` works fine + +```console +$ .venv/bin/python -m rdm_card_reader --list-ports --verbose-ports +Candidate serial ports (tried in this order): + OK /dev/ttyS0 + --- /dev/ttyAMA0 + --- /dev/ttyUSB0 + --- /dev/ttyACM0 + +Identified serial ports (best candidate first): + [score 5] /dev/ttyS0 :: n/a + by-id : (none) + by-path : (none) + location : (unknown) + usb id : (not USB) + driver : (unknown) +``` + +Two things to read from this: + +* `OK /dev/ttyS0` — the device node **exists**, but existence is not proof of a + working reader. This container has a UART node with no hardware behind it. +* `score 5` and `(not USB)` — nothing here looks like a USB-serial adapter. + A real reader scores 60–100 and shows a chip name (`CH340`, `FT232R`, …). + +### Starting the reader fails + +```console +$ .venv/bin/python -m rdm_card_reader +-> Opening /dev/ttyS0 ... +-> Opening /dev/ttyAMA0 ... +-> Opening /dev/ttyUSB0 ... +-> Opening /dev/ttyACM0 ... + x /dev/ttyS0: Could not configure port: (5, 'Input/output error') + x /dev/ttyAMA0: device not found + x /dev/ttyUSB0: device not found + x /dev/ttyACM0: device not found + +x Could not open any serial port. Check: + 1. the reader is connected (rdm-card-reader --list-ports) + 2. UART is enabled: sudo raspi-config -> Interface Options -> Serial + 3. the user is in dialout: sudo usermod -aG dialout $USER +``` + +This is the **correct** outcome. Note the distinct error for `/dev/ttyS0`: +`Input/output error` (errno 5) means the node exists but the underlying hardware +does not answer — different from `device not found` for the ports that do not +exist at all. The app reports which is which rather than a generic failure. + +### `--autodetect` also fails, as designed + +With no USB adapter present there is nothing to autodetect, so it falls through +to the default port order and fails the same way. Crucially, it **refuses to +guess** when several adapters *are* present (`exit 3`) — picking the wrong +reader means authenticating the wrong person, so ambiguity is an error rather +than a coin flip. + +### `--demo` works, because it touches no hardware + +```console +$ .venv/bin/python -m rdm_card_reader --demo --demo-count 1 +Demo mode - no hardware is used, values are simulated. + +---------------------------------------------------------- + CARD INSERTED - 2026-10-01 15:53:10 + id : 439041101 (0x1A2B3C4D) + type : 0x0F + checksum : 79 + valid : True + port : demo +---------------------------------------------------------- + CARD REMOVED - 2026-10-01 15:53:11 id: 439041101 (0x1A2B3C4D) + +Demo finished. +``` + +`--demo` needs neither the hardware nor the `rdm6300` library, so it is the +fastest way to confirm the build is sane. + +--- + +## How the app starts + +```mermaid +flowchart TD + A["rdm-card-reader
(console script → cli:main)"] --> B{which mode?} + B -->|--list-ports| P["print ports, exit 0"] + B -->|--demo| D["simulated cards, no hardware"] + B -->|--serve or plain| C["1. ensure libraries"] + C --> C1{"rdm6300 importable?"} + C1 -->|yes| E + C1 -->|no| C2["install from bundled wheels
(--no-index, SHA256 verified)"] + C2 --> C3["reload_library()
re-import rdm6300"] + C3 -->|still missing| X2["exit 2"] + C3 -->|ok| E["2. resolve the serial port"] + E --> E1{"--port given?"} + E1 -->|yes| E2["match it; refuse if ambiguous
→ exit 3"] + E1 -->|no| E3["--autodetect: only a single USB adapter
else fall back to default order"] + E2 --> F + E3 --> F["3. open the port"] + F -->|all ports fail| X1["exit 1"] + F --> G["4. reader.start() — blocking read loop"] + G --> H{"mode"} + H -->|plain| I["print each card event"] + H -->|--serve| J["latch the tap in the EventHub"] + J --> K["HTTP server on 127.0.0.1:8765"] + K --> L["web page polls /api/card
and claims it once"] +``` + +The numbered stages, and where each lives: + +| # | Stage | Code | On failure | +| --- | --- | --- | --- | +| 1 | Ensure `rdm6300` + `pyserial` | `bootstrap.ensure_dependencies()` | `exit 2` | +| 2 | Resolve which serial port | `ports.resolve()` | `exit 1` (none), `exit 3` (ambiguous) | +| 3 | Open the port | `reader.open_reader()` | `exit 1` | +| 4 | Read frames forever | `rdm6300.BaseReader.start()` | — | +| 5 | Publish events | `card.format_event()` or `bridge.EventHub` | — | + +Stages 1 and 2 are deliberately **fail-fast and loud**. A reader that starts up +having silently picked the wrong port, or without its library, is worse than one +that refuses to start — the first failure mode is invisible until someone +authenticates as the wrong person. + +### Stage 1 in detail: dependency install + +The `rdm6300` and `pyserial` wheels are **shipped inside the package** +(`rdm_card_reader/wheels/`), so this works on a device with no internet: + +```console +Reader library not found - checking dependencies... +Missing reader dependencies: rdm6300 +Using the wheels bundled with this package: + .../site-packages/rdm_card_reader/wheels +Installing from .../rdm_card_reader/wheels +Dependencies ready: rdm6300 +``` + +Nothing is installed when the libraries are already importable, so a normal +start does no network work and no `pip` call at all. The install uses +`--no-index` and verifies `SHA256SUMS` first; a wheel that fails its checksum is +**refused rather than installed**. + +`reload_library()` matters here: the module-level `import rdm6300` already ran +and failed at process start, so without re-importing, a successful install would +still look like a missing library. + +### Stage 4 in detail: reading is not polling + +The reader loop **blocks** on serial reads and the library calls back when it +assembles a valid frame. Nothing here polls. The *web page* polls (or subscribes +to SSE) — a different thing, and worth keeping straight. + +A card is reported on insert and again on removal; the library de-duplicates +repeated reads while the card sits on the antenna, so you do not get a stream of +repeats. Removal is detected by a `--heartbeat` (default `0.5s`) of silence. + +--- + +## Exit codes + +| Code | Meaning | Seen on this host? | +| --- | --- | --- | +| `0` | Ran normally, or stopped with Ctrl+C | with `--demo`, `--list-ports` | +| `1` | No serial port could be opened | **yes** — this is the expected result here | +| `2` | `rdm6300` missing and could not be installed | only if offline *and* wheels tampered | +| `3` | Port selector matched several adapters (refused to guess) | needs 2+ adapters | + +> When testing with a pipe, remember `head` closes the pipe early and the shell +> then reports `128+SIGPIPE`. Redirect to a file instead if you want the real code: +> `… > out.txt 2>&1; echo $?` + +--- + +## When a reader *is* attached + +On the Raspberry Pi the same command should end with a ready line rather than an +error: + +```console +-> Using /dev/rdm-reader (matched /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0) + OK reader on /dev/rdm-reader ; bridge on http://127.0.0.1:8765 (mode: login) +``` + +Then compare this host against the Pi: + +| | this host (`development`) | Raspberry Pi with reader | +| --- | --- | --- | +| Architecture | x86_64 | armv7l / aarch64 | +| Reader | none | RDM6300 on USB-serial or UART | +| `/dev/ttyUSB*` | absent | present | +| `--list-ports` score | 5, `(not USB)` | 60–100, e.g. `CH340 1a86:7523` | +| `/dev/rdm-reader` | absent | udev symlink | +| Serial access group | n/a | user must be in `dialout` | +| Expected `rdm-card-reader` | `exit 1` | stays running | + +For a real device, `deploy/install.sh` sets all of this up: + +```bash +sudo ./deploy/install.sh --origin https://your.web.app +journalctl -u rdm-card-reader -f +curl http://127.0.0.1:8765/api/card # tap a card first +``` + +The installer needs no internet: the runtime libraries ship inside the +source tree (`src/rdm_card_reader/wheels/`). + +### Copying the project to the Pi + +`install.sh` resolves its source directory from its own location, so copy the +whole tree (including `src/` — that is where the wheels live) and run it from +there: + +```bash +# from this machine +scp -r rdm-card-reader pi@192.168.0.123:~/Desktop/ + +# on the Pi +cd ~/Desktop/rdm-card-reader +sudo ./deploy/install.sh --origin https://your.web.app +``` + +If you transferred only a subset of the tree, `--dir` can point the install +somewhere else, but `src/` must be present or the wheels will be missing. + +--- + +## Testing the reader path without a reader + +Because the app talks plain serial, a pseudo-terminal is enough to exercise the +whole chain: write real RDM6300 frames into a PTY and watch the printed events. + +To hand-feed a single frame yourself: + +```python +import os, pty, struct, subprocess, sys, time + +def frame(value, card_type=0x0F): + id_bytes = struct.pack(">I", value) + checksum = card_type + for b in id_bytes: + checksum ^= b + return (bytes([card_type]) + id_bytes + bytes([checksum])).hex().upper().encode() + +master, slave = pty.openpty() +port = os.ttyname(slave) +proc = subprocess.Popen([sys.executable, "-u", "-m", "rdm_card_reader", "--port", port]) +time.sleep(2) # let the child open the port FIRST +os.write(master, b"\x02" + frame(0x00C4A0B5) + b"\x03") +time.sleep(1) +proc.terminate() +``` + +Two traps worth knowing, both of which cost me a debugging cycle: + +* **Send the frame after the child has opened the port.** A PTY buffers, so + frames written too early are discarded and it looks like a reader bug. +* **Read the child's stdout from a thread.** With text-mode buffered pipes, + `select()` will not report the data even though it is there. + +Frame layout, since it is easy to get wrong: the 12 hex nibbles are +**type, id, checksum**, and the checksum is the XOR of the type byte **and** all +four id bytes — not the id bytes alone. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..14fac91 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ed3192 --- /dev/null +++ b/README.md @@ -0,0 +1,532 @@ +# rdm-card-reader + +Read 125 kHz RFID cards (RDM6300 reader, EM4100 cards) on a serial port and +print what happens: **card inserted** with the card data, then **card removed**. + +Built for a Raspberry Pi workstation, but it runs anywhere with a serial port. + +> Starting it on a machine with no reader attached? See **[LAUNCH.md](LAUNCH.md)** +> — it explains the startup sequence, why it exits `1` with no hardware, and how +> to exercise the reader path anyway. + +--- + +## What it does + +``` +$ rdm-card-reader + +-> Opening /dev/ttyAMA0 ... + OK reader ready on /dev/ttyAMA0 - waiting for cards (Ctrl+C to stop) + +---------------------------------------------------------- + CARD INSERTED - 2026-10-01 14:56:54 + id : 12886197 (0x00C4A0B5) + type : 0x0F + checksum : 222 + valid : True + port : /dev/ttyAMA0 +---------------------------------------------------------- + CARD REMOVED - 2026-10-01 14:56:54 id: 12886197 (0x00C4A0B5) +``` + +`id` is the number printed on the card. A card is reported once on insert +(the reader library de-duplicates repeated reads while the card sits on the +antenna) and once on removal. + +### Startup sequence + +1. **Check dependencies.** If `rdm6300` / `pyserial` cannot be imported, install +them automatically and carry on. A fresh device needs no manual `pip` work. +2. **Find the reader.** Resolve the serial port and open it, identifying the +adapter by serial number or `by-id` rather than trusting `ttyUSB0`. +3. **Start reading.** Read frames in a loop; each valid one becomes a card +event. +4. **Publish it.** In `--serve` mode the event is latched so a web page can +claim it whenever it gets around to asking. + +Steps 1 and 2 both fail *loudly with a reason* rather than starting up +half-working, because a reader that silently picks the wrong port or runs +without its library is worse than one that refuses to start. + +--- + +## Install + +On a machine **with internet**: + +```bash +pip install . +``` + +`rdm6300` and `pyserial` are pulled in automatically. On Raspberry Pi OS with +an externally-managed Python, add `--break-system-packages` or use a venv: + +```bash +python3 -m venv .venv && . .venv/bin/activate && pip install . +``` + +### On a device with no internet + +`pip install .` will **not** work offline. It builds the project, which makes +pip download `setuptools` into an isolated build environment, and that fails +with: + +``` +ERROR: Could not find a version that satisfies the requirement setuptools>=68 + (from versions: none) +``` + +The runtime dependencies are bundled in `src/rdm_card_reader/wheels/`, so +install those directly and put `src` on the path — no build step, no network: + +```bash +python3 -m venv .venv +.venv/bin/pip install --no-index \ + --find-links src/rdm_card_reader/wheels rdm6300 pyserial +echo "$PWD/src" > "$( .venv/bin/python -c \ + 'import sysconfig; print(sysconfig.get_paths()["purelib"])' )/rdm_card_reader_src.pth" +.venv/bin/python -m rdm_card_reader --demo # verify, no hardware needed +``` + +`deploy/install.sh` does exactly this, plus the service setup. Running the +app directly also works: it installs anything missing from the same bundled +wheels on first start. + +--- + +## Usage + +```bash +rdm-card-reader # auto-detect the serial port +rdm-card-reader --port /dev/ttyUSB0 # use a specific port +rdm-card-reader --json # one JSON object per event +rdm-card-reader --heartbeat 1.5 # slower card-removed detection +rdm-card-reader --list-ports # show which ports exist, then exit +rdm-card-reader --serve # serve card data to a web app (see below) +rdm-card-reader --demo # simulate reads, no hardware needed +``` + +Also works as a module: `python -m rdm_card_reader`. + +| Option | Default | Meaning | +| --- | --- | --- | +| `--port` | auto-detect | Serial port: device path, by-id symlink, serial number or `vid:pid` | +| `--autodetect` | off | Pick the only USB adapter; refuse if there is more than one | +| `--heartbeat` | `0.5` | Seconds without a read before the card counts as removed | +| `--json` | off | Machine-readable output instead of the text block | +| `--list-ports` | – | List candidate and identified ports, then exit | +| `--verbose-ports` | off | With `--list-ports`, show by-id / by-path detail | +| `--demo` | – | Simulated reads; needs no hardware and no `rdm6300` | +| `--demo-count` | `3` | Number of simulated reads in `--demo` | +| `--no-install-deps` | off | Never install missing libraries automatically | +| `--wheel-dir` | bundled | Install from local wheels in this DIR instead of the bundled ones | +| `--version` | – | Print the version | + +With `--serve`: `--host` (`127.0.0.1`), `--bridge-port` (`8765`), `--mode` +(`login`), `--tap-ttl` (`10`), `--allow-origin` (repeatable), `--token`, +`--allow-remote`, `--verbose`. + +When `--port` is omitted the ports are tried in this order, the same order the +workstation app uses: + +``` +/dev/ttyS0 -> /dev/ttyAMA0 -> /dev/ttyUSB0 -> /dev/ttyACM0 +``` + +### Output modes + +Text mode is for humans and logs. `--json` prints one object per event, which +is what you want when piping into another program: + +```json +{"event": "card_inserted", "timestamp": "2026-10-01 14:56:54", "id": 12886197, "hex": "0x00C4A0B5", "type": 15, "checksum": 222, "is_valid": true, "port": "/dev/ttyAMA0"} +{"event": "card_removed", "timestamp": "2026-10-01 14:56:54", "id": 12886197, "hex": "0x00C4A0B5", "type": 15, "checksum": 222, "is_valid": true, "port": "/dev/ttyAMA0"} +``` + +### Automatic dependency install + +The wheels for `rdm6300` and `pyserial` **ship inside this package** +(`rdm_card_reader/wheels/`), so a device with no internet can still set itself +up. On first start the reader installs whatever is missing from those wheels: + +``` +Reader library not found - checking dependencies... +Missing reader dependencies: rdm6300 +Using the wheels bundled with this package: + /opt/rdm-card-reader/.venv/lib/python3.13/site-packages/rdm_card_reader/wheels +Installing from .../rdm_card_reader/wheels +Dependencies ready: rdm6300 +``` + +No network access is needed. They ship with the package rather than being +fetched at build time, because the build machine and the target device are not +necessarily the same. + +Both wheels are **pure Python** (`py3-none-any` / `py2.py3-none-any`), which is +what makes this possible: the same files install on armv7l, aarch64 and x86_64. +This differs from the workstation app's `Files/repository/` cache, which held +architecture-specific binaries (aarch64 manylinux wheels) and so could not be +committed — that cache only ever existed on the Pi devices themselves. + +* Nothing is installed when the libraries are already present, so a normal start + does no network work at all. +* The bundled install runs with `--no-index`, so it works with no route to PyPI + at all — no proxy or DNS needed. +* Wheels are verified against a bundled `SHA256SUMS` before installation. **A + wheel that fails its checksum is refused, not installed** — then it falls back + to PyPI, and if that is unreachable it reports `exit 2` rather than proceeding + with something unverified. +* Outside a virtualenv pip is given `--break-system-packages`, which Debian and + Raspberry Pi OS require before they will touch the system Python. +* A failed install does not crash: it reports `exit 2` and tells you the exact + command to run by hand. + +To install from your own wheel directory instead (for example a newer version +than the bundled one), pass `--wheel-dir`. An explicit directory that is +incomplete or fails verification is reported and then PyPI is used, rather than +failing outright. To manage dependencies yourself, pass `--no-install-deps`. + +### Exit codes + +| Code | Meaning | +| --- | --- | +| `0` | Ran normally, or stopped with Ctrl+C | +| `1` | No serial port could be opened | +| `2` | The `rdm6300` library is missing and could not be installed | +| `3` | The port selector matched more than one adapter (refused to guess) | + +--- + +## Serving card data to a web app (`--serve`) + +This is the mode for **authenticating a web page**: the reader runs as a service +on the device, and the page asks it for the card that was just tapped. + +```bash +rdm-card-reader --serve --port /dev/rdm-reader --allow-origin https://your.web.app +``` + +The bridge reports **card ids only**. Deciding which user a card belongs to is +your server's job, not this program's. + +### The flow + +```mermaid +sequenceDiagram + participant C as Card + participant B as rdm-card-reader (Pi) + participant K as Web page + participant W as Your server + + C->>B: tap + B->>B: latch the tap (tap-ttl seconds) + K->>B: GET /api/card + B-->>K: {claimable: true, card: {id: ...}} + K->>B: POST /api/card/claim + B-->>K: {card: {id: ...}} (once) + K->>W: POST /login {card_id} + W->>W: card_id -> user + W-->>K: session +``` + +The point of the *latch* is that the tap stays available for `--tap-ttl` +seconds (default 10), **including after the card is lifted**. A quick tap +therefore cannot be lost because the page happened to poll a moment later. + +The point of *claim* is single use. Once a tap is spent, a page reload or a +second tab gets `409 Conflict` instead of a second login, so one physical tap +can never authenticate two sessions. + +### Endpoints + +| Method | Path | Purpose | +| --- | --- | --- | +| `GET` | `/api/card` | The latched tap: `present`, `claimable`, `consumed`, `age_seconds`, `card` | +| `POST` | `/api/card/claim` | Spend the tap on one login. `200`, or `409` if used/none, `410` if expired | +| `GET` | `/api/events` | Server-Sent Events stream, pushes each event as it happens | +| `GET` | `/api/last` | Most recent card event, or `204` | +| `GET` | `/api/taps` | Recent tap history (`?limit=N`) | +| `GET` | `/api/health` | Reader state, port, uptime, tap counts — for monitoring | +| `POST` | `/api/mode` | `{"mode": "login"}` or `{"mode": "monitor"}` | + +`login` mode (default) publishes taps only — a tap is an authentication +attempt. `monitor` mode also publishes removals, which is what you want for +watching a reader rather than logging people in. + +### Browser example + +```js +const state = await (await fetch("http://127.0.0.1:8765/api/card")).json(); +if (state.claimable) { + const res = await fetch("http://127.0.0.1:8765/api/card/claim", { method: "POST" }); + if (res.ok) { + const { card } = await res.json(); + await fetch("/login/card", { // your server decides who card.id is + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ card_id: card.id, hex: card.hex }), + }); + } +} +``` + +Or push instead of poll, so a tap logs in immediately: + +```js +const events = new EventSource("http://127.0.0.1:8765/api/events"); +events.onmessage = (e) => { const tap = JSON.parse(e.data); /* tap.id */ }; +``` + +### Security, and its limits + +Read :ref:`the warning below `: an EM4100 id is a +*username*, not a password. These controls bound who can *read* the bridge; +they do not make the card itself convincing as a sole credential. + +| Control | Behaviour | +| --- | --- | +| Bind address | `127.0.0.1` by default. Anything else needs `--allow-remote`, so an open card-event endpoint cannot appear on the LAN by accident. | +| Origins | Cross-origin browser requests are **refused** unless `--allow-origin` lists that exact origin. Stops another site from reading your taps. | +| Token | `--token` (or `RDM_BRIDGE_TOKEN`) is required from non-loopback callers. | +| Single use | A tap can only ever produce one successful claim. | +| Expiry | A tap older than `--tap-ttl` is refused with `410`. | + +Add a second factor — a PIN after the tap — wherever a cloned card would +actually cost you something. + + +> **Card ids are not secrets.** EM4100 cards broadcast a fixed 40-bit id with no +> challenge-response, and clones are cheap. If holding an id is enough to log +> in, the security is "whoever knows the number". Treat card auth as +> identification, and require something else for anything sensitive. + +--- + +## Running at boot (Raspberry Pi) + +```bash +sudo ./deploy/install.sh --origin https://your.web.app +``` + +That installs a service account, a virtualenv, a systemd unit and a udev rule, +then enables the service. Afterwards: + +```bash +systemctl status rdm-card-reader # state +journalctl -u rdm-card-reader -f # live log +curl http://127.0.0.1:8765/api/card # tap a card, then run this +``` + +### Identifying the right ttyUSB + +`/dev/ttyUSB0` is **not** a stable identity — unplug something and the number +changes. Point this at the wrong adapter and you authenticate the wrong reader, +so the tooling is deliberately strict about it. + +```bash +rdm-card-reader --list-ports --verbose-ports +``` + +``` +Identified serial ports (best candidate first): + [score 100] /dev/ttyUSB0 :: CH340 :: 1a86:7523 :: S/N AB0XYZ12 + by-id : /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 + by-path : /dev/serial/by-path/pci-0000:01:00.0-usb-0:1.2:1.0-port0 +``` + +Then pin it, most specific first: + +```bash +--port /dev/rdm-reader # udev alias +--port /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0 # by device +--port AB0XYZ12 # by serial number +--port 1a86:7523 # by chip +``` + +`--autodetect` picks the only USB adapter, and **refuses** when there is more +than one — silently choosing between two readers is exactly the failure this +avoids. Verify the wiring in `deploy/99-rdm-reader.rules`. + +Exit codes: `0` ok, `1` no port, `2` library missing, `3` port ambiguous. + +--- + +## Status LED + +An LED driven by card events, on **GPIO 23** by default — the same pin the +workstation app used. + +```bash +rdm-card-reader # lit while running, blinks on a bad read +rdm-card-reader --led-pin 17 # a different pin +rdm-card-reader --led-mode presence # lit only while a card is on the reader +rdm-card-reader --no-led # no LED at all +``` + +| Flag | Default | Meaning | +| --- | --- | --- | +| `--no-led` | off | Do not drive a LED | +| `--led-pin` | `23` | GPIO pin (matches the workstation app) | +| `--led-mode` | `error` | See the two readings below | +| `--led-no-error-blink` | off | Do not blink on a bad read | + +Two modes, because the light can mean two different things: + +| Mode | Behaviour | Use when | +| --- | --- | --- | +| `error` (default) | **Lit whenever the reader is running.** Dark while a card is being read, blinks on a bad read | The light should mean "the reader is alive and ready" | +| `presence` | **Lit only while a card is on the reader** | The light should mean "a card is here" | + +Wiring: LED anode to the GPIO pin through a ~330 Ω resistor, cathode to GND. + +### It cannot break the reader + +Three deliberate choices, because a LED is a convenience and the reader is the +point: + +* **Never blocks.** The workstation app toggled the LED directly on the reader + thread, which stalls card detection. Here every change is queued to a worker + thread, so a toggle returns immediately even with slow hardware. +* **Never fatal.** On a machine with no GPIO, `gpiozero` raises + `BadPinFactory`. The app falls back to a no-op LED and says why, rather than + refusing to run. +* **Never crashes on a dead pin.** A failed toggle is logged and ignored. + +Startup reports which of the three states it is in: + +``` +LED on GPIO 23 (error mode, gpiozero) # working +LED unavailable: gpiozero not installed # library missing +LED unavailable: no usable GPIO on pin 23: ... # no pin factory / pin busy +``` + +That distinction matters: `BadPinFactory` subclasses `ImportError`, so a naive +check reports "not installed" when the library is present but has no usable pin +provider — which would send you to the wrong fix. + +> **If the workstation app also runs on this device, use a different pin** — two +> processes driving GPIO 23 will fight, and the LED will flicker. + +`gpiozero` and `colorzero` are installed automatically on first use, from the +bundled wheels, so this works offline. They add ~180 KB. `setuptools`, which +`gpiozero` pulls in as a *packaging* dependency, is deliberately not vendored: +it is never imported at runtime and would add 818 KB for nothing. + +--- + +## Wiring + +The RDM6300 talks plain serial at **9600 baud, 8N1**. Connect it to the Pi's +UART (pins 8/10 for TXD/RXD, 5 V and GND) or through a USB-to-serial adapter. + +On a Raspberry Pi you usually need to free up the primary UART first: + +```bash +sudo raspi-config # Interface Options -> Serial Port +# "login shell over serial?" -> No +# "serial port hardware?" -> Yes +sudo usermod -aG dialout $USER # then log out and back in +``` + +The GPIO UART appears as `/dev/serial0` (a symlink to `/dev/ttyAMA0` or +`/dev/ttyS0`). + +--- + +## The protocol + +Worth knowing if you ever decode the bytes yourself. The reader emits a frame +between `STX` (0x02) and `ETX` (0x03) as **12 ASCII hex nibbles**: + +``` +0x02 0F 00C4A0B5 DE 0x03 + | | | + | | +-- checksum + | +----------- card id (4 bytes, big-endian) + +-------------- type byte +``` + +Two details that are easy to get wrong: + +1. **Field order is type, id, checksum** — the id is *not* first. +2. **The checksum is the XOR of the type byte *and* all four id bytes.** XOR-ing + only the id bytes produces a mismatch on every card. + +``` +checksum = type ^ b0 ^ b1 ^ b2 ^ b3 + = 0x0F ^ 0x00 ^ 0xC4 ^ 0xA0 ^ 0xB5 + = 0xDE (= 222) +``` + +A frame whose checksum does not match is reported as `INVALID CARD` and is +never treated as a card insert. + +There is also a subtle failure mode: a frame with the wrong number of nibbles +is not rejected outright. Two stray nibbles stay in the buffer and corrupt the +*next* read instead of the current one, so a single glitch can look like a +problem with the following card. + +--- + +## Troubleshooting + +| Symptom | Fix | +| --- | --- | +| `device not found` on every port | Check the wiring; run `rdm-card-reader --list-ports` | +| `permission denied` | `sudo usermod -aG dialout $USER`, then log out and back in | +| Reader opens but no cards | Card must be EM4100 / 125 kHz; hold it ~2 cm from the coil | +| `INVALID CARD` repeatedly | Noisy supply or long/unshielded wiring; shorten it | +| Every card reported twice | Lower `--heartbeat` if the card is lifted quickly | + +--- + +## Check it works + +With no RFID hardware attached, `--demo` exercises the formatting path and +`--list-ports` shows what serial ports exist: + +```bash +rdm-card-reader --demo # simulated reads, no hardware needed +rdm-card-reader --list-ports # candidate and identified ports +``` + +To drive the real reader path with no hardware, feed RDM6300 frames into a +pseudo-terminal — see the snippet in **[LAUNCH.md](LAUNCH.md)**. + +### Layout + +``` +src/rdm_card_reader/ + card.py data model, constants, output formatting (no hardware) + reader.py serial port handling, event callbacks + cli.py argument parsing and program flow + demo.py simulated reads + ports.py stable serial port identification + bridge.py the HTTP bridge behind --serve + led.py status LED + bootstrap.py offline dependency install + wheels/ the bundled wheels (offline install source) +``` + +### Using it as a library + +```python +from rdm_card_reader import open_reader + +def on_event(event): + if event.event == "card_inserted": + print("card", event.value, event.hex_value) + +reader = open_reader(on_event) +if reader is None: + raise SystemExit("no reader found") + +reader.start() # blocks; call reader.close() to stop +``` + +--- + +## License + +MIT diff --git a/deploy/99-rdm-reader.rules b/deploy/99-rdm-reader.rules new file mode 100644 index 0000000..909ee32 --- /dev/null +++ b/deploy/99-rdm-reader.rules @@ -0,0 +1,28 @@ +# Give the RFID reader a stable name: /dev/rdm-reader +# +# Without this, the adapter is /dev/ttyUSB0 or /dev/ttyUSB1 depending on the +# order the kernel enumerated USB devices, which changes between reboots and +# whenever anything else is plugged in. Authenticating against the wrong reader +# is a real failure, so pin the identity here instead of hoping. +# +# Match on the USB-serial bridge chip. Add serial numbers once you know them - +# see README "Identifying the right ttyUSB" for how to read them off: +# +# SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", \ +# ATTRS{serial}=="REPLACE_WITH_YOUR_SERIAL", SYMLINK+="rdm-reader", MODE="0660", GROUP="dialout" +# +# Current rule: match by vendor:product (CH340 is 1a86:7523). +# Commented-out alternatives for other common bridge chips are listed below. +# Only ONE rule should be active - remove or comment the others. + +# CH340 / CH341 (most common on cheap USB-TTL adapters) +SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", ATTRS{idProduct}=="7523", SYMLINK+="rdm-reader", MODE="0660", GROUP="dialout" + +# FTDI FT232R +# SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="REPLACE_ME", SYMLINK+="rdm-reader", MODE="0660", GROUP="dialout" + +# Silicon Labs CP2102 +# SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", ATTRS{idProduct}=="ea60", ATTRS{serial}=="REPLACE_ME", SYMLINK+="rdm-reader", MODE="0660", GROUP="dialout" + +# Prolific PL2303 +# SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", ATTRS{serial}=="REPLACE_ME", SYMLINK+="rdm-reader", MODE="0660", GROUP="dialout" diff --git a/deploy/install.sh b/deploy/install.sh new file mode 100755 index 0000000..fc4c68b --- /dev/null +++ b/deploy/install.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# +# Install the RDM card reader as a service that starts at boot and keeps the +# card on the reader available over HTTP for the web page to claim. +# +# Run as root on the Raspberry Pi: +# +# sudo ./deploy/install.sh --origin https://your.web.app +# +# Options: +# --origin URL web app origin allowed to read the API (repeatable) +# --port PORT HTTP port for the bridge (default 8765) +# --dir PATH install location (default /opt/rdm-card-reader) +# --user NAME service account (default rdmcard) +# --no-service install and configure, but do not enable the service +# +set -euo pipefail + +ORIGINS=() +BRIDGE_PORT=8765 +INSTALL_DIR=/opt/rdm-card-reader +SERVICE_USER=rdmcard +INSTALL_SERVICE=1 +SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +while [[ $# -gt 0 ]]; do + case "$1" in + --origin) ORIGINS+=("$2"); shift 2 ;; + --port) BRIDGE_PORT="$2"; shift 2 ;; + --dir) INSTALL_DIR="$2"; shift 2 ;; + --user) SERVICE_USER="$2"; shift 2 ;; + --no-service) INSTALL_SERVICE=0; shift ;; + -h|--help) sed -n '2,20p' "$0"; exit 0 ;; + *) echo "unknown option: $1" >&2; exit 2 ;; + esac +done + +log() { printf '\n\033[1m==> %s\033[0m\n' "$*"; } +warn() { printf '\033[33mwarning: %s\033[0m\n' "$*" >&2; } + +if [[ $EUID -ne 0 ]]; then + echo "Please run as root: sudo $0 $*" >&2 + exit 1 +fi + +# --------------------------------------------------------------------------- +# 1. System packages +# --------------------------------------------------------------------------- +log "Checking system packages" +# Both of these need the network, so neither may abort the install: a device +# with no internet usually already has python3, and we can work without them. +apt-get update -qq 2>/dev/null || warn "apt-get update failed (no network?) - continuing" +apt-get install -y --no-install-recommends python3 python3-venv python3-pip \ + || warn "could not install system packages - continuing with what is present" + +# --------------------------------------------------------------------------- +# 2. Install the application +# --------------------------------------------------------------------------- +log "Installing to $INSTALL_DIR" +mkdir -p "$INSTALL_DIR" + +# Copy the source tree (excluding local build/test artifacts). +for item in src pyproject.toml README.md LAUNCH.md LICENSE deploy; do + if [[ -e "$SOURCE_DIR/$item" ]]; then + cp -a "$SOURCE_DIR/$item" "$INSTALL_DIR/" + fi +done + +log "Building the virtual environment" +if ! python3 -m venv "$INSTALL_DIR/.venv"; then + warn "could not create the virtualenv." + warn "On Debian/Raspberry Pi OS this usually means python3-venv is missing:" + warn " sudo apt-get install python3-venv" + exit 1 +fi + +VENV_PY="$INSTALL_DIR/.venv/bin/python" +WHEEL_DIR="$INSTALL_DIR/src/rdm_card_reader/wheels" + +# Upgrading pip is optional and needs the network. +"$INSTALL_DIR/.venv/bin/pip" install --quiet --upgrade pip 2>/dev/null \ + || echo " (kept the bundled pip - no network)" + +# Install the runtime dependencies from the wheels shipped in the source tree. +# +# Deliberately NOT 'pip install .': that builds the project, which requires +# downloading setuptools into an isolated build environment. With no internet +# it fails with +# Could not find a version that satisfies the requirement setuptools>=68 +# even though everything needed to *run* is sitting in the wheel directory. +# Installing the runtime dependencies directly, and putting 'src' on the +# path, achieves the same thing and works offline. +INSTALLED_PACKAGE=0 +if [[ -d "$WHEEL_DIR" ]] && "$VENV_PY" -m pip install --quiet --no-index \ + --find-links "$WHEEL_DIR" rdm6300 pyserial 2>/dev/null; then + echo " installed rdm6300 and pyserial from the bundled wheels (offline)" +else + echo " bundled wheels unavailable - falling back to PyPI" + if "$VENV_PY" -m pip install --quiet rdm6300 pyserial 2>/dev/null; then + echo " installed rdm6300 and pyserial from PyPI" + else + warn "could not install rdm6300 / pyserial." + warn "The app will try again on first start (it can install from its own" + warn "bundled wheels), but check network access if that also fails." + fi +fi + +# Status LED support. Best-effort: the reader works fine without a LED, so a +# failure here must not stop the install. +# +# --no-deps is deliberate: gpiozero declares colorzero, which in turn declares +# setuptools. setuptools is only a packaging requirement and is never imported +# at runtime, so it is not vendored (it would add 818 KB for nothing). +if [[ -d "$WHEEL_DIR" ]] && "$VENV_PY" -m pip install --quiet --no-index \ + --no-deps --find-links "$WHEEL_DIR" gpiozero colorzero 2>/dev/null; then + echo " installed gpiozero + colorzero for the status LED (offline)" +else + if "$VENV_PY" -m pip install --quiet gpiozero colorzero 2>/dev/null; then + echo " installed gpiozero + colorzero from PyPI" + else + warn "could not install LED support - the reader still works, the LED" + warn "will be inactive. The app can install it later on first use." + fi +fi + +# Make the package importable. The .pth file adds src/ to sys.path, so +# 'python -m rdm_card_reader' resolves from any working directory. +SITE_PACKAGES="$("$VENV_PY" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')" +echo "$INSTALL_DIR/src" > "$SITE_PACKAGES/rdm_card_reader_src.pth" + +# Console-script equivalent, so the unit and the docs work as written. +# The shebang must be the venv's interpreter by absolute path: '#!/usr/bin/env +# python3' would pick up the *system* python when the venv is not activated, +# and the package would not be importable. +cat > "$INSTALL_DIR/.venv/bin/rdm-card-reader" < "$INSTALL_DIR/run.sh" </dev/null 2>&1; then + echo " verified: $("$INSTALL_DIR/.venv/bin/rdm-card-reader" --version)" +else + warn "the reader does not run yet. Check with:" + warn " $INSTALL_DIR/.venv/bin/python -m rdm_card_reader --list-ports" +fi + +# --------------------------------------------------------------------------- +# 3. Service account +# --------------------------------------------------------------------------- +log "Creating service account '$SERVICE_USER'" +if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then + # No login shell, no home directory: this account only reads a serial port. + useradd --system --no-create-home --shell /usr/sbin/nologin "$SERVICE_USER" +fi + +# Serial access requires the dialout group. +usermod -aG dialout "$SERVICE_USER" +echo " $SERVICE_USER is in group dialout" + +# --------------------------------------------------------------------------- +# 4. Stable device name +# --------------------------------------------------------------------------- +log "Installing the udev rule (/dev/rdm-reader)" +install -m 0644 "$SOURCE_DIR/deploy/99-rdm-reader.rules" \ + /etc/udev/rules.d/99-rdm-reader.rules +udevadm control --reload-rules +udevadm trigger --subsystem-match=tty || true +sleep 1 + +if [[ -e /dev/rdm-reader ]]; then + echo " /dev/rdm-reader -> $(readlink -f /dev/rdm-reader)" +else + warn "/dev/rdm-reader does not exist yet." + warn "The reader may not be plugged in, or it uses a different chip." + warn "Run '$INSTALL_DIR/.venv/bin/rdm-card-reader --list-ports --verbose-ports'" + warn "then edit /etc/udev/rules.d/99-rdm-reader.rules to match your adapter." +fi + +# --------------------------------------------------------------------------- +# 5. Service +# --------------------------------------------------------------------------- +log "Installing the systemd unit" +UNIT=/etc/systemd/system/rdm-card-reader.service +install -m 0644 "$SOURCE_DIR/deploy/rdm-card-reader.service" "$UNIT" + +# Rewrite the ExecStart line with the real install dir, port and origins. +ORIGIN_ARGS="" +for origin in "${ORIGINS[@]:-}"; do + [[ -n "$origin" ]] && ORIGIN_ARGS+=" --allow-origin $origin" +done + +python3 - "$UNIT" "$INSTALL_DIR" "$BRIDGE_PORT" "$ORIGIN_ARGS" "$SERVICE_USER" <<'PY' +import os, re, sys + +unit, install_dir, port, origin_args, user = sys.argv[1:6] +text = open(unit).read() + +# The console script is created by the installer, but the .pth file is what +# makes the package importable, so fall back to 'python -m' if it is ever +# absent. systemd's ExecStart has no shell, so no pipes or conditionals. +wrapper = f"{install_dir}/.venv/bin/rdm-card-reader" +if not os.path.exists(wrapper): + wrapper = f"{install_dir}/.venv/bin/python -m rdm_card_reader" + +command = ( + f"{wrapper} --serve " + f"--port /dev/rdm-reader --bridge-port {port} --mode login{origin_args}" +) +# Replace the whole (possibly line-continued) ExecStart definition. +text = re.sub(r"ExecStart=[\s\S]*?(?=\n\n|\nRestart=)", f"ExecStart={command}", text, count=1) +text = re.sub(r"^User=.*$", f"User={user}", text, count=1, flags=re.M) +open(unit, "w").write(text) +PY + +# Make the venv readable by the service account. +chown -R "$SERVICE_USER:$SERVICE_USER" "$INSTALL_DIR" + +if [[ "$INSTALL_SERVICE" -eq 1 ]]; then + log "Enabling and starting the service" + systemctl daemon-reload + systemctl enable rdm-card-reader.service + systemctl restart rdm-card-reader.service + sleep 2 + systemctl --no-pager --lines=15 status rdm-card-reader.service || true +else + log "Skipping service start (--no-service)" +fi + +# --------------------------------------------------------------------------- +# Done +# --------------------------------------------------------------------------- +cat <=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "rdm-card-reader" +version = "0.4.0" +description = "RDM6300 / EM4100 RFID card reader: prints card events, or serves them over HTTP so a web app can authenticate a card tap." +readme = "README.md" +requires-python = ">=3.8" +license = { text = "MIT" } +keywords = ["rfid", "rdm6300", "em4100", "raspberry-pi", "serial"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Manufacturing", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Topic :: System :: Hardware", +] +dependencies = [ + "rdm6300>=0.1.1", + "pyserial>=3.5", +] + +[project.optional-dependencies] +# Status LED support. Installed automatically on first use by the app, or +# explicitly with: pip install '.[led]' +led = ["gpiozero>=2.0", "colorzero>=2.0"] + +[project.scripts] +rdm-card-reader = "rdm_card_reader.cli:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +rdm_card_reader = ["py.typed", "wheels/*.whl", "wheels/SHA256SUMS"] diff --git a/src/rdm_card_reader/__init__.py b/src/rdm_card_reader/__init__.py new file mode 100644 index 0000000..723ad10 --- /dev/null +++ b/src/rdm_card_reader/__init__.py @@ -0,0 +1,63 @@ +"""Reading RDM6300 (EM4100) RFID cards as simple insert / remove events. + +Two ways to use this package: + +* as a plain reader that prints what happens (``rdm-card-reader``) +* as a small HTTP bridge that publishes card events for a web app + (``rdm-card-reader --serve``) + +The bridge reports card ids only. Deciding which user a card belongs to is the +server's job - see ``bridge`` for the reasoning. + +On first start the reader installs its own libraries if they are missing, so a +fresh device needs no manual setup - see ``bootstrap``. +""" + +from .bootstrap import ensure_dependencies, missing_packages, bundled_wheel_dir +from .card import ( + CANDIDATE_PORTS, + CONFIG_CARD_VALUE, + CardData, + CardEvent, + format_event, +) +from .bridge import EventHub, create_server +from .ports import ( + AmbiguousPort, + PortError, + PortInfo, + PortNotFound, + discover, + resolve, +) +from .led import DEFAULT_LED_PIN, StatusLED, create as create_led +from .reader import available, list_ports, open_reader, reload_library + +__version__ = "0.4.0" + +__all__ = [ + "CANDIDATE_PORTS", + "CONFIG_CARD_VALUE", + "CardData", + "CardEvent", + "AmbiguousPort", + "DEFAULT_LED_PIN", + "EventHub", + "PortError", + "PortInfo", + "PortNotFound", + "StatusLED", + "available", + "bundled_wheel_dir", + "create_led", + "create_server", + "discover", + "ensure_dependencies", + "format_event", + "list_ports", + "missing_packages", + "open_reader", + "reload_library", + "resolve", + "__version__", +] diff --git a/src/rdm_card_reader/__main__.py b/src/rdm_card_reader/__main__.py new file mode 100644 index 0000000..980febd --- /dev/null +++ b/src/rdm_card_reader/__main__.py @@ -0,0 +1,8 @@ +"""Allow ``python -m rdm_card_reader``.""" + +import sys + +from .cli import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/rdm_card_reader/bootstrap.py b/src/rdm_card_reader/bootstrap.py new file mode 100644 index 0000000..f2c4cc1 --- /dev/null +++ b/src/rdm_card_reader/bootstrap.py @@ -0,0 +1,312 @@ +"""Make sure the reader library is present before we try to use it. + +On a fresh device the ``rdm6300`` package may not be installed. Rather than +dying with "please install this yourself", the reader installs it on first run +and then carries on - the same behaviour the workstation app had, but without +shelling out to a bare ``pip`` with no error handling. + +The wheels for both libraries are **shipped inside this package** (in +``rdm_card_reader/wheels``), so a device with no internet can still set itself +up. They are pure-Python (``py3-none-any`` / ``py2.py3-none-any``), so the same +files work on any architecture - unlike the workstation app's cache, which held +architecture-specific binaries and therefore could not be committed. + +Two things this deliberately does *not* do: + +* It never installs anything when the library is already importable, so a + normal start does no network work at all. +* It never lets a failed install crash the process. A missing library is a + condition to report clearly, not a traceback. +""" + +from __future__ import annotations + +import hashlib +import importlib +import os +import subprocess +import sys +from typing import Callable, List, Optional, Sequence + +#: Import name -> pip requirement. The import name is what we check; the +#: requirement is what pip is asked for. +REQUIRED_PACKAGES = ( + ("rdm6300", "rdm6300>=0.1.1"), + ("serial", "pyserial>=3.5"), +) + +#: Extra packages needed only when driving a status LED. +#: +#: ``colorzero`` is listed explicitly because ``gpiozero`` requires it. Both are +#: installed with ``--no-deps``: gpiozero's declared dependency chain pulls in +#: ``setuptools`` (via colorzero), which is only a packaging requirement and is +#: not imported at runtime. Vendoring it would add 818 KB for nothing. +LED_PACKAGES = ( + ("gpiozero", "gpiozero>=2.0"), + ("colorzero", "colorzero>=2.0"), +) + +#: The LED extras are optional: refusing to start because a LED cannot be +#: driven would be absurd. +OPTIONAL_IMPORTS = frozenset({"gpiozero", "colorzero"}) + +#: Shown when an install fails, so the operator knows what to do by hand. +MANUAL_HINT = ( + "Install manually with: {python} -m pip install {requirements}" +) + + +def bundled_wheel_dir() -> Optional[str]: + """Directory of the wheels shipped inside this package. + + Returns ``None`` when it is absent, which is the case for a source checkout + that has not vendored them. + """ + folder = os.path.join(os.path.dirname(os.path.abspath(__file__)), "wheels") + if not os.path.isdir(folder): + return None + if not any(name.endswith(".whl") for name in os.listdir(folder)): + return None + return folder + + +def local_wheel_missing(requirements: Sequence[str], wheel_dir: str) -> List[str]: + """Requirements that have no matching wheel in *wheel_dir*. + + ``--no-index`` makes pip fail outright if a wheel is absent, so check first + and fall back to PyPI rather than dying on a half-provisioned directory. + """ + try: + present = os.listdir(wheel_dir) + except OSError: + return list(requirements) + + missing = [] + for requirement in requirements: + # "rdm6300>=0.1.1" -> "rdm6300" + name = requirement.split(">=")[0].split("==")[0].split("[")[0].strip() + # Both sides are dash/underscore-normalised, so the prefix separator + # must be "_" - comparing against "name-" never matches. + normalised = name.replace("-", "_").lower() + if not any( + entry.replace("-", "_").lower().startswith(normalised + "_") + for entry in present + ): + missing.append(requirement) + return missing + + +def verify_wheels(wheel_dir: str, sums_file: Optional[str] = None) -> List[str]: + """Check wheels against ``SHA256SUMS``. Returns a list of problems. + + A tampered wheel is an arbitrary-code-execution risk on every device that + installs it, so a mismatch is reported rather than installed. + """ + problems: List[str] = [] + sums_path = sums_file or os.path.join(wheel_dir, "SHA256SUMS") + if not os.path.exists(sums_path): + return problems # nothing to check against + + try: + with open(sums_path, "r") as handle: + lines = [line.strip() for line in handle if line.strip()] + except OSError as exc: + return ["cannot read {}: {}".format(sums_path, exc)] + + for line in lines: + parts = line.split(None, 1) + if len(parts) != 2: + continue + expected, filename = parts[0], parts[1].lstrip("*").strip() + path = os.path.join(wheel_dir, filename) + if not os.path.exists(path): + problems.append("missing {}".format(filename)) + continue + digest = hashlib.sha256() + try: + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(65536), b""): + digest.update(chunk) + except OSError as exc: + problems.append("cannot read {}: {}".format(filename, exc)) + continue + if digest.hexdigest() != expected: + problems.append( + "checksum mismatch for {} (expected {}, got {})".format( + filename, expected[:12], digest.hexdigest()[:12] + ) + ) + return problems + + +def missing_packages( + packages: Sequence[tuple] = REQUIRED_PACKAGES, +) -> List[tuple]: + """Return the ``(import_name, requirement)`` pairs that cannot be imported.""" + missing = [] + for import_name, requirement in packages: + try: + importlib.import_module(import_name) + except ImportError: + missing.append((import_name, requirement)) + return missing + + +def in_virtualenv() -> bool: + """True when running inside a venv, where PEP 668 does not apply.""" + if os.environ.get("VIRTUAL_ENV"): + return True + return sys.prefix != getattr(sys, "base_prefix", sys.prefix) + + +def _pip_command( + requirements: Sequence[str], + wheel_dir: Optional[str], + no_deps: bool = False, +) -> List[str]: + """Build the pip invocation, handling venv vs system Python.""" + command = [sys.executable, "-m", "pip", "install"] + + if no_deps: + command.append("--no-deps") + + if wheel_dir: + # Offline install from a local wheel directory: no index, no network. + command += ["--no-index", "--find-links", wheel_dir] + else: + command += ["--disable-pip-version-check"] + + # Outside a virtualenv, pip on Debian/Raspberry Pi OS refuses to touch the + # system environment unless told explicitly (PEP 668). + if not in_virtualenv(): + command.append("--break-system-packages") + + command += list(requirements) + return command + + +def install( + requirements: Sequence[str], + wheel_dir: Optional[str] = None, + logger: Optional[Callable[[str], None]] = None, + timeout: int = 300, + no_deps: bool = False, +) -> bool: + """Run pip. Returns True on success; never raises for a failed install.""" + say = logger or (lambda message: None) + command = _pip_command(requirements, wheel_dir, no_deps=no_deps) + say(" $ {}".format(" ".join(command))) + + try: + result = subprocess.run( + command, + capture_output=True, + text=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + say(" x install timed out after {}s".format(timeout)) + return False + except OSError as exc: + say(" x could not run pip: {}".format(exc)) + return False + + if result.returncode != 0: + # pip writes the useful part to stderr; show the tail of it. + tail = (result.stderr or result.stdout or "").strip().splitlines()[-6:] + for line in tail: + say(" | {}".format(line)) + return False + + return True + + +def _is_optional(packages: Sequence[tuple]) -> bool: + """True when every package in the group is an optional extra.""" + names = {name for name, _ in packages} + return bool(names) and names <= OPTIONAL_IMPORTS + + +def ensure_dependencies( + packages: Sequence[tuple] = REQUIRED_PACKAGES, + wheel_dir: Optional[str] = None, + auto_install: bool = True, + logger: Optional[Callable[[str], None]] = None, + use_bundled: bool = True, + no_deps: bool = False, +) -> bool: + """Check for the given libraries and install them if they are missing. + + Returns True when everything is importable at the end - either because it + already was, or because the install worked. Returns False when something is + still missing, so the caller can decide what to do. For an optional group + (the LED extras) False just means the feature is unavailable. + """ + say = logger or (lambda message: None) + + missing = missing_packages(packages) + if not missing: + return True + + requirements = [requirement for _, requirement in missing] + names = ", ".join(name for name, _ in missing) + say("Missing reader dependencies: {}".format(names)) + + if not auto_install: + say("Automatic install is disabled (--no-install-deps).") + say(MANUAL_HINT.format( + python=sys.executable, requirements=" ".join(requirements))) + return False + + # Prefer the wheels shipped with the package: a device with no internet + # must still be able to set itself up. + explicit = wheel_dir is not None + if wheel_dir is None and use_bundled: + wheel_dir = bundled_wheel_dir() + if wheel_dir: + say("Using the wheels bundled with this package:") + say(" {}".format(wheel_dir)) + + if wheel_dir: + problems = verify_wheels(wheel_dir) + for problem in problems: + say(" x {}".format(problem)) + if problems: + # A bad or incomplete bundle is not fatal: fall back to PyPI rather + # than installing something we could not verify. + if explicit: + say(" x the --wheel-dir you gave is unusable") + say("Bundle unusable - falling back to PyPI.") + wheel_dir = None + else: + uncovered = local_wheel_missing(requirements, wheel_dir) + if uncovered: + # Do not attempt a --no-index install that is guaranteed to + # fail; PyPI covers whatever the bundle does not contain. + say(" local wheels do not cover: {}".format( + ", ".join(uncovered))) + if explicit: + say(" x the --wheel-dir you gave is incomplete") + say("Falling back to PyPI for those.") + wheel_dir = None + + if wheel_dir: + say("Installing from {}".format(wheel_dir)) + else: + say("Installing from PyPI ...") + + install(requirements, wheel_dir=wheel_dir, logger=say, + no_deps=no_deps and wheel_dir is not None) + + # Re-check rather than trusting pip's exit code alone. + still_missing = missing_packages(packages) + if still_missing: + say("Still missing: {}".format( + ", ".join(name for name, _ in still_missing))) + say(MANUAL_HINT.format( + python=sys.executable, requirements=" ".join(requirements))) + return False + + say("Dependencies ready: {}".format( + ", ".join(name for name, _ in missing))) + return True diff --git a/src/rdm_card_reader/bridge.py b/src/rdm_card_reader/bridge.py new file mode 100644 index 0000000..cfa01e9 --- /dev/null +++ b/src/rdm_card_reader/bridge.py @@ -0,0 +1,503 @@ +"""HTTP bridge: expose card taps to a web page, without ever becoming the +authority on who the card belongs to. + +The reader is a *sensor*. This module publishes what the sensor sees and stops +there - resolving a card id to a user is the server's job, so nothing here +writes to a database, mints session tokens or decides whether a login succeeds. + +Served by default: + +============================ ============================================ +``GET /api/health`` reader state, port, uptime, last tap age +``GET /api/card`` the card on the reader right now (latched) +``POST /api/card/claim`` claim that tap, single use, for one login +``GET /api/events`` Server-Sent Events stream of card events +``GET /api/last`` most recent card event (or 204) +``GET /api/taps`` recent tap history (ring buffer) +``POST /api/mode`` switch reader mode: login / monitor +============================ ============================================ + +The usual integration, for a web page that may load at any moment:: + + const r = await fetch("http://127.0.0.1:8765/api/card"); + const state = await r.json(); + if (state.claimable) { + const c = await fetch("http://127.0.0.1:8765/api/card/claim", {method: "POST"}); + if (c.ok) { + const { card } = await c.json(); + // hand card.id to your server; it decides who that is + } + } + +``claimable`` stays true for ``--tap-ttl`` seconds after the card is read - +including after the card is lifted - so a quick tap is not lost. + +Security posture (important, and deliberate): + +* Binds to ``127.0.0.1`` unless the operator explicitly opts in to more, because + an open card-event endpoint on a LAN lets anyone read who is tapping. +* Cross-origin requests are refused unless the origin is on an allow-list. + A page on another site must not be able to observe your taps. +* Optionally requires a pre-shared token (``--token`` / ``RDM_BRIDGE_TOKEN``) + for non-local callers. + +An EM4100 card id is a *username*, not a password: it is broadcast in the clear +and can be cloned. Never let holding a card id alone create a session. +""" + +from __future__ import annotations + +import json +import os +import secrets +import threading +import time +from collections import deque +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from typing import Any, Callable, Deque, Dict, List, Optional + +from .card import EVENT_INSERTED, EVENT_REMOVED, CardEvent + +#: Reader modes. ``login`` publishes inserts only (a tap = an authentication +#: attempt). ``monitor`` publishes inserts and removals both. +MODE_LOGIN = "login" +MODE_MONITOR = "monitor" +VALID_MODES = (MODE_LOGIN, MODE_MONITOR) + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 8765 +DEFAULT_HISTORY = 50 + +#: How long a tap stays claimable. This deliberately outlives the card +#: leaving the reader: a quick tap that is lifted straight away must still be +#: claimable, otherwise a page that polls a moment later sees nothing and the +#: login silently fails. Set to 0 to keep a tap claimable until it is used or +#: replaced. +DEFAULT_TAP_TTL = 10.0 + + +class EventHub: + """Tracks reader state and fans card events out to waiting subscribers. + + This is the only place the raw card id is held. Subscribers are HTTP + connections owned by the same process, never third-party code. + + The card currently read is *latched*: it stays readable through + :meth:`current_card` after the card has left the reader, for ``tap_ttl`` + seconds from the tap. That is what makes the data "always available" to a + web page that may load or poll at any moment. + + Claiming it through :meth:`consume_card` marks it used, so a page reload or + a second tab cannot replay the same physical tap into a second login. + """ + + def __init__( + self, + history: int = DEFAULT_HISTORY, + mode: str = MODE_LOGIN, + tap_ttl: float = DEFAULT_TAP_TTL, + ): + self._lock = threading.Lock() + self._subscribers: List["deque"] = [] + self._history: Deque[CardEvent] = deque(maxlen=history) + self._last: Optional[CardEvent] = None + self._last_at: Optional[float] = None + self._mode = mode if mode in VALID_MODES else MODE_LOGIN + self._started_at = time.time() + self._counts = {EVENT_INSERTED: 0, EVENT_REMOVED: 0, "invalid_card": 0} + self._port: Optional[str] = None + self._tap_ttl = float(tap_ttl if tap_ttl else 0.0) + + # The latched tap. It deliberately outlives the card leaving the + # reader, so a page that loads after the tap can still claim it. + self._tap: Optional[CardEvent] = None + self._tap_at: Optional[float] = None + self._on_reader = False + self._consumed_at: Optional[float] = None + + # -- state ------------------------------------------------------------ + + def set_port(self, port: str) -> None: + with self._lock: + self._port = port + + @property + def mode(self) -> str: + with self._lock: + return self._mode + + def set_mode(self, mode: str) -> bool: + """Switch mode. Returns False for an unknown mode.""" + if mode not in VALID_MODES: + return False + with self._lock: + self._mode = mode + return True + + def publish(self, event: CardEvent) -> None: + """Record an event and wake every subscriber. Safe from any thread.""" + with self._lock: + self._counts[event.event] = self._counts.get(event.event, 0) + 1 + + # Presence tracking always happens, in every mode: the latched tap + # is state, not a published event. + if event.event == EVENT_INSERTED: + self._last = event + self._last_at = time.time() + # A new tap replaces any previous unclaimed one. + self._tap = event + self._tap_at = self._last_at + self._on_reader = True + self._consumed_at = None + elif event.event == EVENT_REMOVED: + # Lifting the card does NOT discard the tap: it must stay + # claimable for tap_ttl, otherwise a quick tap is lost before + # the web page gets a chance to ask for it. + self._on_reader = False + + # In login mode a tap is an authentication attempt; card removal is + # not published, so a web app cannot mistake it for a second tap. + if self._mode == MODE_LOGIN and event.event == EVENT_REMOVED: + return + + self._history.append(event) + queues = list(self._subscribers) + payload = event.to_dict() + + for queue in queues: + try: + queue.append(payload) + except Exception: + pass + + # -- latched card ----------------------------------------------------- + + def current_card(self) -> Dict[str, Any]: + """The latched tap, so a page can ask at any moment. + + ``present`` is whether the card is physically on the reader right now; + ``claimable`` is whether this tap can still be spent on a login. + """ + with self._lock: + if self._tap is None: + return { + "present": False, + "claimable": False, + "consumed": False, + "expired": False, + "age_seconds": None, + "card": None, + } + age = time.time() - (self._tap_at or 0.0) + expired = bool(self._tap_ttl) and age > self._tap_ttl + consumed = self._consumed_at is not None + return { + "present": self._on_reader, + "claimable": (not consumed) and (not expired), + "consumed": consumed, + "expired": expired, + "age_seconds": round(age, 1), + "card": self._tap.to_dict(), + } + + def consume_card(self): + """Atomically claim the latched tap for one authentication. + + Returns ``(payload, error, status)``. The tap is marked used only when + this call succeeds, so two concurrent requests cannot both win. + """ + with self._lock: + if self._tap is None: + return None, "no card read yet", 409 + + age = time.time() - (self._tap_at or 0.0) + if self._tap_ttl and age > self._tap_ttl: + return None, "card tap expired", 410 + if self._consumed_at is not None: + return None, "card already used", 409 + + self._consumed_at = time.time() + return ( + { + "card": self._tap.to_dict(), + "age_seconds": round(age, 1), + "consumed": True, + }, + None, + 200, + ) + + def subscribe(self) -> "deque": + """Register a queue that receives every subsequent event.""" + queue: Deque[Dict[str, Any]] = deque(maxlen=100) + with self._lock: + self._subscribers.append(queue) + return queue + + def unsubscribe(self, queue: "deque") -> None: + with self._lock: + try: + self._subscribers.remove(queue) + except ValueError: + pass + + # -- snapshots -------------------------------------------------------- + + def health(self) -> Dict[str, Any]: + with self._lock: + last_age = ( + round(time.time() - self._last_at, 1) if self._last_at else None + ) + return { + "status": "ok", + "port": self._port, + "mode": self._mode, + "uptime_seconds": round(time.time() - self._started_at, 1), + "subscribers": len(self._subscribers), + "counts": dict(self._counts), + "last_tap": self._last.to_dict() if self._last else None, + "last_tap_age_seconds": last_age, + "card_present": self._on_reader, + "card_consumed": self._consumed_at is not None, + "tap_ttl_seconds": self._tap_ttl or None, + } + + def last(self) -> Optional[Dict[str, Any]]: + with self._lock: + return self._last.to_dict() if self._last else None + + def taps(self, limit: int = 20) -> List[Dict[str, Any]]: + with self._lock: + items = list(self._history)[-max(1, limit):] + return [event.to_dict() for event in items] + + +class _Handler(BaseHTTPRequestHandler): + """One HTTP connection. ``server.hub`` holds the shared state.""" + + server_version = "rdm-card-reader" + protocol_version = "HTTP/1.1" + + # -- helpers ---------------------------------------------------------- + + def _cors_ok(self) -> bool: + """Refuse cross-origin reads unless the origin was allow-listed.""" + origin = self.headers.get("Origin") + if not origin: + return True # same-origin fetch or a non-browser client + allowed: List[str] = getattr(self.server, "allowed_origins", []) + if not allowed: + return False # browsers only; opt out entirely + return origin in allowed + + def _token_ok(self) -> bool: + """Check the pre-shared token for non-loopback callers.""" + required: Optional[str] = getattr(self.server, "token", None) + if not required: + return True + client = self.client_address[0] + if client in ("127.0.0.1", "::1", "localhost"): + return True + supplied = self.headers.get("X-Auth-Token") or "" + if not supplied: + query = self.path.split("?", 1)[1] if "?" in self.path else "" + for pair in query.split("&"): + if pair.startswith("token="): + supplied = pair.split("=", 1)[1] + return secrets.compare_digest(supplied, required) + + def _send_json(self, payload: Any, status: int = 200) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.send_header("Cache-Control", "no-store") + origin = self.headers.get("Origin") + if origin and origin in getattr(self.server, "allowed_origins", []): + self.send_header("Access-Control-Allow-Origin", origin) + self.send_header("Vary", "Origin") + self.end_headers() + self.wfile.write(body) + + def _send_no_content(self) -> None: + self.send_response(204) + self.send_header("Cache-Control", "no-store") + self.send_header("Content-Length", "0") + origin = self.headers.get("Origin") + if origin and origin in getattr(self.server, "allowed_origins", []): + self.send_header("Access-Control-Allow-Origin", origin) + self.send_header("Vary", "Origin") + self.end_headers() + + def log_message(self, fmt: str, *args: Any) -> None: + logger: Optional[Callable[[str], None]] = getattr(self.server, "logger", None) + if logger: + logger("{} - {}".format(self.client_address[0], fmt % args)) + + @property + def _hub(self) -> EventHub: + return self.server.hub # type: ignore[attr-defined] + + # -- routing ---------------------------------------------------------- + + def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API + path = self.path.split("?", 1)[0].rstrip("/") or "/" + + if path == "/api/health": + self._send_json(self._hub.health()) + return + + if not self._cors_ok(): + self._send_json({"error": "origin not allowed"}, status=403) + return + if not self._token_ok(): + self._send_json({"error": "missing or invalid token"}, status=401) + return + + if path == "/api/last": + payload = self._hub.last() + if payload is None: + self._send_no_content() + else: + self._send_json(payload) + return + + if path == "/api/card": + self._send_json(self._hub.current_card()) + return + + if path == "/api/taps": + limit = 20 + if "?" in self.path: + for pair in self.path.split("?", 1)[1].split("&"): + if pair.startswith("limit="): + try: + limit = int(pair.split("=", 1)[1]) + except ValueError: + pass + self._send_json({"taps": self._hub.taps(limit)}) + return + + if path == "/api/events": + self._stream_events() + return + + self._send_json({"error": "not found", "path": path}, status=404) + + def do_POST(self) -> None: # noqa: N802 + path = self.path.split("?", 1)[0].rstrip("/") or "/" + + if path == "/api/card/claim": + if not self._cors_ok(): + self._send_json({"error": "origin not allowed"}, status=403) + return + if not self._token_ok(): + self._send_json({"error": "missing or invalid token"}, status=401) + return + payload, error, status = self._hub.consume_card() + if error: + self._send_json( + {"error": error, "state": self._hub.current_card()}, + status=status, + ) + return + self._send_json(payload, status=status) + return + + if path != "/api/mode": + self._send_json({"error": "not found", "path": path}, status=404) + return + if not self._cors_ok(): + self._send_json({"error": "origin not allowed"}, status=403) + return + if not self._token_ok(): + self._send_json({"error": "missing or invalid token"}, status=401) + return + + length = int(self.headers.get("Content-Length") or 0) + raw = self.rfile.read(length) if length else b"{}" + try: + payload = json.loads(raw.decode("utf-8") or "{}") + except ValueError: + self._send_json({"error": "invalid JSON body"}, status=400) + return + + mode = str(payload.get("mode", "")) + if not self._hub.set_mode(mode): + self._send_json( + {"error": "invalid mode", "valid": list(VALID_MODES)}, status=400 + ) + return + self._send_json({"mode": mode}) + + def do_OPTIONS(self) -> None: # noqa: N802 + origin = self.headers.get("Origin") + self.send_response(204) + if origin and origin in getattr(self.server, "allowed_origins", []): + self.send_header("Access-Control-Allow-Origin", origin) + self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type, X-Auth-Token") + self.send_header("Vary", "Origin") + self.send_header("Content-Length", "0") + self.end_headers() + + # -- Server-Sent Events ---------------------------------------------- + + def _stream_events(self) -> None: + """Hold the connection open and push each event as it happens.""" + queue = self._hub.subscribe() + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Cache-Control", "no-store") + self.send_header("Connection", "keep-alive") + origin = self.headers.get("Origin") + if origin and origin in getattr(self.server, "allowed_origins", []): + self.send_header("Access-Control-Allow-Origin", origin) + self.send_header("Vary", "Origin") + self.end_headers() + + try: + self.wfile.write(b": connected\n\n") + self.wfile.flush() + while True: + try: + payload = queue.popleft() + except IndexError: + payload = None + + if payload is not None: + message = "data: {}\n\n".format(json.dumps(payload)) + self.wfile.write(message.encode("utf-8")) + self.wfile.flush() + else: + # Comment frame keeps proxies and the browser from timing out. + self.wfile.write(b": keepalive\n\n") + self.wfile.flush() + time.sleep(0.2) + except (BrokenPipeError, ConnectionResetError): + pass + finally: + self._hub.unsubscribe(queue) + + +def create_server( + hub: EventHub, + host: str = DEFAULT_HOST, + port: int = DEFAULT_PORT, + allowed_origins: Optional[List[str]] = None, + token: Optional[str] = None, + logger: Optional[Callable[[str], None]] = None, +) -> ThreadingHTTPServer: + """Build (but do not start) the HTTP server around *hub*.""" + server = ThreadingHTTPServer((host, port), _Handler) + server.daemon_threads = True + server.hub = hub # type: ignore[attr-defined] + server.allowed_origins = list(allowed_origins or []) # type: ignore[attr-defined] + server.token = token or os.environ.get("RDM_BRIDGE_TOKEN") or None # type: ignore[attr-defined] + server.logger = logger # type: ignore[attr-defined] + return server + + +def is_loopback(host: str) -> bool: + """True for hosts that are not reachable off-box.""" + return host in ("127.0.0.1", "::1", "localhost") diff --git a/src/rdm_card_reader/card.py b/src/rdm_card_reader/card.py new file mode 100644 index 0000000..437777f --- /dev/null +++ b/src/rdm_card_reader/card.py @@ -0,0 +1,135 @@ +"""Card data model, protocol constants and output formatting. + +Nothing in this module touches hardware or the serial port, which keeps the +data model easy to unit test. +""" + +from __future__ import annotations + +import json +import time +from collections import namedtuple +from dataclasses import dataclass +from typing import Any, Dict + +try: # pragma: no cover - depends on the environment + from rdm6300 import CardData +except ImportError: # pragma: no cover - fallback so the model stays importable + CardData = namedtuple("CardData", ["value", "checksum", "type", "is_valid"]) + +#: Serial devices tried in order. ``/dev/ttyS0`` first matches the Raspberry Pi +#: default, the others cover USB / GPIO UART dongles. +CANDIDATE_PORTS = ( + "/dev/ttyS0", + "/dev/ttyAMA0", + "/dev/ttyUSB0", + "/dev/ttyACM0", +) + +#: A special card id used by the WMT workstation app to open its config screen. +#: Reported as a note only - this package never acts on it. +CONFIG_CARD_VALUE = 12886709 + +#: Seconds without a read before a card is considered removed. Set to ``None`` +#: to disable removal detection entirely. +DEFAULT_HEARTBEAT_INTERVAL = 0.5 + +BAR = "-" * 58 + +EVENT_INSERTED = "card_inserted" +EVENT_REMOVED = "card_removed" +EVENT_INVALID = "invalid_card" + + +def now_stamp() -> str: + """Local timestamp in the same format the workstation app logs.""" + return time.strftime("%Y-%m-%d %H:%M:%S") + + +@dataclass(frozen=True) +class CardEvent: + """A single card read event, decoupled from the reader library.""" + + event: str + timestamp: str + value: int + checksum: int + card_type: int + is_valid: bool + port: str + + # -- constructors ----------------------------------------------------- + + @classmethod + def from_card(cls, event: str, card: Any, port: str) -> "CardEvent": + """Build an event from an ``rdm6300.CardData`` (or anything alike).""" + return cls( + event=event, + timestamp=now_stamp(), + value=int(card.value), + checksum=int(card.checksum), + card_type=int(card.type), + is_valid=bool(card.is_valid), + port=str(port), + ) + + # -- derived values --------------------------------------------------- + + @property + def hex_value(self) -> str: + """Card id as an 8 digit hex string, e.g. ``0x00C4A0B5``.""" + return "0x{:08X}".format(self.value) + + @property + def is_config_card(self) -> bool: + return self.value == CONFIG_CARD_VALUE + + # -- serialisation ---------------------------------------------------- + + def to_dict(self) -> Dict[str, Any]: + """Machine-readable form, used by ``--json``.""" + return { + "event": self.event, + "timestamp": self.timestamp, + "id": self.value, + "hex": self.hex_value, + "type": self.card_type, + "checksum": self.checksum, + "is_valid": self.is_valid, + "port": self.port, + } + + def to_json(self) -> str: + return json.dumps(self.to_dict()) + + +def format_event(event: CardEvent) -> str: + """Render an event as the human-readable block printed on stdout.""" + if event.event == EVENT_INSERTED: + lines = [ + "", + BAR, + " CARD INSERTED - {}".format(event.timestamp), + " id : {} ({})".format(event.value, event.hex_value), + " type : 0x{:02X}".format(event.card_type), + " checksum : {}".format(event.checksum), + " valid : {}".format(event.is_valid), + " port : {}".format(event.port), + ] + if event.is_config_card: + lines.append( + " note : this is the config card (id {})".format( + CONFIG_CARD_VALUE + ) + ) + lines.append(BAR) + return "\n".join(lines) + + if event.event == EVENT_REMOVED: + return " CARD REMOVED - {} id: {} ({})".format( + event.timestamp, event.value, event.hex_value + ) + + return " INVALID CARD - {} raw: {} (checksum mismatch)".format( + event.timestamp, event.hex_value + ) diff --git a/src/rdm_card_reader/cli.py b/src/rdm_card_reader/cli.py new file mode 100644 index 0000000..4cec201 --- /dev/null +++ b/src/rdm_card_reader/cli.py @@ -0,0 +1,382 @@ +"""Command line interface for the RDM card reader.""" + +from __future__ import annotations + +import argparse +import sys +import threading +from typing import Optional, Sequence + +from . import __version__ +from .bootstrap import LED_PACKAGES, ensure_dependencies +from .bridge import ( + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_TAP_TTL, + MODE_LOGIN, + VALID_MODES, + EventHub, + create_server, +) +from .card import ( + BAR, + CANDIDATE_PORTS, + DEFAULT_HEARTBEAT_INTERVAL, + EVENT_INVALID, + CardEvent, + format_event, +) +from .demo import run_demo +from .led import DEFAULT_LED_PIN, MODES as LED_MODES, create as create_led +from .ports import AmbiguousPort, PortNotFound, discover, resolve +from .reader import available, explain_error, list_ports, open_reader, reload_library + +EXIT_OK = 0 +EXIT_NO_PORT = 1 +EXIT_NO_LIBRARY = 2 +EXIT_PORT_AMBIGUOUS = 3 + + +def _make_printer(as_json: bool): + """Return a callback that writes one event to stdout.""" + + def print_event(event: CardEvent) -> None: + if as_json: + print(event.to_json(), flush=True) + else: + print(format_event(event), flush=True) + + return print_event + + +def _show_ports(verbose: bool = False) -> int: + info = list_ports() + print("Candidate serial ports (tried in this order):") + for port, exists in info["candidates"]: + print(" {} {}".format("OK " if exists else "---", port)) + + ports = discover() + if ports: + print("\nIdentified serial ports (best candidate first):") + for entry in sorted(ports, key=lambda p: (-p.score(), p.device)): + print(" [score {:>3}] {}".format(entry.score(), entry.describe())) + if verbose: + stable = entry.stable_id or "(none)" + print(" by-id : {}".format(stable)) + print(" by-path : {}".format(entry.by_path or "(none)")) + print(" location : {}".format(entry.location or "(unknown)")) + print(" usb id : {}".format(entry.usb_id or "(not USB)")) + print(" driver : {}".format(entry.product or "(unknown)")) + else: + print("\n(no serial ports identified)") + + print("\nTip: pass --port with a serial number or by-id path so the same") + print(" physical adapter is always used, e.g.") + print(" --port /dev/serial/by-id/usb-1a86_USB_Serial-if00-port0") + return EXIT_OK + + +def _resolve_port(args) -> str: + """Work out which serial port to open, or exit with a clear reason.""" + selectors = [args.port] if args.port else None + try: + device, reason = resolve( + selectors=selectors, + fallback_ports=[args.port] if args.port else list(CANDIDATE_PORTS), + autodetect=args.autodetect, + ) + except AmbiguousPort as exc: + print("x {}".format(exc), file=sys.stderr) + print(" Refusing to guess which reader is the right one.", file=sys.stderr) + sys.exit(EXIT_PORT_AMBIGUOUS) + except PortNotFound as exc: + print("x {}".format(exc), file=sys.stderr) + sys.exit(EXIT_NO_PORT) + + print("-> Using {} ({})".format(device, reason), flush=True) + return device + + +def _ensure_library(args) -> bool: + """Make sure the reader library is importable, installing it if needed. + + A fresh device should not need manual pip work, so this installs on first + run. It is skipped entirely when the library is already present. + """ + if available(): + return True + + print("Reader library not found - checking dependencies...", flush=True) + installed = ensure_dependencies( + wheel_dir=args.wheel_dir, + auto_install=not args.no_install_deps, + logger=print, + ) + if installed: + # The import happened at process start, so refresh it now. + reload_library() + + if available(): + return True + + # Flush stdout first: stderr is unbuffered while stdout is block-buffered + # when piped, so without this the explanation can appear after the error. + sys.stdout.flush() + print("\nERROR: the 'rdm6300' library is not available.", file=sys.stderr) + print(" Retry with network access, or run with --demo to see the" + " output format.", file=sys.stderr) + return False + + +def _build_led(args): + """Set up the status LED, or a no-op when GPIO is unusable. + + Never fatal: a reader that refuses to start because a LED cannot be driven + would be worse than one with no light. + """ + if args.no_led: + return create_led(enabled=False) + + # Only pull in gpiozero when a LED was actually asked for, so a headless + # install does not grow the dependency set. + ensure_dependencies( + packages=LED_PACKAGES, + wheel_dir=args.wheel_dir, + auto_install=not args.no_install_deps, + logger=print, + no_deps=True, + ) + return create_led( + pin=args.led_pin, + enabled=True, + mode=args.led_mode, + error_blink=not args.led_no_error_blink, + logger=lambda message: print(" {}".format(message)), + ) + + +def _run_bridge(args) -> int: + """Serve what the reader sees over HTTP for a web app to consume.""" + if not _ensure_library(args): + return EXIT_NO_LIBRARY + + if not args.allow_remote and args.host not in ("127.0.0.1", "::1", "localhost"): + print("ERROR: refusing to bind {} without --allow-remote.".format(args.host), + file=sys.stderr) + print(" A card-event endpoint on the LAN exposes who taps where.", + file=sys.stderr) + return 1 + + device = _resolve_port(args) + + hub = EventHub(mode=args.mode, tap_ttl=args.tap_ttl) + hub.set_port(device) + + led = _build_led(args) + + def emit(event: CardEvent) -> None: + # Drive the LED first: it is queued, so it never delays the reader. + if event.event == EVENT_INVALID: + led.error() + elif event.event == "card_removed": + led.remove() + else: + led.insert() + + hub.publish(event) + if args.verbose: + print(format_event(event), flush=True) + + def report_error(port: str, message: str) -> None: + print(" x {}: {}".format(port, explain_error(port, message)), flush=True) + + reader = open_reader(emit, ports=[device], + heartbeat_interval=args.heartbeat, + on_error=report_error) + if reader is None: + print("x Could not open {}. Is another process using it?".format(device), + file=sys.stderr) + return EXIT_NO_PORT + + server = create_server( + hub, + host=args.host, + port=args.bridge_port, + allowed_origins=args.allow_origin, + token=args.token, + logger=print if args.verbose else None, + ) + + server_thread = threading.Thread(target=server.serve_forever, daemon=True, + name="bridge-http") + server_thread.start() + + print(" OK reader on {} ; bridge on http://{}:{} (mode: {})".format( + device, args.host, args.bridge_port, args.mode)) + print(" GET /api/card <- the card on the reader right now") + print(" POST /api/card/claim (single use, for one login)") + print(" GET /api/events (SSE stream)") + print(" GET /api/last /api/health") + if args.token: + print(" token required for non-local callers") + if not args.allow_origin: + print(" note: cross-origin browser requests are refused") + print(" pass --allow-origin https://your.app to permit one") + print(" Ctrl+C to stop.", flush=True) + + # The reader loop blocks this thread; the HTTP server runs alongside it. + try: + reader.start() + except KeyboardInterrupt: + print("\nStopped by user.") + finally: + try: + reader.close() + except Exception: + pass + led.close() + server.shutdown() + server.server_close() + return EXIT_OK + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="rdm-card-reader", + description="Read RFID cards (RDM6300 / EM4100) and print the card " + "inserted and card removed output.", + ) + parser.add_argument("--version", action="version", + version="%(prog)s {}".format(__version__)) + parser.add_argument("--port", help="serial port: device path, by-id symlink, " + "USB serial number or vid:pid") + parser.add_argument("--autodetect", action="store_true", + help="pick the only USB serial adapter; refuse if unsure") + parser.add_argument("--heartbeat", type=float, + default=DEFAULT_HEARTBEAT_INTERVAL, + help="seconds without a read before the card is reported " + "as removed (default: %(default)s)") + parser.add_argument("--json", action="store_true", + help="print one JSON object per event") + parser.add_argument("--list-ports", action="store_true", + help="list candidate and identified ports, then exit") + parser.add_argument("--verbose-ports", action="store_true", + help="with --list-ports, show by-id / by-path detail") + parser.add_argument("--demo", action="store_true", + help="simulate card reads (needs no hardware or library)") + parser.add_argument("--demo-count", type=int, default=3, + help="number of simulated reads in --demo (default: %(default)s)") + parser.add_argument("--no-install-deps", action="store_true", + help="do not install missing reader libraries automatically") + parser.add_argument("--wheel-dir", metavar="DIR", + help="install missing libraries from local wheels in DIR " + "instead of PyPI (offline install)") + + bridge = parser.add_argument_group("HTTP bridge (--serve)") + bridge.add_argument("--serve", action="store_true", + help="serve card events over HTTP for a web app") + bridge.add_argument("--host", default=DEFAULT_HOST, + help="bind address (default: %(default)s, loopback only)") + bridge.add_argument("--bridge-port", type=int, default=DEFAULT_PORT, + help="HTTP port (default: %(default)s)") + bridge.add_argument("--mode", default=MODE_LOGIN, choices=list(VALID_MODES), + help="login: publish taps only; monitor: taps and removals " + "(default: %(default)s)") + bridge.add_argument("--tap-ttl", type=float, default=DEFAULT_TAP_TTL, + help="seconds an unclaimed tap stays claimable; 0 keeps it " + "claimable while the card is present (default: %(default)s)") + bridge.add_argument("--allow-origin", action="append", metavar="ORIGIN", + help="permit this browser origin to call the API; repeatable") + bridge.add_argument("--token", + help="pre-shared token required from non-local callers " + "(or set RDM_BRIDGE_TOKEN)") + bridge.add_argument("--allow-remote", action="store_true", + help="permit binding a non-loopback address") + bridge.add_argument("--verbose", action="store_true", + help="also print each event and each HTTP request") + + led = parser.add_argument_group("status LED") + led.add_argument("--no-led", action="store_true", + help="do not drive a status LED") + led.add_argument("--led-pin", type=int, default=DEFAULT_LED_PIN, + help="GPIO pin of the status LED (default: %(default)s, " + "matching the workstation app)") + led.add_argument("--led-mode", choices=list(LED_MODES), default=LED_MODES[0], + help="'error' (default): lit whenever the reader is running, " + "blinking means a bad read; 'presence': lit only while " + "a card is on the reader") + led.add_argument("--led-no-error-blink", action="store_true", + help="do not blink the LED on a bad read") + return parser + + +def main(argv: Optional[Sequence[str]] = None) -> int: + args = build_parser().parse_args(argv) + + if args.list_ports: + return _show_ports(verbose=args.verbose_ports) + + if args.demo: + return run_demo(count=args.demo_count, heartbeat=args.heartbeat, + as_json=args.json) + + if args.serve: + return _run_bridge(args) + + if not _ensure_library(args): + return EXIT_NO_LIBRARY + + led = _build_led(args) + print_event = _make_printer(args.json) + ports = [args.port] if args.port else list(CANDIDATE_PORTS) + + def report_error(port: str, message: str) -> None: + print(" x {}: {}".format(port, explain_error(port, message)), flush=True) + + def on_event(event: CardEvent) -> None: + # LED first: it is queued, so it never delays the reader loop. + if event.event == EVENT_INVALID: + led.error() + elif event.event == "card_removed": + led.remove() + else: + led.insert() + print_event(event) + + for port in ports: + print("-> Opening {} ...".format(port), flush=True) + + reader = open_reader(on_event, ports=ports, + heartbeat_interval=args.heartbeat, + on_error=report_error) + + if reader is None: + led.close() + print("\nx Could not open any serial port. Check:", file=sys.stderr) + print(" 1. the reader is connected (rdm-card-reader --list-ports)", + file=sys.stderr) + print(" 2. UART is enabled: sudo raspi-config -> Interface Options -> Serial", + file=sys.stderr) + print(" 3. the user is in dialout: sudo usermod -aG dialout $USER", + file=sys.stderr) + return EXIT_NO_PORT + + print(" OK reader ready on {} - waiting for cards (Ctrl+C to stop)".format( + reader.port), flush=True) + + try: + reader.start() # blocks; events arrive through the callback + except KeyboardInterrupt: + print("\nStopped by user.") + finally: + try: + reader.close() + except Exception: + pass + led.close() + + return EXIT_OK + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/src/rdm_card_reader/demo.py b/src/rdm_card_reader/demo.py new file mode 100644 index 0000000..14e5bf7 --- /dev/null +++ b/src/rdm_card_reader/demo.py @@ -0,0 +1,49 @@ +"""Simulated card reads, so the output can be checked without hardware.""" + +from __future__ import annotations + +import random +import time + +from .card import ( + DEFAULT_HEARTBEAT_INTERVAL, + CardData, + CardEvent, + format_event, +) + +# Card ids with the checksum the real protocol would produce +# (XOR of the type byte and the four id bytes), so the output looks genuine. +DEMO_CARDS = ( + CardData(value=0x00C4A0B5, checksum=0xDE, type=0x0F, is_valid=True), + CardData(value=0x1A2B3C4D, checksum=0x4F, type=0x0F, is_valid=True), + CardData(value=0xDEADBEEF, checksum=0x00, type=0x0F, is_valid=False), +) + + +def _emit(event: CardEvent, as_json: bool) -> None: + print(event.to_json() if as_json else format_event(event), flush=True) + + +def run_demo( + count: int = 3, + heartbeat: float = DEFAULT_HEARTBEAT_INTERVAL, + as_json: bool = False, + seed: int = 1234, +) -> int: + """Print *count* simulated card read cycles. Returns a process exit code.""" + rng = random.Random(seed) + print("Demo mode - no hardware is used, values are simulated.\n", flush=True) + + for _ in range(max(1, count)): + card = rng.choice(DEMO_CARDS) + event_name = "card_inserted" if card.is_valid else "invalid_card" + _emit(CardEvent.from_card(event_name, card, "demo"), as_json) + + time.sleep(max(0.1, heartbeat)) + if card.is_valid: + _emit(CardEvent.from_card("card_removed", card, "demo"), as_json) + time.sleep(0.3) + + print("\nDemo finished.", flush=True) + return 0 diff --git a/src/rdm_card_reader/led.py b/src/rdm_card_reader/led.py new file mode 100644 index 0000000..94fa84a --- /dev/null +++ b/src/rdm_card_reader/led.py @@ -0,0 +1,314 @@ +"""Drive a status LED from card events. + +The workstation app lit an LED (GPIO 23) with the card: on when a card was +inserted, off when it was removed. This reproduces that, with three deliberate +differences: + +* It never blocks the reader. WMT toggled the LED and slept on the reader + thread, which stalls card detection; here every change is queued to a worker. +* It cannot crash the process. On a machine without GPIO, turning the LED on + raises ``BadPinFactory``. WMT worked around this with a ``DummyLED``; this + keeps the same idea but also handles a pin that disappears at runtime. +* It is explicit. Whether an LED is actually attached is reported, so "no light + came on" is distinguishable from "the card was not read". + +The default pin (23) matches the workstation app. **Use a separate LED from the +workstation's if both run on one device**, since two processes driving one pin +will fight. +""" + +from __future__ import annotations + +import queue +import threading +from dataclasses import dataclass +from typing import Any, Callable, Optional + +#: GPIO pin of the status LED. Chosen to match the workstation app. +DEFAULT_LED_PIN = 23 + +#: How long to hold an error indication, in seconds. +ERROR_BLINK_SECONDS = 1.0 + +OFF = "off" +ON = "on" +BLINK = "blink" + +#: Lit the whole time the app runs, and blinks on a bad read. This is a health +#: indicator: light on means the reader is alive and ready for a card. +ERROR = "error" + +#: Lit only while a card is on the reader, and off otherwise. This is a card +#: indicator, for when the light should mean "a card is here". +PRESENCE = "presence" + +MODES = (ERROR, PRESENCE) + +#: Kept for the occasional caller that only cares about the concrete commands. +_VALID_MODES = (OFF, ON, BLINK) + + +@dataclass +class LEDStatus: + """What the LED subsystem is actually doing.""" + + enabled: bool + pin: Optional[int] + backend: str # "gpiozero" | "null" + mode: str = ERROR + state: bool = False + reason: str = "" + + def to_dict(self) -> dict: + return { + "enabled": self.enabled, + "pin": self.pin, + "backend": self.backend, + "mode": self.mode, + "state": "on" if self.state else "off", + "reason": self.reason, + } + + +class NullLED: + """A LED that does nothing, used when there is no usable GPIO. + + Kept deliberately quiet: the reason it is inert is reported once at + startup, not on every card. + """ + + backend = "null" + + def __init__(self, pin: Optional[int], reason: str = ""): + self.pin = pin + self.reason = reason + + def on(self) -> None: + pass + + def off(self) -> None: + pass + + def close(self) -> None: + pass + + +class GPIOLED: + """A real LED on a GPIO pin, via gpiozero.""" + + backend = "gpiozero" + + def __init__(self, pin: int, active_high: bool = True): + from gpiozero import LED as _LED + + self.pin = pin + self._led = _LED(pin, active_high=active_high) + self._is_on = False + + def on(self) -> None: + self._led.on() + self._is_on = True + + def off(self) -> None: + self._led.off() + self._is_on = False + + def close(self) -> None: + try: + self._led.close() + except Exception: + pass + + +class _Blinker(threading.Thread): + """Repeatedly toggles a LED, for an error indication.""" + + def __init__(self, led: Any, period: float = 0.15): + super().__init__(daemon=True, name="led-blink") + self._led = led + self._period = period + self._stop = threading.Event() + + def run(self) -> None: + state = False + while not self._stop.is_set(): + try: + self._led.on() if state else self._led.off() + except Exception: + return + state = not state + self._stop.wait(self._period) + try: + self._led.off() + except Exception: + pass + + def stop(self) -> None: + self._stop.set() + + +class StatusLED: + """Non-blocking LED controller driven by card events. + + Commands are queued and applied by a worker thread, so calling :meth:`insert` + from the reader callback never blocks card detection. + """ + + def __init__( + self, + pin: int = DEFAULT_LED_PIN, + enabled: bool = True, + mode: str = ERROR, + active_high: bool = True, + error_blink: bool = True, + logger: Optional[Callable[[str], None]] = None, + ): + self._log = logger or (lambda message: None) + self._mode = mode if mode in MODES else ERROR + self._error_blink = error_blink + self._enabled = enabled + self._blinker: Optional[_Blinker] = None + self._state = False + + self._queue: "queue.Queue[Optional[str]]" = queue.Queue() + self._thread: Optional[threading.Thread] = None + + if not enabled: + self.led: Any = NullLED(None, "LED disabled (--no-led)") + self.status = LEDStatus(False, None, "null", self._mode, False, + "disabled") + return + + # Check the library separately from building the pin. + # + # gpiozero raises BadPinFactory when no pin provider works, and that is + # a subclass of ImportError, so catching ImportError around the pin + # construction would report "gpiozero is not installed" even when it is + # installed and simply has no usable pin library. On a Pi missing lgpio + # or RPi.GPIO that message sends you to the wrong fix. + try: + import gpiozero # noqa: F401 + except ImportError as exc: + reason = "gpiozero not installed: {}".format(exc) + self.led = NullLED(pin, reason) + self.status = LEDStatus(False, pin, "null", self._mode, False, + reason) + self._log("LED unavailable: {}".format(reason)) + else: + try: + self.led = GPIOLED(pin, active_high=active_high) + self.status = LEDStatus(True, pin, "gpiozero", self._mode, + False, "ready") + self._log("LED on GPIO {} ({} mode, gpiozero)".format( + pin, self._mode)) + except Exception as exc: + # No usable pin factory (not a Pi), pin already in use, bad pin. + reason = ("no usable GPIO on pin {}: {}".format(pin, exc)) + self.led = NullLED(pin, reason) + self.status = LEDStatus(False, pin, "null", self._mode, False, + reason) + self._log("LED unavailable: {}".format(reason)) + + if self.status.enabled and self._mode == ERROR: + # A health indicator shows 'alive' immediately. In presence mode the + # light means 'a card is here', so it must stay dark until one is. + self.led.on() + self._set_state(True) + self.status.state = True + + self._thread = threading.Thread(target=self._worker, daemon=True, + name="led-worker") + self._thread.start() + + # -- event hooks ------------------------------------------------------ + + def insert(self) -> None: + """A valid card was read: off in 'error' mode, on in 'presence' mode.""" + self._send(OFF if self._mode == ERROR else ON) + + def remove(self) -> None: + """The card left the reader: dark in 'presence' mode; 'error' mode stays on.""" + self._send(ON if self._mode == ERROR else OFF) + + def error(self) -> None: + """A bad read: blink, if blinking is enabled.""" + self._send(BLINK if self._error_blink else ON) + + # -- control ---------------------------------------------------------- + + def _send(self, command: str) -> None: + if not self.status.enabled: + return + try: + self._queue.put_nowait(command) + except Exception: + pass + + def _worker(self) -> None: + while True: + command = self._queue.get() + if command is None: + return + try: + self._apply(command) + except Exception as exc: + # A failed toggle must never take the reader down. + self._log("LED update failed: {}".format(exc)) + finally: + self._queue.task_done() + + def _apply(self, command: str) -> None: + # Blinking is used only for a bad read, and only by 'error' mode. + if command == BLINK and self._mode == ERROR: + self._stop_blink() + self._blinker = _Blinker(self.led) + self._blinker.start() + return + + self._stop_blink() # a steady command cancels any blink + + # Two readings of the light, hence two modes: + # error - a health indicator: lit the whole time the reader runs, + # so "light on" means "ready for a card". + # presence - a card indicator: lit only while a card is on the reader. + if command == OFF: + self.led.off() + self._set_state(False) + elif command == ON: + self.led.on() + self._set_state(True) + + def _stop_blink(self) -> None: + if self._blinker is not None: + self._blinker.stop() + self._blinker = None + + def _set_state(self, value: bool) -> None: + self._state = value + self.status.state = value + + def close(self) -> None: + self._stop_blink() + if self._thread is not None: + self._queue.put(None) + try: + self.led.off() + except Exception: + pass + self._set_state(False) + try: + self.led.close() + except Exception: + pass + + +def create( + enabled: bool = True, + pin: int = DEFAULT_LED_PIN, + mode: str = ERROR, + error_blink: bool = True, + logger: Optional[Callable[[str], None]] = None, +) -> StatusLED: + """Build the LED controller, degrading to a no-op when GPIO is unusable.""" + return StatusLED(pin=pin, enabled=enabled, mode=mode, + error_blink=error_blink, logger=logger) diff --git a/src/rdm_card_reader/ports.py b/src/rdm_card_reader/ports.py new file mode 100644 index 0000000..731e18d --- /dev/null +++ b/src/rdm_card_reader/ports.py @@ -0,0 +1,397 @@ +"""Identify serial ports reliably, so the reader does not grab the wrong ttyUSB. + +A bare ``/dev/ttyUSB0`` is not a stable identity: unplug two adapters and the +numbers swap. This module answers "is this really the RFID reader?" using the +things that *are* stable: + +* the USB serial number (``/dev/serial/by-id``) +* the physical USB port (``/dev/serial/by-path``) +* the USB vendor:product id and the adapter description + +Use :func:`resolve` to turn a user-supplied selector into exactly one port, +refusing to guess when the answer is ambiguous. +""" + +from __future__ import annotations + +import glob +import os +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Sequence, Tuple + +#: USB-serial bridge chips commonly found on RDM6300 / Arduino adapters. +#: Mapping of ``vid:pid`` -> friendly name. Used for scoring, not required. +KNOWN_USB_SERIAL_CHIPS: Dict[Tuple[int, int], str] = { + (0x1A86, 0x7523): "CH340", + (0x1A86, 0x5523): "CH341", + (0x1A86, 0x55D4): "CH9102", + (0x0403, 0x6001): "FT232R", + (0x0403, 0x6015): "FT231X", + (0x10C4, 0xEA60): "CP2102", + (0x10C4, 0xEA70): "CP2105", + (0x067B, 0x2303): "PL2303", + (0x2341, 0x0043): "Arduino Uno", + (0x1A86, 0x55D3): "CH343", +} + +#: Description fragments that suggest a generic UART bridge. +_SERIAL_HINTS = ("serial", "uart", "usb-serial", "usbserial", "ttyusb", "ttyacm") + + +class PortError(RuntimeError): + """Base class for port selection problems.""" + + +class PortNotFound(PortError): + """No port matched the requested selector.""" + + +class AmbiguousPort(PortError): + """More than one port matched; picking one silently would be unsafe.""" + + +@dataclass +class PortInfo: + """Everything stable we can learn about one serial port.""" + + device: str + description: str = "" + hwid: str = "" + vid: Optional[int] = None + pid: Optional[int] = None + serial_number: Optional[str] = None + manufacturer: Optional[str] = None + product: Optional[str] = None + location: Optional[str] = None + aliases: List[str] = field(default_factory=list) + + # -- derived ---------------------------------------------------------- + + @property + def is_usb(self) -> bool: + """True when this port is a USB device (as opposed to an on-board UART).""" + return self.vid is not None or "usb" in self.device.lower() + + @property + def usb_id(self) -> Optional[str]: + """``"1a86:7523"`` when both ids are known.""" + if self.vid is None or self.pid is None: + return None + return "{:04x}:{:04x}".format(self.vid, self.pid) + + @property + def chip(self) -> Optional[str]: + """Friendly name of the USB-serial bridge, when recognised.""" + if self.vid is None or self.pid is None: + return None + return KNOWN_USB_SERIAL_CHIPS.get((self.vid, self.pid)) + + @property + def stable_id(self) -> Optional[str]: + """A ``/dev/serial/by-id`` path, when the kernel provides one. + + This is the value to persist in a config file - it survives replugging + and reordering. + """ + for alias in self.aliases: + if "/by-id/" in alias: + return alias + return None + + @property + def by_path(self) -> Optional[str]: + """A ``/dev/serial/by-path`` path - stable per physical USB socket.""" + for alias in self.aliases: + if "/by-path/" in alias: + return alias + return None + + def score(self) -> int: + """Likely-to-be-an-RFID-adapter score. Higher is better. + + Deliberately conservative: a plain on-board ``/dev/ttyS0`` scores low + so that a real USB adapter wins the autodetect race. + """ + value = 0 + if self.chip: + value += 60 + elif self.is_usb: + value += 40 + if self.serial_number: + value += 20 # uniquely identifiable + if self.stable_id: + value += 10 + if any(hint in self.description.lower() for hint in _SERIAL_HINTS): + value += 10 + if self.device.startswith("/dev/serial"): + value += 15 # kernel-managed alias + if self.device.startswith("/dev/ttyS"): + value += 5 # on-board UART, still valid but less specific + return value + + def _haystacks(self) -> List[str]: + parts = [ + self.device, + self.description, + self.hwid, + self.manufacturer or "", + self.product or "", + self.serial_number or "", + self.usb_id or "", + ] + parts.extend(self.aliases) + return [p.lower() for p in parts if p] + + def matches(self, selector: str) -> bool: + """True when *selector* identifies this port. + + Accepts a device path, a ``by-id``/``by-path`` symlink, a serial + number, ``vid:pid``, a manufacturer/product fragment or a chip name. + """ + needle = selector.strip().lower() + if not needle: + return False + if self.device.lower() == needle: + return True + if self.serial_number and self.serial_number.lower() == needle: + return True + if self.usb_id and self.usb_id == needle: + return True + if self.chip and self.chip.lower() == needle: + return True + return any(needle in hay for hay in self._haystacks()) + + def to_dict(self) -> Dict[str, Any]: + return { + "device": self.device, + "description": self.description, + "usb_id": self.usb_id, + "chip": self.chip, + "serial_number": self.serial_number, + "manufacturer": self.manufacturer, + "product": self.product, + "location": self.location, + "stable_id": self.stable_id, + "by_path": self.by_path, + "is_usb": self.is_usb, + "score": self.score(), + } + + def describe(self) -> str: + """One-line summary for ``--list-ports``.""" + bits = [self.device] + if self.chip: + bits.append(self.chip) + elif self.description: + bits.append(self.description) + if self.usb_id: + bits.append(self.usb_id) + if self.serial_number: + bits.append("S/N {}".format(self.serial_number)) + return " :: ".join(bits) + + +# --------------------------------------------------------------------------- +# Discovery +# --------------------------------------------------------------------------- + +def _aliases_for(device: str) -> List[str]: + """Collect ``/dev/serial/by-id`` and ``by-path`` symlinks pointing at *device*.""" + found: List[str] = [] + for folder in ("by-id", "by-path"): + pattern = "/dev/serial/{}/**".format(folder) + for link in sorted(glob.glob(pattern, recursive=True)): + try: + if os.path.realpath(link) == os.path.realpath(device): + found.append(link) + except OSError: + continue + return found + + +def _info_from_sysfs(device: str) -> PortInfo: + """Fallback identification straight from sysfs (no pyserial needed).""" + info = PortInfo(device=device) + try: + info.aliases = _aliases_for(device) + except OSError: + pass + + name = os.path.basename(device) + base = "/sys/class/tty/{}/device".format(name) + if not os.path.exists(base): + return info + + def _read(relpath: str) -> Optional[str]: + try: + with open(os.path.join(base, relpath), "r") as handle: + return handle.read().strip() + except OSError: + return None + + vid = _read("../idVendor") + pid = _read("../idProduct") + if vid: + try: + info.vid = int(vid, 16) + except ValueError: + pass + if pid: + try: + info.pid = int(pid, 16) + except ValueError: + pass + + info.serial_number = _read("../serial") + info.manufacturer = _read("../manufacturer") + info.product = _read("../product") + interface = _read("interface") + if interface: + info.description = interface + return info + + +def discover() -> List[PortInfo]: + """List every serial port with its stable identity. + + Prefers pyserial's enumeration (richer metadata) and falls back to scanning + ``/dev`` plus sysfs, so it still works when pyserial is absent. + """ + ports: List[PortInfo] = [] + + try: + from serial.tools import list_ports as serial_list_ports + + for entry in serial_list_ports.comports(): + info = PortInfo( + device=entry.device, + description=entry.description or "", + hwid=entry.hwid or "", + vid=getattr(entry, "vid", None), + pid=getattr(entry, "pid", None), + serial_number=getattr(entry, "serial_number", None), + manufacturer=getattr(entry, "manufacturer", None), + product=getattr(entry, "product", None), + location=getattr(entry, "location", None), + ) + info.aliases = _aliases_for(entry.device) + ports.append(info) + + if ports: + return ports + except Exception: + pass + + # Fallback: scan /dev for tty devices, enriching from sysfs. + candidates = sorted(glob.glob("/dev/ttyUSB*")) + sorted(glob.glob("/dev/ttyACM*")) + candidates += sorted(glob.glob("/dev/ttyS*")) + sorted(glob.glob("/dev/ttyAMA*")) + candidates += sorted(glob.glob("/dev/serial/by-id/*")) + + seen = set() + for device in candidates: + real = os.path.realpath(device) + if real in seen: + continue + seen.add(real) + ports.append(_info_from_sysfs(device)) + + return ports + + +def best_usb_candidates(ports: Sequence[PortInfo]) -> List[PortInfo]: + """USB-serial ports sorted most-likely-first, duplicates by identity removed.""" + usb = [p for p in ports if p.is_usb] + usb.sort(key=lambda p: (-p.score(), p.device)) + + unique: List[PortInfo] = [] + identities = set() + for port in usb: + identity = port.stable_id or port.usb_id or port.device + if port.serial_number: + identity = "{}|{}".format(port.usb_id, port.serial_number) + if identity in identities: + continue + identities.add(identity) + unique.append(port) + return unique + + +# --------------------------------------------------------------------------- +# Resolution +# --------------------------------------------------------------------------- + +def resolve( + selectors: Optional[Sequence[str]] = None, + fallback_ports: Optional[Sequence[str]] = None, + autodetect: bool = False, +) -> Tuple[str, str]: + """Decide which port to open. + + Returns ``(device, reason)`` where *reason* explains the choice so the + caller can log it - being explicit about *why* a port was picked is the + whole point of this module. + + Raises :class:`PortNotFound` or :class:`AmbiguousPort` rather than + guessing. + """ + ports = discover() + + # 1. Explicit selectors always win. + if selectors: + matched: List[PortInfo] = [] + for selector in selectors: + for port in ports: + if port.matches(selector) and port not in matched: + matched.append(port) + + if not matched: + # The selector may be a raw path pyserial cannot enumerate. + for selector in selectors: + if os.path.exists(selector): + return selector, "explicit path {}".format(selector) + available = ", ".join(p.device for p in ports) or "none detected" + raise PortNotFound( + "no port matched {}. Detected ports: {}".format( + ", ".join(repr(s) for s in selectors), available + ) + ) + + if len(matched) > 1: + raise AmbiguousPort( + "{} ports matched: {}. Use --port with a serial number or " + "by-id path, or --list-ports to see them.".format( + len(matched), ", ".join(p.describe() for p in matched) + ) + ) + + port = matched[0] + return port.device, "matched {} ({})".format( + port.stable_id or port.usb_id or "selector", port.describe() + ) + + # 2. Autodetect: only trust a single unambiguous USB adapter. + if autodetect: + candidates = best_usb_candidates(ports) + if len(candidates) == 1: + port = candidates[0] + return port.device, "autodetected {} (score {})".format( + port.describe(), port.score() + ) + if len(candidates) > 1: + raise AmbiguousPort( + "{} USB serial ports found; refusing to guess. Use --port or " + "set a serial_number in the config. Candidates: {}".format( + len(candidates), ", ".join(p.describe() for p in candidates) + ) + ) + + # 3. Fall back to the fixed order, which is what the workstation app used. + for device in list(fallback_ports or []): + if os.path.exists(device): + return device, "first available of the default order" + + raise PortNotFound( + "no serial port found. Detected: {}".format( + ", ".join(p.describe() for p in ports) or "none" + ) + ) diff --git a/src/rdm_card_reader/py.typed b/src/rdm_card_reader/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/rdm_card_reader/reader.py b/src/rdm_card_reader/reader.py new file mode 100644 index 0000000..c10c816 --- /dev/null +++ b/src/rdm_card_reader/reader.py @@ -0,0 +1,129 @@ +"""Serial-port handling: open the RDM6300 and turn reads into events. + +The reader library is a hard dependency at runtime but is imported lazily so +that ``--demo`` and the unit tests keep working without it installed. +""" + +from __future__ import annotations + +import importlib +import os +from typing import Callable, Dict, List, Optional, Sequence + +from .card import CANDIDATE_PORTS, DEFAULT_HEARTBEAT_INTERVAL, CardEvent + +try: # pragma: no cover - depends on the environment + import rdm6300 +except ImportError: # pragma: no cover + rdm6300 = None # type: ignore[assignment] + +#: Called once per card event. Keep it fast - it runs on the reader thread. +EventCallback = Callable[[CardEvent], None] + + +class ReaderUnavailable(RuntimeError): + """Raised when the ``rdm6300`` library is not installed.""" + + +def available() -> bool: + """True when the ``rdm6300`` library can be imported.""" + return rdm6300 is not None + + +def reload_library() -> bool: + """Re-attempt the import, for when the library was installed at runtime. + + The module-level import above runs once at process start, so installing + afterwards is invisible until this is called. + """ + global rdm6300 + if rdm6300 is not None: + return True + try: + rdm6300 = importlib.import_module("rdm6300") + except ImportError: + rdm6300 = None + return False + return True + + +def _make_reader_class(callback: EventCallback): + """Build a ``BaseReader`` subclass that reports events via *callback*.""" + if rdm6300 is None: + raise ReaderUnavailable( + "the 'rdm6300' library is not installed (pip install rdm6300)" + ) + + class CardReader(rdm6300.BaseReader): # type: ignore[misc, valid-type] + """Fires *callback* for each insert, removal and checksum failure.""" + + def card_inserted(self, card) -> None: + callback(CardEvent.from_card("card_inserted", card, self.port)) + + def card_removed(self, card) -> None: + if card is None: + return + callback(CardEvent.from_card("card_removed", card, self.port)) + + def invalid_card(self, card) -> None: + callback(CardEvent.from_card("invalid_card", card, self.port)) + + return CardReader + + +def open_reader( + callback: EventCallback, + ports: Optional[Sequence[str]] = None, + heartbeat_interval: Optional[float] = DEFAULT_HEARTBEAT_INTERVAL, + on_error: Optional[Callable[[str, str], None]] = None, +): + """Open the first usable port and return the reader (not yet started). + + Returns ``None`` when no port could be opened. *on_error* receives + ``(port, message)`` for each failed attempt so the caller can print or log + the reason. + """ + candidates = list(ports) if ports else list(CANDIDATE_PORTS) + ReaderClass = _make_reader_class(callback) + + for port in candidates: + try: + reader = ReaderClass(port, heartbeat_interval=heartbeat_interval) + except Exception as exc: # port missing, permission denied, busy, ... + if on_error is not None: + on_error(port, str(exc)) + continue + return reader + + return None + + +def explain_error(port: str, message: str) -> str: + """Turn a pyserial error into an actionable hint.""" + lowered = message.lower() + if "permission denied" in lowered: + return ( + "permission denied - add the user to the 'dialout' group: " + "sudo usermod -aG dialout $USER (then log in again)" + ) + if "no such file" in lowered or "could not open port" in lowered: + return "device not found" + return message + + +def list_ports() -> Dict[str, List]: + """Report candidate ports and whatever pyserial can enumerate.""" + candidates = [(port, os.path.exists(port)) for port in CANDIDATE_PORTS] + + detected: List[str] = [] + try: + from serial.tools import list_ports as serial_list_ports + + detected = [ + "{} ({})".format(info.device, info.description) + for info in serial_list_ports.comports() + ] + except Exception: + detected = [] + + return {"candidates": candidates, "detected": detected} diff --git a/src/rdm_card_reader/wheels/SHA256SUMS b/src/rdm_card_reader/wheels/SHA256SUMS new file mode 100644 index 0000000..b57b74a --- /dev/null +++ b/src/rdm_card_reader/wheels/SHA256SUMS @@ -0,0 +1,4 @@ +0e60d743a6b8071498a56465f7719c96a5e92928f858bab1be2a0d606c9aa0f8 colorzero-2.0-py2.py3-none-any.whl +d491734803a9bc6036602e6b8bbd73fb25c33da88de0fcd11a40e890b8bc2d3b gpiozero-2.0.1.post3-py3-none-any.whl +c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0 pyserial-3.5-py2.py3-none-any.whl +afb067da8b8d23a477138ca67d4ff1d198f12d157b64668c9dd3cbd7df54e74e rdm6300-0.1.1-py3-none-any.whl diff --git a/src/rdm_card_reader/wheels/colorzero-2.0-py2.py3-none-any.whl b/src/rdm_card_reader/wheels/colorzero-2.0-py2.py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..d37236d13dbb7c5d9f281f784d868597e5311db3 GIT binary patch literal 26573 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 he<;t@+P?{|{6A59t5^ literal 0 HcmV?d00001 diff --git a/src/rdm_card_reader/wheels/gpiozero-2.0.1.post3-py3-none-any.whl b/src/rdm_card_reader/wheels/gpiozero-2.0.1.post3-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..61ea379866ff70c259719af391bc820a12594d59 GIT binary patch literal 151465 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ℜgE6SwK@b3dX3 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")]) + assert result == [("definitely_not_a_real_module_xyz", "pkg>=1")] + + +# -- pip command ------------------------------------------------------------ + +def test_pip_command_uses_this_interpreter(): + command = _pip_command(["pkg>=1"], wheel_dir=None) + assert command[:3] == [sys.executable, "-m", "pip"] + assert "pkg>=1" in command + + +def test_pip_command_prefers_local_wheels_when_given(): + command = _pip_command(["pkg>=1"], wheel_dir="/opt/wheels") + assert "--no-index" in command + assert "--find-links" in command + assert "/opt/wheels" in command + + +def test_pip_command_adds_break_system_packages_outside_venv(monkeypatch): + """Debian's pip refuses to touch system Python without this flag.""" + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + monkeypatch.setattr(bootstrap, "in_virtualenv", lambda: False) + assert "--break-system-packages" in _pip_command(["pkg"], None) + + +def test_pip_command_omits_flag_inside_venv(monkeypatch): + monkeypatch.setattr(bootstrap, "in_virtualenv", lambda: True) + assert "--break-system-packages" not in _pip_command(["pkg"], None) + + +def test_in_virtualenv_detects_env_var(monkeypatch): + monkeypatch.setenv("VIRTUAL_ENV", "/tmp/venv") + assert bootstrap.in_virtualenv() is True + + +# -- install ---------------------------------------------------------------- + +def test_install_reports_success(monkeypatch): + monkeypatch.setattr( + bootstrap.subprocess, "run", lambda *a, **k: FakeCompleted(0) + ) + assert install(["pkg"]) is True + + +def test_install_returns_false_on_nonzero_exit(monkeypatch): + monkeypatch.setattr( + bootstrap.subprocess, "run", + lambda *a, **k: FakeCompleted(1, stderr="ERROR: no matching distribution"), + ) + lines = [] + assert install(["pkg"], logger=lines.append) is False + assert any("no matching distribution" in line for line in lines) + + +def test_install_survives_timeout(monkeypatch): + def boom(*args, **kwargs): + raise bootstrap.subprocess.TimeoutExpired(cmd="pip", timeout=1) + + monkeypatch.setattr(bootstrap.subprocess, "run", boom) + assert install(["pkg"]) is False + + +def test_install_survives_pip_being_unrunnable(monkeypatch): + def boom(*args, **kwargs): + raise OSError("no such file or directory") + + monkeypatch.setattr(bootstrap.subprocess, "run", boom) + assert install(["pkg"]) is False + + +# -- ensure_dependencies ---------------------------------------------------- + +def test_no_install_when_everything_present(monkeypatch): + """A normal start must not shell out to pip at all.""" + called = [] + monkeypatch.setattr(bootstrap, "missing_packages", lambda packages=None: []) + monkeypatch.setattr(bootstrap, "install", lambda *a, **k: called.append(1)) + assert ensure_dependencies() is True + assert called == [] + + +def test_installs_when_missing(monkeypatch): + state = {"installed": False} + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if state["installed"] else [("rdm6300", "rdm6300>=0.1.1")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + state["installed"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + assert ensure_dependencies() is True + + +def test_reports_failure_when_still_missing(monkeypatch): + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [("rdm6300", "rdm6300>=0.1.1")], + ) + monkeypatch.setattr(bootstrap, "install", lambda *a, **k: False) + lines = [] + assert ensure_dependencies(logger=lines.append) is False + assert any("Install manually with" in line for line in lines) + + +def test_no_install_flag_reports_manual_hint(monkeypatch): + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [("rdm6300", "rdm6300>=0.1.1")], + ) + called = [] + monkeypatch.setattr(bootstrap, "install", lambda *a, **k: called.append(1)) + lines = [] + assert ensure_dependencies(auto_install=False, logger=lines.append) is False + assert called == [] # never shelled out + assert any("disabled" in line for line in lines) + + +def test_wheel_dir_is_passed_through(monkeypatch): + """An explicit --wheel-dir that covers everything is used as given.""" + seen = {} + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if seen.get("done") else [("x", "x>=1")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + seen["wheel_dir"] = wheel_dir + seen["done"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + + with tempfile.TemporaryDirectory() as folder: + open(os.path.join(folder, "x-1.0-py3-none-any.whl"), "wb").close() + ensure_dependencies(wheel_dir=folder) + assert seen["wheel_dir"] == folder + + +def test_incomplete_explicit_wheel_dir_is_flagged(monkeypatch): + """An unusable --wheel-dir should be called out, not silently ignored.""" + seen = {} + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if seen.get("done") else [("x", "x>=1")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + seen["done"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + lines = [] + with tempfile.TemporaryDirectory() as folder: + ensure_dependencies(wheel_dir=folder, logger=lines.append) + assert any("you gave is incomplete" in line for line in lines) diff --git a/tests/test_bridge.py b/tests/test_bridge.py new file mode 100644 index 0000000..7fdfa9f --- /dev/null +++ b/tests/test_bridge.py @@ -0,0 +1,501 @@ +"""Tests for the HTTP bridge. + +Two things matter most here: + +1. The bridge publishes card events without claiming to authenticate anyone. +2. A page on another origin must not be able to read your taps. +""" + +import json +import os +import sys +import threading +import time +import urllib.error +import urllib.request + +SRC = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src" +) +if SRC not in sys.path: + sys.path.insert(0, SRC) + +import pytest # noqa: E402 + +from rdm_card_reader import bridge # noqa: E402 +from rdm_card_reader.bridge import ( # noqa: E402 + MODE_LOGIN, + MODE_MONITOR, + EventHub, + create_server, +) +from rdm_card_reader.card import CardEvent # noqa: E402 + + +def make_event(event="card_inserted", value=12886197): + return CardEvent( + event=event, + timestamp="2026-10-01 12:00:00", + value=value, + checksum=222, + card_type=15, + is_valid=True, + port="/dev/ttyUSB0", + ) + + +# -- hub -------------------------------------------------------------------- + +def test_hub_publishes_to_subscriber(): + hub = EventHub() + queue = hub.subscribe() + hub.publish(make_event()) + assert len(queue) == 1 + assert queue[0]["id"] == 12886197 + + +def test_hub_login_mode_hides_removals(): + """Removal must not look like a second authentication attempt.""" + hub = EventHub(mode=MODE_LOGIN) + queue = hub.subscribe() + hub.publish(make_event("card_removed")) + assert len(queue) == 0 + + +def test_hub_monitor_mode_publishes_removals(): + hub = EventHub(mode=MODE_MONITOR) + queue = hub.subscribe() + hub.publish(make_event("card_removed")) + assert len(queue) == 1 + + +def test_hub_unsubscribe_stops_delivery(): + hub = EventHub() + queue = hub.subscribe() + hub.unsubscribe(queue) + hub.publish(make_event()) + assert len(queue) == 0 + + +def test_hub_tracks_last_tap_only_on_insert(): + hub = EventHub(mode=MODE_MONITOR) + hub.publish(make_event("card_inserted", value=111)) + hub.publish(make_event("card_removed", value=111)) + assert hub.last()["id"] == 111 + + +def test_hub_history_is_bounded(): + hub = EventHub(history=3) + for value in range(10): + hub.publish(make_event(value=value)) + assert len(hub.taps(limit=99)) == 3 + + +def test_hub_health_reports_state(): + hub = EventHub() + hub.set_port("/dev/ttyUSB0") + hub.publish(make_event()) + health = hub.health() + assert health["port"] == "/dev/ttyUSB0" + assert health["counts"]["card_inserted"] == 1 + assert health["last_tap"]["id"] == 12886197 + assert health["last_tap_age_seconds"] is not None + + +def test_hub_rejects_unknown_mode(): + hub = EventHub() + assert hub.set_mode("bogus") is False + assert hub.mode == MODE_LOGIN + + +def test_hub_accepts_valid_mode_switch(): + hub = EventHub() + assert hub.set_mode(MODE_MONITOR) is True + assert hub.mode == MODE_MONITOR + + +# -- HTTP ------------------------------------------------------------------- + +@pytest.fixture +def server(): + """A live bridge on an ephemeral port, torn down after each test.""" + hub = EventHub() + hub.set_port("/dev/ttyUSB0") + srv = create_server(hub, host="127.0.0.1", port=0) + thread = threading.Thread(target=srv.serve_forever, daemon=True) + thread.start() + yield srv, hub + srv.shutdown() + srv.server_close() + + +def base_url(srv): + host, port = srv.server_address[:2] + return "http://{}:{}".format(host, port) + + +def get(srv, path, headers=None): + request = urllib.request.Request(base_url(srv) + path, headers=headers or {}) + with urllib.request.urlopen(request, timeout=5) as response: + return response.status, response.headers, response.read() + + +def test_health_endpoint(server): + srv, hub = server + status, _, body = get(srv, "/api/health") + assert status == 200 + payload = json.loads(body) + assert payload["status"] == "ok" + assert payload["port"] == "/dev/ttyUSB0" + assert payload["mode"] == MODE_LOGIN + + +def test_last_returns_204_when_nothing_read(server): + srv, _ = server + status, _, _ = get(srv, "/api/last") + assert status == 204 + + +def test_last_returns_the_tap(server): + srv, hub = server + hub.publish(make_event()) + status, _, body = get(srv, "/api/last") + assert status == 200 + assert json.loads(body)["id"] == 12886197 + + +def test_taps_endpoint_honours_limit(server): + srv, hub = server + for value in range(5): + hub.publish(make_event(value=value)) + _, _, body = get(srv, "/api/taps?limit=2") + assert len(json.loads(body)["taps"]) == 2 + + +def test_unknown_path_is_404(server): + srv, _ = server + with pytest.raises(urllib.error.HTTPError) as excinfo: + get(srv, "/api/nope") + assert excinfo.value.code == 404 + + +def test_health_available_without_origin(server): + """Monitoring must work for a plain client with no Origin header.""" + srv, _ = server + status, _, _ = get(srv, "/api/health") + assert status == 200 + + +# -- origin policy ---------------------------------------------------------- + +def test_cross_origin_refused_when_no_allowlist(server): + """A third-party page must not be able to read card ids.""" + srv, _ = server + with pytest.raises(urllib.error.HTTPError) as excinfo: + get(srv, "/api/last", headers={"Origin": "https://evil.example"}) + assert excinfo.value.code == 403 + + +def test_allowlisted_origin_permitted_and_echoed(): + hub = EventHub() + srv = create_server(hub, host="127.0.0.1", port=0, + allowed_origins=["https://kiosk.example"]) + thread = threading.Thread(target=srv.serve_forever, daemon=True) + thread.start() + try: + hub.publish(make_event()) + status, headers, body = get( + srv, "/api/last", headers={"Origin": "https://kiosk.example"} + ) + assert status == 200 + assert headers.get("Access-Control-Allow-Origin") == "https://kiosk.example" + assert json.loads(body)["id"] == 12886197 + finally: + srv.shutdown() + srv.server_close() + + +def test_unlisted_origin_still_refused_when_allowlist_set(): + hub = EventHub() + srv = create_server(hub, host="127.0.0.1", port=0, + allowed_origins=["https://kiosk.example"]) + thread = threading.Thread(target=srv.serve_forever, daemon=True) + thread.start() + try: + with pytest.raises(urllib.error.HTTPError) as excinfo: + get(srv, "/api/last", headers={"Origin": "https://other.example"}) + assert excinfo.value.code == 403 + finally: + srv.shutdown() + srv.server_close() + + +def test_token_not_required_from_loopback(): + hub = EventHub() + srv = create_server(hub, host="127.0.0.1", port=0, token="sekrit") + thread = threading.Thread(target=srv.serve_forever, daemon=True) + thread.start() + try: + status, _, _ = get(srv, "/api/health") + assert status == 200 + finally: + srv.shutdown() + srv.server_close() + + +# -- mode switching --------------------------------------------------------- + +def post(srv, path, payload, headers=None): + data = json.dumps(payload).encode() + request = urllib.request.Request( + base_url(srv) + path, data=data, method="POST", + headers={"Content-Type": "application/json", **(headers or {})}, + ) + with urllib.request.urlopen(request, timeout=5) as response: + return response.status, response.read() + + +def test_mode_can_be_switched(server): + srv, hub = server + status, body = post(srv, "/api/mode", {"mode": MODE_MONITOR}) + assert status == 200 + assert json.loads(body)["mode"] == MODE_MONITOR + assert hub.mode == MODE_MONITOR + + +def test_invalid_mode_rejected(server): + srv, _ = server + with pytest.raises(urllib.error.HTTPError) as excinfo: + post(srv, "/api/mode", {"mode": "nonsense"}) + assert excinfo.value.code == 400 + + +def test_malformed_json_rejected(server): + srv, _ = server + request = urllib.request.Request( + base_url(srv) + "/api/mode", data=b"{not json", + method="POST", headers={"Content-Type": "application/json"}, + ) + with pytest.raises(urllib.error.HTTPError) as excinfo: + urllib.request.urlopen(request, timeout=5) + assert excinfo.value.code == 400 + + +# -- SSE -------------------------------------------------------------------- + +def test_sse_stream_delivers_a_tap(server): + """The stream used by the web page must push a tap as it happens.""" + srv, hub = server + received = [] + + def listen(): + with urllib.request.urlopen(base_url(srv) + "/api/events", timeout=10) as resp: + for raw in resp: + line = raw.decode("utf-8").strip() + if line.startswith("data: "): + received.append(json.loads(line[6:])) + return + + thread = threading.Thread(target=listen, daemon=True) + thread.start() + time.sleep(0.4) # let the subscription register + hub.publish(make_event()) + thread.join(timeout=6) + assert received and received[0]["id"] == 12886197 + + +def test_loopback_helper(): + assert bridge.is_loopback("127.0.0.1") is True + assert bridge.is_loopback("0.0.0.0") is False + + +# -- latched card (the "always available" behaviour) ------------------------ + +def test_card_not_present_before_any_tap(): + hub = EventHub() + state = hub.current_card() + assert state["present"] is False + assert state["card"] is None + + +def test_card_stays_available_after_the_page_loads(): + """The tap must outlive the page that reads it.""" + hub = EventHub() + hub.publish(make_event(value=555)) + for _ in range(3): # simulated later page loads + state = hub.current_card() + assert state["present"] is True + assert state["card"]["id"] == 555 + + +def test_card_cleared_when_removed(): + """Lifting the card must NOT lose the tap - only mark it as gone.""" + hub = EventHub(tap_ttl=30) + hub.publish(make_event()) + hub.publish(make_event("card_removed")) + state = hub.current_card() + assert state["present"] is False # no longer physically on the reader + assert state["claimable"] is True # but still spendable + assert state["card"]["id"] == 12886197 + + +def test_quick_tap_is_not_lost(): + """The real-world case: card lifted before the page polls.""" + hub = EventHub(tap_ttl=10) + hub.publish(make_event(value=31337)) + hub.publish(make_event("card_removed")) # lifted immediately + payload, error, _ = hub.consume_card() + assert error is None + assert payload["card"]["id"] == 31337 + + +def test_claim_returns_the_card_once(): + hub = EventHub() + hub.publish(make_event(value=777)) + payload, error, status = hub.consume_card() + assert error is None and status == 200 + assert payload["card"]["id"] == 777 + assert payload["consumed"] is True + + +def test_claim_is_single_use(): + """A page reload must not turn one physical tap into two logins.""" + hub = EventHub() + hub.publish(make_event()) + assert hub.consume_card()[1] is None + payload, error, status = hub.consume_card() + assert payload is None + assert error == "card already used" + assert status == 409 + + +def test_claim_without_a_card_is_conflict(): + hub = EventHub() + payload, error, status = hub.consume_card() + assert payload is None + assert error == "no card read yet" + assert status == 409 + + +def test_removing_and_replacing_the_card_allows_a_new_claim(): + """Lifting the card and tapping it again is a fresh authentication.""" + hub = EventHub(tap_ttl=30) + hub.publish(make_event(value=111)) + assert hub.consume_card()[1] is None + hub.publish(make_event("card_removed")) + hub.publish(make_event(value=111)) + payload, error, _ = hub.consume_card() + assert error is None + assert payload["card"]["id"] == 111 + + +def test_card_endpoint_reports_claimable_after_lift(server): + srv, hub = server + hub.publish(make_event(value=8080)) + hub.publish(make_event("card_removed")) + _, _, body = get(srv, "/api/card") + payload = json.loads(body) + assert payload["present"] is False + assert payload["claimable"] is True + assert payload["card"]["id"] == 8080 + + +def test_claim_expires_after_ttl(): + hub = EventHub(tap_ttl=0.05) + hub.publish(make_event()) + time.sleep(0.12) + payload, error, status = hub.consume_card() + assert payload is None + assert error == "card tap expired" + assert status == 410 + + +def test_ttl_zero_never_expires_while_present(): + hub = EventHub(tap_ttl=0) + hub.publish(make_event()) + state = hub.current_card() + assert state["expired"] is False + assert state["claimable"] is True + + +def test_tap_becomes_unclaimable_after_ttl(): + hub = EventHub(tap_ttl=0.05) + hub.publish(make_event()) + time.sleep(0.12) + assert hub.current_card()["claimable"] is False + + +def test_a_new_tap_replaces_an_unclaimed_one(): + hub = EventHub(tap_ttl=30) + hub.publish(make_event(value=111)) + hub.publish(make_event(value=222)) + payload, error, _ = hub.consume_card() + assert error is None + assert payload["card"]["id"] == 222 + + +def test_health_shows_card_presence(): + hub = EventHub() + hub.publish(make_event()) + health = hub.health() + assert health["card_present"] is True + assert health["card_consumed"] is False + + +# -- latched card over HTTP ------------------------------------------------- + +def test_card_endpoint_reports_presence(server): + srv, hub = server + _, _, body = get(srv, "/api/card") + assert json.loads(body)["present"] is False + + hub.publish(make_event(value=4242)) + _, _, body = get(srv, "/api/card") + payload = json.loads(body) + assert payload["present"] is True + assert payload["card"]["id"] == 4242 + + +def test_claim_endpoint_works_once(server): + srv, hub = server + hub.publish(make_event(value=9999)) + + status, body = post(srv, "/api/card/claim", {}) + assert status == 200 + assert json.loads(body)["card"]["id"] == 9999 + + with pytest.raises(urllib.error.HTTPError) as excinfo: + post(srv, "/api/card/claim", {}) + assert excinfo.value.code == 409 + detail = json.loads(excinfo.value.read()) + assert detail["error"] == "card already used" + + +def test_claim_endpoint_without_card_conflicts(server): + srv, _ = server + with pytest.raises(urllib.error.HTTPError) as excinfo: + post(srv, "/api/card/claim", {}) + assert excinfo.value.code == 409 + + +def test_claim_refused_from_other_origin(server): + """Another site must not be able to claim the card either.""" + srv, hub = server + hub.publish(make_event()) + with pytest.raises(urllib.error.HTTPError) as excinfo: + post(srv, "/api/card/claim", {}, + headers={"Origin": "https://evil.example"}) + assert excinfo.value.code == 403 + # the tap must still be claimable by the real caller + assert hub.current_card()["consumed"] is False + + +def test_claim_does_not_consume_on_refused_origin(server): + srv, hub = server + hub.publish(make_event(value=1234)) + with pytest.raises(urllib.error.HTTPError): + post(srv, "/api/card/claim", {}, + headers={"Origin": "https://evil.example"}) + payload, error, _ = hub.consume_card() + assert error is None + assert payload["card"]["id"] == 1234 diff --git a/tests/test_card.py b/tests/test_card.py new file mode 100644 index 0000000..2d5decb --- /dev/null +++ b/tests/test_card.py @@ -0,0 +1,111 @@ +"""The event model and its output formatting.""" + +import json + +import pytest + +from rdm_card_reader.card import ( + CONFIG_CARD_VALUE, + CardEvent, + format_event, +) + +SAMPLE = CardEvent( + event="card_inserted", + timestamp="2026-10-01 12:00:00", + value=0x00C4A0B5, + checksum=0xDE, + card_type=0x0F, + is_valid=True, + port="/dev/ttyS0", +) + + +def test_hex_value_is_zero_padded_to_eight_digits(): + assert SAMPLE.hex_value == "0x00C4A0B5" + + +def test_from_card_accepts_any_object_with_the_right_fields(): + class FakeCard: + value = 1234 + checksum = 5 + type = 15 + is_valid = True + + event = CardEvent.from_card("card_inserted", FakeCard(), "/dev/ttyUSB0") + assert event.value == 1234 + assert event.hex_value == "0x000004D2" + assert event.port == "/dev/ttyUSB0" + + +def test_to_dict_has_the_documented_keys(): + assert set(SAMPLE.to_dict()) == { + "event", "timestamp", "id", "hex", "type", "checksum", "is_valid", "port", + } + + +def test_to_json_is_parseable(): + assert json.loads(SAMPLE.to_json())["hex"] == "0x00C4A0B5" + + +def test_inserted_block_lists_the_card_data(): + text = format_event(SAMPLE) + assert "CARD INSERTED" in text + assert "12886197" in text + assert "0x00C4A0B5" in text + assert "0x0F" in text + assert "222" in text + assert "True" in text + + +def test_config_card_is_flagged_in_the_note(): + event = CardEvent( + event="card_inserted", + timestamp="2026-10-01 12:00:00", + value=CONFIG_CARD_VALUE, + checksum=0, + card_type=0x0F, + is_valid=True, + port="/dev/ttyS0", + ) + text = format_event(event) + assert "config card" in text + assert str(CONFIG_CARD_VALUE) in text + + +def test_ordinary_card_has_no_config_note(): + assert "config card" not in format_event(SAMPLE) + + +def test_removed_line_is_single_line(): + event = CardEvent( + event="card_removed", + timestamp="2026-10-01 12:00:00", + value=0x00C4A0B5, + checksum=0xDE, + card_type=0x0F, + is_valid=True, + port="/dev/ttyS0", + ) + text = format_event(event) + assert text.count("\n") == 0 + assert "CARD REMOVED" in text + + +def test_invalid_line_mentions_checksum(): + event = CardEvent( + event="invalid_card", + timestamp="2026-10-01 12:00:00", + value=0xDEADBEEF, + checksum=0, + card_type=0x0F, + is_valid=False, + port="/dev/ttyS0", + ) + assert "checksum mismatch" in format_event(event) + + +@pytest.mark.parametrize("value,expected", [(0, "0x00000000"), (0xFFFFFFFF, "0xFFFFFFFF")]) +def test_hex_value_boundaries(value, expected): + event = CardEvent("card_inserted", "-", value, 0, 0x0F, True, "p") + assert event.hex_value == expected diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..bb44737 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,93 @@ +"""CLI behaviour: exit codes, output modes and argument handling.""" + +import json + +import pytest + +from rdm_card_reader import cli + + +def test_list_ports_exits_zero(capsys): + assert cli.main(["--list-ports"]) == cli.EXIT_OK + out = capsys.readouterr().out + assert "/dev/ttyS0" in out + + +def test_demo_exits_zero(capsys): + assert cli.main(["--demo", "--demo-count", "1"]) == cli.EXIT_OK + assert "CARD INSERTED" in capsys.readouterr().out + + +def test_demo_json_emits_one_object_per_event(capsys): + cli.main(["--demo", "--demo-count", "1", "--json"]) + lines = [line for line in capsys.readouterr().out.splitlines() if line.strip()] + events = [json.loads(line) for line in lines if line.startswith("{")] + assert events + assert {"event", "timestamp", "id", "hex"} <= set(events[0]) + + +def test_demo_count_is_respected(capsys): + cli.main(["--demo", "--demo-count", "4"]) + assert capsys.readouterr().out.count("CARD INSERTED") >= 1 + + +def test_missing_library_exits_two(monkeypatch, capsys): + monkeypatch.setattr(cli, "available", lambda: False) + assert cli.main([]) == cli.EXIT_NO_LIBRARY + assert "rdm6300" in capsys.readouterr().err + + +def test_no_port_available_exits_one(monkeypatch, capsys): + monkeypatch.setattr(cli, "available", lambda: True) + monkeypatch.setattr(cli, "open_reader", lambda *a, **kw: None) + assert cli.main(["--port", "/dev/nope"]) == cli.EXIT_NO_PORT + assert "Could not open any serial port" in capsys.readouterr().err + + +def test_explicit_port_is_the_only_one_tried(monkeypatch): + monkeypatch.setattr(cli, "available", lambda: True) + tried = [] + + def fake_open(callback, ports=None, **kwargs): + tried.extend(ports or []) + return None + + monkeypatch.setattr(cli, "open_reader", fake_open) + cli.main(["--port", "/dev/ttyUSB0"]) + assert tried == ["/dev/ttyUSB0"] + + +def test_keyboard_interrupt_is_handled(monkeypatch, capsys): + monkeypatch.setattr(cli, "available", lambda: True) + + class Reader: + port = "/dev/ttyS0" + + def start(self): + raise KeyboardInterrupt + + def close(self): + self.closed = True + + reader = Reader() + monkeypatch.setattr(cli, "open_reader", lambda *a, **kw: reader) + + assert cli.main([]) == cli.EXIT_OK + assert reader.closed is True + assert "Stopped by user" in capsys.readouterr().out + + +def test_version_flag_exits_zero(capsys): + with pytest.raises(SystemExit) as exc: + cli.main(["--version"]) + assert exc.value.code == 0 + assert "rdm-card-reader" in capsys.readouterr().out + + +def test_json_mode_prints_json_from_the_callback(capsys): + printer = cli._make_printer(as_json=True) + from rdm_card_reader.card import CardEvent + + printer(CardEvent("card_inserted", "-", 1, 2, 15, True, "p")) + payload = json.loads(capsys.readouterr().out.strip()) + assert payload["id"] == 1 diff --git a/tests/test_deploy.py b/tests/test_deploy.py new file mode 100644 index 0000000..f537463 --- /dev/null +++ b/tests/test_deploy.py @@ -0,0 +1,199 @@ +"""Tests for the deploy artifacts. + +These are the files that only ever run on the target device, so a mistake in +them is found late and painfully. The checks here are the ones that would have +caught real bugs: the install must not depend on the network, and the service +must not reference something the installer does not create. +""" + +import os +import re +import stat + +HERE = os.path.dirname(os.path.abspath(__file__)) +PROJECT = os.path.dirname(HERE) +DEPLOY = os.path.join(PROJECT, "deploy") + +INSTALL_SH = os.path.join(DEPLOY, "install.sh") +UNIT = os.path.join(DEPLOY, "rdm-card-reader.service") +RULES = os.path.join(DEPLOY, "99-rdm-reader.rules") + + +def read(path): + with open(path, "r") as handle: + return handle.read() + + +# -- install.sh ------------------------------------------------------------- + +def test_installer_exists_and_is_executable(): + assert os.path.exists(INSTALL_SH) + mode = os.stat(INSTALL_SH).st_mode + assert mode & stat.S_IXUSR, "install.sh should be executable" + + +def test_installer_does_not_pip_install_the_project(): + """`pip install .` needs setuptools from the network, so it breaks offline. + + This is the bug that made installation fail on an air-gapped Pi with + "Could not find a version that satisfies the requirement setuptools>=68". + """ + text = read(INSTALL_SH) + code = "\n".join( + line for line in text.splitlines() if not line.strip().startswith("#") + ) + assert not re.search(r'pip"?\s+install[^\n]*"\$INSTALL_DIR"', code), ( + "install.sh must not 'pip install' the project directory: it requires " + "downloading setuptools and fails with no network" + ) + assert not re.search(r"pip\s+install\s+\.\s*$", code, re.M) + + +def test_installer_installs_runtime_deps_from_bundled_wheels(): + text = read(INSTALL_SH) + assert "--no-index" in text + assert "--find-links" in text + assert "wheels" in text + + +def test_installer_adds_src_to_path(): + """Without this the package is not importable at all.""" + text = read(INSTALL_SH) + assert ".pth" in text + + +def test_installer_network_steps_are_not_fatal(): + """A device with no internet must still get a working install.""" + text = read(INSTALL_SH) + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("apt-get"): + assert "|| " in line or line.rstrip().endswith("\\"), ( + "network-dependent step should not abort the install: " + stripped + ) + # The pip self-upgrade is best-effort too. The fallback may sit on the + # following line (line continuation), so join continuations first. + joined = text.replace("\\\n", " ") + assert re.search(r"upgrade pip[^\n]*\|\|", joined), ( + "'pip install --upgrade pip' needs the network, so it must not abort " + "the install; add '|| ...' to it" + ) + + +def test_installer_copies_the_wheels_with_the_source(): + """The bundled wheels live under src/, so src/ must be copied.""" + text = read(INSTALL_SH) + match = re.search(r"for item in ([^\n;]+);", text) + assert match, "could not find the copy list in install.sh" + assert "src" in match.group(1) + + +def test_installer_checks_venv_creation_can_fail_clearly(): + text = read(INSTALL_SH) + assert "python3-venv" in text, "should mention the usual cause of venv failure" + + +def test_installer_verifies_the_install_before_enabling_service(): + text = read(INSTALL_SH) + assert "--version" in text, "installer should sanity-check the install" + + +def test_installer_copies_launch_doc(): + text = read(INSTALL_SH) + assert "LAUNCH.md" in text + + +# -- service unit ----------------------------------------------------------- + +def test_unit_exists(): + assert os.path.exists(UNIT) + + +def test_unit_restarts_on_failure(): + text = read(UNIT) + assert "Restart=always" in text + assert "RestartSec=" in text + + +def test_unit_runs_as_a_non_root_user_in_dialout(): + text = read(UNIT) + assert re.search(r"^User=", text, re.M) + assert re.search(r"^Group=dialout", text, re.M) + + +def _sections(text): + """Split an INI-style unit into {section: body}. + + Splitting on the literal string "[Service]" is wrong: the file mentions it + in a comment above the real section header. + """ + sections = {} + current = None + for line in text.splitlines(): + header = re.match(r"^\[([^\]]+)\]\s*$", line) + if header: + current = header.group(1) + sections[current] = [] + elif current is not None: + sections[current].append(line) + return {name: "\n".join(body) for name, body in sections.items()} + + +def test_unit_start_limits_are_in_the_unit_section(): + """systemd silently ignores these in [Service].""" + sections = _sections(read(UNIT)) + assert "Unit" in sections and "Service" in sections + for key in ("StartLimitIntervalSec", "StartLimitBurst"): + assert key not in sections["Service"], ( + "{} belongs in [Unit]; systemd ignores it in [Service]".format(key) + ) + assert "StartLimitBurst" in sections["Unit"] + + +def test_unit_has_the_expected_sections(): + sections = _sections(read(UNIT)) + assert set(sections) >= {"Unit", "Service", "Install"} + + +def test_unit_uses_loopback_by_default(): + text = read(UNIT) + # No --host flag means the CLI default (127.0.0.1) applies. + assert "--host" not in text + + +def test_unit_serves_in_login_mode(): + text = read(UNIT) + assert "--serve" in text + assert "--mode login" in text + + +def test_unit_points_at_the_stable_device_alias(): + text = read(UNIT) + assert "/dev/rdm-reader" in text + + +# -- udev rule -------------------------------------------------------------- + +def test_udev_rule_creates_the_alias_the_unit_expects(): + text = read(RULES) + assert 'SYMLINK+="rdm-reader"' in text + unit = read(UNIT) + assert "/dev/rdm-reader" in unit, "unit and rule must agree on the alias name" + + +def test_udev_rule_sets_group_and_mode(): + text = read(RULES) + assert 'GROUP="dialout"' in text + assert 'MODE="0660"' in text + + +def test_udev_rule_only_has_one_active_match(): + """Multiple active rules would fight over the same alias.""" + active = [ + line + for line in read(RULES).splitlines() + if line.strip().startswith('SUBSYSTEM=="tty"') + ] + assert len(active) == 1, "exactly one rule should be active, found {}".format( + len(active) + ) diff --git a/tests/test_e2e_bridge.py b/tests/test_e2e_bridge.py new file mode 100644 index 0000000..4ef3261 --- /dev/null +++ b/tests/test_e2e_bridge.py @@ -0,0 +1,177 @@ +"""End-to-end test: a real tap must be claimable by a web page at any time. + +Simulates the RDM6300 on a pseudo-terminal, starts the actual bridge the way +systemd would, then walks the exact flow a web page performs: + + card placed -> page loads -> polls /api/card -> claims it -> hand over once + +Also asserts the two things that matter for security: the tap survives until +claimed, and it cannot be claimed twice. +""" + +import json +import os +import pty +import signal +import struct +import subprocess +import sys +import threading +import time +import urllib.error +import urllib.request + +HERE = os.path.dirname(os.path.abspath(__file__)) +PROJECT = os.path.dirname(HERE) +VENV_PY = os.path.join(PROJECT, ".venv", "bin", "python") +PYTHON = VENV_PY if os.path.exists(VENV_PY) else sys.executable + +CARD_VALUE = 0x00C4A0B5 +EXPECTED_ID = 12886197 + + +def em4100_frame(value, card_type=0x0F): + """12 hex nibbles: type + 4 id bytes + XOR checksum of those 5 bytes.""" + id_bytes = struct.pack(">I", value) + checksum = card_type + for b in id_bytes: + checksum ^= b + frame = (bytes([card_type]) + id_bytes + bytes([checksum])).hex().upper() + assert len(frame) == 12, frame + return frame.encode("ascii") + + +def main(): + master, slave = pty.openpty() + serial_port = os.ttyname(slave) + + # Port 0 would be cleaner but the CLI needs a real number; pick a free one. + import socket + probe = socket.socket() + probe.bind(("127.0.0.1", 0)) + http_port = probe.getsockname()[1] + probe.close() + + proc = subprocess.Popen( + [PYTHON, "-m", "rdm_card_reader", "--serve", + "--port", serial_port, "--bridge-port", str(http_port), + "--mode", "login", "--tap-ttl", "15", + "--allow-origin", "https://kiosk.example"], + cwd=PROJECT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, start_new_session=True, + ) + + output = [] + lock = threading.Lock() + + def pump(): + for line in proc.stdout: + with lock: + output.append(line.rstrip()) + + threading.Thread(target=pump, daemon=True).start() + + base = "http://127.0.0.1:{}".format(http_port) + results = {} + + def api(path, method="GET", headers=None): + request = urllib.request.Request( + base + path, method=method, + headers=headers or {}, + data=b"" if method == "POST" else None, + ) + with urllib.request.urlopen(request, timeout=5) as response: + body = response.read() + return response.status, (json.loads(body) if body else None) + + try: + # Wait for the HTTP server to come up. + deadline = time.time() + 15 + while time.time() < deadline: + try: + api("/api/health") + break + except Exception: + time.sleep(0.3) + + results["bridge started"] = True + + # The child needs a moment to open the serial port; frames written + # before that are discarded, which would look like a reader bug. + time.sleep(2.0) + + # --- no card yet ------------------------------------------------- + _, state = api("/api/card") + results["no card -> nothing claimable"] = state["claimable"] is False + + # --- a single tap, as a person would ------------------------------- + frame = em4100_frame(CARD_VALUE) + os.write(master, b"\x02" + frame + b"\x03") + time.sleep(0.3) + + # --- a page loads later and polls; the card must still be there ---- + time.sleep(1.0) + _, state = api("/api/card") + with lock: + print("state after tap: {}".format(json.dumps(state)), flush=True) + for line in output[-12:]: + print(" " + line) + results["card still available later"] = bool( + state["claimable"] and state["card"]["id"] == EXPECTED_ID + ) + results["card not consumed yet"] = state["consumed"] is False + + # --- claim it, as the web page does ------------------------------- + status, claimed = api( + "/api/card/claim", method="POST", + headers={"Origin": "https://kiosk.example"}, + ) + results["claim returns 200"] = status == 200 + results["claim gives the right card"] = claimed["card"]["id"] == EXPECTED_ID + + # --- single use --------------------------------------------------- + try: + api("/api/card/claim", method="POST", + headers={"Origin": "https://kiosk.example"}) + results["second claim refused"] = False + except urllib.error.HTTPError as exc: + results["second claim refused"] = exc.code == 409 + + # --- a foreign origin cannot grab it either ------------------------ + _, state = api("/api/card") + results["card marked consumed"] = state["consumed"] is True + + finally: + try: + os.killpg(os.getpgid(proc.pid), signal.SIGINT) + except Exception: + pass + try: + proc.wait(timeout=8) + except Exception: + proc.kill() + try: + os.close(master) + os.close(slave) + except Exception: + pass + + with lock: + lines = list(output) + + print("======== bridge output ========") + for line in lines[:14]: + print(line) + print("===============================") + print() + + ok = True + for name, passed in results.items(): + print(" [{}] {}".format("PASS" if passed else "FAIL", name)) + ok = ok and bool(passed) + + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_e2e_led.py b/tests/test_e2e_led.py new file mode 100644 index 0000000..0aec5a8 --- /dev/null +++ b/tests/test_e2e_led.py @@ -0,0 +1,209 @@ +"""End-to-end: a real card read must drive the LED on and off. + +Uses a pseudo-terminal for the reader and a fake pin for the LED, so the whole +chain runs — serial frame -> card event -> LED worker -> pin state — without a +Pi and without GPIO hardware. +""" + +import os +import pty +import struct +import subprocess +import sys +import threading +import time + +HERE = os.path.dirname(os.path.abspath(__file__)) +PROJECT = os.path.dirname(HERE) +VENV_PY = os.path.join(PROJECT, ".venv", "bin", "python") +PYTHON = VENV_PY if os.path.exists(VENV_PY) else sys.executable + +CARD_VALUE = 0x00C4A0B5 + +# Runs the app's own CLI with gpiozero's LED replaced by a recorder, so we can +# observe what the app would have done to the pin. +DRIVER = r''' +import os, sys, threading, time, json + +sys.path.insert(0, os.environ["PROJECT_SRC"]) + +from rdm_card_reader import led as led_module + +TRANSITIONS = [] +_LOCK = threading.Lock() + + +class RecordingLED: + def __init__(self, pin=None, active_high=True): + self.pin = pin + self._state = False + + def _record(self, value): + self._state = value + with _LOCK: + TRANSITIONS.append(value) + # Write through immediately: the test terminates this process, so + # anything buffered for the end would be lost. + sys.stderr.write("TRANSITION=" + value + "\n") + sys.stderr.flush() + + def on(self): + self._record("on") + + def off(self): + self._record("off") + + def close(self): + pass + + +class FakeGPIOLED(led_module.GPIOLED): + def __init__(self, pin, active_high=True): + self.pin = pin + self._led = RecordingLED(pin, active_high) + self._is_on = False + + def on(self): + self._led.on() + self._is_on = True + + def off(self): + self._led.off() + self._is_on = False + + def close(self): + self._led.close() + + +led_module.GPIOLED = FakeGPIOLED + +from rdm_card_reader.cli import main + +code = main(["--port", os.environ["SERIAL_PORT"], "--heartbeat", "0.3", + "--led-mode", os.environ.get("LED_MODE", "error"), "--json"]) +sys.exit(code) +''' + + +def em4100_frame(value, card_type=0x0F): + id_bytes = struct.pack(">I", value) + checksum = card_type + for b in id_bytes: + checksum ^= b + frame = (bytes([card_type]) + id_bytes + bytes([checksum])).hex().upper() + assert len(frame) == 12, frame + return frame.encode("ascii") + + +def pump(stream, sink): + """Read a stream on its own thread. + + Text-mode pipes buffer, so select() would not report this data. + """ + for line in stream: + sink.append(line.rstrip()) + + +def run_case(mode): + """Drive a card through the app and return (events, led transitions).""" + master, slave = pty.openpty() + serial_port = os.ttyname(slave) + + env = dict(os.environ) + env["SERIAL_PORT"] = serial_port + env["PROJECT_SRC"] = os.path.join(PROJECT, "src") + env["LED_MODE"] = mode + env.pop("PYTHONPATH", None) + + proc = subprocess.Popen( + [PYTHON, "-u", "-c", DRIVER], + cwd=PROJECT, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, bufsize=1, + ) + + stdout, stderr = [], [] + t1 = threading.Thread(target=pump, args=(proc.stdout, stdout), daemon=True) + t2 = threading.Thread(target=pump, args=(proc.stderr, stderr), daemon=True) + t1.start() + t2.start() + + frame = em4100_frame(CARD_VALUE) + try: + time.sleep(2.0) # let the reader open the port + for _ in range(6): # card present + os.write(master, b"\x02" + frame + b"\x03") + time.sleep(0.1) + time.sleep(1.2) # stop writing -> card removed + finally: + try: + proc.terminate() + except Exception: + pass + try: + proc.wait(timeout=5) + except Exception: + proc.kill() + for t in (t1, t2): + t.join(timeout=3) + try: + os.close(master) + os.close(slave) + except Exception: + pass + + transitions = [ + line.split("=", 1)[1] + for line in stderr + if line.startswith("TRANSITION=") + ] + return stdout, stderr, transitions + + +def main(): + failures = 0 + + # --- error mode: lit while running, dark while a card is read ---------- + stdout, stderr, transitions = run_case("error") + print("======== error mode ========") + for line in stdout[:2]: + print(" " + line) + print(" events: {}".format( + [l.split('"event": "')[1].split('"')[0] + for l in stdout if '"event"' in l])) + print(" transitions: {}".format(transitions)) + + checks = { + "card was read": any("card_inserted" in l for l in stdout), + "card removal seen": any("card_removed" in l for l in stdout), + "lit at startup": transitions[:1] == ["on"], + "dark while reading": "off" in transitions, + "lit again after": transitions[-1:] == ["on"], + } + + # --- presence mode: dark until a card arrives ------------------------- + stdout2, stderr2, transitions2 = run_case("presence") + print("\n======== presence mode ========") + print(" transitions: {}".format(transitions2)) + checks.update({ + "presence: read still works": + any("card_inserted" in l for l in stdout2), + "presence: dark at startup": transitions2[:1] == ["on"] or not transitions2, + "presence: lit on the card": "on" in transitions2, + "presence: dark after": transitions2[-1:] == ["off"], + }) + + print() + ok = True + for name, passed in checks.items(): + print(" [{}] {}".format("PASS" if passed else "FAIL", name)) + ok = ok and bool(passed) + + if not ok: + failures = 1 + print("\n--- driver stderr (error mode) ---") + print("\n".join(stderr)[:1200]) + return failures + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_e2e_pty.py b/tests/test_e2e_pty.py new file mode 100644 index 0000000..ac36ba1 --- /dev/null +++ b/tests/test_e2e_pty.py @@ -0,0 +1,165 @@ +"""End-to-end test over a pseudo-terminal, with no RFID hardware. + +The test owns the master side of a PTY, points the CLI at the slave side and +writes real RDM6300 frames. This exercises the same code path a physical +reader would: serial open -> byte stream -> parse -> callback -> stdout. +""" + +import json +import os +import pty +import signal +import subprocess +import sys +import threading +import time + +import pytest + +from rdm_card_reader.card import CONFIG_CARD_VALUE + +from .test_protocol import encode_stream, encode_frame # noqa: F401 + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(ROOT, "src") + +TIMEOUT = 15.0 + + +class ReaderProcess: + """Run the CLI against a PTY and collect its stdout in the background.""" + + def __init__(self, extra_args=()): + self.master, slave = pty.openpty() + self.port = os.ttyname(slave) + env = dict(os.environ) + env["PYTHONPATH"] = SRC + os.pathsep + env.get("PYTHONPATH", "") + self.proc = subprocess.Popen( + [sys.executable, "-u", "-m", "rdm_card_reader", + "--port", self.port, "--heartbeat", "0.3", *extra_args], + cwd=ROOT, env=env, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, bufsize=1, + start_new_session=True, + ) + self.lines = [] + self._lock = threading.Lock() + self._thread = threading.Thread(target=self._pump, daemon=True) + self._thread.start() + + def _pump(self): + for line in self.proc.stdout: + with self._lock: + self.lines.append(line.rstrip()) + + def output(self): + with self._lock: + return "\n".join(self.lines) + + def send(self, frame): + os.write(self.master, b"\x02" + frame + b"\x03") + + def wait_for(self, needle, timeout=TIMEOUT): + deadline = time.time() + timeout + while time.time() < deadline: + if needle in self.output(): + return True + time.sleep(0.05) + return False + + def __enter__(self): + self.wait_for("waiting for cards") + return self + + def __exit__(self, *exc): + try: + os.killpg(os.getpgid(self.proc.pid), signal.SIGINT) + except Exception: + pass + try: + self.proc.wait(timeout=5) + except Exception: + self.proc.kill() + self._thread.join(timeout=3) + for fd in (self.master, self.proc.stdout): + try: + fd.close() + except Exception: + pass + return False + + +@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only") +def test_card_insert_and_remove_are_reported(): + card_id = 0x00C4A0B5 + frame = encode_frame(card_id) + + with ReaderProcess() as rp: + deadline = time.time() + TIMEOUT + while time.time() < deadline and not rp.wait_for("CARD INSERTED", 0.2): + rp.send(frame) + + assert rp.wait_for("CARD INSERTED"), rp.output() + assert rp.wait_for("CARD REMOVED") + + text = rp.output() + assert rp.port in text + assert str(card_id) in text + assert "0x00C4A0B5" in text + assert "222" in text # checksum = 0xDE + assert "0x0F" in text # type + assert "True" in text # valid + + +@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only") +def test_corrupt_checksum_is_reported_as_invalid(): + good = bytearray(encode_frame(0x1A2B3C4D)) + good[-1] = ord("0") # break the checksum + bad = bytes(good) + + with ReaderProcess() as rp: + deadline = time.time() + TIMEOUT + while time.time() < deadline and not rp.wait_for("INVALID CARD", 0.2): + rp.send(bad) + + assert "INVALID CARD" in rp.output() + assert "checksum mismatch" in rp.output() + + +@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only") +def test_json_mode_emits_machine_readable_events(): + with ReaderProcess(extra_args=("--json",)) as rp: + deadline = time.time() + TIMEOUT + while time.time() < deadline and not rp.wait_for('"event"', 0.2): + rp.send(encode_frame(0x00C4A0B5)) + + payloads = [ + json.loads(line) + for line in rp.output().splitlines() + if line.strip().startswith("{") + ] + assert payloads, rp.output() + assert any(p["event"] == "card_inserted" for p in payloads) + assert any(p["id"] == 0x00C4A0B5 for p in payloads) + assert all(p["port"] == rp.port for p in payloads) + + +@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only") +def test_config_card_is_annotated(): + with ReaderProcess() as rp: + deadline = time.time() + TIMEOUT + while time.time() < deadline and not rp.wait_for("config card", 0.2): + rp.send(encode_frame(CONFIG_CARD_VALUE)) + + assert "config card" in rp.output() + + +@pytest.mark.skipif(sys.platform != "linux", reason="PTY test is Linux only") +def test_removal_after_heartbeat_expires(): + with ReaderProcess() as rp: + deadline = time.time() + TIMEOUT + while time.time() < deadline and not rp.wait_for("CARD INSERTED", 0.2): + rp.send(encode_frame(0x00C4A0B5)) + # stop sending and let the 0.3 s heartbeat lapse + assert rp.wait_for("CARD REMOVED", timeout=5) + + assert "CARD REMOVED" in rp.output() diff --git a/tests/test_led.py b/tests/test_led.py new file mode 100644 index 0000000..8b52de2 --- /dev/null +++ b/tests/test_led.py @@ -0,0 +1,306 @@ +"""Tests for the status LED. + +Two things matter: the LED must never be able to break the reader, and it must +behave the way the workstation app did (on with the card, off without). Real +GPIO is unavailable here, so a fake pin is injected to test the logic. +""" + +import os +import sys +import threading +import time + +SRC = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src" +) +if SRC not in sys.path: + sys.path.insert(0, SRC) + +import pytest # noqa: E402 + +from rdm_card_reader import led as led_module # noqa: E402 +from rdm_card_reader.led import ( # noqa: E402 + BLINK, + DEFAULT_LED_PIN, + ERROR, + OFF, + ON, + PRESENCE, + NullLED, + StatusLED, + create, +) + + +class FakeLED: + """Stands in for gpiozero.LED, recording what it was told to do.""" + + def __init__(self, pin, active_high=True): + self.pin = pin + self.active_high = active_high + self.state = False + self.transitions = [] + self.closed = False + + def on(self): + self.state = True + self.transitions.append("on") + + def off(self): + self.state = False + self.transitions.append("off") + + def close(self): + self.closed = True + + +@pytest.fixture +def fake_gpio(monkeypatch): + """Make StatusLED build a FakeLED instead of touching GPIO.""" + created = {} + + class FakeGPIOLED(led_module.GPIOLED): + def __init__(self, pin, active_high=True): + self.pin = pin + self._led = FakeLED(pin, active_high) + self._is_on = False + created["led"] = self._led + + def on(self): + self._led.on() + self._is_on = True + + def off(self): + self._led.off() + self._is_on = False + + def close(self): + self._led.close() + + monkeypatch.setattr(led_module, "GPIOLED", FakeGPIOLED) + return created + + +def wait_for_state(led_obj, expected, timeout=3.0): + """Wait for the worker thread to apply a command.""" + deadline = time.time() + timeout + while time.time() < deadline: + if led_obj.state is expected: + return True + time.sleep(0.01) + return False + + +# -- defaults --------------------------------------------------------------- + +def test_default_pin_matches_the_workstation_app(): + assert DEFAULT_LED_PIN == 23 + + +# -- graceful degradation --------------------------------------------------- + +def test_no_gpio_gives_a_working_no_op(monkeypatch): + """On a machine without GPIO the LED must be inert, not fatal.""" + def boom(pin, active_high=True): + raise RuntimeError("Unable to load any default pin factory!") + + monkeypatch.setattr(led_module, "GPIOLED", boom) + status = StatusLED(pin=23) + assert status.status.enabled is False + assert status.status.backend == "null" + # Every hook must still be safe to call. + status.insert() + status.remove() + status.error() + status.close() + + +def test_missing_gpiozero_is_reported_not_raised(monkeypatch): + """gpiozero absent: say so, and do not raise.""" + import builtins + + real_import = builtins.__import__ + + def blocked(name, *args, **kwargs): + if name == "gpiozero": + raise ImportError("No module named 'gpiozero'") + return real_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", blocked) + status = StatusLED(pin=23) + assert status.status.enabled is False + assert "not installed" in status.status.reason + + +def test_no_pin_factory_is_not_reported_as_missing_library(monkeypatch): + """BadPinFactory subclasses ImportError, so the two must be distinguished. + + Reporting 'gpiozero is not installed' when it is installed but has no + usable pin library would send you to the wrong fix. + """ + class FakeBadPinFactory(ImportError): + pass + + def boom(pin, active_high=True): + raise FakeBadPinFactory( + "Unable to load any default pin factory!" + ) + + monkeypatch.setattr(led_module, "GPIOLED", boom) + status = StatusLED(pin=23) + assert status.status.enabled is False + assert "not installed" not in status.status.reason + assert "pin factory" in status.status.reason + + +def test_disabled_led_is_inert(fake_gpio): + status = StatusLED(pin=23, enabled=False) + assert status.status.enabled is False + assert isinstance(status.led, NullLED) + status.insert() + time.sleep(0.05) + assert "led" not in fake_gpio + + +def test_null_led_methods_are_safe(): + null = NullLED(23, "no gpio") + null.on() + null.off() + null.close() + + +# -- behaviour -------------------------------------------------------------- + +def test_error_mode_lights_up_at_startup(fake_gpio): + """A health indicator should show 'alive' without waiting for a card.""" + status = StatusLED(pin=23, mode=ERROR) + assert fake_gpio["led"].state is True + status.close() + + +def test_error_mode_blinks_on_a_bad_read(fake_gpio): + status = StatusLED(pin=23, mode=ERROR, error_blink=True) + status.error() + time.sleep(0.25) + assert len(fake_gpio["led"].transitions) >= 2 + status.close() + + +def test_error_mode_settles_back_after_a_blink(fake_gpio): + status = StatusLED(pin=23, mode=ERROR, error_blink=True) + status.error() + time.sleep(0.25) + status.remove() # any steady command cancels blinking + assert wait_for_state(fake_gpio["led"], True) + status.close() + + +def test_error_mode_without_blink_stays_lit(fake_gpio): + status = StatusLED(pin=23, mode=ERROR, error_blink=False) + status.error() + assert wait_for_state(fake_gpio["led"], True) + status.close() + + +def test_presence_mode_is_dark_until_a_card_arrives(fake_gpio): + status = StatusLED(pin=23, mode=PRESENCE) + assert fake_gpio["led"].state is False + status.insert() + assert wait_for_state(fake_gpio["led"], True) + status.remove() + assert wait_for_state(fake_gpio["led"], False) + status.close() + + +def test_close_turns_the_led_off(fake_gpio): + status = StatusLED(pin=23, mode=ERROR) + assert fake_gpio["led"].state is True + status.close() + assert fake_gpio["led"].closed is True + assert fake_gpio["led"].state is False + + +def test_pin_and_active_high_are_passed_through(fake_gpio): + status = StatusLED(pin=17, active_high=False) + assert fake_gpio["led"].pin == 17 + assert fake_gpio["led"].active_high is False + status.close() + + +# -- never blocks or crashes ------------------------------------------------ + +def test_insert_does_not_block(monkeypatch): + """The reader thread calls this, so it must return immediately.""" + class SlowLED(led_module.GPIOLED): + def __init__(self, pin, active_high=True): + self.pin = pin + self._is_on = False + + def on(self): + time.sleep(0.5) # deliberately slow hardware + + def off(self): + pass + + def close(self): + pass + + monkeypatch.setattr(led_module, "GPIOLED", SlowLED) + # mode=presence so the startup light-up does not pay the slow 'on' cost + status = StatusLED(pin=23, mode=PRESENCE) + start = time.time() + for _ in range(20): + status.insert() + elapsed = time.time() - start + assert elapsed < 0.2, "insert() blocked for {:.2f}s".format(elapsed) + status.close() + + +def test_failing_toggle_does_not_raise(fake_gpio, monkeypatch): + """A dead pin must not take the reader down.""" + status = StatusLED(pin=23) + assert fake_gpio["led"] is not None + + def boom(): + raise OSError("pin went away") + + monkeypatch.setattr(status.led, "on", boom) + status.insert() + time.sleep(0.15) # worker swallows the error + status.close() # and close() still works + + +def test_hardware_error_is_reported_to_the_logger(monkeypatch): + lines = [] + + def boom(pin, active_high=True): + raise RuntimeError("no pin factory") + + monkeypatch.setattr(led_module, "GPIOLED", boom) + StatusLED(pin=23, logger=lines.append) + assert any("LED unavailable" in line for line in lines) + + +# -- factory ---------------------------------------------------------------- + +def test_create_defaults_to_error_mode_on_the_workstation_pin(fake_gpio): + status = create() + assert status.status.enabled is True + assert status.status.pin == DEFAULT_LED_PIN + assert status.status.mode == ERROR + status.close() + + +def test_create_disabled_returns_a_no_op(): + status = create(enabled=False) + assert status.status.enabled is False + status.close() + + +def test_status_dict_is_json_serialisable(fake_gpio): + import json + + status = create() + payload = json.dumps(status.status.to_dict()) + assert "gpiozero" in payload + status.close() diff --git a/tests/test_ports.py b/tests/test_ports.py new file mode 100644 index 0000000..b1e8ede --- /dev/null +++ b/tests/test_ports.py @@ -0,0 +1,228 @@ +"""Tests for reliable serial-port identification. + +The point of these tests is that picking the wrong ttyUSB silently is the +failure mode that matters, so ambiguity must raise rather than guess. +""" + +import os +import sys + +SRC = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src" +) +if SRC not in sys.path: + sys.path.insert(0, SRC) + +import pytest # noqa: E402 + +from rdm_card_reader import ports # noqa: E402 +from rdm_card_reader.ports import ( # noqa: E402 + AmbiguousPort, + PortInfo, + PortNotFound, + best_usb_candidates, + resolve, +) + + +def make_port(device, vid=None, pid=None, serial=None, description="", + aliases=None, manufacturer=None, product=None): + return PortInfo( + device=device, + vid=vid, + pid=pid, + serial_number=serial, + description=description, + aliases=list(aliases or []), + manufacturer=manufacturer, + product=product, + ) + + +@pytest.fixture +def two_adapters(monkeypatch): + """Two identical CH340 adapters plus an on-board UART.""" + a = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "CH340A", + "USB Serial", ["/dev/serial/by-id/usb-1a86_A-if00-port0"]) + b = make_port("/dev/ttyUSB1", 0x1A86, 0x7523, "CH340B", + "USB Serial", ["/dev/serial/by-id/usb-1a86_B-if00-port0"]) + onboard = make_port("/dev/ttyS0", description="ttyS0") + monkeypatch.setattr(ports, "discover", lambda: [a, b, onboard]) + return a, b, onboard + + +# -- identity --------------------------------------------------------------- + +def test_usb_id_and_chip_name(): + port = make_port("/dev/ttyUSB0", 0x1A86, 0x7523) + assert port.usb_id == "1a86:7523" + assert port.chip == "CH340" + assert port.is_usb is True + + +def test_unknown_chip_still_identified_as_usb(): + port = make_port("/dev/ttyUSB0", 0xFFFF, 0x0001) + assert port.chip is None + assert port.is_usb is True + + +def test_stable_id_prefers_by_id_symlink(): + port = make_port( + "/dev/ttyUSB0", + aliases=["/dev/serial/by-path/pci-1-usb-0:1.2:1.0-port0", + "/dev/serial/by-id/usb-1a86_USB-if00-port0"], + ) + assert port.stable_id == "/dev/serial/by-id/usb-1a86_USB-if00-port0" + assert port.by_path.endswith("port0") + + +def test_onboard_uart_is_not_usb(): + assert make_port("/dev/ttyS0").is_usb is False + + +# -- scoring ---------------------------------------------------------------- + +def test_usb_adapter_scores_above_onboard_uart(): + adapter = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "SN1") + onboard = make_port("/dev/ttyS0") + assert adapter.score() > onboard.score() + + +def test_serial_number_raises_score(): + with_sn = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "SN1") + without = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, None) + assert with_sn.score() > without.score() + + +# -- matching --------------------------------------------------------------- + +def test_match_by_device_path(): + assert make_port("/dev/ttyUSB0").matches("/dev/ttyUSB0") + + +def test_match_by_serial_number(): + port = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "CH340A") + assert port.matches("CH340A") + + +def test_match_by_vid_pid(): + port = make_port("/dev/ttyUSB0", 0x1A86, 0x7523) + assert port.matches("1a86:7523") + + +def test_match_by_chip_name(): + assert make_port("/dev/ttyUSB0", 0x1A86, 0x7523).matches("ch340") + + +def test_match_by_id_symlink(): + port = make_port("/dev/ttyUSB0", + aliases=["/dev/serial/by-id/usb-1a86_A-if00-port0"]) + assert port.matches("/dev/serial/by-id/usb-1a86_A-if00-port0") + + +def test_empty_selector_never_matches(): + assert not make_port("/dev/ttyUSB0").matches("") + + +# -- resolution ------------------------------------------------------------- + +def test_resolve_by_serial_number_picks_right_adapter(two_adapters): + device, reason = resolve(selectors=["CH340B"]) + assert device == "/dev/ttyUSB1" + assert "matched" in reason + + +def test_resolve_by_by_id_path(two_adapters): + device, _ = resolve(selectors=["/dev/serial/by-id/usb-1a86_B-if00-port0"]) + assert device == "/dev/ttyUSB1" + + +def test_resolve_refuses_when_selector_matches_both(two_adapters): + """A vid:pid shared by two adapters must not silently pick one.""" + with pytest.raises(AmbiguousPort): + resolve(selectors=["1a86:7523"]) + + +def test_resolve_unknown_selector_raises_not_found(two_adapters): + with pytest.raises(PortNotFound): + resolve(selectors=["NOSUCHDEVICE"]) + + +def test_resolve_falls_back_to_raw_path_when_not_enumerable(monkeypatch, tmp_path): + fake = tmp_path / "myport" + fake.write_text("") + monkeypatch.setattr(ports, "discover", lambda: []) + device, reason = resolve(selectors=[str(fake)]) + assert device == str(fake) + assert "explicit path" in reason + + +def test_autodetect_requires_exactly_one(monkeypatch): + single = make_port("/dev/ttyUSB3", 0x1A86, 0x7523, "ONLY") + monkeypatch.setattr(ports, "discover", lambda: [single, make_port("/dev/ttyS0")]) + device, reason = resolve(autodetect=True) + assert device == "/dev/ttyUSB3" + assert "autodetected" in reason + + +def test_autodetect_refuses_with_two_adapters(two_adapters): + with pytest.raises(AmbiguousPort): + resolve(autodetect=True) + + +def test_autodetect_ignores_onboard_uart(monkeypatch): + """An on-board UART must not make a single USB adapter look ambiguous.""" + adapter = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "ONLY") + monkeypatch.setattr( + ports, "discover", + lambda: [adapter, make_port("/dev/ttyS0"), make_port("/dev/ttyAMA0")], + ) + device, _ = resolve(autodetect=True) + assert device == "/dev/ttyUSB0" + + +def test_fallback_order_used_when_no_usb(monkeypatch): + """Must not depend on the host actually having a serial port.""" + monkeypatch.setattr(ports, "discover", lambda: []) + monkeypatch.setattr(ports.os.path, "exists", lambda path: path == "/dev/ttyS0") + device, reason = resolve(fallback_ports=["/dev/ttyS0", "/dev/ttyUSB0"]) + assert device == "/dev/ttyS0" + assert "default order" in reason + + +def test_fallback_order_skips_missing_ports(monkeypatch): + monkeypatch.setattr(ports, "discover", lambda: []) + monkeypatch.setattr( + ports.os.path, "exists", lambda path: path == "/dev/ttyUSB0" + ) + device, _ = resolve(fallback_ports=["/dev/ttyS0", "/dev/ttyUSB0"]) + assert device == "/dev/ttyUSB0" + + +def test_no_port_at_all_raises(monkeypatch): + monkeypatch.setattr(ports, "discover", lambda: []) + with pytest.raises(PortNotFound): + resolve(fallback_ports=[]) + + +# -- helper ---------------------------------------------------------------- + +def test_best_usb_candidates_dedupes_same_identity(): + a = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "SAME") + dup = make_port("/dev/ttyUSB1", 0x1A86, 0x7523, "SAME") + result = best_usb_candidates([a, dup]) + assert len(result) == 1 + + +def test_best_usb_candidates_keeps_distinct_serial_numbers(): + a = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "A") + b = make_port("/dev/ttyUSB1", 0x1A86, 0x7523, "B") + assert len(best_usb_candidates([a, b])) == 2 + + +def test_describe_is_human_readable(): + port = make_port("/dev/ttyUSB0", 0x1A86, 0x7523, "ABC", "USB Serial") + text = port.describe() + assert "/dev/ttyUSB0" in text + assert "CH340" in text + assert "ABC" in text diff --git a/tests/test_protocol.py b/tests/test_protocol.py new file mode 100644 index 0000000..927cc7a --- /dev/null +++ b/tests/test_protocol.py @@ -0,0 +1,108 @@ +"""RDM6300 wire format: type + id + checksum, 12 hex nibbles per frame. + +The checksum is the XOR of the type byte *and* the four id bytes. Getting this +wrong (e.g. XOR-ing only the id) makes every card read fail its checksum, so +the exact bytes are pinned here. +""" + +import struct + +import pytest + +from rdm_card_reader.card import CardData + +START = 0x02 +END = 0x03 + + +def encode_frame(value, card_type=0x0F): + """Encode a card as the 12 nibbles the RDM6300 emits between STX and ETX.""" + id_bytes = struct.pack(">I", value) + checksum = card_type + for byte in id_bytes: + checksum ^= byte + frame = bytes([card_type]) + id_bytes + bytes([checksum]) + encoded = frame.hex().upper().encode("ascii") + assert len(encoded) == 12 + return encoded + + +def encode_stream(value, card_type=0x0F): + return bytes([START]) + encode_frame(value, card_type) + bytes([END]) + + +def decode_frame(frame): + """Reference decoder mirroring rdm6300.BaseReader._parse_fragment.""" + nibbles = [int(ch, 16) for ch in frame.decode("ascii")] + + def to_int(chunk): + out = 0 + for item in chunk: + out = (out << 4) | item + return out + + calculated = 0 + for i in range(0, 10, 2): + calculated ^= (nibbles[i] << 4) | nibbles[i + 1] + received = to_int(nibbles[10:12]) + + return CardData( + value=to_int(nibbles[2:10]), + checksum=received, + is_valid=received == calculated, + type=to_int(nibbles[0:2]), + ) + + +@pytest.mark.parametrize( + "value,expected", + [ + (0x00C4A0B5, b"0F00C4A0B5DE"), + (0x1A2B3C4D, b"0F1A2B3C4D4F"), + ], +) +def test_encoded_frame_bytes_are_stable(value, expected): + assert encode_frame(value) == expected + + +def test_frame_is_twelve_nibbles(): + assert len(encode_frame(0x00C4A0B5)) == 12 + + +def test_checksum_covers_type_byte_not_only_id(): + """Same id with a different type byte must change the checksum.""" + assert encode_frame(0x00C4A0B5, 0x0F) != encode_frame(0x00C4A0B5, 0x10) + + +def test_round_trip_yields_valid_card(): + card = decode_frame(encode_frame(0x00C4A0B5)) + assert card.value == 0x00C4A0B5 + assert card.type == 0x0F + assert card.checksum == 0xDE + assert card.is_valid is True + + +def test_round_trip_of_second_sample(): + card = decode_frame(encode_frame(0x1A2B3C4D)) + assert card.value == 0x1A2B3C4D + assert card.is_valid is True + + +def test_corrupted_checksum_is_invalid(): + frame = bytearray(encode_frame(0x00C4A0B5)) + frame[-1] = ord("0") + assert decode_frame(bytes(frame)).is_valid is False + + +def test_frame_with_wrong_nibble_count_is_rejected(): + """A 14 nibble frame leaves 2 nibbles buffered in the real reader.""" + assert len(encode_frame(0x00C4A0B5)) == 12 + fourteen = b"0F00C4A0B5DE00" + assert len(fourteen) == 14 + + +def test_stream_wrapper_uses_stx_and_etx(): + stream = encode_stream(0x00C4A0B5) + assert stream[0] == START + assert stream[-1] == END + assert stream[1:-1] == encode_frame(0x00C4A0B5) diff --git a/tests/test_reader.py b/tests/test_reader.py new file mode 100644 index 0000000..398fca0 --- /dev/null +++ b/tests/test_reader.py @@ -0,0 +1,149 @@ +"""Port selection and error explanation, using a fake reader library.""" + +import types + +import pytest + +import rdm_card_reader.reader as reader_module +from rdm_card_reader.card import CANDIDATE_PORTS +from rdm_card_reader.reader import explain_error, list_ports, open_reader + + +class FakeBaseReader: + """Stands in for rdm6300.BaseReader; records the port it was built with.""" + + fail_on = () + + def __init__(self, port, heartbeat_interval=0.5): + if port in type(self).fail_on: + raise OSError("could not open port {}".format(port)) + self.port = port + self.heartbeat_interval = heartbeat_interval + self.started = False + self.closed = False + + def start(self): + self.started = True + + def close(self): + self.closed = True + + +@pytest.fixture +def fake_library(monkeypatch): + module = types.SimpleNamespace(BaseReader=FakeBaseReader) + monkeypatch.setattr(reader_module, "rdm6300", module) + return module + + +@pytest.fixture(autouse=True) +def restore_library(): + original = reader_module.rdm6300 + yield + reader_module.rdm6300 = original + + +def test_default_port_order_matches_the_workstation_app(): + assert CANDIDATE_PORTS == ( + "/dev/ttyS0", "/dev/ttyAMA0", "/dev/ttyUSB0", "/dev/ttyACM0", + ) + + +def test_first_working_port_wins(fake_library): + FakeBaseReader.fail_on = ("/dev/ttyS0", "/dev/ttyAMA0") + reader = open_reader(lambda event: None, + ports=["/dev/ttyS0", "/dev/ttyAMA0", "/dev/ttyUSB0"]) + assert reader is not None + assert reader.port == "/dev/ttyUSB0" + + +def test_returns_none_when_no_port_works(fake_library): + FakeBaseReader.fail_on = ("/dev/ttyS0", "/dev/ttyAMA0") + assert open_reader(lambda event: None, + ports=["/dev/ttyS0", "/dev/ttyAMA0"]) is None + + +def test_failures_are_reported_with_the_port(fake_library): + FakeBaseReader.fail_on = ("/dev/ttyS0",) + seen = [] + open_reader(lambda event: None, ports=["/dev/ttyS0"], + on_error=lambda port, message: seen.append((port, message))) + assert seen and seen[0][0] == "/dev/ttyS0" + + +def test_heartbeat_interval_is_passed_through(fake_library): + FakeBaseReader.fail_on = () + reader = open_reader(lambda event: None, ports=["/dev/ttyS0"], + heartbeat_interval=1.5) + assert reader.heartbeat_interval == 1.5 + + +def test_heartbeat_none_disables_removal_detection(fake_library): + FakeBaseReader.fail_on = () + reader = open_reader(lambda event: None, ports=["/dev/ttyS0"], + heartbeat_interval=None) + assert reader.heartbeat_interval is None + + +def test_events_are_forwarded_to_the_callback(fake_library): + FakeBaseReader.fail_on = () + received = [] + reader = open_reader(received.append, ports=["/dev/ttyS0"]) + + class Card: + value = 7 + checksum = 1 + type = 15 + is_valid = True + + reader.card_inserted(Card()) + assert len(received) == 1 + assert received[0].value == 7 + assert received[0].event == "card_inserted" + + +def test_card_removed_with_none_is_ignored(fake_library): + FakeBaseReader.fail_on = () + received = [] + reader = open_reader(received.append, ports=["/dev/ttyS0"]) + reader.card_removed(None) + assert received == [] + + +def test_invalid_card_is_forwarded(fake_library): + FakeBaseReader.fail_on = () + received = [] + reader = open_reader(received.append, ports=["/dev/ttyS0"]) + + class Card: + value = 1 + checksum = 2 + type = 15 + is_valid = False + + reader.invalid_card(Card()) + assert received[0].event == "invalid_card" + + +def test_missing_library_raises(monkeypatch): + monkeypatch.setattr(reader_module, "rdm6300", None) + with pytest.raises(reader_module.ReaderUnavailable): + open_reader(lambda event: None) + + +@pytest.mark.parametrize( + "message,expected_fragment", + [ + ("[Errno 13] Permission denied: '/dev/ttyS0'", "dialout"), + ("could not open port /dev/nope", "device not found"), + ("something else", "something else"), + ], +) +def test_explain_error(message, expected_fragment): + assert expected_fragment in explain_error("/dev/ttyS0", message) + + +def test_list_ports_shape(): + info = list_ports() + assert set(info) == {"candidates", "detected"} + assert [port for port, _ in info["candidates"]] == list(CANDIDATE_PORTS) diff --git a/tests/test_wheels.py b/tests/test_wheels.py new file mode 100644 index 0000000..48cfd40 --- /dev/null +++ b/tests/test_wheels.py @@ -0,0 +1,229 @@ +"""Tests for the vendored wheels and their verification. + +The bundled wheels let a device with no internet set itself up, so the things +that matter are: they are actually present and pure-Python (portable), the +checksums match, and a tampered wheel is refused rather than installed. +""" + +import os +import shutil +import sys + +SRC = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src" +) +if SRC not in sys.path: + sys.path.insert(0, SRC) + +import pytest # noqa: E402 + +from rdm_card_reader import bootstrap # noqa: E402 +from rdm_card_reader.bootstrap import ( # noqa: E402 + bundled_wheel_dir, + ensure_dependencies, + local_wheel_missing, + verify_wheels, +) + + +# -- the wheels that ship with the package --------------------------------- + +def test_bundled_wheel_dir_exists(): + folder = bundled_wheel_dir() + assert folder is not None + assert os.path.isdir(folder) + + +def test_both_required_wheels_are_present(): + folder = bundled_wheel_dir() + names = os.listdir(folder) + assert any(n.startswith("rdm6300-") and n.endswith(".whl") for n in names) + assert any(n.startswith("pyserial-") and n.endswith(".whl") for n in names) + + +def test_bundled_wheels_are_pure_python(): + """A compiled wheel would only work on one architecture; these must not.""" + folder = bundled_wheel_dir() + wheels = [n for n in os.listdir(folder) if n.endswith(".whl")] + assert wheels + for name in wheels: + assert name.endswith("-none-any.whl"), ( + "{} is architecture-specific and cannot be shipped".format(name) + ) + + +def test_bundled_wheels_verify_against_their_checksums(): + folder = bundled_wheel_dir() + assert verify_wheels(folder) == [] + + +def test_checksums_file_is_shipped(): + folder = bundled_wheel_dir() + assert os.path.exists(os.path.join(folder, "SHA256SUMS")) + + +def test_total_vendored_size_is_reasonable(): + """Guard against someone dropping a huge binary into the repo.""" + folder = bundled_wheel_dir() + total = sum( + os.path.getsize(os.path.join(folder, name)) + for name in os.listdir(folder) + ) + assert total < 2 * 1024 * 1024 + + +# -- verification ----------------------------------------------------------- + +def test_tampered_wheel_is_detected(tmp_path): + folder = tmp_path / "wheels" + folder.mkdir() + wheel = folder / "rdm6300-0.1.1-py3-none-any.whl" + wheel.write_bytes(b"not the real wheel") + (folder / "SHA256SUMS").write_text( + "afb067da8b8d23a477138ca67d4ff1d198f12d157b64668c9dd3cbd7df54e74e" + " rdm6300-0.1.1-py3-none-any.whl\n" + ) + problems = verify_wheels(str(folder)) + assert problems + assert any("checksum mismatch" in p for p in problems) + + +def test_missing_wheel_is_reported(tmp_path): + folder = tmp_path / "wheels" + folder.mkdir() + (folder / "SHA256SUMS").write_text( + "abc123 rdm6300-0.1.1-py3-none-any.whl\n" + ) + problems = verify_wheels(str(folder)) + assert any("missing" in p for p in problems) + + +def test_no_sums_file_means_nothing_to_check(tmp_path): + folder = tmp_path / "wheels" + folder.mkdir() + (folder / "x-1.0-py3-none-any.whl").write_bytes(b"x") + assert verify_wheels(str(folder)) == [] + + +def test_verification_passes_for_untouched_copy(tmp_path): + source = bundled_wheel_dir() + dest = tmp_path / "wheels" + shutil.copytree(source, dest) + assert verify_wheels(str(dest)) == [] + + +# -- coverage check --------------------------------------------------------- + +def test_local_wheel_missing_detects_absent_requirement(tmp_path): + folder = tmp_path / "wheels" + folder.mkdir() + assert local_wheel_missing(["rdm6300>=0.1.1"], str(folder)) == ["rdm6300>=0.1.1"] + + +def test_local_wheel_missing_accepts_present_requirement(): + folder = bundled_wheel_dir() + assert local_wheel_missing(["rdm6300>=0.1.1", "pyserial>=3.5"], folder) == [] + + +def test_local_wheel_missing_normalises_underscores(tmp_path): + folder = tmp_path / "wheels" + folder.mkdir() + (folder / "charset_normalizer-3.4.1-py3-none-any.whl").write_bytes(b"x") + assert local_wheel_missing(["charset-normalizer>=3"], str(folder)) == [] + + +# -- integration into ensure_dependencies ----------------------------------- + +def test_bundled_wheels_used_automatically(monkeypatch): + """No --wheel-dir given, so the bundled ones must be picked up.""" + seen = {} + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if seen.get("done") else [("rdm6300", "rdm6300>=0.1.1")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + seen["wheel_dir"] = wheel_dir + seen["done"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + lines = [] + assert ensure_dependencies(logger=lines.append) is True + assert seen["wheel_dir"] == bundled_wheel_dir() + assert any("bundled" in line for line in lines) + + +def test_tampered_bundle_falls_back_to_pypi(monkeypatch, tmp_path): + """A bad checksum must not block installation - but must be reported.""" + folder = tmp_path / "wheels" + folder.mkdir() + (folder / "rdm6300-0.1.1-py3-none-any.whl").write_bytes(b"tampered") + (folder / "SHA256SUMS").write_text("deadbeef rdm6300-0.1.1-py3-none-any.whl\n") + + seen = {} + monkeypatch.setattr(bootstrap, "bundled_wheel_dir", lambda: str(folder)) + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if seen.get("done") else [("rdm6300", "rdm6300>=0.1.1")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + seen["wheel_dir"] = wheel_dir + seen["done"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + lines = [] + assert ensure_dependencies(logger=lines.append) is True + assert seen["wheel_dir"] is None # fell back to PyPI + assert any("checksum mismatch" in line for line in lines) + + +def test_explicit_wheel_dir_overrides_bundled(monkeypatch, tmp_path): + folder = tmp_path / "wheels" + folder.mkdir() + (folder / "rdm6300-0.1.1-py3-none-any.whl").write_bytes(b"x") + (folder / "pyserial-3.5-py2.py3-none-any.whl").write_bytes(b"x") + + seen = {} + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if seen.get("done") else [("rdm6300", "rdm6300>=0.1.1")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + seen["wheel_dir"] = wheel_dir + seen["done"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + ensure_dependencies(wheel_dir=str(folder)) + assert seen["wheel_dir"] == str(folder) + + +def test_bundle_not_covering_a_requirement_falls_back(monkeypatch, tmp_path): + """A bundle missing one package must not cause a guaranteed pip failure.""" + folder = tmp_path / "wheels" + folder.mkdir() + (folder / "rdm6300-0.1.1-py3-none-any.whl").write_bytes(b"x") + + seen = {} + monkeypatch.setattr(bootstrap, "bundled_wheel_dir", lambda: str(folder)) + monkeypatch.setattr( + bootstrap, "missing_packages", + lambda packages=None: [] if seen.get("done") + else [("rdm6300", "rdm6300>=0.1.1"), ("serial", "pyserial>=3.5")], + ) + + def fake_install(requirements, wheel_dir=None, logger=None, timeout=300, no_deps=False): + seen["wheel_dir"] = wheel_dir + seen["wheel_dir_at_call"] = wheel_dir + seen["done"] = True + return True + + monkeypatch.setattr(bootstrap, "install", fake_install) + lines = [] + ensure_dependencies(logger=lines.append) + assert seen["wheel_dir"] is None + assert any("do not cover" in line for line in lines)