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 BatteryEvent {
48    /// Decodes one event payload for this feature, or returns `None` for an
49    /// unsupported function or wire value.
50    ///
51    /// Consumers that already own a channel-level listener can use this
52    /// without constructing a second [`UnifiedBatteryFeature`].
53    #[must_use]
54    pub fn decode(function_id: u8, payload: &[u8; 16]) -> Option<Self> {
55        <Self as DecodeEvent>::decode(function_id, payload)
56    }
57}
58
59impl UnifiedBatteryFeature {
60    /// Retrieves the capabilities of this feature and the battery in general.
61    pub async fn get_battery_capabilities(&self) -> Result<BatteryCapabilities, Hidpp20Error> {
62        let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
63
64        Ok(BatteryCapabilities::from([payload[0], payload[1]]))
65    }
66
67    /// Retrieves the current information about the battery status.
68    pub async fn get_battery_info(&self) -> Result<BatteryInfo, Hidpp20Error> {
69        let payload = self.endpoint.call(1, [0; 3]).await?.extend_payload();
70
71        // payload[3] contains some kind of information about the status of the external
72        // power source (maybe 0 = disconnected and 1 = connected, I don't have enough
73        // info about that), according to https://github.com/torvalds/linux/blob/a8662bcd2ff152bfbc751cab20f33053d74d0963/drivers/hid/hid-logitech-hidpp.c#L1608
74        // and
75        // https://github.com/torvalds/linux/blob/a8662bcd2ff152bfbc751cab20f33053d74d0963/drivers/hid/hid-logitech-hidpp.c#L1679
76
77        Ok(BatteryInfo {
78            charging_percentage: payload[0],
79            level: BatteryLevel::try_from(payload[1])
80                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
81            status: BatteryStatus::try_from(payload[2])
82                .map_err(|_| Hidpp20Error::UnsupportedResponse)?,
83        })
84    }
85}
86
87/// Represents the capabilities of this feature and the battery itself.
88#[derive(Clone, Debug, PartialEq, Eq)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize))]
90#[non_exhaustive]
91pub struct BatteryCapabilities {
92    /// All [`BatteryLevel`] variants the feature supports and reports.
93    pub reported_levels: HashSet<BatteryLevel>,
94
95    /// Whether the battery is rechargeable.
96    pub rechargeable: bool,
97
98    /// Whether the device supports reporting the current battery charge
99    /// percentage in [`BatteryInfo::charging_percentage`].
100    pub percentage: bool,
101}
102
103impl From<[u8; 2]> for BatteryCapabilities {
104    fn from(value: [u8; 2]) -> Self {
105        let mut reported_levels = HashSet::new();
106        if value[0] & 1 != 0 {
107            reported_levels.insert(BatteryLevel::Critical);
108        }
109        if value[0] & (1 << 1) != 0 {
110            reported_levels.insert(BatteryLevel::Low);
111        }
112        if value[0] & (1 << 2) != 0 {
113            reported_levels.insert(BatteryLevel::Good);
114        }
115        if value[0] & (1 << 3) != 0 {
116            reported_levels.insert(BatteryLevel::Full);
117        }
118
119        Self {
120            reported_levels,
121            rechargeable: value[1] & 1 != 0,
122            percentage: value[1] & (1 << 1) != 0,
123        }
124    }
125}
126
127/// Represents infirmation about the current battery charge.
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize))]
130#[non_exhaustive]
131pub struct BatteryInfo {
132    /// The current charge of the battery in percent.
133    ///
134    /// If [`BatteryCapabilities::percentage`] is set to `false`, this is always
135    /// zero.
136    pub charging_percentage: u8,
137
138    /// The current (approximate) level of the battery.
139    ///
140    /// This can only reach values present in
141    /// [`BatteryCapabilities::reported_levels`].
142    pub level: BatteryLevel,
143
144    /// The current charging status of the battery.
145    pub status: BatteryStatus,
146}
147
148/// Represents an approximate level of the battery charge.
149#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
150#[cfg_attr(feature = "serde", derive(serde::Serialize))]
151#[non_exhaustive]
152#[repr(u8)]
153pub enum BatteryLevel {
154    /// Critical battery level.
155    Critical = 1,
156    /// Low battery level.
157    Low = 1 << 1,
158    /// Good battery level.
159    Good = 1 << 2,
160    /// Full battery level.
161    Full = 1 << 3,
162}
163
164/// Represents the charging status of the battery, as reported in the `0x1004`
165/// `getStatus` battery-status byte.
166#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
167#[cfg_attr(feature = "serde", derive(serde::Serialize))]
168#[non_exhaustive]
169#[repr(u8)]
170pub enum BatteryStatus {
171    /// Battery is discharging.
172    Discharging = 0,
173    /// Battery is charging.
174    Charging = 1,
175    /// Battery is charging and in its final stage (nearly full).
176    ChargingNearlyFull = 2,
177    /// Battery charge is complete.
178    Full = 3,
179    /// Battery is recharging below optimal speed.
180    ChargingSlow = 4,
181    /// The battery type is invalid.
182    InvalidBattery = 5,
183    /// The battery subsystem reported a thermal error.
184    ThermalError = 6,
185    /// The battery subsystem reported a charging error.
186    ChargingError = 7,
187}
188
189/// Represents an event emitted by the [`UnifiedBatteryFeature`] feature.
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize))]
192#[non_exhaustive]
193pub enum BatteryEvent {
194    /// Is emitted whenever the battery information changes.
195    ///
196    /// This event is always enabled.
197    InfoUpdate(BatteryInfo),
198}