What changed, and why it matters
This commit is a routine feature implementation for the BitBox03 hardware wallet. It adds a new status screen (success/error) and an internal delay/timer helper used to show that screen for two seconds. There is no security fix or vulnerability present in the diff.
No security action required; review as normal UI/infrastructure code.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces a Rust wrapper around LVGL timers (LvTimer) with explicit Drop cleanup, a completion-future based delay_for() helper, and a status screen builder (status.rs). It also refactors existing LVGL tests to share a new test_util::lock_and_init() helper. The changes are additive UI/infrastructure code; no cryptographic, memory-safety bug fix, or security-hardening change is visible.
Changed components
src/rust/bitbox-lvgl/src/lib.rssrc/rust/bitbox-lvgl/src/test_util.rssrc/rust/bitbox-lvgl/src/timer.rssrc/rust/bitbox-lvgl/src/widgets/obj.rssrc/rust/bitbox03/src/delay.rssrc/rust/bitbox03/src/lib.rssrc/rust/bitbox03/src/ui.rssrc/rust/bitbox03/src/ui/status.rsInspect captured patch +221 / −16
diff --git a/src/rust/bitbox-lvgl/src/lib.rs b/src/rust/bitbox-lvgl/src/lib.rs
index 330494d..2261000 100644
--- a/src/rust/bitbox-lvgl/src/lib.rs
+++ b/src/rust/bitbox-lvgl/src/lib.rs
@@ -57,6 +57,8 @@ pub mod display;
pub mod indev;
pub mod log;
pub mod system;
+#[cfg(test)]
+mod test_util;
pub mod tick;
pub mod timer;
mod util;
diff --git a/src/rust/bitbox-lvgl/src/test_util.rs b/src/rust/bitbox-lvgl/src/test_util.rs
new file mode 100644
index 0000000..16fcb81
--- /dev/null
+++ b/src/rust/bitbox-lvgl/src/test_util.rs
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+
+extern crate std;
+
+use std::sync::{Mutex, MutexGuard, Once};
+
+static LVGL_TEST_LOCK: Mutex<()> = Mutex::new(());
+static INIT: Once = Once::new();
+
+pub(crate) fn lock_and_init() -> MutexGuard<'static, ()> {
+ let lock = LVGL_TEST_LOCK.lock().unwrap();
+ INIT.call_once(crate::system::init);
+ lock
+}
diff --git a/src/rust/bitbox-lvgl/src/timer.rs b/src/rust/bitbox-lvgl/src/timer.rs
index 5b26099..2aa5945 100644
--- a/src/rust/bitbox-lvgl/src/timer.rs
+++ b/src/rust/bitbox-lvgl/src/timer.rs
@@ -1,5 +1,8 @@
// SPDX-License-Identifier: Apache-2.0
+use alloc::{boxed::Box, rc::Rc};
+use core::{cell::RefCell, ffi::c_void, ptr::NonNull};
+
use crate::ffi;
pub fn handler() {
@@ -7,3 +10,125 @@ pub fn handler() {
ffi::lv_timer_handler();
}
}
+
+type TimerCallback = RefCell<Box<dyn FnMut() + 'static>>;
+
+pub struct LvTimer {
+ raw: NonNull<ffi::lv_timer_t>,
+ _callback: Rc<TimerCallback>,
+}
+
+unsafe extern "C" fn timer_cb_trampoline(timer: *mut ffi::lv_timer_t) {
+ let user_data = unsafe { ffi::lv_timer_get_user_data(timer) };
+ if user_data.is_null() {
+ return;
+ }
+
+ let callback_ptr = user_data.cast::<TimerCallback>().cast_const();
+ // Keep the callback alive for the duration of this call, even if the timer drops itself.
+ unsafe {
+ Rc::increment_strong_count(callback_ptr);
+ }
+ let callback = unsafe { Rc::from_raw(callback_ptr) };
+ let mut callback = callback.borrow_mut();
+ callback.as_mut()();
+}
+
+impl LvTimer {
+ /// Creates a timer that invokes `cb` every `period_ms`.
+ ///
+ /// LVGL's auto-delete is disabled so the timer lifetime is fully owned by this Rust wrapper
+ /// and cleanup remains explicit through `Drop`.
+ pub fn new<F>(period_ms: u32, cb: F) -> Option<Self>
+ where
+ F: FnMut() + 'static,
+ {
+ let callback: Rc<TimerCallback> = Rc::new(RefCell::new(Box::new(cb)));
+ let callback_ptr = Rc::as_ptr(&callback);
+ let raw = NonNull::new(unsafe {
+ ffi::lv_timer_create(
+ Some(timer_cb_trampoline),
+ period_ms,
+ callback_ptr.cast_mut().cast::<c_void>(),
+ )
+ })?;
+ unsafe {
+ ffi::lv_timer_set_auto_delete(raw.as_ptr(), false);
+ }
+ Some(Self {
+ raw,
+ _callback: callback,
+ })
+ }
+
+ pub fn pause(&self) {
+ unsafe {
+ ffi::lv_timer_pause(self.raw.as_ptr());
+ }
+ }
+
+ pub fn resume(&self) {
+ unsafe {
+ ffi::lv_timer_resume(self.raw.as_ptr());
+ }
+ }
+
+ pub fn set_period(&self, period_ms: u32) {
+ unsafe {
+ ffi::lv_timer_set_period(self.raw.as_ptr(), period_ms);
+ }
+ }
+
+ pub fn ready(&self) {
+ unsafe {
+ ffi::lv_timer_ready(self.raw.as_ptr());
+ }
+ }
+
+ pub fn set_repeat_count(&self, repeat_count: i32) {
+ unsafe {
+ ffi::lv_timer_set_repeat_count(self.raw.as_ptr(), repeat_count);
+ }
+ }
+}
+
+impl Drop for LvTimer {
+ fn drop(&mut self) {
+ unsafe {
+ ffi::lv_timer_delete(self.raw.as_ptr());
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate std;
+
+ use alloc::rc::Rc;
+ use core::cell::{Cell, RefCell};
+
+ use super::*;
+
+ #[test]
+ fn test_drop_during_callback_is_safe() {
+ let _lock = crate::test_util::lock_and_init();
+
+ let called = Rc::new(Cell::new(false));
+ let called_cb = Rc::clone(&called);
+ let timer_slot = Rc::new(RefCell::new(None));
+ let timer_slot_cb = Rc::clone(&timer_slot);
+
+ let timer = LvTimer::new(0, move || {
+ called_cb.set(true);
+ let dropped = timer_slot_cb.borrow_mut().take();
+ drop(dropped);
+ })
+ .unwrap();
+ *timer_slot.borrow_mut() = Some(timer);
+
+ handler();
+
+ assert!(called.get());
+ assert!(timer_slot.borrow().is_none());
+ }
+}
diff --git a/src/rust/bitbox-lvgl/src/widgets/obj.rs b/src/rust/bitbox-lvgl/src/widgets/obj.rs
index a2cad17..720ece8 100644
--- a/src/rust/bitbox-lvgl/src/widgets/obj.rs
+++ b/src/rust/bitbox-lvgl/src/widgets/obj.rs
@@ -352,17 +352,9 @@ mod tests {
use alloc::rc::Rc;
use core::cell::Cell;
use core::ptr;
- use std::sync::{Mutex, Once};
use super::*;
- static LVGL_TEST_LOCK: Mutex<()> = Mutex::new(());
-
- fn init_lvgl() {
- static INIT: Once = Once::new();
- INIT.call_once(crate::system::init);
- }
-
#[test]
fn test_style_methods_exist() {
let _: fn(&LvObj, LvColor, LvStyleSelector) = <LvObj as ObjExt>::set_style_text_color;
@@ -378,8 +370,7 @@ mod tests {
#[test]
fn test_add_event_cb_invokes_callback() {
- let _lock = LVGL_TEST_LOCK.lock().unwrap();
- init_lvgl();
+ let _lock = crate::test_util::lock_and_init();
let display = crate::LvDisplay::new(16, 16).unwrap();
let screen = display.screen_active().unwrap();
@@ -406,8 +397,7 @@ mod tests {
#[test]
fn test_add_event_cb_delete_event_invokes_callback() {
- let _lock = LVGL_TEST_LOCK.lock().unwrap();
- init_lvgl();
+ let _lock = crate::test_util::lock_and_init();
let display = crate::LvDisplay::new(16, 16).unwrap();
let screen = display.screen_active().unwrap();
@@ -426,8 +416,7 @@ mod tests {
#[test]
fn test_add_event_cb_delete_during_callback_is_safe() {
- let _lock = LVGL_TEST_LOCK.lock().unwrap();
- init_lvgl();
+ let _lock = crate::test_util::lock_and_init();
let display = crate::LvDisplay::new(16, 16).unwrap();
let screen = display.screen_active().unwrap();
diff --git a/src/rust/bitbox03/src/delay.rs b/src/rust/bitbox03/src/delay.rs
new file mode 100644
index 0000000..19e5010
--- /dev/null
+++ b/src/rust/bitbox03/src/delay.rs
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use core::time::Duration;
+
+use bitbox_lvgl::timer::LvTimer;
+
+pub async fn delay_for(duration: Duration) {
+ let (responder, result) = util::futures::completion::completion();
+ let timer = LvTimer::new(duration.as_millis() as u32, move || responder.resolve(()))
+ .expect("failed to create delay timer");
+ timer.set_repeat_count(1);
+ let _timer = timer;
+ result.await;
+}
diff --git a/src/rust/bitbox03/src/lib.rs b/src/rust/bitbox03/src/lib.rs
index cd49238..9fcb9fb 100644
--- a/src/rust/bitbox03/src/lib.rs
+++ b/src/rust/bitbox03/src/lib.rs
@@ -3,6 +3,7 @@
#![no_std]
extern crate alloc;
+pub mod delay;
use core::cell::UnsafeCell;
mod eeprom;
pub mod io;
diff --git a/src/rust/bitbox03/src/ui.rs b/src/rust/bitbox03/src/ui.rs
index 405231d..c35bab0 100644
--- a/src/rust/bitbox03/src/ui.rs
+++ b/src/rust/bitbox03/src/ui.rs
@@ -1,3 +1,5 @@
+use core::time::Duration;
+
use alloc::vec::Vec;
use bitbox_hal as hal;
use bitbox_lvgl::{
@@ -9,6 +11,7 @@ use util::futures::completion;
mod confirm;
mod enter_string;
+mod status;
const LOGO: &[u8] = include_bytes!("../splash.png");
@@ -73,8 +76,10 @@ impl hal::ui::Ui for BitBox03Ui {
todo!()
}
- async fn status(&mut self, _title: &str, _status_success: bool) {
- todo!()
+ async fn status(&mut self, title: &str, status_success: bool) {
+ let screen = status::build_status_screen(title, status_success);
+ let _screen = self.push_guard(screen);
+ crate::delay::delay_for(Duration::from_millis(2000)).await;
}
fn print_screen(&mut self, _duration: core::time::Duration, _msg: &str) {
diff --git a/src/rust/bitbox03/src/ui/status.rs b/src/rust/bitbox03/src/ui/status.rs
new file mode 100644
index 0000000..d5cff49
--- /dev/null
+++ b/src/rust/bitbox03/src/ui/status.rs
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use bitbox_lvgl::{
+ self as lvgl, LabelExt, LvAlign, LvLabel, LvLabelLongMode, LvObj, LvOpacityLevel, ObjExt,
+};
+
+pub(super) fn build_status_screen(title: &str, status_success: bool) -> LvObj {
+ let screen = LvObj::new().unwrap();
+ screen.set_layout(lvgl::LvLayout::LV_LAYOUT_FLEX);
+ screen.set_flex_flow(lvgl::LvFlexFlow::LV_FLEX_FLOW_COLUMN);
+ screen.set_style_bg_color(lvgl::color::black(), 0);
+ screen.set_style_text_color(lvgl::color::white(), 0);
+ screen.set_style_pad_top(96, 0);
+ screen.set_style_pad_right(50, 0);
+ screen.set_style_pad_bottom(40, 0);
+ screen.set_style_pad_left(50, 0);
+ screen.set_style_pad_row(40, 0);
+ screen.set_style_flex_cross_place(lvgl::LvFlexAlign::LV_FLEX_ALIGN_CENTER, 0);
+
+ let badge = LvObj::with_parent(&screen).unwrap();
+ badge.set_size(112, 112);
+ badge.set_style_radius(56, 0);
+ badge.set_style_bg_color(
+ if status_success {
+ lvgl::color::hex(0x0d8f4b)
+ } else {
+ lvgl::color::hex(0xb3261e)
+ },
+ 0,
+ );
+ badge.set_style_bg_opa(LvOpacityLevel::LV_OPA_COVER as u8, 0);
+ badge.set_style_border_width(0, 0);
+
+ let badge_label = LvLabel::new(&badge).unwrap();
+ badge_label
+ .set_text(if status_success { "OK" } else { "ERR" })
+ .unwrap();
+ badge_label.set_style_text_font(
+ lvgl::fonts::INTER_BOLD_32,
+ lvgl::LvState::LV_STATE_DEFAULT as u32,
+ );
+ badge_label.align(LvAlign::LV_ALIGN_CENTER, 0, 0);
+
+ let title_label = LvLabel::new(&screen).unwrap();
+ title_label.set_width(380);
+ title_label.set_long_mode(LvLabelLongMode::LV_LABEL_LONG_MODE_WRAP);
+ title_label.set_text(title).unwrap();
+ title_label.set_style_text_align(lvgl::LvTextAlign::LV_TEXT_ALIGN_CENTER, 0);
+ title_label.set_style_text_font(
+ lvgl::fonts::INTER_BOLD_48,
+ lvgl::LvState::LV_STATE_DEFAULT as u32,
+ );
+
+ screen
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.