From c292b2cf54e2737cf32500f9b86a50b51dac7b7f Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Mon, 16 Feb 2026 12:38:13 -0800 Subject: [PATCH] feat: add display driver abstraction layer Gfx / GfxSprite types: - TFT_eSPI path: zero-cost typedefs to TFT_eSPI / TFT_eSprite - Arduino_GFX path: adapter classes with TFT_eSPI-compatible API DisplayDriverGFX.cpp compiles to nothing on TFT_eSPI targets. GfxSprite on Arduino_GFX uses direct-draw (no offscreen buffer yet). --- .../doorbell-touch-esp32-32e/DisplayDriver.h | 117 +++++++++++ .../DisplayDriverGFX.cpp | 185 ++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 sketches/doorbell-touch-esp32-32e/DisplayDriver.h create mode 100644 sketches/doorbell-touch-esp32-32e/DisplayDriverGFX.cpp diff --git a/sketches/doorbell-touch-esp32-32e/DisplayDriver.h b/sketches/doorbell-touch-esp32-32e/DisplayDriver.h new file mode 100644 index 0000000..21f3d26 --- /dev/null +++ b/sketches/doorbell-touch-esp32-32e/DisplayDriver.h @@ -0,0 +1,117 @@ +#pragma once +#include "BoardConfig.h" + +// ═══════════════════════════════════════════════════════════════════ +// Display driver abstraction +// +// TFT_eSPI path: zero-cost typedefs — compiles identically to before +// Arduino_GFX path: adapter classes providing TFT_eSPI-compatible API +// ═══════════════════════════════════════════════════════════════════ + +#if USE_TFT_ESPI +// ───────────────────────────────────────────────────────────────── +// TFT_eSPI — straight typedefs, zero overhead +// ───────────────────────────────────────────────────────────────── +#include + +using Gfx = TFT_eSPI; +using GfxSprite = TFT_eSprite; + +#elif USE_ARDUINO_GFX +// ───────────────────────────────────────────────────────────────── +// Arduino_GFX — adapter wrapping Arduino_GFX with a +// TFT_eSPI-compatible interface for Dashboard / DisplayManager +// ───────────────────────────────────────────────────────────────── +#include + +// Text datum constants (matching TFT_eSPI definitions) +#ifndef MC_DATUM +#define TL_DATUM 0 +#define TC_DATUM 1 +#define TR_DATUM 2 +#define ML_DATUM 3 +#define CL_DATUM 3 +#define MC_DATUM 4 +#define CC_DATUM 4 +#define MR_DATUM 5 +#define CR_DATUM 5 +#define BL_DATUM 6 +#define BC_DATUM 7 +#define BR_DATUM 8 +#endif + +class Gfx; // forward declaration for GfxSprite + +// ── Sprite adapter ────────────────────────────────────────────── +class GfxSprite { +public: + explicit GfxSprite(Gfx* parent); + + void createSprite(int16_t w, int16_t h); + void deleteSprite(); + void fillSprite(uint16_t color); + void fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t color); + void drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t color); + void setTextColor(uint16_t fg, uint16_t bg); + void setTextFont(uint8_t font); + void setTextSize(uint8_t size); + void setTextDatum(uint8_t datum); + void drawString(const char* str, int32_t x, int32_t y); + void pushSprite(int32_t x, int32_t y); + +private: + Gfx* _parent; + int16_t _w = 0; + int16_t _h = 0; + int32_t _pushX = 0; + int32_t _pushY = 0; + uint8_t _textDatum = TL_DATUM; + uint8_t _textSize = 1; + uint16_t _textFg = 0xFFFF; + uint16_t _textBg = 0x0000; +}; + +// ── Display adapter ───────────────────────────────────────────── +class Gfx { +public: + Gfx(); + + void init(); + void setRotation(uint8_t r); + + // Drawing primitives + void fillScreen(uint16_t color); + void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, + uint16_t color); + void fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t color); + void drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t color); + void drawFastVLine(int32_t x, int32_t y, int32_t h, uint16_t color); + + // Text (datum-based API matching TFT_eSPI) + void setTextColor(uint16_t fg, uint16_t bg); + void setTextFont(uint8_t font); + void setTextSize(uint8_t size); + void setTextDatum(uint8_t datum); + void drawString(const char* str, int32_t x, int32_t y); + int16_t textWidth(const char* str); + void setCursor(int32_t x, int32_t y); + void print(const char* str); + + // Escape hatch for direct Arduino_GFX access + Arduino_GFX* raw() { return _gfx; } + +private: + Arduino_GFX* _gfx = nullptr; + uint8_t _textDatum = TL_DATUM; + uint8_t _textSize = 1; + uint16_t _textFg = 0xFFFF; + uint16_t _textBg = 0x0000; +}; + +#else +#error "No display driver selected — check BoardConfig.h" +#endif diff --git a/sketches/doorbell-touch-esp32-32e/DisplayDriverGFX.cpp b/sketches/doorbell-touch-esp32-32e/DisplayDriverGFX.cpp new file mode 100644 index 0000000..95aa1f2 --- /dev/null +++ b/sketches/doorbell-touch-esp32-32e/DisplayDriverGFX.cpp @@ -0,0 +1,185 @@ +// ═══════════════════════════════════════════════════════════════════ +// Arduino_GFX adapter implementation +// Only compiled when USE_ARDUINO_GFX is set (Waveshare path). +// For the TFT_eSPI path this file compiles to nothing. +// ═══════════════════════════════════════════════════════════════════ +#include "BoardConfig.h" + +#if USE_ARDUINO_GFX + +#include "DisplayDriver.h" + +// ───────────────────────────────────────────────────────────────── +// Gfx adapter +// ───────────────────────────────────────────────────────────────── + +Gfx::Gfx() {} + +void Gfx::init() { + // Waveshare ESP32-S3 Touch LCD 4.3" — RGB parallel, ST7262 panel + Arduino_ESP32RGBPanel *rgbpanel = new Arduino_ESP32RGBPanel( + LCD_DE, LCD_VSYNC, LCD_HSYNC, LCD_PCLK, + LCD_R0, LCD_R1, LCD_R2, LCD_R3, LCD_R4, + LCD_G0, LCD_G1, LCD_G2, LCD_G3, LCD_G4, LCD_G5, + LCD_B0, LCD_B1, LCD_B2, LCD_B3, LCD_B4, + 1 /* hsync_polarity */, 46 /* hsync_front_porch */, + 2 /* hsync_pulse_width */, 44 /* hsync_back_porch */, + 1 /* vsync_polarity */, 50 /* vsync_front_porch */, + 16 /* vsync_pulse_width */, 16 /* vsync_back_porch */ + ); + + _gfx = new Arduino_RGB_Display( + SCREEN_WIDTH, SCREEN_HEIGHT, rgbpanel, + DISPLAY_ROTATION, true /* auto_flush */); + + if (!_gfx->begin()) { + Serial.println("[GFX] Display init FAILED"); + return; + } + Serial.printf("[GFX] Display OK: %dx%d\n", SCREEN_WIDTH, SCREEN_HEIGHT); +} + +void Gfx::setRotation(uint8_t r) { if (_gfx) _gfx->setRotation(r); } +void Gfx::fillScreen(uint16_t c) { if (_gfx) _gfx->fillScreen(c); } + +void Gfx::fillRect(int32_t x, int32_t y, int32_t w, int32_t h, uint16_t c) { + if (_gfx) _gfx->fillRect(x, y, w, h, c); +} +void Gfx::fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t c) { + if (_gfx) _gfx->fillRoundRect(x, y, w, h, r, c); +} +void Gfx::drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t c) { + if (_gfx) _gfx->drawRoundRect(x, y, w, h, r, c); +} +void Gfx::drawFastVLine(int32_t x, int32_t y, int32_t h, uint16_t c) { + if (_gfx) _gfx->drawFastVLine(x, y, h, c); +} + +void Gfx::setTextColor(uint16_t fg, uint16_t bg) { + _textFg = fg; _textBg = bg; + if (_gfx) _gfx->setTextColor(fg, bg); +} + +void Gfx::setTextFont(uint8_t font) { + // TFT_eSPI font IDs don't map 1:1 to Arduino_GFX. + // Using default built-in font; setTextSize controls scale. + // TODO: Map to GFXfont pointers for better visual fidelity. + (void)font; +} + +void Gfx::setTextSize(uint8_t s) { + _textSize = s; + if (_gfx) _gfx->setTextSize(s); +} + +void Gfx::setTextDatum(uint8_t d) { _textDatum = d; } + +void Gfx::drawString(const char* str, int32_t x, int32_t y) { + if (!_gfx || !str) return; + + int16_t bx, by; + uint16_t tw, th; + _gfx->getTextBounds(str, 0, 0, &bx, &by, &tw, &th); + + // Horizontal alignment from datum + int hAlign = _textDatum % 3; + if (hAlign == 1) x -= (int32_t)tw / 2; // center + else if (hAlign == 2) x -= (int32_t)tw; // right + + // Vertical alignment from datum + int vAlign = _textDatum / 3; + if (vAlign == 1) y -= (int32_t)th / 2; // middle + else if (vAlign == 2) y -= (int32_t)th; // bottom + + _gfx->setCursor(x - bx, y - by); + _gfx->print(str); +} + +int16_t Gfx::textWidth(const char* str) { + if (!_gfx || !str) return 0; + int16_t bx, by; + uint16_t tw, th; + _gfx->getTextBounds(str, 0, 0, &bx, &by, &tw, &th); + return (int16_t)tw; +} + +void Gfx::setCursor(int32_t x, int32_t y) { + if (_gfx) _gfx->setCursor(x, y); +} + +void Gfx::print(const char* str) { + if (_gfx) _gfx->print(str); +} + +// ───────────────────────────────────────────────────────────────── +// GfxSprite adapter +// +// On the 800x480 RGB panel the LCD controller has its own GRAM, +// so direct drawing rarely tears. This implementation is +// intentionally minimal — it renders tiles as direct draws. +// +// TODO: For flicker-free tile updates, allocate an Arduino_Canvas +// backed by PSRAM and blit via draw16bitBeRGBBitmap(). +// ───────────────────────────────────────────────────────────────── + +GfxSprite::GfxSprite(Gfx* parent) : _parent(parent) {} + +void GfxSprite::createSprite(int16_t w, int16_t h) { + _w = w; _h = h; + Serial.printf("[GFX] Sprite %dx%d created (direct-draw mode)\n", w, h); +} + +void GfxSprite::deleteSprite() { _w = 0; _h = 0; } + +void GfxSprite::fillSprite(uint16_t color) { + if (_parent && _parent->raw()) + _parent->raw()->fillRect(_pushX, _pushY, _w, _h, color); +} + +void GfxSprite::fillRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t c) { + if (_parent && _parent->raw()) + _parent->raw()->fillRoundRect(_pushX + x, _pushY + y, w, h, r, c); +} + +void GfxSprite::drawRoundRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t r, uint16_t c) { + if (_parent && _parent->raw()) + _parent->raw()->drawRoundRect(_pushX + x, _pushY + y, w, h, r, c); +} + +void GfxSprite::setTextColor(uint16_t fg, uint16_t bg) { + _textFg = fg; _textBg = bg; + if (_parent && _parent->raw()) _parent->raw()->setTextColor(fg, bg); +} + +void GfxSprite::setTextFont(uint8_t font) { (void)font; } + +void GfxSprite::setTextSize(uint8_t size) { + _textSize = size; + if (_parent && _parent->raw()) _parent->raw()->setTextSize(size); +} + +void GfxSprite::setTextDatum(uint8_t datum) { _textDatum = datum; } + +void GfxSprite::drawString(const char* str, int32_t x, int32_t y) { + if (!_parent) return; + _parent->setTextDatum(_textDatum); + _parent->setTextSize(_textSize); + _parent->setTextColor(_textFg, _textBg); + _parent->drawString(str, _pushX + x, _pushY + y); +} + +void GfxSprite::pushSprite(int32_t x, int32_t y) { + // Record the offset for subsequent draw calls in the next cycle. + // NOTE: In the TFT_eSPI path, sprite drawing happens BEFORE pushSprite + // (draws into offscreen buffer, then blits). In this direct-draw stub, + // the offset from the PREVIOUS pushSprite call is used. After one full + // drawAll() cycle, all tiles render at the correct positions. + _pushX = x; + _pushY = y; +} + +#endif // USE_ARDUINO_GFX