From 7d90c4fbe6d77d8c81d4b3609d0dc5f30d43ae52 Mon Sep 17 00:00:00 2001 From: Cyril Fougeray Date: Tue, 3 Sep 2024 10:26:13 +0200 Subject: [PATCH] orb-ui: simulation: select on future instead of spawning a new thread for each task, await concurrently. --- orb-ui/cone/examples/cone-simulation.rs | 292 ++++++++++++------------ orb-ui/cone/src/lcd.rs | 16 +- orb-ui/cone/src/led.rs | 13 +- orb-ui/cone/src/lib.rs | 14 +- 4 files changed, 176 insertions(+), 159 deletions(-) diff --git a/orb-ui/cone/examples/cone-simulation.rs b/orb-ui/cone/examples/cone-simulation.rs index 9fd1603..7fd4196 100644 --- a/orb-ui/cone/examples/cone-simulation.rs +++ b/orb-ui/cone/examples/cone-simulation.rs @@ -4,21 +4,141 @@ use color_eyre::eyre; use color_eyre::eyre::{eyre, Context}; use tokio::sync::broadcast; use tokio::sync::broadcast::error::RecvError; -use tokio::task; -use tokio::task::JoinHandle; use tracing::level_filters::LevelFilter; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{fmt, EnvFilter}; use orb_cone::led::CONE_LED_COUNT; -use orb_cone::{ButtonState, ConeEvent}; +use orb_cone::{ButtonState, Cone, ConeEvent}; use orb_rgb::Argb; const CONE_LED_STRIP_DIMMING_DEFAULT: u8 = 10_u8; const CONE_SIMULATION_UPDATE_PERIOD_S: u64 = 2; const CONE_LED_STRIP_MAXIMUM_BRIGHTNESS: u8 = 20; +enum SimulationState { + Idle = 0, + Red, + Green, + Blue, + Logo, + QrCode, + StateCount, +} + +impl From for SimulationState { + fn from(value: u8) -> Self { + match value { + 0 => SimulationState::Idle, + 1 => SimulationState::Red, + 2 => SimulationState::Green, + 3 => SimulationState::Blue, + 4 => SimulationState::Logo, + 5 => SimulationState::QrCode, + _ => SimulationState::Idle, + } + } +} + +async fn simulation_task(cone: &mut Cone) -> eyre::Result<()> { + let mut counter = SimulationState::Idle; + loop { + tokio::time::sleep(std::time::Duration::from_secs( + CONE_SIMULATION_UPDATE_PERIOD_S, + )) + .await; + + let mut pixels = [Argb::default(); CONE_LED_COUNT]; + match counter { + SimulationState::Idle => { + cone.queue_lcd_fill(Argb::DIAMOND_USER_IDLE)?; + for pixel in pixels.iter_mut() { + *pixel = Argb::DIAMOND_USER_IDLE; + } + } + SimulationState::Red => { + cone.queue_lcd_fill(Argb::FULL_RED)?; + for pixel in pixels.iter_mut() { + *pixel = Argb::FULL_RED; + pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT); + } + } + SimulationState::Green => { + cone.queue_lcd_fill(Argb::FULL_GREEN)?; + for pixel in pixels.iter_mut() { + *pixel = Argb::FULL_GREEN; + pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT); + } + } + SimulationState::Blue => { + cone.queue_lcd_fill(Argb::FULL_BLUE)?; + for pixel in pixels.iter_mut() { + *pixel = Argb::FULL_BLUE; + pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT); + } + } + SimulationState::Logo => { + // show logo if file exists + let filename = "logo.bmp"; + if std::path::Path::new(filename).exists() { + cone.queue_lcd_bmp(String::from(filename))?; + } else { + tracing::debug!("🚨 File not found: {filename}"); + cone.queue_lcd_fill(Argb::FULL_BLACK)?; + } + for pixel in pixels.iter_mut() { + *pixel = Argb( + Some(CONE_LED_STRIP_DIMMING_DEFAULT), + // random + rand::random::() % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS, + rand::random::() % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS, + rand::random::() % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS, + ); + } + } + SimulationState::QrCode => { + cone.queue_lcd_qr_code(String::from("https://www.worldcoin.org/"))?; + for pixel in pixels.iter_mut() { + *pixel = Argb::DIAMOND_USER_AMBER; + } + } + _ => {} + } + cone.queue_rgb_leds(&pixels)?; + counter = SimulationState::from( + (counter as u8 + 1) % SimulationState::StateCount as u8, + ); + } +} + +async fn listen_cone_events( + mut rx: broadcast::Receiver, +) -> eyre::Result<()> { + let mut button_state = ButtonState::Released; + loop { + match rx.recv().await { + Ok(event) => match event { + ConeEvent::Button(state) => { + if state != button_state { + tracing::info!("🔘 Button {:?}", state); + button_state = state; + } + } + ConeEvent::Cone(state) => { + tracing::info!("🔌 Cone {:?}", state); + } + }, + Err(RecvError::Closed) => { + return Err(eyre!("Cone events channel closed, cone disconnected?")); + } + Err(RecvError::Lagged(skipped)) => { + tracing::warn!("🚨 Skipped {} cone events", skipped); + } + } + } +} + #[tokio::main] async fn main() -> eyre::Result<()> { let registry = tracing_subscriber::registry(); @@ -38,151 +158,31 @@ async fn main() -> eyre::Result<()> { tracing::debug!("Device: {:?}", device); } - loop { - let (tx, mut rx) = broadcast::channel(10); - if let Ok((mut cone, cone_handles)) = orb_cone::Cone::spawn(tx) { - // spawn a thread to receive events - let button_listener_task: JoinHandle> = - task::spawn(async move { - let mut button_state = ButtonState::Released; - loop { - match rx.recv().await { - Ok(event) => match event { - ConeEvent::Button(state) => { - if state != button_state { - tracing::info!("🔘 Button {:?}", state); - button_state = state; - } - } - ConeEvent::Cone(state) => { - tracing::info!("🔌 Cone {:?}", state); - } - }, - Err(RecvError::Closed) => { - return Err(eyre!( - "Cone events channel closed, cone disconnected?" - )) - } - Err(RecvError::Lagged(skipped)) => { - tracing::warn!("🚨 Skipped {} cone events", skipped); - } - } - } - }); - - // create one shot to gracefully terminate simulation - let (kill_sim_tx, mut kill_sim_rx) = tokio::sync::oneshot::channel::<()>(); - let simulation_task: JoinHandle> = tokio::task::spawn( - async move { - let mut counter = 0; - loop { - tokio::select! { - _ = &mut kill_sim_rx => { - return Ok(()); - } - _ = tokio::time::sleep(std::time::Duration::from_secs(CONE_SIMULATION_UPDATE_PERIOD_S)) => { - let mut pixels = [Argb::default(); CONE_LED_COUNT]; - match counter { - 0 => { - cone.queue_lcd_fill(Argb::DIAMOND_USER_IDLE)?; - for pixel in pixels.iter_mut() { - *pixel = Argb::DIAMOND_USER_IDLE; - } - } - 1 => { - cone.queue_lcd_fill(Argb::FULL_RED)?; - for pixel in pixels.iter_mut() { - *pixel = Argb::FULL_RED; - pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT); - } - } - 2 => { - cone.queue_lcd_fill(Argb::FULL_GREEN)?; - for pixel in pixels.iter_mut() { - *pixel = Argb::FULL_GREEN; - pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT); - } - } - 3 => { - cone.queue_lcd_fill(Argb::FULL_BLUE)?; - for pixel in pixels.iter_mut() { - *pixel = Argb::FULL_BLUE; - pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT); - } - } - 4 => { - // show logo if file exists - let filename = "logo.bmp"; - if std::path::Path::new(filename).exists() { - cone.queue_lcd_bmp(String::from(filename))?; - } else { - tracing::debug!("🚨 File not found: {filename}"); - cone.queue_lcd_fill(Argb::FULL_BLACK)?; - } - for pixel in pixels.iter_mut() { - *pixel = Argb( - Some(CONE_LED_STRIP_DIMMING_DEFAULT), - // random - rand::random::() - % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS, - rand::random::() - % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS, - rand::random::() - % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS, - ); - } - } - 5 => { - cone.queue_lcd_qr_code(String::from( - "https://www.worldcoin.org/", - ))?; - for pixel in pixels.iter_mut() { - *pixel = Argb::DIAMOND_USER_AMBER; - } - } - _ => {} - } - cone.queue_rgb_leds(&pixels)?; - } - } // end tokio::select! - counter = (counter + 1) % 6; - } - }, - ); + let (cone_events_tx, cone_events_rx) = broadcast::channel(10); + let (mut cone, cone_handles) = Cone::spawn(cone_events_tx)?; - tracing::info!("🍦 Cone up and running!"); - tracing::info!("Press ctrl-c to exit."); + tracing::info!("🍦 Cone up and running!"); + tracing::info!("Press ctrl-c to exit."); - // upon completion of either task, cancel all the other tasks - // and return the result - let res = tokio::select! { - res = button_listener_task => { - tracing::debug!("Button listener task completed"); - res? - }, - res = simulation_task => { - tracing::debug!("Simulation task completed"); - res? - }, - // Needed to cleanly call destructors. - result = tokio::signal::ctrl_c() => { - tracing::debug!("ctrl-c received"); - result.wrap_err("failed to listen for ctrl-c") - } - }; - - // to drop the cone, stop the simulation - // then wait for all tasks to stop - drop(kill_sim_tx); - cone_handles.join().await?; - - if res.is_ok() { - return Ok(()); - } - } else { - tracing::error!("Failed to connect to cone..."); + // upon completion of either task, select! will cancel all the other branches + let res = tokio::select! { + res = listen_cone_events(cone_events_rx) => { + tracing::debug!("Button listener task completed"); + res + }, + res = simulation_task(&mut cone) => { + tracing::debug!("Simulation task completed"); + res + }, + // Needed to cleanly call destructors. + result = tokio::signal::ctrl_c() => { + tracing::debug!("ctrl-c received"); + result.wrap_err("failed to listen for ctrl-c") } + }; - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } + // wait for all tasks to stop + cone_handles.join().await?; + + res } diff --git a/orb-ui/cone/src/lcd.rs b/orb-ui/cone/src/lcd.rs index 7690753..c63c473 100644 --- a/orb-ui/cone/src/lcd.rs +++ b/orb-ui/cone/src/lcd.rs @@ -21,6 +21,12 @@ type LcdDisplayDriver<'a> = Gc9a01< pub struct LcdJoinHandle(pub JoinHandle>); +/// LcdCommand channel size +/// At least a second should be spent between commands for the user +/// to actually see the changes on the screen so the limit should +/// never be a blocker. +const LCD_COMMAND_CHANNEL_SIZE: usize = 2; + /// Lcd handle to send commands to the LCD screen. /// /// The LCD is controlled by a separate task. @@ -30,7 +36,7 @@ pub struct Lcd { /// Used to signal that the task should be cleanly terminated. pub kill_tx: oneshot::Sender<()>, /// Send commands to the LCD task - cmd_tx: mpsc::UnboundedSender, + cmd_tx: mpsc::Sender, } /// Commands to the LCD @@ -44,7 +50,7 @@ pub enum LcdCommand { impl Lcd { pub(crate) fn spawn() -> eyre::Result<(Lcd, LcdJoinHandle)> { - let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel(); + let (cmd_tx, mut cmd_rx) = mpsc::channel(LCD_COMMAND_CHANNEL_SIZE); let (kill_tx, kill_rx) = oneshot::channel(); let task_handle = @@ -53,14 +59,14 @@ impl Lcd { Ok((Lcd { cmd_tx, kill_tx }, LcdJoinHandle(task_handle))) } - pub(crate) fn send(&mut self, cmd: LcdCommand) -> eyre::Result<()> { - self.cmd_tx.send(cmd).wrap_err("failed to send") + pub(crate) fn tx(&self) -> &mpsc::Sender { + &self.cmd_tx } } /// Entry point for the lcd update task fn do_lcd_update( - cmd_rx: &mut mpsc::UnboundedReceiver, + cmd_rx: &mut mpsc::Receiver, mut kill_rx: oneshot::Receiver<()>, ) -> eyre::Result<()> { let mut delay = Delay::new(); diff --git a/orb-ui/cone/src/led.rs b/orb-ui/cone/src/led.rs index 2208555..0ae9f7c 100644 --- a/orb-ui/cone/src/led.rs +++ b/orb-ui/cone/src/led.rs @@ -14,14 +14,19 @@ pub const CONE_LED_COUNT: usize = 64; pub struct LedStrip { /// Used to signal that the task should be cleanly terminated. pub kill_tx: oneshot::Sender<()>, - tx: mpsc::UnboundedSender<[Argb; CONE_LED_COUNT]>, + tx: mpsc::Sender<[Argb; CONE_LED_COUNT]>, } pub struct LedJoinHandle(pub task::JoinHandle>); +/// The channel will buffer up to 2 LED frames. +/// If the receiver is full, the frame should be dropped, so that any new frame containing +/// the latest state can be sent once the receiver is ready to receive them. +const LED_CHANNEL_SIZE: usize = 2; + impl LedStrip { pub(crate) fn spawn() -> eyre::Result<(Self, LedJoinHandle)> { - let (tx, mut rx) = mpsc::unbounded_channel(); + let (tx, mut rx) = mpsc::channel(LED_CHANNEL_SIZE); let (kill_tx, mut kill_rx) = oneshot::channel(); // spawn receiver thread @@ -70,8 +75,8 @@ impl LedStrip { Ok((LedStrip { tx, kill_tx }, LedJoinHandle(task))) } - pub(crate) fn send(&mut self, values: &[Argb; CONE_LED_COUNT]) -> eyre::Result<()> { - self.tx.send(*values).wrap_err("failed to send") + pub(crate) fn tx(&self) -> &mpsc::Sender<[Argb; CONE_LED_COUNT]> { + &self.tx } } diff --git a/orb-ui/cone/src/lib.rs b/orb-ui/cone/src/lib.rs index 3196c66..ad31501 100644 --- a/orb-ui/cone/src/lib.rs +++ b/orb-ui/cone/src/lib.rs @@ -123,15 +123,19 @@ impl Cone { pixels: &[Argb; CONE_LED_COUNT], ) -> eyre::Result<()> { self.led_strip - .send(pixels) + .tx() + .try_send(*pixels) .wrap_err("Failed to send LED strip values") } + /// Fill the LCD screen with a color. + /// `color` is the color to fill the screen with. pub fn queue_lcd_fill(&mut self, color: Argb) -> eyre::Result<()> { let color = Rgb565::new(color.1, color.2, color.3); tracing::debug!("LCD fill color: {:?}", color); self.lcd - .send(LcdCommand::Fill(color)) + .tx() + .try_send(LcdCommand::Fill(color)) .wrap_err("Failed to send") } @@ -150,7 +154,8 @@ impl Cone { qr_code.write_to(&mut buffer, ImageFormat::Bmp)?; tracing::debug!("LCD QR: {:?}", qr_str); self.lcd - .send(LcdCommand::ImageBmp(buffer.into_inner(), Rgb565::WHITE)) + .tx() + .try_send(LcdCommand::ImageBmp(buffer.into_inner(), Rgb565::WHITE)) .wrap_err("Failed to send") } @@ -171,7 +176,8 @@ impl Cone { tracing::debug!("LCD image: {:?}", absolute_path); let bmp_data = fs::read(absolute_path)?; self.lcd - .send(LcdCommand::ImageBmp(bmp_data, Rgb565::BLACK)) + .tx() + .try_send(LcdCommand::ImageBmp(bmp_data, Rgb565::BLACK)) .wrap_err("Failed to send") } else { Err(eyre::eyre!(