123 lines
3.8 KiB
Rust
123 lines
3.8 KiB
Rust
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<T> = Result<T, Box<dyn Error>>;
|
|
|
|
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<MonitorCommand>) {
|
|
let mut last_sent_command: HashMap<&str, Option<Instant>> = 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::<Vec<_>>();
|
|
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<Sender<MonitorCommand>>) -> StdError<()> {
|
|
match cmd {
|
|
Command::Monitor((idx, c)) => txes.get(idx).ok_or("Bad monitor index")?.send(c)?,
|
|
};
|
|
Ok(())
|
|
}
|