Skip to main content

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