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