diff --git a/README.md b/README.md index cdaa9e3..93ef6cf 100644 --- a/README.md +++ b/README.md @@ -558,6 +558,9 @@ what `--list-ports` reports. | `--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 | +| `--error-led-pin N` | `24` | BCM GPIO of a second LED, lit briefly on a bad read | +| `--no-error-led` | off | No error LED; blink the status LED instead | +| `--error-pulse S` | `1.0` | Seconds to hold the error LED on a bad read | | `--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 | @@ -666,30 +669,48 @@ Exit codes: `0` ok, `1` no port, `2` library missing, `3` port ambiguous. ## Status LED -An LED driven by card events, on **GPIO 23** (physical pin **16**) by default — -the same pin the workstation app used. Optional: everything works without it. +Two LEDs, each saying one thing. Both optional: everything works without +either. -By default the light **tracks the card**: lit while a card is on the reader, -dark otherwise. +| LED | Default pin | Means | +| --- | --- | --- | +| **Status** | GPIO 23 (physical pin **16**) | Lit while a card is on the reader (`presence` mode) | +| **Error** | GPIO 24 (physical pin **18**) | Lit briefly when a read fails (checksum mismatch) | + +The status pin matches the workstation app. The error LED exists because one +light cannot say two things at once: in the default `presence` mode the status +LED is already carrying "a card is here", so **without a separate error LED a +bad read is reported not at all**. A dedicated red LED on its own pin is the +clearer arrangement. ```bash -rdm-card-reader # lit only while a card is on the reader -rdm-card-reader --led-mode error # lit whenever running, blinks on a bad read -rdm-card-reader --led-pin 17 # a different pin -rdm-card-reader --no-led # no LED at all +rdm-card-reader # status LED tracks the card; error LED pulses on a bad read +rdm-card-reader --led-mode error # status LED lit whenever running, blinks on a bad read +rdm-card-reader --error-led-pin 25 # error LED on a different pin +rdm-card-reader --no-error-led # no error LED; blink the status LED instead +rdm-card-reader --no-led # no LEDs at all ``` | Flag | Default | Meaning | | --- | --- | --- | -| `--no-led` | off | Do not drive a LED | -| `--led-pin` | `23` | **BCM** GPIO number — not the physical header pin | +| `--no-led` | off | Do not drive any LED | +| `--led-pin` | `23` | **BCM** GPIO number of the status LED — not the physical header pin | | `--led-mode` | `presence` | See the two readings below | -| `--led-no-error-blink` | off | Do not blink on a bad read | +| `--led-active-low` | off | Status LED wired active-low (cathode to the pin) | +| `--error-led-pin` | `24` | BCM GPIO of the error LED | +| `--error-led-active-low` | off | Error LED wired active-low | +| `--error-pulse` | `1.0` | Seconds to hold the error LED on a bad read | +| `--no-error-led` | off | No error LED; blink the status LED instead | +| `--led-no-error-blink` | off | With `--no-error-led`, do not blink on a bad read | `--led-pin` takes the **BCM GPIO number**. The two are easy to confuse, and it is the second most common reason a LED "does not work" — the default `23` is **physical pin 16**, not pin 23. The full mapping is under **Wiring** below. +Giving the error LED the same pin as the status LED is treated as "no error +LED": two lights on one pin cannot be driven independently, so the app says so +and falls back to the status blink rather than failing. + Two modes, because the light can mean two different things: | Mode | Behaviour | Use when | @@ -704,8 +725,9 @@ Two modes, because the light can mean two different things: 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. +transistor/MOSFET driver that inverts — pass **`--led-active-low`** (or +`--error-led-active-low` for the error LED), or the light will do the opposite +of what is wanted. ### It cannot break the reader @@ -720,11 +742,12 @@ point: refusing to run. * **Never crashes on a dead pin.** A failed toggle is logged and ignored. -Startup reports which of the three states it is in (the mode shown is -`presence` by default): +Startup reports the state of each LED (the mode shown is `presence` by +default): ``` -LED on GPIO 23 (presence mode, active-high, gpiozero) # working +LED on GPIO 23 (presence mode, active-high, gpiozero) # status LED working +Error LED on GPIO 24 (lit for 1.0s on a bad read, ...) # error LED working LED unavailable: gpiozero not installed # library missing LED unavailable: no usable GPIO on pin 23: ... # no pin factory / pin busy ``` @@ -747,6 +770,7 @@ 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, active-high, gpiozero)` | LED is initialised — suspect wiring, pin number, or simply no card read yet | See the wiring note below | +| No error indication on a bad read | The error LED pin is wrong or the LED is unwired, and the status LED only blinks in `error` mode | Check `Error LED on GPIO …` in the journal; see 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 @@ -867,9 +891,10 @@ so the Pi's TX line (pin 8) has nothing to send. Only pin 10 carries data. > on the reader-TX → pin-10 line. This is not a cause of "no cards read" if you > already see data, but it is worth fixing before relying on the device. -For the status LED: **anode** → GPIO pin through a ~330 Ω resistor, -**cathode** → GND. See the pin numbering table below, and note the LED is -optional — the reader works without it. +For each LED: **anode** → GPIO pin through a ~330 Ω resistor, +**cathode** → GND. Status LED on GPIO 23 (physical pin 16) and error LED on +GPIO 24 (physical pin 18) by default. See the pin numbering table below, and +note both LEDs are optional — the reader works without them. On a Raspberry Pi you usually need to free up the primary UART first: @@ -941,18 +966,20 @@ Look up the pin your wire is actually on, and pass the BCM number: | 12 | `18` | | 29 | `5` | | 13 | `27` | | 31 | `6` | | 15 | `22` | | 32 | `12` | -| **16** | **`23`** (the default) | | 33 | `13` | -| 18 | `24` | | 35 | `19` | +| **16** | **`23`** (status LED default) | | 33 | `13` | +| **18** | **`24`** (error LED default) | | 35 | `19` | | 19 | `10` | | 36 | `16` | | 21 | `9` | | 37 | `26` | | 22 | `25` | | 38 | `20` | | **23** | **`11`** | | 40 | `21` | So an LED wired to **physical pin 23** needs `--led-pin 11`, and the default -`--led-pin 23` requires the wire on **physical pin 16**. +`--led-pin 23` requires the wire on **physical pin 16**. The error LED defaults +to GPIO 24, which is **physical pin 18**. ```bash -sudo ./deploy/install.sh --device /dev/serial0 --led-pin 11 # wire on physical pin 23 +sudo ./deploy/install.sh --device /dev/serial0 --led-pin 11 # status wire on physical pin 23 +sudo ./deploy/install.sh --device /dev/serial0 --error-led-pin 25 # error wire on physical pin 22 ``` Do **not** use physical pin 8 or 10 (GPIO 14 / 15 — the reader's own UART), diff --git a/deploy/install.sh b/deploy/install.sh index 151c79b..fa799fc 100755 --- a/deploy/install.sh +++ b/deploy/install.sh @@ -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 diff --git a/src/rdm_card_reader/cli.py b/src/rdm_card_reader/cli.py index 3156a3c..bf4862e 100644 --- a/src/rdm_card_reader/cli.py +++ b/src/rdm_card_reader/cli.py @@ -29,8 +29,10 @@ from .card import ( ) from .demo import run_demo from .led import ( + DEFAULT_ERROR_LED_PIN, DEFAULT_LED_MODE, DEFAULT_LED_PIN, + ERROR_BLINK_SECONDS, MODES as LED_MODES, create as create_led, ) @@ -143,6 +145,11 @@ def _build_led(args): if args.no_led: return create_led(enabled=False) + # A dedicated error LED is the default. The old single-LED behaviour is + # still reachable: --no-error-led blinks the status LED instead, and + # --error-led-pin with the status pin's number does the same thing. + error_pin = None if args.no_error_led else args.error_led_pin + # Only pull in gpiozero when a LED was actually asked for, so a headless # install does not grow the dependency set. ensure_dependencies( @@ -158,6 +165,9 @@ def _build_led(args): mode=args.led_mode, active_high=not args.led_active_low, error_blink=not args.led_no_error_blink, + error_pin=error_pin, + error_active_high=not args.error_led_active_low, + error_pulse=args.error_pulse, logger=lambda message: print(" {}".format(message)), ) @@ -327,6 +337,18 @@ def build_parser() -> argparse.ArgumentParser: 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") + led.add_argument("--error-led-pin", type=int, default=DEFAULT_ERROR_LED_PIN, + metavar="N", + help="BCM GPIO of a dedicated error LED, lit briefly on a " + "bad read (default: %(default)s, matching the " + "workstation app)") + led.add_argument("--no-error-led", action="store_true", + help="no dedicated error LED; blink the status LED instead") + led.add_argument("--error-led-active-low", action="store_true", + help="the error LED is wired active-low") + led.add_argument("--error-pulse", type=float, default=ERROR_BLINK_SECONDS, + help="seconds to hold the error LED on a bad read " + "(default: %(default)s)") return parser diff --git a/src/rdm_card_reader/led.py b/src/rdm_card_reader/led.py index 1b9b289..3ac63b1 100644 --- a/src/rdm_card_reader/led.py +++ b/src/rdm_card_reader/led.py @@ -12,21 +12,35 @@ differences: * It is explicit. Whether an LED is actually attached is reported, so "no light came on" is distinguishable from "the card was not read". -The default pin (23) matches the workstation app. **Use a separate LED from the -workstation's if both run on one device**, since two processes driving one pin -will fight. +Two LEDs, because one cannot say two things at once: + +* the **status** LED (default GPIO 23) tracks the card: lit while one is on the + reader, in ``presence`` mode. +* the **error** LED (default GPIO 24) is for a bad read: dark normally, lit + briefly on a checksum failure. Without it, ``presence`` mode - the default - + has no way at all to report an invalid card, because the status light is + already carrying the card's presence. + +The default status pin (23) matches the workstation app. **Use a separate LED +from the workstation's if both run on one device**, since two processes driving +one pin will fight. """ from __future__ import annotations import queue import threading +import time from dataclasses import dataclass -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, Tuple #: GPIO pin of the status LED. Chosen to match the workstation app. DEFAULT_LED_PIN = 23 +#: GPIO pin of the error LED. Separate from the status pin so the two can be +#: read independently: one says "a card is here", the other "the read failed". +DEFAULT_ERROR_LED_PIN = 24 + #: How long to hold an error indication, in seconds. ERROR_BLINK_SECONDS = 1.0 @@ -151,6 +165,39 @@ class _Blinker(threading.Thread): self._stop.set() +def _open_led( + pin: int, active_high: bool, log: Callable[[str], None], label: str +) -> Tuple[Any, bool, str]: + """Build the pin object for *pin*. + + Returns ``(led, usable, reason)``. *led* is a :class:`NullLED` when no pin + is usable, so callers never have to special-case it. + + The library import is checked **separately** from the pin construction on + purpose: ``gpiozero`` raises ``BadPinFactory`` when no pin provider works, + and that is a subclass of ``ImportError``. Catching ``ImportError`` around + the pin construction would therefore report "gpiozero is not installed" + even when it is installed and simply has no usable pin library - on a Pi + missing ``lgpio`` or ``RPi.GPIO`` that message sends you to the wrong fix. + """ + try: + import gpiozero # noqa: F401 + except ImportError as exc: + reason = "gpiozero not installed: {}".format(exc) + log("{} unavailable: {}".format(label, reason)) + return NullLED(pin, reason), False, reason + + try: + built = GPIOLED(pin, active_high=active_high) + 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) + log("{} unavailable: {}".format(label, reason)) + return NullLED(pin, reason), False, reason + + return built, True, "ready" + + class StatusLED: """Non-blocking LED controller driven by card events. @@ -166,13 +213,19 @@ class StatusLED: active_high: bool = True, error_blink: bool = True, logger: Optional[Callable[[str], None]] = None, + error_pin: Optional[int] = None, + error_active_high: bool = True, + error_pulse: float = ERROR_BLINK_SECONDS, ): self._log = logger or (lambda message: None) self._mode = mode if mode in MODES else DEFAULT_LED_MODE self._error_blink = error_blink self._enabled = enabled self._blinker: Optional[_Blinker] = None + self._error_pulse = max(0.0, float(error_pulse)) self._state = False + self._error_until: Optional[float] = None + self._error_state = False self._queue: "queue.Queue[Optional[str]]" = queue.Queue() self._thread: Optional[threading.Thread] = None @@ -181,38 +234,55 @@ class StatusLED: self.led: Any = NullLED(None, "LED disabled (--no-led)") self.status = LEDStatus(False, None, "null", self._mode, False, "disabled", active_high) + self.error_led: Any = NullLED(None, "LED disabled (--no-led)") + self.error_status = LEDStatus(False, None, "null", self._mode, + False, "disabled", error_active_high) return - # Check the library separately from building the pin. - # - # gpiozero raises BadPinFactory when no pin provider works, and that is - # a subclass of ImportError, so catching ImportError around the pin - # construction would report "gpiozero is not installed" even when it is - # installed and simply has no usable pin library. On a Pi missing lgpio - # or RPi.GPIO that message sends you to the wrong fix. - try: - import gpiozero # noqa: F401 - except ImportError as exc: - reason = "gpiozero not installed: {}".format(exc) - self.led = NullLED(pin, reason) - self.status = LEDStatus(False, pin, "null", self._mode, False, - reason, active_high) - self._log("LED unavailable: {}".format(reason)) + self.led, usable, reason = _open_led(pin, active_high, self._log, "LED") + self.status = LEDStatus(usable, pin, + "gpiozero" if usable else "null", self._mode, + False, reason, active_high) + if usable: + self._log("LED on GPIO {} ({} mode, {}, gpiozero)".format( + pin, self._mode, + "active-high" if active_high else "active-low")) + + # A dedicated error LED, when one is wired. It reports a bad read on its + # own pin, so the status light stays free to mean "a card is here". + if error_pin is not None and error_pin == pin: + # Two LEDs on one pin cannot be driven independently, and opening + # the pin twice makes the second open fail anyway. Treat it as "no + # dedicated error LED" so the status LED blink is used instead. + self._log("Error LED pin {} is the status pin; using the status " + "LED to indicate bad reads".format(error_pin)) + error_pin = None + + if error_pin is None: + self.error_led = NullLED(None, "no error LED configured") + self.error_status = LEDStatus(False, None, "null", ERROR, False, + "not configured", error_active_high) else: - try: - self.led = GPIOLED(pin, active_high=active_high) - self.status = LEDStatus(True, pin, "gpiozero", 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, active_high) - self._log("LED unavailable: {}".format(reason)) + self.error_led, e_usable, e_reason = _open_led( + error_pin, error_active_high, self._log, "Error LED") + self.error_status = LEDStatus(e_usable, error_pin, + "gpiozero" if e_usable else "null", + ERROR, False, e_reason, + error_active_high) + if e_usable: + self._log("Error LED on GPIO {} (lit for {}s on a bad read, " + "{}, gpiozero)".format( + error_pin, self._error_pulse, + "active-high" if error_active_high + else "active-low")) + + self._thread = threading.Thread(target=self._worker, daemon=True, + name="led-worker") + # The worker only needs to wake on a timer when there is an error pulse + # to expire. Without one it blocks on the queue, as before. + self._poll = (0.05 if (self.error_status.enabled + and self._error_pulse > 0) else None) + self._thread.start() if self.status.enabled and self._mode == ERROR: # A health indicator shows 'alive' immediately. In presence mode the @@ -221,10 +291,6 @@ class StatusLED: self._set_state(True) self.status.state = True - self._thread = threading.Thread(target=self._worker, daemon=True, - name="led-worker") - self._thread.start() - # -- event hooks ------------------------------------------------------ def insert(self) -> None: @@ -236,8 +302,31 @@ class StatusLED: self._send(ON if self._mode == ERROR else OFF) def error(self) -> None: - """A bad read: blink, if blinking is enabled.""" - self._send(BLINK if self._error_blink else ON) + """A bad read: blink the status LED, or pulse the error LED if wired. + + With a dedicated error LED the indication goes there and the status + light is left alone, so the two LEDs stay independent: one says "a card + is here", the other "the read failed". Without one, the status LED + blinks as before - but only in ``error`` mode, since a blink and "card + present" cannot both be shown on one light. + """ + if self.error_status.enabled and self._error_pulse > 0: + self._error_until = time.monotonic() + self._error_pulse + return + if self._error_blink: + self._send(BLINK) + + def _pulse_done(self, now: float) -> bool: + return self._error_until is not None and now >= self._error_until + + def _error_indicate(self, on: bool) -> None: + """Set the error LED, remembering the state for :meth:`close`.""" + if on: + self.error_led.on() + else: + self.error_led.off() + self._error_state = on + self.error_status.state = on # -- control ---------------------------------------------------------- @@ -251,7 +340,12 @@ class StatusLED: def _worker(self) -> None: while True: - command = self._queue.get() + try: + command = self._queue.get(timeout=self._poll) + except queue.Empty: + # No command pending: run the time-based error pulse, if any. + self._tick_error_pulse() + continue if command is None: return try: @@ -262,6 +356,20 @@ class StatusLED: finally: self._queue.task_done() + def _tick_error_pulse(self) -> None: + """Light the error LED for its window, then darken it again.""" + if self._error_until is None: + return + now = time.monotonic() + if now < self._error_until: + if not self._error_state: + self._error_indicate(True) + elif self._error_state: + self._error_indicate(False) + else: + # Window passed and already dark: stop tracking it. + self._error_until = None + def _apply(self, command: str) -> None: # Blinking is used only for a bad read, and only by 'error' mode. if command == BLINK and self._mode == ERROR: @@ -295,16 +403,28 @@ class StatusLED: def close(self) -> None: self._stop_blink() if self._thread is not None: - self._queue.put(None) + try: + self._queue.put_nowait(None) + except Exception: + pass try: self.led.off() except Exception: pass self._set_state(False) + try: + self.error_led.off() + except Exception: + pass + self.error_status.state = False try: self.led.close() except Exception: pass + try: + self.error_led.close() + except Exception: + pass def create( @@ -314,13 +434,21 @@ def create( active_high: bool = True, error_blink: bool = True, logger: Optional[Callable[[str], None]] = None, + error_pin: Optional[int] = None, + error_active_high: bool = True, + error_pulse: float = ERROR_BLINK_SECONDS, ) -> StatusLED: """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. + + *error_pin* enables a second, dedicated LED for bad reads. Pass the same + value as *pin* to fall back to the single-LED behaviour. """ return StatusLED(pin=pin, enabled=enabled, mode=mode, active_high=active_high, - error_blink=error_blink, logger=logger) + error_blink=error_blink, logger=logger, + error_pin=error_pin, error_active_high=error_active_high, + error_pulse=error_pulse)