diff --git a/Cargo.toml b/Cargo.toml index c20e28d..209aee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ max-usb-power = [] wokwi = ["max-usb-power"] mpu = ["dep:mpu6050-dmp"] gps = ["dep:nmea"] -oled = ["dep:ssd1306", "dep:embedded-graphics"] +oled = ["dep:ssd1306"] demo = [] [dependencies] @@ -77,7 +77,7 @@ esp-wifi = { version = "0.15.0", optional = true, features = [ "ble" ] } bleps = { git = "https://github.com/bjoernQ/bleps", optional = true, package = "bleps", rev = "a5148d8ae679e021b78f53fd33afb8bb35d0b62e", features = [ "macros", "async"] } -embedded-graphics = { version = "0.8.1", features = ["nalgebra_support"], optional = true} +embedded-graphics = { version = "0.8.1", features = ["nalgebra_support"] } ssd1306 = { version = "0.10.0", features = ["async"], optional = true } # Sensors diff --git a/src/animation.rs b/src/animation.rs index b7219a8..2a1df0c 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -4,7 +4,7 @@ use figments_render::output::Brightness; use core::{fmt::Debug, ops::{Deref, DerefMut}}; use log::*; -use crate::display::DisplayControls; +use crate::graphics::display::DisplayControls; #[derive(Default, Debug, Clone, Copy)] pub struct Animation { diff --git a/src/bin/main.rs b/src/bin/main.rs index 7bfcd9e..0e2dc59 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -8,6 +8,7 @@ use core::ptr::addr_of_mut; +use alloc::sync::Arc; use embassy_executor::Spawner; use embassy_time::{Instant, Timer}; @@ -19,7 +20,7 @@ use esp_hal::{ use esp_hal_embassy::{Executor, InterruptExecutor}; use log::*; -use renderbug_embassy::{logging::RenderbugLogger, tasks::{safetyui::{safety_ui_main, SafetyUi}, ui::UiSurfacePool}}; +use renderbug_embassy::{logging::RenderbugLogger, tasks::{oled::{oled_ui, OledUI, OledUiSurfacePool}, safetyui::{safety_ui_main, SafetyUi}, ui::UiSurfacePool}}; use renderbug_embassy::events::BusGarage; use static_cell::StaticCell; use esp_backtrace as _; @@ -29,6 +30,8 @@ use renderbug_embassy::tasks::{ ui::{Ui, ui_main} }; +use figments_render::output::OutputAsync; + extern crate alloc; // This creates a default app-descriptor required by the esp-idf bootloader. @@ -68,6 +71,11 @@ async fn main(spawner: Spawner) { info!("Setting up rendering pipeline"); let mut surfaces = UiSurfacePool::default(); let ui = Ui::new(&mut surfaces); + + let mut oled_surfaces = OledUiSurfacePool::default(); + let oled_uniforms = Default::default(); + let oledui = OledUI::new(&mut oled_surfaces, garage.oled_display.clone(), Arc::clone(&oled_uniforms)); + let mut safety_surfaces = UiSurfacePool::default(); let safety_ui = SafetyUi::new(&mut safety_surfaces, garage.display.clone()); @@ -102,14 +110,15 @@ async fn main(spawner: Spawner) { #[cfg(feature="oled")] { use esp_hal::i2c::master::{Config, I2c}; + use renderbug_embassy::graphics::ssd1306::SsdOutput; let rst = Output::new(peripherals.GPIO21, esp_hal::gpio::Level::Low, OutputConfig::default()); let i2c = I2c::new( peripherals.I2C0, Config::default().with_frequency(Rate::from_khz(400)) ).unwrap().with_scl(peripherals.GPIO18).with_sda(peripherals.GPIO17).into_async(); - - spawner.must_spawn(renderbug_embassy::tasks::oled::oled_task(i2c, rst, garage.telemetry.dyn_subscriber().unwrap())); + let output = SsdOutput::new(i2c, rst, garage.oled_display.clone()).await; + spawner.must_spawn(renderbug_embassy::tasks::oled_render::oled_render(output, oled_surfaces, oled_uniforms)); } #[cfg(feature="simulation")] @@ -136,6 +145,8 @@ async fn main(spawner: Spawner) { spawner.must_spawn(safety_ui_main(garage.notify.dyn_subscriber().unwrap(), safety_ui)); info!("Launching UI"); spawner.must_spawn(ui_main(garage.notify.dyn_subscriber().unwrap(), garage.telemetry.dyn_publisher().unwrap(), ui)); + info!("Launching OLED UI"); + spawner.must_spawn(oled_ui(garage.telemetry.dyn_subscriber().unwrap(), oledui)); #[cfg(feature="demo")] { warn!("Launching with demo sequencer"); diff --git a/src/events.rs b/src/events.rs index 78c6fb4..c93badf 100644 --- a/src/events.rs +++ b/src/events.rs @@ -3,7 +3,7 @@ use embassy_sync::{blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, use embassy_time::Duration; use nalgebra::{Vector2, Vector3}; -use crate::{display::DisplayControls, ego::engine::MotionState}; +use crate::{graphics::display::DisplayControls, ego::engine::MotionState}; #[derive(Clone, Copy, Default, Debug)] pub enum Scene { @@ -80,7 +80,8 @@ pub struct BusGarage { pub notify: PubSubChannel, pub predict: Channel, pub telemetry: PubSubChannel, - pub display: DisplayControls + pub display: DisplayControls, + pub oled_display: DisplayControls } impl Default for BusGarage { @@ -90,7 +91,8 @@ impl Default for BusGarage { notify: PubSubChannel::new(), predict: Channel::new(), telemetry: PubSubChannel::new(), - display: Default::default() + display: Default::default(), + oled_display: Default::default() } } } \ No newline at end of file diff --git a/src/display.rs b/src/graphics/display.rs similarity index 99% rename from src/display.rs rename to src/graphics/display.rs index 484b8ef..22a3002 100644 --- a/src/display.rs +++ b/src/graphics/display.rs @@ -159,13 +159,13 @@ impl Default for ControlData { } // A watch that indicates whether or not the rendering engine is running. If the display is off or sleeping, this will be false. -static RENDER_IS_RUNNING: Watch = Watch::new(); +static RENDER_IS_RUNNING: Watch = Watch::new(); // TODO: Implement something similar for a system-wide sleep mechanism pub struct DisplayControls { data: Arc, render_pause: Arc>, - render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 5> + render_run_receiver: Receiver<'static, CriticalSectionRawMutex, bool, 7> } impl Clone for DisplayControls { diff --git a/src/graphics/mod.rs b/src/graphics/mod.rs index 2575e87..47d52c6 100644 --- a/src/graphics/mod.rs +++ b/src/graphics/mod.rs @@ -1 +1,12 @@ -pub mod ssd1306; \ No newline at end of file +pub mod ssd1306; +pub mod display; +pub mod shaders; +pub mod oled_ui; + +mod images { + use embedded_graphics::{ + image::ImageRaw, + pixelcolor::BinaryColor + }; + include!("../../target/images.rs"); +} \ No newline at end of file diff --git a/src/graphics/oled_ui.rs b/src/graphics/oled_ui.rs new file mode 100644 index 0000000..542ee2d --- /dev/null +++ b/src/graphics/oled_ui.rs @@ -0,0 +1,182 @@ +use core::f32::consts::PI; +use core::fmt::Binary; + +use alloc::format; +use embedded_graphics::mono_font::ascii::*; +use embedded_graphics::mono_font::{MonoTextStyle, MonoTextStyleBuilder}; +use embedded_graphics::pixelcolor::BinaryColor; +use embedded_graphics::prelude::Size; +use embedded_graphics::primitives::{Line, PrimitiveStyle, PrimitiveStyleBuilder, StyledDrawable}; +use embedded_graphics::text::{Alignment, Text}; +use embedded_graphics::{image::Image, prelude::Point, Drawable}; +use figments::liber8tion::trig::sin8; +use figments::mappings::embedded_graphics::Matrix2DSpace; +use figments::{liber8tion::trig::cos8, mappings::embedded_graphics::EmbeddedGraphicsSampler}; +use figments::prelude::*; +use nalgebra::Vector2; +use micromath::F32Ext; +use embedded_graphics::geometry::OriginDimensions; + +use crate::graphics::images; +use crate::{ego::engine::MotionState, events::Scene}; + +#[derive(Default, Debug)] +pub struct OledUI { + pub scene: Scene, + pub motion: MotionState, + pub brakelight: bool, + pub headlight: bool, + pub gps_online: bool, + pub imu_online: bool, + pub velocity: f32, + pub location: Vector2, + pub sleep: bool, +} + +#[derive(Default, Debug)] +pub struct OledUniforms { + pub ui: OledUI, + pub frame: usize, + pub current_screen: Screen +} + +#[derive(Debug, Default, Clone, Copy)] +pub enum Screen { + #[default] + Blank, + Bootsplash, + Home, + Sleeping, + Waking +} + +impl Screen { + pub fn draw_screen<'a, T>(&self, sampler: &mut EmbeddedGraphicsSampler<'a, T>, state: &OledUniforms) where T: Sample<'a, Matrix2DSpace>, T::Output: PixelSink { + match self { + Screen::Blank => (), + Screen::Bootsplash => { + Image::new(&images::BOOT_LOGO, Point::zero()).draw(sampler).unwrap(); + const SPARKLE_COUNT: i32 = 8; + for n in 0..SPARKLE_COUNT { + let sparkle_center = Point::new(128u8.scale8(cos8(state.frame.wrapping_mul(n as usize))) as i32, 64u8.scale8(sin8(state.frame.wrapping_mul(n as usize) as u8)) as i32); + let offset = (state.frame / 2 % 32) as i32 - 16; + let rotation = PI * 2.0 * (sin8(state.frame) as f32 / 255.0); + let normal = Point::new((rotation.cos() * offset as f32) as i32, (rotation.sin() * offset as f32) as i32); + let cross_normal = Point::new((rotation.sin() * offset as f32) as i32, (rotation.cos() * offset as f32) as i32); + // Draw horizontal + Line::new(sparkle_center - normal, sparkle_center + normal) + .draw_styled(&SPARKLE_STYLE, sampler).unwrap(); + // Draw vertical + Line::new(sparkle_center - cross_normal, sparkle_center + cross_normal) + .draw_styled(&SPARKLE_STYLE, sampler).unwrap(); + } + }, + Screen::Home => { + // Status bar + // Sensor indicators + let gps_img = if state.ui.gps_online { + &images::GPS_ON + } else { + &images::GPS_OFF + }; + let imu_img = if state.ui.imu_online { + &images::IMU_ON + } else { + &images::IMU_OFF + }; + Image::new(gps_img, Point::zero()).draw(sampler).unwrap(); + Image::new(imu_img, Point::new((gps_img.size().width + 2) as i32, 0)).draw(sampler).unwrap(); + + #[cfg(feature="demo")] + Text::with_alignment("Demo", Point::new(128, 10), TEXT_STYLE, Alignment::Right) + .draw(sampler) + .unwrap(); + + #[cfg(feature="simulation")] + Text::with_alignment("Sim", Point::new(128, 10), TEXT_STYLE, Alignment::Right) + .draw(sampler) + .unwrap(); + + // Separates the status bar from the UI + Line::new(Point::new(0, 18), Point::new(128, 18)).draw_styled(&INACTIVE_STYLE, sampler).unwrap(); + + // Speed display at the top + Text::with_alignment(&format!("{}", state.ui.velocity), Point::new(128 / 2, 12), SPEED_STYLE, Alignment::Center) + .draw(sampler) + .unwrap(); + + // The main UI content + Image::new(&images::BIKE, Point::new((128 / 2 - images::BIKE.size().width / 2) as i32, 24)).draw(sampler).unwrap(); + + let headlight_img = if state.ui.headlight { + &images::HEADLIGHT_ON + } else { + &images::HEADLIGHT_OFF + }; + let brakelight_img = if state.ui.brakelight { + &images::BRAKELIGHT_ON + } else { + &images::BRAKELIGHT_OFF + }; + Image::new(headlight_img, Point::new(((128 / 2 - images::BIKE.size().width / 2) - 18) as i32, 28)).draw(sampler).unwrap(); + Image::new(brakelight_img, Point::new(((128 / 2 + images::BIKE.size().width / 2) + 2) as i32, 28)).draw(sampler).unwrap(); + + // TODO: Replace the state texts with cute animations or smth + // Current prediction from the motion engine + Text::with_alignment(&format!("{:?}", state.ui.motion), Point::new(128 / 2, 64 - 3), TEXT_STYLE, Alignment::Center) + .draw(sampler) + .unwrap(); + + // Current scene in the UI + Text::with_alignment(&format!("{:?}", state.ui.scene), Point::new(128 / 2, 64 - 13), TEXT_STYLE, Alignment::Center) + .draw(sampler) + .unwrap(); + }, + Screen::Sleeping => { + Text::with_alignment("so eepy Zzz...", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center) + .draw(sampler) + .unwrap(); + }, + Screen::Waking => { + Text::with_alignment("OwO hewwo again :D", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center) + .draw(sampler) + .unwrap(); + } + } + } +} + +impl> RenderSource for Screen { + fn render_to<'a, Smp>(&'a self, output: &'a mut Smp, uniforms: &OledUniforms) + where + Smp: Sample<'a, Matrix2DSpace, Output = HwPixel> { + let mut sampler = EmbeddedGraphicsSampler(output, embedded_graphics::primitives::Rectangle::new(Point::new(0, 0), Size::new(128, 64))); + self.draw_screen(&mut sampler, uniforms); + } +} + +const SPARKLE_STYLE: PrimitiveStyle = PrimitiveStyleBuilder::new() + .stroke_color(BinaryColor::On) + .stroke_width(2) + .build(); + +const TEXT_STYLE: MonoTextStyle = MonoTextStyleBuilder::new() + .font(&FONT_6X10) + .text_color(BinaryColor::On) + .build(); + +const LOGO_STYLE: MonoTextStyle = MonoTextStyleBuilder::new() + .font(&FONT_9X18_BOLD) + .text_color(BinaryColor::On) + .build(); + +const SPEED_STYLE: MonoTextStyle = MonoTextStyleBuilder::new() + .font(&FONT_10X20) + .text_color(BinaryColor::On) + .build(); + +const INACTIVE_STYLE: PrimitiveStyle = PrimitiveStyleBuilder::new() + .fill_color(BinaryColor::Off) + .stroke_color(BinaryColor::On) + .stroke_width(2) + .build(); \ No newline at end of file diff --git a/src/shaders.rs b/src/graphics/shaders.rs similarity index 98% rename from src/shaders.rs rename to src/graphics/shaders.rs index 8d86702..09188c4 100644 --- a/src/shaders.rs +++ b/src/graphics/shaders.rs @@ -3,7 +3,7 @@ use core::cmp::max; use figments::{liber8tion::{interpolate::{ease_in_out_quad}, noise::inoise8, trig::{cos8, sin8}}, prelude::*}; use rgb::Rgba; -use crate::display::{SegmentSpace, Uniforms}; +use crate::graphics::display::{SegmentSpace, Uniforms}; #[derive(Clone, Copy, Default)] pub struct Movement { diff --git a/src/graphics/ssd1306.rs b/src/graphics/ssd1306.rs index f325d5e..9a7e142 100644 --- a/src/graphics/ssd1306.rs +++ b/src/graphics/ssd1306.rs @@ -1,17 +1,25 @@ #![cfg(feature="oled")] -use core::{cmp::min, sync::atomic::{AtomicBool, AtomicU8}}; +use core::cmp::min; -use alloc::sync::Arc; use display_interface::DisplayError; use embedded_graphics::prelude::*; -use esp_hal::rng; +use esp_hal::{gpio::Output, i2c::master::I2c, Async}; use figments::{liber8tion::{interpolate::Fract8, noise}, mappings::embedded_graphics::Matrix2DSpace, prelude::*}; use embedded_graphics::pixelcolor::BinaryColor; use figments::pixels::PixelSink; -use figments_render::{gamma::GammaCurve, output::{Brightness, GammaCorrected, NullControls, OutputAsync}}; -use log::*; +use figments_render::output::OutputAsync; +use ssd1306::{prelude::DisplayRotation, size::DisplaySize128x64, I2CDisplayInterface, Ssd1306Async}; +use embassy_time::Delay; -pub struct SsdOutput([u8; 128 * 64 / 8], ssd1306::Ssd1306Async>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>, SsdControls); +use crate::graphics::display::DisplayControls; + +pub struct SsdOutput { + pixbuf: [u8; 128 * 64 / 8], + target: ssd1306::Ssd1306Async>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>, + controls: DisplayControls, + is_on: bool, + last_brightness: u8 +} #[derive(Copy, Clone)] pub struct SsdPixel(*mut u8, u8, Coordinates); @@ -42,7 +50,7 @@ impl PixelSink for SsdPixel { impl PixelBlend for SsdPixel { fn blend_pixel(self, overlay: BinaryColor, opacity: Fract8) -> Self { - let scale = 32; + let scale = 48; let x = self.2.x * scale; let y = self.2.y * scale; let stiple_idx = noise::inoise8(x as i16, y as i16); @@ -60,18 +68,6 @@ impl PixelBlend for SsdPixel { } } -static mut DUMMY: u8 = 0; - -impl Default for SsdPixel { - fn default() -> Self { - Self ( - unsafe { &mut DUMMY }, - 0, - Coordinates::new(0, 0) - ) - } -} - pub struct SsdSampler<'a> { buf: &'a mut SsdOutput, rect: figments::prelude::Rectangle, @@ -110,12 +106,23 @@ impl SsdOutput { let idx = (coords.y / 8 * 128 + coords.x) as usize; let bit = (coords.y % 8) as u8; //info!("sample {coords:?} {idx}"); - let pixref = unsafe { &mut self.0[idx] as *mut u8}; + let pixref = &mut self.pixbuf[idx] as *mut u8; Some(SsdPixel(pixref, bit, coords)) } - pub fn new(display: ssd1306::Ssd1306Async>, ssd1306::prelude::DisplaySize128x64, ssd1306::mode::BasicMode>) -> Self { - Self([0; 128 * 64 / 8], display, Default::default()) + pub async fn new(i2c: I2c<'static, Async>, mut reset_pin: Output<'static>, controls: DisplayControls) -> Self { + let interface = I2CDisplayInterface::new(i2c); + let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0); + display.reset(&mut reset_pin, &mut Delay).await.unwrap(); + display.init_with_addr_mode(ssd1306::command::AddrMode::Horizontal).await.unwrap(); + + Self { + pixbuf: [0; 128 * 64 / 8], + target: display, + controls, + last_brightness: 255, + is_on: true + } } } @@ -148,12 +155,6 @@ impl DrawTarget for SsdOutput { where I: IntoIterator> { for epix in pixels { - /*let point = figments::geometry::Rectangle::new( - epix.0.into(), epix.0.into() - ); - for (coords, pix) in self.sample(&point) { - pix.set_pixel(epix.1); - }*/ self.get_pixel(epix.0.into()).unwrap().set_pixel(epix.1); } Ok(()) @@ -166,7 +167,7 @@ impl DrawTarget for SsdOutput { area.top_left.into(), area.bottom_right().unwrap().into() ); - for (color, (coords, pix)) in colors.into_iter().zip(self.sample(&rect)) { + for (color, (_coords, pix)) in colors.into_iter().zip(self.sample(&rect)) { pix.set_pixel(color); } Ok(()) @@ -177,7 +178,7 @@ impl DrawTarget for SsdOutput { area.top_left.into(), area.bottom_right().unwrap().into() ); - for (coords, pix) in self.sample(&rect) { + for (_coords, pix) in self.sample(&rect) { pix.set_pixel(color); } Ok(()) @@ -185,9 +186,9 @@ impl DrawTarget for SsdOutput { fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> { if color.is_on() { - self.0.fill(255); + self.pixbuf.fill(255); } else { - self.0.fill(0); + self.pixbuf.fill(0); } Ok(()) } @@ -202,50 +203,24 @@ impl OriginDimensions for SsdOutput { impl<'a> OutputAsync<'a, Matrix2DSpace> for SsdOutput { type Error = DisplayError; - type Controls = SsdControls; + type Controls = DisplayControls; async fn commit_async(&mut self) -> Result<(), Self::Error> { - self.1.set_brightness(ssd1306::prelude::Brightness::custom(1, self.2.data.brightness.load(core::sync::atomic::Ordering::Relaxed))).await.unwrap(); - self.1.set_display_on(self.2.data.is_on.load(core::sync::atomic::Ordering::Relaxed)).await.unwrap(); - self.1.draw(&self.0).await + let new_brightness = self.controls.brightness(); + let new_power = self.controls.is_on(); + if self.is_on != new_power { + self.target.set_display_on(self.controls.is_on()).await.unwrap(); + self.is_on = new_power; + } + if self.last_brightness != new_brightness { + self.target.set_brightness(ssd1306::prelude::Brightness::custom(1, new_brightness)).await.unwrap(); + self.last_brightness = new_brightness; + } + + self.target.draw(&self.pixbuf).await } fn controls(&self) -> Option<&Self::Controls> { - Some(&self.2) + Some(&self.controls) } -} - -// TODO: We can probably just replace this entirely with a DisplayControls instance -#[derive(Debug)] -struct SsdControlData { - is_on: AtomicBool, - brightness: AtomicU8 -} - -impl Default for SsdControlData { - fn default() -> Self { - Self { - is_on: AtomicBool::new(true), - brightness: AtomicU8::new(255) - } - } -} - -#[derive(Default, Debug, Clone)] -pub struct SsdControls { - data: Arc -} - -impl Brightness for SsdControls { - fn set_brightness(&mut self, brightness: u8) { - self.data.brightness.store(brightness, core::sync::atomic::Ordering::Relaxed); - } - - fn set_on(&mut self, is_on: bool) { - self.data.is_on.store(is_on, core::sync::atomic::Ordering::Relaxed); - } -} - -impl GammaCorrected for SsdControls { - fn set_gamma(&mut self, gamma: GammaCurve) {} // Not handled } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index e1e410e..fa24809 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,8 @@ #![no_std] -pub mod display; pub mod backoff; pub mod events; pub mod tasks; -pub mod shaders; pub mod ego; pub mod animation; pub mod idle; diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index 7335e46..a9af01d 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -9,7 +9,7 @@ pub mod simulation; #[cfg(feature="demo")] pub mod demo; #[cfg(feature="oled")] -pub mod oled; +pub mod oled_render; // Prediction engines pub mod predict; @@ -18,4 +18,5 @@ pub mod motion; // Graphics stack pub mod ui; pub mod safetyui; -pub mod render; \ No newline at end of file +pub mod render; +pub mod oled; \ No newline at end of file diff --git a/src/tasks/oled.rs b/src/tasks/oled.rs index a4780e9..9c5a062 100644 --- a/src/tasks/oled.rs +++ b/src/tasks/oled.rs @@ -1,312 +1,113 @@ -use core::{cell::{Cell, RefCell}, cmp::min, f32::consts::PI, fmt::Binary, ops::DerefMut}; - -use alloc::{format, sync::Arc}; +use alloc::sync::Arc; use display_interface::DisplayError; -use embassy_executor::{raw, Spawner}; -use embassy_sync::{blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, mutex::Mutex, pubsub::DynSubscriber}; -use embassy_time::{Delay, Duration, Instant, Timer}; -use embedded_graphics::{image::Image, mono_font::{ascii::{FONT_6X10, FONT_10X20, FONT_9X18_BOLD}, MonoTextStyleBuilder}, pixelcolor::BinaryColor, prelude::*, primitives::{Line, PrimitiveStyleBuilder, Rectangle, StyledDrawable}, text::{Alignment, Baseline, Text}}; -use esp_hal::{i2c::master::I2c, Async, gpio::Output}; -use figments::{liber8tion::{interpolate::{ease_in_out_quad, scale8, Fract8}, trig::{cos8, sin8}}, mappings::{embedded_graphics::{EmbeddedGraphicsSampler, Matrix2DSpace}, linear::LinearSpace}, pixels::{PixelBlend, PixelSink}, prelude::{CoordinateSpace, Coordinates, Fract8Ops}, render::{RenderSource, Sample}, surface::{BufferedSurface, BufferedSurfacePool, Surface, SurfaceBuilder, Surfaces}}; -use figments_render::output::{Brightness, NullControls, OutputAsync}; -use futures::{join, FutureExt}; -use nalgebra::{Matrix, Vector2}; -use ssd1306::{mode::{BufferedGraphicsModeAsync, DisplayConfigAsync}, prelude::{DisplayRotation, I2CInterface}, size::DisplaySize128x64, I2CDisplayInterface, Ssd1306Async}; +use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::DynSubscriber}; +use embassy_time::{Duration, Instant, Timer}; +use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget}; +use figments::{mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::{RenderSource, Shader}, surface::{BufferedSurfacePool, NullSurface, NullBufferPool, Surface, SurfaceBuilder, Surfaces}}; +use figments_render::output::{Brightness, OutputAsync}; use log::*; -use embedded_graphics::mono_font::MonoTextStyle; -use embedded_graphics::primitives::PrimitiveStyle; -use nalgebra::ComplexField; -use crate::{animation::{AnimatedSurface, Animation}, backoff::Backoff, ego::engine::MotionState, events::{Notification, Prediction, Scene, SensorSource, Telemetry}, graphics::ssd1306::{SsdControls, SsdOutput, SsdPixel}, tasks::ui}; +use crate::{animation::Animation, backoff::Backoff, events::{Notification, Prediction, SensorSource, Telemetry}, graphics::{display::DisplayControls, oled_ui::{OledUniforms, Screen}}}; -mod images { - use embedded_graphics::{ - image::ImageRaw, - pixelcolor::BinaryColor - }; - include!("../../target/images.rs"); +#[cfg(feature="oled")] +pub type OledUiSurfacePool = BufferedSurfacePool; + +#[cfg(not(feature="oled"))] +pub type OledUiSurfacePool = NullBufferPool>; + +type OledSurface = >::Surface; + +pub type LockedUniforms = Arc>; + +pub struct OledUI { + overlay: S, + controls: DisplayControls, + uniforms: Arc> } -#[derive(Default, Debug)] -struct OledUI { - scene: Scene, - motion: MotionState, - brakelight: bool, - headlight: bool, - gps_online: bool, - imu_online: bool, - velocity: f32, - location: Vector2, - sleep: bool, +struct OverlayShader {} +impl Shader for OverlayShader { + fn draw(&self, _surface_coords: &Coordinates, _uniforms: &OledUniforms) -> BinaryColor { + BinaryColor::On + } } -#[derive(Default, Debug)] -struct OledUniforms { - ui: OledUI, - frame: usize, - current_screen: Screen, - next_screen: Screen -} - -#[derive(Debug, Default, Clone, Copy)] -enum Screen { - #[default] - Blank, - Bootsplash, - Home, - Sleeping, - Waking -} - -impl Screen { - pub fn draw_screen<'a, T>(&self, sampler: &mut EmbeddedGraphicsSampler<'a, T>, state: &OledUniforms) where T: Sample<'a, Matrix2DSpace>, T::Output: PixelSink { - match self { - Screen::Blank => (), - Screen::Bootsplash => { - Image::new(&images::BOOT_LOGO, Point::zero()).draw(sampler).unwrap(); - const SPARKLE_COUNT: i32 = 8; - for n in 0..SPARKLE_COUNT { - let sparkle_center = Point::new(128u8.scale8(cos8(state.frame.wrapping_mul(n as usize))) as i32, 64u8.scale8(sin8(state.frame.wrapping_mul(n as usize) as u8)) as i32); - let offset = (state.frame / 2 % 32) as i32 - 16; - let rotation = PI * 2.0 * (sin8(state.frame) as f32 / 255.0); - let normal = Point::new((rotation.cos() * offset as f32) as i32, (rotation.sin() * offset as f32) as i32); - let cross_normal = Point::new((rotation.sin() * offset as f32) as i32, (rotation.cos() * offset as f32) as i32); - // Draw horizontal - Line::new(sparkle_center - normal, sparkle_center + normal) - .draw_styled(&SPARKLE_STYLE, sampler).unwrap(); - // Draw vertical - Line::new(sparkle_center - cross_normal, sparkle_center + cross_normal) - .draw_styled(&SPARKLE_STYLE, sampler).unwrap(); - } - }, - Screen::Home => { - // Status bar - // Sensor indicators - let gps_img = if state.ui.gps_online { - &images::GPS_ON - } else { - &images::GPS_OFF - }; - let imu_img = if state.ui.imu_online { - &images::IMU_ON - } else { - &images::IMU_OFF - }; - Image::new(gps_img, Point::zero()).draw(sampler).unwrap(); - Image::new(imu_img, Point::new((gps_img.size().width + 2) as i32, 0)).draw(sampler).unwrap(); - - #[cfg(feature="demo")] - Text::with_alignment("Demo", Point::new(128, 10), TEXT_STYLE, Alignment::Right) - .draw(sampler) - .unwrap(); - - #[cfg(feature="simulation")] - Text::with_alignment("Sim", Point::new(128, 10), TEXT_STYLE, Alignment::Right) - .draw(sampler) - .unwrap(); - - // Separates the status bar from the UI - Line::new(Point::new(0, 18), Point::new(128, 18)).draw_styled(&INACTIVE_STYLE, sampler).unwrap(); - - // Speed display at the top - Text::with_alignment(&format!("{}", state.ui.velocity), Point::new(128 / 2, 12), SPEED_STYLE, Alignment::Center) - .draw(sampler) - .unwrap(); - - // The main UI content - Image::new(&images::BIKE, Point::new((128 / 2 - images::BIKE.size().width / 2) as i32, 24)).draw(sampler).unwrap(); - - let headlight_img = if state.ui.headlight { - &images::HEADLIGHT_ON - } else { - &images::HEADLIGHT_OFF - }; - let brakelight_img = if state.ui.brakelight { - &images::BRAKELIGHT_ON - } else { - &images::BRAKELIGHT_OFF - }; - Image::new(headlight_img, Point::new(((128 / 2 - images::BIKE.size().width / 2) - 18) as i32, 28)).draw(sampler).unwrap(); - Image::new(brakelight_img, Point::new(((128 / 2 + images::BIKE.size().width / 2) + 2) as i32, 28)).draw(sampler).unwrap(); - - // TODO: Replace the state texts with cute animations or smth - // Current prediction from the motion engine - Text::with_alignment(&format!("{:?}", state.ui.motion), Point::new(128 / 2, 64 - 3), TEXT_STYLE, Alignment::Center) - .draw(sampler) - .unwrap(); - - // Current scene in the UI - Text::with_alignment(&format!("{:?}", state.ui.scene), Point::new(128 / 2, 64 - 13), TEXT_STYLE, Alignment::Center) - .draw(sampler) - .unwrap(); - }, - Screen::Sleeping => { - Text::with_alignment("so eepy Zzz...", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center) - .draw(sampler) - .unwrap(); - }, - Screen::Waking => { - Text::with_alignment("OwO hewwo again :D", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center) - .draw(sampler) - .unwrap(); - } +impl> OledUI { + pub fn new>(surfaces: &mut SS, controls: DisplayControls, uniforms: LockedUniforms) -> Self where SS::Error: core::fmt::Debug { + Self { + overlay: SurfaceBuilder::build(surfaces) + .rect(Rectangle::everything()) + .shader(OverlayShader{}) + .opacity(0) + .finish().unwrap(), + controls, + uniforms } } -} -impl RenderSource for Screen { - fn render_to<'a, Smp>(&'a self, output: &'a mut Smp, uniforms: &OledUniforms) - where - Smp: Sample<'a, Matrix2DSpace, Output = SsdPixel> { - let mut sampler = EmbeddedGraphicsSampler(output, embedded_graphics::primitives::Rectangle::new(Point::new(0, 0), Size::new(128, 64))); - self.draw_screen(&mut sampler, uniforms); - } -} - -const SPARKLE_STYLE: PrimitiveStyle = PrimitiveStyleBuilder::new() - .stroke_color(BinaryColor::On) - .stroke_width(2) - .build(); - -const TEXT_STYLE: MonoTextStyle = MonoTextStyleBuilder::new() - .font(&FONT_6X10) - .text_color(BinaryColor::On) - .build(); - -const LOGO_STYLE: MonoTextStyle = MonoTextStyleBuilder::new() - .font(&FONT_9X18_BOLD) - .text_color(BinaryColor::On) - .build(); - -const SPEED_STYLE: MonoTextStyle = MonoTextStyleBuilder::new() - .font(&FONT_10X20) - .text_color(BinaryColor::On) - .build(); - -const DRAW_STYLE: PrimitiveStyle = PrimitiveStyleBuilder::new() - .fill_color(BinaryColor::On) - .build(); - -const INACTIVE_STYLE: PrimitiveStyle = PrimitiveStyleBuilder::new() - .fill_color(BinaryColor::Off) - .stroke_color(BinaryColor::On) - .stroke_width(2) - .build(); - -#[embassy_executor::task] -async fn oled_render(mut output: SsdOutput, surfaces: BufferedSurfacePool, uniforms: Arc>) { - Backoff::from_secs(1).forever().attempt::<_, (), DisplayError>(async || { - const FPS: u64 = 30; - const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS); - - const ANIMATION_TPS: u64 = 30; - const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS); - info!("Starting Oled renderer"); - loop { - let start = Instant::now(); - { - let mut locked = uniforms.lock().await; - output.clear(BinaryColor::Off).unwrap(); - locked.frame = (Instant::now().as_millis() / ANIMATION_FRAME_TIME.as_millis()) as usize; - locked.current_screen.render_to(&mut output, &locked); - surfaces.render_to(&mut output, &locked); - } - output.commit_async().await?; - let frame_time = Instant::now() - start; - if frame_time < RENDER_BUDGET { - Timer::after(RENDER_BUDGET - frame_time).await; - } else { - //warn!("OLED Frame took too long to render! {}ms", frame_time.as_millis()); - } + pub async fn screen_transition(&mut self, next_screen: Screen) { + const FADE_IN: Animation = Animation::new().from(0).to(255).duration(Duration::from_millis(300)); + const FADE_OUT: Animation = Animation::new().from(255).to(0).duration(Duration::from_millis(300)); + info!("Fading in to screen {next_screen:?}"); + FADE_IN.apply(&mut self.overlay).await; + { + let mut locked = self.uniforms.lock().await; + locked.current_screen = next_screen; } - }).await.unwrap(); -} - -async fn screen_transition(overlay: &mut BufferedSurface, uniforms: &mut Arc>, next_screen: Screen) { - const FADE_IN: Animation = Animation::new().from(0).to(255).duration(Duration::from_millis(300)); - const FADE_OUT: Animation = Animation::new().from(255).to(0).duration(Duration::from_millis(300)); - info!("Fading in to screen {next_screen:?}"); - FADE_IN.apply(overlay).await; - { - let mut locked = uniforms.lock().await; - locked.current_screen = next_screen; + FADE_OUT.apply(&mut self.overlay).await; } - FADE_OUT.apply(overlay).await; -} -#[embassy_executor::task] -async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut overlay: BufferedSurface, mut uniforms: Arc>, mut controls: SsdControls) { - - screen_transition(&mut overlay, &mut uniforms, Screen::Bootsplash).await; - Timer::after_secs(3).await; - screen_transition(&mut overlay, &mut uniforms, Screen::Home).await; - - loop { - let evt = events.next_message_pure().await; - //info!("Oled UI event: {evt:?}"); - match evt { + pub async fn on_event(&mut self, event: Telemetry) { + match event { + // Waking and sleeping Telemetry::Notification(Notification::Sleep) => { - screen_transition(&mut overlay, &mut uniforms, Screen::Sleeping).await; + warn!("Putting OLED display to sleep"); + self.screen_transition(Screen::Sleeping).await; Timer::after_secs(1).await; - screen_transition(&mut overlay, &mut uniforms, Screen::Blank).await; - controls.set_on(false); + self.screen_transition(Screen::Blank).await; + self.controls.set_on(false); //ui_state.sleep = true }, Telemetry::Notification(Notification::WakeUp) => { - controls.set_on(true); - screen_transition(&mut overlay, &mut uniforms, Screen::Waking).await; + warn!("Waking up OLED display"); + self.controls.set_on(true); + self.screen_transition(Screen::Waking).await; Timer::after_secs(1).await; - screen_transition(&mut overlay, &mut uniforms, Screen::Home).await; + self.screen_transition(Screen::Home).await; //ui_state.sleep = false }, - _ => () - } - let mut locked = uniforms.lock().await; - let ui_state = &mut locked.ui; - match evt { - Telemetry::Prediction(Prediction::Velocity(v)) => ui_state.velocity = v, - Telemetry::Prediction(Prediction::Location(loc)) => ui_state.location = loc, - Telemetry::Prediction(Prediction::Motion(motion)) => ui_state.motion = motion, - Telemetry::Notification(Notification::SceneChange(scene)) => ui_state.scene = scene, - Telemetry::Notification(Notification::SetBrakelight(b)) => ui_state.brakelight = b, - Telemetry::Notification(Notification::SetHeadlight(b)) => ui_state.headlight = b, - Telemetry::Notification(Notification::SensorOffline(SensorSource::IMU)) => ui_state.imu_online = false, - Telemetry::Notification(Notification::SensorOnline(SensorSource::IMU)) => ui_state.imu_online = true, - Telemetry::Notification(Notification::SensorOffline(SensorSource::GPS)) => ui_state.gps_online = false, - Telemetry::Notification(Notification::SensorOnline(SensorSource::GPS)) => ui_state.gps_online = true, + + // State updates + Telemetry::Prediction(Prediction::Velocity(v)) => self.with_uniforms(|state| {state.ui.velocity = v;}).await, + Telemetry::Prediction(Prediction::Location(loc)) => self.with_uniforms(|state| {state.ui.location = loc}).await, + Telemetry::Prediction(Prediction::Motion(motion)) => self.with_uniforms(|state| {state.ui.motion = motion}).await, + Telemetry::Notification(Notification::SceneChange(scene)) => self.with_uniforms(|state| {state.ui.scene = scene}).await, + Telemetry::Notification(Notification::SetBrakelight(b)) => self.with_uniforms(|state| {state.ui.brakelight = b}).await, + Telemetry::Notification(Notification::SetHeadlight(b)) => self.with_uniforms(|state| {state.ui.headlight = b}).await, + Telemetry::Notification(Notification::SensorOffline(SensorSource::IMU)) => self.with_uniforms(|state| {state.ui.imu_online = false}).await, + Telemetry::Notification(Notification::SensorOnline(SensorSource::IMU)) => self.with_uniforms(|state| {state.ui.imu_online = true}).await, + Telemetry::Notification(Notification::SensorOffline(SensorSource::GPS)) => self.with_uniforms(|state| {state.ui.gps_online = false}).await, + Telemetry::Notification(Notification::SensorOnline(SensorSource::GPS)) => self.with_uniforms(|state| {state.ui.gps_online = true}).await, _ => () } } - /*for (coords, pix) in output.sample(&figments::prelude::Rectangle::everything()) { - pix.set(&BinaryColor::Off); + + pub async fn with_uniforms(&self, f: impl Fn(&mut OledUniforms)) { + let mut locked = self.uniforms.lock().await; + let mutref: &mut OledUniforms = &mut locked; + f(mutref); } - Text::with_alignment("Foo", Point::new(128 / 2, 64 / 2), LOGO_STYLE, Alignment::Center).draw(&mut output).unwrap(); - output.commit_async().await.unwrap(); - Timer::after_secs(1).await; - - let painter = UiPainter { current_screen: Screen::Bootsplash, next_screen: Screen::Bootsplash }; - painter.render_to(&mut output, &OledUniforms { frame: 0, ui: ui_state });*/ - } #[embassy_executor::task] -pub async fn oled_task(i2c: I2c<'static, Async>, mut reset_pin: Output<'static>, events: DynSubscriber<'static, Telemetry>) { - let interface = I2CDisplayInterface::new(i2c); - let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0); - display.reset(&mut reset_pin, &mut Delay).await.unwrap(); - display.init_with_addr_mode(ssd1306::command::AddrMode::Horizontal).await.unwrap(); +pub async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut ui: OledUI) { + + ui.screen_transition(Screen::Bootsplash).await; + Timer::after_secs(3).await; + ui.screen_transition(Screen::Home).await; - let output = SsdOutput::new(display); - let mut surfaces = BufferedSurfacePool::default(); - let uniforms = Default::default(); + loop { + ui.on_event(events.next_message_pure().await).await; + } - let sfc = SurfaceBuilder::build(&mut surfaces).shader(|coords: &Coordinates, uniforms: &OledUniforms| { - BinaryColor::On - }).opacity(0).finish().unwrap(); - - info!("Starting OLED display tasks!"); - let spawner = Spawner::for_current_executor().await; - let ui_controls = output.controls().unwrap().clone(); - spawner.must_spawn(oled_render(output, surfaces, Arc::clone(&uniforms))); - spawner.must_spawn(oled_ui(events, sfc, uniforms, ui_controls)); } \ No newline at end of file diff --git a/src/tasks/oled_render.rs b/src/tasks/oled_render.rs new file mode 100644 index 0000000..6682786 --- /dev/null +++ b/src/tasks/oled_render.rs @@ -0,0 +1,39 @@ +use display_interface::DisplayError; +use embassy_time::{Duration, Instant, Timer}; +use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget}; +use figments::render::RenderSource; +use figments_render::output::OutputAsync; +use log::*; + +use crate::{backoff::Backoff, graphics::ssd1306::SsdOutput, tasks::oled::{LockedUniforms, OledUiSurfacePool}}; + + +#[embassy_executor::task] +pub async fn oled_render(mut output: SsdOutput, surfaces: OledUiSurfacePool, uniforms: LockedUniforms) { + warn!("Starting OLED rendering task"); + Backoff::from_secs(1).forever().attempt::<_, (), DisplayError>(async || { + const FPS: u64 = 30; + const RENDER_BUDGET: Duration = Duration::from_millis(1000 / FPS); + + const ANIMATION_TPS: u64 = 30; + const ANIMATION_FRAME_TIME: Duration = Duration::from_millis(1000 / ANIMATION_TPS); + info!("Starting Oled renderer"); + loop { + let start = Instant::now(); + { + let mut locked = uniforms.lock().await; + output.clear(BinaryColor::Off).unwrap(); + locked.frame = (Instant::now().as_millis() / ANIMATION_FRAME_TIME.as_millis()) as usize; + locked.current_screen.render_to(&mut output, &locked); + surfaces.render_to(&mut output, &locked); + } + output.commit_async().await?; + let frame_time = Instant::now() - start; + if frame_time < RENDER_BUDGET { + Timer::after(RENDER_BUDGET - frame_time).await; + } else { + //warn!("OLED Frame took too long to render! {}ms", frame_time.as_millis()); + } + } + }).await.unwrap(); +} diff --git a/src/tasks/render.rs b/src/tasks/render.rs index 7290172..4577fce 100644 --- a/src/tasks/render.rs +++ b/src/tasks/render.rs @@ -1,14 +1,13 @@ use embassy_time::{Duration, Instant, Timer}; use esp_hal::{gpio::AnyPin, rmt::Rmt, time::Rate, timer::timg::Wdt}; use esp_hal_smartled::{buffer_size_async, SmartLedsAdapterAsync}; -use figments::{prelude::*, surface::Surfaces}; +use figments::prelude::*; use figments_render::gamma::GammaCurve; use figments_render::output::{GammaCorrected, OutputAsync}; use log::{info, warn}; -use micromath::F32Ext; -use crate::display::NUM_PIXELS; -use crate::{display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool}; +use crate::graphics::display::NUM_PIXELS; +use crate::{graphics::display::{BikeOutput, DisplayControls, Uniforms}, tasks::ui::UiSurfacePool}; #[embassy_executor::task] pub async fn render(rmt: esp_hal::peripherals::RMT<'static>, gpio: AnyPin<'static>, surfaces: UiSurfacePool, safety_surfaces: UiSurfacePool, mut controls: DisplayControls, mut wdt: Wdt>) { diff --git a/src/tasks/safetyui.rs b/src/tasks/safetyui.rs index 6ed08c4..18ee18c 100644 --- a/src/tasks/safetyui.rs +++ b/src/tasks/safetyui.rs @@ -7,7 +7,7 @@ use core::fmt::Debug; use futures::join; use log::*; -use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{DisplayControls, SegmentSpace, Uniforms}, events::Notification, shaders::*, tasks::ui::UiSurfacePool}; +use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, graphics::display::{DisplayControls, SegmentSpace, Uniforms}, events::Notification, graphics::shaders::*, tasks::ui::UiSurfacePool}; pub struct SafetyUi { // Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer diff --git a/src/tasks/ui.rs b/src/tasks/ui.rs index 08cf7ef..a92aa4d 100644 --- a/src/tasks/ui.rs +++ b/src/tasks/ui.rs @@ -6,7 +6,7 @@ use core::fmt::Debug; use futures::join; use log::*; -use crate::{animation::{AnimatedSurface, Animation}, display::{SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource, Telemetry}, shaders::*}; +use crate::{animation::{AnimatedSurface, Animation}, graphics::display::{SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource, Telemetry}, graphics::shaders::*}; pub struct Ui { // Background layer provides an always-running background for everything to draw on diff --git a/src/tasks/wifi.rs b/src/tasks/wifi.rs index e3a269a..8c56298 100644 --- a/src/tasks/wifi.rs +++ b/src/tasks/wifi.rs @@ -29,6 +29,8 @@ pub async fn ble_task(_notify: DynSubscriber<'static, Notification>, wifi_init: info!("Read serial data! {data:?}"); }; + // https://nextcloud.malloc.hackerbots.net/nextcloud/index.php/apps/phonetrack/logGet/cc668656ef51680c99b0eb6e5323a459/renderbug?lat=LAT&lon=LON&alt=ALT&acc=ACC&bat=BAT&sat=SAT&speed=SPD&bearing=DIR×tamp=TIME + // Other useful characteristics: // 0x2A67 - Location and speed // 0x2A00 - Device name