diff options
Diffstat (limited to 'src/bin/probe.rs')
| -rw-r--r-- | src/bin/probe.rs | 182 |
1 files changed, 100 insertions, 82 deletions
diff --git a/src/bin/probe.rs b/src/bin/probe.rs index 008565c..cf51d81 100644 --- a/src/bin/probe.rs +++ b/src/bin/probe.rs @@ -11,13 +11,14 @@ use mmx150::hid::{find_devices_by_vendor, Hidraw, BEYERDYNAMIC_VID}; fn main() -> ExitCode { let args: Vec<String> = std::env::args().skip(1).collect(); + let rest: &[String] = args.get(1..).unwrap_or(&[]); match args.first().map(String::as_str) { - Some("list") => cmd_list(&args[1..]), + Some("list") => cmd_list(rest), Some("scan") => cmd_scan(), - Some("send") => cmd_send(&args[1..]), - Some("sweep") => cmd_sweep(&args[1..]), + Some("send") => cmd_send(rest), + Some("sweep") => cmd_sweep(rest), Some("status") => cmd_status_dump(), - Some("selenum") => cmd_sel_enum(&args[1..]), + Some("selenum") => cmd_sel_enum(rest), _ => { eprintln!("usage: mmx150-probe <list|scan|send|sweep|status|selenum>"); ExitCode::FAILURE @@ -122,12 +123,15 @@ fn cmd_send(rest: &[String]) -> ExitCode { 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 { + let Some(id_text) = rest.first() else { + return ExitCode::FAILURE; + }; + let Ok(report_id) = u8::from_str_radix(id_text.trim_start_matches("0x"), 16) else { eprintln!("bad report id"); return ExitCode::FAILURE; }; let mut data = vec![report_id]; - for part in &rest[1..] { + for part in rest.get(1..).unwrap_or(&[]) { let bytes = parse_hex(part); if bytes.is_empty() { eprintln!("bad hex: {part}"); @@ -142,11 +146,11 @@ fn cmd_send(rest: &[String]) -> ExitCode { println!( ">> 0x{report_id:02x} [{}B] {}", - data.len() - 1, - hex(&data[1..]) + data.len().saturating_sub(1), + hex(data.get(1..).unwrap_or(&[])) ); match send_and_read(&mut hid, &data) { - Some(resp) => println!("<< [{}B] {}", resp.len(), hex(&resp)), + Some(response) => println!("<< [{}B] {}", response.len(), hex(&response)), None => println!("<< no response"), } ExitCode::SUCCESS @@ -176,48 +180,20 @@ fn cmd_status_dump() -> ExitCode { // 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 - }), + ("0x2a echo/sel=0x0b", 0x2a, cmd_payload(&[0x09, 0x22, 0x0b])), + ( + "0x2a printf family 09 22", + 0x2a, + cmd_payload(&[0x09, 0x22, 0x01]), + ), + ( + "0x2a eq family 09 00", + 0x2a, + cmd_payload(&[0x09, 0x00, 0x0b]), + ), + ("0x3a sel=0x0b", 0x3a, cmd_payload(&[0x09, 0x22, 0x0b])), + ("0x3a sel=0x05", 0x3a, cmd_payload(&[0x09, 0x22, 0x05])), + ("0x3a sel=0x01", 0x3a, cmd_payload(&[0x09, 0x22, 0x01])), ("0x2d signature", 0x2d, vec![0u8; 16]), ("0x3d signature", 0x3d, vec![0u8; 16]), ]; @@ -225,21 +201,33 @@ fn cmd_status_dump() -> ExitCode { for (label, rid, payload) in cmds { let mut frame = vec![rid]; frame.extend(payload); - let resp = send_and_read(&mut hid, &frame); + let response = send_and_read(&mut hid, &frame); println!( "{label:<26} >>0x{rid:02x} << {}", - resp.map(|b| hex(&b)).unwrap_or_else(|| "<none>".into()) + response + .as_deref() + .map_or_else(|| "<none>".to_string(), hex) ); std::thread::sleep(Duration::from_millis(120)); } ExitCode::SUCCESS } +/// A 60-byte command payload with `bytes` at the front and zero padding. +fn cmd_payload(bytes: &[u8]) -> Vec<u8> { + let mut payload = vec![0u8; 60]; + let n = bytes.len().min(60); + if let (Some(slot), Some(source)) = (payload.get_mut(..n), bytes.get(..n)) { + slot.copy_from_slice(source); + } + payload +} + /// 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) +/// usage: mmx150-probe selenum `[report_id]` (default: 0x3a) fn cmd_sel_enum(rest: &[String]) -> ExitCode { let report_id = rest .first() @@ -252,25 +240,18 @@ fn cmd_sel_enum(rest: &[String]) -> ExitCode { 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); + frame.extend(cmd_payload(&[0x09, 0x22, sel])); + let response = 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)); + if let Some(bytes) = response { + if bytes + .get(6..) + .is_some_and(|tail| tail.iter().any(|&x| x != 0x00)) + { + println!("sel={sel:02x} resp={}", hex(&bytes)); } std::thread::sleep(Duration::from_millis(8)); } @@ -283,7 +264,10 @@ fn cmd_sweep(rest: &[String]) -> ExitCode { 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 { + let Some(id_text) = rest.first() else { + return ExitCode::FAILURE; + }; + let Ok(report_id) = u8::from_str_radix(id_text.trim_start_matches("0x"), 16) else { eprintln!("bad report id"); return ExitCode::FAILURE; }; @@ -294,16 +278,16 @@ fn cmd_sweep(rest: &[String]) -> ExitCode { }; // Baseline: all-zero command. - let base = { + let baseline = { let mut cmd = vec![report_id]; cmd.extend(vec![0u8; 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()) + baseline + .as_deref() + .map_or_else(|| "<none>".to_string(), hex) ); // Try setting individual low nibbles / bytes across the header to probe @@ -312,10 +296,12 @@ fn cmd_sweep(rest: &[String]) -> ExitCode { for val in [0x01u8, 0x22, 0x2c, 0x2f, 0x80, 0x5a, 0xff] { let mut cmd = vec![report_id]; cmd.extend(vec![0u8; plen]); - cmd[idx] = val; + if let Some(slot) = cmd.get_mut(idx) { + *slot = 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)); + Some(response) if Some(&response) != baseline.as_ref() => { + println!("cmd[{idx:02}]=0x{val:02x} -> {}", hex(&response)); } _ => {} } @@ -338,12 +324,44 @@ fn parse_hex(s: &str) -> Vec<u8> { 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) + .chunks_exact(2) + .map(|pair| { + let [hi, lo] = pair else { return None }; + let hi = char::from(*hi).to_digit(16)?; + let lo = char::from(*lo).to_digit(16)?; + u8::try_from((hi << 4) | lo).ok() }) .collect::<Option<Vec<u8>>>() .unwrap_or_default() } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_hex_pairs() { + assert_eq!(parse_hex("0922"), vec![0x09, 0x22]); + assert_eq!(parse_hex("0x0922"), vec![0x09, 0x22]); + assert_eq!(parse_hex("ff"), vec![0xff]); + assert_eq!(parse_hex("FF"), vec![0xff]); + assert_eq!(parse_hex("0A0b"), vec![0x0a, 0x0b]); + } + + #[test] + fn rejects_odd_length() { + assert!(parse_hex("092").is_empty()); + } + + #[test] + fn rejects_invalid_digits() { + assert!(parse_hex("zz").is_empty()); + assert!(parse_hex("0g").is_empty()); + } + + #[test] + fn empty_input_yields_empty() { + assert!(parse_hex("").is_empty()); + assert!(parse_hex("0x").is_empty()); + } +} |
