Repo of no-std crates for my personal embedded projects
1//! Crate for calculating Battery levels as percentages, based on voltage/pct profiles via 2//! [`BatteryDischargeProfile`]. 3#![no_std] 4 5use core::ops::Range; 6 7pub struct BatteryDischargeProfile { 8 voltage_range: Range<f32>, 9 pct_range: Range<f32>, 10} 11 12impl BatteryDischargeProfile { 13 /// Creates a new discharge profile. Internally, it stores the voltages high/low and pct high/low 14 /// as ranges. 15 #[inline] 16 pub const fn new(voltage_high: f32, voltage_low: f32, pct_high: f32, pct_low: f32) -> Self { 17 Self { 18 voltage_range: voltage_low..voltage_high, 19 pct_range: pct_low..pct_high, 20 } 21 } 22 23 /// Calculates a battery percentage according to the specified range of the discharge profile. 24 /// If the voltage is outside of the discharge profile, this method returns `None`. 25 /// 26 /// ``` 27 /// use sachy_battery::BatteryDischargeProfile; 28 /// 29 /// let level = BatteryDischargeProfile::new(3.0, 2.0, 1.0, 0.0); 30 /// 31 /// assert_eq!(level.calc_pct(2.5), Some(0.5)); 32 /// ``` 33 pub fn calc_pct(&self, voltage: f32) -> Option<f32> { 34 if self.voltage_range.contains(&voltage) { 35 Some( 36 self.pct_range.start 37 + (voltage - self.voltage_range.start) 38 * ((self.pct_range.end - self.pct_range.start) 39 / (self.voltage_range.end - self.voltage_range.start)), 40 ) 41 } else { 42 None 43 } 44 } 45 46 /// Calculates a battery level from a range of discharge profiles. Assumes the first 47 /// discharge level is the highest, so the levels go from high to low. Percentages values 48 /// are from 1.0 to 0.0. 49 /// 50 /// ``` 51 /// use sachy_battery::BatteryDischargeProfile; 52 /// 53 /// let levels = [ 54 /// BatteryDischargeProfile::new(3.0, 2.5, 1.0, 0.5), 55 /// BatteryDischargeProfile::new(2.5, 2.0, 0.5, 0.0), 56 /// ]; 57 /// 58 /// assert_eq!(BatteryDischargeProfile::calc_pct_from_profile_range(2.75, levels.iter()), 0.75); 59 /// ``` 60 pub fn calc_pct_from_profile_range<'a>( 61 voltage: f32, 62 levels: impl Iterator<Item = &'a BatteryDischargeProfile>, 63 ) -> f32 { 64 let mut levels = levels.peekable(); 65 66 if levels 67 .peek() 68 .is_some_and(|&level| voltage >= level.voltage_range.end) 69 { 70 return 1.0; 71 } 72 73 levels 74 .find_map(|level| level.calc_pct(voltage)) 75 .unwrap_or(0.0) 76 } 77} 78 79#[cfg(test)] 80mod tests { 81 use super::*; 82 83 #[test] 84 fn battery_level_from_one_profile() { 85 let level = BatteryDischargeProfile::new(3.0, 2.0, 1.0, 0.0); 86 87 assert_eq!(level.calc_pct(2.5), Some(0.5)); 88 assert_eq!(level.calc_pct(3.5), None); 89 assert_eq!(level.calc_pct(1.5), None); 90 } 91 92 #[test] 93 fn battery_level_from_profile_range() { 94 let levels = [ 95 BatteryDischargeProfile::new(3.0, 2.5, 1.0, 0.5), 96 BatteryDischargeProfile::new(2.5, 2.0, 0.5, 0.0), 97 ]; 98 99 let expect_results: [(f32, f32); 4] = [(3.5, 1.0), (2.75, 0.75), (2.25, 0.25), (1.5, 0.0)]; 100 101 for (voltage, pct) in expect_results { 102 assert_eq!( 103 BatteryDischargeProfile::calc_pct_from_profile_range(voltage, levels.iter()), 104 pct 105 ); 106 } 107 } 108}