Skip to main content

hidpp/protocol/
mod.rs

1//! Implements the protocol-specific parts of HID++.
2
3use std::fmt::Debug;
4
5use crate::{
6    channel::{ChannelError, HidppChannel},
7    nibble::{self, U4},
8};
9
10pub mod v10;
11pub mod v20;
12
13/// Represents the protocol version a device supports.
14#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16pub enum ProtocolVersion {
17    /// The older HID++1.0 protocol. Mostly used for receivers.
18    V10,
19
20    /// All newer protocols starting from HID+2.0.
21    ///
22    /// Traditionally, the version was split into a major and a minor version,
23    /// defining the concrete protocol version. These two values were later
24    /// redefined to serve the purpose of indicating which host software to
25    /// target.
26    V20 {
27        /// The protocol number is a field that hints the host software if it
28        /// should support the device.
29        ///
30        /// `protocol_num = 2` : Intended target SW is Logitech SetPoint\
31        /// `protocol_num = 3` : Intended OEM SW described in `target_sw` field\
32        /// `protocol_num = 4` : Intended target SW described in `target_sw`
33        /// field
34        protocol_num: u8,
35
36        /// When `protocol_num >= 3` this field further hints at which software
37        /// should support the device. Otherwise the value is zero.
38        ///
39        /// See <https://drive.google.com/file/d/1ULmw9uJL8b8iwwUo5xjSS9F5Zvno-86y/view>
40        /// for more information.
41        target_sw: u8,
42    },
43}
44
45/// Tries to determine the protocol version of a specific device.
46///
47/// Returns `Ok(None)` if no device was found for the given device index.
48pub async fn determine_version(
49    chan: &HidppChannel,
50    device_index: u8,
51) -> Result<Option<ProtocolVersion>, ChannelError> {
52    // To determine the protocol version, we send a HID++2.0 ping message
53    // feature with index 0x00, function 0x01).
54    // Devices supporting protocol >=2.0 will respond with a defined response
55    // including the particular protocol version.
56    // Devices only supporting protocol 1.0 will respond with an error message
57    // indicating 0x00 is no valid sub ID. We make use of this to pin them to
58    // version 1.0.
59
60    let sw_id = chan.get_sw_id();
61    let msg = v20::Message::Short(
62        v20::MessageHeader {
63            device_index,
64            feature_index: 0x00,
65            function_id: U4::from_lo(0x1),
66            software_id: sw_id,
67        },
68        [0x00, 0x00, 0x00],
69    );
70
71    let response = chan
72        .send(msg.into(), move |resp| {
73            // If we receive a valid HID++2.0 response, we'll use that.
74            if v20::Message::from(*resp).header() == msg.header() {
75                return true;
76            }
77
78            // We only care about HID++1.0 error messages, which are always short according
79            // to the spec.
80            if let v10::Message::Short(header, payload) = v10::Message::from(*resp)
81                && header.device_index == device_index
82                    && header.sub_id == v10::MessageType::Error.into()
83                    // The feature index we sent would be interpreted as the sub ID by HID++1.0, which is included in the error message.
84                    && payload[0] == 0x00
85                    // The function & software IDs would be interpreted as the register address in HID++1.0.
86                    && payload[1] == nibble::combine(msg.header().function_id, sw_id)
87            {
88                return true;
89            }
90
91            false
92        })
93        .await?;
94
95    let v20_msg = v20::Message::from(response);
96    if v20_msg.header() == msg.header() {
97        let payload = v20_msg.extend_payload();
98        return Ok(Some(ProtocolVersion::V20 {
99            protocol_num: payload[0],
100            target_sw: payload[1],
101        }));
102    }
103
104    let v10::Message::Short(_, payload) = v10::Message::from(response) else {
105        return Ok(None);
106    };
107
108    if payload[2] == v10::ErrorType::InvalidSubId.into() {
109        Ok(Some(ProtocolVersion::V10))
110    } else {
111        Ok(None)
112    }
113}