diff --git a/Cargo.toml b/Cargo.toml index 16a5a75..eaf4daf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/build.rs b/build.rs index e9ae3d9..f78d992 100644 --- a/build.rs +++ b/build.rs @@ -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() { diff --git a/src/bin/main.rs b/src/bin/main.rs index 9129ab4..54e353d 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -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 = 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)); diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index 7ab70de..eca5cc9 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -10,4 +10,6 @@ pub mod wifi; #[cfg(feature="simulation")] pub mod simulation; pub mod predict; -pub mod demo; \ No newline at end of file +pub mod demo; +pub mod safetyui; +pub mod oled; \ No newline at end of file diff --git a/src/tasks/safetyui.rs b/src/tasks/safetyui.rs new file mode 100644 index 0000000..69d8c99 --- /dev/null +++ b/src/tasks/safetyui.rs @@ -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 { + // Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer + headlight: AnimatedSurface, + brakelight: AnimatedSurface, + + // The overlay covers everything and is used to implement a power-on and power-off animation. + overlay: AnimatedSurface, + display: DisplayControls +} + +impl>> SafetyUi { + pub fn new>(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<>::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; + } +} \ No newline at end of file diff --git a/src/tasks/ui.rs b/src/tasks/ui.rs index 50cc3a9..1846c5b 100644 --- a/src/tasks/ui.rs +++ b/src/tasks/ui.rs @@ -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 { // Background layer provides an always-running background for everything to draw on @@ -21,18 +21,10 @@ pub struct Ui { // Notification layer sits on top of the content, and is used for transient event notifications (gps lost, wifi found, etc) notification: AnimatedSurface, - - // Headlight and brakelight layers can only be overpainted by the bootsplash overlay layer - headlight: AnimatedSurface, - brakelight: AnimatedSurface, - - // The overlay covers everything and is used to implement a power-on and power-off animation. - overlay: AnimatedSurface, - display: Arc } impl>> Ui { - pub fn new>(surfaces: &mut SS, display: Arc) -> Self where SS::Error: Debug { + pub fn new>(surfaces: &mut SS) -> Self where SS::Error: Debug { Self { background: SurfaceBuilder::build(surfaces) .rect(Rectangle::everything()) @@ -58,24 +50,7 @@ impl [&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 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>> Surface for Ui { + type Uniforms = S::Uniforms; + + type CoordinateSpace = S::CoordinateSpace; + + type Pixel = S::Pixel; + + fn set_shader + 'static>(&mut self, shader: T) { + unimplemented!() + } + + fn clear_shader(&mut self) { + self.as_slice().clear_shader(); + } + + fn set_rect(&mut self, rect: Rectangle) { + 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.as_slice().set_offset(offset); + } +} + #[cfg(feature="headless")] pub type UiSurfacePool = NullBufferPool>>; #[cfg(not(feature="headless"))] pub type UiSurfacePool = BufferedSurfacePool>; #[embassy_executor::task] -pub async fn ui_main(events: DynamicReceiver<'static, Notification>, mut ui: Ui<>::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<>::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; } } \ No newline at end of file