bump a lot of big changes I dont want to break down into individual commits

This commit is contained in:
2025-10-11 16:34:09 +02:00
parent 0e9e0c1b13
commit 8280f38185
48 changed files with 1275467 additions and 394056 deletions

View File

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