hid-descriptor 0.2.0

Tools for building USB HID report descriptors
Documentation
//! # Tools for building USB HID report descriptors.
//!
//! This is a utility for generating a binary USB HID report descriptor from Rust code.
//! It can be used from `build.rs` so that the descriptor is generated at compile time,
//! and the descriptor bytes can be included in the code, with no runtime cost.
//!
//! Alternatively, you can edit one of the example binaries, and copy the resulting
//! descriptor file into your project.
//!
//! See [Device Class Definition for Human Interface Devices](https://www.usb.org/document-library/device-class-definition-hid-111)
//! for a detailed description of HID report descriptors.
//!
//! # Example
//! ```no_run
//! // `build.rs`
//! # use std::path::PathBuf;
//! use hid_descriptor::*;
//! use hid_types::id::{IoFlags, KnownCollectionType, KnownUsagePage};
//! use hid_types::id::usage::{generic_desktop, keyboard_keypad};
//!
//! fn main() {
//!     println!("cargo:rerun-if-changed=build.rs");
//!     let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
//!     let descriptor_path = out_dir.join("descriptor.bin");
//!     println!("cargo:rerun-if-changed={}", descriptor_path.display());
//!
//!     let bytes = [
//!         usage_page(KnownUsagePage::GenericDesktop),
//!         usage(generic_desktop::KnownUsage::Gamepad),
//!         collection(KnownCollectionType::Application),
//!         usage_page(KnownUsagePage::Button),
//!         usage_minimum(1u16),
//!         usage_maximum(8u16),
//!         logical_minimum(0),
//!         logical_maximum(1),
//!         report_size(1),
//!         report_count(8),
//!         input(IoFlags::data().variable()),
//!         end_collection(),
//!     ];
//!     write_to_file(bytes, descriptor_path).unwrap();
//! }
//! ```
//!
//! ```ignore
//! //! In your crate, compile in the descriptor bytes.
//! static GAMEPAD_DESCRIPTOR: &[u8] =
//!     include_bytes!(concat!(env!("OUT_DIR"), "/descriptor.bin"));
//! ```

#![warn(clippy::print_stderr, clippy::print_stdout, clippy::dbg_macro)]
#![warn(clippy::todo)]
#![warn(missing_docs)]

// Re-export everything from the `hid_types` crate, so that callers
// only need to track one direct dependency.
pub use hid_types;

// Re-export the EncodeItem trait because that module isn't public.
pub use encode::EncodeItem;

use hid_types::id::{CollectionType, InputFlags, IoFlags, Unit, UnitExponent, UsagePage};
use hid_types::item::usage::{ExtendedUsage, Usage};
use hid_types::item::{Global, Item, Local, Main};
use tinyvec::TinyVec;

use std::path::Path;

mod encode;

/// A variable-length short chunk of descriptor bytes.
pub type DescriptorBytes = TinyVec<[u8; 4]>;

/// Encode a sequence of items into a binary HID report descriptor.
pub fn encode_items(items: &[Item]) -> Vec<u8> {
    items.iter().flat_map(|item| item.encode()).collect()
}

/// Create a __Usage Page__.
pub fn usage_page(usage_page: impl Into<UsagePage>) -> DescriptorBytes {
    let usage_page: UsagePage = usage_page.into();
    Global::UsagePage(usage_page).encode()
}

/// Create a __Collection__.
pub fn collection(value: impl Into<CollectionType>) -> DescriptorBytes {
    Main::Collection(value.into()).encode()
}

/// Create an __End Collection__.
pub fn end_collection() -> DescriptorBytes {
    Main::EndCollection.encode()
}

/// Create a __Logical Minimum__.
pub fn logical_minimum(value: i32) -> DescriptorBytes {
    Global::LogicalMinimum(value).encode()
}

/// Create a __Logical Maximum__.
pub fn logical_maximum(value: i32) -> DescriptorBytes {
    Global::LogicalMaximum(value).encode()
}

/// Create a __Physical Minimum__.
pub fn physical_minimum(value: i32) -> DescriptorBytes {
    Global::PhysicalMinimum(value).encode()
}

