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

+37
View File
@@ -0,0 +1,37 @@
/// Arithmetic mean
pub trait Mean {
/// Result type
type Result;
/// Compute the arithmetic mean
fn mean(self) -> Self::Result;
}
impl<I> Mean for I
where
I: Iterator<Item = f32>,
{
type Result = f32;
fn mean(self) -> f32 {
let mut num_items = 0;
let mut sum = 0.0;
for item in self {
num_items += 1;
sum += item;
}
sum / (num_items as f32)
}
}
#[cfg(test)]
mod tests {
use super::Mean;
#[test]
fn mean_test() {
assert_eq!([1.0, 3.0, 5.0].iter().cloned().mean(), 3.0);
}
}
+35
View File
@@ -0,0 +1,35 @@
use super::variance::Variance;
#[allow(unused_imports)]
use crate::F32Ext;
/// Compute standard deviation
pub trait StdDev {
/// Result type
type Result;
/// Compute standard deviation
fn stddev(self) -> Self::Result;
}
impl<T> StdDev for &[T]
where
T: Copy + Into<f32>,
{
/// Result type
type Result = f32;
fn stddev(self) -> f32 {
self.variance().sqrt()
}
}
#[cfg(test)]
mod tests {
use super::StdDev;
#[test]
fn stddev_test() {
assert_eq!([1.0, 3.0, 5.0].as_ref().stddev(), 2.0);
assert_eq!([1.0, 3.0, 5.0].as_ref().stddev(), 2.0);
}
}
+123
View File
@@ -0,0 +1,123 @@
//! Iterate over input slices after culling statistical outliers.
use super::mean::Mean;
#[allow(unused_imports)]
use crate::F32Ext;
use core::{iter, slice};
/// Default number of standard deviations at which a value should be
/// considered an outlier.
pub const DEFAULT_THRESHOLD: f32 = 2.0;
/// Iterate over the given input after culling outliers.
pub trait Trim {
/// Result type
type Result: Copy + Into<f32>;
/// Trim this collection (cull outliers) at the default threshold of 2 standard
/// deviations.
fn trim(&self) -> Iter<'_, Self::Result> {
self.trim_at(DEFAULT_THRESHOLD)
}
/// Trim this collection (cull outliers) at the specified number of standard deviations.
fn trim_at(&self, threshold: f32) -> Iter<'_, Self::Result>;
}
impl<N> Trim for &[N]
where
N: Copy,
f32: From<N>,
{
type Result = N;
fn trim_at(&self, threshold: f32) -> Iter<'_, Self::Result> {
Iter::new(self, threshold)
}
}
/// A "trimmed" iterator which culls outliers at a given number of standard
/// deviations from the mean.
pub struct Iter<'a, N: Copy> {
/// Iterator over the input values
input: iter::Cloned<slice::Iter<'a, N>>,
/// Arithmetic mean of the input values as a float
mean: f32,
/// Standard deviation of the input values
stddev: f32,
/// Number of standard deviations at which values are considered outliers
threshold: f32,
}
impl<'a, N> Iter<'a, N>
where
N: Copy,
f32: From<N>,
{
/// Create a new trimmed iterator over an input slice.
///
/// Inputs will be considered outliers at the given `threshold` number
/// of standard deviations (e.g. `2.0`).
pub fn new(input: &'a [N], threshold: f32) -> Self {
let len = input.len() as f32;
let input = input.iter().cloned();
let input_f32 = input.clone().map(f32::from);
// TODO(tarcieri): eliminate duplication with mean/variance/stddev in super
let mean = input_f32.clone().mean();
let sum = input_f32.fold(0.0, |sum, n| {
let n = n - mean;
sum + n * n
});
let variance = sum / (len - 1.0);
let stddev = variance.sqrt();
Self {
input,
mean,
stddev,
threshold,
}
}
}
impl<'a, N> Iterator for Iter<'a, N>
where
N: Copy,
f32: From<N>,
{
type Item = N;
fn next(&mut self) -> Option<N> {
while let Some(n) = self.input.next() {
let distance = (f32::from(n) - self.mean).abs();
// TODO(tarcieri): better method for finding outliers? (e.g. MAD, IQD)
if (distance / self.stddev) < self.threshold {
return Some(n);
}
}
None
}
}
#[cfg(test)]
mod tests {
use super::Trim;
#[test]
fn stddev_test() {
let input: &[f32] = &[1.0, 2.0, 3.0, 999.0, 5.0];
let mut trimmed = input.trim_at(1.0);
assert_eq!(trimmed.next().unwrap(), 1.0);
assert_eq!(trimmed.next().unwrap(), 2.0);
assert_eq!(trimmed.next().unwrap(), 3.0);
assert_eq!(trimmed.next().unwrap(), 5.0);
assert_eq!(trimmed.next(), None);
}
}
+40
View File
@@ -0,0 +1,40 @@
use super::mean::Mean;
/// Statistical variance
pub trait Variance {
/// Result type
type Result;
/// Compute the statistical variance
fn variance(self) -> Self::Result;
}
impl<T> Variance for &[T]
where
T: Copy + Into<f32>,
{
/// Result type
type Result = f32;
fn variance(self) -> f32 {
let mean = self.iter().map(|n| (*n).into()).mean();
let mut sum = 0.0;
for item in self {
let n = (*item).into() - mean;
sum += n * n;
}
sum / (self.len() as f32 - 1.0)
}
}
#[cfg(test)]
mod tests {
use super::Variance;
#[test]
fn variance_test() {
assert_eq!([1.0, 3.0, 5.0].as_ref().variance(), 4.0);
}
}