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>
631 lines
20 KiB
Rust
631 lines
20 KiB
Rust
#[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(())
|
|
}
|
|
}
|