kywy/
battery.rs

1// SPDX-FileCopyrightText: 2025 KOINSLOT Inc.
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5//! Battery monitor for the Koinslot device. With battery icon drawing capabilities.
6
7use embassy_rp::{
8    Peri,
9    adc::{self, Adc, Async, Channel, Config as AdcConfig},
10    bind_interrupts,
11    gpio::{Input, Pull},
12    peripherals::{ADC, PIN_10, PIN_11, PIN_26},
13};
14use embedded_graphics::{
15    draw_target::DrawTarget,
16    geometry::{OriginDimensions, Point, Size},
17    image::Image,
18    pixelcolor::BinaryColor,
19    prelude::*,
20};
21use embedded_iconoir::prelude::*;
22use heapless::Vec;
23
24bind_interrupts!(struct Irqs {
25    ADC_IRQ_FIFO => adc::InterruptHandler;
26});
27
28#[derive(Debug, Copy, Clone, Eq, PartialEq)]
29pub enum BatteryStatus {
30    Charging,
31    Charged,
32    NotCharging,
33}
34
35const VOLTAGE_AVG_SAMPLES: usize = 8;
36
37pub struct BatteryMonitor<'a> {
38    adc: Adc<'a, Async>,
39    channel: Channel<'a>,
40    charging: Input<'a>,
41    standby: Input<'a>,
42    position: Point,
43    color: BinaryColor,
44    voltage_buffer: Vec<u16, VOLTAGE_AVG_SAMPLES>,
45    last_percent: u8,
46}
47
48impl<'a> BatteryMonitor<'a> {
49    pub async fn new(
50        adc_pin: Peri<'a, PIN_26>,
51        charging_pin: Peri<'a, PIN_10>,
52        standby_pin: Peri<'a, PIN_11>,
53        adc_periph: Peri<'a, ADC>,
54        position: Point,
55        color: BinaryColor,
56    ) -> Self {
57        let adc = Adc::new(adc_periph, Irqs, AdcConfig::default());
58        let channel = Channel::new_pin(adc_pin, Pull::None);
59        let charging = Input::new(charging_pin, Pull::Up);
60        let standby = Input::new(standby_pin, Pull::Up);
61        let mut voltage_buffer: Vec<u16, VOLTAGE_AVG_SAMPLES> = Vec::new();
62
63        let mut temp_monitor = BatteryMonitor {
64            adc,
65            channel,
66            charging,
67            standby,
68            position,
69            color,
70            voltage_buffer: Vec::new(),
71            last_percent: 100, // temporary
72        };
73
74        for _ in 0..VOLTAGE_AVG_SAMPLES {
75            let raw = temp_monitor
76                .adc
77                .read(&mut temp_monitor.channel)
78                .await
79                .unwrap_or(0);
80            let adc_mv = raw as u32 * 3300 / 4095;
81            let mv = (adc_mv * 2) as u16;
82            voltage_buffer.push(mv).ok();
83        }
84
85        let initial_mv = *voltage_buffer.last().unwrap_or(&4200);
86        let initial_percent = Self::voltage_to_percent(initial_mv);
87
88        BatteryMonitor {
89            adc: temp_monitor.adc,
90            channel: temp_monitor.channel,
91            charging: temp_monitor.charging,
92            standby: temp_monitor.standby,
93            position,
94            color,
95            voltage_buffer,
96            last_percent: initial_percent,
97        }
98    }
99
100    pub fn move_to(&mut self, new_position: Point) {
101        self.position = new_position;
102    }
103
104    fn update_voltage_buffer(&mut self, mv: u16) -> u16 {
105        if self.voltage_buffer.len() == VOLTAGE_AVG_SAMPLES {
106            self.voltage_buffer.remove(0);
107        }
108        self.voltage_buffer.push(mv).ok();
109        let sum: u32 = self.voltage_buffer.iter().copied().map(|v| v as u32).sum();
110        (sum / self.voltage_buffer.len() as u32) as u16
111    }
112
113    pub async fn read_voltage_mv(&mut self) -> u16 {
114        let raw = self.adc.read(&mut self.channel).await.unwrap_or(0);
115        let adc_mv = raw as u32 * 3300 / 4095;
116        let mv = (adc_mv * 2) as u16;
117        self.update_voltage_buffer(mv)
118    }
119
120    pub async fn battery_percentage(&mut self) -> u8 {
121        let mv = self.read_voltage_mv().await;
122        let raw_percent = Self::voltage_to_percent(mv);
123
124        let hysteresis = 20;
125        if (raw_percent as i16 - self.last_percent as i16).abs() > hysteresis {
126            self.last_percent = raw_percent;
127        }
128
129        self.last_percent
130    }
131
132    fn voltage_to_percent(mv: u16) -> u8 {
133        match mv {
134            v if v >= 4200 => 100,
135            v if v >= 3900 => 85 + ((v - 3900) * 15 / 300) as u8,
136            v if v >= 3600 => 60 + ((v - 3600) * 25 / 300) as u8,
137            v if v >= 3300 => 25 + ((v - 3300) * 35 / 300) as u8,
138            v if v >= 3100 => 5 + ((v - 3100) * 20 / 200) as u8,
139            v if v >= 3000 => ((v - 3000) * 5 / 100) as u8,
140            _ => 0,
141        }
142    }
143
144    pub fn status(&self) -> BatteryStatus {
145        let charging = self.charging.is_low();
146        let standby = self.standby.is_low();
147
148        match (charging, standby) {
149            (true, _) => BatteryStatus::Charging,
150            (false, true) => BatteryStatus::Charged,
151            _ => BatteryStatus::NotCharging,
152        }
153    }
154
155    pub async fn draw_async<D>(&mut self, display: &mut D) -> Result<(), D::Error>
156    where
157        D: DrawTarget<Color = BinaryColor>,
158    {
159        let percent = self.battery_percentage().await;
160        let status = self.status();
161        let position = self.position;
162        let color = self.color;
163
164        match status {
165            BatteryStatus::Charging => {
166                let icon = icons::size16px::system::BatteryCharging::new(color);
167                Image::new(&icon, position).draw(display)
168            }
169            BatteryStatus::Charged => {
170                let icon = icons::size16px::system::BatteryFull::new(color);
171                Image::new(&icon, position).draw(display)
172            }
173            BatteryStatus::NotCharging => match percent {
174                85..=100 => Image::new(&icons::size16px::system::BatteryFull::new(color), position)
175                    .draw(display),
176                60..=84 => Image::new(
177                    &icons::size16px::system::BatterySevenFive::new(color),
178                    position,
179                )
180                .draw(display),
181                25..=59 => Image::new(
182                    &icons::size16px::system::BatteryFiveZero::new(color),
183                    position,
184                )
185                .draw(display),
186                5..=24 => Image::new(
187                    &icons::size16px::system::BatteryTwoFive::new(color),
188                    position,
189                )
190                .draw(display),
191                _ => Image::new(&icons::size16px::system::BatteryEmpty::new(color), position)
192                    .draw(display),
193            },
194        }
195    }
196}
197
198impl OriginDimensions for BatteryMonitor<'_> {
199    fn size(&self) -> Size {
200        Size::new(16, 16)
201    }
202}