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

+96
View File
@@ -0,0 +1,96 @@
use defmt::trace;
use embedded_hal_async::spi::{Operation, SpiDevice};
use crate::mod_params::RadioError::{self, SPI};
use crate::mod_traits::InterfaceVariant;
pub(crate) struct SpiInterface<SPI, IV> {
pub(crate) spi: SPI,
pub(crate) iv: IV,
}
impl<SPI, IV> SpiInterface<SPI, IV>
where
SPI: SpiDevice<u8>,
IV: InterfaceVariant,
{
pub fn new(spi: SPI, iv: IV) -> Self {
Self { spi, iv }
}
// Write a buffer to the radio.
pub async fn write(&mut self, write_buffer: &[u8], is_sleep_command: bool) -> Result<(), RadioError> {
self.spi.write(write_buffer).await.map_err(|_| SPI)?;
trace!("write: {=[u8]:02x}", write_buffer);
if !is_sleep_command {
self.iv.wait_on_busy().await?;
}
Ok(())
}
// Write
pub async fn write_with_payload(
&mut self,
write_buffer: &[u8],
payload: &[u8],
is_sleep_command: bool,
) -> Result<(), RadioError> {
let mut ops = [Operation::Write(write_buffer), Operation::Write(payload)];
self.spi.transaction(&mut ops).await.map_err(|_| SPI)?;
trace!("write_buf: {=[u8]:02x} -> {=[u8]:02x}", write_buffer, payload);
if !is_sleep_command {
self.iv.wait_on_busy().await?;
}
Ok(())
}
// Request a read, filling the provided buffer.
pub async fn read(&mut self, write_buffer: &[u8], read_buffer: &mut [u8]) -> Result<(), RadioError> {
{
let mut ops = [Operation::Write(write_buffer), Operation::Read(read_buffer)];
self.spi.transaction(&mut ops).await.map_err(|_| SPI)?;
}
self.iv.wait_on_busy().await?;
trace!(
"read: addr={=[u8]:02x}, len={}, data={=[u8]:02x}",
write_buffer,
read_buffer.len(),
read_buffer
);
Ok(())
}
// Request a read with status, filling the provided buffer and returning the status.
pub async fn read_with_status(&mut self, write_buffer: &[u8], read_buffer: &mut [u8]) -> Result<u8, RadioError> {
let mut status = [0u8];
{
let mut ops = [
Operation::Write(write_buffer),
Operation::Read(&mut status),
Operation::Read(read_buffer),
];
self.spi.transaction(&mut ops).await.map_err(|_| SPI)?;
}
self.iv.wait_on_busy().await?;
trace!(
"read: addr={=[u8]:02x}, len={}, status={:02x}, buf={=[u8]:02x}",
write_buffer,
read_buffer.len(),
status[0],
read_buffer
);
Ok(status[0])
}
}