From c0704be3439d3db3c36308fb7c010641f2072ac5 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Sun, 10 May 2026 22:46:30 +0200 Subject: [PATCH] feat: DPMS fallback via wlr-randr for non-CEC desktop monitors --- kiosk/src/cec.rs | 104 +++++++++++++++++++++++++++++++++++------------ kiosk/src/ui.rs | 4 +- 2 files changed, 80 insertions(+), 28 deletions(-) diff --git a/kiosk/src/cec.rs b/kiosk/src/cec.rs index 98ecf3e..f394f54 100644 --- a/kiosk/src/cec.rs +++ b/kiosk/src/cec.rs @@ -1,44 +1,49 @@ -//! CEC (HDMI Consumer Electronics Control) — manages display power state -//! via `cec-ctl` subprocess. v4l-utils package provides cec-ctl on Pi5. +//! Display power management — CEC for TVs, DPMS (wlr-randr/xset) for monitors. //! -//! Commands: -//! - standby: tell TV to sleep -//! - image-view-on: wake TV -//! - is-active-source: query state +//! Tries CEC first (works for HDMI TVs). +//! Falls back to compositor-level output disable for desktop monitors that +//! don't speak CEC. use std::process::Command; use tracing::{info, warn}; const CEC_DEVICE: &str = "/dev/cec0"; -/// Send CEC standby (sleep) to all connected devices. -pub fn standby() -> bool { - info!("cec: standby"); +/// Put the display to sleep — try CEC first, fall back to compositor DPMS. +pub fn standby() { + info!("power: standby"); + if !cec_standby() { + wlr_output_off(); + } +} + +/// Wake the display — try CEC first, fall back to compositor DPMS. +pub fn wake() { + info!("power: wake"); + if !cec_wake() { + wlr_output_on(); + } +} + +fn cec_standby() -> bool { run_cec(&["--standby", "--to", "0"]) } -/// Send CEC image-view-on (wake) to TV. -pub fn wake() -> bool { - info!("cec: wake"); +fn cec_wake() -> bool { run_cec(&["--image-view-on", "--to", "0"]) } -/// Switch HDMI input on TV to this device. -pub fn become_active_source() -> bool { - info!("cec: become active source"); - run_cec(&["--active-source", "phys-addr=0.0.0.0"]) -} - fn run_cec(args: &[&str]) -> bool { - match Command::new("cec-ctl") - .arg("-d") - .arg(CEC_DEVICE) - .args(args) - .output() - { - Ok(out) if out.status.success() => true, + if !std::path::Path::new(CEC_DEVICE).exists() { + return false; + } + match Command::new("cec-ctl").arg("-d").arg(CEC_DEVICE).args(args).output() { + Ok(out) if out.status.success() => { + info!("cec: ok"); + true + } Ok(out) => { - warn!("cec-ctl failed: {}", String::from_utf8_lossy(&out.stderr)); + warn!("cec failed: {}", String::from_utf8_lossy(&out.stderr)); false } Err(e) => { @@ -47,3 +52,50 @@ fn run_cec(args: &[&str]) -> bool { } } } + +/// Turn off all outputs via wlr-randr (Wayland compositors: labwc, wayfire, sway). +fn wlr_output_off() { + // Get list of outputs + let outputs = list_outputs(); + if outputs.is_empty() { + warn!("dpms: no outputs found"); + return; + } + for output in outputs { + match Command::new("wlr-randr") + .args(["--output", &output, "--off"]) + .output() + { + Ok(out) if out.status.success() => info!("dpms: {output} off"), + Ok(out) => warn!("dpms off {output} failed: {}", String::from_utf8_lossy(&out.stderr)), + Err(e) => warn!("wlr-randr unavailable: {e}"), + } + } +} + +fn wlr_output_on() { + let outputs = list_outputs(); + for output in outputs { + match Command::new("wlr-randr") + .args(["--output", &output, "--on"]) + .output() + { + Ok(out) if out.status.success() => info!("dpms: {output} on"), + Ok(out) => warn!("dpms on {output} failed: {}", String::from_utf8_lossy(&out.stderr)), + Err(e) => warn!("wlr-randr unavailable: {e}"), + } + } +} + +fn list_outputs() -> Vec { + let out = match Command::new("wlr-randr").output() { + Ok(out) => out, + Err(_) => return Vec::new(), + }; + let text = String::from_utf8_lossy(&out.stdout); + text.lines() + .filter(|l| !l.starts_with(' ') && !l.is_empty()) + .map(|l| l.split_whitespace().next().unwrap_or("").to_string()) + .filter(|s| !s.is_empty()) + .collect() +} diff --git a/kiosk/src/ui.rs b/kiosk/src/ui.rs index 485af6c..13c6eab 100644 --- a/kiosk/src/ui.rs +++ b/kiosk/src/ui.rs @@ -89,8 +89,8 @@ fn activate(app: &Application) { let bundle = server::fetch_bundle(&server_for_reload, &key_for_reload); let _ = tx_for_reload.send(WorkerMsg::RenderBundle(bundle)); } - ServerMsg::Standby => { cec::standby(); } - ServerMsg::Wake => { cec::wake(); } + ServerMsg::Standby => cec::standby(), + ServerMsg::Wake => cec::wake(), } } });