//! mmx150-probe — reverse-engineering tool for the MMX 150 dongle protocol. //! //! Phase 1 workflow: send candidate output reports to the dongle and observe //! what input reports come back, correlating fields with headset on/off, //! battery charge level and button presses. use std::process::ExitCode; use std::time::Duration; use mmx150::hid::{find_devices_by_vendor, Hidraw, BEYERDYNAMIC_VID}; fn main() -> ExitCode { let args: Vec = std::env::args().skip(1).collect(); match args.first().map(String::as_str) { Some("list") => cmd_list(&args[1..]), Some("scan") => cmd_scan(), Some("send") => cmd_send(&args[1..]), _ => { eprintln!("usage: mmx150-probe ...>"); ExitCode::FAILURE } } } fn cmd_list(_rest: &[String]) -> ExitCode { match find_devices_by_vendor(BEYERDYNAMIC_VID) { Ok(devices) => { for d in &devices { println!( "{:04x}:{:04x} {} {}", d.vid, d.pid, d.devnode.display(), d.name ); } if devices.is_empty() { eprintln!("no beyerdynamic hidraw devices found"); return ExitCode::FAILURE; } ExitCode::SUCCESS } Err(e) => { eprintln!("error: {e}"); ExitCode::FAILURE } } } fn cmd_scan() -> ExitCode { let Some(dev) = mmx150::hid::find_mmx150().unwrap_or(None) else { eprintln!("MMX 150 dongle not found"); return ExitCode::FAILURE; }; println!("device: {} at {}", dev.name, dev.devnode.display()); let hid = match Hidraw::open(&dev.devnode) { Ok(h) => h, Err(e) => { eprintln!("open failed: {e}"); return ExitCode::FAILURE; } }; // Known report sizes from the report descriptor: // 62-byte reports: 0x24/0x27, 0x80/0x81, 0x34/0x37 // 60-byte reports: 0x2a/0x2b, 0x3a/0x3b // 16-byte reports: 0x2c/0x2d, 0x3c/0x3d for (report_id, len) in [ (0x24, 63usize), (0x34, 63), (0x80, 63), (0x3a, 61), (0x3b, 61), (0x2a, 61), (0x2b, 61), (0x3d, 17), (0x2d, 17), ] { match hid.get_feature(report_id, len) { Ok(buf) => println!("FEATURE 0x{report_id:02x}: {}", hex(&buf)), Err(e) => println!("FEATURE 0x{report_id:02x}: {e}"), } } for (report_id, len) in [ (0x27, 63usize), (0x37, 63), (0x81, 63), (0x3c, 17), (0x3d, 17), (0x2c, 17), (0x2d, 17), ] { match hid.get_input(report_id, len) { Ok(buf) => println!("GINPUT 0x{report_id:02x}: {}", hex(&buf)), Err(e) => println!("GINPUT 0x{report_id:02x}: {e}"), } } ExitCode::SUCCESS } fn cmd_send(rest: &[String]) -> ExitCode { if rest.len() < 2 { eprintln!("usage: mmx150-probe send "); return ExitCode::FAILURE; } let Ok(report_id) = u8::from_str_radix(rest[0].trim_start_matches("0x"), 16) else { eprintln!("bad report id"); return ExitCode::FAILURE; }; let mut data = vec![report_id]; for part in &rest[1..] { let bytes = parse_hex(part); if bytes.is_empty() { eprintln!("bad hex: {part}"); return ExitCode::FAILURE; } data.extend(bytes); } let Some(dev) = mmx150::hid::find_mmx150().unwrap_or(None) else { eprintln!("MMX 150 dongle not found"); return ExitCode::FAILURE; }; let hid = match Hidraw::open(&dev.devnode) { Ok(h) => h, Err(e) => { eprintln!("open failed: {e}"); return ExitCode::FAILURE; } }; let mut hid = hid; println!( ">> 0x{report_id:02x} [{}B] {}", data.len() - 1, hex(&data[1..]) ); // Try both the interrupt OUT endpoint and the control-transfer output // report, then watch for a response on the interrupt IN endpoint. match hid.write(&data) { Ok(n) => println!("write() ok ({n}B)"), Err(e) => println!("write() failed: {e}"), } match hid.send_output(&data) { Ok(n) => println!("HIDIOCGSOUTPUT ok ({n}B)"), Err(e) => println!("HIDIOCGSOUTPUT failed: {e}"), } read_responses(&mut hid); ExitCode::SUCCESS } /// Poll the interrupt IN endpoint for a few seconds, printing reports. fn read_responses(hid: &mut Hidraw) { let deadline = std::time::Instant::now() + Duration::from_millis(3000); loop { let now = std::time::Instant::now(); if now >= deadline { break; } let mut set = unsafe { std::mem::zeroed::() }; unsafe { libc::FD_ZERO(&mut set) }; unsafe { libc::FD_SET(hid.as_raw_fd(), &mut set) }; let remaining = deadline.saturating_duration_since(now).as_millis(); let mut timeout = libc::timeval { tv_sec: (remaining / 1000) as i64, tv_usec: ((remaining % 1000) * 1000) as i64, }; let n = unsafe { libc::select( hid.as_raw_fd() + 1, &mut set, std::ptr::null_mut(), std::ptr::null_mut(), &mut timeout, ) }; if n <= 0 { break; } let mut buf = [0u8; 256]; match hid.read(&mut buf) { Ok(len) => println!("<< [{}B] {}", len, hex(&buf[..len])), Err(e) => { println!("read failed: {e}"); break; } } } } fn hex(bytes: &[u8]) -> String { bytes .iter() .map(|b| format!("{b:02x}")) .collect::>() .join(" ") } fn parse_hex(s: &str) -> Vec { let s = s.trim_start_matches("0x"); if s.len() % 2 != 0 { return Vec::new(); } s.as_bytes() .chunks(2) .map(|c| { let hi = (c[0] as char).to_digit(16)?; let lo = (c[1] as char).to_digit(16)?; Some((hi * 16 + lo) as u8) }) .collect::>>() .unwrap_or_default() }