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
+314
@@ -0,0 +1,314 @@
|
||||
use crate::error::Needed;
|
||||
use crate::stream::Checkpoint;
|
||||
use crate::stream::Compare;
|
||||
use crate::stream::CompareResult;
|
||||
use crate::stream::Location;
|
||||
use crate::stream::Offset;
|
||||
#[cfg(feature = "unstable-recover")]
|
||||
#[cfg(feature = "std")]
|
||||
use crate::stream::Recover;
|
||||
use crate::stream::SliceLen;
|
||||
use crate::stream::Stream;
|
||||
use crate::stream::StreamIsPartial;
|
||||
use crate::stream::UpdateSlice;
|
||||
use core::iter::Enumerate;
|
||||
use core::slice::Iter;
|
||||
|
||||
/// Specialized input for parsing lexed tokens
|
||||
///
|
||||
/// Helpful impls
|
||||
/// - Any `PartialEq` type (e.g. a `TokenKind` or `&str`) can be used with
|
||||
/// [`literal`][crate::token::literal]
|
||||
/// - A `PartialEq` for `&str` allows for using `&str` as a parser for tokens
|
||||
/// - [`ContainsToken`][crate::stream::ContainsToken] for `T` to for parsing with token sets
|
||||
/// - [`Location`] for `T` to extract spans from tokens
|
||||
///
|
||||
/// See also [Lexing and Parsing][crate::_topic::lexing].
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct TokenSlice<'t, T> {
|
||||
initial: &'t [T],
|
||||
input: &'t [T],
|
||||
}
|
||||
|
||||
impl<'t, T> TokenSlice<'t, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
/// Make a stream to parse tokens
|
||||
#[inline]
|
||||
pub fn new(input: &'t [T]) -> Self {
|
||||
Self {
|
||||
initial: input,
|
||||
input,
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the stream to the start
|
||||
///
|
||||
/// This is useful for formats that encode a graph with addresses relative to the start of the
|
||||
/// input.
|
||||
#[doc(alias = "fseek")]
|
||||
#[inline]
|
||||
pub fn reset_to_start(&mut self) {
|
||||
let start = self.initial.checkpoint();
|
||||
self.input.reset(&start);
|
||||
}
|
||||
|
||||
/// Iterate over consumed tokens starting with the last emitted
|
||||
///
|
||||
/// This is intended to help build up appropriate context when reporting errors.
|
||||
#[inline]
|
||||
pub fn previous_tokens(&self) -> impl Iterator<Item = &'t T> {
|
||||
let offset = self.input.offset_from(&self.initial);
|
||||
self.initial[0..offset].iter().rev()
|
||||
}
|
||||
}
|
||||
|
||||
/// Track locations by implementing [`Location`] on the Token.
|
||||
impl<T> TokenSlice<'_, T>
|
||||
where
|
||||
T: Location,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn previous_token_end(&self) -> Option<usize> {
|
||||
let index = self.input.offset_from(&self.initial);
|
||||
index
|
||||
.checked_sub(1)
|
||||
.map(|i| self.initial[i].previous_token_end())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn current_token_start(&self) -> Option<usize> {
|
||||
self.input.first().map(|t| t.current_token_start())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for TokenSlice<'_, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new(&[])
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> core::ops::Deref for TokenSlice<'_, T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.input
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: core::fmt::Debug> core::fmt::Debug for TokenSlice<'_, T> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.input.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> SliceLen for TokenSlice<'_, T> {
|
||||
#[inline(always)]
|
||||
fn slice_len(&self) -> usize {
|
||||
self.input.slice_len()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T> Stream for TokenSlice<'t, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
type Token = &'t T;
|
||||
type Slice = &'t [T];
|
||||
|
||||
type IterOffsets = Enumerate<Iter<'t, T>>;
|
||||
|
||||
type Checkpoint = Checkpoint<&'t [T], Self>;
|
||||
|
||||
#[inline(always)]
|
||||
fn iter_offsets(&self) -> Self::IterOffsets {
|
||||
self.input.iter().enumerate()
|
||||
}
|
||||
#[inline(always)]
|
||||
fn eof_offset(&self) -> usize {
|
||||
self.input.eof_offset()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn next_token(&mut self) -> Option<Self::Token> {
|
||||
let (token, next) = self.input.split_first()?;
|
||||
self.input = next;
|
||||
Some(token)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn peek_token(&self) -> Option<Self::Token> {
|
||||
self.input.first()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn offset_for<P>(&self, predicate: P) -> Option<usize>
|
||||
where
|
||||
P: Fn(Self::Token) -> bool,
|
||||
{
|
||||
self.input.iter().position(predicate)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn offset_at(&self, tokens: usize) -> Result<usize, Needed> {
|
||||
self.input.offset_at(tokens)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn next_slice(&mut self, offset: usize) -> Self::Slice {
|
||||
self.input.next_slice(offset)
|
||||
}
|
||||
#[inline(always)]
|
||||
unsafe fn next_slice_unchecked(&mut self, offset: usize) -> Self::Slice {
|
||||
// SAFETY: Passing up invariants
|
||||
unsafe { self.input.next_slice_unchecked(offset) }
|
||||
}
|
||||
#[inline(always)]
|
||||
fn peek_slice(&self, offset: usize) -> Self::Slice {
|
||||
self.input.peek_slice(offset)
|
||||
}
|
||||
#[inline(always)]
|
||||
unsafe fn peek_slice_unchecked(&self, offset: usize) -> Self::Slice {
|
||||
// SAFETY: Passing up invariants
|
||||
unsafe { self.input.peek_slice_unchecked(offset) }
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn checkpoint(&self) -> Self::Checkpoint {
|
||||
Checkpoint::<_, Self>::new(self.input)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn reset(&mut self, checkpoint: &Self::Checkpoint) {
|
||||
self.input = checkpoint.inner;
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn raw(&self) -> &dyn core::fmt::Debug {
|
||||
#![allow(deprecated)]
|
||||
self.input.raw()
|
||||
}
|
||||
|
||||
fn trace(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
self.input.trace(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Location for TokenSlice<'_, T>
|
||||
where
|
||||
T: Location,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn previous_token_end(&self) -> usize {
|
||||
self.previous_token_end()
|
||||
.or_else(|| self.current_token_start())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
#[inline(always)]
|
||||
fn current_token_start(&self) -> usize {
|
||||
self.current_token_start()
|
||||
.or_else(|| self.previous_token_end())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "unstable-recover")]
|
||||
#[cfg(feature = "std")]
|
||||
impl<T, E> Recover<E> for TokenSlice<'_, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn record_err(
|
||||
&mut self,
|
||||
_token_start: &Self::Checkpoint,
|
||||
_err_start: &Self::Checkpoint,
|
||||
err: E,
|
||||
) -> Result<(), E> {
|
||||
Err(err)
|
||||
}
|
||||
|
||||
/// Report whether the [`Stream`] can save off errors for recovery
|
||||
#[inline(always)]
|
||||
fn is_recovery_supported() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T> StreamIsPartial for TokenSlice<'t, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
type PartialState = <&'t [T] as StreamIsPartial>::PartialState;
|
||||
|
||||
#[inline]
|
||||
fn complete(&mut self) -> Self::PartialState {
|
||||
#![allow(clippy::semicolon_if_nothing_returned)]
|
||||
self.input.complete()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn restore_partial(&mut self, state: Self::PartialState) {
|
||||
self.input.restore_partial(state);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_partial_supported() -> bool {
|
||||
<&[T] as StreamIsPartial>::is_partial_supported()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn is_partial(&self) -> bool {
|
||||
self.input.is_partial()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Offset for TokenSlice<'_, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn offset_from(&self, other: &Self) -> usize {
|
||||
self.offset_from(&other.checkpoint())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Offset<<TokenSlice<'_, T> as Stream>::Checkpoint> for TokenSlice<'_, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn offset_from(&self, other: &<TokenSlice<'_, T> as Stream>::Checkpoint) -> usize {
|
||||
self.checkpoint().offset_from(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, O> Compare<O> for TokenSlice<'_, T>
|
||||
where
|
||||
T: PartialEq<O> + Eq,
|
||||
{
|
||||
#[inline]
|
||||
fn compare(&self, t: O) -> CompareResult {
|
||||
if let Some(token) = self.first() {
|
||||
if *token == t {
|
||||
CompareResult::Ok(1)
|
||||
} else {
|
||||
CompareResult::Error
|
||||
}
|
||||
} else {
|
||||
CompareResult::Incomplete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UpdateSlice for TokenSlice<'_, T>
|
||||
where
|
||||
T: core::fmt::Debug + Clone,
|
||||
{
|
||||
#[inline(always)]
|
||||
fn update_slice(mut self, inner: Self::Slice) -> Self {
|
||||
self.input = <&[T] as UpdateSlice>::update_slice(self.input, inner);
|
||||
self
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user