Skip to content

Commit

Permalink
orb-ui: cone library to control button, leds and display (#171)
Browse files Browse the repository at this point in the history
* cone library
* simulation: await concurrently (select! on futures)
* led tests with mocked spi interface
  • Loading branch information
fouge authored Sep 12, 2024
1 parent 655ef89 commit ae5b17d
Show file tree
Hide file tree
Showing 11 changed files with 1,461 additions and 10 deletions.
483 changes: 475 additions & 8 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ members = [
"orb-supervisor",
"orb-thermal-cam-ctrl",
"orb-ui",
"orb-ui/cone",
"orb-ui/pid",
"orb-ui/sound",
"orb-ui/uart",
Expand Down Expand Up @@ -65,6 +66,7 @@ tracing = "0.1"
tracing-journald = "0.3.0"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
zbus = { version = "4", default-features = false, features = ["tokio"] }
ftdi-embedded-hal = { version = "0.22.0", features = ["libftd2xx", "libftd2xx-static"] }

can-rs.path = "can"
orb-build-info.path = "build-info"
Expand Down
2 changes: 1 addition & 1 deletion hil/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ camino = "1.1.6"
clap = { workspace = true, features = ["derive"] }
cmd_lib = "1.9.3"
color-eyre.workspace = true
ftdi-embedded-hal = { version = "0.22.0", features = ["libftd2xx-static"] }
ftdi-embedded-hal.workspace = true
futures.workspace = true
indicatif = { version = "0.17.8", features = ["tokio"] }
libftd2xx = { version = "0.32.4", features = ["static"] }
Expand Down
26 changes: 26 additions & 0 deletions orb-ui/cone/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "orb-cone"
version = "0.0.0"
authors = ["Cyril Fougeray <cyril.fougeray@toolsforhumanity.com>"]
publish = false

edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
color-eyre.workspace = true
tracing.workspace = true
tokio.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
image = { version = "0.25.1", features = [] }
rand = "0.8.5"
gc9a01-rs = "0.2.1"
tinybmp = "0.5.0"
embedded-graphics = "0.8.1"
orb-rgb = { path = "../rgb" }
qrcode = "0.14.1"
ftdi-embedded-hal.workspace = true
thiserror.workspace = true
futures.workspace = true
15 changes: 15 additions & 0 deletions orb-ui/cone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Cone library

Cross-platform library to control the cone components over USB:

- LCD screen
- LED strip
- Button

## Running the example

With an FTDI module (`FT4232H`) connected to the host over USB:

```bash
cargo-zigbuild run --example cone-simulation --release
```
237 changes: 237 additions & 0 deletions orb-ui/cone/examples/cone-simulation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
/// This is an example that shows how to initialize and
/// control devices connected to the cone through FTDI chips
use color_eyre::eyre;
use color_eyre::eyre::{eyre, Context};
use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError;
use tracing::level_filters::LevelFilter;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{fmt, EnvFilter};

use orb_cone::lcd::LcdCommand;
use orb_cone::led::CONE_LED_COUNT;
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];
let state_res = match counter {
SimulationState::Idle => {
for pixel in pixels.iter_mut() {
*pixel = Argb::DIAMOND_USER_IDLE;
}
cone.lcd
.tx()
.try_send(LcdCommand::try_from(Argb::DIAMOND_USER_IDLE)?)
.wrap_err("unable to send DIAMOND_USER_IDLE to lcd")
}
SimulationState::Red => {
for pixel in pixels.iter_mut() {
*pixel = Argb::FULL_RED;
pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT);
}
cone.lcd
.tx()
.try_send(
LcdCommand::try_from(Argb::FULL_RED)
.wrap_err("unable to convert Argb")?,
)
.wrap_err("unable to send FULL_RED to lcd")
}
SimulationState::Green => {
for pixel in pixels.iter_mut() {
*pixel = Argb::FULL_GREEN;
pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT);
}
cone.lcd
.tx()
.try_send(
LcdCommand::try_from(Argb::FULL_GREEN)
.wrap_err("unable to convert Argb")?,
)
.wrap_err("unable to send FULL_GREEN to lcd")
}
SimulationState::Blue => {
for pixel in pixels.iter_mut() {
*pixel = Argb::FULL_BLUE;
pixel.0 = Some(CONE_LED_STRIP_DIMMING_DEFAULT);
}
cone.lcd
.tx()
.try_send(
LcdCommand::try_from(Argb::FULL_BLUE)
.wrap_err("unable to convert Argb")?,
)
.wrap_err("unable to send FULL_BLUE to lcd")
}
SimulationState::Logo => {
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,
);
}
// show logo if file exists
let filename = "logo.bmp";
let path = std::path::Path::new(filename);

match LcdCommand::try_from(path) {
Ok(cmd) => cone
.lcd
.tx()
.try_send(cmd)
.wrap_err("unable to send image to lcd"),
Err(e) => {
tracing::debug!("🚨 File \"{filename}\" cannot be used: {e}");
cone.lcd
.tx()
.try_send(
LcdCommand::try_from(Argb::FULL_BLACK)
.wrap_err("unable to convert Argb")?,
)
.wrap_err("unable to send FULL_BLACK to lcd")
}
}
}
SimulationState::QrCode => {
for pixel in pixels.iter_mut() {
*pixel = Argb::DIAMOND_USER_AMBER;
}

let cmd =
LcdCommand::try_from(String::from("https://www.worldcoin.org/"))
.wrap_err("unable to create qr code image")?;
cone.lcd
.tx()
.try_send(cmd)
.wrap_err("unable to send to lcd")
}
_ => Err(eyre!("Unhandled")),
};

// because the goal is to test/simulate
// some use cases, let's just print any error
// that might have occurred and continue
if let Err(e) = state_res {
tracing::error!("{e}");
}

cone.led_strip.tx().try_send(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();
registry
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.from_env_lossy(),
)
.init();

let devices = ftdi_embedded_hal::libftd2xx::list_devices()?;
for device in devices.iter() {
tracing::debug!("Device: {:?}", device);
}

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.");

// 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")
}
};

// wait for all tasks to stop
drop(cone);
cone_handles.join().await?;

res
}
Loading

0 comments on commit ae5b17d

Please sign in to comment.