logiops_core/features/
feature_set.rs

1//! `IFeatureSet` feature (0x0001) - Feature enumeration.
2//!
3//! This feature provides complete enumeration of all features supported
4//! by a device, including their types (hidden, obsolete, software hidden).
5
6use hidpp_transport::HidapiChannel;
7use tracing::{debug, trace};
8
9use crate::error::{HidppErrorCode, ProtocolError, Result};
10use crate::protocol::{build_long_request, get_error_code, is_error_response};
11
12/// `IFeatureSet` feature implementation.
13pub struct FeatureSetFeature {
14    device_index: u8,
15    feature_index: u8,
16}
17
18/// Information about a feature.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct FeatureInfo {
21    /// The feature ID.
22    pub feature_id: u16,
23    /// The feature index on this device.
24    pub index: u8,
25    /// Feature type flags.
26    pub feature_type: FeatureType,
27}
28
29/// Feature type flags.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
31pub struct FeatureType {
32    /// Feature is obsolete (deprecated).
33    pub obsolete: bool,
34    /// Feature is hidden from enumeration (internal use).
35    pub hidden: bool,
36    /// Feature should be hidden from user-facing software.
37    pub software_hidden: bool,
38}
39
40impl FeatureType {
41    /// Parses feature type from byte.
42    #[must_use]
43    pub fn from_byte(byte: u8) -> Self {
44        Self {
45            obsolete: (byte & 0x80) != 0,
46            hidden: (byte & 0x40) != 0,
47            software_hidden: (byte & 0x20) != 0,
48        }
49    }
50
51    /// Returns true if this is a normal, visible feature.
52    #[must_use]
53    pub fn is_visible(&self) -> bool {
54        !self.hidden && !self.software_hidden
55    }
56}
57
58impl FeatureSetFeature {
59    /// Creates a new feature set feature accessor.
60    ///
61    /// # Arguments
62    /// * `device_index` - Device index (0xFF for direct)
63    /// * `feature_index` - Feature index from root feature discovery
64    #[must_use]
65    pub fn new(device_index: u8, feature_index: u8) -> Self {
66        Self {
67            device_index,
68            feature_index,
69        }
70    }
71
72    /// Gets the number of features supported by the device.
73    ///
74    /// # Errors
75    /// Returns an error if HID++ communication fails.
76    pub async fn get_count(&self, channel: &HidapiChannel) -> Result<u8> {
77        // getCount: function_id=0
78        let request = build_long_request(self.device_index, self.feature_index, 0x00, &[]);
79
80        trace!("getting feature count");
81        let response = channel.request(&request, 5).await?;
82
83        if is_error_response(&response) {
84            let code = get_error_code(&response).unwrap_or(0);
85            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
86        }
87
88        if response.len() < 5 {
89            return Err(ProtocolError::InvalidResponse(
90                "feature count response too short".to_string(),
91            ));
92        }
93
94        let count = response[4];
95        debug!(count, "got feature count");
96        Ok(count)
97    }
98
99    /// Gets feature information at the given index.
100    ///
101    /// # Arguments
102    /// * `channel` - HID channel
103    /// * `index` - Feature index (0-based)
104    ///
105    /// # Errors
106    /// Returns an error if HID++ communication fails or the index is out of range.
107    pub async fn get_feature_id(&self, channel: &HidapiChannel, index: u8) -> Result<FeatureInfo> {
108        // getFeatureID: function_id=1, param=index
109        let request = build_long_request(self.device_index, self.feature_index, 0x01, &[index]);
110
111        trace!(index, "getting feature ID");
112        let response = channel.request(&request, 5).await?;
113
114        if is_error_response(&response) {
115            let code = get_error_code(&response).unwrap_or(0);
116            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
117        }
118
119        if response.len() < 7 {
120            return Err(ProtocolError::InvalidResponse(
121                "feature ID response too short".to_string(),
122            ));
123        }
124
125        // Response: [report_id, device_idx, feature_idx, func_sw_id, feature_id_hi, feature_id_lo, feature_type]
126        let feature_id = u16::from_be_bytes([response[4], response[5]]);
127        let feature_type = FeatureType::from_byte(response[6]);
128
129        debug!(
130            index,
131            feature_id = format!("0x{feature_id:04X}"),
132            obsolete = feature_type.obsolete,
133            hidden = feature_type.hidden,
134            "got feature ID"
135        );
136
137        Ok(FeatureInfo {
138            feature_id,
139            index,
140            feature_type,
141        })
142    }
143
144    /// Enumerates all features on the device.
145    ///
146    /// Returns a list of all features supported by the device.
147    ///
148    /// # Errors
149    /// Returns an error if HID++ communication fails.
150    pub async fn enumerate_all(&self, channel: &HidapiChannel) -> Result<Vec<FeatureInfo>> {
151        let count = self.get_count(channel).await?;
152        let mut features = Vec::with_capacity(count as usize);
153
154        for i in 0..count {
155            match self.get_feature_id(channel, i).await {
156                Ok(info) => features.push(info),
157                Err(e) => {
158                    debug!(index = i, error = %e, "failed to get feature, skipping");
159                }
160            }
161        }
162
163        debug!(total = features.len(), "enumerated all features");
164        Ok(features)
165    }
166
167    /// Enumerates only visible features (not hidden or software-hidden).
168    ///
169    /// # Errors
170    /// Returns an error if HID++ communication fails.
171    pub async fn enumerate_visible(&self, channel: &HidapiChannel) -> Result<Vec<FeatureInfo>> {
172        let all = self.enumerate_all(channel).await?;
173        Ok(all
174            .into_iter()
175            .filter(|f| f.feature_type.is_visible())
176            .collect())
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_feature_type_parsing() {
186        let normal = FeatureType::from_byte(0x00);
187        assert!(!normal.obsolete);
188        assert!(!normal.hidden);
189        assert!(!normal.software_hidden);
190        assert!(normal.is_visible());
191
192        let obsolete = FeatureType::from_byte(0x80);
193        assert!(obsolete.obsolete);
194        assert!(!obsolete.hidden);
195        assert!(obsolete.is_visible());
196
197        let hidden = FeatureType::from_byte(0x40);
198        assert!(hidden.hidden);
199        assert!(!hidden.is_visible());
200
201        let sw_hidden = FeatureType::from_byte(0x20);
202        assert!(sw_hidden.software_hidden);
203        assert!(!sw_hidden.is_visible());
204    }
205}