logiops_core/
protocol.rs

1//! HID++ protocol constants, types, and helpers.
2
3use hidpp_transport::channel::report;
4
5/// Software ID to include in requests.
6/// Used to match responses to requests.
7pub const SOFTWARE_ID: u8 = 0x01;
8
9/// Device index for direct USB/Bluetooth connections.
10pub const DEVICE_INDEX_DIRECT: u8 = 0xFF;
11
12/// Device indices for receivers (1-6 for paired devices).
13pub const DEVICE_INDEX_RECEIVER_MIN: u8 = 0x01;
14pub const DEVICE_INDEX_RECEIVER_MAX: u8 = 0x06;
15
16/// HID++ 2.0 feature IDs.
17pub mod feature_id {
18    /// Root feature - always at index 0.
19    pub const IROOT: u16 = 0x0000;
20    /// Feature set enumeration.
21    pub const IFEATURE_SET: u16 = 0x0001;
22    /// Device firmware info.
23    pub const FIRMWARE_INFO: u16 = 0x0003;
24    /// Device name.
25    pub const DEVICE_NAME: u16 = 0x0005;
26    /// Unified battery.
27    pub const UNIFIED_BATTERY: u16 = 0x1000;
28    /// Battery status (older).
29    pub const BATTERY_STATUS: u16 = 0x1001;
30    /// Reprogrammable controls.
31    pub const REPROG_CONTROLS: u16 = 0x1B04;
32    /// `SmartShift` wheel.
33    pub const SMART_SHIFT: u16 = 0x2110;
34    /// Hi-res scrolling.
35    pub const HIRES_SCROLLING: u16 = 0x2121;
36    /// Low-res scrolling (legacy).
37    pub const LOWRES_SCROLLING: u16 = 0x2130;
38    /// Thumb wheel.
39    pub const THUMB_WHEEL: u16 = 0x2150;
40    /// Adjustable DPI.
41    pub const ADJUSTABLE_DPI: u16 = 0x2201;
42    /// On-board profiles.
43    pub const ONBOARD_PROFILES: u16 = 0x8100;
44}
45
46/// HID++ protocol versions.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ProtocolVersion {
49    /// HID++ 1.0 (legacy, limited support).
50    V1_0,
51    /// HID++ 2.0.
52    V2_0,
53    /// Unknown version.
54    Unknown(u8, u8),
55}
56
57impl ProtocolVersion {
58    /// Creates protocol version from major/minor bytes.
59    #[must_use]
60    pub fn new(major: u8, minor: u8) -> Self {
61        match (major, minor) {
62            (1, 0) => Self::V1_0,
63            (2 | 4, _) => Self::V2_0, // 4.x is HID++ 2.0 with extensions
64            _ => Self::Unknown(major, minor),
65        }
66    }
67
68    /// Returns the major version number.
69    #[must_use]
70    pub fn major(&self) -> u8 {
71        match self {
72            Self::V1_0 => 1,
73            Self::V2_0 => 2,
74            Self::Unknown(major, _) => *major,
75        }
76    }
77
78    /// Returns the minor version number.
79    #[must_use]
80    pub fn minor(&self) -> u8 {
81        match self {
82            Self::V1_0 | Self::V2_0 => 0,
83            Self::Unknown(_, minor) => *minor,
84        }
85    }
86
87    /// Returns true if this version supports HID++ 2.0 features.
88    #[must_use]
89    pub fn supports_hidpp2(&self) -> bool {
90        matches!(self, Self::V2_0 | Self::Unknown(2..=4, _))
91    }
92}
93
94impl std::fmt::Display for ProtocolVersion {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::V1_0 => write!(f, "HID++ 1.0"),
98            Self::V2_0 => write!(f, "HID++ 2.0"),
99            Self::Unknown(major, minor) => write!(f, "HID++ {major}.{minor}"),
100        }
101    }
102}
103
104/// Builds a short HID++ request (7 bytes).
105///
106/// # Arguments
107/// * `device_index` - Device index (0xFF for direct, 1-6 for receiver)
108/// * `feature_index` - Feature index from feature discovery
109/// * `function_id` - Function ID within the feature
110/// * `params` - Optional parameters (max 3 bytes)
111#[must_use]
112pub fn build_short_request(
113    device_index: u8,
114    feature_index: u8,
115    function_id: u8,
116    params: &[u8],
117) -> [u8; report::SHORT_REPORT_LEN] {
118    let mut buf = [0u8; report::SHORT_REPORT_LEN];
119    buf[0] = report::SHORT_REPORT_ID;
120    buf[1] = device_index;
121    buf[2] = feature_index;
122    buf[3] = (function_id << 4) | (SOFTWARE_ID & 0x0F);
123
124    let param_len = params.len().min(3);
125    buf[4..4 + param_len].copy_from_slice(&params[..param_len]);
126
127    buf
128}
129
130/// Builds a long HID++ request (20 bytes).
131///
132/// # Arguments
133/// * `device_index` - Device index (0xFF for direct, 1-6 for receiver)
134/// * `feature_index` - Feature index from feature discovery
135/// * `function_id` - Function ID within the feature
136/// * `params` - Optional parameters (max 16 bytes)
137#[must_use]
138pub fn build_long_request(
139    device_index: u8,
140    feature_index: u8,
141    function_id: u8,
142    params: &[u8],
143) -> [u8; report::LONG_REPORT_LEN] {
144    let mut buf = [0u8; report::LONG_REPORT_LEN];
145    buf[0] = report::LONG_REPORT_ID;
146    buf[1] = device_index;
147    buf[2] = feature_index;
148    buf[3] = (function_id << 4) | (SOFTWARE_ID & 0x0F);
149
150    let param_len = params.len().min(16);
151    buf[4..4 + param_len].copy_from_slice(&params[..param_len]);
152
153    buf
154}
155
156/// Parses the software ID from a response.
157#[must_use]
158pub fn parse_software_id(response: &[u8]) -> Option<u8> {
159    response.get(3).map(|b| b & 0x0F)
160}
161
162/// Parses the function ID from a response.
163#[must_use]
164pub fn parse_function_id(response: &[u8]) -> Option<u8> {
165    response.get(3).map(|b| b >> 4)
166}
167
168/// Checks if a response is an error response.
169#[must_use]
170pub fn is_error_response(response: &[u8]) -> bool {
171    response.first() == Some(&0x8F)
172}
173
174/// Gets the error code from an error response.
175#[must_use]
176pub fn get_error_code(response: &[u8]) -> Option<u8> {
177    if is_error_response(response) && response.len() >= 5 {
178        Some(response[4])
179    } else {
180        None
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_build_short_request() {
190        let request = build_short_request(0xFF, 0x00, 0x01, &[0x00, 0x00]);
191        assert_eq!(request[0], report::SHORT_REPORT_ID);
192        assert_eq!(request[1], 0xFF);
193        assert_eq!(request[2], 0x00);
194        assert_eq!(request[3], 0x11); // function_id=1 << 4 | software_id=1
195    }
196
197    #[test]
198    fn test_protocol_version() {
199        assert!(ProtocolVersion::V2_0.supports_hidpp2());
200        assert!(!ProtocolVersion::V1_0.supports_hidpp2());
201
202        let v45 = ProtocolVersion::new(4, 5);
203        assert!(v45.supports_hidpp2());
204    }
205}