logiops_core/features/
root.rs

1//! `IRoot` feature (0x0000) - Protocol version and feature discovery.
2//!
3//! The root feature is always at index 0 and provides:
4//! - Protocol version detection (ping)
5//! - Feature index lookup by feature ID
6
7use hidpp_transport::HidapiChannel;
8use tracing::{debug, trace};
9
10use crate::error::{HidppErrorCode, ProtocolError, Result};
11use crate::protocol::{
12    build_long_request, feature_id, get_error_code, is_error_response, ProtocolVersion,
13    DEVICE_INDEX_DIRECT,
14};
15
16/// `IRoot` feature implementation.
17pub struct RootFeature {
18    device_index: u8,
19}
20
21impl RootFeature {
22    /// Creates a new root feature accessor.
23    ///
24    /// # Arguments
25    /// * `device_index` - Device index (0xFF for direct USB/Bluetooth)
26    #[must_use]
27    pub fn new(device_index: u8) -> Self {
28        Self { device_index }
29    }
30
31    /// Creates root feature for direct connection.
32    #[must_use]
33    pub fn direct() -> Self {
34        Self::new(DEVICE_INDEX_DIRECT)
35    }
36
37    /// Pings the device and returns the protocol version.
38    ///
39    /// This is the primary way to detect if a device supports HID++ and
40    /// which version it uses.
41    ///
42    /// # Arguments
43    /// * `channel` - HID channel to communicate over
44    ///
45    /// # Errors
46    /// Returns an error if the device doesn't respond, returns invalid data,
47    /// or returns an HID++ error.
48    pub async fn ping(&self, channel: &HidapiChannel) -> Result<ProtocolVersion> {
49        // Ping request: feature_index=0 (IRoot), function_id=1, params=[0, 0, ping_data]
50        // Use long reports for better compatibility (some Bluetooth devices don't support short)
51        let ping_data: u8 = 0x5A; // Arbitrary value to verify echo
52        let request = build_long_request(self.device_index, 0x00, 0x01, &[0x00, 0x00, ping_data]);
53
54        trace!("sending ping request (long report)");
55        let response = channel.request(&request, 5).await?;
56
57        if is_error_response(&response) {
58            let code = get_error_code(&response).unwrap_or(0);
59            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
60        }
61
62        if response.len() < 7 {
63            return Err(ProtocolError::InvalidResponse(
64                "ping response too short".to_string(),
65            ));
66        }
67
68        // Response format: [report_id, device_idx, feature_idx, func_sw_id, major, minor, ping_data]
69        let major = response[4];
70        let minor = response[5];
71        let echo = response[6];
72
73        if echo != ping_data {
74            return Err(ProtocolError::InvalidResponse(format!(
75                "ping echo mismatch: expected 0x{ping_data:02X}, got 0x{echo:02X}"
76            )));
77        }
78
79        let version = ProtocolVersion::new(major, minor);
80        debug!(
81            version = %version,
82            major,
83            minor,
84            "device ping successful"
85        );
86
87        Ok(version)
88    }
89
90    /// Gets the feature index for a given feature ID.
91    ///
92    /// # Arguments
93    /// * `channel` - HID channel to communicate over
94    /// * `feature_id` - The feature ID to look up (e.g., 0x2201 for DPI)
95    ///
96    /// # Returns
97    /// The feature index if found, or None if the feature is not supported.
98    ///
99    /// # Errors
100    /// Returns an error if HID++ communication fails or the device returns an error
101    /// (other than "unknown feature").
102    pub async fn get_feature_index(
103        &self,
104        channel: &HidapiChannel,
105        feature_id: u16,
106    ) -> Result<Option<u8>> {
107        // GetFeatureID request: function_id=0, params=[feature_id_hi, feature_id_lo]
108        // Use long reports for better compatibility (some Bluetooth devices don't support short)
109        let feature_bytes = feature_id.to_be_bytes();
110        let request = build_long_request(self.device_index, 0x00, 0x00, &feature_bytes);
111
112        trace!(
113            feature_id = format!("0x{feature_id:04X}"),
114            "getting feature index"
115        );
116        let response = channel.request(&request, 5).await?;
117
118        if is_error_response(&response) {
119            let code = get_error_code(&response).unwrap_or(0);
120            if code == 0x01 {
121                // Unknown = feature not supported
122                return Ok(None);
123            }
124            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
125        }
126
127        if response.len() < 5 {
128            return Err(ProtocolError::InvalidResponse(
129                "feature index response too short".to_string(),
130            ));
131        }
132
133        let index = response[4];
134        if index == 0 && feature_id != feature_id::IROOT {
135            // Index 0 is reserved for IRoot
136            return Ok(None);
137        }
138
139        debug!(
140            feature_id = format!("0x{feature_id:04X}"),
141            index, "feature index found"
142        );
143        Ok(Some(index))
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_root_feature_creation() {
153        let root = RootFeature::direct();
154        assert_eq!(root.device_index, DEVICE_INDEX_DIRECT);
155
156        let root = RootFeature::new(0x01);
157        assert_eq!(root.device_index, 0x01);
158    }
159}