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:
17624 files changed
+4564928
-38
No files matched your search
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
//! A [serde]-compatible spanned Value
|
||||
//!
|
||||
//! This allows capturing the location, in bytes, for a value in the original parsed document for
|
||||
//! compatible deserializers.
|
||||
//!
|
||||
//! [serde]: https://serde.rs/
|
||||
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs)]
|
||||
// Makes rustc abort compilation if there are any unsafe blocks in the crate.
|
||||
// Presence of this annotation is picked up by tools such as cargo-geiger
|
||||
// and lets them ensure that there is indeed no unsafe code as opposed to
|
||||
// something they couldn't detect (e.g. unsafe added via macro expansion, etc).
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(clippy::print_stderr)]
|
||||
#![warn(clippy::print_stdout)]
|
||||
|
||||
mod spanned;
|
||||
pub use crate::spanned::Spanned;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "serde")]
|
||||
pub mod __unstable {
|
||||
pub use crate::spanned::is_spanned;
|
||||
pub use crate::spanned::END_FIELD;
|
||||
pub use crate::spanned::NAME;
|
||||
pub use crate::spanned::START_FIELD;
|
||||
pub use crate::spanned::VALUE_FIELD;
|
||||
}
|
||||
|
||||
#[doc = include_str!("../README.md")]
|
||||
#[cfg(doctest)]
|
||||
pub struct ReadmeDoctests;
|
||||
Vendored
+244
@@ -0,0 +1,244 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
// Currently serde itself doesn't have a spanned type, so we map our `Spanned`
|
||||
// to a special value in the serde data model. Namely one with these special
|
||||
// fields/struct names.
|
||||
//
|
||||
// In general, supported deserializers should catch this and not literally emit
|
||||
// these strings but rather emit `Spanned` as they're intended.
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "serde")]
|
||||
pub const NAME: &str = "$__serde_spanned_private_Spanned";
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "serde")]
|
||||
pub const START_FIELD: &str = "$__serde_spanned_private_start";
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "serde")]
|
||||
pub const END_FIELD: &str = "$__serde_spanned_private_end";
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "serde")]
|
||||
pub const VALUE_FIELD: &str = "$__serde_spanned_private_value";
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "serde")]
|
||||
pub fn is_spanned(name: &'static str, fields: &'static [&'static str]) -> bool {
|
||||
name == NAME && fields == [START_FIELD, END_FIELD, VALUE_FIELD]
|
||||
}
|
||||
|
||||
/// A spanned value, indicating the range at which it is defined in the source.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Spanned<T> {
|
||||
/// Byte range
|
||||
span: std::ops::Range<usize>,
|
||||
/// The spanned value.
|
||||
value: T,
|
||||
}
|
||||
|
||||
impl<T> Spanned<T> {
|
||||
/// Create a spanned value encompassing the given byte range.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Transposing a `Spanned<Enum<T>>` into `Enum<Spanned<T>>`:
|
||||
///
|
||||
/// ```
|
||||
/// use serde::de::{Deserialize, Deserializer};
|
||||
/// use serde_untagged::UntaggedEnumVisitor;
|
||||
/// use toml::Spanned;
|
||||
///
|
||||
/// pub enum Dependency {
|
||||
/// Simple(Spanned<String>),
|
||||
/// Detailed(Spanned<DetailedDependency>),
|
||||
/// }
|
||||
///
|
||||
/// impl<'de> Deserialize<'de> for Dependency {
|
||||
/// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
/// where
|
||||
/// D: Deserializer<'de>,
|
||||
/// {
|
||||
/// enum DependencyKind {
|
||||
/// Simple(String),
|
||||
/// Detailed(DetailedDependency),
|
||||
/// }
|
||||
///
|
||||
/// impl<'de> Deserialize<'de> for DependencyKind {
|
||||
/// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
/// where
|
||||
/// D: Deserializer<'de>,
|
||||
/// {
|
||||
/// UntaggedEnumVisitor::new()
|
||||
/// .expecting(
|
||||
/// "a version string like \"0.9.8\" or a \
|
||||
/// detailed dependency like { version = \"0.9.8\" }",
|
||||
/// )
|
||||
/// .string(|value| Ok(DependencyKind::Simple(value.to_owned())))
|
||||
/// .map(|value| value.deserialize().map(DependencyKind::Detailed))
|
||||
/// .deserialize(deserializer)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let spanned: Spanned<DependencyKind> = Deserialize::deserialize(deserializer)?;
|
||||
/// let range = spanned.span();
|
||||
/// Ok(match spanned.into_inner() {
|
||||
/// DependencyKind::Simple(simple) => Dependency::Simple(Spanned::new(range, simple)),
|
||||
/// DependencyKind::Detailed(detailed) => Dependency::Detailed(Spanned::new(range, detailed)),
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// #
|
||||
/// # type DetailedDependency = std::collections::BTreeMap<String, String>;
|
||||
/// ```
|
||||
pub fn new(range: std::ops::Range<usize>, value: T) -> Self {
|
||||
Spanned { span: range, value }
|
||||
}
|
||||
|
||||
/// Byte range
|
||||
pub fn span(&self) -> std::ops::Range<usize> {
|
||||
self.span.clone()
|
||||
}
|
||||
|
||||
/// Consumes the spanned value and returns the contained value.
|
||||
pub fn into_inner(self) -> T {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Returns a reference to the contained value.
|
||||
pub fn get_ref(&self) -> &T {
|
||||
&self.value
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the contained value.
|
||||
pub fn get_mut(&mut self) -> &mut T {
|
||||
&mut self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl std::borrow::Borrow<str> for Spanned<String> {
|
||||
fn borrow(&self) -> &str {
|
||||
self.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Spanned<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
self.get_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsMut<T> for Spanned<T> {
|
||||
fn as_mut(&mut self) -> &mut T {
|
||||
self.get_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialEq for Spanned<T> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.value.eq(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Eq> Eq for Spanned<T> {}
|
||||
|
||||
impl<T: Hash> Hash for Spanned<T> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.value.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialOrd> PartialOrd for Spanned<T> {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.value.partial_cmp(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Ord for Spanned<T> {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.value.cmp(&other.value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de, T> serde::de::Deserialize<'de> for Spanned<T>
|
||||
where
|
||||
T: serde::de::Deserialize<'de>,
|
||||
{
|
||||
fn deserialize<D>(deserializer: D) -> Result<Spanned<T>, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
struct SpannedVisitor<T>(::std::marker::PhantomData<T>);
|
||||
|
||||
impl<'de, T> serde::de::Visitor<'de> for SpannedVisitor<T>
|
||||
where
|
||||
T: serde::de::Deserialize<'de>,
|
||||
{
|
||||
type Value = Spanned<T>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a spanned value")
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut visitor: V) -> Result<Spanned<T>, V::Error>
|
||||
where
|
||||
V: serde::de::MapAccess<'de>,
|
||||
{
|
||||
let mut start: Option<usize> = None;
|
||||
let mut end: Option<usize> = None;
|
||||
let mut value: Option<T> = None;
|
||||
while let Some(key) = visitor.next_key()? {
|
||||
match key {
|
||||
START_FIELD => {
|
||||
if start.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field(START_FIELD));
|
||||
}
|
||||
start = Some(visitor.next_value()?);
|
||||
}
|
||||
END_FIELD => {
|
||||
if end.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field(END_FIELD));
|
||||
}
|
||||
end = Some(visitor.next_value()?);
|
||||
}
|
||||
VALUE_FIELD => {
|
||||
if value.is_some() {
|
||||
return Err(serde::de::Error::duplicate_field(VALUE_FIELD));
|
||||
}
|
||||
value = Some(visitor.next_value()?);
|
||||
}
|
||||
field => {
|
||||
return Err(serde::de::Error::unknown_field(
|
||||
field,
|
||||
&[START_FIELD, END_FIELD, VALUE_FIELD],
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
match (start, end, value) {
|
||||
(Some(start), Some(end), Some(value)) => Ok(Spanned {
|
||||
span: start..end,
|
||||
value,
|
||||
}),
|
||||
(None, _, _) => Err(serde::de::Error::missing_field(START_FIELD)),
|
||||
(_, None, _) => Err(serde::de::Error::missing_field(END_FIELD)),
|
||||
(_, _, None) => Err(serde::de::Error::missing_field(VALUE_FIELD)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static FIELDS: [&str; 3] = [START_FIELD, END_FIELD, VALUE_FIELD];
|
||||
|
||||
let visitor = SpannedVisitor(::std::marker::PhantomData);
|
||||
|
||||
deserializer.deserialize_struct(NAME, &FIELDS, visitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<T: serde::ser::Serialize> serde::ser::Serialize for Spanned<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
self.value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user