src: implement ego tracking models, and port shaders from renderbug into here
This commit is contained in:
246
src/tasks/ui.rs
Normal file
246
src/tasks/ui.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
use embassy_sync::channel::DynamicReceiver;
|
||||
use embassy_time::{Duration, Instant, Timer};
|
||||
use figments::{liber8tion::interpolate::{ease_in_out_quad, Fract8}, prelude::*};
|
||||
use rgb::{Rgb, Rgba};
|
||||
use log::*;
|
||||
|
||||
use crate::{display::BikeSpace, events::{DisplayControls, Notification, Scene}, shaders::*, tasks::render::Uniforms};
|
||||
|
||||
|
||||
pub struct Ui<S: Surface> {
|
||||
// Background layer provides an always-running background for everything to draw on
|
||||
background: S,
|
||||
|
||||
// Tail and panels provide content
|
||||
tail: S,
|
||||
panels: S,
|
||||
|
||||
// Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc)
|
||||
notification:S,
|
||||
|
||||
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
|
||||
headlight: S,
|
||||
brakelight: 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
|
||||
}
|
||||
|
||||
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 {
|
||||
Self {
|
||||
background: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.shader(Background::default())
|
||||
.opacity(32)
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
tail: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 5, 255, 5))
|
||||
.opacity(96)
|
||||
.shader(Tail::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
panels: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 1, 255, 4))
|
||||
.opacity(128)
|
||||
.shader(Panel::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
notification: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.shader(Background::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
headlight: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::new_from_coordinates(0, 0, 255, 0))
|
||||
.shader(Headlight::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
brakelight: SurfaceBuilder::build(surfaces)
|
||||
.rect(Brakelight::rectangle())
|
||||
.shader(Brakelight::default())
|
||||
.visible(false)
|
||||
.finish().unwrap(),
|
||||
overlay: SurfaceBuilder::build(surfaces)
|
||||
.rect(Rectangle::everything())
|
||||
.visible(true)
|
||||
.opacity(0)
|
||||
.shader(Thinking::default())
|
||||
.finish().unwrap(),
|
||||
headlight_on: false,
|
||||
brakelight_on: false,
|
||||
display
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn flash_notification_color(&mut self, color: Rgb<u8>) {
|
||||
self.notification.set_shader(Background::from_color(color));
|
||||
self.notification.set_opacity(255);
|
||||
self.notification.set_visible(true);
|
||||
Self::animate_duration(Duration::from_secs(3), |pct| {
|
||||
self.notification.set_opacity(255 * pct);
|
||||
}).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 startup_fade_sequence(&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);
|
||||
self.overlay.set_visible(true);
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.overlay.set_opacity(ease_in_out_quad(pct));
|
||||
}).await;
|
||||
// When the overlay is fully opaque and all lower layers will be hidden, turn them on
|
||||
self.apply_scene(Scene::Startup).await;
|
||||
// Then fade out the overlay over 3 seconds, allowing the base animations to be shown
|
||||
Self::animate_duration(Duration::from_secs(3), |pct| {
|
||||
self.overlay.set_opacity(ease_in_out_quad(255 - pct));
|
||||
}).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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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::ParkedIdle => {
|
||||
// Ensure the display is on
|
||||
self.display.on.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
// Hide the content; only notifications will remain
|
||||
self.background.set_visible(true);
|
||||
self.tail.set_visible(false);
|
||||
self.panels.set_visible(false);
|
||||
Self::animate_duration(Duration::from_secs(1), |pct| {
|
||||
self.display.brightness.store(128.scale8(255 - pct), core::sync::atomic::Ordering::Relaxed);
|
||||
}).await;
|
||||
},
|
||||
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);
|
||||
}
|
||||
_ => unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn on_event(&mut self, event: Notification) {
|
||||
match event {
|
||||
// Apply the scene when it is changed
|
||||
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
|
||||
|
||||
// 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?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[embassy_executor::task]
|
||||
pub async fn ui_main(events: DynamicReceiver<'static, Notification>, mut ui: Ui<BufferedSurface<Uniforms, BikeSpace, Rgba<u8>>>) {
|
||||
// Run the fade sequence
|
||||
ui.startup_fade_sequence().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;
|
||||
loop {
|
||||
ui.on_event(events.receive().await).await;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user