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