diff --git a/.crushmemory b/.crushmemory index c562f81..8415ace 100644 --- a/.crushmemory +++ b/.crushmemory @@ -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 diff --git a/boards/esp32-32e-4/DisplayDriverTFT.cpp b/boards/esp32-32e-4/DisplayDriverTFT.cpp index 40a6e52..81c1c33 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.cpp +++ b/boards/esp32-32e-4/DisplayDriverTFT.cpp @@ -1,4 +1,7 @@ #include "DisplayDriverTFT.h" +#include + +extern DisplayManager display; void DisplayDriverTFT::begin() { // Backlight @@ -134,25 +137,19 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) { _tft.setCursor(DISPLAY_WIDTH - 50, 5); _tft.printf("WiFi:%s", st.wifiSsid.length() > 0 ? "ON" : "OFF"); - // Render tiles via DisplayManager (abstracted to library) - // For now, draw directly using our tile implementation - constexpr int cols = 2; - constexpr int rows = 2; - constexpr int tileW = DISPLAY_WIDTH / cols; - constexpr int tileH = (DISPLAY_HEIGHT - 30) / rows; - constexpr int margin = 8; + // Get tile layouts from library helper + int tileCount = display.calculateDashboardLayouts(30, 8); + const TileLayout* layouts = display.getTileLayouts(); const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" }; const uint16_t tileColors[] = { 0x0280, 0x0400, 0x0440, 0x0100 }; - for(int i = 0; i < 4; 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 < 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]); @@ -180,7 +177,6 @@ TouchEvent DisplayDriverTFT::readTouch() { evt.pressed = true; evt.x = tx; evt.y = ty; - Serial.printf("[TOUCH] x=%d, y=%d\n", tx, ty); } return evt; } @@ -196,22 +192,6 @@ void DisplayDriverTFT::transformTouch(int* x, int* y) { *y = temp; } -void DisplayDriverTFT::drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) { - // Tile background - _tft.fillRoundRect(x, y, w, h, 8, bgColor); - - // Tile border - _tft.drawRoundRect(x, y, w, h, 8, TFT_WHITE); - - // Tile label - _tft.setTextColor(TFT_WHITE); - _tft.setTextSize(2); - int textLen = strlen(label); - int textW = textLen * 12; // approx width - _tft.setCursor(x + w/2 - textW/2, y + h/2 - 10); - _tft.print(label); -} - HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) { HoldState h; TouchEvent t = readTouch(); diff --git a/boards/esp32-32e-4/DisplayDriverTFT.h b/boards/esp32-32e-4/DisplayDriverTFT.h index 2dc2abc..7654426 100644 --- a/boards/esp32-32e-4/DisplayDriverTFT.h +++ b/boards/esp32-32e-4/DisplayDriverTFT.h @@ -14,12 +14,11 @@ public: uint16_t getRawTouchZ(); 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 tiles - library handles grid math, we just draw + // Dashboard - uses transform for touch coordinate correction void transformTouch(int* x, int* y) override; - void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) override; private: void drawBoot(const ScreenState& st); diff --git a/boards/esp32-32e-4/esp32-32e-4.ino b/boards/esp32-32e-4/esp32-32e-4.ino index 065c08d..78971d9 100644 --- a/boards/esp32-32e-4/esp32-32e-4.ino +++ b/boards/esp32-32e-4/esp32-32e-4.ino @@ -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(); diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp index 235c6a7..ba9251f 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp @@ -5,9 +5,11 @@ #include "board_config.h" #include +#include // ── 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 ── @@ -96,22 +98,6 @@ int DisplayDriverGFX::dashboardTouch(int x, int y) { return row * cols + col; } -void DisplayDriverGFX::drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) { - // Tile background (use fillRect - LovyanGFX may not have fillRoundRect) - _gfx->fillRect(x, y, w, h, bgColor); - - // Tile border - _gfx->drawRect(x, y, w, h, 0xFFFF); - - // Tile label - _gfx->setTextColor(0xFFFF); - _gfx->setTextSize(2); - int textLen = strlen(label); - int textW = textLen * 12; - _gfx->setCursor(x + w/2 - textW/2, y + h/2 - 10); - _gfx->print(label); -} - HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) { HoldState state; @@ -256,24 +242,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); @@ -281,10 +261,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]); } } diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h index d5d89d6..b0f8ec7 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h @@ -18,9 +18,6 @@ public: int width() override; int height() override; - // Dashboard tiles - library handles grid math, we just draw - void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) override; - // ── Internal ── static DisplayDriverGFX& instance(); diff --git a/libraries/KlubhausCore/src/DisplayManager.h b/libraries/KlubhausCore/src/DisplayManager.h index fb6e05d..7441866 100644 --- a/libraries/KlubhausCore/src/DisplayManager.h +++ b/libraries/KlubhausCore/src/DisplayManager.h @@ -2,6 +2,117 @@ #include "IDisplayDriver.h" #include "ScreenState.h" +#include + +/// 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: @@ -19,8 +130,8 @@ public: } void render(const ScreenState& st) { - if(_drv) - _drv->render(st); + if(!_drv) return; + _drv->render(st); } TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent {}; } @@ -35,75 +146,6 @@ public: int width() { return _drv ? _drv->width() : 0; } int height() { return _drv ? _drv->height() : 0; } - /// Auto-calculate grid dimensions for dashboard tiles - /// Prefers landscape (more columns than rows) for wide displays - void getTileGrid(int tileCount, int* outRows, int* outCols) const { - if(tileCount <= 0) { - *outRows = *outCols = 0; - return; - } - // Calculate cols: try to make it wider than tall - int cols = (int)sqrt(tileCount); - if(cols * cols < tileCount) cols++; - - // Make cols >= rows for landscape preference - while(cols * ((tileCount + cols - 1) / cols) < tileCount) { - cols++; - } - - int rows = (tileCount + cols - 1) / cols; - - // If display is wider, prefer more columns - if(_drv) { - int w = _drv->width(); - int h = _drv->height(); - if(w > h) { - // Landscape display - maximize columns - cols = tileCount; - rows = 1; - } - } - - *outRows = rows; - *outCols = cols; - } - - /// Render all dashboard tiles with auto-calculated grid - void renderDashboard(const ScreenState& st) { - if(!_drv) return; - - int rows, cols; - getTileGrid(DASHBOARD_TILE_COUNT, &rows, &cols); - - int dispW = _drv->width(); - int dispH = _drv->height(); - - // Reserve space for header - int headerH = 30; - int contentH = dispH - headerH; - - // Calculate tile sizes - int tileW = dispW / cols; - int tileH = contentH / rows; - int margin = 8; - - for(int i = 0; i < DASHBOARD_TILE_COUNT; i++) { - int row = i / cols; - int col = i % cols; - - int x = col * tileW + margin; - int y = headerH + row * tileH + margin; - int w = tileW - 2 * margin; - int h = tileH - 2 * margin; - - _drv->drawTileAt(x, y, w, h, DASHBOARD_TILES[i].label, DASHBOARD_TILES[i].bgColor); - } - - // Store grid for touch calculations - _gridRows = rows; - _gridCols = cols; - } - /// Handle dashboard touch - returns action for tapped tile, or NONE TileAction handleDashboardTouch(int x, int y) const { if(!_drv || _gridCols <= 0) return TileAction::NONE; @@ -118,29 +160,56 @@ public: // Check if in header area if(y < headerH) return TileAction::NONE; - // Calculate which tile was touched - int tileW = dispW / _gridCols; - int contentH = dispH - headerH; - int tileH = contentH / _gridRows; + // Calculate which tile was touched using grid + int cellW = dispW / _gridCols; + int cellH = (dispH - headerH) / _gridRows; - int col = x / tileW; - int row = (y - headerH) / tileH; + int col = x / cellW; + int row = (y - headerH) / cellH; // Bounds check if(col < 0 || col >= _gridCols || row < 0 || row >= _gridRows) { return TileAction::NONE; } - int index = row * _gridCols + col; - if(index < 0 || index >= DASHBOARD_TILE_COUNT) { - 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 DASHBOARD_TILES[index].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; - mutable int _gridRows = 0; - mutable int _gridCols = 0; + TileLayout _layouts[DASHBOARD_TILE_COUNT]; + int _tileCount = 0; + int _gridCols = 0; + int _gridRows = 0; }; diff --git a/libraries/KlubhausCore/src/IDisplayDriver.h b/libraries/KlubhausCore/src/IDisplayDriver.h index 4049c18..3409336 100644 --- a/libraries/KlubhausCore/src/IDisplayDriver.h +++ b/libraries/KlubhausCore/src/IDisplayDriver.h @@ -15,6 +15,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,17 +28,11 @@ public: // ── Touch ── virtual TouchEvent readTouch() = 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; - // ── Dashboard tiles ── - /// Transform raw touch coordinates (for rotated touch panels) + // ── Touch transform (for rotated panels) ── virtual void transformTouch(int* x, int* y) { /* default: no transform */ } - /// Draw a tile at specified position (library handles grid math) - virtual void drawTileAt(int x, int y, int w, int h, const char* label, uint16_t bgColor) = 0; }; diff --git a/libraries/KlubhausCore/src/ScreenState.h b/libraries/KlubhausCore/src/ScreenState.h index 9997e5e..b114404 100644 --- a/libraries/KlubhausCore/src/ScreenState.h +++ b/libraries/KlubhausCore/src/ScreenState.h @@ -16,19 +16,35 @@ enum class TileAction { 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) +/// Standard dashboard tiles (auto-gridded based on count and constraints) static constexpr DashboardTile DASHBOARD_TILES[] = { - { "Alert", 0x0280, TileAction::ALERT }, - { "Silent", 0x0400, TileAction::SILENCE }, - { "Status", 0x0440, TileAction::STATUS }, - { "Reboot", 0x0100, TileAction::REBOOT }, + { "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]); diff --git a/mise.toml b/mise.toml index 6fec025..d3c67e8 100644 --- a/mise.toml +++ b/mise.toml @@ -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)" diff --git a/scripts/lockfile.sh b/scripts/lockfile.sh index f998448..9c5caec 100755 --- a/scripts/lockfile.sh +++ b/scripts/lockfile.sh @@ -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)