1. **Added `active` parameter to hint animation** - `updateHint()` now accepts a boolean `active` parameter across both display drivers (TFT and GFX) - When `active=true`: faster pulse animation (500ms period) during active hold - When `active=false`: slower pulse animation (2000ms period) during idle state 2. **Improved animation calculations** - Replaced modulo operator with `fmodf()` for cleaner float calculations - Standardized to `static_cast<uint8_t>()` for type conversions - Fixed GFX driver to use `color565()` method instead of manual bit shifting 3. **Updated hint display logic** - Now differentiates between "holding" state (fast pulse) and "idle" state (slow pulse) - Hint draws at both states when `holdStartX >= 0` (touch position captured) 4. **Added code formatter task** - New `mise.toml` task for running clang-format across all source files - Users get **visual feedback differentiation**: fast pulsing during active hold vs. slow pulsing when idle - More intuitive UI that clearly indicates whether a long-press is in progress or just waiting - Cleaner, more maintainable code with standardized calculations and type conversions
260 lines
6.3 KiB
C++
260 lines
6.3 KiB
C++
// boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp
|
||
#include "DisplayDriverGFX.h"
|
||
|
||
#include "LovyanPins.h"
|
||
#include "board_config.h"
|
||
|
||
#include <Arduino.h>
|
||
|
||
// ── Globals ──
|
||
static LGFX* _gfx = nullptr;
|
||
|
||
// ── Forward declarations ──
|
||
static void initDisplay();
|
||
|
||
// ── Dimensions ──
|
||
static constexpr int DISP_W = 800;
|
||
static constexpr int DISP_H = 480;
|
||
|
||
// ── Display initialization ──
|
||
static void initDisplay() {
|
||
Serial.println("LovyanGFX init...");
|
||
|
||
// Note: LovyanGFX handles I2C internally (port 1 for touch, port 0 for CH422G)
|
||
// No need to call Wire.begin() or Wire1.begin()
|
||
|
||
_gfx = new LGFX();
|
||
_gfx->init();
|
||
_gfx->setRotation(0); // Landscape
|
||
_gfx->fillScreen(0x000000);
|
||
|
||
Serial.println("Display ready");
|
||
}
|
||
|
||
// ── Singleton ──
|
||
DisplayDriverGFX& DisplayDriverGFX::instance() {
|
||
static DisplayDriverGFX inst;
|
||
return inst;
|
||
}
|
||
|
||
// ── IDisplayDriver implementation ──
|
||
|
||
void DisplayDriverGFX::begin() {
|
||
initDisplay();
|
||
// Turn on backlight immediately
|
||
setBacklight(true);
|
||
}
|
||
|
||
void DisplayDriverGFX::setBacklight(bool on) {
|
||
if(_gfx) {
|
||
// LovyanGFX handles backlight via setBrightness
|
||
_gfx->setBrightness(on ? 255 : 0);
|
||
}
|
||
}
|
||
|
||
int DisplayDriverGFX::width() { return DISP_W; }
|
||
|
||
int DisplayDriverGFX::height() { return DISP_H; }
|
||
|
||
// ── Touch handling ──
|
||
|
||
TouchEvent DisplayDriverGFX::readTouch() {
|
||
TouchEvent evt;
|
||
if(!_gfx)
|
||
return evt;
|
||
|
||
int32_t x, y;
|
||
bool pressed = _gfx->getTouch(&x, &y);
|
||
|
||
// Only report NEW touches (debounce - ignore held touches)
|
||
evt.pressed = pressed && !_lastTouch.pressed;
|
||
|
||
if(pressed) {
|
||
evt.x = static_cast<int>(x);
|
||
evt.y = static_cast<int>(y);
|
||
_pressStartMs = millis();
|
||
}
|
||
|
||
_lastTouch.pressed = pressed;
|
||
return evt;
|
||
}
|
||
|
||
int DisplayDriverGFX::dashboardTouch(int x, int y) {
|
||
// Dashboard tiles: 2 rows × 4 columns
|
||
constexpr int cols = 4;
|
||
constexpr int rows = 2;
|
||
constexpr int tileW = DISP_W / cols;
|
||
constexpr int tileH = DISP_H / rows;
|
||
|
||
if(x < 0 || x >= DISP_W || y < 0 || y >= DISP_H) {
|
||
return -1;
|
||
}
|
||
|
||
int col = x / tileW;
|
||
int row = y / tileH;
|
||
|
||
return row * cols + col;
|
||
}
|
||
|
||
HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) {
|
||
HoldState state;
|
||
|
||
if(!_lastTouch.pressed) {
|
||
_isHolding = false;
|
||
return state;
|
||
}
|
||
|
||
unsigned long elapsed = millis() - _pressStartMs;
|
||
|
||
if(!_isHolding) {
|
||
_isHolding = true;
|
||
state.started = true;
|
||
}
|
||
|
||
state.active = true;
|
||
state.progress = static_cast<float>(elapsed) / static_cast<float>(holdMs);
|
||
|
||
if(state.progress >= 1.0f) {
|
||
state.progress = 1.0f;
|
||
state.completed = true;
|
||
}
|
||
|
||
return state;
|
||
}
|
||
|
||
// ── Rendering ──
|
||
|
||
void DisplayDriverGFX::render(const ScreenState& state) {
|
||
if(!_gfx)
|
||
return;
|
||
|
||
// Check if we need full redraw
|
||
if(state.screen != _lastScreen) {
|
||
_needsRedraw = true;
|
||
_lastScreen = state.screen;
|
||
}
|
||
|
||
switch(state.screen) {
|
||
case ScreenID::BOOT:
|
||
if(_needsRedraw) {
|
||
_gfx->fillScreen(0x000000);
|
||
_gfx->setTextColor(0xFFFF);
|
||
_gfx->setTextSize(2);
|
||
_gfx->setCursor(10, 10);
|
||
_gfx->print("KLUBHAUS BOOT");
|
||
_needsRedraw = false;
|
||
}
|
||
break;
|
||
|
||
case ScreenID::OFF:
|
||
if(_needsRedraw) {
|
||
_gfx->fillScreen(0x000000);
|
||
_needsRedraw = false;
|
||
}
|
||
break;
|
||
|
||
case ScreenID::ALERT:
|
||
// Only redraw on first entry or screen change
|
||
if(_needsRedraw) {
|
||
drawAlert(state);
|
||
_needsRedraw = false;
|
||
}
|
||
break;
|
||
|
||
case ScreenID::DASHBOARD:
|
||
if(_needsRedraw) {
|
||
drawDashboard(state);
|
||
_needsRedraw = false;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
|
||
void DisplayDriverGFX::drawAlert(const ScreenState& state) {
|
||
uint32_t elapsed = millis() - state.alertStartMs;
|
||
uint8_t pulse = static_cast<uint8_t>(180.0f + 75.0f * sinf(elapsed / 300.0f));
|
||
uint16_t bg = _gfx->color565(pulse, 0, 0);
|
||
|
||
_gfx->fillScreen(bg);
|
||
_gfx->setTextColor(0xFFFF, bg);
|
||
|
||
_gfx->setTextSize(3);
|
||
_gfx->setCursor(10, 20);
|
||
_gfx->print(state.alertTitle.length() > 0 ? state.alertTitle.c_str() : "ALERT");
|
||
|
||
_gfx->setTextSize(2);
|
||
_gfx->setCursor(10, 80);
|
||
_gfx->print(state.alertBody);
|
||
|
||
_gfx->setTextSize(1);
|
||
_gfx->setCursor(10, DISP_H - 20);
|
||
_gfx->print("Hold to silence...");
|
||
}
|
||
|
||
void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
||
_gfx->fillScreen(0x001030); // Dark blue
|
||
|
||
// Header
|
||
_gfx->fillRect(0, 0, DISP_W, 30, 0x1A1A); // Dark gray
|
||
_gfx->setFont(&fonts::Font0); // Built-in minimal font
|
||
_gfx->setTextColor(0xFFFF);
|
||
_gfx->setTextSize(1);
|
||
_gfx->setCursor(5, 10);
|
||
_gfx->printf("KLUBHAUS");
|
||
|
||
// WiFi status
|
||
_gfx->setCursor(DISP_W - 100, 10);
|
||
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
|
||
|
||
// Tiles: 2 rows × 4 columns
|
||
constexpr int cols = 4;
|
||
constexpr int rows = 2;
|
||
constexpr int tileW = DISP_W / cols;
|
||
constexpr int tileH = (DISP_H - 30) / rows;
|
||
constexpr int margin = 8;
|
||
|
||
// Draw placeholder tiles (8 total for 2x4 grid)
|
||
const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" };
|
||
|
||
for(int i = 0; i < 8; i++) {
|
||
int col = i % cols;
|
||
int row = i / cols;
|
||
|
||
int x = col * tileW + margin;
|
||
int y = 30 + row * tileH + margin;
|
||
int w = tileW - 2 * margin;
|
||
int h = tileH - 2 * margin;
|
||
|
||
// Tile background
|
||
_gfx->fillRoundRect(x, y, w, h, 8, 0x0220);
|
||
|
||
// Tile border
|
||
_gfx->drawRoundRect(x, y, w, h, 8, 0xFFFF);
|
||
|
||
// Tile number
|
||
_gfx->setTextColor(0xFFFF);
|
||
_gfx->setTextSize(2);
|
||
_gfx->setCursor(x + w / 2 - 10, y + h / 2 - 10);
|
||
_gfx->print(tileLabels[i]);
|
||
}
|
||
}
|
||
|
||
void DisplayDriverGFX::updateHint(int x, int y, bool active) {
|
||
if(!_gfx)
|
||
return;
|
||
|
||
static uint32_t lastTime = 0;
|
||
uint32_t now = millis();
|
||
if(now - lastTime < 100)
|
||
return;
|
||
lastTime = now;
|
||
|
||
// active=true: faster pulse (500ms), active=false: slower pulse (2000ms)
|
||
float period = active ? 500.0f : 2000.0f;
|
||
float t = fmodf(now, period) / period;
|
||
uint8_t v = static_cast<uint8_t>(30.0f + 30.0f * sinf(t * 2.0f * 3.14159f));
|
||
uint16_t col = _gfx->color565(v, v, v);
|
||
|
||
_gfx->drawCircle(x, y, 50, col);
|
||
}
|