Skip to main content

hidpp/feature/extended_dpi/
mod.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 std::sync::Arc;
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    channel::{HidppChannel, MessageListenerGuard},
27    event::EventEmitter,
28    feature::{CreatableFeature, EmittingFeature, Feature, FeatureEndpoint, event_payload},
29    protocol::v20::Hidpp20Error,
30};
31
32/// Upper bound on the number of `getSensorDpiRanges` pages fetched before the
33/// device is considered to be returning a malformed, unterminated list.
34const MAX_RANGE_PAGES: u8 = 16;
35
36/// Implements the `ExtendedAdjustableDpi` / `0x2202` feature.
37pub struct ExtendedDpiFeature {
38    /// The endpoint this feature talks to.
39    endpoint: FeatureEndpoint,
40
41    /// The emitter used to publish decoded events.
42    emitter: Arc<EventEmitter<ExtendedDpiEvent>>,
43
44    /// Removes the message listener when the feature is dropped.
45    _msg_listener: MessageListenerGuard,
46}
47
48impl CreatableFeature for ExtendedDpiFeature {
49    const ID: u16 = 0x2202;
50    const STARTING_VERSION: u8 = 0;
51
52    fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
53        let emitter = Arc::new(EventEmitter::new());
54
55        let listener = chan.add_msg_listener_guarded({
56            let emitter = Arc::clone(&emitter);
57
58            move |raw, matched| {
59                let Some((func, payload)) =
60                    event_payload(raw, matched, device_index, feature_index)
61                else {
62                    return;
63                };
64
65                if let Some(event) = event::decode_event(func.to_lo(), &payload) {
66                    emitter.emit(event);
67                }
68            }
69        });
70
71        Self {
72            endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
73            emitter,
74            _msg_listener: listener,
75        }
76    }
77}
78
79impl Feature for ExtendedDpiFeature {}
80
81impl EmittingFeature<ExtendedDpiEvent> for ExtendedDpiFeature {
82    fn listen(&self) -> async_channel::Receiver<ExtendedDpiEvent> {
83        self.emitter.create_receiver()
84    }
85}
86
87impl ExtendedDpiFeature {
88    /// Retrieves the number of motion sensors the device exposes.
89    pub async fn get_sensor_count(&self) -> Result<u8, Hidpp20Error> {
90        Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
91    }
92
93    /// Retrieves the capabilities and DPI-level count of `sensor_index`.
94    pub async fn get_sensor_capabilities(
95        &self,
96        sensor_index: u8,
97    ) -> Result<SensorCapabilitiesInfo, Hidpp20Error> {
98        let payload = self
99            .endpoint
100            .call(1, [sensor_index, 0, 0])
101            .await?
102            .extend_payload();
103        Ok(SensorCapabilitiesInfo {
104            sensor_index: payload[0],
105            dpi_level_count: payload[1],
106            capabilities: SensorCapabilities::from_bits_retain(payload[2]),
107        })
108    }
109
110    /// Retrieves the supported DPI of `sensor_index` along `direction` as a mix
111    /// of fixed values and stepped ranges.
112    ///
113    /// The device may split the description across several pages; this fetches
114    /// them until the `0x0000` end-of-list terminator is seen, then decodes the
115    /// accumulated stream. A device that never terminates the list within a
116    /// bounded number of pages yields [`Hidpp20Error::UnsupportedResponse`].
117    pub async fn get_sensor_dpi_ranges(
118        &self,
119        sensor_index: u8,
120        direction: DpiDirection,
121    ) -> Result<Vec<DpiRange>, Hidpp20Error> {
122        let mut stream = Vec::new();
123        for page in 0..MAX_RANGE_PAGES {
124            let payload = self
125                .endpoint
126                .call(2, [sensor_index, direction.into(), page])
127                .await?
128                .extend_payload();
129            // Validate the echoed addressing (sensor, direction, page) before
130            // trusting the page body, so a mismatched page cannot corrupt the
131            // accumulated stream.
132            if payload[0] != sensor_index || payload[1] != u8::from(direction) || payload[2] != page
133            {
134                return Err(Hidpp20Error::UnsupportedResponse);
135            }
136            stream.extend_from_slice(&payload[3..16]);
137            if terminated_word_len(&stream).is_some() {
138                return parse_dpi_ranges(&stream);
139            }
140        }
141        Err(Hidpp20Error::UnsupportedResponse)
142    }
143
144    /// Retrieves the current profile's DPI list for `sensor_index` along
145    /// `direction`.
146    ///
147    /// Only meaningful when the sensor supports profiles
148    /// ([`SensorCapabilities::PROFILE`]); otherwise the device returns an error.
149    pub async fn get_sensor_dpi_list(
150        &self,
151        sensor_index: u8,
152        direction: DpiDirection,
153    ) -> Result<Vec<u16>, Hidpp20Error> {
154        let payload = self
155            .endpoint
156            .call(3, [sensor_index, direction.into(), 0])
157            .await?
158            .extend_payload();
159        // Skip the echoed sensor index and direction in bytes 0 and 1.
160        parse_dpi_list(&payload[2..])
161    }
162
163    /// Retrieves the current profile's lift-off-distance list for
164    /// `sensor_index`.
165    ///
166    /// The list length is the sensor's DPI-level count
167    /// ([`SensorCapabilitiesInfo::dpi_level_count`]), which the caller passes as
168    /// `dpi_level_count`; the device does not delimit the list.
169    pub async fn get_sensor_lod_list(
170        &self,
171        sensor_index: u8,
172        dpi_level_count: u8,
173    ) -> Result<Vec<Lod>, Hidpp20Error> {
174        let payload = self
175            .endpoint
176            .call(4, [sensor_index, 0, 0])
177            .await?
178            .extend_payload();
179        // Skip the echoed sensor index in byte 0.
180        parse_lod_list(&payload[1..], usize::from(dpi_level_count))
181    }
182
183    /// Retrieves the current and default DPI parameters of `sensor_index`.
184    pub async fn get_sensor_dpi_parameters(
185        &self,
186        sensor_index: u8,
187    ) -> Result<DpiParameters, Hidpp20Error> {
188        let payload = self
189            .endpoint
190            .call(5, [sensor_index, 0, 0])
191            .await?
192            .extend_payload();
193        Ok(DpiParameters {
194            sensor_index: payload[0],
195            dpi_x: u16::from_be_bytes([payload[1], payload[2]]),
196            default_dpi_x: u16::from_be_bytes([payload[3], payload[4]]),
197            dpi_y: u16::from_be_bytes([payload[5], payload[6]]),
198            default_dpi_y: u16::from_be_bytes([payload[7], payload[8]]),
199            lod: Lod::try_from(payload[9]).map_err(|_| Hidpp20Error::UnsupportedResponse)?,
200        })
201    }
202
203    /// Sets the DPI and lift-off distance of `sensor_index`.
204    ///
205    /// `params.dpi_y` must be `0` when the sensor has no independent Y axis.
206    pub async fn set_sensor_dpi_parameters(
207        &self,
208        sensor_index: u8,
209        params: SetDpiParameters,
210    ) -> Result<(), Hidpp20Error> {
211        let [dpi_x_hi, dpi_x_lo] = params.dpi_x.to_be_bytes();
212        let [dpi_y_hi, dpi_y_lo] = params.dpi_y.to_be_bytes();
213        let mut args = [0; 16];
214        args[..6].copy_from_slice(&[
215            sensor_index,
216            dpi_x_hi,
217            dpi_x_lo,
218            dpi_y_hi,
219            dpi_y_lo,
220            params.lod.into(),
221        ]);
222        self.endpoint.call_long(6, args).await?;
223        Ok(())
224    }
225
226    /// Asks the device to show `params.dpi_level` on its DPI status LED.
227    ///
228    /// Valid only while the device is in host mode.
229    pub async fn show_sensor_dpi_status(
230        &self,
231        sensor_index: u8,
232        params: ShowDpiStatus,
233    ) -> Result<(), Hidpp20Error> {
234        let mut args = [0; 16];
235        args[..4].copy_from_slice(&[
236            sensor_index,
237            params.dpi_level,
238            params.led_hold_type.into(),
239            params.button_num,
240        ]);
241        self.endpoint.call_long(7, args).await?;
242        Ok(())
243    }
244
245    /// Retrieves the reference information needed to start a calibration of
246    /// `sensor_index`.
247    pub async fn get_dpi_calibration_info(
248        &self,
249        sensor_index: u8,
250    ) -> Result<DpiCalibrationInfo, Hidpp20Error> {
251        let payload = self
252            .endpoint
253            .call(8, [sensor_index, 0, 0])
254            .await?
255            .extend_payload();
256        Ok(DpiCalibrationInfo {
257            sensor_index: payload[0],
258            mouse_width: payload[1],
259            mouse_length: u16::from_be_bytes([payload[2], payload[3]]),
260            calib_dpi_x: u16::from_be_bytes([payload[4], payload[5]]),
261            calib_dpi_y: u16::from_be_bytes([payload[6], payload[7]]),
262        })
263    }
264
265    /// Starts a DPI calibration of `sensor_index`.
266    ///
267    /// Requires [`SensorCapabilities::CALIBRATION`]. The device reports the
268    /// outcome through an [`ExtendedDpiEvent::CalibrationCompleted`] event; for a
269    /// [`CalibrationType::Software`] calibration the result is then applied with
270    /// [`Self::set_dpi_calibration`].
271    pub async fn start_dpi_calibration(
272        &self,
273        sensor_index: u8,
274        params: StartDpiCalibration,
275    ) -> Result<(), Hidpp20Error> {
276        let [count_hi, count_lo] = params.expected_count.to_be_bytes();
277        let mut args = [0; 16];
278        args[..8].copy_from_slice(&[
279            sensor_index,
280            params.direction.into(),
281            count_hi,
282            count_lo,
283            params.calib_type.into(),
284            params.start_timeout,
285            params.hw_process_timeout,
286            params.sw_process_timeout,
287        ]);
288        self.endpoint.call_long(9, args).await?;
289        Ok(())
290    }
291
292    /// Applies a calibration correction to `sensor_index` along `direction`.
293    ///
294    /// Allowed only while a calibration started by [`Self::start_dpi_calibration`]
295    /// is in progress (or to revert, see [`DpiCalibrationCorrection`]).
296    pub async fn set_dpi_calibration(
297        &self,
298        sensor_index: u8,
299        direction: DpiDirection,
300        correction: DpiCalibrationCorrection,
301    ) -> Result<(), Hidpp20Error> {
302        let [cor_hi, cor_lo] = correction.to_wire()?.to_be_bytes();
303        let mut args = [0; 16];
304        args[..4].copy_from_slice(&[sensor_index, direction.into(), cor_hi, cor_lo]);
305        self.endpoint.call_long(10, args).await?;
306        Ok(())
307    }
308}