include display driver,
and use rust_tls to make crosscompile easy
This commit is contained in:
1859
Cargo.lock
generated
1859
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -2,11 +2,22 @@
|
||||
name = "killpaper"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
#rust-version = "1.64"
|
||||
|
||||
[dependencies]
|
||||
d = "0.0.1"
|
||||
embedded-graphics = "0.8.1"
|
||||
embedded-graphics-core = { version = "0.4", optional = true }
|
||||
embedded-graphics = "0.8"
|
||||
embedded-hal = "1.0.0"
|
||||
gpiocdev-embedded-hal = "0.1.2"
|
||||
image = { version = "0.25.6", default-features = false, features = ["jpeg", "png"] }
|
||||
reqwest = { version = "0.12", features = ["json", "blocking", "gzip", "brotli"] }
|
||||
linux-embedded-hal = "0.4.0"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["http2","rustls-tls-native-roots","json", "blocking", "gzip", "brotli"] }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
serde_json = "1.0.143"
|
||||
|
||||
|
||||
[features]
|
||||
# Remove the linux-dev feature to build the tests on non unix systems
|
||||
default = ["graphics"]
|
||||
graphics = []
|
||||
|
||||
|
||||
561
src/color.rs
Normal file
561
src/color.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
//! B/W Color for EPDs
|
||||
//!
|
||||
//! EPD representation of multicolor with separate buffers
|
||||
//! for each bit makes it hard to properly represent colors here
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
#[cfg(feature = "graphics")]
|
||||
use embedded_graphics::pixelcolor::PixelColor;
|
||||
|
||||
/// When trying to parse u8 to one of the color types
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct OutOfColorRangeParseError(u8);
|
||||
impl core::fmt::Display for OutOfColorRangeParseError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "Outside of possible Color Range: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl OutOfColorRangeParseError {
|
||||
fn _new(size: u8) -> OutOfColorRangeParseError {
|
||||
OutOfColorRangeParseError(size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Only for the Black/White-Displays
|
||||
// TODO : 'color' is not a good name for black and white, rename it to BiColor/BWColor ?
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum Color {
|
||||
/// Black color
|
||||
Black,
|
||||
/// White color
|
||||
#[default]
|
||||
White,
|
||||
}
|
||||
|
||||
/// Only for the Black/White/Color-Displays
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum TriColor {
|
||||
/// Black color
|
||||
Black,
|
||||
/// White color
|
||||
#[default]
|
||||
White,
|
||||
/// Chromatic color
|
||||
Chromatic,
|
||||
}
|
||||
|
||||
/// For the 7 Color Displays
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||
pub enum OctColor {
|
||||
/// Black Color
|
||||
Black = 0x00,
|
||||
/// White Color
|
||||
#[default]
|
||||
White = 0x01,
|
||||
/// Green Color
|
||||
Green = 0x02,
|
||||
/// Blue Color
|
||||
Blue = 0x03,
|
||||
/// Red Color
|
||||
Red = 0x04,
|
||||
/// Yellow Color
|
||||
Yellow = 0x05,
|
||||
/// Orange Color
|
||||
Orange = 0x06,
|
||||
/// HiZ / Clean Color
|
||||
HiZ = 0x07,
|
||||
}
|
||||
|
||||
/// Color trait for use in `Display`s
|
||||
pub trait ColorType {
|
||||
/// Number of bit used to represent this color type in a single buffer.
|
||||
/// To get the real number of bits per pixel you should multiply this by `BUFFER_COUNT`
|
||||
const BITS_PER_PIXEL_PER_BUFFER: usize;
|
||||
|
||||
/// Number of buffer used to represent this color type
|
||||
/// splitted buffer like tricolo is 2, otherwise this should be 1.
|
||||
const BUFFER_COUNT: usize;
|
||||
|
||||
/// Return the data used to set a pixel color
|
||||
///
|
||||
/// * bwrbit is used to tell the value of the unused bit when a chromatic
|
||||
/// color is set (TriColor only as for now)
|
||||
/// * pos is the pixel position in the line, used to know which pixels must be set
|
||||
///
|
||||
/// Return values are :
|
||||
/// * .0 is the mask used to exclude this pixel from the byte (eg: 0x7F in BiColor)
|
||||
/// * .1 are the bits used to set the color in the byte (eg: 0x80 in BiColor)
|
||||
/// this is u16 because we set 2 bytes in case of split buffer
|
||||
fn bitmask(&self, bwrbit: bool, pos: u32) -> (u8, u16);
|
||||
}
|
||||
|
||||
impl ColorType for Color {
|
||||
const BITS_PER_PIXEL_PER_BUFFER: usize = 1;
|
||||
const BUFFER_COUNT: usize = 1;
|
||||
fn bitmask(&self, _bwrbit: bool, pos: u32) -> (u8, u16) {
|
||||
let bit = 0x80 >> (pos % 8);
|
||||
match self {
|
||||
Color::Black => (!bit, 0u16),
|
||||
Color::White => (!bit, bit as u16),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorType for TriColor {
|
||||
const BITS_PER_PIXEL_PER_BUFFER: usize = 1;
|
||||
const BUFFER_COUNT: usize = 2;
|
||||
fn bitmask(&self, bwrbit: bool, pos: u32) -> (u8, u16) {
|
||||
let bit = 0x80 >> (pos % 8);
|
||||
match self {
|
||||
TriColor::Black => (!bit, 0u16),
|
||||
TriColor::White => (!bit, bit as u16),
|
||||
TriColor::Chromatic => (
|
||||
!bit,
|
||||
if bwrbit {
|
||||
(bit as u16) << 8
|
||||
} else {
|
||||
(bit as u16) << 8 | bit as u16
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorType for OctColor {
|
||||
const BITS_PER_PIXEL_PER_BUFFER: usize = 4;
|
||||
const BUFFER_COUNT: usize = 1;
|
||||
fn bitmask(&self, _bwrbit: bool, pos: u32) -> (u8, u16) {
|
||||
let mask = !(0xF0 >> ((pos % 2) * 4));
|
||||
let bits = self.get_nibble() as u16;
|
||||
(mask, if pos % 2 == 1 { bits } else { bits << 4 })
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<BinaryColor> for OctColor {
|
||||
fn from(b: BinaryColor) -> OctColor {
|
||||
match b {
|
||||
BinaryColor::On => OctColor::Black,
|
||||
BinaryColor::Off => OctColor::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<OctColor> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
fn from(b: OctColor) -> Self {
|
||||
let (r, g, b) = b.rgb();
|
||||
Self::new(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb888> for OctColor {
|
||||
fn from(p: embedded_graphics::pixelcolor::Rgb888) -> OctColor {
|
||||
use embedded_graphics::prelude::RgbColor;
|
||||
let colors = [
|
||||
OctColor::Black,
|
||||
OctColor::White,
|
||||
OctColor::Green,
|
||||
OctColor::Blue,
|
||||
OctColor::Red,
|
||||
OctColor::Yellow,
|
||||
OctColor::Orange,
|
||||
OctColor::HiZ,
|
||||
];
|
||||
// if the user has already mapped to the right color space, it will just be in the list
|
||||
if let Some(found) = colors.iter().find(|c| c.rgb() == (p.r(), p.g(), p.b())) {
|
||||
return *found;
|
||||
}
|
||||
|
||||
// This is not ideal but just pick the nearest color
|
||||
*colors
|
||||
.iter()
|
||||
.map(|c| (c, c.rgb()))
|
||||
.map(|(c, (r, g, b))| {
|
||||
let dist = (i32::from(r) - i32::from(p.r())).pow(2)
|
||||
+ (i32::from(g) - i32::from(p.g())).pow(2)
|
||||
+ (i32::from(b) - i32::from(p.b())).pow(2);
|
||||
(c, dist)
|
||||
})
|
||||
.min_by_key(|(_c, dist)| *dist)
|
||||
.map(|(c, _)| c)
|
||||
.unwrap_or(&OctColor::White)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::raw::RawU4> for OctColor {
|
||||
fn from(b: embedded_graphics::pixelcolor::raw::RawU4) -> Self {
|
||||
use embedded_graphics::prelude::RawData;
|
||||
OctColor::from_nibble(b.into_inner()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl PixelColor for OctColor {
|
||||
type Raw = embedded_graphics::pixelcolor::raw::RawU4;
|
||||
}
|
||||
|
||||
impl OctColor {
|
||||
/// Gets the Nibble representation of the Color as needed by the display
|
||||
pub fn get_nibble(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
/// Converts two colors into a single byte for the Display
|
||||
pub fn colors_byte(a: OctColor, b: OctColor) -> u8 {
|
||||
a.get_nibble() << 4 | b.get_nibble()
|
||||
}
|
||||
|
||||
///Take the nibble (lower 4 bits) and convert to an OctColor if possible
|
||||
pub fn from_nibble(nibble: u8) -> Result<OctColor, OutOfColorRangeParseError> {
|
||||
match nibble & 0xf {
|
||||
0x00 => Ok(OctColor::Black),
|
||||
0x01 => Ok(OctColor::White),
|
||||
0x02 => Ok(OctColor::Green),
|
||||
0x03 => Ok(OctColor::Blue),
|
||||
0x04 => Ok(OctColor::Red),
|
||||
0x05 => Ok(OctColor::Yellow),
|
||||
0x06 => Ok(OctColor::Orange),
|
||||
0x07 => Ok(OctColor::HiZ),
|
||||
e => Err(OutOfColorRangeParseError(e)),
|
||||
}
|
||||
}
|
||||
///Split the nibbles of a single byte and convert both to an OctColor if possible
|
||||
pub fn split_byte(byte: u8) -> Result<(OctColor, OctColor), OutOfColorRangeParseError> {
|
||||
let low = OctColor::from_nibble(byte & 0xf)?;
|
||||
let high = OctColor::from_nibble((byte >> 4) & 0xf)?;
|
||||
Ok((high, low))
|
||||
}
|
||||
/// Converts to limited range of RGB values.
|
||||
pub fn rgb(self) -> (u8, u8, u8) {
|
||||
match self {
|
||||
OctColor::White => (0xff, 0xff, 0xff),
|
||||
OctColor::Black => (0x00, 0x00, 0x00),
|
||||
OctColor::Green => (0x00, 0xff, 0x00),
|
||||
OctColor::Blue => (0x00, 0x00, 0xff),
|
||||
OctColor::Red => (0xff, 0x00, 0x00),
|
||||
OctColor::Yellow => (0xff, 0xff, 0x00),
|
||||
OctColor::Orange => (0xff, 0x80, 0x00),
|
||||
OctColor::HiZ => (0x80, 0x80, 0x80), /* looks greyish */
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO: Rename get_bit_value to bit() and get_byte_value to byte() ?
|
||||
|
||||
impl Color {
|
||||
/// Get the color encoding of the color for one bit
|
||||
pub fn get_bit_value(self) -> u8 {
|
||||
match self {
|
||||
Color::White => 1u8,
|
||||
Color::Black => 0u8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a full byte of black or white pixels
|
||||
pub fn get_byte_value(self) -> u8 {
|
||||
match self {
|
||||
Color::White => 0xff,
|
||||
Color::Black => 0x00,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses from u8 to Color
|
||||
fn from_u8(val: u8) -> Self {
|
||||
match val {
|
||||
0 => Color::Black,
|
||||
1 => Color::White,
|
||||
e => panic!(
|
||||
"DisplayColor only parses 0 and 1 (Black and White) and not `{}`",
|
||||
e
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the inverse of the given color.
|
||||
///
|
||||
/// Black returns White and White returns Black
|
||||
pub fn inverse(self) -> Color {
|
||||
match self {
|
||||
Color::White => Color::Black,
|
||||
Color::Black => Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for Color {
|
||||
fn from(value: u8) -> Self {
|
||||
Color::from_u8(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::raw::RawU1> for Color {
|
||||
fn from(b: embedded_graphics::pixelcolor::raw::RawU1) -> Self {
|
||||
use embedded_graphics::prelude::RawData;
|
||||
if b.into_inner() == 0 {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::raw::RawU1 {
|
||||
fn from(color: Color) -> Self {
|
||||
Self::new(color.get_bit_value())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl PixelColor for Color {
|
||||
type Raw = embedded_graphics::pixelcolor::raw::RawU1;
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<BinaryColor> for Color {
|
||||
fn from(b: BinaryColor) -> Color {
|
||||
match b {
|
||||
BinaryColor::On => Color::Black,
|
||||
BinaryColor::Off => Color::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb888> for Color {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb888) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
if rgb == RgbColor::BLACK {
|
||||
Color::Black
|
||||
} else if rgb == RgbColor::WHITE {
|
||||
Color::White
|
||||
} else {
|
||||
// choose closest color
|
||||
if (rgb.r() as u16 + rgb.g() as u16 + rgb.b() as u16) > 255 * 3 / 2 {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
fn from(color: Color) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
match color {
|
||||
Color::Black => Self::BLACK,
|
||||
Color::White => Self::WHITE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb565> for Color {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb565) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
if rgb == RgbColor::BLACK {
|
||||
Color::Black
|
||||
} else if rgb == RgbColor::WHITE {
|
||||
Color::White
|
||||
} else {
|
||||
// choose closest color
|
||||
if (rgb.r() as u16 + rgb.g() as u16 + rgb.b() as u16) > 255 * 3 / 2 {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::Rgb565 {
|
||||
fn from(color: Color) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
match color {
|
||||
Color::Black => Self::BLACK,
|
||||
Color::White => Self::WHITE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb555> for Color {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb555) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
if rgb == RgbColor::BLACK {
|
||||
Color::Black
|
||||
} else if rgb == RgbColor::WHITE {
|
||||
Color::White
|
||||
} else {
|
||||
// choose closest color
|
||||
if (rgb.r() as u16 + rgb.g() as u16 + rgb.b() as u16) > 255 * 3 / 2 {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<Color> for embedded_graphics::pixelcolor::Rgb555 {
|
||||
fn from(color: Color) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
match color {
|
||||
Color::Black => Self::BLACK,
|
||||
Color::White => Self::WHITE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TriColor {
|
||||
/// Get the color encoding of the color for one bit
|
||||
pub fn get_bit_value(self) -> u8 {
|
||||
match self {
|
||||
TriColor::White => 1u8,
|
||||
TriColor::Black | TriColor::Chromatic => 0u8,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a full byte of black or white pixels
|
||||
pub fn get_byte_value(self) -> u8 {
|
||||
match self {
|
||||
TriColor::White => 0xff,
|
||||
TriColor::Black | TriColor::Chromatic => 0x00,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::raw::RawU2> for TriColor {
|
||||
fn from(b: embedded_graphics::pixelcolor::raw::RawU2) -> Self {
|
||||
use embedded_graphics::prelude::RawData;
|
||||
if b.into_inner() == 0b00 {
|
||||
TriColor::White
|
||||
} else if b.into_inner() == 0b01 {
|
||||
TriColor::Black
|
||||
} else {
|
||||
TriColor::Chromatic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl PixelColor for TriColor {
|
||||
type Raw = embedded_graphics::pixelcolor::raw::RawU2;
|
||||
}
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<BinaryColor> for TriColor {
|
||||
fn from(b: BinaryColor) -> TriColor {
|
||||
match b {
|
||||
BinaryColor::On => TriColor::Black,
|
||||
BinaryColor::Off => TriColor::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<embedded_graphics::pixelcolor::Rgb888> for TriColor {
|
||||
fn from(rgb: embedded_graphics::pixelcolor::Rgb888) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
if rgb == RgbColor::BLACK {
|
||||
TriColor::Black
|
||||
} else if rgb == RgbColor::WHITE {
|
||||
TriColor::White
|
||||
} else {
|
||||
// there is no good approximation here since we don't know which color is 'chromatic'
|
||||
TriColor::Chromatic
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "graphics")]
|
||||
impl From<TriColor> for embedded_graphics::pixelcolor::Rgb888 {
|
||||
fn from(tri_color: TriColor) -> Self {
|
||||
use embedded_graphics::pixelcolor::RgbColor;
|
||||
match tri_color {
|
||||
TriColor::Black => embedded_graphics::pixelcolor::Rgb888::BLACK,
|
||||
TriColor::White => embedded_graphics::pixelcolor::Rgb888::WHITE,
|
||||
// assume chromatic is red
|
||||
TriColor::Chromatic => embedded_graphics::pixelcolor::Rgb888::new(255, 0, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_u8() {
|
||||
assert_eq!(Color::Black, Color::from(0u8));
|
||||
assert_eq!(Color::White, Color::from(1u8));
|
||||
}
|
||||
|
||||
// test all values aside from 0 and 1 which all should panic
|
||||
#[test]
|
||||
fn from_u8_panic() {
|
||||
for val in 2..=u8::MAX {
|
||||
extern crate std;
|
||||
let result = std::panic::catch_unwind(|| Color::from(val));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u8_conversion_black() {
|
||||
assert_eq!(Color::from(Color::Black.get_bit_value()), Color::Black);
|
||||
assert_eq!(Color::from(0u8).get_bit_value(), 0u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn u8_conversion_white() {
|
||||
assert_eq!(Color::from(Color::White.get_bit_value()), Color::White);
|
||||
assert_eq!(Color::from(1u8).get_bit_value(), 1u8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_oct() {
|
||||
let left = OctColor::Red;
|
||||
let right = OctColor::Green;
|
||||
assert_eq!(
|
||||
OctColor::split_byte(OctColor::colors_byte(left, right)),
|
||||
Ok((left, right))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tricolor_bitmask() {
|
||||
assert_eq!(
|
||||
TriColor::Black.bitmask(false, 0),
|
||||
(0b01111111, u16::from_le_bytes([0b00000000, 0b00000000]))
|
||||
);
|
||||
assert_eq!(
|
||||
TriColor::White.bitmask(false, 0),
|
||||
(0b01111111, u16::from_le_bytes([0b10000000, 0b00000000]))
|
||||
);
|
||||
assert_eq!(
|
||||
TriColor::Chromatic.bitmask(false, 0),
|
||||
(0b01111111, u16::from_le_bytes([0b10000000, 0b10000000]))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
TriColor::Black.bitmask(true, 0),
|
||||
(0b01111111, u16::from_le_bytes([0b00000000, 0b00000000]))
|
||||
);
|
||||
assert_eq!(
|
||||
TriColor::White.bitmask(true, 0),
|
||||
(0b01111111, u16::from_le_bytes([0b10000000, 0b00000000]))
|
||||
);
|
||||
assert_eq!(
|
||||
TriColor::Chromatic.bitmask(true, 0),
|
||||
(0b01111111, u16::from_le_bytes([0b00000000, 0b10000000]))
|
||||
);
|
||||
}
|
||||
}
|
||||
169
src/display.rs
169
src/display.rs
@@ -2,8 +2,7 @@ use embedded_graphics::{
|
||||
framebuffer::Framebuffer,
|
||||
mono_font::{MonoTextStyle, ascii::FONT_8X13},
|
||||
pixelcolor::{
|
||||
Gray8,
|
||||
raw::{LittleEndian, RawU8},
|
||||
raw::{BigEndian},
|
||||
},
|
||||
prelude::*,
|
||||
primitives::{PrimitiveStyle, Rectangle, StyledDrawable},
|
||||
@@ -13,8 +12,8 @@ use embedded_graphics::{
|
||||
use image::RgbImage;
|
||||
use image::{GrayImage};
|
||||
|
||||
|
||||
|
||||
use embedded_graphics::pixelcolor::BinaryColor;
|
||||
use embedded_graphics::pixelcolor::raw::RawU1;
|
||||
use crate::killinfo::KillInfo;
|
||||
|
||||
const WIDTH: usize = 800;
|
||||
@@ -23,7 +22,7 @@ const HEIGHT: usize = 480;
|
||||
pub struct Display {
|
||||
width: usize,
|
||||
height: usize,
|
||||
buffer: Framebuffer<Gray8, RawU8, LittleEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>,
|
||||
buffer: Framebuffer<BinaryColor, RawU1, BigEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>,
|
||||
}
|
||||
|
||||
impl Display {
|
||||
@@ -32,44 +31,54 @@ impl Display {
|
||||
width : WIDTH,
|
||||
height : HEIGHT,
|
||||
buffer:
|
||||
Framebuffer::<Gray8, RawU8, LittleEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>::new(),
|
||||
Framebuffer::<BinaryColor, RawU1, BigEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>::new(),
|
||||
}
|
||||
}
|
||||
pub fn clear(&mut self, on: bool) {
|
||||
let color = if on {
|
||||
BinaryColor::On
|
||||
} else {
|
||||
BinaryColor::Off
|
||||
};
|
||||
|
||||
self.buffer.clear(color).ok();
|
||||
}
|
||||
|
||||
pub fn draw_rect(&mut self, x: i32, y: i32, w: u32, h: u32, color: u8) {
|
||||
let rect = Rectangle::new(Point::new(x, y), Size::new(w, h));
|
||||
let style = PrimitiveStyle::with_fill(Gray8::new(color));
|
||||
let bw = if color > 127 { BinaryColor::On } else { BinaryColor::Off };
|
||||
let style = PrimitiveStyle::with_fill(bw);
|
||||
rect.draw_styled(&style, &mut self.buffer).ok();
|
||||
}
|
||||
|
||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: u8) {
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::new(color));
|
||||
let bw = if color > 127 { BinaryColor::On } else { BinaryColor::Off };
|
||||
let style = MonoTextStyle::new(&FONT_8X13, bw);
|
||||
Text::new(text, Point::new(x, y), style)
|
||||
.draw(&mut self.buffer)
|
||||
.ok();
|
||||
}
|
||||
|
||||
|
||||
pub fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(alliance) = &kill.victim.alliance {
|
||||
let gray_vec = rgb_to_gray_vec(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&gray_vec, alliance.logo.width() as u32);
|
||||
let bw_vec = rgb_to_bw_dithered(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, alliance.logo.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y)).draw(&mut self.buffer).ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
|
||||
let gray_vec = rgb_to_gray_vec(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Gray8>::new(&gray_vec, ship.icon.width() as u32);
|
||||
let bw_vec = rgb_to_bw_dithered(&ship.icon);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y)).draw(&mut self.buffer).ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, Gray8::new(255));
|
||||
let style = MonoTextStyle::new(&FONT_8X13, BinaryColor::Off );
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
@@ -79,7 +88,16 @@ impl Display {
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
let char_line = format!("{} [{}]", kill.victim.character.name, alliance_short);
|
||||
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style).draw(&mut self.buffer).ok();
|
||||
current_y += 16;
|
||||
|
||||
@@ -94,23 +112,130 @@ impl Display {
|
||||
}
|
||||
|
||||
|
||||
pub fn flush(&self) {
|
||||
|
||||
pub fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(character) = &kill.victim.character {
|
||||
let bw_vec = rgb_to_bw_dithered(&character.portrait);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, character.portrait.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y)).draw(&mut self.buffer).ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
|
||||
let bw_vec = rgb_to_bw_dithered(&ship.icon);
|
||||
let logo_raw = ImageRaw::<BinaryColor>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y)).draw(&mut self.buffer).ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyle::new(&FONT_8X13, BinaryColor::Off );
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style).draw(&mut self.buffer).ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill.victim.ship.as_ref().map(|s| s.name.as_str()).unwrap_or("Unknown");
|
||||
let value_line = format!("{} - {:.2}M ISK", ship_name, kill.total_value / 1_000_000f64);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style).draw(&mut self.buffer).ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style).draw(&mut self.buffer).ok();
|
||||
}
|
||||
|
||||
|
||||
pub fn flush(&self) {
|
||||
let buf = self.buffer.data();
|
||||
let img = GrayImage::from_raw(self.width as u32, self.height as u32, buf.iter().map(|p| *p).collect()).unwrap();
|
||||
let img_data: Vec<u8> = buf
|
||||
.iter()
|
||||
.flat_map(|byte| (0..8).map(move |i| if (byte >> (7-i)) & 1 == 1 { 255 } else { 0 }))
|
||||
.take(self.width * self.height)
|
||||
.collect();
|
||||
|
||||
let img = GrayImage::from_raw(self.width as u32, self.height as u32, img_data).unwrap();
|
||||
img.save("output.png").unwrap();
|
||||
println!("Saved framebuffer to {:?}", "output.png");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn rgb_to_bw_dithered(rgb: &RgbImage) -> Vec<u8> {
|
||||
let w = rgb.width() as usize;
|
||||
let h = rgb.height() as usize;
|
||||
|
||||
|
||||
fn rgb_to_gray_vec(rgb: &RgbImage) -> Vec<u8> {
|
||||
rgb.pixels()
|
||||
// Convert to grayscale f32 buffer
|
||||
let mut gray: Vec<f32> = rgb
|
||||
.pixels()
|
||||
.map(|p| {
|
||||
let [r, g, b] = p.0;
|
||||
(0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32) as u8
|
||||
0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
|
||||
let mut bits = Vec::new();
|
||||
let mut byte = 0u8;
|
||||
let mut bit_idx = 7;
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let idx = y * w + x;
|
||||
let old = gray[idx];
|
||||
let new = if old >= 128.0 { 255.0 } else { 0.0 };
|
||||
let error = old - new;
|
||||
|
||||
// Quantize to B/W
|
||||
let bw = new > 127.0;
|
||||
if bw {
|
||||
byte |= 1 << bit_idx;
|
||||
}
|
||||
bit_idx -= 1;
|
||||
if bit_idx == -1 {
|
||||
bits.push(byte);
|
||||
byte = 0;
|
||||
bit_idx = 7;
|
||||
}
|
||||
|
||||
// Distribute error (Floyd–Steinberg)
|
||||
if x + 1 < w {
|
||||
gray[idx + 1] += error * 7.0 / 16.0;
|
||||
}
|
||||
if y + 1 < h {
|
||||
if x > 0 {
|
||||
gray[idx + w - 1] += error * 3.0 / 16.0;
|
||||
}
|
||||
gray[idx + w] += error * 5.0 / 16.0;
|
||||
if x + 1 < w {
|
||||
gray[idx + w + 1] += error * 1.0 / 16.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bit_idx > 0 {
|
||||
bits.push(byte);
|
||||
}
|
||||
|
||||
bits
|
||||
}
|
||||
274
src/epaper.rs
Normal file
274
src/epaper.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use embedded_graphics::{
|
||||
image::{Image, ImageRaw}, mono_font::{MonoTextStyleBuilder},prelude::*, text::{Baseline, Text, TextStyleBuilder}
|
||||
};
|
||||
|
||||
use crate::epd7in5_v2::Epd7in5;
|
||||
use crate::epd7in5_v2::Display7in5;
|
||||
use crate::graphics::Display;
|
||||
use linux_embedded_hal::{
|
||||
spidev::{self, SpidevOptions}, Delay, SPIError, SpidevDevice
|
||||
};
|
||||
|
||||
use crate::traits::{*};
|
||||
use crate::color::Color;
|
||||
use embedded_hal::digital::OutputPin;
|
||||
use embedded_hal::digital::PinState;
|
||||
|
||||
|
||||
use crate::killinfo::KillInfo;
|
||||
use image::RgbImage;
|
||||
|
||||
|
||||
pub struct EPaper {
|
||||
spi : SpidevDevice,
|
||||
epd: Epd7in5<SpidevDevice, gpiocdev_embedded_hal::InputPin, gpiocdev_embedded_hal::OutputPin, gpiocdev_embedded_hal::OutputPin, Delay>,
|
||||
display: Display<800, 480, false, 48000 , Color>,
|
||||
delay :Delay,
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl EPaper {
|
||||
pub fn new() -> Result<EPaper, SPIError> {
|
||||
|
||||
let mut spi = SpidevDevice::open("/dev/spidev0.0").expect("spidev directory");
|
||||
let options = SpidevOptions::new()
|
||||
.bits_per_word(8)
|
||||
.max_speed_hz(10_000_000)
|
||||
.mode(spidev::SpiModeFlags::SPI_MODE_0)
|
||||
.build();
|
||||
spi.configure(&options).expect("spi configuration");
|
||||
|
||||
// setup display pins
|
||||
let mut cs = gpiocdev_embedded_hal::OutputPin::new("/dev/gpiochip0", 26, PinState::High).unwrap();
|
||||
cs.set_high().unwrap();
|
||||
let busy = gpiocdev_embedded_hal::InputPin::new("/dev/gpiochip0", 24).unwrap();
|
||||
let dc = gpiocdev_embedded_hal::OutputPin::new("/dev/gpiochip0", 25, PinState::High).unwrap();
|
||||
let rst = gpiocdev_embedded_hal::OutputPin::new("/dev/gpiochip0", 17, PinState::High).unwrap();
|
||||
|
||||
let mut delay = Delay {};
|
||||
println!("START");
|
||||
let epd7in5 = Epd7in5::new(&mut spi, busy, dc, rst, &mut delay, None).expect("epd new");
|
||||
let display = Display7in5::default();
|
||||
println!("Device successfully initialized!");
|
||||
|
||||
return Ok(EPaper { spi, epd: epd7in5, display, delay : Delay {}});
|
||||
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Result<(), SPIError> {
|
||||
self.epd.update_and_display_frame(&mut self.spi, self.display.buffer(), &mut self.delay)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn sleep(&mut self) -> Result<(), SPIError> {
|
||||
self.epd.sleep(&mut self.spi, &mut self.delay)?;
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
pub fn clear(&mut self, on: bool) {
|
||||
let color = if on {
|
||||
Color::Black
|
||||
} else {
|
||||
Color::White
|
||||
};
|
||||
|
||||
self.display.clear(color).ok();
|
||||
}
|
||||
|
||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str) {
|
||||
let style = MonoTextStyleBuilder::new()
|
||||
.font(&embedded_graphics::mono_font::ascii::FONT_8X13)
|
||||
.text_color(Color::White)
|
||||
.background_color(Color::Black)
|
||||
.build();
|
||||
|
||||
let text_style = TextStyleBuilder::new().baseline(Baseline::Top).build();
|
||||
|
||||
let _ = Text::with_text_style(text, Point::new(x, y), style, text_style).draw(&mut self.display);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(alliance) = &kill.victim.alliance {
|
||||
let bw_vec = rgb_to_bw_dithered(&alliance.logo);
|
||||
let logo_raw = ImageRaw::<Color>::new(bw_vec.as_slice(), alliance.logo.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y)).draw(&mut self.display).ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
|
||||
let bw_vec = rgb_to_bw_dithered(&ship.icon);
|
||||
let width = ship.icon.width() as u32;
|
||||
let raw_image: ImageRaw<'_, Color> = ImageRaw::<Color>::new(&bw_vec, width);
|
||||
Image::new(&raw_image, Point::new(x + 64 + 4, current_y)).draw(&mut self.display).ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyleBuilder::new()
|
||||
.font(&embedded_graphics::mono_font::ascii::FONT_8X13)
|
||||
.text_color(Color::White)
|
||||
.background_color(Color::Black)
|
||||
.build();
|
||||
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style).draw(&mut self.display).ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill.victim.ship.as_ref().map(|s| s.name.as_str()).unwrap_or("Unknown");
|
||||
let value_line = format!("{} - {:.2}M ISK", ship_name, kill.total_value / 1_000_000f64);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style).draw(&mut self.display).ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style).draw(&mut self.display).ok();
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn draw_loss_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
|
||||
let mut current_y = y;
|
||||
|
||||
// Draw Alliance logo (64x64) if present
|
||||
if let Some(character) = &kill.victim.character {
|
||||
let bw_vec = rgb_to_bw_dithered(&character.portrait);
|
||||
let logo_raw = ImageRaw::<Color>::new(&bw_vec, character.portrait.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x, y)).draw(&mut self.display).ok();
|
||||
}
|
||||
|
||||
// Draw Ship icon (64x64) if present
|
||||
if let Some(ship) = &kill.victim.ship {
|
||||
|
||||
let bw_vec = rgb_to_bw_dithered(&ship.icon);
|
||||
let logo_raw = ImageRaw::<Color>::new(&bw_vec, ship.icon.width() as u32);
|
||||
Image::new(&logo_raw, Point::new(x + 64 + 4, current_y)).draw(&mut self.display).ok();
|
||||
}
|
||||
|
||||
let text_x = x + 64 + 64 + 8;
|
||||
let style = MonoTextStyleBuilder::new()
|
||||
.font(&embedded_graphics::mono_font::ascii::FONT_8X13)
|
||||
.text_color(Color::Black)
|
||||
.background_color(Color::White)
|
||||
.build();
|
||||
current_y += 8;
|
||||
|
||||
// Character name + Alliance short
|
||||
let alliance_short = kill
|
||||
.victim
|
||||
.alliance
|
||||
.as_ref()
|
||||
.map(|a| a.short.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
|
||||
let char_name = kill
|
||||
.victim
|
||||
.character
|
||||
.as_ref()
|
||||
.map(|a| a.name.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
let char_line = format!("{} [{}]", char_name, alliance_short);
|
||||
Text::new(&char_line, Point::new(text_x, current_y), style).draw(&mut self.display).ok();
|
||||
current_y += 16;
|
||||
|
||||
// Ship name + total value
|
||||
let ship_name = kill.victim.ship.as_ref().map(|s| s.name.as_str()).unwrap_or("Unknown");
|
||||
let value_line = format!("{} - {:.2}M ISK", ship_name, kill.total_value / 1_000_000f64);
|
||||
Text::new(&value_line, Point::new(text_x, current_y), style).draw(&mut self.display).ok();
|
||||
current_y += 16;
|
||||
|
||||
// System name
|
||||
Text::new(&kill.system_name, Point::new(text_x, current_y), style).draw(&mut self.display).ok();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
fn rgb_to_bw_dithered(rgb: &RgbImage) -> Vec<u8> {
|
||||
let w = rgb.width() as usize;
|
||||
let h = rgb.height() as usize;
|
||||
|
||||
// Convert to grayscale f32 buffer
|
||||
let mut gray: Vec<f32> = rgb
|
||||
.pixels()
|
||||
.map(|p| {
|
||||
let [r, g, b] = p.0;
|
||||
0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut bits = Vec::new();
|
||||
let mut byte = 0u8;
|
||||
let mut bit_idx = 7;
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let idx = y * w + x;
|
||||
let old = gray[idx];
|
||||
let new = if old >= 128.0 { 255.0 } else { 0.0 };
|
||||
let error = old - new;
|
||||
|
||||
// Quantize to B/W
|
||||
let bw = new > 127.0;
|
||||
if bw {
|
||||
byte |= 1 << bit_idx;
|
||||
}
|
||||
bit_idx -= 1;
|
||||
if bit_idx == -1 {
|
||||
bits.push(byte);
|
||||
byte = 0;
|
||||
bit_idx = 7;
|
||||
}
|
||||
|
||||
// Distribute error (Floyd–Steinberg)
|
||||
if x + 1 < w {
|
||||
gray[idx + 1] += error * 7.0 / 16.0;
|
||||
}
|
||||
if y + 1 < h {
|
||||
if x > 0 {
|
||||
gray[idx + w - 1] += error * 3.0 / 16.0;
|
||||
}
|
||||
gray[idx + w] += error * 5.0 / 16.0;
|
||||
if x + 1 < w {
|
||||
gray[idx + w + 1] += error * 1.0 / 16.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bit_idx > 0 {
|
||||
bits.push(byte);
|
||||
}
|
||||
|
||||
bits
|
||||
}
|
||||
153
src/epd7in5_v2/command.rs
Normal file
153
src/epd7in5_v2/command.rs
Normal file
@@ -0,0 +1,153 @@
|
||||
//! SPI Commands for the Waveshare 7.5" E-Ink Display
|
||||
|
||||
use crate::traits;
|
||||
|
||||
/// Epd7in5 commands
|
||||
///
|
||||
/// Should rarely (never?) be needed directly.
|
||||
///
|
||||
/// For more infos about the addresses and what they are doing look into the PDFs.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum Command {
|
||||
/// Set Resolution, LUT selection, BWR pixels, gate scan direction, source shift
|
||||
/// direction, booster switch, soft reset.
|
||||
PanelSetting = 0x00,
|
||||
|
||||
/// Selecting internal and external power
|
||||
PowerSetting = 0x01,
|
||||
|
||||
/// After the Power Off command, the driver will power off following the Power Off
|
||||
/// Sequence; BUSY signal will become "0". This command will turn off charge pump,
|
||||
/// T-con, source driver, gate driver, VCOM, and temperature sensor, but register
|
||||
/// data will be kept until VDD becomes OFF. Source Driver output and Vcom will remain
|
||||
/// as previous condition, which may have 2 conditions: 0V or floating.
|
||||
PowerOff = 0x02,
|
||||
|
||||
/// Setting Power OFF sequence
|
||||
PowerOffSequenceSetting = 0x03,
|
||||
|
||||
/// Turning On the Power
|
||||
///
|
||||
/// After the Power ON command, the driver will power on following the Power ON
|
||||
/// sequence. Once complete, the BUSY signal will become "1".
|
||||
PowerOn = 0x04,
|
||||
|
||||
/// Starting data transmission
|
||||
BoosterSoftStart = 0x06,
|
||||
|
||||
/// This command makes the chip enter the deep-sleep mode to save power.
|
||||
///
|
||||
/// The deep sleep mode would return to stand-by by hardware reset.
|
||||
///
|
||||
/// The only one parameter is a check code, the command would be excuted if check code = 0xA5.
|
||||
DeepSleep = 0x07,
|
||||
|
||||
/// This command starts transmitting data and write them into SRAM. To complete data
|
||||
/// transmission, command DSP (Data Stop) must be issued. Then the chip will start to
|
||||
/// send data/VCOM for panel.
|
||||
///
|
||||
/// BLACK/WHITE or OLD_DATA
|
||||
DataStartTransmission1 = 0x10,
|
||||
|
||||
/// To stop data transmission, this command must be issued to check the `data_flag`.
|
||||
///
|
||||
/// After this command, BUSY signal will become "0" until the display update is
|
||||
/// finished.
|
||||
DataStop = 0x11,
|
||||
|
||||
/// After this command is issued, driver will refresh display (data/VCOM) according to
|
||||
/// SRAM data and LUT.
|
||||
///
|
||||
/// After Display Refresh command, BUSY signal will become "0" until the display
|
||||
/// update is finished.
|
||||
DisplayRefresh = 0x12,
|
||||
|
||||
/// RED or NEW_DATA
|
||||
DataStartTransmission2 = 0x13,
|
||||
|
||||
/// Dual SPI - what for?
|
||||
DualSpi = 0x15,
|
||||
|
||||
/// This command builds the VCOM Look-Up Table (LUTC).
|
||||
LutForVcom = 0x20,
|
||||
/// This command builds the Black Look-Up Table (LUTB).
|
||||
LutBlack = 0x21,
|
||||
/// This command builds the White Look-Up Table (LUTW).
|
||||
LutWhite = 0x22,
|
||||
/// This command builds the Gray1 Look-Up Table (LUTG1).
|
||||
LutGray1 = 0x23,
|
||||
/// This command builds the Gray2 Look-Up Table (LUTG2).
|
||||
LutGray2 = 0x24,
|
||||
/// This command builds the Red0 Look-Up Table (LUTR0).
|
||||
LutRed0 = 0x25,
|
||||
/// This command builds the Red1 Look-Up Table (LUTR1).
|
||||
LutRed1 = 0x26,
|
||||
/// This command builds the Red2 Look-Up Table (LUTR2).
|
||||
LutRed2 = 0x27,
|
||||
/// This command builds the Red3 Look-Up Table (LUTR3).
|
||||
LutRed3 = 0x28,
|
||||
/// This command builds the XON Look-Up Table (LUTXON).
|
||||
LutXon = 0x29,
|
||||
|
||||
/// The command controls the PLL clock frequency.
|
||||
PllControl = 0x30,
|
||||
|
||||
/// This command reads the temperature sensed by the temperature sensor.
|
||||
TemperatureSensor = 0x40,
|
||||
/// This command selects the Internal or External temperature sensor.
|
||||
TemperatureCalibration = 0x41,
|
||||
/// This command could write data to the external temperature sensor.
|
||||
TemperatureSensorWrite = 0x42,
|
||||
/// This command could read data from the external temperature sensor.
|
||||
TemperatureSensorRead = 0x43,
|
||||
|
||||
/// This command indicates the interval of Vcom and data output. When setting the
|
||||
/// vertical back porch, the total blanking will be kept (20 Hsync).
|
||||
VcomAndDataIntervalSetting = 0x50,
|
||||
/// This command indicates the input power condition. Host can read this flag to learn
|
||||
/// the battery condition.
|
||||
LowPowerDetection = 0x51,
|
||||
|
||||
/// This command defines non-overlap period of Gate and Source.
|
||||
TconSetting = 0x60,
|
||||
/// This command defines alternative resolution and this setting is of higher priority
|
||||
/// than the RES\[1:0\] in R00H (PSR).
|
||||
TconResolution = 0x61,
|
||||
/// This command defines MCU host direct access external memory mode.
|
||||
SpiFlashControl = 0x65,
|
||||
|
||||
/// The LUT_REV / Chip Revision is read from OTP address = 25001 and 25000.
|
||||
Revision = 0x70,
|
||||
/// This command reads the IC status.
|
||||
GetStatus = 0x71,
|
||||
|
||||
/// This command implements related VCOM sensing setting.
|
||||
AutoMeasurementVcom = 0x80,
|
||||
/// This command gets the VCOM value.
|
||||
ReadVcomValue = 0x81,
|
||||
/// This command sets `VCOM_DC` value.
|
||||
VcmDcSetting = 0x82,
|
||||
// /// This is in all the Waveshare controllers for Epd7in5, but it's not documented
|
||||
// /// anywhere in the datasheet `¯\_(ツ)_/¯`
|
||||
// FlashMode = 0xE5,
|
||||
}
|
||||
|
||||
impl traits::Command for Command {
|
||||
/// Returns the address of the command
|
||||
fn address(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::traits::Command as CommandTrait;
|
||||
|
||||
#[test]
|
||||
fn command_addr() {
|
||||
assert_eq!(Command::PanelSetting.address(), 0x00);
|
||||
assert_eq!(Command::DisplayRefresh.address(), 0x12);
|
||||
}
|
||||
}
|
||||
260
src/epd7in5_v2/mod.rs
Normal file
260
src/epd7in5_v2/mod.rs
Normal file
@@ -0,0 +1,260 @@
|
||||
//! A simple Driver for the Waveshare 7.5" E-Ink Display (V2) via SPI
|
||||
//!
|
||||
//! # References
|
||||
//!
|
||||
//! - [Datasheet](https://www.waveshare.com/wiki/7.5inch_e-Paper_HAT)
|
||||
//! - [Waveshare C driver](https://github.com/waveshare/e-Paper/blob/702def0/RaspberryPi%26JetsonNano/c/lib/e-Paper/EPD_7in5_V2.c)
|
||||
//! - [Waveshare Python driver](https://github.com/waveshare/e-Paper/blob/702def0/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd7in5_V2.py)
|
||||
//!
|
||||
//! Important note for V2:
|
||||
//! Revision V2 has been released on 2019.11, the resolution is upgraded to 800×480, from 640×384 of V1.
|
||||
//! The hardware and interface of V2 are compatible with V1, however, the related software should be updated.
|
||||
|
||||
use embedded_hal::{
|
||||
delay::DelayNs,
|
||||
digital::{InputPin, OutputPin},
|
||||
spi::SpiDevice,
|
||||
};
|
||||
|
||||
use crate::color::Color;
|
||||
use crate::interface::DisplayInterface;
|
||||
use crate::traits::{InternalWiAdditions, RefreshLut, WaveshareDisplay};
|
||||
|
||||
pub(crate) mod command;
|
||||
use self::command::Command;
|
||||
use crate::buffer_len;
|
||||
|
||||
/// Full size buffer for use with the 7in5 v2 EPD
|
||||
#[cfg(feature = "graphics")]
|
||||
pub type Display7in5 = crate::graphics::Display<
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
false,
|
||||
{ buffer_len(WIDTH as usize, HEIGHT as usize) },
|
||||
Color,
|
||||
>;
|
||||
|
||||
/// Width of the display
|
||||
pub const WIDTH: u32 = 800;
|
||||
/// Height of the display
|
||||
pub const HEIGHT: u32 = 480;
|
||||
/// Default Background Color
|
||||
pub const DEFAULT_BACKGROUND_COLOR: Color = Color::White;
|
||||
const IS_BUSY_LOW: bool = true;
|
||||
const SINGLE_BYTE_WRITE: bool = false;
|
||||
|
||||
/// Epd7in5 (V2) driver
|
||||
///
|
||||
pub struct Epd7in5<SPI, BUSY, DC, RST, DELAY> {
|
||||
/// Connection Interface
|
||||
interface: DisplayInterface<SPI, BUSY, DC, RST, DELAY, SINGLE_BYTE_WRITE>,
|
||||
/// Background Color
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl<SPI, BUSY, DC, RST, DELAY> InternalWiAdditions<SPI, BUSY, DC, RST, DELAY>
|
||||
for Epd7in5<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> {
|
||||
// Reset the device
|
||||
self.interface.reset(delay, 10_000, 2_000);
|
||||
|
||||
// V2 procedure as described here:
|
||||
// https://github.com/waveshare/e-Paper/blob/master/RaspberryPi%26JetsonNano/python/lib/waveshare_epd/epd7in5bc_V2.py
|
||||
// and as per specs:
|
||||
// https://www.waveshare.com/w/upload/6/60/7.5inch_e-Paper_V2_Specification.pdf
|
||||
|
||||
self.cmd_with_data(spi, Command::PowerSetting, &[0x07, 0x07, 0x3f, 0x3f])?;
|
||||
self.cmd_with_data(spi, Command::BoosterSoftStart, &[0x17, 0x17, 0x28, 0x17])?;
|
||||
self.command(spi, Command::PowerOn)?;
|
||||
delay.delay_ms(100);
|
||||
self.wait_until_idle(spi, delay)?;
|
||||
self.cmd_with_data(spi, Command::PanelSetting, &[0x1F])?;
|
||||
self.cmd_with_data(spi, Command::TconResolution, &[0x03, 0x20, 0x01, 0xE0])?;
|
||||
self.cmd_with_data(spi, Command::DualSpi, &[0x00])?;
|
||||
self.cmd_with_data(spi, Command::VcomAndDataIntervalSetting, &[0x10, 0x07])?;
|
||||
self.cmd_with_data(spi, Command::TconSetting, &[0x22])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<SPI, BUSY, DC, RST, DELAY> WaveshareDisplay<SPI, BUSY, DC, RST, DELAY>
|
||||
for Epd7in5<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
type DisplayColor = Color;
|
||||
fn new(
|
||||
spi: &mut SPI,
|
||||
busy: BUSY,
|
||||
dc: DC,
|
||||
rst: RST,
|
||||
delay: &mut DELAY,
|
||||
delay_us: Option<u32>,
|
||||
) -> Result<Self, SPI::Error> {
|
||||
let interface = DisplayInterface::new(busy, dc, rst, delay_us);
|
||||
let color = DEFAULT_BACKGROUND_COLOR;
|
||||
|
||||
let mut epd = Epd7in5 { interface, color };
|
||||
|
||||
epd.init(spi, delay)?;
|
||||
|
||||
Ok(epd)
|
||||
}
|
||||
|
||||
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> {
|
||||
self.init(spi, delay)
|
||||
}
|
||||
|
||||
fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> {
|
||||
self.wait_until_idle(spi, delay)?;
|
||||
self.command(spi, Command::PowerOff)?;
|
||||
self.wait_until_idle(spi, delay)?;
|
||||
self.cmd_with_data(spi, Command::DeepSleep, &[0xA5])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.wait_until_idle(spi, delay)?;
|
||||
self.cmd_with_data(spi, Command::DataStartTransmission2, buffer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_partial_frame(
|
||||
&mut self,
|
||||
_spi: &mut SPI,
|
||||
_delay: &mut DELAY,
|
||||
_buffer: &[u8],
|
||||
_x: u32,
|
||||
_y: u32,
|
||||
_width: u32,
|
||||
_height: u32,
|
||||
) -> Result<(), SPI::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> {
|
||||
self.wait_until_idle(spi, delay)?;
|
||||
self.command(spi, Command::DisplayRefresh)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_and_display_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.update_frame(spi, buffer, delay)?;
|
||||
self.command(spi, Command::DisplayRefresh)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> {
|
||||
self.wait_until_idle(spi, delay)?;
|
||||
self.send_resolution(spi)?;
|
||||
|
||||
self.command(spi, Command::DataStartTransmission1)?;
|
||||
self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?;
|
||||
|
||||
self.command(spi, Command::DataStartTransmission2)?;
|
||||
self.interface.data_x_times(spi, 0x00, WIDTH / 8 * HEIGHT)?;
|
||||
|
||||
self.command(spi, Command::DisplayRefresh)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_background_color(&mut self, color: Color) {
|
||||
self.color = color;
|
||||
}
|
||||
|
||||
fn background_color(&self) -> &Color {
|
||||
&self.color
|
||||
}
|
||||
|
||||
fn width(&self) -> u32 {
|
||||
WIDTH
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
HEIGHT
|
||||
}
|
||||
|
||||
fn set_lut(
|
||||
&mut self,
|
||||
_spi: &mut SPI,
|
||||
_delay: &mut DELAY,
|
||||
_refresh_rate: Option<RefreshLut>,
|
||||
) -> Result<(), SPI::Error> {
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
fn wait_until_idle(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error> {
|
||||
self.interface
|
||||
.wait_until_idle_with_cmd(spi, delay, IS_BUSY_LOW, Command::GetStatus)
|
||||
}
|
||||
}
|
||||
|
||||
impl<SPI, BUSY, DC, RST, DELAY> Epd7in5<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
fn command(&mut self, spi: &mut SPI, command: Command) -> Result<(), SPI::Error> {
|
||||
self.interface.cmd(spi, command)
|
||||
}
|
||||
|
||||
fn send_data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> {
|
||||
self.interface.data(spi, data)
|
||||
}
|
||||
|
||||
fn cmd_with_data(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
command: Command,
|
||||
data: &[u8],
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.interface.cmd_with_data(spi, command, data)
|
||||
}
|
||||
|
||||
fn send_resolution(&mut self, spi: &mut SPI) -> Result<(), SPI::Error> {
|
||||
let w = self.width();
|
||||
let h = self.height();
|
||||
|
||||
self.command(spi, Command::TconResolution)?;
|
||||
self.send_data(spi, &[(w >> 8) as u8])?;
|
||||
self.send_data(spi, &[w as u8])?;
|
||||
self.send_data(spi, &[(h >> 8) as u8])?;
|
||||
self.send_data(spi, &[h as u8])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn epd_size() {
|
||||
assert_eq!(WIDTH, 800);
|
||||
assert_eq!(HEIGHT, 480);
|
||||
assert_eq!(DEFAULT_BACKGROUND_COLOR, Color::White);
|
||||
}
|
||||
}
|
||||
487
src/graphics.rs
Normal file
487
src/graphics.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
//! Graphics Support for EPDs
|
||||
|
||||
use crate::color::{ColorType, TriColor};
|
||||
use core::marker::PhantomData;
|
||||
use embedded_graphics::prelude::*;
|
||||
|
||||
/// Display rotation, only 90° increments supported
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub enum DisplayRotation {
|
||||
/// No rotation
|
||||
#[default]
|
||||
Rotate0,
|
||||
/// Rotate by 90 degrees clockwise
|
||||
Rotate90,
|
||||
/// Rotate by 180 degrees clockwise
|
||||
Rotate180,
|
||||
/// Rotate 270 degrees clockwise
|
||||
Rotate270,
|
||||
}
|
||||
|
||||
/// count the number of bytes per line knowing that it may contains padding bits
|
||||
const fn line_bytes(width: u32, bits_per_pixel: usize) -> usize {
|
||||
// round to upper 8 bit count
|
||||
(width as usize * bits_per_pixel + 7) / 8
|
||||
}
|
||||
|
||||
/// Display buffer used for drawing with embedded graphics
|
||||
/// This can be rendered on EPD using ...
|
||||
///
|
||||
/// - WIDTH: width in pixel when display is not rotated
|
||||
/// - HEIGHT: height in pixel when display is not rotated
|
||||
/// - BWRBIT: mandatory value of the B/W when chromatic bit is set, can be any value for non
|
||||
/// tricolor epd
|
||||
/// - COLOR: color type used by the target display
|
||||
/// - BYTECOUNT: This is redundant with previous data and should be removed when const generic
|
||||
/// expressions are stabilized
|
||||
///
|
||||
/// More on BWRBIT:
|
||||
///
|
||||
/// Different chromatic displays differently treat the bits in chromatic color planes.
|
||||
/// Some of them ([crate::epd2in13bc]) will render a color pixel if bit is set for that pixel,
|
||||
/// which is a `BWRBIT = true` mode.
|
||||
///
|
||||
/// Other displays, like [crate::epd5in83b_v2] in opposite, will draw color pixel if bit is
|
||||
/// cleared for that pixel, which is a `BWRBIT = false` mode.
|
||||
///
|
||||
/// BWRBIT=true: chromatic doesn't override white, white bit cleared for black, white bit set for white, both bits set for chromatic
|
||||
/// BWRBIT=false: chromatic does override white, both bits cleared for black, white bit set for white, red bit set for black
|
||||
pub struct Display<
|
||||
const WIDTH: u32,
|
||||
const HEIGHT: u32,
|
||||
const BWRBIT: bool,
|
||||
const BYTECOUNT: usize,
|
||||
COLOR: ColorType + PixelColor,
|
||||
> {
|
||||
buffer: [u8; BYTECOUNT],
|
||||
rotation: DisplayRotation,
|
||||
_color: PhantomData<COLOR>,
|
||||
}
|
||||
|
||||
impl<
|
||||
const WIDTH: u32,
|
||||
const HEIGHT: u32,
|
||||
const BWRBIT: bool,
|
||||
const BYTECOUNT: usize,
|
||||
COLOR: ColorType + PixelColor,
|
||||
> Default for Display<WIDTH, HEIGHT, BWRBIT, BYTECOUNT, COLOR>
|
||||
{
|
||||
/// Initialize display with the color '0', which may not be the same on all device.
|
||||
/// Many devices have a bit parameter polarity that should be changed if this is not the right
|
||||
/// one.
|
||||
/// However, every device driver should implement a DEFAULT_COLOR constant to indicate which
|
||||
/// color this represents (TODO)
|
||||
///
|
||||
/// If you want a specific default color, you can still call clear() to set one.
|
||||
// inline is necessary here to allow heap allocation via Box on stack limited programs
|
||||
#[inline(always)]
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// default color must be 0 for every bit in a pixel to make this work everywere
|
||||
buffer: [0u8; BYTECOUNT],
|
||||
rotation: DisplayRotation::default(),
|
||||
_color: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// For use with embedded_grahics
|
||||
impl<
|
||||
const WIDTH: u32,
|
||||
const HEIGHT: u32,
|
||||
const BWRBIT: bool,
|
||||
const BYTECOUNT: usize,
|
||||
COLOR: ColorType + PixelColor,
|
||||
> DrawTarget for Display<WIDTH, HEIGHT, BWRBIT, BYTECOUNT, COLOR>
|
||||
{
|
||||
type Color = COLOR;
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||
where
|
||||
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||
{
|
||||
for pixel in pixels {
|
||||
self.set_pixel(pixel);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// For use with embedded_grahics
|
||||
impl<
|
||||
const WIDTH: u32,
|
||||
const HEIGHT: u32,
|
||||
const BWRBIT: bool,
|
||||
const BYTECOUNT: usize,
|
||||
COLOR: ColorType + PixelColor,
|
||||
> OriginDimensions for Display<WIDTH, HEIGHT, BWRBIT, BYTECOUNT, COLOR>
|
||||
{
|
||||
fn size(&self) -> Size {
|
||||
match self.rotation {
|
||||
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => Size::new(WIDTH, HEIGHT),
|
||||
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => Size::new(HEIGHT, WIDTH),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
const WIDTH: u32,
|
||||
const HEIGHT: u32,
|
||||
const BWRBIT: bool,
|
||||
const BYTECOUNT: usize,
|
||||
COLOR: ColorType + PixelColor,
|
||||
> Display<WIDTH, HEIGHT, BWRBIT, BYTECOUNT, COLOR>
|
||||
{
|
||||
/// get internal buffer to use it (to draw in epd)
|
||||
pub fn buffer(&self) -> &[u8] {
|
||||
&self.buffer
|
||||
}
|
||||
|
||||
/// Set the display rotation.
|
||||
///
|
||||
/// This only concerns future drawing made to it. Anything aready drawn
|
||||
/// stays as it is in the buffer.
|
||||
pub fn set_rotation(&mut self, rotation: DisplayRotation) {
|
||||
self.rotation = rotation;
|
||||
}
|
||||
|
||||
/// Get current rotation
|
||||
pub fn rotation(&self) -> DisplayRotation {
|
||||
self.rotation
|
||||
}
|
||||
|
||||
/// Set a specific pixel color on this display
|
||||
pub fn set_pixel(&mut self, pixel: Pixel<COLOR>) {
|
||||
set_pixel(
|
||||
&mut self.buffer,
|
||||
WIDTH,
|
||||
HEIGHT,
|
||||
self.rotation,
|
||||
BWRBIT,
|
||||
pixel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Some Tricolor specifics
|
||||
impl<const WIDTH: u32, const HEIGHT: u32, const BWRBIT: bool, const BYTECOUNT: usize>
|
||||
Display<WIDTH, HEIGHT, BWRBIT, BYTECOUNT, TriColor>
|
||||
{
|
||||
/// get black/white internal buffer to use it (to draw in epd)
|
||||
pub fn bw_buffer(&self) -> &[u8] {
|
||||
&self.buffer[..self.buffer.len() / 2]
|
||||
}
|
||||
|
||||
/// get chromatic internal buffer to use it (to draw in epd)
|
||||
pub fn chromatic_buffer(&self) -> &[u8] {
|
||||
&self.buffer[self.buffer.len() / 2..]
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `Display`, except that its characteristics are defined at runtime.
|
||||
/// See display for documentation as everything is the same except that default
|
||||
/// is replaced by a `new` method.
|
||||
pub struct VarDisplay<'a, COLOR: ColorType + PixelColor> {
|
||||
width: u32,
|
||||
height: u32,
|
||||
bwrbit: bool,
|
||||
buffer: &'a mut [u8],
|
||||
rotation: DisplayRotation,
|
||||
_color: PhantomData<COLOR>,
|
||||
}
|
||||
|
||||
/// For use with embedded_grahics
|
||||
impl<COLOR: ColorType + PixelColor> DrawTarget for VarDisplay<'_, COLOR> {
|
||||
type Color = COLOR;
|
||||
type Error = core::convert::Infallible;
|
||||
|
||||
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
|
||||
where
|
||||
I: IntoIterator<Item = Pixel<Self::Color>>,
|
||||
{
|
||||
for pixel in pixels {
|
||||
self.set_pixel(pixel);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// For use with embedded_grahics
|
||||
impl<COLOR: ColorType + PixelColor> OriginDimensions for VarDisplay<'_, COLOR> {
|
||||
fn size(&self) -> Size {
|
||||
match self.rotation {
|
||||
DisplayRotation::Rotate0 | DisplayRotation::Rotate180 => {
|
||||
Size::new(self.width, self.height)
|
||||
}
|
||||
DisplayRotation::Rotate90 | DisplayRotation::Rotate270 => {
|
||||
Size::new(self.height, self.width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error found during usage of VarDisplay
|
||||
#[derive(Debug)]
|
||||
pub enum VarDisplayError {
|
||||
/// The provided buffer was too small
|
||||
BufferTooSmall,
|
||||
}
|
||||
|
||||
impl<'a, COLOR: ColorType + PixelColor> VarDisplay<'a, COLOR> {
|
||||
/// You must allocate the buffer by yourself, it must be large enough to contain all pixels.
|
||||
///
|
||||
/// Parameters are documented in `Display` as they are the same as the const generics there.
|
||||
/// bwrbit should be false for non tricolor displays
|
||||
pub fn new(
|
||||
width: u32,
|
||||
height: u32,
|
||||
buffer: &'a mut [u8],
|
||||
bwrbit: bool,
|
||||
) -> Result<Self, VarDisplayError> {
|
||||
let myself = Self {
|
||||
width,
|
||||
height,
|
||||
bwrbit,
|
||||
buffer,
|
||||
rotation: DisplayRotation::default(),
|
||||
_color: PhantomData,
|
||||
};
|
||||
// enfore some constraints dynamicly
|
||||
if myself.buffer_size() > myself.buffer.len() {
|
||||
return Err(VarDisplayError::BufferTooSmall);
|
||||
}
|
||||
Ok(myself)
|
||||
}
|
||||
|
||||
/// get the number of used bytes in the buffer
|
||||
fn buffer_size(&self) -> usize {
|
||||
self.height as usize
|
||||
* line_bytes(
|
||||
self.width,
|
||||
COLOR::BITS_PER_PIXEL_PER_BUFFER * COLOR::BUFFER_COUNT,
|
||||
)
|
||||
}
|
||||
|
||||
/// get internal buffer to use it (to draw in epd)
|
||||
pub fn buffer(&self) -> &[u8] {
|
||||
&self.buffer[..self.buffer_size()]
|
||||
}
|
||||
|
||||
/// Set the display rotation.
|
||||
///
|
||||
/// This only concerns future drawing made to it. Anything aready drawn
|
||||
/// stays as it is in the buffer.
|
||||
pub fn set_rotation(&mut self, rotation: DisplayRotation) {
|
||||
self.rotation = rotation;
|
||||
}
|
||||
|
||||
/// Get current rotation
|
||||
pub fn rotation(&self) -> DisplayRotation {
|
||||
self.rotation
|
||||
}
|
||||
|
||||
/// Set a specific pixel color on this display
|
||||
pub fn set_pixel(&mut self, pixel: Pixel<COLOR>) {
|
||||
let size = self.buffer_size();
|
||||
set_pixel(
|
||||
&mut self.buffer[..size],
|
||||
self.width,
|
||||
self.height,
|
||||
self.rotation,
|
||||
self.bwrbit,
|
||||
pixel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Some Tricolor specifics
|
||||
impl VarDisplay<'_, TriColor> {
|
||||
/// get black/white internal buffer to use it (to draw in epd)
|
||||
pub fn bw_buffer(&self) -> &[u8] {
|
||||
&self.buffer[..self.buffer_size() / 2]
|
||||
}
|
||||
|
||||
/// get chromatic internal buffer to use it (to draw in epd)
|
||||
pub fn chromatic_buffer(&self) -> &[u8] {
|
||||
&self.buffer[self.buffer_size() / 2..self.buffer_size()]
|
||||
}
|
||||
}
|
||||
|
||||
// This is a function to share code between `Display` and `VarDisplay`
|
||||
// It sets a specific pixel in a buffer to a given color.
|
||||
// The big number of parameters is due to the fact that it is an internal function to both
|
||||
// strctures.
|
||||
fn set_pixel<COLOR: ColorType + PixelColor>(
|
||||
buffer: &mut [u8],
|
||||
width: u32,
|
||||
height: u32,
|
||||
rotation: DisplayRotation,
|
||||
bwrbit: bool,
|
||||
pixel: Pixel<COLOR>,
|
||||
) {
|
||||
let Pixel(point, color) = pixel;
|
||||
|
||||
// final coordinates
|
||||
let (x, y) = match rotation {
|
||||
// as i32 = never use more than 2 billion pixel per line or per column
|
||||
DisplayRotation::Rotate0 => (point.x, point.y),
|
||||
DisplayRotation::Rotate90 => (width as i32 - 1 - point.y, point.x),
|
||||
DisplayRotation::Rotate180 => (width as i32 - 1 - point.x, height as i32 - 1 - point.y),
|
||||
DisplayRotation::Rotate270 => (point.y, height as i32 - 1 - point.x),
|
||||
};
|
||||
|
||||
// Out of range check
|
||||
if (x < 0) || (x >= width as i32) || (y < 0) || (y >= height as i32) {
|
||||
// don't do anything in case of out of range
|
||||
return;
|
||||
}
|
||||
|
||||
let index = x as usize * COLOR::BITS_PER_PIXEL_PER_BUFFER / 8
|
||||
+ y as usize * line_bytes(width, COLOR::BITS_PER_PIXEL_PER_BUFFER);
|
||||
let (mask, bits) = color.bitmask(bwrbit, x as u32);
|
||||
|
||||
if COLOR::BUFFER_COUNT == 2 {
|
||||
// split buffer is for tricolor displays that use 2 buffer for 2 bits per pixel
|
||||
buffer[index] = buffer[index] & mask | (bits & 0xFF) as u8;
|
||||
let index = index + buffer.len() / 2;
|
||||
buffer[index] = buffer[index] & mask | (bits >> 8) as u8;
|
||||
} else {
|
||||
buffer[index] = buffer[index] & mask | bits as u8;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::color::*;
|
||||
use embedded_graphics::{
|
||||
primitives::{Line, PrimitiveStyle},
|
||||
};
|
||||
|
||||
// test buffer length
|
||||
#[test]
|
||||
fn graphics_size() {
|
||||
// example definition taken from epd1in54
|
||||
let display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default();
|
||||
assert_eq!(display.buffer().len(), 5000);
|
||||
}
|
||||
|
||||
// test default background color on all bytes
|
||||
#[test]
|
||||
fn graphics_default() {
|
||||
let display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default();
|
||||
for &byte in display.buffer() {
|
||||
assert_eq!(byte, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_rotation_0() {
|
||||
let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default();
|
||||
let _ = Line::new(Point::new(0, 0), Point::new(7, 0))
|
||||
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
.draw(&mut display);
|
||||
|
||||
let buffer = display.buffer();
|
||||
|
||||
assert_eq!(buffer[0], Color::Black.get_byte_value());
|
||||
|
||||
for &byte in buffer.iter().skip(1) {
|
||||
assert_eq!(byte, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_rotation_90() {
|
||||
let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default();
|
||||
display.set_rotation(DisplayRotation::Rotate90);
|
||||
let _ = Line::new(Point::new(0, 192), Point::new(0, 199))
|
||||
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
.draw(&mut display);
|
||||
|
||||
let buffer = display.buffer();
|
||||
|
||||
assert_eq!(buffer[0], Color::Black.get_byte_value());
|
||||
|
||||
for &byte in buffer.iter().skip(1) {
|
||||
assert_eq!(byte, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_rotation_180() {
|
||||
let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default();
|
||||
display.set_rotation(DisplayRotation::Rotate180);
|
||||
let _ = Line::new(Point::new(192, 199), Point::new(199, 199))
|
||||
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
.draw(&mut display);
|
||||
|
||||
let buffer = display.buffer();
|
||||
|
||||
extern crate std;
|
||||
std::println!("{:?}", buffer);
|
||||
|
||||
assert_eq!(buffer[0], Color::Black.get_byte_value());
|
||||
|
||||
for &byte in buffer.iter().skip(1) {
|
||||
assert_eq!(byte, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_rotation_270() {
|
||||
let mut display = Display::<200, 200, false, { 200 * 200 / 8 }, Color>::default();
|
||||
display.set_rotation(DisplayRotation::Rotate270);
|
||||
let _ = Line::new(Point::new(199, 0), Point::new(199, 7))
|
||||
.into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
.draw(&mut display);
|
||||
|
||||
let buffer = display.buffer();
|
||||
|
||||
extern crate std;
|
||||
std::println!("{:?}", buffer);
|
||||
|
||||
assert_eq!(buffer[0], Color::Black.get_byte_value());
|
||||
|
||||
for &byte in buffer.iter().skip(1) {
|
||||
assert_eq!(byte, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_set_pixel_tricolor_false() {
|
||||
let mut display = Display::<4, 4, false, { 4 * 4 * 2 / 8 }, TriColor>::default();
|
||||
display.set_pixel(Pixel(Point::new(0, 0), TriColor::White));
|
||||
display.set_pixel(Pixel(Point::new(1, 0), TriColor::Chromatic));
|
||||
display.set_pixel(Pixel(Point::new(2, 0), TriColor::Black));
|
||||
|
||||
let bw_buffer = display.bw_buffer();
|
||||
let chromatic_buffer = display.chromatic_buffer();
|
||||
|
||||
extern crate std;
|
||||
std::println!("{:?}", bw_buffer);
|
||||
std::println!("{:?}", chromatic_buffer);
|
||||
|
||||
assert_eq!(bw_buffer, [192, 0]);
|
||||
assert_eq!(chromatic_buffer, [64, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graphics_set_pixel_tricolor_true() {
|
||||
let mut display = Display::<4, 4, true, { 4 * 4 * 2 / 8 }, TriColor>::default();
|
||||
display.set_pixel(Pixel(Point::new(0, 0), TriColor::White));
|
||||
display.set_pixel(Pixel(Point::new(1, 0), TriColor::Chromatic));
|
||||
display.set_pixel(Pixel(Point::new(2, 0), TriColor::Black));
|
||||
|
||||
let bw_buffer = display.bw_buffer();
|
||||
let chromatic_buffer = display.chromatic_buffer();
|
||||
|
||||
extern crate std;
|
||||
std::println!("{:?}", bw_buffer);
|
||||
std::println!("{:?}", chromatic_buffer);
|
||||
|
||||
assert_eq!(bw_buffer, [128, 0]);
|
||||
assert_eq!(chromatic_buffer, [64, 0]);
|
||||
}
|
||||
}
|
||||
208
src/interface.rs
Normal file
208
src/interface.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use crate::traits::Command;
|
||||
use core::marker::PhantomData;
|
||||
use embedded_hal::{delay::*, digital::*, spi::SpiDevice};
|
||||
|
||||
/// The Connection Interface of all (?) Waveshare EPD-Devices
|
||||
///
|
||||
/// SINGLE_BYTE_WRITE defines if a data block is written bytewise
|
||||
/// or blockwise to the spi device
|
||||
pub(crate) struct DisplayInterface<SPI, BUSY, DC, RST, DELAY, const SINGLE_BYTE_WRITE: bool> {
|
||||
/// SPI
|
||||
_spi: PhantomData<SPI>,
|
||||
/// DELAY
|
||||
_delay: PhantomData<DELAY>,
|
||||
/// Low for busy, Wait until display is ready!
|
||||
busy: BUSY,
|
||||
/// Data/Command Control Pin (High for data, Low for command)
|
||||
dc: DC,
|
||||
/// Pin for Resetting
|
||||
rst: RST,
|
||||
/// number of ms the idle loop should sleep on
|
||||
delay_us: u32,
|
||||
}
|
||||
|
||||
impl<SPI, BUSY, DC, RST, DELAY, const SINGLE_BYTE_WRITE: bool>
|
||||
DisplayInterface<SPI, BUSY, DC, RST, DELAY, SINGLE_BYTE_WRITE>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
/// Creates a new `DisplayInterface` struct
|
||||
///
|
||||
/// If no delay is given, a default delay of 10ms is used.
|
||||
pub fn new(busy: BUSY, dc: DC, rst: RST, delay_us: Option<u32>) -> Self {
|
||||
// default delay of 10ms
|
||||
let delay_us = delay_us.unwrap_or(10_000);
|
||||
DisplayInterface {
|
||||
_spi: PhantomData,
|
||||
_delay: PhantomData,
|
||||
busy,
|
||||
dc,
|
||||
rst,
|
||||
delay_us,
|
||||
}
|
||||
}
|
||||
|
||||
/// Basic function for sending [Commands](Command).
|
||||
///
|
||||
/// Enables direct interaction with the device with the help of [data()](DisplayInterface::data())
|
||||
pub(crate) fn cmd<T: Command>(&mut self, spi: &mut SPI, command: T) -> Result<(), SPI::Error> {
|
||||
// low for commands
|
||||
let _ = self.dc.set_low();
|
||||
|
||||
// Transfer the command over spi
|
||||
self.write(spi, &[command.address()])
|
||||
}
|
||||
|
||||
/// Basic function for sending an array of u8-values of data over spi
|
||||
///
|
||||
/// Enables direct interaction with the device with the help of [command()](Epd4in2::command())
|
||||
pub(crate) fn data(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> {
|
||||
// high for data
|
||||
let _ = self.dc.set_high();
|
||||
|
||||
if SINGLE_BYTE_WRITE {
|
||||
for val in data.iter().copied() {
|
||||
// Transfer data one u8 at a time over spi
|
||||
self.write(spi, &[val])?;
|
||||
}
|
||||
} else {
|
||||
self.write(spi, data)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Basic function for sending [Commands](Command) and the data belonging to it.
|
||||
///
|
||||
/// TODO: directly use ::write? cs wouldn't needed to be changed twice than
|
||||
pub(crate) fn cmd_with_data<T: Command>(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
command: T,
|
||||
data: &[u8],
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.cmd(spi, command)?;
|
||||
self.data(spi, data)
|
||||
}
|
||||
|
||||
/// Basic function for sending the same byte of data (one u8) multiple times over spi
|
||||
///
|
||||
/// Enables direct interaction with the device with the help of [command()](ConnectionInterface::command())
|
||||
pub(crate) fn data_x_times(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
val: u8,
|
||||
repetitions: u32,
|
||||
) -> Result<(), SPI::Error> {
|
||||
// high for data
|
||||
let _ = self.dc.set_high();
|
||||
// Transfer data (u8) over spi
|
||||
for _ in 0..repetitions {
|
||||
self.write(spi, &[val])?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// spi write helper/abstraction function
|
||||
fn write(&mut self, spi: &mut SPI, data: &[u8]) -> Result<(), SPI::Error> {
|
||||
// transfer spi data
|
||||
// Be careful!! Linux has a default limit of 4096 bytes per spi transfer
|
||||
// see https://raspberrypi.stackexchange.com/questions/65595/spi-transfer-fails-with-buffer-size-greater-than-4096
|
||||
if cfg!(target_os = "linux") {
|
||||
for data_chunk in data.chunks(4096) {
|
||||
spi.write(data_chunk)?;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
spi.write(data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits until device isn't busy anymore (busy == HIGH)
|
||||
///
|
||||
/// This is normally handled by the more complicated commands themselves,
|
||||
/// but in the case you send data and commands directly you might need to check
|
||||
/// if the device is still busy
|
||||
///
|
||||
/// is_busy_low
|
||||
///
|
||||
/// - TRUE for epd4in2, epd2in13, epd2in7, epd5in83, epd7in5
|
||||
/// - FALSE for epd2in9, epd1in54 (for all Display Type A ones?)
|
||||
///
|
||||
/// Most likely there was a mistake with the 2in9 busy connection
|
||||
pub(crate) fn _wait_until_idle(&mut self, delay: &mut DELAY, is_busy_low: bool) {
|
||||
while self.is_busy(is_busy_low) {
|
||||
// This has been removed and added many time :
|
||||
// - it is faster to not have it
|
||||
// - it is complicated to pass the delay everywhere all the time
|
||||
// - busy waiting can consume more power that delaying
|
||||
// - delay waiting enables task switching on realtime OS
|
||||
// -> keep it and leave the decision to the user
|
||||
if self.delay_us > 0 {
|
||||
delay.delay_us(self.delay_us);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as `wait_until_idle` for device needing a command to probe Busy pin
|
||||
pub(crate) fn wait_until_idle_with_cmd<T: Command>(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
is_busy_low: bool,
|
||||
status_command: T,
|
||||
) -> Result<(), SPI::Error> {
|
||||
self.cmd(spi, status_command)?;
|
||||
if self.delay_us > 0 {
|
||||
delay.delay_us(self.delay_us);
|
||||
}
|
||||
while self.is_busy(is_busy_low) {
|
||||
self.cmd(spi, status_command)?;
|
||||
if self.delay_us > 0 {
|
||||
delay.delay_us(self.delay_us);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks if device is still busy
|
||||
///
|
||||
/// This is normally handled by the more complicated commands themselves,
|
||||
/// but in the case you send data and commands directly you might need to check
|
||||
/// if the device is still busy
|
||||
///
|
||||
/// is_busy_low
|
||||
///
|
||||
/// - TRUE for epd4in2, epd2in13, epd2in7, epd5in83, epd7in5
|
||||
/// - FALSE for epd2in9, epd1in54 (for all Display Type A ones?)
|
||||
///
|
||||
/// Most likely there was a mistake with the 2in9 busy connection
|
||||
/// //TODO: use the #cfg feature to make this compile the right way for the certain types
|
||||
pub(crate) fn is_busy(&mut self, is_busy_low: bool) -> bool {
|
||||
(is_busy_low && self.busy.is_low().unwrap_or(false))
|
||||
|| (!is_busy_low && self.busy.is_high().unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Resets the device.
|
||||
///
|
||||
/// Often used to awake the module from deep sleep. See [Epd4in2::sleep()](Epd4in2::sleep())
|
||||
///
|
||||
/// The timing of keeping the reset pin low seems to be important and different per device.
|
||||
/// Most displays seem to require keeping it low for 10ms, but the 7in5_v2 only seems to reset
|
||||
/// properly with 2ms
|
||||
pub(crate) fn reset(&mut self, delay: &mut DELAY, initial_delay: u32, duration: u32) {
|
||||
let _ = self.rst.set_high();
|
||||
delay.delay_us(initial_delay);
|
||||
|
||||
let _ = self.rst.set_low();
|
||||
delay.delay_us(duration);
|
||||
let _ = self.rst.set_high();
|
||||
//TODO: the upstream libraries always sleep for 200ms here
|
||||
// 10ms works fine with just for the 7in5_v2 but this needs to be validated for other devices
|
||||
delay.delay_us(200_000);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ use crate::services::esi_static::{EsiClient, EsiError};
|
||||
use crate::services::zkill::{ZkillClient, ZkillError};
|
||||
|
||||
pub struct Individual {
|
||||
pub character: Character,
|
||||
pub character: Option<Character>,
|
||||
pub alliance: Option<Alliance>,
|
||||
pub corporation: Corporation,
|
||||
pub ship: Option<Ship>,
|
||||
@@ -111,7 +111,13 @@ impl Individual {
|
||||
alli_id: u32,
|
||||
ship_id: u32,
|
||||
) -> Result<Individual, EsiError> {
|
||||
let character = client.get_character(char_id)?;
|
||||
|
||||
let character = if char_id != 0 {
|
||||
client.get_character(char_id).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let corporation = client.get_corporation(corp_id)?;
|
||||
|
||||
// Only try fetching alliance if alli_id != 0, else None
|
||||
|
||||
116
src/lib.rs
116
src/lib.rs
@@ -1,3 +1,119 @@
|
||||
//! A simple Driver for the [Waveshare](https://github.com/waveshare/e-Paper) E-Ink Displays via SPI
|
||||
//!
|
||||
//! - Built using [`embedded-hal`] traits.
|
||||
//! - Graphics support is added through [`embedded-graphics`]
|
||||
//!
|
||||
//! [`embedded-graphics`]: https://docs.rs/embedded-graphics/
|
||||
//! [`embedded-hal`]: https://docs.rs/embedded-hal
|
||||
//!
|
||||
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//!```rust, no_run
|
||||
//!# use embedded_hal_mock::eh1::*;
|
||||
//!# fn main() -> Result<(), embedded_hal::spi::ErrorKind> {
|
||||
//!use embedded_graphics::{
|
||||
//! pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle},
|
||||
//!};
|
||||
//!use epd_waveshare::{epd1in54::*, prelude::*};
|
||||
//!#
|
||||
//!# let expectations = [];
|
||||
//!# let mut spi = spi::Mock::new(&expectations);
|
||||
//!# let expectations = [];
|
||||
//!# let cs_pin = digital::Mock::new(&expectations);
|
||||
//!# let busy_in = digital::Mock::new(&expectations);
|
||||
//!# let dc = digital::Mock::new(&expectations);
|
||||
//!# let rst = digital::Mock::new(&expectations);
|
||||
//!# let mut delay = delay::NoopDelay::new();
|
||||
//!
|
||||
//!// Setup EPD
|
||||
//!let mut epd = Epd1in54::new(&mut spi, busy_in, dc, rst, &mut delay, None)?;
|
||||
//!
|
||||
//!// Use display graphics from embedded-graphics
|
||||
//!let mut display = Display1in54::default();
|
||||
//!
|
||||
//!// Use embedded graphics for drawing a line
|
||||
//!
|
||||
//!let _ = Line::new(Point::new(0, 120), Point::new(0, 295))
|
||||
//! .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
//! .draw(&mut display);
|
||||
//!
|
||||
//! // Display updated frame
|
||||
//!epd.update_frame(&mut spi, &display.buffer(), &mut delay)?;
|
||||
//!epd.display_frame(&mut spi, &mut delay)?;
|
||||
//!
|
||||
//!// Set the EPD to sleep
|
||||
//!epd.sleep(&mut spi, &mut delay)?;
|
||||
//!# Ok(())
|
||||
//!# }
|
||||
//!```
|
||||
//!
|
||||
//! # Other information and requirements
|
||||
//!
|
||||
//! - Buffersize: Wherever a buffer is used it always needs to be of the size: `width / 8 * length`,
|
||||
//! where width and length being either the full e-ink size or the partial update window size
|
||||
//!
|
||||
//! ### SPI
|
||||
//!
|
||||
//! MISO is not connected/available. SPI_MODE_0 is used (CPHL = 0, CPOL = 0) with 8 bits per word, MSB first.
|
||||
//!
|
||||
//! Maximum speed tested by myself was 8Mhz but more should be possible (Ben Krasnow used 18Mhz with his implemenation)
|
||||
//!
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
pub mod graphics;
|
||||
|
||||
pub mod traits;
|
||||
|
||||
pub mod color;
|
||||
|
||||
pub mod rect;
|
||||
|
||||
/// Interface for the physical connection between display and the controlling device
|
||||
pub mod interface;
|
||||
|
||||
pub mod epd7in5_v2;
|
||||
|
||||
|
||||
pub mod killinfo;
|
||||
pub mod model;
|
||||
pub mod services;
|
||||
|
||||
|
||||
pub mod type_a;
|
||||
|
||||
/// Includes everything important besides the chosen Display
|
||||
pub mod prelude {
|
||||
pub use crate::color::{Color, OctColor, TriColor};
|
||||
pub use crate::traits::{
|
||||
QuickRefresh, RefreshLut, WaveshareDisplay, WaveshareThreeColorDisplay,
|
||||
};
|
||||
|
||||
pub use crate::SPI_MODE;
|
||||
|
||||
#[cfg(feature = "graphics")]
|
||||
pub use crate::graphics::{Display, DisplayRotation};
|
||||
}
|
||||
|
||||
/// Computes the needed buffer length. Takes care of rounding up in case width
|
||||
/// is not divisible by 8.
|
||||
///
|
||||
/// unused
|
||||
/// bits width
|
||||
/// <----><------------------------>
|
||||
/// \[XXXXX210\]\[76543210\]...\[76543210\] ^
|
||||
/// \[XXXXX210\]\[76543210\]...\[76543210\] | height
|
||||
/// \[XXXXX210\]\[76543210\]...\[76543210\] v
|
||||
pub const fn buffer_len(width: usize, height: usize) -> usize {
|
||||
(width + 7) / 8 * height
|
||||
}
|
||||
|
||||
use embedded_hal::spi::{Mode, Phase, Polarity};
|
||||
|
||||
/// SPI mode -
|
||||
/// For more infos see [Requirements: SPI](index.html#spi)
|
||||
pub const SPI_MODE: Mode = Mode {
|
||||
phase: Phase::CaptureOnFirstTransition,
|
||||
polarity: Polarity::IdleLow,
|
||||
};
|
||||
|
||||
90
src/main.rs
90
src/main.rs
@@ -1,73 +1,37 @@
|
||||
use embedded_graphics::{
|
||||
mono_font::{MonoTextStyle, ascii::FONT_6X10},
|
||||
pixelcolor::Gray8,
|
||||
};
|
||||
use image::{GrayImage, Pixel, RgbImage};
|
||||
pub mod graphics;
|
||||
|
||||
pub mod traits;
|
||||
|
||||
pub mod color;
|
||||
|
||||
pub mod rect;
|
||||
|
||||
/// Interface for the physical connection between display and the controlling device
|
||||
pub mod interface;
|
||||
|
||||
pub mod epd7in5_v2;
|
||||
|
||||
pub mod display;
|
||||
pub mod killinfo;
|
||||
pub mod model;
|
||||
pub mod services;
|
||||
pub mod epaper;
|
||||
|
||||
pub mod type_a;
|
||||
use crate::killinfo::KillInfo;
|
||||
use crate::services::esi_static::EsiClient;
|
||||
use crate::services::zkill::ZkillClient;
|
||||
|
||||
fn rgb_to_gray8(img: &RgbImage) -> GrayImage {
|
||||
let mut gray = GrayImage::new(img.width(), img.height());
|
||||
for (x, y, pixel) in img.enumerate_pixels() {
|
||||
let luma = pixel.to_luma()[0]; // simple conversion
|
||||
gray.put_pixel(x, y, image::Luma([luma]));
|
||||
}
|
||||
gray
|
||||
|
||||
pub const fn buffer_len(width: usize, height: usize) -> usize {
|
||||
(width + 7) / 8 * height
|
||||
}
|
||||
|
||||
// Example function to render kill info on a display buffer
|
||||
fn render_killmails(killmails: &[KillInfo], buffer: &mut GrayImage, x_off: i64) {
|
||||
let mut y_offset = 0;
|
||||
|
||||
for k in killmails {
|
||||
// Draw victim portrait
|
||||
|
||||
if let Some(alli) = &k.victim.alliance {
|
||||
let logo = &alli.logo;
|
||||
let gray_logo = rgb_to_gray8(logo);
|
||||
// copy portrait into buffer at (0, y_offset)
|
||||
image::imageops::overlay(buffer, &gray_logo, x_off + 0, y_offset as i64);
|
||||
}
|
||||
|
||||
if let Some(ship) = &k.victim.ship {
|
||||
let icon = &ship.icon;
|
||||
let gray_icon = rgb_to_gray8(icon);
|
||||
// copy portrait into buffer at (0, y_offset)
|
||||
image::imageops::overlay(buffer, &gray_icon, x_off + 64, y_offset as i64);
|
||||
}
|
||||
|
||||
// Draw victim name
|
||||
//let text_style = MonoTextStyle::new(&FONT_6X10, Gray8::new(255));
|
||||
// You can use embedded_graphics Text or a real framebuffer draw function
|
||||
// Example with embedded_graphics (requires DrawTarget)
|
||||
// Text::new(&k.victim.character.name, Point::new(50, y_offset as i32), text_style)
|
||||
// .draw(display)?;
|
||||
|
||||
// Draw system name and value
|
||||
println!(
|
||||
"{} {:.2}M ISK in {}",
|
||||
k.victim.character.name,
|
||||
k.total_value / 1_000_000.0,
|
||||
k.system_name
|
||||
);
|
||||
|
||||
// Advance y offset
|
||||
y_offset += 60; // adjust spacing
|
||||
if y_offset >= buffer.height() as usize {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Simulated 800x480 grayscale display buffer
|
||||
let mut buffer = GrayImage::new(800, 480);
|
||||
|
||||
|
||||
let esi = EsiClient::new();
|
||||
let zkill = ZkillClient::new();
|
||||
@@ -76,6 +40,12 @@ fn main() {
|
||||
let my_corp_id = 98685373;
|
||||
|
||||
let mut display = display::Display::new();
|
||||
let mut epaper = epaper::EPaper::new().expect("DisplayError");
|
||||
|
||||
display.clear(true);
|
||||
epaper.clear(true);
|
||||
epaper.flush().expect("flush error");
|
||||
|
||||
|
||||
let response = zkill
|
||||
.get_corporation_kills(my_corp_id, past_seconds)
|
||||
@@ -95,6 +65,7 @@ fn main() {
|
||||
let y : i32 = ii * 60;
|
||||
|
||||
display.draw_kill_info(k, 0, y);
|
||||
epaper.draw_kill_info(k, 0, y);
|
||||
}
|
||||
|
||||
let response = zkill
|
||||
@@ -112,11 +83,12 @@ fn main() {
|
||||
let ii = i as i32;
|
||||
let y : i32 = ii * 60;
|
||||
|
||||
display.draw_kill_info(k, 400, y);
|
||||
display.draw_loss_info(k, 400, y);
|
||||
epaper.draw_loss_info(k, 400, y);
|
||||
}
|
||||
|
||||
display.flush();;
|
||||
display.flush();
|
||||
epaper.flush().expect("flush error");
|
||||
|
||||
|
||||
// Save buffer for debugging
|
||||
buffer.save("killmails_display.png").unwrap();
|
||||
}
|
||||
|
||||
87
src/rect.rs
Normal file
87
src/rect.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! Rectangle operations for bigger displays with multiple _windows_
|
||||
use core::cmp;
|
||||
|
||||
/// A rectangle
|
||||
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
|
||||
pub struct Rect {
|
||||
/// Origin X
|
||||
pub x: u32,
|
||||
/// Origin Y
|
||||
pub y: u32,
|
||||
/// Width
|
||||
pub w: u32,
|
||||
/// Height
|
||||
pub h: u32,
|
||||
}
|
||||
|
||||
impl Rect {
|
||||
/// Construct a new rectangle
|
||||
pub const fn new(x: u32, y: u32, w: u32, h: u32) -> Rect {
|
||||
Rect { x, y, w, h }
|
||||
}
|
||||
/// Compute intersection with another rectangle
|
||||
pub fn intersect(&self, other: Rect) -> Rect {
|
||||
let x = cmp::max(self.x, other.x);
|
||||
let y = cmp::max(self.y, other.y);
|
||||
let w = cmp::min(self.x + self.w, other.x + other.w).saturating_sub(x);
|
||||
let h = cmp::min(self.y + self.h, other.y + other.h).saturating_sub(y);
|
||||
Rect { x, y, w, h }
|
||||
}
|
||||
/// Move rectangle by (-dx,-dy)
|
||||
pub fn sub_offset(&self, dx: u32, dy: u32) -> Rect {
|
||||
Rect {
|
||||
x: self.x - dx,
|
||||
y: self.y - dy,
|
||||
w: self.w,
|
||||
h: self.h,
|
||||
}
|
||||
}
|
||||
/// Test whether the rectangle is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.w == 0 || self.h == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_intersect() {
|
||||
let r1 = Rect::new(0, 0, 10, 10);
|
||||
let r2 = Rect::new(6, 3, 10, 10);
|
||||
let r3 = r1.intersect(r2);
|
||||
assert!(matches!(
|
||||
r3,
|
||||
Rect {
|
||||
x: 6,
|
||||
y: 3,
|
||||
w: 4,
|
||||
h: 7
|
||||
}
|
||||
));
|
||||
|
||||
let r1 = Rect::new(0, 0, 10, 10);
|
||||
let r2 = Rect::new(10, 11, 10, 10);
|
||||
let r3 = r1.intersect(r2);
|
||||
assert!(matches!(
|
||||
r3,
|
||||
Rect {
|
||||
x: _,
|
||||
y: _,
|
||||
w: 0,
|
||||
h: 0
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_offset() {
|
||||
let r1 = Rect::new(10, 10, 10, 10);
|
||||
let r2 = r1.sub_offset(10, 5);
|
||||
assert!(matches!(
|
||||
r2,
|
||||
Rect {
|
||||
x: 0,
|
||||
y: 5,
|
||||
w: 10,
|
||||
h: 10
|
||||
}
|
||||
));
|
||||
}
|
||||
378
src/traits.rs
Normal file
378
src/traits.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
use core::marker::Sized;
|
||||
use embedded_hal::{delay::*, digital::*, spi::SpiDevice};
|
||||
|
||||
/// All commands need to have this trait which gives the address of the command
|
||||
/// which needs to be send via SPI with activated CommandsPin (Data/Command Pin in CommandMode)
|
||||
pub(crate) trait Command: Copy {
|
||||
fn address(self) -> u8;
|
||||
}
|
||||
|
||||
/// Seperates the different LUT for the Display Refresh process
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, Default)]
|
||||
pub enum RefreshLut {
|
||||
/// The "normal" full Lookuptable for the Refresh-Sequence
|
||||
#[default]
|
||||
Full,
|
||||
/// The quick LUT where not the full refresh sequence is followed.
|
||||
/// This might lead to some
|
||||
Quick,
|
||||
}
|
||||
|
||||
pub(crate) trait InternalWiAdditions<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
/// This initialises the EPD and powers it up
|
||||
///
|
||||
/// This function is already called from
|
||||
/// - [new()](WaveshareDisplay::new())
|
||||
/// - [`wake_up`]
|
||||
///
|
||||
///
|
||||
/// This function calls [reset](WaveshareDisplay::reset),
|
||||
/// so you don't need to call reset your self when trying to wake your device up
|
||||
/// after setting it to sleep.
|
||||
fn init(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
}
|
||||
|
||||
/// Functions to interact with three color panels
|
||||
pub trait WaveshareThreeColorDisplay<SPI, BUSY, DC, RST, DELAY>:
|
||||
WaveshareDisplay<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
/// Transmit data to the SRAM of the EPD
|
||||
///
|
||||
/// Updates both the black and the secondary color layers
|
||||
fn update_color_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
black: &[u8],
|
||||
chromatic: &[u8],
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Update only the black/white data of the display.
|
||||
///
|
||||
/// This must be finished by calling `update_chromatic_frame`.
|
||||
fn update_achromatic_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
black: &[u8],
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Update only the chromatic data of the display.
|
||||
///
|
||||
/// This should be preceded by a call to `update_achromatic_frame`.
|
||||
/// This data takes precedence over the black/white data.
|
||||
fn update_chromatic_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
chromatic: &[u8],
|
||||
) -> Result<(), SPI::Error>;
|
||||
}
|
||||
|
||||
/// All the functions to interact with the EPDs
|
||||
///
|
||||
/// This trait includes all public functions to use the EPDs
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
///```rust, no_run
|
||||
///# use embedded_hal_mock::eh1::*;
|
||||
///# fn main() -> Result<(), embedded_hal::spi::ErrorKind> {
|
||||
///use embedded_graphics::{
|
||||
/// pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle},
|
||||
///};
|
||||
///use epd_waveshare::{epd4in2::*, prelude::*};
|
||||
///#
|
||||
///# let expectations = [];
|
||||
///# let mut spi = spi::Mock::new(&expectations);
|
||||
///# let expectations = [];
|
||||
///# let cs_pin = digital::Mock::new(&expectations);
|
||||
///# let busy_in = digital::Mock::new(&expectations);
|
||||
///# let dc = digital::Mock::new(&expectations);
|
||||
///# let rst = digital::Mock::new(&expectations);
|
||||
///# let mut delay = delay::NoopDelay::new();
|
||||
///
|
||||
///// Setup EPD
|
||||
///let mut epd = Epd4in2::new(&mut spi, busy_in, dc, rst, &mut delay, None)?;
|
||||
///
|
||||
///// Use display graphics from embedded-graphics
|
||||
///let mut display = Display4in2::default();
|
||||
///
|
||||
///// Use embedded graphics for drawing a line
|
||||
///
|
||||
///let _ = Line::new(Point::new(0, 120), Point::new(0, 295))
|
||||
/// .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
/// .draw(&mut display);
|
||||
///
|
||||
/// // Display updated frame
|
||||
///epd.update_frame(&mut spi, &display.buffer(), &mut delay)?;
|
||||
///epd.display_frame(&mut spi, &mut delay)?;
|
||||
///
|
||||
///// Set the EPD to sleep
|
||||
///epd.sleep(&mut spi, &mut delay)?;
|
||||
///# Ok(())
|
||||
///# }
|
||||
///```
|
||||
///
|
||||
/// # Heap allocation
|
||||
///
|
||||
/// For systems where stack space is limited but heap space is available
|
||||
/// (i.e. ESP32 platforms with PSRAM) it's possible to allocate the display buffer
|
||||
/// in heap; the Displayxxxx:default() is implemented as always inline, so you can
|
||||
/// use Box::new to request heap space for the display buffer.
|
||||
///
|
||||
///```rust, no_run
|
||||
///# use epd_waveshare::epd4in2::Display4in2;
|
||||
///# use epd_waveshare::prelude::*;
|
||||
///# use embedded_graphics_core::prelude::*;
|
||||
///# use embedded_graphics::primitives::*;
|
||||
///let mut display = Box::new(Display4in2::default());
|
||||
///let _ = Line::new(Point::new(0, 120), Point::new(0, 295))
|
||||
/// .into_styled(PrimitiveStyle::with_stroke(Color::Black, 1))
|
||||
/// .draw(&mut *display);
|
||||
///```
|
||||
pub trait WaveshareDisplay<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
/// The Color Type used by the Display
|
||||
type DisplayColor;
|
||||
/// Creates a new driver from a SPI peripheral, CS Pin, Busy InputPin, DC
|
||||
///
|
||||
/// `delay_us` is the number of us the idle loop should sleep on.
|
||||
/// Setting it to 0 implies busy waiting.
|
||||
/// Setting it to None means a default value is used.
|
||||
///
|
||||
/// This already initialises the device.
|
||||
fn new(
|
||||
spi: &mut SPI,
|
||||
busy: BUSY,
|
||||
dc: DC,
|
||||
rst: RST,
|
||||
delay: &mut DELAY,
|
||||
delay_us: Option<u32>,
|
||||
) -> Result<Self, SPI::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Let the device enter deep-sleep mode to save power.
|
||||
///
|
||||
/// The deep sleep mode returns to standby with a hardware reset.
|
||||
fn sleep(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Wakes the device up from sleep
|
||||
///
|
||||
/// Also reintialises the device if necessary.
|
||||
fn wake_up(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Sets the backgroundcolor for various commands like [clear_frame](WaveshareDisplay::clear_frame)
|
||||
fn set_background_color(&mut self, color: Self::DisplayColor);
|
||||
|
||||
/// Get current background color
|
||||
fn background_color(&self) -> &Self::DisplayColor;
|
||||
|
||||
/// Get the width of the display
|
||||
fn width(&self) -> u32;
|
||||
|
||||
/// Get the height of the display
|
||||
fn height(&self) -> u32;
|
||||
|
||||
/// Transmit a full frame to the SRAM of the EPD
|
||||
fn update_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Transmits partial data to the SRAM of the EPD
|
||||
///
|
||||
/// (x,y) is the top left corner
|
||||
///
|
||||
/// BUFFER needs to be of size: width / 8 * height !
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_partial_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
buffer: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Displays the frame data from SRAM
|
||||
///
|
||||
/// This function waits until the device isn`t busy anymore
|
||||
fn display_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Provide a combined update&display and save some time (skipping a busy check in between)
|
||||
fn update_and_display_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Clears the frame buffer on the EPD with the declared background color
|
||||
///
|
||||
/// The background color can be changed with [`WaveshareDisplay::set_background_color`]
|
||||
fn clear_frame(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Trait for using various Waveforms from different LUTs
|
||||
/// E.g. for partial refreshes
|
||||
///
|
||||
/// A full refresh is needed after a certain amount of quick refreshes!
|
||||
///
|
||||
/// WARNING: Quick Refresh might lead to ghosting-effects/problems with your display. Especially for the 4.2in Display!
|
||||
///
|
||||
/// If None is used the old value will be loaded on the LUTs once more
|
||||
fn set_lut(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
refresh_rate: Option<RefreshLut>,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Wait until the display has stopped processing data
|
||||
///
|
||||
/// You can call this to make sure a frame is displayed before goin further
|
||||
fn wait_until_idle(&mut self, spi: &mut SPI, delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
}
|
||||
|
||||
/// Allows quick refresh support for displays that support it; lets you send both
|
||||
/// old and new frame data to support this.
|
||||
///
|
||||
/// When using the quick refresh look-up table, the display must receive separate display
|
||||
/// buffer data marked as old, and new. This is used to determine which pixels need to change,
|
||||
/// and how they will change. This isn't required when using full refreshes.
|
||||
///
|
||||
/// (todo: Example ommitted due to CI failures.)
|
||||
/// Example:
|
||||
///```rust, no_run
|
||||
///# use embedded_hal_mock::eh1::*;
|
||||
///# fn main() -> Result<(), embedded_hal::spi::ErrorKind> {
|
||||
///# use embedded_graphics::{
|
||||
///# pixelcolor::BinaryColor::On as Black, prelude::*, primitives::{Line, PrimitiveStyle},
|
||||
///# };
|
||||
///# use epd_waveshare::{epd4in2::*, prelude::*};
|
||||
///# use epd_waveshare::graphics::VarDisplay;
|
||||
///#
|
||||
///# let expectations = [];
|
||||
///# let mut spi = spi::Mock::new(&expectations);
|
||||
///# let expectations = [];
|
||||
///# let cs_pin = digital::Mock::new(&expectations);
|
||||
///# let busy_in = digital::Mock::new(&expectations);
|
||||
///# let dc = digital::Mock::new(&expectations);
|
||||
///# let rst = digital::Mock::new(&expectations);
|
||||
///# let mut delay = delay::NoopDelay::new();
|
||||
///#
|
||||
///# // Setup EPD
|
||||
///# let mut epd = Epd4in2::new(&mut spi, busy_in, dc, rst, &mut delay, None)?;
|
||||
///let (x, y, frame_width, frame_height) = (20, 40, 80,80);
|
||||
///
|
||||
///let mut buffer = [DEFAULT_BACKGROUND_COLOR.get_byte_value(); 80 / 8 * 80];
|
||||
///let mut display = VarDisplay::new(frame_width, frame_height, &mut buffer,false).unwrap();
|
||||
///
|
||||
///epd.update_partial_old_frame(&mut spi, &mut delay, display.buffer(), x, y, frame_width, frame_height)
|
||||
/// .ok();
|
||||
///
|
||||
///display.clear(Color::White).ok();
|
||||
///// Execute drawing commands here.
|
||||
///
|
||||
///epd.update_partial_new_frame(&mut spi, &mut delay, display.buffer(), x, y, frame_width, frame_height)
|
||||
/// .ok();
|
||||
///# Ok(())
|
||||
///# }
|
||||
///```
|
||||
pub trait QuickRefresh<SPI, BUSY, DC, RST, DELAY>
|
||||
where
|
||||
SPI: SpiDevice,
|
||||
BUSY: InputPin,
|
||||
DC: OutputPin,
|
||||
RST: OutputPin,
|
||||
DELAY: DelayNs,
|
||||
{
|
||||
/// Updates the old frame.
|
||||
fn update_old_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Updates the new frame.
|
||||
fn update_new_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Displays the new frame
|
||||
fn display_new_frame(&mut self, spi: &mut SPI, _delay: &mut DELAY) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Updates and displays the new frame.
|
||||
fn update_and_display_new_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
buffer: &[u8],
|
||||
delay: &mut DELAY,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Updates the old frame for a portion of the display.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_partial_old_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
buffer: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Updates the new frame for a portion of the display.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn update_partial_new_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
buffer: &[u8],
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), SPI::Error>;
|
||||
|
||||
/// Clears the partial frame buffer on the EPD with the declared background color
|
||||
/// The background color can be changed with [`WaveshareDisplay::set_background_color`]
|
||||
fn clear_partial_frame(
|
||||
&mut self,
|
||||
spi: &mut SPI,
|
||||
delay: &mut DELAY,
|
||||
x: u32,
|
||||
y: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Result<(), SPI::Error>;
|
||||
}
|
||||
103
src/type_a/command.rs
Normal file
103
src/type_a/command.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! SPI Commands for the Waveshare 2.9" and 1.54" E-Ink Display
|
||||
|
||||
use crate::traits;
|
||||
|
||||
/// Epd1in54 and EPD2IN9 commands
|
||||
///
|
||||
/// Should rarely (never?) be needed directly.
|
||||
///
|
||||
/// For more infos about the addresses and what they are doing look into the pdfs
|
||||
#[allow(dead_code)]
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum Command {
|
||||
/// Driver Output control
|
||||
/// 3 Databytes:
|
||||
/// A[7:0]
|
||||
/// 0.. A[8]
|
||||
/// 0.. B[2:0]
|
||||
/// Default: Set A[8:0] = 0x127 and B[2:0] = 0x0
|
||||
DriverOutputControl = 0x01,
|
||||
GateDrivingVoltage = 0x03,
|
||||
SourceDrivingVoltage = 0x04,
|
||||
/// Booster Soft start control
|
||||
/// 3 Databytes:
|
||||
/// 1.. A[6:0]
|
||||
/// 1.. B[6:0]
|
||||
/// 1.. C[6:0]
|
||||
/// Default: A[7:0] = 0xCF, B[7:0] = 0xCE, C[7:0] = 0x8D
|
||||
BoosterSoftStartControl = 0x0C,
|
||||
GateScanStartPosition = 0x0F,
|
||||
//TODO: useful?
|
||||
// GateScanStartPosition = 0x0F,
|
||||
/// Deep Sleep Mode Control
|
||||
/// 1 Databyte:
|
||||
/// 0.. A[0]
|
||||
/// Values:
|
||||
/// A[0] = 0: Normal Mode (POR)
|
||||
/// A[0] = 1: Enter Deep Sleep Mode
|
||||
DeepSleepMode = 0x10,
|
||||
// /// Data Entry mode setting
|
||||
DataEntryModeSetting = 0x11,
|
||||
|
||||
SwReset = 0x12,
|
||||
|
||||
TemperatureSensorSelection = 0x18,
|
||||
|
||||
TemperatureSensorControl = 0x1A,
|
||||
|
||||
MasterActivation = 0x20,
|
||||
|
||||
DisplayUpdateControl1 = 0x21,
|
||||
|
||||
DisplayUpdateControl2 = 0x22,
|
||||
|
||||
WriteRam = 0x24,
|
||||
|
||||
WriteRam2 = 0x26,
|
||||
|
||||
WriteVcomRegister = 0x2C,
|
||||
|
||||
WriteLutRegister = 0x32,
|
||||
|
||||
WriteOtpSelection = 0x37,
|
||||
|
||||
SetDummyLinePeriod = 0x3A,
|
||||
|
||||
SetGateLineWidth = 0x3B,
|
||||
|
||||
BorderWaveformControl = 0x3C,
|
||||
|
||||
WriteLutRegisterEnd = 0x3f,
|
||||
|
||||
SetRamXAddressStartEndPosition = 0x44,
|
||||
|
||||
SetRamYAddressStartEndPosition = 0x45,
|
||||
|
||||
SetRamXAddressCounter = 0x4E,
|
||||
|
||||
SetRamYAddressCounter = 0x4F,
|
||||
|
||||
Nop = 0xFF,
|
||||
}
|
||||
|
||||
impl traits::Command for Command {
|
||||
/// Returns the address of the command
|
||||
fn address(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Command;
|
||||
use crate::traits::Command as CommandTrait;
|
||||
|
||||
#[test]
|
||||
fn command_addr() {
|
||||
assert_eq!(Command::DriverOutputControl.address(), 0x01);
|
||||
|
||||
assert_eq!(Command::SetRamXAddressCounter.address(), 0x4E);
|
||||
|
||||
assert_eq!(Command::Nop.address(), 0xFF);
|
||||
}
|
||||
}
|
||||
0
src/type_a/constants.rs
Normal file
0
src/type_a/constants.rs
Normal file
2
src/type_a/mod.rs
Normal file
2
src/type_a/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
pub(crate) mod command;
|
||||
pub(crate) mod constants;
|
||||
Reference in New Issue
Block a user