ui: split out safety ui into its own dedicated set of layers

This commit is contained in:
2025-10-17 14:40:07 +02:00
parent 818f8af49a
commit a97a28bf9f
6 changed files with 235 additions and 114 deletions

View File

@@ -25,6 +25,7 @@ wokwi = ["max-usb-power"]
headless = []
mpu = ["dep:mpu6050-dmp"]
gps = ["dep:nmea"]
oled = []
demo = []
[dependencies]
@@ -137,3 +138,4 @@ overflow-checks = false
[build-dependencies]
image = "0.25.8"
rmp = "0.8"

View File

@@ -3,6 +3,7 @@ use std::io::Write;
use std::path::Path;
use std::fs::File;
use image::GenericImageView;
use rmp;
fn main() {
linker_be_nice();
@@ -54,6 +55,13 @@ fn main() {
println!("cargo::rerun-if-changed={fname_str}");
}
}
/*let test_data_path = Path::new("test-data");
let gps_data = File::open(test_data_path.join("LocationGps.csv")).unwrap();
let accel_data = File::open(test_data_path.join("AccelerometerUncalibrated.csv")).unwrap();
let gyro_data = File::open(test_data_path.join("GyroscopeUncalibrated.csv")).unwrap();
let mut test_data_output = File::create(Path::new("target/test_data.rs")).unwrap();*/
}
fn linker_be_nice() {

View File

@@ -11,6 +11,7 @@ use core::ptr::addr_of_mut;
use embassy_executor::Spawner;
use embassy_time::{Instant, Timer};
use esp_hal::{gpio::{Output, OutputConfig}, peripherals, time::Rate};
#[allow(unused_imports)]
use esp_hal::{
clock::CpuClock, interrupt::software::SoftwareInterruptControl, system::{AppCoreGuard, CpuControl, Stack}, timer::{systimer::SystemTimer, timg::{TimerGroup, Wdt}},
@@ -19,7 +20,7 @@ use esp_hal::{
use esp_hal_embassy::{Executor, InterruptExecutor};
use log::*;
use renderbug_embassy::{logging::RenderbugLogger, tasks::ui::UiSurfacePool};
use renderbug_embassy::{logging::RenderbugLogger, tasks::{safetyui::{safety_ui_main, SafetyUi}, ui::UiSurfacePool}};
use renderbug_embassy::events::BusGarage;
use static_cell::StaticCell;
use esp_backtrace as _;
@@ -45,7 +46,6 @@ static CORE_HANDLE: StaticCell<AppCoreGuard> = StaticCell::new();
async fn main(spawner: Spawner) {
critical_section::with(|_| {
RenderbugLogger::init_logger();
//esp_println::logger::init_logger_from_env();
});
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
@@ -62,13 +62,15 @@ async fn main(spawner: Spawner) {
let timer1 = TimerGroup::new(peripherals.TIMG1);
let mut ui_wdt = timer1.wdt;
ui_wdt.set_timeout(esp_hal::timer::timg::MwdtStage::Stage0, esp_hal::time::Duration::from_secs(10));
//ui_wdt.enable(); //FIXME: Re-enable UI watchdog once we have a brain task running
ui_wdt.enable(); //FIXME: Re-enable UI watchdog once we have a brain task running
let garage = BUS_GARAGE.init(Default::default());
info!("Setting up rendering pipeline");
let mut surfaces = UiSurfacePool::default();
let ui = Ui::new(&mut surfaces, garage.display.clone());
let ui = Ui::new(&mut surfaces);
let mut safety_surfaces = UiSurfacePool::default();
let safety_ui = SafetyUi::new(&mut safety_surfaces, garage.display.clone());
let mut wdt = timer0.wdt;
wdt.set_timeout(esp_hal::timer::timg::MwdtStage::Stage0, esp_hal::time::Duration::from_secs(3));
@@ -80,7 +82,7 @@ async fn main(spawner: Spawner) {
#[cfg(not(feature="headless"))]
{
wdt.enable();
hi_spawn.must_spawn(renderbug_embassy::tasks::render::render(peripherals.RMT, peripherals.GPIO5.degrade(), surfaces, garage.display.clone(), wdt));
hi_spawn.must_spawn(renderbug_embassy::tasks::render::render(peripherals.RMT, peripherals.GPIO5.degrade(), surfaces, safety_surfaces, garage.display.clone(), wdt));
}
#[cfg(feature="headless")]
garage.display.notify_render_is_running(true);
@@ -104,6 +106,26 @@ async fn main(spawner: Spawner) {
spawner.must_spawn(renderbug_embassy::tasks::gps::gps_task(garage.motion.dyn_sender(), I2cDevice::new(i2c_bus)));
}
#[cfg(feature="oled")]
{
use esp_hal::i2c::master::{Config, I2c};
let mut rst = Output::new(peripherals.GPIO21, esp_hal::gpio::Level::Low, OutputConfig::default());
Timer::after_millis(10).await;
rst.set_high();
Timer::after_millis(10).await;
rst.set_low();
Timer::after_millis(10).await;
rst.set_high();
let i2c = I2c::new(
peripherals.I2C0,
Config::default().with_frequency(Rate::from_khz(400))
).unwrap().with_scl(peripherals.GPIO18).with_sda(peripherals.GPIO17).into_async();
spawner.must_spawn(renderbug_embassy::tasks::oled::oled_task(i2c, garage.telemetry.dyn_subscriber().unwrap()));
}
#[cfg(feature="simulation")]
{
spawner.must_spawn(renderbug_embassy::tasks::simulation::motion_simulation_task(garage.motion.dyn_sender()));
@@ -111,7 +133,7 @@ async fn main(spawner: Spawner) {
}
info!("Launching motion engine");
spawner.must_spawn(motion_task(garage.motion.dyn_receiver(), garage.notify.dyn_sender(), garage.predict.dyn_sender()));
spawner.must_spawn(motion_task(garage.motion.dyn_receiver(), garage.notify.dyn_publisher().unwrap(), garage.predict.dyn_sender()));
info!("Starting core 2");
let mut cpu_control = CpuControl::new(peripherals.CPU_CTRL);
@@ -122,19 +144,21 @@ async fn main(spawner: Spawner) {
#[cfg(feature="radio")]
{
info!("Launching wifi");
spawner.must_spawn(renderbug_embassy::tasks::wifi::wireless_task(garage.notify.dyn_receiver(), timer0.timer0.into(), peripherals.RNG, peripherals.WIFI, peripherals.BT));
//spawner.must_spawn(renderbug_embassy::tasks::wifi::wireless_task(garage.notify.dyn_receiver().unwrap(), timer0.timer0.into(), peripherals.RNG, peripherals.WIFI, peripherals.BT));
}
info!("Launching Safety UI");
spawner.must_spawn(safety_ui_main(garage.notify.dyn_subscriber().unwrap(), safety_ui));
info!("Launching UI");
spawner.must_spawn(ui_main(garage.notify.dyn_receiver(), ui));
spawner.must_spawn(ui_main(garage.notify.dyn_subscriber().unwrap(), garage.telemetry.dyn_publisher().unwrap(), ui));
#[cfg(feature="demo")]
{
warn!("Launching with demo sequencer");
spawner.must_spawn(renderbug_embassy::tasks::demo::demo_task(garage.notify.dyn_sender()));
spawner.must_spawn(renderbug_embassy::tasks::demo::demo_task(garage.notify.dyn_publisher().unwrap()));
}
#[cfg(not(feature="demo"))]
{
info!("Launching prediction engine");
spawner.must_spawn(renderbug_embassy::tasks::predict::prediction_task(garage.predict.dyn_receiver(), garage.notify.dyn_sender()));
spawner.must_spawn(renderbug_embassy::tasks::predict::prediction_task(garage.predict.dyn_receiver(), garage.notify.dyn_publisher().unwrap(), garage.telemetry.dyn_publisher().unwrap()));
}
info!("Launching core 2 watchdog");
spawner.must_spawn(wdt_task(ui_wdt));

View File

@@ -11,3 +11,5 @@ pub mod wifi;
pub mod simulation;
pub mod predict;
pub mod demo;
pub mod safetyui;
pub mod oled;

136
src/tasks/safetyui.rs Normal file
View File

@@ -0,0 +1,136 @@
use embassy_sync::{channel::DynamicReceiver, pubsub::DynSubscriber};
use embassy_time::{Duration, Timer};
use figments::prelude::*;
use figments_render::output::Brightness;
use rgb::{Rgb, Rgba};
use log::*;
use alloc::sync::Arc;
use core::fmt::Debug;
use futures::join;
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{DisplayControls, SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource}, shaders::*, tasks::ui::UiSurfacePool};
pub struct SafetyUi<S: Surface> {
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
headlight: AnimatedSurface<S>,
brakelight: AnimatedSurface<S>,
// The overlay covers everything and is used to implement a power-on and power-off animation.
overlay: AnimatedSurface<S>,
display: DisplayControls
}
impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pixel = Rgba<u8>>> SafetyUi<S> {
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS, display: DisplayControls) -> Self where SS::Error: Debug {
Self {
overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())
.shader(Thinking::default())
.visible(false)
.finish().unwrap().into(),
headlight: SurfaceBuilder::build(surfaces)
.rect(Rectangle::new_from_coordinates(0, 0, 255, 0))
.shader(Headlight::default())
.visible(false)
.opacity(0)
.finish().unwrap().into(),
brakelight: SurfaceBuilder::build(surfaces)
.rect(Brakelight::rectangle())
.shader(Brakelight::default())
.visible(false)
.opacity(0)
.finish().unwrap().into(),
display
}
}
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(&mut self.display);
fade_out.apply(&mut disp_anim).await;
warn!("Resetting safety lights");
self.brakelight.set_visible(false);
self.headlight.set_visible(false);
warn!("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;
}
pub async fn wake(&mut self) {
info!("Running startup fade sequence");
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);
info!("Fading in brightness with overlay");
self.overlay.set_opacity(255);
self.overlay.set_visible(true);
fade_in.apply(&mut AnimDisplay(&mut self.display)).await;
warn!("Turning on safety lights");
self.headlight.set_opacity(0);
self.headlight.set_visible(true);
self.brakelight.set_opacity(0);
self.brakelight.set_visible(true);
join!(
fade_in.apply(&mut self.headlight),
fade_in.apply(&mut self.brakelight)
);
let fade_out = Animation::default().duration(Duration::from_secs(1)).from(255).to(0);
info!("Fade out overlay");
fade_out.apply(&mut self.overlay).await;
self.overlay.set_visible(false);
Timer::after_secs(3).await;
warn!("Turning off safety lights");
join!(
fade_out.apply(&mut self.headlight),
fade_out.apply(&mut self.brakelight)
);
info!("Wakeup complete!");
}
pub async fn on_event(&mut self, event: Notification) {
match event {
Notification::SceneChange(_) => (), // We already log this inside apply_scene()
evt => info!("SafetyUI event: {evt:?}")
}
match event {
// Toggling head and brake lights
// FIXME: These should be a Off/Low/High enum, so the stopping brake looks different from the dayrunning brake.
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.wake().await,
_ => ()
}
}
}
#[embassy_executor::task]
pub async fn safety_ui_main(mut events: DynSubscriber<'static, Notification>, mut ui: SafetyUi<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
// Wait for the renderer to start running
//ui.display.render_is_running.wait().await;
ui.display.wait_until_render_is_running().await;
// Run the wake sequence, and turn on the lights
ui.wake().await;
// Enter the event loop
loop {
ui.on_event(events.next_message_pure().await).await;
}
}

View File

@@ -1,5 +1,5 @@
use embassy_sync::channel::DynamicReceiver;
use embassy_time::Duration;
use embassy_sync::{channel::{DynamicReceiver, DynamicSender}, pubsub::{DynPublisher, DynSubscriber}};
use embassy_time::{Duration, Timer};
use figments::prelude::*;
use rgb::{Rgb, Rgba};
use log::*;
@@ -7,7 +7,7 @@ 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::*};
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, display::{SegmentSpace, Uniforms}, events::{Notification, Scene, SensorSource, Telemetry}, shaders::*};
pub struct Ui<S: Surface> {
// Background layer provides an always-running background for everything to draw on
@@ -21,18 +21,10 @@ pub struct Ui<S: Surface> {
// Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc)
notification: AnimatedSurface<S>,
// Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer
headlight: AnimatedSurface<S>,
brakelight: AnimatedSurface<S>,
// The overlay covers everything and is used to implement a power-on and power-off animation.
overlay: AnimatedSurface<S>,
display: Arc<DisplayControls>
}
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 {
pub fn new<SS: Surfaces<SegmentSpace, Surface = S>>(surfaces: &mut SS) -> Self where SS::Error: Debug {
Self {
background: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())
@@ -58,24 +50,7 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
.rect(Rectangle::everything())
.shader(Background::default())
.visible(false)
.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().into(),
brakelight: SurfaceBuilder::build(surfaces)
.rect(Brakelight::rectangle())
.shader(Brakelight::default())
.visible(false)
.finish().unwrap().into(),
overlay: SurfaceBuilder::build(surfaces)
.rect(Rectangle::everything())
.shader(Thinking::default())
.visible(false)
.finish().unwrap().into(),
display
.finish().unwrap().into()
}
}
@@ -102,71 +77,18 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
self.notification.set_visible(false);
}
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
pub fn as_slice(&mut self) -> [&mut S; 4] {
[
&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 async fn wakeup(&mut self) {
info!("Running startup fade sequence");
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);
join!(
fade_in.apply(&mut self.overlay),
fade_in.apply(&mut disp_anim)
);
pub async fn show(&mut self) {
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!("Wakeup complete!");
self.as_slice().set_visible(true);
}
// TODO: Brakelight should only be toggled when actually braking or stationary
@@ -225,18 +147,13 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
// Scene change
Notification::SceneChange(scene) => self.apply_scene(scene).await,
// Toggling head and brake lights
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;
}
Notification::WakeUp => self.show().await,
_ => ()
// Other event ideas:
@@ -252,24 +169,56 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
}
}
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(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<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
// Wait for the renderer to start running
//ui.display.render_is_running.wait().await;
pub async fn ui_main(mut events: DynSubscriber<'static, Notification>, telemetery: DynPublisher<'static, Telemetry>, 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;
// 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;
// Enter the startup scene
ui.apply_scene(Scene::Ready).await;
// Enter the event loop
loop {
ui.on_event(events.receive().await).await;
let evt = events.next_message_pure().await;
ui.on_event(evt).await;
telemetery.publish(Telemetry::Notification(evt)).await;
}
}