Add manual LED control to the status page

"When the light does not come on" has two very different causes -
the pin is wrong, or the wiring is wrong - and nothing so far could
tell them apart. The status page now has an LED panel with buttons
to switch either LED on or off by hand, which takes card events out
of the picture entirely: if the light comes on, the pin, backend and
permissions are all fine.

Backed by GET/POST /api/led. A forced state latches, holding through
card events and the error pulse, until released - otherwise the next
tap would immediately undo it and the test would be useless. A button
whose LED has no usable pin is disabled rather than silently doing
nothing, and the POST returns 409 rather than a misleading 200 when
there is no pin to drive.

Both LED states are recorded at the moment a request is served, not
only when the worker thread gets to it, so a snapshot cannot
contradict the response that was just sent.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-10-05 22:30:28 +03:00
parent a399542a6e
commit c2b3eda649
5 changed files with 286 additions and 6 deletions
+37
View File
@@ -329,6 +329,20 @@ consume a real login just for being open. Claim from your own app instead.
Disable it with `--no-page` if you want the port to serve the API only.
The page includes an **LED panel** with buttons to switch either LED on or off
by hand, for the case where the light does not come on and you need to know
whether the software can drive the pin at all. A forced state is held until you
press *hand back to card events*, so card events do not immediately undo it.
The same is reachable without a browser:
```bash
curl -sS http://127.0.0.1:8765/api/led # read the state
curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"led":"status","on":true}' http://127.0.0.1:8765/api/led
curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"release":true}' http://127.0.0.1:8765/api/led
```
### Endpoints
| Method | Path | Purpose |
@@ -341,6 +355,8 @@ Disable it with `--no-page` if you want the port to serve the API only.
| `GET` | `/api/taps` | Recent tap history (`?limit=N`) |
| `GET` | `/api/health` | Reader state, port, uptime, tap counts, both TTLs — for monitoring |
| `POST` | `/api/mode` | `{"mode": "login"}` or `{"mode": "monitor"}` |
| `GET` | `/api/led` | Both LEDs: pin, polarity, on/off, whether held manually |
| `POST` | `/api/led` | `{"led": "status"\|"error", "on": true\|false}`, or `{"release": true}` |
`login` mode (default) publishes taps only — a tap is an authentication
attempt. `monitor` mode also publishes removals, which is what you want for
@@ -782,6 +798,27 @@ confirm with the journal, which now names the polarity:
LED on GPIO 23 (presence mode, active-low, gpiozero)
```
**Check 0b — can the software drive the pin at all?** Before suspecting the
wiring, prove the pin works. Open the status page and use the **LED panel** to
switch each light on and off by hand, which takes card events out of the
picture entirely:
```bash
curl -sS -X POST -H 'Content-Type: application/json' \
-d '{"led":"status","on":true}' http://127.0.0.1:8765/api/led
```
- **The light comes on** → the pin, backend and permissions are all fine, so
the problem is card events, not hardware. Check the journal for
`CARD INSERTED` when you tap.
- **Nothing happens, HTTP 200** → the pin is being driven but no light is
connected to it. Wiring, or the wrong pin.
- **HTTP 409 `that LED is not available`** → the LED has no usable pin; look at
`reason` in the response, which is the same text as the journal line.
Press *hand back to card events* (or POST `{"release": true}`) when finished, or
the LEDs stay held where you left them.
To isolate polarity from everything else, run the service in `error` mode,
where the light is lit the whole time the app runs: