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

+15
View File
@@ -0,0 +1,15 @@
fn main() {
defmt::info!("hello");
}
#[defmt::global_logger]
struct Logger;
unsafe impl defmt::Logger for Logger {
fn acquire() {}
unsafe fn flush() {}
unsafe fn release() {}
unsafe fn write(_bytes: &[u8]) {}
}
defmt::timestamp!("{=u32}", 0);
+36
View File
@@ -0,0 +1,36 @@
extern crate defmt as defmt2;
fn main() {
let baz: Baz<Qux> = Default::default();
defmt::info!("{}", baz);
}
trait Foo {
type Bar;
}
#[derive(defmt::Format, Default)]
struct Baz<T: Foo> {
field: T::Bar,
field2: Quux<T>,
}
#[derive(defmt::Format, Default)]
struct Qux;
impl Foo for Qux {
type Bar = Qux;
}
#[allow(dead_code)]
#[derive(defmt::Format, Default)]
enum Quux<T: Foo> {
#[default]
None,
Variant1(T),
Variant2 {
f: T::Bar,
},
Variant3(T::Bar),
}
#[allow(dead_code)]
#[derive(defmt::Format)]
#[defmt(crate = defmt2)]
struct Quz;
+1011
View File
File diff suppressed because it is too large. Load diff
+15
View File
@@ -0,0 +1,15 @@
#[test]
fn ui() {
// only test error messages on the stable channel (nightly may change too often)
if rustc_version::version_meta()
.map(|meta| meta.channel == rustc_version::Channel::Stable)
.unwrap_or(false)
&& std::env::var_os("SKIP_UI_TESTS").is_none()
{
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
t.pass("tests/basic_usage.rs");
t.pass("tests/derive-bounds.rs");
}
}
+7
View File
@@ -0,0 +1,7 @@
#[derive(defmt::Format)]
struct S {
#[defmt()]
f: bool,
}
fn main() {}
+5
View File
@@ -0,0 +1,5 @@
error: expected 1 attribute argument
--> $DIR/derive-empty-attr.rs:3:7
|
3 | #[defmt()]
| ^^^^^^^
+7
View File
@@ -0,0 +1,7 @@
#[derive(defmt::Format)]
#[defmt(FooBar)]
struct S {
f: bool,
}
fn main() {}
@@ -0,0 +1,5 @@
error: unknown defmt attribute `FooBar`
--> tests/ui/derive-invalid-attr-arg-item.rs:2:9
|
2 | #[defmt(FooBar)]
| ^^^^^^
+7
View File
@@ -0,0 +1,7 @@
#[derive(defmt::Format)]
struct S {
#[defmt(FooBar)]
f: bool,
}
fn main() {}
+5
View File
@@ -0,0 +1,5 @@
error: expected `Debug2Format` or `Display2Format`
--> $DIR/derive-invalid-attr-arg.rs:3:13
|
3 | #[defmt(FooBar)]
| ^^^^^^
+13
View File
@@ -0,0 +1,13 @@
#[derive(defmt::Format)]
#[defmt(crate = unresolved)]
struct S {}
#[derive(defmt::Format)]
#[defmt(crate = "not a path")]
struct S2 {}
#[derive(defmt::Format)]
#[defmt(crate(defmt))]
struct S3 {}
fn main() {}
@@ -0,0 +1,25 @@
error: expected identifier
--> tests/ui/derive-invalid-crate-overwrite.rs:6:17
|
6 | #[defmt(crate = "not a path")]
| ^^^^^^^^^^^^
error: expected `=`
--> tests/ui/derive-invalid-crate-overwrite.rs:10:14
|
10 | #[defmt(crate(defmt))]
| ^
error[E0432]: unresolved import `unresolved`
--> tests/ui/derive-invalid-crate-overwrite.rs:1:10
|
1 | #[derive(defmt::Format)]
| ^^^^^^^^^^^^^ no external crate `unresolved`
|
= note: this error originates in the derive macro `defmt::Format` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0433]: failed to resolve: use of undeclared crate or module `unresolved`
--> tests/ui/derive-invalid-crate-overwrite.rs:2:17
|
2 | #[defmt(crate = unresolved)]
| ^^^^^^^^^^ use of undeclared crate or module `unresolved`
+8
View File
@@ -0,0 +1,8 @@
#[derive(defmt::Format)]
struct S {
#[defmt(Debug2Format)]
#[defmt()]
f: bool,
}
fn main() {}
+7
View File
@@ -0,0 +1,7 @@
error: multiple `defmt` attributes not supported
--> $DIR/derive-multi-attr.rs:3:5
|
3 | / #[defmt(Debug2Format)]
4 | | #[defmt()]
5 | | f: bool,
| |___________^
+3
View File
@@ -0,0 +1,3 @@
fn main() {
defmt::info!("{=u8:dunno}", 42)
}
+5
View File
@@ -0,0 +1,5 @@
error: unknown display hint: "dunno"
--> $DIR/log-invalid-hint.rs:2:18
|
2 | defmt::info!("{=u8:dunno}", 42)
| ^^^^^^^^^^^^^
+5
View File
@@ -0,0 +1,5 @@
#![no_main]
#![no_std]
#[defmt::panic_handler]
fn foo(x: bool) -> ! { info!("{:?}", x); loop {} }
+5
View File
@@ -0,0 +1,5 @@
error: function must have signature `fn() -> !`
--> $DIR/panic-handler-argument.rs:5:4
|
5 | fn foo(x: bool) -> ! { info!("{:?}", x); loop {} }
| ^^^
+5
View File
@@ -0,0 +1,5 @@
#![no_main]
#![no_std]
#[defmt::panic_handler]
fn foo() {}
+5
View File
@@ -0,0 +1,5 @@
error: function must have signature `fn() -> !`
--> $DIR/panic-handler-empty.rs:5:4
|
5 | fn foo() {}
| ^^^
+8
View File
@@ -0,0 +1,8 @@
#![no_main]
#![no_std]
#[defmt::panic_handler]
#[export_name = "hello"]
fn foo() -> ! {
loop {}
}
@@ -0,0 +1,5 @@
error: `#[panic_handler]` attribute cannot be used together with `#[export_name]`
--> $DIR/panic-handler-export-name.rs:5:1
|
5 | #[export_name = "hello"]
| ^^^^^^^^^^^^^^^^^^^^^^^^
+8
View File
@@ -0,0 +1,8 @@
#![no_main]
#![no_std]
#[defmt::panic_handler]
#[no_mangle]
fn panic() -> ! {
loop {}
}
+5
View File
@@ -0,0 +1,5 @@
error: `#[panic_handler]` attribute cannot be used together with `#[no_mangle]`
--> $DIR/panic-handler-no-mangle.rs:5:1
|
5 | #[no_mangle]
| ^^^^^^^^^^^^
+9
View File
@@ -0,0 +1,9 @@
struct S;
impl defmt::Format for S {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "{=u8:dunno}", 42)
}
}
fn main() {}
+5
View File
@@ -0,0 +1,5 @@
error: unknown display hint: "dunno"
--> $DIR/write-invalid-hint.rs:5:26
|
5 | defmt::write!(f, "{=u8:dunno}", 42)
| ^^^^^^^^^^^^^