hidpp/feature/unified_battery/
mod.rs1use 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
16pub struct UnifiedBatteryFeature {
18 chan: Arc<HidppChannel>,
20
21 device_index: u8,
23
24 feature_index: u8,
26
27 emitter: Arc<EventEmitter<BatteryEvent>>,
29
30 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 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
157#[cfg_attr(feature = "serde", derive(serde::Serialize))]
158#[non_exhaustive]
159pub struct BatteryCapabilities {
160 pub reported_levels: HashSet<BatteryLevel>,
162
163 pub rechargeable: bool,
165
166 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
197#[cfg_attr(feature = "serde", derive(serde::Serialize))]
198#[non_exhaustive]
199pub struct BatteryInfo {
200 pub charging_percentage: u8,
205
206 pub level: BatteryLevel,
211
212 pub status: BatteryStatus,
214}
215
216#[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#[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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize))]
244#[non_exhaustive]
245pub enum BatteryEvent {
246 InfoUpdate(BatteryInfo),
250}