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

+90
View File
@@ -0,0 +1,90 @@
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
clippy::use_self,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use enum_as_inner::EnumAsInner;
pub mod name_collisions {
#![allow(dead_code, missing_copy_implementations, missing_docs)]
pub struct Option;
pub struct Some;
pub struct None;
pub struct Result;
pub struct Ok;
pub struct Err;
}
#[allow(unused_imports)]
use name_collisions::*;
#[derive(Debug, EnumAsInner)]
enum EmptyTest {}
#[test]
fn test_empty() {
let empty = std::option::Option::None::<EmptyTest>;
assert!(empty.is_none());
}
#[derive(Debug, EnumAsInner)]
enum EmptyParendsTest {
Empty(),
}
#[test]
fn test_empty_parends() {
let mut empty = EmptyParendsTest::Empty();
assert!(empty.is_empty());
empty
.as_empty()
.expect("should have been something and a unit");
empty
.as_empty_mut()
.expect("should have been something and a unit");
empty
.into_empty()
.expect("should have been something and a unit");
}
#[derive(Debug, EnumAsInner)]
enum OneTest {
One(u32),
}
#[test]
fn test_one() {
let mut empty = OneTest::One(1);
assert!(empty.is_one());
assert_eq!(*empty.as_one().unwrap(), 1);
assert_eq!(*empty.as_one_mut().unwrap(), 1);
assert_eq!(empty.into_one().unwrap(), 1);
}
#[derive(Debug, EnumAsInner)]
enum MultiTest {
Multi(u32, u32),
}
#[test]
fn test_multi() {
let mut multi = MultiTest::Multi(1, 1);
assert!(multi.is_multi());
assert_eq!(multi.as_multi().unwrap(), (&1_u32, &1_u32));
assert_eq!(multi.as_multi_mut().unwrap(), (&mut 1_u32, &mut 1_u32));
assert_eq!(multi.into_multi().unwrap(), (1_u32, 1_u32));
}
+53
View File
@@ -0,0 +1,53 @@
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
clippy::use_self,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use enum_as_inner::EnumAsInner;
pub mod name_collisions {
#![allow(dead_code, missing_copy_implementations, missing_docs)]
pub struct Option;
pub struct Some;
pub struct None;
pub struct Result;
pub struct Ok;
pub struct Err;
}
#[allow(unused_imports)]
use name_collisions::*;
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, EnumAsInner)]
enum WithGenerics<T: Clone + Copy> {
A(T),
B(T),
}
#[test]
fn with_generics() {
let mut with_generics = WithGenerics::A(100);
assert!(with_generics.is_a());
assert!(!with_generics.is_b());
assert!(with_generics.as_a().is_some());
assert!(with_generics.as_b().is_none());
assert_eq!(with_generics.into_a().unwrap(), 100);
assert_eq!(*with_generics.as_a().unwrap(), 100);
assert_eq!(*with_generics.as_a_mut().unwrap(), 100);
assert!(with_generics.into_b().is_err());
assert!(with_generics.as_b().is_none());
assert!(with_generics.as_b_mut().is_none());
}
+99
View File
@@ -0,0 +1,99 @@
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
clippy::use_self,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use enum_as_inner::EnumAsInner;
pub mod name_collisions {
#![allow(dead_code, missing_copy_implementations, missing_docs)]
pub struct Option;
pub struct Some;
pub struct None;
pub struct Result;
pub struct Ok;
pub struct Err;
}
#[allow(unused_imports)]
use name_collisions::*;
#[derive(Debug, EnumAsInner)]
enum ManyVariants {
One { one: u32 },
Two { one: u32, two: i32 },
Three { one: bool, two: u32, three: i64 },
}
#[test]
fn test_one_named() {
let mut many = ManyVariants::One { one: 1 };
assert!(many.is_one());
assert!(!many.is_two());
assert!(!many.is_three());
assert!(many.as_one().is_some());
assert!(many.as_two().is_none());
assert!(many.as_three().is_none());
assert!(many.as_one_mut().is_some());
assert!(many.as_two_mut().is_none());
assert!(many.as_three_mut().is_none());
assert_eq!(*many.as_one().unwrap(), 1_u32);
assert_eq!(*many.as_one_mut().unwrap(), 1_u32);
}
#[test]
fn test_two_named() {
let mut many = ManyVariants::Two { one: 1, two: 2 };
assert!(!many.is_one());
assert!(many.is_two());
assert!(!many.is_three());
assert!(many.as_one().is_none());
assert!(many.as_two().is_some());
assert!(many.as_three().is_none());
assert!(many.as_one_mut().is_none());
assert!(many.as_two_mut().is_some());
assert!(many.as_three_mut().is_none());
assert_eq!(many.as_two().unwrap(), (&1_u32, &2_i32));
assert_eq!(many.as_two_mut().unwrap(), (&mut 1_u32, &mut 2_i32));
assert_eq!(many.into_two().unwrap(), (1_u32, 2_i32));
}
#[test]
fn test_three_named() {
let mut many = ManyVariants::Three {
one: true,
two: 1,
three: 2,
};
assert!(!many.is_one());
assert!(!many.is_two());
assert!(many.is_three());
assert!(many.as_one().is_none());
assert!(many.as_two().is_none());
assert!(many.as_three().is_some());
assert!(many.as_one_mut().is_none());
assert!(many.as_two_mut().is_none());
assert!(many.as_three_mut().is_some());
assert_eq!(many.as_three().unwrap(), (&true, &1_u32, &2_i64));
assert_eq!(
many.as_three_mut().unwrap(),
(&mut true, &mut 1_u32, &mut 2_i64)
);
assert_eq!(many.into_three().unwrap(), (true, 1_u32, 2_i64));
}
+69
View File
@@ -0,0 +1,69 @@
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
clippy::use_self,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use enum_as_inner::EnumAsInner;
pub mod name_collisions {
#![allow(dead_code, missing_copy_implementations, missing_docs)]
pub struct Option;
pub struct Some;
pub struct None;
pub struct Result;
pub struct Ok;
pub struct Err;
}
#[allow(unused_imports)]
use name_collisions::*;
#[derive(Debug, EnumAsInner)]
#[allow(non_camel_case_types)]
#[allow(clippy::upper_case_acronyms)]
enum MixedCaseVariants {
XMLIsNotCool,
Rust_IsCoolThough(u32),
YMCA { named: i16 },
}
#[test]
fn test_xml_unit() {
let mixed = MixedCaseVariants::XMLIsNotCool;
assert!(mixed.is_xml_is_not_cool());
assert!(mixed.as_rust_is_cool_though().is_none());
assert!(mixed.as_ymca().is_none());
}
#[test]
fn test_rust_unnamed() {
let mixed = MixedCaseVariants::Rust_IsCoolThough(42);
assert!(!mixed.is_xml_is_not_cool());
assert!(mixed.as_rust_is_cool_though().is_some());
assert!(mixed.as_ymca().is_none());
assert_eq!(*mixed.as_rust_is_cool_though().unwrap(), 42);
assert_eq!(mixed.into_rust_is_cool_though().unwrap(), 42);
}
#[test]
fn test_ymca_named() {
let mixed = MixedCaseVariants::YMCA { named: -32_768 };
assert!(!mixed.is_xml_is_not_cool());
assert!(mixed.as_rust_is_cool_though().is_none());
assert!(mixed.as_ymca().is_some());
assert_eq!(*mixed.as_ymca().unwrap(), (-32_768));
assert_eq!(mixed.into_ymca().unwrap(), (-32_768));
}
+61
View File
@@ -0,0 +1,61 @@
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
clippy::use_self,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use enum_as_inner::EnumAsInner;
pub mod name_collisions {
#![allow(dead_code, missing_copy_implementations, missing_docs)]
pub struct Option;
pub struct Some;
pub struct None;
pub struct Result;
pub struct Ok;
pub struct Err;
}
#[allow(unused_imports)]
use name_collisions::*;
#[derive(EnumAsInner)]
enum UnitVariants {
Zero,
One,
Two,
}
#[test]
fn test_zero_unit() {
let unit = UnitVariants::Zero;
assert!(unit.is_zero());
assert!(!unit.is_one());
assert!(!unit.is_two());
}
#[test]
fn test_one_unit() {
let unit = UnitVariants::One;
assert!(!unit.is_zero());
assert!(unit.is_one());
assert!(!unit.is_two());
}
#[test]
fn test_two_unit() {
let unit = UnitVariants::Two;
assert!(!unit.is_zero());
assert!(!unit.is_one());
assert!(unit.is_two());
}
+100
View File
@@ -0,0 +1,100 @@
#![warn(
clippy::default_trait_access,
clippy::dbg_macro,
clippy::print_stdout,
clippy::unimplemented,
clippy::use_self,
missing_copy_implementations,
missing_docs,
non_snake_case,
non_upper_case_globals,
rust_2018_idioms,
unreachable_pub
)]
use enum_as_inner::EnumAsInner;
pub mod name_collisions {
#![allow(dead_code, missing_copy_implementations, missing_docs)]
pub struct Option;
pub struct Some;
pub struct None;
pub struct Result;
pub struct Ok;
pub struct Err;
}
#[allow(unused_imports)]
use name_collisions::*;
#[derive(Debug, EnumAsInner)]
enum ManyVariants {
One(u32),
Two(u32, i32),
Three(bool, u32, i64),
}
#[test]
fn test_one_unnamed() {
let mut many = ManyVariants::One(1);
assert!(many.is_one());
assert!(!many.is_two());
assert!(!many.is_three());
assert!(many.as_one().is_some());
assert!(many.as_two().is_none());
assert!(many.as_three().is_none());
assert!(many.as_one_mut().is_some());
assert!(many.as_two_mut().is_none());
assert!(many.as_three_mut().is_none());
assert_eq!(*many.as_one().unwrap(), 1_u32);
assert_eq!(*many.as_one_mut().unwrap(), 1_u32);
assert_eq!(many.into_one().unwrap(), 1_u32);
}
#[test]
fn test_two_unnamed() {
let mut many = ManyVariants::Two(1, 2);
assert!(!many.is_one());
assert!(many.is_two());
assert!(!many.is_three());
assert!(many.as_one().is_none());
assert!(many.as_two().is_some());
assert!(many.as_three().is_none());
assert!(many.as_one_mut().is_none());
assert!(many.as_two_mut().is_some());
assert!(many.as_three_mut().is_none());
assert_eq!(many.as_two().unwrap(), (&1_u32, &2_i32));
assert_eq!(many.as_two_mut().unwrap(), (&mut 1_u32, &mut 2_i32));
assert_eq!(many.into_two().unwrap(), (1_u32, 2_i32));
}
#[test]
fn test_three_unnamed() {
let mut many = ManyVariants::Three(true, 1, 2);
assert!(!many.is_one());
assert!(!many.is_two());
assert!(many.is_three());
assert!(many.as_one().is_none());
assert!(many.as_two().is_none());
assert!(many.as_three().is_some());
assert!(many.as_one_mut().is_none());
assert!(many.as_two_mut().is_none());
assert!(many.as_three_mut().is_some());
assert_eq!(many.as_three().unwrap(), (&true, &1_u32, &2_i64));
assert_eq!(
many.as_three_mut().unwrap(),
(&mut true, &mut 1_u32, &mut 2_i64)
);
assert_eq!(many.into_three().unwrap(), (true, 1_u32, 2_i64));
}