From d22d2b92acd163b7af5cdeb172546c4f1e72b7aa Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Sun, 29 Mar 2026 20:37:03 -0600 Subject: [PATCH] feat: show MAC address on splash screen Splash/waiting screen now shows: Row 1: "DongLoRa" (bold) Row 2: "Board Name vX.Y.Z" Row 3: MAC address (XX:XX:XX:XX:XX:XX) Row 4: "Waiting for host..." Each board reads its MAC at init: - ESP32-S3: efuse MAC via esp_hal::efuse::Efuse::mac_address() - nRF52840: FICR device address registers BoardInfo struct bundles name/version/mac to keep dashboard() under clippy's argument limit. --- src/board/heltec_v3.rs | 4 +++- src/board/heltec_v4.rs | 4 +++- src/board/rak_wisblock_4631.rs | 19 ++++++++++++++++++- src/display/render.rs | 27 +++++++++++++++++++-------- src/display/task.rs | 32 ++++++++++++++++++++++++-------- 5 files changed, 67 insertions(+), 19 deletions(-) diff --git a/src/board/heltec_v3.rs b/src/board/heltec_v3.rs index a703bce3..9de603a5 100644 --- a/src/board/heltec_v3.rs +++ b/src/board/heltec_v3.rs @@ -41,6 +41,7 @@ pub struct UsbParts { pub struct DisplayParts { pub i2c: DisplayI2c, + pub mac: [u8; 6], } // ── Board init ─────────────────────────────────────────────────────── @@ -130,7 +131,8 @@ impl Board { .with_sda(p.GPIO17) .with_scl(p.GPIO18) .into_async(); - let display = Some(DisplayParts { i2c }); + let mac = esp_hal::efuse::Efuse::mac_address(); + let display = Some(DisplayParts { i2c, mac }); (radio, usb, display) } diff --git a/src/board/heltec_v4.rs b/src/board/heltec_v4.rs index 4ab4913b..1d33ad55 100644 --- a/src/board/heltec_v4.rs +++ b/src/board/heltec_v4.rs @@ -41,6 +41,7 @@ pub struct UsbParts { pub struct DisplayParts { pub i2c: DisplayI2c, + pub mac: [u8; 6], } // ── Board init ─────────────────────────────────────────────────────── @@ -141,7 +142,8 @@ impl Board { .with_sda(p.GPIO17) .with_scl(p.GPIO18) .into_async(); - let display = Some(DisplayParts { i2c }); + let mac = esp_hal::efuse::Efuse::mac_address(); + let display = Some(DisplayParts { i2c, mac }); (radio, usb, display) } diff --git a/src/board/rak_wisblock_4631.rs b/src/board/rak_wisblock_4631.rs index 35c43c9b..d1f1c6f5 100644 --- a/src/board/rak_wisblock_4631.rs +++ b/src/board/rak_wisblock_4631.rs @@ -49,6 +49,7 @@ pub struct UsbParts { pub struct DisplayParts { pub i2c: DisplayI2c, + pub mac: [u8; 6], } // ── Board init ─────────────────────────────────────────────────────── @@ -110,7 +111,23 @@ impl Board { // via SSD1306 init (fails gracefully if no display attached). let i2c_cfg = twim::Config::default(); let i2c = Twim::new(p.TWISPI0, Irqs, p.P0_13, p.P0_14, i2c_cfg); - let display = Some(DisplayParts { i2c }); + // Read device address from FICR (factory info registers). + // FICR is at fixed address 0x10000000 on nRF52840. + let mac = unsafe { + let ficr = 0x10000000 as *const u32; + // DEVICEADDR[0] at offset 0x0A4, DEVICEADDR[1] at offset 0x0A8 + let addr0 = core::ptr::read_volatile(ficr.byte_add(0x0A4)); + let addr1 = core::ptr::read_volatile(ficr.byte_add(0x0A8)); + [ + addr0 as u8, + (addr0 >> 8) as u8, + (addr0 >> 16) as u8, + (addr0 >> 24) as u8, + addr1 as u8, + (addr1 >> 8) as u8, + ] + }; + let display = Some(DisplayParts { i2c, mac }); (radio, usb, display) } diff --git a/src/display/render.rs b/src/display/render.rs index a39d2953..98b218ea 100644 --- a/src/display/render.rs +++ b/src/display/render.rs @@ -26,6 +26,13 @@ const SPARK_H: i32 = 19; // y=45..63 const RSSI_MIN: i16 = -120; const RSSI_MAX: i16 = 0; +/// Static board identity info for display rendering. +pub struct BoardInfo<'a> { + pub name: &'a str, + pub version: &'a str, + pub mac: &'a str, +} + /// Render the main status dashboard. pub fn dashboard( target: &mut impl DrawTarget, @@ -33,16 +40,14 @@ pub fn dashboard( rssi_history: &[i16; RSSI_HISTORY_LEN], tx_history: &[bool; RSSI_HISTORY_LEN], rssi_count: usize, - board_name: &str, - version: &str, + board: &BoardInfo, ) { let _ = target.clear(BinaryColor::Off); let style = MonoTextStyle::new(&FONT_6X10, BinaryColor::On); + let mut buf: String<32> = String::new(); match status.config { Some(cfg) => { - // Row 0: state + frequency, TX power right-justified - let mut buf: String<32> = String::new(); let freq_mhz = cfg.freq_hz / 1_000_000; let freq_khz = (cfg.freq_hz % 1_000_000) / 1_000; let _ = write!(buf, " {}.{:03}MHz", freq_mhz, freq_khz); @@ -145,24 +150,30 @@ pub fn dashboard( rssi_sparkline(target, rssi_history, tx_history, rssi_count); } None => { + // Row 1: "DongLoRa" in bold let title_style = MonoTextStyle::new(&FONT_9X15_BOLD, BinaryColor::On); Text::with_alignment( "DongLoRa", - Point::new(W / 2, 24), + Point::new(W / 2, 15), title_style, Alignment::Center, ) .draw(target) .ok(); - Text::with_alignment(version, Point::new(W / 2, 38), style, Alignment::Center) + // Row 2: "Board Name v0.1.0" + buf.clear(); + let _ = write!(buf, "{} v{}", board.name, board.version); + Text::with_alignment(&buf, Point::new(W / 2, 28), style, Alignment::Center) .draw(target) .ok(); - Text::with_alignment(board_name, Point::new(W / 2, 50), style, Alignment::Center) + // Row 3: MAC address + Text::with_alignment(board.mac, Point::new(W / 2, 41), style, Alignment::Center) .draw(target) .ok(); + // Row 4: status Text::with_alignment( "Waiting for host...", - Point::new(W / 2, 63), + Point::new(W / 2, 54), style, Alignment::Center, ) diff --git a/src/display/task.rs b/src/display/task.rs index a91b5be8..e440e2ba 100644 --- a/src/display/task.rs +++ b/src/display/task.rs @@ -81,6 +81,23 @@ pub async fn display_task( status: &'static StatusWatch, display_commands: &'static DisplayCommandChannel, ) { + // Format MAC address as "XX:XX:XX:XX:XX:XX" + let mut mac_str: heapless::String<18> = heapless::String::new(); + let m = parts.mac; + let _ = core::fmt::Write::write_fmt( + &mut mac_str, + format_args!( + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + m[0], m[1], m[2], m[3], m[4], m[5] + ), + ); + + let board_info = render::BoardInfo { + name: BOARD_NAME, + version: env!("CARGO_PKG_VERSION"), + mac: &mac_str, + }; + let interface = I2CDisplayInterface::new(parts.i2c); let mut display = Ssd1306Async::new(interface, DisplaySize128x64, DisplayRotation::Rotate0) .into_buffered_graphics_mode(); @@ -104,7 +121,7 @@ pub async fn display_task( }; // Show splash/waiting screen (dashboard with no config renders it) - render_and_flush(&mut display, &state).await; + render_and_flush(&mut display, &state, &board_info).await; loop { match select3( @@ -129,7 +146,7 @@ pub async fn display_task( state.last_status = radio_status; if state.display_on { - render_and_flush(&mut display, &state).await; + render_and_flush(&mut display, &state, &board_info).await; } } Either3::Second(cmd) => match cmd { @@ -145,18 +162,18 @@ pub async fn display_task( if let Some(s) = receiver.try_get() { state.last_status = s; } - render_and_flush(&mut display, &state).await; + render_and_flush(&mut display, &state, &board_info).await; } DisplayCommand::Reset => { state = DisplayState::new(); - render_and_flush(&mut display, &state).await; + render_and_flush(&mut display, &state, &board_info).await; } }, Either3::Third(()) => { // Timer tick: advance sparkline slot state.advance_slot(); if state.display_on { - render_and_flush(&mut display, &state).await; + render_and_flush(&mut display, &state, &board_info).await; } } } @@ -171,7 +188,7 @@ type Display = ssd1306::Ssd1306Async< ssd1306::mode::BufferedGraphicsModeAsync, >; -async fn render_and_flush(display: &mut Display, state: &DisplayState) +async fn render_and_flush(display: &mut Display, state: &DisplayState, board: &render::BoardInfo<'_>) where I: display_interface::AsyncWriteOnlyDataCommand, { @@ -181,8 +198,7 @@ where &state.rssi_history, &state.tx_history, state.rssi_count, - BOARD_NAME, - env!("CARGO_PKG_VERSION"), + board, ); let _ = display.flush().await; }