Compare commits
3 Commits
deda456d35
...
1161733b36
| Author | SHA1 | Date | |
|---|---|---|---|
| 1161733b36 | |||
| fb4b865954 | |||
| d1fa0e20b1 |
@@ -1,36 +1,26 @@
|
||||
CompileFlags:
|
||||
Add:
|
||||
- "-std=c++17"
|
||||
- "-DARDUINO=200"
|
||||
- "-DESP32"
|
||||
- "-DCORE_DEBUG_LEVEL=0"
|
||||
- "-DBOARD_HAS_PSRAM"
|
||||
- "-DLGFX_USE_V1"
|
||||
- "-DDEBUG_MODE"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/cores/esp32"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries"
|
||||
- "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include"
|
||||
- "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/freertos/FreeRTOS-Kernel/include"
|
||||
- "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/freertos/config/include/freertos"
|
||||
- "-I/home/david/.arduino15/packages/esp32/tools/esp32-libs/3.3.6/include/freertos/config/include"
|
||||
- "-I/home/david/.arduino15/packages/arduino/hardware/arduino/1.8.6/cores/arduino"
|
||||
- "-I/home/david/.arduino15/packages/arduino/hardware/arduino/1.8.6/libraries/WiFi/src"
|
||||
- "-I/home/david/Arduino/sketchbook/libraries/ArduinoJson/src"
|
||||
- "-I/home/david/Arduino/sketchbook/libraries/NTPClient"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools/sdk/esp32/include"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/tools/sdk/esp32/include/esp_hw_support"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/WiFi/src"
|
||||
- "-I/home/david/.arduino15/packages/esp32/hardware/esp32/3.3.6/libraries/EEPROM/src"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src"
|
||||
- "-I/home/david/Arduino/libraries/TFT_eSPI"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/libraries/KlubhausCore/src"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/boards/esp32-s3-lcd-43"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src/lgfx"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src/lgfx/v0"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-s3-lcd-43/LovyanGFX/src/lgfx/v0/platforms/esp32"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/boards/esp32-32e"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/boards/esp32-32e-4"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-32e/TFT_eSPI"
|
||||
- "-I/home/david/Arduino/sketches/doorbell-touch/vendor/esp32-32e-4/TFT_eSPI"
|
||||
- "-I/home/david/Arduino/sketchbook/libraries/XPT2046_Touchscreen"
|
||||
- "-DARDUINO=200"
|
||||
- "-DESC32"
|
||||
- "-DESP_PLATFORM"
|
||||
- "-Dcore_debug=0"
|
||||
|
||||
Diagnostics:
|
||||
ClangTidy:
|
||||
Remove: [readability-*, modernize-*, performance-*, bugprone-*]
|
||||
Add: [clang-diagnostic-*, modernize-use-trailing-return-type]
|
||||
UnusedIncludes: Strict
|
||||
Add:
|
||||
- modernize-*
|
||||
- performance-*
|
||||
- readability-*
|
||||
- bugprone-*
|
||||
Remove:
|
||||
- modernize-use-trailing-return-type
|
||||
- readability-magic-numbers
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Doorbell Touch - Build Harness
|
||||
|
||||
## Session Handoff Note
|
||||
⚠️ ALWAYS check `mise.toml` for available build tasks before compiling/uploading. Run `mise tasks` to list.
|
||||
|
||||
## Quick Commands
|
||||
```bash
|
||||
BOARD=esp32-32e-4 mise run compile # Compile
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "DisplayDriverTFT.h"
|
||||
#include <KlubhausCore.h>
|
||||
|
||||
extern DisplayManager display;
|
||||
|
||||
void DisplayDriverTFT::begin() {
|
||||
// Backlight
|
||||
@@ -123,28 +126,45 @@ void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
||||
|
||||
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
||||
_tft.fillScreen(TFT_BLACK);
|
||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
|
||||
// Header
|
||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||
_tft.setTextSize(1);
|
||||
_tft.setCursor(5, 5);
|
||||
_tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState));
|
||||
_tft.printf("KLUBHAUS");
|
||||
|
||||
int y = 30;
|
||||
_tft.setCursor(5, y);
|
||||
y += 18;
|
||||
_tft.printf("WiFi: %s %ddBm", st.wifiSsid.c_str(), st.wifiRssi);
|
||||
// WiFi indicator
|
||||
_tft.setCursor(DISPLAY_WIDTH - 50, 5);
|
||||
_tft.printf("WiFi:%s", st.wifiSsid.length() > 0 ? "ON" : "OFF");
|
||||
|
||||
_tft.setCursor(5, y);
|
||||
y += 18;
|
||||
_tft.printf("IP: %s", st.ipAddr.c_str());
|
||||
// Get tile layouts from library helper
|
||||
int tileCount = display.calculateDashboardLayouts(30, 8);
|
||||
const TileLayout* layouts = display.getTileLayouts();
|
||||
|
||||
_tft.setCursor(5, y);
|
||||
y += 18;
|
||||
_tft.printf("Up: %lus Heap: %d", st.uptimeMs / 1000, ESP.getFreeHeap());
|
||||
const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" };
|
||||
const uint16_t tileColors[] = { 0x0280, 0x0400, 0x0440, 0x0100 };
|
||||
|
||||
_tft.setCursor(5, y);
|
||||
y += 18;
|
||||
_tft.printf("Last poll: %lus ago", st.lastPollMs > 0 ? (millis() - st.lastPollMs) / 1000 : 0);
|
||||
for(int i = 0; i < tileCount && i < 4; i++) {
|
||||
const TileLayout& lay = layouts[i];
|
||||
int x = lay.x;
|
||||
int y = lay.y;
|
||||
int w = lay.w;
|
||||
int h = lay.h;
|
||||
|
||||
// Tile background
|
||||
_tft.fillRoundRect(x, y, w, h, 8, tileColors[i]);
|
||||
|
||||
// Tile border
|
||||
_tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE);
|
||||
|
||||
// Tile label
|
||||
_tft.setTextColor(TFT_WHITE);
|
||||
_tft.setTextSize(2);
|
||||
int textLen = strlen(tileLabels[i]);
|
||||
int textW = textLen * 12;
|
||||
_tft.setCursor(x + w/2 - textW/2, y + h/2 - 10);
|
||||
_tft.print(tileLabels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Touch ───────────────────────────────────────────────────
|
||||
@@ -153,12 +173,32 @@ TouchEvent DisplayDriverTFT::readTouch() {
|
||||
TouchEvent evt;
|
||||
uint16_t tx, ty;
|
||||
uint8_t touched = _tft.getTouch(&tx, &ty, 100);
|
||||
if(touched) {
|
||||
|
||||
// Detect transitions (press/release)
|
||||
if(touched && !_touchWasPressed) {
|
||||
// Press transition: finger just touched down
|
||||
evt.pressed = true;
|
||||
_touchDownX = tx;
|
||||
_touchDownY = ty;
|
||||
evt.downX = _touchDownX;
|
||||
evt.downY = _touchDownY;
|
||||
} else if(!touched && _touchWasPressed) {
|
||||
// Release transition: finger just lifted
|
||||
evt.released = true;
|
||||
evt.downX = _touchDownX;
|
||||
evt.downY = _touchDownY;
|
||||
}
|
||||
|
||||
// Current position if still touched
|
||||
if(touched) {
|
||||
evt.x = tx;
|
||||
evt.y = ty;
|
||||
Serial.printf("[TOUCH] x=%d, y=%d\n", tx, ty);
|
||||
evt.downX = _touchDownX;
|
||||
evt.downY = _touchDownY;
|
||||
}
|
||||
|
||||
// Track previous state for next call
|
||||
_touchWasPressed = touched;
|
||||
return evt;
|
||||
}
|
||||
|
||||
@@ -166,18 +206,11 @@ uint16_t DisplayDriverTFT::getRawTouchZ() {
|
||||
return _tft.getTouchRawZ();
|
||||
}
|
||||
|
||||
int DisplayDriverTFT::dashboardTouch(int x, int y) {
|
||||
// 2x2 grid, accounting for 30px header
|
||||
if(y < 30)
|
||||
return -1;
|
||||
|
||||
int col = (x * 2) / DISPLAY_WIDTH; // 0 or 1
|
||||
int row = ((y - 30) * 2) / (DISPLAY_HEIGHT - 30); // 0 or 1
|
||||
|
||||
if(col < 0 || col > 1 || row < 0 || row > 1)
|
||||
return -1;
|
||||
|
||||
return row * 2 + col; // 0, 1, 2, or 3
|
||||
void DisplayDriverTFT::transformTouch(int* x, int* y) {
|
||||
// Resistive touch panel is rotated 90° vs display - swap coordinates
|
||||
int temp = *x;
|
||||
*x = *y;
|
||||
*y = temp;
|
||||
}
|
||||
|
||||
HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
|
||||
|
||||
@@ -12,11 +12,13 @@ public:
|
||||
void render(const ScreenState& state) override;
|
||||
TouchEvent readTouch() override;
|
||||
uint16_t getRawTouchZ();
|
||||
int dashboardTouch(int x, int y) override;
|
||||
HoldState updateHold(unsigned long holdMs) override;
|
||||
void updateHint(int x, int y, bool active) override;
|
||||
int width() override { return DISPLAY_WIDTH; }
|
||||
int height() override { return DISPLAY_HEIGHT; }
|
||||
int width() override { return _tft.width(); }
|
||||
int height() override { return _tft.height(); }
|
||||
|
||||
// Dashboard - uses transform for touch coordinate correction
|
||||
void transformTouch(int* x, int* y) override;
|
||||
|
||||
private:
|
||||
void drawBoot(const ScreenState& st);
|
||||
@@ -30,4 +32,9 @@ private:
|
||||
ScreenID _lastScreen = ScreenID::BOOT;
|
||||
BootStage _lastBootStage = BootStage::SPLASH;
|
||||
bool _needsRedraw = true;
|
||||
|
||||
// Touch tracking for press/release detection
|
||||
bool _touchWasPressed = false;
|
||||
int _touchDownX = -1;
|
||||
int _touchDownY = -1;
|
||||
};
|
||||
|
||||
@@ -24,11 +24,6 @@ void loop() {
|
||||
// Read touch
|
||||
TouchEvent evt = display.readTouch();
|
||||
|
||||
// Touch debug (useful for new boards)
|
||||
if(evt.pressed) {
|
||||
Serial.printf("[TOUCH] pressed: x=%d, y=%d\n", evt.x, evt.y);
|
||||
}
|
||||
|
||||
// State machine tick
|
||||
logic.update();
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
#include "board_config.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <KlubhausCore.h>
|
||||
|
||||
// ── Globals ──
|
||||
static LGFX* _gfx = nullptr;
|
||||
extern DisplayManager display;
|
||||
|
||||
// ── Forward declarations ──
|
||||
static void initDisplay();
|
||||
@@ -52,9 +54,9 @@ void DisplayDriverGFX::setBacklight(bool on) {
|
||||
}
|
||||
}
|
||||
|
||||
int DisplayDriverGFX::width() { return DISP_W; }
|
||||
int DisplayDriverGFX::width() { return _gfx ? _gfx->width() : DISP_W; }
|
||||
|
||||
int DisplayDriverGFX::height() { return DISP_H; }
|
||||
int DisplayDriverGFX::height() { return _gfx ? _gfx->height() : DISP_H; }
|
||||
|
||||
// ── Touch handling ──
|
||||
|
||||
@@ -66,16 +68,36 @@ TouchEvent DisplayDriverGFX::readTouch() {
|
||||
int32_t x, y;
|
||||
bool pressed = _gfx->getTouch(&x, &y);
|
||||
|
||||
// Only report NEW touches (debounce - ignore held touches)
|
||||
evt.pressed = pressed && !_lastTouch.pressed;
|
||||
// Detect transitions (press/release)
|
||||
if(pressed && !_lastTouch.pressed) {
|
||||
// Press transition: finger just touched down
|
||||
evt.pressed = true;
|
||||
evt.downX = static_cast<int>(x);
|
||||
evt.downY = static_cast<int>(y);
|
||||
_lastTouch.downX = evt.downX;
|
||||
_lastTouch.downY = evt.downY;
|
||||
} else if(!pressed && _lastTouch.pressed) {
|
||||
// Release transition: finger just lifted
|
||||
evt.released = true;
|
||||
evt.downX = _lastTouch.downX;
|
||||
evt.downY = _lastTouch.downY;
|
||||
}
|
||||
|
||||
// Current position if still touched
|
||||
if(pressed) {
|
||||
evt.x = static_cast<int>(x);
|
||||
evt.y = static_cast<int>(y);
|
||||
evt.downX = _lastTouch.downX;
|
||||
evt.downY = _lastTouch.downY;
|
||||
_pressStartMs = millis();
|
||||
}
|
||||
|
||||
// Track previous state
|
||||
_lastTouch.pressed = pressed;
|
||||
if(pressed) {
|
||||
_lastTouch.x = evt.x;
|
||||
_lastTouch.y = evt.y;
|
||||
}
|
||||
return evt;
|
||||
}
|
||||
|
||||
@@ -240,24 +262,18 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
||||
_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;
|
||||
// Get tile layouts from library helper
|
||||
int tileCount = display.calculateDashboardLayouts(30, 8);
|
||||
const TileLayout* layouts = display.getTileLayouts();
|
||||
|
||||
// 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;
|
||||
for(int i = 0; i < tileCount && i < 8; i++) {
|
||||
const TileLayout& lay = layouts[i];
|
||||
int x = lay.x;
|
||||
int y = lay.y;
|
||||
int w = lay.w;
|
||||
int h = lay.h;
|
||||
|
||||
// Tile background
|
||||
_gfx->fillRoundRect(x, y, w, h, 8, 0x0220);
|
||||
@@ -265,10 +281,10 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
||||
// Tile border
|
||||
_gfx->drawRoundRect(x, y, w, h, 8, 0xFFFF);
|
||||
|
||||
// Tile number
|
||||
// Tile label
|
||||
_gfx->setTextColor(0xFFFF);
|
||||
_gfx->setTextSize(2);
|
||||
_gfx->setCursor(x + w / 2 - 10, y + h / 2 - 10);
|
||||
_gfx->setCursor(x + w/2 - 10, y + h/2 - 10);
|
||||
_gfx->print(tileLabels[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ public:
|
||||
void render(const ScreenState& state) override;
|
||||
|
||||
TouchEvent readTouch() override;
|
||||
int dashboardTouch(int x, int y) override;
|
||||
HoldState updateHold(unsigned long holdMs) override;
|
||||
void updateHint(int x, int y, bool active) override;
|
||||
|
||||
@@ -29,7 +28,7 @@ private:
|
||||
void drawDashboard(const ScreenState& state);
|
||||
|
||||
// Touch handling
|
||||
TouchEvent _lastTouch = { false, 0, 0 };
|
||||
TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 };
|
||||
unsigned long _pressStartMs = 0;
|
||||
bool _isHolding = false;
|
||||
|
||||
|
||||
@@ -1,6 +1,118 @@
|
||||
#pragma once
|
||||
|
||||
#include "IDisplayDriver.h"
|
||||
#include "ScreenState.h"
|
||||
#include <cmath>
|
||||
|
||||
/// Layout helper for dashboard tiles - computes positions based on constraints
|
||||
class TileLayoutHelper {
|
||||
public:
|
||||
/// Calculate tile layouts for the given display dimensions
|
||||
/// Returns array of TileLayout (must have capacity for DASHBOARD_TILE_COUNT)
|
||||
/// Returns the number of columns and rows used
|
||||
static int calculateLayouts(
|
||||
int displayW,
|
||||
int displayH,
|
||||
int headerH,
|
||||
int margin,
|
||||
TileLayout* outLayouts,
|
||||
int* outCols,
|
||||
int* outRows
|
||||
) {
|
||||
int contentH = displayH - headerH;
|
||||
int tileCount = DASHBOARD_TILE_COUNT;
|
||||
|
||||
// Determine base grid based on display aspect ratio
|
||||
int cols, rows;
|
||||
calculateGrid(tileCount, displayW, contentH, &cols, &rows);
|
||||
|
||||
// Calculate base cell sizes
|
||||
int cellW = displayW / cols;
|
||||
int cellH = contentH / rows;
|
||||
|
||||
// Simple first-fit: place tiles in order, respecting min sizes
|
||||
// For more complex layouts, tiles could specify preferred positions
|
||||
for(int i = 0; i < tileCount; i++) {
|
||||
const DashboardTile& tile = DASHBOARD_TILES[i];
|
||||
int tileCols = tile.constraint.minCols;
|
||||
int tileRows = tile.constraint.minRows;
|
||||
|
||||
// Find next available position
|
||||
int col = 0, row = 0;
|
||||
findNextPosition(outLayouts, i, cols, rows, &col, &row);
|
||||
|
||||
// Ensure tile fits within grid
|
||||
if(col + tileCols > cols) tileCols = cols - col;
|
||||
if(row + tileRows > rows) tileRows = rows - row;
|
||||
|
||||
// Calculate pixel position
|
||||
int x = col * cellW + margin;
|
||||
int y = headerH + row * cellH + margin;
|
||||
int w = tileCols * cellW - 2 * margin;
|
||||
int h = tileRows * cellH - 2 * margin;
|
||||
|
||||
outLayouts[i] = {x, y, w, h, col, row, tileCols, tileRows};
|
||||
}
|
||||
|
||||
*outCols = cols;
|
||||
*outRows = rows;
|
||||
return tileCount;
|
||||
}
|
||||
|
||||
private:
|
||||
/// Calculate optimal grid dimensions based on display and tile constraints
|
||||
static void calculateGrid(int tileCount, int displayW, int contentH, int* outCols, int* outRows) {
|
||||
// Calculate aspect ratio to determine preferred layout
|
||||
float aspectRatio = (float)displayW / contentH;
|
||||
|
||||
// Start with simple square-ish grid
|
||||
int cols = (int)std::sqrt(tileCount * aspectRatio);
|
||||
if(cols < 1) cols = 1;
|
||||
if(cols > tileCount) cols = tileCount;
|
||||
|
||||
int rows = (tileCount + cols - 1) / cols;
|
||||
|
||||
// For wide displays (landscape), prefer more columns
|
||||
if(aspectRatio > 1.5f && tileCount <= 6) {
|
||||
cols = tileCount;
|
||||
rows = 1;
|
||||
}
|
||||
// For tall displays (portrait), prefer more rows
|
||||
else if(aspectRatio < 0.8f && tileCount <= 6) {
|
||||
rows = tileCount;
|
||||
cols = 1;
|
||||
}
|
||||
|
||||
*outCols = cols;
|
||||
*outRows = rows;
|
||||
}
|
||||
|
||||
/// Find next available grid position
|
||||
static void findNextPosition(const TileLayout* layouts, int count, int gridCols, int gridRows, int* outCol, int* outRow) {
|
||||
// Simple: find first empty cell
|
||||
// Could be enhanced to pack tightly based on tile sizes
|
||||
for(int r = 0; r < gridRows; r++) {
|
||||
for(int c = 0; c < gridCols; c++) {
|
||||
bool occupied = false;
|
||||
for(int i = 0; i < count; i++) {
|
||||
if(layouts[i].col <= c && c < layouts[i].col + layouts[i].cols &&
|
||||
layouts[i].row <= r && r < layouts[i].row + layouts[i].rows) {
|
||||
occupied = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!occupied) {
|
||||
*outCol = c;
|
||||
*outRow = r;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback: just use count position
|
||||
*outCol = count % gridCols;
|
||||
*outRow = count / gridCols;
|
||||
}
|
||||
};
|
||||
|
||||
class DisplayManager {
|
||||
public:
|
||||
@@ -18,14 +130,12 @@ public:
|
||||
}
|
||||
|
||||
void render(const ScreenState& st) {
|
||||
if(_drv)
|
||||
_drv->render(st);
|
||||
if(!_drv) return;
|
||||
_drv->render(st);
|
||||
}
|
||||
|
||||
TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent {}; }
|
||||
|
||||
int dashboardTouch(int x, int y) { return _drv ? _drv->dashboardTouch(x, y) : -1; }
|
||||
|
||||
HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState {}; }
|
||||
|
||||
void updateHint(int x, int y, bool active) {
|
||||
@@ -33,10 +143,135 @@ public:
|
||||
_drv->updateHint(x, y, active);
|
||||
}
|
||||
|
||||
int width() { return _drv ? _drv->width() : 0; }
|
||||
/// Show touch feedback - highlights the tile at given coordinates
|
||||
/// Returns true if a valid tile is being touched
|
||||
bool showTouchFeedback(int x, int y) {
|
||||
if(!_drv || _gridCols <= 0) return false;
|
||||
|
||||
// Transform touch coordinates
|
||||
_drv->transformTouch(&x, &y);
|
||||
|
||||
int headerH = 30;
|
||||
if(y < headerH) return false;
|
||||
|
||||
// Calculate which cell
|
||||
int cellW = _drv->width() / _gridCols;
|
||||
int cellH = (_drv->height() - headerH) / _gridRows;
|
||||
|
||||
int col = x / cellW;
|
||||
int row = (y - headerH) / cellH;
|
||||
|
||||
if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows)
|
||||
return false;
|
||||
|
||||
// Find which tile is at this position
|
||||
for(int i = 0; i < _tileCount; i++) {
|
||||
const TileLayout& lay = _layouts[i];
|
||||
if(lay.col <= col && col < lay.col + lay.cols &&
|
||||
lay.row <= row && lay.row + lay.rows > row) {
|
||||
// Found the tile - draw highlight via driver
|
||||
_drv->updateHint(lay.x, lay.y, true); // active=true means show feedback
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Clear touch feedback
|
||||
void clearTouchFeedback() {
|
||||
if(_drv)
|
||||
_drv->updateHint(0, 0, false); // active=false means clear
|
||||
}
|
||||
|
||||
/// Check if current position is still in same tile as touch-down
|
||||
bool isSameTile(int downX, int downY, int currentX, int currentY) const {
|
||||
if(!_drv || _gridCols <= 0 || downX < 0) return false;
|
||||
|
||||
int dx = downX, dy = downY;
|
||||
int cx = currentX, cy = currentY;
|
||||
|
||||
_drv->transformTouch(&dx, &dy);
|
||||
_drv->transformTouch(&cx, &cy);
|
||||
|
||||
int headerH = 30;
|
||||
int cellW = _drv->width() / _gridCols;
|
||||
int cellH = (_drv->height() - headerH) / _gridRows;
|
||||
|
||||
int downCol = dx / cellW;
|
||||
int downRow = (dy - headerH) / cellH;
|
||||
int curCol = cx / cellW;
|
||||
int curRow = (cy - headerH) / cellH;
|
||||
|
||||
return downCol == curCol && downRow == curRow;
|
||||
}
|
||||
|
||||
int width() { return _drv ? _drv->width() : 0; }
|
||||
int height() { return _drv ? _drv->height() : 0; }
|
||||
|
||||
/// Handle dashboard touch - returns action for tapped tile, or NONE
|
||||
TileAction handleDashboardTouch(int x, int y) const {
|
||||
if(!_drv || _gridCols <= 0) return TileAction::NONE;
|
||||
|
||||
// Transform touch coordinates (handles rotated touch panels)
|
||||
_drv->transformTouch(&x, &y);
|
||||
|
||||
int dispW = _drv->width();
|
||||
int dispH = _drv->height();
|
||||
int headerH = 30;
|
||||
|
||||
// Check if in header area
|
||||
if(y < headerH) return TileAction::NONE;
|
||||
|
||||
// Calculate which tile was touched using grid
|
||||
int cellW = dispW / _gridCols;
|
||||
int cellH = (dispH - headerH) / _gridRows;
|
||||
|
||||
int col = x / cellW;
|
||||
int row = (y - headerH) / cellH;
|
||||
|
||||
// Bounds check
|
||||
if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows) {
|
||||
return TileAction::NONE;
|
||||
}
|
||||
|
||||
// Find which tile occupies this cell
|
||||
for(int i = 0; i < _tileCount; i++) {
|
||||
const TileLayout& layout = _layouts[i];
|
||||
if(layout.col <= col && col < layout.col + layout.cols &&
|
||||
layout.row <= row && row < layout.row + layout.rows) {
|
||||
return DASHBOARD_TILES[i].action;
|
||||
}
|
||||
}
|
||||
|
||||
return TileAction::NONE;
|
||||
}
|
||||
|
||||
/// Calculate and store layouts for dashboard tiles
|
||||
/// Called by drivers who want to use the layout helper
|
||||
int calculateDashboardLayouts(int headerH = 30, int margin = 8) {
|
||||
if(!_drv) return 0;
|
||||
|
||||
_tileCount = TileLayoutHelper::calculateLayouts(
|
||||
_drv->width(),
|
||||
_drv->height(),
|
||||
headerH,
|
||||
margin,
|
||||
_layouts,
|
||||
&_gridCols,
|
||||
&_gridRows
|
||||
);
|
||||
return _tileCount;
|
||||
}
|
||||
|
||||
/// Get calculated layout for a specific tile
|
||||
const TileLayout* getTileLayouts() const { return _layouts; }
|
||||
int getGridCols() const { return _gridCols; }
|
||||
int getGridRows() const { return _gridRows; }
|
||||
|
||||
private:
|
||||
IDisplayDriver* _drv;
|
||||
TileLayout _layouts[DASHBOARD_TILE_COUNT];
|
||||
int _tileCount = 0;
|
||||
int _gridCols = 0;
|
||||
int _gridRows = 0;
|
||||
};
|
||||
|
||||
@@ -310,29 +310,62 @@ void DoorbellLogic::setScreen(ScreenID s) {
|
||||
}
|
||||
|
||||
int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
||||
if(!evt.pressed)
|
||||
return -1;
|
||||
// Handle press - show visual feedback
|
||||
if(evt.pressed) {
|
||||
// Reset inactivity timer on any touch
|
||||
_lastActivityMs = millis();
|
||||
|
||||
// Reset inactivity timer on any touch
|
||||
_lastActivityMs = millis();
|
||||
|
||||
if(_state.screen == ScreenID::OFF) {
|
||||
Serial.printf("[%lu] [TOUCH] OFF → DASHBOARD\n", millis());
|
||||
setScreen(ScreenID::DASHBOARD);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(_state.screen == ScreenID::DASHBOARD) {
|
||||
int tile = _display->dashboardTouch(evt.x, evt.y);
|
||||
if(tile >= 0) {
|
||||
Serial.printf("[DASH] Tile %d tapped\n", tile);
|
||||
if(_state.screen == ScreenID::OFF) {
|
||||
Serial.printf("[%lu] [TOUCH] OFF → DASHBOARD\n", millis());
|
||||
setScreen(ScreenID::DASHBOARD);
|
||||
return -1;
|
||||
}
|
||||
return tile;
|
||||
|
||||
// Show touch feedback on press
|
||||
if(_state.screen == ScreenID::DASHBOARD) {
|
||||
_display->showTouchFeedback(evt.x, evt.y);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(_state.screen == ScreenID::ALERT) {
|
||||
Serial.println("[TOUCH] ALERT tap");
|
||||
return -1;
|
||||
// Handle release - fire action if same tile
|
||||
if(evt.released) {
|
||||
// Clear feedback
|
||||
_display->clearTouchFeedback();
|
||||
|
||||
if(_state.screen == ScreenID::DASHBOARD) {
|
||||
// Only fire action if finger stayed on same tile
|
||||
if(evt.downX >= 0 && _display->isSameTile(evt.downX, evt.downY, evt.x, evt.y)) {
|
||||
TileAction action = _display->handleDashboardTouch(evt.downX, evt.downY);
|
||||
if(action != TileAction::NONE) {
|
||||
Serial.printf("[DASH] Action: %d\n", (int)action);
|
||||
switch(action) {
|
||||
case TileAction::ALERT:
|
||||
onAlert("Manual Alert", "Tile tap");
|
||||
break;
|
||||
case TileAction::SILENCE:
|
||||
if(_state.deviceState == DeviceState::ALERTING)
|
||||
silenceAlert();
|
||||
break;
|
||||
case TileAction::STATUS:
|
||||
heartbeat();
|
||||
break;
|
||||
case TileAction::REBOOT:
|
||||
flushStatus("REBOOT (tile)");
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return (int)action;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(_state.screen == ScreenID::ALERT) {
|
||||
Serial.println("[TOUCH] ALERT tap");
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
#include "ScreenState.h"
|
||||
|
||||
struct TouchEvent {
|
||||
bool pressed = false;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
bool pressed = false; // finger just touched down
|
||||
bool released = false; // finger just lifted (after being pressed)
|
||||
int x = 0; // current x position
|
||||
int y = 0; // current y position
|
||||
int downX = -1; // x position where touch started
|
||||
int downY = -1; // y position where touch started
|
||||
};
|
||||
|
||||
struct HoldState {
|
||||
@@ -15,6 +18,7 @@ struct HoldState {
|
||||
};
|
||||
|
||||
/// Abstract display driver — implemented per-board.
|
||||
/// Drivers own all rendering. Library provides tile layout helper via TileLayoutHelper.
|
||||
class IDisplayDriver {
|
||||
public:
|
||||
virtual ~IDisplayDriver() = default;
|
||||
@@ -27,13 +31,11 @@ public:
|
||||
|
||||
// ── Touch ──
|
||||
virtual TouchEvent readTouch() = 0;
|
||||
/// Returns tile index at (x,y), or -1 if none.
|
||||
virtual int dashboardTouch(int x, int y) = 0;
|
||||
/// Track a long-press gesture; returns progress/completion.
|
||||
virtual HoldState updateHold(unsigned long holdMs) = 0;
|
||||
/// Idle hint animation (e.g. pulsing ring) while alert is showing.
|
||||
/// @param active If true, show "holding" animation; if false, show "idle" animation.
|
||||
virtual void updateHint(int x, int y, bool active) = 0;
|
||||
virtual int width() = 0;
|
||||
virtual int height() = 0;
|
||||
|
||||
// ── Touch transform (for rotated panels) ──
|
||||
virtual void transformTouch(int* x, int* y) { /* default: no transform */ }
|
||||
};
|
||||
|
||||
@@ -7,6 +7,47 @@ enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD };
|
||||
|
||||
enum class BootStage { SPLASH, INIT_DISPLAY, INIT_NETWORK, CONNECTING_WIFI, READY, DONE };
|
||||
|
||||
/// Dashboard tile action handlers
|
||||
enum class TileAction {
|
||||
NONE,
|
||||
ALERT, // Trigger alert
|
||||
SILENCE, // Silence alert
|
||||
STATUS, // Send heartbeat/status
|
||||
REBOOT, // Reboot device
|
||||
};
|
||||
|
||||
/// Dashboard tile layout constraints for flexible grid
|
||||
struct TileConstraint {
|
||||
uint8_t minCols = 1; // minimum columns this tile needs
|
||||
uint8_t minRows = 1; // minimum rows this tile needs
|
||||
uint8_t weight = 1; // priority for growing/shrinking (higher = more flexible)
|
||||
};
|
||||
|
||||
/// Computed tile position in the grid
|
||||
struct TileLayout {
|
||||
int x, y; // pixel position
|
||||
int w, h; // pixel size
|
||||
int col, row; // grid position
|
||||
int cols, rows; // grid span
|
||||
};
|
||||
|
||||
/// Dashboard tile definitions — shared across all boards
|
||||
struct DashboardTile {
|
||||
const char* label;
|
||||
uint16_t bgColor; // RGB565
|
||||
TileAction action;
|
||||
TileConstraint constraint;
|
||||
};
|
||||
|
||||
/// Standard dashboard tiles (auto-gridded based on count and constraints)
|
||||
static constexpr DashboardTile DASHBOARD_TILES[] = {
|
||||
{ "Alert", 0x0280, TileAction::ALERT, {1, 1, 1} },
|
||||
{ "Silent", 0x0400, TileAction::SILENCE, {1, 1, 1} },
|
||||
{ "Status", 0x0440, TileAction::STATUS, {1, 1, 1} },
|
||||
{ "Reboot", 0x0100, TileAction::REBOOT, {1, 1, 1} },
|
||||
};
|
||||
static constexpr int DASHBOARD_TILE_COUNT = sizeof(DASHBOARD_TILES) / sizeof(DASHBOARD_TILES[0]);
|
||||
|
||||
struct ScreenState {
|
||||
DeviceState deviceState = DeviceState::BOOTED;
|
||||
ScreenID screen = ScreenID::BOOT;
|
||||
|
||||
@@ -27,14 +27,27 @@ arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-propert
|
||||
|
||||
[tasks.upload]
|
||||
description = "Upload (uses BOARD env var)"
|
||||
depends = ["compile"]
|
||||
run = """
|
||||
# Kill any processes using the serial port first
|
||||
source ./boards/$BOARD/board-config.sh
|
||||
PORT="${PORT:-$PORT}"
|
||||
fuser -k "$PORT" 2>/dev/null || true
|
||||
for pid in $(pgrep -f "monitor-agent.py" 2>/dev/null || true); do
|
||||
kill "$pid" 2>/dev/null || true
|
||||
done
|
||||
rm -f "/tmp/doorbell-${BOARD}.lock" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
source ./scripts/lockfile.sh
|
||||
|
||||
FORCE=1 TASK_NAME=upload acquire_lock || exit 1
|
||||
|
||||
PORT="${PORT:-$PORT}"
|
||||
arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --warnings default ./boards/$BOARD && \
|
||||
arduino-cli upload --fqbn "$FQBN" --port "$PORT" ./boards/$BOARD
|
||||
|
||||
# Restart monitor in background
|
||||
python3 ./scripts/monitor-agent.py "$BOARD" &
|
||||
echo "[OK] Monitor restarted in background"
|
||||
"""
|
||||
|
||||
[tasks.monitor-raw]
|
||||
@@ -62,10 +75,25 @@ tio --map INLCRNL "$TARGET" -e
|
||||
|
||||
[tasks.kill]
|
||||
description = "Kill running monitor/upload for BOARD"
|
||||
run = """
|
||||
source ./scripts/lockfile.sh
|
||||
kill_locked
|
||||
"""
|
||||
run = '''
|
||||
set +e
|
||||
# Kill any processes using the serial port
|
||||
source ./boards/$BOARD/board-config.sh
|
||||
PORT="${PORT:-$PORT}"
|
||||
fuser -k "$PORT" 2>/dev/null
|
||||
|
||||
# Kill monitor-agent processes for this board
|
||||
for pid in $(pgrep -f "monitor-agent.py"); do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
|
||||
# Also clean up lockfile
|
||||
rm -f "/tmp/doorbell-${BOARD}.lock" 2>/dev/null
|
||||
|
||||
sleep 1
|
||||
echo "[OK] Killed processes for $BOARD"
|
||||
exit 0
|
||||
'''
|
||||
|
||||
[tasks.monitor]
|
||||
description = "Monitor agent with JSON log + command pipe (Python-based)"
|
||||
|
||||
@@ -37,8 +37,8 @@ release_lock() {
|
||||
|
||||
kill_locked() {
|
||||
if [ ! -f "$LOCKFILE" ]; then
|
||||
echo "No lockfile found: $LOCKFILE"
|
||||
return 1
|
||||
echo "No lockfile found: $LOCKFILE (nothing to kill)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
OLD_PID=$(cat "$LOCKFILE" 2>/dev/null)
|
||||
|
||||
@@ -66,7 +66,7 @@ def cmd_reader():
|
||||
if ready:
|
||||
cmd = fifo.read().strip()
|
||||
if cmd:
|
||||
ser.write((cmd + "\r").encode())
|
||||
ser.write((cmd + "\n").encode())
|
||||
print(f"[SENT] {cmd}")
|
||||
except Exception as e:
|
||||
if cmd_running:
|
||||
|
||||
Reference in New Issue
Block a user