Files
renderbug-bike/src/tasks/ui.rs

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;
}
}