Inspiration
An OLED monitor and an OLED TV, both burning pixels every minute nobody's in the room. The naive fix — screen timeout — is either too aggressive (blanks while you're reading) or too lazy (30 minutes of static desktop before it kicks in). Presence sensors solve that: the room knows when it's empty.
But every blanking tool we tried had the same fatal flaw: turning off the display kills the audio. DPMS tears down the ALSA device with the video signal — that's architectural, not a setting. So music stops the moment the screen sleeps. Fine for an office, useless for a living room where the TV is also the sound system.
We wanted the panel dark, the audio alive, and zero pixels of burn-in risk — driven by an mmWave radar that costs less than a pizza.
What it does
dormant is a Rust daemon that blanks OLED screens when presence sensors say the room is empty and wakes them the instant you walk back in.
Sensors come in over MQTT, Home Assistant WebSocket, or raw USB-serial radar. Displays go dark through whichever path actually preserves audio: DDC/CI power-off for monitors (the panel blanks, the OS output and sound stay alive), KEY_PICTURE_OFF or a backlight-to-zero call for Samsung TVs (the movie keeps playing, the panel goes black), with plain DPMS as a documented last resort. If every hardware path fails, a Wayland overlay paints the screen black in software.
Around the core: a per-panel wear ledger that tracks brightness-weighted on-time on a grid (with a heat map in the web UI), a screensaver ladder with pixel-shift for burn-in mitigation, a KDE tray and a native macOS menu-bar item, a loopback web dashboard, a doctor command that probes your actual hardware, and an emergency wake that works even when the daemon is dead.
The one rule that never bends: a sensor going silent means present, not absent. The daemon never blanks a room it can't see.
How we built it
A Cargo workspace of nine crates with a hard split: pure logic (state machine, zone fusion, rules) in crates with zero I/O, hardware controllers behind traits, one file per sensor or display type. Every timing knob is a config key with a documented default. Log events and error codes are literal strings you can grep.
The build order was hardware-first: before writing a controller we probed the real device. The Samsung TV got a live JSON-RPC probe session that discovered an undocumented backlightControl method on port 1516 — panel to near-black, Prime Video audio never hiccuped. The AOC monitor got DDC/CI verification across standby depths. The KWin DPMS path got tested and turned out to be global, not per-output, which reshaped the design.
CI runs a 34-check matrix: Linux, macOS, and Windows portability, clippy pedantic, MSRV, a Dockerised MQTT integration suite, release-artifact smoke tests, and label-gated 200× stress runs for the reload paths that used to flake. Releases ship six binaries across two platforms plus Homebrew and AUR packages from one tag push.
Challenges we ran into
Samsung's local API fights you. Port 1516 rejects lowercase HTTP header names — reqwest sends content-type, Samsung wants Content-Type, and the difference is a 400. createAccessToken fails if the JSON body contains an empty params key. The backlight read isn't where the docs imply. Three stacked protocol quirks, each invisible until isolated with curl differentials against a raw Python probe that worked.
A research report told us audio-safe TV blanking was impossible. It also claimed Wake-on-LAN didn't work on 2024 Samsungs and that the control ports were closed. We tested the actual TV: WoL works from deep standby, all three ports are open, and the "impossible" audio-safe dim is the brightness_zero mode shipping today. Confident negative claims from secondary sources are the most dangerous kind — every one gets verified on hardware now.
Our crash-recovery design rested on a false premise. We designed four layers of gamma-restore recovery for macOS on the belief that a dead process leaves the screen black. A spike on real hardware showed WindowServer auto-reverts gamma on process death — the panel wakes itself in the failure case. Half the recovery architecture became insurance instead of load-bearing, and the design got simpler.
The daemon held 265 MB after the screensaver stopped. glibc arena math: 16 tokio workers × 64 MB malloc arenas retaining freed frame buffers. Capping worker threads and calling malloc_trim after teardown dropped the post-screensaver floor to 89 MB.
Accomplishments that we're proud of
- Audio-safe blanking that actually ships. Verified live on a QD-OLED monitor (DDC/CI) and an S90D TV (picture-off and backlight-zero) — panel dark, sound uninterrupted, wake in about a second.
- It runs the house. Both the Linux desktop and a Mac drive the same physical desk from one mmWave radar, daily, as the maintainer's production setup. Dogfooding found bugs no test suite would have — including a daemon that couldn't start while its display was unplugged.
- A wear ledger nothing else has. Per-panel, brightness-weighted, grid-level exposure tracking, seeded from the monitor's own usage-hours register (VCP 0xC0), surviving renames and daemon restarts.
- v0.5.0 out the door. 34 release assets, three OS targets, Homebrew tap, AUR package, mdBook docs, and a CI pipeline whose reload tests went from recurring flakes to 200/200 stress-green after we rebuilt them around causal receipts instead of timing windows.
What we learned
- Probe the hardware before believing anything written about it. The monitor's own capability report lists input codes it doesn't use and omits ones it does. The empirically-built map is the only trustworthy one.
- Fail-safe direction is a design axis, not a feature. Sensor unavailable → present. Wake failures → escalate and notify. Config crash-loop → roll back to last-known-good. Every failure path has to land on "screen visible," because a screen that won't wake is the one bug users never forgive.
- Negative results steer better than positive ones. "DPMS can't preserve audio," "WindowServer reverts gamma," "0x60 reads per-port through the standard API" — each falsified assumption cut work or redirected the design before code got written on top of it.
- Deterministic beats clever in test suites too. Reload races died when we replaced sleep-and-hope timing windows with correlated receipts and generation barriers. The flake ledger has been quiet since.
What's next for dormant
- Multi-machine coordination. Two computers share the physical OLED through a KVM; both run dormant. We proved on hardware that the monitor's active-input register is readable from the inactive machine — so each daemon can know whose input is on glass and gate itself. mDNS discovery and code-confirmed pairing are designed and in build now.
- The Mac's built-in display joins the managed set with the same gamma-black path the external panel uses.
- HDMI-CEC for TVs that aren't Samsung, BLE phone presence as a zero-hardware sensor, and LD2450 support for true 2D zone tracking from a single radar.
- A presence API, so the room-is-empty signal dormant already computes can drive lights, HVAC, anything.
Built With
- mqtt
- react
- rust
- tokio
- typescript
Log in or sign up for Devpost to join the conversation.