#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RadioTiming {
pub scan_interval_ms: u32,
pub scan_window_ms: u32,
pub adv_interval_ms: u32,
pub conn_interval_ms: u32,
pub supervision_timeout_ms: u32,
pub slave_latency: u16,
}
impl RadioTiming {
pub fn duty_cycle_percent(&self) -> f32 {
let scan_duty = (self.scan_window_ms as f32 / self.scan_interval_ms as f32) * 100.0;
let adv_duration_ms = 2.0;
let adv_duty = (adv_duration_ms / self.adv_interval_ms as f32) * 100.0;
let conn_duration_ms = 2.0;
let effective_conn_interval =
self.conn_interval_ms as f32 * (1.0 + self.slave_latency as f32);
let conn_duty = (conn_duration_ms / effective_conn_interval) * 100.0;
scan_duty + adv_duty + conn_duty
}
pub fn estimated_battery_hours(&self, battery_capacity_mah: u16) -> f32 {
let active_current_ma = 15.0;
let sleep_current_ma = 0.005;
let duty = self.duty_cycle_percent() / 100.0;
let average_current = (active_current_ma * duty) + (sleep_current_ma * (1.0 - duty));
let total_current = average_current + 5.0;
battery_capacity_mah as f32 / total_current
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PowerProfile {
Aggressive,
Balanced,
#[default]
LowPower,
Custom(RadioTiming),
}
impl PowerProfile {
pub fn timing(&self) -> RadioTiming {
match self {
PowerProfile::Aggressive => RadioTiming {
scan_interval_ms: 100,
scan_window_ms: 50,
adv_interval_ms: 100,
conn_interval_ms: 15,
supervision_timeout_ms: 4000,
slave_latency: 0,
},
PowerProfile::Balanced => RadioTiming {
scan_interval_ms: 500,
scan_window_ms: 50,
adv_interval_ms: 500,
conn_interval_ms: 30,
supervision_timeout_ms: 4000,
slave_latency: 2,
},
PowerProfile::LowPower => RadioTiming {
scan_interval_ms: 5000,
scan_window_ms: 100,
adv_interval_ms: 2000,
conn_interval_ms: 100,
supervision_timeout_ms: 6000,
slave_latency: 4,
},
PowerProfile::Custom(timing) => *timing,
}
}
pub fn duty_cycle_percent(&self) -> f32 {
self.timing().duty_cycle_percent()
}
pub fn estimated_battery_hours(&self, battery_capacity_mah: u16) -> f32 {
self.timing().estimated_battery_hours(battery_capacity_mah)
}
pub fn custom(timing: RadioTiming) -> Self {
PowerProfile::Custom(timing)
}
pub fn name(&self) -> &'static str {
match self {
PowerProfile::Aggressive => "aggressive",
PowerProfile::Balanced => "balanced",
PowerProfile::LowPower => "low_power",
PowerProfile::Custom(_) => "custom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BatteryState {
pub level_percent: u8,
pub is_charging: bool,
pub low_threshold: u8,
pub critical_threshold: u8,
}
impl Default for BatteryState {
fn default() -> Self {
Self {
level_percent: 100,
is_charging: false,
low_threshold: 20,
critical_threshold: 10,
}
}
}
impl BatteryState {
pub fn new(level_percent: u8, is_charging: bool) -> Self {
Self {
level_percent: level_percent.min(100),
is_charging,
..Default::default()
}
}
pub fn is_low(&self) -> bool {
!self.is_charging && self.level_percent <= self.low_threshold
}
pub fn is_critical(&self) -> bool {
!self.is_charging && self.level_percent <= self.critical_threshold
}
pub fn suggested_profile(&self, current: PowerProfile) -> PowerProfile {
if self.is_charging {
current
} else if self.is_critical() {
PowerProfile::LowPower
} else if self.is_low() {
match current {
PowerProfile::Aggressive => PowerProfile::Balanced,
_ => PowerProfile::LowPower,
}
} else {
current
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_defaults() {
assert_eq!(PowerProfile::default(), PowerProfile::LowPower);
}
#[test]
fn test_aggressive_timing() {
let timing = PowerProfile::Aggressive.timing();
assert_eq!(timing.scan_interval_ms, 100);
assert_eq!(timing.scan_window_ms, 50);
assert_eq!(timing.adv_interval_ms, 100);
assert_eq!(timing.conn_interval_ms, 15);
}
#[test]
fn test_balanced_timing() {
let timing = PowerProfile::Balanced.timing();
assert_eq!(timing.scan_interval_ms, 500);
assert_eq!(timing.adv_interval_ms, 500);
}
#[test]
fn test_low_power_timing() {
let timing = PowerProfile::LowPower.timing();
assert_eq!(timing.scan_interval_ms, 5000);
assert_eq!(timing.scan_window_ms, 100);
assert_eq!(timing.adv_interval_ms, 2000);
}
#[test]
fn test_custom_profile() {
let custom_timing = RadioTiming {
scan_interval_ms: 1000,
scan_window_ms: 100,
adv_interval_ms: 1000,
conn_interval_ms: 50,
supervision_timeout_ms: 5000,
slave_latency: 3,
};
let profile = PowerProfile::custom(custom_timing);
assert_eq!(profile.timing(), custom_timing);
assert_eq!(profile.name(), "custom");
}
#[test]
fn test_duty_cycle_ordering() {
let aggressive = PowerProfile::Aggressive.duty_cycle_percent();
let balanced = PowerProfile::Balanced.duty_cycle_percent();
let low_power = PowerProfile::LowPower.duty_cycle_percent();
assert!(aggressive > balanced, "aggressive > balanced");
assert!(balanced > low_power, "balanced > low_power");
}
#[test]
fn test_low_power_duty_cycle() {
let duty = PowerProfile::LowPower.duty_cycle_percent();
assert!(duty < 5.0, "LowPower duty cycle {} should be < 5%", duty);
}
#[test]
fn test_battery_life_ordering() {
let battery_mah = 300;
let aggressive = PowerProfile::Aggressive.estimated_battery_hours(battery_mah);
let balanced = PowerProfile::Balanced.estimated_battery_hours(battery_mah);
let low_power = PowerProfile::LowPower.estimated_battery_hours(battery_mah);
assert!(low_power > balanced, "low_power > balanced battery life");
assert!(balanced > aggressive, "balanced > aggressive battery life");
}
#[test]
fn test_battery_state_default() {
let state = BatteryState::default();
assert_eq!(state.level_percent, 100);
assert!(!state.is_charging);
assert!(!state.is_low());
assert!(!state.is_critical());
}
#[test]
fn test_battery_state_low() {
let state = BatteryState::new(20, false);
assert!(state.is_low());
assert!(!state.is_critical());
}
#[test]
fn test_battery_state_critical() {
let state = BatteryState::new(5, false);
assert!(state.is_low());
assert!(state.is_critical());
}
#[test]
fn test_battery_charging_not_low() {
let state = BatteryState::new(10, true);
assert!(!state.is_low(), "charging should not be considered low");
assert!(
!state.is_critical(),
"charging should not be considered critical"
);
}
#[test]
fn test_suggested_profile_critical() {
let state = BatteryState::new(5, false);
let suggested = state.suggested_profile(PowerProfile::Aggressive);
assert_eq!(suggested, PowerProfile::LowPower);
}
#[test]
fn test_suggested_profile_low() {
let state = BatteryState::new(15, false);
let suggested = state.suggested_profile(PowerProfile::Aggressive);
assert_eq!(suggested, PowerProfile::Balanced);
}
#[test]
fn test_suggested_profile_charging() {
let state = BatteryState::new(10, true);
let suggested = state.suggested_profile(PowerProfile::Aggressive);
assert_eq!(
suggested,
PowerProfile::Aggressive,
"charging keeps current profile"
);
}
#[test]
fn test_profile_names() {
assert_eq!(PowerProfile::Aggressive.name(), "aggressive");
assert_eq!(PowerProfile::Balanced.name(), "balanced");
assert_eq!(PowerProfile::LowPower.name(), "low_power");
}
}