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

+247
View File
@@ -0,0 +1,247 @@
//! Metadata for Espressif devices, primarily intended for use in build scripts.
use std::sync::OnceLock;
use anyhow::{bail, Result};
use strum::IntoEnumIterator;
macro_rules! include_toml {
($type:ty, $file:expr) => {{
static LOADED_TOML: OnceLock<$type> = OnceLock::new();
LOADED_TOML.get_or_init(|| basic_toml::from_str(include_str!($file)).unwrap())
}};
}
/// Supported device architectures.
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::AsRefStr,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum Arch {
/// RISC-V architecture
RiscV,
/// Xtensa architecture
Xtensa,
}
/// Device core count.
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::AsRefStr,
)]
pub enum Cores {
/// Single CPU core
#[serde(rename = "single_core")]
#[strum(serialize = "single_core")]
Single,
/// Two or more CPU cores
#[serde(rename = "multi_core")]
#[strum(serialize = "multi_core")]
Multi,
}
/// Supported devices.
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
serde::Deserialize,
serde::Serialize,
strum::Display,
strum::EnumIter,
strum::EnumString,
strum::AsRefStr,
)]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum Chip {
/// ESP32
Esp32,
/// ESP32-C2, ESP8684
Esp32c2,
/// ESP32-C3, ESP8685
Esp32c3,
/// ESP32-C6
Esp32c6,
/// ESP32-H2
Esp32h2,
/// ESP32-S2
Esp32s2,
/// ESP32-S3
Esp32s3,
}
impl Chip {
pub fn target(&self) -> &str {
use Chip::*;
match self {
Esp32 => "xtensa-esp32-none-elf",
Esp32c2 | Esp32c3 => "riscv32imc-unknown-none-elf",
Esp32c6 | Esp32h2 => "riscv32imac-unknown-none-elf",
Esp32s2 => "xtensa-esp32s2-none-elf",
Esp32s3 => "xtensa-esp32s3-none-elf",
}
}
pub fn has_lp_core(&self) -> bool {
use Chip::*;
matches!(self, Esp32c6 | Esp32s2 | Esp32s3)
}
pub fn lp_target(&self) -> Result<&str> {
use Chip::*;
match self {
Esp32c6 => Ok("riscv32imac-unknown-none-elf"),
Esp32s2 | Esp32s3 => Ok("riscv32imc-unknown-none-elf"),
_ => bail!("Chip does not contain an LP core: '{}'", self),
}
}
pub fn pretty_name(&self) -> &str {
match self {
Chip::Esp32 => "ESP32",
Chip::Esp32c2 => "ESP32-C2",
Chip::Esp32c3 => "ESP32-C3",
Chip::Esp32c6 => "ESP32-C6",
Chip::Esp32h2 => "ESP32-H2",
Chip::Esp32s2 => "ESP32-S2",
Chip::Esp32s3 => "ESP32-S3",
}
}
pub fn is_xtensa(&self) -> bool {
matches!(self, Chip::Esp32 | Chip::Esp32s2 | Chip::Esp32s3)
}
pub fn is_riscv(&self) -> bool {
!self.is_xtensa()
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
struct Device {
pub name: String,
pub arch: Arch,
pub cores: Cores,
pub peripherals: Vec<String>,
pub symbols: Vec<String>,
}
/// Device configuration file format.
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Config {
device: Device,
}
impl Config {
/// The configuration for the specified chip.
pub fn for_chip(chip: &Chip) -> &Self {
match chip {
Chip::Esp32 => include_toml!(Config, "../devices/esp32.toml"),
Chip::Esp32c2 => include_toml!(Config, "../devices/esp32c2.toml"),
Chip::Esp32c3 => include_toml!(Config, "../devices/esp32c3.toml"),
Chip::Esp32c6 => include_toml!(Config, "../devices/esp32c6.toml"),
Chip::Esp32h2 => include_toml!(Config, "../devices/esp32h2.toml"),
Chip::Esp32s2 => include_toml!(Config, "../devices/esp32s2.toml"),
Chip::Esp32s3 => include_toml!(Config, "../devices/esp32s3.toml"),
}
}
/// The name of the device.
pub fn name(&self) -> String {
self.device.name.clone()
}
/// The CPU architecture of the device.
pub fn arch(&self) -> Arch {
self.device.arch
}
/// The core count of the device.
pub fn cores(&self) -> Cores {
self.device.cores
}
/// The peripherals of the device.
pub fn peripherals(&self) -> &[String] {
&self.device.peripherals
}
/// User-defined symbols for the device.
pub fn symbols(&self) -> &[String] {
&self.device.symbols
}
/// All configuration values for the device.
pub fn all(&self) -> impl Iterator<Item = &str> + '_ {
[
self.device.name.as_str(),
self.device.arch.as_ref(),
self.device.cores.as_ref(),
]
.into_iter()
.chain(self.device.peripherals.iter().map(|s| s.as_str()))
.chain(self.device.symbols.iter().map(|s| s.as_str()))
}
/// Does the configuration contain `item`?
pub fn contains(&self, item: &str) -> bool {
self.all().any(|i| i == item)
}
/// Define all symbols for a given configuration.
pub fn define_symbols(&self) {
define_all_possible_symbols();
// Define all necessary configuration symbols for the configured device:
for symbol in self.all() {
println!("cargo:rustc-cfg={symbol}");
}
}
}
/// Defines all possible symbols that _could_ be output from this crate
/// regardless of the chosen configuration.
///
/// This is required to avoid triggering the unexpected-cfgs lint.
fn define_all_possible_symbols() {
for chip in Chip::iter() {
let config = Config::for_chip(&chip);
for symbol in config.all() {
// https://doc.rust-lang.org/cargo/reference/build-scripts.html#rustc-check-cfg
println!("cargo:rustc-check-cfg=cfg({})", symbol);
}
}
}