#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct BatteryPercent(u8);
impl BatteryPercent {
#[must_use]
pub const fn saturating(percent: u8) -> Self {
Self(if percent > 100 { 100 } else { percent })
}
#[must_use]
pub const fn get(self) -> u8 {
self.0
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChargingState {
Charging,
Idle,
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ExternalPowerState {
Absent,
Present { charging: ChargingState },
Unknown,
}
impl ExternalPowerState {
#[must_use]
pub const fn from_presence(present: bool) -> Self {
if present {
Self::Present {
charging: ChargingState::Unknown,
}
} else {
Self::Absent
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PowerSnapshot {
battery: Option<BatteryPercent>,
external_power: ExternalPowerState,
}
impl PowerSnapshot {
pub const UNKNOWN: Self = Self::new(None, ExternalPowerState::Unknown);
#[must_use]
pub const fn new(battery: Option<BatteryPercent>, external_power: ExternalPowerState) -> Self {
Self {
battery,
external_power,
}
}
#[must_use]
pub const fn battery(self) -> Option<BatteryPercent> {
self.battery
}
#[must_use]
pub const fn external_power(self) -> ExternalPowerState {
self.external_power
}
}
pub trait BatterySource {
fn read_millivolts(&mut self) -> Option<u32>;
fn external_power(&mut self) -> ExternalPowerState {
ExternalPowerState::Unknown
}
}
pub struct BatteryGauge {
ema_mv: u32,
empty_mv: u32,
full_mv: u32,
absent_mv: u32,
}
impl BatteryGauge {
#[must_use]
pub const fn lipo() -> Self {
Self {
ema_mv: 0,
empty_mv: 3300,
full_mv: 4100,
absent_mv: 3000,
}
}
pub fn sample(&mut self, source: &mut impl BatterySource) -> PowerSnapshot {
let millivolts = source.read_millivolts();
let external_power = source.external_power();
self.update(millivolts, external_power)
}
pub fn update(
&mut self,
millivolts: Option<u32>,
external_power: ExternalPowerState,
) -> PowerSnapshot {
let battery = millivolts
.filter(|millivolts| *millivolts >= self.absent_mv)
.map(|millivolts| {
self.ema_mv = if self.ema_mv == 0 {
millivolts
} else {
(self.ema_mv * 7 + millivolts) / 8
};
let span = self.full_mv - self.empty_mv;
BatteryPercent::saturating(
(self.ema_mv.saturating_sub(self.empty_mv) * 100 / span).min(100) as u8,
)
});
PowerSnapshot::new(battery, external_power)
}
}
pub struct NoBattery;
impl BatterySource for NoBattery {
fn read_millivolts(&mut self) -> Option<u32> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn battery_percent_saturates_at_full() {
assert_eq!(BatteryPercent::saturating(99).get(), 99);
assert_eq!(BatteryPercent::saturating(101).get(), 100);
}
#[test]
fn external_power_survives_an_absent_battery_reading() {
let snapshot = BatteryGauge::lipo().update(
None,
ExternalPowerState::Present {
charging: ChargingState::Unknown,
},
);
assert_eq!(snapshot.battery(), None);
assert_eq!(
snapshot.external_power(),
ExternalPowerState::Present {
charging: ChargingState::Unknown,
}
);
}
struct TrendProbe {
read: bool,
}
impl BatterySource for TrendProbe {
fn read_millivolts(&mut self) -> Option<u32> {
self.read = true;
Some(3_700)
}
fn external_power(&mut self) -> ExternalPowerState {
assert!(self.read, "power evidence must inspect the current sample");
ExternalPowerState::Unknown
}
}
#[test]
fn source_power_evidence_observes_the_current_voltage_sample() {
let mut source = TrendProbe { read: false };
let snapshot = BatteryGauge::lipo().sample(&mut source);
assert_eq!(snapshot.battery(), Some(BatteryPercent::saturating(50)));
assert_eq!(snapshot.external_power(), ExternalPowerState::Unknown);
}
}