Skip to main content

hidpp/feature/
extended_dpi.rs

1//! Implements the `ExtendedAdjustableDpi` feature (ID `0x2202`).
2//!
3//! This is the modern successor to [`AdjustableDpi`](super::adjustable_dpi)
4//! (`0x2201`). On top of a single per-sensor DPI it adds independent X/Y DPI,
5//! lift-off distance, DPI-status LED control and a DPI calibration flow, and it
6//! describes the supported DPI as fixed values and stepped ranges rather than a
7//! flat list.
8
9pub mod event;
10pub mod types;
11
12#[cfg(test)]
13mod tests;
14
15use openlogi_hidpp_derive::Feature;
16
17pub use event::{DpiCalibrationCompleted, DpiParametersChanged, ExtendedDpiEvent};
18pub use types::{
19    CalibrationType, DpiCalibrationCorrection, DpiCalibrationInfo, DpiDirection, DpiParameters,
20    DpiRange, LedHoldType, Lod, SensorCapabilities, SensorCapabilitiesInfo, SetDpiParameters,
21    ShowDpiStatus, StartDpiCalibration,
22};
23
24use self::types::{parse_dpi_list, parse_dpi_ranges, parse_lod_list, terminated_word_len};
25use crate::{
26    feature::{EventSource, FeatureEndpoint},
27    protocol::v20::Hidpp20Error,
28};
29
30/// Upper bound on the number of `getSensorDpiRanges` pages fetched before the
31/// device is considered to be returning a malformed, unterminated list.
32const MAX_RANGE_PAGES: u8 = 16;
33
34/// Implements the `ExtendedAdjustableDpi` / `0x2202` feature.
35#[derive(Feature)]
36#[creatable(id = 0x2202, version = 0)]
37pub struct ExtendedDpiFeature {
38    /// The endpoint this feature talks to.
39    endpoint: FeatureEndpoint,
40
41    /// Publishes decoded events to listeners.
42    events: EventSource<ExtendedDpiEvent>,
43}
44
45impl ExtendedDpiFeature {
46    /// Retrieves the number of motion sensors the device exposes.
47    pub async fn get_sensor_count(&self) -> Result<u8, Hidpp20Error> {
48        Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
49    }
50
51    /// Retrieves the capabilities and DPI-level count of `sensor_index`.
52    pub async fn get_sensor_capabilities(
53        &self,
54        sensor_index: u8,
55    ) -> Result<SensorCapabilitiesInfo, Hidpp20Error> {
56        let payload = self
57            .endpoint
58            .call(1, [sensor_index, 0, 0])
59            .await?
60            .extend_payload();
61        Ok(SensorCapabilitiesInfo {
62            sensor_index: payload[0],
63            dpi_level_count: payload[1],
64            capabilities: SensorCapabilities::from_bits_retain(payload[2]),
65        })
66    }
67
68    /// Retrieves the supported DPI of `sensor_index` along `direction` as a mix
69    /// of fixed values and stepped ranges.
70    ///
71    /// The device may split the description across several pages; this fetches
72    /// them until the `0x0000` end-of-list terminator is seen, then decodes the
73    /// accumulated stream. A device that never terminates the list within a
74    /// bounded number of pages yields [`Hidpp20Error::UnsupportedResponse`].
75    pub async fn get_sensor_dpi_ranges(
76        &self,
77        sensor_index: u8,
78        direction: DpiDirection,
79    ) -> Result<Vec<DpiRange>, Hidpp20Error> {
80        let mut stream = Vec::new();
81        for page in 0..MAX_RANGE_PAGES {
82            let payload = self
83                .endpoint
84                .call(2, [sensor_index, direction.into(), page])
85                .await?
86                .extend_payload();
87            // Validate the echoed addressing (sensor, direction, page) before
88            // trusting the page body, so a mismatched page cannot corrupt the
89            // accumulated stream.
90            if payload[0] != sensor_index || payload[1] != u8::from(direction) || payload[2] != page
91            {
92                return Err(Hidpp20Error::UnsupportedResponse);
93            }
94            stream.extend_from_slice(&payload[3..16]);
95            if terminated_word_len(&stream).is_some() {
96                return parse_dpi_ranges(&stream);
97            }
98        }
99        Err(Hidpp20Error::UnsupportedResponse)
100    }
101
102    /// Retrieves the current profile's DPI list for `sensor_index` along
103    /// `direction`.
104    ///
105    /// Only meaningful when the sensor supports profiles
106    /// ([`SensorCapabilities::PROFILE`]); otherwise the device returns an error.
107    pub async fn get_sensor_dpi_list(
108        &self,
109        sensor_index: u8,
110        direction: DpiDirection,
111    ) -> Result<Vec<u16>, Hidpp20Error> {
112        let payload = self
113            .endpoint
114            .call(3, [sensor_index, direction.into(), 0])
115            .await?
116            .extend_payload();
117        // Skip the echoed sensor index and direction in bytes 0 and 1.
118        Ok(parse_dpi_list(&payload[2..]))
119    }
120
121    /// Retrieves the current profile's lift-off-distance list for
122    /// `sensor_index`.
123    ///
124    /// The list length is the sensor's DPI-level count
125    /// ([`SensorCapabilitiesInfo::dpi_level_count`]), which the caller passes as
126    /// `dpi_level_count`; the device does not delimit the list.
127    pub async fn get_sensor_lod_list(
128        &self,
129        sensor_index: u8,
130        dpi_level_count: u8,
131    ) -> Result<Vec<Lod>, Hidpp20Error> {
132        let payload = self
133            .endpoint
134            .call(4, [sensor_index, 0, 0])
135            .await?
136            .extend_payload();
137        // Skip the echoed sensor index in byte 0.
138        parse_lod_list(&payload[1..], usize::from(dpi_level_count))
139    }
140
141    /// Retrieves the current and default DPI parameters of `sensor_index`.
142    pub async fn get_sensor_dpi_parameters(
143        &self,
144        sensor_index: u8,
145    ) -> Result<DpiParameters, Hidpp20Error> {
146        let payload = self
147            .endpoint
148            .call(5, [sensor_index, 0, 0])
149            .await?
150            .extend_payload();
151        Ok(DpiParameters {
152            sensor_index: payload[0],
153            dpi_x: u16::from_be_bytes([payload[1], payload[2]]),
154            default_dpi_x: u16::from_be_bytes([payload[3], payload[4]]),
155            dpi_y: u16::from_be_bytes([payload[5], payload[6]]),
156            default_dpi_y: u16::from_be_bytes([payload[7], payload[8]]),
157            lod: Lod::try_from(payload[9]).map_err(|_| Hidpp20Error::UnsupportedResponse)?,
158        })
159    }
160
161    /// Sets the DPI and lift-off distance of `sensor_index`.
162    ///
163    /// `params.dpi_y` must be `0` when the sensor has no independent Y axis.
164    pub async fn set_sensor_dpi_parameters(
165        &self,
166        sensor_index: u8,
167        params: SetDpiParameters,
168    ) -> Result<(), Hidpp20Error> {
169        let mut args = [0; 16];
170        args[0] = sensor_index;
171        args[1..3].copy_from_slice(&params.dpi_x.to_be_bytes());
172        args[3..5].copy_from_slice(&params.dpi_y.to_be_bytes());
173        args[5] = params.lod.into();
174        self.endpoint.call_long(6, args).await?;
175        Ok(())
176    }
177
178    /// Asks the device to show `params.dpi_level` on its DPI status LED.
179    ///
180    /// Valid only while the device is in host mode.
181    pub async fn show_sensor_dpi_status(
182        &self,
183        sensor_index: u8,
184        params: ShowDpiStatus,
185    ) -> Result<(), Hidpp20Error> {
186        let mut args = [0; 16];
187        args[..4].copy_from_slice(&[
188            sensor_index,
189            params.dpi_level,
190            params.led_hold_type.into(),
191            params.button_num,
192        ]);
193        self.endpoint.call_long(7, args).await?;
194        Ok(())
195    }
196
197    /// Retrieves the reference information needed to start a calibration of
198    /// `sensor_index`.
199    pub async fn get_dpi_calibration_info(
200        &self,
201        sensor_index: u8,
202    ) -> Result<DpiCalibrationInfo, Hidpp20Error> {
203        let payload = self
204            .endpoint
205            .call(8, [sensor_index, 0, 0])
206            .await?
207            .extend_payload();
208        Ok(DpiCalibrationInfo {
209            sensor_index: payload[0],
210            mouse_width: payload[1],
211            mouse_length: u16::from_be_bytes([payload[2], payload[3]]),
212            calib_dpi_x: u16::from_be_bytes([payload[4], payload[5]]),
213            calib_dpi_y: u16::from_be_bytes([payload[6], payload[7]]),
214        })
215    }
216
217    /// Starts a DPI calibration of `sensor_index`.
218    ///
219    /// Requires [`SensorCapabilities::CALIBRATION`]. The device reports the
220    /// outcome through an [`ExtendedDpiEvent::CalibrationCompleted`] event; for a
221    /// [`CalibrationType::Software`] calibration the result is then applied with
222    /// [`Self::set_dpi_calibration`].
223    pub async fn start_dpi_calibration(
224        &self,
225        sensor_index: u8,
226        params: StartDpiCalibration,
227    ) -> Result<(), Hidpp20Error> {
228        let [count_hi, count_lo] = params.expected_count.to_be_bytes();
229        let mut args = [0; 16];
230        args[..8].copy_from_slice(&[
231            sensor_index,
232            params.direction.into(),
233            count_hi,
234            count_lo,
235            params.calib_type.into(),
236            params.start_timeout,
237            params.hw_process_timeout,
238            params.sw_process_timeout,
239        ]);
240        self.endpoint.call_long(9, args).await?;
241        Ok(())
242    }
243
244    /// Applies a calibration correction to `sensor_index` along `direction`.
245    ///
246    /// Allowed only while a calibration started by [`Self::start_dpi_calibration`]
247    /// is in progress (or to revert, see [`DpiCalibrationCorrection`]).
248    pub async fn set_dpi_calibration(
249        &self,
250        sensor_index: u8,
251        direction: DpiDirection,
252        correction: DpiCalibrationCorrection,
253    ) -> Result<(), Hidpp20Error> {
254        let [cor_hi, cor_lo] = correction.to_wire()?.to_be_bytes();
255        let mut args = [0; 16];
256        args[..4].copy_from_slice(&[sensor_index, direction.into(), cor_hi, cor_lo]);
257        self.endpoint.call_long(10, args).await?;
258        Ok(())
259    }
260}