Skip to main content

hidpp/feature/
unified_battery.rs

1//! Implements the `UnifiedBattery` feature (ID `0x1004`) that provides
2//! information about the battery status of the device.
3
4use std::{collections::HashSet, hash::Hash};
5
6use num_enum::{IntoPrimitive, TryFromPrimitive};
7use openlogi_hidpp_derive::Feature;
8
9use crate::{
10    feature::{DecodeEvent, EventSource, FeatureEndpoint},
11    protocol::v20::Hidpp20Error,
12};
13
14/// Implements the `UnifiedBattery` / `0x1004` feature.
15#[derive(Feature)]
16#[creatable(id = 0x1004, version = 0)]
17pub struct UnifiedBatteryFeature {
18    /// The endpoint this feature talks to.
19    endpoint: FeatureEndpoint,
20
21    /// Publishes decoded events to listeners.
22    events: EventSource<BatteryEvent>,
23}
24
25impl DecodeEvent for BatteryEvent {
26    fn decode(sub_id: u8, payload: &[u8; 16]) -> Option<Self> {
27        // The battery broadcast is the only event and carries sub-id 0.
28        if sub_id != 0 {
29            return None;
30        }
31
32        let (Ok(level), Ok(status)) = (
33            BatteryLevel::try_from(payload[1]),
34            BatteryStatus::try_from(payload[2]),
35        ) else {
36            return None;
37        };
38
39        Some(BatteryEvent::InfoUpdate(BatteryInfo {
40            charging_percentage: payload[0],
41            level,
42            status,
43        }))
44    }
45}
46
47impl UnifiedBatteryFeature {
48    /// Retrieves the capabilities of this feature and the battery in general.
49    pub async fn get_battery_capabilities(&self) -> Result<BatteryCapabilities, Hidpp20Error> {
50        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
51
52        Ok(BatteryCapabilities::from([payload[0], payload[1]]))
53    }
54
55    /// Retrieves the current information about the battery status.
56    pub async fn get_battery_info(&self) -> Result<BatteryInfo, Hidpp20Error> {
57        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
58
59        // payload[3] contains some kind of information about the status of the external
60        // power source (maybe 0 = disconnected and 1 = connected, I don't have enough
61        // info about that), according to https://github.com/torvalds/linux/blob/a8662bcd2ff152bfbc751cab20f33053d74d0963/drivers/hid/hid-logitech-hidpp.c#L1608
62        // and
63        // https://github.com/torvalds/linux/blob/a8662bcd2ff152bfbc751cab20f33053d74d0963/drivers/hid/hid-logitech-hidpp.c#L1679
64
65        Ok(BatteryInfo {
66            charging_percentage: payload[0],
67            level: BatteryLevel::try_from(payload[1])
68                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
69            status: BatteryStatus::try_from(payload[2])
70                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
71        })
72    }
73}
74
75/// Represents the capabilites of this feature and the battery itself.
76#[derive(Clone, Debug, PartialEq, Eq)]
77#[cfg_attr(feature = "serde", derive(serde::Serialize))]
78#[non_exhaustive]
79pub struct BatteryCapabilities {
80    /// All [`BatteryLevel`] variants the feature supports and reports.
81    pub reported_levels: HashSet<BatteryLevel>,
82
83    /// Whether the battery is rechargeable.
84    pub rechargeable: bool,
85
86    /// Whether the device supports reporting the current battery charge
87    /// percentage in [`BatteryInfo::charging_percentage`].
88    pub percentage: bool,
89}
90
91impl From<[u8; 2]> for BatteryCapabilities {
92    fn from(value: [u8; 2]) -> Self {
93        let mut reported_levels = HashSet::new();
94        if value[0] & 1 != 0 {
95            reported_levels.insert(BatteryLevel::Critical);
96        }
97        if value[0] & (1 << 1) != 0 {
98            reported_levels.insert(BatteryLevel::Low);
99        }
100        if value[0] & (1 << 2) != 0 {
101            reported_levels.insert(BatteryLevel::Good);
102        }
103        if value[0] & (1 << 3) != 0 {
104            reported_levels.insert(BatteryLevel::Full);
105        }
106
107        Self {
108            reported_levels,
109            rechargeable: value[1] & 1 != 0,
110            percentage: value[1] & (1 << 1) != 0,
111        }
112    }
113}
114
115/// Represents infirmation about the current battery charge.
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize))]
118#[non_exhaustive]
119pub struct BatteryInfo {
120    /// The current charge of the battery in percent.
121    ///
122    /// If [`BatteryCapabilities::percentage`] is set to `false`, this is always
123    /// zero.
124    pub charging_percentage: u8,
125
126    /// The current (approximate) level of the battery.
127    ///
128    /// This can only reach values present in
129    /// [`BatteryCapabilities::reported_levels`].
130    pub level: BatteryLevel,
131
132    /// The current charging status of the battery.
133    pub status: BatteryStatus,
134}
135
136/// Represents an approximate level of the battery charge.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize))]
139#[non_exhaustive]
140#[repr(u8)]
141pub enum BatteryLevel {
142    /// Critical battery level.
143    Critical = 1,
144    /// Low battery level.
145    Low = 1 << 1,
146    /// Good battery level.
147    Good = 1 << 2,
148    /// Full battery level.
149    Full = 1 << 3,
150}
151
152/// Represents the charging status of the battery, as reported in the `0x1004`
153/// `getStatus` battery-status byte.
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
155#[cfg_attr(feature = "serde", derive(serde::Serialize))]
156#[non_exhaustive]
157#[repr(u8)]
158pub enum BatteryStatus {
159    /// Battery is discharging.
160    Discharging = 0,
161    /// Battery is charging.
162    Charging = 1,
163    /// Battery is charging and in its final stage (nearly full).
164    ChargingNearlyFull = 2,
165    /// Battery charge is complete.
166    Full = 3,
167    /// Battery is recharging below optimal speed.
168    ChargingSlow = 4,
169    /// The battery type is invalid.
170    InvalidBattery = 5,
171    /// The battery subsystem reported a thermal error.
172    ThermalError = 6,
173    /// The battery subsystem reported a charging error.
174    ChargingError = 7,
175}
176
177/// Represents an event emitted by the [`UnifiedBatteryFeature`] feature.
178#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
179#[cfg_attr(feature = "serde", derive(serde::Serialize))]
180#[non_exhaustive]
181pub enum BatteryEvent {
182    /// Is emitted whenever the battery information changes.
183    ///
184    /// This event is always enabled.
185    InfoUpdate(BatteryInfo),
186}