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>
207 lines
5.4 KiB
Rust
207 lines
5.4 KiB
Rust
use core::slice;
|
|
use core::mem::MaybeUninit;
|
|
|
|
use byteorder::{ByteOrder, LE};
|
|
|
|
use crate::Hasher as _;
|
|
|
|
/// 32-bit MurmurHash3 hasher
|
|
pub struct Hasher {
|
|
buf: Buffer,
|
|
index: Index,
|
|
processed: u32,
|
|
state: State,
|
|
}
|
|
|
|
struct State(u32);
|
|
|
|
#[derive(Clone, Copy)]
|
|
#[repr(align(4))]
|
|
struct Buffer {
|
|
bytes: MaybeUninit<[u8; 4]>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq)]
|
|
enum Index {
|
|
_0,
|
|
_1,
|
|
_2,
|
|
_3,
|
|
}
|
|
|
|
impl Index {
|
|
fn usize(&self) -> usize {
|
|
match *self {
|
|
Index::_0 => 0,
|
|
Index::_1 => 1,
|
|
Index::_2 => 2,
|
|
Index::_3 => 3,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<usize> for Index {
|
|
fn from(x: usize) -> Self {
|
|
match x % 4 {
|
|
0 => Index::_0,
|
|
1 => Index::_1,
|
|
2 => Index::_2,
|
|
3 => Index::_3,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Hasher {
|
|
fn push(&mut self, buf: &[u8]) {
|
|
let start = self.index.usize();
|
|
let len = buf.len();
|
|
// NOTE(unsafe) avoid calling `memcpy` on a 0-3 byte copy
|
|
// self.buf.bytes[start..start+len].copy_from(buf);
|
|
for i in 0..len {
|
|
unsafe {
|
|
*self.buf.bytes.assume_init_mut().get_unchecked_mut(start + i) = *buf.get_unchecked(i);
|
|
}
|
|
}
|
|
self.index = Index::from(start + len);
|
|
}
|
|
}
|
|
|
|
impl Default for Hasher {
|
|
#[allow(deprecated)]
|
|
fn default() -> Self {
|
|
Hasher {
|
|
buf: Buffer { bytes: MaybeUninit::uninit() },
|
|
index: Index::_0,
|
|
processed: 0,
|
|
state: State(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl crate::Hasher for Hasher {
|
|
fn finish32(&self) -> u32 {
|
|
// tail
|
|
let mut state = match self.index {
|
|
Index::_3 => {
|
|
let mut block = 0;
|
|
unsafe {
|
|
block ^= u32::from(self.buf.bytes.assume_init_ref()[2]) << 16;
|
|
block ^= u32::from(self.buf.bytes.assume_init_ref()[1]) << 8;
|
|
block ^= u32::from(self.buf.bytes.assume_init_ref()[0]);
|
|
}
|
|
self.state.0 ^ pre_mix(block)
|
|
}
|
|
Index::_2 => {
|
|
let mut block = 0;
|
|
unsafe {
|
|
block ^= u32::from(self.buf.bytes.assume_init_ref()[1]) << 8;
|
|
block ^= u32::from(self.buf.bytes.assume_init_ref()[0]);
|
|
}
|
|
self.state.0 ^ pre_mix(block)
|
|
}
|
|
Index::_1 => {
|
|
let mut block = 0;
|
|
unsafe {
|
|
block ^= u32::from(self.buf.bytes.assume_init_ref()[0]);
|
|
}
|
|
self.state.0 ^ pre_mix(block)
|
|
}
|
|
Index::_0 => self.state.0,
|
|
};
|
|
|
|
// finalization mix
|
|
state ^= self.processed;
|
|
state ^= state >> 16;
|
|
state = state.wrapping_mul(0x85ebca6b);
|
|
state ^= state >> 13;
|
|
state = state.wrapping_mul(0xc2b2ae35);
|
|
state ^= state >> 16;
|
|
|
|
state
|
|
}
|
|
}
|
|
|
|
impl core::hash::Hasher for Hasher {
|
|
#[inline]
|
|
fn write(&mut self, bytes: &[u8]) {
|
|
let len = bytes.len();
|
|
self.processed += len as u32;
|
|
|
|
let body = if self.index == Index::_0 {
|
|
bytes
|
|
} else {
|
|
let index = self.index.usize();
|
|
if len + index >= 4 {
|
|
// we can complete a block using the data left in the buffer
|
|
// NOTE(unsafe) avoid panicking branch (`slice_index_len_fail`)
|
|
// let (head, body) = bytes.split_at(4 - index);
|
|
let mid = 4 - index;
|
|
let head = unsafe { slice::from_raw_parts(bytes.as_ptr(), mid) };
|
|
let body = unsafe {
|
|
slice::from_raw_parts(bytes.as_ptr().offset(mid as isize), len - mid)
|
|
};
|
|
|
|
// NOTE(unsafe) avoid calling `memcpy` on a 0-3 byte copy
|
|
// self.buf.bytes[index..].copy_from_slice(head);
|
|
for i in 0..4 - index {
|
|
unsafe {
|
|
*self.buf.bytes.assume_init_mut().get_unchecked_mut(index + i) = *head.get_unchecked(i);
|
|
}
|
|
}
|
|
|
|
self.index = Index::_0;
|
|
|
|
self.state.process_block(&self.buf.bytes);
|
|
|
|
body
|
|
} else {
|
|
bytes
|
|
}
|
|
};
|
|
|
|
for block in body.chunks(4) {
|
|
if block.len() == 4 {
|
|
self.state
|
|
.process_block(unsafe { &*(block.as_ptr() as *const _) });
|
|
} else {
|
|
self.push(block);
|
|
}
|
|
}
|
|
|
|
// XXX is this faster?
|
|
// for block in body.exact_chunks(4) {
|
|
// self.state
|
|
// .process_block(unsafe { &*(block.as_ptr() as *const _) });
|
|
// }
|
|
|
|
// let tail = body.split_at(body.len() / 4 * 4).1;
|
|
|
|
// self.push(tail);
|
|
}
|
|
|
|
#[inline]
|
|
fn finish(&self) -> u64 {
|
|
self.finish32().into()
|
|
}
|
|
}
|
|
|
|
const C1: u32 = 0xcc9e2d51;
|
|
const C2: u32 = 0x1b873593;
|
|
const R1: u32 = 15;
|
|
|
|
impl State {
|
|
fn process_block(&mut self, block: &MaybeUninit<[u8; 4]>) {
|
|
self.0 ^= pre_mix(LE::read_u32(unsafe { block.assume_init_ref() }));
|
|
self.0 = self.0.rotate_left(13);
|
|
self.0 = 5u32.wrapping_mul(self.0).wrapping_add(0xe6546b64);
|
|
}
|
|
}
|
|
|
|
fn pre_mix(mut block: u32) -> u32 {
|
|
block = block.wrapping_mul(C1);
|
|
block = block.rotate_left(R1);
|
|
block = block.wrapping_mul(C2);
|
|
block
|
|
}
|