hid-descriptor 0.1.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::hid::{KnownCollectionType, ioflags};
//! use hid_types::id::usage::{KnownUsagePage, 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::Keyboard),
//!         collection(KnownCollectionType::Application),
//!         report_size(1),
//!         report_count(8),
//!         usage_page(KnownUsagePage::KeyboardKeypad),
//!         usage_minimum(keyboard_keypad::KnownUsage::LeftControl),
//!         usage_maximum(keyboard_keypad::KnownUsage::RightGui),
//!         logical_minimum(0),
//!         logical_maximum(1),
//!         input(ioflags::DATA_VARIABLE_ABSOLUTE),
//!     ];
//!     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;

use std::path::Path;

use hid_types::encoding::{TagTypeSize, encode_signed, encode_unsigned};
use hid_types::hid::{CollectionType, IoFlags};
use hid_types::id::tag::{GlobalItem, IntoTagType, LocalItem, MainItem};
use hid_types::id::usage::UsagePage;

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

fn short_encode_unsigned<T: IntoTagType>(tag: T, data: &[u8]) -> DescriptorBytes {
    let data = encode_unsigned(data);
    let tag = TagTypeSize::from_tag(tag, data.len());

    [tag.into_bits()]
        .into_iter()
        .chain(data.iter().copied())
        .collect()
}

fn short_encode_signed<T: IntoTagType>(tag: T, data: &[u8]) -> DescriptorBytes {
    let data = encode_signed(data);
    let tag = TagTypeSize::from_tag(tag, data.len());

    [tag.into_bits()]
        .into_iter()
        .chain(data.iter().copied())
        .collect()
}

/// Create a __Usage Page__.
pub fn usage_page(page: impl Into<UsagePage>) -> DescriptorBytes {
    let page: UsagePage = page.into();
    let page_number = page.to_integer();
    let data = page_number.to_le_bytes();
    short_encode_unsigned(GlobalItem::UsagePage, &data)
}

/// 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 value = value.into();
    short_encode_unsigned(LocalItem::Usage, &value.to_le_bytes())
}

/// Create a __Collection__.
pub fn collection(value: impl Into<CollectionType>) -> DescriptorBytes {
    let value = value.into().to_integer();
    short_encode_unsigned(MainItem::Collection, &value.to_le_bytes())
}

/// Create an __End Collection__.
pub fn end_collection() -> DescriptorBytes {
    short_encode_unsigned(MainItem::EndCollection, &[])
}

/// Create a __Logical Minimum__.
pub fn logical_minimum(value: i32) -> DescriptorBytes {
    short_encode_signed(GlobalItem::LogicalMinimum, &value.to_le_bytes())
}

/// Create a __Logical Maximum__.
pub fn logical_maximum(value: i32) -> DescriptorBytes {
    short_encode_signed(GlobalItem::LogicalMaximum, &value.to_le_bytes())
}

/// Create an __Input__ item.
pub fn input(flags: impl Into<IoFlags>) -> DescriptorBytes {
    let flags = flags.into();
    let data: u32 = flags.into();
    short_encode_unsigned(MainItem::Input, &data.to_le_bytes())
}

/// Create an __Output__ item.
pub fn output(flags: impl Into<IoFlags>) -> DescriptorBytes {
    let flags = flags.into();
    let data: u32 = flags.into();
    short_encode_unsigned(MainItem::Output, &data.to_le_bytes())
}

/// Create a __Feature__ item.
pub fn feature(flags: impl Into<IoFlags>) -> DescriptorBytes {
    let flags = flags.into();
    let data: u32 = flags.into();
    short_encode_unsigned(MainItem::Feature, &data.to_le_bytes())
}

/// 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();
    short_encode_unsigned(LocalItem::UsageMinimum, &value.to_le_bytes())
}

/// 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();
    short_encode_unsigned(LocalItem::UsageMaximum, &value.to_le_bytes())
}

/// 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 {
    short_encode_unsigned(GlobalItem::ReportId, &value.to_le_bytes())
}

/// Create a __Report Size__ value.
pub fn report_size(value: u8) -> DescriptorBytes {
    short_encode_unsigned(GlobalItem::ReportSize, &value.to_le_bytes())
}

/// Create a __Report Count__ value.
pub fn report_count(value: u8) -> DescriptorBytes {
    short_encode_unsigned(GlobalItem::ReportCount, &value.to_le_bytes())
}

/// 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::hid::{IoFlags, KnownCollectionType};
    use hid_types::id::usage::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
            ]
        );
    }
}