feat(eckhart): introduce ConnectionIndicator
What changed, and why it matters
This commit adds a new on-screen status icon for the Trezor hardware wallet's Eckhart layout. The icon simply shows whether the device is currently connected via USB or Bluetooth. It is purely a user-interface change and does not alter how the device handles secrets, transactions, or security checks.
No security action required. Treat as a normal UI feature review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces ConnectionIndicator, a new Rust UI component for the layout_eckhart design. It listens to USB Configured/Deconfigured events and, when the ble feature is enabled, BLE Connected/Disconnected events, then renders a green circle/rounded-square indicator when a connection is active. The component is wired into the module exports and adds a new theme color GREEN_BRIGHT. No cryptographic, authorization, or protocol logic is changed.
Changed components
core/embed/rust/src/ui/layout_eckhart/component/connection_indicator.rscore/embed/rust/src/ui/layout_eckhart/component/mod.rscore/embed/rust/src/ui/layout_eckhart/cshape/connected.rscore/embed/rust/src/ui/layout_eckhart/cshape/mod.rscore/embed/rust/src/ui/layout_eckhart/theme/mod.rsInspect captured patch +157 / −0
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/connection_indicator.rs b/core/embed/rust/src/ui/layout_eckhart/component/connection_indicator.rs
new file mode 100644
index 00000000..39c34e8e
--- /dev/null
+++ b/core/embed/rust/src/ui/layout_eckhart/component/connection_indicator.rs
@@ -0,0 +1,120 @@
+use crate::{
+ trezorhal::usb,
+ ui::{
+ component::{Component, Event, EventCtx},
+ event::USBEvent,
+ geometry::Rect,
+ shape::Renderer,
+ },
+};
+
+#[cfg(feature = "ble")]
+use crate::{trezorhal::ble, ui::event::BLEEvent};
+
+use super::super::cshape::{render_connected_indicator, INDICATOR_OUTER_RADIUS};
+
+pub struct ConnectionIndicator {
+ pub area: Rect,
+ pub connected: bool,
+}
+
+impl ConnectionIndicator {
+ pub const AREA_SIZE_NEEDED: i16 = 2 * INDICATOR_OUTER_RADIUS + 4;
+ pub const fn new() -> Self {
+ Self {
+ area: Rect::zero(),
+ connected: false,
+ }
+ }
+
+ /// Create with current actual connection status polled at construction
+ /// time.
+ pub fn new_polled() -> Self {
+ Self {
+ area: Rect::zero(),
+ connected: is_connected(),
+ }
+ }
+
+ pub fn content_width(&self) -> i16 {
+ if self.connected {
+ Self::AREA_SIZE_NEEDED
+ } else {
+ 0
+ }
+ }
+}
+
+impl Component for ConnectionIndicator {
+ type Msg = ();
+
+ fn place(&mut self, bounds: Rect) -> Rect {
+ // enforce that the bounds are big enough to fit the indicator + padding
+ debug_assert_eq!(bounds.width(), Self::AREA_SIZE_NEEDED);
+ debug_assert_eq!(bounds.height(), Self::AREA_SIZE_NEEDED);
+ self.area = bounds;
+ self.area
+ }
+
+ /// Return Some(()) when the connection status changes, None otherwise
+ fn event(&mut self, ctx: &mut EventCtx, event: Event) -> Option<Self::Msg> {
+ let old_connected = self.connected;
+ match event {
+ Event::Attach(_) => {
+ // Only poll on attach
+ self.connected = is_connected();
+ }
+ Event::USB(USBEvent::Configured) => {
+ self.connected = true;
+ }
+ Event::USB(USBEvent::Deconfigured) => {
+ // Only update if BLE is also disconnected
+ #[cfg(feature = "ble")]
+ {
+ self.connected = ble::is_connected();
+ }
+ #[cfg(not(feature = "ble"))]
+ {
+ self.connected = false;
+ }
+ }
+ #[cfg(feature = "ble")]
+ Event::BLE(BLEEvent::Connected) => {
+ self.connected = true;
+ }
+ #[cfg(feature = "ble")]
+ Event::BLE(BLEEvent::Disconnected) => {
+ // Only update if USB is also disconnected
+ self.connected = usb::usb_configured();
+ }
+ _ => {}
+ }
+ if self.connected != old_connected {
+ ctx.request_paint();
+ Some(())
+ } else {
+ None
+ }
+ }
+
+ fn render<'s>(&'s self, target: &mut impl Renderer<'s>) {
+ if self.connected {
+ render_connected_indicator(self.area.center(), target);
+ }
+ }
+}
+
+fn is_connected() -> bool {
+ let connected = usb::usb_configured();
+ #[cfg(feature = "ble")]
+ let connected = connected | ble::is_connected();
+ connected
+}
+
+#[cfg(feature = "ui_debug")]
+impl crate::trace::Trace for ConnectionIndicator {
+ fn trace(&self, t: &mut dyn crate::trace::Tracer) {
+ t.component("ConnectionIndicator");
+ t.bool("connected", self.connected);
+ }
+}
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs
index 51a90b25..8cff579c 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/mod.rs
@@ -1,4 +1,5 @@
mod button;
+mod connection_indicator;
mod error;
mod fuel_gauge;
mod update_screen;
@@ -7,6 +8,7 @@ mod welcome_screen;
pub use button::{
Button, ButtonContent, ButtonMsg, ButtonStyle, ButtonStyleSheet, HapticMode, IconText,
};
+pub use connection_indicator::ConnectionIndicator;
pub use error::ErrorScreen;
pub use fuel_gauge::FuelGauge;
pub use update_screen::UpdateScreen;
diff --git a/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs b/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs
new file mode 100644
index 00000000..e7d9fbba
--- /dev/null
+++ b/core/embed/rust/src/ui/layout_eckhart/cshape/connected.rs
@@ -0,0 +1,31 @@
+use crate::ui::{
+ display::Color,
+ geometry::{Offset, Point, Rect},
+ shape::{self, Renderer},
+};
+
+use super::super::theme;
+
+// outer circle
+pub const INDICATOR_OUTER_RADIUS: i16 = 10;
+const INDICATOR_OUTER_COLOR: Color = theme::GREEN_BRIGHT;
+
+// inner rectangle
+const INDICATOR_INNER_SIZE: i16 = 9;
+const INDICATOR_INNER_COLOR: Color = theme::GREEN;
+
+pub fn render_connected_indicator<'s>(point: Point, target: &mut impl Renderer<'s>) {
+ shape::Circle::new(point, INDICATOR_OUTER_RADIUS)
+ .with_fg(INDICATOR_OUTER_COLOR)
+ .with_bg(INDICATOR_OUTER_COLOR)
+ .render(target);
+ shape::Bar::new(Rect::snap(
+ point,
+ Offset::uniform(INDICATOR_INNER_SIZE),
+ Alignment2D::CENTER,
+ ))
+ .with_fg(INDICATOR_INNER_COLOR)
+ .with_bg(INDICATOR_INNER_COLOR)
+ .with_radius(1)
+ .render(target);
+}
diff --git a/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs b/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs
index e9a27727..2e52f482 100644
--- a/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/cshape/mod.rs
@@ -1,5 +1,7 @@
+mod connected;
mod loader;
mod screen_border;
+pub use connected::{render_connected_indicator, INDICATOR_OUTER_RADIUS};
pub use loader::{render_loader, render_loader_indeterminate};
pub use screen_border::ScreenBorder;
diff --git a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
index fa96d9af..51b33c6e 100644
--- a/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/theme/mod.rs
@@ -39,6 +39,8 @@ pub const GREEN: Color = Color::rgb(0x08, 0x74, 0x48);
pub const GREEN_DARK: Color = Color::rgb(0x06, 0x1E, 0x19);
pub const GREEN_EXTRA_DARK: Color = Color::rgb(0x03, 0x10, 0x0C);
+pub const GREEN_BRIGHT: Color = Color::rgb(0x60, 0xE1, 0x98); // used for Connected indicator
+
pub const ORANGE: Color = Color::rgb(0xFF, 0x63, 0x30);
pub const ORANGE_DIMMED: Color = Color::rgb(0x9E, 0x57, 0x42);
pub const ORANGE_DARK: Color = Color::rgb(0x18, 0x0C, 0x0A);
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.