diff --git a/src/rust/bitbox-hal/src/ui.rs b/src/rust/bitbox-hal/src/ui.rs index 5fa0e43e7f..1c300b0ecd 100644 --- a/src/rust/bitbox-hal/src/ui.rs +++ b/src/rust/bitbox-hal/src/ui.rs @@ -72,6 +72,7 @@ pub trait Empty {} pub trait Ui { type Progress: Progress; type Empty: Empty; + type UnlockAnimation; /// Returns `Ok(())` if the user accepts, `Err(UserAbort)` if the user rejects. async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort>; @@ -88,7 +89,9 @@ pub trait Ui { longtouch: bool, ) -> Result<(), UserAbort>; - async fn unlock_animation(&mut self); + fn unlock_animation_create(&mut self) -> Self::UnlockAnimation; + + async fn unlock_animation_play(&mut self, animation: Self::UnlockAnimation); async fn status(&mut self, title: &str, status_success: bool); diff --git a/src/rust/bitbox-lvgl-sys/build.rs b/src/rust/bitbox-lvgl-sys/build.rs index 78253248d5..e7954cdb68 100644 --- a/src/rust/bitbox-lvgl-sys/build.rs +++ b/src/rust/bitbox-lvgl-sys/build.rs @@ -68,6 +68,8 @@ fn main() -> Result<(), &'static str> { println!("cargo::rerun-if-changed={}", lvgl_dir.display()); let target = env::var("TARGET").expect("TARGET not set"); + let target_os = env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); + let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); let mut cflags = vec![format!("-I{}", lvgl_dir.display())]; let mut cmake_build = cmake::Config::new(&lvgl_dir); @@ -160,6 +162,13 @@ fn main() -> Result<(), &'static str> { let dst = cmake_build.build(); println!("cargo::rustc-link-search=native={}/lib", dst.display()); println!("cargo::rustc-link-lib=static=lvgl"); + if !target.starts_with("thumb") { + match (target_os.as_str(), target_env.as_str()) { + ("macos", _) | ("ios", _) => println!("cargo::rustc-link-lib=dylib=c++"), + ("windows", "msvc") => {} + _ => println!("cargo::rustc-link-lib=dylib=stdc++"), + } + } let mut fonts = cc::Build::new(); fonts.file(manifest_dir.join("../../ui/fonts/inter_regular_32.c")); diff --git a/src/rust/bitbox-lvgl-sys/lv_conf.h b/src/rust/bitbox-lvgl-sys/lv_conf.h index 8e0ad70994..51ec990cfe 100644 --- a/src/rust/bitbox-lvgl-sys/lv_conf.h +++ b/src/rust/bitbox-lvgl-sys/lv_conf.h @@ -584,11 +584,11 @@ #define LV_ATTRIBUTE_EXTERN_DATA /** Use `float` as `lv_value_precise_t` */ -#define LV_USE_FLOAT 0 +#define LV_USE_FLOAT 1 /** Enable matrix support * - Requires `LV_USE_FLOAT = 1` */ -#define LV_USE_MATRIX 0 +#define LV_USE_MATRIX 1 /** Include `lvgl_private.h` in `lvgl.h` to access internal data and functions by default */ #ifndef LV_USE_PRIVATE_API @@ -773,7 +773,7 @@ #define LV_USE_LIST 1 -#define LV_USE_LOTTIE 0 /**< Requires: lv_canvas, thorvg */ +#define LV_USE_LOTTIE 1 /**< Requires: lv_canvas, thorvg */ #define LV_USE_MENU 1 @@ -996,11 +996,11 @@ /** Enable Vector Graphic APIs * Requires `LV_USE_MATRIX = 1` */ -#define LV_USE_VECTOR_GRAPHIC 0 +#define LV_USE_VECTOR_GRAPHIC 1 /** Enable ThorVG (vector graphics library) from the src/libs folder. * Requires LV_USE_VECTOR_GRAPHIC */ -#define LV_USE_THORVG_INTERNAL 0 +#define LV_USE_THORVG_INTERNAL 1 /** Enable ThorVG by assuming that its installed and linked to the project * Requires LV_USE_VECTOR_GRAPHIC */ diff --git a/src/rust/bitbox-lvgl/src/lib.rs b/src/rust/bitbox-lvgl/src/lib.rs index 2261000641..1d1f92787b 100644 --- a/src/rust/bitbox-lvgl/src/lib.rs +++ b/src/rust/bitbox-lvgl/src/lib.rs @@ -78,6 +78,7 @@ pub use widgets::class; pub use widgets::image::ImageExt; pub use widgets::keyboard::{KeyboardExt, LvKeyboard, LvKeyboardMapEntry, keyboard_def_event_cb}; pub use widgets::label::{LabelExt, LvLabel, LvLabelTextError}; +pub use widgets::lottie::{LottieExt, LvLottie, LvLottieCreateError}; pub use widgets::obj; pub use widgets::obj::LvObj; pub use widgets::obj::{LvHandle, LvTypeError, ObjExt}; diff --git a/src/rust/bitbox-lvgl/src/widgets/class.rs b/src/rust/bitbox-lvgl/src/widgets/class.rs index 4d05ba81bc..22efabe462 100644 --- a/src/rust/bitbox-lvgl/src/widgets/class.rs +++ b/src/rust/bitbox-lvgl/src/widgets/class.rs @@ -19,7 +19,6 @@ pub struct ImageTag; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct CanvasTag; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ArcTag; #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/src/rust/bitbox-lvgl/src/widgets/lottie.rs b/src/rust/bitbox-lvgl/src/widgets/lottie.rs new file mode 100644 index 0000000000..c5388f53d4 --- /dev/null +++ b/src/rust/bitbox-lvgl/src/widgets/lottie.rs @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 + +use alloc::boxed::Box; +use alloc::rc::Rc; +use alloc::vec; +use core::cell::RefCell; +use core::ffi::c_void; +use core::ptr::NonNull; + +use super::canvas::{CanvasExt, LvCanvas}; +use super::image::ImageExt; +use super::obj::ObjExt; +use crate::{LvHandle, LvObj, class, ffi}; + +#[derive(Debug, PartialEq, Eq)] +pub struct LvLottie { + inner: LvHandle, +} + +type AnimationCallback = RefCell>; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LvLottieCreateError { + InvalidDimensions, + CreateFailed, + EventRegistrationFailed, +} + +unsafe extern "C" fn animation_completed_trampoline(anim: *mut ffi::lv_anim_t) { + let user_data = unsafe { ffi::lv_anim_get_user_data(anim) }; + if user_data.is_null() { + return; + } + + let callback_ptr = user_data.cast::().cast_const(); + unsafe { + Rc::increment_strong_count(callback_ptr); + } + let callback = unsafe { Rc::from_raw(callback_ptr) }; + let mut callback = callback.borrow_mut(); + callback.as_mut()(); +} + +unsafe extern "C" fn animation_deleted_trampoline(anim: *mut ffi::lv_anim_t) { + let user_data = unsafe { ffi::lv_anim_get_user_data(anim) }; + if user_data.is_null() { + return; + } + + drop(unsafe { Rc::from_raw(user_data.cast::().cast_const()) }); +} + +pub trait LottieExt: CanvasExt { + fn set_src_data(&self, src: &'static [u8]) { + unsafe { + ffi::lv_lottie_set_src_data(self.as_ptr(), src.as_ptr().cast::(), src.len()) + } + } + + fn pause(&self) { + unsafe { ffi::lv_anim_pause(self.animation()) } + } + + fn resume(&self) { + unsafe { ffi::lv_anim_resume(self.animation()) } + } + + fn set_repeat_count(&self, repeat_count: u32) { + unsafe { ffi::lv_anim_set_repeat_count(self.animation(), repeat_count) } + } + + fn set_completed_cb(&self, cb: F) + where + F: FnMut() + 'static, + { + let callback: Rc = Rc::new(RefCell::new(Box::new(cb))); + let user_data = Rc::into_raw(callback).cast_mut().cast::(); + unsafe { + ffi::lv_anim_set_user_data(self.animation(), user_data); + ffi::lv_anim_set_completed_cb(self.animation(), Some(animation_completed_trampoline)); + ffi::lv_anim_set_deleted_cb(self.animation(), Some(animation_deleted_trampoline)); + } + } + + fn animation(&self) -> *mut ffi::lv_anim_t { + unsafe { ffi::lv_lottie_get_anim(self.as_ptr()) } + } +} + +impl LvLottie { + pub fn new( + parent: &LvHandle

, + width: u32, + height: u32, + ) -> Result { + let Ok(width_i32) = i32::try_from(width) else { + return Err(LvLottieCreateError::InvalidDimensions); + }; + let Ok(height_i32) = i32::try_from(height) else { + return Err(LvLottieCreateError::InvalidDimensions); + }; + let Some(pixel_count) = width.checked_mul(height) else { + return Err(LvLottieCreateError::InvalidDimensions); + }; + let Ok(pixel_count) = usize::try_from(pixel_count) else { + return Err(LvLottieCreateError::InvalidDimensions); + }; + + let Some(lottie) = NonNull::new(unsafe { ffi::lv_lottie_create(parent.as_ptr()) }) else { + return Err(LvLottieCreateError::CreateFailed); + }; + let lottie = LvLottie { + inner: LvHandle::from_ptr(lottie), + }; + + let attachment = super::util::attach_to_object(&lottie.inner, vec![0u32; pixel_count]) + .map_err(|_| LvLottieCreateError::EventRegistrationFailed)?; + + unsafe { + ffi::lv_lottie_set_buffer( + lottie.as_ptr(), + width_i32, + height_i32, + (*attachment.as_ptr()).as_mut_ptr().cast::(), + ) + }; + + Ok(lottie) + } + + pub fn to_canvas(self) -> LvCanvas { + self.inner + } + + pub fn to_obj(self) -> LvObj { + self.inner.cast() + } +} + +impl ObjExt for LvLottie { + fn as_ptr(&self) -> *mut ffi::lv_obj_t { + self.inner.as_ptr() + } +} + +impl ImageExt for LvLottie {} +impl CanvasExt for LvLottie {} +impl LottieExt for LvLottie {} diff --git a/src/rust/bitbox-lvgl/src/widgets/mod.rs b/src/rust/bitbox-lvgl/src/widgets/mod.rs index a37cd6cda7..15c2c687e6 100644 --- a/src/rust/bitbox-lvgl/src/widgets/mod.rs +++ b/src/rust/bitbox-lvgl/src/widgets/mod.rs @@ -9,6 +9,7 @@ pub mod class; pub mod image; pub mod keyboard; pub mod label; +pub mod lottie; pub mod obj; pub mod slider; pub mod span; diff --git a/src/rust/bitbox-platform-host/Cargo.toml b/src/rust/bitbox-platform-host/Cargo.toml index 56b48eab14..991e52dda1 100644 --- a/src/rust/bitbox-platform-host/Cargo.toml +++ b/src/rust/bitbox-platform-host/Cargo.toml @@ -15,6 +15,8 @@ zeroize = { workspace = true } [features] app-u2f = ["bitbox-hal/app-u2f"] +simulator-graphical = [] +testing = [] [dev-dependencies] async_test = { path = "../async_test" } diff --git a/src/rust/bitbox-platform-host/src/securechip.rs b/src/rust/bitbox-platform-host/src/securechip.rs index a785d25663..161558f9ac 100644 --- a/src/rust/bitbox-platform-host/src/securechip.rs +++ b/src/rust/bitbox-platform-host/src/securechip.rs @@ -3,6 +3,9 @@ use alloc::collections::VecDeque; use alloc::{boxed::Box, vec::Vec}; +#[cfg(feature = "simulator-graphical")] +use bitbox_hal::Timer; + use bitcoin::hashes::Hash; use hex_lit::hex; @@ -128,6 +131,12 @@ impl bitbox_hal::SecureChip for FakeSecureChip { )); engine.input(msg); let hmac_result: Hmac = Hmac::from_engine(engine); + + // Keep KDF completion visibly delayed on the host so unlock animation start/play can be + // manually tested in the graphical simulators. + #[cfg(all(feature = "simulator-graphical", not(feature = "testing")))] + crate::timer::HostTimer::delay_for(core::time::Duration::from_millis(1000)).await; + Ok(Box::new(zeroize::Zeroizing::new( hmac_result.to_byte_array(), ))) diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml index 81208f716d..757c54fb32 100644 --- a/src/rust/bitbox02-rust/Cargo.toml +++ b/src/rust/bitbox02-rust/Cargo.toml @@ -100,6 +100,7 @@ app-cardano = [ testing = [ "dep:bitbox-platform-host", + "bitbox-platform-host?/testing", "bitbox02/testing", "bitbox-secp256k1/testing", "util/testing" diff --git a/src/rust/bitbox02-rust/src/hal/testing/ui.rs b/src/rust/bitbox02-rust/src/hal/testing/ui.rs index 8bd46613d0..36e963552d 100644 --- a/src/rust/bitbox02-rust/src/hal/testing/ui.rs +++ b/src/rust/bitbox02-rust/src/hal/testing/ui.rs @@ -48,6 +48,8 @@ pub enum Screen { choices: Vec, selected: u8, }, + UnlockAnimationPaused, + UnlockAnimationPlayed, More, } @@ -78,9 +80,12 @@ pub struct NoopEmpty; impl Empty for NoopEmpty {} +pub struct NoopUnlockAnimation; + impl Ui for TestingUi<'_> { type Progress = NoopProgress; type Empty = NoopEmpty; + type UnlockAnimation = NoopUnlockAnimation; fn progress_create(&mut self, _title: &str) -> Self::Progress { NoopProgress @@ -90,6 +95,11 @@ impl Ui for TestingUi<'_> { NoopEmpty } + fn unlock_animation_create(&mut self) -> Self::UnlockAnimation { + self.screens.push(Screen::UnlockAnimationPaused); + NoopUnlockAnimation + } + async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> { self.confirm_display_sizes.push(params.display_size); self.screens.push(Screen::Confirm { @@ -159,7 +169,9 @@ impl Ui for TestingUi<'_> { Ok(()) } - async fn unlock_animation(&mut self) {} + async fn unlock_animation_play(&mut self, _animation: Self::UnlockAnimation) { + self.screens.push(Screen::UnlockAnimationPlayed); + } async fn status(&mut self, title: &str, status_success: bool) { self.screens.push(Screen::Status { @@ -513,4 +525,15 @@ mod tests { Err(UserAbort) )); } + + #[async_test::test] + async fn test_unlock_animation_records_screen() { + let mut ui = TestingUi::new(); + let animation = ui.unlock_animation_create(); + ui.unlock_animation_play(animation).await; + assert_eq!( + ui.screens, + vec![Screen::UnlockAnimationPaused, Screen::UnlockAnimationPlayed] + ); + } } diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs index 9476628711..17a41b6d68 100644 --- a/src/rust/bitbox02-rust/src/hww.rs +++ b/src/rust/bitbox02-rust/src/hww.rs @@ -309,10 +309,14 @@ mod tests { .unwrap(); assert_eq!( mock_hal.ui.screens, - vec![Screen::Status { - title: "Success".into(), - success: true, - }] + vec![ + Screen::Status { + title: "Success".into(), + success: true, + }, + Screen::UnlockAnimationPaused, + Screen::UnlockAnimationPlayed, + ] ); assert!(!crate::keystore::is_locked()); @@ -500,10 +504,14 @@ mod tests { .unwrap(); assert_eq!( mock_hal.ui.screens, - vec![Screen::Status { - title: "Success".into(), - success: true, - }] + vec![ + Screen::Status { + title: "Success".into(), + success: true, + }, + Screen::UnlockAnimationPaused, + Screen::UnlockAnimationPlayed, + ] ); mock_hal.ui = crate::hal::testing::TestingUi::new(); @@ -703,7 +711,9 @@ mod tests { Screen::Status { title: "Success".into(), success: true, - } + }, + Screen::UnlockAnimationPaused, + Screen::UnlockAnimationPlayed, ] ); diff --git a/src/rust/bitbox02-rust/src/hww/api/change_password.rs b/src/rust/bitbox02-rust/src/hww/api/change_password.rs index e331527b98..cb9ed3d9ef 100644 --- a/src/rust/bitbox02-rust/src/hww/api/change_password.rs +++ b/src/rust/bitbox02-rust/src/hww/api/change_password.rs @@ -59,8 +59,10 @@ mod tests { keystore::encrypt_and_store_seed(&mut hal, &seed, old_password) .await .unwrap(); - unlock::unlock_bip39(&mut hal, &seed).await; + let unlock_animation = hal.ui.unlock_animation_create(); + unlock::unlock_bip39(&mut hal, &seed, unlock_animation).await; hal.memory.set_initialized().unwrap(); + hal.ui.screens.clear(); // Allow exactly 3 prompts hal.ui.set_enter_string(Box::new(|params| { @@ -141,9 +143,11 @@ mod tests { keystore::encrypt_and_store_seed(&mut hal, &seed, correct_password) .await .unwrap(); - unlock::unlock_bip39(&mut hal, &seed).await; + let unlock_animation = hal.ui.unlock_animation_create(); + unlock::unlock_bip39(&mut hal, &seed, unlock_animation).await; hal.memory.set_initialized().unwrap(); keystore::lock(); + hal.ui.screens.clear(); hal.ui.set_enter_string(Box::new(|params| { prompt_counter += 1; @@ -200,9 +204,11 @@ mod tests { keystore::encrypt_and_store_seed(&mut hal, &seed, old_password) .await .unwrap(); - unlock::unlock_bip39(&mut hal, &seed).await; + let unlock_animation = hal.ui.unlock_animation_create(); + unlock::unlock_bip39(&mut hal, &seed, unlock_animation).await; hal.memory.set_initialized().unwrap(); keystore::lock(); + hal.ui.screens.clear(); hal.ui.set_enter_string(Box::new(|params| { prompt_counter += 1; diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs index c7d35b6fd7..46b4bbced9 100644 --- a/src/rust/bitbox02-rust/src/hww/api/restore.rs +++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs @@ -56,8 +56,10 @@ pub async fn from_file( } let password = password::enter_twice(hal).await?; + let unlock_animation = hal.ui().unlock_animation_create(); let seed = data.get_seed(); if let Err(err) = crate::keystore::encrypt_and_store_seed(hal, seed, &password).await { + drop(unlock_animation); hal.ui() .status(&format!("Could not\nrestore backup\n{:?}", err), false) .await; @@ -79,7 +81,7 @@ pub async fn from_file( // Ignore non-critical error. let _ = hal.memory().set_device_name(&metadata.name); - unlock::unlock_bip39(hal, seed).await; + unlock::unlock_bip39(hal, seed, unlock_animation).await; Ok(Response::Success(pb::Success {})) } @@ -132,8 +134,10 @@ pub async fn from_mnemonic( Ok(password) => break password, } }; + let unlock_animation = hal.ui().unlock_animation_create(); if let Err(err) = crate::keystore::encrypt_and_store_seed(hal, &seed, &password).await { + drop(unlock_animation); hal.ui() .status(&format!("Could not\nrestore backup\n{:?}", err), false) .await; @@ -149,7 +153,7 @@ pub async fn from_mnemonic( hal.memory().set_initialized().or(Err(Error::Memory))?; - unlock::unlock_bip39(hal, &seed).await; + unlock::unlock_bip39(hal, &seed, unlock_animation).await; Ok(Response::Success(pb::Success {})) } diff --git a/src/rust/bitbox02-rust/src/hww/api/set_password.rs b/src/rust/bitbox02-rust/src/hww/api/set_password.rs index 614646f500..d7bc24c7ba 100644 --- a/src/rust/bitbox02-rust/src/hww/api/set_password.rs +++ b/src/rust/bitbox02-rust/src/hww/api/set_password.rs @@ -24,12 +24,14 @@ pub async fn process( return Err(Error::InvalidInput); } let password = password::enter_twice(hal).await?; + let unlock_animation = hal.ui().unlock_animation_create(); if let Err(err) = keystore::create_and_store_seed(hal, &password, entropy).await { + drop(unlock_animation); hal.ui().status(&format!("Error\n{:?}", err), false).await; return Err(Error::Generic); } let seed = keystore::copy_seed(hal).await?; - unlock::unlock_bip39(hal, &seed).await; + unlock::unlock_bip39(hal, &seed, unlock_animation).await; Ok(Response::Success(pb::Success {})) } diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs index 498caf5d74..638250e6a9 100644 --- a/src/rust/bitbox02-rust/src/workflow/unlock.rs +++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs @@ -40,6 +40,7 @@ async fn confirm_mnemonic_passphrase( hal.ui().confirm(¶ms).await } +#[derive(Debug)] pub enum UnlockError { UserAbort, IncorrectPassword, @@ -131,7 +132,15 @@ pub async fn unlock_keystore( /// Performs the BIP39 keystore unlock, including unlock animation. If the optional passphrase /// feature is enabled, the user will be asked for the passphrase. -pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal, seed: &[u8]) { +/// +/// `unlock_animation` must already be on the screen stack. It stays paused on the first frame +/// while the workflow transitions from password entry to the BIP39 unlock, then starts playing +/// immediately before the BIP39 unlock work begins. +pub async fn unlock_bip39( + hal: &mut H, + seed: &[u8], + unlock_animation: ::UnlockAnimation, +) { // Empty passphrase by default. let mut mnemonic_passphrase = zeroize::Zeroizing::new("".into()); @@ -169,7 +178,7 @@ pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal, seed: &[u8]) { crate::keystore::KeystoreHalImpl::new(eeprom, memory, random, securechip); let ((), result) = futures_lite::future::zip( - ui.unlock_animation(), + ui.unlock_animation_play(unlock_animation), crate::keystore::unlock_bip39( &mut keystore_hal, seed, @@ -207,10 +216,12 @@ pub async fn unlock(hal: &mut impl crate::hal::Hal) -> Result<(), ()> { return Ok(()); } + let unlock_animation = hal.ui().unlock_animation_create(); + // Loop unlock until the password is correct or the device resets. loop { if let Ok(seed) = unlock_keystore(hal, "Enter password", CanCancel::No).await { - unlock_bip39(hal, &seed).await; + unlock_bip39(hal, &seed, unlock_animation).await; return Ok(()); } } @@ -256,6 +267,10 @@ mod tests { assert_eq!(mock_hal.securechip.get_event_counter(), 6); assert!(!crate::keystore::is_locked()); + assert_eq!( + mock_hal.ui.screens, + vec![Screen::UnlockAnimationPaused, Screen::UnlockAnimationPlayed,] + ); assert_eq!( crate::keystore::copy_bip39_seed(&mut mock_hal) @@ -271,6 +286,56 @@ mod tests { assert!(password_entered); } + #[async_test::test] + async fn test_unlock_retry_uses_same_unlock_animation_handle() { + let mut password_entries = 0; + let mut mock_hal = TestingHal::new(); + + crate::keystore::encrypt_and_store_seed( + &mut mock_hal, + &hex!("c7940c13479b8d9a6498f4e50d5a42e0d617bc8e8ac9f2b8cecf97e94c2b035c"), + "password", + ) + .await + .unwrap(); + mock_hal.memory.set_initialized().unwrap(); + crate::keystore::lock(); + + mock_hal.ui.set_enter_string(Box::new(|_params| { + password_entries += 1; + match password_entries { + 1 => Ok("wrong password".into()), + 2 => Ok("password".into()), + _ => panic!("too many user inputs"), + } + })); + + assert_eq!(unlock(&mut mock_hal).await, Ok(())); + + assert_eq!( + mock_hal.ui.screens, + vec![ + Screen::UnlockAnimationPaused, + Screen::Status { + title: "Wrong password".into(), + success: false, + }, + Screen::Confirm { + title: "WARNING".into(), + body: format!( + "You have {}\npassword attempts\nleft.", + crate::keystore::MAX_UNLOCK_ATTEMPTS - 1 + ), + longtouch: false, + }, + Screen::UnlockAnimationPlayed, + ], + ); + + drop(mock_hal); // to remove mutable borrow of `password_entries` + assert_eq!(password_entries, 2); + } + #[async_test::test] async fn test_unlock_keystore_wrong_password() { let mut password_entered = false; diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs index 6565fd207a..67998c8494 100644 --- a/src/rust/bitbox02-sys/build.rs +++ b/src/rust/bitbox02-sys/build.rs @@ -165,6 +165,7 @@ const ALLOWLIST_FNS: &[&str] = &[ "trinary_choice_create", "trinary_input_string_create", "trinary_input_string_set_input", + "unlock_animation_play", "u2f_packet_init", "u2f_packet_process", "u2f_packet_timeout_get", diff --git a/src/rust/bitbox02/src/hal/ui.rs b/src/rust/bitbox02/src/hal/ui.rs index eaf66808e2..962ecfc01e 100644 --- a/src/rust/bitbox02/src/hal/ui.rs +++ b/src/rust/bitbox02/src/hal/ui.rs @@ -92,6 +92,7 @@ impl Default for BitBox02Ui { impl Ui for BitBox02Ui { type Progress = BitBox02Progress; type Empty = BitBox02Empty; + type UnlockAnimation = crate::ui::UnlockAnimation; fn progress_create(&mut self, title: &str) -> Self::Progress { let mut component = crate::ui::progress_create(title); @@ -107,6 +108,10 @@ impl Ui for BitBox02Ui { } } + fn unlock_animation_create(&mut self) -> Self::UnlockAnimation { + crate::ui::unlock_animation_create() + } + #[inline(always)] async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> { let params = to_bitbox02_confirm_params(params); @@ -146,8 +151,8 @@ impl Ui for BitBox02Ui { } #[inline(always)] - async fn unlock_animation(&mut self) { - crate::ui::unlock_animation().await + async fn unlock_animation_play(&mut self, animation: Self::UnlockAnimation) { + animation.play().await } #[inline(always)] diff --git a/src/rust/bitbox02/src/ui/ui.rs b/src/rust/bitbox02/src/ui/ui.rs index a7feea63d1..32904ca22a 100644 --- a/src/rust/bitbox02/src/ui/ui.rs +++ b/src/rust/bitbox02/src/ui/ui.rs @@ -44,6 +44,41 @@ impl Drop for Component { } } +struct UnlockAnimationSharedState { + waker: Option, + result: Option<()>, +} + +pub struct UnlockAnimation { + component: Component, + shared_state: Box>, +} + +impl UnlockAnimation { + pub async fn play(self) { + let _no_screensaver = crate::screen_saver::ScreensaverInhibitor::new(); + unsafe { + bitbox02_sys::unlock_animation_play(self.component.component); + } + + let UnlockAnimation { + component: _component, + shared_state, + } = self; + core::future::poll_fn(move |cx| { + let mut shared_state = shared_state.borrow_mut(); + + if let Some(result) = shared_state.result.take() { + Poll::Ready(result) + } else { + shared_state.waker = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await + } +} + pub async fn trinary_input_string( params: &TrinaryInputStringParams<'_>, can_cancel: bool, @@ -757,22 +792,16 @@ pub fn empty_create() -> Component { } } -pub async fn unlock_animation() { - let _no_screensaver = crate::screen_saver::ScreensaverInhibitor::new(); - - // Shared between the async context and the c callback - struct SharedState { - waker: Option, - result: Option<()>, - } - let shared_state = Box::new(RefCell::new(SharedState { +pub fn unlock_animation_create() -> UnlockAnimation { + let shared_state = Box::new(RefCell::new(UnlockAnimationSharedState { waker: None, result: None, })); - let shared_state_ptr = shared_state.as_ref() as *const RefCell as *mut c_void; + let shared_state_ptr = + shared_state.as_ref() as *const RefCell as *mut c_void; unsafe extern "C" fn callback(user_data: *mut c_void) { - let shared_state = unsafe { &*(user_data as *mut RefCell) }; + let shared_state = unsafe { &*(user_data as *mut RefCell) }; let mut shared_state = shared_state.borrow_mut(); if shared_state.result.is_none() { shared_state.result = Some(()); @@ -795,21 +824,10 @@ pub async fn unlock_animation() { }; component.screen_stack_push(); - core::future::poll_fn({ - let shared_state = &shared_state; - move |cx| { - let mut shared_state = shared_state.borrow_mut(); - - if let Some(result) = shared_state.result.take() { - Poll::Ready(result) - } else { - // Store the waker so the callback can wake up this task - shared_state.waker = Some(cx.waker().clone()); - Poll::Pending - } - } - }) - .await + UnlockAnimation { + component, + shared_state, + } } pub async fn choose_orientation() -> bool { diff --git a/src/rust/bitbox02/src/ui/ui_stub.rs b/src/rust/bitbox02/src/ui/ui_stub.rs index f5ef5e91a4..c10d4ac726 100644 --- a/src/rust/bitbox02/src/ui/ui_stub.rs +++ b/src/rust/bitbox02/src/ui/ui_stub.rs @@ -99,7 +99,21 @@ pub fn empty_create() -> Component { Component { is_pushed: false } } -pub async fn unlock_animation() {} +pub struct UnlockAnimation { + _component: Component, +} + +pub fn unlock_animation_create() -> UnlockAnimation { + let mut component = Component { is_pushed: false }; + component.screen_stack_push(); + UnlockAnimation { + _component: component, + } +} + +impl UnlockAnimation { + pub async fn play(self) {} +} pub async fn choose_orientation() -> bool { false diff --git a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs index b6c3746545..e53ff8ebad 100644 --- a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs +++ b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs @@ -122,7 +122,21 @@ pub fn empty_create() -> Component { Component { is_pushed: false } } -pub async fn unlock_animation() {} +pub struct UnlockAnimation { + _component: Component, +} + +pub fn unlock_animation_create() -> UnlockAnimation { + let mut component = Component { is_pushed: false }; + component.screen_stack_push(); + UnlockAnimation { + _component: component, + } +} + +impl UnlockAnimation { + pub async fn play(self) {} +} pub async fn choose_orientation() -> bool { false diff --git a/src/rust/bitbox03/Unlock.json b/src/rust/bitbox03/Unlock.json new file mode 100644 index 0000000000..a4c2b62810 --- /dev/null +++ b/src/rust/bitbox03/Unlock.json @@ -0,0 +1 @@ +{"nm":"Locked","ddd":0,"h":200,"w":200,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":3,"nm":"Null 1","sr":1,"st":0,"op":900,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[34,34,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[99,149,0],"ix":2},"r":{"a":1,"k":[{"o":{"x":1,"y":0},"i":{"x":0.833,"y":1},"s":[0],"t":0},{"o":{"x":1,"y":0},"i":{"x":0.664,"y":0.825},"s":[-10.402],"t":3},{"o":{"x":0.341,"y":0.229},"i":{"x":0,"y":1},"s":[0],"t":6},{"o":{"x":1,"y":0},"i":{"x":0.695,"y":0.873},"s":[8],"t":9},{"o":{"x":1,"y":0.779},"i":{"x":0.253,"y":1},"s":[0],"t":12},{"o":{"x":1,"y":0},"i":{"x":0.667,"y":1},"s":[-4],"t":15},{"o":{"x":0.167,"y":0},"i":{"x":0.65,"y":1.342},"s":[0],"t":18},{"o":{"x":0.341,"y":-0.229},"i":{"x":0.833,"y":1},"s":[0],"t":45},{"o":{"x":1,"y":0},"i":{"x":0.664,"y":0.773},"s":[-8],"t":47},{"o":{"x":0.341,"y":0.229},"i":{"x":0,"y":1},"s":[0],"t":49},{"o":{"x":0.167,"y":0},"i":{"x":0.833,"y":1},"s":[8],"t":51},{"s":[0],"t":53}],"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":0,"ix":11}},"ef":[],"ind":1},{"ty":4,"nm":"shackle","sr":1,"st":0,"op":900,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100.167,89.562,0],"ix":1},"s":{"a":0,"k":[294.118,294.118,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0,"y":1},"s":[3.432,-174.816,0],"t":48,"ti":[0,5.451,0],"to":[0,-5.451,0]},{"s":[3.432,-207.522,0],"t":69}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 2","ix":1,"cix":2,"np":2,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.833,"y":1},"s":[{"c":false,"i":[[0,0],[0,0],[-14.045,0],[0,-14.045],[0,0]],"o":[[0,0],[0,-14.045],[14.044,0],[0,0],[0,0]],"v":[[-25.43,42.438],[-25.43,-17.008],[0,-42.438],[25.43,-17.008],[25.36,-0.563]]}],"t":48},{"s":[{"c":false,"i":[[0,0],[0,0],[-14.045,0],[0,-14.045],[0,0]],"o":[[0,0],[0,-14.045],[14.044,0],[0,0],[0,0]],"v":[[-25.43,42.438],[-25.43,-17.008],[0,-42.438],[25.43,-17.008],[25.47,-11.063]]}],"t":59}],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Stroke 1","lc":1,"lj":2,"ml":1,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":14,"ix":5},"c":{"a":0,"k":[1,1,1],"ix":3}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[100.167,89.562],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2,"parent":1},{"ty":4,"nm":"body","sr":1,"st":0,"op":900,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,117.217,0],"ix":1},"s":{"a":0,"k":[294.118,294.118,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[2.942,-93.479,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Group 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,-4.226],[0,0],[-3.123,0],[0,0],[0,4.204],[0,0],[3.123,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[-3.123,0],[0,0],[0,4.204],[0,0],[3.123,0],[0,0],[0.021,-4.226],[0,0],[0,0]],"v":[[18.941,-32.783],[0.951,-32.783],[-0.953,-32.783],[-18.942,-32.783],[-32.31,-32.783],[-34.343,-32.783],[-40.01,-25.167],[-40.01,25.144],[-34.343,32.783],[34.32,32.783],[39.989,25.144],[39.989,-25.167],[34.341,-32.783],[32.31,-32.783]]},"ix":2}},{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 2","ix":2,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[1.605,-1.672],[0,0],[1.668,0],[0,0],[0,1.74],[0,0],[0,2.554],[-4.491,0.181],[-0.47,-0.023],[0,-4.814]],"o":[[0,0],[0,1.74],[0,0],[-1.647,0],[0,0],[-1.604,-1.672],[0,-4.814],[0.471,-0.023],[4.492,0.181],[0,2.532]],"v":[[6.428,-0.057],[6.428,15.154],[3.391,18.363],[-3.412,18.363],[-6.45,15.154],[-6.45,-0.057],[-8.974,-6.566],[-0.953,-15.719],[0.951,-15.719],[8.973,-6.566]]},"ix":2}},{"ty":"mm","bm":0,"hd":false,"mn":"ADBE Vector Filter - Merge","nm":"Merge Paths 1","mm":1},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[100.011,117.217],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3,"parent":1}],"v":"5.5.8","fr":60,"op":181,"ip":0,"assets":[]} \ No newline at end of file diff --git a/src/rust/bitbox03/src/ui.rs b/src/rust/bitbox03/src/ui.rs index 366bff8519..cd29e3c5e1 100644 --- a/src/rust/bitbox03/src/ui.rs +++ b/src/rust/bitbox03/src/ui.rs @@ -14,6 +14,7 @@ use util::futures::completion; mod confirm; mod enter_string; mod status; +mod unlock_animation; const LOGO: &[u8] = include_bytes!("../splash.png"); @@ -49,6 +50,7 @@ impl hal::ui::Ui for BitBox03Ui { type Progress = BitBox03UiProgress; type Empty = BitBox03UiEmpty; + type UnlockAnimation = unlock_animation::UnlockAnimation; async fn confirm( &mut self, @@ -90,8 +92,8 @@ impl hal::ui::Ui for BitBox03Ui { todo!() } - async fn unlock_animation(&mut self) { - self.status("TODO\nunlock_animation", true).await + async fn unlock_animation_play(&mut self, animation: Self::UnlockAnimation) { + animation.play(self).await } async fn status(&mut self, title: &str, status_success: bool) { @@ -122,6 +124,12 @@ impl hal::ui::Ui for BitBox03Ui { todo!() } + fn unlock_animation_create(&mut self) -> Self::UnlockAnimation { + let animation = unlock_animation::build_unlock_animation(); + self.push(animation.screen); + animation.handle + } + async fn enter_string( &mut self, params: &bitbox_hal::ui::EnterStringParams<'_>, diff --git a/src/rust/bitbox03/src/ui/unlock_animation.rs b/src/rust/bitbox03/src/ui/unlock_animation.rs new file mode 100644 index 0000000000..a0bf6909ee --- /dev/null +++ b/src/rust/bitbox03/src/ui/unlock_animation.rs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 + +use alloc::boxed::Box; +use core::cell::RefCell; +use core::task::{Poll, Waker}; + +use bitbox_lvgl::{self as lvgl, LottieExt, LvAlign, LvLottie, LvObj, LvOpacityLevel, ObjExt}; + +use super::BitBox03Ui; + +const UNLOCK_ANIMATION: &[u8] = include_bytes!("../../Unlock.json"); +const UNLOCK_ANIMATION_SIZE: u32 = 144; + +struct SharedState { + waker: Option, + result: bool, +} + +pub struct UnlockAnimation { + lottie: LvLottie, + shared_state: Box>, +} + +pub struct UnlockAnimationScreen { + pub screen: LvObj, + pub handle: UnlockAnimation, +} + +impl UnlockAnimation { + pub async fn play(self, ui: &mut BitBox03Ui) { + self.lottie.resume(); + + let UnlockAnimation { + lottie: _lottie, + shared_state, + } = self; + + core::future::poll_fn(move |cx| { + let mut shared_state = shared_state.borrow_mut(); + if shared_state.result { + Poll::Ready(()) + } else { + shared_state.waker = Some(cx.waker().clone()); + Poll::Pending + } + }) + .await; + + ui.pop(); + } +} + +pub(super) fn build_unlock_animation() -> UnlockAnimationScreen { + let shared_state = Box::new(RefCell::new(SharedState { + waker: None, + result: false, + })); + let shared_state_cb = shared_state.as_ref() as *const RefCell; + + let screen = LvObj::new().unwrap(); + screen.set_style_bg_color(lvgl::color::black(), 0); + screen.set_style_bg_opa(LvOpacityLevel::LV_OPA_COVER as u8, 0); + screen.set_style_text_color(lvgl::color::white(), 0); + + let lottie = LvLottie::new(&screen, UNLOCK_ANIMATION_SIZE, UNLOCK_ANIMATION_SIZE).unwrap(); + lottie.set_src_data(UNLOCK_ANIMATION); + lottie.set_repeat_count(0); + lottie.set_completed_cb(move || { + let shared_state = unsafe { &*shared_state_cb }; + let mut shared_state = shared_state.borrow_mut(); + if !shared_state.result { + shared_state.result = true; + if let Some(waker) = shared_state.waker.as_ref() { + waker.wake_by_ref(); + } + } + }); + lottie.pause(); + lottie.align(LvAlign::LV_ALIGN_CENTER, 0, 0); + + UnlockAnimationScreen { + screen, + handle: UnlockAnimation { + lottie, + shared_state, + }, + } +} diff --git a/src/ui/components/unlock_animation.c b/src/ui/components/unlock_animation.c index b246aa57d4..016d86b72d 100644 --- a/src/ui/components/unlock_animation.c +++ b/src/ui/components/unlock_animation.c @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -29,6 +30,7 @@ typedef struct { int frame; + bool playing; void (*on_done)(void*); void* on_done_param; } data_t; @@ -152,6 +154,16 @@ static const uint8_t* _get_frame(int frame_idx) static void _render(component_t* component) { data_t* data = (data_t*)component->data; + if (!data->playing) { + position_t pos = { + .left = (SCREEN_WIDTH - LOCK_ANIMATION_FRAME_WIDTH) / 2, + .top = (SCREEN_HEIGHT - LOCK_ANIMATION_FRAME_HEIGHT) / 2}; + dimension_t dim = { + .width = LOCK_ANIMATION_FRAME_WIDTH, .height = LOCK_ANIMATION_FRAME_HEIGHT}; + in_buffer_t image = {.data = _get_frame(0), .len = LOCK_ANIMATION_FRAME_SIZE}; + graphics_draw_image(&pos, &dim, &image); + return; + } int frame = data->frame / SLOWDOWN_FACTOR; if (frame >= LOCK_ANIMATION_N_FRAMES + LOCK_ANIMATION_FRAMES_STOP_TIME) { @@ -201,3 +213,9 @@ component_t* unlock_animation_create(void (*on_done)(void*), void* on_done_param component->dimension.height = SCREEN_HEIGHT; return component; } + +void unlock_animation_play(component_t* component) +{ + data_t* data = (data_t*)component->data; + data->playing = true; +} diff --git a/src/ui/components/unlock_animation.h b/src/ui/components/unlock_animation.h index 37b55ac890..85aae10353 100644 --- a/src/ui/components/unlock_animation.h +++ b/src/ui/components/unlock_animation.h @@ -6,5 +6,6 @@ #include component_t* unlock_animation_create(void (*on_done)(void*), void* on_done_param); +void unlock_animation_play(component_t* component); #endif diff --git a/test/simulator-graphical-bb03/Cargo.toml b/test/simulator-graphical-bb03/Cargo.toml index a48bc18add..f34f62a5b7 100644 --- a/test/simulator-graphical-bb03/Cargo.toml +++ b/test/simulator-graphical-bb03/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] bitbox-hal = { path = "../../src/rust/bitbox-hal" } -bitbox-platform-host = { path = "../../src/rust/bitbox-platform-host" } +bitbox-platform-host = { path = "../../src/rust/bitbox-platform-host", features = ["simulator-graphical"] } bitbox-usb-report-queue = { path = "../../src/rust/bitbox-usb-report-queue" } bitbox02-rust = { path = "../../src/rust/bitbox02-rust", features = ["simulator-graphical"] } bitbox-aes = { path = "../../src/rust/bitbox-aes" } diff --git a/test/simulator-graphical-bb03/lv_conf.h b/test/simulator-graphical-bb03/lv_conf.h index 744f24bd22..e386eb8a42 100644 --- a/test/simulator-graphical-bb03/lv_conf.h +++ b/test/simulator-graphical-bb03/lv_conf.h @@ -588,11 +588,11 @@ #define LV_ATTRIBUTE_EXTERN_DATA /** Use `float` as `lv_value_precise_t` */ -#define LV_USE_FLOAT 0 +#define LV_USE_FLOAT 1 /** Enable matrix support * - Requires `LV_USE_FLOAT = 1` */ -#define LV_USE_MATRIX 0 +#define LV_USE_MATRIX 1 /** Include `lvgl_private.h` in `lvgl.h` to access internal data and functions by default */ #ifndef LV_USE_PRIVATE_API @@ -777,7 +777,7 @@ #define LV_USE_LIST 1 -#define LV_USE_LOTTIE 0 /**< Requires: lv_canvas, thorvg */ +#define LV_USE_LOTTIE 1 /**< Requires: lv_canvas, thorvg */ #define LV_USE_MENU 1 @@ -1000,11 +1000,11 @@ /** Enable Vector Graphic APIs * Requires `LV_USE_MATRIX = 1` */ -#define LV_USE_VECTOR_GRAPHIC 0 +#define LV_USE_VECTOR_GRAPHIC 1 /** Enable ThorVG (vector graphics library) from the src/libs folder. * Requires LV_USE_VECTOR_GRAPHIC */ -#define LV_USE_THORVG_INTERNAL 0 +#define LV_USE_THORVG_INTERNAL 1 /** Enable ThorVG by assuming that its installed and linked to the project * Requires LV_USE_VECTOR_GRAPHIC */ diff --git a/test/simulator-graphical/Cargo.toml b/test/simulator-graphical/Cargo.toml index d5effad1bd..ea81ec827f 100644 --- a/test/simulator-graphical/Cargo.toml +++ b/test/simulator-graphical/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" bitbox02-rust = { path="../../src/rust/bitbox02-rust", features=["simulator-graphical"] } bitbox02-rust-c = { path="../../src/rust/bitbox02-rust-c", features=["simulator-graphical"] } bitbox02 = { path="../../src/rust/bitbox02", features=["simulator-graphical"] } -bitbox-platform-host = { path = "../../src/rust/bitbox-platform-host" } +bitbox-platform-host = { path = "../../src/rust/bitbox-platform-host", features = ["simulator-graphical"] } bitbox-aes = { path="../../src/rust/bitbox-aes"} winit = "0.30.12" tracing = {version = "0.1.41", features=["log"]}