//! mmx150 — connection monitoring for the beyerdynamic MMX 150 wireless //! dongle. use std::process::{Child, Command, ExitCode, Stdio}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use mmx150::hid::{find_mmx150, Hidraw}; use mmx150::protocol::{self, LinkState, StateTracker, Transition}; fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); match args.first().map(String::as_str) { Some("status") => cmd_status(), Some("daemon") => cmd_daemon(&args[1..]), Some("probe") => cmd_probe(), _ => { print_usage(); ExitCode::FAILURE } } } fn print_usage() { eprintln!( "usage: mmx150 \n\ \n\ daemon options:\n\ \x20 --on-connect CMD run CMD (via sh -c) when the headset connects\n\ \x20 --on-disconnect CMD run CMD (via sh -c) when the headset disconnects\n\ \x20 --interval SECS poll interval (default: 2)\n\ \x20 --no-initial do not fire a hook for the state found at startup\n\ \n\ status exit codes: 0 connected, 1 disconnected, 2 dongle missing/error\n\ \n\ example:\n\ \x20 mmx150 daemon \\\n\ \x20 --on-connect 'wpctl set-default ' \\\n\ \x20 --on-disconnect 'wpctl set-default '\n\ \n\ find sink ids with: wpctl status" ); } fn cmd_status() -> ExitCode { let Some(dev) = find_mmx150().unwrap_or(None) else { eprintln!("dongle not found"); return ExitCode::from(2); }; let hid = match Hidraw::open(&dev.devnode) { Ok(h) => h, Err(e) => { eprintln!("error: {e}"); return ExitCode::from(2); } }; let mut hid = hid; match protocol::link_status(&mut hid) { Ok(LinkState::Connected) => { println!("connected"); ExitCode::SUCCESS } Ok(LinkState::Disconnected) => { println!("disconnected"); ExitCode::from(1) } Err(e) => { eprintln!("error: {e}"); ExitCode::from(2) } } } fn cmd_daemon(args: &[String]) -> ExitCode { let mut on_connect: Option = None; let mut on_disconnect: Option = None; let mut interval = Duration::from_secs(2); let mut no_initial = false; let mut it = args.iter(); while let Some(arg) = it.next() { match arg.as_str() { "--on-connect" => match it.next() { Some(c) => on_connect = Some(c.clone()), None => { eprintln!("--on-connect needs an argument"); return ExitCode::FAILURE; } }, "--on-disconnect" => match it.next() { Some(c) => on_disconnect = Some(c.clone()), None => { eprintln!("--on-disconnect needs an argument"); return ExitCode::FAILURE; } }, "--interval" => match it.next().and_then(|s| s.parse::().ok()) { Some(secs) if secs > 0 => interval = Duration::from_secs(secs), _ => { eprintln!("--interval needs a number of seconds > 0"); return ExitCode::FAILURE; } }, "--no-initial" => no_initial = true, other => { eprintln!("unknown option: {other}"); return ExitCode::FAILURE; } } } let mut hid: Option = None; let mut tracker = StateTracker::new(!no_initial); let mut children: Vec = Vec::new(); loop { let cycle = Instant::now(); reap_children(&mut children); let link_up = poll(&mut hid); match tracker.update(link_up) { Some(Transition::ToConnected { initial }) => { log_state("connected", initial); if let Some(cmd) = &on_connect { spawn_hook(cmd, &mut children); } } Some(Transition::ToDisconnected { initial }) => { log_state("disconnected", initial); if let Some(cmd) = &on_disconnect { spawn_hook(cmd, &mut children); } } None => {} } let elapsed = cycle.elapsed(); if elapsed < interval { std::thread::sleep(interval - elapsed); } } } /// One poll cycle: (re)open the dongle if needed, then query the link. /// Returns `true` if the headset answered. fn poll(hid: &mut Option) -> bool { if hid.is_none() { match find_mmx150() { Ok(Some(dev)) => match Hidraw::open(&dev.devnode) { Ok(h) => *hid = Some(h), Err(_) => return false, }, _ => return false, } } let Some(h) = hid.as_mut() else { return false; }; match protocol::link_status(h) { Ok(LinkState::Connected) => true, Ok(LinkState::Disconnected) => false, Err(_) => { *hid = None; false } } } fn spawn_hook(cmd: &str, children: &mut Vec) { match Command::new("sh") .arg("-c") .arg(cmd) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .spawn() { Ok(child) => children.push(child), Err(e) => eprintln!("[{}] hook spawn failed: {e}", timestamp()), } } /// Reap finished hook processes so they don't linger as zombies. fn reap_children(children: &mut Vec) { children.retain_mut(|c| c.try_wait().map_or(true, |s| s.is_none())); } fn log_state(state: &str, initial: bool) { println!( "[{}] headset {}{}", timestamp(), state, if initial { " (initial)" } else { "" } ); } fn timestamp() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0) } fn cmd_probe() -> ExitCode { eprintln!("use `mmx150-probe` binary instead"); ExitCode::FAILURE }