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}