From 117e040227133d0430aeef855fa32464bbd968f0 Mon Sep 17 00:00:00 2001 From: Cyril Fougeray Date: Wed, 31 Jul 2024 11:01:54 +0200 Subject: [PATCH 1/3] orb-rgb: new crate to manage LED colors and transformations --- Cargo.lock | 8 +++ orb-ui/Cargo.toml | 1 + orb-ui/rgb/Cargo.toml | 13 ++++ orb-ui/{src/engine/rgb.rs => rgb/src/lib.rs} | 59 +++++++++---------- orb-ui/src/engine/animations/alert.rs | 2 +- orb-ui/src/engine/animations/arc_dash.rs | 2 +- orb-ui/src/engine/animations/arc_pulse.rs | 2 +- orb-ui/src/engine/animations/fake_progress.rs | 2 +- orb-ui/src/engine/animations/idle.rs | 2 +- orb-ui/src/engine/animations/mod.rs | 2 +- orb-ui/src/engine/animations/progress.rs | 2 +- orb-ui/src/engine/animations/segmented.rs | 2 +- orb-ui/src/engine/animations/slider.rs | 2 +- orb-ui/src/engine/animations/spinner.rs | 2 +- orb-ui/src/engine/animations/static.rs | 2 +- orb-ui/src/engine/animations/wave.rs | 2 +- orb-ui/src/engine/diamond.rs | 16 ++--- orb-ui/src/engine/mod.rs | 4 +- orb-ui/src/engine/operator/bar.rs | 2 +- orb-ui/src/engine/operator/battery.rs | 2 +- orb-ui/src/engine/operator/blink.rs | 2 +- orb-ui/src/engine/operator/idle.rs | 2 +- orb-ui/src/engine/operator/pulse.rs | 2 +- orb-ui/src/engine/operator/signup_phase.rs | 2 +- orb-ui/src/engine/pearl.rs | 2 +- 25 files changed, 79 insertions(+), 60 deletions(-) create mode 100644 orb-ui/rgb/Cargo.toml rename orb-ui/{src/engine/rgb.rs => rgb/src/lib.rs} (57%) diff --git a/Cargo.lock b/Cargo.lock index d52f455..b97b680 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3035,6 +3035,13 @@ dependencies = [ "uuid", ] +[[package]] +name = "orb-rgb" +version = "0.0.0" +dependencies = [ + "serde", +] + [[package]] name = "orb-security-utils" version = "0.0.4" @@ -3107,6 +3114,7 @@ dependencies = [ "futures", "orb-build-info", "orb-messages", + "orb-rgb", "orb-sound", "orb-uart", "pid", diff --git a/orb-ui/Cargo.toml b/orb-ui/Cargo.toml index 64ad2ee..2d08b0a 100644 --- a/orb-ui/Cargo.toml +++ b/orb-ui/Cargo.toml @@ -21,6 +21,7 @@ orb-build-info.path = "../build-info" orb-messages.workspace = true orb-sound.path = "sound" orb-uart.path = "uart" +orb-rgb.path = "rgb" pid.path = "pid" prost = "0.12.3" serde.workspace = true diff --git a/orb-ui/rgb/Cargo.toml b/orb-ui/rgb/Cargo.toml new file mode 100644 index 0000000..f6ca761 --- /dev/null +++ b/orb-ui/rgb/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "orb-rgb" +version = "0.0.0" +authors = ["Cyril Fougeray "] +publish = false + +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +serde.workspace = true diff --git a/orb-ui/src/engine/rgb.rs b/orb-ui/rgb/src/lib.rs similarity index 57% rename from orb-ui/src/engine/rgb.rs rename to orb-ui/rgb/src/lib.rs index 5664011..f05f8bf 100644 --- a/orb-ui/src/engine/rgb.rs +++ b/orb-ui/rgb/src/lib.rs @@ -53,49 +53,46 @@ impl ops::MulAssign for Argb { #[allow(missing_docs)] impl Argb { - pub(crate) const DIMMING_MAX_VALUE: u8 = 31; - pub(crate) const OFF: Argb = Argb(Some(0), 0, 0, 0); - pub(crate) const OPERATOR_DEV: Argb = - { Argb(Some(Self::DIMMING_MAX_VALUE), 0, 20, 0) }; + pub const DIMMING_MAX_VALUE: u8 = 31; + pub const OFF: Argb = Argb(Some(0), 0, 0, 0); + pub const OPERATOR_DEV: Argb = { Argb(Some(Self::DIMMING_MAX_VALUE), 0, 20, 0) }; - pub(crate) const PEARL_OPERATOR_AMBER: Argb = Argb(None, 20, 16, 0); - pub(crate) const PEARL_OPERATOR_DEFAULT: Argb = { Argb(None, 20, 20, 20) }; - pub(crate) const PEARL_OPERATOR_VERSIONS_DEPRECATED: Argb = Argb(None, 128, 128, 0); - pub(crate) const PEARL_OPERATOR_VERSIONS_OUTDATED: Argb = Argb(None, 255, 0, 0); - pub(crate) const PEARL_USER_AMBER: Argb = Argb(None, 23, 13, 0); - pub(crate) const PEARL_USER_QR_SCAN: Argb = Argb(None, 24, 24, 24); - pub(crate) const PEARL_USER_RED: Argb = Argb(None, 30, 2, 0); - pub(crate) const PEARL_USER_SIGNUP: Argb = Argb(None, 31, 31, 31); - pub(crate) const PEARL_USER_FLASH: Argb = Argb(None, 255, 255, 255); + pub const PEARL_OPERATOR_AMBER: Argb = Argb(None, 20, 16, 0); + pub const PEARL_OPERATOR_DEFAULT: Argb = { Argb(None, 20, 20, 20) }; + pub const PEARL_OPERATOR_VERSIONS_DEPRECATED: Argb = Argb(None, 128, 128, 0); + pub const PEARL_OPERATOR_VERSIONS_OUTDATED: Argb = Argb(None, 255, 0, 0); + pub const PEARL_USER_AMBER: Argb = Argb(None, 23, 13, 0); + pub const PEARL_USER_QR_SCAN: Argb = Argb(None, 24, 24, 24); + pub const PEARL_USER_RED: Argb = Argb(None, 30, 2, 0); + pub const PEARL_USER_SIGNUP: Argb = Argb(None, 31, 31, 31); + pub const PEARL_USER_FLASH: Argb = Argb(None, 255, 255, 255); - pub(crate) const DIAMOND_OPERATOR_AMBER: Argb = + pub const DIAMOND_OPERATOR_AMBER: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 20, 16, 0); // To help quickly distinguish dev vs prod software, // the default operator LED color is white for prod, yellow for dev - pub(crate) const DIAMOND_OPERATOR_DEFAULT: Argb = + pub const DIAMOND_OPERATOR_DEFAULT: Argb = { Argb(Some(Self::DIMMING_MAX_VALUE), 20, 25, 20) }; - pub(crate) const DIAMOND_OPERATOR_VERSIONS_DEPRECATED: Argb = + pub const DIAMOND_OPERATOR_VERSIONS_DEPRECATED: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 128, 128, 0); - pub(crate) const DIAMOND_OPERATOR_VERSIONS_OUTDATED: Argb = + pub const DIAMOND_OPERATOR_VERSIONS_OUTDATED: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 255, 0, 0); - pub(crate) const DIAMOND_USER_AMBER: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 23, 13, 0); - pub(crate) const DIAMOND_USER_SHROUD: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 20, 6, 1); + pub const DIAMOND_USER_AMBER: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 20, 16, 1); #[allow(dead_code)] - pub(crate) const DIAMOND_USER_IDLE: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 18, 23, 18); - pub(crate) const DIAMOND_USER_QR_SCAN: Argb = + pub const DIAMOND_USER_IDLE: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 18, 23, 18); + pub const DIAMOND_USER_QR_SCAN: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 24, 29, 24); - pub(crate) const DIAMOND_USER_RED: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 30, 2, 0); - pub(crate) const DIAMOND_USER_SIGNUP: Argb = + pub const DIAMOND_USER_SIGNUP: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 32, 26, 1); - pub(crate) const DIAMOND_USER_FLASH: Argb = + pub const DIAMOND_USER_FLASH: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 255, 255, 255); - #[allow(dead_code)] - pub(crate) const DIAMOND_CONE_AMBER: Argb = - Argb(Some(Self::DIMMING_MAX_VALUE), 25, 8, 1); + pub const DIAMOND_CONE_AMBER: Argb = Argb(Some(Self::DIMMING_MAX_VALUE), 25, 18, 1); + + pub const FULL_RED: Argb = Argb(None, 255, 0, 0); + pub const FULL_GREEN: Argb = Argb(None, 0, 255, 0); + pub const FULL_BLUE: Argb = Argb(None, 0, 0, 255); + pub const FULL_WHITE: Argb = Argb(None, 255, 255, 255); + pub const FULL_BLACK: Argb = Argb(None, 0, 0, 0); pub fn is_off(&self) -> bool { self.0 == Some(0) || (self.1 == 0 && self.2 == 0 && self.3 == 0) diff --git a/orb-ui/src/engine/animations/alert.rs b/orb-ui/src/engine/animations/alert.rs index 99849c5..19b4cbd 100644 --- a/orb-ui/src/engine/animations/alert.rs +++ b/orb-ui/src/engine/animations/alert.rs @@ -1,5 +1,5 @@ -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState}; +use orb_rgb::Argb; use std::any::Any; use std::f64::consts::PI; diff --git a/orb-ui/src/engine/animations/arc_dash.rs b/orb-ui/src/engine/animations/arc_dash.rs index f2bdfd4..0841bf8 100644 --- a/orb-ui/src/engine/animations/arc_dash.rs +++ b/orb-ui/src/engine/animations/arc_dash.rs @@ -1,6 +1,6 @@ use crate::engine::animations::render_lines; -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState, RingFrame, PEARL_RING_LED_COUNT}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI, ops::Range}; /// Maximum number of arcs. diff --git a/orb-ui/src/engine/animations/arc_pulse.rs b/orb-ui/src/engine/animations/arc_pulse.rs index 7e89c51..a642ef0 100644 --- a/orb-ui/src/engine/animations/arc_pulse.rs +++ b/orb-ui/src/engine/animations/arc_pulse.rs @@ -1,6 +1,6 @@ use crate::engine::animations::render_lines; -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState, RingFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; const SPEED: f64 = PI * 2.0 / 3.0; // 3 seconds per wave diff --git a/orb-ui/src/engine/animations/fake_progress.rs b/orb-ui/src/engine/animations/fake_progress.rs index e4bb2a9..42768ba 100644 --- a/orb-ui/src/engine/animations/fake_progress.rs +++ b/orb-ui/src/engine/animations/fake_progress.rs @@ -1,6 +1,6 @@ use crate::engine::animations::render_lines; -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState, RingFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; /// Progress growing from the center of the left and the right halves. diff --git a/orb-ui/src/engine/animations/idle.rs b/orb-ui/src/engine/animations/idle.rs index 94a66f4..380843e 100644 --- a/orb-ui/src/engine/animations/idle.rs +++ b/orb-ui/src/engine/animations/idle.rs @@ -1,6 +1,6 @@ -use crate::engine::rgb::Argb; use crate::engine::Animation; use crate::engine::{AnimationState, RingFrame}; +use orb_rgb::Argb; use std::any::Any; /// Idle / not animated ring = all LEDs in one color diff --git a/orb-ui/src/engine/animations/mod.rs b/orb-ui/src/engine/animations/mod.rs index 1484d44..1e6a75e 100644 --- a/orb-ui/src/engine/animations/mod.rs +++ b/orb-ui/src/engine/animations/mod.rs @@ -17,8 +17,8 @@ pub use self::r#static::Static; pub use self::slider::Slider; pub use self::spinner::Spinner; pub use self::wave::Wave; -use crate::engine::rgb::Argb; use crate::engine::{RingFrame, DIAMOND_RING_LED_COUNT, GAMMA}; +use orb_rgb::Argb; use std::{f64::consts::PI, ops::Range}; const LIGHT_BLEEDING_OFFSET_RAD: f64 = PI / 180.0 * 6.0; // 6° offset of the start to compensate for light bleeding. diff --git a/orb-ui/src/engine/animations/progress.rs b/orb-ui/src/engine/animations/progress.rs index 511bfbd..3d8de5a 100644 --- a/orb-ui/src/engine/animations/progress.rs +++ b/orb-ui/src/engine/animations/progress.rs @@ -1,6 +1,6 @@ use crate::engine::animations::{render_lines, LIGHT_BLEEDING_OFFSET_RAD}; -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState, RingFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; const RC: f64 = 0.5; diff --git a/orb-ui/src/engine/animations/segmented.rs b/orb-ui/src/engine/animations/segmented.rs index d7ed082..305eef5 100644 --- a/orb-ui/src/engine/animations/segmented.rs +++ b/orb-ui/src/engine/animations/segmented.rs @@ -1,6 +1,6 @@ -use crate::engine::rgb::Argb; use crate::engine::Animation; use crate::engine::{AnimationState, RingFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; const PULSE_SPEED: f64 = PI * 2.0 / 3.0; // 3 seconds per pulse diff --git a/orb-ui/src/engine/animations/slider.rs b/orb-ui/src/engine/animations/slider.rs index 6908eb5..300592b 100644 --- a/orb-ui/src/engine/animations/slider.rs +++ b/orb-ui/src/engine/animations/slider.rs @@ -1,5 +1,5 @@ -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState, RingFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; use crate::engine::animations::arc_pulse::ArcPulse; diff --git a/orb-ui/src/engine/animations/spinner.rs b/orb-ui/src/engine/animations/spinner.rs index 1fffd5e..0606b5a 100644 --- a/orb-ui/src/engine/animations/spinner.rs +++ b/orb-ui/src/engine/animations/spinner.rs @@ -1,6 +1,6 @@ use crate::engine::animations::{render_lines, Progress}; -use crate::engine::rgb::Argb; use crate::engine::{Animation, AnimationState, RingFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI, ops::Range}; /// Maximum number of arcs. diff --git a/orb-ui/src/engine/animations/static.rs b/orb-ui/src/engine/animations/static.rs index 4f88783..2fb9fbb 100644 --- a/orb-ui/src/engine/animations/static.rs +++ b/orb-ui/src/engine/animations/static.rs @@ -1,6 +1,6 @@ -use crate::engine::rgb::Argb; use crate::engine::Animation; use crate::engine::AnimationState; +use orb_rgb::Argb; use std::any::Any; /// Static color. diff --git a/orb-ui/src/engine/animations/wave.rs b/orb-ui/src/engine/animations/wave.rs index 7f2f2d9..29d3958 100644 --- a/orb-ui/src/engine/animations/wave.rs +++ b/orb-ui/src/engine/animations/wave.rs @@ -1,6 +1,6 @@ -use crate::engine::rgb::Argb; use crate::engine::Animation; use crate::engine::{AnimationState, PEARL_CENTER_LED_COUNT}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; /// Pulsing wave animation. diff --git a/orb-ui/src/engine/diamond.rs b/orb-ui/src/engine/diamond.rs index 84949e4..2bf92e4 100644 --- a/orb-ui/src/engine/diamond.rs +++ b/orb-ui/src/engine/diamond.rs @@ -5,6 +5,7 @@ use futures::future::Either; use futures::{future, StreamExt}; use orb_messages::mcu_main::mcu_message::Message; use orb_messages::mcu_main::{jetson_to_mcu, JetsonToMcu}; +use orb_rgb::Argb; use pid::{InstantTimer, Timer}; use std::f64::consts::PI; use std::time::Duration; @@ -13,7 +14,6 @@ use tokio::time; use tokio_stream::wrappers::{IntervalStream, UnboundedReceiverStream}; use crate::engine::animations::alert::BlinkDurations; -use crate::engine::rgb::Argb; use crate::engine::{ animations, operator, Animation, AnimationsStack, CenterFrame, ConeFrame, Event, EventHandler, OperatorFrame, OrbType, QrScanSchema, QrScanUnexpectedReason, @@ -302,8 +302,8 @@ impl EventHandler for Runner { self.set_cone( LEVEL_NOTICE, animations::Alert::::new( - Argb::DIAMOND_USER_QR_SCAN, - BlinkDurations::from(vec![0.0, 0.3, 0.3]), + Argb::DIAMOND_USER_AMBER, + BlinkDurations::from(vec![0.0, 0.5, 1.0]), None, false, ), @@ -332,7 +332,7 @@ impl EventHandler for Runner { self.set_center( LEVEL_FOREGROUND, animations::Static::::new( - Argb::DIAMOND_USER_SHROUD, + Argb::DIAMOND_USER_AMBER, None, ), ); @@ -368,7 +368,7 @@ impl EventHandler for Runner { self.set_center( LEVEL_FOREGROUND, animations::Alert::::new( - Argb::DIAMOND_USER_SHROUD, + Argb::DIAMOND_USER_AMBER, BlinkDurations::from(vec![0.0, 0.5, 0.5]), None, false, @@ -434,7 +434,7 @@ impl EventHandler for Runner { self.set_center( LEVEL_NOTICE, animations::Alert::::new( - Argb::DIAMOND_USER_SHROUD, + Argb::DIAMOND_USER_AMBER, BlinkDurations::from(vec![0.0, 0.5, 0.5]), None, false, @@ -555,7 +555,7 @@ impl EventHandler for Runner { self.set_center( LEVEL_FOREGROUND, animations::Wave::::new( - Argb::DIAMOND_USER_SHROUD, + Argb::DIAMOND_USER_AMBER, 4.0, 0.0, false, @@ -860,7 +860,7 @@ impl EventHandler for Runner { self.set_ring( LEVEL_NOTICE, animations::Spinner::::triple( - Argb::DIAMOND_USER_RED, + Argb::DIAMOND_USER_AMBER, ), ); } diff --git a/orb-ui/src/engine/mod.rs b/orb-ui/src/engine/mod.rs index 066807f..37a1a9d 100644 --- a/orb-ui/src/engine/mod.rs +++ b/orb-ui/src/engine/mod.rs @@ -1,11 +1,12 @@ //! LED engine. use crate::sound; -use crate::{engine::rgb::Argb, tokio_spawn}; +use crate::tokio_spawn; use async_trait::async_trait; use eyre::Result; use futures::channel::mpsc::Sender; use orb_messages::mcu_main::mcu_message::Message; +use orb_rgb::Argb; use pid::InstantTimer; use serde::{Deserialize, Serialize}; use std::{any::Any, collections::BTreeMap}; @@ -15,7 +16,6 @@ pub mod animations; mod diamond; pub mod operator; mod pearl; -mod rgb; pub const PEARL_RING_LED_COUNT: usize = 224; pub const PEARL_CENTER_LED_COUNT: usize = 9; diff --git a/orb-ui/src/engine/operator/bar.rs b/orb-ui/src/engine/operator/bar.rs index f8cfa72..3082ec5 100644 --- a/orb-ui/src/engine/operator/bar.rs +++ b/orb-ui/src/engine/operator/bar.rs @@ -1,7 +1,7 @@ use super::Animation; use crate::engine; -use crate::engine::rgb::Argb; use crate::engine::{AnimationState, OperatorFrame}; +use orb_rgb::Argb; use std::any::Any; /// Simple progress bar that goes from 0 to 100% or from 100 to 0%, in case `inverted`, diff --git a/orb-ui/src/engine/operator/battery.rs b/orb-ui/src/engine/operator/battery.rs index e42b5db..ef7ab08 100644 --- a/orb-ui/src/engine/operator/battery.rs +++ b/orb-ui/src/engine/operator/battery.rs @@ -1,6 +1,6 @@ use super::{compute_smooth_blink_color_multiplier, Animation}; use crate::engine; -use crate::engine::rgb::Argb; +use orb_rgb::Argb; use crate::engine::{AnimationState, OperatorFrame, OrbType}; use std::any::Any; diff --git a/orb-ui/src/engine/operator/blink.rs b/orb-ui/src/engine/operator/blink.rs index f1860d6..bcf1962 100644 --- a/orb-ui/src/engine/operator/blink.rs +++ b/orb-ui/src/engine/operator/blink.rs @@ -1,7 +1,7 @@ use super::Animation; use crate::engine; -use crate::engine::rgb::Argb; use crate::engine::{AnimationState, OperatorFrame}; +use orb_rgb::Argb; use std::any::Any; /// Blink with all LEDs. diff --git a/orb-ui/src/engine/operator/idle.rs b/orb-ui/src/engine/operator/idle.rs index 04ee617..933cf7f 100644 --- a/orb-ui/src/engine/operator/idle.rs +++ b/orb-ui/src/engine/operator/idle.rs @@ -1,6 +1,6 @@ use super::{compute_smooth_blink_color_multiplier, Animation}; -use crate::engine::rgb::Argb; use crate::engine::{AnimationState, OperatorFrame, OrbType}; +use orb_rgb::Argb; use std::any::Any; /// Controls operator LEDs states when Orb is idle. diff --git a/orb-ui/src/engine/operator/pulse.rs b/orb-ui/src/engine/operator/pulse.rs index 59a26b3..ef55e28 100644 --- a/orb-ui/src/engine/operator/pulse.rs +++ b/orb-ui/src/engine/operator/pulse.rs @@ -1,7 +1,7 @@ use super::Animation; use crate::engine; -use crate::engine::rgb::Argb; use crate::engine::{AnimationState, OperatorFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; /// Pulse with all LEDs. diff --git a/orb-ui/src/engine/operator/signup_phase.rs b/orb-ui/src/engine/operator/signup_phase.rs index 9d0b10e..8d4f33e 100644 --- a/orb-ui/src/engine/operator/signup_phase.rs +++ b/orb-ui/src/engine/operator/signup_phase.rs @@ -1,6 +1,6 @@ use crate::engine; -use crate::engine::rgb::Argb; use crate::engine::{AnimationState, OperatorFrame}; +use orb_rgb::Argb; use std::{any::Any, f64::consts::PI}; use super::Animation; diff --git a/orb-ui/src/engine/pearl.rs b/orb-ui/src/engine/pearl.rs index f7d0df4..46e94e8 100644 --- a/orb-ui/src/engine/pearl.rs +++ b/orb-ui/src/engine/pearl.rs @@ -16,7 +16,6 @@ use tokio_stream::wrappers::{IntervalStream, UnboundedReceiverStream}; use pid::{InstantTimer, Timer}; use crate::engine::animations::alert::BlinkDurations; -use crate::engine::rgb::Argb; use crate::engine::{ animations, operator, Animation, AnimationsStack, CenterFrame, Event, EventHandler, OperatorFrame, OrbType, QrScanSchema, QrScanUnexpectedReason, RingFrame, Runner, @@ -26,6 +25,7 @@ use crate::engine::{ }; use crate::sound; use crate::sound::Player; +use orb_rgb::Argb; struct WrappedMessage(Message); From 366d05e29cef5bf1c9f1f3faafedd398a8796a15 Mon Sep 17 00:00:00 2001 From: Cyril Fougeray Date: Wed, 31 Jul 2024 12:02:32 +0200 Subject: [PATCH 2/3] orb-ui: cone library --- Cargo.lock | 493 +++++++++++++++++++++++- Cargo.toml | 2 + hil/Cargo.toml | 2 +- orb-ui/cone/Cargo.toml | 26 ++ orb-ui/cone/README.md | 15 + orb-ui/cone/examples/cone-simulation.rs | 127 ++++++ orb-ui/cone/src/button.rs | 86 +++++ orb-ui/cone/src/lcd.rs | 186 +++++++++ orb-ui/cone/src/led.rs | 140 +++++++ orb-ui/cone/src/lib.rs | 159 ++++++++ 10 files changed, 1224 insertions(+), 12 deletions(-) create mode 100644 orb-ui/cone/Cargo.toml create mode 100644 orb-ui/cone/README.md create mode 100644 orb-ui/cone/examples/cone-simulation.rs create mode 100644 orb-ui/cone/src/button.rs create mode 100644 orb-ui/cone/src/lcd.rs create mode 100644 orb-ui/cone/src/led.rs create mode 100644 orb-ui/cone/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b97b680..dc323cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "alkali" version = "0.3.0" @@ -170,12 +176,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + [[package]] name = "arc-swap" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -486,6 +509,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876c75a42f6364451a033496a14c44bffe41f5f4a8236f697391f11024e596d2" +dependencies = [ + "arrayvec", +] + [[package]] name = "axum" version = "0.6.20" @@ -573,7 +619,7 @@ dependencies = [ "bitflags 2.4.2", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -605,6 +651,12 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bitstream-io" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c9989a51171e2e81038ab168b6ae22886fe9ded214430dbb4f41c28cf176da" + [[package]] name = "blake3" version = "1.5.0" @@ -643,12 +695,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "built" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41bfbdb21256b87a8b5e80fab81a8eed158178e812fd7ba451907518b2742f16" + [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + [[package]] name = "bytemuck" version = "1.14.0" @@ -717,7 +781,7 @@ checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" name = "can-rs" version = "0.0.0" dependencies = [ - "itertools", + "itertools 0.10.5", "libc", "paste", "thiserror", @@ -748,6 +812,16 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -814,7 +888,7 @@ version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.48", @@ -1260,6 +1334,24 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "display-interface" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba2aab1ef3793e6f7804162debb5ac5edb93b3d650fbcc5aeb72fcd0e6c03a0" + +[[package]] +name = "display-interface-spi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9ec30048b1955da2038fcc3c017f419ab21bb0001879d16c0a3749dc6b7a" +dependencies = [ + "byte-slice-cast", + "display-interface", + "embedded-hal 1.0.0", + "embedded-hal-async", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1272,6 +1364,29 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "embedded-graphics" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0649998afacf6d575d126d83e68b78c0ab0e00ca2ac7e9b3db11b4cbe8274ef0" +dependencies = [ + "az", + "byteorder", + "embedded-graphics-core", + "float-cmp", + "micromath", +] + +[[package]] +name = "embedded-graphics-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba9ecd261f991856250d2207f6d8376946cd9f412a2165d3b75bc87a0bc7a044" +dependencies = [ + "az", + "byteorder", +] + [[package]] name = "embedded-hal" version = "0.2.7" @@ -1288,6 +1403,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "361a90feb7004eca4019fb28352a9465666b24f840f5c3cddf0ff13920590b89" +[[package]] +name = "embedded-hal-async" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4c685bbef7fe13c3c6dd4da26841ed3980ef33e841cddfa15ce8a8fb3f1884" +dependencies = [ + "embedded-hal 1.0.0", +] + [[package]] name = "embedded-hal-nb" version = "1.0.0" @@ -1529,6 +1653,15 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "flume" version = "0.11.0" @@ -1714,6 +1847,18 @@ dependencies = [ "slab", ] +[[package]] +name = "gc9a01-rs" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b806317c4447766a784ef9ea927ab519c783f2e502dd0d0a352283c4c8a87d9" +dependencies = [ + "display-interface", + "display-interface-spi", + "embedded-graphics-core", + "embedded-hal 1.0.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1758,6 +1903,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1858,6 +2013,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.4" @@ -2059,7 +2220,7 @@ dependencies = [ "byteorder", "color_quant", "exr", - "gif", + "gif 0.12.0", "jpeg-decoder", "num-traits", "png", @@ -2067,6 +2228,45 @@ dependencies = [ "tiff", ] +[[package]] +name = "image" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif 0.13.1", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a84a25dcae3ac487bc24ef280f9e20c79c9b1a3e5e32cbed3041d1c514aa87c" +dependencies = [ + "byteorder", + "thiserror", +] + +[[package]] +name = "imgref" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" + [[package]] name = "indenter" version = "0.3.3" @@ -2124,6 +2324,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "io-kit-sys" version = "0.4.0" @@ -2181,6 +2392,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.10" @@ -2353,6 +2573,17 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libloading" version = "0.8.1" @@ -2411,6 +2642,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "mach2" version = "0.4.2" @@ -2435,6 +2675,16 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "memchr" version = "2.7.1" @@ -2468,6 +2718,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "micromath" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c8dda44ff03a2f238717214da50f65d5a53b45cd213a7370424ffdb6fae815" + [[package]] name = "miette" version = "5.10.0" @@ -2604,6 +2860,12 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.24.3" @@ -2651,6 +2913,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2683,6 +2951,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2693,6 +2972,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -2773,7 +3064,7 @@ dependencies = [ "jni 0.20.0", "ndk", "ndk-context", - "num-derive", + "num-derive 0.3.3", "num-traits", "oboe-sys", ] @@ -2947,6 +3238,24 @@ dependencies = [ "color-eyre", ] +[[package]] +name = "orb-cone" +version = "0.0.0" +dependencies = [ + "color-eyre", + "embedded-graphics", + "ftdi-embedded-hal", + "gc9a01-rs", + "image 0.25.1", + "orb-rgb", + "qrcode", + "rand 0.8.5", + "tinybmp", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "orb-const-concat" version = "0.0.0" @@ -3006,7 +3315,7 @@ dependencies = [ "color-eyre", "crc32fast", "futures", - "image", + "image 0.24.8", "orb-build-info", "orb-mcu-interface", "tokio", @@ -3416,6 +3725,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d84d1d7a6ac92673717f9f6d1518374ef257669c24ebc5ac25d5033828be58" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8021cf59c8ec9c432cfc2526ac6b8aa508ecaf29cd415f271b8406c1b851c3fd" +dependencies = [ + "quote", + "syn 2.0.48", +] + [[package]] name = "prost" version = "0.12.3" @@ -3433,8 +3761,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" dependencies = [ "bytes", - "heck", - "itertools", + "heck 0.4.1", + "itertools 0.10.5", "log", "multimap", "once_cell", @@ -3455,7 +3783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" dependencies = [ "anyhow", - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.48", @@ -3479,12 +3807,27 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "qrcode" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec" +dependencies = [ + "image 0.25.1", +] + [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.35" @@ -3565,6 +3908,56 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive 0.4.2", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc13288f5ab39e6d7c9d501759712e6969fcc9734220846fc9ed26cae2cc4234" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error 2.0.1", + "rav1e", + "rayon", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -3704,6 +4097,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" +[[package]] +name = "rgb" +version = "0.8.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05aaa8004b64fd573fc9d002f4e632d51ad4f026c2b5ba95fcb6c2f32c2c47d8" +dependencies = [ + "bytemuck", +] + [[package]] name = "riff" version = "2.0.0" @@ -3853,7 +4255,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", - "quick-error", + "quick-error 1.2.3", "tempfile", "wait-timeout", ] @@ -4191,6 +4593,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simple_asn1" version = "0.6.2" @@ -4235,7 +4646,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "syn 1.0.109", @@ -4410,6 +4821,19 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + [[package]] name = "tar" version = "0.4.40" @@ -4421,6 +4845,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "tempfile" version = "3.9.0" @@ -4513,6 +4943,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinybmp" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197cc000e382175ff15abd9c54c694ef80ef20cb07e7f956c71e3ea97fc8dc60" +dependencies = [ + "embedded-graphics", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -4967,6 +5406,17 @@ dependencies = [ "getrandom 0.2.12", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4995,6 +5445,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -5579,6 +6035,12 @@ dependencies = [ "flate2", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -5588,6 +6050,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "zune-jpeg" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec866b44a2a1fd6133d363f073ca1b179f438f99e7e5bfb1e33f7181facfe448" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.0.2" diff --git a/Cargo.toml b/Cargo.toml index 2acbc21..2dbf284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "orb-slot-ctrl", "orb-thermal-cam-ctrl", "orb-ui", + "orb-ui/cone", "orb-ui/pid", "orb-ui/sound", "orb-ui/uart", @@ -58,6 +59,7 @@ tokio-test = "0.4.4" tracing = "0.1" 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"] } orb-security-utils.path = "security-utils" diff --git a/hil/Cargo.toml b/hil/Cargo.toml index 24c9ed3..0fce404 100644 --- a/hil/Cargo.toml +++ b/hil/Cargo.toml @@ -15,7 +15,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 libftd2xx = { version = "0.32.4", features = ["static"] } nusb = { git = "https://github.com/thebutlah/nusb", rev = "ca8b2edc9a8dad773a609b2d65b26d6b738670ad" } orb-build-info.path = "../build-info" diff --git a/orb-ui/cone/Cargo.toml b/orb-ui/cone/Cargo.toml new file mode 100644 index 0000000..450a28d --- /dev/null +++ b/orb-ui/cone/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "orb-cone" +version = "0.0.0" +authors = ["Cyril Fougeray"] +publish = false + +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[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 diff --git a/orb-ui/cone/README.md b/orb-ui/cone/README.md new file mode 100644 index 0000000..e9b8661 --- /dev/null +++ b/orb-ui/cone/README.md @@ -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 +``` diff --git a/orb-ui/cone/examples/cone-simulation.rs b/orb-ui/cone/examples/cone-simulation.rs new file mode 100644 index 0000000..0f23fe4 --- /dev/null +++ b/orb-ui/cone/examples/cone-simulation.rs @@ -0,0 +1,127 @@ +/// This is an example that shows how to initialize and +/// control devices connected to the cone (FTDI chip) +use color_eyre::eyre; +use tokio::sync::mpsc; +use tokio::task; +use tracing::info; +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::ConeEvents; +use orb_rgb::Argb; + +const CONE_LED_STRIP_DIMMING_DEFAULT: u8 = 10_u8; +const CONE_LED_STRIP_RAINBOW_PERIOD_S: u64 = 2; +const CONE_LED_STRIP_MAXIMUM_BRIGHTNESS: u8 = 20; + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let registry = tracing_subscriber::registry(); + #[cfg(tokio_unstable)] + let registry = registry.with(console_subscriber::spawn()); + 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 (tx, mut rx) = mpsc::unbounded_channel(); + let mut cone = orb_cone::Cone::new(tx)?; + + // spawn a thread to receive events + task::spawn(async move { + let mut button_pressed = false; + loop { + match rx.recv().await { + Some(event) => match event { + ConeEvents::ButtonPressed(state) => { + if state != button_pressed { + info!( + "🔘 Button {}", + if state { "pressed" } else { "released" } + ); + button_pressed = state; + } + } + }, + None => { + tracing::error!("Cone events channel closed"); + break; + } + } + } + }); + + info!("🍦 Cone initialized"); + + let mut counter = 0; + loop { + 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 => { + cone.queue_lcd_bmp(String::from("examples/logo.bmp"))?; + 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)?; + + std::thread::sleep(std::time::Duration::from_secs( + CONE_LED_STRIP_RAINBOW_PERIOD_S, + )); + counter = (counter + 1) % 6; + } +} diff --git a/orb-ui/cone/src/button.rs b/orb-ui/cone/src/button.rs new file mode 100644 index 0000000..65a57eb --- /dev/null +++ b/orb-ui/cone/src/button.rs @@ -0,0 +1,86 @@ +use crate::{ConeEvents, Status}; +use color_eyre::eyre; +use ftdi_embedded_hal::libftd2xx::{BitMode, Ft4232h, Ftdi, FtdiCommon}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use tokio::sync::mpsc; +use tracing::debug; + +const BUTTON_GPIO_PIN: u8 = 0; +const BUTTON_GPIO_MASK: u8 = 1 << BUTTON_GPIO_PIN; + +pub struct Button { + thread_handle: Option>, + terminate: Arc, +} + +/// Poll the button state. +/// Events are sent to the event queue when the button is pressed or released +/// The thread that polls the button is also used to check the connection status +impl Button { + pub(crate) fn spawn( + event_queue: mpsc::UnboundedSender, + connection: Arc>, + ) -> eyre::Result { + let mut device: Ft4232h = Ftdi::with_index(7)?.try_into()?; + let mask: u8 = !BUTTON_GPIO_MASK; // button pin as input, all others as output + device.set_bit_mode(mask, BitMode::AsyncBitbang)?; + debug!("Button GPIO initialized"); + + let terminate = Arc::new(AtomicBool::new(false)); + let terminate_clone = Arc::clone(&terminate); + + // spawn a thread to poll the button + let thread_handle = std::thread::spawn(move || { + // keep state so that we send an event only on state change + let mut last_state = false; + loop { + if terminate_clone.load(Ordering::Relaxed) { + return; + } + match device.bit_mode() { + Ok(mode) => { + // connected + if let Ok(mut status) = connection.lock() { + *status = Status::Connected; + } + // button is active low + let pressed = mode & BUTTON_GPIO_MASK == 0; + if pressed != last_state { + if let Err(e) = + event_queue.send(ConeEvents::ButtonPressed(pressed)) + { + tracing::error!("Error sending event: {:?} - no receiver? stopping producer", e); + return; + } + last_state = pressed; + } + } + Err(e) => { + // disconnected + if let Ok(mut status) = connection.lock() { + *status = Status::Disconnected; + } + tracing::trace!("Error reading button state: {:?}", e); + } + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + }); + + Ok(Button { + thread_handle: Some(thread_handle), + terminate, + }) + } +} + +impl Drop for Button { + fn drop(&mut self) { + self.terminate.store(true, Ordering::Relaxed); + if let Some(handle) = self.thread_handle.take() { + handle.join().unwrap(); + debug!("Button thread joined"); + } + } +} diff --git a/orb-ui/cone/src/lcd.rs b/orb-ui/cone/src/lcd.rs new file mode 100644 index 0000000..4a76c6a --- /dev/null +++ b/orb-ui/cone/src/lcd.rs @@ -0,0 +1,186 @@ +use color_eyre::eyre; +use color_eyre::eyre::Context; +use embedded_graphics::pixelcolor::Rgb565; +use embedded_graphics::primitives::{PrimitiveStyleBuilder, Rectangle}; +use embedded_graphics::{image::Image, prelude::*}; +use ftdi_embedded_hal::eh1::digital::OutputPin; +use ftdi_embedded_hal::libftd2xx::{Ft4232h, Ftdi, FtdiCommon}; +use ftdi_embedded_hal::{Delay, SpiDevice}; +use gc9a01::{mode::BufferedGraphics, prelude::*, Gc9a01, SPIDisplayInterface}; +use tinybmp::Bmp; +use tokio::sync::watch::Receiver; +use tokio::sync::{mpsc, watch}; +use tokio::task; +use tokio::task::JoinHandle; +use tracing::debug; + +type LcdDisplayDriver<'a> = Gc9a01< + SPIInterface<&'a SpiDevice, ftdi_embedded_hal::OutputPin>, + DisplayResolution240x240, + BufferedGraphics, +>; + +/// Lcd handle to send commands to the LCD screen. +/// +/// The LCD is controlled by a separate task. +/// The task is spawned when the Lcd is created +/// and stopped when the Lcd is dropped +pub struct Lcd { + tx: mpsc::UnboundedSender, + shutdown_signal: watch::Sender<()>, + task_handle: Option>>, +} + +/// Commands to the LCD +pub enum LcdCommand { + /// Display a BMP image on the LCD with a background color, image is centered on the screen + ImageBmp(Vec, Rgb565), + /// Fill the LCD with a color + Fill(Rgb565), +} + +impl Lcd { + pub(crate) fn spawn() -> eyre::Result { + let (tx, mut rx) = mpsc::unbounded_channel(); + let (shutdown_signal, shutdown_receiver) = watch::channel(()); + + let task_handle = + task::spawn( + async move { handle_lcd_update(&mut rx, shutdown_receiver).await }, + ); + + Ok(Lcd { + tx, + shutdown_signal, + task_handle: Some(task_handle), + }) + } + + pub(crate) fn clone_tx(&self) -> mpsc::UnboundedSender { + self.tx.clone() + } +} + +async fn handle_lcd_update( + rx: &mut mpsc::UnboundedReceiver, + mut shutdown_receiver: Receiver<()>, +) -> eyre::Result<()> { + let mut delay = Delay::new(); + let mut device: Ft4232h = Ftdi::with_index(4)?.try_into()?; + device.reset().wrap_err("Failed to reset")?; + let hal = ftdi_embedded_hal::FtHal::init_freq(device, 30_000_000)?; + let spi = Box::pin(hal.spi_device(3)?); + let mut rst = hal.ad4()?; + let mut bl = hal.ad5()?; + let dc = hal.ad6()?; + + bl.set_low() + .map_err(|e| eyre::eyre!("Error setting backlight low: {:?}", e))?; + + let interface = SPIDisplayInterface::new(spi.as_ref().get_ref(), dc); + let mut display = Gc9a01::new( + interface, + DisplayResolution240x240, + DisplayRotation::Rotate180, + ) + .into_buffered_graphics(); + display + .reset(&mut rst, &mut delay) + .map_err(|e| eyre::eyre!("Error resetting display: {:?}", e))?; + display + .init(&mut delay) + .map_err(|e| eyre::eyre!("Error initializing display: {:?}", e))?; + display.fill(0x0000); + display + .flush() + .map_err(|e| eyre::eyre!("Error flushing display: {:?}", e))?; + + loop { + tokio::select! { + _ = shutdown_receiver.changed() => { + debug!("LCD task shutting down"); + return Ok(()) + } + command = rx.recv() => { + // turn back on in case it was turned off + if let Err(e) = bl + .set_high() { + tracing::info!("Backlight: {e:?}"); + } + display.clear(); + + match command { + Some(LcdCommand::ImageBmp(image, bg_color)) => { + match Bmp::from_slice(image.as_slice()) { + Ok(bmp) => { + // draw background color + if let Err(e) = fill_color(&mut display, bg_color) { + tracing::info!("{e:?}"); + } + + // compute center position for image + let width = bmp.size().width as i32; + let height = bmp.size().height as i32; + let x = (DisplayResolution240x240::WIDTH as i32 - width) / 2; + let y = (DisplayResolution240x240::HEIGHT as i32 - height) / 2; + + // draw image + let image = Image::new(&bmp, Point::new(x, y)); + if let Err(e) = image.draw(&mut display) { + tracing::info!("{e:?}"); + } + } + Err(e) => { + tracing::info!("Error loading image: {e:?}"); + } + } + } + Some(LcdCommand::Fill(color)) => { + if let Err(e) = fill_color(&mut display, color) { + tracing::info!("{e:?}"); + } + } + None => { + tracing::info!("LCD channel closed"); + return Err(eyre::eyre!("LCD channel closed")); + } + } + + if let Err(e) = display + .flush() + .map_err(|e| eyre::eyre!("Error flushing: {e:?}")) { + tracing::info!("{e}"); + } + } + } + } +} + +impl Drop for Lcd { + fn drop(&mut self) { + let _ = self.shutdown_signal.send(()); + // wait for task_handle to finish + if let Some(task_handle) = self.task_handle.take() { + task::spawn_blocking(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let _ = task_handle.await.unwrap(); + debug!("LCD task finished"); + }); + }); + } + } +} + +fn fill_color(display: &mut LcdDisplayDriver, color: Rgb565) -> eyre::Result<()> { + Rectangle::new( + Point::new(0, 0), + Size::new( + DisplayResolution240x240::WIDTH as u32, + DisplayResolution240x240::HEIGHT as u32, + ), + ) + .into_styled(PrimitiveStyleBuilder::new().fill_color(color).build()) + .draw(display) + .map_err(|e| eyre::eyre!("Error drawing the rectangle: {e:?}")) +} diff --git a/orb-ui/cone/src/led.rs b/orb-ui/cone/src/led.rs new file mode 100644 index 0000000..84a3557 --- /dev/null +++ b/orb-ui/cone/src/led.rs @@ -0,0 +1,140 @@ +use color_eyre::eyre; +use color_eyre::eyre::Context; +use ftdi_embedded_hal::eh1::spi::SpiBus; +use ftdi_embedded_hal::libftd2xx::{Ft4232h, Ftdi, FtdiCommon}; +use orb_rgb::Argb; +use std::sync::{Arc, Mutex}; +use tokio::sync::{mpsc, watch}; +use tokio::task; + +pub struct Led { + spi: ftdi_embedded_hal::Spi, + led_strip_tx: mpsc::UnboundedSender<[Argb; CONE_LED_COUNT]>, + task_handle: Option>>, + shutdown_signal: watch::Sender<()>, +} + +pub const CONE_LED_COUNT: usize = 64; + +impl Led { + pub(crate) fn spawn() -> eyre::Result>> { + let (tx, mut rx) = mpsc::unbounded_channel(); + let (shutdown_signal, mut shutdown_receiver) = watch::channel(()); + + let spi = { + let mut device: Ft4232h = Ftdi::with_index(5)?.try_into()?; + device.reset().wrap_err("Failed to reset")?; + let hal = ftdi_embedded_hal::FtHal::init_freq(device, 3_000_000)?; + hal.spi()? + }; + + let led = Arc::new(Mutex::new(Led { + spi, + led_strip_tx: tx.clone(), + task_handle: None, + shutdown_signal, + })); + + // spawn receiver thread + // where SPI communication happens + let led_clone = Arc::clone(&led); + let task = task::spawn(async move { + loop { + // todo do we want to update the LED strip at a fixed rate? + // todo do we want to only take the last message and ignore previous ones + tokio::select! { + _ = shutdown_receiver.changed() => { + return Ok(()); + } + msg = rx.recv() => { + if let Some(msg) = msg { + if let Ok(mut led) = led_clone.lock() { + if let Err(e) = led.spi_rgb_led_update_rgb(&msg) { + tracing::debug!("Failed to update LED strip: {e}"); + } + } + } else { + // none: channel closed + return Err(eyre::eyre!("LED strip receiver channel closed")); + } + } + } + } + }); + + if let Ok(led) = &mut led.lock() { + led.task_handle = Some(task); + } + + tracing::debug!("LED strip initialized"); + + Ok(led) + } + + pub(crate) fn clone_tx(&self) -> mpsc::UnboundedSender<[Argb; CONE_LED_COUNT]> { + self.led_strip_tx.clone() + } + + fn spi_rgb_led_update(&mut self, buffer: &[u8]) -> eyre::Result<()> { + const ZEROS: [u8; 4] = [0_u8; 4]; + let size = buffer.len(); + let ones_len = (size / 4) / 8 / 2 + 1; + let ones = vec![0xFF; ones_len]; + + // Start frame: at least 32 zeros + self.spi.write(&ZEROS)?; + + // LED data itself + self.spi.write(buffer)?; + + // End frame: at least (size / 4) / 2 ones to clock remaining bits + self.spi.write(ones.as_slice())?; + + Ok(()) + } + + fn spi_rgb_led_update_rgb( + &mut self, + pixels: &[Argb; CONE_LED_COUNT], + ) -> eyre::Result<()> { + let mut buffer = vec![0; pixels.len() * 4]; + for (i, pixel) in pixels.iter().enumerate() { + let prefix = if let Some(dimming) = pixel.0 { + 0xE0 | (dimming & 0x1F) + } else { + 0xE0 | 0x1F + }; + + // APA102 LED strip uses BGR order + buffer[i * 4] = prefix; + buffer[i * 4 + 1] = pixel.3; + buffer[i * 4 + 2] = pixel.2; + buffer[i * 4 + 3] = pixel.1; + } + + self.spi_rgb_led_update(buffer.as_slice()) + } + + /// Shutdown the LED strip task + /// This will free at least one Arc> reference + /// owned by the inner task. + pub fn shutdown(&self) { + let _ = self.shutdown_signal.send(()); + } +} + +impl Drop for Led { + fn drop(&mut self) { + let _ = self.shutdown_signal.send(()); + // wait for task_handle to finish + if let Some(task_handle) = self.task_handle.take() { + task::spawn_blocking(move || { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + let _ = task_handle.await.unwrap(); + tracing::debug!("LED strip task finished"); + }); + }); + } + } +} diff --git a/orb-ui/cone/src/lib.rs b/orb-ui/cone/src/lib.rs new file mode 100644 index 0000000..732f9d3 --- /dev/null +++ b/orb-ui/cone/src/lib.rs @@ -0,0 +1,159 @@ +pub mod button; +pub mod lcd; +pub mod led; + +use crate::button::Button; +use crate::lcd::{Lcd, LcdCommand}; +use crate::led::{Led, CONE_LED_COUNT}; +use color_eyre::eyre; +use color_eyre::eyre::Context; +use embedded_graphics::pixelcolor::{Rgb565, RgbColor}; +use ftdi_embedded_hal::libftd2xx::{Ft4232h, Ftdi, FtdiCommon}; +use image::{ImageFormat, Luma}; +use orb_rgb::Argb; +use std::sync::{Arc, Mutex}; +use std::{env, fs}; +use tokio::sync::mpsc; + +const CONE_FTDI_DEVICE_COUNT: usize = 8; + +#[derive(Debug)] +enum Status { + Connected, + Disconnected, +} + +/// Cone can be created only if connected to the host over USB. +pub struct Cone { + connection_status: Arc>, + lcd: Lcd, + led_strip: Arc>, + _button: Button, +} + +pub enum ConeEvents { + ButtonPressed(bool), +} + +impl Cone { + /// Create a new Cone instance. + pub fn new(event_queue: mpsc::UnboundedSender) -> eyre::Result { + let connection_status = if ftdi_embedded_hal::libftd2xx::list_devices()?.len() + != CONE_FTDI_DEVICE_COUNT + { + return Err(eyre::eyre!( + "FTDI device count mismatch: cone not connected?" + )); + } else { + let mut device: Ft4232h = Ftdi::with_index(6)? + .try_into() + .wrap_err("Failed to initialize FTDI device")?; + device.reset().wrap_err("Failed to reset")?; + Arc::new(Mutex::new(Status::Connected)) + }; + + let lcd = Lcd::spawn()?; + let led_strip = Led::spawn()?; + let _button = Button::spawn(event_queue.clone(), connection_status.clone())?; + + let cone = Cone { + connection_status, + lcd, + led_strip, + _button, + }; + + Ok(cone) + } + + pub fn is_connected(&self) -> bool { + if let Ok(status) = self.connection_status.lock() { + matches!(*status, Status::Connected) + } else { + false + } + } + + /// Update the RGB LEDs by passing the values to the LED strip sender. + pub fn queue_rgb_leds( + &mut self, + pixels: &[Argb; CONE_LED_COUNT], + ) -> eyre::Result<()> { + self.led_strip + .lock() + .expect("cannot lock LED strip mutex") + .clone_tx() + .send(*pixels) + .wrap_err("Failed to send LED strip values") + } + + 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 + .clone_tx() + .send(LcdCommand::Fill(color)) + .wrap_err("Failed to send") + } + + /// Update the LCD screen with a QR code. + /// `qr_str` is encoded as a QR code and sent to the LCD screen. + pub fn queue_lcd_qr_code(&mut self, qr_str: String) -> eyre::Result<()> { + let qr_code = qrcode::QrCode::new(qr_str.as_bytes())? + .render::>() + .dark_color(Luma([0_u8])) + .light_color(Luma([255_u8])) + .quiet_zone(true) // disable quiet zone (white border) + .min_dimensions(200, 200) + .max_dimensions(230, 230) // sets maximum image size + .build(); + let mut buffer = std::io::Cursor::new(vec![]); + qr_code.write_to(&mut buffer, ImageFormat::Bmp)?; + tracing::debug!("LCD QR: {:?}", qr_str); + self.lcd + .clone_tx() + .send(LcdCommand::ImageBmp(buffer.into_inner(), Rgb565::WHITE)) + .wrap_err("Failed to send") + } + + /// Update the LCD screen with a BMP image. + pub fn queue_lcd_bmp(&mut self, image: String) -> eyre::Result<()> { + // check if file exists, use absolute path for better understanding of the error + let absolute_path = env::current_dir()?.join(image); + if !absolute_path.exists() { + return Err(eyre::eyre!("File not found: {:?}", absolute_path)); + } + + // check if file is a bmp image + if absolute_path + .extension() + .ok_or(eyre::eyre!("Unable to get file extension"))? + == "bmp" + { + tracing::debug!("LCD image: {:?}", absolute_path); + let bmp_data = fs::read(absolute_path)?; + self.lcd + .clone_tx() + .send(LcdCommand::ImageBmp(bmp_data, Rgb565::BLACK)) + .wrap_err("Failed to send") + } else { + Err(eyre::eyre!( + "File is not a .bmp image, format is not supported: {:?}", + absolute_path + )) + } + } +} + +impl Drop for Cone { + fn drop(&mut self) { + tracing::debug!("Dropping the Cone"); + // we own an `Arc>` so we need to call `shutdown()` to drop the other + // reference to the `Arc>`. + // Otherwise, dropping the `Arc` would simply decrement the reference count and + // the `Led` would not be dropped. + self.led_strip.lock().unwrap().shutdown(); + + // the rest can be dropped normally + } +} From b58e06ce520fc16a948f408955575bb5bb1786c2 Mon Sep 17 00:00:00 2001 From: Cyril Fougeray Date: Wed, 31 Jul 2024 12:16:42 +0200 Subject: [PATCH 3/3] orb-ui: diamond: self-serve with cone support initial cone support --- Cargo.lock | 1 + orb-ui/Cargo.toml | 1 + orb-ui/README.md | 1 + orb-ui/src/dbus.rs | 30 +- orb-ui/src/engine/animations/static.rs | 10 + orb-ui/src/engine/diamond.rs | 398 ++++++++++--------------- orb-ui/src/engine/mod.rs | 74 ++++- orb-ui/src/engine/pearl.rs | 171 +++++------ orb-ui/src/hal/mod.rs | 194 ++++++++++++ orb-ui/src/{ => hal}/serial.rs | 7 + orb-ui/src/main.rs | 33 +- orb-ui/src/observer.rs | 4 +- 12 files changed, 535 insertions(+), 389 deletions(-) create mode 100644 orb-ui/src/hal/mod.rs rename orb-ui/src/{ => hal}/serial.rs (86%) diff --git a/Cargo.lock b/Cargo.lock index dc323cd..3ce3c4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3422,6 +3422,7 @@ dependencies = [ "eyre", "futures", "orb-build-info", + "orb-cone", "orb-messages", "orb-rgb", "orb-sound", diff --git a/orb-ui/Cargo.toml b/orb-ui/Cargo.toml index 2d08b0a..6148b2e 100644 --- a/orb-ui/Cargo.toml +++ b/orb-ui/Cargo.toml @@ -21,6 +21,7 @@ orb-build-info.path = "../build-info" orb-messages.workspace = true orb-sound.path = "sound" orb-uart.path = "uart" +orb-cone.path = "cone" orb-rgb.path = "rgb" pid.path = "pid" prost = "0.12.3" diff --git a/orb-ui/README.md b/orb-ui/README.md index 8e4e2f1..52c4f9e 100644 --- a/orb-ui/README.md +++ b/orb-ui/README.md @@ -24,6 +24,7 @@ Test new event with the orb-ui daemon running: ```shell busctl --user call org.worldcoin.OrbUiState1 /org/worldcoin/OrbUiState1 org.worldcoin.OrbUiState1 OrbSignupStateEvent s "\"Bootup\"" +busctl --user call org.worldcoin.OrbUserEvent1 /org/worldcoin/OrbUserEvent1 org.worldcoin.OrbUserEvent1 UserEvent s "\"ConeButtonPressed\"" ``` ## Platform Support diff --git a/orb-ui/src/dbus.rs b/orb-ui/src/dbus.rs index b102c65..78299fc 100644 --- a/orb-ui/src/dbus.rs +++ b/orb-ui/src/dbus.rs @@ -1,29 +1,28 @@ //! Dbus interface definitions. -use crate::engine; -use crate::engine::Event; +use crate::engine::RxEvent; use tokio::sync::mpsc; -use zbus::interface; +use zbus::{interface, proxy}; /// Dbus interface object for OrbUiState1. #[derive(Debug)] -pub struct Interface { - events: mpsc::UnboundedSender, +pub struct InboundInterface { + events: mpsc::UnboundedSender, } -impl Interface { - pub fn new(events: mpsc::UnboundedSender) -> Self { +impl InboundInterface { + pub fn new(events: mpsc::UnboundedSender) -> Self { Self { events } } } #[interface(name = "org.worldcoin.OrbUiState1")] -impl Interface { - /// Forward events to UI engine by sending serialized engine::Event to the event channel. +impl InboundInterface { + /// Forward events to UI engine by sending serialized engine::TxEvent to the event channel. async fn orb_signup_state_event(&mut self, event: String) -> zbus::fdo::Result<()> { - // parse event to engine::Event using json_serde + // parse serialized event tracing::debug!("received JSON event: {}", event); - let event: engine::Event = serde_json::from_str(&event).map_err(|e| { + let event: RxEvent = serde_json::from_str(&event).map_err(|e| { zbus::fdo::Error::InvalidArgs(format!( "invalid event: failed to parse {}", e @@ -35,3 +34,12 @@ impl Interface { Ok(()) } } + +#[proxy( + default_service = "org.worldcoin.OrbUserEvent1", + default_path = "/org/worldcoin/OrbUserEvent1", + interface = "org.worldcoin.OrbUserEvent1" +)] +trait OutboundInterface { + fn user_event(&self, event: String) -> zbus::fdo::Result<()>; +} diff --git a/orb-ui/src/engine/animations/static.rs b/orb-ui/src/engine/animations/static.rs index 2fb9fbb..0574eb0 100644 --- a/orb-ui/src/engine/animations/static.rs +++ b/orb-ui/src/engine/animations/static.rs @@ -23,6 +23,16 @@ impl Static { } } +impl Default for Static { + fn default() -> Self { + Self { + current_color: Argb::OFF, + max_time: None, + stop: false, + } + } +} + impl Animation for Static { type Frame = [Argb; N]; diff --git a/orb-ui/src/engine/diamond.rs b/orb-ui/src/engine/diamond.rs index 2bf92e4..0c913ad 100644 --- a/orb-ui/src/engine/diamond.rs +++ b/orb-ui/src/engine/diamond.rs @@ -1,6 +1,5 @@ use async_trait::async_trait; use eyre::Result; -use futures::channel::mpsc::Sender; use futures::future::Either; use futures::{future, StreamExt}; use orb_messages::mcu_main::mcu_message::Message; @@ -9,32 +8,27 @@ use orb_rgb::Argb; use pid::{InstantTimer, Timer}; use std::f64::consts::PI; use std::time::Duration; +use tokio::sync::mpsc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::time; use tokio_stream::wrappers::{IntervalStream, UnboundedReceiverStream}; use crate::engine::animations::alert::BlinkDurations; use crate::engine::{ - animations, operator, Animation, AnimationsStack, CenterFrame, ConeFrame, Event, - EventHandler, OperatorFrame, OrbType, QrScanSchema, QrScanUnexpectedReason, - RingFrame, Runner, RunningAnimation, SignupFailReason, DIAMOND_CENTER_LED_COUNT, - DIAMOND_CONE_LED_COUNT, DIAMOND_RING_LED_COUNT, LED_ENGINE_FPS, LEVEL_BACKGROUND, - LEVEL_FOREGROUND, LEVEL_NOTICE, + animations, operator, Animation, AnimationsStack, CenterFrame, ConeDisplay, + ConeFrame, EventHandler, OperatorFrame, OrbType, QrScanSchema, + QrScanUnexpectedReason, RingFrame, Runner, RunningAnimation, RxEvent, + SignupFailReason, DIAMOND_CENTER_LED_COUNT, DIAMOND_CONE_LED_COUNT, + DIAMOND_RING_LED_COUNT, LED_ENGINE_FPS, LEVEL_BACKGROUND, LEVEL_FOREGROUND, + LEVEL_NOTICE, }; +use crate::hal::HalMessage; use crate::sound; use crate::sound::Player; -struct WrappedCenterMessage(Message); - -struct WrappedRingMessage(Message); - -struct WrappedConeMessage(Message); - -struct WrappedOperatorMessage(Message); - -impl From> for WrappedCenterMessage { +impl From> for HalMessage { fn from(value: CenterFrame) -> Self { - WrappedCenterMessage(Message::JMessage( + HalMessage::Mcu(Message::JMessage( JetsonToMcu { ack_number: 0, payload: Some(jetson_to_mcu::Payload::CenterLedsSequence( @@ -50,9 +44,9 @@ impl From> for WrappedCenterMessage { } } -impl From> for WrappedRingMessage { +impl From> for HalMessage { fn from(value: RingFrame) -> Self { - WrappedRingMessage(Message::JMessage( + HalMessage::Mcu(Message::JMessage( JetsonToMcu { ack_number: 0, payload: Some(jetson_to_mcu::Payload::RingLedsSequence( @@ -68,45 +62,9 @@ impl From> for WrappedRingMessage { } } -impl From> for WrappedConeMessage { - fn from(value: ConeFrame) -> Self { - WrappedConeMessage(Message::JMessage( - JetsonToMcu { - ack_number: 0, - payload: Some(jetson_to_mcu::Payload::ConeLedsSequence( - orb_messages::mcu_main::ConeLeDsSequence { - data_format: Some( - orb_messages::mcu_main::cone_le_ds_sequence::DataFormat::Argb32Uncompressed( - value.iter().flat_map(|&Argb(a, r, g, b)| [a.unwrap_or(0_u8), r, g, b]).collect(), - )) - } - )), - } - )) - } -} - -impl From for WrappedOperatorMessage { - fn from(value: OperatorFrame) -> Self { - WrappedOperatorMessage(Message::JMessage( - JetsonToMcu { - ack_number: 0, - payload: Some(jetson_to_mcu::Payload::DistributorLedsSequence( - orb_messages::mcu_main::DistributorLeDsSequence { - data_format: Some( - orb_messages::mcu_main::distributor_le_ds_sequence::DataFormat::Argb32Uncompressed( - value.iter().rev().flat_map(|&Argb(a, r, g, b)| [a.unwrap_or(0_u8), r, g, b]).collect(), - )) - } - )), - } - )) - } -} - pub async fn event_loop( - rx: UnboundedReceiver, - mcu_tx: Sender, + rx: UnboundedReceiver, + mut hal_tx: mpsc::Sender, ) -> Result<()> { let mut interval = time::interval(Duration::from_millis(1000 / LED_ENGINE_FPS)); interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); @@ -130,12 +88,12 @@ pub async fn event_loop( } Either::Left((Some(event), _)) => { if let Err(e) = runner.event(&event) { - tracing::error!("Error handling event: {:?}", e); + tracing::warn!("Error handling event: {:?}", e); } } Either::Right(_) => { - if let Err(e) = runner.run(&mut mcu_tx.clone()).await { - tracing::error!("Error running UI: {:?}", e); + if let Err(e) = runner.run(&mut hal_tx).await { + tracing::warn!("Error running UI: {:?}", e); } } } @@ -145,13 +103,18 @@ pub async fn event_loop( impl Runner { pub(crate) fn new(sound: sound::Jetson) -> Self { + let (tx, rx) = mpsc::channel(1); Self { timer: InstantTimer::default(), ring_animations_stack: AnimationsStack::new(), center_animations_stack: AnimationsStack::new(), cone_animations_stack: Some(AnimationsStack::new()), + cone_display_queue_tx: Some(tx), + cone_display_queue_rx: Some(rx), ring_frame: [Argb(Some(0), 0, 0, 0); DIAMOND_RING_LED_COUNT], - cone_frame: None, + cone_frame: Some(ConeFrame( + [Argb(Some(0), 0, 0, 0); DIAMOND_CONE_LED_COUNT], + )), center_frame: [Argb(Some(0), 0, 0, 0); DIAMOND_CENTER_LED_COUNT], operator_frame: OperatorFrame::default(), operator_idle: operator::Idle::new(OrbType::Diamond), @@ -163,6 +126,7 @@ impl Runner { capture_sound: sound::capture::CaptureLoopSound::default(), is_api_mode: false, paused: false, + self_serve: true, } } @@ -177,10 +141,21 @@ impl Runner { fn set_cone( &mut self, level: u8, - animation: impl Animation>, + animation: impl Animation>, ) { if let Some(animations) = &mut self.cone_animations_stack { animations.set(level, Box::new(animation)); + } else { + tracing::warn!("Trying to set cone animation, but cone animations stack is not initialized"); + } + } + + /// fixme: if it fails, screen not updated, and not retried + fn set_cone_display(&mut self, new_state: ConeDisplay) { + if let Some(tx) = &self.cone_display_queue_tx { + if let Err(e) = tx.try_send(new_state) { + tracing::error!("Failed to send new cone display state: {:?}", e); + } } } @@ -205,57 +180,66 @@ impl Runner { fn stop_center(&mut self, level: u8, force: bool) { self.center_animations_stack.stop(level, force); } + + fn reset_front_ui(&mut self) { + self.stop_ring(LEVEL_BACKGROUND, true); + self.stop_center(LEVEL_BACKGROUND, true); + self.stop_cone(LEVEL_BACKGROUND, true); + self.stop_ring(LEVEL_FOREGROUND, true); + self.stop_center(LEVEL_FOREGROUND, true); + self.stop_cone(LEVEL_FOREGROUND, true); + self.stop_ring(LEVEL_NOTICE, true); + self.stop_center(LEVEL_NOTICE, true); + self.stop_cone(LEVEL_NOTICE, true); + + self.set_ring( + LEVEL_BACKGROUND, + animations::Idle::::default(), + ); + self.set_center( + LEVEL_BACKGROUND, + animations::Static::::default(), + ); + self.set_cone( + LEVEL_BACKGROUND, + animations::Idle::::default(), + ); + } } #[async_trait] impl EventHandler for Runner { #[allow(clippy::too_many_lines)] - fn event(&mut self, event: &Event) -> Result<()> { - tracing::trace!("UI event: {}", serde_json::to_string(event)?.as_str()); + fn event(&mut self, event: &RxEvent) -> Result<()> { + tracing::info!("UI event: {}", serde_json::to_string(event)?.as_str()); match event { - Event::Bootup => { - self.stop_ring(LEVEL_NOTICE, true); - self.stop_center(LEVEL_NOTICE, true); - self.stop_cone(LEVEL_NOTICE, true); - self.set_ring( - LEVEL_BACKGROUND, - animations::Idle::::default(), - ); + RxEvent::Bootup => { + self.reset_front_ui(); self.operator_pulse.trigger(1., 1., false, false); } - Event::BootComplete { api_mode } => { + RxEvent::BootComplete { api_mode } => { + self.reset_front_ui(); self.sound .queue(sound::Type::Melody(sound::Melody::BootUp))?; self.operator_pulse.stop(); self.operator_idle.api_mode(*api_mode); + self.set_cone_display(ConeDisplay::FillColor(Argb::FULL_WHITE)); self.is_api_mode = *api_mode; } - Event::Shutdown { requested } => { + RxEvent::Shutdown { requested: _ } => { self.sound .queue(sound::Type::Melody(sound::Melody::PoweringDown))?; - // overwrite any existing animation by setting notice-level animation - // as the last animation before shutdown - self.set_center( - LEVEL_NOTICE, - animations::Alert::::new( - if *requested { - Argb::DIAMOND_USER_QR_SCAN - } else { - Argb::DIAMOND_USER_AMBER - }, - BlinkDurations::from(vec![0.0, 0.3, 0.45, 0.3, 0.45, 0.45]), - None, - false, - ), - ); self.set_ring( LEVEL_NOTICE, animations::Static::::new(Argb::OFF, None), ); + // turn off cone + self.stop_cone(LEVEL_FOREGROUND, true); + self.set_cone_display(ConeDisplay::FillColor(Argb::FULL_BLACK)); self.operator_action .trigger(1.0, Argb::OFF, true, false, true); } - Event::SignupStart => { + RxEvent::SignupStart => { self.capture_sound.reset(); self.sound .queue(sound::Type::Melody(sound::Melody::StartSignup))?; @@ -272,33 +256,7 @@ impl EventHandler for Runner { self.operator_signup_phase.signup_phase_started(); // stop all - self.set_center( - LEVEL_BACKGROUND, - animations::Static::::new( - Argb::OFF, - None, - ), - ); - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_NOTICE, true); - - self.set_ring( - LEVEL_BACKGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_QR_SCAN, - None, - ), - ); - self.set_ring( - LEVEL_NOTICE, - animations::Alert::::new( - Argb::DIAMOND_USER_QR_SCAN, - BlinkDurations::from(vec![0.0, 0.3, 0.3]), - None, - false, - ), - ); + self.reset_front_ui(); self.set_cone( LEVEL_NOTICE, animations::Alert::::new( @@ -309,7 +267,7 @@ impl EventHandler for Runner { ), ); } - Event::QrScanStart { schema } => { + RxEvent::QrScanStart { schema } => { match schema { QrScanSchema::Operator => { self.set_ring( @@ -336,15 +294,19 @@ impl EventHandler for Runner { None, ), ); + self.stop_cone(LEVEL_FOREGROUND, true); + self.set_cone_display(ConeDisplay::QrCode( + "https://www.worldcoin.org/".to_string(), + )); } }; } - Event::QrScanCapture => { + RxEvent::QrScanCapture => { self.stop_center(LEVEL_FOREGROUND, true); self.sound .queue(sound::Type::Melody(sound::Melody::QrCodeCapture))?; } - Event::QrScanCompleted { schema } => { + RxEvent::QrScanCompleted { schema } => { self.stop_ring(LEVEL_FOREGROUND, true); self.stop_center(LEVEL_FOREGROUND, true); // reset ring background to black/off so that it's turned off in next animations @@ -378,7 +340,7 @@ impl EventHandler for Runner { QrScanSchema::Wifi => {} } } - Event::QrScanUnexpected { schema, reason } => { + RxEvent::QrScanUnexpected { schema, reason } => { match reason { QrScanUnexpectedReason::Invalid => { self.sound @@ -401,13 +363,13 @@ impl EventHandler for Runner { } self.stop_center(LEVEL_FOREGROUND, true); } - Event::QrScanFail { schema } => { + RxEvent::QrScanFail { schema } => { self.sound .queue(sound::Type::Melody(sound::Melody::SoundError))?; match schema { QrScanSchema::User | QrScanSchema::Operator => { - self.stop_ring(LEVEL_FOREGROUND, true); self.stop_center(LEVEL_FOREGROUND, true); + self.stop_cone(LEVEL_FOREGROUND, true); self.set_center( LEVEL_FOREGROUND, animations::Static::::new( @@ -416,12 +378,13 @@ impl EventHandler for Runner { ), ); self.operator_signup_phase.failure(); + self.set_cone_display(ConeDisplay::FillColor(Argb::FULL_WHITE)) } QrScanSchema::Wifi => {} } self.stop_ring(LEVEL_FOREGROUND, true); } - Event::QrScanSuccess { schema } => match schema { + RxEvent::QrScanSuccess { schema } => match schema { QrScanSchema::Operator => { self.sound .queue(sound::Type::Melody(sound::Melody::QrLoadSuccess))?; @@ -440,29 +403,27 @@ impl EventHandler for Runner { false, ), ); - // wave center LEDs to transition to biometric capture + // fixme bring back waves self.set_center( LEVEL_FOREGROUND, - animations::Wave::::new( + animations::Static::::new( Argb::DIAMOND_USER_AMBER, - 4.0, - 0.0, - false, + None, ), ); self.stop_cone(LEVEL_FOREGROUND, true); + self.set_cone_display(ConeDisplay::FillColor(Argb::FULL_BLACK)); } QrScanSchema::Wifi => { self.sound .queue(sound::Type::Melody(sound::Melody::QrLoadSuccess))?; } }, - Event::QrScanTimeout { schema } => { + RxEvent::QrScanTimeout { schema } => { self.sound .queue(sound::Type::Voice(sound::Voice::Timeout))?; match schema { QrScanSchema::User | QrScanSchema::Operator => { - self.stop_ring(LEVEL_FOREGROUND, true); self.stop_center(LEVEL_FOREGROUND, true); self.set_center( LEVEL_FOREGROUND, @@ -472,12 +433,13 @@ impl EventHandler for Runner { ), ); self.operator_signup_phase.failure(); + self.set_cone_display(ConeDisplay::FillColor(Argb::FULL_WHITE)) } QrScanSchema::Wifi => {} } self.stop_ring(LEVEL_FOREGROUND, true); } - Event::MagicQrActionCompleted { success } => { + RxEvent::MagicQrActionCompleted { success } => { let melody = if *success { sound::Melody::QrLoadSuccess } else { @@ -488,18 +450,18 @@ impl EventHandler for Runner { // to inform the operator to press the button. self.operator_signup_phase.failure(); } - Event::NetworkConnectionSuccess => { + RxEvent::NetworkConnectionSuccess => { self.sound.queue(sound::Type::Melody( sound::Melody::InternetConnectionSuccessful, ))?; } - Event::BiometricCaptureHalfObjectivesCompleted => { + RxEvent::BiometricCaptureHalfObjectivesCompleted => { // do nothing } - Event::BiometricCaptureAllObjectivesCompleted => { + RxEvent::BiometricCaptureAllObjectivesCompleted => { self.operator_signup_phase.irises_captured(); } - Event::BiometricCaptureProgress { progress } => { + RxEvent::BiometricCaptureProgress { progress } => { if self .ring_animations_stack .stack @@ -534,59 +496,14 @@ impl EventHandler for Runner { ring_progress.set_progress(*progress, None); } } - Event::BiometricCaptureOcclusion { occlusion_detected } => { - // don't set a new wave animation if already waving - // to not interrupt the current animation - let waving = self - .center_animations_stack - .stack - .get_mut(&LEVEL_FOREGROUND) - .and_then(|RunningAnimation { animation, .. }| { - animation - .as_any_mut() - .downcast_mut::>( - ) - }) - .is_some(); + RxEvent::BiometricCaptureOcclusion { occlusion_detected } => { if *occlusion_detected { - if !waving { - self.stop_center(LEVEL_FOREGROUND, true); - // wave center LEDs - self.set_center( - LEVEL_FOREGROUND, - animations::Wave::::new( - Argb::DIAMOND_USER_AMBER, - 4.0, - 0.0, - false, - ), - ); - } self.operator_signup_phase.capture_occlusion_issue(); } else { - self.stop_center(LEVEL_FOREGROUND, true); - self.set_center( - LEVEL_FOREGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_AMBER, - None, - ), - ); self.operator_signup_phase.capture_occlusion_ok(); } } - Event::BiometricCaptureDistance { in_range } => { - let waving = self - .center_animations_stack - .stack - .get_mut(&LEVEL_FOREGROUND) - .and_then(|RunningAnimation { animation, .. }| { - animation - .as_any_mut() - .downcast_mut::>( - ) - }) - .is_some(); + RxEvent::BiometricCaptureDistance { in_range } => { if *in_range { self.operator_signup_phase.capture_distance_ok(); if let Some(melody) = self.capture_sound.peekable().peek() { @@ -594,28 +511,7 @@ impl EventHandler for Runner { self.capture_sound.next(); } } - self.stop_center(LEVEL_FOREGROUND, true); - self.set_center( - LEVEL_FOREGROUND, - animations::Static::::new( - Argb::DIAMOND_USER_AMBER, - None, - ), - ); } else { - if !waving { - self.stop_center(LEVEL_FOREGROUND, true); - // wave center LEDs - self.set_center( - LEVEL_FOREGROUND, - animations::Wave::::new( - Argb::DIAMOND_USER_AMBER, - 4.0, - 0.0, - false, - ), - ); - } self.operator_signup_phase.capture_distance_issue(); self.capture_sound = sound::capture::CaptureLoopSound::default(); let _ = self @@ -623,7 +519,7 @@ impl EventHandler for Runner { .try_queue(sound::Type::Voice(sound::Voice::Silence)); } } - Event::BiometricCaptureSuccess => { + RxEvent::BiometricCaptureSuccess => { self.sound .queue(sound::Type::Melody(sound::Melody::IrisScanSuccess))?; // custom alert animation on ring @@ -653,7 +549,7 @@ impl EventHandler for Runner { self.operator_signup_phase.iris_scan_complete(); } - Event::BiometricPipelineProgress { progress } => { + RxEvent::BiometricPipelineProgress { progress } => { let ring_animation = self .ring_animations_stack .stack @@ -678,7 +574,7 @@ impl EventHandler for Runner { self.operator_signup_phase.processing_2(); } } - Event::StartingEnrollment => { + RxEvent::StartingEnrollment => { let progress = self .ring_animations_stack .stack @@ -695,7 +591,7 @@ impl EventHandler for Runner { } self.operator_signup_phase.uploading(); } - Event::BiometricPipelineSuccess => { + RxEvent::BiometricPipelineSuccess => { let progress = self .ring_animations_stack .stack @@ -713,7 +609,7 @@ impl EventHandler for Runner { self.operator_signup_phase.biometric_pipeline_successful(); } - Event::SignupFail { reason } => { + RxEvent::SignupFail { reason } => { self.sound .queue(sound::Type::Melody(sound::Melody::SoundError))?; match reason { @@ -786,7 +682,7 @@ impl EventHandler for Runner { } self.stop_ring(LEVEL_FOREGROUND, false); } - Event::SignupSuccess => { + RxEvent::SignupSuccess => { self.sound .queue(sound::Type::Melody(sound::Melody::SignupSuccess))?; @@ -803,46 +699,53 @@ impl EventHandler for Runner { ), ); } - Event::Idle => { - self.stop_ring(LEVEL_FOREGROUND, true); - self.stop_center(LEVEL_FOREGROUND, true); - self.stop_cone(LEVEL_FOREGROUND, true); - self.stop_ring(LEVEL_NOTICE, false); - self.stop_center(LEVEL_NOTICE, false); - self.stop_cone(LEVEL_NOTICE, false); + RxEvent::Idle => { + self.reset_front_ui(); + if self.self_serve { + self.set_cone_display(ConeDisplay::FillColor(Argb::FULL_WHITE)); + self.set_cone( + LEVEL_FOREGROUND, + animations::wave::Wave::::new( + Argb::DIAMOND_USER_AMBER, + 4.0, + 0.0, + false, + ), + ); + } self.operator_signup_phase.idle(); } - Event::GoodInternet => { + RxEvent::GoodInternet => { self.operator_idle.good_internet(); } - Event::SlowInternet => { + RxEvent::SlowInternet => { self.operator_idle.slow_internet(); } - Event::NoInternet => { + RxEvent::NoInternet => { self.operator_idle.no_internet(); } - Event::GoodWlan => { + RxEvent::GoodWlan => { self.operator_idle.good_wlan(); } - Event::SlowWlan => { + RxEvent::SlowWlan => { self.operator_idle.slow_wlan(); } - Event::NoWlan => { + RxEvent::NoWlan => { self.operator_idle.no_wlan(); } - Event::BatteryCapacity { percentage } => { + RxEvent::BatteryCapacity { percentage } => { self.operator_idle.battery_capacity(*percentage); } - Event::BatteryIsCharging { is_charging } => { + RxEvent::BatteryIsCharging { is_charging } => { self.operator_idle.battery_charging(*is_charging); } - Event::Pause => { + RxEvent::Pause => { self.paused = true; } - Event::Resume => { + RxEvent::Resume => { self.paused = false; } - Event::RecoveryImage => { + RxEvent::RecoveryImage => { self.sound .queue(sound::Type::Voice(sound::Voice::PleaseDontShutDown))?; // check that ring is not already in recovery mode @@ -865,20 +768,20 @@ impl EventHandler for Runner { ); } } - Event::NoInternetForSignup => { + RxEvent::NoInternetForSignup => { self.sound.queue(sound::Type::Voice( sound::Voice::InternetConnectionTooSlowToPerformSignups, ))?; } - Event::SlowInternetForSignup => { + RxEvent::SlowInternetForSignup => { self.sound.queue(sound::Type::Voice( sound::Voice::InternetConnectionTooSlowSignupsMightTakeLonger, ))?; } - Event::SoundVolume { level } => { + RxEvent::SoundVolume { level } => { self.sound.set_master_volume(*level); } - Event::SoundLanguage { lang } => { + RxEvent::SoundLanguage { lang } => { let language = lang.clone(); let sound = self.sound.clone(); // spawn a new task because we need some async work here @@ -889,7 +792,7 @@ impl EventHandler for Runner { } }); } - Event::SoundTest => { + RxEvent::SoundTest => { self.sound .queue(sound::Type::Melody(sound::Melody::BootUp))?; } @@ -897,11 +800,11 @@ impl EventHandler for Runner { Ok(()) } - async fn run(&mut self, interface_tx: &mut Sender) -> Result<()> { + async fn run(&mut self, hal_tx: &mut mpsc::Sender) -> Result<()> { let dt = self.timer.get_dt().unwrap_or(0.0); self.center_animations_stack.run(&mut self.center_frame, dt); if !self.paused { - interface_tx.try_send(WrappedCenterMessage::from(self.center_frame).0)?; + hal_tx.try_send(HalMessage::from(self.center_frame))?; } self.operator_idle @@ -915,26 +818,35 @@ impl EventHandler for Runner { self.operator_action .animate(&mut self.operator_frame, dt, false); if !self.paused { - // 2ms sleep to make sure UART communication is over - time::sleep(Duration::from_millis(2)).await; - interface_tx - .try_send(WrappedOperatorMessage::from(self.operator_frame).0)?; + hal_tx.try_send(HalMessage::from(self.operator_frame))?; } self.ring_animations_stack.run(&mut self.ring_frame, dt); if !self.paused { - time::sleep(Duration::from_millis(2)).await; - interface_tx.try_send(WrappedRingMessage::from(self.ring_frame).0)?; + hal_tx.try_send(HalMessage::from(self.ring_frame))?; } if let Some(animation) = &mut self.cone_animations_stack { if let Some(frame) = &mut self.cone_frame { - animation.run(frame, dt); + animation.run(&mut frame.0, dt); if !self.paused { - time::sleep(Duration::from_millis(2)).await; - interface_tx.try_send(WrappedConeMessage::from(*frame).0)?; + hal_tx.try_send(HalMessage::from(frame))?; + } + } + } + + if let Some(display_queue) = &mut self.cone_display_queue_rx { + match display_queue.try_recv() { + Ok(ConeDisplay::QrCode(txt)) if !self.paused => { + hal_tx.try_send(HalMessage::ConeLcdQrCode(txt))?; } + Ok(ConeDisplay::FillColor(color)) if !self.paused => { + hal_tx.try_send(HalMessage::ConeLcdFillColor(color))?; + } + Err(_) => { /* no new display state? */ } + Ok(_) => { /* paused? skip display */ } } } + // one last update of the UI has been performed since api_mode has been set, // (to set the api_mode UI state), so we can now pause the engine if self.is_api_mode && !self.paused { diff --git a/orb-ui/src/engine/mod.rs b/orb-ui/src/engine/mod.rs index 37a1a9d..37dff53 100644 --- a/orb-ui/src/engine/mod.rs +++ b/orb-ui/src/engine/mod.rs @@ -1,11 +1,12 @@ //! LED engine. +use crate::hal::HalMessage; use crate::sound; use crate::tokio_spawn; use async_trait::async_trait; use eyre::Result; -use futures::channel::mpsc::Sender; use orb_messages::mcu_main::mcu_message::Message; +use orb_messages::mcu_main::{jetson_to_mcu, JetsonToMcu}; use orb_rgb::Argb; use pid::InstantTimer; use serde::{Deserialize, Serialize}; @@ -80,7 +81,7 @@ macro_rules! event_enum { $(#[doc = $doc])? fn $method(&self, $($($field: $ty,)*)?) { let event = $name::$event $({$($field,)*})?; - self.tx.send(event).expect("LED engine is not running"); + self.tx.send(event).expect("Ui engine is not running"); } )* @@ -95,7 +96,7 @@ macro_rules! event_enum { $(#[doc = $doc])? fn $method(&self, $($($field: $ty,)*)?) { let event = $name::$event $({$($field,)*})?; - self.tx.send(event).expect("LED engine is not running"); + self.tx.send(event).expect("Ui engine is not running"); } )* @@ -180,7 +181,7 @@ impl From for SignupFailReason { event_enum! { /// Definition of all the events #[allow(dead_code)] - pub enum Event { + pub enum RxEvent { /// Orb boot up. #[event_enum(method = bootup)] Bootup, @@ -347,6 +348,15 @@ event_enum! { } } +/// Events sent over dbus +#[derive(Debug, Deserialize, Serialize)] +pub enum TxEvent { + /// Button pressed. + ConeButtonPressed, + /// Button released. + ConeButtonReleased, +} + /// Returned by [`Animation::animate`] #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum AnimationState { @@ -395,11 +405,11 @@ pub trait Animation: Send + 'static { /// LED engine for the Orb hardware. pub struct PearlJetson { - tx: mpsc::UnboundedSender, + tx: mpsc::UnboundedSender, } pub struct DiamondJetson { - tx: mpsc::UnboundedSender, + tx: mpsc::UnboundedSender, } /// LED engine interface which does nothing. @@ -412,10 +422,39 @@ pub type RingFrame = [Argb; RING_LED_COUNT]; pub type CenterFrame = [Argb; CENTER_LED_COUNT]; /// Frame for the cone LEDs. -pub type ConeFrame = [Argb; CONE_LED_COUNT]; +pub struct ConeFrame([Argb; orb_cone::led::CONE_LED_COUNT]); + +pub enum ConeDisplay { + QrCode(String), + FillColor(Argb), +} pub type OperatorFrame = [Argb; 5]; +impl From for HalMessage { + fn from(value: OperatorFrame) -> Self { + HalMessage::Mcu(Message::JMessage( + JetsonToMcu { + ack_number: 0, + payload: Some(jetson_to_mcu::Payload::DistributorLedsSequence( + orb_messages::mcu_main::DistributorLeDsSequence { + data_format: Some( + orb_messages::mcu_main::distributor_le_ds_sequence::DataFormat::RgbUncompressed( + value.iter().flat_map(|&Argb(_, r, g, b)| [r, g, b]).collect(), + )) + } + )), + } + )) + } +} + +impl From<&mut ConeFrame> for HalMessage { + fn from(value: &mut ConeFrame) -> Self { + HalMessage::ConeLed(value.0) + } +} + type DynamicAnimation = Box>; struct Runner { @@ -423,8 +462,10 @@ struct Runner { ring_animations_stack: AnimationsStack>, center_animations_stack: AnimationsStack>, cone_animations_stack: Option>>, + cone_display_queue_tx: Option>, + cone_display_queue_rx: Option>, ring_frame: RingFrame, - cone_frame: Option>, + cone_frame: Option, center_frame: CenterFrame, operator_frame: OperatorFrame, operator_idle: operator::Idle, @@ -438,13 +479,14 @@ struct Runner { is_api_mode: bool, /// Pause engine paused: bool, + self_serve: bool, } #[async_trait] trait EventHandler { - fn event(&mut self, event: &Event) -> Result<()>; + fn event(&mut self, event: &RxEvent) -> Result<()>; - async fn run(&mut self, interface_tx: &mut Sender) -> Result<()>; + async fn run(&mut self, hal_tx: &mut mpsc::Sender) -> Result<()>; } struct AnimationsStack { @@ -459,7 +501,7 @@ struct RunningAnimation { impl PearlJetson { /// Creates a new LED engine. #[must_use] - pub(crate) fn spawn(interface_tx: &mut Sender) -> Self { + pub(crate) fn spawn(interface_tx: &mut mpsc::Sender) -> Self { let (tx, rx) = mpsc::unbounded_channel(); tokio_spawn( "pearl event_loop", @@ -472,28 +514,28 @@ impl PearlJetson { impl DiamondJetson { /// Creates a new LED engine. #[must_use] - pub(crate) fn spawn(interface_tx: &mut Sender) -> Self { + pub(crate) fn spawn(hal_tx: &mut mpsc::Sender) -> Self { let (tx, rx) = mpsc::unbounded_channel(); tokio_spawn( "diamond event_loop", - diamond::event_loop(rx, interface_tx.clone()), + diamond::event_loop(rx, hal_tx.clone()), ); Self { tx } } } pub trait EventChannel: Sync + Send { - fn clone_tx(&self) -> mpsc::UnboundedSender; + fn clone_tx(&self) -> mpsc::UnboundedSender; } impl EventChannel for PearlJetson { - fn clone_tx(&self) -> mpsc::UnboundedSender { + fn clone_tx(&self) -> mpsc::UnboundedSender { self.tx.clone() } } impl EventChannel for DiamondJetson { - fn clone_tx(&self) -> mpsc::UnboundedSender { + fn clone_tx(&self) -> mpsc::UnboundedSender { self.tx.clone() } } diff --git a/orb-ui/src/engine/pearl.rs b/orb-ui/src/engine/pearl.rs index 46e94e8..4056d96 100644 --- a/orb-ui/src/engine/pearl.rs +++ b/orb-ui/src/engine/pearl.rs @@ -3,12 +3,11 @@ use std::time::Duration; use async_trait::async_trait; use eyre::Result; -use futures::channel::mpsc; -use futures::channel::mpsc::Sender; use futures::future::Either; use futures::{future, StreamExt}; use orb_messages::mcu_main::mcu_message::Message; use orb_messages::mcu_main::{jetson_to_mcu, JetsonToMcu}; +use tokio::sync::mpsc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::time; use tokio_stream::wrappers::{IntervalStream, UnboundedReceiverStream}; @@ -17,21 +16,20 @@ use pid::{InstantTimer, Timer}; use crate::engine::animations::alert::BlinkDurations; use crate::engine::{ - animations, operator, Animation, AnimationsStack, CenterFrame, Event, EventHandler, + animations, operator, Animation, AnimationsStack, CenterFrame, EventHandler, OperatorFrame, OrbType, QrScanSchema, QrScanUnexpectedReason, RingFrame, Runner, - RunningAnimation, SignupFailReason, BIOMETRIC_PIPELINE_MAX_PROGRESS, + RunningAnimation, RxEvent, SignupFailReason, BIOMETRIC_PIPELINE_MAX_PROGRESS, LED_ENGINE_FPS, LEVEL_BACKGROUND, LEVEL_FOREGROUND, LEVEL_NOTICE, PEARL_CENTER_LED_COUNT, PEARL_RING_LED_COUNT, }; +use crate::hal::HalMessage; use crate::sound; use crate::sound::Player; use orb_rgb::Argb; -struct WrappedMessage(Message); - -impl From> for WrappedMessage { +impl From> for HalMessage { fn from(value: CenterFrame) -> Self { - WrappedMessage(Message::JMessage( + HalMessage::Mcu(Message::JMessage( JetsonToMcu { ack_number: 0, payload: Some(jetson_to_mcu::Payload::CenterLedsSequence( @@ -47,9 +45,9 @@ impl From> for WrappedMessage { } } -impl From> for WrappedMessage { +impl From> for HalMessage { fn from(value: RingFrame) -> Self { - WrappedMessage(Message::JMessage( + HalMessage::Mcu(Message::JMessage( JetsonToMcu { ack_number: 0, payload: Some(jetson_to_mcu::Payload::RingLedsSequence( @@ -65,46 +63,9 @@ impl From> for WrappedMessage { } } -/// Dummy implementation, not used since Pearl cannot be connected to a cone -impl From> for WrappedMessage { - fn from(value: RingFrame<64>) -> Self { - WrappedMessage(Message::JMessage( - JetsonToMcu { - ack_number: 0, - payload: Some(jetson_to_mcu::Payload::ConeLedsSequence( - orb_messages::mcu_main::ConeLeDsSequence { - data_format: Some( - orb_messages::mcu_main::cone_le_ds_sequence::DataFormat::Argb32Uncompressed( - value.iter().flat_map(|&Argb(a, r, g, b)| [a.unwrap_or(0_u8), r, g, b]).collect(), - )) - } - )), - } - )) - } -} - -impl From for WrappedMessage { - fn from(value: OperatorFrame) -> Self { - WrappedMessage(Message::JMessage( - JetsonToMcu { - ack_number: 0, - payload: Some(jetson_to_mcu::Payload::DistributorLedsSequence( - orb_messages::mcu_main::DistributorLeDsSequence { - data_format: Some( - orb_messages::mcu_main::distributor_le_ds_sequence::DataFormat::RgbUncompressed( - value.iter().flat_map(|&Argb(_, r, g, b)| [r, g, b]).collect(), - )) - } - )), - } - )) - } -} - pub async fn event_loop( - rx: UnboundedReceiver, - mcu_tx: Sender, + rx: UnboundedReceiver, + mut hal_tx: mpsc::Sender, ) -> Result<()> { let mut interval = time::interval(Duration::from_millis(1000 / LED_ENGINE_FPS)); interval.set_missed_tick_behavior(time::MissedTickBehavior::Delay); @@ -122,12 +83,12 @@ pub async fn event_loop( } Either::Left((Some(event), _)) => { if let Err(e) = runner.event(&event) { - tracing::error!("Error handling event: {:?}", e); + tracing::warn!("Error handling event: {:?}", e); } } Either::Right(_) => { - if let Err(e) = runner.run(&mut mcu_tx.clone()).await { - tracing::error!("Error running UI: {:?}", e); + if let Err(e) = runner.run(&mut hal_tx).await { + tracing::warn!("Error running UI: {:?}", e); } } } @@ -142,6 +103,8 @@ impl Runner { ring_animations_stack: AnimationsStack::new(), center_animations_stack: AnimationsStack::new(), cone_animations_stack: None, + cone_display_queue_tx: None, + cone_display_queue_rx: None, ring_frame: [Argb(None, 0, 0, 0); PEARL_RING_LED_COUNT], center_frame: [Argb(None, 0, 0, 0); PEARL_CENTER_LED_COUNT], cone_frame: None, @@ -155,6 +118,7 @@ impl Runner { capture_sound: sound::capture::CaptureLoopSound::default(), is_api_mode: false, paused: false, + self_serve: false, } } @@ -186,10 +150,10 @@ impl Runner { #[async_trait] impl EventHandler for Runner { #[allow(clippy::too_many_lines)] - fn event(&mut self, event: &Event) -> Result<()> { + fn event(&mut self, event: &RxEvent) -> Result<()> { tracing::trace!("UI event: {}", serde_json::to_string(event)?.as_str()); match event { - Event::Bootup => { + RxEvent::Bootup => { self.stop_ring(LEVEL_NOTICE, true); self.stop_center(LEVEL_NOTICE, true); self.set_ring( @@ -198,14 +162,14 @@ impl EventHandler for Runner { ); self.operator_pulse.trigger(1., 1., false, false); } - Event::BootComplete { api_mode } => { + RxEvent::BootComplete { api_mode } => { self.sound .queue(sound::Type::Melody(sound::Melody::BootUp))?; self.operator_pulse.stop(); self.operator_idle.api_mode(*api_mode); self.is_api_mode = *api_mode; } - Event::Shutdown { requested } => { + RxEvent::Shutdown { requested } => { self.sound .queue(sound::Type::Melody(sound::Melody::PoweringDown))?; // overwrite any existing animation by setting notice-level animation @@ -230,7 +194,7 @@ impl EventHandler for Runner { self.operator_action .trigger(1.0, Argb::OFF, true, false, true); } - Event::SignupStart => { + RxEvent::SignupStart => { self.capture_sound.reset(); self.sound .queue(sound::Type::Melody(sound::Melody::StartSignup))?; @@ -261,7 +225,7 @@ impl EventHandler for Runner { animations::Static::::new(Argb::OFF, None), ); } - Event::QrScanStart { schema } => { + RxEvent::QrScanStart { schema } => { self.set_center( LEVEL_FOREGROUND, animations::Wave::::new( @@ -295,13 +259,13 @@ impl EventHandler for Runner { } }; } - Event::QrScanCapture => { + RxEvent::QrScanCapture => { // stop wave (foreground) & show alert/blinks (notice) self.stop_center(LEVEL_FOREGROUND, true); self.sound .queue(sound::Type::Melody(sound::Melody::QrCodeCapture))?; } - Event::QrScanCompleted { schema } => { + RxEvent::QrScanCompleted { schema } => { // stop wave (foreground) & show alert/blinks (notice) self.stop_center(LEVEL_FOREGROUND, true); self.set_center( @@ -320,7 +284,7 @@ impl EventHandler for Runner { QrScanSchema::Wifi => {} } } - Event::QrScanUnexpected { schema, reason } => { + RxEvent::QrScanUnexpected { schema, reason } => { match reason { QrScanUnexpectedReason::Invalid => { self.sound @@ -346,7 +310,7 @@ impl EventHandler for Runner { // stop wave self.stop_center(LEVEL_FOREGROUND, true); } - Event::QrScanFail { schema } => { + RxEvent::QrScanFail { schema } => { self.sound .queue(sound::Type::Melody(sound::Melody::SoundError))?; match schema { @@ -360,7 +324,7 @@ impl EventHandler for Runner { // stop wave self.stop_center(LEVEL_FOREGROUND, true); } - Event::QrScanSuccess { schema } => { + RxEvent::QrScanSuccess { schema } => { match schema { QrScanSchema::Operator => { self.sound @@ -399,7 +363,7 @@ impl EventHandler for Runner { } } } - Event::QrScanTimeout { schema } => { + RxEvent::QrScanTimeout { schema } => { self.sound .queue(sound::Type::Voice(sound::Voice::Timeout))?; match schema { @@ -413,7 +377,7 @@ impl EventHandler for Runner { // stop wave self.stop_center(LEVEL_FOREGROUND, true); } - Event::MagicQrActionCompleted { success } => { + RxEvent::MagicQrActionCompleted { success } => { let melody = if *success { sound::Melody::QrLoadSuccess } else { @@ -424,18 +388,18 @@ impl EventHandler for Runner { // to inform the operator to press the button. self.operator_signup_phase.failure(); } - Event::NetworkConnectionSuccess => { + RxEvent::NetworkConnectionSuccess => { self.sound.queue(sound::Type::Melody( sound::Melody::InternetConnectionSuccessful, ))?; } - Event::BiometricCaptureHalfObjectivesCompleted => { + RxEvent::BiometricCaptureHalfObjectivesCompleted => { // do nothing } - Event::BiometricCaptureAllObjectivesCompleted => { + RxEvent::BiometricCaptureAllObjectivesCompleted => { self.operator_signup_phase.irises_captured(); } - Event::BiometricCaptureProgress { progress } => { + RxEvent::BiometricCaptureProgress { progress } => { if self .ring_animations_stack .stack @@ -471,14 +435,14 @@ impl EventHandler for Runner { ring_progress.set_progress(*progress, true); } } - Event::BiometricCaptureOcclusion { occlusion_detected } => { + RxEvent::BiometricCaptureOcclusion { occlusion_detected } => { if *occlusion_detected { self.operator_signup_phase.capture_occlusion_issue(); } else { self.operator_signup_phase.capture_occlusion_ok(); } } - Event::BiometricCaptureDistance { in_range } => { + RxEvent::BiometricCaptureDistance { in_range } => { if *in_range { self.operator_signup_phase.capture_distance_ok(); if let Some(melody) = self.capture_sound.peekable().peek() { @@ -494,7 +458,7 @@ impl EventHandler for Runner { .try_queue(sound::Type::Voice(sound::Voice::Silence)); } } - Event::BiometricCaptureSuccess => { + RxEvent::BiometricCaptureSuccess => { self.sound .queue(sound::Type::Melody(sound::Melody::IrisScanSuccess))?; // set ring to full circle based on previous progress animation @@ -526,7 +490,7 @@ impl EventHandler for Runner { self.operator_signup_phase.iris_scan_complete(); } - Event::BiometricPipelineProgress { progress } => { + RxEvent::BiometricPipelineProgress { progress } => { let ring_animation = self .ring_animations_stack .stack @@ -551,7 +515,7 @@ impl EventHandler for Runner { self.operator_signup_phase.processing_2(); } } - Event::StartingEnrollment => { + RxEvent::StartingEnrollment => { let slider = self .ring_animations_stack .stack @@ -567,7 +531,7 @@ impl EventHandler for Runner { } self.operator_signup_phase.uploading(); } - Event::BiometricPipelineSuccess => { + RxEvent::BiometricPipelineSuccess => { let slider = self .ring_animations_stack .stack @@ -583,7 +547,7 @@ impl EventHandler for Runner { } self.operator_signup_phase.biometric_pipeline_successful(); } - Event::SignupFail { reason } => { + RxEvent::SignupFail { reason } => { self.sound .queue(sound::Type::Melody(sound::Melody::SoundError))?; match reason { @@ -646,7 +610,7 @@ impl EventHandler for Runner { ), ); } - Event::SignupSuccess => { + RxEvent::SignupSuccess => { self.sound .queue(sound::Type::Melody(sound::Melody::SignupSuccess))?; @@ -676,44 +640,44 @@ impl EventHandler for Runner { ), ); } - Event::Idle => { + RxEvent::Idle => { self.stop_ring(LEVEL_FOREGROUND, false); self.stop_ring(LEVEL_NOTICE, false); self.stop_center(LEVEL_FOREGROUND, false); self.stop_center(LEVEL_NOTICE, false); self.operator_signup_phase.idle(); } - Event::GoodInternet => { + RxEvent::GoodInternet => { self.operator_idle.good_internet(); } - Event::SlowInternet => { + RxEvent::SlowInternet => { self.operator_idle.slow_internet(); } - Event::NoInternet => { + RxEvent::NoInternet => { self.operator_idle.no_internet(); } - Event::GoodWlan => { + RxEvent::GoodWlan => { self.operator_idle.good_wlan(); } - Event::SlowWlan => { + RxEvent::SlowWlan => { self.operator_idle.slow_wlan(); } - Event::NoWlan => { + RxEvent::NoWlan => { self.operator_idle.no_wlan(); } - Event::BatteryCapacity { percentage } => { + RxEvent::BatteryCapacity { percentage } => { self.operator_idle.battery_capacity(*percentage); } - Event::BatteryIsCharging { is_charging } => { + RxEvent::BatteryIsCharging { is_charging } => { self.operator_idle.battery_charging(*is_charging); } - Event::Pause => { + RxEvent::Pause => { self.paused = true; } - Event::Resume => { + RxEvent::Resume => { self.paused = false; } - Event::RecoveryImage => { + RxEvent::RecoveryImage => { self.sound .queue(sound::Type::Voice(sound::Voice::PleaseDontShutDown))?; // check that ring is not already in recovery mode @@ -736,20 +700,20 @@ impl EventHandler for Runner { ); } } - Event::NoInternetForSignup => { + RxEvent::NoInternetForSignup => { self.sound.queue(sound::Type::Voice( sound::Voice::InternetConnectionTooSlowToPerformSignups, ))?; } - Event::SlowInternetForSignup => { + RxEvent::SlowInternetForSignup => { self.sound.queue(sound::Type::Voice( sound::Voice::InternetConnectionTooSlowSignupsMightTakeLonger, ))?; } - Event::SoundVolume { level } => { + RxEvent::SoundVolume { level } => { self.sound.set_master_volume(*level); } - Event::SoundLanguage { lang } => { + RxEvent::SoundLanguage { lang } => { let language = lang.clone(); let sound = self.sound.clone(); // spawn a new task because we need some async work here @@ -760,7 +724,7 @@ impl EventHandler for Runner { } }); } - Event::SoundTest => { + RxEvent::SoundTest => { self.sound .queue(sound::Type::Melody(sound::Melody::BootUp))?; } @@ -768,11 +732,11 @@ impl EventHandler for Runner { Ok(()) } - async fn run(&mut self, interface_tx: &mut mpsc::Sender) -> Result<()> { + async fn run(&mut self, hal_tx: &mut mpsc::Sender) -> Result<()> { let dt = self.timer.get_dt().unwrap_or(0.0); self.center_animations_stack.run(&mut self.center_frame, dt); if !self.paused { - interface_tx.try_send(WrappedMessage::from(self.center_frame).0)?; + hal_tx.try_send(HalMessage::from(self.center_frame))?; } self.operator_idle @@ -785,24 +749,23 @@ impl EventHandler for Runner { .animate(&mut self.operator_frame, dt, false); self.operator_action .animate(&mut self.operator_frame, dt, false); - // 2ms sleep to make sure UART communication is over - time::sleep(Duration::from_millis(2)).await; - interface_tx.try_send(WrappedMessage::from(self.operator_frame).0)?; + if !self.paused { + hal_tx.try_send(HalMessage::from(self.operator_frame))?; + } self.ring_animations_stack.run(&mut self.ring_frame, dt); if !self.paused { - time::sleep(Duration::from_millis(2)).await; - interface_tx.try_send(WrappedMessage::from(self.ring_frame).0)?; + hal_tx.try_send(HalMessage::from(self.ring_frame))?; } if let Some(animation) = &mut self.cone_animations_stack { if let Some(frame) = &mut self.cone_frame { - animation.run(frame, dt); + animation.run(&mut frame.0, dt); if !self.paused { - time::sleep(Duration::from_millis(2)).await; - interface_tx.try_send(WrappedMessage::from(*frame).0)?; + hal_tx.try_send(HalMessage::from(frame))?; } } } + // one last update of the UI has been performed since api_mode has been set, // (to set the api_mode UI state), so we can now pause the engine if self.is_api_mode && !self.paused { diff --git a/orb-ui/src/hal/mod.rs b/orb-ui/src/hal/mod.rs new file mode 100644 index 0000000..85f3dcc --- /dev/null +++ b/orb-ui/src/hal/mod.rs @@ -0,0 +1,194 @@ +use crate::dbus::OutboundInterfaceProxy; +use crate::engine::TxEvent; +use orb_cone::{Cone, ConeEvents}; +use orb_messages::mcu_main::mcu_message::{Message as MainMcuMessage, Message}; +use orb_rgb::Argb; +use tokio::sync::mpsc; +use tokio::task; +use tracing::{debug, info}; +use zbus::Connection; + +pub mod serial; + +const CONE_RECONNECT_INTERVAL_SECONDS: u64 = 10; + +#[allow(clippy::large_enum_variant)] +pub enum HalMessage { + Mcu(MainMcuMessage), + ConeLed([Argb; orb_cone::led::CONE_LED_COUNT]), + ConeLcdQrCode(String), + ConeLcdFillColor(Argb), + #[allow(dead_code)] + ConeLcdImage(String), +} + +pub const INPUT_CAPACITY: usize = 100; + +/// HAL - Hardware Abstraction Layer +pub struct Hal { + _thread_to_cone: task::JoinHandle<()>, + _thread_from_cone: task::JoinHandle>, +} + +impl Hal { + pub fn spawn( + hal_rx: mpsc::Receiver, + has_cone: bool, + ) -> eyre::Result { + let (cone_tx, mut cone_rx) = mpsc::unbounded_channel(); + let (serial_tx, serial_rx) = futures::channel::mpsc::channel(INPUT_CAPACITY); + serial::Serial::spawn(serial_rx)?; + + // send messages to mcu and cone + let to_hardware = task::spawn(async move { + handle_hal( + if has_cone { + Some(cone_tx.clone()) + } else { + None + }, + hal_rx, + serial_tx, + ) + .await + }); + + // handle messages from cone and relay them via dbus + let from_cone = task::spawn(async move { + if let Err(e) = handle_cone_events(&mut cone_rx).await { + tracing::error!("Error handling cone events: {:?}", e); + Err(e) + } else { + Ok(()) + } + }); + + Ok(Hal { + _thread_to_cone: to_hardware, + _thread_from_cone: from_cone, + }) + } +} + +/// Handle messages from the HAL and send them to the appropriate hardware +/// interface. +/// This function is responsible for managing the connection to the cone. +/// If the connection is lost, it will try to reconnect every 10 seconds. +async fn handle_hal( + cone_tx: Option>, + mut hal_rx: mpsc::Receiver, + mut serial_tx: futures::channel::mpsc::Sender, +) { + let mut cone = match &cone_tx { + Some(tx) => { + let cone = Cone::new(tx.clone()).map_err(|e| info!("{:}", e)).ok(); + if cone.is_some() { + info!("Cone connected"); + } else { + info!("Cone not connected"); + } + cone + } + None => None, + }; + let mut reconnect_cone_time = std::time::Instant::now(); + loop { + // try to create a cone if it doesn't exist every 10 seconds + if cone.is_none() + && reconnect_cone_time.elapsed().as_secs() > CONE_RECONNECT_INTERVAL_SECONDS + { + reconnect_cone_time = std::time::Instant::now(); + if let Some(tx) = &cone_tx { + cone = Cone::new(tx.clone()).map_err(|e| info!("{:}", e)).ok(); + if cone.is_some() { + info!("Cone connected"); + } + } + } else if cone.is_some() { + if let Some(c) = &mut cone { + if !c.is_connected() { + info!("Cone disconnected"); + drop(cone); + cone = None; + } + } + reconnect_cone_time = std::time::Instant::now(); + } + + match hal_rx.recv().await { + Some(HalMessage::Mcu(m)) => { + if let Err(e) = serial_tx.try_send(m) { + tracing::error!( + "Failed to send message to serial interface: {:?}", + e + ); + } + } + Some(HalMessage::ConeLed(leds)) => { + if let Some(cone) = &mut cone { + if let Err(s) = cone.queue_rgb_leds(&leds) { + tracing::error!("Failed to update LEDs: {:?}", s) + } + } + } + Some(HalMessage::ConeLcdImage(lcd)) => { + if let Some(cone) = &mut cone { + if let Err(e) = cone.queue_lcd_bmp(lcd) { + tracing::error!("Failed to update LCD (bmp image): {:?}", e) + } + } + } + Some(HalMessage::ConeLcdQrCode(data)) => { + if let Some(cone) = &mut cone { + if let Err(e) = cone.queue_lcd_qr_code(data) { + tracing::error!("Failed to update LCD (raw): {:?}", e) + } + } + } + Some(HalMessage::ConeLcdFillColor(color)) => { + if let Some(cone) = &mut cone { + if let Err(e) = cone.queue_lcd_fill(color) { + tracing::error!("Failed to update LCD (fill): {:?}", e) + } + } + } + None => { + info!("UI event channel closed, stopping cone interface"); + break; + } + } + } +} + +async fn handle_cone_events( + cone_rx: &mut mpsc::UnboundedReceiver, +) -> eyre::Result<()> { + let mut button_pressed = false; + let connection = Connection::session().await?; + let proxy = OutboundInterfaceProxy::new(&connection).await?; + + loop { + match cone_rx + .recv() + .await + .ok_or(eyre::eyre!("Cone event channel closed"))? + { + ConeEvents::ButtonPressed(state) => { + let tx_event = if state { + TxEvent::ConeButtonPressed + } else { + TxEvent::ConeButtonReleased + }; + if let Err(e) = + proxy.user_event(serde_json::to_string(&tx_event)?).await + { + tracing::warn!("Error: {:#?}", e); + } + if state != button_pressed { + debug!("🔘 Button {}", if state { "pressed" } else { "released" }); + } + button_pressed = state; + } + } + } +} diff --git a/orb-ui/src/serial.rs b/orb-ui/src/hal/serial.rs similarity index 86% rename from orb-ui/src/serial.rs rename to orb-ui/src/hal/serial.rs index 38f49b2..5b2e3ba 100644 --- a/orb-ui/src/serial.rs +++ b/orb-ui/src/hal/serial.rs @@ -1,6 +1,7 @@ //! Serial interface. use std::io::Write; +use std::time::Duration; use eyre::Result; use futures::{channel::mpsc, prelude::*}; @@ -10,6 +11,7 @@ use orb_uart::{BaudRate, Device}; use tokio::runtime; const SERIAL_DEVICE: &str = "/dev/ttyTHS0"; +const DELAY_BETWEEN_UART_MESSAGES_US: u64 = 200; pub struct Serial {} @@ -39,6 +41,11 @@ impl Serial { while let Some(message) = rt.block_on(input_rx.next()) { Self::write_message(&mut device, message) .expect("failed to transmit a message to MCU via UART"); + // mark a pause between messages to avoid flooding the MCU + // and ensure that messages are correctly received + std::thread::sleep(Duration::from_micros( + DELAY_BETWEEN_UART_MESSAGES_US, + )); } }) .expect("failed to spawn thread"); diff --git a/orb-ui/src/main.rs b/orb-ui/src/main.rs index 067a7f5..51112f4 100644 --- a/orb-ui/src/main.rs +++ b/orb-ui/src/main.rs @@ -6,8 +6,8 @@ use std::{env, fs}; use clap::Parser; use eyre::{Context, Result}; -use futures::channel::mpsc; use orb_build_info::{make_build_info, BuildInfo}; +use tokio::sync::mpsc; use tokio::time; use tracing::debug; use tracing_subscriber::layer::SubscriberExt; @@ -16,17 +16,15 @@ use tracing_subscriber::{filter::LevelFilter, fmt, EnvFilter}; use crate::engine::{Engine, EventChannel}; use crate::observer::listen; -use crate::serial::Serial; use crate::simulation::signup_simulation; mod dbus; mod engine; +mod hal; // hardware abstraction layer mod observer; -mod serial; mod simulation; pub mod sound; -const INPUT_CAPACITY: usize = 100; const BUILD_INFO: BuildInfo = make_build_info!(); /// Utility args @@ -94,35 +92,44 @@ async fn main() -> Result<()> { let args = Args::parse(); let hw = get_hw_version()?; - let (mut serial_input_tx, serial_input_rx) = mpsc::channel(INPUT_CAPACITY); - Serial::spawn(serial_input_rx)?; + tracing::info!("Orb: {}", hw); + let (mut hal_tx, hal_rx) = mpsc::channel(hal::INPUT_CAPACITY); match args.subcmd { SubCommand::Daemon => { if hw.contains("Diamond") { - let ui = engine::DiamondJetson::spawn(&mut serial_input_tx); + let ui = engine::DiamondJetson::spawn(&mut hal_tx); + let _interface = hal::Hal::spawn(hal_rx, true)?; let send_ui: &dyn EventChannel = &ui; listen(send_ui).await?; } else { - let ui = engine::PearlJetson::spawn(&mut serial_input_tx); + let ui = engine::PearlJetson::spawn(&mut hal_tx); + let _interface = hal::Hal::spawn(hal_rx, false)?; let send_ui: &dyn EventChannel = &ui; listen(send_ui).await?; }; } SubCommand::Simulation => { let ui: Box = if hw.contains("Diamond") { - Box::new(engine::DiamondJetson::spawn(&mut serial_input_tx)) + let engine = engine::DiamondJetson::spawn(&mut hal_tx); + let _interface = hal::Hal::spawn(hal_rx, true)?; + Box::new(engine) } else { - Box::new(engine::PearlJetson::spawn(&mut serial_input_tx)) + let engine = engine::PearlJetson::spawn(&mut hal_tx); + let _interface = hal::Hal::spawn(hal_rx, false)?; + Box::new(engine) }; signup_simulation(ui.as_ref()).await?; } SubCommand::Recovery => { let ui: Box = if hw.contains("Diamond") { - Box::new(engine::DiamondJetson::spawn(&mut serial_input_tx)) + let engine = engine::DiamondJetson::spawn(&mut hal_tx); + let _interface = hal::Hal::spawn(hal_rx, true)?; + Box::new(engine) } else { - Box::new(engine::PearlJetson::spawn(&mut serial_input_tx)) + let engine = engine::PearlJetson::spawn(&mut hal_tx); + let _interface = hal::Hal::spawn(hal_rx, false)?; + Box::new(engine) }; - loop { ui.recovery(); time::sleep(Duration::from_secs(45)).await; diff --git a/orb-ui/src/observer.rs b/orb-ui/src/observer.rs index c5f0454..0bd07f1 100644 --- a/orb-ui/src/observer.rs +++ b/orb-ui/src/observer.rs @@ -28,12 +28,12 @@ pub async fn listen(send_ui: &dyn EventChannel) -> Result<()> { // serve dbus interface // on session bus - let _iface_ref: zbus::InterfaceRef = { + let _iface_ref: zbus::InterfaceRef = { let conn = zbus::ConnectionBuilder::session() .wrap_err("failed to establish user session dbus connection")? .name("org.worldcoin.OrbUiState1") .wrap_err("failed to get name")? - .serve_at(IFACE_PATH, dbus::Interface::new(send_ui.clone_tx())) + .serve_at(IFACE_PATH, dbus::InboundInterface::new(send_ui.clone_tx())) .wrap_err("failed to serve at")? .build() .await