use embassy_sync::pubsub::DynSubscriber; use embassy_time::{Duration, Timer}; use figments::prelude::*; use rgb::{Rgb, Rgba}; use core::fmt::Debug; use futures::join; use log::*; use crate::{animation::{AnimatedSurface, Animation}, ego::engine::MotionState, events::{Personality, Prediction, Scene, SensorSource, SensorState}, graphics::{display::{SegmentSpace, Uniforms}, shaders::*}}; pub struct Ui { // Background layer provides an always-running background for everything to draw on background: AnimatedSurface, // Tail and panels provide content tail: AnimatedSurface, panels: AnimatedSurface, motion: AnimatedSurface, // Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc) notification: AnimatedSurface, } impl>> Ui { pub fn new>(surfaces: &mut SS) -> Self where SS::Error: Debug { Self { background: SurfaceBuilder::build(surfaces) .rect(Rectangle::everything()) .shader(Background::default()) .visible(false) .finish().unwrap().into(), tail: SurfaceBuilder::build(surfaces) .rect(Rectangle::new_from_coordinates(0, 5, 255, 5)) .shader(Tail::default()) .visible(false) .finish().unwrap().into(), panels: SurfaceBuilder::build(surfaces) .rect(Rectangle::new_from_coordinates(0, 1, 255, 4)) .shader(Panel::default()) .visible(false) .finish().unwrap().into(), motion: SurfaceBuilder::build(surfaces) .rect(Rectangle::everything()) .shader(Movement::default()) .visible(false) .finish().unwrap().into(), notification: SurfaceBuilder::build(surfaces) .rect(Rectangle::everything()) .shader(Background::default()) .visible(false) .finish().unwrap().into() } } pub async fn flash_notification_color(&mut self, color: Rgb) { let fade_in = Animation::default().from(0).to(255).duration(Duration::from_millis(30)); let pulse_out = Animation::default().from(255).to(60).duration(Duration::from_millis(100)); let pulse_in = Animation::default().from(0).to(255).duration(Duration::from_millis(100)); let fade_out = Animation::default().from(255).to(0).duration(Duration::from_secs(2)); info!("Flashing notification {color}"); self.notification.set_visible(true); self.notification.set_shader(Background::from_color(color)); fade_in.apply(&mut self.notification).await; // Pulse quickly 5 times for _ in 0..5 { pulse_out.apply(&mut self.notification).await; pulse_in.apply(&mut self.notification).await; } fade_out.apply(&mut self.notification).await; self.notification.set_visible(false); } pub fn as_slice(&mut self) -> [&mut S; 4] { [ &mut *self.tail, &mut *self.panels, &mut *self.background, &mut *self.motion ] } pub async fn show(&mut self) { info!("Flipping on surfaces"); self.as_slice().set_visible(true); } // TODO: Brakelight should only be toggled when actually braking or stationary pub async fn apply_scene(&mut self, next_scene: Scene) { info!("Activating scene {next_scene:?}"); match next_scene { Scene::Ready => { let tail = Animation::default().duration(Duration::from_millis(300)).to(96); let panels = Animation::default().duration(Duration::from_millis(300)).to(128); let bg = Animation::default().duration(Duration::from_millis(300)).to(32); let motion = Animation::default().duration(Duration::from_secs(1)).to(0); join!( tail.apply(&mut self.tail), panels.apply(&mut self.panels), bg.apply(&mut self.background), motion.apply(&mut self.motion) ); self.background.set_shader(Background::default()); }, Scene::Idle => { // FIXME: The safety UI task should handle setting the display brightness to 50% here self.background.set_shader(Thinking::default()); let fg_fade = Animation::default().duration(Duration::from_millis(300)).to(0); let bg_fade = Animation::default().duration(Duration::from_millis(300)).to(128); // FIXME: The scenes shouldn't be touching the brake/headlights at all here. In fact, they should be dealt with in a whole separate task from the main UI, maybe running on the motion prediction executor join!( fg_fade.apply(&mut self.tail), fg_fade.apply(&mut self.panels), bg_fade.apply(&mut self.background), fg_fade.apply(&mut self.motion) ); }, Scene::Accelerating => { self.motion.set_shader(Movement::default()); Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await; }, Scene::Decelerating => { self.motion.set_shader(Movement::default().reversed()); Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await; } } } pub async fn on_event(&mut self, event: Prediction) { match event { Prediction::SetPersonality(personality) => match personality { Personality::Active => self.apply_scene(Scene::Ready).await, Personality::Parked => self.apply_scene(Scene::Idle).await, Personality::Waking => self.show().await, _ => () }, Prediction::Motion { prev: _, next: MotionState::Accelerating } => self.apply_scene(Scene::Accelerating).await, Prediction::Motion { prev: _, next: MotionState::Decelerating } => self.apply_scene(Scene::Decelerating).await, Prediction::Motion { prev: _, next: MotionState::Steady } => self.apply_scene(Scene::Ready).await, Prediction::Motion { prev: _, next: MotionState::Stationary } => self.apply_scene(Scene::Ready).await, Prediction::SensorStatus(SensorSource::IMU, SensorState::Online) => self.flash_notification_color(Rgb::new(0, 255, 0)).await, Prediction::SensorStatus(SensorSource::Location, SensorState::Degraded) => self.flash_notification_color(Rgb::new(255, 0, 0)).await, Prediction::SensorStatus(SensorSource::GPS, SensorState::Online) => self.flash_notification_color(Rgb::new(0, 255, 255)).await, _ => () } // Other event ideas: // - Bike has crashed, or is laid down // - Unstable physics right before crashing? // - Turning left/right // - BPM sync with phone app // - GPS data is being synchronized with nextcloud/whatever // - A periodic flash when re-initializing MPU and GPS, to indicate there might be a problem? // - Bluetooth/BLE connect/disconnect events // - Bike is waking up from stationary? } } impl>> Surface for Ui { type Uniforms = S::Uniforms; type CoordinateSpace = S::CoordinateSpace; type Pixel = S::Pixel; fn set_shader + 'static>(&mut self, _shader: T) { unimplemented!() } fn clear_shader(&mut self) { self.as_slice().clear_shader(); } fn set_rect(&mut self, rect: Rectangle) { self.as_slice().set_rect(rect); } fn set_opacity(&mut self, opacity: u8) { self.as_slice().set_opacity(opacity); } fn set_visible(&mut self, visible: bool) { self.as_slice().set_visible(visible); } fn set_offset(&mut self, offset: Coordinates) { self.as_slice().set_offset(offset); } } #[cfg(not(feature="real-output"))] pub type UiSurfacePool = NullBufferPool>; #[cfg(feature="real-output")] pub type UiSurfacePool = BufferedSurfacePool>; #[embassy_executor::task] pub async fn ui_main(mut events: DynSubscriber<'static, Prediction>, mut ui: Ui<>::Surface>) { // FIXME: This should instead wait on some kind of flag set by the safety UI, or else we risk painting before we even have a display up and running Timer::after_secs(3).await; ui.show().await; // Enter the startup scene ui.apply_scene(Scene::Ready).await; // Enter the event loop loop { let evt = events.next_message_pure().await; ui.on_event(evt).await; } }