# 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.