Skip to main content

hidpp/feature/
pointer_motion_scaling.rs

1//! Implements the `PointerMotionScaling` feature (ID `0x2205`), a
2//! device-specific multiplier applied to pointer motion.
3//!
4//! The value is 8.8 fixed point: `0x0100` is 1×. The spec recommends
5//! `0x0010`–`0x1000` and says firmware clips out-of-range writes, but an MX
6//! Ergo (`MPM06.03_B0022`) was observed storing both `0x0001` and `0xffff`
7//! unchanged — callers must bound the values they write themselves.
8
9use std::num::NonZeroU16;
10
11use openlogi_hidpp_derive::Feature;
12
13use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
14
15/// A pointer-motion multiplier in 8.8 fixed point (`0x0100` is 1×).
16///
17/// Zero is excluded: it is not a meaningful multiplier, so a zero on the wire
18/// surfaces as [`Hidpp20Error::UnsupportedResponse`].
19#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
20#[cfg_attr(feature = "serde", derive(serde::Serialize))]
21pub struct PointerScaling(NonZeroU16);
22
23impl PointerScaling {
24    /// Unscaled pointer motion (1×), the spec's default.
25    pub const ONE: Self = match NonZeroU16::new(0x0100) {
26        Some(raw) => Self(raw),
27        None => panic!("0x0100 is non-zero"),
28    };
29
30    /// Wraps a raw 8.8 fixed-point value; `None` for zero.
31    #[must_use]
32    pub const fn from_raw(raw: u16) -> Option<Self> {
33        match NonZeroU16::new(raw) {
34            Some(raw) => Some(Self(raw)),
35            None => None,
36        }
37    }
38
39    /// The raw 8.8 fixed-point value.
40    #[must_use]
41    pub const fn raw(self) -> u16 {
42        self.0.get()
43    }
44
45    /// The multiplier as a float (`1.0` for [`Self::ONE`]).
46    #[must_use]
47    pub fn multiplier(self) -> f32 {
48        f32::from(self.raw()) / 256.0
49    }
50
51    fn from_payload(payload: &[u8; 16]) -> Result<Self, Hidpp20Error> {
52        Self::from_raw(u16::from_be_bytes([payload[0], payload[1]]))
53            .ok_or(Hidpp20Error::UnsupportedResponse)
54    }
55}
56
57/// Implements the `PointerMotionScaling` / `0x2205` feature.
58#[derive(Clone, Feature)]
59#[creatable(id = 0x2205, version = 0)]
60pub struct PointerMotionScalingFeature {
61    /// The endpoint this feature talks to.
62    endpoint: FeatureEndpoint,
63}
64
65impl PointerMotionScalingFeature {
66    /// Retrieves the current scaling (`GetPointerScalingValue`).
67    pub async fn get_pointer_scaling(&self) -> Result<PointerScaling, Hidpp20Error> {
68        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
69        PointerScaling::from_payload(&payload)
70    }
71
72    /// Requests a new scaling (`SetPointerScalingValue`) and returns the value
73    /// the device echoes. The spec allows firmware to clip the request, so read
74    /// the value back when the applied scaling matters.
75    pub async fn set_pointer_scaling(
76        &self,
77        scaling: PointerScaling,
78    ) -> Result<PointerScaling, Hidpp20Error> {
79        let [msb, lsb] = scaling.raw().to_be_bytes();
80        let payload = self
81            .endpoint
82            .call(1, [msb, lsb, 0x00])
83            .await?
84            .extend_payload();
85        PointerScaling::from_payload(&payload)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use std::assert_matches;
92
93    use super::PointerScaling;
94    use crate::protocol::v20::Hidpp20Error;
95
96    fn payload(msb: u8, lsb: u8) -> [u8; 16] {
97        let mut payload = [0; 16];
98        payload[0] = msb;
99        payload[1] = lsb;
100        payload
101    }
102
103    #[test]
104    fn parses_scaling_most_significant_byte_first() {
105        let scaling = PointerScaling::from_payload(&payload(0x01, 0x80)).unwrap();
106
107        assert_eq!(scaling.raw(), 0x0180);
108        assert!((scaling.multiplier() - 1.5).abs() < f32::EPSILON);
109    }
110
111    #[test]
112    fn rejects_a_zero_scaling() {
113        assert_matches!(
114            PointerScaling::from_payload(&payload(0x00, 0x00)),
115            Err(Hidpp20Error::UnsupportedResponse)
116        );
117        assert_eq!(PointerScaling::from_raw(0), None);
118    }
119
120    #[test]
121    fn one_is_the_unscaled_default() {
122        assert_eq!(PointerScaling::ONE.raw(), 0x0100);
123        assert!((PointerScaling::ONE.multiplier() - 1.0).abs() < f32::EPSILON);
124    }
125}