hidpp/feature/
battery_voltage.rs1use openlogi_hidpp_derive::Feature;
20
21use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
22
23#[derive(Feature)]
25#[creatable(id = 0x1001, version = 0)]
26pub struct BatteryVoltageFeature {
27 endpoint: FeatureEndpoint,
29}
30
31impl BatteryVoltageFeature {
32 pub async fn get_battery_info(&self) -> Result<VoltageBatteryInfo, Hidpp20Error> {
35 let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
36 Ok(VoltageBatteryInfo::from_wire(&payload))
37 }
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize))]
43#[non_exhaustive]
44pub struct VoltageBatteryInfo {
45 pub voltage_mv: u16,
48
49 pub status: VoltageChargingStatus,
51
52 pub critical: bool,
54}
55
56impl VoltageBatteryInfo {
57 #[must_use]
60 pub fn from_wire(payload: &[u8; 16]) -> Self {
61 let flags = payload[2];
62 Self {
63 voltage_mv: u16::from_be_bytes([payload[0], payload[1]]),
64 status: VoltageChargingStatus::from_flags(flags),
65 critical: flags & (1 << 5) != 0,
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77#[non_exhaustive]
78pub enum VoltageChargingStatus {
79 Discharging,
81 Charging,
83 ChargingFast,
85 ChargingSlow,
87 Full,
89 NotCharging,
92}
93
94impl VoltageChargingStatus {
95 fn from_flags(flags: u8) -> Self {
99 if flags & (1 << 7) == 0 {
100 return Self::Discharging;
101 }
102 match flags & 0x03 {
103 0x01 | 0x03 => Self::Full,
104 0x02 => Self::NotCharging,
105 _ => {
106 if flags & (1 << 3) != 0 {
107 Self::ChargingFast
108 } else if flags & (1 << 4) != 0 {
109 Self::ChargingSlow
110 } else {
111 Self::Charging
112 }
113 }
114 }
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::{VoltageBatteryInfo, VoltageChargingStatus};
121
122 fn payload(voltage_mv: u16, flags: u8) -> [u8; 16] {
124 let mut payload = [0; 16];
125 payload[..2].copy_from_slice(&voltage_mv.to_be_bytes());
126 payload[2] = flags;
127 payload
128 }
129
130 #[test]
131 fn discharging_reading_decodes_voltage_and_status() {
132 let info = VoltageBatteryInfo::from_wire(&payload(3781, 0x00));
133 assert_eq!(info.voltage_mv, 3781);
134 assert_eq!(info.status, VoltageChargingStatus::Discharging);
135 assert!(!info.critical);
136 }
137
138 #[test]
139 fn external_power_flag_alone_means_standard_charging() {
140 let info = VoltageBatteryInfo::from_wire(&payload(4100, 0x80));
141 assert_eq!(info.status, VoltageChargingStatus::Charging);
142 }
143
144 #[test]
145 fn charge_status_bits_take_precedence_over_rate_bits() {
146 let info = VoltageBatteryInfo::from_wire(&payload(4186, 0x80 | 0x08 | 0x01));
148 assert_eq!(info.status, VoltageChargingStatus::Full);
149 let info = VoltageBatteryInfo::from_wire(&payload(4000, 0x80 | 0x02));
150 assert_eq!(info.status, VoltageChargingStatus::NotCharging);
151 }
152
153 #[test]
154 fn rate_bits_split_fast_and_slow_charging() {
155 let fast = VoltageBatteryInfo::from_wire(&payload(3900, 0x80 | 0x08));
156 assert_eq!(fast.status, VoltageChargingStatus::ChargingFast);
157 let slow = VoltageBatteryInfo::from_wire(&payload(3900, 0x80 | 0x10));
158 assert_eq!(slow.status, VoltageChargingStatus::ChargingSlow);
159 }
160
161 #[test]
162 fn critical_bit_is_surfaced_independently_of_status() {
163 let info = VoltageBatteryInfo::from_wire(&payload(3520, 0x20));
164 assert_eq!(info.status, VoltageChargingStatus::Discharging);
165 assert!(info.critical);
166 }
167
168 #[test]
169 fn without_external_power_the_rate_bits_are_meaningless() {
170 let info = VoltageBatteryInfo::from_wire(&payload(3700, 0x1b));
172 assert_eq!(info.status, VoltageChargingStatus::Discharging);
173 }
174}