use ddc::Ddc; use ddc_i2c::{I2cDeviceDdc, I2cDeviceEnumerator}; use std::collections::HashMap; use std::env; use std::error::Error; use std::sync::mpsc::{channel, Receiver, Sender}; use std::thread; use std::time::{Duration, Instant}; use tracing::{event, Level}; mod mqtt; mod osc; pub type StdError = Result>; enum Command { Monitor((usize, MonitorCommand)), } #[derive(Debug)] pub enum MonitorBrightnessCmd { Absolute(u8), Relative(i8), } #[derive(Debug)] pub enum MonitorCommand { Brightness(MonitorBrightnessCmd), Input(u8), } impl MonitorCommand { fn cmd_str(&self) -> &'static str { match self { Self::Brightness(_) => "brightness", Self::Input(_) => "input", } } fn vcp(&self) -> u8 { match self { Self::Brightness(_) => 0x10, Self::Input(_) => 0x60, } } } // Run an i2c command handler for a specific i2c device, rate limiting each type of command fn run_i2c(idx: usize, mut dev: I2cDeviceDdc, command_channel: Receiver) { let mut last_sent_command: HashMap<&str, Option> = HashMap::new(); loop { let cmd = command_channel.recv().unwrap(); if let Some(last) = last_sent_command.entry(cmd.cmd_str()).or_insert(None) { if (*last + Duration::from_millis(100)) > Instant::now() { // rate limit continue; } } event!(Level::INFO, monitor_index = idx, ?cmd, "Sending DDC comand"); if let Err(e) = handle_monitor_cmd(&cmd, &mut dev) { event!(Level::WARN, err = %e, "Error sending DDC command"); } last_sent_command.insert(cmd.cmd_str(), Some(Instant::now())); } } fn handle_monitor_cmd(cmd: &MonitorCommand, dev: &mut I2cDeviceDdc) -> StdError<()> { match cmd { MonitorCommand::Brightness(MonitorBrightnessCmd::Absolute(b)) => { event!(Level::INFO, brightness = b, "Setting monitor brightness"); dev.set_vcp_feature(cmd.vcp(), (*b).into()) .map_err(|e| e.into()) } MonitorCommand::Brightness(MonitorBrightnessCmd::Relative(b)) => { let current = dev.get_vcp_feature(cmd.vcp())?; let (current, max) = (current.value(), current.maximum()); event!(Level::INFO, current, max, "Got brightness of monitor"); if max != 100 { return Err(format!("Expected maximum brightness to be 100, got {}", max).into()); } let mut new = current.saturating_add_signed(*b as i16); // Clamp to 0-100 if new > 100 { new = 100; } event!(Level::INFO, brightness = new, "Setting monitor brightness"); dev.set_vcp_feature(cmd.vcp(), new).map_err(|e| e.into()) } // Hack - add 15 to align with DELL monitors MonitorCommand::Input(i) => dev .set_vcp_feature(cmd.vcp(), (i + 15).into()) .map_err(|e| e.into()), } } pub fn main() -> StdError<()> { event!(Level::INFO, "Starting"); let displays = I2cDeviceEnumerator::new().unwrap().collect::>(); event!(Level::INFO, "Enumerated {} displays", displays.len()); let txes: Vec<_> = displays .into_iter() .enumerate() .map(|(idx, d)| { let (tx, rx) = channel(); thread::spawn(move || run_i2c(idx, d, rx)); tx }) .collect(); if let Ok(_) = env::var("OSC_MODE") { osc::run_osc(&txes)?; } else { mqtt::run_mqtt(&txes)?; } Ok(()) } fn handle_cmd(cmd: Command, txes: &Vec>) -> StdError<()> { match cmd { Command::Monitor((idx, c)) => txes.get(idx).ok_or("Bad monitor index")?.send(c)?, }; Ok(()) }