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
+311
@@ -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())
|
||||
}
|
||||
}
|
||||
Vendored
+80
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+630
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user