logiops_core/features/
device_name.rs

1//! `DeviceName` feature (0x0005) - Device name and type.
2
3use hidpp_transport::HidapiChannel;
4use tracing::{debug, trace};
5
6use crate::error::{HidppErrorCode, ProtocolError, Result};
7use crate::protocol::{build_long_request, get_error_code, is_error_response};
8
9/// `DeviceName` feature implementation.
10pub struct DeviceNameFeature {
11    device_index: u8,
12    feature_index: u8,
13}
14
15impl DeviceNameFeature {
16    /// Creates a new device name feature accessor.
17    ///
18    /// # Arguments
19    /// * `device_index` - Device index (0xFF for direct)
20    /// * `feature_index` - Feature index from root feature discovery
21    #[must_use]
22    pub fn new(device_index: u8, feature_index: u8) -> Self {
23        Self {
24            device_index,
25            feature_index,
26        }
27    }
28
29    /// Gets the number of characters in the device name.
30    ///
31    /// # Errors
32    /// Returns an error if HID++ communication fails or the device returns an error.
33    pub async fn get_name_length(&self, channel: &HidapiChannel) -> Result<u8> {
34        // getDeviceNameCount: function_id=0
35        let request = build_long_request(self.device_index, self.feature_index, 0x00, &[]);
36
37        trace!("getting device name length");
38        let response = channel.request(&request, 5).await?;
39
40        if is_error_response(&response) {
41            let code = get_error_code(&response).unwrap_or(0);
42            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
43        }
44
45        if response.len() < 5 {
46            return Err(ProtocolError::InvalidResponse(
47                "name length response too short".to_string(),
48            ));
49        }
50
51        Ok(response[4])
52    }
53
54    /// Gets the device name.
55    ///
56    /// This may require multiple requests for long names.
57    ///
58    /// # Errors
59    /// Returns an error if HID++ communication fails or the device returns an error.
60    pub async fn get_name(&self, channel: &HidapiChannel) -> Result<String> {
61        let length = self.get_name_length(channel).await?;
62        let mut name = String::with_capacity(length as usize);
63
64        // Each request returns up to 15 characters (short report has 3 params bytes)
65        // but we get 16 payload bytes in response after the header
66        let mut offset = 0u8;
67        while offset < length {
68            // getDeviceName: function_id=1, param=char_index
69            let request =
70                build_long_request(self.device_index, self.feature_index, 0x01, &[offset]);
71
72            trace!(offset, "getting device name chunk");
73            let response = channel.request(&request, 5).await?;
74
75            if is_error_response(&response) {
76                let code = get_error_code(&response).unwrap_or(0);
77                return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
78            }
79
80            // Name bytes start at index 4
81            for &byte in response.iter().skip(4) {
82                if byte == 0 || offset >= length {
83                    break;
84                }
85                if byte.is_ascii() {
86                    name.push(byte as char);
87                }
88                offset += 1;
89            }
90        }
91
92        debug!(name = %name, "got device name");
93        Ok(name)
94    }
95
96    /// Gets the device type.
97    ///
98    /// Returns a device type code as defined by Logitech.
99    ///
100    /// # Errors
101    /// Returns an error if HID++ communication fails or the device returns an error.
102    pub async fn get_device_type(&self, channel: &HidapiChannel) -> Result<DeviceKind> {
103        // getDeviceType: function_id=2
104        let request = build_long_request(self.device_index, self.feature_index, 0x02, &[]);
105
106        trace!("getting device type");
107        let response = channel.request(&request, 5).await?;
108
109        if is_error_response(&response) {
110            let code = get_error_code(&response).unwrap_or(0);
111            return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
112        }
113
114        if response.len() < 5 {
115            return Err(ProtocolError::InvalidResponse(
116                "device type response too short".to_string(),
117            ));
118        }
119
120        let device_type = DeviceKind::from_byte(response[4]);
121        debug!(device_type = ?device_type, "got device type");
122        Ok(device_type)
123    }
124}
125
126/// Device type/kind codes.
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum DeviceKind {
129    /// Keyboard.
130    Keyboard,
131    /// Remote control.
132    RemoteControl,
133    /// Numpad.
134    Numpad,
135    /// Mouse.
136    Mouse,
137    /// Touchpad.
138    Touchpad,
139    /// Trackball.
140    Trackball,
141    /// Presenter.
142    Presenter,
143    /// Receiver.
144    Receiver,
145    /// Headset.
146    Headset,
147    /// Unknown device type.
148    Unknown(u8),
149}
150
151impl DeviceKind {
152    /// Parses device kind from byte.
153    #[must_use]
154    pub fn from_byte(code: u8) -> Self {
155        match code {
156            0x00 => Self::Keyboard,
157            0x01 => Self::RemoteControl,
158            0x02 => Self::Numpad,
159            0x03 => Self::Mouse,
160            0x04 => Self::Touchpad,
161            0x05 => Self::Trackball,
162            0x06 => Self::Presenter,
163            0x07 => Self::Receiver,
164            0x08 => Self::Headset,
165            code => Self::Unknown(code),
166        }
167    }
168
169    /// Returns true if this is a pointing device (mouse, trackball, touchpad).
170    #[must_use]
171    pub fn is_pointing_device(&self) -> bool {
172        matches!(self, Self::Mouse | Self::Trackball | Self::Touchpad)
173    }
174}
175
176impl std::fmt::Display for DeviceKind {
177    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
178        match self {
179            Self::Keyboard => write!(f, "Keyboard"),
180            Self::RemoteControl => write!(f, "Remote Control"),
181            Self::Numpad => write!(f, "Numpad"),
182            Self::Mouse => write!(f, "Mouse"),
183            Self::Touchpad => write!(f, "Touchpad"),
184            Self::Trackball => write!(f, "Trackball"),
185            Self::Presenter => write!(f, "Presenter"),
186            Self::Receiver => write!(f, "Receiver"),
187            Self::Headset => write!(f, "Headset"),
188            Self::Unknown(code) => write!(f, "Unknown(0x{code:02X})"),
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_device_kind() {
199        assert!(DeviceKind::Mouse.is_pointing_device());
200        assert!(DeviceKind::Trackball.is_pointing_device());
201        assert!(!DeviceKind::Keyboard.is_pointing_device());
202    }
203}