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>
This commit is contained in:
@@ -557,6 +557,7 @@ what `--list-ports` reports.
|
|||||||
| `--device PATH` | `/dev/serial0` | Serial device the service opens (UART; use `/dev/rdm-reader` for USB) |
|
| `--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-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-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 |
|
| `--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 |
|
| `--port PORT` | `8765` | HTTP port for the bridge |
|
||||||
| `--dir PATH` | `/opt/rdm-card-reader` | Install location |
|
| `--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.
|
> read lights it before concluding the LED is broken.
|
||||||
|
|
||||||
Wiring: LED anode to the GPIO pin through a ~330 Ω resistor, cathode to GND.
|
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
|
### It cannot break the reader
|
||||||
|
|
||||||
@@ -720,7 +724,7 @@ Startup reports which of the three states it is in (the mode shown is
|
|||||||
`presence` by default):
|
`presence` by default):
|
||||||
|
|
||||||
```
|
```
|
||||||
LED on GPIO 23 (presence mode, gpiozero) # working
|
LED on GPIO 23 (presence mode, active-high, gpiozero) # working
|
||||||
LED unavailable: gpiozero not installed # library missing
|
LED unavailable: gpiozero not installed # library missing
|
||||||
LED unavailable: no usable GPIO on pin 23: ... # no pin factory / pin busy
|
LED unavailable: no usable GPIO on pin 23: ... # no pin factory / pin busy
|
||||||
```
|
```
|
||||||
@@ -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: 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 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
|
**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,
|
Python, but the library it uses to talk to the pins (`lgpio` on Bookworm,
|
||||||
|
|||||||
+11
-4
@@ -13,6 +13,9 @@
|
|||||||
# --device PATH serial device the service opens (default /dev/serial0)
|
# --device PATH serial device the service opens (default /dev/serial0)
|
||||||
# --led-pin N BCM GPIO number of the status LED (default 23)
|
# --led-pin N BCM GPIO number of the status LED (default 23)
|
||||||
# --led-mode M 'presence' (default) or 'error'
|
# --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
|
# --heartbeat S seconds before a silent reader counts as card removed
|
||||||
# (default 2.0; this is also the presence/LED window)
|
# (default 2.0; this is also the presence/LED window)
|
||||||
# --dir PATH install location (default /opt/rdm-card-reader)
|
# --dir PATH install location (default /opt/rdm-card-reader)
|
||||||
@@ -33,6 +36,7 @@ INSTALL_SERVICE=1
|
|||||||
SERIAL_DEVICE=/dev/serial0
|
SERIAL_DEVICE=/dev/serial0
|
||||||
LED_PIN=23
|
LED_PIN=23
|
||||||
LED_MODE=presence
|
LED_MODE=presence
|
||||||
|
LED_ACTIVE_LOW=0
|
||||||
HEARTBEAT=2.0
|
HEARTBEAT=2.0
|
||||||
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
@@ -45,6 +49,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--device) SERIAL_DEVICE="$2"; shift 2 ;;
|
--device) SERIAL_DEVICE="$2"; shift 2 ;;
|
||||||
--led-pin) LED_PIN="$2"; shift 2 ;;
|
--led-pin) LED_PIN="$2"; shift 2 ;;
|
||||||
--led-mode) LED_MODE="$2"; shift 2 ;;
|
--led-mode) LED_MODE="$2"; shift 2 ;;
|
||||||
|
--led-active-low) LED_ACTIVE_LOW=1; shift ;;
|
||||||
--heartbeat) HEARTBEAT="$2"; shift 2 ;;
|
--heartbeat) HEARTBEAT="$2"; shift 2 ;;
|
||||||
--no-service) INSTALL_SERVICE=0; shift ;;
|
--no-service) INSTALL_SERVICE=0; shift ;;
|
||||||
-h|--help) sed -n '2,33p' "$0"; exit 0 ;;
|
-h|--help) sed -n '2,33p' "$0"; exit 0 ;;
|
||||||
@@ -274,11 +279,11 @@ for origin in "${ORIGINS[@]:-}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
python3 - "$UNIT" "$INSTALL_DIR" "$BRIDGE_PORT" "$ORIGIN_ARGS" "$SERVICE_USER" \
|
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
|
import os, re, sys
|
||||||
|
|
||||||
(unit, install_dir, port, origin_args, user, device, led_pin, led_mode,
|
(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()
|
text = open(unit).read()
|
||||||
|
|
||||||
# The console script is created by the installer, but the .pth file is what
|
# 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):
|
if not os.path.exists(wrapper):
|
||||||
wrapper = f"{install_dir}/.venv/bin/python -m rdm_card_reader"
|
wrapper = f"{install_dir}/.venv/bin/python -m rdm_card_reader"
|
||||||
|
|
||||||
|
led_active_args = " --led-active-low" if led_active_low == "1" else ""
|
||||||
|
|
||||||
command = (
|
command = (
|
||||||
f"{wrapper} --serve "
|
f"{wrapper} --serve "
|
||||||
f"--port {device} --bridge-port {port} --mode login "
|
f"--port {device} --bridge-port {port} --mode login "
|
||||||
f"--led-pin {led_pin} --led-mode {led_mode} --heartbeat {heartbeat}"
|
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
|
Bridge : http://127.0.0.1:$BRIDGE_PORT
|
||||||
Device : $SERIAL_DEVICE
|
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)
|
Heartbeat : ${HEARTBEAT}s (presence/LED window)
|
||||||
Card state : curl http://127.0.0.1:$BRIDGE_PORT/api/card
|
Card state : curl http://127.0.0.1:$BRIDGE_PORT/api/card
|
||||||
Health : curl http://127.0.0.1:$BRIDGE_PORT/api/health
|
Health : curl http://127.0.0.1:$BRIDGE_PORT/api/health
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ def _build_led(args):
|
|||||||
pin=args.led_pin,
|
pin=args.led_pin,
|
||||||
enabled=True,
|
enabled=True,
|
||||||
mode=args.led_mode,
|
mode=args.led_mode,
|
||||||
|
active_high=not args.led_active_low,
|
||||||
error_blink=not args.led_no_error_blink,
|
error_blink=not args.led_no_error_blink,
|
||||||
logger=lambda message: print(" {}".format(message)),
|
logger=lambda message: print(" {}".format(message)),
|
||||||
)
|
)
|
||||||
@@ -323,6 +324,9 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
"blinking means a bad read")
|
"blinking means a bad read")
|
||||||
led.add_argument("--led-no-error-blink", action="store_true",
|
led.add_argument("--led-no-error-blink", action="store_true",
|
||||||
help="do not blink the LED on a bad read")
|
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
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -59,14 +59,18 @@ class LEDStatus:
|
|||||||
mode: str = ERROR
|
mode: str = ERROR
|
||||||
state: bool = False
|
state: bool = False
|
||||||
reason: str = ""
|
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,
|
"enabled": self.enabled,
|
||||||
"pin": self.pin,
|
"pin": self.pin,
|
||||||
"backend": self.backend,
|
"backend": self.backend,
|
||||||
"mode": self.mode,
|
"mode": self.mode,
|
||||||
"state": "on" if self.state else "off",
|
"state": "on" if self.state else "off",
|
||||||
"reason": self.reason,
|
"reason": self.reason,
|
||||||
|
"active_high": self.active_high,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -176,7 +180,7 @@ class StatusLED:
|
|||||||
if not enabled:
|
if not enabled:
|
||||||
self.led: Any = NullLED(None, "LED disabled (--no-led)")
|
self.led: Any = NullLED(None, "LED disabled (--no-led)")
|
||||||
self.status = LEDStatus(False, None, "null", self._mode, False,
|
self.status = LEDStatus(False, None, "null", self._mode, False,
|
||||||
"disabled")
|
"disabled", active_high)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check the library separately from building the pin.
|
# Check the library separately from building the pin.
|
||||||
@@ -192,21 +196,22 @@ class StatusLED:
|
|||||||
reason = "gpiozero not installed: {}".format(exc)
|
reason = "gpiozero not installed: {}".format(exc)
|
||||||
self.led = NullLED(pin, reason)
|
self.led = NullLED(pin, reason)
|
||||||
self.status = LEDStatus(False, pin, "null", self._mode, False,
|
self.status = LEDStatus(False, pin, "null", self._mode, False,
|
||||||
reason)
|
reason, active_high)
|
||||||
self._log("LED unavailable: {}".format(reason))
|
self._log("LED unavailable: {}".format(reason))
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.led = GPIOLED(pin, active_high=active_high)
|
self.led = GPIOLED(pin, active_high=active_high)
|
||||||
self.status = LEDStatus(True, pin, "gpiozero", self._mode,
|
self.status = LEDStatus(True, pin, "gpiozero", self._mode,
|
||||||
False, "ready")
|
False, "ready", active_high)
|
||||||
self._log("LED on GPIO {} ({} mode, gpiozero)".format(
|
self._log("LED on GPIO {} ({} mode, {}, gpiozero)".format(
|
||||||
pin, self._mode))
|
pin, self._mode,
|
||||||
|
"active-high" if active_high else "active-low"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# No usable pin factory (not a Pi), pin already in use, bad pin.
|
# No usable pin factory (not a Pi), pin already in use, bad pin.
|
||||||
reason = ("no usable GPIO on pin {}: {}".format(pin, exc))
|
reason = ("no usable GPIO on pin {}: {}".format(pin, exc))
|
||||||
self.led = NullLED(pin, reason)
|
self.led = NullLED(pin, reason)
|
||||||
self.status = LEDStatus(False, pin, "null", self._mode, False,
|
self.status = LEDStatus(False, pin, "null", self._mode, False,
|
||||||
reason)
|
reason, active_high)
|
||||||
self._log("LED unavailable: {}".format(reason))
|
self._log("LED unavailable: {}".format(reason))
|
||||||
|
|
||||||
if self.status.enabled and self._mode == ERROR:
|
if self.status.enabled and self._mode == ERROR:
|
||||||
@@ -306,9 +311,16 @@ def create(
|
|||||||
enabled: bool = True,
|
enabled: bool = True,
|
||||||
pin: int = DEFAULT_LED_PIN,
|
pin: int = DEFAULT_LED_PIN,
|
||||||
mode: str = DEFAULT_LED_MODE,
|
mode: str = DEFAULT_LED_MODE,
|
||||||
|
active_high: bool = True,
|
||||||
error_blink: bool = True,
|
error_blink: bool = True,
|
||||||
logger: Optional[Callable[[str], None]] = None,
|
logger: Optional[Callable[[str], None]] = None,
|
||||||
) -> StatusLED:
|
) -> 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,
|
return StatusLED(pin=pin, enabled=enabled, mode=mode,
|
||||||
|
active_high=active_high,
|
||||||
error_blink=error_blink, logger=logger)
|
error_blink=error_blink, logger=logger)
|
||||||
|
|||||||
Reference in New Issue
Block a user