From be625dc771273289d2d9681c02e8e767c3ffbc2f Mon Sep 17 00:00:00 2001 From: jamie Date: Sun, 16 Aug 2026 21:20:57 +0100 Subject: chore: init --- src/bin/probe.rs | 215 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/bin/probe.rs (limited to 'src/bin/probe.rs') diff --git a/src/bin/probe.rs b/src/bin/probe.rs new file mode 100644 index 0000000..d250faa --- /dev/null +++ b/src/bin/probe.rs @@ -0,0 +1,215 @@ +//! 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() +} -- cgit v1.3.1