openlogi-hidpp 0.7.1

OpenLogi's hard fork of the `hidpp` crate (Logitech HID++ protocol).
Documentation
//! Implements the `ExtendedAdjustableDpi` feature (ID `0x2202`).
//!
//! This is the modern successor to [`AdjustableDpi`](super::adjustable_dpi)
//! (`0x2201`). On top of a single per-sensor DPI it adds independent X/Y DPI,
//! lift-off distance, DPI-status LED control and a DPI calibration flow, and it
//! describes the supported DPI as fixed values and stepped ranges rather than a
//! flat list.

pub mod event;
pub mod types;

#[cfg(test)]
mod tests;

use openlogi_hidpp_derive::Feature;

pub use event::{DpiCalibrationCompleted, DpiParametersChanged, ExtendedDpiEvent};
pub use types::{
    CalibrationType, DpiCalibrationCorrection, DpiCalibrationInfo, DpiDirection, DpiParameters,
    DpiRange, LedHoldType, Lod, SensorCapabilities, SensorCapabilitiesInfo, SetDpiParameters,
    ShowDpiStatus, StartDpiCalibration,
};

use self::types::{parse_dpi_list, parse_dpi_ranges, parse_lod_list, terminated_word_len};
use crate::{
    feature::{EventSource, FeatureEndpoint},
    protocol::v20::Hidpp20Error,
};

/// Upper bound on the number of `getSensorDpiRanges` pages fetched before the
/// device is considered to be returning a malformed, unterminated list.
const MAX_RANGE_PAGES: u8 = 16;

/// Implements the `ExtendedAdjustableDpi` / `0x2202` feature.
#[derive(Feature)]
#[creatable(id = 0x2202, version = 0)]
pub struct ExtendedDpiFeature {
    /// The endpoint this feature talks to.
    endpoint: FeatureEndpoint,

    /// Publishes decoded events to listeners.
    events: EventSource<ExtendedDpiEvent>,
}

impl ExtendedDpiFeature {
    /// Retrieves the number of motion sensors the device exposes.
    pub async fn get_sensor_count(&self) -> Result<u8, Hidpp20Error> {
        Ok(self.endpoint.call(0, [0; 3]).await?.extend_payload()[0])
    }

    /// Retrieves the capabilities and DPI-level count of `sensor_index`.
    pub async fn get_sensor_capabilities(
        &self,
        sensor_index: u8,
    ) -> Result<SensorCapabilitiesInfo, Hidpp20Error> {
        let payload = self
            .endpoint
            .call(1, [sensor_index, 0, 0])
            .await?
            .extend_payload();
        Ok(SensorCapabilitiesInfo {
            sensor_index: payload[0],
            dpi_level_count: payload[1],
            capabilities: SensorCapabilities::from_bits_retain(payload[2]),
        })
    }

    /// Retrieves the supported DPI of `sensor_index` along `direction` as a mix
    /// of fixed values and stepped ranges.
    ///
    /// The device may split the description across several pages; this fetches
    /// them until the `0x0000` end-of-list terminator is seen, then decodes the
    /// accumulated stream. A device that never terminates the list within a
    /// bounded number of pages yields [`Hidpp20Error::UnsupportedResponse`].
    pub async fn get_sensor_dpi_ranges(
        &self,
        sensor_index: u8,
        direction: DpiDirection,
    ) -> Result<Vec<DpiRange>, Hidpp20Error> {
        let mut stream = Vec::new();
        for page in 0..MAX_RANGE_PAGES {
            let payload = self
                .endpoint
                .call(2, [sensor_index, direction.into(), page])
                .await?
                .extend_payload();
            // Validate the echoed addressing (sensor, direction, page) before
            // trusting the page body, so a mismatched page cannot corrupt the
            // accumulated stream.
            if payload[0] != sensor_index || payload[1] != u8::from(direction) || payload[2] != page
            {
                return Err(Hidpp20Error::UnsupportedResponse);
            }
            stream.extend_from_slice(&payload[3..16]);
            if terminated_word_len(&stream).is_some() {
                return parse_dpi_ranges(&stream);
            }
        }
        Err(Hidpp20Error::UnsupportedResponse)
    }

    /// Retrieves the current profile's DPI list for `sensor_index` along
    /// `direction`.
    ///
    /// Only meaningful when the sensor supports profiles
    /// ([`SensorCapabilities::PROFILE`]); otherwise the device returns an error.
    pub async fn get_sensor_dpi_list(
        &self,
        sensor_index: u8,
        direction: DpiDirection,
    ) -> Result<Vec<u16>, Hidpp20Error> {
        let payload = self
            .endpoint
            .call(3, [sensor_index, direction.into(), 0])
            .await?
            .extend_payload();
        // Skip the echoed sensor index and direction in bytes 0 and 1.
        Ok(parse_dpi_list(&payload[2..]))
    }