/// Create a __Physical Maximum__.
pub fn physical_maximum(value: i32) -> DescriptorBytes {
    Global::PhysicalMaximum(value).encode()
}

/// Create a __Unit__.
pub fn unit(unit: impl Into<Unit>) -> DescriptorBytes {
    let unit: Unit = unit.into();
    Global::Unit(unit).encode()
}

/// Create a __Unit Exponent__.
pub fn unit_exponent(unit_exponent: impl Into<UnitExponent>) -> DescriptorBytes {
    let unit: UnitExponent = unit_exponent.into();
    Global::UnitExponent(unit).encode()
}

/// Create a __Report ID__.
///
/// See [Device Class Definition for Human Interface Devices (HID)](https://www.usb.org/document-library/device-class-definition-hid-111),
/// sections 5.6 and 6.2.2.7.
///
/// > Report ID items are used to indicate which data fields are represented in each report
/// > structure. A Report ID item tag assigns a 1-byte identification prefix to each
/// > report transfer. If no Report ID item tags are present in the Report descriptor, it
/// > can be assumed that only one Input, Output, and Feature report structure exists
/// > and together they represent all of the device’s data.
pub fn report_id(value: u8) -> DescriptorBytes {
    Global::ReportId(value).encode()
}

/// Create a __Report Size__ value.
pub fn report_size(value: u32) -> DescriptorBytes {
    Global::ReportSize(value).encode()
}

/// Create a __Report Count__ value.
pub fn report_count(value: u32) -> DescriptorBytes {
    Global::ReportCount(value).encode()
}

/// Create a __Push__.
pub fn push() -> DescriptorBytes {
    Global::Push.encode()
}

/// Create a __Pop__.
pub fn pop() -> DescriptorBytes {
    Global::Pop.encode()
}

/// Create an __Input__ item.
pub fn input(flags: impl Into<IoFlags>) -> DescriptorBytes {
    let flags = flags.into();
    Main::Input(InputFlags::from_unchecked(flags)).encode()
}

/// Create an __Output__ item.
pub fn output(flags: impl Into<IoFlags>) -> DescriptorBytes {
    let flags = flags.into();
    Main::Output(flags.into()).encode()
}

/// Create a __Feature__ item.
pub fn feature(flags: impl Into<IoFlags>) -> DescriptorBytes {
    let flags = flags.into();
    Main::Feature(flags.into()).encode()
}

/// Create a __Usage__ value.
///
/// Note: this does not implement the "extended usage" format which also contains a usage page.
pub fn usage(value: impl Into<u16>) -> DescriptorBytes {
    let usage = Usage::without_page(value.into());
    Local::Usage(usage).encode()
}

/// Create an extended __Usage__ value with a __Usage Page__ attached.
pub fn extended_usage(usage_page: impl Into<UsagePage>, usage: impl Into<u16>) -> DescriptorBytes {
    let extended_usage: ExtendedUsage = ExtendedUsage::new(usage_page.into(), usage.into());
    Local::ExtendedUsage(extended_usage).encode()
}

/// Create a __Usage Minimum__ value.
///
/// Note: this does not implement the "extended usage" format which also contains a usage page.
pub fn usage_minimum(value: impl Into<u16>) -> DescriptorBytes {
    let value = value.into();
    Local::UsageMinimum(value.into()).encode()
}

/// Create a __Usage Maximum__ value.
///
/// Note: this does not implement the "extended usage" format which also contains a usage page.
pub fn usage_maximum(value: impl Into<u16>) -> DescriptorBytes {
    let value = value.into();
    Local::UsageMaximum(value.into()).encode()
}

/// Create a __Designator Index__.
pub fn designator_index(value: u32) -> DescriptorBytes {
    Local::DesignatorIndex(value).encode()
}

/// Create a __Designator Minimum__.
pub fn designator_minimum(value: u32) -> DescriptorBytes {
    Local::DesignatorMinimum(value).encode()
}

/// Create a __Designator Maximum__.
pub fn designator_maximum(value: u32) -> DescriptorBytes {
    Local::DesignatorMaximum(value).encode()
}

