1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct RadioTiming {
24 pub scan_interval_ms: u32,
26 pub scan_window_ms: u32,
28 pub adv_interval_ms: u32,
30 pub conn_interval_ms: u32,
32 pub supervision_timeout_ms: u32,
34 pub slave_latency: u16,
36}
37
38impl RadioTiming {
39 pub fn duty_cycle_percent(&self) -> f32 {
41 let scan_duty = (self.scan_window_ms as f32 / self.scan_interval_ms as f32) * 100.0;
43
44 let adv_duration_ms = 2.0;
46 let adv_duty = (adv_duration_ms / self.adv_interval_ms as f32) * 100.0;
47
48 let conn_duration_ms = 2.0;
50 let effective_conn_interval =
51 self.conn_interval_ms as f32 * (1.0 + self.slave_latency as f32);
52 let conn_duty = (conn_duration_ms / effective_conn_interval) * 100.0;
53
54 scan_duty + adv_duty + conn_duty
56 }
57
58 pub fn estimated_battery_hours(&self, battery_capacity_mah: u16) -> f32 {
60 let active_current_ma = 15.0;
62 let sleep_current_ma = 0.005;
63
64 let duty = self.duty_cycle_percent() / 100.0;
65 let average_current = (active_current_ma * duty) + (sleep_current_ma * (1.0 - duty));
66
67 let total_current = average_current + 5.0;
69
70 battery_capacity_mah as f32 / total_current
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
76pub enum PowerProfile {
77 Aggressive,
80
81 Balanced,
84
85 #[default]
88 LowPower,
89
90 Custom(RadioTiming),
92}
93
94impl PowerProfile {
95 pub fn timing(&self) -> RadioTiming {
97 match self {
98 PowerProfile::Aggressive => RadioTiming {
99 scan_interval_ms: 100,
100 scan_window_ms: 50,
101 adv_interval_ms: 100,
102 conn_interval_ms: 15,
103 supervision_timeout_ms: 4000,
104 slave_latency: 0,
105 },
106 PowerProfile::Balanced => RadioTiming {
107 scan_interval_ms: 500,
108 scan_window_ms: 50,
109 adv_interval_ms: 500,
110 conn_interval_ms: 30,
111 supervision_timeout_ms: 4000,
112 slave_latency: 2,
113 },
114 PowerProfile::LowPower => RadioTiming {
115 scan_interval_ms: 5000,
116 scan_window_ms: 100,
117 adv_interval_ms: 2000,
118 conn_interval_ms: 100,
119 supervision_timeout_ms: 6000,
120 slave_latency: 4,
121 },
122 PowerProfile::Custom(timing) => *timing,
123 }
124 }
125
126 pub fn duty_cycle_percent(&self) -> f32 {
128 self.timing().duty_cycle_percent()
129 }
130
131 pub fn estimated_battery_hours(&self, battery_capacity_mah: u16) -> f32 {
133 self.timing().estimated_battery_hours(battery_capacity_mah)
134 }
135
136 pub fn custom(timing: RadioTiming) -> Self {
138 PowerProfile::Custom(timing)
139 }
140
141 pub fn name(&self) -> &'static str {
143 match self {
144 PowerProfile::Aggressive => "aggressive",
145 PowerProfile::Balanced => "balanced",
146 PowerProfile::LowPower => "low_power",
147 PowerProfile::Custom(_) => "custom",
148 }
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub struct BatteryState {
155 pub level_percent: u8,
157 pub is_charging: bool,
159 pub low_threshold: u8,
161 pub critical_threshold: u8,
163}
164
165impl Default for BatteryState {
166 fn default() -> Self {
167 Self {
168 level_percent: 100,
169 is_charging: false,
170 low_threshold: 20,
171 critical_threshold: 10,
172 }
173 }
174}
175
176impl BatteryState {
177 pub fn new(level_percent: u8, is_charging: bool) -> Self {
179 Self {
180 level_percent: level_percent.min(100),
181 is_charging,
182 ..Default::default()
183 }
184 }
185
186 pub fn is_low(&self) -> bool {
188 !self.is_charging && self.level_percent <= self.low_threshold
189 }
190
191 pub fn is_critical(&self) -> bool {
193 !self.is_charging && self.level_percent <= self.critical_threshold
194 }
195
196 pub fn suggested_profile(&self, current: PowerProfile) -> PowerProfile {
198 if self.is_charging {
199 current
201 } else if self.is_critical() {
202 PowerProfile::LowPower
204 } else if self.is_low() {
205 match current {
207 PowerProfile::Aggressive => PowerProfile::Balanced,
208 _ => PowerProfile::LowPower,
209 }
210 } else {
211 current
212 }
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_profile_defaults() {
222 assert_eq!(PowerProfile::default(), PowerProfile::LowPower);
223 }
224
225 #[test]
226 fn test_aggressive_timing() {
227 let timing = PowerProfile::Aggressive.timing();
228 assert_eq!(timing.scan_interval_ms, 100);
229 assert_eq!(timing.scan_window_ms, 50);
230 assert_eq!(timing.adv_interval_ms, 100);
231 assert_eq!(timing.conn_interval_ms, 15);
232 }
233
234 #[test]
235 fn test_balanced_timing() {
236 let timing = PowerProfile::Balanced.timing();
237 assert_eq!(timing.scan_interval_ms, 500);
238 assert_eq!(timing.adv_interval_ms, 500);
239 }
240
241 #[test]
242 fn test_low_power_timing() {
243 let timing = PowerProfile::LowPower.timing();
244 assert_eq!(timing.scan_interval_ms, 5000);
245 assert_eq!(timing.scan_window_ms, 100);
246 assert_eq!(timing.adv_interval_ms, 2000);
247 }
248
249 #[test]
250 fn test_custom_profile() {
251 let custom_timing = RadioTiming {
252 scan_interval_ms: 1000,
253 scan_window_ms: 100,
254 adv_interval_ms: 1000,
255 conn_interval_ms: 50,
256 supervision_timeout_ms: 5000,
257 slave_latency: 3,
258 };
259 let profile = PowerProfile::custom(custom_timing);
260 assert_eq!(profile.timing(), custom_timing);
261 assert_eq!(profile.name(), "custom");
262 }
263
264 #[test]
265 fn test_duty_cycle_ordering() {
266 let aggressive = PowerProfile::Aggressive.duty_cycle_percent();
268 let balanced = PowerProfile::Balanced.duty_cycle_percent();
269 let low_power = PowerProfile::LowPower.duty_cycle_percent();
270
271 assert!(aggressive > balanced, "aggressive > balanced");
272 assert!(balanced > low_power, "balanced > low_power");
273 }
274
275 #[test]
276 fn test_low_power_duty_cycle() {
277 let duty = PowerProfile::LowPower.duty_cycle_percent();
279 assert!(duty < 5.0, "LowPower duty cycle {} should be < 5%", duty);
280 }
281
282 #[test]
283 fn test_battery_life_ordering() {
284 let battery_mah = 300;
285
286 let aggressive = PowerProfile::Aggressive.estimated_battery_hours(battery_mah);
287 let balanced = PowerProfile::Balanced.estimated_battery_hours(battery_mah);
288 let low_power = PowerProfile::LowPower.estimated_battery_hours(battery_mah);
289
290 assert!(low_power > balanced, "low_power > balanced battery life");
292 assert!(balanced > aggressive, "balanced > aggressive battery life");
293 }
294
295 #[test]
296 fn test_battery_state_default() {
297 let state = BatteryState::default();
298 assert_eq!(state.level_percent, 100);
299 assert!(!state.is_charging);
300 assert!(!state.is_low());
301 assert!(!state.is_critical());
302 }
303
304 #[test]
305 fn test_battery_state_low() {
306 let state = BatteryState::new(20, false);
307 assert!(state.is_low());
308 assert!(!state.is_critical());
309 }
310
311 #[test]
312 fn test_battery_state_critical() {
313 let state = BatteryState::new(5, false);
314 assert!(state.is_low());
315 assert!(state.is_critical());
316 }
317
318 #[test]
319 fn test_battery_charging_not_low() {
320 let state = BatteryState::new(10, true);
321 assert!(!state.is_low(), "charging should not be considered low");
322 assert!(
323 !state.is_critical(),
324 "charging should not be considered critical"
325 );
326 }
327
328 #[test]
329 fn test_suggested_profile_critical() {
330 let state = BatteryState::new(5, false);
331 let suggested = state.suggested_profile(PowerProfile::Aggressive);
332 assert_eq!(suggested, PowerProfile::LowPower);
333 }
334
335 #[test]
336 fn test_suggested_profile_low() {
337 let state = BatteryState::new(15, false);
338 let suggested = state.suggested_profile(PowerProfile::Aggressive);
339 assert_eq!(suggested, PowerProfile::Balanced);
340 }
341
342 #[test]
343 fn test_suggested_profile_charging() {
344 let state = BatteryState::new(10, true);
345 let suggested = state.suggested_profile(PowerProfile::Aggressive);
346 assert_eq!(
347 suggested,
348 PowerProfile::Aggressive,
349 "charging keeps current profile"
350 );
351 }
352
353 #[test]
354 fn test_profile_names() {
355 assert_eq!(PowerProfile::Aggressive.name(), "aggressive");
356 assert_eq!(PowerProfile::Balanced.name(), "balanced");
357 assert_eq!(PowerProfile::LowPower.name(), "low_power");
358 }
359}