1use hidpp_transport::HidapiChannel;
10use tracing::{debug, trace};
11
12use crate::error::{HidppErrorCode, ProtocolError, Result};
13use crate::protocol::{build_long_request, get_error_code, is_error_response};
14
15mod function_id {
17 pub const GET_PROFILES_DESC: u8 = 0x00;
19 pub const SET_ONBOARD_MODE: u8 = 0x01;
21 pub const GET_ONBOARD_MODE: u8 = 0x02;
23 pub const GET_CURRENT_PROFILE: u8 = 0x04;
25 pub const SET_CURRENT_PROFILE: u8 = 0x05;
27 pub const GET_CURRENT_DPI_INDEX: u8 = 0x06;
29 pub const SET_CURRENT_DPI_INDEX: u8 = 0x07;
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35#[repr(u8)]
36pub enum MemoryModel {
37 None = 0,
39 G402 = 1,
41 G303 = 2,
43 G900 = 3,
45 G915 = 4,
47 Unknown(u8),
49}
50
51impl From<u8> for MemoryModel {
52 fn from(value: u8) -> Self {
53 match value {
54 0 => Self::None,
55 1 => Self::G402,
56 2 => Self::G303,
57 3 => Self::G900,
58 4 => Self::G915,
59 v => Self::Unknown(v),
60 }
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66#[repr(u8)]
67pub enum OnboardMode {
68 Onboard = 1,
70 Host = 2,
72}
73
74impl TryFrom<u8> for OnboardMode {
75 type Error = ();
76
77 fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
78 match value {
79 1 => Ok(Self::Onboard),
80 2 => Ok(Self::Host),
81 _ => Err(()),
82 }
83 }
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct ProfilesDescription {
89 pub memory_model: MemoryModel,
91 pub profile_format: u8,
93 pub macro_format: u8,
95 pub profile_count: u8,
97 pub profile_count_oob: u8,
99 pub button_count: u8,
101 pub sector_count: u8,
103 pub sector_size: u16,
105 pub mechanical_layout: u8,
107 pub num_modes: u8,
109}
110
111pub struct OnboardProfilesFeature {
113 device_index: u8,
114 feature_index: u8,
115}
116
117impl OnboardProfilesFeature {
118 #[must_use]
124 pub fn new(device_index: u8, feature_index: u8) -> Self {
125 Self {
126 device_index,
127 feature_index,
128 }
129 }
130
131 pub async fn get_profiles_description(
136 &self,
137 channel: &HidapiChannel,
138 ) -> Result<ProfilesDescription> {
139 let request = build_long_request(
140 self.device_index,
141 self.feature_index,
142 function_id::GET_PROFILES_DESC,
143 &[],
144 );
145
146 trace!("getting profiles description");
147 let response = channel.request(&request, 5).await?;
148
149 if is_error_response(&response) {
150 let code = get_error_code(&response).unwrap_or(0);
151 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
152 }
153
154 if response.len() < 16 {
155 return Err(ProtocolError::InvalidResponse(
156 "profiles description response too short".to_string(),
157 ));
158 }
159
160 let desc = ProfilesDescription {
172 memory_model: MemoryModel::from(response[4]),
173 profile_format: response[5],
174 macro_format: response[6],
175 profile_count: response[7],
176 profile_count_oob: response[8],
177 button_count: response[9],
178 sector_count: response[10],
179 sector_size: u16::from_be_bytes([response[11], response[12]]),
180 mechanical_layout: response[13],
181 num_modes: response[14],
182 };
183
184 debug!(
185 memory_model = ?desc.memory_model,
186 profile_count = desc.profile_count,
187 button_count = desc.button_count,
188 "got profiles description"
189 );
190
191 Ok(desc)
192 }
193
194 pub async fn get_onboard_mode(&self, channel: &HidapiChannel) -> Result<OnboardMode> {
199 let request = build_long_request(
200 self.device_index,
201 self.feature_index,
202 function_id::GET_ONBOARD_MODE,
203 &[],
204 );
205
206 trace!("getting onboard mode");
207 let response = channel.request(&request, 5).await?;
208
209 if is_error_response(&response) {
210 let code = get_error_code(&response).unwrap_or(0);
211 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
212 }
213
214 if response.len() < 5 {
215 return Err(ProtocolError::InvalidResponse(
216 "onboard mode response too short".to_string(),
217 ));
218 }
219
220 let mode = OnboardMode::try_from(response[4]).map_err(|()| {
221 ProtocolError::InvalidResponse(format!("invalid onboard mode: {}", response[4]))
222 })?;
223
224 debug!(mode = ?mode, "got onboard mode");
225 Ok(mode)
226 }
227
228 pub async fn set_onboard_mode(
237 &self,
238 channel: &HidapiChannel,
239 mode: OnboardMode,
240 ) -> Result<()> {
241 let request = build_long_request(
242 self.device_index,
243 self.feature_index,
244 function_id::SET_ONBOARD_MODE,
245 &[mode as u8],
246 );
247
248 trace!(mode = ?mode, "setting onboard mode");
249 let response = channel.request(&request, 5).await?;
250
251 if is_error_response(&response) {
252 let code = get_error_code(&response).unwrap_or(0);
253 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
254 }
255
256 debug!(mode = ?mode, "set onboard mode");
257 Ok(())
258 }
259
260 pub async fn get_current_profile(&self, channel: &HidapiChannel) -> Result<u8> {
267 let request = build_long_request(
268 self.device_index,
269 self.feature_index,
270 function_id::GET_CURRENT_PROFILE,
271 &[],
272 );
273
274 trace!("getting current profile");
275 let response = channel.request(&request, 5).await?;
276
277 if is_error_response(&response) {
278 let code = get_error_code(&response).unwrap_or(0);
279 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
280 }
281
282 if response.len() < 5 {
283 return Err(ProtocolError::InvalidResponse(
284 "current profile response too short".to_string(),
285 ));
286 }
287
288 let profile = response[4];
289 debug!(profile, "got current profile");
290 Ok(profile)
291 }
292
293 pub async fn set_current_profile(
302 &self,
303 channel: &HidapiChannel,
304 profile_index: u8,
305 ) -> Result<()> {
306 let request = build_long_request(
307 self.device_index,
308 self.feature_index,
309 function_id::SET_CURRENT_PROFILE,
310 &[profile_index],
311 );
312
313 trace!(profile = profile_index, "setting current profile");
314 let response = channel.request(&request, 5).await?;
315
316 if is_error_response(&response) {
317 let code = get_error_code(&response).unwrap_or(0);
318 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
319 }
320
321 debug!(profile = profile_index, "set current profile");
322 Ok(())
323 }
324
325 pub async fn get_current_dpi_index(&self, channel: &HidapiChannel) -> Result<u8> {
332 let request = build_long_request(
333 self.device_index,
334 self.feature_index,
335 function_id::GET_CURRENT_DPI_INDEX,
336 &[],
337 );
338
339 trace!("getting current DPI index");
340 let response = channel.request(&request, 5).await?;
341
342 if is_error_response(&response) {
343 let code = get_error_code(&response).unwrap_or(0);
344 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
345 }
346
347 if response.len() < 5 {
348 return Err(ProtocolError::InvalidResponse(
349 "current DPI index response too short".to_string(),
350 ));
351 }
352
353 let dpi_index = response[4];
354 debug!(dpi_index, "got current DPI index");
355 Ok(dpi_index)
356 }
357
358 pub async fn set_current_dpi_index(
367 &self,
368 channel: &HidapiChannel,
369 dpi_index: u8,
370 ) -> Result<()> {
371 let request = build_long_request(
372 self.device_index,
373 self.feature_index,
374 function_id::SET_CURRENT_DPI_INDEX,
375 &[dpi_index],
376 );
377
378 trace!(dpi_index, "setting current DPI index");
379 let response = channel.request(&request, 5).await?;
380
381 if is_error_response(&response) {
382 let code = get_error_code(&response).unwrap_or(0);
383 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
384 }
385
386 debug!(dpi_index, "set current DPI index");
387 Ok(())
388 }
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_memory_model() {
397 assert_eq!(MemoryModel::from(0), MemoryModel::None);
398 assert_eq!(MemoryModel::from(1), MemoryModel::G402);
399 assert_eq!(MemoryModel::from(3), MemoryModel::G900);
400 assert!(matches!(MemoryModel::from(99), MemoryModel::Unknown(99)));
401 }
402
403 #[test]
404 fn test_onboard_mode() {
405 assert_eq!(OnboardMode::try_from(1), Ok(OnboardMode::Onboard));
406 assert_eq!(OnboardMode::try_from(2), Ok(OnboardMode::Host));
407 assert!(OnboardMode::try_from(0).is_err());
408 assert!(OnboardMode::try_from(3).is_err());
409 }
410}