    /// Retrieves the current profile's lift-off-distance list for
    /// `sensor_index`.
    ///
    /// The list length is the sensor's DPI-level count
    /// ([`SensorCapabilitiesInfo::dpi_level_count`]), which the caller passes as
    /// `dpi_level_count`; the device does not delimit the list.
    pub async fn get_sensor_lod_list(
        &self,
        sensor_index: u8,
        dpi_level_count: u8,
    ) -> Result<Vec<Lod>, Hidpp20Error> {
        let payload = self
            .endpoint
            .call(4, [sensor_index, 0, 0])
            .await?
            .extend_payload();
        // Skip the echoed sensor index in byte 0.
        parse_lod_list(&payload[1..], usize::from(dpi_level_count))
    }

    /// Retrieves the current and default DPI parameters of `sensor_index`.
    pub async fn get_sensor_dpi_parameters(
        &self,
        sensor_index: u8,
    ) -> Result<DpiParameters, Hidpp20Error> {
        let payload = self
            .endpoint
            .call(5, [sensor_index, 0, 0])
            .await?
            .extend_payload();
        Ok(DpiParameters {
            sensor_index: payload[0],
            dpi_x: u16::from_be_bytes([payload[1], payload[2]]),
            default_dpi_x: u16::from_be_bytes([payload[3], payload[4]]),
            dpi_y: u16::from_be_bytes([payload[5], payload[6]]),
            default_dpi_y: u16::from_be_bytes([payload[7], payload[8]]),
            lod: Lod::try_from(payload[9]).map_err(|_| Hidpp20Error::UnsupportedResponse)?,
        })
    }

    /// Sets the DPI and lift-off distance of `sensor_index`.
    ///
    /// `params.dpi_y` must be `0` when the sensor has no independent Y axis.
    pub async fn set_sensor_dpi_parameters(
        &self,
        sensor_index: u8,
        params: SetDpiParameters,
    ) -> Result<(), Hidpp20Error> {
        let mut args = [0; 16];
        args[0] = sensor_index;
        args[1..3].copy_from_slice(&params.dpi_x.to_be_bytes());
        args[3..5].copy_from_slice(&params.dpi_y.to_be_bytes());
        args[5] = params.lod.into();
        self.endpoint.call_long(6, args).await?;
        Ok(())
    }

    /// Asks the device to show `params.dpi_level` on its DPI status LED.
    ///
    /// Valid only while the device is in host mode.
    pub async fn show_sensor_dpi_status(
        &self,
        sensor_index: u8,
        params: ShowDpiStatus,
    ) -> Result<(), Hidpp20Error> {
        let mut args = [0; 16];
        args[..4].copy_from_slice(&[
            sensor_index,
            params.dpi_level,
            params.led_hold_type.into(),
            params.button_num,
        ]);
        self.endpoint.call_long(7, args).await?;
        Ok(())
    }

    /// Retrieves the reference information needed to start a calibration of
    /// `sensor_index`.
    pub async fn get_dpi_calibration_info(
        &self,
        sensor_index: u8,
    ) -> Result<DpiCalibrationInfo, Hidpp20Error> {
        let payload = self
            .endpoint
            .call(8, [sensor_index, 0, 0])
            .await?
            .extend_payload();
        Ok(DpiCalibrationInfo {
            sensor_index: payload[0],
            mouse_width: payload[1],
            mouse_length: u16::from_be_bytes([payload[2], payload[3]]),
            calib_dpi_x: u16::from_be_bytes([payload[4], payload[5]]),
            calib_dpi_y: u16::from_be_bytes([payload[6], payload[7]]),
        })
    }

    /// Starts a DPI calibration of `sensor_index`.
    ///
    /// Requires [`SensorCapabilities::CALIBRATION`]. The device reports the
    /// outcome through an [`ExtendedDpiEvent::CalibrationCompleted`] event; for a
    /// [`CalibrationType::Software`] calibration the result is then applied with
    /// [`Self::set_dpi_calibration`].
    pub async fn start_dpi_calibration(
        &self,
        sensor_index: u8,
        params: StartDpiCalibration,
    ) -> Result<(), Hidpp20Error> {
        let [count_hi, count_lo] = params.expected_count.to_be_bytes();
        let mut args = [0; 16];
        args[..8].copy_from_slice(&[
            sensor_index,
            params.direction.into(),
            count_hi,
            count_lo,
            params.calib_type.into(),
            params.start_timeout,
            params.hw_process_timeout,
            params.sw_process_timeout,
        ]);
        self.endpoint.call_long(9, args).await?;
        Ok(())
    }

    /// Applies a calibration correction to `sensor_index` along `direction`.
    ///
    /// Allowed only while a calibration started by [`Self::start_dpi_calibration`]
    /// is in progress (or to revert, see [`DpiCalibrationCorrection`]).
    pub async fn set_dpi_calibration(
        &self,
        sensor_index: u8,
        direction: DpiDirection,
        correction: DpiCalibrationCorrection,
    ) -> Result<(), Hidpp20Error> {
        let [cor_hi, cor_lo] = correction.to_wire()?.to_be_bytes();
        let mut args = [0; 16];
        args[..4].copy_from_slice(&[sensor_index, direction.into(), cor_hi, cor_lo]);
        self.endpoint.call_long(10, args).await?;
        Ok(())
    }
}