A personal rust firmware for the Badger 2040 W
at main 323 lines 12 kB view raw
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(&current_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}