aboutsummaryrefslogtreecommitdiff
path: root/src/bin/probe.rs
diff options
context:
space:
mode:
authorjamie <git@skilet.ro>2026-08-16 21:28:39 +0100
committerjamie <git@skilet.ro>2026-08-29 22:06:00 +0100
commite411f045ee1e8205a651be40434297549af91b11 (patch)
tree534febe39cacaaae48f71ba8ed814300cd77d8f2 /src/bin/probe.rs
parentbe625dc771273289d2d9681c02e8e767c3ffbc2f (diff)
feat(probe): status dump + connection-detect via 0x3a report
Diffstat (limited to '')
-rw-r--r--src/bin/probe.rs187
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 {