Commit Graph

6 Commits

Author SHA1 Message Date
ske087 031cf0f25a Add a status LED driven by card events
The workstation app lit an LED on GPIO 23 with the card. This brings that
across, with three deliberate differences: it never blocks the reader, it
cannot be fatal, and its state is reported.

WMT toggled the LED directly inside card_inserted, on the reader thread, where
a slow write stalls card detection. Here every change is queued to a worker, so
insert() returns immediately (tested with a deliberately slow pin).

Two modes, because the light can mean two things:
  error    (default) lit while the reader runs, dark while reading, blinks on a
                     bad read - a health indicator
  presence           lit only while a card is on the reader

gpiozero raises BadPinFactory on a machine with no GPIO, and that subclasses
ImportError - so a single except ImportError reports 'gpiozero is not
installed' when the library is present but has no usable pin provider. The
library check and the pin construction are therefore separate.

Vendors gpiozero + colorzero (~180 KB) so the LED installs offline too, and
deliberately not setuptools: gpiozero declares it via colorzero, but it is only
a packaging requirement and is never imported at runtime. Installing to the
bundled wheels therefore uses --no-deps, saving 818 KB.

Verified end to end over a pseudo-terminal with a fake pin: a real RDM6300
frame drives the LED through the app's own CLI in both modes.
2026-10-01 16:43:29 +03:00
ske087 2ded37e190 Fix install.sh failing on offline Raspberry Pi at the venv step
install.sh ran 'pip install .', which builds the project: pip downloads
setuptools into an isolated build environment. With no internet it fails at
the 'Building the virtual environment' step with

    Could not find a version that satisfies the requirement setuptools>=68
    (from versions: none)

even though the runtime libraries are already in the wheel directory. The
build backend is only needed to package, never to run.

- Install the runtime deps straight from the bundled wheels with --no-index,
  and put src/ on the path via a .pth file, so no build step happens at all.
- Generate the console script with the venv's interpreter as an absolute
  shebang. '#\!/usr/bin/env python3' resolves to the *system* python when the
  venv is not activated, so the package was not importable from the unit.
- apt-get update/install and 'pip install --upgrade pip' are now best-effort:
  all three need the network and none should abort the install.
- Detect a failed 'python3 -m venv' and name python3-venv as the usual cause.
- Sanity-check the install with --version before enabling the service.
- The unit falls back to 'python -m rdm_card_reader' if the console script is
  ever absent (no shell in ExecStart, so the installer picks the path).

Reproduced the failure offline, then verified the fix offline as well: a venv
created on a copy of the tree self-provisions from the bundled wheels and runs
from cwd=/ with no PYTHONPATH and no venv activation.

Added tests/test_deploy.py (20 tests) covering the installer and the deploy
artifacts, including a regression test for 'pip install .' that was confirmed
to fail when the bug is reintroduced.
2026-10-01 16:19:33 +03:00
ske087 674d91a5ca Add LAUNCH.md: startup sequence and why it fails without a reader
development is x86_64 with no RFID reader attached, so the reader path exits 1
by design. That looks like breakage when it is not, so document it.

- The expected output on this host, including why /dev/ttyS0 gives
  'Input/output error' (node exists, hardware does not answer) rather than
  'device not found' - the app distinguishes the two.
- The 5-stage startup as a diagram, with the code and exit code for each stage,
  and why stages 1-2 fail fast on purpose.
- How to exercise the reader path with no hardware: the PTY tests, plus a
  standalone snippet that writes a real RDM6300 frame. Both the snippet and the
  documented outputs were run verbatim, not written from memory.
- The pipe trap: head closes the pipe early and the shell then reports
  128+SIGPIPE, so redirect to a file when checking the real exit code.
2026-10-01 15:55:16 +03:00
ske087 23cd8f2df5 Ship the reader wheels inside the package for offline install
A device with no internet could not set itself up: the auto-install needed
PyPI. The rdm6300 and pyserial wheels now ship inside the package, so first
start works with no network at all.

Both wheels are pure Python (py3-none-any / py2.py3-none-any), which is what
makes this possible - the same files work on armv7l, aarch64 and x86_64. This
is the difference from the workstation app's Files/repository cache, which
held aarch64 manylinux binaries and so could not be committed; that cache only
ever existed on the Pi devices.

- bundled_wheel_dir() locates them; ensure_dependencies() prefers them
  automatically, so the service needs no --wheel-dir.
- verify_wheels() checks SHA256SUMS before installing. A wheel that fails its
  checksum is refused rather than installed, then PyPI is tried, and if that is
  unreachable it reports exit 2 instead of proceeding unverified.
- local_wheel_missing() falls back to PyPI when the bundle does not cover a
  requirement, instead of attempting a --no-index install that must fail.
- An explicit --wheel-dir that is incomplete or unverified is called out, not
  silently swapped for PyPI.

Verified end to end: built wheel and sdist both contain the wheels; a venv
with no rdm6300 self-provisions with a dead proxy and no PIP_NO_INDEX; and a
tampered wheel is refused with nothing installed.
2026-10-01 15:48:34 +03:00
ske087 eaf3f394fc 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.
2026-10-01 15:40:56 +03:00
ske087 576f22268f RDM card reader: print card events, select the right port, serve taps to a web app
Card reading (rdm6300 + pyserial) printing card inserted/removed with the
full card data, plus a --serve mode that publishes taps over HTTP so a web
page can authenticate a tap.

The bridge reports card ids only - mapping a card to a user stays on the
server. The tap is latched for --tap-ttl seconds, including after the card is
lifted, and is single use, so a page reload cannot replay one tap into two
logins.

Port selection identifies the reader by serial number or by-id path instead of
/dev/ttyUSBn, and refuses to guess when a selector matches several adapters.

deploy/ has the systemd unit, a udev rule for a stable /dev/rdm-reader, and
install.sh. Covered by unit tests plus PTY end-to-end tests.
2026-10-01 15:31:04 +03:00