At rest a person blinks roughly 15 to 20 times a minute. In front of a screen that collapses to around a third of it, which is most of why long sessions end in dry, burning eyes. The rate also moves the other way: as attention degrades and fatigue builds, blinking speeds up and gets less regular.
So blink rate carries two useful signals at once, and a camera can read both without touching you. The problem is what you do with the signal. A desktop notification arrives in the same channel as the work and gets dismissed without being read.
Lumen puts the signal somewhere else. It drives an ambient lamp in your peripheral vision. You never look at it directly. You notice it has changed.
What it doesA USB camera watches the face. MediaPipe FaceMesh gives facial landmarks, and six points per eye form the eye aspect ratio, the ratio of lid opening to eye width. A drop below a per-user threshold is a blink.
From the blink stream Lumen derives a rolling rate and sorts the last few minutes into three states:
- CALM — rate near the resting baseline. The lamp is a low warm white.
- ATTENTION — rate suppressed, deep focus, dry-eye risk climbing. The lamp cools and dims.
- BREAK — rate rising and irregular, the fatigue signature. The lamp shifts amber and breathes slowly until you stand up.
This is the question judges are actually asking, so it deserves a direct answer rather than a feature list.
The vision half needs a real CPU. Face mesh inference at camera rate is not an MCU workload, and the earlier version of Lumen proved it by needing a laptop tethered to an ESP32 over serial. That tether is the whole reason it was a prototype and not a thing you could leave on a desk.
The lamp half needs steady timing. A slow breathing fade is only calming if the steps are even. Drive it from Linux while a camera pipeline is running and the fade stutters, which reads as a fault rather than as ambience.
The UNO Q collapses both into one board. The Dragonwing QRB2210 runs Debian and the vision loop. The STM32U585 owns the lamp and nothing else, so the fade is deterministic. They talk over the Bridge RPC layer: Python calls a function, the sketch receives it as a callback. No serial protocol to write, no laptop.
How it is built
The app follows the standard App Lab layout.
lumen/
├── app.yaml manifest
├── python/main.py camera, face mesh, blink rate, state machine
├── python/blink.py EAR detection with a rolling per-user baseline
└── sketch/sketch.ino lamp driver, non-blocking fadesThe MPU side calls four functions on the MCU:
Bridge.call("set_state", state) # CALM / ATTENTION / BREAK
Bridge.call("set_level", brightness) # 0-100and the sketch registers them at startup:
Bridge.begin();
Bridge.provide("set_state", set_state);
Bridge.provide("set_level", set_level);Nothing in the sketch blocks. A delay() inside the fade would stall the Bridge router and the next state change would arrive late, so fade steps are timed against millis() in loop() instead.
The closure threshold is a fraction of the user's own open-eye baseline rather than a fixed constant. Eye shape varies enough between people that a hard number like 0.21 fails outright on a meaningful fraction of them. The baseline is recomputed from open-eye frames only, so it tracks lighting and head pose drift without being dragged down by the blinks it is meant to detect.
Wiring- Update the UNO Q firmware through App Lab, set wifi and a password.
- Create a new App, or copy the project folder to
~/ArduinoApps/lumenover SSH and runarduino-app-cli app start ~/ArduinoApps/lumen. - Wire the lamp per the diagram.
- First launch installs Python dependencies and flashes the sketch. Expect several minutes.
- Sit in frame for about five seconds so the baseline settles.
- Limitations
- Single fixed camera. Turn away and tracking stops.
- Thresholds are compile-time constants. Real use needs a calibration pass.
- Blink rate is a proxy for fatigue, not a measurement of it. Lumen is not a medical device and has not been clinically validated.
- Tested on 5 people











Comments