From b357b5755a506471a52284265eae0097a280d3ce Mon Sep 17 00:00:00 2001 From: scheianu Date: Mon, 5 Oct 2026 22:11:27 +0300 Subject: [PATCH] Add --led-active-low for inverted LED wiring active_high was hardcoded to True, so an LED wired the other way round - cathode to the pin, anode to 3.3 V, or an inverting transistor driver - had no configuration at all. The symptom is easy to misread as a dark LED: the light actually does the opposite, on with no card and off with one. Exposed as --led-active-low on the app and on install.sh, which rewrites the unit's ExecStart like the other LED options. The startup line now names the polarity, so the journal says which configuration is actually running: LED on GPIO 23 (presence mode, active-high, gpiozero) Default is unchanged, so existing installs behave identically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 33 +++++++++++++++++++++++++++++---- deploy/install.sh | 15 +++++++++++---- src/rdm_card_reader/cli.py | 4 ++++ src/rdm_card_reader/led.py | 28 ++++++++++++++++++++-------- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 32821af..cdaa9e3 100644 --- a/README.md +++ b/README.md @@ -557,6 +557,7 @@ what `--list-ports` reports. | `--device PATH` | `/dev/serial0` | Serial device the service opens (UART; use `/dev/rdm-reader` for USB) | | `--led-pin N` | `23` | BCM GPIO number of the status LED (physical pin 16) | | `--led-mode M` | `presence` | `presence`: lit while a card is on the reader; `error`: lit whenever running | +| `--led-active-low` | off | LED is wired active-low (cathode to the pin, anode to 3.3 V); inverts the signal | | `--heartbeat S` | `2.0` | Seconds before a silent reader counts as card removed — also the presence/LED window | | `--port PORT` | `8765` | HTTP port for the bridge | | `--dir PATH` | `/opt/rdm-card-reader` | Install location | @@ -702,6 +703,9 @@ Two modes, because the light can mean two different things: > read lights it before concluding the LED is broken. Wiring: LED anode to the GPIO pin through a ~330 Ω resistor, cathode to GND. +For the opposite arrangement — cathode to the pin, anode to 3.3 V, or a +transistor/MOSFET driver that inverts — pass **`--led-active-low`**, or the +light will do the opposite of what is wanted. ### It cannot break the reader @@ -720,9 +724,9 @@ Startup reports which of the three states it is in (the mode shown is `presence` by default): ``` -LED on GPIO 23 (presence mode, gpiozero) # working -LED unavailable: gpiozero not installed # library missing -LED unavailable: no usable GPIO on pin 23: ... # no pin factory / pin busy +LED on GPIO 23 (presence mode, active-high, 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 @@ -742,7 +746,28 @@ journalctl -u rdm-card-reader -n 40 --no-pager | grep -i led | --- | --- | --- | | `LED unavailable: no usable GPIO on pin 23: ...` | gpiozero cannot reach a pin backend, or systemd/group permission is missing | See the three checks below | | `LED unavailable: gpiozero not installed: ...` | LED extras missing from the venv | Install from the bundled wheels (below) | -| `LED on GPIO 23 (presence mode, gpiozero)` | LED is initialised — suspect wiring, or simply no card read yet | See the wiring note below | +| `LED on GPIO 23 (presence mode, active-high, gpiozero)` | LED is initialised — suspect wiring, pin number, or simply no card read yet | See the wiring note below | + +**Check 0 — is the light doing the opposite?** A dark LED and an *inverted* LED +are different faults. If the light is **on with no card and goes dark when a +card is placed**, the pin number is right and the polarity is wrong: the LED is +wired active-low. Pass `--led-active-low` (installer, or the app directly), and +confirm with the journal, which now names the polarity: + +``` +LED on GPIO 23 (presence mode, active-low, gpiozero) +``` + +To isolate polarity from everything else, run the service in `error` mode, +where the light is lit the whole time the app runs: + +```bash +sudo ./deploy/install.sh --device /dev/serial0 --led-mode error +``` + +Lit continuously means the hardware path is good. Dark means wiring, pin +number or backend. Lit-while-idle *and* dark-when-a-card-arrives in `presence` +mode means polarity. **Check 1 — the venv must see the system pin backend.** `gpiozero` is pure Python, but the library it uses to talk to the pins (`lgpio` on Bookworm, diff --git a/deploy/install.sh b/deploy/install.sh index 73ffc96..151c79b 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -13,6 +13,9 @@ # --device PATH serial device the service opens (default /dev/serial0) # --led-pin N BCM GPIO number of the status LED (default 23) # --led-mode M 'presence' (default) or 'error' +# --led-active-low LED wired active-low (cathode to the pin, anode to +# 3.3 V, or an inverting driver); or the light will be on +# with no card and off with one # --heartbeat S seconds before a silent reader counts as card removed # (default 2.0; this is also the presence/LED window) # --dir PATH install location (default /opt/rdm-card-reader) @@ -33,6 +36,7 @@ INSTALL_SERVICE=1 SERIAL_DEVICE=/dev/serial0 LED_PIN=23 LED_MODE=presence +LED_ACTIVE_LOW=0 HEARTBEAT=2.0 SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -45,6 +49,7 @@ while [[ $# -gt 0 ]]; do --device) SERIAL_DEVICE="$2"; shift 2 ;; --led-pin) LED_PIN="$2"; shift 2 ;; --led-mode) LED_MODE="$2"; shift 2 ;; + --led-active-low) LED_ACTIVE_LOW=1; shift ;; --heartbeat) HEARTBEAT="$2"; shift 2 ;; --no-service) INSTALL_SERVICE=0; shift ;; -h|--help) sed -n '2,33p' "$0"; exit 0 ;; @@ -274,11 +279,11 @@ for origin in "${ORIGINS[@]:-}"; do done python3 - "$UNIT" "$INSTALL_DIR" "$BRIDGE_PORT" "$ORIGIN_ARGS" "$SERVICE_USER" \ - "$SERIAL_DEVICE" "$LED_PIN" "$LED_MODE" "$HEARTBEAT" <<'PY' + "$SERIAL_DEVICE" "$LED_PIN" "$LED_MODE" "$LED_ACTIVE_LOW" "$HEARTBEAT" <<'PY' import os, re, sys (unit, install_dir, port, origin_args, user, device, led_pin, led_mode, - heartbeat) = sys.argv[1:10] + led_active_low, heartbeat) = sys.argv[1:11] text = open(unit).read() # The console script is created by the installer, but the .pth file is what @@ -288,11 +293,13 @@ 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" +led_active_args = " --led-active-low" if led_active_low == "1" else "" + command = ( f"{wrapper} --serve " f"--port {device} --bridge-port {port} --mode login " f"--led-pin {led_pin} --led-mode {led_mode} --heartbeat {heartbeat}" - f"{origin_args}" + f"{led_active_args}{origin_args}" ) @@ -348,7 +355,7 @@ $(log "Installed") Bridge : http://127.0.0.1:$BRIDGE_PORT Device : $SERIAL_DEVICE - LED : GPIO $LED_PIN, $LED_MODE mode + LED : GPIO $LED_PIN, $LED_MODE mode$( [[ "$LED_ACTIVE_LOW" -eq 1 ]] && echo ", active-low" ) Heartbeat : ${HEARTBEAT}s (presence/LED window) Card state : curl http://127.0.0.1:$BRIDGE_PORT/api/card Health : curl http://127.0.0.1:$BRIDGE_PORT/api/health diff --git a/src/rdm_card_reader/cli.py b/src/rdm_card_reader/cli.py index 9b8231c..3156a3c 100644 --- a/src/rdm_card_reader/cli.py +++ b/src/rdm_card_reader/cli.py @@ -156,6 +156,7 @@ def _build_led(args): pin=args.led_pin, enabled=True, mode=args.led_mode, + active_high=not args.led_active_low, error_blink=not args.led_no_error_blink, logger=lambda message: print(" {}".format(message)), ) @@ -323,6 +324,9 @@ def build_parser() -> argparse.ArgumentParser: "blinking means a bad read") led.add_argument("--led-no-error-blink", action="store_true", help="do not blink the LED on a bad read") + led.add_argument("--led-active-low", action="store_true", + help="LED is wired active-low (cathode to the pin, anode " + "to 3.3 V); inverts the signal") return parser diff --git a/src/rdm_card_reader/led.py b/src/rdm_card_reader/led.py index 68cc485..1b9b289 100644 --- a/src/rdm_card_reader/led.py +++ b/src/rdm_card_reader/led.py @@ -59,14 +59,18 @@ class LEDStatus: mode: str = ERROR state: bool = False reason: str = "" + #: False when the LED is wired active-low (cathode to the pin). + active_high: bool = True - def to_dict(self) -> dict: return { + 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, + "active_high": self.active_high, } @@ -176,7 +180,7 @@ class StatusLED: if not enabled: self.led: Any = NullLED(None, "LED disabled (--no-led)") self.status = LEDStatus(False, None, "null", self._mode, False, - "disabled") + "disabled", active_high) return # Check the library separately from building the pin. @@ -192,21 +196,22 @@ class StatusLED: reason = "gpiozero not installed: {}".format(exc) self.led = NullLED(pin, reason) self.status = LEDStatus(False, pin, "null", self._mode, False, - reason) + reason, active_high) 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)) + False, "ready", active_high) + self._log("LED on GPIO {} ({} mode, {}, gpiozero)".format( + pin, self._mode, + "active-high" if active_high else "active-low")) 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) + reason, active_high) self._log("LED unavailable: {}".format(reason)) if self.status.enabled and self._mode == ERROR: @@ -306,9 +311,16 @@ def create( enabled: bool = True, pin: int = DEFAULT_LED_PIN, mode: str = DEFAULT_LED_MODE, + active_high: bool = True, error_blink: bool = True, logger: Optional[Callable[[str], None]] = None, ) -> StatusLED: - """Build the LED controller, degrading to a no-op when GPIO is unusable.""" + """Build the LED controller, degrading to a no-op when GPIO is unusable. + + *active_high* is False for an LED wired active-low - cathode to the GPIO + pin, anode to 3.3 V, or a driver that inverts. Getting this wrong does not + fail: the light simply does the opposite of what is wanted. + """ return StatusLED(pin=pin, enabled=enabled, mode=mode, + active_high=active_high, error_blink=error_blink, logger=logger)