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
+81
@@ -0,0 +1,81 @@
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error2::{abort, abort_call_site};
|
||||
use quote::{quote, ToTokens};
|
||||
use syn::{parse_macro_input, parse_quote, Data, DeriveInput};
|
||||
|
||||
mod codegen;
|
||||
|
||||
pub(crate) fn expand(input: TokenStream) -> TokenStream {
|
||||
let DeriveInput {
|
||||
attrs,
|
||||
vis: _,
|
||||
ident,
|
||||
mut generics,
|
||||
data,
|
||||
} = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let defmt_path = match defmt_crate_path(&attrs) {
|
||||
Ok(defmt_path) => defmt_path,
|
||||
Err(e) => abort!(e),
|
||||
};
|
||||
|
||||
let encode_data = match &data {
|
||||
Data::Enum(data) => codegen::encode_enum_data(&ident, data, &defmt_path),
|
||||
Data::Struct(data) => codegen::encode_struct_data(&ident, data, &defmt_path),
|
||||
Data::Union(_) => abort_call_site!("`#[derive(Format)]` does not support unions"),
|
||||
};
|
||||
|
||||
let codegen::EncodeData {
|
||||
format_tag,
|
||||
stmts,
|
||||
where_predicates,
|
||||
} = match encode_data {
|
||||
Ok(data) => data,
|
||||
Err(e) => return e.into_compile_error().into(),
|
||||
};
|
||||
|
||||
let codegen::Generics {
|
||||
impl_generics,
|
||||
type_generics,
|
||||
where_clause,
|
||||
} = codegen::Generics::codegen(&mut generics, where_predicates);
|
||||
|
||||
quote!(
|
||||
#[automatically_derived]
|
||||
impl #impl_generics #defmt_path::Format for #ident #type_generics #where_clause {
|
||||
fn format(&self, f: #defmt_path::Formatter) {
|
||||
use #defmt_path as defmt;
|
||||
#defmt_path::unreachable!()
|
||||
}
|
||||
|
||||
fn _format_tag() -> #defmt_path::Str {
|
||||
#format_tag
|
||||
}
|
||||
|
||||
fn _format_data(&self) {
|
||||
#(#stmts)*
|
||||
}
|
||||
}
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn defmt_crate_path(attrs: &[syn::Attribute]) -> Result<syn::Path, syn::Error> {
|
||||
let mut defmt_path = parse_quote!(defmt);
|
||||
let res = attrs
|
||||
.iter()
|
||||
.filter(|attr| attr.path().is_ident("defmt"))
|
||||
.try_for_each(|attr| {
|
||||
attr.parse_nested_meta(|meta| {
|
||||
if meta.path.get_ident().is_some_and(|ident| ident == "crate") {
|
||||
meta.input.parse::<syn::Token![=]>()?;
|
||||
defmt_path = meta.input.parse::<syn::Path>()?;
|
||||
Ok(())
|
||||
} else {
|
||||
let path = meta.path.to_token_stream().to_string().replace(' ', "");
|
||||
Err(meta.error(format_args!("unknown defmt attribute `{path}`")))
|
||||
}
|
||||
})
|
||||
});
|
||||
res.map(|()| defmt_path)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::quote;
|
||||
use syn::{DataStruct, Ident, ImplGenerics, TypeGenerics, WhereClause, WherePredicate};
|
||||
|
||||
pub(crate) use enum_data::encode as encode_enum_data;
|
||||
|
||||
use crate::construct;
|
||||
|
||||
mod enum_data;
|
||||
mod fields;
|
||||
|
||||
pub(crate) struct EncodeData {
|
||||
pub(crate) format_tag: TokenStream2,
|
||||
pub(crate) stmts: Vec<TokenStream2>,
|
||||
pub(crate) where_predicates: Vec<WherePredicate>,
|
||||
}
|
||||
|
||||
pub(crate) fn encode_struct_data(
|
||||
ident: &Ident,
|
||||
data: &DataStruct,
|
||||
defmt_path: &syn::Path,
|
||||
) -> syn::Result<EncodeData> {
|
||||
let mut format_string = ident.to_string();
|
||||
let mut stmts = vec![];
|
||||
let mut field_patterns = vec![];
|
||||
|
||||
let (encode_fields_stmts, where_predicates) = fields::codegen(
|
||||
&data.fields,
|
||||
&mut format_string,
|
||||
&mut field_patterns,
|
||||
defmt_path,
|
||||
)?;
|
||||
|
||||
stmts.push(quote!(match self {
|
||||
Self { #(#field_patterns),* } => {
|
||||
#(#encode_fields_stmts;)*
|
||||
}
|
||||
}));
|
||||
|
||||
let format_tag = construct::interned_string(&format_string, "derived", false, None, defmt_path);
|
||||
Ok(EncodeData {
|
||||
format_tag,
|
||||
stmts,
|
||||
where_predicates,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) struct Generics<'a> {
|
||||
pub(crate) impl_generics: ImplGenerics<'a>,
|
||||
pub(crate) type_generics: TypeGenerics<'a>,
|
||||
pub(crate) where_clause: WhereClause,
|
||||
}
|
||||
|
||||
impl<'a> Generics<'a> {
|
||||
pub(crate) fn codegen(
|
||||
generics: &'a mut syn::Generics,
|
||||
where_predicates: Vec<WherePredicate>,
|
||||
) -> Self {
|
||||
let mut where_clause = generics.make_where_clause().clone();
|
||||
let (impl_generics, type_generics, _) = generics.split_for_impl();
|
||||
|
||||
// Extend where-clause with `Format` bounds for all field types.
|
||||
where_clause.predicates.extend(where_predicates);
|
||||
|
||||
Self {
|
||||
impl_generics,
|
||||
type_generics,
|
||||
where_clause,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use proc_macro_error2::abort_call_site;
|
||||
use quote::quote;
|
||||
use syn::{DataEnum, Ident};
|
||||
|
||||
use crate::construct;
|
||||
|
||||
use super::EncodeData;
|
||||
|
||||
pub(crate) fn encode(
|
||||
ident: &Ident,
|
||||
data: &DataEnum,
|
||||
defmt_path: &syn::Path,
|
||||
) -> syn::Result<EncodeData> {
|
||||
if data.variants.is_empty() {
|
||||
return Ok(EncodeData {
|
||||
stmts: vec![quote!(match *self {})],
|
||||
format_tag: construct::interned_string("!", "derived", false, None, defmt_path),
|
||||
where_predicates: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
let mut format_string = String::new();
|
||||
let mut where_predicates = vec![];
|
||||
|
||||
let mut match_arms = vec![];
|
||||
let mut is_first_variant = true;
|
||||
let discriminant_encoder = DiscriminantEncoder::new(data.variants.len());
|
||||
let enum_ident = ident;
|
||||
for (index, variant) in data.variants.iter().enumerate() {
|
||||
let variant_ident = &variant.ident;
|
||||
|
||||
if is_first_variant {
|
||||
is_first_variant = false;
|
||||
} else {
|
||||
format_string.push('|');
|
||||
}
|
||||
format_string.push_str(&variant_ident.to_string());
|
||||
|
||||
let mut field_patterns = vec![];
|
||||
let (encode_fields_stmts, encode_field_where_predicates) = super::fields::codegen(
|
||||
&variant.fields,
|
||||
&mut format_string,
|
||||
&mut field_patterns,
|
||||
defmt_path,
|
||||
)?;
|
||||
where_predicates.extend(encode_field_where_predicates.into_iter());
|
||||
let pattern = quote!( { #(#field_patterns),* } );
|
||||
|
||||
let encode_discriminant_stmt = discriminant_encoder.encode(index, defmt_path);
|
||||
|
||||
match_arms.push(quote!(
|
||||
#enum_ident::#variant_ident #pattern => {
|
||||
#encode_discriminant_stmt
|
||||
#(#encode_fields_stmts;)*
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
let format_tag = construct::interned_string(&format_string, "derived", false, None, defmt_path);
|
||||
let stmts = vec![quote!(match self {
|
||||
#(#match_arms)*
|
||||
})];
|
||||
where_predicates.dedup_by(|a, b| a == b);
|
||||
|
||||
Ok(EncodeData {
|
||||
format_tag,
|
||||
stmts,
|
||||
where_predicates,
|
||||
})
|
||||
}
|
||||
|
||||
enum DiscriminantEncoder {
|
||||
Nop,
|
||||
U8,
|
||||
U16,
|
||||
U32,
|
||||
U64,
|
||||
}
|
||||
|
||||
impl DiscriminantEncoder {
|
||||
fn new(number_of_variants: usize) -> Self {
|
||||
if number_of_variants == 1 {
|
||||
Self::Nop
|
||||
} else if number_of_variants <= usize::from(u8::MAX) {
|
||||
Self::U8
|
||||
} else if number_of_variants <= usize::from(u16::MAX) {
|
||||
Self::U16
|
||||
} else if number_of_variants as u128 > u128::from(u64::MAX) {
|
||||
// unreachable on existing hardware?
|
||||
abort_call_site!(
|
||||
"`#[derive(Format)]` does not support enums with more than {} variants",
|
||||
number_of_variants
|
||||
)
|
||||
} else if number_of_variants as u64 <= u64::from(u32::MAX) {
|
||||
Self::U32
|
||||
} else {
|
||||
Self::U64
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE this assumes `index` < `number_of_variants` used to construct `self`
|
||||
fn encode(&self, index: usize, defmt_path: &syn::Path) -> TokenStream2 {
|
||||
match self {
|
||||
// For single-variant enums, there is no need to encode the discriminant.
|
||||
DiscriminantEncoder::Nop => quote!(),
|
||||
DiscriminantEncoder::U8 => {
|
||||
let index = index as u8;
|
||||
quote!(#defmt_path::export::u8(&#index);)
|
||||
}
|
||||
DiscriminantEncoder::U16 => {
|
||||
let index = index as u16;
|
||||
quote!(#defmt_path::export::u16(&#index);)
|
||||
}
|
||||
DiscriminantEncoder::U32 => {
|
||||
let index = index as u32;
|
||||
quote!(#defmt_path::export::u32(&#index);)
|
||||
}
|
||||
DiscriminantEncoder::U64 => {
|
||||
let index = index as u64;
|
||||
quote!(#defmt_path::export::u64(&#index);)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{format_ident, quote};
|
||||
use syn::{parse_quote, Field, Fields, Index, Type, WherePredicate};
|
||||
|
||||
use crate::consts;
|
||||
|
||||
pub(crate) fn codegen(
|
||||
fields: &Fields,
|
||||
format_string: &mut String,
|
||||
patterns: &mut Vec<TokenStream2>,
|
||||
defmt_path: &syn::Path,
|
||||
) -> syn::Result<(Vec<TokenStream2>, Vec<WherePredicate>)> {
|
||||
let (fields, fields_are_named) = match fields {
|
||||
Fields::Named(named) => (&named.named, true),
|
||||
Fields::Unit => return Ok((vec![], vec![])),
|
||||
Fields::Unnamed(unnamed) => (&unnamed.unnamed, false),
|
||||
};
|
||||
|
||||
if fields.is_empty() {
|
||||
return Ok((vec![], vec![]));
|
||||
}
|
||||
|
||||
if fields_are_named {
|
||||
format_string.push_str(" {{ ");
|
||||
} else {
|
||||
format_string.push('(');
|
||||
}
|
||||
|
||||
let mut stmts = vec![];
|
||||
let mut where_predicates = vec![];
|
||||
let mut is_first = true;
|
||||
for (index, field) in fields.iter().enumerate() {
|
||||
if is_first {
|
||||
is_first = false;
|
||||
} else {
|
||||
format_string.push_str(", ");
|
||||
}
|
||||
|
||||
let format_opt = get_defmt_format_option(field)?;
|
||||
// Find out if the field type is natively supported by defmt. `ty` will be None if not.
|
||||
let ty = as_native_type(&field.ty);
|
||||
// `field_ty` will be the field's type if it is not natively supported by defmt
|
||||
let field_ty = if ty.is_none() { Some(&field.ty) } else { None };
|
||||
// Get the field format specifier. Either the native specifier or '?'.
|
||||
let ty = ty.unwrap_or_else(|| consts::TYPE_FORMAT.to_string());
|
||||
let ident = field
|
||||
.ident
|
||||
.clone()
|
||||
.unwrap_or_else(|| format_ident!("arg{}", index));
|
||||
// Find the required trait bounds for the field and add the formatting statement depending on the field type and the formatting options
|
||||
let bound: Option<syn::Path> = if let Some(FormatOption::Debug2Format) = format_opt {
|
||||
stmts.push(quote!(#defmt_path::export::fmt(&#defmt_path::Debug2Format(&#ident))));
|
||||
field_ty.map(|_| parse_quote!(::core::fmt::Debug))
|
||||
} else if let Some(FormatOption::Display2Format) = format_opt {
|
||||
stmts.push(quote!(#defmt_path::export::fmt(&#defmt_path::Display2Format(&#ident))));
|
||||
field_ty.map(|_| parse_quote!(::core::fmt::Display))
|
||||
} else if ty == consts::TYPE_FORMAT {
|
||||
stmts.push(quote!(#defmt_path::export::fmt(#ident)));
|
||||
field_ty.map(|_| parse_quote!(#defmt_path::Format))
|
||||
} else {
|
||||
let method = format_ident!("{}", ty);
|
||||
stmts.push(quote!(#defmt_path::export::#method(#ident)));
|
||||
field_ty.map(|_| parse_quote!(#defmt_path::Format))
|
||||
};
|
||||
if let Some(bound) = bound {
|
||||
where_predicates.push(parse_quote!(#field_ty: #bound));
|
||||
}
|
||||
|
||||
if field.ident.is_some() {
|
||||
// Named field.
|
||||
write!(format_string, "{ident}: {{={ty}:?}}").ok();
|
||||
|
||||
patterns.push(quote!( #ident ));
|
||||
} else {
|
||||
// Unnamed (tuple) field.
|
||||
write!(format_string, "{{={ty}}}").ok();
|
||||
|
||||
let index = Index::from(index);
|
||||
patterns.push(quote!( #index: #ident ));
|
||||
}
|
||||
}
|
||||
|
||||
if fields_are_named {
|
||||
format_string.push_str(" }}");
|
||||
} else {
|
||||
format_string.push(')');
|
||||
}
|
||||
|
||||
Ok((stmts, where_predicates))
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum FormatOption {
|
||||
Debug2Format,
|
||||
Display2Format,
|
||||
}
|
||||
|
||||
/// If the field has a valid defmt attribute (e.g. `#[defmt(Debug2Format)]`), returns `Ok(Some(FormatOption))`.
|
||||
/// Returns `Err` if we can't parse a valid defmt attribute.
|
||||
/// Returns `Ok(None)` if there are no `defmt` attributes on the field.
|
||||
fn get_defmt_format_option(field: &Field) -> syn::Result<Option<FormatOption>> {
|
||||
let mut format_option = None;
|
||||
|
||||
for attr in &field.attrs {
|
||||
if attr.path().is_ident("defmt") {
|
||||
if format_option.is_some() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
field,
|
||||
"multiple `defmt` attributes not supported",
|
||||
));
|
||||
}
|
||||
|
||||
let mut parsed_format = None;
|
||||
|
||||
attr.parse_nested_meta(|meta| {
|
||||
// #[defmt(Debug2Format)]
|
||||
if meta.path.is_ident("Debug2Format") {
|
||||
parsed_format = Some(FormatOption::Debug2Format);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// #[defmt(Display2Format)]
|
||||
if meta.path.is_ident("Display2Format") {
|
||||
parsed_format = Some(FormatOption::Display2Format);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(meta.error("expected `Debug2Format` or `Display2Format`"))
|
||||
})?;
|
||||
|
||||
if parsed_format.is_none() {
|
||||
return Err(syn::Error::new_spanned(
|
||||
&attr.meta,
|
||||
"expected 1 attribute argument",
|
||||
));
|
||||
}
|
||||
|
||||
format_option = parsed_format;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format_option)
|
||||
}
|
||||
|
||||
/// Returns `Some` if `ty` refers to a builtin Rust type that has native support from defmt and does
|
||||
/// not have to go through the `Format` trait.
|
||||
///
|
||||
/// This should return `Some` for all types that can be used as `{=TYPE}`.
|
||||
///
|
||||
/// Note: This is technically incorrect, since builtin types can be shadowed. However the efficiency
|
||||
/// gains are too big to pass up, so we expect user code to not do that.
|
||||
fn as_native_type(ty: &Type) -> Option<String> {
|
||||
match ty {
|
||||
Type::Path(path) => {
|
||||
let ident = path.path.get_ident()?;
|
||||
let ty_name = ident.to_string();
|
||||
|
||||
match &*ty_name {
|
||||
"u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "i8" | "i16" | "i32" | "i64"
|
||||
| "i128" | "isize" | "f32" | "f64" | "bool" | "str" => Some(ty_name),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Type::Reference(ty_ref) => as_native_type(&ty_ref.elem),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user