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
+361
@@ -0,0 +1,361 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::environment::Environment;
|
||||
use crate::error::{Error, ErrorKind};
|
||||
use crate::value::Value;
|
||||
use crate::vm::loop_object::LoopState;
|
||||
|
||||
#[cfg(feature = "macros")]
|
||||
use crate::vm::closure_object::Closure;
|
||||
|
||||
type Locals<'env> = BTreeMap<&'env str, Value>;
|
||||
|
||||
pub(crate) struct Frame<'env> {
|
||||
pub(crate) locals: Locals<'env>,
|
||||
pub(crate) ctx: Value,
|
||||
pub(crate) current_loop: Option<LoopState>,
|
||||
|
||||
// normally a frame does not carry a closure, but it can when a macro is
|
||||
// declared. Once that happens, all writes to the frames locals are also
|
||||
// duplicated into the closure. Macros declared on that level, then share
|
||||
// the closure object to enclose the parent values. This emulates the
|
||||
// behavior of closures in Jinja2.
|
||||
#[cfg(feature = "macros")]
|
||||
pub(crate) closure: Option<Arc<Closure>>,
|
||||
}
|
||||
|
||||
impl<'env> Default for Frame<'env> {
|
||||
fn default() -> Frame<'env> {
|
||||
Frame::new(Value::UNDEFINED)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'env> Frame<'env> {
|
||||
/// Creates a new frame with the given context and no validation
|
||||
pub fn new(ctx: Value) -> Frame<'env> {
|
||||
Frame {
|
||||
locals: Locals::new(),
|
||||
ctx,
|
||||
current_loop: None,
|
||||
#[cfg(feature = "macros")]
|
||||
closure: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new frame with the given context and validates the value is not invalid
|
||||
pub fn new_checked(root: Value) -> Result<Frame<'env>, Error> {
|
||||
Ok(Frame::new(ok!(root.validate())))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "internal_debug")]
|
||||
impl fmt::Debug for Frame<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut m = f.debug_map();
|
||||
m.entry(&"locals", &self.locals);
|
||||
if let Some(LoopState {
|
||||
object: ref controller,
|
||||
..
|
||||
}) = self.current_loop
|
||||
{
|
||||
m.entry(&"loop", controller);
|
||||
}
|
||||
if !self.ctx.is_undefined() {
|
||||
m.entry(&"ctx", &self.ctx);
|
||||
}
|
||||
m.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "internal_debug", derive(Debug))]
|
||||
pub(crate) struct Stack {
|
||||
values: Vec<Value>,
|
||||
}
|
||||
|
||||
impl Default for Stack {
|
||||
fn default() -> Stack {
|
||||
Stack {
|
||||
values: Vec::with_capacity(24),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Stack {
|
||||
pub fn push(&mut self, arg: Value) {
|
||||
self.values.push(arg);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn pop(&mut self) -> Value {
|
||||
self.values.pop().unwrap()
|
||||
}
|
||||
|
||||
pub fn reverse_top(&mut self, n: usize) {
|
||||
let start = self.values.len() - n;
|
||||
self.values[start..].reverse();
|
||||
}
|
||||
|
||||
pub fn get_call_args(&mut self, n: Option<u16>) -> &[Value] {
|
||||
let n = match n {
|
||||
Some(n) => n as usize,
|
||||
None => self.pop().as_usize().unwrap(),
|
||||
};
|
||||
&self.values[self.values.len() - n..]
|
||||
}
|
||||
|
||||
pub fn drop_top(&mut self, n: usize) {
|
||||
self.values.truncate(self.values.len() - n);
|
||||
}
|
||||
|
||||
pub fn try_pop(&mut self) -> Option<Value> {
|
||||
self.values.pop()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn peek(&self) -> &Value {
|
||||
self.values.last().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Value>> for Stack {
|
||||
fn from(values: Vec<Value>) -> Stack {
|
||||
Stack { values }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Context<'env> {
|
||||
env: &'env Environment<'env>,
|
||||
stack: Vec<Frame<'env>>,
|
||||
outer_stack_depth: usize,
|
||||
recursion_limit: usize,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Context<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut vars = Vec::from_iter(self.known_variables(false));
|
||||
vars.sort();
|
||||
f.debug_map()
|
||||
.entries(vars.into_iter().map(|key| {
|
||||
let value = self.load(&key).unwrap_or_default();
|
||||
(key, value)
|
||||
}))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'env> Context<'env> {
|
||||
/// Creates an empty context.
|
||||
pub fn new(env: &'env Environment<'env>) -> Context<'env> {
|
||||
Context {
|
||||
env,
|
||||
stack: Vec::with_capacity(40),
|
||||
outer_stack_depth: 0,
|
||||
recursion_limit: env.recursion_limit(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a context
|
||||
pub fn new_with_frame(env: &'env Environment<'env>, frame: Frame<'env>) -> Context<'env> {
|
||||
let mut rv = Context::new(env);
|
||||
rv.stack.push(frame);
|
||||
rv
|
||||
}
|
||||
|
||||
/// The env
|
||||
#[inline(always)]
|
||||
pub fn env(&self) -> &'env Environment<'env> {
|
||||
self.env
|
||||
}
|
||||
|
||||
/// Stores a variable in the context.
|
||||
pub fn store(&mut self, key: &'env str, value: Value) {
|
||||
let top = self.stack.last_mut().unwrap();
|
||||
#[cfg(feature = "macros")]
|
||||
{
|
||||
if let Some(ref closure) = top.closure {
|
||||
closure.store(key, value.clone());
|
||||
}
|
||||
}
|
||||
top.locals.insert(key, value);
|
||||
}
|
||||
|
||||
/// Adds a value to a closure if missing.
|
||||
///
|
||||
/// All macros declare on a certain level reuse the same closure. This is done
|
||||
/// to emulate the behavior of how scopes work in Jinja2 in Python. The
|
||||
/// unfortunate downside is that this has to be done with a `Mutex`.
|
||||
#[cfg(feature = "macros")]
|
||||
pub fn enclose(&mut self, key: &str) {
|
||||
self.stack
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.closure
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.clone()
|
||||
.store_if_missing(key, || self.load(key).unwrap_or(Value::UNDEFINED));
|
||||
}
|
||||
|
||||
/// Loads the closure and returns it.
|
||||
#[cfg(feature = "macros")]
|
||||
pub fn closure(&mut self) -> Option<&Arc<Closure>> {
|
||||
self.stack.last_mut().unwrap().closure.as_ref()
|
||||
}
|
||||
|
||||
/// Temporarily takes the closure.
|
||||
///
|
||||
/// This is done because includes are in the same scope as the module that
|
||||
/// triggers the import, but we do not want to allow closures to be modified
|
||||
/// from another file as this would be very confusing.
|
||||
///
|
||||
/// This means that if you override a variable referenced by a macro after
|
||||
/// including in the parent template, it will not override the value seen by
|
||||
/// the macro.
|
||||
#[cfg(all(feature = "multi_template", feature = "macros"))]
|
||||
pub fn take_closure(&mut self) -> Option<Arc<Closure>> {
|
||||
self.stack.last_mut().unwrap().closure.take()
|
||||
}
|
||||
|
||||
/// Puts the closure back.
|
||||
#[cfg(feature = "macros")]
|
||||
pub fn reset_closure(&mut self, closure: Option<Arc<Closure>>) {
|
||||
self.stack.last_mut().unwrap().closure = closure;
|
||||
}
|
||||
|
||||
/// Return the base context value
|
||||
#[cfg(feature = "macros")]
|
||||
pub fn clone_base(&self) -> Value {
|
||||
self.stack
|
||||
.first()
|
||||
.map(|x| x.ctx.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Looks up a variable in the context.
|
||||
pub fn load(&self, key: &str) -> Option<Value> {
|
||||
for frame in self.stack.iter().rev() {
|
||||
// look at locals first
|
||||
if let Some(value) = frame.locals.get(key) {
|
||||
return Some(value.clone());
|
||||
}
|
||||
|
||||
// if we are a loop, check if we are looking up the special loop var.
|
||||
if let Some(ref l) = frame.current_loop {
|
||||
if l.with_loop_var && key == "loop" {
|
||||
return Some(Value::from_dyn_object(l.object.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// perform a fast lookup. This one will not produce errors if the
|
||||
// context is undefined or of the wrong type.
|
||||
if let Some(rv) = frame.ctx.get_attr_fast(key) {
|
||||
return Some(rv);
|
||||
}
|
||||
}
|
||||
|
||||
self.env.get_global(key)
|
||||
}
|
||||
|
||||
/// Returns an iterable of all declared variables.
|
||||
pub fn known_variables(&self, with_globals: bool) -> HashSet<Cow<'_, str>> {
|
||||
let mut seen = HashSet::<Cow<'_, str>>::new();
|
||||
for frame in self.stack.iter().rev() {
|
||||
for key in frame.locals.keys() {
|
||||
if !seen.contains(&Cow::Borrowed(*key)) {
|
||||
seen.insert(Cow::Borrowed(key));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref l) = frame.current_loop {
|
||||
if l.with_loop_var && !seen.contains("loop") {
|
||||
seen.insert(Cow::Borrowed("loop"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(iter) = frame.ctx.try_iter() {
|
||||
for key in iter {
|
||||
if let Some(str_key) = key.as_str() {
|
||||
if !seen.contains(&Cow::Borrowed(str_key))
|
||||
&& frame.ctx.get_item(&key).is_ok()
|
||||
{
|
||||
seen.insert(Cow::Owned(str_key.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if with_globals {
|
||||
seen.extend(self.env.globals().map(|x| Cow::Borrowed(x.0)));
|
||||
}
|
||||
seen
|
||||
}
|
||||
|
||||
/// Pushes a new layer.
|
||||
pub fn push_frame(&mut self, layer: Frame<'env>) -> Result<(), Error> {
|
||||
ok!(self.check_depth());
|
||||
self.stack.push(layer);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pops the topmost layer.
|
||||
#[track_caller]
|
||||
pub fn pop_frame(&mut self) -> Frame<'env> {
|
||||
self.stack.pop().unwrap()
|
||||
}
|
||||
|
||||
/// Returns the root locals (exports)
|
||||
#[track_caller]
|
||||
pub fn exports(&self) -> &Locals<'env> {
|
||||
&self.stack.first().unwrap().locals
|
||||
}
|
||||
|
||||
/// Returns the current locals mutably.
|
||||
#[track_caller]
|
||||
#[cfg(feature = "multi_template")]
|
||||
pub fn current_locals_mut(&mut self) -> &mut Locals<'env> {
|
||||
&mut self.stack.last_mut().unwrap().locals
|
||||
}
|
||||
|
||||
/// Returns the current innermost loop state.
|
||||
pub fn current_loop(&mut self) -> Option<&mut LoopState> {
|
||||
self.stack
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.filter_map(|x| x.current_loop.as_mut())
|
||||
.next()
|
||||
}
|
||||
|
||||
/// The real depth of the context.
|
||||
pub fn depth(&self) -> usize {
|
||||
self.outer_stack_depth + self.stack.len()
|
||||
}
|
||||
|
||||
/// Increase the stack depth.
|
||||
#[allow(unused)]
|
||||
pub fn incr_depth(&mut self, delta: usize) -> Result<(), Error> {
|
||||
self.outer_stack_depth += delta;
|
||||
ok!(self.check_depth());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrease the stack depth.
|
||||
#[allow(unused)]
|
||||
pub fn decr_depth(&mut self, delta: usize) {
|
||||
self.outer_stack_depth -= delta;
|
||||
}
|
||||
|
||||
fn check_depth(&self) -> Result<(), Error> {
|
||||
if self.depth() > self.recursion_limit {
|
||||
return Err(Error::new(
|
||||
ErrorKind::InvalidOperation,
|
||||
"recursion limit exceeded",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user