From 1fe3da90542bd63b4d43b8400addcb48ee216fa2 Mon Sep 17 00:00:00 2001 From: Marko Bencun Date: Fri, 17 Apr 2026 19:42:59 +0000 Subject: [PATCH 1/3] ui: don't show waiting screen during password stretch Before the securechip operations became async, the UI did not process during keystore unlock and froze at the last screen of the password confirmation, and UI continued with the BIP39 unlock animation. Now that they are async, the password screen is popped and the waiting screen would be shown for roughly 1s before the unlock animation started. To improve UI there, we start showing first screen of the unlock animation (the locked lock) as the filler screen. Instead of the default waiting screen being shown, during these operations the first animation frame component is shown and held, with animation starting only in bip39_unlock. The downside is that when entering the wrong password, it is also briefly shown before the wrong-password error message, but it's acceptable. There are other workflows where the waiting screen is also shown during the stretch, not addresed by this commit: show mnemonic, create backup, and change password (any call to `workflows::unlock::unlock_keystore()` to check the password). Co-Authored-By: Niklas Dusenlund --- src/rust/bitbox-hal/src/ui.rs | 5 +- src/rust/bitbox02-rust/src/hal/testing/ui.rs | 25 ++++++- src/rust/bitbox02-rust/src/hww.rs | 28 +++++--- .../src/hww/api/change_password.rs | 12 +++- src/rust/bitbox02-rust/src/hww/api/restore.rs | 8 ++- .../bitbox02-rust/src/hww/api/set_password.rs | 4 +- src/rust/bitbox02-rust/src/workflow/unlock.rs | 71 ++++++++++++++++++- src/rust/bitbox02-sys/build.rs | 1 + src/rust/bitbox02/src/hal/ui.rs | 9 ++- src/rust/bitbox02/src/ui/ui.rs | 70 +++++++++++------- src/rust/bitbox02/src/ui/ui_stub.rs | 16 ++++- .../bitbox02/src/ui/ui_stub_c_unit_tests.rs | 16 ++++- src/rust/bitbox03/src/ui.rs | 7 +- src/ui/components/unlock_animation.c | 18 +++++ src/ui/components/unlock_animation.h | 1 + 15 files changed, 240 insertions(+), 51 deletions(-) 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/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/src/ui.rs b/src/rust/bitbox03/src/ui.rs index 366bff8519..ebdbe81be5 100644 --- a/src/rust/bitbox03/src/ui.rs +++ b/src/rust/bitbox03/src/ui.rs @@ -49,6 +49,7 @@ impl hal::ui::Ui for BitBox03Ui { type Progress = BitBox03UiProgress; type Empty = BitBox03UiEmpty; + type UnlockAnimation = BitBox03UiEmpty; async fn confirm( &mut self, @@ -90,7 +91,7 @@ impl hal::ui::Ui for BitBox03Ui { todo!() } - async fn unlock_animation(&mut self) { + async fn unlock_animation_play(&mut self, _animation: Self::UnlockAnimation) { self.status("TODO\nunlock_animation", true).await } @@ -122,6 +123,10 @@ impl hal::ui::Ui for BitBox03Ui { todo!() } + fn unlock_animation_create(&mut self) -> Self::UnlockAnimation { + todo!() + } + async fn enter_string( &mut self, params: &bitbox_hal::ui::EnterStringParams<'_>, 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 From 371809d9eb3447dbd24ecac5f016ae72fcba3577 Mon Sep 17 00:00:00 2001 From: Niklas Dusenlund Date: Mon, 20 Apr 2026 11:27:36 +0200 Subject: [PATCH 2/3] Simulate securechip stretching as well --- src/rust/bitbox-platform-host/Cargo.toml | 2 ++ src/rust/bitbox-platform-host/src/securechip.rs | 9 +++++++++ src/rust/bitbox02-rust/Cargo.toml | 1 + test/simulator-graphical-bb03/Cargo.toml | 2 +- test/simulator-graphical/Cargo.toml | 2 +- 5 files changed, 14 insertions(+), 2 deletions(-) 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/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/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"]} From 15d66b21b142d397cb2cc8fb26e06e1c7dcf7ca4 Mon Sep 17 00:00:00 2001 From: Niklas Dusenlund Date: Mon, 20 Apr 2026 09:43:13 +0200 Subject: [PATCH 3/3] bitbox03/ui: enable lottie and add demo unlock animation --- src/rust/bitbox-lvgl-sys/build.rs | 9 ++ src/rust/bitbox-lvgl-sys/lv_conf.h | 10 +- src/rust/bitbox-lvgl/src/lib.rs | 1 + src/rust/bitbox-lvgl/src/widgets/class.rs | 1 - src/rust/bitbox-lvgl/src/widgets/lottie.rs | 148 +++++++++++++++++++ src/rust/bitbox-lvgl/src/widgets/mod.rs | 1 + src/rust/bitbox03/Unlock.json | 1 + src/rust/bitbox03/src/ui.rs | 11 +- src/rust/bitbox03/src/ui/unlock_animation.rs | 88 +++++++++++ test/simulator-graphical-bb03/lv_conf.h | 10 +- 10 files changed, 265 insertions(+), 15 deletions(-) create mode 100644 src/rust/bitbox-lvgl/src/widgets/lottie.rs create mode 100644 src/rust/bitbox03/Unlock.json create mode 100644 src/rust/bitbox03/src/ui/unlock_animation.rs 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/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 ebdbe81be5..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,7 +50,7 @@ impl hal::ui::Ui for BitBox03Ui { type Progress = BitBox03UiProgress; type Empty = BitBox03UiEmpty; - type UnlockAnimation = BitBox03UiEmpty; + type UnlockAnimation = unlock_animation::UnlockAnimation; async fn confirm( &mut self, @@ -91,8 +92,8 @@ impl hal::ui::Ui for BitBox03Ui { todo!() } - async fn unlock_animation_play(&mut self, _animation: Self::UnlockAnimation) { - 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) { @@ -124,7 +125,9 @@ impl hal::ui::Ui for BitBox03Ui { } fn unlock_animation_create(&mut self) -> Self::UnlockAnimation { - todo!() + let animation = unlock_animation::build_unlock_animation(); + self.push(animation.screen); + animation.handle } async fn enter_string( 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/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 */