Skip to main content

device_envoy_esp/
cyd.rs

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