1use std::sync::Arc;
4
5use hidpp::{
6 channel::HidppChannel,
7 device::Device,
8 feature::CreatableFeature,
9 feature::hires_wheel::{
10 HiResWheelFeature, WheelEventTarget, WheelMode as HidppWheelMode,
11 WheelResolution as HidppWheelResolution,
12 },
13};
14pub use openlogi_core::config::ScrollResolution;
15pub use openlogi_core::hid::scroll::{ScrollReportingTarget, ScrollWheelMode};
16use tracing::debug;
17
18use super::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
19use crate::SharedChannel;
20use crate::backend::HidBackend;
21use crate::channel::route::DeviceRoute;
22
23pub async fn get_scroll_wheel_mode(
25 backend: &dyn HidBackend,
26 route: &DeviceRoute,
27) -> Result<ScrollWheelMode, WriteError> {
28 let index = route.device_index();
29 with_route(backend, route, move |channel| async move {
30 get_scroll_wheel_mode_on_channel(&channel, index).await
31 })
32 .await
33}
34
35pub async fn get_scroll_wheel_mode_on(
37 shared: &SharedChannel,
38) -> Result<ScrollWheelMode, WriteError> {
39 get_scroll_wheel_mode_on_channel(shared.channel(), shared.device_index()).await
40}
41
42async fn get_scroll_wheel_mode_on_channel(
43 channel: &Arc<HidppChannel>,
44 index: u8,
45) -> Result<ScrollWheelMode, WriteError> {
46 let mut device = open_device(channel, index).await?;
47 let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
48 read_mode(&feature).await
49}
50
51pub async fn set_scroll_resolution(
54 backend: &dyn HidBackend,
55 route: &DeviceRoute,
56 resolution: ScrollResolution,
57) -> Result<ScrollWheelMode, WriteError> {
58 let index = route.device_index();
59 with_route(backend, route, move |channel| async move {
60 change_wheel_mode_on_channel(&channel, index, Some(resolution), None, false).await
61 })
62 .await
63}
64
65pub async fn set_scroll_resolution_on(
67 shared: &SharedChannel,
68 resolution: ScrollResolution,
69) -> Result<ScrollWheelMode, WriteError> {
70 change_wheel_mode_on_channel(
71 shared.channel(),
72 shared.device_index(),
73 Some(resolution),
74 None,
75 false,
76 )
77 .await
78}
79
80pub async fn set_scroll_wheel_mode(
85 backend: &dyn HidBackend,
86 route: &DeviceRoute,
87 resolution: ScrollResolution,
88 inverted: bool,
89) -> Result<ScrollWheelMode, WriteError> {
90 let index = route.device_index();
91 with_route(backend, route, move |channel| async move {
92 change_wheel_mode_on_channel(&channel, index, Some(resolution), Some(inverted), true).await
93 })
94 .await
95}
96
97pub async fn set_scroll_wheel_mode_on(
99 shared: &SharedChannel,
100 resolution: ScrollResolution,
101 inverted: bool,
102) -> Result<ScrollWheelMode, WriteError> {
103 change_wheel_mode_on_channel(
104 shared.channel(),
105 shared.device_index(),
106 Some(resolution),
107 Some(inverted),
108 true,
109 )
110 .await
111}
112
113pub async fn set_scroll_inversion(
121 backend: &dyn HidBackend,
122 route: &DeviceRoute,
123 inverted: bool,
124) -> Result<(), WriteError> {
125 let index = route.device_index();
126 with_route(backend, route, move |channel| async move {
127 change_wheel_mode_on_channel(&channel, index, None, Some(inverted), true)
128 .await
129 .map(|_| ())
130 })
131 .await
132}
133
134pub async fn set_scroll_inversion_on(
137 shared: &SharedChannel,
138 inverted: bool,
139) -> Result<(), WriteError> {
140 change_wheel_mode_on_channel(
141 shared.channel(),
142 shared.device_index(),
143 None,
144 Some(inverted),
145 true,
146 )
147 .await
148 .map(|_| ())
149}
150
151async fn change_wheel_mode_on_channel(
152 channel: &Arc<HidppChannel>,
153 index: u8,
154 resolution: Option<ScrollResolution>,
155 inverted: Option<bool>,
156 require_invert_support: bool,
157) -> Result<ScrollWheelMode, WriteError> {
158 let mut device = open_device(channel, index).await?;
159 let feature = open_feature::<HiResWheelFeature>(&mut device).await?;
160 if require_invert_support {
161 let capabilities = feature.get_wheel_capabilities().await.map_err(|error| {
162 classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
163 })?;
164 if !capabilities.has_invert {
165 return Err(WriteError::FeatureUnsupported {
166 feature_hex: HiResWheelFeature::ID,
167 });
168 }
169 }
170
171 let current = read_mode(&feature).await?;
172 let desired = desired_mode(current, resolution, inverted);
173 if current == desired {
174 debug!(index, ?desired, "wheel mode already set; skipping");
175 return Ok(current);
176 }
177
178 let written = feature
179 .set_wheel_mode(
180 target_to_hidpp(desired.target),
181 resolution_to_hidpp(desired.resolution),
182 desired.inverted,
183 )
184 .await
185 .map_err(|error| {
186 classify_hidpp_error(error, HidppOperation::WriteWheelMode, HiResWheelFeature::ID)
187 })?;
188 validate_applied(mode_from_hidpp(written)?, desired)?;
189
190 let read_back = read_mode(&feature).await?;
191 validate_applied(read_back, desired)?;
192 debug!(index, ?read_back, "wheel mode written and verified");
193 Ok(read_back)
194}
195
196async fn open_device(channel: &Arc<HidppChannel>, index: u8) -> Result<Device, WriteError> {
197 Device::new(Arc::clone(channel), index)
198 .await
199 .map_err(|_| WriteError::DeviceUnreachable { index })
200}
201
202async fn read_mode(feature: &HiResWheelFeature) -> Result<ScrollWheelMode, WriteError> {
203 let mode = feature.get_wheel_mode().await.map_err(|error| {
204 classify_hidpp_error(error, HidppOperation::ReadWheelMode, HiResWheelFeature::ID)
205 })?;
206 mode_from_hidpp(mode)
207}
208
209fn desired_mode(
210 current: ScrollWheelMode,
211 resolution: Option<ScrollResolution>,
212 inverted: Option<bool>,
213) -> ScrollWheelMode {
214 ScrollWheelMode {
215 resolution: resolution.unwrap_or(current.resolution),
216 inverted: inverted.unwrap_or(current.inverted),
217 target: if resolution.is_some() || inverted == Some(true) {
222 ScrollReportingTarget::Native
223 } else {
224 current.target
225 },
226 }
227}
228
229fn validate_applied(actual: ScrollWheelMode, desired: ScrollWheelMode) -> Result<(), WriteError> {
230 if actual == desired {
231 Ok(())
232 } else {
233 Err(WriteError::UnsupportedResponse {
234 operation: HidppOperation::WriteWheelMode,
235 feature_hex: HiResWheelFeature::ID,
236 })
237 }
238}
239
240fn resolution_from_hidpp(resolution: HidppWheelResolution) -> Result<ScrollResolution, WriteError> {
241 Ok(match resolution {
242 HidppWheelResolution::Low => ScrollResolution::Low,
243 HidppWheelResolution::High => ScrollResolution::High,
244 _ => return Err(unsupported_read_response()),
245 })
246}
247
248fn resolution_to_hidpp(resolution: ScrollResolution) -> HidppWheelResolution {
249 match resolution {
250 ScrollResolution::Low => HidppWheelResolution::Low,
251 ScrollResolution::High => HidppWheelResolution::High,
252 }
253}
254
255fn target_from_hidpp(target: WheelEventTarget) -> Result<ScrollReportingTarget, WriteError> {
256 match target {
257 WheelEventTarget::Native => Ok(ScrollReportingTarget::Native),
258 WheelEventTarget::Diverted => Ok(ScrollReportingTarget::Diverted),
259 _ => Err(unsupported_read_response()),
260 }
261}
262
263fn target_to_hidpp(target: ScrollReportingTarget) -> WheelEventTarget {
264 match target {
265 ScrollReportingTarget::Native => WheelEventTarget::Native,
266 ScrollReportingTarget::Diverted => WheelEventTarget::Diverted,
267 }
268}
269
270fn mode_from_hidpp(mode: HidppWheelMode) -> Result<ScrollWheelMode, WriteError> {
271 Ok(ScrollWheelMode {
272 resolution: resolution_from_hidpp(mode.resolution)?,
273 inverted: mode.inverted,
274 target: target_from_hidpp(mode.target)?,
275 })
276}
277
278fn unsupported_read_response() -> WriteError {
279 WriteError::UnsupportedResponse {
280 operation: HidppOperation::ReadWheelMode,
281 feature_hex: HiResWheelFeature::ID,
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 fn native(resolution: ScrollResolution, inverted: bool) -> ScrollWheelMode {
290 ScrollWheelMode {
291 resolution,
292 inverted,
293 target: ScrollReportingTarget::Native,
294 }
295 }
296
297 #[test]
298 fn mode_value_conversions_preserve_known_wire_values() -> Result<(), WriteError> {
299 assert_eq!(
300 resolution_from_hidpp(HidppWheelResolution::Low)?,
301 ScrollResolution::Low
302 );
303 assert_eq!(
304 resolution_from_hidpp(HidppWheelResolution::High)?,
305 ScrollResolution::High
306 );
307 assert_eq!(
308 target_from_hidpp(WheelEventTarget::Native)?,
309 ScrollReportingTarget::Native
310 );
311 assert_eq!(
312 target_from_hidpp(WheelEventTarget::Diverted)?,
313 ScrollReportingTarget::Diverted
314 );
315 assert_eq!(
316 target_to_hidpp(ScrollReportingTarget::Native),
317 WheelEventTarget::Native
318 );
319 assert_eq!(
320 target_to_hidpp(ScrollReportingTarget::Diverted),
321 WheelEventTarget::Diverted
322 );
323 Ok(())
324 }
325
326 #[test]
327 fn resolution_only_preserves_inversion_and_targets_native() {
328 let current = ScrollWheelMode {
329 resolution: ScrollResolution::High,
330 inverted: true,
331 target: ScrollReportingTarget::Diverted,
332 };
333 assert_eq!(
334 desired_mode(current, Some(ScrollResolution::Low), None),
335 native(ScrollResolution::Low, true)
336 );
337 }
338
339 #[test]
340 fn inversion_only_preserves_resolution_and_targets_native() {
341 let current = ScrollWheelMode {
342 resolution: ScrollResolution::Low,
343 inverted: false,
344 target: ScrollReportingTarget::Diverted,
345 };
346 assert_eq!(
347 desired_mode(current, None, Some(true)),
348 native(ScrollResolution::Low, true)
349 );
350 }
351
352 #[test]
353 fn default_non_inverted_setting_preserves_diverted_reporting() {
354 let current = ScrollWheelMode {
355 resolution: ScrollResolution::High,
356 inverted: false,
357 target: ScrollReportingTarget::Diverted,
358 };
359 assert_eq!(desired_mode(current, None, Some(false)), current);
360 }
361
362 #[test]
363 fn mismatched_set_or_read_back_is_rejected() {
364 let desired = native(ScrollResolution::Low, false);
365 let actual = native(ScrollResolution::High, false);
366 assert!(matches!(
367 validate_applied(actual, desired),
368 Err(WriteError::UnsupportedResponse {
369 operation: HidppOperation::WriteWheelMode,
370 feature_hex: 0x2121,
371 })
372 ));
373 }
374}