/// Create a __String Index__.
pub fn string_index(value: u32) -> DescriptorBytes {
    Local::StringIndex(value).encode()
}

/// Create a __String Minimum__.
pub fn string_minimum(value: u32) -> DescriptorBytes {
    Local::StringMinimum(value).encode()
}

/// Create a __String Maximum__.
pub fn string_maximum(value: u32) -> DescriptorBytes {
    Local::StringMaximum(value).encode()
}

/// Create a __Delimiter__.
pub fn string_(value: bool) -> DescriptorBytes {
    Local::Delimiter(value).encode()
}

/// Concatenate a bunch of descriptor bytes into one iterator.
///
/// This is just a more descriptive name for `.into_iter().flatten()`.
pub fn concatenate_bytes(
    items: impl IntoIterator<Item = impl IntoIterator<Item = u8>>,
) -> impl Iterator<Item = u8> {
    items.into_iter().flatten()
}

/// Write a binary encoded descriptor to a file.
///
/// This function just concatenates a list of byte vectors into a `Vec<u8>` and then writes them.
/// It's easy to write your own version of this function-- just peek at the source code.
pub fn write_to_file(
    items: impl IntoIterator<Item = impl IntoIterator<Item = u8>>,
    path: impl AsRef<Path>,
) -> std::io::Result<()> {
    let descriptor: Vec<u8> = items.into_iter().flatten().collect();
    std::fs::write(path, descriptor)
}

#[cfg(test)]
mod tests {
    use hid_types::id::{KnownCollectionType, KnownUsagePage};

    use super::*;

    #[test]
    fn test_usage_page() {
        let bytes = usage_page(KnownUsagePage::GenericDesktop);
        assert_eq!(bytes.as_slice(), &[0x05, 0x01]);

        let bytes = usage_page(UsagePage::Reserved(0xFF00));
        assert_eq!(bytes.as_slice(), &[0x06, 0x00, 0xFF]);

        // Usage Page values are defined to be unsigned, so it's safe to shorten the data to one byte.
        let bytes = usage_page(UsagePage::Reserved(0x00FF));
        assert_eq!(bytes.as_slice(), &[0x05, 0xFF]);
    }

    #[test]
    fn concatenate() {
        use hid_types::id::usage::generic_desktop::KnownUsage as Usage;

        let bytes = [
            usage_page(KnownUsagePage::GenericDesktop),
            usage(Usage::Joystick),
            collection(KnownCollectionType::Application),
            usage(Usage::Pointer),
            collection(KnownCollectionType::Physical),
            usage(Usage::X),
            usage(Usage::Y),
            logical_minimum(-1),
            logical_maximum(255),
            report_size(8),
            report_count(2),
            input(IoFlags::data().variable()),
            end_collection(),
            usage_page(KnownUsagePage::Button),
            usage_minimum(1u16),
            usage_maximum(8u16),
            logical_minimum(0),
            logical_maximum(1),
            report_size(1),
            report_count(8),
            input(IoFlags::data().variable()),
            end_collection(),
        ];
        let desc: Vec<u8> = concatenate_bytes(bytes).collect();
        assert_eq!(
            desc,
            &[
                0x05, 0x01, // usage page
                0x09, 0x04, // usage
                0xA1, 0x01, // collection(application)
                0x09, 0x01, // usage
                0xa1, 0x00, // collection(physical)
                0x09, 0x30, // X
                0x09, 0x31, // Y
                0x15, 0xFF, // logical minimum
                0x26, 0xFF, 0x00, // logical maximum
                0x75, 0x08, // report size
                0x95, 0x02, // report count
                0x81, 0x02, // input
                0xc0, // end collection
                0x05, 0x09, // usage page (button)
                0x19, 0x01, // usage minimum
                0x29, 0x08, // usage maximum
                0x15, 0x00, // logical minimum
                0x25, 0x01, // logical maximum
                0x75, 0x01, // report size
                0x95, 0x08, // report count
                0x81, 0x02, // input
                0xc0, // end collection
            ]
        );
    }
}