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
+177
@@ -0,0 +1,177 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use darling::export::NestedMeta;
|
||||
use darling::FromMeta;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{ReturnType, Type};
|
||||
|
||||
use crate::util::*;
|
||||
|
||||
enum Flavor {
|
||||
Standard,
|
||||
Wasm,
|
||||
}
|
||||
|
||||
pub(crate) struct Arch {
|
||||
default_entry: Option<&'static str>,
|
||||
flavor: Flavor,
|
||||
}
|
||||
|
||||
pub static ARCH_AVR: Arch = Arch {
|
||||
default_entry: Some("avr_device::entry"),
|
||||
flavor: Flavor::Standard,
|
||||
};
|
||||
|
||||
pub static ARCH_RISCV: Arch = Arch {
|
||||
default_entry: Some("riscv_rt::entry"),
|
||||
flavor: Flavor::Standard,
|
||||
};
|
||||
|
||||
pub static ARCH_CORTEX_M: Arch = Arch {
|
||||
default_entry: Some("cortex_m_rt::entry"),
|
||||
flavor: Flavor::Standard,
|
||||
};
|
||||
|
||||
pub static ARCH_SPIN: Arch = Arch {
|
||||
default_entry: None,
|
||||
flavor: Flavor::Standard,
|
||||
};
|
||||
|
||||
pub static ARCH_STD: Arch = Arch {
|
||||
default_entry: None,
|
||||
flavor: Flavor::Standard,
|
||||
};
|
||||
|
||||
pub static ARCH_WASM: Arch = Arch {
|
||||
default_entry: Some("wasm_bindgen::prelude::wasm_bindgen(start)"),
|
||||
flavor: Flavor::Wasm,
|
||||
};
|
||||
|
||||
#[derive(Debug, FromMeta, Default)]
|
||||
struct Args {
|
||||
#[darling(default)]
|
||||
entry: Option<String>,
|
||||
}
|
||||
|
||||
pub fn run(args: TokenStream, item: TokenStream, arch: &Arch) -> TokenStream {
|
||||
let mut errors = TokenStream::new();
|
||||
|
||||
// If any of the steps for this macro fail, we still want to expand to an item that is as close
|
||||
// to the expected output as possible. This helps out IDEs such that completions and other
|
||||
// related features keep working.
|
||||
let f: ItemFn = match syn::parse2(item.clone()) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return token_stream_with_error(item, e),
|
||||
};
|
||||
|
||||
let args = match NestedMeta::parse_meta_list(args) {
|
||||
Ok(x) => x,
|
||||
Err(e) => return token_stream_with_error(item, e),
|
||||
};
|
||||
|
||||
let args = match Args::from_list(&args) {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
errors.extend(e.write_errors());
|
||||
Args::default()
|
||||
}
|
||||
};
|
||||
|
||||
let fargs = f.sig.inputs.clone();
|
||||
|
||||
if f.sig.asyncness.is_none() {
|
||||
error(&mut errors, &f.sig, "main function must be async");
|
||||
}
|
||||
if !f.sig.generics.params.is_empty() {
|
||||
error(&mut errors, &f.sig, "main function must not be generic");
|
||||
}
|
||||
if !f.sig.generics.where_clause.is_none() {
|
||||
error(&mut errors, &f.sig, "main function must not have `where` clauses");
|
||||
}
|
||||
if !f.sig.abi.is_none() {
|
||||
error(&mut errors, &f.sig, "main function must not have an ABI qualifier");
|
||||
}
|
||||
if !f.sig.variadic.is_none() {
|
||||
error(&mut errors, &f.sig, "main function must not be variadic");
|
||||
}
|
||||
match &f.sig.output {
|
||||
ReturnType::Default => {}
|
||||
ReturnType::Type(_, ty) => match &**ty {
|
||||
Type::Tuple(tuple) if tuple.elems.is_empty() => {}
|
||||
Type::Never(_) => {}
|
||||
_ => error(
|
||||
&mut errors,
|
||||
&f.sig,
|
||||
"main function must either not return a value, return `()` or return `!`",
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
if fargs.len() != 1 {
|
||||
error(&mut errors, &f.sig, "main function must have 1 argument: the spawner.");
|
||||
}
|
||||
|
||||
let entry = match args.entry.as_deref().or(arch.default_entry) {
|
||||
None => TokenStream::new(),
|
||||
Some(x) => match TokenStream::from_str(x) {
|
||||
Ok(x) => quote!(#[#x]),
|
||||
Err(e) => {
|
||||
error(&mut errors, &f.sig, e);
|
||||
TokenStream::new()
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let f_body = f.body;
|
||||
let out = &f.sig.output;
|
||||
|
||||
let (main_ret, mut main_body) = match arch.flavor {
|
||||
Flavor::Standard => (
|
||||
quote!(!),
|
||||
quote! {
|
||||
unsafe fn __make_static<T>(t: &mut T) -> &'static mut T {
|
||||
::core::mem::transmute(t)
|
||||
}
|
||||
|
||||
let mut executor = ::embassy_executor::Executor::new();
|
||||
let executor = unsafe { __make_static(&mut executor) };
|
||||
executor.run(|spawner| {
|
||||
spawner.must_spawn(__embassy_main(spawner));
|
||||
})
|
||||
},
|
||||
),
|
||||
Flavor::Wasm => (
|
||||
quote!(Result<(), wasm_bindgen::JsValue>),
|
||||
quote! {
|
||||
let executor = ::std::boxed::Box::leak(::std::boxed::Box::new(::embassy_executor::Executor::new()));
|
||||
|
||||
executor.start(|spawner| {
|
||||
spawner.must_spawn(__embassy_main(spawner));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
if !errors.is_empty() {
|
||||
main_body = quote! {loop{}};
|
||||
}
|
||||
|
||||
let result = quote! {
|
||||
#[::embassy_executor::task()]
|
||||
async fn __embassy_main(#fargs) #out {
|
||||
#f_body
|
||||
}
|
||||
|
||||
#entry
|
||||
fn main() -> #main_ret {
|
||||
#main_body
|
||||
}
|
||||
|
||||
#errors
|
||||
};
|
||||
|
||||
result
|
||||
}
|
||||
Reference in new issue
Block a user