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}