Skip to content

Commit

Permalink
Merge pull request #207 from martinling/hid-report
Browse files Browse the repository at this point in the history
Add decoding of HID report descriptors
  • Loading branch information
miek authored Oct 31, 2024
2 parents 28deb23 + 40cac92 commit 83331c6
Show file tree
Hide file tree
Showing 24 changed files with 538 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
/tests/*/output.txt
/tests/*/devices-output.txt
/tests/ui/*/output.txt
/tests/hid/*/output.txt
/vcpkg
/wix/LICENSE-dynamic-libraries.txt
/wix/LICENSE-packetry.txt
Expand Down
20 changes: 20 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ anyhow = { version = "1.0.79", features = ["backtrace"] }
crc = "3.2.1"
usb-ids = "1.2024.4"
dark-light = "1.1.1"
hidreport = "0.4.1"
hut = "0.2.1"

[dev-dependencies]
serde = { version = "1.0.196", features = ["derive"] }
Expand Down
4 changes: 2 additions & 2 deletions src/capture.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1750,9 +1750,9 @@ impl ItemSource<TrafficItem, TrafficViewMode> for CaptureReader {
"End of SOF groups"),
(Request(transfer), true) if detail => write!(s,
"Control transfer on device {addr}\n{}",
transfer.summary()),
transfer.summary(true)),
(Request(transfer), true) => write!(s,
"{}", transfer.summary()),
"{}", transfer.summary(false)),
(IncompleteRequest, true) => write!(s,
"Incomplete control transfer on device {addr}"),
(Request(_) | IncompleteRequest, false) => write!(s,
Expand Down
199 changes: 198 additions & 1 deletion src/usb.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use std::collections::BTreeMap;
use std::fmt::Formatter;
use std::mem::size_of;
use std::ops::Range;

use bytemuck_derive::{Pod, Zeroable};
use bytemuck::pod_read_unaligned;
use crc::{Crc, CRC_16_USB};
use hidreport::{
Field,
LogicalMaximum,
LogicalMinimum,
ParserError,
Report,
ReportCount,
ReportDescriptor,
};
use itertools::{Itertools, Position};
use num_enum::{IntoPrimitive, FromPrimitive};
use derive_more::{From, Into, Display};
use usb_ids::FromId;
Expand Down Expand Up @@ -1160,7 +1171,7 @@ pub struct ControlTransfer {
}

impl ControlTransfer {
pub fn summary(&self) -> String {
pub fn summary(&self, detail: bool) -> String {
let request_type = self.fields.type_fields.request_type();
let direction = self.fields.type_fields.direction();
let request = self.fields.request;
Expand Down Expand Up @@ -1221,6 +1232,14 @@ impl ControlTransfer {
parts.push(
format!(": {}", UTF16Bytes(&self.data[2..size])));
},
(RequestType::Standard,
StandardRequest::GetDescriptor,
DescriptorType::Class(0x22))
if detail && self.recipient_class == Some(ClassId::HID) =>
{
parts.push(
format!("\n{}", HidReportDescriptor::from(&self.data)));
}
(..) => {}
};
let summary = parts.concat();
Expand Down Expand Up @@ -1281,9 +1300,152 @@ impl std::fmt::Display for UTF16ByteVec {
}
}

struct HidReportDescriptor(Result<ReportDescriptor, ParserError>);

impl HidReportDescriptor {
fn from(data: &[u8]) -> Self {
Self(ReportDescriptor::try_from(data))
}
}

impl std::fmt::Display for HidReportDescriptor{
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
match &self.0 {
Ok(desc) => {
for report in desc.input_reports() {
write_report(f, "Input", report)?;
}
for report in desc.output_reports() {
write_report(f, "Output", report)?;
}
},
Err(parse_err) => write!(f,
"\nFailed to parse report descriptor: {parse_err}")?,
}
Ok(())
}
}

fn write_report(f: &mut Formatter<'_>, kind: &str, report: &impl Report)
-> Result<(), std::fmt::Error>
{
use Field::*;
use Position::*;
write!(f, "\n○ {kind} report ")?;
match (report.report_id(), report.size_in_bytes()) {
(Some(id), 1) => writeln!(f, "#{id} (1 byte):")?,
(Some(id), n) => writeln!(f, "#{id} ({n} bytes):")?,
(None, 1) => writeln!(f, "(1 byte):")?,
(None, n) => writeln!(f, "({n} bytes):")?,
}
for (position, field) in report.fields().iter().with_position() {
match position {
First | Middle => write!(f, "├── ")?,
Last | Only => write!(f, "└── ")?,
};
match &field {
Array(array) => {
write!(f, "Array of {} {}: ",
array.report_count,
if array.report_count == ReportCount::from(1) {
"button"
} else {
"buttons"
}
)?;
write_bits(f, &array.bits)?;
let usage_range = array.usage_range();
write!(f, " [")?;
write_usage(f, &usage_range.minimum())?;
write!(f, " — ")?;
write_usage(f, &usage_range.maximum())?;
write!(f, "]")?;
}
Variable(var) => {
write_usage(f, &var.usage)?;
write!(f, ": ")?;
write_bits(f, &var.bits)?;
let bit_count = var.bits.end - var.bits.start;
if bit_count > 1 {
let max = (1 << bit_count) - 1;
if var.logical_minimum != LogicalMinimum::from(0) ||
var.logical_maximum != LogicalMaximum::from(max)
{
write!(f, " (values {} to {})",
var.logical_minimum,
var.logical_maximum)?;
}
}
},
Constant(constant) => {
write!(f, "Padding: ")?;
write_bits(f, &constant.bits)?;
},
};
writeln!(f)?;
}
Ok(())
}

fn write_usage<T>(f: &mut Formatter, usage: T)
-> Result<(), std::fmt::Error>
where u32: From<T>
{
let usage_code: u32 = usage.into();
match hut::Usage::try_from(usage_code) {
Ok(usage) => write!(f, "{usage}")?,
Err(_) => {
let page: u16 = (usage_code >> 16) as u16;
let id: u16 = usage_code as u16;
match hut::UsagePage::try_from(page) {
Ok(page) => write!(f,
"{} usage 0x{id:02X}", page.name())?,
Err(_) => write!(f,
"Unknown page 0x{page:02X} usage 0x{id:02X}")?,
}
}
};
Ok(())
}

fn write_bits(f: &mut Formatter, bit_range: &Range<usize>)
-> Result<(), std::fmt::Error>
{
let bit_count = bit_range.end - bit_range.start;
let byte_range = (bit_range.start / 8)..((bit_range.end - 1)/ 8);
let byte_count = byte_range.end - byte_range.start;
match (byte_count, bit_count) {
(_, 1) => write!(f,
"byte {} bit {}",
byte_range.start,
bit_range.start % 8)?,
(0, n) if n == 8 && bit_range.start % 8 == 0 => write!(f,
"byte {}",
byte_range.start)?,
(0, _) => write!(f,
"byte {} bits {}-{}",
byte_range.start,
bit_range.start % 8, (bit_range.end - 1) % 8)?,
(_, n) if n % 8 == 0 && bit_range.start % 8 == 0 => write!(f,
"bytes {}-{}",
byte_range.start,
byte_range.end)?,
(_, _) => write!(f,
"byte {} bit {} — byte {} bit {}",
byte_range.start,
bit_range.start % 8,
byte_range.end,
(bit_range.end - 1) % 8)?,
}
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;

#[test]
fn test_parse_sof() {
Expand Down Expand Up @@ -1339,6 +1501,41 @@ mod tests {
panic!("Expected Data but got {:?}", p);
}
}

#[test]
fn test_parse_hid() {
let test_dir = PathBuf::from("./tests/hid/");
let mut list_path = test_dir.clone();
list_path.push("tests.txt");
let list_file = File::open(list_path).unwrap();
for test_name in BufReader::new(list_file).lines() {
let mut test_path = test_dir.clone();
test_path.push(test_name.unwrap());
let mut desc_path = test_path.clone();
let mut ref_path = test_path.clone();
let mut out_path = test_path.clone();
desc_path.push("descriptor.bin");
ref_path.push("reference.txt");
out_path.push("output.txt");
{
let data = std::fs::read(desc_path).unwrap();
let descriptor = HidReportDescriptor::from(&data);
let out_file = File::create(out_path.clone()).unwrap();
let mut writer = BufWriter::new(out_file);
write!(writer, "{descriptor}").unwrap();
}
let ref_file = File::open(ref_path).unwrap();
let out_file = File::open(out_path.clone()).unwrap();
let ref_reader = BufReader::new(ref_file);
let out_reader = BufReader::new(out_file);
let mut out_lines = out_reader.lines();
for line in ref_reader.lines() {
let expected = line.unwrap();
let actual = out_lines.next().unwrap().unwrap();
assert_eq!(actual, expected);
}
}
}
}

pub mod prelude {
Expand Down
Binary file added tests/hid/amp/descriptor.bin
Binary file not shown.
67 changes: 67 additions & 0 deletions tests/hid/amp/reference.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@

○ Input report #3 (2 bytes):
├── Volume Increment: byte 1 bit 0
├── Volume Decrement: byte 1 bit 1
├── Play/Pause: byte 1 bit 2
├── Voice Command: byte 1 bit 3
├── Scan Previous Track: byte 1 bit 4
├── Scan Next Track: byte 1 bit 5
└── Padding: byte 1 bits 6-7

○ Input report #5 (17 bytes):
├── Consumer usage 0x00: byte 1 (values 0 to 1)
├── Consumer usage 0x00: byte 2 (values 0 to 1)
├── Consumer usage 0x00: byte 3 (values 0 to 1)
├── Consumer usage 0x00: byte 4 (values 0 to 1)
├── Consumer usage 0x00: byte 5 (values 0 to 1)
├── Consumer usage 0x00: byte 6 (values 0 to 1)
├── Consumer usage 0x00: byte 7 (values 0 to 1)
├── Consumer usage 0x00: byte 8 (values 0 to 1)
├── Consumer usage 0x00: byte 9 (values 0 to 1)
├── Consumer usage 0x00: byte 10 (values 0 to 1)
├── Consumer usage 0x00: byte 11 (values 0 to 1)
├── Consumer usage 0x00: byte 12 (values 0 to 1)
├── Consumer usage 0x00: byte 13 (values 0 to 1)
├── Consumer usage 0x00: byte 14 (values 0 to 1)
├── Consumer usage 0x00: byte 15 (values 0 to 1)
└── Consumer usage 0x00: byte 16 (values 0 to 1)

○ Output report #4 (39 bytes):
├── Consumer usage 0x00: byte 1 (values 0 to 1)
├── Consumer usage 0x00: byte 2 (values 0 to 1)
├── Consumer usage 0x00: byte 3 (values 0 to 1)
├── Consumer usage 0x00: byte 4 (values 0 to 1)
├── Consumer usage 0x00: byte 5 (values 0 to 1)
├── Consumer usage 0x00: byte 6 (values 0 to 1)
├── Consumer usage 0x00: byte 7 (values 0 to 1)
├── Consumer usage 0x00: byte 8 (values 0 to 1)
├── Consumer usage 0x00: byte 9 (values 0 to 1)
├── Consumer usage 0x00: byte 10 (values 0 to 1)
├── Consumer usage 0x00: byte 11 (values 0 to 1)
├── Consumer usage 0x00: byte 12 (values 0 to 1)
├── Consumer usage 0x00: byte 13 (values 0 to 1)
├── Consumer usage 0x00: byte 14 (values 0 to 1)
├── Consumer usage 0x00: byte 15 (values 0 to 1)
├── Consumer usage 0x00: byte 16 (values 0 to 1)
├── Consumer usage 0x00: byte 17 (values 0 to 1)
├── Consumer usage 0x00: byte 18 (values 0 to 1)
├── Consumer usage 0x00: byte 19 (values 0 to 1)
├── Consumer usage 0x00: byte 20 (values 0 to 1)
├── Consumer usage 0x00: byte 21 (values 0 to 1)
├── Consumer usage 0x00: byte 22 (values 0 to 1)
├── Consumer usage 0x00: byte 23 (values 0 to 1)
├── Consumer usage 0x00: byte 24 (values 0 to 1)
├── Consumer usage 0x00: byte 25 (values 0 to 1)
├── Consumer usage 0x00: byte 26 (values 0 to 1)
├── Consumer usage 0x00: byte 27 (values 0 to 1)
├── Consumer usage 0x00: byte 28 (values 0 to 1)
├── Consumer usage 0x00: byte 29 (values 0 to 1)
├── Consumer usage 0x00: byte 30 (values 0 to 1)
├── Consumer usage 0x00: byte 31 (values 0 to 1)
├── Consumer usage 0x00: byte 32 (values 0 to 1)
├── Consumer usage 0x00: byte 33 (values 0 to 1)
├── Consumer usage 0x00: byte 34 (values 0 to 1)
├── Consumer usage 0x00: byte 35 (values 0 to 1)
├── Consumer usage 0x00: byte 36 (values 0 to 1)
├── Consumer usage 0x00: byte 37 (values 0 to 1)
└── Consumer usage 0x00: byte 38 (values 0 to 1)
Binary file added tests/hid/headset/descriptor.bin
Binary file not shown.
19 changes: 19 additions & 0 deletions tests/hid/headset/reference.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

○ Input report (4 bytes):
├── Volume Increment: byte 0 bit 0
├── Volume Decrement: byte 0 bit 1
├── Mute: byte 0 bit 2
├── Consumer usage 0x00: byte 0 bit 3
├── Hook Switch: byte 0 bit 4
├── Consumer usage 0x00: byte 0 bit 5
├── Consumer usage 0x00: byte 0 bit 6
├── Consumer usage 0x00: byte 0 bit 7
├── Consumer usage 0x00: byte 1
├── Consumer usage 0x00: byte 2
└── Consumer usage 0x00: byte 3

○ Output report (4 bytes):
├── Consumer usage 0x00: byte 0
├── Consumer usage 0x00: byte 1
├── Consumer usage 0x00: byte 2
└── Consumer usage 0x00: byte 3
Binary file added tests/hid/hub/descriptor.bin
Binary file not shown.
Loading

0 comments on commit 83331c6

Please sign in to comment.