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>
164 lines
5.0 KiB
Rust
164 lines
5.0 KiB
Rust
extern crate usbd_hid_descriptors;
|
|
|
|
use crate::spec::*;
|
|
use alloc::{
|
|
format,
|
|
string::{String, ToString},
|
|
};
|
|
use syn::{parse, Expr, ExprLit, Field, Fields, Ident, Lit, Result, Type, TypePath};
|
|
use usbd_hid_descriptors::*;
|
|
|
|
// MainItem describes all the mandatory data points of a Main item.
|
|
#[derive(Debug, Default, Clone)]
|
|
pub struct MainItem {
|
|
pub kind: MainItemKind,
|
|
pub logical_minimum: isize,
|
|
pub logical_maximum: isize,
|
|
pub report_count: u16,
|
|
pub report_size: u16,
|
|
pub padding_bits: Option<u16>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct ReportUnaryField {
|
|
pub bit_width: usize,
|
|
pub descriptor_item: MainItem,
|
|
pub ident: Ident,
|
|
}
|
|
|
|
/// analyze_field constructs a main item from an item spec & field.
|
|
pub fn analyze_field(field: Field, ft: Type, item: &ItemSpec) -> Result<ReportUnaryField> {
|
|
let (p, size) = parse_type(&field, ft)?;
|
|
|
|
if p.path.segments.len() != 1 {
|
|
return Err(parse::Error::new(
|
|
field.ident.unwrap().span(),
|
|
"`#[gen_hid_descriptor]` internal error when unwrapping type",
|
|
));
|
|
}
|
|
let type_ident = p.path.segments[0].ident.clone();
|
|
|
|
let type_str = type_ident.to_string();
|
|
let (sign, size_str) = type_str.as_str().split_at(1);
|
|
let bit_width = size_str.parse();
|
|
let type_setter: Option<fn(&mut ReportUnaryField, usize)> = match sign {
|
|
"u" => Some(set_unsigned_unary_item),
|
|
"i" => Some(set_signed_unary_item),
|
|
&_ => None,
|
|
};
|
|
|
|
if bit_width.is_err() || type_setter.is_none() {
|
|
return Err(parse::Error::new(
|
|
type_ident.span(),
|
|
"`#[gen_hid_descriptor]` type not supported",
|
|
));
|
|
}
|
|
let bit_width = bit_width.unwrap();
|
|
|
|
if bit_width >= 64 {
|
|
return Err(parse::Error::new(
|
|
type_ident.span(),
|
|
"`#[gen_hid_descriptor]` integer larger than 64 is not supported in ssmarshal",
|
|
));
|
|
}
|
|
|
|
let mut output = unary_item(field.ident.clone().unwrap(), item.kind, bit_width);
|
|
|
|
if let Some(want_bits) = item.want_bits {
|
|
// bitpack
|
|
output.descriptor_item.logical_minimum = 0;
|
|
output.descriptor_item.logical_maximum = 1;
|
|
output.descriptor_item.report_count = want_bits;
|
|
output.descriptor_item.report_size = 1;
|
|
let width = output.bit_width * size;
|
|
if width < want_bits as usize {
|
|
return Err(parse::Error::new(
|
|
field.ident.unwrap().span(),
|
|
format!(
|
|
"`#[gen_hid_descriptor]` not enough space, missing {} bit(s)",
|
|
want_bits as usize - width
|
|
),
|
|
));
|
|
}
|
|
let remaining_bits = width as u16 - want_bits;
|
|
if remaining_bits > 0 {
|
|
output.descriptor_item.padding_bits = Some(remaining_bits);
|
|
}
|
|
} else {
|
|
// array of reports
|
|
type_setter.unwrap()(&mut output, bit_width);
|
|
output.descriptor_item.report_count *= size as u16;
|
|
}
|
|
|
|
Ok(output)
|
|
}
|
|
|
|
fn parse_type(field: &Field, ft: Type) -> Result<(TypePath, usize)> {
|
|
match ft {
|
|
Type::Array(a) => {
|
|
let mut size: usize = 0;
|
|
|
|
if let Expr::Lit(ExprLit {
|
|
lit: Lit::Int(lit), ..
|
|
}) = a.len
|
|
{
|
|
if let Ok(num) = lit.base10_parse::<usize>() {
|
|
size = num;
|
|
}
|
|
}
|
|
if size == 0 {
|
|
Err(parse::Error::new(
|
|
field.ident.as_ref().unwrap().span(),
|
|
"`#[gen_hid_descriptor]` array has invalid length",
|
|
))
|
|
} else {
|
|
Ok((parse_type(field, *a.elem)?.0, size))
|
|
}
|
|
}
|
|
Type::Path(p) => Ok((p, 1)),
|
|
_ => Err(parse::Error::new(
|
|
field.ident.as_ref().unwrap().span(),
|
|
"`#[gen_hid_descriptor]` cannot handle field type",
|
|
)),
|
|
}
|
|
}
|
|
|
|
fn set_signed_unary_item(out: &mut ReportUnaryField, bit_width: usize) {
|
|
let bound = 2u32.pow((bit_width - 1) as u32) as isize - 1;
|
|
out.descriptor_item.logical_minimum = -bound;
|
|
out.descriptor_item.logical_maximum = bound;
|
|
}
|
|
|
|
fn set_unsigned_unary_item(out: &mut ReportUnaryField, bit_width: usize) {
|
|
out.descriptor_item.logical_minimum = 0;
|
|
out.descriptor_item.logical_maximum = 2u32.pow(bit_width as u32) as isize - 1;
|
|
}
|
|
|
|
fn unary_item(id: Ident, kind: MainItemKind, bit_width: usize) -> ReportUnaryField {
|
|
ReportUnaryField {
|
|
ident: id,
|
|
bit_width,
|
|
descriptor_item: MainItem {
|
|
kind,
|
|
logical_minimum: 0,
|
|
logical_maximum: 0,
|
|
report_count: 1,
|
|
report_size: bit_width as u16,
|
|
padding_bits: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
pub fn field_decl(fields: &Fields, name: String) -> Field {
|
|
for field in fields {
|
|
let ident = field.ident.clone().unwrap().to_string();
|
|
if ident == name {
|
|
return field.clone();
|
|
}
|
|
}
|
|
panic!(
|
|
"internal error: could not find field {} which should exist",
|
|
name
|
|
)
|
|
}
|