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
+190
@@ -0,0 +1,190 @@
|
||||
#![allow(
|
||||
clippy::elidable_lifetime_names,
|
||||
clippy::let_underscore_untyped,
|
||||
clippy::manual_range_contains,
|
||||
clippy::needless_lifetimes,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::type_complexity
|
||||
)]
|
||||
|
||||
#[path = "../src/tokens.rs"]
|
||||
#[allow(dead_code)]
|
||||
mod tokens;
|
||||
|
||||
use crate::tokens::{Error, Token, Tokenizer};
|
||||
use std::borrow::Cow;
|
||||
|
||||
fn err(input: &str, err: Error) {
|
||||
let mut t = Tokenizer::new(input);
|
||||
let token = t.next().unwrap_err();
|
||||
assert_eq!(token, err);
|
||||
assert!(t.next().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_strings() {
|
||||
fn t(input: &str, val: &str, multiline: bool) {
|
||||
let mut t = Tokenizer::new(input);
|
||||
let (_, token) = t.next().unwrap().unwrap();
|
||||
assert_eq!(
|
||||
token,
|
||||
Token::String {
|
||||
src: input,
|
||||
val: Cow::Borrowed(val),
|
||||
multiline,
|
||||
}
|
||||
);
|
||||
assert!(t.next().unwrap().is_none());
|
||||
}
|
||||
|
||||
t("''", "", false);
|
||||
t("''''''", "", true);
|
||||
t("'''\n'''", "", true);
|
||||
t("'a'", "a", false);
|
||||
t("'\"a'", "\"a", false);
|
||||
t("''''a'''", "'a", true);
|
||||
t("'''\n'a\n'''", "'a\n", true);
|
||||
t("'''a\n'a\r\n'''", "a\n'a\n", true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_strings() {
|
||||
fn t(input: &str, val: &str, multiline: bool) {
|
||||
let mut t = Tokenizer::new(input);
|
||||
let (_, token) = t.next().unwrap().unwrap();
|
||||
assert_eq!(
|
||||
token,
|
||||
Token::String {
|
||||
src: input,
|
||||
val: Cow::Borrowed(val),
|
||||
multiline,
|
||||
}
|
||||
);
|
||||
assert!(t.next().unwrap().is_none());
|
||||
}
|
||||
|
||||
t(r#""""#, "", false);
|
||||
t(r#""""""""#, "", true);
|
||||
t(r#""a""#, "a", false);
|
||||
t(r#""""a""""#, "a", true);
|
||||
t(r#""\t""#, "\t", false);
|
||||
t(r#""\u0000""#, "\0", false);
|
||||
t(r#""\U00000000""#, "\0", false);
|
||||
t(r#""\U000A0000""#, "\u{A0000}", false);
|
||||
t(r#""\\t""#, "\\t", false);
|
||||
t("\"\t\"", "\t", false);
|
||||
t("\"\"\"\n\t\"\"\"", "\t", true);
|
||||
t("\"\"\"\\\n\"\"\"", "", true);
|
||||
t(
|
||||
"\"\"\"\\\n \t \t \\\r\n \t \n \t \r\n\"\"\"",
|
||||
"",
|
||||
true,
|
||||
);
|
||||
t(r#""\r""#, "\r", false);
|
||||
t(r#""\n""#, "\n", false);
|
||||
t(r#""\b""#, "\u{8}", false);
|
||||
t(r#""a\fa""#, "a\u{c}a", false);
|
||||
t(r#""\"a""#, "\"a", false);
|
||||
t("\"\"\"\na\"\"\"", "a", true);
|
||||
t("\"\"\"\n\"\"\"", "", true);
|
||||
t(r#""""a\"""b""""#, "a\"\"\"b", true);
|
||||
err(r#""\a"#, Error::InvalidEscape(2, 'a'));
|
||||
err("\"\\\n", Error::InvalidEscape(2, '\n'));
|
||||
err("\"\\\r\n", Error::InvalidEscape(2, '\n'));
|
||||
err("\"\\", Error::UnterminatedString(0));
|
||||
err("\"\u{0}", Error::InvalidCharInString(1, '\u{0}'));
|
||||
err(r#""\U00""#, Error::InvalidHexEscape(5, '"'));
|
||||
err(r#""\U00"#, Error::UnterminatedString(0));
|
||||
err(r#""\uD800"#, Error::InvalidEscapeValue(2, 0xd800));
|
||||
err(r#""\UFFFFFFFF"#, Error::InvalidEscapeValue(2, 0xffff_ffff));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keylike() {
|
||||
fn t(input: &str) {
|
||||
let mut t = Tokenizer::new(input);
|
||||
let (_, token) = t.next().unwrap().unwrap();
|
||||
assert_eq!(token, Token::Keylike(input));
|
||||
assert!(t.next().unwrap().is_none());
|
||||
}
|
||||
t("foo");
|
||||
t("0bar");
|
||||
t("bar0");
|
||||
t("1234");
|
||||
t("a-b");
|
||||
t("a_B");
|
||||
t("-_-");
|
||||
t("___");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all() {
|
||||
fn t(input: &str, expected: &[((usize, usize), Token, &str)]) {
|
||||
let mut tokens = Tokenizer::new(input);
|
||||
let mut actual: Vec<((usize, usize), Token, &str)> = Vec::new();
|
||||
while let Some((span, token)) = tokens.next().unwrap() {
|
||||
actual.push((span.into(), token, &input[span.start..span.end]));
|
||||
}
|
||||
for (a, b) in actual.iter().zip(expected) {
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
assert_eq!(actual.len(), expected.len());
|
||||
}
|
||||
|
||||
t(
|
||||
" a ",
|
||||
&[
|
||||
((0, 1), Token::Whitespace(" "), " "),
|
||||
((1, 2), Token::Keylike("a"), "a"),
|
||||
((2, 3), Token::Whitespace(" "), " "),
|
||||
],
|
||||
);
|
||||
|
||||
t(
|
||||
" a\t [[]] \t [] {} , . =\n# foo \r\n#foo \n ",
|
||||
&[
|
||||
((0, 1), Token::Whitespace(" "), " "),
|
||||
((1, 2), Token::Keylike("a"), "a"),
|
||||
((2, 4), Token::Whitespace("\t "), "\t "),
|
||||
((4, 5), Token::LeftBracket, "["),
|
||||
((5, 6), Token::LeftBracket, "["),
|
||||
((6, 7), Token::RightBracket, "]"),
|
||||
((7, 8), Token::RightBracket, "]"),
|
||||
((8, 11), Token::Whitespace(" \t "), " \t "),
|
||||
((11, 12), Token::LeftBracket, "["),
|
||||
((12, 13), Token::RightBracket, "]"),
|
||||
((13, 14), Token::Whitespace(" "), " "),
|
||||
((14, 15), Token::LeftBrace, "{"),
|
||||
((15, 16), Token::RightBrace, "}"),
|
||||
((16, 17), Token::Whitespace(" "), " "),
|
||||
((17, 18), Token::Comma, ","),
|
||||
((18, 19), Token::Whitespace(" "), " "),
|
||||
((19, 20), Token::Period, "."),
|
||||
((20, 21), Token::Whitespace(" "), " "),
|
||||
((21, 22), Token::Equals, "="),
|
||||
((22, 23), Token::Newline, "\n"),
|
||||
((23, 29), Token::Comment("# foo "), "# foo "),
|
||||
((29, 31), Token::Newline, "\r\n"),
|
||||
((31, 36), Token::Comment("#foo "), "#foo "),
|
||||
((36, 37), Token::Newline, "\n"),
|
||||
((37, 38), Token::Whitespace(" "), " "),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_cr_bad() {
|
||||
err("\r", Error::Unexpected(0, '\r'));
|
||||
err("'\n", Error::NewlineInString(1));
|
||||
err("'\u{0}", Error::InvalidCharInString(1, '\u{0}'));
|
||||
err("'", Error::UnterminatedString(0));
|
||||
err("\u{0}", Error::Unexpected(0, '\u{0}'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bad_comment() {
|
||||
let mut t = Tokenizer::new("#\u{0}");
|
||||
t.next().unwrap().unwrap();
|
||||
assert_eq!(t.next(), Err(Error::Unexpected(1, '\u{0}')));
|
||||
assert!(t.next().unwrap().is_none());
|
||||
}
|
||||
Reference in new issue
Block a user