Skip to main content

hidpp/protocol/
v10.rs

1//! Implements functionality specific to HID++1.0.
2
3use num_enum::{IntoPrimitive, TryFromPrimitive};
4use thiserror::Error;
5
6use crate::channel::{
7    ChannelError, HidppChannel, HidppMessage, LONG_REPORT_LENGTH, SHORT_REPORT_LENGTH,
8};
9
10/// Represents the header that every [`HidppMessage`] of HID++1.0 starts with.
11#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize))]
13pub struct MessageHeader {
14    /// The index of the device involved in the communication.
15    pub device_index: u8,
16
17    /// The sub ID of the message.
18    pub sub_id: u8,
19}
20
21/// Represents a HID++1.0 message.
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize))]
24pub enum Message {
25    /// Represents a short HID++1.0 message with 4 bytes of payload.
26    Short(MessageHeader, [u8; SHORT_REPORT_LENGTH - 3]),
27
28    /// Represents a long HID++1.0 message with 17 bytes of payload.
29    Long(MessageHeader, [u8; LONG_REPORT_LENGTH - 3]),
30}
31
32impl Message {
33    /// Extracts the header of the message.
34    #[must_use]
35    pub fn header(&self) -> MessageHeader {
36        match *self {
37            Message::Short(header, _) | Message::Long(header, _) => header,
38        }
39    }
40
41    /// Extracts the payload of the message and fits it into an array capable of
42    /// containing the longest possible payload, filling the rest up with
43    /// zeroes.
44    #[must_use]
45    pub fn extend_payload(&self) -> [u8; LONG_REPORT_LENGTH - 3] {
46        match *self {
47            Message::Short(_, payload) => {
48                let mut data = [0; LONG_REPORT_LENGTH - 3];
49                data[..SHORT_REPORT_LENGTH - 3].copy_from_slice(&payload);
50                data
51            }
52            Message::Long(_, payload) => payload,
53        }
54    }
55}
56
57impl From<HidppMessage> for Message {
58    fn from(msg: HidppMessage) -> Self {
59        match msg {
60            HidppMessage::Short(payload) => {
61                let [_, _, rest @ ..] = payload;
62                Message::Short(
63                    MessageHeader {
64                        device_index: payload[0],
65                        sub_id: payload[1],
66                    },
67                    rest,
68                )
69            }
70            HidppMessage::Long(payload) => {
71                let [_, _, rest @ ..] = payload;
72                Message::Long(
73                    MessageHeader {
74                        device_index: payload[0],
75                        sub_id: payload[1],
76                    },
77                    rest,
78                )
79            }
80        }
81    }
82}
83
84impl From<Message> for HidppMessage {
85    fn from(msg: Message) -> Self {
86        match msg {
87            Message::Short(header, payload) => {
88                let mut data = [0u8; SHORT_REPORT_LENGTH - 1];
89                data[0] = header.device_index;
90                data[1] = header.sub_id;
91                data[2..].copy_from_slice(&payload);
92
93                HidppMessage::Short(data)
94            }
95            Message::Long(header, payload) => {
96                let mut data = [0u8; LONG_REPORT_LENGTH - 1];
97                data[0] = header.device_index;
98                data[1] = header.sub_id;
99                data[2..].copy_from_slice(&payload);
100
101                HidppMessage::Long(data)
102            }
103        }
104    }
105}
106
107/// Whether `msg` answers the RAP request `(device, msg_type, address)`.
108///
109/// `echo` narrows the match to replies whose first data byte repeats that
110/// value. Sub-register reads need it: every slot of the receiver's `0xB5`
111/// register is asked through the same `(device, sub id, address)` header and
112/// differs only in the sub-register byte, which the receiver repeats as the
113/// first byte of its reply. Without the check, one slot's read is satisfied by
114/// any other slot's reply — including one requested by another process sharing
115/// the node, which is how a phantom "slot 3" appeared in inventories whenever
116/// two OpenLogi processes probed a Bolt receiver at once. Error replies carry
117/// no data byte and are matched on the header alone.
118fn is_rap_response(
119    device: u8,
120    msg_type: MessageType,
121    address: u8,
122    echo: Option<u8>,
123    msg: &HidppMessage,
124) -> bool {
125    let raw: [u8; 4] = match msg {
126        HidppMessage::Short(d) => [d[0], d[1], d[2], d[3]],
127        HidppMessage::Long(d) => [d[0], d[1], d[2], d[3]],
128    };
129
130    raw[0] == device
131        && ((raw[1] == msg_type.into()
132            && raw[2] == address
133            && echo.is_none_or(|echo| raw[3] == echo))
134            || (raw[1] == MessageType::Error.into()
135                && raw[2] == msg_type.into()
136                && raw[3] == address))
137}
138
139impl HidppChannel {
140    /// Reads the data from a short 3-byte register using HID++1.0/RAP.
141    pub async fn read_register(
142        &self,
143        device: u8,
144        address: u8,
145        parameters: [u8; 3],
146    ) -> Result<[u8; 3], Hidpp10Error> {
147        let mut data = [address, 0x00, 0x00, 0x00];
148        data[1..].copy_from_slice(&parameters);
149
150        let response = Message::from(
151            self.send(
152                Message::Short(
153                    MessageHeader {
154                        device_index: device,
155                        sub_id: MessageType::GetRegister.into(),
156                    },
157                    data,
158                )
159                .into(),
160                move |raw| is_rap_response(device, MessageType::GetRegister, address, None, raw),
161            )
162            .await?,
163        );
164
165        let payload = response.extend_payload();
166
167        if response.header().sub_id == MessageType::Error.into() {
168            let err =
169                ErrorType::try_from(payload[2]).map_err(|_| Hidpp10Error::UnsupportedResponse)?;
170
171            return Err(Hidpp10Error::RegisterAccess(err));
172        }
173
174        let [_, p1, p2, p3, ..] = payload;
175        Ok([p1, p2, p3])
176    }
177
178    /// Writes data to a short 3-byte register using HID++1.0/RAP.
179    pub async fn write_register(
180        &self,
181        device: u8,
182        address: u8,
183        payload: [u8; 3],
184    ) -> Result<(), Hidpp10Error> {
185        let mut data = [address, 0x00, 0x00, 0x00];
186        data[1..].copy_from_slice(&payload);
187
188        let response = Message::from(
189            self.send(
190                Message::Short(
191                    MessageHeader {
192                        device_index: device,
193                        sub_id: MessageType::SetRegister.into(),
194                    },
195                    data,
196                )
197                .into(),
198                move |raw| is_rap_response(device, MessageType::SetRegister, address, None, raw),
199            )
200            .await?,
201        );
202
203        if response.header().sub_id == MessageType::Error.into() {
204            let err = ErrorType::try_from(response.extend_payload()[2])
205                .map_err(|_| Hidpp10Error::UnsupportedResponse)?;
206
207            return Err(Hidpp10Error::RegisterAccess(err));
208        }
209
210        Ok(())
211    }
212
213    /// Reads the data from a long 16-byte register using HID++1.0/RAP.
214    pub async fn read_long_register(
215        &self,
216        device: u8,
217        address: u8,
218        parameters: [u8; 3],
219    ) -> Result<[u8; 16], Hidpp10Error> {
220        self.read_long_register_matching(device, address, parameters, None)
221            .await
222    }
223
224    /// Reads one sub-register of a long register using HID++1.0/RAP.
225    ///
226    /// `sub_register` goes out as the first parameter, and only a reply that
227    /// repeats it as its first data byte is accepted. The receiver's
228    /// pairing-information register (`0xB5`) keys every paired slot through
229    /// one address, so the header alone cannot tell the slots' replies apart
230    /// (the matcher is `is_rap_response`). The returned data starts with the echoed byte,
231    /// the same layout [`Self::read_long_register`] returns.
232    pub async fn read_long_sub_register(
233        &self,
234        device: u8,
235        address: u8,
236        sub_register: u8,
237        parameters: [u8; 2],
238    ) -> Result<[u8; 16], Hidpp10Error> {
239        self.read_long_register_matching(
240            device,
241            address,
242            [sub_register, parameters[0], parameters[1]],
243            Some(sub_register),
244        )
245        .await
246    }
247
248    async fn read_long_register_matching(
249        &self,
250        device: u8,
251        address: u8,
252        parameters: [u8; 3],
253        echo: Option<u8>,
254    ) -> Result<[u8; 16], Hidpp10Error> {
255        let mut data = [address, 0x00, 0x00, 0x00];
256        data[1..].copy_from_slice(&parameters);
257
258        let response = Message::from(
259            self.send(
260                Message::Short(
261                    MessageHeader {
262                        device_index: device,
263                        sub_id: MessageType::GetLongRegister.into(),
264                    },
265                    data,
266                )
267                .into(),
268                move |raw| {
269                    is_rap_response(device, MessageType::GetLongRegister, address, echo, raw)
270                },
271            )
272            .await?,
273        );
274
275        let payload = response.extend_payload();
276
277        if response.header().sub_id == MessageType::Error.into() {
278            let err =
279                ErrorType::try_from(payload[2]).map_err(|_| Hidpp10Error::UnsupportedResponse)?;
280
281            return Err(Hidpp10Error::RegisterAccess(err));
282        }
283
284        let [_, rest @ ..] = payload;
285        Ok(rest)
286    }
287
288    /// Writes data to a long 16-byte register using HID++1.0/RAP.
289    pub async fn write_long_register(
290        &self,
291        device: u8,
292        address: u8,
293        payload: [u8; 16],
294    ) -> Result<(), Hidpp10Error> {
295        let mut data = [0u8; 17];
296        data[0] = address;
297        data[1..].copy_from_slice(&payload);
298
299        let response = Message::from(
300            self.send(
301                Message::Long(
302                    MessageHeader {
303                        device_index: device,
304                        sub_id: MessageType::SetLongRegister.into(),
305                    },
306                    data,
307                )
308                .into(),
309                move |raw| {
310                    is_rap_response(device, MessageType::SetLongRegister, address, None, raw)
311                },
312            )
313            .await?,
314        );
315
316        if response.header().sub_id == MessageType::Error.into() {
317            let err = ErrorType::try_from(response.extend_payload()[2])
318                .map_err(|_| Hidpp10Error::UnsupportedResponse)?;
319
320            return Err(Hidpp10Error::RegisterAccess(err));
321        }
322
323        Ok(())
324    }
325}
326
327/// Represents a globally defined sub ID of a HID++1.0 message.
328///
329/// This enum only includes sub IDs that are defined globally across all
330/// devices. Most devices (e.g. the Unifying Receiver) define additional sub IDs
331/// specific to their functionality.
332#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
333#[cfg_attr(feature = "serde", derive(serde::Serialize))]
334#[non_exhaustive]
335#[repr(u8)]
336pub enum MessageType {
337    /// Used to set a 3-byte register value. A sent message of this type is
338    /// usually responded with a response message of the same type (or
339    /// [`Self::Error`]).
340    SetRegister = 0x80,
341
342    /// Used to retrieve a 3-byte register value. A sent message of this type is
343    /// usually responded with a response message of the same type (or
344    /// [`Self::Error`]).
345    GetRegister = 0x81,
346
347    /// Used to set a 16-byte register value. A sent message of this type is
348    /// usually responded with a response message of the same type (or
349    /// [`Self::Error`]).
350    SetLongRegister = 0x82,
351
352    /// Used to retrieve a 16-byte register value. A sent message of this type
353    /// is usually responded with a response message of the same type (or
354    /// [`Self::Error`]).
355    GetLongRegister = 0x83,
356
357    /// Used to indicate an error response. The error code usually included in
358    /// the message can be mapped using [`ErrorType::try_from`].
359    Error = 0x8f,
360}
361
362/// Represents the type of an error a HID++1.0 device returns as part of a
363/// message with the [`MessageType::Error`] type.
364#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, IntoPrimitive, TryFromPrimitive)]
365#[cfg_attr(feature = "serde", derive(serde::Serialize))]
366#[non_exhaustive]
367#[repr(u8)]
368pub enum ErrorType {
369    /// No error.
370    Success = 0x00,
371
372    /// The sub ID of a sent message is invalid.
373    InvalidSubId = 0x01,
374
375    /// The address included in a sent message is invalid.
376    InvalidAddress = 0x02,
377
378    /// The value included in a sent message is invalid.
379    InvalidValue = 0x03,
380
381    /// A connection request failed on the receiver's side.
382    ConnectFail = 0x04,
383
384    /// The receiver indicates that too many devices are connected to it.
385    TooManyDevices = 0x05,
386
387    /// The receiver indicates that something already exists. This error is not
388    /// further documented, please let me know what it means.
389    AlreadyExists = 0x06,
390
391    /// The receiver is currently handling a downstream (to device) message and
392    /// cannot process a second one.
393    Busy = 0x07,
394
395    /// Trying to send a message to a device (device index) where there is no
396    /// device paired.
397    UnknownDevice = 0x08,
398
399    /// This error is returned by the receiver when a HID++ command has been
400    /// sent to a device that is in disconnected mode. When a device is in
401    /// disconnected mode it cannot receive commands from the host until it
402    /// reconnects. A device reconnects when the user interacts with it. In most
403    /// cases, a device disconnects after several minutes of inactivity.
404    ResourceError = 0x09,
405
406    /// A sent request is not available in the current context.
407    RequestUnavailable = 0x0a,
408
409    /// A request parameter has an unsupported value.
410    InvalidParamValue = 0x0b,
411
412    /// The PIN code of a device was wrong.
413    WrongPinCode = 0x0c,
414}
415
416/// Represents an error that may occur when accessing registers using HID++1.0.
417#[derive(Debug, Error)]
418#[non_exhaustive]
419pub enum Hidpp10Error {
420    /// Indicates that an error occurred while communicating across the HID++
421    /// channel.
422    #[error("the HID++ channel returned an error")]
423    Channel(#[from] ChannelError),
424
425    /// Indicates that a register access failed.
426    #[error("a HID++1.0 register access resulted in an error")]
427    RegisterAccess(ErrorType),
428
429    /// Indicates that a received response is not fully supported.
430    #[error("the received response from the device is (partly) unsupported")]
431    UnsupportedResponse,
432}
433
434#[cfg(test)]
435mod tests {
436    use std::sync::Arc;
437
438    use futures::join;
439
440    use super::MessageType;
441    use crate::channel::{
442        HidppMessage,
443        mock::{MockRawHidChannel, channel_with_reader},
444    };
445
446    /// A long RAP reply from the receiver for register `address` whose first
447    /// data byte is `first` — the sub-register echo for `0xB5` reads.
448    fn long_register_reply(address: u8, first: u8) -> HidppMessage {
449        let mut data = [0u8; 19];
450        data[0] = 0xff;
451        data[1] = MessageType::GetLongRegister.into();
452        data[2] = address;
453        data[3] = first;
454        data[4] = 0xaa;
455        HidppMessage::Long(data)
456    }
457
458    #[test]
459    fn sub_register_read_ignores_another_sub_registers_reply() {
460        futures::executor::block_on(async {
461            let (raw, handle) = MockRawHidChannel::new();
462            let channel = Arc::new(channel_with_reader(raw).await);
463            // Arrives on the request's write: slot 3's pairing information,
464            // as a second reader of the same node produces while this read is
465            // pending. Matching on the header alone would accept it.
466            handle.queue_response(long_register_reply(0xb5, 0x53));
467
468            let read = channel.read_long_sub_register(0xff, 0xb5, 0x52, [0, 0]);
469            let inject = handle.send_incoming(long_register_reply(0xb5, 0x52));
470            let (result, ()) = join!(read, inject);
471
472            let data = result.expect("the reply echoing the sub-register answers the read");
473            assert_eq!(data[0], 0x52, "slot 2's read must not take slot 3's reply");
474        });
475    }
476
477    #[test]
478    fn plain_register_read_matches_on_the_header_alone() {
479        futures::executor::block_on(async {
480            let (raw, handle) = MockRawHidChannel::new();
481            let channel = Arc::new(channel_with_reader(raw).await);
482            // Registers without a sub-register carry data in the first byte, so
483            // the plain read must keep accepting whatever that byte holds.
484            handle.queue_response(long_register_reply(0x02, 0x00));
485
486            let data = channel
487                .read_long_register(0xff, 0x02, [0, 0, 0])
488                .await
489                .expect("a header match answers a plain register read");
490            assert_eq!(data[0], 0x00);
491        });
492    }
493}