Skip to content

Commit

Permalink
orb-ui: simulation: select on future
Browse files Browse the repository at this point in the history
instead of spawning a new thread for each task, await concurrently.
  • Loading branch information
fouge committed Sep 3, 2024
1 parent db436eb commit 7d90c4f
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 159 deletions.
292 changes: 146 additions & 146 deletions orb-ui/cone/examples/cone-simulation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8> 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::<u8>() % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS,
rand::random::<u8>() % CONE_LED_STRIP_MAXIMUM_BRIGHTNESS,
rand::random::<u8>() % 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<ConeEvent>,
) -> 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();
Expand All @@ -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<eyre::Result<()>> =
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<eyre::Result<()>> = 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::<u8>()
% CONE_LED_STRIP_MAXIMUM_BRIGHTNESS,
rand::random::<u8>()
% CONE_LED_STRIP_MAXIMUM_BRIGHTNESS,
rand::random::<u8>()
% 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
}
16 changes: 11 additions & 5 deletions orb-ui/cone/src/lcd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ type LcdDisplayDriver<'a> = Gc9a01<

pub struct LcdJoinHandle(pub JoinHandle<eyre::Result<()>>);

/// 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.
Expand All @@ -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<LcdCommand>,
cmd_tx: mpsc::Sender<LcdCommand>,
}

/// Commands to the LCD
Expand All @@ -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 =
Expand All @@ -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<LcdCommand> {
&self.cmd_tx
}
}

/// Entry point for the lcd update task
fn do_lcd_update(
cmd_rx: &mut mpsc::UnboundedReceiver<LcdCommand>,
cmd_rx: &mut mpsc::Receiver<LcdCommand>,
mut kill_rx: oneshot::Receiver<()>,
) -> eyre::Result<()> {
let mut delay = Delay::new();
Expand Down
Loading

0 comments on commit 7d90c4f

Please sign in to comment.