diff --git a/boards/esp32-32e-4/board_config.h b/boards/esp32-32e-4/board_config.h index 2acd33c..a616a4d 100644 --- a/boards/esp32-32e-4/board_config.h +++ b/boards/esp32-32e-4/board_config.h @@ -1,6 +1,6 @@ #pragma once -#define BOARD_NAME "WS_32E_4" +#define BOARD_NAME "esp32-32e-4" // ══════════════════════════════════════════════════════════ // Hosyond ESP32-32E 4" (320x480) with ST7796 + XPT2046 diff --git a/boards/esp32-32e/board_config.h b/boards/esp32-32e/board_config.h index 88edfdb..d0b17f5 100644 --- a/boards/esp32-32e/board_config.h +++ b/boards/esp32-32e/board_config.h @@ -1,6 +1,6 @@ #pragma once -#define BOARD_NAME "WS_32E" +#define BOARD_NAME "esp32-32e" // ══════════════════════════════════════════════════════════ // TODO: Set these to match YOUR display + wiring. diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp index cc9493d..4edd0e1 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.cpp @@ -395,16 +395,37 @@ void DisplayDriverGFX::drawAlert(const ScreenState& state) { void DisplayDriverGFX::drawDashboard(const ScreenState& state) { _gfx->fillScreen(STYLE_COLOR_BG); - // Header + // Header - use Layout for safe positioning _gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER); + Layout header = Layout::header(DISP_W, STYLE_HEADER_HEIGHT); + Layout safeText = header.padded(STYLE_SPACING_X); + setBodyFont(); _gfx->setTextColor(STYLE_COLOR_FG); - _gfx->setCursor(STYLE_SPACING_X, 12); - _gfx->printf("KLUBHAUS"); - // WiFi status - _gfx->setCursor(DISP_W - 120, 12); - _gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF"); + // Title with scrolling support + _headerScroller.setText("KLUBHAUS"); + _headerScroller.setScrollSpeed(80); + _headerScroller.setPauseDuration(2000); + _headerScroller.render( + [&](int16_t x, const char* s) { + _gfx->setCursor(safeText.x + x, safeText.y + 4); + _gfx->print(s); + }, + safeText.w); + + // WiFi status - right aligned with scrolling + Layout wifiArea(DISP_W - 150, 0, 140, STYLE_HEADER_HEIGHT); + Layout safeWifi = wifiArea.padded(4); + _wifiScroller.setText(state.wifiSsid.length() > 0 ? state.wifiSsid.c_str() : "WiFi: OFF"); + _wifiScroller.setScrollSpeed(60); + _wifiScroller.setPauseDuration(1500); + _wifiScroller.render( + [&](int16_t x, const char* s) { + _gfx->setCursor(safeWifi.x + x, safeWifi.y + 4); + _gfx->print(s); + }, + safeWifi.w); // Get tile layouts from library helper int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP); diff --git a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h index 5572d4c..33a81cc 100644 --- a/boards/esp32-s3-lcd-43/DisplayDriverGFX.h +++ b/boards/esp32-s3-lcd-43/DisplayDriverGFX.h @@ -1,6 +1,7 @@ #pragma once #include "IDisplayDriver.h" +#include "Style.h" #include @@ -54,4 +55,8 @@ private: ScreenID _lastScreen = ScreenID::BOOT; BootStage _lastBootStage = BootStage::SPLASH; bool _needsRedraw = true; + + // Text scrollers for scrolling elements + TextScroller _headerScroller; + TextScroller _wifiScroller; }; diff --git a/boards/esp32-s3-lcd-43/board-config.sh b/boards/esp32-s3-lcd-43/board-config.sh index d27dbe5..f1e3c3b 100644 --- a/boards/esp32-s3-lcd-43/board-config.sh +++ b/boards/esp32-s3-lcd-43/board-config.sh @@ -1,4 +1,4 @@ FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" -PORT="/dev/ttyACM0" +PORT="/dev/ttyUSB0" LIBS="--library ./vendor/esp32-s3-lcd-43/LovyanGFX" OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1 -DLOCAL_SECRETS" diff --git a/boards/esp32-s3-lcd-43/board_config.h b/boards/esp32-s3-lcd-43/board_config.h index d6bc5a3..c01b675 100644 --- a/boards/esp32-s3-lcd-43/board_config.h +++ b/boards/esp32-s3-lcd-43/board_config.h @@ -1,6 +1,6 @@ #pragma once -#define BOARD_NAME "WS_S3_43" +#define BOARD_NAME "esp32-s3-lcd-43" #define DISPLAY_WIDTH 800 #define DISPLAY_HEIGHT 480 #define DISPLAY_ROTATION 0 diff --git a/libraries/KlubhausCore/src/DoorbellLogic.cpp b/libraries/KlubhausCore/src/DoorbellLogic.cpp index 74fdd46..a8ad3c9 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.cpp +++ b/libraries/KlubhausCore/src/DoorbellLogic.cpp @@ -304,8 +304,10 @@ void DoorbellLogic::onSerialCommand(const String& cmd) { Serial.println(); Serial.printf("[NET] %s RSSI:%d IP:%s\n", _state.wifiSsid.c_str(), _state.wifiRssi, _state.ipAddr.c_str()); + } else if(cmd == "board") { + Serial.printf("[BOARD] %s\n", _board); } else - Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status")); + Serial.println(F("[CMD] alert|silence|reboot|dashboard|off|status|board")); } void DoorbellLogic::setScreen(ScreenID s) { diff --git a/libraries/KlubhausCore/src/DoorbellLogic.h b/libraries/KlubhausCore/src/DoorbellLogic.h index 3c190be..1d5012f 100644 --- a/libraries/KlubhausCore/src/DoorbellLogic.h +++ b/libraries/KlubhausCore/src/DoorbellLogic.h @@ -23,6 +23,7 @@ public: void processSerial(); const ScreenState& getScreenState() const { return _state; } + const char* getBoard() const { return _board; } /// Externally trigger silence (e.g. hold-to-silence gesture). void silenceAlert(); diff --git a/libraries/KlubhausCore/src/Style.h b/libraries/KlubhausCore/src/Style.h index 9345bad..0a05544 100644 --- a/libraries/KlubhausCore/src/Style.h +++ b/libraries/KlubhausCore/src/Style.h @@ -1,5 +1,6 @@ #pragma once +#include #include struct Layout { @@ -36,6 +37,89 @@ struct Layout { uint16_t y = row * (tileH + gap); return Layout(x, y, tileW, tileH); } + + Layout clamp(uint16_t maxW, uint16_t maxH) const { + return Layout( + x < maxW ? x : maxW, y < maxH ? y : maxH, w <= maxW ? w : maxW, h <= maxH ? h : maxH); + } + + Layout padded(uint16_t padding) const { + uint16_t newX = x + padding; + uint16_t newY = y + padding; + uint16_t newW = w > 2 * padding ? w - 2 * padding : 0; + uint16_t newH = h > 2 * padding ? h - 2 * padding : 0; + return Layout(newX, newY, newW, newH); + } + + bool contains(uint16_t px, uint16_t py) const { + return px >= x && px < x + w && py >= y && py < y + h; + } +}; + +class TextScroller { +public: + TextScroller() + : _text() + , _scrollOffset(0) + , _lastUpdateMs(0) + , _scrollSpeed(50) + , _pauseMs(2000) { } + + void setText(const char* text) { + if(text != _text.c_str()) { + _text = text; + _scrollOffset = 0; + _lastUpdateMs = 0; + _paused = false; + } + } + + void setScrollSpeed(uint16_t msPerPixel) { _scrollSpeed = msPerPixel; } + void setPauseDuration(uint16_t ms) { _pauseMs = ms; } + + template void render(DrawFn draw, uint16_t maxWidth) { + uint32_t now = millis(); + + if(_text.length() <= maxWidth / 8) { + draw(0, _text.c_str()); + return; + } + + if(!_paused && now - _lastUpdateMs > _scrollSpeed) { + _scrollOffset++; + _lastUpdateMs = now; + + if(_scrollOffset > (int)_text.length() * 6) { + _paused = true; + _pauseStartMs = now; + } + } + + if(_paused && now - _pauseStartMs > _pauseMs) { + _paused = false; + _scrollOffset = 0; + _lastUpdateMs = now; + } + + int16_t x = -_scrollOffset; + char buf[2] = { 0 }; + for(size_t i = 0; i < _text.length(); i++) { + buf[0] = _text.charAt(i); + if(x + 8 > 0 && x < (int)maxWidth) { + draw(x, buf); + } + x += 6; + } + } + +private: + String _text; + int16_t _scrollOffset; + uint32_t _lastUpdateMs; + uint32_t _pauseStartMs; + uint16_t _scrollSpeed; + uint16_t _pauseMs; + bool _paused; }; struct TileMetrics { diff --git a/mise.toml b/mise.toml index ee02db6..46a2304 100644 --- a/mise.toml +++ b/mise.toml @@ -135,6 +135,10 @@ run = """ ./boards/$BOARD/install.sh """ +[tasks.detect] +description = "Detect connected doorbell board" +run = "bash ./scripts/detect-device.sh" + # Convenience [tasks.clean] diff --git a/scripts/detect-device.sh b/scripts/detect-device.sh new file mode 100755 index 0000000..a65fa47 --- /dev/null +++ b/scripts/detect-device.sh @@ -0,0 +1,160 @@ +#!/bin/bash +# +# detect-device.sh - Detect which doorbell board is connected +# +# Uses two methods: +# 1. Firmware query (if flashed): sends "board" command, expects "[BOARD] xxx" +# 2. Hardware probe (fallback): uses esptool to query chip type and flash size +# +# Usage: ./scripts/detect-device.sh [port] +# If no port specified, tries common ports: /dev/ttyUSB0 /dev/ttyACM0 + +set -e + +# Configuration +TIMEOUT_FIRMWARE=2 +TIMEOUT_HARDWARE=5 + +# Known boards and their characteristics +declare -A BOARD_CHIP=( + ["esp32-s3-lcd-43"]="ESP32-S3" + ["esp32-32e"]="ESP32" + ["esp32-32e-4"]="ESP32" +) + +# Find available serial ports +find_ports() { + local ports=() + for p in /dev/ttyUSB0 /dev/ttyUSB1 /dev/ttyACM0 /dev/ttyACM1; do + if [ -e "$p" ]; then + ports+=("$p") + fi + done + printf '%s\n' "${ports[@]}" +} + +# Query firmware for board name +query_firmware() { + local port="$1" + + # Try using Python for more reliable serial communication + local output + output=$(python3 -c " +import serial +import time +try: + s = serial.Serial('$port', 115200, timeout=$TIMEOUT_FIRMWARE) + time.sleep(0.5) + s.write(b'board\r\n') + time.sleep($TIMEOUT_FIRMWARE) + print(s.read(200).decode('utf-8', errors='ignore')) + s.close() +except: + pass +" 2>/dev/null || true) + + # Look for [BOARD] xxx pattern + if echo "$output" | grep -q '\[BOARD\]'; then + local board + board=$(echo "$output" | grep '\[BOARD\]' | sed 's/.*\[BOARD\] //' | tr -d '\r\n') + if [ -n "$board" ]; then + echo "$board" + return 0 + fi + fi + return 1 +} + +# Hardware probe using esptool +probe_hardware() { + local port="$1" + + # Get chip info + local chip_info + chip_info=$(timeout "$TIMEOUT_HARDWARE" esptool --port "$port" chip_id 2>/dev/null || true) + + if [ -z "$chip_info" ]; then + return 1 + fi + + # Check for ESP32-S3 + if echo "$chip_info" | grep -q "ESP32-S3"; then + echo "esp32-s3-lcd-43" + return 0 + fi + + # For ESP32, check flash size + if echo "$chip_info" | grep -q "ESP32"; then + local flash_size + flash_size=$(echo "$chip_info" | grep -i "flash size" | grep -oE '[0-9]+' | head -1 || echo "0") + + if [ -n "$flash_size" ] && [ "$flash_size" -ge 8 ]; then + # 8MB+ flash - likely esp32-32e-4 + echo "esp32-32e-4 (or esp32-32e)" + return 0 + else + # 4MB or less - likely esp32-32e + echo "esp32-32e (or esp32-32e-4)" + return 0 + fi + fi + + return 1 +} + +# Main detection logic +detect() { + local port="$1" + + # Try firmware query first (100% accurate if firmware responds) + echo "Trying firmware query on $port..." >&2 + local result + result=$(query_firmware "$port" || true) + + if [ -n "$result" ]; then + echo "Detected via firmware: $result" + return 0 + fi + + # Fall back to hardware probe + echo "Trying hardware probe on $port..." >&2 + result=$(probe_hardware "$port" || true) + + if [ -n "$result" ]; then + echo "Detected via hardware: $result" + return 0 + fi + + echo "No device detected on $port" >&2 + return 1 +} + +# Entry point +main() { + local target_port="$1" + + if [ -n "$target_port" ]; then + # Specific port requested + if [ ! -e "$target_port" ]; then + echo "Error: Port $target_port does not exist" >&2 + exit 1 + fi + detect "$target_port" + else + # Auto-detect: try all available ports + local found=0 + for port in $(find_ports); do + if detect "$port"; then + found=1 + break + fi + done + + if [ "$found" -eq 0 ]; then + echo "No doorbell device detected" >&2 + exit 1 + fi + fi +} + +main "$@" diff --git a/vendor/.gitignore b/vendor/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/vendor/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore