fix(core/eckhart): synchronize LED and background in tutorial
What changed, and why it matters
This commit fixes a visual timing issue in the setup tutorial on the Trezor T3W1 hardware wallet. Previously, the on-screen background color and the device's RGB LED could change at slightly different moments because they were driven by separate timers. The patch removes the separate LED timer and ties the LED color change directly to the same progress check used for the on-screen loader/background, so they stay synchronized. There is no indication this was a security vulnerability.
No security action required. Treat as a normal UI/UX bug fix. If reviewing for release readiness, verify the LED and background now remain synchronized across animation_enabled and animation_disabled configurations.
Security signals we found
No security-relevant signals present in diff or metadata
Change is purely UI synchronization in a tutorial screen
No memory safety, cryptographic, authentication, or authorization changes
Evidence from the diff
In core/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rs, the TutorialWelcomeScreen previously maintained a dedicated Timeout (timer) and led_color state field under #[cfg(feature = “rgb_led”)]. The LED color was updated in event() when that timer fired, while the background color was updated in render() based on a stopwatch. The patch removes the timer and led_color fields, removes the event() branch that advanced LED state, and instead checks the single loader_running boolean in render(). When the loader has finished, it both renders the background with LED_COLOR and calls target.set_led_color(LED_COLOR) in the same place. This eliminates a race/desync between the LED and the display during the tutorial welcome animation.
Changed components
core/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rsTrezor T3W1 tutorial welcome screen UIRGB LED behavior during onboarding tutorialInspect captured patch +8 / −25
diff --git a/core/.changelog.d/6022.fixed b/core/.changelog.d/6022.fixed
new file mode 100644
index 00000000..2fedf0ef
--- /dev/null
+++ b/core/.changelog.d/6022.fixed
@@ -0,0 +1 @@
+[T3W1] Synchronize LED and background in tutorial.
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rs
index ac9f7648..10c0a5a6 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rs
@@ -2,7 +2,7 @@ use crate::{
time::{Duration, Stopwatch},
translations::TR,
ui::{
- component::{swipe_detect::SwipeConfig, Component, Event, EventCtx, Label, Timeout},
+ component::{swipe_detect::SwipeConfig, Component, Event, EventCtx, Label},
display::{toif::Toif, Color},
flow::Swipable,
geometry::{Alignment, Alignment2D, Insets, Offset, Point, Rect},
@@ -38,11 +38,6 @@ pub enum TutorialWelcomeScreenMsg {
pub struct TutorialWelcomeScreen {
text: Label<'static>,
action_bar: ActionBar,
- /// Timer for the led color change
- #[cfg(feature = "rgb_led")]
- timer: Timeout,
- #[cfg(feature = "rgb_led")]
- led_color: Color,
/// Stopwatch for the loader animation
stopwatch: Stopwatch,
border: ScreenBorder,
@@ -58,14 +53,6 @@ impl TutorialWelcomeScreen {
)
.top_aligned(),
action_bar: ActionBar::new_timeout(Button::empty(), TOTAL_DURATION),
- #[cfg(feature = "rgb_led")]
- timer: Timeout::new(if animation_disabled() {
- 0
- } else {
- LOADER_DURATION.to_millis()
- }),
- #[cfg(feature = "rgb_led")]
- led_color: Color::black(),
stopwatch: Stopwatch::new_started(),
border: ScreenBorder::new(theme::GREEN_LIME),
}
@@ -92,12 +79,6 @@ impl Component for TutorialWelcomeScreen {
return Some(TutorialWelcomeScreenMsg::Confirmed);
}
- #[cfg(feature = "rgb_led")]
- if self.timer.event(ctx, event).is_some() {
- self.led_color = LED_COLOR;
- return None;
- }
-
// TutorialWelcomeScreen reacts to ANIM_FRAME_TIMER
match event {
_ if animation_disabled() => {
@@ -122,8 +103,12 @@ impl Component for TutorialWelcomeScreen {
.bottom_center()
.ofs(Offset::new(0, -theme::ACTION_BAR_HEIGHT / 2));
- if !self.stopwatch.is_running_within(LOADER_DURATION) {
+ let loader_running = self.stopwatch.is_running_within(LOADER_DURATION);
+
+ #[cfg(feature = "rgb_led")]
+ if !loader_running {
ScreenBackground::new(Some(LED_COLOR), None).render(target);
+ target.set_led_color(LED_COLOR);
}
self.text.render(target);
@@ -145,14 +130,11 @@ impl Component for TutorialWelcomeScreen {
.with_fg(theme::GREY_EXTRA_LIGHT)
.render(target);
- if self.stopwatch.is_running_within(LOADER_DURATION) {
+ if loader_running {
let progress = self.stopwatch.elapsed() / LOADER_DURATION;
let loader_val = (progress * LOADER_MAX_VAL as f32) as u16;
render_loader_indeterminate(loader_val, &self.border, target);
}
-
- #[cfg(feature = "rgb_led")]
- target.set_led_color(self.led_color);
}
}
Why this scored 19/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.