feat: add display UI with splash screen, dashboard, and RSSI sparkline

Implement full SSD1306 OLED display support:
- Splash screen on boot (1.5s) with project name, version, board
- Dashboard showing radio state, config, counters, RSSI/SNR
- RSSI history sparkline (64-sample ring buffer, 2px-wide bars)
- DisplayOn/DisplayOff host commands routed via dedicated channel
- Pure render functions against generic DrawTarget (no hardware coupling)
- Display on/off control bypasses radio task entirely

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
// Copyright © 2018–2025 Trevor Spiteri
// This library is free software: you can redistribute it and/or
// modify it under the terms of either
//
// * the Apache License, Version 2.0 or
// * the MIT License
//
// at your option.
//
// You should have recieved copies of the Apache License and the MIT
// License along with the library. If not, see
// <https://www.apache.org/licenses/LICENSE-2.0> and
// <https://opensource.org/licenses/MIT>.
use core::cell::Cell;
use core::fmt::{Debug, Formatter, Result as FmtResult, Write};
#[cfg(target_has_atomic = "32")]
use core::sync::atomic::{AtomicU32, Ordering};
// This is an ugly hack to check whether a `Formatter` has `debug_lower_hex` or
// `debug_upper_hex`.
//
// We do a dummy write with format string "{:x?}" to get `debug_lower_hex`, and
// a dummy write with format string "{:X?}" to get `debug_upper_hex`. Each time,
// we get the flags using the deprecated `Formatter::flags`.
//
// If AtomicU32 is supported, we cache the flags.
#[cfg(target_has_atomic = "32")]
static LOWER_FLAGS: AtomicU32 = AtomicU32::new(0);
#[cfg(target_has_atomic = "32")]
static UPPER_FLAGS: AtomicU32 = AtomicU32::new(0);
fn get_flags(f: &Formatter) -> u32 {
#[allow(deprecated)]
f.flags()
}
struct StoreFlags(Cell<u32>);
impl Debug for StoreFlags {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
self.0.set(get_flags(f));
Ok(())
}
}
struct Discard;
impl Write for Discard {
fn write_str(&mut self, _s: &str) -> FmtResult {
Ok(())
}
}
pub enum IsDebugHex {
No,
Lower,
Upper,
}
pub fn is_debug_hex(f: &Formatter) -> IsDebugHex {
let flags = get_flags(f);
// avoid doing unnecessary work if flags is zero
if flags == 0 {
return IsDebugHex::No;
}
let (lower_mask, upper_mask) = get_flag_masks();
if flags & lower_mask != 0 {
IsDebugHex::Lower
} else if flags & upper_mask != 0 {
IsDebugHex::Upper
} else {
IsDebugHex::No
}
}
#[cfg(target_has_atomic = "32")]
fn load_cache() -> Option<(u32, u32)> {
let cached_lower = LOWER_FLAGS.load(Ordering::Relaxed);
let cached_upper = UPPER_FLAGS.load(Ordering::Relaxed);
if cached_lower == u32::MAX || cached_upper == u32::MAX {
// error was detected, so no need to repeat the error generation
return Some((0, 0));
}
if cached_lower == 0 || cached_upper == 0 {
return None;
}
Some((cached_lower, cached_upper))
}
#[cfg(target_has_atomic = "32")]
fn store_cache(mut lower: u32, mut upper: u32) {
if lower == 0 || upper == 0 {
lower = u32::MAX;
upper = u32::MAX;
}
LOWER_FLAGS.store(lower, Ordering::Relaxed);
UPPER_FLAGS.store(upper, Ordering::Relaxed);
}
#[cfg(not(target_has_atomic = "32"))]
fn load_cache() -> Option<(u32, u32)> {
None
}
#[cfg(not(target_has_atomic = "32"))]
fn store_cache(_lower: u32, _upper: u32) {}
fn get_flag_masks() -> (u32, u32) {
if let Some(cached) = load_cache() {
return cached;
}
let store_flags = StoreFlags(Cell::new(0));
if write!(Discard, "{store_flags:x?}").is_err() {
store_cache(u32::MAX, u32::MAX);
return (0, 0);
}
let lower_flags = store_flags.0.get();
if write!(Discard, "{store_flags:X?}").is_err() {
store_cache(u32::MAX, u32::MAX);
return (0, 0);
}
let upper_flags = store_flags.0.get();
let lower_mask = lower_flags & !upper_flags;
let upper_mask = upper_flags & !lower_flags;
store_cache(lower_mask, upper_mask);
(lower_mask, upper_mask)
}