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
Vendored
+53
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+327
@@ -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,
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/// Errors in this crate
|
||||
#[derive(Debug)]
|
||||
pub enum Error<CommE, PinE> {
|
||||
/// Communication error
|
||||
Comm(CommE),
|
||||
/// Pin setting error
|
||||
Pin(PinE),
|
||||
}
|
||||
Vendored
+64
@@ -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)
|
||||
}
|
||||
}
|
||||
Vendored
+560
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
Vendored
+18
@@ -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;
|
||||
Vendored
+14
@@ -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,
|
||||
}
|
||||
Vendored
+188
@@ -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
|
||||
Vendored
+111
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user