events: rewrite the eventing system to reduce mutex usage to just the measurement bus

This commit is contained in:
2025-12-24 09:11:16 +01:00
parent 36f232f43c
commit 046406291a
11 changed files with 185 additions and 284 deletions

View File

@@ -12,7 +12,6 @@ pub mod demo;
pub mod oled_render;
// Prediction engines
pub mod predict;
pub mod motion;
// Graphics stack

View File

@@ -1,10 +1,10 @@
use embassy_sync::{channel::{DynamicReceiver, DynamicSender}, pubsub::DynPublisher};
use embassy_sync::{channel::DynamicReceiver, pubsub::DynPublisher};
use log::*;
use crate::{ego::engine::BikeStates, events::{Measurement, Notification, Prediction, SensorSource, SensorState}};
use crate::{ego::engine::BikeStates, events::{Measurement, Prediction}};
#[embassy_executor::task]
pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, prediction_sink: DynamicSender<'static, Prediction>) {
pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, prediction_sink: DynPublisher<'static, Prediction>) {
let mut states = BikeStates::default();
loop {
@@ -26,7 +26,7 @@ pub async fn motion_task(src: DynamicReceiver<'static, Measurement>, prediction_
// FIXME: This needs harmonized with the automatic data timeout from above, somehow?
Measurement::SensorHardwareStatus(source, state) => {
warn!("Sensor {source:?} reports {state:?}!");
prediction_sink.send(Prediction::SensorStatus(source, state)).await;
prediction_sink.publish(Prediction::SensorStatus(source, state)).await;
},
Measurement::SimulationProgress(source, duration, _pct) => debug!("{source:?} simulation time: {}", duration.as_secs()),
Measurement::Annotation => ()

View File

@@ -1,13 +1,12 @@
use alloc::sync::Arc;
use display_interface::DisplayError;
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex, pubsub::DynSubscriber};
use embassy_time::{Duration, Instant, Timer};
use embedded_graphics::{pixelcolor::BinaryColor, prelude::DrawTarget};
use figments::{mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::{RenderSource, Shader}, surface::{BufferedSurfacePool, NullSurface, NullBufferPool, Surface, SurfaceBuilder, Surfaces}};
use figments_render::output::{Brightness, OutputAsync};
use embassy_time::{Duration, Timer};
use embedded_graphics::pixelcolor::BinaryColor;
use figments::{mappings::embedded_graphics::Matrix2DSpace, prelude::{Coordinates, Rectangle}, render::Shader, surface::{BufferedSurfacePool, NullBufferPool, Surface, SurfaceBuilder, Surfaces}};
use figments_render::output::Brightness;
use log::*;
use crate::{animation::Animation, backoff::Backoff, events::{Notification, Prediction, SensorSource, SensorState, Telemetry}, graphics::{display::DisplayControls, oled_ui::{OledUniforms, Screen}}};
use crate::{animation::Animation, events::{Personality, Prediction}, graphics::{display::DisplayControls, oled_ui::{OledUniforms, Screen}}};
#[cfg(feature="oled")]
pub type OledUiSurfacePool = BufferedSurfacePool<OledUniforms, Matrix2DSpace, BinaryColor>;
@@ -22,7 +21,7 @@ pub type LockedUniforms = Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>;
pub struct OledUI<S: Surface + core::fmt::Debug> {
overlay: S,
controls: DisplayControls,
uniforms: Arc<Mutex<CriticalSectionRawMutex, OledUniforms>>
uniforms: LockedUniforms
}
struct OverlayShader {}
@@ -57,35 +56,32 @@ impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = Bina
FADE_OUT.apply(&mut self.overlay).await;
}
pub async fn on_event(&mut self, event: Telemetry) {
pub async fn on_event(&mut self, event: Prediction) {
match event {
// Waking and sleeping
Telemetry::Notification(Notification::Sleep) => {
warn!("Putting OLED display to sleep");
self.screen_transition(Screen::Sleeping).await;
Timer::after_secs(1).await;
self.screen_transition(Screen::Blank).await;
self.controls.set_on(false);
//ui_state.sleep = true
Prediction::SetPersonality(personality) => {
match personality {
Personality::Waking => {
warn!("Waking up OLED display");
self.controls.set_on(true);
self.screen_transition(Screen::Waking).await;
Timer::after_secs(1).await;
self.screen_transition(Screen::Home).await;
},
Personality::Sleeping => {
warn!("Putting OLED display to sleep");
self.screen_transition(Screen::Sleeping).await;
Timer::after_secs(1).await;
self.screen_transition(Screen::Blank).await;
self.controls.set_on(false);
},
_ => ()
}
self.with_uniforms(|state| { state.ui.personality = personality }).await;
},
Telemetry::Notification(Notification::WakeUp) => {
warn!("Waking up OLED display");
self.controls.set_on(true);
self.screen_transition(Screen::Waking).await;
Timer::after_secs(1).await;
self.screen_transition(Screen::Home).await;
//ui_state.sleep = false
},
// State updates
Telemetry::Prediction(Prediction::Velocity(v)) => self.with_uniforms(|state| {state.ui.velocity = v;}).await,
Telemetry::Prediction(Prediction::Location(loc)) => self.with_uniforms(|state| {state.ui.location = loc}).await,
Telemetry::Prediction(Prediction::Motion(motion)) => self.with_uniforms(|state| {state.ui.motion = motion}).await,
Telemetry::Notification(Notification::SceneChange(scene)) => self.with_uniforms(|state| {state.ui.scene = scene}).await,
Telemetry::Notification(Notification::SetBrakelight(b)) => self.with_uniforms(|state| {state.ui.brakelight = b}).await,
Telemetry::Notification(Notification::SetHeadlight(b)) => self.with_uniforms(|state| {state.ui.headlight = b}).await,
Telemetry::Notification(Notification::SensorStatus(src, sensor_state)) => self.with_uniforms(|state| {state.ui.sensor_states[src] = sensor_state}).await,
_ => ()
Prediction::Velocity(v) => self.with_uniforms(|state| {state.ui.velocity = v;}).await,
Prediction::Location(loc) => self.with_uniforms(|state| {state.ui.location = loc}).await,
Prediction::Motion{ prev: _, next: motion } => self.with_uniforms(|state| {state.ui.motion = motion}).await,
Prediction::SensorStatus(src, sensor_state, ) => self.with_uniforms(|state| {state.ui.sensor_states[src] = sensor_state}).await,
}
}
@@ -97,7 +93,7 @@ impl<S: core::fmt::Debug + Surface<CoordinateSpace = Matrix2DSpace, Pixel = Bina
}
#[embassy_executor::task]
pub async fn oled_ui(mut events: DynSubscriber<'static, Telemetry>, mut ui: OledUI<OledSurface>) {
pub async fn oled_ui(mut events: DynSubscriber<'static, Prediction>, mut ui: OledUI<OledSurface>) {
ui.screen_transition(Screen::Bootsplash).await;
Timer::after_secs(3).await;

View File

@@ -1,112 +0,0 @@
use embassy_sync::{channel::DynamicReceiver, pubsub::DynPublisher};
use embassy_time::Duration;
use log::*;
use crate::{ego::engine::{gps_to_local_meters_haversine, MotionState}, events::{Notification, Prediction, Scene, Telemetry}, idle::IdleClock};
#[embassy_executor::task]
pub async fn prediction_task(prediction_src: DynamicReceiver<'static, Prediction>, notify: DynPublisher<'static, Notification>, telemetery: DynPublisher<'static, Telemetry>) {
let mut last_velocity = 0.0;
let mut first_position = None;
let mut last_position = Default::default();
let mut parking_timer = IdleClock::new(Duration::from_secs(10));
let mut sleep_timer = IdleClock::new(Duration::from_secs(30));
let mut stationary = true;
loop {
let d = first_position.map(|x| {
gps_to_local_meters_haversine(&x, &last_position).norm()
});
if let Ok(next_evt) = embassy_time::with_timeout(Duration::from_secs(1), prediction_src.receive()).await {
telemetery.publish(Telemetry::Prediction(next_evt)).await;
match next_evt {
Prediction::WakeRequested => {
if sleep_timer.wake() {
warn!("Wake requested during sleep");
notify.publish(Notification::WakeUp).await;
notify.publish(Notification::SetHeadlight(true)).await;
notify.publish(Notification::SetBrakelight(true)).await;
// Also reset the parking timer
parking_timer.wake();
} else if parking_timer.wake() {
info!("Wake requested while parked");
// If we weren't asleep but we were parked, then switch back to the Ready state and turn on the lights
notify.publish(Notification::SetHeadlight(true)).await;
notify.publish(Notification::SetBrakelight(true)).await;
notify.publish(Notification::SceneChange(Scene::Ready)).await;
}
}
Prediction::Velocity(v) => {
last_velocity = v;
// TODO: Probably makes sense to only print this based on an IdleTimer, so that a long period of slightly variable movement doesn't get lost, but we can still report values to the UI / telemetry outputs
//info!("Velocity predict: velocity={v}\tpos={last_position:?}\tdistance={d:?}");
},
Prediction::Location(loc) => {
if first_position.is_none() {
info!("Got location={loc:?}");
first_position = Some(loc);
}
last_position = loc;
}
Prediction::Motion(motion) => {
info!("Motion predict:\t{motion:?}\tvelocity={last_velocity}\tpos={last_position:?}\tdistance={d:?}");
if sleep_timer.wake() {
notify.publish(Notification::WakeUp).await;
notify.publish(Notification::SetHeadlight(true)).await;
notify.publish(Notification::SetBrakelight(true)).await
}
if parking_timer.wake() {
notify.publish(Notification::SetHeadlight(true)).await;
notify.publish(Notification::SetBrakelight(true)).await
}
match motion {
MotionState::Accelerating => {
if stationary {
// If we are going from standing still to immediately accelerating, first transition to the 'ready' scene
notify.publish(Notification::SceneChange(Scene::Ready)).await;
}
notify.publish(Notification::SceneChange(Scene::Accelerating)).await;
stationary = false;
},
MotionState::Decelerating => {
if stationary {
// If we are going from standing still to immediately accelerating, first transition to the 'ready' scene
notify.publish(Notification::SceneChange(Scene::Ready)).await;
}
notify.publish(Notification::SceneChange(Scene::Decelerating)).await;
stationary = false;
},
MotionState::Steady => {
notify.publish(Notification::SceneChange(Scene::Ready)).await;
stationary = false;
},
MotionState::Stationary => {
notify.publish(Notification::SceneChange(Scene::Ready)).await;
stationary = true
}
}
},
Prediction::SensorStatus(src, status) => {
notify.publish(Notification::SensorStatus(src, status)).await;
}
}
}
// TODO: Need a way to detect if sensors are dead for some reason. Probably should be done in the motion engine, since it would be a prediction?
if stationary {
if parking_timer.check() {
warn!("Engaging parking brake");
notify.publish(Notification::SceneChange(Scene::Idle)).await;
notify.publish(Notification::SetHeadlight(false)).await;
notify.publish(Notification::SetBrakelight(false)).await
}
if sleep_timer.check() {
warn!("Sleeping!");
notify.publish(Notification::Sleep).await;
}
}
}
}

View File

@@ -1,5 +1,5 @@
use embassy_sync::pubsub::DynSubscriber;
use embassy_time::{Duration, Timer};
use embassy_time::Duration;
use figments::prelude::*;
use figments_render::output::Brightness;
use rgb::Rgba;
@@ -7,7 +7,7 @@ use core::fmt::Debug;
use futures::join;
use log::*;
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, events::{Notification, Prediction}, graphics::{display::{DisplayControls, SegmentSpace, Uniforms}, shaders::*}, tasks::ui::UiSurfacePool};
use crate::{animation::{AnimDisplay, AnimatedSurface, Animation}, events::{Personality, Prediction}, graphics::{display::{DisplayControls, SegmentSpace, Uniforms}, shaders::*}, tasks::ui::UiSurfacePool};
#[derive(Debug)]
pub struct SafetyUi<S: Surface> {
@@ -92,43 +92,35 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
info!("Fade out overlay");
TURN_OFF.apply(&mut self.overlay).await;
self.overlay.set_visible(false);
Timer::after_secs(3).await;
warn!("Turning off safety lights");
join!(
TURN_OFF.apply(&mut self.headlight),
TURN_OFF.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) => {
if is_on {
TURN_ON.apply(&mut self.brakelight).await;
} else {
TURN_OFF.apply(&mut self.brakelight).await;
}
pub async fn on_event(&mut self, event: Prediction) {
if let Prediction::SetPersonality(personality) = event { match personality {
Personality::Active => {
// FIXME: These should be a Off/Low/High enum, so the stopping brake looks different from the dayrunning brake.
warn!("Active personality: Turning on safety lights");
join!(
TURN_ON.apply(&mut self.brakelight),
TURN_ON.apply(&mut self.headlight)
);
},
Notification::SetHeadlight(is_on) => {
if is_on {
TURN_ON.apply(&mut self.headlight).await;
} else {
TURN_OFF.apply(&mut self.headlight).await;
}
Personality::Parked => {
warn!("Idle personality: Turning off safety lights");
join!(
TURN_OFF.apply(&mut self.brakelight),
TURN_OFF.apply(&mut self.headlight)
);
},
Notification::Sleep => self.sleep().await,
Notification::WakeUp => self.wake().await,
_ => ()
}
Personality::Sleeping => {
warn!("Sleeping personality: Safety UI is going to sleep");
self.sleep().await;
},
Personality::Waking => {
warn!("Waking personality: Waking up safety UI");
self.wake().await;
},
} }
}
}
@@ -136,7 +128,7 @@ const TURN_ON: Animation = Animation::new().duration(Duration::from_secs(1)).fro
const TURN_OFF: Animation = Animation::new().duration(Duration::from_secs(1)).from(255).to(0);
#[embassy_executor::task]
pub async fn safety_ui_main(mut events: DynSubscriber<'static, Notification>, mut ui: SafetyUi<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
pub async fn safety_ui_main(mut events: DynSubscriber<'static, Prediction>, mut ui: SafetyUi<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
// Wait for the renderer to start running
//ui.display.render_is_running.wait().await;
trace!("spooling until render starts ui={ui:?}");

View File

@@ -1,4 +1,4 @@
use embassy_sync::pubsub::{DynPublisher, DynSubscriber};
use embassy_sync::pubsub::DynSubscriber;
use embassy_time::{Duration, Timer};
use figments::prelude::*;
use rgb::{Rgb, Rgba};
@@ -6,7 +6,7 @@ use core::fmt::Debug;
use futures::join;
use log::*;
use crate::{animation::{AnimatedSurface, Animation}, events::{Notification, Scene, SensorSource, SensorState, Telemetry}, graphics::{display::{SegmentSpace, Uniforms}, shaders::*}};
use crate::{animation::{AnimatedSurface, Animation}, ego::engine::MotionState, events::{Personality, Prediction, Scene, SensorSource, SensorState}, graphics::{display::{SegmentSpace, Uniforms}, shaders::*}};
pub struct Ui<S: Surface> {
// Background layer provides an always-running background for everything to draw on
@@ -132,33 +132,32 @@ impl<S: Debug + Surface<Uniforms = Uniforms, CoordinateSpace = SegmentSpace, Pix
}
}
pub async fn on_event(&mut self, event: Notification) {
pub async fn on_event(&mut self, event: Prediction) {
match event {
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::SensorStatus(SensorSource::IMU, SensorState::Online) => self.flash_notification_color(Rgb::new(0, 255, 0)).await,
Notification::SensorStatus(SensorSource::Location, SensorState::Degraded) => self.flash_notification_color(Rgb::new(255, 0, 0)).await,
Notification::SensorStatus(SensorSource::GPS, SensorState::Online) => self.flash_notification_color(Rgb::new(0, 255, 255)).await,
// Scene change
Notification::SceneChange(scene) => self.apply_scene(scene).await,
Notification::WakeUp => self.show().await,
Prediction::SetPersonality(personality) => match personality {
Personality::Active => self.apply_scene(Scene::Ready).await,
Personality::Parked => self.apply_scene(Scene::Idle).await,
Personality::Waking => self.show().await,
_ => ()
},
Prediction::Motion { prev: _, next: MotionState::Accelerating } => self.apply_scene(Scene::Accelerating).await,
Prediction::Motion { prev: _, next: MotionState::Decelerating } => self.apply_scene(Scene::Decelerating).await,
Prediction::Motion { prev: _, next: MotionState::Steady } => self.apply_scene(Scene::Ready).await,
Prediction::Motion { prev: _, next: MotionState::Stationary } => self.apply_scene(Scene::Ready).await,
Prediction::SensorStatus(SensorSource::IMU, SensorState::Online) => self.flash_notification_color(Rgb::new(0, 255, 0)).await,
Prediction::SensorStatus(SensorSource::Location, SensorState::Degraded) => self.flash_notification_color(Rgb::new(255, 0, 0)).await,
Prediction::SensorStatus(SensorSource::GPS, SensorState::Online) => self.flash_notification_color(Rgb::new(0, 255, 255)).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?
}
// 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?
}
}
@@ -200,7 +199,7 @@ pub type UiSurfacePool = NullBufferPool<Uniforms, SegmentSpace, Rgba<u8>>;
pub type UiSurfacePool = BufferedSurfacePool<Uniforms, SegmentSpace, Rgba<u8>>;
#[embassy_executor::task]
pub async fn ui_main(mut events: DynSubscriber<'static, Notification>, telemetery: DynPublisher<'static, Telemetry>, mut ui: Ui<<UiSurfacePool as Surfaces<SegmentSpace>>::Surface>) {
pub async fn ui_main(mut events: DynSubscriber<'static, Prediction>, 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;
@@ -212,6 +211,5 @@ pub async fn ui_main(mut events: DynSubscriber<'static, Notification>, telemeter
loop {
let evt = events.next_message_pure().await;
ui.on_event(evt).await;
telemetery.publish(Telemetry::Notification(evt)).await;
}
}

View File

@@ -13,7 +13,7 @@ use nalgebra::Vector2;
use reqwless::client::{HttpClient, TlsConfig};
use static_cell::StaticCell;
use crate::{backoff::Backoff, events::{Prediction, Telemetry}};
use crate::{backoff::Backoff, events::{Prediction}};
#[embassy_executor::task]
async fn net_task(mut runner: embassy_net::Runner<'static, WifiDevice<'static>>) {
@@ -25,7 +25,7 @@ static RESOURCES: StaticCell<StackResources<5>> = StaticCell::new();
// TODO: Wifi task needs to know when there is data to upload, so it only connects when needed.
#[embassy_executor::task]
pub async fn wireless_task(mut telemetry: DynSubscriber<'static, Telemetry>, wifi_init: &'static mut Controller<'static>, wifi_device: esp_hal::peripherals::WIFI<'static>) {
pub async fn wireless_task(mut predictions: DynSubscriber<'static, Prediction>, wifi_init: &'static mut Controller<'static>, wifi_device: esp_hal::peripherals::WIFI<'static>) {
let (mut wifi, interfaces) = esp_radio::wifi::new(wifi_init, wifi_device, esp_radio::wifi::Config::default())
.expect("Failed to initialize WIFI!");
wifi.set_config(&esp_radio::wifi::ModeConfig::Client(
@@ -81,7 +81,7 @@ pub async fn wireless_task(mut telemetry: DynSubscriber<'static, Telemetry>, wif
let mut client = HttpClient::new_with_tls(&tcp, &dns, tls);
loop {
if let Telemetry::Prediction(Prediction::Location(coords)) = telemetry.next_message_pure().await {
if let Prediction::Location(coords) = predictions.next_message_pure().await {
if let Err(e) = push_location(&mut client, coords).await {
error!("HTTP error in publishing location: {e:?}");
break