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
+327
@@ -0,0 +1,327 @@
|
||||
// SPDX-License-Identifier: Apache-2.0 OR MIT
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use crate::sys;
|
||||
|
||||
/// A specialized [`Result`] type for I/O operations.
|
||||
///
|
||||
/// See [`std::io::Result` documentation][std] for details.
|
||||
///
|
||||
/// [std]: https://doc.rust-lang.org/std/io/type.Result.html
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// The error type for I/O operations.
|
||||
///
|
||||
/// See [`std::io::Error` documentation][std] for details.
|
||||
///
|
||||
/// [std]: https://doc.rust-lang.org/std/io/struct.Error.html
|
||||
pub struct Error {
|
||||
repr: Repr,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.repr, f)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! const_error {
|
||||
($kind:expr, $message:expr $(,)?) => {
|
||||
$crate::io::Error::from_static_message({
|
||||
const MESSAGE_DATA: $crate::io::error::SimpleMessage =
|
||||
$crate::io::error::SimpleMessage { kind: $kind, message: $message };
|
||||
&MESSAGE_DATA
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Error {
|
||||
pub(crate) const INVALID_UTF8: Self =
|
||||
const_error!(ErrorKind::InvalidData, "stream did not contain valid UTF-8");
|
||||
|
||||
pub(crate) const READ_EXACT_EOF: Self =
|
||||
const_error!(ErrorKind::UnexpectedEof, "failed to fill whole buffer");
|
||||
|
||||
pub(crate) const UNKNOWN_THREAD_COUNT: Self = const_error!(
|
||||
ErrorKind::NotFound,
|
||||
"The number of hardware threads is not known for the target platform",
|
||||
);
|
||||
|
||||
pub(crate) const UNSUPPORTED_PLATFORM: Self =
|
||||
const_error!(ErrorKind::Unsupported, "operation not supported on this platform");
|
||||
|
||||
pub(crate) const WRITE_ALL_EOF: Self =
|
||||
const_error!(ErrorKind::WriteZero, "failed to write whole buffer");
|
||||
|
||||
pub(crate) const ZERO_TIMEOUT: Self =
|
||||
const_error!(ErrorKind::InvalidInput, "cannot set a 0 duration timeout");
|
||||
|
||||
#[cfg(feature = "fs")]
|
||||
pub(crate) const EINVAL: Self = Self { repr: Repr::Os(sys::EINVAL) };
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl From<alloc::ffi::NulError> for Error {
|
||||
/// Converts a [`alloc::ffi::NulError`] into a [`Error`].
|
||||
fn from(_: alloc::ffi::NulError) -> Error {
|
||||
const_error!(ErrorKind::InvalidInput, "data provided contains a nul byte")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
impl From<alloc::collections::TryReserveError> for Error {
|
||||
/// Converts `TryReserveError` to an error with [`ErrorKind::OutOfMemory`].
|
||||
///
|
||||
/// `TryReserveError` won't be available as the error `source()`,
|
||||
/// but this may change in the future.
|
||||
fn from(_: alloc::collections::TryReserveError) -> Error {
|
||||
// ErrorData::Custom allocates, which isn't great for handling OOM errors.
|
||||
ErrorKind::OutOfMemory.into()
|
||||
}
|
||||
}
|
||||
|
||||
enum Repr {
|
||||
Os(RawOsError),
|
||||
Simple(ErrorKind),
|
||||
SimpleMessage(&'static SimpleMessage),
|
||||
}
|
||||
|
||||
/// The type of raw OS error codes returned by [`Error::raw_os_error`].
|
||||
///
|
||||
/// See [`std::io::RawOsError` documentation][std] for details.
|
||||
///
|
||||
/// [std]: https://doc.rust-lang.org/nightly/std/io/type.RawOsError.html
|
||||
pub type RawOsError = i32;
|
||||
|
||||
pub(crate) struct SimpleMessage {
|
||||
pub(crate) kind: ErrorKind,
|
||||
pub(crate) message: &'static str,
|
||||
}
|
||||
|
||||
/// A list specifying general categories of I/O error.
|
||||
///
|
||||
/// See [`std::io::ErrorKind` documentation][std] for details.
|
||||
///
|
||||
/// [std]: https://doc.rust-lang.org/std/io/enum.ErrorKind.html
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum ErrorKind {
|
||||
NotFound,
|
||||
PermissionDenied,
|
||||
ConnectionRefused,
|
||||
ConnectionReset,
|
||||
HostUnreachable,
|
||||
NetworkUnreachable,
|
||||
ConnectionAborted,
|
||||
NotConnected,
|
||||
AddrInUse,
|
||||
AddrNotAvailable,
|
||||
NetworkDown,
|
||||
BrokenPipe,
|
||||
AlreadyExists,
|
||||
WouldBlock,
|
||||
NotADirectory,
|
||||
IsADirectory,
|
||||
DirectoryNotEmpty,
|
||||
ReadOnlyFilesystem,
|
||||
#[doc(hidden)]
|
||||
__FilesystemLoop, // unstable https://github.com/rust-lang/rust/issues/130188
|
||||
StaleNetworkFileHandle,
|
||||
InvalidInput,
|
||||
InvalidData,
|
||||
TimedOut,
|
||||
WriteZero,
|
||||
StorageFull,
|
||||
NotSeekable,
|
||||
QuotaExceeded,
|
||||
FileTooLarge,
|
||||
ResourceBusy,
|
||||
ExecutableFileBusy,
|
||||
Deadlock,
|
||||
CrossesDevices,
|
||||
TooManyLinks,
|
||||
InvalidFilename,
|
||||
ArgumentListTooLong,
|
||||
Interrupted,
|
||||
Unsupported,
|
||||
UnexpectedEof,
|
||||
OutOfMemory,
|
||||
#[doc(hidden)]
|
||||
__InProgress, // unstable https://github.com/rust-lang/rust/issues/130840
|
||||
Other,
|
||||
// Uncategorized, // unstable, private api
|
||||
}
|
||||
|
||||
impl ErrorKind {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
#[allow(clippy::enum_glob_use)]
|
||||
use ErrorKind::*;
|
||||
match self {
|
||||
AddrInUse => "address in use",
|
||||
AddrNotAvailable => "address not available",
|
||||
AlreadyExists => "entity already exists",
|
||||
ArgumentListTooLong => "argument list too long",
|
||||
BrokenPipe => "broken pipe",
|
||||
ConnectionAborted => "connection aborted",
|
||||
ConnectionRefused => "connection refused",
|
||||
ConnectionReset => "connection reset",
|
||||
CrossesDevices => "cross-device link or rename",
|
||||
Deadlock => "deadlock",
|
||||
DirectoryNotEmpty => "directory not empty",
|
||||
ExecutableFileBusy => "executable file busy",
|
||||
__FilesystemLoop => "filesystem loop or indirection limit (e.g. symlink loop)",
|
||||
FileTooLarge => "file too large",
|
||||
HostUnreachable => "host unreachable",
|
||||
__InProgress => "in progress",
|
||||
Interrupted => "operation interrupted",
|
||||
InvalidData => "invalid data",
|
||||
InvalidFilename => "invalid filename",
|
||||
InvalidInput => "invalid input parameter",
|
||||
IsADirectory => "is a directory",
|
||||
NetworkDown => "network down",
|
||||
NetworkUnreachable => "network unreachable",
|
||||
NotADirectory => "not a directory",
|
||||
NotConnected => "not connected",
|
||||
NotFound => "entity not found",
|
||||
NotSeekable => "seek on unseekable file",
|
||||
Other => "other error",
|
||||
OutOfMemory => "out of memory",
|
||||
PermissionDenied => "permission denied",
|
||||
QuotaExceeded => "quota exceeded",
|
||||
ReadOnlyFilesystem => "read-only filesystem or storage medium",
|
||||
ResourceBusy => "resource busy",
|
||||
StaleNetworkFileHandle => "stale network file handle",
|
||||
StorageFull => "no storage space",
|
||||
TimedOut => "timed out",
|
||||
TooManyLinks => "too many links",
|
||||
// Uncategorized => "uncategorized error",
|
||||
UnexpectedEof => "unexpected end of file",
|
||||
Unsupported => "unsupported",
|
||||
WouldBlock => "operation would block",
|
||||
WriteZero => "write zero",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
/// Shows a human-readable description of the `ErrorKind`.
|
||||
///
|
||||
/// This is similar to `impl Display for Error`, but doesn't require first converting to Error.
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// Intended for use for errors not exposed to the user, where allocating onto
|
||||
/// the heap (for normal construction via Error::new) is too costly.
|
||||
impl From<ErrorKind> for Error {
|
||||
/// Converts an [`ErrorKind`] into an [`Error`].
|
||||
///
|
||||
/// This conversion creates a new error with a simple representation of error kind.
|
||||
#[inline]
|
||||
fn from(kind: ErrorKind) -> Self {
|
||||
Self { repr: Repr::Simple(kind) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
#[inline]
|
||||
pub(crate) const fn from_static_message(msg: &'static SimpleMessage) -> Error {
|
||||
Self { repr: Repr::SimpleMessage(msg) }
|
||||
}
|
||||
|
||||
// TODO(io,alloc): provide new,other when alloc feature is enabled?
|
||||
|
||||
// TODO(io): last_os_error: Arm semihosting has sys_errno, but MIPS UHI doesn't.
|
||||
|
||||
/// Creates a new instance of an `Error` from a particular OS error code.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn from_raw_os_error(os: RawOsError) -> Self {
|
||||
Self { repr: Repr::Os(os) }
|
||||
}
|
||||
|
||||
/// Returns the OS error that this error represents (if any).
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn raw_os_error(&self) -> Option<RawOsError> {
|
||||
match self.repr {
|
||||
Repr::Os(code) => Some(code),
|
||||
// Repr::Custom(..) |
|
||||
Repr::Simple(..) | Repr::SimpleMessage(..) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the corresponding [`ErrorKind`] for this error.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
match self.repr {
|
||||
Repr::Os(code) => sys::decode_error_kind(code),
|
||||
// Repr::Custom(ref c) => c.kind,
|
||||
Repr::Simple(kind) => kind,
|
||||
Repr::SimpleMessage(msg) => msg.kind,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_interrupted(&self) -> bool {
|
||||
match self.repr {
|
||||
Repr::Os(code) => sys::is_interrupted(code),
|
||||
// Repr::Custom(ref c) => c.kind == ErrorKind::Interrupted,
|
||||
Repr::Simple(kind) => kind == ErrorKind::Interrupted,
|
||||
Repr::SimpleMessage(m) => m.kind == ErrorKind::Interrupted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Repr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Os(code) => f
|
||||
.debug_struct("Os")
|
||||
.field("code", &code)
|
||||
.field("kind", &sys::decode_error_kind(*code))
|
||||
// TODO(io)
|
||||
// .field("message", &sys::os::error_string(code))
|
||||
.finish(),
|
||||
// Self::Custom(c) => fmt::Debug::fmt(&c, fmt),
|
||||
Self::Simple(kind) => f.debug_tuple("Kind").field(&kind).finish(),
|
||||
Self::SimpleMessage(msg) => f
|
||||
.debug_struct("Error")
|
||||
.field("kind", &msg.kind)
|
||||
.field("message", &msg.message)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.repr {
|
||||
Repr::Os(code) => {
|
||||
// TODO(io)
|
||||
// let detail = sys::os::error_string(code);
|
||||
// write!(f, "{detail} (os error {code})")
|
||||
let detail = sys::decode_error_kind(code);
|
||||
write!(f, "{detail} (os error {code})")
|
||||
}
|
||||
// Repr::Custom(ref c) => c.error.fmt(fmt),
|
||||
Repr::Simple(kind) => f.write_str(kind.as_str()),
|
||||
Repr::SimpleMessage(msg) => msg.message.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(semihosting_no_error_in_core))]
|
||||
impl core::error::Error for Error {
|
||||
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
|
||||
match self.repr {
|
||||
Repr::Os(..) | Repr::Simple(..) | Repr::SimpleMessage(..) => None,
|
||||
// Repr::Custom(c) => c.error.source(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user