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>
381 lines
11 KiB
Rust
381 lines
11 KiB
Rust
// Copyright © 2018–2025 Trevor Spiteri
|
||
|
||
// This library is free software: you can redistribute it and/or
|
||
// modify it under the terms of either
|
||
//
|
||
// * the Apache License, Version 2.0 or
|
||
// * the MIT License
|
||
//
|
||
// at your option.
|
||
//
|
||
// You should have recieved copies of the Apache License and the MIT
|
||
// License along with the library. If not, see
|
||
// <https://www.apache.org/licenses/LICENSE-2.0> and
|
||
// <https://opensource.org/licenses/MIT>.
|
||
|
||
// Kept trimmed: no underscores at beginning or end of slice
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct DigitsUnds<'a> {
|
||
bytes: &'a [u8],
|
||
digits: usize,
|
||
}
|
||
|
||
impl<'a> DigitsUnds<'a> {
|
||
pub const EMPTY: DigitsUnds<'a> = DigitsUnds::new(&[]);
|
||
|
||
pub const fn new(bytes: &'a [u8]) -> DigitsUnds<'a> {
|
||
let mut digits = 0;
|
||
let mut leading_unds = 0;
|
||
let mut trailing_unds = 0;
|
||
let mut rem_bytes = bytes;
|
||
while let Some((&byte, rem)) = rem_bytes.split_first() {
|
||
rem_bytes = rem;
|
||
|
||
if byte == b'_' {
|
||
trailing_unds += 1;
|
||
} else {
|
||
if digits == 0 {
|
||
leading_unds = trailing_unds;
|
||
}
|
||
digits += 1;
|
||
trailing_unds = 0;
|
||
}
|
||
}
|
||
let without_trailing_unds = bytes.split_at(bytes.len() - trailing_unds).0;
|
||
let without_leading_unds = without_trailing_unds.split_at(leading_unds).1;
|
||
DigitsUnds {
|
||
bytes: without_leading_unds,
|
||
digits,
|
||
}
|
||
}
|
||
|
||
#[inline]
|
||
pub const fn len(self) -> usize {
|
||
self.digits
|
||
}
|
||
|
||
#[inline]
|
||
pub const fn is_empty(self) -> bool {
|
||
self.digits == 0
|
||
}
|
||
|
||
pub const fn split_at(self, mid: usize) -> (DigitsUnds<'a>, DigitsUnds<'a>) {
|
||
let mut remaining_digits = mid;
|
||
let mut unds = 0;
|
||
let mut rem_bytes = self.bytes;
|
||
while let Some((&byte, rem)) = rem_bytes.split_first() {
|
||
rem_bytes = rem;
|
||
|
||
if byte == b'_' {
|
||
unds += 1;
|
||
} else {
|
||
remaining_digits -= 1;
|
||
if remaining_digits == 0 {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
if remaining_digits > 0 {
|
||
panic!("index out of bounds");
|
||
}
|
||
let first = DigitsUnds {
|
||
bytes: self.bytes.split_at(mid + unds).0,
|
||
digits: mid,
|
||
};
|
||
|
||
// skip over underscores between first part and last part
|
||
while let Some((&b'_', rem)) = rem_bytes.split_first() {
|
||
rem_bytes = rem;
|
||
}
|
||
(
|
||
first,
|
||
DigitsUnds {
|
||
bytes: rem_bytes,
|
||
digits: self.digits - mid,
|
||
},
|
||
)
|
||
}
|
||
|
||
#[inline]
|
||
pub const fn split_first(self) -> Option<(u8, DigitsUnds<'a>)> {
|
||
let Some((&first, mut rem_bytes)) = self.bytes.split_first() else {
|
||
return None;
|
||
};
|
||
|
||
// first byte is never underscore
|
||
debug_assert!(first != b'_');
|
||
|
||
// skip over underscores between first digit and last part
|
||
while let Some((&b'_', rem)) = rem_bytes.split_first() {
|
||
rem_bytes = rem;
|
||
}
|
||
Some((
|
||
first,
|
||
DigitsUnds {
|
||
bytes: rem_bytes,
|
||
digits: self.digits - 1,
|
||
},
|
||
))
|
||
}
|
||
|
||
#[inline]
|
||
const fn split_last(self) -> Option<(u8, DigitsUnds<'a>)> {
|
||
let Some((&last, mut rem_bytes)) = self.bytes.split_last() else {
|
||
return None;
|
||
};
|
||
|
||
// last byte is never underscore
|
||
debug_assert!(last != b'_');
|
||
|
||
// skip over underscores between first part and last digit
|
||
while let Some((&b'_', rem)) = rem_bytes.split_last() {
|
||
rem_bytes = rem;
|
||
}
|
||
Some((
|
||
last,
|
||
DigitsUnds {
|
||
bytes: rem_bytes,
|
||
digits: self.digits - 1,
|
||
},
|
||
))
|
||
}
|
||
|
||
const fn split_leading_zeros(self) -> (usize, DigitsUnds<'a>) {
|
||
let mut zeros = 0;
|
||
let mut rem = self;
|
||
while let Some((b'0', rest)) = rem.split_first() {
|
||
zeros += 1;
|
||
rem = rest;
|
||
}
|
||
(zeros, rem)
|
||
}
|
||
|
||
const fn split_trailing_zeros(self) -> (usize, DigitsUnds<'a>) {
|
||
let mut zeros = 0;
|
||
let mut rem = self;
|
||
while let Some((b'0', rest)) = rem.split_last() {
|
||
zeros += 1;
|
||
rem = rest;
|
||
}
|
||
(zeros, rem)
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct DigitsExp<'a> {
|
||
leading_zeros: usize,
|
||
part1: DigitsUnds<'a>,
|
||
part2: DigitsUnds<'a>,
|
||
trailing_zeros: usize,
|
||
}
|
||
|
||
impl<'a> DigitsExp<'a> {
|
||
const EMPTY: DigitsExp<'a> = DigitsExp {
|
||
leading_zeros: 0,
|
||
part1: DigitsUnds::EMPTY,
|
||
part2: DigitsUnds::EMPTY,
|
||
trailing_zeros: 0,
|
||
};
|
||
|
||
const fn new1(digits: DigitsUnds<'a>) -> DigitsExp<'a> {
|
||
let (leading_zeros, rest) = digits.split_leading_zeros();
|
||
let (trailing_zeros, rest) = rest.split_trailing_zeros();
|
||
DigitsExp {
|
||
leading_zeros,
|
||
part1: rest,
|
||
part2: DigitsUnds::EMPTY,
|
||
trailing_zeros,
|
||
}
|
||
}
|
||
|
||
const fn new2(digits1: DigitsUnds<'a>, digits2: DigitsUnds<'a>) -> DigitsExp<'a> {
|
||
let (mut leading_zeros, mut digits1) = digits1.split_leading_zeros();
|
||
let digits2 = if digits1.is_empty() {
|
||
let (more_leading_zeros, new_digits1) = digits2.split_leading_zeros();
|
||
leading_zeros += more_leading_zeros;
|
||
digits1 = new_digits1;
|
||
DigitsUnds::EMPTY
|
||
} else {
|
||
digits2
|
||
};
|
||
let (mut trailing_zeros, digits2) = digits2.split_trailing_zeros();
|
||
if digits2.is_empty() {
|
||
let (more_trailing_zeros, new_digits1) = digits1.split_trailing_zeros();
|
||
trailing_zeros += more_trailing_zeros;
|
||
digits1 = new_digits1;
|
||
}
|
||
DigitsExp {
|
||
leading_zeros,
|
||
part1: digits1,
|
||
part2: digits2,
|
||
trailing_zeros,
|
||
}
|
||
}
|
||
|
||
// exp.unsigned_abs() must fit in usize, and results must have lengths that fit in usize
|
||
pub const fn new_int_frac(
|
||
int: DigitsUnds<'a>,
|
||
frac: DigitsUnds<'a>,
|
||
exp: i32,
|
||
) -> Option<(DigitsExp<'a>, DigitsExp<'a>)> {
|
||
if int.len() > usize::MAX - frac.len() {
|
||
return None;
|
||
}
|
||
let abs_exp = exp.unsigned_abs() as usize;
|
||
if abs_exp as u32 != exp.unsigned_abs() {
|
||
return None;
|
||
}
|
||
|
||
let (mut int, mut frac) = if exp == 0 {
|
||
(DigitsExp::new1(int), DigitsExp::new1(frac))
|
||
} else if exp < 0 {
|
||
if let Some(extra_zeros) = abs_exp.checked_sub(int.len()) {
|
||
let mut frac = DigitsExp::new2(int, frac);
|
||
frac.trailing_zeros = 0;
|
||
if extra_zeros > usize::MAX - frac.len() {
|
||
return None;
|
||
}
|
||
frac.leading_zeros += extra_zeros;
|
||
(DigitsExp::EMPTY, frac)
|
||
} else {
|
||
let int = int.split_at(int.len() - abs_exp);
|
||
(DigitsExp::new1(int.0), DigitsExp::new2(int.1, frac))
|
||
}
|
||
} else {
|
||
// exp > 0
|
||
if let Some(extra_zeros) = abs_exp.checked_sub(frac.len()) {
|
||
let mut int = DigitsExp::new2(int, frac);
|
||
int.leading_zeros = 0;
|
||
if extra_zeros > usize::MAX - int.len() {
|
||
return None;
|
||
}
|
||
int.trailing_zeros += extra_zeros;
|
||
(int, DigitsExp::EMPTY)
|
||
} else {
|
||
let frac = frac.split_at(abs_exp);
|
||
(DigitsExp::new2(int, frac.0), DigitsExp::new1(frac.1))
|
||
}
|
||
};
|
||
int.leading_zeros = 0;
|
||
if int.part1.is_empty() && int.part2.is_empty() {
|
||
int.trailing_zeros = 0;
|
||
}
|
||
frac.trailing_zeros = 0;
|
||
if frac.part2.is_empty() && frac.part1.is_empty() {
|
||
frac.leading_zeros = 0;
|
||
}
|
||
Some((int, frac))
|
||
}
|
||
|
||
#[inline]
|
||
pub const fn len(self) -> usize {
|
||
self.leading_zeros + self.part1.len() + self.part2.len() + self.trailing_zeros
|
||
}
|
||
|
||
#[inline]
|
||
pub const fn is_empty(self) -> bool {
|
||
self.len() == 0
|
||
}
|
||
|
||
pub const fn split_at(self, mut mid: usize) -> (DigitsExp<'a>, DigitsExp<'a>) {
|
||
let mut first = DigitsExp::EMPTY;
|
||
let mut last = self;
|
||
if mid == 0 {
|
||
return (first, last);
|
||
}
|
||
|
||
if mid < self.leading_zeros {
|
||
(first.leading_zeros, last.leading_zeros) = (mid, self.leading_zeros - mid);
|
||
return (first, last);
|
||
}
|
||
|
||
(first.leading_zeros, last.leading_zeros) = (self.leading_zeros, 0);
|
||
mid -= self.leading_zeros;
|
||
if mid == 0 {
|
||
return (first, last);
|
||
}
|
||
|
||
if mid < self.part1.len() {
|
||
(first.part1, last.part1) = self.part1.split_at(mid);
|
||
return (first, last);
|
||
}
|
||
|
||
first.part1 = self.part1;
|
||
last.part1 = self.part2;
|
||
last.part2 = DigitsUnds::EMPTY;
|
||
mid -= self.part1.len();
|
||
if mid == 0 {
|
||
return (first, last);
|
||
}
|
||
|
||
if mid < self.part2.len() {
|
||
(first.part2, last.part1) = self.part2.split_at(mid);
|
||
return (first, last);
|
||
}
|
||
|
||
first.part2 = self.part2;
|
||
last.leading_zeros = self.trailing_zeros;
|
||
last.part1 = DigitsUnds::EMPTY;
|
||
last.trailing_zeros = 0;
|
||
mid -= self.part2.len();
|
||
if mid == 0 {
|
||
return (first, last);
|
||
}
|
||
|
||
if mid < self.trailing_zeros {
|
||
(first.trailing_zeros, last.leading_zeros) = (mid, self.trailing_zeros - mid);
|
||
return (first, last);
|
||
}
|
||
|
||
(first.trailing_zeros, last.leading_zeros) = (self.trailing_zeros, 0);
|
||
mid -= self.trailing_zeros;
|
||
if mid == 0 {
|
||
return (first, last);
|
||
}
|
||
|
||
panic!("index out of bounds");
|
||
}
|
||
|
||
// no automatic renormalization done after split_first
|
||
#[inline]
|
||
pub const fn split_first(self) -> Option<(u8, DigitsExp<'a>)> {
|
||
if self.leading_zeros > 0 {
|
||
return Some((
|
||
b'0',
|
||
DigitsExp {
|
||
leading_zeros: self.leading_zeros - 1,
|
||
..self
|
||
},
|
||
));
|
||
}
|
||
if let Some((first, rest)) = self.part1.split_first() {
|
||
return Some((
|
||
first,
|
||
DigitsExp {
|
||
part1: rest,
|
||
..self
|
||
},
|
||
));
|
||
}
|
||
if let Some((first, rest)) = self.part2.split_first() {
|
||
return Some((
|
||
first,
|
||
DigitsExp {
|
||
part2: rest,
|
||
..self
|
||
},
|
||
));
|
||
}
|
||
if self.trailing_zeros > 0 {
|
||
return Some((
|
||
b'0',
|
||
DigitsExp {
|
||
trailing_zeros: self.trailing_zeros - 1,
|
||
..self
|
||
},
|
||
));
|
||
}
|
||
None
|
||
}
|
||
}
|