Add a dedicated error LED on GPIO 24

One LED cannot say two things at once, and in the default presence
mode the status LED is already carrying "a card is here". The result
was that a bad read - a checksum failure - was reported not at all:
verified by driving the LED worker, which showed presence mode
producing no light on invalid_card while error mode blinked.

A second LED on its own pin (GPIO 24 = physical pin 18, matching the
workstation app) now pulses for --error-pulse seconds on a bad read,
leaving the status light to keep tracking the card. This also gives
ERROR_BLINK_SECONDS, previously dead, a real use.

  --error-led-pin N / --error-led-active-low / --error-pulse S
  --no-error-led  restores the single-LED blink

install.sh rewrites the unit's ExecStart for all of it, verified for
the default, active-low and no-error-led cases.

Also extracts the repeated backend-opening code into _open_led(),
which keeps the BadPinFactory-is-an-ImportError subtlety in one place
instead of two, and treats the error pin colliding with the status
pin as "no error LED" rather than opening one pin twice.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-10-05 22:14:49 +03:00
parent b357b5755a
commit a399542a6e
4 changed files with 268 additions and 68 deletions
+26 -3
View File
@@ -16,6 +16,9 @@
# --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
# --error-led-pin N BCM GPIO of a second LED for bad reads (default 24)
# --no-error-led do not use an error LED; blink the status LED instead
# --error-pulse S seconds to hold the error LED on a bad read (default 1)
# --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)
@@ -37,6 +40,9 @@ SERIAL_DEVICE=/dev/serial0
LED_PIN=23
LED_MODE=presence
LED_ACTIVE_LOW=0
ERROR_LED_PIN=24
ERROR_LED_ACTIVE_LOW=0
ERROR_PULSE=1.0
HEARTBEAT=2.0
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -50,6 +56,10 @@ while [[ $# -gt 0 ]]; do
--led-pin) LED_PIN="$2"; shift 2 ;;
--led-mode) LED_MODE="$2"; shift 2 ;;
--led-active-low) LED_ACTIVE_LOW=1; shift ;;
--error-led-pin) ERROR_LED_PIN="$2"; shift 2 ;;
--error-led-active-low) ERROR_LED_ACTIVE_LOW=1; shift ;;
--error-pulse) ERROR_PULSE="$2"; shift 2 ;;
--no-error-led) ERROR_LED_PIN=""; shift ;;
--heartbeat) HEARTBEAT="$2"; shift 2 ;;
--no-service) INSTALL_SERVICE=0; shift ;;
-h|--help) sed -n '2,33p' "$0"; exit 0 ;;
@@ -279,11 +289,13 @@ for origin in "${ORIGINS[@]:-}"; do
done
python3 - "$UNIT" "$INSTALL_DIR" "$BRIDGE_PORT" "$ORIGIN_ARGS" "$SERVICE_USER" \
"$SERIAL_DEVICE" "$LED_PIN" "$LED_MODE" "$LED_ACTIVE_LOW" "$HEARTBEAT" <<'PY'
"$SERIAL_DEVICE" "$LED_PIN" "$LED_MODE" "$LED_ACTIVE_LOW" "$HEARTBEAT" \
"$ERROR_LED_PIN" "$ERROR_LED_ACTIVE_LOW" "$ERROR_PULSE" <<'PY'
import os, re, sys
(unit, install_dir, port, origin_args, user, device, led_pin, led_mode,
led_active_low, heartbeat) = sys.argv[1:11]
led_active_low, heartbeat, error_led_pin, error_led_active_low,
error_pulse) = sys.argv[1:14]
text = open(unit).read()
# The console script is created by the installer, but the .pth file is what
@@ -295,11 +307,21 @@ if not os.path.exists(wrapper):
led_active_args = " --led-active-low" if led_active_low == "1" else ""
# An empty pin means --no-error-led: the status LED blinks instead.
if error_led_pin:
error_args = f" --error-led-pin {error_led_pin}"
if error_led_active_low == "1":
error_args += " --error-led-active-low"
if error_pulse:
error_args += f" --error-pulse {error_pulse}"
else:
error_args = " --no-error-led"
command = (
f"{wrapper} --serve "
f"--port {device} --bridge-port {port} --mode login "
f"--led-pin {led_pin} --led-mode {led_mode} --heartbeat {heartbeat}"
f"{led_active_args}{origin_args}"
f"{led_active_args}{error_args}{origin_args}"
)
@@ -356,6 +378,7 @@ $(log "Installed")
Bridge : http://127.0.0.1:$BRIDGE_PORT
Device : $SERIAL_DEVICE
LED : GPIO $LED_PIN, $LED_MODE mode$( [[ "$LED_ACTIVE_LOW" -eq 1 ]] && echo ", active-low" )
Error LED : $( [[ -n "$ERROR_LED_PIN" ]] && echo "GPIO $ERROR_LED_PIN, ${ERROR_PULSE}s on a bad read" || echo "none (status LED blinks)" )
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