aboutsummaryrefslogtreecommitdiff
path: root/src/bin/probe.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/probe.rs182
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());
+ }
+}