use std::collections::BTreeMap;
use std::sync::mpsc;
use std::time::Duration;
use ddc_hi::{Ddc, Display as DdcDisplay, DisplayInfo, FeatureCode};
use displays_physical_types::{
PhysicalDisplayMetadata, PhysicalDisplayState, PhysicalDisplayUpdate,
};
use displays_types::Brightness;
use crate::error::{ApplyError, QueryError};
use crate::types::{remaining_update, Backend, DdcApplyUpdate, DisplayHandle};
const PER_MONITOR_APPLY_TIMEOUT: Duration = Duration::from_millis(3500);
pub(crate) fn enumerate_handles() -> Result<Vec<DisplayHandle>, QueryError> {
let mut handles = Vec::new();
for (display_index, mut display) in DdcDisplay::enumerate().into_iter().enumerate() {
let info = display.info.clone();
let display_id = info.id.clone();
let brightness = match display.handle.get_vcp_feature(FeatureCode::from(0x10)) {
Ok(vcp) => {
let maximum = vcp.maximum();
if maximum == 0 {
tracing::warn!(
"Brightness is unavailable for display '{}' because max was reported as 0",
display_id
);
None
} else {
Some(Brightness::new(
(((vcp.value() as f64 / maximum as f64) * 100.0).round() as u8).min(100),
))
}
}
Err(err) => {
let message = err.to_string();
let detail = if is_io_error(&message) {
"I/O error"
} else {
"query error"
};
tracing::warn!(
"Brightness is unavailable for display '{}' due to {}: {}",
display_id,
detail,
message
);
None
}
};
handles.push(DisplayHandle {
metadata: metadata_from_info(&info),
state: PhysicalDisplayState { brightness },
backend: Backend::Ddc { display_index },
});
}
Ok(handles)
}
pub(crate) fn apply_updates(updates: Vec<DdcApplyUpdate>) -> Vec<PhysicalDisplayUpdate> {
if updates.is_empty() {
return Vec::new();
}
let mut remaining_updates = Vec::new();
let mut display_by_index: BTreeMap<usize, DdcDisplay> =
DdcDisplay::enumerate().into_iter().enumerate().collect();
for update in updates {
let Some(brightness_percent) = update.content.brightness else {
continue;
};
let Some(display) = display_by_index.remove(&update.display_index) else {
remaining_updates.push(remaining_update(update.id, brightness_percent));
continue;
};
let display_id = display.info.id.clone();
if let Err(err) = set_brightness_with_timeout(display, brightness_percent) {
tracing::warn!(
"Failed to set brightness for display '{}': {}",
display_id,
err
);
remaining_updates.push(remaining_update(update.id, brightness_percent));
}
}
remaining_updates
}
fn set_brightness(display: &mut DdcDisplay, brightness_percent: u32) -> Result<(), ApplyError> {
let ddc_id = display.info.id.clone();
let normalized = brightness_percent.min(100);
let target_value = if normalized == 0 {
0
} else {
let vcp = display
.handle
.get_vcp_feature(FeatureCode::from(0x10))
.map_err(|err| classify_apply_error(ddc_id.clone(), err.to_string()))?;
let max = vcp.maximum();
if max == 0 {
return Err(ApplyError::UnsupportedMonitor {
display_id: ddc_id,
message: "reported brightness max value is 0".to_string(),
});
}
let percent = normalized as f64 / 100.0;
(percent * max as f64).round() as u16
};
display
.handle
.set_vcp_feature(FeatureCode::from(0x10), target_value)
.map_err(|err| classify_apply_error(display.info.id.clone(), err.to_string()))
}
fn set_brightness_with_timeout(
display: DdcDisplay,
brightness_percent: u32,
) -> Result<(), ApplyError> {
let display_id = display.info.id.clone();
let (sender, receiver) = mpsc::channel();
std::thread::spawn(move || {
let mut display = display;
let result = set_brightness(&mut display, brightness_percent);
let _ = sender.send(result);
});
match receiver.recv_timeout(PER_MONITOR_APPLY_TIMEOUT) {
Ok(result) => result,
Err(mpsc::RecvTimeoutError::Timeout) => Err(ApplyError::DdcOperation {
display_id,
message: format!("timed out after {:?}", PER_MONITOR_APPLY_TIMEOUT),
}),
Err(mpsc::RecvTimeoutError::Disconnected) => Err(ApplyError::DdcOperation {
display_id,
message: "apply worker disconnected unexpectedly".to_string(),
}),
}
}
fn metadata_from_info(info: &DisplayInfo) -> PhysicalDisplayMetadata {
let model = info
.model_name
.clone()
.or_else(|| info.model_id.map(|model_id| format!("0x{model_id:04X}")));
let name = info
.model_name
.clone()
.unwrap_or_else(|| format!("Display {}", info.id));
let serial_number = info.serial_number.clone().or_else(|| {
info.serial
.filter(|serial| *serial != 0)
.map(|serial| serial.to_string())
});
PhysicalDisplayMetadata {
path: info.id.clone(),
name,
manufacturer: info.manufacturer_id.clone(),
model,
serial_number,
}
}
#[cfg(test)]
mod tests {
use ddc_hi::{Backend, DisplayInfo};
use super::metadata_from_info;
#[test]
fn metadata_from_info_uses_available_display_fields() {
let info = DisplayInfo {
backend: Backend::I2cDevice,
id: "/dev/i2c-7".to_string(),
manufacturer_id: Some("DEL".to_string()),
model_id: Some(0x1234),
version: None,
serial: Some(42),
manufacture_year: None,
manufacture_week: None,
model_name: Some("U2723QE".to_string()),
serial_number: Some("ABC123".to_string()),
edid_data: None,
mccs_version: None,
mccs_database: Default::default(),
};
let metadata = metadata_from_info(&info);
assert_eq!(metadata.path, "/dev/i2c-7");
assert_eq!(metadata.name, "U2723QE");
assert_eq!(metadata.manufacturer.as_deref(), Some("DEL"));
assert_eq!(metadata.model.as_deref(), Some("U2723QE"));
assert_eq!(metadata.serial_number.as_deref(), Some("ABC123"));
}
}
fn classify_apply_error(display_id: String, message: String) -> ApplyError {
let lowercase = message.to_lowercase();
if lowercase.contains("permission denied") {
return ApplyError::PermissionDenied { display_id };
}
if lowercase.contains("/dev/i2c") || lowercase.contains("no such file") {
return ApplyError::MissingI2cAccess { display_id };
}
if lowercase.contains("unsupported") || lowercase.contains("vcp") {
return ApplyError::UnsupportedMonitor {
display_id,
message,
};
}
ApplyError::DdcOperation {
display_id,
message,
}
}
fn is_io_error(message: &str) -> bool {
let lowercase = message.to_lowercase();
lowercase.contains("input/output error") || lowercase.contains("os error 5")
}