ui: split out safety ui into its own dedicated set of layers

This commit is contained in:
2025-10-17 14:40:07 +02:00
parent 818f8af49a
commit a97a28bf9f
6 changed files with 235 additions and 114 deletions

View File

@@ -1,5 +1,5 @@
use embassy_sync::channel::DynamicReceiver;
use embassy_time::Duration;
use embassy_sync::{channel::{DynamicReceiver, DynamicSender}, pubsub::{DynPublisher, DynSubscriber}};
use embassy_time::{Duration, Timer};
use figments::prelude::*;
use rgb::{Rgb, Rgba};
use log::*;
@@ -7,7 +7,7 @@ use alloc::sync::Arc;
use core::fmt::Debug;
use futures::join;
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{SegmentSpace, Uniforms}, events::{DisplayControls, Notification, Scene, SensorSource}, shaders::*};
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource, Telemetry}, shaders::*};
pub struct Ui<S: Surface> {
// Background layer provides an always-running background for everything to draw on
@@ -21,18 +21,10 @@ pub struct Ui<S: Surface> {
// Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc)
notification: AnimatedSurface<S>,
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
headlight: AnimatedSurface<S>,
brakelight: AnimatedSurface<S>,
// The overlay covers everything and is used to implement a power-on and power-off animation.
overlay: AnimatedSurface<S>,
display: Arc<DisplayControls>
}
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> Ui<S> {
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS, display: Arc<DisplayControls>) -> Self where SS::Error: Debug {
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS) -> Self where SS::Error: Debug {
Self {
background: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())
@@ -58,24 +50,7 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
.rect(Rectangle::everything())
.shader(Background::default())
.visible(false)
.finish().unwrap().into(),
// FIXME: Headlight, brakelight, and the overlay should all be handled as part of the safety UI
headlight: SurfaceBuilder::build(surfaces)
.rect(Rectangle::new_from_coordinates(0, 0, 255, 0))
.shader(Headlight::default())
.visible(false)
.finish().unwrap().into(),
brakelight: SurfaceBuilder::build(surfaces)
.rect(Brakelight::rectangle())
.shader(Brakelight::default())
.visible(false)
.finish().unwrap().into(),
overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())
.shader(Thinking::default())
.visible(false)
.finish().unwrap().into(),
display
.finish().unwrap().into()
}
}
@@ -102,71 +77,18 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
self.notification.set_visible(false);
}
pub async fn sleep(&mut self) {
info!("Running sleep sequence");
let fade_out = Animation::default().duration(Duration::from_secs(1)).from(255).to(0);
let mut disp_anim = AnimDisplay(&self.display);
fade_out.apply(&mut disp_anim).await;
// Reset layers to the initial state now that the display should be off
pub fn as_slice(&mut self) -> [&mut S; 4] {
[
&mut *self.tail,
&mut *self.panels,
&mut *self.background,
&mut *self.motion
].set_opacity(0);
[
&mut *self.tail,
&mut *self.panels,
&mut *self.background,
&mut *self.motion
].set_visible(false);
info!("Turning off display");
self.display.set_on(false);
// Wait for the display hardware to actually turn off, before we return to process the next event, which could cause funky behaviors.
// FIXME: also deadlocks :(
//self.display.render_is_running.wait().await;
info!("Display is now sleeping.");
]
}
pub async fn wakeup(&mut self) {
info!("Running startup fade sequence");
info!("Turning on display");
// Turn on the display hardware
self.display.set_brightness(0);
self.display.set_on(true);
// Wait for the renderer to start running again
// FIXME: This deadlocks :(
//self.display.render_is_running.wait().await;
let fade_in = Animation::default().duration(Duration::from_secs(3)).from(0).to(255);
let fade_out = Animation::default().duration(Duration::from_secs(1)).from(255).to(0);
let mut disp_anim = AnimDisplay(&self.display);
info!("Fade in overlay");
self.overlay.set_visible(true);
join!(
fade_in.apply(&mut self.overlay),
fade_in.apply(&mut disp_anim)
);
pub async fn show(&mut self) {
info!("Flipping on surfaces");
// Flip on all the layers
[
&mut *self.panels,
&mut *self.tail,
&mut *self.background,
&mut *self.motion
].set_visible(true);
// Enter the startup scene
self.apply_scene(Scene::Ready).await;
info!("Fade out overlay");
fade_out.apply(&mut self.overlay).await;
self.overlay.set_visible(false);
info!("Wakeup complete!");
self.as_slice().set_visible(true);
}
// TODO: Brakelight should only be toggled when actually braking or stationary
@@ -225,18 +147,13 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
// Scene change
Notification::SceneChange(scene) => self.apply_scene(scene).await,
// Toggling head and brake lights
Notification::SetBrakelight(is_on) => self.brakelight.set_on(is_on).await,
Notification::SetHeadlight(is_on) => self.headlight.set_on(is_on).await,
Notification::Sleep => self.sleep().await,
Notification::WakeUp => self.wakeup().await,
Notification::SensorsOffline => {
self.flash_notification_color(Rgb::new(255, 0, 0)).await;
self.flash_notification_color(Rgb::new(0, 255, 0)).await;
self.flash_notification_color(Rgb::new(0, 0, 255)).await;
}
Notification::WakeUp => self.show().await,
_ => ()
// Other event ideas:
@@ -252,24 +169,56 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
}
}
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> Surface for Ui<S> {
type Uniforms = S::Uniforms;
type CoordinateSpace = S::CoordinateSpace;
type Pixel = S::Pixel;
fn set_shader<T: Shader<Self::Uniforms, Self::CoordinateSpace, Self::Pixel> + 'static>(&mut self, shader: T) {
unimplemented!()
}
fn clear_shader(&mut self) {
self.as_slice().clear_shader();
}
fn set_rect(&mut self, rect: Rectangle<Self::CoordinateSpace>) {
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::CoordinateSpace>) {
self.as_slice().set_offset(offset);
}
}
#[cfg(feature="headless")]
pub type UiSurfacePool = NullBufferPool<NullSurface<Uniforms, SegmentSpace, Rgba<u8>>>;
#[cfg(not(feature="headless"))]
pub type UiSurfacePool = BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>;
#[embassy_executor::task]
pub async fn ui_main(events: DynamicReceiver<'static, Notification>, mut ui: Ui<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
// Wait for the renderer to start running
//ui.display.render_is_running.wait().await;
pub async fn ui_main(mut events: DynSubscriber<'static, Notification>, telemetery: DynPublisher<'static, Telemetry>, mut ui: Ui<<UiSurfacePool as Surfaces<SegmentSpace>>::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;
// Run the wake sequence, and turn on the lights
ui.wakeup().await;
// FIXME: Moving the safety lights into another task will let us turn them both on in parallel with the wakeup sequence
ui.headlight.set_on(true).await;
ui.brakelight.set_on(true).await;
// Enter the startup scene
ui.apply_scene(Scene::Ready).await;
// Enter the event loop
loop {
ui.on_event(events.receive().await).await;
let evt = events.next_message_pure().await;
ui.on_event(evt).await;
telemetery.publish(Telemetry::Notification(evt)).await;
}
}