#![cfg_attr(
feature = "doc-images",
doc = ::embed_doc_image::embed_image!(
"linkage_blaze_gallery",
"docs/assets/linkage_blaze_gallery.png"
)
)]
#![cfg_attr(
feature = "doc-images",
doc = "\n[![Linkage Blaze gallery showing CYD applications][linkage_blaze_gallery]][interactive Linkage Blaze gallery]\n"
)]
mod buffer;
mod display;
mod one_spi;
mod text;
#[path = "cyd/touch.rs"]
mod touch_driver;
use core::{convert::Infallible, fmt};
use embedded_graphics::{
Pixel,
mono_font::MonoFont,
pixelcolor::{IntoStorage, Rgb565, Rgb888},
prelude::{Dimensions, DrawTarget, OriginDimensions, Point, Size},
primitives::Rectangle,
};
use embedded_hal::spi::SpiDevice;
use static_cell::StaticCell;
use buffer::DynPixelBuffer;
use buffer::{PixelBuffer, RegionView};
use device_envoy_core::button::Button;
use device_envoy_core::cyd::backend;
use device_envoy_core::cyd::{
SCREEN_PIXELS,
backend::{CalibrationConfig, RawTouchEvent, TouchUncalibrated},
display::CydFrame,
touch::TouchEvent,
};
use device_envoy_core::pixel_target::PixelTarget;
pub use display::DEFAULT_DISPLAY_SPI_HZ;
pub use device_envoy_core::cyd::{
Cyd, CydDisplay, CydTouch,
display::{Orientation, tiling},
touch,
};
pub use one_spi::CydEspOneSpi;
pub use text::DEFAULT_FONT;
use touch_driver::TOUCH_SPI_HZ;
use crate::flash_block::FlashBlockEsp;
use display::CydDisplayEsp as CydDisplayEspDevice;
use touch_driver::CydTouchEsp as CydTouchEspDevice;
pub struct CydDisplayEsp<D: SpiDevice<u8> = display::CydDisplaySpiDevice> {
display: CydDisplayEspDevice<D>,
orientation: Orientation,
pixel_buffer: &'static mut dyn DynPixelBuffer,
background_color: Rgb888,
foreground_color: Rgb888,
background565: Rgb565,
foreground565: Rgb565,
font: &'static MonoFont<'static>,
}
pub(crate) struct CydTouchUncalibratedEsp<D = touch_driver::CydTouchSpiDevice> {
touch: CydTouchEspDevice<D>,
}
pub struct CydTouchEsp<D = touch_driver::CydTouchSpiDevice> {
raw: CydTouchUncalibratedEsp<D>,
calibration_config: CalibrationConfig,
orientation: Orientation,
}
pub struct CydEsp {
pub display: CydDisplayEsp,
pub touch: CydTouchEsp,
}
pub(crate) struct CydEspUncalibrated {
pub display: CydDisplayEsp,
pub touch: CydTouchUncalibratedEsp,
}
pub struct CydStaticEsp<const PIXEL_COUNT: usize> {
pixel_buffer: StaticCell<PixelBuffer<PIXEL_COUNT>>,
}
impl<const PIXEL_COUNT: usize> CydStaticEsp<PIXEL_COUNT> {
pub(crate) const fn new() -> Self {
assert!(
PIXEL_COUNT <= SCREEN_PIXELS,
"PIXEL_COUNT must not exceed SCREEN_PIXELS"
);
Self {
pixel_buffer: StaticCell::new(),
}
}
}
pub struct CydFrameEsp<'a, D: SpiDevice<u8> = display::CydDisplaySpiDevice> {
display: &'a mut CydDisplayEspDevice<D>,
view: RegionView<'a>,
rectangle: Rectangle,
pub(crate) background565: Rgb565,
pub(crate) foreground565: Rgb565,
pub(crate) font: &'static MonoFont<'static>,
}
impl<'a, D: SpiDevice<u8>> CydFrameEsp<'a, D> {
pub fn fill(&mut self, color: Rgb565) -> &mut Self {
self.view.fill(color);
self
}
#[must_use]
pub fn width(&self) -> usize {
self.view.width()
}
#[must_use]
pub fn height(&self) -> usize {
self.view.height()
}
pub fn raw_pixels_mut(&mut self) -> &mut [u16] {
self.view.raw_pixels_mut()
}
pub fn flush(&mut self) -> Result<(), Error> {
Ok(self.display.flush_buffer(
self.view.size().width as usize,
self.view.size().height as usize,
self.view.raw_pixels(),
self.rectangle.top_left,
)?)
}
fn local_x(&self, x: i32) -> Option<usize> {
usize::try_from(x.checked_sub(self.rectangle.top_left.x)?).ok()
}
fn local_y(&self, y: i32) -> Option<usize> {
usize::try_from(y.checked_sub(self.rectangle.top_left.y)?).ok()
}
}
impl<D: SpiDevice<u8>> DrawTarget for CydFrameEsp<'_, D> {
type Color = Rgb565;
type Error = Infallible;
fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
self.fill(color);
Ok(())
}
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
for Pixel(point, color) in pixels {
let Some(local_x) = self.local_x(point.x) else {
continue;
};
let Some(local_y) = self.local_y(point.y) else {
continue;
};
if local_x < self.view.width() && local_y < self.view.height() {
let index = local_y * self.view.width() + local_x;
self.raw_pixels_mut()[index] = color.into_storage();
}
}
Ok(())
}
}
impl<D: SpiDevice<u8>> Dimensions for CydFrameEsp<'_, D> {
fn bounding_box(&self) -> Rectangle {
self.rectangle
}
}
impl<D: SpiDevice<u8>> PixelTarget for CydFrameEsp<'_, D> {
fn width(&self) -> usize {
usize::try_from(self.rectangle.top_left.x)
.expect("frame top-left x must be non-negative")
.checked_add(self.width())
.expect("frame width must fit in usize")
}
fn height(&self) -> usize {
usize::try_from(self.rectangle.top_left.y)
.expect("frame top-left y must be non-negative")
.checked_add(self.height())
.expect("frame height must fit in usize")
}
fn put_pixel(&mut self, x: usize, y: usize, color: Rgb888) {
let Some(local_x) = self.local_x(x as i32) else {
return;
};
let Some(local_y) = self.local_y(y as i32) else {
return;
};
if local_x >= self.view.width() || local_y >= self.view.height() {
return;
}
let stride = self.view.width();
self.raw_pixels_mut()[local_y * stride + local_x] = Rgb565::from(color).into_storage();
}
fn put_pixel_565(&mut self, x: usize, y: usize, rgb565: u16) {
let Some(local_x) = self.local_x(x as i32) else {
return;
};
let Some(local_y) = self.local_y(y as i32) else {
return;
};
if local_x >= self.view.width() || local_y >= self.view.height() {
return;
}
let stride = self.view.width();
self.raw_pixels_mut()[local_y * stride + local_x] = rgb565;
}
}
#[derive(Debug)]
pub enum Error {
ConfigureDisplaySpi(esp_hal::spi::master::ConfigError),
InitDisplay,
ConfigureTouchSpi(esp_hal::spi::master::ConfigError),
FlushFrameBuffer,
SetOrientation,
}
impl<D: SpiDevice<u8>> CydDisplayEsp<D> {
fn set_orientation(&mut self, orientation: Orientation) -> Result<(), Error> {
self.display.set_orientation(orientation)?;
self.orientation = orientation;
Ok(())
}
fn from_display_device(
mut display: CydDisplayEspDevice<D>,
orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
pixel_buffer: &'static mut dyn DynPixelBuffer,
) -> Result<Self, Error> {
let background565 = rgb565(background_color);
display.fill(background565)?;
Ok(Self {
display,
orientation,
pixel_buffer,
background_color,
foreground_color,
background565,
foreground565: rgb565(foreground_color),
font,
})
}
pub(crate) fn new_from_device(
spi_device: D,
dc_pin: impl esp_hal::gpio::OutputPin + 'static,
rst_pin: impl esp_hal::gpio::OutputPin + 'static,
backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
pixel_buffer: &'static mut dyn DynPixelBuffer,
) -> Result<Self, Error> {
let display = CydDisplayEspDevice::new_from_device(
spi_device,
dc_pin,
rst_pin,
backlight_pin,
orientation,
)?;
Self::from_display_device(
display,
orientation,
background_color,
foreground_color,
font,
pixel_buffer,
)
}
}
impl CydDisplayEsp<display::CydDisplaySpiDevice> {
pub fn new<const PIXEL_COUNT: usize>(
statics: &'static CydStaticEsp<PIXEL_COUNT>,
display_spi: impl esp_hal::spi::master::Instance + 'static,
display_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
display_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
display_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
display_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
display_dc_pin: impl esp_hal::gpio::OutputPin + 'static,
display_rst_pin: impl esp_hal::gpio::OutputPin + 'static,
display_backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
display_spi_hz: u32,
orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
) -> Result<Self, Error> {
let pixel_buffer = PixelBuffer::init_static(&statics.pixel_buffer);
let display = CydDisplayEspDevice::new(
display_spi,
display_sck_pin,
display_mosi_pin,
display_miso_pin,
display_cs_pin,
display_dc_pin,
display_rst_pin,
display_backlight_pin,
display_spi_hz,
orientation,
)?;
Self::from_display_device(
display,
orientation,
background_color,
foreground_color,
font,
pixel_buffer,
)
}
}
impl<D: SpiDevice<u8>> CydTouchUncalibratedEsp<D> {
pub(crate) fn from_device(
touch_spi_device: D,
touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
) -> Self {
Self {
touch: CydTouchEspDevice::from_device(touch_spi_device, touch_irq_pin),
}
}
}
impl CydTouchUncalibratedEsp<touch_driver::CydTouchSpiDevice> {
pub(crate) fn new(
touch_spi: impl esp_hal::spi::master::Instance + 'static,
touch_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
touch_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
touch_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
touch_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
) -> Result<Self, Error> {
Ok(Self {
touch: CydTouchEspDevice::new(
touch_spi,
touch_sck_pin,
touch_mosi_pin,
touch_miso_pin,
touch_cs_pin,
touch_irq_pin,
)?,
})
}
}
impl CydEsp {
pub const SCREEN_PIXELS: usize = SCREEN_PIXELS;
#[must_use]
pub const fn new_static<const PIXEL_COUNT: usize>() -> CydStaticEsp<PIXEL_COUNT> {
CydStaticEsp::new()
}
pub async fn new<const PIXEL_COUNT: usize, R: Button>(
statics: &'static CydStaticEsp<PIXEL_COUNT>,
display_spi: impl esp_hal::spi::master::Instance + 'static,
display_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
display_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
display_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
display_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
display_dc_pin: impl esp_hal::gpio::OutputPin + 'static,
display_rst_pin: impl esp_hal::gpio::OutputPin + 'static,
display_backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
display_spi_hz: u32,
orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
touch_spi: impl esp_hal::spi::master::Instance + 'static,
touch_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
touch_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
touch_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
touch_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
calibration_flash_block: &mut FlashBlockEsp,
recalibration_button: &mut R,
) -> crate::Result<Self> {
let CydEspUncalibrated { mut display, touch } = CydEspUncalibrated::new(
statics,
display_spi,
display_sck_pin,
display_mosi_pin,
display_miso_pin,
display_cs_pin,
display_dc_pin,
display_rst_pin,
display_backlight_pin,
display_spi_hz,
orientation,
background_color,
foreground_color,
font,
touch_spi,
touch_sck_pin,
touch_mosi_pin,
touch_miso_pin,
touch_cs_pin,
touch_irq_pin,
)?;
let touch = backend::ensure_calibration(
&mut display,
touch,
calibration_flash_block,
recalibration_button,
None,
orientation,
)
.await
.map_err(|error| match error {
backend::Error::Device(cyd_error) => crate::Error::from(cyd_error),
backend::Error::Flash(flash_error) => flash_error,
})?;
display.set_orientation(orientation)?;
Ok(Self { display, touch })
}
}
impl Cyd for CydEsp {
type Error = Error;
type Display = CydDisplayEsp;
type Touch = CydTouchEsp;
fn parts(&mut self) -> (&mut Self::Display, &mut Self::Touch) {
(&mut self.display, &mut self.touch)
}
fn orientation(&self) -> Orientation {
self.display.orientation
}
}
impl CydEspUncalibrated {
pub(crate) fn new<const PIXEL_COUNT: usize>(
statics: &'static CydStaticEsp<PIXEL_COUNT>,
display_spi: impl esp_hal::spi::master::Instance + 'static,
display_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
display_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
display_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
display_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
display_dc_pin: impl esp_hal::gpio::OutputPin + 'static,
display_rst_pin: impl esp_hal::gpio::OutputPin + 'static,
display_backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
display_spi_hz: u32,
_orientation: Orientation,
background_color: Rgb888,
foreground_color: Rgb888,
font: &'static MonoFont<'static>,
touch_spi: impl esp_hal::spi::master::Instance + 'static,
touch_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
touch_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
touch_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
touch_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
) -> Result<Self, Error> {
Ok(Self {
display: CydDisplayEsp::new(
statics,
display_spi,
display_sck_pin,
display_mosi_pin,
display_miso_pin,
display_cs_pin,
display_dc_pin,
display_rst_pin,
display_backlight_pin,
display_spi_hz,
Orientation::Landscape,
background_color,
foreground_color,
font,
)?,
touch: CydTouchUncalibratedEsp::new(
touch_spi,
touch_sck_pin,
touch_mosi_pin,
touch_miso_pin,
touch_cs_pin,
touch_irq_pin,
)?,
})
}
}
fn rgb565(color: Rgb888) -> Rgb565 {
Rgb565::from(color)
}
impl<D: SpiDevice<u8>> fmt::Debug for CydDisplayEsp<D> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CydDisplayEsp")
.field("orientation", &self.orientation)
.finish_non_exhaustive()
}
}
impl<D> fmt::Debug for CydTouchUncalibratedEsp<D> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CydTouchUncalibratedEsp")
.finish_non_exhaustive()
}
}
impl<D> fmt::Debug for CydTouchEsp<D> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CydTouchEsp")
.field("calibration_config", &self.calibration_config)
.field("orientation", &self.orientation)
.finish_non_exhaustive()
}
}
impl fmt::Debug for CydEsp {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CydEsp")
.field("orientation", &self.display.orientation)
.finish_non_exhaustive()
}
}
impl fmt::Debug for CydEspUncalibrated {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CydEspUncalibrated")
.field("orientation", &self.display.orientation)
.finish_non_exhaustive()
}
}
impl<D: SpiDevice<u8>> backend::DisplayBackend for CydDisplayEsp<D> {
type Error = Error;
type Frame<'a>
= CydFrameEsp<'a, D>
where
Self: 'a;
fn create_frame_mut(&mut self, rectangle: Rectangle) -> Self::Frame<'_> {
self.display.make_frame(
self.pixel_buffer,
rectangle,
self.background565,
self.foreground565,
self.font,
)
}
}
impl<D: SpiDevice<u8>> CydDisplay for CydDisplayEsp<D> {
#[inline]
fn screen_size(&self) -> Size {
self.display.size()
}
fn background_color(&self) -> Rgb888 {
self.background_color
}
fn foreground_color(&self) -> Rgb888 {
self.foreground_color
}
fn background_565(&self) -> Rgb565 {
self.background565
}
fn foreground_565(&self) -> Rgb565 {
self.foreground565
}
#[inline]
fn fill_rectangle(&mut self, rectangle: Rectangle, color: Rgb565) -> Result<(), Error> {
Ok(self.display.fill_rectangle(rectangle, color)?)
}
#[inline]
fn fill_contiguous<I>(&mut self, rectangle: Rectangle, pixels: I) -> Result<(), Error>
where
I: IntoIterator<Item = Rgb565>,
{
Ok(self.display.fill_contiguous(rectangle, pixels)?)
}
}
impl<D: SpiDevice<u8>> TouchUncalibrated for CydTouchUncalibratedEsp<D> {
type Error = Error;
type Calibrated = CydTouchEsp<D>;
fn read_raw_touch_event(&mut self) -> Result<Option<RawTouchEvent>, Self::Error> {
Ok(self.touch.read_raw_touch_event())
}
fn calibrate(
self,
calibration_config: CalibrationConfig,
orientation: Orientation,
) -> Self::Calibrated {
CydTouchEsp {
raw: self,
calibration_config,
orientation,
}
}
}
impl<D: SpiDevice<u8>> CydTouch for CydTouchEsp<D> {
type Error = Error;
fn try_read(&mut self) -> Result<Option<TouchEvent>, Error> {
Ok(self
.raw
.touch
.read_raw_touch_event()
.map(|raw_touch_event| match raw_touch_event {
RawTouchEvent::Down { raw_x, raw_y } => {
let (x, y) = self.calibration_config.map_raw_to_screen(raw_x, raw_y);
TouchEvent::Down {
point: self
.orientation
.map_landscape_point(Point::new(x as i32, y as i32)),
}
}
RawTouchEvent::Move { raw_x, raw_y } => {
let (x, y) = self.calibration_config.map_raw_to_screen(raw_x, raw_y);
TouchEvent::Move {
point: self
.orientation
.map_landscape_point(Point::new(x as i32, y as i32)),
}
}
RawTouchEvent::Up => TouchEvent::Up,
}))
}
}
impl<D: SpiDevice<u8>> CydFrame for CydFrameEsp<'_, D> {
type Error = Error;
fn rectangle(&self) -> Rectangle {
self.rectangle
}
fn fill(&mut self, color: Rgb565) -> &mut Self {
CydFrameEsp::fill(self, color)
}
fn clear(&mut self) -> &mut Self {
self.fill(self.background565)
}
fn write_text(&mut self, text: &str) -> &mut Self {
CydFrameEsp::write_text(self, text)
}
fn copy_from_565(&mut self, src: &[u16]) -> device_envoy_core::Result<()> {
let dst = self.raw_pixels_mut();
if dst.len() != src.len() {
return Err(device_envoy_core::Error::CopySize {
src_len: src.len(),
frame_len: dst.len(),
});
}
dst.copy_from_slice(src);
Ok(())
}
async fn flush(&mut self) -> Result<(), Error> {
CydFrameEsp::flush(self)
}
}