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

+41
View File
@@ -0,0 +1,41 @@
use core::cell::Cell;
use core::task::Waker;
use crate::blocking_mutex::raw::CriticalSectionRawMutex;
use crate::blocking_mutex::Mutex;
/// Utility struct to register and wake a waker.
pub struct AtomicWaker {
waker: Mutex<CriticalSectionRawMutex, Cell<Option<Waker>>>,
}
impl AtomicWaker {
/// Create a new `AtomicWaker`.
pub const fn new() -> Self {
Self {
waker: Mutex::const_new(CriticalSectionRawMutex::new(), Cell::new(None)),
}
}
/// Register a waker. Overwrites the previous waker, if any.
pub fn register(&self, w: &Waker) {
critical_section::with(|cs| {
let cell = self.waker.borrow(cs);
cell.set(match cell.replace(None) {
Some(w2) if (w2.will_wake(w)) => Some(w2),
_ => Some(w.clone()),
})
})
}
/// Wake the registered waker, if any.
pub fn wake(&self) {
critical_section::with(|cs| {
let cell = self.waker.borrow(cs);
if let Some(w) = cell.replace(None) {
w.wake_by_ref();
cell.set(Some(w));
}
})
}
}
@@ -0,0 +1,30 @@
use core::ptr;
use core::ptr::NonNull;
use core::sync::atomic::{AtomicPtr, Ordering};
use core::task::Waker;
/// Utility struct to register and wake a waker.
pub struct AtomicWaker {
waker: AtomicPtr<()>,
}
impl AtomicWaker {
/// Create a new `AtomicWaker`.
pub const fn new() -> Self {
Self {
waker: AtomicPtr::new(ptr::null_mut()),
}
}
/// Register a waker. Overwrites the previous waker, if any.
pub fn register(&self, w: &Waker) {
self.waker.store(w.as_turbo_ptr().as_ptr() as _, Ordering::Release);
}
/// Wake the registered waker, if any.
pub fn wake(&self) {
if let Some(ptr) = NonNull::new(self.waker.load(Ordering::Acquire)) {
unsafe { Waker::from_turbo_ptr(ptr) }.wake();
}
}
}
+11
View File
@@ -0,0 +1,11 @@
//! Async low-level wait queues
#[cfg_attr(feature = "turbowakers", path = "atomic_waker_turbo.rs")]
mod atomic_waker;
pub use atomic_waker::*;
mod waker_registration;
pub use waker_registration::*;
mod multi_waker;
pub use multi_waker::*;
+58
View File
@@ -0,0 +1,58 @@
use core::task::Waker;
use heapless::Vec;
/// Utility struct to register and wake multiple wakers.
pub struct MultiWakerRegistration<const N: usize> {
wakers: Vec<Waker, N>,
}
impl<const N: usize> MultiWakerRegistration<N> {
/// Create a new empty instance
pub const fn new() -> Self {
Self { wakers: Vec::new() }
}
/// Register a waker. If the buffer is full the function returns it in the error
pub fn register<'a>(&mut self, w: &'a Waker) {
// If we already have some waker that wakes the same task as `w`, do nothing.
// This avoids cloning wakers, and avoids unnecessary mass-wakes.
for w2 in &self.wakers {
if w.will_wake(w2) {
return;
}
}
if self.wakers.is_full() {
// All waker slots were full. It's a bit inefficient, but we can wake everything.
// Any future that is still active will simply reregister.
// This won't happen a lot, so it's ok.
self.wake();
}
if self.wakers.push(w.clone()).is_err() {
// This can't happen unless N=0
// (Either `wakers` wasn't full, or it was in which case `wake()` empied it)
panic!("tried to push a waker to a zero-length MultiWakerRegistration")
}
}
/// Wake all registered wakers. This clears the buffer
pub fn wake(&mut self) {
// heapless::Vec has no `drain()`, do it unsafely ourselves...
// First set length to 0, without dropping the contents.
// This is necessary for soundness: if wake() panics and we're using panic=unwind.
// Setting len=0 upfront ensures other code can't observe the vec in an inconsistent state.
// (it'll leak wakers, but that's not UB)
let len = self.wakers.len();
unsafe { self.wakers.set_len(0) }
for i in 0..len {
// Move a waker out of the vec.
let waker = unsafe { self.wakers.as_mut_ptr().add(i).read() };
// Wake it by value, which consumes (drops) it.
waker.wake();
}
}
}
@@ -0,0 +1,52 @@
use core::mem;
use core::task::Waker;
/// Utility struct to register and wake a waker.
#[derive(Debug, Default)]
pub struct WakerRegistration {
waker: Option<Waker>,
}
impl WakerRegistration {
/// Create a new `WakerRegistration`.
pub const fn new() -> Self {
Self { waker: None }
}
/// Register a waker. Overwrites the previous waker, if any.
pub fn register(&mut self, w: &Waker) {
match self.waker {
// Optimization: If both the old and new Wakers wake the same task, we can simply
// keep the old waker, skipping the clone. (In most executor implementations,
// cloning a waker is somewhat expensive, comparable to cloning an Arc).
Some(ref w2) if (w2.will_wake(w)) => {}
_ => {
// clone the new waker and store it
if let Some(old_waker) = mem::replace(&mut self.waker, Some(w.clone())) {
// We had a waker registered for another task. Wake it, so the other task can
// reregister itself if it's still interested.
//
// If two tasks are waiting on the same thing concurrently, this will cause them
// to wake each other in a loop fighting over this WakerRegistration. This wastes
// CPU but things will still work.
//
// If the user wants to have two tasks waiting on the same thing they should use
// a more appropriate primitive that can store multiple wakers.
old_waker.wake()
}
}
}
}
/// Wake the registered waker, if any.
pub fn wake(&mut self) {
if let Some(w) = self.waker.take() {
w.wake()
}
}
/// Returns true if a waker is currently registered
pub fn occupied(&self) -> bool {
self.waker.is_some()
}
}