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