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
+365
@@ -0,0 +1,365 @@
|
||||
use std::fmt;
|
||||
use std::str::from_utf8;
|
||||
|
||||
use recognize::*;
|
||||
|
||||
use common::{self, numeric_identifier};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Version {
|
||||
pub major: u64,
|
||||
pub minor: u64,
|
||||
pub patch: u64,
|
||||
pub pre: Vec<Identifier>,
|
||||
pub build: Vec<Identifier>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum Identifier {
|
||||
/// An identifier that's solely numbers.
|
||||
Numeric(u64),
|
||||
/// An identifier with letters and numbers.
|
||||
AlphaNumeric(String),
|
||||
}
|
||||
|
||||
pub fn parse(version: &str) -> Result<Version, String> {
|
||||
let s = version.trim().as_bytes();
|
||||
let mut i = 0;
|
||||
let major = if let Some((major, len)) = numeric_identifier(&s[i..]) {
|
||||
i += len;
|
||||
major
|
||||
} else {
|
||||
return Err("Error parsing major identifier".to_string());
|
||||
};
|
||||
if let Some(len) = b'.'.p(&s[i..]) {
|
||||
i += len;
|
||||
} else {
|
||||
return Err("Expected dot".to_string());
|
||||
}
|
||||
let minor = if let Some((minor, len)) = numeric_identifier(&s[i..]) {
|
||||
i += len;
|
||||
minor
|
||||
} else {
|
||||
return Err("Error parsing minor identifier".to_string());
|
||||
};
|
||||
if let Some(len) = b'.'.p(&s[i..]) {
|
||||
i += len;
|
||||
} else {
|
||||
return Err("Expected dot".to_string());
|
||||
}
|
||||
let patch = if let Some((patch, len)) = numeric_identifier(&s[i..]) {
|
||||
i += len;
|
||||
patch
|
||||
} else {
|
||||
return Err("Error parsing patch identifier".to_string());
|
||||
};
|
||||
let (pre, pre_len) = common::parse_optional_meta(&s[i..], b'-')?;
|
||||
i += pre_len;
|
||||
let (build, build_len) = common::parse_optional_meta(&s[i..], b'+')?;
|
||||
i += build_len;
|
||||
if i != s.len() {
|
||||
return Err("Extra junk after valid version: ".to_string() +
|
||||
from_utf8(&s[i..]).unwrap());
|
||||
}
|
||||
Ok(Version {
|
||||
major: major,
|
||||
minor: minor,
|
||||
patch: patch,
|
||||
pre: pre,
|
||||
build: build,
|
||||
})
|
||||
}
|
||||
|
||||
impl fmt::Display for Version {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
try!(write!(f, "{}.{}.{}", self.major, self.minor, self.patch));
|
||||
if !self.pre.is_empty() {
|
||||
let strs: Vec<_> =
|
||||
self.pre.iter().map(ToString::to_string).collect();
|
||||
try!(write!(f, "-{}", strs.join(".")));
|
||||
}
|
||||
if !self.build.is_empty() {
|
||||
let strs: Vec<_> =
|
||||
self.build.iter().map(ToString::to_string).collect();
|
||||
try!(write!(f, "+{}", strs.join(".")));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Identifier {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Identifier::Numeric(ref id) => id.fmt(f),
|
||||
Identifier::AlphaNumeric(ref id) => id.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use version;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_empty() {
|
||||
let version = "";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "empty string incorrectly considered a valid parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_blank() {
|
||||
let version = " ";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "blank string incorrectly considered a valid parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_minor_patch() {
|
||||
let version = "1";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_patch() {
|
||||
let version = "1.2";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pre() {
|
||||
let version = "1.2.3-";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_letters() {
|
||||
let version = "a.b.c";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_with_letters() {
|
||||
let version = "1.2.3 a.b.c";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), format!("'{}' incorrectly considered a valid parse", version));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_version() {
|
||||
let version = "1.2.3";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
assert_eq!(1, parsed.major);
|
||||
assert_eq!(2, parsed.minor);
|
||||
assert_eq!(3, parsed.patch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_trims_input() {
|
||||
let version = " 1.2.3 ";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
assert_eq!(1, parsed.major);
|
||||
assert_eq!(2, parsed.minor);
|
||||
assert_eq!(3, parsed.patch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_major_leading_zeroes() {
|
||||
let version = "01.0.0";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "01 incorrectly considered a valid major version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_minor_leading_zeroes() {
|
||||
let version = "0.01.0";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "01 incorrectly considered a valid minor version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_patch_leading_zeroes() {
|
||||
let version = "0.0.01";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "01 incorrectly considered a valid patch version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_major_overflow() {
|
||||
let version = "98765432109876543210.0.0";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid major version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_minor_overflow() {
|
||||
let version = "0.98765432109876543210.0";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid minor version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_no_patch_overflow() {
|
||||
let version = "0.0.98765432109876543210";
|
||||
|
||||
let parsed = version::parse(version);
|
||||
|
||||
assert!(parsed.is_err(), "98765432109876543210 incorrectly considered a valid patch version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_prerelease() {
|
||||
let version = "1.2.3-pre";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("pre"))];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_prerelease_alphanumeric() {
|
||||
let version = "1.2.3-alpha1";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("alpha1"))];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_prerelease_zero() {
|
||||
let version = "1.2.3-pre.0";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("pre")),
|
||||
Identifier::Numeric(0)];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_build() {
|
||||
let version = "1.2.3+build";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_build = vec![Identifier::AlphaNumeric(String::from("build"))];
|
||||
assert_eq!(expected_build, parsed.build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_build_alphanumeric() {
|
||||
let version = "1.2.3+build5";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_build = vec![Identifier::AlphaNumeric(String::from("build5"))];
|
||||
assert_eq!(expected_build, parsed.build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pre_and_build() {
|
||||
let version = "1.2.3-alpha1+build5";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("alpha1"))];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
|
||||
let expected_build = vec![Identifier::AlphaNumeric(String::from("build5"))];
|
||||
assert_eq!(expected_build, parsed.build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_complex_metadata_01() {
|
||||
let version = "1.2.3-1.alpha1.9+build5.7.3aedf ";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::Numeric(1),
|
||||
Identifier::AlphaNumeric(String::from("alpha1")),
|
||||
Identifier::Numeric(9)];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
|
||||
let expected_build = vec![Identifier::AlphaNumeric(String::from("build5")),
|
||||
Identifier::Numeric(7),
|
||||
Identifier::AlphaNumeric(String::from("3aedf"))];
|
||||
assert_eq!(expected_build, parsed.build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_complex_metadata_02() {
|
||||
let version = "0.4.0-beta.1+0851523";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("beta")),
|
||||
Identifier::Numeric(1)];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
|
||||
let expected_build = vec![Identifier::AlphaNumeric(String::from("0851523"))];
|
||||
assert_eq!(expected_build, parsed.build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_metadata_overflow() {
|
||||
let version = "0.4.0-beta.1+98765432109876543210";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("beta")),
|
||||
Identifier::Numeric(1)];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
|
||||
let expected_build = vec![Identifier::AlphaNumeric(String::from("98765432109876543210"))];
|
||||
assert_eq!(expected_build, parsed.build);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_regression_01() {
|
||||
let version = "0.0.0-WIP";
|
||||
|
||||
let parsed = version::parse(version).unwrap();
|
||||
|
||||
assert_eq!(0, parsed.major);
|
||||
assert_eq!(0, parsed.minor);
|
||||
assert_eq!(0, parsed.patch);
|
||||
|
||||
let expected_pre = vec![Identifier::AlphaNumeric(String::from("WIP"))];
|
||||
assert_eq!(expected_pre, parsed.pre);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user