215 lines
9.1 KiB
Rust
215 lines
9.1 KiB
Rust
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<S: Surface> {
|
|
// Background layer provides an always-running background for everything to draw on
|
|
background: AnimatedSurface<S>,
|
|
|
|
// Tail and panels provide content
|
|
tail: AnimatedSurface<S>,
|
|
panels: AnimatedSurface<S>,
|
|
|
|
motion: AnimatedSurface<S>,
|
|
|
|
// Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc)
|
|
notification: AnimatedSurface<S>,
|
|
}
|
|
|
|
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> Ui<S> {
|
|
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(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<u8>) {
|
|
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<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(not(feature="real-output"))]
|
|
pub type UiSurfacePool = NullBufferPool<Uniforms, SegmentSpace, Rgba<u8>>;
|
|
#[cfg(feature="real-output")]
|
|
pub type UiSurfacePool = BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>;
|
|
|
|
#[embassy_executor::task]
|
|
pub async fn ui_main(mut events: DynSubscriber<'static, Prediction>, 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;
|
|
|
|
// 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;
|
|
}
|
|
} |