diff options
| author | jamie <git@skilet.ro> | 2026-08-16 21:28:39 +0100 |
|---|---|---|
| committer | jamie <git@skilet.ro> | 2026-08-29 22:06:00 +0100 |
| commit | e411f045ee1e8205a651be40434297549af91b11 (patch) | |
| tree | 534febe39cacaaae48f71ba8ed814300cd77d8f2 /src/bin | |
| parent | be625dc771273289d2d9681c02e8e767c3ffbc2f (diff) | |
feat(probe): status dump + connection-detect via 0x3a report
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/probe.rs | 187 |
1 files changed, 163 insertions, 24 deletions
diff --git a/src/bin/probe.rs b/src/bin/probe.rs index d250faa..b2e35d4 100644 --- a/src/bin/probe.rs +++ b/src/bin/probe.rs @@ -15,8 +15,10 @@ fn main() -> ExitCode { Some("list") => cmd_list(&args[1..]), Some("scan") => cmd_scan(), Some("send") => cmd_send(&args[1..]), + Some("sweep") => cmd_sweep(&args[1..]), + Some("status") => cmd_status_dump(), _ => { - eprintln!("usage: mmx150-probe <list|scan|send <report_id> <hex>...>"); + eprintln!("usage: mmx150-probe <list|scan|send|sweep|status>"); ExitCode::FAILURE } } @@ -135,35 +137,39 @@ fn cmd_send(rest: &[String]) -> ExitCode { 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}"), + match send_and_read(&mut hid, &data) { + Some(resp) => println!("<< [{}B] {}", resp.len(), hex(&resp)), + None => println!("<< no response"), } - - 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); +/// Send one full output report (including report-id byte as data[0]) via the +/// interrupt OUT endpoint and return the first input report received, if any. +fn send_and_read(hid: &mut Hidraw, data: &[u8]) -> Option<Vec<u8>> { + match hid.write(data) { + Ok(_) => {} + Err(e) => { + eprintln!("write() failed: {e}"); + return None; + } + } + read_one_response(hid, Duration::from_millis(400)) +} + +/// Wait up to `timeout` for a single input report and return it. +fn read_one_response(hid: &mut Hidraw, timeout: Duration) -> Option<Vec<u8>> { + let deadline = std::time::Instant::now() + timeout; loop { let now = std::time::Instant::now(); if now >= deadline { - break; + return None; } 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 { + let mut to = libc::timeval { tv_sec: (remaining / 1000) as i64, tv_usec: ((remaining % 1000) * 1000) as i64, }; @@ -173,21 +179,154 @@ fn read_responses(hid: &mut Hidraw) { &mut set, std::ptr::null_mut(), std::ptr::null_mut(), - &mut timeout, + &mut to, ) }; if n <= 0 { - break; + return None; } 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; + Ok(len) => return Some(buf[..len].to_vec()), + Err(_) => return None, + } + } +} + +/// Sweep a set of candidate commands against the dongle, printing responses +/// that differ from the all-zero baseline. Reports only non-trivial responses. +/// +/// usage: mmx150-probe sweep <report_id> <B> ... +/// <report_id> is the output report id to use, +/// <B> is the report payload length (bytes, excluding the id byte). +fn cmd_status_dump() -> ExitCode { + 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; + + // Each row: label, report id, command payload. + let cmds: Vec<(&str, u8, Vec<u8>)> = vec![ + ("0x2a echo/sel=0x0b", 0x2a, { + let mut c = vec![0u8; 60]; + c[0] = 0x09; + c[1] = 0x22; + c[2] = 0x0b; + c + }), + ("0x2a printf family 09 22", 0x2a, { + let mut c = vec![0u8; 60]; + c[0] = 0x09; + c[1] = 0x22; + c[2] = 0x01; + c + }), + ("0x2a eq family 09 00", 0x2a, { + let mut c = vec![0u8; 60]; + c[0] = 0x09; + c[1] = 0x00; + c[2] = 0x0b; + c + }), + ("0x3a sel=0x0b", 0x3a, { + let mut c = vec![0u8; 60]; + c[0] = 0x09; + c[1] = 0x22; + c[2] = 0x0b; + c + }), + ("0x3a sel=0x05", 0x3a, { + let mut c = vec![0u8; 60]; + c[0] = 0x09; + c[1] = 0x22; + c[2] = 0x05; + c + }), + ("0x3a sel=0x01", 0x3a, { + let mut c = vec![0u8; 60]; + c[0] = 0x09; + c[1] = 0x22; + c[2] = 0x01; + c + }), + ("0x2d signature", 0x2d, vec![0u8; 16]), + ("0x3d signature", 0x3d, vec![0u8; 16]), + ]; + + for (label, rid, payload) in cmds { + let mut frame = vec![rid]; + frame.extend(payload); + let resp = send_and_read(&mut hid, &frame); + println!( + "{label:<26} >>0x{rid:02x} << {}", + resp.map(|b| hex(&b)).unwrap_or_else(|| "<none>".into()) + ); + std::thread::sleep(Duration::from_millis(120)); + } + ExitCode::SUCCESS +} + +fn cmd_sweep(rest: &[String]) -> ExitCode { + if rest.len() < 1 { + eprintln!("usage: mmx150-probe sweep <report_id> <payload_len>"); + 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 plen: usize = rest.get(1).and_then(|s| s.parse().ok()).unwrap_or(60); + + 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; + + // Baseline: all-zero command. + let base = { + let mut cmd = vec![report_id]; + cmd.extend(std::iter::repeat(0u8).take(plen)); + send_and_read(&mut hid, &cmd) + }; + println!( + "baseline (all zero): {}", + base.as_ref() + .map(|b| hex(b.as_slice())) + .unwrap_or_else(|| "<none>".into()) + ); + + // Try setting individual low nibbles / bytes across the header to probe + // which command byte selects a status register. + for idx in 1..plen.clamp(1, 12) { + for val in [0x01u8, 0x22, 0x2c, 0x2f, 0x80, 0x5a, 0xff] { + let mut cmd = vec![report_id]; + cmd.extend(std::iter::repeat(0u8).take(plen)); + cmd[idx] = val; + match send_and_read(&mut hid, &cmd) { + Some(resp) if Some(&resp) != base.as_ref() => { + println!("cmd[{idx:02}]=0x{val:02x} -> {}", hex(&resp)); + } + _ => {} } } } + ExitCode::SUCCESS } fn hex(bytes: &[u8]) -> String { |
