openlogi-hidpp 0.7.1

OpenLogi's hard fork of the `hidpp` crate (Logitech HID++ protocol).
Documentation
//! Implements the `UnifiedBattery` feature (ID `0x1004`) that provides
//! information about the battery status of the device.

use std::{collections::HashSet, hash::Hash};

use num_enum::{IntoPrimitive, TryFromPrimitive};
use openlogi_hidpp_derive::Feature;

use crate::{
    feature::{DecodeEvent, EventSource, FeatureEndpoint},
    protocol::v20::Hidpp20Error,
};

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

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

impl DecodeEvent for BatteryEvent {
    fn decode(sub_id: u8, payload: &[u8; 16]) -> Option<Self> {
        // The battery broadcast is the only event and carries sub-id 0.
        if sub_id != 0 {
            return None;
        }

        let (Ok(level), Ok(status)) = (
            BatteryLevel::try_from(payload[1]),
            BatteryStatus::try_from(payload[2]),
        ) else {
            return None;
        };

        Some(BatteryEvent::InfoUpdate(BatteryInfo {
            charging_percentage: payload[0],
            level,
            status,
        }))
    }
}

impl UnifiedBatteryFeature {
    /// Retrieves the capabilities of this feature and the battery in general.
    pub async fn get_battery_capabilities(&self) -> Result<BatteryCapabilities, Hidpp20Error> {
        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();

        Ok(BatteryCapabilities::from([payload[0], payload[1]]))
    }

    /// Retrieves the current information about the battery status.
    pub async fn get_battery_info(&self) -> Result<BatteryInfo, Hidpp20Error> {
        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();

        // payload[3] contains some kind of information about the status of the external
        // power source (maybe 0 = disconnected and 1 = connected, I don't have enough
        // info about that), according to https://github.com/torvalds/linux/blob/a8662bcd2ff152bfbc751cab20f33053d74d0963/drivers/hid/hid-logitech-hidpp.c#L1608
        // and
        // https://github.com/torvalds/linux/blob/a8662bcd2ff152bfbc751cab20f33053d74d0963/drivers/hid/hid-logitech-hidpp.c#L1679

        Ok(BatteryInfo {
            charging_percentage: payload[0],
            level: BatteryLevel::try_from(payload[1])
                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
            status: BatteryStatus::try_from(payload[2])
                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
        })
    }
}

/// Represents the capabilites of this feature and the battery itself.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct BatteryCapabilities {
    /// All [`BatteryLevel`] variants the feature supports and reports.
    pub reported_levels: HashSet<BatteryLevel>,

    /// Whether the battery is rechargeable.
    pub rechargeable: bool,

    /// Whether the device supports reporting the current battery charge
    /// percentage in [`BatteryInfo::charging_percentage`].
    pub percentage: bool,
}

impl From<[u8; 2]> for BatteryCapabilities {
    fn from(value: [u8; 2]) -> Self {
        let mut reported_levels = HashSet::new();
        if value[0] & 1 != 0 {
            reported_levels.insert(BatteryLevel::Critical);
        }
        if value[0] & (1 << 1) != 0 {
            reported_levels.insert(BatteryLevel::Low);
        }
        if value[0] & (1 << 2) != 0 {
            reported_levels.insert(BatteryLevel::Good);
        }
        if value[0] & (1 << 3) != 0 {
            reported_levels.insert(BatteryLevel::Full);
        }

        Self {
            reported_levels,
            rechargeable: value[1] & 1 != 0,
            percentage: value[1] & (1 << 1) != 0,
        }
    }
}

/// Represents infirmation about the current battery charge.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct BatteryInfo {
    /// The current charge of the battery in percent.
    ///
    /// If [`BatteryCapabilities::percentage`] is set to `false`, this is always
    /// zero.
    pub charging_percentage: u8,

    /// The current (approximate) level of the battery.
    ///
    /// This can only reach values present in
    /// [`BatteryCapabilities::reported_levels`].
    pub level: BatteryLevel,

    /// The current charging status of the battery.
    pub status: BatteryStatus,
}

/// Represents an approximate level of the battery charge.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum BatteryLevel {
    /// Critical battery level.
    Critical = 1,
    /// Low battery level.
    Low = 1 << 1,
    /// Good battery level.
    Good = 1 << 2,
    /// Full battery level.
    Full = 1 << 3,
}

/// Represents the charging status of the battery, as reported in the `0x1004`
/// `getStatus` battery-status byte.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum BatteryStatus {
    /// Battery is discharging.
    Discharging = 0,
    /// Battery is charging.
    Charging = 1,
    /// Battery is charging and in its final stage (nearly full).
    ChargingNearlyFull = 2,
    /// Battery charge is complete.
    Full = 3,
    /// Battery is recharging below optimal speed.
    ChargingSlow = 4,
    /// The battery type is invalid.
    InvalidBattery = 5,
    /// The battery subsystem reported a thermal error.
    ThermalError = 6,
    /// The battery subsystem reported a charging error.
    ChargingError = 7,
}

/// Represents an event emitted by the [`UnifiedBatteryFeature`] feature.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum BatteryEvent {
    /// Is emitted whenever the battery information changes.
    ///
    /// This event is always enabled.
    InfoUpdate(BatteryInfo),
}