include display driver,

and use rust_tls to make crosscompile easy
This commit is contained in:
2025-09-03 03:47:52 +02:00
parent a10f466538
commit b1b4356686
17 changed files with 3240 additions and 1534 deletions

1859
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,11 +2,22 @@
name = "killpaper" name = "killpaper"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
#rust-version = "1.64"
[dependencies] [dependencies]
d = "0.0.1" embedded-graphics-core = { version = "0.4", optional = true }
embedded-graphics = "0.8.1" 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"] } 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 = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143" 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
View 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]))
);
}
}

View File

@@ -2,8 +2,7 @@ use embedded_graphics::{
framebuffer::Framebuffer, framebuffer::Framebuffer,
mono_font::{MonoTextStyle, ascii::FONT_8X13}, mono_font::{MonoTextStyle, ascii::FONT_8X13},
pixelcolor::{ pixelcolor::{
Gray8, raw::{BigEndian},
raw::{LittleEndian, RawU8},
}, },
prelude::*, prelude::*,
primitives::{PrimitiveStyle, Rectangle, StyledDrawable}, primitives::{PrimitiveStyle, Rectangle, StyledDrawable},
@@ -13,8 +12,8 @@ use embedded_graphics::{
use image::RgbImage; use image::RgbImage;
use image::{GrayImage}; use image::{GrayImage};
use embedded_graphics::pixelcolor::BinaryColor;
use embedded_graphics::pixelcolor::raw::RawU1;
use crate::killinfo::KillInfo; use crate::killinfo::KillInfo;
const WIDTH: usize = 800; const WIDTH: usize = 800;
@@ -23,7 +22,7 @@ const HEIGHT: usize = 480;
pub struct Display { pub struct Display {
width: usize, width: usize,
height: usize, height: usize,
buffer: Framebuffer<Gray8, RawU8, LittleEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>, buffer: Framebuffer<BinaryColor, RawU1, BigEndian, WIDTH, HEIGHT, { WIDTH * HEIGHT }>,
} }
impl Display { impl Display {
@@ -32,44 +31,54 @@ impl Display {
width : WIDTH, width : WIDTH,
height : HEIGHT, height : HEIGHT,
buffer: 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) { 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 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(); rect.draw_styled(&style, &mut self.buffer).ok();
} }
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: u8) { 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) Text::new(text, Point::new(x, y), style)
.draw(&mut self.buffer) .draw(&mut self.buffer)
.ok(); .ok();
} }
pub fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) { pub fn draw_kill_info(&mut self, kill: &KillInfo, x: i32, y: i32) {
let mut current_y = y; let mut current_y = y;
// Draw Alliance logo (64x64) if present // Draw Alliance logo (64x64) if present
if let Some(alliance) = &kill.victim.alliance { if let Some(alliance) = &kill.victim.alliance {
let gray_vec = rgb_to_gray_vec(&alliance.logo); let bw_vec = rgb_to_bw_dithered(&alliance.logo);
let logo_raw = ImageRaw::<Gray8>::new(&gray_vec, alliance.logo.width() as u32); 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(); Image::new(&logo_raw, Point::new(x, y)).draw(&mut self.buffer).ok();
} }
// Draw Ship icon (64x64) if present // Draw Ship icon (64x64) if present
if let Some(ship) = &kill.victim.ship { if let Some(ship) = &kill.victim.ship {
let gray_vec = rgb_to_gray_vec(&ship.icon); let bw_vec = rgb_to_bw_dithered(&ship.icon);
let logo_raw = ImageRaw::<Gray8>::new(&gray_vec, ship.icon.width() as u32); 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(); Image::new(&logo_raw, Point::new(x + 64 + 4, current_y)).draw(&mut self.buffer).ok();
} }
let text_x = x + 64 + 64 + 8; 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; current_y += 8;
// Character name + Alliance short // Character name + Alliance short
@@ -79,7 +88,70 @@ impl Display {
.as_ref() .as_ref()
.map(|a| a.short.as_str()) .map(|a| a.short.as_str())
.unwrap_or(""); .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;
// 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 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(); Text::new(&char_line, Point::new(text_x, current_y), style).draw(&mut self.buffer).ok();
current_y += 16; current_y += 16;
@@ -95,22 +167,75 @@ impl Display {
pub fn flush(&self) { pub fn flush(&self) {
let buf = self.buffer.data(); 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(); img.save("output.png").unwrap();
println!("Saved framebuffer to {:?}", "output.png"); 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;
// Convert to grayscale f32 buffer
fn rgb_to_gray_vec(rgb: &RgbImage) -> Vec<u8> { let mut gray: Vec<f32> = rgb
rgb.pixels() .pixels()
.map(|p| { .map(|p| {
let [r, g, b] = p.0; 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 (FloydSteinberg)
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
View 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 (FloydSteinberg)
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
View 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
View 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
View 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
View 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);
}
}

View File

@@ -7,7 +7,7 @@ use crate::services::esi_static::{EsiClient, EsiError};
use crate::services::zkill::{ZkillClient, ZkillError}; use crate::services::zkill::{ZkillClient, ZkillError};
pub struct Individual { pub struct Individual {
pub character: Character, pub character: Option<Character>,
pub alliance: Option<Alliance>, pub alliance: Option<Alliance>,
pub corporation: Corporation, pub corporation: Corporation,
pub ship: Option<Ship>, pub ship: Option<Ship>,
@@ -111,7 +111,13 @@ impl Individual {
alli_id: u32, alli_id: u32,
ship_id: u32, ship_id: u32,
) -> Result<Individual, EsiError> { ) -> 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)?; let corporation = client.get_corporation(corp_id)?;
// Only try fetching alliance if alli_id != 0, else None // Only try fetching alliance if alli_id != 0, else None

View File

@@ -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 killinfo;
pub mod model; pub mod model;
pub mod services; 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,
};

View File

@@ -1,73 +1,37 @@
use embedded_graphics::{ pub mod graphics;
mono_font::{MonoTextStyle, ascii::FONT_6X10},
pixelcolor::Gray8, pub mod traits;
};
use image::{GrayImage, Pixel, RgbImage}; 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 display;
pub mod killinfo; pub mod killinfo;
pub mod model; pub mod model;
pub mod services; pub mod services;
pub mod epaper;
pub mod type_a;
use crate::killinfo::KillInfo; use crate::killinfo::KillInfo;
use crate::services::esi_static::EsiClient; use crate::services::esi_static::EsiClient;
use crate::services::zkill::ZkillClient; use crate::services::zkill::ZkillClient;
fn rgb_to_gray8(img: &RgbImage) -> GrayImage {
let mut gray = GrayImage::new(img.width(), img.height()); pub const fn buffer_len(width: usize, height: usize) -> usize {
for (x, y, pixel) in img.enumerate_pixels() { (width + 7) / 8 * height
let luma = pixel.to_luma()[0]; // simple conversion
gray.put_pixel(x, y, image::Luma([luma]));
}
gray
} }
// 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() { fn main() {
// Simulated 800x480 grayscale display buffer
let mut buffer = GrayImage::new(800, 480);
let esi = EsiClient::new(); let esi = EsiClient::new();
let zkill = ZkillClient::new(); let zkill = ZkillClient::new();
@@ -76,6 +40,12 @@ fn main() {
let my_corp_id = 98685373; let my_corp_id = 98685373;
let mut display = display::Display::new(); 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 let response = zkill
.get_corporation_kills(my_corp_id, past_seconds) .get_corporation_kills(my_corp_id, past_seconds)
@@ -95,6 +65,7 @@ fn main() {
let y : i32 = ii * 60; let y : i32 = ii * 60;
display.draw_kill_info(k, 0, y); display.draw_kill_info(k, 0, y);
epaper.draw_kill_info(k, 0, y);
} }
let response = zkill let response = zkill
@@ -112,11 +83,12 @@ fn main() {
let ii = i as i32; let ii = i as i32;
let y : i32 = ii * 60; 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
View 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
View 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
View 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
View File

2
src/type_a/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub(crate) mod command;
pub(crate) mod constants;