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::{
8        AbandonedReply, ChannelError, HidppChannel, HidppMessage, LONG_REPORT_LENGTH,
9        SEND_RESPONSE_TIMEOUT, SHORT_REPORT_LENGTH,
10    },
11    nibble::{self, U4},
12};
13
14/// Represents the header that every [`HidppMessage`] of HID++2.0 starts with.
15#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize))]
17pub struct MessageHeader {
18    /// The index of the device involved in the communication.
19    pub device_index: u8,
20
21    /// The index of the feature the message belongs to.
22    ///
23    /// This is not the same as the feature ID, but the index returned from a
24    /// feature enumeration request.
25    pub feature_index: u8,
26
27    /// The ID of the function involved in the communication.
28    pub function_id: U4,
29
30    /// The ID of the software communicating with the device.
31    pub software_id: U4,
32}
33
34/// Represents a HID++2.0 message.
35#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize))]
37pub enum Message {
38    /// Represents a short HID++2.0 message with 3 bytes of payload.
39    Short(MessageHeader, [u8; SHORT_REPORT_LENGTH - 4]),
40
41    /// Represents a long HID++2.0 message with 16 bytes of payload.
42    Long(MessageHeader, [u8; LONG_REPORT_LENGTH - 4]),
43}
44
45impl Message {
46    /// Extracts the header of the message.
47    #[must_use]
48    pub fn header(&self) -> MessageHeader {
49        match *self {
50            Message::Short(header, _) | Message::Long(header, _) => header,
51        }
52    }
53
54    /// Extracts the payload of the message and fits it into an array capable of
55    /// containing the longest possible payload, filling the rest up with
56    /// zeroes.
57    #[must_use]
58    pub fn extend_payload(&self) -> [u8; LONG_REPORT_LENGTH - 4] {
59        match *self {
60            Message::Short(_, payload) => {
61                let mut data = [0; LONG_REPORT_LENGTH - 4];
62                data[..SHORT_REPORT_LENGTH - 4].copy_from_slice(&payload);
63                data
64            }
65            Message::Long(_, payload) => payload,
66        }
67    }
68}
69
70impl From<HidppMessage> for Message {
71    fn from(msg: HidppMessage) -> Self {
72        match msg {
73            HidppMessage::Short(payload) => {
74                let [_, _, _, rest @ ..] = payload;
75                Message::Short(
76                    MessageHeader {
77                        device_index: payload[0],
78                        feature_index: payload[1],
79                        function_id: U4::from_hi(payload[2]),
80                        software_id: U4::from_lo(payload[2]),
81                    },
82                    rest,
83                )
84            }
85            HidppMessage::Long(payload) => {
86                let [_, _, _, rest @ ..] = payload;
87                Message::Long(
88                    MessageHeader {
89                        device_index: payload[0],
90                        feature_index: payload[1],
91                        function_id: U4::from_hi(payload[2]),
92                        software_id: U4::from_lo(payload[2]),
93                    },
94                    rest,
95                )
96            }
97        }
98    }
99}
100
101impl From<Message> for HidppMessage {
102    fn from(msg: Message) -> Self {
103        match msg {
104            Message::Short(header, payload) => {
105                let mut data = [0u8; SHORT_REPORT_LENGTH - 1];
106                data[0] = header.device_index;
107                data[1] = header.feature_index;
108                data[2] = nibble::combine(header.function_id, header.software_id);
109                data[3..].copy_from_slice(&payload);
110
111                HidppMessage::Short(data)
112            }
113            Message::Long(header, payload) => {
114                let mut data = [0u8; LONG_REPORT_LENGTH - 1];
115                data[0] = header.device_index;
116                data[1] = header.feature_index;
117                data[2] = nibble::combine(header.function_id, header.software_id);
118                data[3..].copy_from_slice(&payload);
119
120                HidppMessage::Long(data)
121            }
122        }
123    }
124}
125
126impl HidppChannel {
127    /// Sends a HID++2.0 message across the channel and waits for a response
128    /// that matches the message header.
129    ///
130    /// This method simply calls [`Self::send`] with a pre-built response
131    /// predicate comparing the headers of the outgoing and incoming message.
132    pub async fn send_v20(&self, msg: Message) -> Result<Message, Hidpp20Error> {
133        self.send_v20_with(msg, AbandonedReply::Quarantine).await
134    }
135
136    /// [`Self::send_v20`], choosing what to do about a reply still owed to an
137    /// abandoned request with the same header (see [`AbandonedReply`]).
138    pub async fn send_v20_with(
139        &self,
140        msg: Message,
141        abandoned: AbandonedReply,
142    ) -> Result<Message, Hidpp20Error> {
143        let header = msg.header();
144        let response = self
145            .send_with(
146                msg.into(),
147                v20_response_predicate(header),
148                SEND_RESPONSE_TIMEOUT,
149                abandoned,
150            )
151            .await?;
152        decode_v20_response(response)
153    }
154
155    /// Sends a HID++2.0 message without timing out its native write, then waits
156    /// up to [`SEND_RESPONSE_TIMEOUT`] for the matching response.
157    ///
158    /// `matches_payload` further filters successful responses where the feature
159    /// defines echoed fields. Header-matched errors bypass this filter.
160    ///
161    /// The caller **must drive this future to completion**. Requester deadlines
162    /// belong outside the task that owns this future; see
163    /// [`HidppChannel::send_write_through`] for the transport-ordering contract.
164    pub async fn send_v20_write_through(
165        &self,
166        msg: Message,
167        matches_payload: impl Fn(&Message) -> bool + Send + 'static,
168    ) -> Result<Message, Hidpp20Error> {
169        let matches_header = v20_response_predicate(msg.header());
170        let response = self
171            .send_write_through(
172                msg.into(),
173                move |raw| {
174                    let response = Message::from(*raw);
175                    matches_header(raw)
176                        && (response.header().feature_index == 0xff || matches_payload(&response))
177                },
178                SEND_RESPONSE_TIMEOUT,
179            )
180            .await?;
181        decode_v20_response(response)
182    }
183}
184
185fn v20_response_predicate(
186    header: MessageHeader,
187) -> impl Fn(&HidppMessage) -> bool + Send + 'static {
188    move |&response| {
189        let response = Message::from(response);
190        let response_header = response.header();
191
192        // A HID++2.0 error response sets the feature index to 0xFF and moves all header
193        // values starting from the real feature index one byte to the right.
194        let is_error = response_header.device_index == header.device_index
195            && response_header.feature_index == 0xff
196            && nibble::combine(response_header.function_id, response_header.software_id)
197                == header.feature_index
198            && response.extend_payload()[0]
199                == nibble::combine(header.function_id, header.software_id);
200
201        is_error || response_header == header
202    }
203}
204
205fn decode_v20_response(response: HidppMessage) -> Result<Message, Hidpp20Error> {
206    let response = Message::from(response);
207    if response.header().feature_index == 0xff {
208        let err = ErrorType::try_from(response.extend_payload()[1])
209            .map_err(|_| Hidpp20Error::UnsupportedResponse)?;
210
211        return Err(Hidpp20Error::Feature(err));
212    }
213
214    Ok(response)
215}
216
217/// Represents the type of an error a HID++2.0 device returns if a feature
218/// function fails.
219#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
220#[cfg_attr(feature = "serde", derive(serde::Serialize))]
221#[non_exhaustive]
222#[repr(u8)]
223pub enum ErrorType {
224    /// No error.
225    NoError = 0,
226    /// Unknown error.
227    Unknown = 1,
228    /// Invalid argument.
229    InvalidArgument = 2,
230    /// Argument out of range.
231    OutOfRange = 3,
232    /// Hardware error.
233    HwError = 4,
234    /// Logitech-internal firmware error.
235    LogitechInternal = 5,
236    /// Invalid feature index.
237    InvalidFeatureIndex = 6,
238    /// Invalid function ID.
239    InvalidFunctionId = 7,
240    /// Device is busy.
241    Busy = 8,
242    /// Operation is unsupported.
243    Unsupported = 9,
244}
245
246/// Represents an error that may occur when calling a HID++2.0 feature function.
247#[derive(Debug, Error)]
248#[non_exhaustive]
249pub enum Hidpp20Error {
250    /// Indicates that an error occurred while communicating across the HID++
251    /// channel.
252    #[error("the HID++ channel returned an error")]
253    Channel(#[from] ChannelError),
254
255    /// Indicates that a call to a HID++2.0 feature function resulted in an
256    /// error.
257    #[error("a HID++2.0 feature returned an error")]
258    Feature(ErrorType),
259
260    /// Indicates that a received response is not fully supported.
261    #[error("the received response from the device is (partly) unsupported")]
262    UnsupportedResponse,
263}