Dashboard HTTP API¶
The desktop live dashboard is a FastAPI app served by
python -m drowsyguard.cli live, on http://127.0.0.1:8000 by default.
Local tool, no authentication
There is no auth on any endpoint and /stream is your webcam. The default
bind is 127.0.0.1; only pass --host 0.0.0.0 on a network you trust.
FastAPI's own /docs and /redoc are disabled. See
Security.
GET /¶
The dashboard page (src/drowsyguard/static/index.html).
Streams¶
All three are multipart/x-mixed-replace MJPEG with the boundary
drowsyguardframe.
| Endpoint | Rate | Shows |
|---|---|---|
GET /stream |
20 fps | the annotated camera frame |
GET /input-stream |
10 fps | the exact tensor the model receives |
GET /eye-stream |
10 fps | the two eye crops |
/input-stream is the one to open when results look wrong: it shows what the
model actually sees, which is usually where the surprise is.
GET /snapshot.jpg¶
One JPEG of the current annotated frame. Returns 503 before the first frame has been captured.
GET /state¶
The engine snapshot, polled by the page.
| Key | Meaning |
|---|---|
running |
the capture thread is alive |
error |
fatal error, or null |
warning |
non-fatal warnings joined into one string — tracker, eye model and capture warnings can all apply at once, and none silences the others |
p_drowsy |
the current fused probability |
state |
ok, drowsy, cooldown, no-driver or fault. Presence outranks risk and a fault outranks both: a score computed while nobody is there is stale by definition, and reporting it as ok would be the engine saying the driver looks fine when it cannot see one |
presence |
state, health, absent_s, alert_after_s, alerts — the same shape and the same meanings as the device's, because it is the same code |
streak, cooldown_left |
frames over the trigger, and frames still held off |
config |
trigger, required, cooldown, zoom, PERCLOS window, eye threshold |
history |
the rolling p(drowsy) trace behind the chart |
alerts |
the last 10, newest first. Each carries kind (the banner text) and reason (the clip name), because a log of forty undifferentiated alerts says nothing about what happened |
alert_counts |
announcements per reason: drowsy, microsleep, yawning, head_nod, no_driver |
behavior_events |
the last 12 cue events, newest first |
alert_count, frames, fps, infer_ms |
counters |
mode |
eye or face |
eyes |
per-eye P(closed), PERCLOS, closure state |
model |
kind, trained, source, normalize — trained: false is why the page says the probabilities are meaningless |
image_size, crop |
preprocessing geometry |
camera |
backend, index/source, capture timing |
face, face_detect |
tracker state, and whether detection is on. face.present is the gate's confirmed verdict — not the same as found, which is only about this frame — and face.reject names which check refused a candidate, which is what separates "nobody is there" from "something is there and it is not a face" |
POST /config¶
JSON body; every key optional. Applied live, which is what the sliders do.
curl -X POST http://127.0.0.1:8000/config \
-H 'Content-Type: application/json' \
-d '{"trigger": 0.6, "required": 8, "cooldown": 60}'
Two keys control the no-driver alert, and they are here rather than on the device because this is a development tool: the device compiles its thresholds in, by design.
| Key | Range | Meaning |
|---|---|---|
no_driver_after |
seconds, floored at 1.0 | Continuous absence before the alert fires, after the tracking hold has already expired. Floored because below about a second the "absence" being announced is a detector miss. |
no_driver_alert |
boolean | Turn the announcement off entirely. The state and the timer keep being reported — only the alert is withheld — which is what you want for a bench session where an empty seat is the normal condition. |
curl -X POST http://127.0.0.1:8000/config \
-H 'Content-Type: application/json' \
-d '{"no_driver_after": 8.0, "no_driver_alert": true}'
Changing these mid-session deliberately does not reset the timers: a slider moved during a run is a tuning action, not a new episode, and clearing a three-second-old absence because of it would silently cancel an alert that was about to be correct.
| Key | Effect |
|---|---|
trigger |
risk level that starts a streak |
required |
consecutive frames required to alert |
cooldown |
frames held off after an alert |
zoom |
centre-crop fraction (only when no face is detected) |
face_detect |
enable/disable detection and tracking |
perclos_window |
frames in the PERCLOS window |
eye_closed_threshold |
P(closed) above which an eye counts as shut |
no_driver_after |
seconds of absence before the no-driver alert (see below) |
no_driver_alert |
enable/disable the no-driver alert |
Returns the resulting config. The page's Copy as C++ button turns
trigger/required/cooldown into the RiskFilter constructor line for
firmware/esp32s3/main/risk_filter.h.
POST /reset¶
Clears the filter state, the history and the counters. Returns {"ok": true}.
POST /reset and the gate¶
/reset also clears the face track and the presence monitor, not just the risk
filter. That matters for a test run: a confirmed track and a half-elapsed absence
countdown both survive a filter reset otherwise, so the next measurement would start
from someone else's state.
Compared with the device¶
The device API is a fixed-shape object built by one snprintf
under a 6 kB stack; this one is whatever the engine snapshot holds. They are not
interchangeable, and only the decision logic beneath them is shared — but that
sharing is now literal rather than aspirational: facegate.py, presence.py and
behavior.py are parity-tested against the compiled firmware sources, driven through
identical sequences and required to produce identical answers.