logiops_core/features/
hires_scrolling.rs1use bitflags::bitflags;
7use hidpp_transport::HidapiChannel;
8use tracing::{debug, trace};
9
10use crate::error::{HidppErrorCode, ProtocolError, Result};
11use crate::protocol::{build_long_request, get_error_code, is_error_response};
12
13mod function_id {
15 pub const GET_WHEEL_CAPABILITY: u8 = 0x00;
17 pub const GET_WHEEL_MODE: u8 = 0x01;
19 pub const SET_WHEEL_MODE: u8 = 0x02;
21 pub const GET_RATCHET_SWITCH_STATE: u8 = 0x03;
23}
24
25bitflags! {
26 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
28 pub struct WheelCapabilities: u8 {
29 const HAS_RATCHET = 0x04;
31 const HAS_INVERT = 0x08;
33 }
34}
35
36bitflags! {
37 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
39 pub struct WheelModeFlags: u8 {
40 const TARGET = 0x01;
42 const RESOLUTION = 0x02;
44 const INVERT = 0x04;
46 }
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct WheelInfo {
52 pub multiplier: u8,
54 pub capabilities: WheelCapabilities,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct HiResScrollConfig {
61 pub target: bool,
63 pub hires: bool,
65 pub invert: bool,
67}
68
69impl Default for HiResScrollConfig {
70 fn default() -> Self {
71 Self {
72 target: false,
73 hires: true,
74 invert: false,
75 }
76 }
77}
78
79impl From<WheelModeFlags> for HiResScrollConfig {
80 fn from(flags: WheelModeFlags) -> Self {
81 Self {
82 target: flags.contains(WheelModeFlags::TARGET),
83 hires: flags.contains(WheelModeFlags::RESOLUTION),
84 invert: flags.contains(WheelModeFlags::INVERT),
85 }
86 }
87}
88
89impl From<&HiResScrollConfig> for WheelModeFlags {
90 fn from(config: &HiResScrollConfig) -> Self {
91 let mut flags = WheelModeFlags::empty();
92 if config.target {
93 flags |= WheelModeFlags::TARGET;
94 }
95 if config.hires {
96 flags |= WheelModeFlags::RESOLUTION;
97 }
98 if config.invert {
99 flags |= WheelModeFlags::INVERT;
100 }
101 flags
102 }
103}
104
105pub struct HiResScrollingFeature {
107 device_index: u8,
108 feature_index: u8,
109}
110
111impl HiResScrollingFeature {
112 #[must_use]
118 pub fn new(device_index: u8, feature_index: u8) -> Self {
119 Self {
120 device_index,
121 feature_index,
122 }
123 }
124
125 pub async fn get_wheel_capability(&self, channel: &HidapiChannel) -> Result<WheelInfo> {
132 let request = build_long_request(
133 self.device_index,
134 self.feature_index,
135 function_id::GET_WHEEL_CAPABILITY,
136 &[],
137 );
138
139 trace!("getting wheel capability");
140 let response = channel.request(&request, 5).await?;
141
142 if is_error_response(&response) {
143 let code = get_error_code(&response).unwrap_or(0);
144 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
145 }
146
147 if response.len() < 6 {
148 return Err(ProtocolError::InvalidResponse(
149 "wheel capability response too short".to_string(),
150 ));
151 }
152
153 let multiplier = response[4];
154 let flags = response[5];
155
156 let info = WheelInfo {
157 multiplier,
158 capabilities: WheelCapabilities::from_bits_truncate(flags),
159 };
160
161 debug!(
162 multiplier = info.multiplier,
163 has_ratchet = info.capabilities.contains(WheelCapabilities::HAS_RATCHET),
164 has_invert = info.capabilities.contains(WheelCapabilities::HAS_INVERT),
165 "got wheel capability"
166 );
167
168 Ok(info)
169 }
170
171 pub async fn get_wheel_mode(&self, channel: &HidapiChannel) -> Result<HiResScrollConfig> {
176 let request = build_long_request(
177 self.device_index,
178 self.feature_index,
179 function_id::GET_WHEEL_MODE,
180 &[],
181 );
182
183 trace!("getting wheel mode");
184 let response = channel.request(&request, 5).await?;
185
186 if is_error_response(&response) {
187 let code = get_error_code(&response).unwrap_or(0);
188 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
189 }
190
191 if response.len() < 5 {
192 return Err(ProtocolError::InvalidResponse(
193 "wheel mode response too short".to_string(),
194 ));
195 }
196
197 let flags = WheelModeFlags::from_bits_truncate(response[4]);
198 let config = HiResScrollConfig::from(flags);
199
200 debug!(
201 target = config.target,
202 hires = config.hires,
203 invert = config.invert,
204 "got wheel mode"
205 );
206
207 Ok(config)
208 }
209
210 pub async fn set_wheel_mode(
219 &self,
220 channel: &HidapiChannel,
221 config: &HiResScrollConfig,
222 ) -> Result<()> {
223 let flags = WheelModeFlags::from(config);
224 let request = build_long_request(
225 self.device_index,
226 self.feature_index,
227 function_id::SET_WHEEL_MODE,
228 &[flags.bits()],
229 );
230
231 trace!(
232 target = config.target,
233 hires = config.hires,
234 invert = config.invert,
235 "setting wheel mode"
236 );
237 let response = channel.request(&request, 5).await?;
238
239 if is_error_response(&response) {
240 let code = get_error_code(&response).unwrap_or(0);
241 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
242 }
243
244 debug!(
245 target = config.target,
246 hires = config.hires,
247 invert = config.invert,
248 "set wheel mode"
249 );
250
251 Ok(())
252 }
253
254 pub async fn get_ratchet_switch_state(&self, channel: &HidapiChannel) -> Result<bool> {
263 let request = build_long_request(
264 self.device_index,
265 self.feature_index,
266 function_id::GET_RATCHET_SWITCH_STATE,
267 &[],
268 );
269
270 trace!("getting ratchet switch state");
271 let response = channel.request(&request, 5).await?;
272
273 if is_error_response(&response) {
274 let code = get_error_code(&response).unwrap_or(0);
275 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
276 }
277
278 if response.len() < 5 {
279 return Err(ProtocolError::InvalidResponse(
280 "ratchet state response too short".to_string(),
281 ));
282 }
283
284 let is_ratchet = (response[4] & 0x01) != 0;
285 debug!(is_ratchet, "got ratchet switch state");
286
287 Ok(is_ratchet)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn test_wheel_mode_flags_conversion() {
297 let config = HiResScrollConfig {
298 target: true,
299 hires: true,
300 invert: false,
301 };
302 let flags = WheelModeFlags::from(&config);
303 assert!(flags.contains(WheelModeFlags::TARGET));
304 assert!(flags.contains(WheelModeFlags::RESOLUTION));
305 assert!(!flags.contains(WheelModeFlags::INVERT));
306 }
307
308 #[test]
309 fn test_config_from_flags() {
310 let flags = WheelModeFlags::RESOLUTION | WheelModeFlags::INVERT;
311 let config = HiResScrollConfig::from(flags);
312 assert!(!config.target);
313 assert!(config.hires);
314 assert!(config.invert);
315 }
316
317 #[test]
318 fn test_capability_flags() {
319 let caps = WheelCapabilities::HAS_RATCHET | WheelCapabilities::HAS_INVERT;
320 assert!(caps.contains(WheelCapabilities::HAS_RATCHET));
321 assert!(caps.contains(WheelCapabilities::HAS_INVERT));
322 }
323}