1use 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, };
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}