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
+291
@@ -0,0 +1,291 @@
|
||||
//! Non-blocking Hardware Abstraction Layer (HAL) traits for embedded systems, using the `nb` crate.
|
||||
//!
|
||||
//! The `embedded-hal-nb` traits make use of the
|
||||
//! [`nb`][] crate (*please go read that crate documentation before continuing*) to abstract over
|
||||
//! the asynchronous model and to also provide a blocking operation mode.
|
||||
//!
|
||||
//! [`nb`]: https://crates.io/crates/nb
|
||||
//!
|
||||
//! Here's how a HAL trait may look like:
|
||||
//!
|
||||
//! ```
|
||||
//! use embedded_hal_nb;
|
||||
//!
|
||||
//! /// A serial interface
|
||||
//! pub trait Serial {
|
||||
//! /// Error type associated to this serial interface
|
||||
//! type Error: core::fmt::Debug;
|
||||
//!
|
||||
//! /// Reads a single byte
|
||||
//! fn read(&mut self) -> nb::Result<u8, Self::Error>;
|
||||
//!
|
||||
//! /// Writes a single byte
|
||||
//! fn write(&mut self, byte: u8) -> nb::Result<(), Self::Error>;
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! The `nb::Result` enum is used to add a [`WouldBlock`] variant to the errors
|
||||
//! of the serial interface. As explained in the documentation of the `nb` crate this single API,
|
||||
//! when paired with the macros in the `nb` crate, can operate in a blocking manner, or be adapted
|
||||
//! to other asynchronous execution schemes.
|
||||
//!
|
||||
//! [`WouldBlock`]: https://docs.rs/nb/1.0.0/nb/enum.Error.html
|
||||
//!
|
||||
//! Some traits, like the one shown below, may expose possibly blocking APIs that can't fail. In
|
||||
//! those cases `nb::Result<_, Infallible>` is used.
|
||||
//!
|
||||
//! ```
|
||||
//! # use std as core;
|
||||
//! use ::core::convert::Infallible;
|
||||
//!
|
||||
//! /// A count down timer
|
||||
//! pub trait CountDown {
|
||||
//! // ..
|
||||
//!
|
||||
//! /// "waits" until the count down is over
|
||||
//! fn wait(&mut self) -> nb::Result<(), Infallible>;
|
||||
//! }
|
||||
//!
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Suggested implementation
|
||||
//!
|
||||
//! The HAL traits should be implemented for device crates generated via [`svd2rust`] to maximize
|
||||
//! code reuse.
|
||||
//!
|
||||
//! [`svd2rust`]: https://crates.io/crates/svd2rust
|
||||
//!
|
||||
//! Shown below is an implementation of some of the HAL traits for the [`stm32f1xx-hal`] crate. This
|
||||
//! single implementation will work for *any* microcontroller in the `STM32F1xx` family.
|
||||
//!
|
||||
//! [`stm32f1`]: https://crates.io/crates/stm32f1
|
||||
//!
|
||||
//! ```no_run
|
||||
//! // crate: stm32f1xx-hal
|
||||
//! // An implementation of the `embedded-hal` traits for STM32F1xx microcontrollers
|
||||
//!
|
||||
//! use embedded_hal_nb::serial;
|
||||
//! use nb;
|
||||
//!
|
||||
//! // device crate
|
||||
//! use stm32f1::stm32f103::USART1;
|
||||
//!
|
||||
//! /// A serial interface
|
||||
//! // NOTE generic over the USART peripheral
|
||||
//! pub struct Serial<USART> { usart: USART }
|
||||
//!
|
||||
//! // convenience type alias
|
||||
//! pub type Serial1 = Serial<USART1>;
|
||||
//!
|
||||
//! impl serial::ErrorType for Serial<USART1> {
|
||||
//! type Error = serial::ErrorKind;
|
||||
//! }
|
||||
//!
|
||||
//! impl embedded_hal_nb::serial::Read<u8> for Serial<USART1> {
|
||||
//! fn read(&mut self) -> nb::Result<u8, Self::Error> {
|
||||
//! // read the status register
|
||||
//! let isr = self.usart.sr.read();
|
||||
//!
|
||||
//! if isr.ore().bit_is_set() {
|
||||
//! // Error: Buffer overrun
|
||||
//! Err(nb::Error::Other(Self::Error::Overrun))
|
||||
//! }
|
||||
//! // omitted: checks for other errors
|
||||
//! else if isr.rxne().bit_is_set() {
|
||||
//! // Data available: read the data register
|
||||
//! Ok(self.usart.dr.read().bits() as u8)
|
||||
//! } else {
|
||||
//! // No data available yet
|
||||
//! Err(nb::Error::WouldBlock)
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl embedded_hal_nb::serial::Write<u8> for Serial<USART1> {
|
||||
//! fn write(&mut self, byte: u8) -> nb::Result<(), Self::Error> {
|
||||
//! // Similar to the `read` implementation
|
||||
//! # Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! fn flush(&mut self) -> nb::Result<(), Self::Error> {
|
||||
//! // Similar to the `read` implementation
|
||||
//! # Ok(())
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Intended usage
|
||||
//!
|
||||
//! Thanks to the [`nb`] crate the HAL API can be used in a blocking manner
|
||||
//! with the [`block!`] macro or with `futures`.
|
||||
//!
|
||||
//! [`block!`]: https://docs.rs/nb/1.0.0/nb/macro.block.html
|
||||
//!
|
||||
//! ### Blocking mode
|
||||
//!
|
||||
//! An example of writing a string over the serial interface in a blocking
|
||||
//! fashion:
|
||||
//!
|
||||
//! ```
|
||||
//! use stm32f1xx_hal::Serial1;
|
||||
//! use embedded_hal_nb::serial::Write;
|
||||
//! use nb::block;
|
||||
//!
|
||||
//! # fn main() {
|
||||
//! let mut serial: Serial1 = {
|
||||
//! // ..
|
||||
//! # Serial1
|
||||
//! };
|
||||
//!
|
||||
//! for byte in b"Hello, world!" {
|
||||
//! // NOTE `block!` blocks until `serial.write()` completes and returns
|
||||
//! // `Result<(), Error>`
|
||||
//! block!(serial.write(*byte)).unwrap();
|
||||
//! }
|
||||
//! # }
|
||||
//!
|
||||
//! # mod stm32f1xx_hal {
|
||||
//! # use embedded_hal_nb;
|
||||
//! # use core::convert::Infallible;
|
||||
//! # pub struct Serial1;
|
||||
//! # impl Serial1 {
|
||||
//! # pub fn write(&mut self, _: u8) -> nb::Result<(), Infallible> {
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # }
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Generic programming and higher level abstractions
|
||||
//!
|
||||
//! The core of the HAL has been kept minimal on purpose to encourage building **generic** higher
|
||||
//! level abstractions on top of it. Some higher level abstractions that pick an asynchronous model
|
||||
//! or that have blocking behavior and that are deemed useful to build other abstractions can be
|
||||
//! found in the `blocking` module.
|
||||
//!
|
||||
//! Some examples:
|
||||
//!
|
||||
//! **NOTE** All the functions shown below could have been written as trait
|
||||
//! methods with default implementation to allow specialization, but they have
|
||||
//! been written as functions to keep things simple.
|
||||
//!
|
||||
//! - Write a whole buffer to a serial device in blocking a fashion.
|
||||
//!
|
||||
//! ```
|
||||
//! use embedded_hal_nb::serial::Write;
|
||||
//! use nb::block;
|
||||
//!
|
||||
//! fn write_all<S>(serial: &mut S, buffer: &[u8]) -> Result<(), S::Error>
|
||||
//! where
|
||||
//! S: Write<u8>
|
||||
//! {
|
||||
//! for &byte in buffer {
|
||||
//! block!(serial.write(byte))?;
|
||||
//! }
|
||||
//!
|
||||
//! Ok(())
|
||||
//! }
|
||||
//!
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! - Buffered serial interface with periodic flushing in interrupt handler
|
||||
//!
|
||||
//! ```
|
||||
//! # use std as core;
|
||||
//! use embedded_hal_nb::serial::{ErrorKind, Write};
|
||||
//! use nb::block;
|
||||
//!
|
||||
//! fn flush<S>(serial: &mut S, cb: &mut CircularBuffer)
|
||||
//! where
|
||||
//! S: Write<u8, Error = ErrorKind>,
|
||||
//! {
|
||||
//! loop {
|
||||
//! if let Some(byte) = cb.peek() {
|
||||
//! match serial.write(*byte) {
|
||||
//! Err(nb::Error::Other(_)) => unreachable!(),
|
||||
//! Err(nb::Error::WouldBlock) => return,
|
||||
//! Ok(()) => {}, // keep flushing data
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! cb.pop();
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! // The stuff below could be in some other crate
|
||||
//!
|
||||
//! /// Global singleton
|
||||
//! pub struct BufferedSerial1;
|
||||
//!
|
||||
//! // NOTE private
|
||||
//! static BUFFER1: Mutex<CircularBuffer> = {
|
||||
//! // ..
|
||||
//! # Mutex(CircularBuffer)
|
||||
//! };
|
||||
//! static SERIAL1: Mutex<Serial1> = {
|
||||
//! // ..
|
||||
//! # Mutex(Serial1)
|
||||
//! };
|
||||
//!
|
||||
//! impl BufferedSerial1 {
|
||||
//! pub fn write(&self, byte: u8) {
|
||||
//! self.write_all(&[byte])
|
||||
//! }
|
||||
//!
|
||||
//! pub fn write_all(&self, bytes: &[u8]) {
|
||||
//! let mut buffer = BUFFER1.lock();
|
||||
//! for byte in bytes {
|
||||
//! buffer.push(*byte).expect("buffer overrun");
|
||||
//! }
|
||||
//! // omitted: pend / enable interrupt_handler
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn interrupt_handler() {
|
||||
//! let mut serial = SERIAL1.lock();
|
||||
//! let mut buffer = BUFFER1.lock();
|
||||
//!
|
||||
//! flush(&mut *serial, &mut buffer);
|
||||
//! }
|
||||
//!
|
||||
//! # struct Mutex<T>(T);
|
||||
//! # impl<T> Mutex<T> {
|
||||
//! # fn lock(&self) -> RefMut<T> { unimplemented!() }
|
||||
//! # }
|
||||
//! # struct RefMut<'a, T>(&'a mut T) where T: 'a;
|
||||
//! # impl<'a, T> ::core::ops::Deref for RefMut<'a, T> {
|
||||
//! # type Target = T;
|
||||
//! # fn deref(&self) -> &T { self.0 }
|
||||
//! # }
|
||||
//! # impl<'a, T> ::core::ops::DerefMut for RefMut<'a, T> {
|
||||
//! # fn deref_mut(&mut self) -> &mut T { self.0 }
|
||||
//! # }
|
||||
//! # struct Serial1;
|
||||
//! # impl embedded_hal_nb::serial::ErrorType for Serial1 {
|
||||
//! # type Error = ErrorKind;
|
||||
//! # }
|
||||
//! # impl embedded_hal_nb::serial::Write<u8> for Serial1 {
|
||||
//! # fn write(&mut self, _: u8) -> nb::Result<(), Self::Error> { Err(nb::Error::WouldBlock) }
|
||||
//! # fn flush(&mut self) -> nb::Result<(), Self::Error> { Err(nb::Error::WouldBlock) }
|
||||
//! # }
|
||||
//! # struct CircularBuffer;
|
||||
//! # impl CircularBuffer {
|
||||
//! # pub fn peek(&mut self) -> Option<&u8> { None }
|
||||
//! # pub fn pop(&mut self) -> Option<u8> { None }
|
||||
//! # pub fn push(&mut self, _: u8) -> Result<(), ()> { Ok(()) }
|
||||
//! # }
|
||||
//!
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![no_std]
|
||||
|
||||
pub use nb;
|
||||
|
||||
pub mod serial;
|
||||
pub mod spi;
|
||||
Vendored
+132
@@ -0,0 +1,132 @@
|
||||
//! Serial interface.
|
||||
|
||||
/// Serial error.
|
||||
pub trait Error: core::fmt::Debug {
|
||||
/// Convert error to a generic serial error kind
|
||||
///
|
||||
/// By using this method, serial errors freely defined by HAL implementations
|
||||
/// can be converted to a set of generic serial errors upon which generic
|
||||
/// code can act.
|
||||
fn kind(&self) -> ErrorKind;
|
||||
}
|
||||
|
||||
impl Error for core::convert::Infallible {
|
||||
#[inline]
|
||||
fn kind(&self) -> ErrorKind {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serial error kind.
|
||||
///
|
||||
/// This represents a common set of serial operation errors. HAL implementations are
|
||||
/// free to define more specific or additional error types. However, by providing
|
||||
/// a mapping to these common serial errors, generic code can still react to them.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum ErrorKind {
|
||||
/// The peripheral receive buffer was overrun.
|
||||
Overrun,
|
||||
/// Received data does not conform to the peripheral configuration.
|
||||
/// Can be caused by a misconfigured device on either end of the serial line.
|
||||
FrameFormat,
|
||||
/// Parity check failed.
|
||||
Parity,
|
||||
/// Serial line is too noisy to read valid data.
|
||||
Noise,
|
||||
/// A different error occurred. The original error may contain more information.
|
||||
Other,
|
||||
}
|
||||
|
||||
impl Error for ErrorKind {
|
||||
#[inline]
|
||||
fn kind(&self) -> ErrorKind {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ErrorKind {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Overrun => write!(f, "The peripheral receive buffer was overrun"),
|
||||
Self::Parity => write!(f, "Parity check failed"),
|
||||
Self::Noise => write!(f, "Serial line is too noisy to read valid data"),
|
||||
Self::FrameFormat => write!(
|
||||
f,
|
||||
"Received data does not conform to the peripheral configuration"
|
||||
),
|
||||
Self::Other => write!(
|
||||
f,
|
||||
"A different error occurred. The original error may contain more information"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Serial error type trait.
|
||||
///
|
||||
/// This just defines the error type, to be used by the other traits.
|
||||
pub trait ErrorType {
|
||||
/// Error type
|
||||
type Error: Error;
|
||||
}
|
||||
|
||||
impl<T: ErrorType + ?Sized> ErrorType for &mut T {
|
||||
type Error = T::Error;
|
||||
}
|
||||
|
||||
/// Read half of a serial interface.
|
||||
///
|
||||
/// Some serial interfaces support different data sizes (8 bits, 9 bits, etc.);
|
||||
/// This can be encoded in this trait via the `Word` type parameter.
|
||||
pub trait Read<Word: Copy = u8>: ErrorType {
|
||||
/// Reads a single word from the serial interface
|
||||
fn read(&mut self) -> nb::Result<Word, Self::Error>;
|
||||
}
|
||||
|
||||
impl<T: Read<Word> + ?Sized, Word: Copy> Read<Word> for &mut T {
|
||||
#[inline]
|
||||
fn read(&mut self) -> nb::Result<Word, Self::Error> {
|
||||
T::read(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Write half of a serial interface.
|
||||
pub trait Write<Word: Copy = u8>: ErrorType {
|
||||
/// Writes a single word to the serial interface.
|
||||
fn write(&mut self, word: Word) -> nb::Result<(), Self::Error>;
|
||||
|
||||
/// Ensures that none of the previously written words are still buffered.
|
||||
fn flush(&mut self) -> nb::Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl<T: Write<Word> + ?Sized, Word: Copy> Write<Word> for &mut T {
|
||||
#[inline]
|
||||
fn write(&mut self, word: Word) -> nb::Result<(), Self::Error> {
|
||||
T::write(self, word)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn flush(&mut self) -> nb::Result<(), Self::Error> {
|
||||
T::flush(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of `core::fmt::Write` for the HAL's `serial::Write`.
|
||||
///
|
||||
/// TODO write example of usage
|
||||
|
||||
impl<Word, Error: self::Error> core::fmt::Write for dyn Write<Word, Error = Error> + '_
|
||||
where
|
||||
Word: Copy + From<u8>,
|
||||
{
|
||||
#[inline]
|
||||
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||
let _ = s
|
||||
.bytes()
|
||||
.map(|c| nb::block!(self.write(Word::from(c))))
|
||||
.last();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Vendored
+44
@@ -0,0 +1,44 @@
|
||||
//! SPI master mode traits using `nb`.
|
||||
|
||||
pub use embedded_hal::spi::{
|
||||
Error, ErrorKind, ErrorType, Mode, Phase, Polarity, MODE_0, MODE_1, MODE_2, MODE_3,
|
||||
};
|
||||
|
||||
/// Full duplex SPI (master mode).
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// - It's the task of the user of this interface to manage the slave select lines.
|
||||
///
|
||||
/// - Due to how full duplex SPI works each `read` call must be preceded by a `write` call.
|
||||
///
|
||||
/// - `read` calls only return the data received with the last `write` call.
|
||||
/// Previously received data is discarded
|
||||
///
|
||||
/// - Data is only guaranteed to be clocked out when the `read` call succeeds.
|
||||
/// The slave select line shouldn't be released before that.
|
||||
///
|
||||
/// - Some SPIs can work with 8-bit *and* 16-bit words. You can overload this trait with different
|
||||
/// `Word` types to allow operation in both modes.
|
||||
pub trait FullDuplex<Word: Copy = u8>: ErrorType {
|
||||
/// Reads the word stored in the shift register
|
||||
///
|
||||
/// **NOTE** A word must be sent to the slave before attempting to call this
|
||||
/// method.
|
||||
fn read(&mut self) -> nb::Result<Word, Self::Error>;
|
||||
|
||||
/// Writes a word to the slave
|
||||
fn write(&mut self, word: Word) -> nb::Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
impl<T: FullDuplex<Word> + ?Sized, Word: Copy> FullDuplex<Word> for &mut T {
|
||||
#[inline]
|
||||
fn read(&mut self) -> nb::Result<Word, Self::Error> {
|
||||
T::read(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn write(&mut self, word: Word) -> nb::Result<(), Self::Error> {
|
||||
T::write(self, word)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user