fix(core): avoid LED blinking on event loop restart
What changed, and why it matters
This commit fixes a cosmetic bug where the RGB LED on the Trezor T3W1 would briefly blink or flicker when the device's user-interface event loop restarted (for example, when leaving the homescreen). The fix moves LED color control into the normal screen-drawing pipeline so the LED state is set consistently during rendering rather than being turned off when a screen object is destroyed. There is no indication this is a security vulnerability.
No security action required. Treat as a normal quality/user-experience fix. If reviewing, verify that LED color is deterministic per rendered frame and that no component can leave the LED in an unintended state across event-loop restarts.
Security signals we found
No unsafe code added or removed
No cryptographic, authentication, or secret-handling code touched
No buffer, integer, or memory-management changes
Change is confined to LED state machine / rendering pipeline
Changelog entry describes only a visual fix for homescreen LED blinking
Evidence from the diff
The patch refactors RGB LED handling in the Rust UI layer. Previously, Homescreen implemented Drop to call rgb_led::set_color(0), which turned the LED off when the homescreen was destroyed. Because the event loop can restart and recreate UI objects, that drop caused visible LED blinking. The change introduces Renderer::set_led_color and Renderer::led_color methods, stores the desired LED color inside the renderer, and applies it once per frame via rgb_led::set() in SwipeFlow::render() and LayoutObj::render(). Individual components now call target.set_led_color(...) during their render pass instead of directly manipulating the LED. If no component sets a color, the LED defaults to black (off). This is a UI/UX reliability fix, not a memory-safety or cryptographic issue.
Changed components
core/embed/rust/src/trezorhal/rgb_led.rscore/embed/rust/src/ui/flow/swipe.rscore/embed/rust/src/ui/layout/obj.rscore/embed/rust/src/ui/layout_eckhart/component/error.rscore/embed/rust/src/ui/layout_eckhart/firmware/hold_to_confirm.rscore/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rscore/embed/rust/src/ui/layout_eckhart/firmware/tutorial_screen.rscore/embed/rust/src/ui/shape/render.rsInspect captured patch +80 / −34
diff --git a/core/.changelog.d/5990.fixed b/core/.changelog.d/5990.fixed
new file mode 100644
index 00000000..57a7e5a4
--- /dev/null
+++ b/core/.changelog.d/5990.fixed
@@ -0,0 +1 @@
+[T3W1] Fix homescreen LED blinking.
diff --git a/core/embed/rust/src/trezorhal/rgb_led.rs b/core/embed/rust/src/trezorhal/rgb_led.rs
index 379797e5..123ae4ce 100644
--- a/core/embed/rust/src/trezorhal/rgb_led.rs
+++ b/core/embed/rust/src/trezorhal/rgb_led.rs
@@ -1,5 +1,8 @@
#![allow(dead_code)]
+#[cfg(feature = "ui")]
+use crate::ui::display::Color;
+
use super::ffi;
pub fn set_color(color: u32) {
@@ -7,3 +10,9 @@ pub fn set_color(color: u32) {
ffi::rgb_led_set_color(color);
}
}
+
+/// Set LED color if provided, otherwise turn the LED off.
+#[cfg(feature = "ui")]
+pub fn set(color: Color) {
+ set_color(color.to_u32());
+}
diff --git a/core/embed/rust/src/ui/flow/swipe.rs b/core/embed/rust/src/ui/flow/swipe.rs
index 6249481a..d4547074 100644
--- a/core/embed/rust/src/ui/flow/swipe.rs
+++ b/core/embed/rust/src/ui/flow/swipe.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "rgb_led")]
+use crate::trezorhal::rgb_led;
use crate::{
error::{self, Error},
maybe_trace::MaybeTrace,
@@ -58,7 +60,7 @@ where
}
fn render(&'s self, target: &mut R) {
- <Self as Component>::render(self, target)
+ <Self as Component>::render(self, target);
}
#[cfg(feature = "ui_debug")]
@@ -313,6 +315,10 @@ impl Layout<Result<Obj, Error>> for SwipeFlow {
let overflow: bool = false;
render_on_display(None, Some(Color::black()), |target| {
self.current_page().render(target);
+
+ #[cfg(feature = "rgb_led")]
+ rgb_led::set(target.led_color());
+
#[cfg(feature = "ui_debug")]
if target.should_raise_overflow_exception() {
overflow = true;
diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs
index 3c3e05e4..9fafa599 100644
--- a/core/embed/rust/src/ui/layout/obj.rs
+++ b/core/embed/rust/src/ui/layout/obj.rs
@@ -14,6 +14,8 @@ use num_traits::ToPrimitive;
#[cfg(feature = "ble")]
use crate::ui::event::BLEEvent;
+#[cfg(feature = "rgb_led")]
+use crate::trezorhal::rgb_led;
#[cfg(feature = "button")]
use crate::{
trezorhal::button::{PhysicalButton, PhysicalButtonEvent},
@@ -52,7 +54,7 @@ use crate::{
},
};
-#[cfg(feature = "ui_debug")]
+#[cfg(any(feature = "rgb_led", feature = "ui_debug"))]
use crate::ui::shape::Renderer;
impl AttachType {
@@ -157,6 +159,10 @@ where
let mut overflow: bool = false;
render_on_display(None, Some(Color::black()), |target| {
self.inner.render(target);
+
+ #[cfg(feature = "rgb_led")]
+ rgb_led::set(target.led_color());
+
#[cfg(feature = "ui_debug")]
if target.should_raise_overflow_exception() {
overflow = true;
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/error.rs b/core/embed/rust/src/ui/layout_eckhart/component/error.rs
index 660e0c26..072ad4f8 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/error.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/error.rs
@@ -1,6 +1,3 @@
-#[cfg(feature = "rgb_led")]
-use crate::trezorhal::rgb_led;
-
use crate::{
strutil::TString,
ui::{
@@ -88,6 +85,6 @@ impl<'a> Component for ErrorScreen<'a> {
self.wait_for_restart.render(target);
self.screen_border.render(u8::MAX, target);
#[cfg(feature = "rgb_led")]
- rgb_led::set_color(LED_RED.into());
+ target.set_led_color(LED_RED);
}
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/hold_to_confirm.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/hold_to_confirm.rs
index e0f92c8c..2997a9ea 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/hold_to_confirm.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/hold_to_confirm.rs
@@ -21,9 +21,6 @@ use pareen;
#[cfg(feature = "haptic")]
use crate::trezorhal::haptic;
-#[cfg(feature = "rgb_led")]
-use crate::trezorhal::rgb_led;
-
/// A component that displays a border that grows from the bottom of the screen
/// to the top. The animation is parametrizable by color and duration.
pub struct HoldToConfirmAnim {
@@ -37,6 +34,8 @@ pub struct HoldToConfirmAnim {
header_overlay: Option<TString<'static>>,
/// Animation state
state: AnimState,
+ #[cfg(feature = "rgb_led")]
+ led_color: Color,
}
pub enum HoldToConfirmMsg {
@@ -85,6 +84,8 @@ impl HoldToConfirmAnim {
border: ScreenBorder::new(default_color),
header_overlay: None,
state: AnimState::Idle,
+ #[cfg(feature = "rgb_led")]
+ led_color: Color::black(),
}
}
@@ -122,12 +123,17 @@ impl HoldToConfirmAnim {
pub fn finalize(&mut self) {
#[cfg(feature = "rgb_led")]
- rgb_led::set_color(theme::color_to_led_color(self.color).into());
+ self.update_led_color(theme::color_to_led_color(self.color));
self.state = AnimState::Finalizing {
stopwatch: Stopwatch::new_started(),
};
}
+ #[cfg(feature = "rgb_led")]
+ fn update_led_color(&mut self, led_color: Color) {
+ self.led_color = led_color;
+ }
+
fn is_animating(&self) -> bool {
match &self.state {
AnimState::Idle => false,
@@ -164,7 +170,7 @@ impl Component for HoldToConfirmAnim {
if stopwatch.is_running() && !stopwatch.is_running_within(Self::FINALIZING_DURATION)
{
#[cfg(feature = "rgb_led")]
- rgb_led::set_color(0);
+ self.update_led_color(Color::black());
return Some(HoldToConfirmMsg::Finalized);
}
}
@@ -206,6 +212,8 @@ impl Component for HoldToConfirmAnim {
self.border.render(alpha, target);
}
}
+ #[cfg(feature = "rgb_led")]
+ target.set_led_color(self.led_color);
}
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs
index b5592a7c..c0eac0a0 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/homescreen.rs
@@ -1,6 +1,3 @@
-#[cfg(feature = "rgb_led")]
-use crate::trezorhal::rgb_led;
-
use crate::{
error::Error,
io::BinaryData,
@@ -150,14 +147,6 @@ impl Homescreen {
}
}
-impl Drop for Homescreen {
- fn drop(&mut self) {
- // Turn off the LED when homescreen is destroyed
- #[cfg(feature = "rgb_led")]
- rgb_led::set_color(0);
- }
-}
-
impl Component for Homescreen {
type Msg = HomescreenMsg;
@@ -220,11 +209,7 @@ impl Component for Homescreen {
}
#[cfg(feature = "rgb_led")]
- if let Some(rgb_led) = self.led_color {
- rgb_led::set_color(rgb_led.to_u32());
- } else {
- rgb_led::set_color(0);
- }
+ target.set_led_color(self.led_color.unwrap_or_else(Color::black));
}
}
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 e7c01402..ac9f7648 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
@@ -11,9 +11,6 @@ use crate::{
},
};
-#[cfg(feature = "rgb_led")]
-use crate::trezorhal::rgb_led;
-
use super::{
super::{
component::Button,
@@ -44,6 +41,8 @@ pub struct TutorialWelcomeScreen {
/// 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,
@@ -65,6 +64,8 @@ impl TutorialWelcomeScreen {
} else {
LOADER_DURATION.to_millis()
}),
+ #[cfg(feature = "rgb_led")]
+ led_color: Color::black(),
stopwatch: Stopwatch::new_started(),
border: ScreenBorder::new(theme::GREEN_LIME),
}
@@ -88,15 +89,12 @@ impl Component for TutorialWelcomeScreen {
fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
if let Some(ActionBarMsg::Confirmed) = self.action_bar.event(ctx, event) {
- // Turn off the LED when the screen is destroyed
- #[cfg(feature = "rgb_led")]
- rgb_led::set_color(0);
return Some(TutorialWelcomeScreenMsg::Confirmed);
}
#[cfg(feature = "rgb_led")]
if self.timer.event(ctx, event).is_some() {
- rgb_led::set_color(LED_COLOR.to_u32());
+ self.led_color = LED_COLOR;
return None;
}
@@ -152,6 +150,9 @@ impl Component for TutorialWelcomeScreen {
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);
}
}
diff --git a/core/embed/rust/src/ui/shape/render.rs b/core/embed/rust/src/ui/shape/render.rs
index d2745256..47b65500 100644
--- a/core/embed/rust/src/ui/shape/render.rs
+++ b/core/embed/rust/src/ui/shape/render.rs
@@ -59,6 +59,12 @@ pub trait Renderer<'a> {
self.set_viewport(original);
}
+ #[cfg(feature = "rgb_led")]
+ fn set_led_color(&mut self, color: Color);
+
+ #[cfg(feature = "rgb_led")]
+ fn led_color(&self) -> Color;
+
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self);
@@ -80,6 +86,9 @@ where
/// Drawing cache (decompression context, scratch-pad memory)
cache: &'a DrawingCache<'alloc>,
+ #[cfg(feature = "rgb_led")]
+ led_color: Option<Color>,
+
#[cfg(feature = "ui_debug")]
overflow: bool,
}
@@ -104,6 +113,8 @@ where
Self {
canvas,
cache,
+ #[cfg(feature = "rgb_led")]
+ led_color: None,
#[cfg(feature = "ui_debug")]
overflow: false,
}
@@ -132,6 +143,18 @@ where
}
}
+ #[cfg(feature = "rgb_led")]
+ fn set_led_color(&mut self, color: Color) {
+ // LED color should be set once per rendered frame.
+ debug_assert!(self.led_color.is_none_or(|current| current == color));
+ self.led_color = Some(color);
+ }
+
+ #[cfg(feature = "rgb_led")]
+ fn led_color(&self) -> Color {
+ self.led_color.unwrap_or_else(Color::black)
+ }
+
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self) {
self.overflow = true;
@@ -189,6 +212,16 @@ where
self.renderer.render_shape(shape);
}
+ #[cfg(feature = "rgb_led")]
+ fn set_led_color(&mut self, color: Color) {
+ self.renderer.set_led_color(color);
+ }
+
+ #[cfg(feature = "rgb_led")]
+ fn led_color(&self) -> Color {
+ self.renderer.led_color()
+ }
+
#[cfg(feature = "ui_debug")]
fn raise_overflow_exception(&mut self) {
self.renderer.raise_overflow_exception();
Why this scored 18/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.