Skip to main content

hidpp/protocol/
v20.rs

1//! Implements functionality specific to HID++2.0.
2
3use num_enum::{IntoPrimitive, TryFromPrimitive};
4use thiserror::Error;
5
6use crate::{
7    channel::{ChannelError, HidppChannel, HidppMessage, LONG_REPORT_LENGTH, SHORT_REPORT_LENGTH},
8    nibble::{self, U4},
9};
10
11/// Represents the header that every [`HidppMessage`] of HID++2.0 starts with.
12#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize))]
14pub struct MessageHeader {
15    /// The index of the device involved in the communication.
16    pub device_index: u8,
17
18    /// The index of the feature the message belongs to.
19    ///
20    /// This is not the same as the feature ID, but the index returned from a
21    /// feature enumeration request.
22    pub feature_index: u8,
23
24    /// The ID of the function involved in the communication.
25    pub function_id: U4,
26
27    /// The ID of the software communicating with the device.
28    pub software_id: U4,
29}
30
31/// Represents a HID++2.0 message.
32#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
33#[cfg_attr(feature = "serde", derive(serde::Serialize))]
34pub enum Message {
35    /// Represents a short HID++2.0 message with 3 bytes of payload.
36    Short(MessageHeader, [u8; SHORT_REPORT_LENGTH - 4]),
37
38    /// Represents a long HID++2.0 message with 16 bytes of payload.
39    Long(MessageHeader, [u8; LONG_REPORT_LENGTH - 4]),
40}
41
42impl Message {
43    /// Extracts the header of the message.
44    #[must_use]
45    pub fn header(&self) -> MessageHeader {
46        match *self {
47            Message::Short(header, _) | Message::Long(header, _) => header,
48        }
49    }
50
51    /// Extracts the payload of the message and fits it into an array capable of
52    /// containing the longest possible payload, filling the rest up with
53    /// zeroes.
54    #[must_use]
55    pub fn extend_payload(&self) -> [u8; LONG_REPORT_LENGTH - 4] {
56        match *self {
57            Message::Short(_, payload) => {
58                let mut data = [0; LONG_REPORT_LENGTH - 4];
59                data[..SHORT_REPORT_LENGTH - 4].copy_from_slice(&payload);
60                data
61            }
62            Message::Long(_, payload) => payload,
63        }
64    }
65}
66
67impl From<HidppMessage> for Message {
68    fn from(msg: HidppMessage) -> Self {
69        match msg {
70            HidppMessage::Short(payload) => {
71                let [_, _, _, rest @ ..] = payload;
72                Message::Short(
73                    MessageHeader {
74                        device_index: payload[0],
75                        feature_index: payload[1],
76                        function_id: U4::from_hi(payload[2]),
77                        software_id: U4::from_lo(payload[2]),
78                    },
79                    rest,
80                )
81            }
82            HidppMessage::Long(payload) => {
83                let [_, _, _, rest @ ..] = payload;
84                Message::Long(
85                    MessageHeader {
86                        device_index: payload[0],
87                        feature_index: payload[1],
88                        function_id: U4::from_hi(payload[2]),
89                        software_id: U4::from_lo(payload[2]),
90                    },
91                    rest,
92                )
93            }
94        }
95    }
96}
97
98impl From<Message> for HidppMessage {
99    fn from(msg: Message) -> Self {
100        match msg {
101            Message::Short(header, payload) => {
102                let mut data = [0u8; SHORT_REPORT_LENGTH - 1];
103                data[0] = header.device_index;
104                data[1] = header.feature_index;
105                data[2] = nibble::combine(header.function_id, header.software_id);
106                data[3..].copy_from_slice(&payload);
107
108                HidppMessage::Short(data)
109            }
110            Message::Long(header, payload) => {
111                let mut data = [0u8; LONG_REPORT_LENGTH - 1];
112                data[0] = header.device_index;
113                data[1] = header.feature_index;
114                data[2] = nibble::combine(header.function_id, header.software_id);
115                data[3..].copy_from_slice(&payload);
116
117                HidppMessage::Long(data)
118            }
119        }
120    }
121}
122
123impl HidppChannel {
124    /// Sends a HID++2.0 message across the channel and waits for a response
125    /// that matches the message header.
126    ///
127    /// This method simply calls [`Self::send`] with a pre-built response
128    /// predicate comparing the headers of the outgoing and incoming message.
129    pub async fn send_v20(&self, msg: Message) -> Result<Message, Hidpp20Error> {
130        let header = msg.header();
131
132        let response = Message::from(
133            self.send(msg.into(), move |&response| {
134                let resp_msg = Message::from(response);
135                let resp_header = resp_msg.header();
136
137                // A HID++2.0 error response sets the feature index to 0xFF and moves all header
138                // values starting from the real feature index one byte to the right.
139                let is_error = resp_header.device_index == header.device_index
140                    && resp_header.feature_index == 0xff
141                    && nibble::combine(resp_header.function_id, resp_header.software_id)
142                        == header.feature_index
143                    && resp_msg.extend_payload()[0]
144                        == nibble::combine(header.function_id, header.software_id);
145
146                is_error || resp_header == header
147            })
148            .await?,
149        );
150
151        if response.header().feature_index == 0xff {
152            let err = ErrorType::try_from(response.extend_payload()[1])
153                .map_err(|_| Hidpp20Error::UnsupportedResponse)?;
154
155            return Err(Hidpp20Error::Feature(err));
156        }
157
158        Ok(response)
159    }
160}
161
162/// Represents the type of an error a HID++2.0 device returns if a feature
163/// function fails.
164#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize))]
166#[non_exhaustive]
167#[repr(u8)]
168pub enum ErrorType {
169    /// No error.
170    NoError = 0,
171    /// Unknown error.
172    Unknown = 1,
173    /// Invalid argument.
174    InvalidArgument = 2,
175    /// Argument out of range.
176    OutOfRange = 3,
177    /// Hardware error.
178    HwError = 4,
179    /// Logitech-internal firmware error.
180    LogitechInternal = 5,
181    /// Invalid feature index.
182    InvalidFeatureIndex = 6,
183    /// Invalid function ID.
184    InvalidFunctionId = 7,
185    /// Device is busy.
186    Busy = 8,
187    /// Operation is unsupported.
188    Unsupported = 9,
189}
190
191/// Represents an error that may occur when calling a HID++2.0 feature function.
192#[derive(Debug, Error)]
193#[non_exhaustive]
194pub enum Hidpp20Error {
195    /// Indicates that an error occurred while communicating across the HID++
196    /// channel.
197    #[error("the HID++ channel returned an error")]
198    Channel(#[from] ChannelError),
199
200    /// Indicates that a call to a HID++2.0 feature function resulted in an
201    /// error.
202    #[error("a HID++2.0 feature returned an error")]
203    Feature(ErrorType),
204
205    /// Indicates that a received response is not fully supported.
206    #[error("the received response from the device is (partly) unsupported")]
207    UnsupportedResponse,
208}