hidpp/feature/battery_voltage/
mod.rs1use std::sync::Arc;
20
21use crate::{
22 channel::HidppChannel,
23 feature::{CreatableFeature, Feature, FeatureEndpoint},
24 protocol::v20::Hidpp20Error,
25};
26
27pub struct BatteryVoltageFeature {
29 endpoint: FeatureEndpoint,
31}
32
33impl CreatableFeature for BatteryVoltageFeature {
34 const ID: u16 = 0x1001;
35 const STARTING_VERSION: u8 = 0;
36
37 fn new(chan: Arc<HidppChannel>, device_index: u8, feature_index: u8) -> Self {
38 Self {
39 endpoint: FeatureEndpoint::new(chan, device_index, feature_index),
40 }
41 }
42}
43
44impl Feature for BatteryVoltageFeature {}
45
46impl BatteryVoltageFeature {
47 pub async fn get_battery_info(&self) -> Result<VoltageBatteryInfo, Hidpp20Error> {
50 let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
51 Ok(VoltageBatteryInfo::from_wire(&payload))
52 }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize))]
58#[non_exhaustive]
59pub struct VoltageBatteryInfo {
60 pub voltage_mv: u16,
63
64 pub status: VoltageChargingStatus,
66
67 pub critical: bool,
69}
70
71impl VoltageBatteryInfo {
72 #[must_use]
75 pub fn from_wire(payload: &[u8; 16]) -> Self {
76 let flags = payload[2];
77 Self {
78 voltage_mv: u16::from_be_bytes([payload[0], payload[1]]),
79 status: VoltageChargingStatus::from_flags(flags),
80 critical: flags & (1 << 5) != 0,
81 }
82 }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
91#[cfg_attr(feature = "serde", derive(serde::Serialize))]
92#[non_exhaustive]
93pub enum VoltageChargingStatus {
94 Discharging,
96 Charging,
98 ChargingFast,
100 ChargingSlow,
102 Full,
104 NotCharging,
107}
108
109impl VoltageChargingStatus {
110 fn from_flags(flags: u8) -> Self {
114 if flags & (1 << 7) == 0 {
115 return Self::Discharging;
116 }
117 match flags & 0x03 {
118 0x01 | 0x03 => Self::Full,
119 0x02 => Self::NotCharging,
120 _ => {
121 if flags & (1 << 3) != 0 {
122 Self::ChargingFast
123 } else if flags & (1 << 4) != 0 {
124 Self::ChargingSlow
125 } else {
126 Self::Charging
127 }
128 }
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::{VoltageBatteryInfo, VoltageChargingStatus};
136
137 fn payload(voltage_mv: u16, flags: u8) -> [u8; 16] {
139 let mut payload = [0; 16];
140 payload[..2].copy_from_slice(&voltage_mv.to_be_bytes());
141 payload[2] = flags;
142 payload
143 }
144
145 #[test]
146 fn discharging_reading_decodes_voltage_and_status() {
147 let info = VoltageBatteryInfo::from_wire(&payload(3781, 0x00));
148 assert_eq!(info.voltage_mv, 3781);
149 assert_eq!(info.status, VoltageChargingStatus::Discharging);
150 assert!(!info.critical);
151 }
152
153 #[test]
154 fn external_power_flag_alone_means_standard_charging() {
155 let info = VoltageBatteryInfo::from_wire(&payload(4100, 0x80));
156 assert_eq!(info.status, VoltageChargingStatus::Charging);
157 }
158
159 #[test]
160 fn charge_status_bits_take_precedence_over_rate_bits() {
161 let info = VoltageBatteryInfo::from_wire(&payload(4186, 0x80 | 0x08 | 0x01));
163 assert_eq!(info.status, VoltageChargingStatus::Full);
164 let info = VoltageBatteryInfo::from_wire(&payload(4000, 0x80 | 0x02));
165 assert_eq!(info.status, VoltageChargingStatus::NotCharging);
166 }
167
168 #[test]
169 fn rate_bits_split_fast_and_slow_charging() {
170 let fast = VoltageBatteryInfo::from_wire(&payload(3900, 0x80 | 0x08));
171 assert_eq!(fast.status, VoltageChargingStatus::ChargingFast);
172 let slow = VoltageBatteryInfo::from_wire(&payload(3900, 0x80 | 0x10));
173 assert_eq!(slow.status, VoltageChargingStatus::ChargingSlow);
174 }
175
176 #[test]
177 fn critical_bit_is_surfaced_independently_of_status() {
178 let info = VoltageBatteryInfo::from_wire(&payload(3520, 0x20));
179 assert_eq!(info.status, VoltageChargingStatus::Discharging);
180 assert!(info.critical);
181 }
182
183 #[test]
184 fn without_external_power_the_rate_bits_are_meaningless() {
185 let info = VoltageBatteryInfo::from_wire(&payload(3700, 0x1b));
187 assert_eq!(info.status, VoltageChargingStatus::Discharging);
188 }
189}