Skip to main content

hidpp/feature/extended_dpi/
types.rs

1//! Domain types and payload parsers for `ExtendedAdjustableDpi` (`0x2202`).
2
3use num_enum::{IntoPrimitive, TryFromPrimitive};
4
5use crate::protocol::v20::{ErrorType, Hidpp20Error};
6
7/// The axis a DPI value or calibration applies to.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10#[non_exhaustive]
11#[repr(u8)]
12pub enum DpiDirection {
13    /// Horizontal (X) axis.
14    X = 0,
15    /// Vertical (Y) axis.
16    Y = 1,
17}
18
19/// A sensor's lift-off distance setting.
20///
21/// The lift-off distance is the height above the surface at which the sensor
22/// stops tracking motion.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize))]
25#[non_exhaustive]
26#[repr(u8)]
27pub enum Lod {
28    /// Lift-off distance control is not supported.
29    NotSupported = 0,
30    /// Low lift-off distance.
31    Low = 1,
32    /// Medium lift-off distance.
33    Medium = 2,
34    /// High lift-off distance.
35    High = 3,
36}
37
38/// How the device holds the DPI status LED after a
39/// [`ExtendedDpiFeature::show_sensor_dpi_status`] request.
40///
41/// [`ExtendedDpiFeature::show_sensor_dpi_status`]:
42/// super::ExtendedDpiFeature::show_sensor_dpi_status
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize))]
45#[non_exhaustive]
46#[repr(u8)]
47pub enum LedHoldType {
48    /// Turn the LED off once a device-defined timeout elapses.
49    TimerBased = 0,
50    /// Turn the LED off once a device-defined event completes (e.g. releasing a
51    /// DPI-shift button).
52    EventBased = 1,
53    /// Turn the LED on under software control.
54    SwControlOn = 2,
55    /// Turn the LED off under software control.
56    SwControlOff = 3,
57}
58
59/// Where a DPI calibration is computed.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
61#[cfg_attr(feature = "serde", derive(serde::Serialize))]
62#[non_exhaustive]
63#[repr(u8)]
64pub enum CalibrationType {
65    /// Calibration is computed by the sensor firmware / hardware.
66    Hardware = 0,
67    /// Calibration is computed by host software.
68    Software = 1,
69}
70
71bitflags::bitflags! {
72    /// Per-sensor capabilities reported by
73    /// [`ExtendedDpiFeature::get_sensor_capabilities`].
74    ///
75    /// [`ExtendedDpiFeature::get_sensor_capabilities`]:
76    /// super::ExtendedDpiFeature::get_sensor_capabilities
77    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78    #[cfg_attr(feature = "serde", derive(serde::Serialize))]
79    pub struct SensorCapabilities: u8 {
80        /// The sensor supports an independent Y-axis DPI.
81        const DPI_Y = 1 << 0;
82        /// The sensor supports lift-off distance control.
83        const LOD = 1 << 1;
84        /// The sensor supports DPI calibration.
85        const CALIBRATION = 1 << 2;
86        /// The sensor supports DPI profiles.
87        const PROFILE = 1 << 3;
88    }
89}
90
91/// A sensor's capabilities and DPI-level count.
92#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
93#[cfg_attr(feature = "serde", derive(serde::Serialize))]
94#[non_exhaustive]
95pub struct SensorCapabilitiesInfo {
96    /// Index of the sensor the capabilities belong to.
97    pub sensor_index: u8,
98    /// Number of selectable DPI levels, or `0` if the device does not manage DPI
99    /// levels.
100    pub dpi_level_count: u8,
101    /// Supported capabilities.
102    pub capabilities: SensorCapabilities,
103}
104
105/// One entry of a sensor's supported-DPI description.
106///
107/// Returned by [`ExtendedDpiFeature::get_sensor_dpi_ranges`], which can mix
108/// fixed values and stepped ranges. A stepped range's endpoints are inclusive
109/// and adjacent ranges may share an endpoint (the device reports the high value
110/// of one range as the low value of the next).
111///
112/// [`ExtendedDpiFeature::get_sensor_dpi_ranges`]:
113/// super::ExtendedDpiFeature::get_sensor_dpi_ranges
114#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
115#[cfg_attr(feature = "serde", derive(serde::Serialize))]
116pub enum DpiRange {
117    /// A single selectable DPI value.
118    Fixed(u16),
119    /// A contiguous range of selectable DPI values from `from` to `to`
120    /// (inclusive) in increments of `step`.
121    Stepped {
122        /// Lowest selectable DPI in the range (inclusive).
123        from: u16,
124        /// Highest selectable DPI in the range (inclusive).
125        to: u16,
126        /// DPI increment between adjacent selectable values.
127        step: u16,
128    },
129}
130
131/// Current and default DPI parameters of a sensor, returned by
132/// [`ExtendedDpiFeature::get_sensor_dpi_parameters`].
133///
134/// `dpi_y` and `default_dpi_y` are `0` when the sensor does not support an
135/// independent Y axis (see [`SensorCapabilities::DPI_Y`]).
136///
137/// [`ExtendedDpiFeature::get_sensor_dpi_parameters`]:
138/// super::ExtendedDpiFeature::get_sensor_dpi_parameters
139#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize))]
141#[non_exhaustive]
142pub struct DpiParameters {
143    /// Index of the sensor.
144    pub sensor_index: u8,
145    /// Current X-axis DPI.
146    pub dpi_x: u16,
147    /// Default X-axis DPI.
148    pub default_dpi_x: u16,
149    /// Current Y-axis DPI, or `0` when unsupported.
150    pub dpi_y: u16,
151    /// Default Y-axis DPI, or `0` when unsupported.
152    pub default_dpi_y: u16,
153    /// Current lift-off distance.
154    pub lod: Lod,
155}
156
157/// DPI parameters to apply with
158/// [`ExtendedDpiFeature::set_sensor_dpi_parameters`].
159///
160/// [`ExtendedDpiFeature::set_sensor_dpi_parameters`]:
161/// super::ExtendedDpiFeature::set_sensor_dpi_parameters
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
163#[cfg_attr(feature = "serde", derive(serde::Serialize))]
164pub struct SetDpiParameters {
165    /// New X-axis DPI (`1..=57343`).
166    pub dpi_x: u16,
167    /// New Y-axis DPI (`1..=57343`), or `0` when the sensor has no independent Y
168    /// axis.
169    pub dpi_y: u16,
170    /// New lift-off distance.
171    pub lod: Lod,
172}
173
174/// Parameters for [`ExtendedDpiFeature::show_sensor_dpi_status`].
175///
176/// [`ExtendedDpiFeature::show_sensor_dpi_status`]:
177/// super::ExtendedDpiFeature::show_sensor_dpi_status
178#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize))]
180pub struct ShowDpiStatus {
181    /// DPI level to display (`1..=dpi_level_count`).
182    pub dpi_level: u8,
183    /// How the device holds the DPI status LED.
184    pub led_hold_type: LedHoldType,
185    /// HID button number that initiated the DPI change (starts at `1`).
186    pub button_num: u8,
187}
188
189/// Calibration reference information returned by
190/// [`ExtendedDpiFeature::get_dpi_calibration_info`].
191///
192/// [`ExtendedDpiFeature::get_dpi_calibration_info`]:
193/// super::ExtendedDpiFeature::get_dpi_calibration_info
194#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
195#[cfg_attr(feature = "serde", derive(serde::Serialize))]
196#[non_exhaustive]
197pub struct DpiCalibrationInfo {
198    /// Index of the sensor.
199    pub sensor_index: u8,
200    /// Device width in millimetres.
201    pub mouse_width: u8,
202    /// Device length in millimetres.
203    pub mouse_length: u16,
204    /// X-axis DPI configured for calibration.
205    pub calib_dpi_x: u16,
206    /// Y-axis DPI configured for calibration, or `0` when unsupported.
207    pub calib_dpi_y: u16,
208}
209
210/// Parameters for [`ExtendedDpiFeature::start_dpi_calibration`].
211///
212/// [`ExtendedDpiFeature::start_dpi_calibration`]:
213/// super::ExtendedDpiFeature::start_dpi_calibration
214#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize))]
216pub struct StartDpiCalibration {
217    /// Axis to calibrate.
218    pub direction: DpiDirection,
219    /// Expected pixel count for the calibration movement (ignored for
220    /// [`CalibrationType::Software`]).
221    pub expected_count: u16,
222    /// Where the calibration is computed.
223    pub calib_type: CalibrationType,
224    /// Timeout in seconds for the calibration to start (`<= 60`).
225    pub start_timeout: u8,
226    /// Timeout in seconds for the hardware calibration process (`<= 60`).
227    pub hw_process_timeout: u8,
228    /// Timeout in seconds for the software calibration process (`<= 60`).
229    pub sw_process_timeout: u8,
230}
231
232/// A DPI calibration correction to apply with
233/// [`ExtendedDpiFeature::set_dpi_calibration`].
234///
235/// [`ExtendedDpiFeature::set_dpi_calibration`]:
236/// super::ExtendedDpiFeature::set_dpi_calibration
237#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
238#[cfg_attr(feature = "serde", derive(serde::Serialize))]
239pub enum DpiCalibrationCorrection {
240    /// Scale the sensor resolution by `(1024 + value) / 1024`. Valid values are
241    /// `-1023..=1023`; `0` reverts to the out-of-box setting, like
242    /// [`DpiCalibrationCorrection::RevertToOob`].
243    Adjust(i16),
244    /// Revert to the out-of-box (OOB) profile setting (wire value `0x0000`).
245    RevertToOob,
246    /// Revert to the setting stored in the current profile (wire value
247    /// `0x8000`).
248    RevertToProfile,
249}
250
251impl DpiCalibrationCorrection {
252    /// The signed 16-bit wire value for this correction.
253    pub(super) fn to_wire(self) -> Result<i16, Hidpp20Error> {
254        match self {
255            // 0x8000 as a signed 16-bit integer.
256            DpiCalibrationCorrection::RevertToProfile => Ok(i16::MIN),
257            DpiCalibrationCorrection::RevertToOob => Ok(0),
258            DpiCalibrationCorrection::Adjust(value) => {
259                // `i16::MIN` is the `0x8000` "revert to profile" sentinel, not a
260                // correction; an out-of-range adjustment would silently collide
261                // with it instead of eliciting the device's `INVALID_ARGUMENT`.
262                if !(-1023..=1023).contains(&value) {
263                    return Err(Hidpp20Error::Feature(ErrorType::InvalidArgument));
264                }
265                Ok(value)
266            }
267        }
268    }
269}
270
271/// Highest bit pattern that marks a "hyphen" (range step) word; values at or
272/// above `0xe000` are not literal DPI values.
273const HYPHEN_TAG: u16 = 0b111 << 13;
274
275/// Reads `stream` as a sequence of big-endian 16-bit words, returning their
276/// count up to (but excluding) the first `0x0000` end-of-list terminator.
277///
278/// Returns `None` when no terminator is present in the complete words available,
279/// signalling that another `getSensorDpiRanges` page is required.
280pub(super) fn terminated_word_len(stream: &[u8]) -> Option<usize> {
281    let mut offset = 0;
282    while offset + 1 < stream.len() {
283        if u16::from_be_bytes([stream[offset], stream[offset + 1]]) == 0 {
284            return Some(offset);
285        }
286        offset += 2;
287    }
288    None
289}
290
291/// Parses an accumulated `getSensorDpiRanges` byte stream into [`DpiRange`]s.
292///
293/// `stream` is the concatenation of every page's range bytes. Parsing stops at
294/// the first `0x0000` terminator word. Each range is encoded as big-endian
295/// 16-bit words where the top three bits select the meaning: `0b000..=0b110`
296/// tags a literal DPI value and `0b111` tags a "hyphen" carrying the step of the
297/// range whose endpoints are the surrounding literal values.
298pub(super) fn parse_dpi_ranges(stream: &[u8]) -> Result<Vec<DpiRange>, Hidpp20Error> {
299    let len = terminated_word_len(stream).ok_or(Hidpp20Error::UnsupportedResponse)?;
300    let word = |offset: usize| u16::from_be_bytes([stream[offset], stream[offset + 1]]);
301
302    let mut ranges = Vec::new();
303    // The most recent literal value, and whether it was already emitted as a
304    // range endpoint (so it is not also emitted as a standalone fixed value).
305    let mut pending: Option<u16> = None;
306    let mut pending_is_range_end = false;
307    let mut offset = 0;
308
309    while offset < len {
310        let value = word(offset);
311        if value >= HYPHEN_TAG {
312            // A hyphen carries the step and consumes the following literal as the
313            // range's high endpoint.
314            let step = value & !HYPHEN_TAG;
315            let from = pending.ok_or(Hidpp20Error::UnsupportedResponse)?;
316            if step == 0 || offset + 3 >= len {
317                return Err(Hidpp20Error::UnsupportedResponse);
318            }
319            let to = word(offset + 2);
320            if to >= HYPHEN_TAG || to < from {
321                return Err(Hidpp20Error::UnsupportedResponse);
322            }
323            ranges.push(DpiRange::Stepped { from, to, step });
324            pending = Some(to);
325            pending_is_range_end = true;
326            offset += 4;
327        } else {
328            // A literal value: flush the previous standalone literal first.
329            if let Some(previous) = pending
330                && !pending_is_range_end
331            {
332                ranges.push(DpiRange::Fixed(previous));
333            }
334            pending = Some(value);
335            pending_is_range_end = false;
336            offset += 2;
337        }
338    }
339
340    if let Some(previous) = pending
341        && !pending_is_range_end
342    {
343        ranges.push(DpiRange::Fixed(previous));
344    }
345
346    if ranges.is_empty() {
347        return Err(Hidpp20Error::UnsupportedResponse);
348    }
349    Ok(ranges)
350}
351
352/// Parses a `getSensorDpiList` payload (after the echoed sensor index and
353/// direction) into explicit DPI values, stopping at the `0x0000` terminator.
354pub(super) fn parse_dpi_list(bytes: &[u8]) -> Result<Vec<u16>, Hidpp20Error> {
355    let mut values = Vec::new();
356    let mut offset = 0;
357    while offset + 1 < bytes.len() {
358        let value = u16::from_be_bytes([bytes[offset], bytes[offset + 1]]);
359        if value == 0 {
360            break;
361        }
362        values.push(value);
363        offset += 2;
364    }
365    Ok(values)
366}
367
368/// Parses the first `count` lift-off-distance entries of a `getSensorLodList`
369/// payload (after the echoed sensor index).
370pub(super) fn parse_lod_list(bytes: &[u8], count: usize) -> Result<Vec<Lod>, Hidpp20Error> {
371    if count > bytes.len() {
372        return Err(Hidpp20Error::UnsupportedResponse);
373    }
374    bytes[..count]
375        .iter()
376        .map(|&raw| Lod::try_from(raw).map_err(|_| Hidpp20Error::UnsupportedResponse))
377        .collect()
378}