bump a lot of big changes I dont want to break down into individual commits
This commit is contained in:
369
src/tasks/ui.rs
369
src/tasks/ui.rs
@@ -1,272 +1,243 @@
|
||||
use embassy_sync::channel::DynamicReceiver;
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use figments::{liber8tion::interpolate::Fract8, prelude::*};
|
||||
use embassy_time::Duration;
|
||||
use figments::prelude::*;
|
||||
use rgb::{Rgb, Rgba};
|
||||
use log::*;
|
||||
use nalgebra::ComplexField;
|
||||
|
||||
use crate::{display::BikeSpace, events::{DisplayControls, Notification, Scene}, shaders::*, tasks::render::Uniforms};
|
||||
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::*};
|
||||
|
||||
pub struct Ui<S: Surface> {
|
||||
// Background layer provides an always-running background for everything to draw on
|
||||
background: S,
|
||||
background: AnimatedSurface<S>,
|
||||
|
||||
// Tail and panels provide content
|
||||
tail: S,
|
||||
panels: S,
|
||||
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:S,
|
||||
notification: AnimatedSurface<S>,
|
||||
|
||||
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
|
||||
headlight: S,
|
||||
brakelight: S,
|
||||
headlight: AnimatedSurface<S>,
|
||||
brakelight: AnimatedSurface<S>,
|
||||
|
||||
// The overlay covers everything and is used to implement a power-on and power-off animation.
|
||||
overlay:S,
|
||||
headlight_on: bool,
|
||||
brakelight_on: bool,
|
||||
display: &'static DisplayControls
|
||||
overlay: AnimatedSurface<S>,
|
||||
display: Arc<DisplayControls>
|
||||
}
|
||||
|
||||
impl<S: Surface<Uniforms = Uniforms, CoordinateSpace = BikeSpace, Pixel = Rgba<u8>>> Ui<S> {
|
||||
pub fn new(surfaces: &mut impl Surfaces<S::CoordinateSpace, Surface = S>, display: &'static DisplayControls) -> Self {
|
||||
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 {
|
||||
Self {
|
||||
background: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.shader(Background::default())
|
||||
.opacity(32)
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
tail: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 5, 255, 5))
|
||||
.opacity(96)
|
||||
.shader(Tail::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
panels: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 1, 255, 4))
|
||||
.opacity(128)
|
||||
.shader(Panel::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.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(),
|
||||
.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(),
|
||||
.finish().unwrap().into(),
|
||||
brakelight: SurfaceBuilder::build(surfaces)
|
||||
.rect(Brakelight::rectangle())
|
||||
.shader(Brakelight::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
.finish().unwrap().into(),
|
||||
overlay: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.visible(true)
|
||||
.opacity(0)
|
||||
.shader(Thinking::default())
|
||||
.finish().unwrap(),
|
||||
headlight_on: false,
|
||||
brakelight_on: false,
|
||||
.visible(false)
|
||||
.finish().unwrap().into(),
|
||||
display
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn flash_notification_color(&mut self, color: Rgb<u8>) {
|
||||
self.notification.set_shader(Background::from_color(color));
|
||||
self.notification.set_opacity(0);
|
||||
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);
|
||||
// Fade in to full on over 1s
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.notification.set_opacity(pct);
|
||||
}).await;
|
||||
|
||||
// Pulse quickly 3 times
|
||||
for _ in 0..3 {
|
||||
// Brief dimming back to half brightness for half a sec
|
||||
Self::animate_duration(Duration::from_millis(100), |pct| {
|
||||
self.notification.set_opacity(100 + pct/2);
|
||||
}).await;
|
||||
self.notification.set_shader(Background::from_color(color));
|
||||
|
||||
// Back to full
|
||||
Self::animate_duration(Duration::from_millis(100), |pct| {
|
||||
self.notification.set_opacity(100 + (255 - pct) / 2);
|
||||
}).await;
|
||||
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;
|
||||
}
|
||||
|
||||
// Back to off
|
||||
Self::animate_duration(Duration::from_secs(3), |pct| {
|
||||
self.notification.set_opacity(255 - pct);
|
||||
}).await;
|
||||
fade_out.apply(&mut self.notification).await;
|
||||
self.notification.set_visible(false);
|
||||
}
|
||||
|
||||
async fn animate_duration<F: FnMut(Fract8)>(duration: Duration, mut f: F) {
|
||||
let start = Instant::now();
|
||||
let end = start + duration;
|
||||
let full_duration = duration.as_millis() as f64;
|
||||
loop {
|
||||
let now = Instant::now();
|
||||
if now > end {
|
||||
break
|
||||
} else {
|
||||
let pct = (now - start).as_millis() as f64 / full_duration;
|
||||
let frac_pct = (255.0 * pct) as u8;
|
||||
f(frac_pct);
|
||||
Timer::after_millis(5).await;
|
||||
}
|
||||
}
|
||||
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
|
||||
[
|
||||
&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 fn expo_in(t: f32) -> f32 {
|
||||
if t <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
2f32.powf(10.0 * t - 10.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expo_out(t: f32) -> f32 {
|
||||
if 1.0 <= t {
|
||||
1.0
|
||||
} else {
|
||||
1.0 - 2f32.powf(-10.0 * t)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn startup_fade_sequence(&mut self) {
|
||||
pub async fn wakeup(&mut self) {
|
||||
info!("Running startup fade sequence");
|
||||
// Start with a completely transparent overlay, which then fades in over 1 second
|
||||
self.overlay.set_opacity(0);
|
||||
|
||||
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);
|
||||
Self::animate_duration(Duration::from_secs(2), |pct| {
|
||||
self.overlay.set_opacity((Self::expo_in(pct as f32 / 255.0) * 255.0) as u8);
|
||||
}).await;
|
||||
// When the overlay is fully opaque and all lower layers will be hidden, turn them on
|
||||
self.apply_scene(Scene::Startup).await;
|
||||
Timer::after_secs(1).await;
|
||||
// Then fade out the overlay over 3 seconds, allowing the base animations to be shown
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.overlay.set_opacity((Self::expo_in((255 - pct) as f32 / 255.0) * 255.0) as u8);
|
||||
}).await;
|
||||
join!(
|
||||
fade_in.apply(&mut self.overlay),
|
||||
fade_in.apply(&mut disp_anim)
|
||||
);
|
||||
|
||||
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!("Fade sequence completed");
|
||||
}
|
||||
|
||||
pub async fn set_headlight_on(&mut self, is_on: bool) {
|
||||
if self.headlight_on != is_on {
|
||||
self.brakelight_on = is_on;
|
||||
if is_on {
|
||||
info!("Turning on headlight");
|
||||
self.headlight.set_opacity(0);
|
||||
self.headlight.set_visible(true);
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.headlight.set_opacity(pct);
|
||||
}).await;
|
||||
self.headlight.set_opacity(255);
|
||||
} else {
|
||||
info!("Turning off headlight");
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.headlight.set_opacity(255 - pct);
|
||||
}).await;
|
||||
self.headlight.set_visible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_brakelight_on(&mut self, is_on: bool) {
|
||||
if self.brakelight_on != is_on {
|
||||
self.brakelight_on = is_on;
|
||||
if is_on {
|
||||
info!("Turning on brakelight");
|
||||
self.brakelight.set_opacity(0);
|
||||
self.brakelight.set_visible(true);
|
||||
Self::animate_duration(Duration::from_millis(300), |pct| {
|
||||
self.brakelight.set_opacity(pct);
|
||||
}).await;
|
||||
self.brakelight.set_opacity(255);
|
||||
} else {
|
||||
info!("Turning off brakelight");
|
||||
Self::animate_duration(Duration::from_millis(300), |pct| {
|
||||
self.brakelight.set_opacity(255 - pct);
|
||||
}).await;
|
||||
self.brakelight.set_visible(false);
|
||||
}
|
||||
}
|
||||
info!("Wakeup complete!");
|
||||
}
|
||||
|
||||
// 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::Startup => {
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
self.display.brightness.store(255, core::sync::atomic::Ordering::Relaxed);
|
||||
self.background.set_visible(true);
|
||||
self.tail.set_visible(true);
|
||||
self.panels.set_visible(true);
|
||||
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::ParkedIdle => {
|
||||
// Ensure the display is on
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.display.brightness.store(128.scale8(255 - pct), core::sync::atomic::Ordering::Relaxed);
|
||||
}).await;
|
||||
// Hide the content; only notifications will remain
|
||||
self.background.set_visible(true);
|
||||
self.tail.set_visible(false);
|
||||
self.panels.set_visible(false);
|
||||
},
|
||||
Scene::ParkedLongTerm => {
|
||||
// For long-term parking, we turn off the safety lights
|
||||
self.set_headlight_on(false).await;
|
||||
self.set_brakelight_on(false).await;
|
||||
// Then we turn the display off completely
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.display.brightness.store(255 - pct, core::sync::atomic::Ordering::Relaxed);
|
||||
}).await;
|
||||
self.display.on.store(false, core::sync::atomic::Ordering::Relaxed);
|
||||
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.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
self.display.brightness.store(255, core::sync::atomic::Ordering::Relaxed);
|
||||
self.tail.set_shader(Thinking::default());
|
||||
self.panels.set_shader(Panel::default());
|
||||
self.motion.set_shader(Movement::default());
|
||||
Animation::default().duration(Duration::from_secs(1)).to(255).apply(&mut self.motion).await;
|
||||
},
|
||||
Scene::Decelerating => {
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
self.display.brightness.store(255, core::sync::atomic::Ordering::Relaxed);
|
||||
self.panels.set_shader(Thinking::default());
|
||||
self.tail.set_shader(Tail::default());
|
||||
}
|
||||
_ => {
|
||||
warn!("Unimplemented scene {next_scene:?}!");
|
||||
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: Notification) {
|
||||
info!("UI Event: {event:?}");
|
||||
match event {
|
||||
// Apply the scene when it is changed
|
||||
Notification::SceneChange(_) => (), // We already log this inside apply_scene()
|
||||
evt => info!("UI event: {evt:?}")
|
||||
}
|
||||
match event {
|
||||
// TODO: We probably also want some events to indicate when the ESP has no calibration data or otherwise needs re-calibrated and is waiting for the bike to stand still
|
||||
Notification::SensorOnline(SensorSource::IMU) => self.flash_notification_color(Rgb::new(0, 255, 0)).await,
|
||||
Notification::SensorOffline(SensorSource::GPS) => self.flash_notification_color(Rgb::new(255, 0, 0)).await,
|
||||
Notification::SensorOnline(SensorSource::GPS) => self.flash_notification_color(Rgb::new(0, 255, 255)).await,
|
||||
|
||||
// Scene change
|
||||
Notification::SceneChange(scene) => self.apply_scene(scene).await,
|
||||
|
||||
// TODO: We probably also want some events to indicate when the ESP has no calibration data or otherwise needs re-calibrated and is waiting for the bike to stand still
|
||||
Notification::CalibrationComplete => self.flash_notification_color(Rgb::new(0, 255, 0)).await,
|
||||
Notification::GPSLost => self.flash_notification_color(Rgb::new(255, 0, 0)).await,
|
||||
Notification::GPSAcquired => self.flash_notification_color(Rgb::new(0, 255, 255)).await,
|
||||
Notification::WifiConnected => self.flash_notification_color(Rgb::new(0, 0, 255)).await,
|
||||
Notification::WifiDisconnected => self.flash_notification_color(Rgb::new(128, 0, 255)).await,
|
||||
|
||||
// Toggling head and brake lights
|
||||
Notification::SetBrakelight(is_on) => self.set_brakelight_on(is_on).await,
|
||||
Notification::SetHeadlight(is_on) => self.set_headlight_on(is_on).await
|
||||
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;
|
||||
}
|
||||
_ => ()
|
||||
|
||||
// Other event ideas:
|
||||
// - Bike has crashed, or is laid down
|
||||
@@ -281,23 +252,23 @@ impl<S: Surface<Uniforms = Uniforms, CoordinateSpace = BikeSpace, Pixel = Rgba<u
|
||||
}
|
||||
}
|
||||
|
||||
#[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<BufferedSurface<Uniforms, BikeSpace, Rgba<u8>>>) {
|
||||
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;
|
||||
//ui.display.render_is_running.wait().await;
|
||||
|
||||
// Run the fade sequence
|
||||
ui.startup_fade_sequence().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;
|
||||
|
||||
// Briefly turn on the brakelight while the headlight turns on
|
||||
ui.set_brakelight_on(true).await;
|
||||
// Turn on the headlight when we are finished
|
||||
ui.set_headlight_on(true).await;
|
||||
// Turn off the brakelight
|
||||
ui.set_brakelight_on(false).await;
|
||||
|
||||
// Flash white to indicate we are ready
|
||||
ui.flash_notification_color(Rgb::new(255, 255, 255)).await;
|
||||
// Enter the event loop
|
||||
loop {
|
||||
ui.on_event(events.receive().await).await;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user