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

+311
View File
@@ -0,0 +1,311 @@
//! Buffered graphics mode.
use crate::{
command::AddrMode,
rotation::DisplayRotation,
size::{DisplaySize, NewZeroed},
Ssd1306,
};
#[cfg(feature = "async")]
use crate::{size::DisplaySizeAsync, Ssd1306Async};
#[cfg(feature = "async")]
use display_interface::AsyncWriteOnlyDataCommand;
use display_interface::{DisplayError, WriteOnlyDataCommand};
/// Buffered graphics mode.
///
/// This mode keeps a pixel buffer in system memory, up to 1024 bytes for 128x64px displays. This
/// buffer is drawn to by [`set_pixel`](Ssd1306::set_pixel) commands or
/// [`embedded-graphics`](https://docs.rs/embedded-graphics) commands. The display can then be
/// updated using the [`flush`](Ssd1306::flush) method.
#[maybe_async_cfg::maybe(
sync(keep_self),
async(feature = "async", idents(DisplaySize(async = "DisplaySizeAsync")))
)]
#[derive(Clone, Debug)]
pub struct BufferedGraphicsMode<SIZE>
where
SIZE: DisplaySize,
{
buffer: SIZE::Buffer,
min_x: u8,
max_x: u8,
min_y: u8,
max_y: u8,
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(feature = "async", idents(DisplaySize(async = "DisplaySizeAsync")))
)]
impl<SIZE> BufferedGraphicsMode<SIZE>
where
SIZE: DisplaySize,
{
/// Create a new buffered graphics mode instance.
pub(crate) fn new() -> Self {
Self {
buffer: NewZeroed::new_zeroed(),
min_x: 255,
max_x: 0,
min_y: 255,
max_y: 0,
}
}
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
DisplayConfig(async = "DisplayConfigAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
BufferedGraphicsMode(async = "BufferedGraphicsModeAsync"),
)
)
)]
impl<DI, SIZE> DisplayConfig for Ssd1306<DI, SIZE, BufferedGraphicsMode<SIZE>>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
type Error = DisplayError;
/// Set the display rotation
///
/// This method resets the cursor but does not clear the screen.
async fn set_rotation(&mut self, rot: DisplayRotation) -> Result<(), DisplayError> {
self.set_rotation(rot).await
}
/// Initialise and clear the display in graphics mode.
async fn init(&mut self) -> Result<(), DisplayError> {
self.clear_impl(false);
self.init_with_addr_mode(AddrMode::Horizontal).await
}
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
BufferedGraphicsMode(async = "BufferedGraphicsModeAsync")
)
)
)]
impl<DI, SIZE> Ssd1306<DI, SIZE, BufferedGraphicsMode<SIZE>>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
fn clear_impl(&mut self, value: bool) {
self.mode.buffer.as_mut().fill(if value { 0xff } else { 0 });
let (width, height) = self.dimensions();
self.mode.min_x = 0;
self.mode.max_x = width - 1;
self.mode.min_y = 0;
self.mode.max_y = height - 1;
}
/// Clear the underlying framebuffer. You need to call `disp.flush()` for any effect on the screen.
pub fn clear_buffer(&mut self) {
self.clear_impl(false);
}
/// Write out data to a display.
///
/// This only updates the parts of the display that have changed since the last flush.
pub async fn flush(&mut self) -> Result<(), DisplayError> {
// Nothing to do if no pixels have changed since the last update
if self.mode.max_x < self.mode.min_x || self.mode.max_y < self.mode.min_y {
return Ok(());
}
let (width, height) = self.dimensions();
// Determine which bytes need to be sent
let disp_min_x = self.mode.min_x;
let disp_min_y = self.mode.min_y;
let (disp_max_x, disp_max_y) = match self.rotation {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => (
(self.mode.max_x + 1).min(width),
(self.mode.max_y | 7).min(height),
),
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => (
(self.mode.max_x | 7).min(width),
(self.mode.max_y + 1).min(height),
),
};
self.mode.min_x = 255;
self.mode.max_x = 0;
self.mode.min_y = 255;
self.mode.max_y = 0;
// Tell the display to update only the part that has changed
let offset_x = match self.rotation {
DisplayRotation::Rotate0 | DisplayRotation::Rotate270 => SIZE::OFFSETX,
DisplayRotation::Rotate180 | DisplayRotation::Rotate90 => {
// If segment remapping is flipped, we need to calculate
// the offset from the other edge of the display.
SIZE::DRIVER_COLS - SIZE::WIDTH - SIZE::OFFSETX
}
};
match self.rotation {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => {
self.set_draw_area(
(disp_min_x + offset_x, disp_min_y + SIZE::OFFSETY),
(disp_max_x + offset_x, disp_max_y + SIZE::OFFSETY),
)
.await?;
Self::flush_buffer_chunks(
&mut self.interface,
self.mode.buffer.as_mut(),
width as usize,
(disp_min_x, disp_min_y),
(disp_max_x, disp_max_y),
)
.await
}
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => {
self.set_draw_area(
(disp_min_y + offset_x, disp_min_x + SIZE::OFFSETY),
(disp_max_y + offset_x, disp_max_x + SIZE::OFFSETY),
)
.await?;
Self::flush_buffer_chunks(
&mut self.interface,
self.mode.buffer.as_mut(),
height as usize,
(disp_min_y, disp_min_x),
(disp_max_y, disp_max_x),
)
.await
}
}
}
/// Turn a pixel on or off. A non-zero `value` is treated as on, `0` as off. If the X and Y
/// coordinates are out of the bounds of the display, this method call is a noop.
pub fn set_pixel(&mut self, x: u32, y: u32, value: bool) {
let value = value as u8;
let rotation = self.rotation;
let (idx, bit) = match rotation {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => {
let idx = ((y as usize) / 8 * SIZE::WIDTH as usize) + (x as usize);
let bit = y % 8;
(idx, bit)
}
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => {
let idx = ((x as usize) / 8 * SIZE::WIDTH as usize) + (y as usize);
let bit = x % 8;
(idx, bit)
}
};
if let Some(byte) = self.mode.buffer.as_mut().get_mut(idx) {
// Keep track of max and min values
self.mode.min_x = self.mode.min_x.min(x as u8);
self.mode.max_x = self.mode.max_x.max(x as u8);
self.mode.min_y = self.mode.min_y.min(y as u8);
self.mode.max_y = self.mode.max_y.max(y as u8);
// Set pixel value in byte
// Ref this comment https://stackoverflow.com/questions/47981/how-do-you-set-clear-and-toggle-a-single-bit#comment46654671_47990
*byte = *byte & !(1 << bit) | (value << bit);
}
}
}
#[cfg(feature = "graphics")]
use embedded_graphics_core::{
draw_target::DrawTarget,
geometry::Size,
geometry::{Dimensions, OriginDimensions},
pixelcolor::BinaryColor,
Pixel,
};
use super::DisplayConfig;
#[cfg(feature = "async")]
use super::DisplayConfigAsync;
#[cfg(feature = "graphics")]
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
BufferedGraphicsMode(async = "BufferedGraphicsModeAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand")
)
)
)]
impl<DI, SIZE> DrawTarget for Ssd1306<DI, SIZE, BufferedGraphicsMode<SIZE>>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
type Color = BinaryColor;
type Error = DisplayError;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
let bb = self.bounding_box();
pixels
.into_iter()
.filter(|Pixel(pos, _color)| bb.contains(*pos))
.for_each(|Pixel(pos, color)| {
self.set_pixel(pos.x as u32, pos.y as u32, color.is_on());
});
Ok(())
}
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
self.clear_impl(color.is_on());
Ok(())
}
}
#[cfg(feature = "graphics")]
#[maybe_async_cfg::maybe(
sync(keep_self,),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
BufferedGraphicsMode(async = "BufferedGraphicsModeAsync")
)
)
)]
impl<DI, SIZE> OriginDimensions for Ssd1306<DI, SIZE, BufferedGraphicsMode<SIZE>>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
fn size(&self) -> Size {
let (w, h) = self.dimensions();
Size::new(w.into(), h.into())
}
}
+80
View File
@@ -0,0 +1,80 @@
//! Display modes.
mod buffered_graphics;
mod terminal;
use crate::{command::AddrMode, rotation::DisplayRotation, size::DisplaySize, Ssd1306};
pub use buffered_graphics::*;
use display_interface::{DisplayError, WriteOnlyDataCommand};
pub use terminal::*;
/// Common functions to all display modes.
#[maybe_async_cfg::maybe(sync(keep_self,), async(feature = "async"))]
pub trait DisplayConfig {
/// Error.
type Error;
/// Set display rotation.
async fn set_rotation(&mut self, rotation: DisplayRotation) -> Result<(), Self::Error>;
/// Initialise and configure the display for the given mode.
async fn init(&mut self) -> Result<(), Self::Error>;
}
/// A mode with no additional functionality beyond that provided by the base [`Ssd1306`] struct.
#[derive(Debug, Copy, Clone)]
pub struct BasicMode;
impl<DI, SIZE> Ssd1306<DI, SIZE, BasicMode>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
/// Clear the display.
pub fn clear(&mut self) -> Result<(), DisplayError> {
let old_addr_mode = self.addr_mode;
if old_addr_mode != AddrMode::Horizontal {
self.set_addr_mode(AddrMode::Horizontal)?;
}
let dim = self.dimensions();
self.set_draw_area((0, 0), dim)?;
let num_pixels = dim.0 as u16 * dim.1 as u16;
const BITS_PER_BYTE: u16 = 8;
const BYTES_PER_BATCH: u16 = 64;
const PIXELS_PER_BATCH: u16 = BITS_PER_BYTE * BYTES_PER_BATCH;
// Not all screens have number of pixels divisible by 512, so add 1 to cover tail
let num_batches = num_pixels / PIXELS_PER_BATCH + 1;
for _ in 0..num_batches {
self.draw(&[0; BYTES_PER_BATCH as usize])?;
}
if old_addr_mode != AddrMode::Horizontal {
self.set_addr_mode(old_addr_mode)?;
}
Ok(())
}
}
impl<DI, SIZE> DisplayConfig for Ssd1306<DI, SIZE, BasicMode>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
type Error = DisplayError;
/// Set the display rotation.
fn set_rotation(&mut self, rot: DisplayRotation) -> Result<(), DisplayError> {
self.set_rotation(rot)
}
/// Initialise in horizontal addressing mode.
fn init(&mut self) -> Result<(), DisplayError> {
self.init_with_addr_mode(AddrMode::Horizontal)
}
}
+630
View File
@@ -0,0 +1,630 @@
#[cfg(feature = "async")]
use crate::mode::DisplayConfigAsync;
use crate::{command::AddrMode, mode::DisplayConfig, rotation::DisplayRotation, size::*, Ssd1306};
#[cfg(feature = "async")]
use crate::{size::DisplaySizeAsync, Ssd1306Async};
use core::{cmp::min, fmt};
#[cfg(feature = "async")]
use display_interface::AsyncWriteOnlyDataCommand;
use display_interface::{DisplayError, WriteOnlyDataCommand};
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),
DisplaySize(async = "DisplaySizeAsync"),
)
)
)]
/// Extends the [`DisplaySize`](crate::size::DisplaySize) trait
/// to include number of characters that can fit on the display.
pub trait TerminalDisplaySize: DisplaySize {
/// The number of characters that can fit on the display at once (w * h / (8 * 8))
const CHAR_NUM: u8;
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
keep_self,
idents(TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),)
)
)]
impl TerminalDisplaySize for DisplaySize128x64 {
const CHAR_NUM: u8 = 128;
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
keep_self,
idents(TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),)
)
)]
impl TerminalDisplaySize for DisplaySize128x32 {
const CHAR_NUM: u8 = 64;
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
keep_self,
idents(TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),)
)
)]
impl TerminalDisplaySize for DisplaySize96x16 {
const CHAR_NUM: u8 = 24;
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
keep_self,
feature = "async",
idents(TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),)
)
)]
impl TerminalDisplaySize for DisplaySize72x40 {
const CHAR_NUM: u8 = 45;
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
keep_self,
idents(TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),)
)
)]
impl TerminalDisplaySize for DisplaySize64x48 {
const CHAR_NUM: u8 = 48;
}
/// Contains the new row that the cursor has wrapped around to
struct CursorWrapEvent(u8);
#[derive(Copy, Clone, Debug)]
struct Cursor {
col: u8,
row: u8,
width: u8,
height: u8,
}
impl Cursor {
pub fn new(width_pixels: u8, height_pixels: u8) -> Self {
let width = width_pixels / 8;
let height = height_pixels / 8;
Cursor {
col: 0,
row: 0,
width,
height,
}
}
/// Advances the logical cursor by one character.
/// Returns a value indicating if this caused the cursor to wrap to the next line or the next
/// screen.
pub fn advance(&mut self) -> Option<CursorWrapEvent> {
self.col = (self.col + 1) % self.width;
if self.col == 0 {
self.row = (self.row + 1) % self.height;
Some(CursorWrapEvent(self.row))
} else {
None
}
}
/// Advances the logical cursor to the start of the next line
/// Returns a value indicating the now active line
pub fn advance_line(&mut self) -> CursorWrapEvent {
self.row = (self.row + 1) % self.height;
self.col = 0;
CursorWrapEvent(self.row)
}
/// Sets the position of the logical cursor arbitrarily.
/// The position will be capped at the maximal possible position.
pub fn set_position(&mut self, col: u8, row: u8) {
self.col = min(col, self.width - 1);
self.row = min(row, self.height - 1);
}
/// Gets the position of the logical cursor on screen in (col, row) order
pub fn get_position(&self) -> (u8, u8) {
(self.col, self.row)
}
/// Gets the logical dimensions of the screen in terms of characters, as (width, height)
pub fn get_dimensions(&self) -> (u8, u8) {
(self.width, self.height)
}
}
/// Errors which can occur when interacting with the terminal mode
#[derive(Clone)]
pub enum TerminalModeError {
/// An error occurred in the underlying interface layer
InterfaceError(DisplayError),
/// The mode was used before it was initialized
Uninitialized,
/// A location was specified outside the bounds of the screen
OutOfBounds,
}
impl fmt::Debug for TerminalModeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
Self::InterfaceError(_) => "InterfaceError".fmt(f),
Self::Uninitialized => "Uninitialized".fmt(f),
Self::OutOfBounds => "OutOfBound".fmt(f),
}
}
}
impl From<DisplayError> for TerminalModeError {
fn from(value: DisplayError) -> Self {
TerminalModeError::InterfaceError(value)
}
}
/// Terminal mode.
#[maybe_async_cfg::maybe(
sync(keep_self),
async(feature = "async", idents(TerminalMode(async = "TerminalModeAsync")))
)]
#[derive(Debug, Copy, Clone, Default)]
pub struct TerminalMode {
cursor: Option<Cursor>,
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(feature = "async", idents(TerminalMode(async = "TerminalModeAsync")))
)]
impl TerminalMode {
/// Create a new terminal mode config instance.
pub fn new() -> Self {
Self::default()
}
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
DisplayConfig(async = "DisplayConfigAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
TerminalMode(async = "TerminalModeAsync"),
TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),
)
)
)]
impl<DI, SIZE> DisplayConfig for Ssd1306<DI, SIZE, TerminalMode>
where
DI: WriteOnlyDataCommand,
SIZE: TerminalDisplaySize,
{
type Error = TerminalModeError;
/// Set the display rotation
///
/// This method resets the cursor but does not clear the screen.
async fn set_rotation(&mut self, rot: DisplayRotation) -> Result<(), TerminalModeError> {
self.set_rotation(rot).await?;
// Need to reset cursor position, otherwise coordinates can become invalid
self.reset_pos().await
}
/// Initialise the display in page mode (i.e. a byte walks down a column of 8 pixels) with
/// column 0 on the left and column _(SIZE::Width::U8 - 1)_ on the right, but no automatic line
/// wrapping.
async fn init(&mut self) -> Result<(), TerminalModeError> {
self.init_with_addr_mode(AddrMode::Page).await?;
self.reset_pos().await
}
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
DisplayConfig(async = "DisplayConfigAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
TerminalMode(async = "TerminalModeAsync"),
TerminalDisplaySize(async = "TerminalDisplaySizeAsync"),
)
)
)]
impl<DI, SIZE> Ssd1306<DI, SIZE, TerminalMode>
where
DI: WriteOnlyDataCommand,
SIZE: TerminalDisplaySize,
{
/// Clear the display and reset the cursor to the top left corner
pub async fn clear(&mut self) -> Result<(), TerminalModeError> {
// Let the chip handle line wrapping so we can fill the screen with blanks faster
self.set_addr_mode(AddrMode::Horizontal).await?;
let offset_x = match self.rotation() {
DisplayRotation::Rotate0 | DisplayRotation::Rotate270 => SIZE::OFFSETX,
DisplayRotation::Rotate180 | DisplayRotation::Rotate90 => {
// If segment remapping is flipped, we need to calculate
// the offset from the other edge of the display.
SIZE::DRIVER_COLS - SIZE::WIDTH - SIZE::OFFSETX
}
};
self.set_draw_area(
(offset_x, SIZE::OFFSETY),
(SIZE::WIDTH + offset_x, SIZE::HEIGHT + SIZE::OFFSETY),
)
.await?;
// Clear the display
for _ in 0..SIZE::CHAR_NUM {
self.draw(&[0; 8]).await?;
}
// But for normal operation we manage the line wrapping
self.set_addr_mode(AddrMode::Page).await?;
self.reset_pos().await
}
/// Print a character to the display
pub async fn print_char(&mut self, c: char) -> Result<(), TerminalModeError> {
match c {
'\n' => {
let CursorWrapEvent(new_line) = self.ensure_cursor()?.advance_line();
self.set_position(0, new_line).await?;
}
'\r' => {
self.set_column(0).await?;
let (_, cur_line) = self.ensure_cursor()?.get_position();
self.ensure_cursor()?.set_position(0, cur_line);
}
_ => {
let bitmap = match self.rotation {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => {
Self::char_to_bitmap(c)
}
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => {
let bitmap = Self::char_to_bitmap(c);
Self::rotate_bitmap(bitmap)
}
};
self.draw(&bitmap).await?;
// Increment character counter and potentially wrap line
self.advance_cursor().await?;
}
}
Ok(())
}
/// Get the current cursor position, in character coordinates.
/// This is the (column, row) that the next character will be written to.
pub fn position(&self) -> Result<(u8, u8), TerminalModeError> {
self.mode
.cursor
.as_ref()
.map(Cursor::get_position)
.ok_or(TerminalModeError::Uninitialized)
}
/// Set the cursor position, in character coordinates.
/// This is the (column, row) that the next character will be written to.
/// If the position is out of bounds, an Err will be returned.
pub async fn set_position(&mut self, column: u8, row: u8) -> Result<(), TerminalModeError> {
let (width, height) = self.ensure_cursor()?.get_dimensions();
if column >= width || row >= height {
Err(TerminalModeError::OutOfBounds)
} else {
let offset_x = match self.rotation() {
DisplayRotation::Rotate0 | DisplayRotation::Rotate270 => SIZE::OFFSETX,
DisplayRotation::Rotate180 | DisplayRotation::Rotate90 => {
// If segment remapping is flipped, we need to calculate
// the offset from the other edge of the display.
SIZE::DRIVER_COLS - SIZE::WIDTH - SIZE::OFFSETX
}
};
match self.rotation() {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => {
self.set_column(offset_x + column * 8).await?;
self.set_row(SIZE::OFFSETY + row * 8).await?;
}
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => {
self.set_column(offset_x + row * 8).await?;
self.set_row(SIZE::OFFSETY + column * 8).await?;
}
}
self.ensure_cursor()?.set_position(column, row);
Ok(())
}
}
/// Reset the draw area and move pointer to the top left corner
async fn reset_pos(&mut self) -> Result<(), TerminalModeError> {
// Initialise the counter when we know it's valid
let (w, h) = match self.rotation() {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => (SIZE::WIDTH, SIZE::HEIGHT),
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => (SIZE::HEIGHT, SIZE::WIDTH),
};
self.mode.cursor = Some(Cursor::new(w, h));
// Reset cursor position
self.set_position(0, 0).await
}
/// Advance the cursor, automatically wrapping lines and/or screens if necessary
/// Takes in an already-unwrapped cursor to avoid re-unwrapping
async fn advance_cursor(&mut self) -> Result<(), TerminalModeError> {
let cursor = self.ensure_cursor()?;
cursor.advance();
let (c, r) = cursor.get_position();
self.set_position(c, r).await
}
fn ensure_cursor(&mut self) -> Result<&mut Cursor, TerminalModeError> {
self.mode
.cursor
.as_mut()
.ok_or(TerminalModeError::Uninitialized)
}
fn char_to_bitmap(input: char) -> [u8; 8] {
const CHARS: [[u8; 6]; 95] = [
// !
[0x00, 0x2f, 0x00, 0x00, 0x00, 0x00],
// "
[0x03, 0x00, 0x03, 0x00, 0x00, 0x00],
// #
[0x12, 0x3f, 0x12, 0x12, 0x3f, 0x12],
// $
[0x2e, 0x2a, 0x7f, 0x2a, 0x3a, 0x00],
// %
[0x23, 0x13, 0x08, 0x04, 0x32, 0x31],
// &
[0x10, 0x2a, 0x25, 0x2a, 0x10, 0x20],
// '
[0x02, 0x01, 0x00, 0x00, 0x00, 0x00],
// (
[0x1e, 0x21, 0x00, 0x00, 0x00, 0x00],
// )
[0x21, 0x1e, 0x00, 0x00, 0x00, 0x00],
// *
[0x08, 0x2a, 0x1c, 0x2a, 0x08, 0x00],
// +
[0x08, 0x08, 0x3e, 0x08, 0x08, 0x00],
// ,
[0x80, 0x60, 0x00, 0x00, 0x00, 0x00],
// -
[0x08, 0x08, 0x08, 0x08, 0x08, 0x00],
// .
[0x30, 0x30, 0x00, 0x00, 0x00, 0x00],
// /
[0x20, 0x10, 0x08, 0x04, 0x02, 0x00],
// 0
[0x1e, 0x31, 0x29, 0x25, 0x23, 0x1e],
// 1
[0x22, 0x21, 0x3f, 0x20, 0x20, 0x20],
// 2
[0x32, 0x29, 0x29, 0x29, 0x29, 0x26],
// 3
[0x12, 0x21, 0x21, 0x25, 0x25, 0x1a],
// 4
[0x18, 0x14, 0x12, 0x3f, 0x10, 0x00],
// 5
[0x17, 0x25, 0x25, 0x25, 0x25, 0x19],
// 6
[0x1e, 0x25, 0x25, 0x25, 0x25, 0x18],
// 7
[0x01, 0x01, 0x31, 0x09, 0x05, 0x03],
// 8
[0x1a, 0x25, 0x25, 0x25, 0x25, 0x1a],
// 9
[0x06, 0x29, 0x29, 0x29, 0x29, 0x1e],
// :
[0x24, 0x00, 0x00, 0x00, 0x00, 0x00],
// ;
[0x80, 0x64, 0x00, 0x00, 0x00, 0x00],
// <
[0x08, 0x14, 0x22, 0x00, 0x00, 0x00],
// =
[0x14, 0x14, 0x14, 0x14, 0x14, 0x00],
// >
[0x22, 0x14, 0x08, 0x00, 0x00, 0x00],
// ?
[0x02, 0x01, 0x01, 0x29, 0x05, 0x02],
// @
[0x1e, 0x21, 0x2d, 0x2b, 0x2d, 0x0e],
// A
[0x3e, 0x09, 0x09, 0x09, 0x09, 0x3e],
// B
[0x3f, 0x25, 0x25, 0x25, 0x25, 0x1a],
// C
[0x1e, 0x21, 0x21, 0x21, 0x21, 0x12],
// D
[0x3f, 0x21, 0x21, 0x21, 0x12, 0x0c],
// E
[0x3f, 0x25, 0x25, 0x25, 0x25, 0x21],
// F
[0x3f, 0x05, 0x05, 0x05, 0x05, 0x01],
// G
[0x1e, 0x21, 0x21, 0x21, 0x29, 0x1a],
// H
[0x3f, 0x04, 0x04, 0x04, 0x04, 0x3f],
// I
[0x21, 0x21, 0x3f, 0x21, 0x21, 0x00],
// J
[0x10, 0x20, 0x20, 0x20, 0x20, 0x1f],
// K
[0x3f, 0x04, 0x0c, 0x0a, 0x11, 0x20],
// L
[0x3f, 0x20, 0x20, 0x20, 0x20, 0x20],
// M
[0x3f, 0x02, 0x04, 0x04, 0x02, 0x3f],
// N
[0x3f, 0x02, 0x04, 0x08, 0x10, 0x3f],
// O
[0x1e, 0x21, 0x21, 0x21, 0x21, 0x1e],
// P
[0x3f, 0x09, 0x09, 0x09, 0x09, 0x06],
// Q
[0x1e, 0x21, 0x29, 0x31, 0x21, 0x5e],
// R
[0x3f, 0x09, 0x09, 0x09, 0x19, 0x26],
// S
[0x12, 0x25, 0x25, 0x25, 0x25, 0x18],
// T
[0x01, 0x01, 0x01, 0x3f, 0x01, 0x01],
// U
[0x1f, 0x20, 0x20, 0x20, 0x20, 0x1f],
// V
[0x0f, 0x10, 0x20, 0x20, 0x10, 0x0f],
// W
[0x1f, 0x20, 0x10, 0x10, 0x20, 0x1f],
// X
[0x21, 0x12, 0x0c, 0x0c, 0x12, 0x21],
// Y
[0x01, 0x02, 0x3c, 0x02, 0x01, 0x00],
// Z
[0x21, 0x31, 0x29, 0x25, 0x23, 0x21],
// [
[0x3f, 0x21, 0x00, 0x00, 0x00, 0x00],
// \
[0x02, 0x04, 0x08, 0x10, 0x20, 0x00],
// ]
[0x21, 0x3f, 0x00, 0x00, 0x00, 0x00],
// ^
[0x04, 0x02, 0x3f, 0x02, 0x04, 0x00],
// _
[0x40, 0x40, 0x40, 0x40, 0x40, 0x40],
// `
[0x01, 0x02, 0x00, 0x00, 0x00, 0x00],
// a
[0x10, 0x2a, 0x2a, 0x2a, 0x3c, 0x00],
// b
[0x3f, 0x24, 0x24, 0x24, 0x18, 0x00],
// c
[0x1c, 0x22, 0x22, 0x22, 0x00, 0x00],
// d
[0x18, 0x24, 0x24, 0x24, 0x3f, 0x00],
// e
[0x1c, 0x2a, 0x2a, 0x2a, 0x24, 0x00],
// f
[0x00, 0x3e, 0x05, 0x01, 0x00, 0x00],
// g
[0x18, 0xa4, 0xa4, 0xa4, 0x7c, 0x00],
// h
[0x3f, 0x04, 0x04, 0x04, 0x38, 0x00],
// i
[0x00, 0x24, 0x3d, 0x20, 0x00, 0x00],
// j
[0x20, 0x40, 0x40, 0x3d, 0x00, 0x00],
// k
[0x3f, 0x0c, 0x12, 0x20, 0x00, 0x00],
// l
[0x1f, 0x20, 0x20, 0x00, 0x00, 0x00],
// m
[0x3e, 0x02, 0x3c, 0x02, 0x3c, 0x00],
// n
[0x3e, 0x02, 0x02, 0x02, 0x3c, 0x00],
// o
[0x1c, 0x22, 0x22, 0x22, 0x1c, 0x00],
// p
[0xfc, 0x24, 0x24, 0x24, 0x18, 0x00],
// q
[0x18, 0x24, 0x24, 0x24, 0xfc, 0x00],
// r
[0x3e, 0x04, 0x02, 0x02, 0x00, 0x00],
// s
[0x24, 0x2a, 0x2a, 0x2a, 0x10, 0x00],
// t
[0x02, 0x1f, 0x22, 0x20, 0x00, 0x00],
// u
[0x1e, 0x20, 0x20, 0x20, 0x1e, 0x00],
// v
[0x06, 0x18, 0x20, 0x18, 0x06, 0x00],
// w
[0x1e, 0x30, 0x1c, 0x30, 0x1e, 0x00],
// x
[0x22, 0x14, 0x08, 0x14, 0x22, 0x00],
// y
[0x1c, 0xa0, 0xa0, 0xa0, 0x7c, 0x00],
// z
[0x22, 0x32, 0x2a, 0x26, 0x22, 0x00],
// {
[0x0c, 0x3f, 0x21, 0x00, 0x00, 0x00],
// |
[0x3f, 0x00, 0x00, 0x00, 0x00, 0x00],
// }
[0x21, 0x3f, 0x0c, 0x00, 0x00, 0x00],
// ~
[0x02, 0x01, 0x02, 0x01, 0x00, 0x00],
// blank
[0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
];
let g = (input as usize)
.checked_sub(b'!'.into())
.and_then(|idx| CHARS.get(idx))
.unwrap_or(&CHARS[CHARS.len() - 1]);
[0, g[0], g[1], g[2], g[3], g[4], g[5], 0]
}
fn rotate_bitmap(bitmap: [u8; 8]) -> [u8; 8] {
let mut rotated: [u8; 8] = [0; 8];
for (col, source) in bitmap.iter().enumerate() {
// source.msb is the top pixel
for (row, item) in rotated.iter_mut().enumerate() {
let bit = source & (1 << row) != 0;
if bit {
*item |= 1 << col;
}
}
}
rotated
}
}
#[cfg(feature = "async")]
impl<DI, SIZE> Ssd1306Async<DI, SIZE, TerminalModeAsync>
where
DI: AsyncWriteOnlyDataCommand,
SIZE: TerminalDisplaySizeAsync,
{
/// Write a string slice to the display
pub async fn write_str(&mut self, s: &str) -> Result<(), TerminalModeError> {
for c in s.chars() {
self.print_char(c).await?;
}
Ok(())
}
}
impl<DI, SIZE> fmt::Write for Ssd1306<DI, SIZE, TerminalMode>
where
DI: WriteOnlyDataCommand,
SIZE: TerminalDisplaySize,
{
fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
s.chars().map(move |c| self.print_char(c)).next_back();
Ok(())
}
}