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

+8
View File
@@ -0,0 +1,8 @@
#[maybe_async_cfg::maybe(sync(feature = "is_sync"), async(not(feature = "is_sync")))]
#[allow(dead_code)]
trait T {
/// This is a doctest
fn f() {
todo!()
}
}
+15
View File
@@ -0,0 +1,15 @@
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.pass("tests/ui/01-maybe-async.rs");
t.pass("tests/ui/02-must-be-async.rs");
t.pass("tests/ui/03-must-be-sync.rs");
t.pass("tests/ui/04-unit-test-util.rs");
t.pass("tests/ui/05-replace-future-generic-type-with-output.rs");
t.pass("tests/ui/06-sync_impl_async_impl.rs");
t.compile_fail("tests/ui/test_fail/01-empty-test.rs");
t.compile_fail("tests/ui/test_fail/02-unknown-path.rs");
t.compile_fail("tests/ui/test_fail/03-async-gt2.rs");
t.compile_fail("tests/ui/test_fail/04-bad-sync-cond.rs");
}
+64
View File
@@ -0,0 +1,64 @@
#![allow(dead_code)]
#[maybe_async_cfg::maybe(send="Send", sync(feature = "is_sync"), async(not(feature = "is_sync"), async_trait::async_trait))]
trait Trait {
fn sync_fn() {}
async fn declare_async(&self);
async fn async_fn(&self) {
self.declare_async().await
}
}
#[maybe_async_cfg::maybe(send="?Send", sync(feature = "is_sync"), async(not(feature = "is_sync"), async_trait::async_trait))]
pub trait PubTrait {
fn sync_fn() {}
async fn declare_async(&self);
async fn async_fn(&self) {
self.declare_async().await
}
}
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"), async(not(feature = "is_sync")))]
async fn async_fn() {}
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"), async(not(feature = "is_sync")))]
pub async fn pub_async_fn() {
}
struct Struct;
#[maybe_async_cfg::maybe(keep_self, idents(Trait), sync(feature = "is_sync"), async(not(feature = "is_sync"), async_trait::async_trait))]
impl Trait for Struct {
fn sync_fn() {}
async fn declare_async(&self) {}
async fn async_fn(&self) {
async { self.declare_async().await }.await
}
}
#[cfg(feature = "is_sync")]
fn main() -> std::result::Result<(), ()> {
let s = Struct;
s.declare_async();
s.async_fn();
async_fn();
pub_async_fn();
Ok(())
}
#[cfg(not(feature = "is_sync"))]
#[async_std::main]
async fn main() {
let s = Struct;
s.declare_async().await;
s.async_fn().await;
async_fn().await;
pub_async_fn().await;
}
+76
View File
@@ -0,0 +1,76 @@
#![allow(dead_code)]
#[maybe_async_cfg::maybe(send="Send", async(not(feature = "is_sync"), async_trait::async_trait))]
trait Trait {
fn sync_fn() {}
async fn declare_async(&self);
async fn async_fn(&self) {
self.declare_async().await
}
}
#[maybe_async_cfg::maybe(send="?Send", async(not(feature = "is_sync"), async_trait::async_trait))]
trait NotSendTrait {
async fn declare_async_not_send(&self);
async fn async_fn_not_send(&self) {
self.declare_async_not_send().await
}
}
#[maybe_async_cfg::maybe(send="Send", async(not(feature = "is_sync"), async_trait::async_trait))]
pub trait PubTrait {
fn sync_fn() {}
async fn declare_async(&self);
async fn async_fn(&self) {
self.declare_async().await
}
}
#[maybe_async_cfg::maybe(keep_self, async(not(feature = "is_sync")))]
async fn async_fn() {}
#[maybe_async_cfg::maybe(keep_self, async(not(feature = "is_sync")))]
pub async fn pub_async_fn() {}
struct Struct;
#[maybe_async_cfg::maybe(keep_self, idents(Trait), async(not(feature = "is_sync"), async_trait::async_trait))]
impl Trait for Struct {
fn sync_fn() {}
async fn declare_async(&self) {}
async fn async_fn(&self) {
async { self.declare_async().await }.await
}
}
#[maybe_async_cfg::maybe(keep_self, idents(NotSendTrait), send="?Send", async(not(feature = "is_sync"), async_trait::async_trait))]
impl NotSendTrait for Struct {
async fn declare_async_not_send(&self) {}
async fn async_fn_not_send(&self) {
async { self.declare_async_not_send().await }.await
}
}
#[cfg(feature = "is_sync")]
fn main() {}
#[cfg(not(feature = "is_sync"))]
#[async_std::main]
async fn main() {
let s = Struct;
s.declare_async().await;
s.async_fn().await;
async_fn().await;
pub_async_fn().await;
}
+57
View File
@@ -0,0 +1,57 @@
#![allow(dead_code)]
#[maybe_async_cfg::maybe(sync(feature = "is_sync"))]
trait Trait {
fn sync_fn() {}
async fn declare_async(&self);
async fn async_fn(&self) {
self.declare_async().await
}
}
#[maybe_async_cfg::maybe(sync(feature = "is_sync"))]
pub trait PubTrait {
fn sync_fn() {}
async fn declare_async(&self);
async fn async_fn(&self) {
self.declare_async().await
}
}
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"))]
async fn async_fn() {}
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"))]
pub async fn pub_async_fn() {}
struct Struct;
#[maybe_async_cfg::maybe(keep_self, idents(Trait), sync(feature = "is_sync"))]
impl Trait for Struct {
fn sync_fn() {}
async fn declare_async(&self) {}
async fn async_fn(&self) {
async { self.declare_async().await }.await
}
}
#[cfg(feature = "is_sync")]
fn main() -> std::result::Result<(), ()> {
let s = Struct;
s.declare_async();
s.async_fn();
async_fn();
pub_async_fn();
Ok(())
}
#[cfg(not(feature = "is_sync"))]
#[async_std::main]
async fn main() {}
+37
View File
@@ -0,0 +1,37 @@
#[maybe_async_cfg::maybe(sync(feature = "is_sync"), async(not(feature = "is_sync")))]
async fn async_fn() -> bool {
true
}
#[maybe_async_cfg::maybe(
idents(async_fn),
sync(feature = "is_sync", test),
async(all(not(feature="is_sync"), feature = "async_std"), async_std::test),
async(all(not(feature="is_sync"), feature = "tokio"), tokio::test)
)]
async fn test_async_fn() {
let res = async_fn().await;
assert_eq!(res, true);
}
#[maybe_async_cfg::maybe(sync(feature = "is_sync", test), async(not(feature = "is_sync"), async_std::test))]
async fn test_async_fn2() {
let res = async_fn().await;
assert_eq!(res, true);
}
#[maybe_async_cfg::maybe(sync(feature = "is_sync", test), async(not(feature = "is_sync"), async_std::test))]
async fn test_async_fn3() {
let res = async_fn().await;
assert_eq!(res, true);
}
#[maybe_async_cfg::maybe(sync(feature = "is_sync", test), async(not(feature = "is_sync"), async_std::test))]
async fn test_async_fn4() {
let res = async_fn().await;
assert_eq!(res, true);
}
fn main() {
}
@@ -0,0 +1,34 @@
#![allow(unused_imports)]
use std::future::Future;
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"), async(not(feature = "is_sync")))]
pub async fn with_fn<T, F: Sync + std::future::Future<Output = Result<(), ()>>>(
test: T,
) -> Result<(), ()>
where
T: FnOnce() -> F,
{
test().await
}
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"), async(not(feature = "is_sync")))]
pub async fn with_fn_where<T, F>(test: T) -> Result<(), ()>
where
T: FnOnce() -> F,
F: Sync + Future<Output = Result<(), ()>>,
{
test().await
}
#[maybe_async_cfg::maybe(keep_self, sync(feature = "is_sync"))]
fn main() {
with_fn(|| Ok(())).unwrap();
with_fn_where(|| Ok(())).unwrap();
}
#[maybe_async_cfg::maybe(keep_self, async(not(feature = "is_sync")))]
#[tokio::main]
async fn main() {
with_fn(|| async { Ok(()) }).await.unwrap();
with_fn_where(|| async { Ok(()) }).await.unwrap();
}
@@ -0,0 +1,48 @@
#![allow(dead_code, unused_variables)]
/// InnerClient differ a lot between sync and async.
#[maybe_async_cfg::maybe(sync(feature = "is_sync"), async(not(feature = "is_sync"), async_trait::async_trait))]
trait Trait {
async fn maybe_async_fn();
}
/// The higher level API for end user.
pub struct Struct;
/// Synchronous implementation, only compiles when `is_sync` feature is off.
/// Else the compiler will complain that *request is defined multiple times* and
/// blabla.
#[maybe_async_cfg::maybe(keep_self, idents(Trait), sync(feature = "is_sync"), async(not(feature = "is_sync")))]
#[maybe_async::only_if(key="sync")]
impl Trait for Struct {
fn maybe_async_fn() { }
}
/// Asynchronous implementation, only compiles when `is_sync` feature is off.
#[maybe_async_cfg::maybe(keep_self, idents(Trait), sync(feature = "is_sync"), async(not(feature = "is_sync"), async_trait::async_trait))]
#[maybe_async::only_if(key="async")]
impl Trait for Struct {
async fn maybe_async_fn() { }
}
#[maybe_async_cfg::maybe(keep_self, idents(Trait, another_maybe_async_fn(fn)), sync(feature = "is_sync"), async(not(feature = "is_sync")))]
impl Struct {
async fn another_maybe_async_fn() {
<Self as Trait>::maybe_async_fn().await
// When `is_sync` is toggle on, this block will compiles to:
// Self::maybe_async_fn()
}
}
#[maybe_async_cfg::maybe(keep_self, idents(another_maybe_async_fn(fn)), sync(feature = "is_sync"), async(not(feature = "is_sync")))]
#[maybe_async::only_if(key="sync")]
fn main() {
let _ = Struct::another_maybe_async_fn();
}
#[maybe_async_cfg::maybe(keep_self, idents(another_maybe_async_fn(fn)), sync(feature = "is_sync"), async(not(feature = "is_sync")))]
#[maybe_async::only_if(key="async")]
#[tokio::main]
async fn main() {
let _ = Struct::another_maybe_async_fn().await;
}
@@ -0,0 +1,20 @@
#[maybe_async_cfg::maybe(
sync(feature = "is_sync"),
)]
async fn async_fn() -> bool {
true
}
#[maybe_async_cfg::maybe(
idents(async_fn(fn)),
sync(feature = "is_sync"),
async(not(feature = "is_sync")),
)]
async fn test_async_fn() {
let res = async_fn().await;
assert_eq!(res, true);
}
fn main() {
}
@@ -0,0 +1,5 @@
error[E0425]: cannot find function `async_fn_async` in this scope
--> tests/ui/test_fail/01-empty-test.rs:14:15
|
14 | let res = async_fn().await;
| ^^^^^^^^ not found in this scope
@@ -0,0 +1,21 @@
#[maybe_async_cfg::maybe(
sync(feature = "is_sync"),
unknown(not(feature = "is_sync")),
)]
async fn async_fn() -> bool {
true
}
#[maybe_async_cfg::maybe(
idents(async_fn(fn)),
sync(feature = "is_sync"),
async(not(feature = "is_sync")),
)]
async fn test_async_fn() {
let res = async_fn().await;
assert_eq!(res, true);
}
fn main() {
}
@@ -0,0 +1,5 @@
error[E0425]: cannot find function `async_fn_async` in this scope
--> tests/ui/test_fail/02-unknown-path.rs:15:15
|
15 | let res = async_fn().await;
| ^^^^^^^^ not found in this scope
@@ -0,0 +1,23 @@
#[maybe_async_cfg::maybe(
sync(feature = "is_sync"),
async(not(feature = "is_sync")),
)]
async fn async_fn() -> bool {
true
}
//#[maybe_async::test(feature="is_sync", async(feature="async", async_std::test, added))]
#[maybe_async_cfg::maybe(
idents(async_fn(fn)),
sync(feature = "is_sync"),
async(not(feature = "is_sync")),
async(not(feature = "is_sync")),
)]
async fn test_async_fn() {
let res = async_fn().await;
assert_eq!(res, true);
}
fn main() {
}
@@ -0,0 +1,10 @@
error[E0428]: the name `test_async_fn_async` is defined multiple times
--> tests/ui/test_fail/03-async-gt2.rs:16:1
|
16 | async fn test_async_fn() {
| ^^^^^^^^^^^^^^^^^^^^^^^^
| |
| `test_async_fn_async` redefined here
| previous definition of the value `test_async_fn_async` here
|
= note: `test_async_fn_async` must be defined only once in the value namespace of this module
@@ -0,0 +1,23 @@
#[maybe_async_cfg::maybe(
sync(feature = "is_sync"),
async(not(feature = "is_sync")),
)]
async fn async_fn() -> bool {
true
}
// bad sync condition
//#[maybe_async::test(unknown(feature="async", async_std::test))]
#[maybe_async_cfg::maybe(
idents(async_fn(fn)),
sync(unknown = "is_sync"),
async(not(feature = "is_sync")),
)]
async fn test_async_fn() {
let res = async_fn().await;
assert_eq!(res, true);
}
fn main() {
}
@@ -0,0 +1,5 @@
error: Wrong name for name-value pair: unknown
--> tests/ui/test_fail/04-bad-sync-cond.rs:13:10
|
13 | sync(unknown = "is_sync"),
| ^^^^^^^^^^^^^^^^^^^
+36
View File
@@ -0,0 +1,36 @@
#[maybe_async_cfg::maybe(sync(feature = "is_sync"), async(not(feature = "is_sync")))]
async fn some_function() -> bool {
true
}
#[maybe_async_cfg::maybe(
idents(some_function(fn)),
sync(feature = "is_sync", test),
async(not(feature = "is_sync"), async_std::test)
)]
async fn test_async_fn() {
let res = some_function().await;
assert_eq!(res, true);
}
#[maybe_async_cfg::maybe(
idents(some_function(fn)),
sync(feature = "is_sync", test),
async(not(feature = "is_sync"), tokio::test)
)]
async fn test_async_fn2() {
let res = some_function().await;
assert_eq!(res, true);
}
#[maybe_async_cfg::maybe(idents(some_function(fn)), sync(feature = "is_sync", test))]
async fn test_async_fn3() {
let res = some_function().await;
assert_eq!(res, true);
}
#[maybe_async_cfg::maybe(idents(some_function(fn)), sync(feature = "is_sync", test))]
async fn test_sync_fn() {
let res = some_function();
assert_eq!(res, true);
}