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

+66
View File
@@ -0,0 +1,66 @@
use core::cell::Cell;
use core::task::Waker;
use crate::blocking_mutex::raw::{CriticalSectionRawMutex, RawMutex};
use crate::blocking_mutex::Mutex;
/// Utility struct to register and wake a waker.
/// If a waker is registered, registering another waker will replace the previous one without waking it.
/// Intended to wake a task from an interrupt. Therefore, it is generally not expected,
/// that multiple tasks register try to register a waker simultaneously.
pub struct GenericAtomicWaker<M: RawMutex> {
waker: Mutex<M, Cell<Option<Waker>>>,
}
impl<M: RawMutex> GenericAtomicWaker<M> {
/// Create a new `AtomicWaker`.
pub const fn new(mutex: M) -> Self {
Self {
waker: Mutex::const_new(mutex, Cell::new(None)),
}
}
/// Register a waker. Overwrites the previous waker, if any.
pub fn register(&self, w: &Waker) {
self.waker.lock(|cell| {
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) {
self.waker.lock(|cell| {
if let Some(w) = cell.replace(None) {
w.wake_by_ref();
cell.set(Some(w));
}
})
}
}
/// Utility struct to register and wake a waker.
pub struct AtomicWaker {
waker: GenericAtomicWaker<CriticalSectionRawMutex>,
}
impl AtomicWaker {
/// Create a new `AtomicWaker`.
pub const fn new() -> Self {
Self {
waker: GenericAtomicWaker::new(CriticalSectionRawMutex::new()),
}
}
/// Register a waker. Overwrites the previous waker, if any.
pub fn register(&self, w: &Waker) {
self.waker.register(w);
}
/// Wake the registered waker, if any.
pub fn wake(&self) {
self.waker.wake();
}
}
+34
View File
@@ -0,0 +1,34 @@
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.
/// If a waker is registered, registering another waker will replace the previous one without waking it.
/// The intended use case is to wake tasks from interrupts. Therefore, it is generally not expected,
/// that multiple tasks register try to register a waker simultaneously.
#[derive(Debug)]
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::*;
+63
View File
@@ -0,0 +1,63 @@
use core::task::Waker;
use heapless::Vec;
/// Utility struct to register and wake multiple wakers.
/// Queue of wakers with a maximum length of `N`.
/// Intended for waking multiple tasks.
#[derive(Debug)]
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, [wakes all the wakers](Self::wake), clears its buffer and registers the waker.
pub fn register(&mut self, w: &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();
}
}
}
+56
View File
@@ -0,0 +1,56 @@
use core::mem;
use core::task::Waker;
/// Utility struct to register and wake a waker.
/// If a waker is registered, registering another waker will replace the previous one.
/// The previous waker will be woken in this case, giving it a chance to reregister itself.
/// Although it is possible to wake multiple tasks this way,
/// this will cause them to wake each other in a loop registering themselves.
#[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()
}
}