Install missing reader libraries on first start

A freshly flashed device needed manual pip work before the reader would
start. It now checks its dependencies, installs whatever is missing, and
carries on.

- bootstrap.py: import check, pip invocation, and a re-check that does not
  trust pip's exit code alone. Installs nothing when the libraries are
  already present, so a normal start does no network work.
- --wheel-dir installs from local wheels with --no-index, for devices with
  no internet. --no-install-deps opts out entirely.
- Outside a venv pip gets --break-system-packages, which Debian and
  Raspberry Pi OS require before touching the system Python.
- reload_library() re-imports rdm6300 when it was installed at runtime, since
  the module-level import already ran and failed.
- A failed install reports exit 2 and the exact manual command rather than
  raising; stdout is flushed so the explanation is not printed after the
  error when piped.
This commit is contained in:
2026-10-01 15:40:56 +03:00
parent 576f22268f
commit eaf3f394fc
8 changed files with 452 additions and 13 deletions
+48 -1
View File
@@ -30,6 +30,21 @@ $ rdm-card-reader
(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
@@ -73,6 +88,8 @@ Also works as a module: `python -m rdm_card_reader`.
| `--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` | – | Install missing libraries from local wheels (offline) |
| `--version` | – | Print the version |
With `--serve`: `--host` (`127.0.0.1`), `--bridge-port` (`8765`), `--mode`
@@ -96,13 +113,43 @@ is what you want when piping into another program:
{"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
On first start the reader checks that it can import its libraries and installs
any that are missing, so a freshly flashed device works without manual setup:
```
Reader library not found - checking dependencies...
Missing reader dependencies: rdm6300
Installing from PyPI ...
Dependencies ready: rdm6300
```
* Nothing is installed when the libraries are already present, so a normal start
does no network work at all.
* A failed install does not crash: it reports `exit 2` and tells you the exact
command to run by hand.
* Outside a virtualenv pip is given `--break-system-packages`, which Debian and
Raspberry Pi OS require before they will touch the system Python.
For a device with **no internet**, put the wheels next to the app and install
from there — no index is contacted:
```bash
pip download rdm6300 pyserial -d wheels # on a machine with internet
rdm-card-reader --serve --wheel-dir ./wheels
```
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 not installed |
| `2` | The `rdm6300` library is missing and could not be installed |
| `3` | The port selector matched more than one adapter (refused to guess) |
---