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}