aboutsummaryrefslogtreecommitdiff
path: root/src/bin/probe.rs
diff options
context:
space:
mode:
authorjamie <git@skilet.ro>2026-08-29 21:58:43 +0100
committerjamie <git@skilet.ro>2026-08-29 22:06:00 +0100
commitc1110ac81f87ba374696e419f3807f48a3a06d6d (patch)
tree6d03e2747657c1b2e30baba309d4620ce3227eaf /src/bin/probe.rs
parent3061396dbb60b5b294c1c0c62c25da8de5c87ffb (diff)
feat(daemon): link-state hooks for headset connect/disconnect
Diffstat (limited to 'src/bin/probe.rs')
-rw-r--r--src/bin/probe.rs103
1 files changed, 61 insertions, 42 deletions
diff --git a/src/bin/probe.rs b/src/bin/probe.rs
index b2e35d4..e77e6cd 100644
--- a/src/bin/probe.rs
+++ b/src/bin/probe.rs
@@ -17,8 +17,9 @@ fn main() -> ExitCode {
Some("send") => cmd_send(&args[1..]),
Some("sweep") => cmd_sweep(&args[1..]),
Some("status") => cmd_status_dump(),
+ Some("selenum") => cmd_sel_enum(&args[1..]),
_ => {
- eprintln!("usage: mmx150-probe <list|scan|send|sweep|status>");
+ eprintln!("usage: mmx150-probe <list|scan|send|sweep|status|selenum>");
ExitCode::FAILURE
}
}
@@ -154,43 +155,9 @@ fn send_and_read(hid: &mut Hidraw, data: &[u8]) -> Option<Vec<u8>> {
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 {
- 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 to = 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 to,
- )
- };
- if n <= 0 {
- return None;
- }
- let mut buf = [0u8; 256];
- match hid.read(&mut buf) {
- Ok(len) => return Some(buf[..len].to_vec()),
- Err(_) => return None,
- }
- }
+ hid.read_with_timeout(Duration::from_millis(400))
+ .ok()
+ .flatten()
}
/// Sweep a set of candidate commands against the dongle, printing responses
@@ -274,8 +241,60 @@ fn cmd_status_dump() -> ExitCode {
ExitCode::SUCCESS
}
+/// Enumerate many sub-command selectors across a report family, dumping any
+/// response whose payload carries a real data byte (not just an echo of the
+/// selector / a constant). This surfaces battery and state registers.
+///
+/// usage: mmx150-probe selenum [report_id] (default: 0x3a)
+fn cmd_sel_enum(rest: &[String]) -> ExitCode {
+ let report_id = rest
+ .first()
+ .and_then(|s| u8::from_str_radix(s.trim_start_matches("0x"), 16).ok())
+ .unwrap_or(0x3a);
+
+ 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!("sweeping report 0x{report_id:02x}, selector range 0x00..0xff");
+ for sel in 0x00u8..=0xffu8 {
+ let mut payload = vec![0u8; 60];
+ payload[0] = 0x09;
+ payload[1] = 0x22;
+ payload[2] = sel;
+ let mut frame = vec![report_id];
+ frame.extend(payload);
+ let resp = send_and_read(&mut hid, &frame);
+
+ // Only report responses containing a "real" data byte beyond the
+ // id/echo/constant region that could be battery/state.
+ if let Some(b) = resp {
+ if b.len() < 6 {
+ continue;
+ }
+ // b[1] is the echo of sel, b[5] the constant ack. Look for any
+ // non-zero byte among the remaining payload that isn't the ack.
+ let interesting = b[6..].iter().any(|&x| x != 0x00);
+ if interesting {
+ println!("sel={sel:02x} resp={}", hex(&b));
+ }
+ std::thread::sleep(Duration::from_millis(8));
+ }
+ }
+ ExitCode::SUCCESS
+}
+
fn cmd_sweep(rest: &[String]) -> ExitCode {
- if rest.len() < 1 {
+ if rest.is_empty() {
eprintln!("usage: mmx150-probe sweep <report_id> <payload_len>");
return ExitCode::FAILURE;
}
@@ -301,7 +320,7 @@ fn cmd_sweep(rest: &[String]) -> ExitCode {
// Baseline: all-zero command.
let base = {
let mut cmd = vec![report_id];
- cmd.extend(std::iter::repeat(0u8).take(plen));
+ cmd.extend(vec![0u8; plen]);
send_and_read(&mut hid, &cmd)
};
println!(
@@ -316,7 +335,7 @@ fn cmd_sweep(rest: &[String]) -> ExitCode {
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.extend(vec![0u8; plen]);
cmd[idx] = val;
match send_and_read(&mut hid, &cmd) {
Some(resp) if Some(&resp) != base.as_ref() => {
@@ -339,7 +358,7 @@ fn hex(bytes: &[u8]) -> String {
fn parse_hex(s: &str) -> Vec<u8> {
let s = s.trim_start_matches("0x");
- if s.len() % 2 != 0 {
+ if !s.len().is_multiple_of(2) {
return Vec::new();
}
s.as_bytes()