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:
swaitsandClaude Opus 4.6 committed 2026-03-29 20:42:54 -06:00
1 parent 7064110293
commit 3102fc747a
17624 files changed
+4564928 -38

No files matched your search

+327
View File
@@ -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(),
}
}
}
+499
View File
@@ -0,0 +1,499 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
use core::{cmp, fmt, mem};
use crate::io::{self, Read, Seek, SeekFrom, Write};
// -----------------------------------------------------------------------------
// Forwarding implementations
impl<R: ?Sized + Read> Read for &mut R {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
(**self).read(buf)
}
// #[inline]
// fn read_buf(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
// (**self).read_buf(cursor)
// }
// #[inline]
// fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
// (**self).read_vectored(bufs)
// }
// #[inline]
// fn is_read_vectored(&self) -> bool {
// (**self).is_read_vectored()
// }
// #[inline]
// fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
// (**self).read_to_end(buf)
// }
// #[inline]
// fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
// (**self).read_to_string(buf)
// }
#[inline]
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
(**self).read_exact(buf)
}
// #[inline]
// fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
// (**self).read_buf_exact(cursor)
// }
}
impl<W: ?Sized + Write> Write for &mut W {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(**self).write(buf)
}
// #[inline]
// fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
// (**self).write_vectored(bufs)
// }
// #[inline]
// fn is_write_vectored(&self) -> bool {
// (**self).is_write_vectored()
// }
#[inline]
fn flush(&mut self) -> io::Result<()> {
(**self).flush()
}
#[inline]
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
(**self).write_all(buf)
}
// #[inline]
// fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> io::Result<()> {
// (**self).write_all_vectored(bufs)
// }
#[inline]
fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
(**self).write_fmt(fmt)
}
}
impl<S: ?Sized + Seek> Seek for &mut S {
#[inline]
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
(**self).seek(pos)
}
#[inline]
fn rewind(&mut self) -> io::Result<()> {
(**self).rewind()
}
// #[inline]
// fn stream_len(&mut self) -> io::Result<u64> {
// (**self).stream_len()
// }
// #[inline]
// fn stream_position(&mut self) -> io::Result<u64> {
// (**self).stream_position()
// }
// #[inline]
// fn seek_relative(&mut self, offset: i64) -> io::Result<()> {
// (**self).seek_relative(offset)
// }
}
#[cfg(feature = "alloc")]
impl<R: ?Sized + Read> Read for alloc::boxed::Box<R> {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
(**self).read(buf)
}
// #[inline]
// fn read_buf(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
// (**self).read_buf(cursor)
// }
// #[inline]
// fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
// (**self).read_vectored(bufs)
// }
// #[inline]
// fn is_read_vectored(&self) -> bool {
// (**self).is_read_vectored()
// }
// #[inline]
// fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
// (**self).read_to_end(buf)
// }
// #[inline]
// fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
// (**self).read_to_string(buf)
// }
#[inline]
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
(**self).read_exact(buf)
}
// #[inline]
// fn read_buf_exact(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
// (**self).read_buf_exact(cursor)
// }
}
#[cfg(feature = "alloc")]
impl<W: ?Sized + Write> Write for alloc::boxed::Box<W> {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(**self).write(buf)
}
// #[inline]
// fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
// (**self).write_vectored(bufs)
// }
// #[inline]
// fn is_write_vectored(&self) -> bool {
// (**self).is_write_vectored()
// }
#[inline]
fn flush(&mut self) -> io::Result<()> {
(**self).flush()
}
#[inline]
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
(**self).write_all(buf)
}
// #[inline]
// fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> io::Result<()> {
// (**self).write_all_vectored(bufs)
// }
#[inline]
fn write_fmt(&mut self, fmt: fmt::Arguments<'_>) -> io::Result<()> {
(**self).write_fmt(fmt)
}
}
#[cfg(feature = "alloc")]
impl<S: ?Sized + Seek> Seek for alloc::boxed::Box<S> {
#[inline]
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
(**self).seek(pos)
}
#[inline]
fn rewind(&mut self) -> io::Result<()> {
(**self).rewind()
}
// #[inline]
// fn stream_len(&mut self) -> io::Result<u64> {
// (**self).stream_len()
// }
// #[inline]
// fn stream_position(&mut self) -> io::Result<u64> {
// (**self).stream_position()
// }
// #[inline]
// fn seek_relative(&mut self, offset: i64) -> io::Result<()> {
// (**self).seek_relative(offset)
// }
}
// -----------------------------------------------------------------------------
// In-memory buffer implementations
/// Read is implemented for `&[u8]` by copying from the slice.
///
/// Note that reading updates the slice to point to the yet unread part.
/// The slice will be empty when EOF is reached.
impl Read for &[u8] {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let amt = cmp::min(buf.len(), self.len());
let (a, b) = self.split_at(amt);
// First check if the amount of bytes we want to read is small:
// `copy_from_slice` will generally expand to a call to `memcpy`, and
// for a single byte the overhead is significant.
if amt == 1 {
buf[0] = a[0];
} else {
buf[..amt].copy_from_slice(a);
}
*self = b;
Ok(amt)
}
// #[inline]
// fn read_buf(&mut self, mut cursor: BorrowedCursor<'_>) -> io::Result<()> {
// let amt = cmp::min(cursor.capacity(), self.len());
// let (a, b) = self.split_at(amt);
//
// cursor.append(a);
//
// *self = b;
// Ok(())
// }
// #[inline]
// fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
// let mut n_read = 0;
// for buf in bufs {
// n_read += self.read(buf)?;
// if self.is_empty() {
// break;
// }
// }
//
// Ok(n_read)
// }
// #[inline]
// fn is_read_vectored(&self) -> bool {
// true
// }
#[inline]
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
if buf.len() > self.len() {
// `read_exact` makes no promise about the content of `buf` if it
// fails so don't bother about that.
*self = &self[self.len()..];
return Err(io::Error::READ_EXACT_EOF);
}
let (a, b) = self.split_at(buf.len());
// First check if the amount of bytes we want to read is small:
// `copy_from_slice` will generally expand to a call to `memcpy`, and
// for a single byte the overhead is significant.
if buf.len() == 1 {
buf[0] = a[0];
} else {
buf.copy_from_slice(a);
}
*self = b;
Ok(())
}
// #[inline]
// fn read_buf_exact(&mut self, mut cursor: BorrowedCursor<'_>) -> io::Result<()> {
// if cursor.capacity() > self.len() {
// // Append everything we can to the cursor.
// cursor.append(*self);
// *self = &self[self.len()..];
// return Err(io::Error::READ_EXACT_EOF);
// }
// let (a, b) = self.split_at(cursor.capacity());
//
// cursor.append(a);
//
// *self = b;
// Ok(())
// }
// #[inline]
// fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
// let len = self.len();
// buf.try_reserve(len)?;
// buf.extend_from_slice(*self);
// *self = &self[len..];
// Ok(len)
// }
// #[inline]
// fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
// let content = str::from_utf8(self).map_err(|_| io::Error::INVALID_UTF8)?;
// let len = self.len();
// buf.try_reserve(len)?;
// buf.push_str(content);
// *self = &self[len..];
// Ok(len)
// }
}
/// Write is implemented for `&mut [u8]` by copying into the slice, overwriting
/// its data.
///
/// Note that writing updates the slice to point to the yet unwritten part.
/// The slice will be empty when it has been completely overwritten.
///
/// If the number of bytes to be written exceeds the size of the slice, write operations will
/// return short writes: ultimately, `Ok(0)`; in this situation, `write_all` returns an error of
/// kind `ErrorKind::WriteZero`.
impl Write for &mut [u8] {
#[inline]
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
let amt = cmp::min(data.len(), self.len());
let (a, b) = mem::take(self).split_at_mut(amt);
a.copy_from_slice(&data[..amt]);
*self = b;
Ok(amt)
}
// #[inline]
// fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
// let mut n_written = 0;
// for buf in bufs {
// n_written += self.write(buf)?;
// if self.is_empty() {
// break;
// }
// }
//
// Ok(n_written)
// }
// #[inline]
// fn is_write_vectored(&self) -> bool {
// true
// }
#[inline]
fn write_all(&mut self, data: &[u8]) -> io::Result<()> {
if self.write(data)? == data.len() { Ok(()) } else { Err(io::Error::WRITE_ALL_EOF) }
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// Write is implemented for `Vec<u8>` by appending to the vector.
/// The vector will grow as needed.
#[cfg(feature = "alloc")]
impl Write for alloc::vec::Vec<u8> {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.extend_from_slice(buf);
Ok(buf.len())
}
// #[inline]
// fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
// let len = bufs.iter().map(|b| b.len()).sum();
// self.reserve(len);
// for buf in bufs {
// self.extend_from_slice(buf);
// }
// Ok(len)
// }
// #[inline]
// fn is_write_vectored(&self) -> bool {
// true
// }
#[inline]
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.extend_from_slice(buf);
Ok(())
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// Read is implemented for `VecDeque<u8>` by consuming bytes from the front of the `VecDeque`.
#[cfg(feature = "alloc")]
impl Read for alloc::collections::VecDeque<u8> {
/// Fill `buf` with the contents of the "front" slice as returned by
/// [`as_slices`][`alloc::collections::VecDeque::as_slices`]. If the contained byte slices of the `VecDeque` are
/// discontiguous, multiple calls to `read` will be needed to read the entire content.
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
let (ref mut front, _) = self.as_slices();
let n = Read::read(front, buf)?;
self.drain(..n);
Ok(n)
}
fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
let (front, back) = self.as_slices();
// Use only the front buffer if it is big enough to fill `buf`, else use
// the back buffer too.
match split_at_mut_checked(buf, front.len()) {
None => buf.copy_from_slice(&front[..buf.len()]),
Some((buf_front, buf_back)) => match split_at_checked(back, buf_back.len()) {
Some((back, _)) => {
buf_front.copy_from_slice(front);
buf_back.copy_from_slice(back);
}
None => {
self.clear();
return Err(io::Error::READ_EXACT_EOF);
}
},
}
self.drain(..buf.len());
Ok(())
}
// #[inline]
// fn read_buf(&mut self, cursor: BorrowedCursor<'_>) -> io::Result<()> {
// let (ref mut front, _) = self.as_slices();
// let n = cmp::min(cursor.capacity(), front.len());
// Read::read_buf(front, cursor)?;
// self.drain(..n);
// Ok(())
// }
// fn read_buf_exact(&mut self, mut cursor: BorrowedCursor<'_>) -> io::Result<()> {
// let len = cursor.capacity();
// let (front, back) = self.as_slices();
//
// match front.split_at_checked(cursor.capacity()) {
// Some((front, _)) => cursor.append(front),
// None => {
// cursor.append(front);
// match back.split_at_checked(cursor.capacity()) {
// Some((back, _)) => cursor.append(back),
// None => {
// cursor.append(back);
// self.clear();
// return Err(io::Error::READ_EXACT_EOF);
// }
// }
// }
// }
//
// self.drain(..len);
// Ok(())
// }
// #[inline]
// fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
// // The total len is known upfront so we can reserve it in a single call.
// let len = self.len();
// buf.try_reserve(len)?;
//
// let (front, back) = self.as_slices();
// buf.extend_from_slice(front);
// buf.extend_from_slice(back);
// self.clear();
// Ok(len)
// }
// #[inline]
// fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
// // SAFETY: We only append to the buffer
// unsafe { io::append_to_string(buf, |buf| self.read_to_end(buf)) }
// }
}
// <[_]>::split_at_checked requires Rust 1.80.
#[cfg(feature = "alloc")]
#[inline]
#[must_use]
fn split_at_checked<T>(this: &[T], mid: usize) -> Option<(&[T], &[T])> {
if mid <= this.len() { Some(this.split_at(mid)) } else { None }
}
// <[_]>::split_at_mut_checked requires Rust 1.80.
#[cfg(feature = "alloc")]
#[inline]
#[must_use]
fn split_at_mut_checked<T>(this: &mut [T], mid: usize) -> Option<(&mut [T], &mut [T])> {
if mid <= this.len() { Some(this.split_at_mut(mid)) } else { None }
}
/// Write is implemented for `VecDeque<u8>` by appending to the `VecDeque`, growing it as needed.
#[cfg(feature = "alloc")]
impl Write for alloc::collections::VecDeque<u8> {
#[inline]
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.extend(buf);
Ok(buf.len())
}
// #[inline]
// fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
// let len = bufs.iter().map(|b| b.len()).sum();
// self.reserve(len);
// for buf in bufs {
// self.extend(&**buf);
// }
// Ok(len)
// }
// #[inline]
// fn is_write_vectored(&self) -> bool {
// true
// }
#[inline]
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.extend(buf);
Ok(())
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
+242
View File
@@ -0,0 +1,242 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Traits, helpers, and type definitions for core I/O functionality.
//!
//! This module is a subset of the [`std::io`] module except that the stdio functions returns
//! the `Result`.
//!
//! [`std::io`]: https://doc.rust-lang.org/std/io/index.html
// Based on nightly-2025-02-19's std::io module.
// TODO(io): io utilities e.g., Cursor?
pub use self::error::{Error, ErrorKind, RawOsError, Result};
#[macro_use]
mod error;
mod impls;
#[cfg(feature = "stdio")]
pub use self::stdio::{IsTerminal, Stderr, Stdin, Stdout, stderr, stdin, stdout};
#[cfg(feature = "stdio")]
mod stdio;
use core::fmt;
const _: fn() = || {
fn assert_dyn_compatibility<T: ?Sized>() {}
assert_dyn_compatibility::<dyn Read>();
assert_dyn_compatibility::<dyn Write>();
assert_dyn_compatibility::<dyn Seek>();
};
pub(crate) fn default_read_exact<R: ?Sized + Read>(this: &mut R, mut buf: &mut [u8]) -> Result<()> {
while !buf.is_empty() {
match this.read(buf) {
Ok(0) => break,
Ok(n) => {
buf = &mut buf[n..];
}
Err(ref e) if e.is_interrupted() => {}
Err(e) => return Err(e),
}
}
if buf.is_empty() { Ok(()) } else { Err(Error::READ_EXACT_EOF) }
}
/// The `no_std` subset of `std::io::Read`.
///
/// Unless explicitly stated otherwise, API contracts adhere to `std::io::Read`.
///
/// See [`std::io::Read` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Read.html
pub trait Read {
/// Pull some bytes from this source into the specified buffer, returning
/// how many bytes were read.
///
/// See [`std::io::Read::read` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Read.html#tymethod.read
fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
// TODO(io,alloc)
// /// Read all bytes until EOF in this source, placing them into `buf`.
// ///
// /// See [`std::io::Read::read_to_end` documentation][std] for details.
// ///
// /// [std]: https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_end
// #[cfg(feature = "alloc")]
// fn read_to_end(&mut self, buf: &mut Vec<u8>) -> Result<usize> {
// default_read_to_end(self, buf)
// }
//
// /// Read all bytes until EOF in this source, appending them to `buf`.
// ///
// /// See [`std::io::Read::read_to_string` documentation][std] for details.
// ///
// /// [std]: https://doc.rust-lang.org/std/io/trait.Read.html#method.read_to_string
// #[cfg(feature = "alloc")]
// fn read_to_string(&mut self, buf: &mut String) -> Result<usize> {
// default_read_to_string(self, buf)
// }
/// Read the exact number of bytes required to fill `buf`.
///
/// See [`std::io::Read::read_exact` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Read.html#method.read_exact
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
default_read_exact(self, buf)
}
}
/// The `no_std` subset of `std::io::Write`.
///
/// Unless explicitly stated otherwise, API contracts adhere to `std::io::Write`.
///
/// See [`std::io::Write` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Write.html
pub trait Write {
/// Write a buffer into this writer, returning how many bytes were written.
///
/// See [`std::io::Write::write` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.write
fn write(&mut self, buf: &[u8]) -> Result<usize>;
/// Flush this output stream, ensuring that all intermediately buffered
/// contents reach their destination.
///
/// See [`std::io::Write::flush` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush
fn flush(&mut self) -> Result<()>;
/// Attempts to write an entire buffer into this writer.
///
/// See [`std::io::Write::write_all` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Write.html#method.write_all
fn write_all(&mut self, mut buf: &[u8]) -> Result<()> {
while !buf.is_empty() {
match self.write(buf) {
Ok(0) => return Err(Error::WRITE_ALL_EOF),
Ok(n) => buf = &buf[n..],
Err(ref e) if e.is_interrupted() => {}
Err(e) => return Err(e),
}
}
Ok(())
}
/// Writes a formatted string into this writer, returning any error
/// encountered.
///
/// See [`std::io::Write::write_fmt` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Write.html#method.write_fmt
fn write_fmt(&mut self, f: fmt::Arguments<'_>) -> Result<()> {
// Create a shim which translates a Write to a fmt::Write and saves
// off I/O errors. instead of discarding them
struct Adapter<'a, T: ?Sized> {
inner: &'a mut T,
error: Result<()>,
}
impl<T: ?Sized + Write> fmt::Write for Adapter<'_, T> {
fn write_str(&mut self, s: &str) -> fmt::Result {
match self.inner.write_all(s.as_bytes()) {
Ok(()) => Ok(()),
Err(e) => {
self.error = Err(e);
Err(fmt::Error)
}
}
}
}
let mut output = Adapter { inner: self, error: Ok(()) };
match fmt::write(&mut output, f) {
Ok(()) => Ok(()),
Err(..) => {
// check if the error came from the underlying `Write` or not
if output.error.is_err() {
output.error
} else {
// This shouldn't happen: the underlying stream did not error, but somehow
// the formatter still errored?
panic!(
"a formatting trait implementation returned an error when the underlying stream did not"
);
}
}
}
}
}
/// The `no_std` subset of `std::io::Seek`.
///
/// Unless explicitly stated otherwise, API contracts adhere to `std::io::Seek`.
///
/// See [`std::io::Seek` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Seek.html
pub trait Seek {
/// Seek to an offset, in bytes, in a stream.
///
/// See [`std::io::Seek::seek` documentation][std] for details.
///
/// [std]: https://doc.rust-lang.org/std/io/trait.Seek.html#tymethod.seek
fn seek(&mut self, pos: SeekFrom) -> Result<u64>;
/// Rewind to the beginning of a stream.
///
/// This is a convenience method, equivalent to `seek(SeekFrom::Start(0))`.
fn rewind(&mut self) -> Result<()> {
self.seek(SeekFrom::Start(0))?;
Ok(())
}
// /// Returns the current seek position from the start of the stream.
// ///
// /// This is equivalent to `self.seek(SeekFrom::Current(0))`.
// fn stream_position(&mut self) -> Result<u64> {
// self.seek(SeekFrom::Current(0))
// }
// /// Seeks relative to the current position.
// ///
// /// This is equivalent to `self.seek(SeekFrom::Current(offset))` but
// /// doesn't return the new position which can allow some implementations
// /// such as [`BufReader`] to perform more efficient seeks.
// fn seek_relative(&mut self, offset: i64) -> Result<()> {
// self.seek(SeekFrom::Current(offset))?;
// Ok(())
// }
}
/// Enumeration of possible methods to seek within an I/O object.
///
/// It is used by the [`Seek`] trait.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SeekFrom {
/// Sets the offset to the provided number of bytes.
Start(u64),
/// Sets the offset to the size of this object plus the specified number of
/// bytes.
///
/// It is possible to seek beyond the end of an object, but it's an error to
/// seek before byte 0.
End(i64),
// TODO(arm_compat): It appears that SeekFrom::Current cannot be implemented with APIs provided by Arm semihosting...
// /// Sets the offset to the current position plus the specified number of
// /// bytes.
// ///
// /// It is possible to seek beyond the end of an object, but it's an error to
// /// seek before byte 0.
// Current(i64),
}
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT
use core::fmt;
use crate::{fd::AsFd as _, io, sys};
/// Constructs a new handle to the standard input of the current process.
///
/// Unlike [`std::io::stdin`], this function returns the `Result`.
///
/// # Platform-specific behavior
///
/// Currently, this function will always success on MIPS32/MIPS64, On other architectures,
/// this may fail if semihosting is only partially supported.
///
/// Also, we have found that reading from stdin does not work well on MIPS32/MIPS64.
///
/// [`std::io::stdin`]: https://doc.rust-lang.org/std/io/fn.stdin.html
pub fn stdin() -> io::Result<Stdin> {
sys::stdio::stdin().map(Stdin)
}
/// Constructs a new handle to the standard output of the current process.
///
/// Unlike [`std::io::stdout`], this function returns the `Result`.
///
/// # Platform-specific behavior
///
/// Currently, this function will always success on MIPS32/MIPS64, On other architectures,
/// this may fail if semihosting is only partially supported.
///
/// [`std::io::stdout`]: https://doc.rust-lang.org/std/io/fn.stdout.html
pub fn stdout() -> io::Result<Stdout> {
sys::stdio::stdout().map(Stdout)
}
/// Constructs a new handle to the standard error of the current process.
///
/// Unlike [`std::io::stderr`], this function returns the `Result`.
///
/// # Platform-specific behavior
///
/// Currently, this function will always success on MIPS32/MIPS64, On other architectures,
/// this may fail if semihosting is only partially supported.
///
/// [`std::io::stderr`]: https://doc.rust-lang.org/std/io/fn.stderr.html
pub fn stderr() -> io::Result<Stderr> {
sys::stdio::stderr().map(Stderr)
}
/// A handle to the standard input stream of a process.
///
/// Created by the [`io::stdin`] method.
pub struct Stdin(sys::stdio::StdioFd);
/// A handle to the standard output stream of a process.
///
/// Created by the [`io::stdout`] method.
pub struct Stdout(sys::stdio::StdioFd);
/// A handle to the standard error stream of a process.
///
/// Created by the [`io::stderr`] method.
pub struct Stderr(sys::stdio::StdioFd);
impl_as_fd!(Stdin, Stdout, Stderr);
// TODO(stdio): std provides io trait implementations on &Std{in,out,err} as they uses locks.
impl io::Read for Stdin {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
sys::read(self.as_fd(), buf)
}
}
impl io::Write for Stdout {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
sys::write(self.as_fd(), bytes)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl io::Write for Stderr {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
sys::write(self.as_fd(), bytes)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl fmt::Debug for Stdin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Stdin").finish_non_exhaustive()
}
}
impl fmt::Debug for Stdout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Stdout").finish_non_exhaustive()
}
}
impl fmt::Debug for Stderr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Stderr").finish_non_exhaustive()
}
}
/// Trait to determine if a descriptor/handle refers to a terminal/tty.
pub trait IsTerminal: crate::sealed::Sealed {
/// Returns `true` if the descriptor/handle refers to a terminal/tty.
///
/// On platforms where Rust does not know how to detect a terminal yet, this will return
/// `false`. This will also return `false` if an unexpected error occurred, such as from
/// passing an invalid file descriptor.
///
/// The following semihosting calls are currently being used:
///
/// | Platform | Semihosting call |
/// | ------------------------------------------------------------- | ---------------- |
/// | AArch64, Arm, RISC-V, LoongArch, Xtensa (openocd-semihosting) | [SYS_ISTTY] |
/// | MIPS32, MIPS64 | UHI_fstat |
///
/// [SYS_ISTTY]: https://github.com/ARM-software/abi-aa/blob/2025Q1/semihosting/semihosting.rst#sys-istty-0x09
///
/// **Disclaimer:** These semihosting calls might change over time.
#[doc(alias = "isatty")]
#[doc(alias = "SYS_ISTTY")] // arm_compat
fn is_terminal(&self) -> bool;
}
macro_rules! impl_is_terminal {
($($t:ty),*$(,)?) => {$(
impl crate::sealed::Sealed for $t {}
impl crate::io::IsTerminal for $t {
#[inline]
fn is_terminal(&self) -> bool {
use crate::fd::AsFd as _;
crate::sys::stdio::is_terminal(self.as_fd())
}
}
)*}
}
impl_is_terminal!(Stdin, Stdout, Stderr);
#[cfg(feature = "fs")]
impl_is_terminal!(crate::fs::File);