Skip to main content

hidpp/channel/
message.rs

1//! The two report widths a HID++ channel carries, and the raw message
2//! framing shared by both protocol versions.
3
4/// The ID of the HID report that is used to transmit short HID++ messages.
5pub const SHORT_REPORT_ID: u8 = 0x10;
6
7/// The length of short HID++ message reports (including report ID).
8pub const SHORT_REPORT_LENGTH: usize = 7;
9
10/// The ID of the HID report that is used to transmit long HID++ messages.
11pub const LONG_REPORT_ID: u8 = 0x11;
12
13/// The length of long HID++ message reports (including report ID).
14pub const LONG_REPORT_LENGTH: usize = 20;
15
16/// Whether `report_id` is one of the two HID++ report IDs.
17///
18/// A HID node can carry another protocol on the same interface — a Unifying
19/// receiver on Linux also emits Logitech DJ reports (`0x20`/`0x21`) — so an
20/// incoming report with any other ID is foreign traffic rather than a broken
21/// HID++ report.
22#[must_use]
23pub const fn is_hidpp_report_id(report_id: u8) -> bool {
24    matches!(report_id, SHORT_REPORT_ID | LONG_REPORT_ID)
25}
26
27/// Represents an unversioned HID++ message.
28#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
29pub enum HidppMessage {
30    /// Represents a short HID++ message.
31    ///
32    /// Please check
33    /// [`HidppChannel::supports_short`](super::HidppChannel::supports_short)
34    /// before sending this kind of message.
35    Short([u8; SHORT_REPORT_LENGTH - 1]),
36
37    /// Represents a long HID++ message.
38    ///
39    /// Please check
40    /// [`HidppChannel::supports_long`](super::HidppChannel::supports_long)
41    /// before sending this kind of message.
42    Long([u8; LONG_REPORT_LENGTH - 1]),
43}
44
45impl HidppMessage {
46    /// Tries to read a HID++ message from raw data.
47    #[must_use]
48    pub fn read_raw(data: &[u8]) -> Option<Self> {
49        let (&report_id, rest) = data.split_first()?;
50
51        // The empty-remainder patterns enforce the exact report lengths.
52        if report_id == SHORT_REPORT_ID
53            && let Some((&payload, [])) = rest.split_first_chunk()
54        {
55            Some(HidppMessage::Short(payload))
56        } else if report_id == LONG_REPORT_ID
57            && let Some((&payload, [])) = rest.split_first_chunk()
58        {
59            Some(HidppMessage::Long(payload))
60        } else {
61            None
62        }
63    }
64
65    /// Writes a HID++ message in its raw byte form into a buffer.
66    ///
67    /// Returns the amount of written bytes.
68    pub fn write_raw(&self, buf: &mut [u8]) -> usize {
69        match self {
70            Self::Short(payload) => {
71                buf[0] = SHORT_REPORT_ID;
72                buf[1..SHORT_REPORT_LENGTH].copy_from_slice(payload);
73                SHORT_REPORT_LENGTH
74            }
75            Self::Long(payload) => {
76                buf[0] = LONG_REPORT_ID;
77                buf[1..LONG_REPORT_LENGTH].copy_from_slice(payload);
78                LONG_REPORT_LENGTH
79            }
80        }
81    }
82
83    /// The HID++ addressing header `(device_index, feature_index, function)` —
84    /// the first three payload bytes, present on both report kinds. Used only
85    /// for wire tracing (OpenLogi-specific; not in upstream hidpp).
86    pub(super) fn header(&self) -> (u8, u8, u8) {
87        let payload: &[u8] = match self {
88            Self::Short(payload) => payload,
89            Self::Long(payload) => payload,
90        };
91        (payload[0], payload[1], payload[2])
92    }
93
94    /// Re-frames a short message as a long one, leaving a long one untouched.
95    ///
96    /// The HID++ header bytes (device / feature / function|sw) sit at the same
97    /// offsets in both widths, so the only change is the report id plus
98    /// zero-padding the trailing payload. Used to send on a channel that
99    /// exposes only the long HID++ report — see
100    /// [`HidppChannel::normalize_outgoing`](super::HidppChannel). (OpenLogi
101    /// local addition.)
102    #[must_use]
103    pub(super) fn widened(self) -> Self {
104        match self {
105            Self::Short(payload) => {
106                let mut long = [0u8; LONG_REPORT_LENGTH - 1];
107                long[..payload.len()].copy_from_slice(&payload);
108                Self::Long(long)
109            }
110            long @ Self::Long(_) => long,
111        }
112    }
113}
114
115#[cfg(test)]
116mod tests {
117    use super::{LONG_REPORT_ID, SHORT_REPORT_ID, is_hidpp_report_id};
118
119    #[test]
120    fn only_hidpp_report_ids_are_hidpp() {
121        assert!(is_hidpp_report_id(SHORT_REPORT_ID));
122        assert!(is_hidpp_report_id(LONG_REPORT_ID));
123        // Logitech DJ short/long reports share the node on Linux.
124        assert!(!is_hidpp_report_id(0x20));
125        assert!(!is_hidpp_report_id(0x21));
126    }
127}