|  | 
|  | 1 | +use crate::event::DeviceEvent; | 
|  | 2 | +use esp_idf_svc::hal::adc::attenuation::DB_11; | 
|  | 3 | +use esp_idf_svc::hal::adc::oneshot::config::{AdcChannelConfig, Calibration}; | 
|  | 4 | +use esp_idf_svc::hal::adc::oneshot::{AdcChannelDriver, AdcDriver}; | 
|  | 5 | +use esp_idf_svc::hal::adc::Resolution; | 
|  | 6 | +use esp_idf_svc::hal::gpio::ADCPin; | 
|  | 7 | +use esp_idf_svc::hal::peripheral::Peripheral; | 
|  | 8 | +use log::info; | 
|  | 9 | +use std::sync::mpsc; | 
|  | 10 | +use std::thread; | 
|  | 11 | +use std::thread::{Scope, ScopedJoinHandle}; | 
|  | 12 | +use std::time::Duration; | 
|  | 13 | + | 
|  | 14 | +mod capacity_curve { | 
|  | 15 | +    use log::info; | 
|  | 16 | + | 
|  | 17 | +    /// Naive const equivalent of `f.powi(n)`. | 
|  | 18 | +    /// Performance is not a problem, since this is evaluated only at compile time. | 
|  | 19 | +    /// Thanks to RFC 5314, basic floating point arithmetic is now stable in const | 
|  | 20 | +    const fn pow(f: f32, n: i32) -> f32 { | 
|  | 21 | +        let mut out: f32 = 1.0; | 
|  | 22 | +        let mut i = 0; | 
|  | 23 | +        while i < n { | 
|  | 24 | +            out *= f; | 
|  | 25 | +            i += 1; | 
|  | 26 | +        } | 
|  | 27 | +        out | 
|  | 28 | +    } | 
|  | 29 | + | 
|  | 30 | +    /// Maps battery voltage (in mV) to battery capacity (in %). | 
|  | 31 | +    /// Calculated from battery datasheet using https://mycurvefit.com. | 
|  | 32 | +    const fn to_capacity(voltage: u16) -> u8 { | 
|  | 33 | +        const A: f32 = -0.22; | 
|  | 34 | +        // `B` is rounded from ~24.62 in order to use | 
|  | 35 | +        // deterministic version of `f32::powi` | 
|  | 36 | +        // (which is much easier to implement than `f32::powf`) | 
|  | 37 | +        const B: i32 = 25; // ~24.62; | 
|  | 38 | +        const C: f32 = 3662.97; | 
|  | 39 | +        const D: f32 = 104.42; | 
|  | 40 | + | 
|  | 41 | +        let battery_level = (D + (A - D) / (1.0 + pow(voltage as f32 / C, B))) as i8; | 
|  | 42 | +        // `i8::clamp` as of March 2025 is not const | 
|  | 43 | +        if battery_level > 100 { | 
|  | 44 | +            return 100; | 
|  | 45 | +        } else if battery_level < 0 { | 
|  | 46 | +            return 0; | 
|  | 47 | +        } | 
|  | 48 | +        battery_level as u8 | 
|  | 49 | +    } | 
|  | 50 | + | 
|  | 51 | +    /// `LOOKUP_RESOLUTION`mV per step | 
|  | 52 | +    const LOOKUP_RESOLUTION: u16 = 10; | 
|  | 53 | +    const LOOKUP_SIZE: usize = 121; | 
|  | 54 | +    const LOOKUP_START: u16 = 3000; | 
|  | 55 | +    const LOOKUP_END: u16 = LOOKUP_START + (LOOKUP_SIZE as u16 - 1) * LOOKUP_RESOLUTION; | 
|  | 56 | +    /// Maps voltage to capacity % | 
|  | 57 | +    /// Where `0` corresponds to `LOOKUP_START`mV | 
|  | 58 | +    /// and `LOOKUP_SIZE-1` corresponds to `LOOKUP_END`mV | 
|  | 59 | +    const CAPACITY_LOOKUP: [u8; LOOKUP_SIZE] = { | 
|  | 60 | +        let mut lookup: [u8; LOOKUP_SIZE] = [0; LOOKUP_SIZE]; | 
|  | 61 | +        // as of March 2025, for loops are not allowed in const context | 
|  | 62 | +        let mut i = 0; | 
|  | 63 | +        while i < LOOKUP_SIZE { | 
|  | 64 | +            lookup[i] = to_capacity(LOOKUP_START + i as u16 * LOOKUP_RESOLUTION); | 
|  | 65 | +            i += 1; | 
|  | 66 | +        } | 
|  | 67 | +        lookup | 
|  | 68 | +    }; | 
|  | 69 | + | 
|  | 70 | +    /// Battery is connected to ADC pin through a voltage divider: | 
|  | 71 | +    /// `V_ADC = R7/(R6+R7) * V_BAT` | 
|  | 72 | +    /// `ADC_MULTIPLIER = (R6+R7)/R7` | 
|  | 73 | +    const ADC_MULTIPLIER: u8 = 2; | 
|  | 74 | +    #[inline] | 
|  | 75 | +    fn get_battery_voltage(adc_reading: u16) -> u16 { | 
|  | 76 | +        adc_reading * ADC_MULTIPLIER as u16 | 
|  | 77 | +    } | 
|  | 78 | + | 
|  | 79 | +    /// If battery is charging, the ADC reading will show very high readings, above 4.2V. | 
|  | 80 | +    /// This would cause the battery level to always display 100%, when charging. | 
|  | 81 | +    /// There is no way to change this behaviour without hardware modifications. | 
|  | 82 | +    /// We can still naively check for unusually high voltage and assume that battery is charging. | 
|  | 83 | +    const CHARGING_THRESHOLD: u16 = 4300; | 
|  | 84 | + | 
|  | 85 | +    /// Maps `adc_reading` to battery capacity (in %). | 
|  | 86 | +    /// Returns `None`, if battery is charging. | 
|  | 87 | +    #[inline] | 
|  | 88 | +    pub fn get_battery_level(adc_reading: u16) -> Option<u8> { | 
|  | 89 | +        let voltage = get_battery_voltage(adc_reading); | 
|  | 90 | +        info!("[Battery reader] Voltage: {:.2}V", voltage as f32 / 1000.0); | 
|  | 91 | +        if voltage > CHARGING_THRESHOLD { | 
|  | 92 | +            return None; | 
|  | 93 | +        } | 
|  | 94 | +        let lookup_index = | 
|  | 95 | +            ((voltage.clamp(LOOKUP_START, LOOKUP_END) - LOOKUP_START) / LOOKUP_RESOLUTION) as usize; | 
|  | 96 | +        Some(CAPACITY_LOOKUP[lookup_index]) | 
|  | 97 | +    } | 
|  | 98 | +} | 
|  | 99 | + | 
|  | 100 | +pub fn spawn_reader_thread<'scope, T>( | 
|  | 101 | +    scope: &'scope Scope<'scope, '_>, | 
|  | 102 | +    adc: impl Peripheral<P = T::Adc> + 'scope + Send, | 
|  | 103 | +    battery_pin: impl Peripheral<P = T> + 'scope + Send, | 
|  | 104 | +    sender: mpsc::Sender<DeviceEvent>, | 
|  | 105 | +) -> Result<ScopedJoinHandle<'scope, ()>, std::io::Error> | 
|  | 106 | +where | 
|  | 107 | +    T: ADCPin, | 
|  | 108 | +{ | 
|  | 109 | +    thread::Builder::new() | 
|  | 110 | +        .stack_size(8192) | 
|  | 111 | +        .spawn_scoped(scope, move || { | 
|  | 112 | +            info!("[Battery reader] Starting..."); | 
|  | 113 | +            let adc_driver = AdcDriver::new(adc).unwrap(); | 
|  | 114 | +            let mut bat_adc_channel = AdcChannelDriver::new( | 
|  | 115 | +                &adc_driver, | 
|  | 116 | +                battery_pin, | 
|  | 117 | +                &AdcChannelConfig { | 
|  | 118 | +                    attenuation: DB_11, | 
|  | 119 | +                    calibration: Calibration::Line, | 
|  | 120 | +                    resolution: Resolution::Resolution12Bit, | 
|  | 121 | +                }, | 
|  | 122 | +            ) | 
|  | 123 | +            .unwrap(); | 
|  | 124 | +            loop { | 
|  | 125 | +                if let Ok(reading) = adc_driver.read(&mut bat_adc_channel) { | 
|  | 126 | +                    let battery_level = capacity_curve::get_battery_level(reading); | 
|  | 127 | +                    sender | 
|  | 128 | +                        .send(DeviceEvent::BatteryLevel { | 
|  | 129 | +                            data: battery_level, | 
|  | 130 | +                        }) | 
|  | 131 | +                        .ok(); | 
|  | 132 | +                    match battery_level { | 
|  | 133 | +                        Some(_) => { | 
|  | 134 | +                            thread::sleep(Duration::from_secs(60)); | 
|  | 135 | +                        } | 
|  | 136 | +                        None => { | 
|  | 137 | +                            thread::sleep(Duration::from_secs(2)); | 
|  | 138 | +                        } | 
|  | 139 | +                    } | 
|  | 140 | +                } | 
|  | 141 | +            } | 
|  | 142 | +        }) | 
|  | 143 | +} | 
0 commit comments