logiops_core/
device.rs

1//! Device abstraction for HID++ communication.
2
3use std::collections::HashMap;
4
5use hidpp_transport::HidapiChannel;
6use tracing::{debug, info, warn};
7
8use crate::error::Result;
9use crate::features::device_name::DeviceKind;
10use crate::features::{DeviceNameFeature, RootFeature};
11use crate::protocol::{feature_id, ProtocolVersion, DEVICE_INDEX_DIRECT};
12
13/// Information about a connected HID++ device.
14#[derive(Debug, Clone)]
15pub struct DeviceInfo {
16    /// Device name from HID++ query.
17    pub name: String,
18    /// HID++ protocol version.
19    pub protocol_version: ProtocolVersion,
20    /// Device kind (mouse, keyboard, etc.).
21    pub device_kind: DeviceKind,
22    /// Feature index mapping (`feature_id` -> index).
23    pub features: HashMap<u16, u8>,
24}
25
26/// A HID++ device with protocol support.
27pub struct HidppDevice {
28    channel: HidapiChannel,
29    device_index: u8,
30    info: Option<DeviceInfo>,
31}
32
33impl HidppDevice {
34    /// Creates a new HID++ device wrapper.
35    ///
36    /// # Arguments
37    /// * `channel` - HID channel for communication
38    /// * `device_index` - Device index (0xFF for direct, 1-6 for receiver)
39    #[must_use]
40    pub fn new(channel: HidapiChannel, device_index: u8) -> Self {
41        Self {
42            channel,
43            device_index,
44            info: None,
45        }
46    }
47
48    /// Creates a device for direct USB/Bluetooth connection.
49    #[must_use]
50    pub fn direct(channel: HidapiChannel) -> Self {
51        Self::new(channel, DEVICE_INDEX_DIRECT)
52    }
53
54    /// Returns the underlying HID channel.
55    #[must_use]
56    pub fn channel(&self) -> &HidapiChannel {
57        &self.channel
58    }
59
60    /// Returns the device index.
61    #[must_use]
62    pub fn device_index(&self) -> u8 {
63        self.device_index
64    }
65
66    /// Returns device info if initialized.
67    #[must_use]
68    pub fn info(&self) -> Option<&DeviceInfo> {
69        self.info.as_ref()
70    }
71
72    /// Initializes the device by querying protocol version, name, and features.
73    ///
74    /// This should be called after creating the device to populate device info.
75    ///
76    /// # Errors
77    /// Returns an error if HID++ communication fails or the device doesn't respond.
78    ///
79    /// # Panics
80    /// Panics if called after successful initialization (info already set).
81    pub async fn initialize(&mut self) -> Result<&DeviceInfo> {
82        info!(
83            path = %self.channel.path(),
84            device_index = self.device_index,
85            "initializing HID++ device"
86        );
87
88        let root = RootFeature::new(self.device_index);
89
90        // Ping to get protocol version
91        let protocol_version = root.ping(&self.channel).await?;
92
93        if !protocol_version.supports_hidpp2() {
94            warn!(
95                version = %protocol_version,
96                "device uses HID++ 1.0, limited support available"
97            );
98        }
99
100        // Discover features
101        let mut features = HashMap::new();
102
103        // Always have IRoot at index 0
104        features.insert(feature_id::IROOT, 0);
105
106        // Try to find common features
107        let feature_ids = [
108            feature_id::IFEATURE_SET,
109            feature_id::DEVICE_NAME,
110            feature_id::FIRMWARE_INFO,
111            feature_id::UNIFIED_BATTERY,
112            feature_id::BATTERY_STATUS,
113            feature_id::REPROG_CONTROLS,
114            feature_id::SMART_SHIFT,
115            feature_id::HIRES_SCROLLING,
116            feature_id::THUMB_WHEEL,
117            feature_id::ADJUSTABLE_DPI,
118            feature_id::ONBOARD_PROFILES,
119        ];
120
121        for &fid in &feature_ids {
122            match root.get_feature_index(&self.channel, fid).await {
123                Ok(Some(idx)) => {
124                    features.insert(fid, idx);
125                }
126                Ok(None) => {
127                    debug!(feature_id = format!("0x{fid:04X}"), "feature not supported");
128                }
129                Err(e) => {
130                    warn!(
131                        feature_id = format!("0x{fid:04X}"),
132                        error = %e,
133                        "error querying feature"
134                    );
135                }
136            }
137        }
138
139        // Get device name if supported
140        let name = if let Some(&name_idx) = features.get(&feature_id::DEVICE_NAME) {
141            let name_feature = DeviceNameFeature::new(self.device_index, name_idx);
142            match name_feature.get_name(&self.channel).await {
143                Ok(name) => name,
144                Err(e) => {
145                    warn!(error = %e, "failed to get device name");
146                    "Unknown Device".to_string()
147                }
148            }
149        } else {
150            "Unknown Device".to_string()
151        };
152
153        // Get device type if supported
154        let device_kind = if let Some(&name_idx) = features.get(&feature_id::DEVICE_NAME) {
155            let name_feature = DeviceNameFeature::new(self.device_index, name_idx);
156            match name_feature.get_device_type(&self.channel).await {
157                Ok(kind) => kind,
158                Err(e) => {
159                    warn!(error = %e, "failed to get device type");
160                    DeviceKind::Unknown(0xFF)
161                }
162            }
163        } else {
164            DeviceKind::Unknown(0xFF)
165        };
166
167        let info = DeviceInfo {
168            name,
169            protocol_version,
170            device_kind,
171            features,
172        };
173
174        info!(
175            name = %info.name,
176            version = %info.protocol_version,
177            device_type = %info.device_kind,
178            feature_count = info.features.len(),
179            "device initialized"
180        );
181
182        self.info = Some(info);
183        Ok(self.info.as_ref().unwrap())
184    }
185
186    /// Checks if a feature is supported by this device.
187    #[must_use]
188    pub fn has_feature(&self, feature_id: u16) -> bool {
189        self.info
190            .as_ref()
191            .is_some_and(|i| i.features.contains_key(&feature_id))
192    }
193
194    /// Gets the feature index for a given feature ID.
195    #[must_use]
196    pub fn get_feature_index(&self, feature_id: u16) -> Option<u8> {
197        self.info.as_ref()?.features.get(&feature_id).copied()
198    }
199}
200
201impl std::fmt::Debug for HidppDevice {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.debug_struct("HidppDevice")
204            .field("path", &self.channel.path())
205            .field("device_index", &self.device_index)
206            .field("info", &self.info)
207            .finish()
208    }
209}