Skip to main content

device_envoy_esp/
cyd.rs

1#![cfg_attr(
2    feature = "doc-images",
3    doc = ::embed_doc_image::embed_image!(
4        "linkage_blaze_gallery",
5        "docs/assets/linkage_blaze_gallery.png"
6    )
7)]
8//! ESP32 support for Cheap Yellow Display (CYD) boards.
9//!
10//! These boards combine a 320×240 ILI9341 display with XPT2046 resistive
11//! touch. After construction, applications use the portable display and
12//! calibrated-touch interfaces from
13//! [`device_envoy_core::cyd`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/).
14//! The RP, WebAssembly, and in-memory implementations share these interfaces.
15//!
16//! ## See CYD in action
17//!
18//! [Linkage Blaze] demonstrates animated clocks, mechanisms, and figures built
19//! with Device Envoy's portable CYD display APIs. Explore the examples in the
20//! [interactive Linkage Blaze gallery].
21#![cfg_attr(
22    feature = "doc-images",
23    doc = "\n[![Linkage Blaze gallery showing CYD applications][linkage_blaze_gallery]][interactive Linkage Blaze gallery]\n"
24)]
25//!
26//! [Linkage Blaze]: https://github.com/CarlKCarlK/linkage-blaze
27//! [interactive Linkage Blaze gallery]: https://carlkcarlk.github.io/linkage-blaze/demos/
28//!
29//! The portable [Core CYD documentation] provides the shared
30//! [application example], [drawing-strategy guide], [implementation overview],
31//! and [Linkage Blaze gallery].
32//!
33//! [Core CYD documentation]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/
34//! [application example]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#application-example
35//! [drawing-strategy guide]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#choose-a-drawing-strategy
36//! [implementation overview]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#implementations-1
37//! [Linkage Blaze gallery]: https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/#see-cyd-in-action
38//!
39//! Your drawing strategy determines the pixel-buffer capacity selected through
40//! [`CydEsp::new_static`]: use [`CydEsp::SCREEN_PIXELS`] for full-screen frames,
41//! the largest region's pixel count for regional frames,
42//! [`tiling::TileGrid::max_tile_pixel_count`] for tiled drawing, or `0` for
43//! immediate operations and contiguous streaming.
44//!
45//! [Choose a constructor](#choose-a-constructor) explains how to construct an
46//! ESP32 device. The
47//! [ESP32 CYD touch-paint example](https://github.com/CarlKCarlK/device-envoy/blob/main/crates/device-envoy-examples-esp/examples/esp32/generic/cyd_touch_paint.rs)
48//! puts both stages together in a complete program.
49//! ## Choose a constructor
50//!
51//! Construction depends on how many [SPI resources](crate#glossary) the CYD
52//! should use and whether the application needs touch:
53//!
54//! - [`CydEsp`] uses two SPI resources: one for the display and one for touch.
55//!   Choose it when the board has both resources available. Construction also
56//!   loads or runs touch calibration.
57//! - [`CydEspOneSpi`] uses one SPI resource for both the display and touch.
58//!   Choose it when the board has only one available, or when the application
59//!   needs to keep another SPI resource for something else. Device Envoy
60//!   coordinates access internally.
61//! - [`CydDisplayEsp`] uses one SPI resource for the display and omits touch.
62//!   Choose it when the application does not need touch.
63//!
64//! The [`CydEsp::new`], [`CydEspOneSpi::new`], and [`CydDisplayEsp::new`]
65//! examples show the constructor arguments for each choice. After construction,
66//! shared application code can use the [`Cyd`] trait without naming an ESP type.
67
68mod buffer;
69mod display;
70mod one_spi;
71mod text;
72#[path = "cyd/touch.rs"]
73mod touch_driver;
74
75use core::{convert::Infallible, fmt};
76
77use embedded_graphics::{
78    Pixel,
79    mono_font::MonoFont,
80    pixelcolor::{IntoStorage, Rgb565, Rgb888},
81    prelude::{Dimensions, DrawTarget, OriginDimensions, Point, Size},
82    primitives::Rectangle,
83};
84use embedded_hal::spi::SpiDevice;
85use static_cell::StaticCell;
86
87use buffer::DynPixelBuffer;
88use buffer::{PixelBuffer, RegionView};
89use device_envoy_core::button::Button;
90use device_envoy_core::cyd::backend;
91use device_envoy_core::cyd::{
92    SCREEN_PIXELS,
93    backend::{CalibrationConfig, RawTouchEvent, TouchUncalibrated},
94    display::CydFrame,
95    touch::TouchEvent,
96};
97use device_envoy_core::pixel_target::PixelTarget;
98pub use display::DEFAULT_DISPLAY_SPI_HZ;
99// The device abstraction and its neutral support types live in
100// `device-envoy-core::cyd`; re-export the public surface from this device crate.
101pub use device_envoy_core::cyd::{
102    Cyd, CydDisplay, CydTouch,
103    display::{Orientation, tiling},
104    touch,
105};
106pub use one_spi::CydEspOneSpi;
107pub use text::DEFAULT_FONT;
108use touch_driver::TOUCH_SPI_HZ;
109
110use crate::flash_block::FlashBlockEsp;
111use display::CydDisplayEsp as CydDisplayEspDevice;
112use touch_driver::CydTouchEsp as CydTouchEspDevice;
113
114/// An owned CYD-family ESP32 display component.
115///
116/// `D` is the underlying `embedded-hal` SPI device type; it defaults to an
117/// exclusively-owned SPI peripheral. Shared-bus backends (see
118/// [`CydEspOneSpi`]) instantiate this with an
119/// `embedded_hal_bus::spi::RefCellDevice` instead.
120///
121/// Start with the [`cyd`](mod@crate::cyd) module example. The
122/// display-only constructor and static-storage pattern are shown by the
123/// [`CydDisplayEsp::new`] example.
124pub struct CydDisplayEsp<D: SpiDevice<u8> = display::CydDisplaySpiDevice> {
125    display: CydDisplayEspDevice<D>,
126    orientation: Orientation,
127    // Every CydEsp owns exactly one draw buffer. Apps that don't draw through it
128    // pass a zero-sized buffer (e.g. `CydStaticEsp<0>`).
129    pixel_buffer: &'static mut dyn DynPixelBuffer,
130    // Default drawing style. Background clears the device at construction and
131    // fills every new frame; foreground color and font drive `CydFrameEsp::write_text`.
132    // The `Rgb565` versions are precomputed so the hot drawing paths skip the
133    // per-call conversion.
134    background_color: Rgb888,
135    foreground_color: Rgb888,
136    background565: Rgb565,
137    foreground565: Rgb565,
138    font: &'static MonoFont<'static>,
139}
140
141/// An owned uncalibrated CYD-family ESP32 touch component.
142///
143/// `D` is the underlying `embedded-hal` SPI device type; see
144/// [`CydDisplayEsp`] for the shared-bus rationale.
145pub(crate) struct CydTouchUncalibratedEsp<D = touch_driver::CydTouchSpiDevice> {
146    touch: CydTouchEspDevice<D>,
147}
148
149/// An owned calibrated CYD-family ESP32 touch component.
150///
151/// Start with the [`cyd`](mod@crate::cyd) module example. Construction
152/// is covered by [`CydEsp::new`]; applications then call the calibrated
153/// [`CydTouch::try_read`] operation.
154pub struct CydTouchEsp<D = touch_driver::CydTouchSpiDevice> {
155    raw: CydTouchUncalibratedEsp<D>,
156    calibration_config: CalibrationConfig,
157    orientation: Orientation,
158}
159
160/// An ESP32 CYD device containing a display and calibrated touch input.
161///
162/// [`CydEsp::new_static`] creates the pixel buffer storage passed to
163/// [`CydEsp::new`], which constructs the hardware and loads or performs touch
164/// calibration. See the [`cyd`](mod@crate::cyd) module example for normal
165/// drawing and touch input.
166pub struct CydEsp {
167    /// The display component.
168    pub display: CydDisplayEsp,
169    /// The calibrated touch component.
170    pub touch: CydTouchEsp,
171}
172
173/// An uncalibrated CYD-family ESP32 bundle.
174pub(crate) struct CydEspUncalibrated {
175    /// The owned display component.
176    pub display: CydDisplayEsp,
177    /// The owned uncalibrated touch component.
178    pub touch: CydTouchUncalibratedEsp,
179}
180
181/// Static storage for a [`CydEsp`]-owned pixel buffer.
182///
183/// `PIXEL_COUNT` is an RGB565 pixel count, not a byte count. Choose its capacity
184/// through [`CydEsp::new_static`] or [`CydEspOneSpi::new_static`].
185/// Declare the storage at file scope:
186///
187/// ```rust,no_run
188/// #![no_std]
189/// #![no_main]
190/// use device_envoy_esp::cyd::{CydEsp, CydStaticEsp};
191/// static CYD_STATIC: CydStaticEsp<{ CydEsp::SCREEN_PIXELS }> = CydEsp::new_static();
192/// # #[panic_handler]
193/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
194/// ```
195pub struct CydStaticEsp<const PIXEL_COUNT: usize> {
196    pixel_buffer: StaticCell<PixelBuffer<PIXEL_COUNT>>,
197}
198
199impl<const PIXEL_COUNT: usize> CydStaticEsp<PIXEL_COUNT> {
200    /// Internal constructor. Apps create storage via [`CydEsp::new_static`] so all
201    /// construction goes through the `CydEsp` device abstraction.
202    pub(crate) const fn new() -> Self {
203        assert!(
204            PIXEL_COUNT <= SCREEN_PIXELS,
205            "PIXEL_COUNT must not exceed SCREEN_PIXELS"
206        );
207        Self {
208            pixel_buffer: StaticCell::new(),
209        }
210    }
211}
212
213/// The ESP implementation of
214/// [`CydFrame`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html).
215///
216/// Frames are returned by [`CydDisplay::frame_mut`]. See the portable
217/// [`CydFrame`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html)
218/// documentation for normal drawing.
219pub struct CydFrameEsp<'a, D: SpiDevice<u8> = display::CydDisplaySpiDevice> {
220    display: &'a mut CydDisplayEspDevice<D>,
221    view: RegionView<'a>,
222    // Where this frame presents and how large it is: set from the `Rectangle`
223    // passed to `frame_mut`, so `flush` needs no separate position argument.
224    rectangle: Rectangle,
225    // Default foreground color and font, copied from the owning `CydDisplayEsp`, so
226    // `write_text` can render with the device default style.
227    pub(crate) background565: Rgb565,
228    pub(crate) foreground565: Rgb565,
229    pub(crate) font: &'static MonoFont<'static>,
230}
231
232impl<'a, D: SpiDevice<u8>> CydFrameEsp<'a, D> {
233    /// Fill the frame with an explicit color.
234    ///
235    /// See the portable
236    /// [`CydFrame::fill`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html#tymethod.fill)
237    /// documentation.
238    pub fn fill(&mut self, color: Rgb565) -> &mut Self {
239        self.view.fill(color);
240        self
241    }
242
243    /// The buffered frame region's width in pixels.
244    #[must_use]
245    pub fn width(&self) -> usize {
246        self.view.width()
247    }
248
249    /// The buffered frame region's height in pixels.
250    #[must_use]
251    pub fn height(&self) -> usize {
252        self.view.height()
253    }
254
255    /// Borrow the buffered frame region's raw RGB565 pixels in row-major order.
256    pub fn raw_pixels_mut(&mut self) -> &mut [u16] {
257        self.view.raw_pixels_mut()
258    }
259
260    /// Present this frame's pixels at its rectangle's top-left (set by
261    /// [`CydDisplay::frame_mut`]).
262    ///
263    /// This inherent method synchronously writes the buffered rectangle over
264    /// SPI. In generic
265    /// code, call [`CydFrame::flush`](https://docs.rs/device-envoy-core/latest/device_envoy_core/cyd/display/trait.CydFrame.html#tymethod.flush)
266    /// and await its future instead.
267    pub fn flush(&mut self) -> Result<(), Error> {
268        Ok(self.display.flush_buffer(
269            self.view.size().width as usize,
270            self.view.size().height as usize,
271            self.view.raw_pixels(),
272            self.rectangle.top_left,
273        )?)
274    }
275
276    fn local_x(&self, x: i32) -> Option<usize> {
277        usize::try_from(x.checked_sub(self.rectangle.top_left.x)?).ok()
278    }
279
280    fn local_y(&self, y: i32) -> Option<usize> {
281        usize::try_from(y.checked_sub(self.rectangle.top_left.y)?).ok()
282    }
283}
284
285impl<D: SpiDevice<u8>> DrawTarget for CydFrameEsp<'_, D> {
286    type Color = Rgb565;
287    type Error = Infallible;
288
289    fn clear(&mut self, color: Self::Color) -> Result<(), Self::Error> {
290        self.fill(color);
291        Ok(())
292    }
293
294    fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
295    where
296        I: IntoIterator<Item = Pixel<Self::Color>>,
297    {
298        for Pixel(point, color) in pixels {
299            let Some(local_x) = self.local_x(point.x) else {
300                continue;
301            };
302            let Some(local_y) = self.local_y(point.y) else {
303                continue;
304            };
305            if local_x < self.view.width() && local_y < self.view.height() {
306                let index = local_y * self.view.width() + local_x;
307                self.raw_pixels_mut()[index] = color.into_storage();
308            }
309        }
310        Ok(())
311    }
312}
313
314impl<D: SpiDevice<u8>> Dimensions for CydFrameEsp<'_, D> {
315    fn bounding_box(&self) -> Rectangle {
316        self.rectangle
317    }
318}
319
320impl<D: SpiDevice<u8>> PixelTarget for CydFrameEsp<'_, D> {
321    fn width(&self) -> usize {
322        usize::try_from(self.rectangle.top_left.x)
323            .expect("frame top-left x must be non-negative")
324            .checked_add(self.width())
325            .expect("frame width must fit in usize")
326    }
327
328    fn height(&self) -> usize {
329        usize::try_from(self.rectangle.top_left.y)
330            .expect("frame top-left y must be non-negative")
331            .checked_add(self.height())
332            .expect("frame height must fit in usize")
333    }
334
335    fn put_pixel(&mut self, x: usize, y: usize, color: Rgb888) {
336        let Some(local_x) = self.local_x(x as i32) else {
337            return;
338        };
339        let Some(local_y) = self.local_y(y as i32) else {
340            return;
341        };
342        if local_x >= self.view.width() || local_y >= self.view.height() {
343            return;
344        }
345        let stride = self.view.width();
346        self.raw_pixels_mut()[local_y * stride + local_x] = Rgb565::from(color).into_storage();
347    }
348
349    /// The frame buffer already stores RGB565, so a decoded image pixel can be
350    /// written verbatim with no RGB888 round-trip.
351    fn put_pixel_565(&mut self, x: usize, y: usize, rgb565: u16) {
352        let Some(local_x) = self.local_x(x as i32) else {
353            return;
354        };
355        let Some(local_y) = self.local_y(y as i32) else {
356            return;
357        };
358        if local_x >= self.view.width() || local_y >= self.view.height() {
359            return;
360        }
361        let stride = self.view.width();
362        self.raw_pixels_mut()[local_y * stride + local_x] = rgb565;
363    }
364}
365
366/// Error from a CYD ESP display or touch operation.
367///
368/// Most applications propagate this error with `?`. Code that reports errors
369/// differently by operation can match the preserved source-bearing variants:
370///
371/// ```rust,no_run
372/// # #![no_std]
373/// # #![no_main]
374/// # #[panic_handler]
375/// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
376/// use device_envoy_esp::cyd::Error;
377///
378/// fn report(error: Error) {
379///     match error {
380///         Error::ConfigureDisplaySpi(source) | Error::ConfigureTouchSpi(source) => {
381///             // Report the SPI configuration details from `source`.
382///             drop(source);
383///         }
384///         Error::InitDisplay => { /* Display initialization failed. */ }
385///         Error::FlushFrameBuffer => { /* Sending pixels failed. */ }
386///         Error::SetOrientation => { /* Changing orientation failed. */ }
387///     }
388/// }
389/// ```
390#[derive(Debug)]
391pub enum Error {
392    /// Configuring the display SPI peripheral failed.
393    /// See the [`Error`] example.
394    ConfigureDisplaySpi(esp_hal::spi::master::ConfigError),
395    /// The display panel could not be initialized.
396    /// See the [`Error`] example.
397    InitDisplay,
398    /// Configuring the touch SPI peripheral failed.
399    /// See the [`Error`] example.
400    ConfigureTouchSpi(esp_hal::spi::master::ConfigError),
401    /// A frame could not be flushed to the display.
402    /// See the [`Error`] example.
403    FlushFrameBuffer,
404    /// Changing the display orientation failed.
405    /// See the [`Error`] example.
406    SetOrientation,
407}
408
409impl<D: SpiDevice<u8>> CydDisplayEsp<D> {
410    fn set_orientation(&mut self, orientation: Orientation) -> Result<(), Error> {
411        self.display.set_orientation(orientation)?;
412        self.orientation = orientation;
413        Ok(())
414    }
415
416    fn from_display_device(
417        mut display: CydDisplayEspDevice<D>,
418        orientation: Orientation,
419        background_color: Rgb888,
420        foreground_color: Rgb888,
421        font: &'static MonoFont<'static>,
422        pixel_buffer: &'static mut dyn DynPixelBuffer,
423    ) -> Result<Self, Error> {
424        let background565 = rgb565(background_color);
425        display.fill(background565)?;
426
427        Ok(Self {
428            display,
429            orientation,
430            pixel_buffer,
431            background_color,
432            foreground_color,
433            background565,
434            foreground565: rgb565(foreground_color),
435            font,
436        })
437    }
438
439    /// Construct a display component from an already-built SPI device.
440    ///
441    /// Used by shared-bus backends (see [`CydEspOneSpi`]) that build their
442    /// own `SpiDevice` instead of owning an exclusive SPI peripheral.
443    pub(crate) fn new_from_device(
444        spi_device: D,
445        dc_pin: impl esp_hal::gpio::OutputPin + 'static,
446        rst_pin: impl esp_hal::gpio::OutputPin + 'static,
447        backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
448        orientation: Orientation,
449        background_color: Rgb888,
450        foreground_color: Rgb888,
451        font: &'static MonoFont<'static>,
452        pixel_buffer: &'static mut dyn DynPixelBuffer,
453    ) -> Result<Self, Error> {
454        let display = CydDisplayEspDevice::new_from_device(
455            spi_device,
456            dc_pin,
457            rst_pin,
458            backlight_pin,
459            orientation,
460        )?;
461        Self::from_display_device(
462            display,
463            orientation,
464            background_color,
465            foreground_color,
466            font,
467            pixel_buffer,
468        )
469    }
470}
471
472impl CydDisplayEsp<display::CydDisplaySpiDevice> {
473    /// Construct a display-only CYD display component that owns its draw buffer.
474    ///
475    /// Choosing the pixel buffer capacity is the most important construction
476    /// decision: `statics` determines both static RAM use and the largest
477    /// buffered region. See [`CydEsp::new_static`] for the sizing choices.
478    ///
479    /// ```rust,no_run
480    /// # #![no_std]
481    /// # #![no_main]
482    /// use device_envoy_esp::{Result, cyd::{CydDisplay, CydDisplayEsp, CydEsp, DEFAULT_DISPLAY_SPI_HZ, DEFAULT_FONT, Orientation}};
483    /// use embedded_graphics::{pixelcolor::Rgb888, prelude::RgbColor};
484    /// # #[panic_handler]
485    /// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
486    /// async fn construct(p: esp_hal::peripherals::Peripherals) -> Result<()> {
487    ///     static CYD_STATIC: device_envoy_esp::cyd::CydStaticEsp<0> = CydEsp::new_static();
488    ///     let display = CydDisplayEsp::new(&CYD_STATIC, p.SPI2, p.GPIO1, p.GPIO2, p.GPIO3,
489    ///         p.GPIO4, p.GPIO5, p.GPIO7, p.GPIO8, DEFAULT_DISPLAY_SPI_HZ,
490    ///         Orientation::Landscape, Rgb888::BLACK, Rgb888::WHITE, &DEFAULT_FONT)?;
491    ///     assert_eq!(display.screen_size(), Orientation::Landscape.size());
492    ///     Ok(())
493    /// }
494    /// ```
495    pub fn new<const PIXEL_COUNT: usize>(
496        statics: &'static CydStaticEsp<PIXEL_COUNT>,
497        display_spi: impl esp_hal::spi::master::Instance + 'static,
498        display_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
499        display_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
500        display_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
501        display_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
502        display_dc_pin: impl esp_hal::gpio::OutputPin + 'static,
503        display_rst_pin: impl esp_hal::gpio::OutputPin + 'static,
504        display_backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
505        display_spi_hz: u32,
506        orientation: Orientation,
507        background_color: Rgb888,
508        foreground_color: Rgb888,
509        font: &'static MonoFont<'static>,
510    ) -> Result<Self, Error> {
511        let pixel_buffer = PixelBuffer::init_static(&statics.pixel_buffer);
512        let display = CydDisplayEspDevice::new(
513            display_spi,
514            display_sck_pin,
515            display_mosi_pin,
516            display_miso_pin,
517            display_cs_pin,
518            display_dc_pin,
519            display_rst_pin,
520            display_backlight_pin,
521            display_spi_hz,
522            orientation,
523        )?;
524        Self::from_display_device(
525            display,
526            orientation,
527            background_color,
528            foreground_color,
529            font,
530            pixel_buffer,
531        )
532    }
533}
534
535impl<D: SpiDevice<u8>> CydTouchUncalibratedEsp<D> {
536    /// Construct an uncalibrated touch component from an already-built SPI device.
537    ///
538    /// Used by shared-bus backends (see [`CydEspOneSpi`]) that build their
539    /// own `SpiDevice` instead of owning an exclusive SPI peripheral.
540    pub(crate) fn from_device(
541        touch_spi_device: D,
542        touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
543    ) -> Self {
544        Self {
545            touch: CydTouchEspDevice::from_device(touch_spi_device, touch_irq_pin),
546        }
547    }
548}
549
550impl CydTouchUncalibratedEsp<touch_driver::CydTouchSpiDevice> {
551    /// Construct an uncalibrated touch component.
552    pub(crate) fn new(
553        touch_spi: impl esp_hal::spi::master::Instance + 'static,
554        touch_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
555        touch_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
556        touch_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
557        touch_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
558        touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
559    ) -> Result<Self, Error> {
560        Ok(Self {
561            touch: CydTouchEspDevice::new(
562                touch_spi,
563                touch_sck_pin,
564                touch_mosi_pin,
565                touch_miso_pin,
566                touch_cs_pin,
567                touch_irq_pin,
568            )?,
569        })
570    }
571}
572
573impl CydEsp {
574    /// Total pixel count of the CYD panel — fixed hardware, independent of orientation.
575    ///
576    /// Used by the [`CydStaticEsp`] storage example.
577    pub const SCREEN_PIXELS: usize = SCREEN_PIXELS;
578
579    /// Create static storage for a CYD pixel buffer.
580    ///
581    /// Choose any `PIXEL_COUNT` from zero through [`CydEsp::SCREEN_PIXELS`].
582    ///
583    /// - `0` allocates no pixel buffer, so only
584    ///   [immediate operations](CydDisplay::fill_rectangle) and
585    ///   [contiguous streaming](CydDisplay::fill_contiguous) are available.
586    /// - A regional buffer can be sized for the largest rectangle requested
587    ///   through [`CydDisplay::frame_mut`].
588    /// - For tiled drawing, size the buffer to
589    ///   [`tiling::TileGrid::max_tile_pixel_count`], then pass the grid to
590    ///   [`CydDisplay::for_each_tile`]. Only one tile is buffered at a time.
591    /// - [`CydEsp::SCREEN_PIXELS`] allocates a full-screen buffer and is usually
592    ///   the most convenient choice when enough RAM is available.
593    ///
594    /// Attempting to create a frame or tile larger than the allocated buffer
595    /// panics.
596    #[must_use]
597    pub const fn new_static<const PIXEL_COUNT: usize>() -> CydStaticEsp<PIXEL_COUNT> {
598        CydStaticEsp::new()
599    }
600
601    /// Construct a ready-to-use CYD.
602    ///
603    /// The display and touch controller use separate SPI buses. The supplied
604    /// flash block stores touch calibration, and `recalibration_button` requests
605    /// interactive recalibration.
606    ///
607    /// Choosing the pixel buffer capacity is the most important construction
608    /// decision: `statics` determines both static RAM use and the largest
609    /// buffered region. See [`CydEsp::new_static`] for the sizing choices.
610    ///
611    /// Use [`CydEspOneSpi`] for boards where display and touch share one SPI bus.
612    ///
613    /// This example focuses on the board-specific construction. For the normal
614    /// draw/flush/read loop, start with the [`cyd`](mod@crate::cyd) module
615    /// example. For complete startup and wiring in a real program, see the
616    /// [checked ESP32 CYD touch-paint example](https://github.com/CarlKCarlK/device-envoy/blob/main/crates/device-envoy-examples-esp/examples/esp32/generic/cyd_touch_paint.rs).
617    ///
618    /// ```rust,no_run
619    /// # #![no_std]
620    /// # #![no_main]
621    /// # #[panic_handler]
622    /// # fn panic(_info: &core::panic::PanicInfo) -> ! { loop {} }
623    /// # use device_envoy_esp::{Result, button::{ButtonEsp, PressedTo}, cyd::{CydEsp, CydStaticEsp, DEFAULT_DISPLAY_SPI_HZ, DEFAULT_FONT, Orientation}, flash_block::FlashBlockEsp};
624    /// # use embedded_graphics::{pixelcolor::Rgb888, prelude::RgbColor};
625    /// # use esp_hal::spi::master::AnySpi;
626    /// # async fn construct(mut p: esp_hal::peripherals::Peripherals, touch_spi: AnySpi<'static>) -> Result<()> {
627    /// #     let [mut calibration_flash] = FlashBlockEsp::new_array::<1>(p.FLASH)?;
628    /// #     let mut recalibration_button = ButtonEsp::new(p.GPIO6, PressedTo::Ground);
629    ///     static CYD_STATIC: CydStaticEsp<{ CydEsp::SCREEN_PIXELS }> = CydEsp::new_static();
630    ///
631    ///     let cyd = CydEsp::new(
632    ///         &CYD_STATIC,
633    ///
634    ///         // Display SPI and pins:
635    ///         p.SPI2,
636    ///         p.GPIO1,
637    ///         p.GPIO2,
638    ///         p.GPIO3,
639    ///         p.GPIO4,
640    ///         p.GPIO5,
641    ///         p.GPIO7,
642    ///         p.GPIO8,
643    ///         DEFAULT_DISPLAY_SPI_HZ,
644    ///
645    ///         // Presentation:
646    ///         Orientation::Landscape,
647    ///         Rgb888::BLACK,
648    ///         Rgb888::WHITE,
649    ///         &DEFAULT_FONT,
650    ///
651    ///         // Touch SPI and pins:
652    ///         touch_spi,
653    ///         p.GPIO9,
654    ///         p.GPIO10,
655    ///         p.GPIO11,
656    ///         p.GPIO12,
657    ///         p.GPIO13,
658    ///
659    ///         // Calibration storage and recalibration button:
660    ///         &mut calibration_flash,
661    ///         &mut recalibration_button,
662    ///     )
663    ///     .await?;
664    ///
665    /// #     Ok(())
666    /// # }
667    /// ```
668    pub async fn new<const PIXEL_COUNT: usize, R: Button>(
669        statics: &'static CydStaticEsp<PIXEL_COUNT>,
670        display_spi: impl esp_hal::spi::master::Instance + 'static,
671        display_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
672        display_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
673        display_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
674        display_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
675        display_dc_pin: impl esp_hal::gpio::OutputPin + 'static,
676        display_rst_pin: impl esp_hal::gpio::OutputPin + 'static,
677        display_backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
678        display_spi_hz: u32,
679        orientation: Orientation,
680        background_color: Rgb888,
681        foreground_color: Rgb888,
682        font: &'static MonoFont<'static>,
683        touch_spi: impl esp_hal::spi::master::Instance + 'static,
684        touch_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
685        touch_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
686        touch_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
687        touch_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
688        touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
689        calibration_flash_block: &mut FlashBlockEsp,
690        recalibration_button: &mut R,
691    ) -> crate::Result<Self> {
692        let CydEspUncalibrated { mut display, touch } = CydEspUncalibrated::new(
693            statics,
694            display_spi,
695            display_sck_pin,
696            display_mosi_pin,
697            display_miso_pin,
698            display_cs_pin,
699            display_dc_pin,
700            display_rst_pin,
701            display_backlight_pin,
702            display_spi_hz,
703            orientation,
704            background_color,
705            foreground_color,
706            font,
707            touch_spi,
708            touch_sck_pin,
709            touch_mosi_pin,
710            touch_miso_pin,
711            touch_cs_pin,
712            touch_irq_pin,
713        )?;
714        let touch = backend::ensure_calibration(
715            &mut display,
716            touch,
717            calibration_flash_block,
718            recalibration_button,
719            None,
720            orientation,
721        )
722        .await
723        .map_err(|error| match error {
724            backend::Error::Device(cyd_error) => crate::Error::from(cyd_error),
725            backend::Error::Flash(flash_error) => flash_error,
726        })?;
727        display.set_orientation(orientation)?;
728        Ok(Self { display, touch })
729    }
730}
731
732impl Cyd for CydEsp {
733    type Error = Error;
734    type Display = CydDisplayEsp;
735    type Touch = CydTouchEsp;
736
737    fn parts(&mut self) -> (&mut Self::Display, &mut Self::Touch) {
738        (&mut self.display, &mut self.touch)
739    }
740
741    fn orientation(&self) -> Orientation {
742        self.display.orientation
743    }
744}
745
746impl CydEspUncalibrated {
747    pub(crate) fn new<const PIXEL_COUNT: usize>(
748        statics: &'static CydStaticEsp<PIXEL_COUNT>,
749        display_spi: impl esp_hal::spi::master::Instance + 'static,
750        display_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
751        display_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
752        display_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
753        display_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
754        display_dc_pin: impl esp_hal::gpio::OutputPin + 'static,
755        display_rst_pin: impl esp_hal::gpio::OutputPin + 'static,
756        display_backlight_pin: impl esp_hal::gpio::OutputPin + 'static,
757        display_spi_hz: u32,
758        _orientation: Orientation,
759        background_color: Rgb888,
760        foreground_color: Rgb888,
761        font: &'static MonoFont<'static>,
762        touch_spi: impl esp_hal::spi::master::Instance + 'static,
763        touch_sck_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
764        touch_mosi_pin: impl esp_hal::gpio::interconnect::PeripheralOutput<'static>,
765        touch_miso_pin: impl esp_hal::gpio::interconnect::PeripheralInput<'static>,
766        touch_cs_pin: impl esp_hal::gpio::OutputPin + 'static,
767        touch_irq_pin: impl esp_hal::gpio::InputPin + 'static,
768    ) -> Result<Self, Error> {
769        Ok(Self {
770            display: CydDisplayEsp::new(
771                statics,
772                display_spi,
773                display_sck_pin,
774                display_mosi_pin,
775                display_miso_pin,
776                display_cs_pin,
777                display_dc_pin,
778                display_rst_pin,
779                display_backlight_pin,
780                display_spi_hz,
781                Orientation::Landscape,
782                background_color,
783                foreground_color,
784                font,
785            )?,
786            touch: CydTouchUncalibratedEsp::new(
787                touch_spi,
788                touch_sck_pin,
789                touch_mosi_pin,
790                touch_miso_pin,
791                touch_cs_pin,
792                touch_irq_pin,
793            )?,
794        })
795    }
796}
797
798fn rgb565(color: Rgb888) -> Rgb565 {
799    Rgb565::from(color)
800}
801
802impl<D: SpiDevice<u8>> fmt::Debug for CydDisplayEsp<D> {
803    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
804        formatter
805            .debug_struct("CydDisplayEsp")
806            .field("orientation", &self.orientation)
807            .finish_non_exhaustive()
808    }
809}
810
811impl<D> fmt::Debug for CydTouchUncalibratedEsp<D> {
812    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
813        formatter
814            .debug_struct("CydTouchUncalibratedEsp")
815            .finish_non_exhaustive()
816    }
817}
818
819impl<D> fmt::Debug for CydTouchEsp<D> {
820    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
821        formatter
822            .debug_struct("CydTouchEsp")
823            .field("calibration_config", &self.calibration_config)
824            .field("orientation", &self.orientation)
825            .finish_non_exhaustive()
826    }
827}
828
829impl fmt::Debug for CydEsp {
830    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
831        formatter
832            .debug_struct("CydEsp")
833            .field("orientation", &self.display.orientation)
834            .finish_non_exhaustive()
835    }
836}
837
838impl fmt::Debug for CydEspUncalibrated {
839    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
840        formatter
841            .debug_struct("CydEspUncalibrated")
842            .field("orientation", &self.display.orientation)
843            .finish_non_exhaustive()
844    }
845}
846
847impl<D: SpiDevice<u8>> backend::DisplayBackend for CydDisplayEsp<D> {
848    type Error = Error;
849    type Frame<'a>
850        = CydFrameEsp<'a, D>
851    where
852        Self: 'a;
853
854    fn create_frame_mut(&mut self, rectangle: Rectangle) -> Self::Frame<'_> {
855        self.display.make_frame(
856            self.pixel_buffer,
857            rectangle,
858            self.background565,
859            self.foreground565,
860            self.font,
861        )
862    }
863}
864
865impl<D: SpiDevice<u8>> CydDisplay for CydDisplayEsp<D> {
866    #[inline]
867    fn screen_size(&self) -> Size {
868        self.display.size()
869    }
870
871    fn background_color(&self) -> Rgb888 {
872        self.background_color
873    }
874
875    fn foreground_color(&self) -> Rgb888 {
876        self.foreground_color
877    }
878
879    fn background_565(&self) -> Rgb565 {
880        self.background565
881    }
882
883    fn foreground_565(&self) -> Rgb565 {
884        self.foreground565
885    }
886
887    #[inline]
888    fn fill_rectangle(&mut self, rectangle: Rectangle, color: Rgb565) -> Result<(), Error> {
889        Ok(self.display.fill_rectangle(rectangle, color)?)
890    }
891
892    #[inline]
893    fn fill_contiguous<I>(&mut self, rectangle: Rectangle, pixels: I) -> Result<(), Error>
894    where
895        I: IntoIterator<Item = Rgb565>,
896    {
897        Ok(self.display.fill_contiguous(rectangle, pixels)?)
898    }
899}
900
901impl<D: SpiDevice<u8>> TouchUncalibrated for CydTouchUncalibratedEsp<D> {
902    type Error = Error;
903    type Calibrated = CydTouchEsp<D>;
904
905    fn read_raw_touch_event(&mut self) -> Result<Option<RawTouchEvent>, Self::Error> {
906        Ok(self.touch.read_raw_touch_event())
907    }
908
909    fn calibrate(
910        self,
911        calibration_config: CalibrationConfig,
912        orientation: Orientation,
913    ) -> Self::Calibrated {
914        CydTouchEsp {
915            raw: self,
916            calibration_config,
917            orientation,
918        }
919    }
920}
921
922impl<D: SpiDevice<u8>> CydTouch for CydTouchEsp<D> {
923    type Error = Error;
924
925    fn try_read(&mut self) -> Result<Option<TouchEvent>, Error> {
926        Ok(self
927            .raw
928            .touch
929            .read_raw_touch_event()
930            .map(|raw_touch_event| match raw_touch_event {
931                RawTouchEvent::Down { raw_x, raw_y } => {
932                    let (x, y) = self.calibration_config.map_raw_to_screen(raw_x, raw_y);
933                    TouchEvent::Down {
934                        point: self
935                            .orientation
936                            .map_landscape_point(Point::new(x as i32, y as i32)),
937                    }
938                }
939                RawTouchEvent::Move { raw_x, raw_y } => {
940                    let (x, y) = self.calibration_config.map_raw_to_screen(raw_x, raw_y);
941                    TouchEvent::Move {
942                        point: self
943                            .orientation
944                            .map_landscape_point(Point::new(x as i32, y as i32)),
945                    }
946                }
947                RawTouchEvent::Up => TouchEvent::Up,
948            }))
949    }
950}
951
952impl<D: SpiDevice<u8>> CydFrame for CydFrameEsp<'_, D> {
953    type Error = Error;
954
955    fn rectangle(&self) -> Rectangle {
956        self.rectangle
957    }
958
959    fn fill(&mut self, color: Rgb565) -> &mut Self {
960        CydFrameEsp::fill(self, color)
961    }
962
963    fn clear(&mut self) -> &mut Self {
964        self.fill(self.background565)
965    }
966
967    fn write_text(&mut self, text: &str) -> &mut Self {
968        CydFrameEsp::write_text(self, text)
969    }
970
971    fn copy_from_565(&mut self, src: &[u16]) -> device_envoy_core::Result<()> {
972        let dst = self.raw_pixels_mut();
973        if dst.len() != src.len() {
974            return Err(device_envoy_core::Error::CopySize {
975                src_len: src.len(),
976                frame_len: dst.len(),
977            });
978        }
979        dst.copy_from_slice(src);
980        Ok(())
981    }
982
983    // Flushing the panel over SPI is synchronous, so this future resolves on its
984    // first poll. The `async fn` is the device-agnostic frame boundary the
985    // render loop awaits; on the MCU it adds no suspension.
986    async fn flush(&mut self) -> Result<(), Error> {
987        CydFrameEsp::flush(self)
988    }
989}