1use hidpp_transport::HidapiChannel;
8use tracing::{debug, trace};
9
10use crate::error::{HidppErrorCode, ProtocolError, Result};
11use crate::protocol::{build_long_request, get_error_code, is_error_response};
12
13pub struct UnifiedBatteryFeature {
15 device_index: u8,
16 feature_index: u8,
17}
18
19pub struct BatteryStatusFeature {
21 device_index: u8,
22 feature_index: u8,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct BatteryInfo {
28 pub level: u8,
30 pub status: ChargingStatus,
32 pub voltage_mv: Option<u16>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum BatteryLevel {
39 Full,
41 Good,
43 Low,
45 Critical,
47 Empty,
49 Unknown,
51}
52
53impl BatteryLevel {
54 #[must_use]
56 pub fn from_byte(value: u8) -> Self {
57 match value {
58 0 => Self::Empty,
59 1 => Self::Critical,
60 2 => Self::Low,
61 3 => Self::Good,
62 4..=8 => Self::Full,
63 _ => Self::Unknown,
64 }
65 }
66
67 #[must_use]
69 pub fn approximate_percent(&self) -> u8 {
70 match self {
71 Self::Full => 90,
72 Self::Good => 65,
73 Self::Low => 35,
74 Self::Critical => 10,
75 Self::Empty => 0,
76 Self::Unknown => 50,
77 }
78 }
79}
80
81impl std::fmt::Display for BatteryLevel {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 match self {
84 Self::Full => write!(f, "Full"),
85 Self::Good => write!(f, "Good"),
86 Self::Low => write!(f, "Low"),
87 Self::Critical => write!(f, "Critical"),
88 Self::Empty => write!(f, "Empty"),
89 Self::Unknown => write!(f, "Unknown"),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
96pub enum ChargingStatus {
97 #[default]
99 Discharging,
100 Charging,
102 Full,
104 ChargingError,
106 WirelessCharging,
108}
109
110impl ChargingStatus {
111 #[must_use]
113 pub fn from_byte(value: u8) -> Self {
114 match value {
115 1 => Self::Charging,
116 2 => Self::Full,
117 3 => Self::ChargingError,
118 4 => Self::WirelessCharging,
119 _ => Self::Discharging,
120 }
121 }
122
123 #[must_use]
125 pub fn is_charging(&self) -> bool {
126 matches!(self, Self::Charging | Self::WirelessCharging)
127 }
128}
129
130impl std::fmt::Display for ChargingStatus {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 match self {
133 Self::Discharging => write!(f, "Discharging"),
134 Self::Charging => write!(f, "Charging"),
135 Self::Full => write!(f, "Fully Charged"),
136 Self::ChargingError => write!(f, "Charging Error"),
137 Self::WirelessCharging => write!(f, "Wireless Charging"),
138 }
139 }
140}
141
142impl UnifiedBatteryFeature {
143 #[must_use]
149 pub fn new(device_index: u8, feature_index: u8) -> Self {
150 Self {
151 device_index,
152 feature_index,
153 }
154 }
155
156 pub async fn get_capabilities(&self, channel: &HidapiChannel) -> Result<u8> {
161 let request = build_long_request(self.device_index, self.feature_index, 0x00, &[]);
163
164 trace!("getting battery capabilities");
165 let response = channel.request(&request, 5).await?;
166
167 if is_error_response(&response) {
168 let code = get_error_code(&response).unwrap_or(0);
169 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
170 }
171
172 if response.len() < 5 {
173 return Err(ProtocolError::InvalidResponse(
174 "battery capabilities response too short".to_string(),
175 ));
176 }
177
178 let capabilities = response[4];
179 debug!(capabilities, "got battery capabilities");
180 Ok(capabilities)
181 }
182
183 pub async fn get_status(&self, channel: &HidapiChannel) -> Result<BatteryInfo> {
188 let request = build_long_request(self.device_index, self.feature_index, 0x01, &[]);
190
191 trace!("getting battery status");
192 let response = channel.request(&request, 5).await?;
193
194 if is_error_response(&response) {
195 let code = get_error_code(&response).unwrap_or(0);
196 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
197 }
198
199 if response.len() < 7 {
200 return Err(ProtocolError::InvalidResponse(
201 "battery status response too short".to_string(),
202 ));
203 }
204
205 let level = response[4];
207 let battery_level = BatteryLevel::from_byte(response[5]);
208 let status = ChargingStatus::from_byte(response[6]);
209
210 let percent = if level > 0 && level <= 100 {
212 level
213 } else {
214 battery_level.approximate_percent()
215 };
216
217 let info = BatteryInfo {
218 level: percent,
219 status,
220 voltage_mv: None,
221 };
222
223 debug!(
224 level = info.level,
225 status = %info.status,
226 "got battery status"
227 );
228
229 Ok(info)
230 }
231}
232
233impl BatteryStatusFeature {
234 #[must_use]
240 pub fn new(device_index: u8, feature_index: u8) -> Self {
241 Self {
242 device_index,
243 feature_index,
244 }
245 }
246
247 pub async fn get_battery_level(&self, channel: &HidapiChannel) -> Result<BatteryInfo> {
252 let request = build_long_request(self.device_index, self.feature_index, 0x00, &[]);
254
255 trace!("getting battery level");
256 let response = channel.request(&request, 5).await?;
257
258 if is_error_response(&response) {
259 let code = get_error_code(&response).unwrap_or(0);
260 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
261 }
262
263 if response.len() < 7 {
264 return Err(ProtocolError::InvalidResponse(
265 "battery level response too short".to_string(),
266 ));
267 }
268
269 let discharge_level = response[4];
271 let next_level = response[5];
272 let battery_status = response[6];
273
274 let status = match battery_status {
276 0x20..=0x3F => ChargingStatus::Charging,
277 0x80..=0x8F => ChargingStatus::Full,
278 _ => ChargingStatus::Discharging,
279 };
280
281 let percent = if discharge_level <= 7 {
284 (u16::from(discharge_level) * 100 / 7).min(100) as u8
285 } else {
286 discharge_level.min(100)
288 };
289
290 let info = BatteryInfo {
291 level: percent,
292 status,
293 voltage_mv: None,
294 };
295
296 debug!(
297 level = info.level,
298 status = %info.status,
299 next_level,
300 "got battery level"
301 );
302
303 Ok(info)
304 }
305
306 pub async fn get_battery_voltage(&self, channel: &HidapiChannel) -> Result<u16> {
311 let request = build_long_request(self.device_index, self.feature_index, 0x01, &[]);
313
314 trace!("getting battery voltage");
315 let response = channel.request(&request, 5).await?;
316
317 if is_error_response(&response) {
318 let code = get_error_code(&response).unwrap_or(0);
319 return Err(ProtocolError::HidppError(HidppErrorCode::from_byte(code)));
320 }
321
322 if response.len() < 6 {
323 return Err(ProtocolError::InvalidResponse(
324 "battery voltage response too short".to_string(),
325 ));
326 }
327
328 let voltage = u16::from_be_bytes([response[4], response[5]]);
329 debug!(voltage_mv = voltage, "got battery voltage");
330
331 Ok(voltage)
332 }
333}
334
335pub enum BatteryFeature {
337 Unified(UnifiedBatteryFeature),
339 Legacy(BatteryStatusFeature),
341}
342
343impl BatteryFeature {
344 pub async fn get_battery(&self, channel: &HidapiChannel) -> Result<BatteryInfo> {
349 match self {
350 Self::Unified(f) => f.get_status(channel).await,
351 Self::Legacy(f) => f.get_battery_level(channel).await,
352 }
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_battery_level() {
362 assert_eq!(BatteryLevel::from_byte(0), BatteryLevel::Empty);
363 assert_eq!(BatteryLevel::from_byte(1), BatteryLevel::Critical);
364 assert_eq!(BatteryLevel::from_byte(4), BatteryLevel::Full);
365 assert_eq!(BatteryLevel::from_byte(255), BatteryLevel::Unknown);
366 }
367
368 #[test]
369 fn test_charging_status() {
370 assert!(!ChargingStatus::Discharging.is_charging());
371 assert!(ChargingStatus::Charging.is_charging());
372 assert!(ChargingStatus::WirelessCharging.is_charging());
373 assert!(!ChargingStatus::Full.is_charging());
374 }
375}