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

+53
View File
@@ -0,0 +1,53 @@
//! Display brightness
/// Struct that holds display brightness
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Brightness {
pub(crate) precharge: u8,
pub(crate) contrast: u8,
}
impl Default for Brightness {
fn default() -> Self {
Brightness::NORMAL
}
}
impl Brightness {
/// The dimmest predefined brightness level
pub const DIMMEST: Brightness = Brightness::custom(0x1, 0x00);
/// A dim predefined brightness level
pub const DIM: Brightness = Brightness::custom(0x2, 0x2F);
/// A medium predefined brightness level
pub const NORMAL: Brightness = Brightness::custom(0x2, 0x5F);
/// A bright predefined brightness level
pub const BRIGHT: Brightness = Brightness::custom(0x2, 0x9F);
/// The brightest predefined brightness level
pub const BRIGHTEST: Brightness = Brightness::custom(0x2, 0xFF);
/// Create a Brightness object from a precharge period and contrast pair.
///
/// `precharge` sets the `phase 2` argument of the `0xD9 Set Pre-Charge Period` command and must
/// be must be between 1 and 15.
/// The effects of this parameter are hardware dependent. For the common 128x64 displays, values
/// 1 and 2 result in different brightness levels, values above 2 behave the same was as 2.
/// See section 10.1.17 of the SSD1306 datasheet for more information.
///
/// `contrast` sets the value used in the `0x81 Set Contrast Control` command and must be
/// between 0 and 255. See section 10.1.7 of the SSD1306 datasheet for more information.
pub const fn custom(precharge: u8, contrast: u8) -> Self {
debug_assert!(
0 < precharge && precharge <= 15,
"Precharge value must be between 1 and 15"
);
Self {
precharge,
contrast,
}
}
}
+327
View File
@@ -0,0 +1,327 @@
//! Display commands.
// Shamefully taken from https://github.com/EdgewaterDevelopment/rust-ssd1306
#[cfg(feature = "async")]
use display_interface::AsyncWriteOnlyDataCommand;
use display_interface::{DataFormat::U8, DisplayError, WriteOnlyDataCommand};
/// SSD1306 Commands
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async"))]
#[derive(Debug, Copy, Clone)]
pub enum Command {
/// Set contrast. Higher number is higher contrast. Default = 0x7F
Contrast(u8),
/// Turn entire display on. If set, all pixels will
/// be set to on, if not, the value in memory will be used.
AllOn(bool),
/// Invert display.
Invert(bool),
/// Turn display on or off.
DisplayOn(bool),
/// Set up horizontal scrolling.
/// Values are scroll direction, start page, end page,
/// and number of frames per step.
HScrollSetup(HScrollDir, Page, Page, NFrames),
/// Set up horizontal + vertical scrolling.
/// Values are scroll direction, start page, end page,
/// number of frames per step, and vertical scrolling offset.
/// Scrolling offset may be from 0-63
VHScrollSetup(VHScrollDir, Page, Page, NFrames, u8),
/// Enable scrolling
EnableScroll(bool),
/// Setup vertical scroll area.
/// Values are number of rows above scroll area (0-63)
/// and number of rows of scrolling. (0-64)
VScrollArea(u8, u8),
/// Set the lower nibble of the column start address
/// register for Page addressing mode, using the lower
/// 4 bits given.
/// This is only for page addressing mode
LowerColStart(u8),
/// Set the upper nibble of the column start address
/// register for Page addressing mode, using the lower
/// 4 bits given.
/// This is only for page addressing mode
UpperColStart(u8),
/// Set the column start address register for Page addressing mode.
/// Combines LowerColStart and UpperColStart
/// This is only for page addressing mode
ColStart(u8),
/// Set addressing mode
AddressMode(AddrMode),
/// Setup column start and end address
/// values range from 0-127
/// This is only for horizontal or vertical addressing mode
ColumnAddress(u8, u8),
/// Setup page start and end address
/// This is only for horizontal or vertical addressing mode
PageAddress(Page, Page),
/// Set GDDRAM page start address for Page addressing mode
PageStart(Page),
/// Set display start line from 0-63
StartLine(u8),
/// Reverse columns from 127-0
SegmentRemap(bool),
/// Set multiplex ratio from 15-63 (MUX-1)
Multiplex(u8),
/// Scan from COM[n-1] to COM0 (where N is mux ratio)
ReverseComDir(bool),
/// Set vertical shift
DisplayOffset(u8),
/// Setup com hardware configuration
/// First value indicates sequential (false) or alternative (true)
/// pin configuration. Second value disables (false) or enables (true)
/// left/right remap.
ComPinConfig(bool, bool),
/// Set up display clock.
/// First value is oscillator frequency, increasing with higher value
/// Second value is divide ratio - 1
DisplayClockDiv(u8, u8),
/// Set up phase 1 and 2 of precharge period. Each value must be in the range 1 - 15.
PreChargePeriod(u8, u8),
/// Set Vcomh Deselect level
VcomhDeselect(VcomhLevel),
/// NOOP
Noop,
/// Enable charge pump
ChargePump(bool),
/// Select external or internal I REF. Only for 72 x 40 display with SSD1306B driver
InternalIref(bool, bool),
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"))
)
)]
impl Command {
/// Send command to SSD1306
pub async fn send<DI>(self, iface: &mut DI) -> Result<(), DisplayError>
where
DI: WriteOnlyDataCommand,
{
match self {
Command::Contrast(val) => Self::send_commands(iface, &[0x81, val]).await,
Command::AllOn(on) => Self::send_commands(iface, &[0xA4 | (on as u8)]).await,
Command::Invert(inv) => Self::send_commands(iface, &[0xA6 | (inv as u8)]).await,
Command::DisplayOn(on) => Self::send_commands(iface, &[0xAE | (on as u8)]).await,
Command::HScrollSetup(dir, start, end, rate) => {
Self::send_commands(
iface,
&[
0x26 | (dir as u8),
0,
start as u8,
rate as u8,
end as u8,
0,
0xFF,
],
)
.await
}
Command::VHScrollSetup(dir, start, end, rate, offset) => {
Self::send_commands(
iface,
&[
0x28 | (dir as u8),
0,
start as u8,
rate as u8,
end as u8,
offset,
],
)
.await
}
Command::EnableScroll(en) => Self::send_commands(iface, &[0x2E | (en as u8)]).await,
Command::VScrollArea(above, lines) => {
Self::send_commands(iface, &[0xA3, above, lines]).await
}
Command::LowerColStart(addr) => Self::send_commands(iface, &[0xF & addr]).await,
Command::UpperColStart(addr) => {
Self::send_commands(iface, &[0x10 | (0xF & addr)]).await
}
Command::ColStart(addr) => {
Self::send_commands(iface, &[0xF & addr, 0x10 | (0xF & (addr >> 4))]).await
}
Command::AddressMode(mode) => Self::send_commands(iface, &[0x20, mode as u8]).await,
Command::ColumnAddress(start, end) => {
Self::send_commands(iface, &[0x21, start, end]).await
}
Command::PageAddress(start, end) => {
Self::send_commands(iface, &[0x22, start as u8, end as u8]).await
}
Command::PageStart(page) => Self::send_commands(iface, &[0xB0 | (page as u8)]).await,
Command::StartLine(line) => Self::send_commands(iface, &[0x40 | (0x3F & line)]).await,
Command::SegmentRemap(remap) => {
Self::send_commands(iface, &[0xA0 | (remap as u8)]).await
}
Command::Multiplex(ratio) => Self::send_commands(iface, &[0xA8, ratio]).await,
Command::ReverseComDir(rev) => {
Self::send_commands(iface, &[0xC0 | ((rev as u8) << 3)]).await
}
Command::DisplayOffset(offset) => Self::send_commands(iface, &[0xD3, offset]).await,
Command::ComPinConfig(alt, lr) => {
Self::send_commands(iface, &[0xDA, 0x2 | ((alt as u8) << 4) | ((lr as u8) << 5)])
.await
}
Command::DisplayClockDiv(fosc, div) => {
Self::send_commands(iface, &[0xD5, ((0xF & fosc) << 4) | (0xF & div)]).await
}
Command::PreChargePeriod(phase1, phase2) => {
Self::send_commands(iface, &[0xD9, ((0xF & phase2) << 4) | (0xF & phase1)]).await
}
Command::VcomhDeselect(level) => {
Self::send_commands(iface, &[0xDB, (level as u8) << 4]).await
}
Command::Noop => Self::send_commands(iface, &[0xE3]).await,
Command::ChargePump(en) => {
Self::send_commands(iface, &[0x8D, 0x10 | ((en as u8) << 2)]).await
}
Command::InternalIref(en, current) => {
Self::send_commands(iface, &[0xAD, ((current as u8) << 5) | ((en as u8) << 4)])
.await
}
}
}
async fn send_commands<DI>(iface: &mut DI, data: &[u8]) -> Result<(), DisplayError>
where
DI: WriteOnlyDataCommand,
{
iface.send_commands(U8(data)).await
}
}
/// Horizontal Scroll Direction
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum HScrollDir {
/// Left to right
LeftToRight = 0,
/// Right to left
RightToLeft = 1,
}
/// Vertical and horizontal scroll dir
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum VHScrollDir {
/// Vertical and right horizontal
VerticalRight = 0b01,
/// Vertical and left horizontal
VerticalLeft = 0b10,
}
/// Display page
#[derive(Debug, Clone, Copy)]
pub enum Page {
/// Page 0
Page0 = 0b0000,
/// Page 1
Page1 = 0b0001,
/// Page 2
Page2 = 0b0010,
/// Page 3
Page3 = 0b0011,
/// Page 4
Page4 = 0b0100,
/// Page 5
Page5 = 0b0101,
/// Page 6
Page6 = 0b0110,
/// Page 7
Page7 = 0b0111,
/// Page 8
Page8 = 0b1000,
/// Page 9
Page9 = 0b1001,
/// Page 10
Page10 = 0b1010,
/// Page 11
Page11 = 0b1011,
/// Page 12
Page12 = 0b1100,
/// Page 13
Page13 = 0b1101,
/// Page 14
Page14 = 0b1110,
/// Page 15
Page15 = 0b1111,
}
impl From<u8> for Page {
fn from(val: u8) -> Page {
match val / 8 {
0 => Page::Page0,
1 => Page::Page1,
2 => Page::Page2,
3 => Page::Page3,
4 => Page::Page4,
5 => Page::Page5,
6 => Page::Page6,
7 => Page::Page7,
8 => Page::Page8,
9 => Page::Page9,
10 => Page::Page10,
11 => Page::Page11,
12 => Page::Page12,
13 => Page::Page13,
14 => Page::Page14,
15 => Page::Page15,
_ => panic!("Page too high"),
}
}
}
/// Frame interval
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum NFrames {
/// 2 Frames
F2 = 0b111,
/// 3 Frames
F3 = 0b100,
/// 4 Frames
F4 = 0b101,
/// 5 Frames
F5 = 0b000,
/// 25 Frames
F25 = 0b110,
/// 64 Frames
F64 = 0b001,
/// 128 Frames
F128 = 0b010,
/// 256 Frames
F256 = 0b011,
}
/// Address mode
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum AddrMode {
/// Horizontal mode
Horizontal = 0b00,
/// Vertical mode
Vertical = 0b01,
/// Page mode (default)
Page = 0b10,
}
/// Vcomh Deselect level
#[derive(Debug, Clone, Copy)]
#[allow(dead_code)]
pub enum VcomhLevel {
/// 0.65 * Vcc
V065 = 0b001,
/// 0.77 * Vcc
V077 = 0b010,
/// 0.83 * Vcc
V083 = 0b011,
/// Auto
Auto = 0b100,
}
+8
View File
@@ -0,0 +1,8 @@
/// Errors in this crate
#[derive(Debug)]
pub enum Error<CommE, PinE> {
/// Communication error
Comm(CommE),
/// Pin setting error
Pin(PinE),
}
+64
View File
@@ -0,0 +1,64 @@
//! I2C interface factory
use display_interface_i2c::I2CInterface;
/// Helper struct to create preconfigured I2C interfaces for the display.
#[derive(Debug, Copy, Clone)]
pub struct I2CDisplayInterface(());
impl I2CDisplayInterface {
/// Create new builder with a default I2C address of 0x3C
#[cfg(not(feature = "async"))]
#[allow(clippy::new_ret_no_self)]
// pub fn with_i2c<I>(i2c: I) -> I2CInterface<I> // alternative, but breaking change
pub fn new<I>(i2c: I) -> I2CInterface<I>
where
I: embedded_hal::i2c::I2c,
{
Self::new_custom_address(i2c, 0x3C)
}
#[cfg(feature = "async")]
#[allow(clippy::new_ret_no_self)]
/// Create a new async I2C interface with the address 0x3D
pub fn new<I>(i2c: I) -> I2CInterface<I>
where
I: embedded_hal_async::i2c::I2c,
{
Self::new_custom_address(i2c, 0x3C)
}
#[cfg(not(feature = "async"))]
/// Create a new I2C interface with the alternate address 0x3D as specified in the datasheet.
pub fn new_alternate_address<I>(i2c: I) -> I2CInterface<I>
where
I: embedded_hal::i2c::I2c,
{
Self::new_custom_address(i2c, 0x3D)
}
#[cfg(feature = "async")]
/// Create a new async I2C interface with the alternate address 0x3D as specified in the datasheet.
pub fn new_alternate_address<I>(i2c: I) -> I2CInterface<I>
where
I: embedded_hal_async::i2c::I2c,
{
Self::new_custom_address(i2c, 0x3D)
}
#[cfg(not(feature = "async"))]
/// Create a new I2C interface with a custom address.
pub fn new_custom_address<I>(i2c: I, address: u8) -> I2CInterface<I>
where
I: embedded_hal::i2c::I2c,
{
I2CInterface::new(i2c, address, 0x40)
}
#[cfg(feature = "async")]
/// Create a new async I2C interface with a custom address.
pub fn new_custom_address<I>(i2c: I, address: u8) -> I2CInterface<I>
where
I: embedded_hal_async::i2c::I2c,
{
I2CInterface::new(i2c, address, 0x40)
}
}
+560
View File
@@ -0,0 +1,560 @@
//! SSD1306 OLED display driver.
//!
//! This crate provides a driver interface to the popular SSD1306 monochrome OLED display driver. It
//! supports I2C and SPI via the [`display_interface`](https://docs.rs/display_interface) crate.
//!
//! The main driver is created using [`Ssd1306::new`] which accepts an interface instance, display
//! size, rotation and mode. The following display modes are supported:
//!
//! - [`BasicMode`] - A simple mode with lower level methods available.
//! - [`BufferedGraphicsMode`] - A framebuffered mode with additional methods and integration with
//! [embedded-graphics](https://docs.rs/embedded-graphics).
//! - [`TerminalMode`] - A bufferless mode supporting drawing text to the display, as well as
//! setting cursor positions like a simple terminal.
//!
//! # Examples
//!
//! Examples can be found in [the examples/
//! folder](https://github.com/rust-embedded-community/ssd1306/blob/master/examples)
//!
//! ## Draw some text to the display
//!
//! Uses [`BufferedGraphicsMode`] and [embedded_graphics](https://docs.rs/embedded-graphics). [See
//! the complete example
//! here](https://github.com/rust-embedded-community/ssd1306/blob/master/examples/text_i2c.rs).
//!
//! ```rust
//! # use ssd1306::test_helpers::I2cStub;
//! # let i2c = I2cStub;
//! use embedded_graphics::{
//! mono_font::{ascii::FONT_6X10, MonoTextStyleBuilder},
//! pixelcolor::BinaryColor,
//! prelude::*,
//! text::{Baseline, Text},
//! };
//! use ssd1306::{mode::BufferedGraphicsMode, prelude::*, I2CDisplayInterface, Ssd1306};
//!
//! let interface = I2CDisplayInterface::new(i2c);
//! let mut display = Ssd1306::new(
//! interface,
//! DisplaySize128x64,
//! DisplayRotation::Rotate0,
//! ).into_buffered_graphics_mode();
//! display.init().unwrap();
//!
//! let text_style = MonoTextStyleBuilder::new()
//! .font(&FONT_6X10)
//! .text_color(BinaryColor::On)
//! .build();
//!
//! Text::with_baseline("Hello world!", Point::zero(), text_style, Baseline::Top)
//! .draw(&mut display)
//! .unwrap();
//!
//! Text::with_baseline("Hello Rust!", Point::new(0, 16), text_style, Baseline::Top)
//! .draw(&mut display)
//! .unwrap();
//!
//! display.flush().unwrap();
//! ```
//!
//! ## Write text to the display without a framebuffer
//!
//! Uses [`TerminalMode`]. [See the complete example
//! here](https://github.com/rust-embedded-community/ssd1306/blob/master/examples/terminal_i2c.rs).
//!
//! ```rust
//! # use ssd1306::test_helpers::I2cStub;
//! # let i2c = I2cStub;
//! use core::fmt::Write;
//! use ssd1306::{mode::TerminalMode, prelude::*, I2CDisplayInterface, Ssd1306};
//!
//! let interface = I2CDisplayInterface::new(i2c);
//!
//! let mut display = Ssd1306::new(
//! interface,
//! DisplaySize128x64,
//! DisplayRotation::Rotate0,
//! ).into_terminal_mode();
//! display.init().unwrap();
//! display.clear().unwrap();
//!
//! // Spam some characters to the display
//! for c in 97..123 {
//! let _ = display.write_str(unsafe { core::str::from_utf8_unchecked(&[c]) });
//! }
//! for c in 65..91 {
//! let _ = display.write_str(unsafe { core::str::from_utf8_unchecked(&[c]) });
//! }
//!
//! // The `write!()` macro is also supported
//! write!(display, "Hello, {}", "world");
//! ```
//!
//! [featureset]: https://github.com/rust-embedded-community/embedded-graphics#features
//! [`BufferedGraphicsMode`]: crate::mode::BufferedGraphicsMode
//! [`TerminalMode`]: crate::mode::TerminalMode
#![no_std]
#![deny(missing_debug_implementations)]
#![deny(missing_docs)]
#![deny(warnings)]
#![deny(missing_copy_implementations)]
#![deny(trivial_casts)]
#![deny(trivial_numeric_casts)]
#![deny(unsafe_code)]
#![deny(unstable_features)]
#![deny(unused_import_braces)]
#![deny(unused_qualifications)]
#![deny(rustdoc::broken_intra_doc_links)]
#![allow(async_fn_in_trait)]
mod brightness;
pub mod command;
mod error;
mod i2c_interface;
pub mod mode;
pub mod prelude;
pub mod rotation;
pub mod size;
#[doc(hidden)]
pub mod test_helpers;
use core::convert::Infallible;
pub use crate::i2c_interface::I2CDisplayInterface;
use crate::mode::BasicMode;
use brightness::Brightness;
#[cfg(feature = "async")]
use command::CommandAsync;
use command::{AddrMode, Command, VcomhLevel};
#[cfg(feature = "async")]
use display_interface::AsyncWriteOnlyDataCommand;
use display_interface::{DataFormat::U8, DisplayError, WriteOnlyDataCommand};
use embedded_hal::{delay::DelayNs, digital::OutputPin};
#[cfg(feature = "async")]
use embedded_hal_async::delay::DelayNs as DelayNsAsync;
use error::Error;
use mode::{BufferedGraphicsMode, TerminalMode};
#[cfg(feature = "async")]
use mode::{BufferedGraphicsModeAsync, TerminalModeAsync};
use rotation::DisplayRotation;
use size::DisplaySize;
#[cfg(feature = "async")]
use size::DisplaySizeAsync;
/// SSD1306 driver.
///
/// Note that some methods are only available when the display is configured in a certain [`mode`].
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async"))]
#[derive(Copy, Clone, Debug)]
pub struct Ssd1306<DI, SIZE, MODE> {
interface: DI,
mode: MODE,
size: SIZE,
addr_mode: AddrMode,
rotation: DisplayRotation,
}
#[maybe_async_cfg::maybe(
sync(keep_self,),
async(feature = "async", idents(DisplaySize(async = "DisplaySizeAsync")))
)]
impl<DI, SIZE> Ssd1306<DI, SIZE, BasicMode>
where
SIZE: DisplaySize,
{
/// Create a basic SSD1306 interface.
///
/// Use the `into_*_mode` methods to enable more functionality.
pub fn new(interface: DI, size: SIZE, rotation: DisplayRotation) -> Self {
Self {
interface,
size,
addr_mode: AddrMode::Page,
mode: BasicMode,
rotation,
}
}
}
#[maybe_async_cfg::maybe(
sync(keep_self,),
async(
feature = "async",
idents(
DisplaySize(async = "DisplaySizeAsync"),
BufferedGraphicsMode(async = "BufferedGraphicsModeAsync"),
TerminalMode(async = "TerminalModeAsync"),
)
)
)]
impl<DI, SIZE, MODE> Ssd1306<DI, SIZE, MODE>
where
SIZE: DisplaySize,
{
/// Convert the display into another interface mode.
fn into_mode<MODE2>(self, mode: MODE2) -> Ssd1306<DI, SIZE, MODE2> {
Ssd1306 {
mode,
addr_mode: self.addr_mode,
interface: self.interface,
size: self.size,
rotation: self.rotation,
}
}
/// Convert the display into a buffered graphics mode, supporting
/// [embedded-graphics](https://crates.io/crates/embedded-graphics).
///
/// See [`BufferedGraphicsMode`] for more information.
pub fn into_buffered_graphics_mode(self) -> Ssd1306<DI, SIZE, BufferedGraphicsMode<SIZE>> {
self.into_mode(BufferedGraphicsMode::new())
}
/// Convert the display into a text-only, terminal-like mode.
///
/// See [`TerminalMode`] for more information.
pub fn into_terminal_mode(self) -> Ssd1306<DI, SIZE, TerminalMode> {
self.into_mode(TerminalMode::new())
}
}
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
Command(async = "CommandAsync"),
DisplaySize(async = "DisplaySizeAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
)
)
)]
impl<DI, SIZE, MODE> Ssd1306<DI, SIZE, MODE>
where
DI: WriteOnlyDataCommand,
SIZE: DisplaySize,
{
/// Initialise the display in one of the available addressing modes.
pub async fn init_with_addr_mode(&mut self, mode: AddrMode) -> Result<(), DisplayError> {
let rotation = self.rotation;
Command::DisplayOn(false).send(&mut self.interface).await?;
Command::DisplayClockDiv(0x8, 0x0)
.send(&mut self.interface)
.await?;
Command::Multiplex(SIZE::HEIGHT - 1)
.send(&mut self.interface)
.await?;
Command::DisplayOffset(0).send(&mut self.interface).await?;
Command::StartLine(0).send(&mut self.interface).await?;
// TODO: Ability to turn charge pump on/off
Command::ChargePump(true).send(&mut self.interface).await?;
Command::AddressMode(mode).send(&mut self.interface).await?;
self.size.configure(&mut self.interface).await?;
self.set_rotation(rotation).await?;
self.set_brightness(Brightness::default()).await?;
Command::VcomhDeselect(VcomhLevel::Auto)
.send(&mut self.interface)
.await?;
Command::AllOn(false).send(&mut self.interface).await?;
Command::Invert(false).send(&mut self.interface).await?;
Command::EnableScroll(false)
.send(&mut self.interface)
.await?;
Command::DisplayOn(true).send(&mut self.interface).await?;
self.addr_mode = mode;
Ok(())
}
/// Change the addressing mode
pub async fn set_addr_mode(&mut self, mode: AddrMode) -> Result<(), DisplayError> {
Command::AddressMode(mode).send(&mut self.interface).await?;
self.addr_mode = mode;
Ok(())
}
/// Send the data to the display for drawing at the current position in the framebuffer
/// and advance the position accordingly. Cf. `set_draw_area` to modify the affected area by
/// this method.
///
/// This method takes advantage of a bounding box for faster writes.
pub async fn bounded_draw(
&mut self,
buffer: &[u8],
disp_width: usize,
upper_left: (u8, u8),
lower_right: (u8, u8),
) -> Result<(), DisplayError> {
Self::flush_buffer_chunks(
&mut self.interface,
buffer,
disp_width,
upper_left,
lower_right,
)
.await
}
/// Send a raw buffer to the display.
pub async fn draw(&mut self, buffer: &[u8]) -> Result<(), DisplayError> {
self.interface.send_data(U8(buffer)).await
}
/// Get display dimensions, taking into account the current rotation of the display
///
/// ```rust
/// # use ssd1306::test_helpers::StubInterface;
/// # let interface = StubInterface;
/// use ssd1306::{mode::TerminalMode, prelude::*, Ssd1306};
///
/// let mut display = Ssd1306::new(
/// interface,
/// DisplaySize128x64,
/// DisplayRotation::Rotate0,
/// ).into_terminal_mode();
/// assert_eq!(display.dimensions(), (128, 64));
///
/// # let interface = StubInterface;
/// let mut rotated_display = Ssd1306::new(
/// interface,
/// DisplaySize128x64,
/// DisplayRotation::Rotate90,
/// ).into_terminal_mode();
/// assert_eq!(rotated_display.dimensions(), (64, 128));
/// ```
pub fn dimensions(&self) -> (u8, u8) {
match self.rotation {
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => (SIZE::WIDTH, SIZE::HEIGHT),
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => (SIZE::HEIGHT, SIZE::WIDTH),
}
}
/// Get the display rotation.
pub fn rotation(&self) -> DisplayRotation {
self.rotation
}
/// Set the display rotation.
pub async fn set_rotation(&mut self, rotation: DisplayRotation) -> Result<(), DisplayError> {
self.rotation = rotation;
match rotation {
DisplayRotation::Rotate0 => {
Command::SegmentRemap(true)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(true)
.send(&mut self.interface)
.await?;
}
DisplayRotation::Rotate90 => {
Command::SegmentRemap(false)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(true)
.send(&mut self.interface)
.await?;
}
DisplayRotation::Rotate180 => {
Command::SegmentRemap(false)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(false)
.send(&mut self.interface)
.await?;
}
DisplayRotation::Rotate270 => {
Command::SegmentRemap(true)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(false)
.send(&mut self.interface)
.await?;
}
};
Ok(())
}
/// Set mirror enabled/disabled.
pub async fn set_mirror(&mut self, mirror: bool) -> Result<(), DisplayError> {
if mirror {
match self.rotation {
DisplayRotation::Rotate0 => {
Command::SegmentRemap(false)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(true)
.send(&mut self.interface)
.await?;
}
DisplayRotation::Rotate90 => {
Command::SegmentRemap(false)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(false)
.send(&mut self.interface)
.await?;
}
DisplayRotation::Rotate180 => {
Command::SegmentRemap(true)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(false)
.send(&mut self.interface)
.await?;
}
DisplayRotation::Rotate270 => {
Command::SegmentRemap(true)
.send(&mut self.interface)
.await?;
Command::ReverseComDir(true)
.send(&mut self.interface)
.await?;
}
};
} else {
self.set_rotation(self.rotation).await?;
}
Ok(())
}
/// Change the display brightness.
pub async fn set_brightness(&mut self, brightness: Brightness) -> Result<(), DisplayError> {
Command::PreChargePeriod(1, brightness.precharge)
.send(&mut self.interface)
.await?;
Command::Contrast(brightness.contrast)
.send(&mut self.interface)
.await
}
/// Turn the display on or off. The display can be drawn to and retains all
/// of its memory even while off.
pub async fn set_display_on(&mut self, on: bool) -> Result<(), DisplayError> {
Command::DisplayOn(on).send(&mut self.interface).await
}
/// Set the position in the framebuffer of the display limiting where any sent data should be
/// drawn. This method can be used for changing the affected area on the screen as well
/// as (re-)setting the start point of the next `draw` call.
pub async fn set_draw_area(
&mut self,
start: (u8, u8),
end: (u8, u8),
) -> Result<(), DisplayError> {
Command::ColumnAddress(start.0, end.0.saturating_sub(1))
.send(&mut self.interface)
.await?;
if self.addr_mode != AddrMode::Page {
Command::PageAddress(start.1.into(), (end.1.saturating_sub(1)).into())
.send(&mut self.interface)
.await?;
}
Ok(())
}
/// Set the column address in the framebuffer of the display where any sent data should be
/// drawn.
pub async fn set_column(&mut self, column: u8) -> Result<(), DisplayError> {
Command::ColStart(column).send(&mut self.interface).await
}
/// Set the page address (row 8px high) in the framebuffer of the display where any sent data
/// should be drawn.
///
/// Note that the parameter is in pixels, but the page will be set to the start of the 8px
/// row which contains the passed-in row.
pub async fn set_row(&mut self, row: u8) -> Result<(), DisplayError> {
Command::PageStart(row.into())
.send(&mut self.interface)
.await
}
/// Set the screen pixel on/off inversion
pub async fn set_invert(&mut self, invert: bool) -> Result<(), DisplayError> {
Command::Invert(invert).send(&mut self.interface).await
}
async fn flush_buffer_chunks(
interface: &mut DI,
buffer: &[u8],
disp_width: usize,
upper_left: (u8, u8),
lower_right: (u8, u8),
) -> Result<(), DisplayError> {
// Divide by 8 since each row is actually 8 pixels tall
let num_pages = ((lower_right.1 - upper_left.1) / 8) as usize + 1;
// Each page is 8 bits tall, so calculate which page number to start at (rounded down) from
// the top of the display
let starting_page = (upper_left.1 / 8) as usize;
// Calculate start and end X coordinates for each page
let page_lower = upper_left.0 as usize;
let page_upper = lower_right.0 as usize;
for c in buffer
.chunks(disp_width)
.skip(starting_page)
.take(num_pages)
.map(|s| &s[page_lower..page_upper])
{
interface.send_data(U8(c)).await?
}
Ok(())
}
/// Release the contained interface.
pub fn release(self) -> DI {
self.interface
}
}
// SPI-only reset
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(
Command(async = "CommandAsync"),
DisplaySize(async = "DisplaySizeAsync"),
WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"),
DelayNs(async = "DelayNsAsync")
)
)
)]
impl<DI, SIZE, MODE> Ssd1306<DI, SIZE, MODE> {
/// Reset the display.
pub async fn reset<RST, DELAY>(
&mut self,
rst: &mut RST,
delay: &mut DELAY,
) -> Result<(), Error<Infallible, RST::Error>>
where
RST: OutputPin,
DELAY: DelayNs,
{
async fn inner_reset<RST, DELAY>(rst: &mut RST, delay: &mut DELAY) -> Result<(), RST::Error>
where
RST: OutputPin,
DELAY: DelayNs,
{
rst.set_high()?;
delay.delay_ms(1).await;
rst.set_low()?;
delay.delay_ms(10).await;
rst.set_high()
}
inner_reset(rst, delay).await.map_err(Error::Pin)
}
}
+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(())
}
}
+18
View File
@@ -0,0 +1,18 @@
//! Crate prelude
pub use display_interface::WriteOnlyDataCommand;
pub use display_interface_i2c::I2CInterface;
pub use display_interface_spi::SPIInterface;
pub use super::{
brightness::Brightness,
mode::DisplayConfig,
rotation::DisplayRotation,
size::{
DisplaySize, DisplaySize128x32, DisplaySize128x64, DisplaySize64x32, DisplaySize64x48,
DisplaySize72x40, DisplaySize96x16,
},
};
#[cfg(feature = "async")]
pub use super::mode::DisplayConfigAsync;
+14
View File
@@ -0,0 +1,14 @@
//! Display rotation.
/// Display rotation.
#[derive(Copy, Clone, Debug)]
pub enum DisplayRotation {
/// No rotation, normal display
Rotate0,
/// Rotate by 90 degrees clockwise
Rotate90,
/// Rotate by 180 degrees clockwise
Rotate180,
/// Rotate 270 degrees clockwise
Rotate270,
}
+188
View File
@@ -0,0 +1,188 @@
//! Display size.
use super::command::Command;
#[cfg(feature = "async")]
use super::command::CommandAsync;
#[cfg(feature = "async")]
use display_interface::AsyncWriteOnlyDataCommand;
use display_interface::{DisplayError, WriteOnlyDataCommand};
/// Workaround trait, since `Default` is only implemented to arrays up to 32 of size
pub trait NewZeroed {
/// Creates a new value with its memory set to zero
fn new_zeroed() -> Self;
}
impl<const N: usize> NewZeroed for [u8; N] {
fn new_zeroed() -> Self {
[0u8; N]
}
}
/// Display information.
///
/// This trait describes information related to a particular display.
/// This includes resolution, offset and framebuffer size.
#[maybe_async_cfg::maybe(
sync(keep_self),
async(
feature = "async",
idents(WriteOnlyDataCommand(async = "AsyncWriteOnlyDataCommand"))
)
)]
pub trait DisplaySize {
/// Width in pixels
const WIDTH: u8;
/// Height in pixels
const HEIGHT: u8;
/// Maximum width supported by the display driver
const DRIVER_COLS: u8 = 128;
/// Maximum height supported by the display driver
const DRIVER_ROWS: u8 = 64;
/// Horizontal offset in pixels
const OFFSETX: u8 = 0;
/// Vertical offset in pixels
const OFFSETY: u8 = 0;
/// Size of framebuffer. Because the display is monochrome, this is
/// width * height / 8
type Buffer: AsMut<[u8]> + NewZeroed;
/// Send resolution and model-dependent configuration to the display
///
/// See [`Command::ComPinConfig`]
/// and [`Command::InternalIref`]
/// for more information
async fn configure(&self, iface: &mut impl WriteOnlyDataCommand) -> Result<(), DisplayError>;
}
maybe_async_cfg::content! {
#![maybe_async_cfg::default(
idents(
WriteOnlyDataCommand(sync, async = "AsyncWriteOnlyDataCommand"),
Command(sync, async = "CommandAsync"),
DisplaySize(sync, async="DisplaySizeAsync")
)
)]
/// Size information for the common 128x64 variants
#[derive(Debug, Copy, Clone)]
pub struct DisplaySize128x64;
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", keep_self))]
impl DisplaySize for DisplaySize128x64 {
const WIDTH: u8 = 128;
const HEIGHT: u8 = 64;
type Buffer = [u8; <Self as DisplaySize>::WIDTH as usize *
<Self as DisplaySize>::HEIGHT as usize / 8];
async fn configure(
&self,
iface: &mut impl WriteOnlyDataCommand,
) -> Result<(), DisplayError> {
Command::ComPinConfig(true, false).send(iface).await
}
}
/// Size information for the common 128x32 variants
#[derive(Debug, Copy, Clone)]
pub struct DisplaySize128x32;
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", keep_self))]
impl DisplaySize for DisplaySize128x32 {
const WIDTH: u8 = 128;
const HEIGHT: u8 = 32;
type Buffer = [u8; <Self as DisplaySize>::WIDTH as usize *
<Self as DisplaySize>::HEIGHT as usize / 8];
async fn configure(
&self,
iface: &mut impl WriteOnlyDataCommand,
) -> Result<(), DisplayError> {
Command::ComPinConfig(false, false).send(iface).await
}
}
/// Size information for the common 96x16 variants
#[derive(Debug, Copy, Clone)]
pub struct DisplaySize96x16;
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", keep_self))]
impl DisplaySize for DisplaySize96x16 {
const WIDTH: u8 = 96;
const HEIGHT: u8 = 16;
type Buffer = [u8; <Self as DisplaySize>::WIDTH as usize *
<Self as DisplaySize>::HEIGHT as usize / 8];
async fn configure(
&self,
iface: &mut impl WriteOnlyDataCommand,
) -> Result<(), DisplayError> {
Command::ComPinConfig(false, false).send(iface).await
}
}
/// Size information for the common 72x40 variants
#[derive(Debug, Copy, Clone)]
pub struct DisplaySize72x40;
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", keep_self))]
impl DisplaySize for DisplaySize72x40 {
const WIDTH: u8 = 72;
const HEIGHT: u8 = 40;
const OFFSETX: u8 = 28;
const OFFSETY: u8 = 0;
type Buffer = [u8; <Self as DisplaySize>::WIDTH as usize *
<Self as DisplaySize>::HEIGHT as usize / 8];
async fn configure(
&self,
iface: &mut impl WriteOnlyDataCommand,
) -> Result<(), DisplayError> {
Command::ComPinConfig(true, false).send(iface).await?;
Command::InternalIref(true, true).send(iface).await
}
}
/// Size information for the common 64x48 variants
#[derive(Debug, Copy, Clone)]
pub struct DisplaySize64x48;
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", keep_self))]
impl DisplaySize for DisplaySize64x48 {
const WIDTH: u8 = 64;
const HEIGHT: u8 = 48;
const OFFSETX: u8 = 32;
const OFFSETY: u8 = 0;
type Buffer = [u8; <Self as DisplaySize>::WIDTH as usize *
<Self as DisplaySize>::HEIGHT as usize / 8];
async fn configure(
&self,
iface: &mut impl WriteOnlyDataCommand,
) -> Result<(), DisplayError> {
Command::ComPinConfig(true, false).send(iface).await
}
}
/// Size information for the common 64x32 variants
#[derive(Debug, Copy, Clone)]
pub struct DisplaySize64x32;
#[maybe_async_cfg::maybe(sync(keep_self), async(feature = "async", keep_self))]
impl DisplaySize for DisplaySize64x32 {
const WIDTH: u8 = 64;
const HEIGHT: u8 = 32;
const OFFSETX: u8 = 32;
const OFFSETY: u8 = 0;
type Buffer = [u8; <Self as DisplaySize>::WIDTH as usize *
<Self as DisplaySize>::HEIGHT as usize / 8];
async fn configure(
&self,
iface: &mut impl WriteOnlyDataCommand,
) -> Result<(), DisplayError> {
Command::ComPinConfig(true, false).send(iface).await
}
}
} // content
+111
View File
@@ -0,0 +1,111 @@
//! Helpers for use in examples and tests
use display_interface::{DisplayError, WriteOnlyDataCommand};
use embedded_hal::{
digital::{ErrorType, OutputPin},
i2c,
spi::{self, SpiBus},
};
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
pub struct Error {}
impl embedded_hal::digital::Error for Error {
fn kind(&self) -> embedded_hal::digital::ErrorKind {
embedded_hal::digital::ErrorKind::Other
}
}
impl i2c::Error for Error {
fn kind(&self) -> i2c::ErrorKind {
i2c::ErrorKind::Other
}
}
impl spi::Error for Error {
fn kind(&self) -> spi::ErrorKind {
spi::ErrorKind::Other
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct SpiStub;
impl spi::ErrorType for SpiStub {
type Error = Error;
}
impl SpiBus<u8> for SpiStub {
fn read(&mut self, _words: &mut [u8]) -> Result<(), Self::Error> {
todo!()
}
fn write(&mut self, _words: &[u8]) -> Result<(), Self::Error> {
Ok(())
}
fn transfer(&mut self, _read: &mut [u8], _write: &[u8]) -> Result<(), Self::Error> {
Ok(())
}
fn transfer_in_place(&mut self, _words: &mut [u8]) -> Result<(), Self::Error> {
todo!()
}
fn flush(&mut self) -> Result<(), Self::Error> {
todo!()
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct I2cStub;
impl i2c::ErrorType for I2cStub {
type Error = Error;
}
impl i2c::I2c for I2cStub {
fn transaction(
&mut self,
_address: u8,
_operations: &mut [i2c::Operation<'_>],
) -> Result<(), Self::Error> {
Ok(())
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct PinStub;
impl ErrorType for PinStub {
type Error = Error;
}
impl OutputPin for PinStub {
fn set_low(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn set_high(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
pub struct StubInterface;
impl WriteOnlyDataCommand for StubInterface {
fn send_commands(
&mut self,
_cmd: display_interface::DataFormat<'_>,
) -> Result<(), DisplayError> {
Ok(())
}
fn send_data(&mut self, _buf: display_interface::DataFormat<'_>) -> Result<(), DisplayError> {
Ok(())
}
}