A personal rust firmware for the Badger 2040 W
1pub mod display_image;
2
3use core::{
4 cell::RefCell,
5 sync::atomic::{AtomicBool, AtomicU32, AtomicU8},
6};
7use defmt::*;
8use display_image::get_current_image;
9use embassy_embedded_hal::shared_bus::asynch::spi::SpiDevice;
10use embassy_rp::gpio;
11use embassy_rp::gpio::Input;
12use embassy_sync::blocking_mutex::{self, raw::CriticalSectionRawMutex};
13use embassy_time::{Delay, Duration, Timer};
14use embedded_graphics::{
15 image::Image,
16 mono_font::{ascii::*, MonoTextStyle},
17 pixelcolor::BinaryColor,
18 prelude::*,
19 primitives::{PrimitiveStyle, PrimitiveStyleBuilder, Rectangle},
20 text::Text,
21};
22use embedded_text::{
23 alignment::HorizontalAlignment,
24 style::{HeightMode, TextBoxStyleBuilder},
25 TextBox,
26};
27use gpio::Output;
28use heapless::{String, Vec};
29use tinybmp::Bmp;
30use uc8151::LUT;
31use uc8151::WIDTH;
32use uc8151::{asynch::Uc8151, HEIGHT};
33use {defmt_rtt as _, panic_probe as _};
34
35use crate::{
36 env::env_value,
37 helpers::{easy_format, easy_format_str},
38 Spi0Bus,
39};
40
41pub type RecentWifiNetworksVec = Vec<String<32>, 4>;
42
43//Display state
44pub static SCREEN_TO_SHOW: blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<Screen>> =
45 blocking_mutex::Mutex::new(RefCell::new(Screen::Badge));
46pub static RECENT_WIFI_NETWORKS: blocking_mutex::Mutex<
47 CriticalSectionRawMutex,
48 RefCell<RecentWifiNetworksVec>,
49> = blocking_mutex::Mutex::new(RefCell::new(RecentWifiNetworksVec::new()));
50
51pub static FORCE_SCREEN_REFRESH: AtomicBool = AtomicBool::new(true);
52pub static DISPLAY_CHANGED: AtomicBool = AtomicBool::new(false);
53pub static CURRENT_IMAGE: AtomicU8 = AtomicU8::new(0);
54pub static CHANGE_IMAGE: AtomicBool = AtomicBool::new(true);
55pub static WIFI_COUNT: AtomicU32 = AtomicU32::new(0);
56pub static RTC_TIME_STRING: blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<String<8>>> =
57 blocking_mutex::Mutex::new(RefCell::new(String::<8>::new()));
58pub static TEMP: AtomicU8 = AtomicU8::new(0);
59pub static HUMIDITY: AtomicU8 = AtomicU8::new(0);
60
61#[derive(Debug, Clone, Copy, PartialEq, defmt::Format)]
62pub enum Screen {
63 Badge,
64 WifiList,
65}
66
67#[embassy_executor::task]
68pub async fn run_the_display(
69 spi_bus: &'static Spi0Bus,
70 cs: Output<'static>,
71 dc: Output<'static>,
72 busy: Input<'static>,
73 reset: Output<'static>,
74) {
75 let spi_dev = SpiDevice::new(&spi_bus, cs);
76
77 let mut display = Uc8151::new(spi_dev, dc, busy, reset, Delay);
78
79 display.reset().await;
80
81 // Initialise display with speed
82 let _ = display.setup(LUT::Medium).await;
83
84 // Note we're setting the Text color to `Off`. The driver is set up to treat Off as Black so that BMPs work as expected.
85 let character_style = MonoTextStyle::new(&FONT_9X18_BOLD, BinaryColor::Off);
86 let textbox_style = TextBoxStyleBuilder::new()
87 .height_mode(HeightMode::FitToText)
88 .alignment(HorizontalAlignment::Left)
89 .paragraph_spacing(6)
90 .build();
91
92 // Bounding box for our text. Fill it with the opposite color so we can read the text.
93 let name_and_detail_bounds = Rectangle::new(Point::new(0, 40), Size::new(WIDTH - 75, 0));
94 name_and_detail_bounds
95 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
96 .draw(&mut display)
97 .unwrap();
98 info!("Name: {}", env_value("NAME"));
99 info!("Details: {}", env_value("DETAILS"));
100 let mut name_and_details_buffer = [0; 128];
101 let name_and_details = easy_format_str(
102 format_args!("{}\n{}", env_value("NAME"), env_value("DETAILS")),
103 &mut name_and_details_buffer,
104 );
105
106 let name_and_detail_box = TextBox::with_textbox_style(
107 &name_and_details.unwrap(),
108 name_and_detail_bounds,
109 character_style,
110 textbox_style,
111 );
112
113 // let _ = display.update().await;
114
115 //Each cycle is half a second
116 let cycle: Duration = Duration::from_millis(500);
117
118 //New start every 120 cycles or 60 seconds
119 let cycles_to_clear_at: i32 = 120;
120 let mut cycles_since_last_clear = 0;
121 let mut current_screen = Screen::Badge;
122 loop {
123 let mut force_screen_refresh =
124 FORCE_SCREEN_REFRESH.load(core::sync::atomic::Ordering::Relaxed);
125 //Timed based display events
126 if DISPLAY_CHANGED.load(core::sync::atomic::Ordering::Relaxed) {
127 let clear_rectangle = Rectangle::new(Point::new(0, 0), Size::new(WIDTH, HEIGHT));
128 clear_rectangle
129 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
130 .draw(&mut display)
131 .unwrap();
132 let _ = display.update().await;
133 DISPLAY_CHANGED.store(false, core::sync::atomic::Ordering::Relaxed);
134 force_screen_refresh = true;
135 }
136
137 SCREEN_TO_SHOW.lock(|x| current_screen = *x.borrow());
138 // info!("Current Screen: {:?}", current_screen);
139 if current_screen == Screen::Badge {
140 if force_screen_refresh {
141 // Draw the text box.
142 name_and_detail_box.draw(&mut display).unwrap();
143 }
144
145 //Updates the top bar
146 //Runs every 60 cycles/30 seconds and first run
147 if cycles_since_last_clear % 60 == 0 || force_screen_refresh {
148 let count = WIFI_COUNT.load(core::sync::atomic::Ordering::Relaxed);
149 info!("Wifi count: {}", count);
150 let temp = TEMP.load(core::sync::atomic::Ordering::Relaxed);
151 let humidity = HUMIDITY.load(core::sync::atomic::Ordering::Relaxed);
152 let top_text: String<64> = easy_format::<64>(format_args!(
153 "{}F {}% Wifi found: {}",
154 temp, humidity, count
155 ));
156 let top_bounds = Rectangle::new(Point::new(0, 0), Size::new(WIDTH, 24));
157 top_bounds
158 .into_styled(
159 PrimitiveStyleBuilder::default()
160 .stroke_color(BinaryColor::Off)
161 .fill_color(BinaryColor::On)
162 .stroke_width(1)
163 .build(),
164 )
165 .draw(&mut display)
166 .unwrap();
167
168 Text::new(top_text.as_str(), Point::new(8, 16), character_style)
169 .draw(&mut display)
170 .unwrap();
171
172 // Draw the text box.
173 let result = display.partial_update(top_bounds.try_into().unwrap()).await;
174 match result {
175 Ok(_) => {}
176 Err(_) => {
177 info!("Error updating display");
178 }
179 }
180 }
181
182 //Runs every 120 cycles/60 seconds and first run
183 if cycles_since_last_clear == 0 || force_screen_refresh {
184 let mut time_text: String<8> = String::<8>::new();
185
186 let time_box_rectangle_location = Point::new(0, 96);
187 RTC_TIME_STRING.lock(|x| {
188 time_text.push_str(x.borrow().as_str()).unwrap();
189 });
190
191 //The bounds of the box for time and refresh area
192 let time_bounds = Rectangle::new(time_box_rectangle_location, Size::new(88, 24));
193 time_bounds
194 .into_styled(
195 PrimitiveStyleBuilder::default()
196 .stroke_color(BinaryColor::Off)
197 .fill_color(BinaryColor::On)
198 .stroke_width(1)
199 .build(),
200 )
201 .draw(&mut display)
202 .unwrap();
203
204 //Adding a y offset to the box location to fit inside the box
205 Text::new(
206 time_text.as_str(),
207 (
208 time_box_rectangle_location.x + 8,
209 time_box_rectangle_location.y + 16,
210 )
211 .into(),
212 character_style,
213 )
214 .draw(&mut display)
215 .unwrap();
216
217 let result = display
218 .partial_update(time_bounds.try_into().unwrap())
219 .await;
220 match result {
221 Ok(_) => {}
222 Err(_) => {
223 info!("Error updating display");
224 }
225 }
226 }
227
228 //Manually triggered display events
229
230 if CHANGE_IMAGE.load(core::sync::atomic::Ordering::Relaxed) || force_screen_refresh {
231 let current_image = get_current_image();
232 let tga: Bmp<BinaryColor> = Bmp::from_slice(¤t_image.image()).unwrap();
233 let image = Image::new(&tga, current_image.image_location());
234 //clear image location by writing a white rectangle over previous image location
235 let clear_rectangle = Rectangle::new(
236 current_image.previous().image_location(),
237 Size::new(157, 101),
238 );
239 clear_rectangle
240 .into_styled(PrimitiveStyle::with_fill(BinaryColor::On))
241 .draw(&mut display)
242 .unwrap();
243
244 let _ = image.draw(&mut display);
245 //TODO need to look up the reginal area display
246 let _ = display.update().await;
247 CHANGE_IMAGE.store(false, core::sync::atomic::Ordering::Relaxed);
248 }
249 } else {
250 if force_screen_refresh {
251 let top_bounds = Rectangle::new(Point::new(0, 0), Size::new(WIDTH, 24));
252 top_bounds
253 .into_styled(
254 PrimitiveStyleBuilder::default()
255 .stroke_color(BinaryColor::Off)
256 .fill_color(BinaryColor::On)
257 .stroke_width(1)
258 .build(),
259 )
260 .draw(&mut display)
261 .unwrap();
262
263 let top_text: String<64> = easy_format::<64>(format_args!(
264 "Wifi found: {}",
265 WIFI_COUNT.load(core::sync::atomic::Ordering::Relaxed)
266 ));
267
268 Text::new(top_text.as_str(), Point::new(8, 16), character_style)
269 .draw(&mut display)
270 .unwrap();
271
272 let result = display.partial_update(top_bounds.try_into().unwrap()).await;
273 match result {
274 Ok(_) => {}
275 Err(_) => {
276 info!("Error updating display");
277 }
278 }
279
280 //write the wifi list
281 let mut y_offset = 24;
282 let wifi_list = RECENT_WIFI_NETWORKS.lock(|x| x.borrow().clone());
283 for wifi in wifi_list.iter() {
284 // let wifi_text: String<64> = easy_format::<64>(format_args!("{}", wifi));
285 let wifi_bounds = Rectangle::new(Point::new(0, y_offset), Size::new(WIDTH, 24));
286 wifi_bounds
287 .into_styled(
288 PrimitiveStyleBuilder::default()
289 .stroke_color(BinaryColor::Off)
290 .fill_color(BinaryColor::On)
291 .stroke_width(1)
292 .build(),
293 )
294 .draw(&mut display)
295 .unwrap();
296
297 Text::new(wifi.trim(), Point::new(8, y_offset + 16), character_style)
298 .draw(&mut display)
299 .unwrap();
300
301 let result = display
302 .partial_update(wifi_bounds.try_into().unwrap())
303 .await;
304 match result {
305 Ok(_) => {}
306 Err(_) => {
307 info!("Error updating display");
308 }
309 }
310 y_offset += 24;
311 }
312 }
313 }
314
315 cycles_since_last_clear += 1;
316 if cycles_since_last_clear >= cycles_to_clear_at {
317 cycles_since_last_clear = 0;
318 }
319 FORCE_SCREEN_REFRESH.store(false, core::sync::atomic::Ordering::Relaxed);
320 // info!("Display Cycle: {}", cycles_since_last_clear);
321 Timer::after(cycle).await;
322 }
323}