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:
17624 files changed
+4564928
-38
No files matched your search
+37
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user