aboutsummaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/probe.rs215
1 files changed, 215 insertions, 0 deletions
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<String> = 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 <list|scan|send <report_id> <hex>...>");
+ 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 <report_id> <hex bytes...>");
+ 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::<libc::fd_set>() };
+ 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::<Vec<_>>()
+ .join(" ")
+}
+
+fn parse_hex(s: &str) -> Vec<u8> {
+ 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::<Option<Vec<u8>>>()
+ .unwrap_or_default()
+}