Compare commits
12 Commits
dfd511e499
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d17e7bab9 | |||
| 4030048401 | |||
| 72636f9ecf | |||
| 688b1905e5 | |||
| b68d36bb85 | |||
| ba8789797c | |||
| fd04dea8dd | |||
| 9c8f67dccb | |||
| 6d51234f21 | |||
| 66f09c3468 | |||
| 913373ca72 | |||
| f367bd365b |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,4 +16,5 @@ vendor/esp32-s3-lcd-43/GFX_Library_for_Arduino/
|
|||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
.board-last
|
||||||
.cache/
|
.cache/
|
||||||
|
|||||||
496
AGENTS.md
496
AGENTS.md
@@ -1,455 +1,143 @@
|
|||||||
# AGENTS.md — Klubhaus Doorbell
|
# AGENTS.md — Klubhaus Doorbell
|
||||||
|
|
||||||
Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh.
|
Multi-target Arduino/ESP32 doorbell alert system using ntfy.sh. Default BOARD: `esp32-s3-lcd-43`.
|
||||||
|
|
||||||
## Quick Reference
|
## Build Commands
|
||||||
|
|
||||||
**Default BOARD**: `esp32-s3-lcd-43` (set in mise.toml). To switch boards:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mise set BOARD=esp32-32e-4 # switch to ESP32-32E-4"
|
# Set target board
|
||||||
mise set BOARD=esp32-32e # switch to ESP32-32E
|
mise set BOARD=esp32-32e-4 # ESP32-32E 4" (320x480 ST7796)
|
||||||
mise set BOARD=esp32-s3-lcd-43 # switch to ESP32-S3-LCD-4.3
|
mise set BOARD=esp32-32e # ESP32-32E 3.5" (320x240 ILI9341)
|
||||||
```
|
mise set BOARD=esp32-s3-lcd-43 # ESP32-S3-Touch-LCD-4.3 (800x480 RGB)
|
||||||
|
|
||||||
Then run commands without `BOARD=` prefix:
|
# Core commands
|
||||||
|
mise run compile # compile for current BOARD
|
||||||
|
mise run upload # upload (auto-kills monitor first)
|
||||||
|
mise run monitor # start JSON monitor daemon
|
||||||
|
mise run kill # kill monitor/release serial port
|
||||||
|
|
||||||
```bash
|
# Formatting & cleanup
|
||||||
mise run compile # compile
|
mise run format # format code with clang-format
|
||||||
mise run upload # upload (auto-kills monitor)
|
mise run clean # remove build artifacts
|
||||||
mise run monitor # start JSON monitor daemon
|
|
||||||
mise run log-tail # watch colored logs
|
|
||||||
mise run cmd COMMAND=dashboard # send command
|
|
||||||
mise run state # show device state
|
|
||||||
|
|
||||||
# Install libs (run after cloning)
|
# Debugging
|
||||||
mise run install-libs-shared # shared libs (ArduinoJson, NTPClient)
|
mise run log-tail # tail colored logs
|
||||||
mise run install # shared + board-specific libs
|
mise run cmd COMMAND=dashboard # send command to device
|
||||||
```
|
mise run state # show device state
|
||||||
|
mise run monitor-raw # raw serial monitor (115200 baud)
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
Three board targets share business logic via a common library:
|
|
||||||
|
|
||||||
| Board | Display | Library | Build Command |
|
|
||||||
|-------|---------|---------|--------------|
|
|
||||||
| ESP32-32E | SPI TFT 320x240 (ILI9341) | TFT_eSPI | `mise set BOARD=esp32-32e && mise run compile` |
|
|
||||||
| ESP32-32E-4" | SPI TFT 320x480 (ST7796) | TFT_eSPI | `mise set BOARD=esp32-32e-4 && mise run compile` |
|
|
||||||
| ESP32-S3-Touch-LCD-4.3 | 800x480 RGB parallel | LovyanGFX | `mise set BOARD=esp32-s3-lcd-43 && mise run compile` |
|
|
||||||
|
|
||||||
## Essential Commands
|
|
||||||
|
|
||||||
All commands run via **mise**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Running multiple tasks - use && to chain them
|
|
||||||
mise run compile && mise run upload && mise run monitor
|
|
||||||
|
|
||||||
# Install all dependencies (shared libs + vendored display libs)
|
|
||||||
mise run install-libs-shared
|
|
||||||
mise run install # install shared + board-specific libs (requires BOARD env)
|
|
||||||
|
|
||||||
# Generic commands (set BOARD with mise set first)
|
|
||||||
mise set BOARD=esp32-32e # switch to ESP32-32E
|
|
||||||
mise run compile # compile for ESP32-32E
|
|
||||||
mise run upload # upload to ESP32-32E
|
|
||||||
mise run monitor # monitor ESP32-32E
|
|
||||||
|
|
||||||
mise set BOARD=esp32-32e-4 # switch to ESP32-32E-4"
|
|
||||||
mise run compile # compile for ESP32-32E-4"
|
|
||||||
mise run upload # upload to ESP32-32E-4"
|
|
||||||
mise run monitor # monitor ESP32-32E-4"
|
|
||||||
|
|
||||||
mise set BOARD=esp32-s3-lcd-43 # switch to ESP32-S3-LCD-4.3
|
|
||||||
mise run compile # compile for ESP32-S3-LCD-4.3
|
|
||||||
mise run upload # upload to ESP32-S3-LCD-4.3
|
|
||||||
mise run monitor # monitor ESP32-S3-LCD-4.3
|
|
||||||
|
|
||||||
# Other useful tasks
|
|
||||||
mise run format # format code
|
|
||||||
mise run clean # clean build artifacts
|
|
||||||
mise run kill # kill running monitor/upload for BOARD
|
|
||||||
mise run log-tail # tail colored logs (requires BOARD)
|
|
||||||
mise run cmd COMMAND=dashboard # send command to device (requires BOARD)
|
|
||||||
mise run state # show device state (requires BOARD)
|
|
||||||
|
|
||||||
# LSP / IDE support
|
|
||||||
mise run gen-compile-commands # generate compile_commands.json for LSP
|
|
||||||
mise run gen-crush-config # generate .crush.json with BOARD-based FQBN
|
|
||||||
|
|
||||||
# Arduino maintenance
|
|
||||||
mise run arduino-clean # clear Arduino CLI cache (staging + packages)
|
|
||||||
|
|
||||||
# Raw serial access
|
|
||||||
mise run monitor-raw # raw serial monitor (arduino-cli)
|
|
||||||
mise run monitor-tio # show tio command for terminal UI
|
mise run monitor-tio # show tio command for terminal UI
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
mise run install-libs-shared # shared libs (ArduinoJson, NTPClient)
|
||||||
|
mise run install # shared + board-specific libs
|
||||||
|
|
||||||
|
# LSP / IDE
|
||||||
|
mise run gen-compile-commands # generate compile_commands.json
|
||||||
|
mise run gen-crush-config # generate .crush.json for BOARD
|
||||||
```
|
```
|
||||||
|
|
||||||
# Board Configuration Files
|
**Serial debug commands** (115200 baud): `alert`, `silence`, `dashboard`, `off`, `status`, `reboot`
|
||||||
|
|
||||||
Each board directory contains `board-config.sh` which defines:
|
**No unit tests exist** — verify changes by compiling and deploying to hardware.
|
||||||
|
|
||||||
| Variable | Description |
|
## Code Style
|
||||||
|----------|-------------|
|
|
||||||
| `FQBN` | Fully Qualified Board Name for arduino-cli |
|
|
||||||
| `PORT` | Serial port for upload/monitoring (default: `/dev/ttyUSB0`) |
|
|
||||||
| `LIBS` | Vendor library path for `--libraries` flag |
|
|
||||||
| `OPTS` | Additional compiler flags (e.g., `-DDEBUG_MODE`) |
|
|
||||||
|
|
||||||
**Example** (`boards/esp32-32e-4/board-config.sh`):
|
### Formatting (.clang-format)
|
||||||
|
- BasedOnStyle: WebKit
|
||||||
```bash
|
- 4-space indentation, no tabs
|
||||||
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
- Column limit: 100
|
||||||
PORT="/dev/ttyUSB0"
|
- Opening brace on same line (`BreakBeforeBraces: Attach`)
|
||||||
LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI"
|
- Run `mise run format` to format code
|
||||||
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Port override**: `mise set PORT=/dev/ttyXXX` before running upload/monitor commands.
|
|
||||||
|
|
||||||
**Prerequisites**: arduino-cli with `esp32:esp32` platform installed, mise.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```text
|
|
||||||
libraries/KlubhausCore/src/ Shared Arduino library
|
|
||||||
├── KlubhausCore.h Umbrella include (board sketches use this)
|
|
||||||
├── Config.h Constants, timing, WiFiCred struct
|
|
||||||
├── ScreenState.h State enums/structs
|
|
||||||
├── IDisplayDriver.h Pure virtual display interface
|
|
||||||
├── DisplayManager.h Thin wrapper delegating to IDisplayDriver
|
|
||||||
├── NetManager.* WiFi, HTTP, NTP
|
|
||||||
└── DoorbellLogic.* State machine, ntfy polling
|
|
||||||
|
|
||||||
boards/
|
|
||||||
├── esp32-32e/
|
|
||||||
│ ├── esp32-32e.ino Main sketch
|
|
||||||
│ ├── board_config.h Board-specific config
|
|
||||||
│ ├── secrets.h.example WiFi creds template (copy to secrets.h)
|
|
||||||
│ ├── tft_user_setup.h TFT_eSPI config
|
|
||||||
│ └── DisplayDriverTFT.* Concrete IDisplayDriver for TFT
|
|
||||||
├── esp32-32e-4/
|
|
||||||
│ ├── esp32-32e-4.ino Main sketch
|
|
||||||
│ ├── board_config.h Board-specific config
|
|
||||||
│ ├── secrets.h.example WiFi creds template (copy to secrets.h)
|
|
||||||
│ ├── tft_user_setup.h TFT_eSPI config
|
|
||||||
│ └── DisplayDriverTFT.* Concrete IDisplayDriver for TFT
|
|
||||||
└── esp32-s3-lcd-43/
|
|
||||||
├── esp32-s3-lcd-43.ino Main sketch
|
|
||||||
├── board_config.h Board-specific config
|
|
||||||
├── secrets.h.example WiFi creds template (copy to secrets.h)
|
|
||||||
├── LovyanPins.h Pin definitions
|
|
||||||
└── DisplayDriverGFX.* Concrete IDisplayDriver for LovyanGFX
|
|
||||||
|
|
||||||
vendor/ Vendored display libs (recreated by install-libs)
|
|
||||||
├── esp32-32e/TFT_eSPI/
|
|
||||||
├── esp32-32e-4/TFT_eSPI/
|
|
||||||
└── esp32-s3-lcd-43/LovyanGFX/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Code Patterns
|
|
||||||
|
|
||||||
### Header Guards
|
### Header Guards
|
||||||
|
|
||||||
Use `#pragma once` (not `#ifndef` guards).
|
Use `#pragma once` (not `#ifndef` guards).
|
||||||
|
|
||||||
### Formatting
|
|
||||||
|
|
||||||
- 4-space indentation, no tabs
|
|
||||||
- WebKit-based style (see `.clang-format`)
|
|
||||||
- Column limit: 100
|
|
||||||
- Opening brace stays on same line (`BreakBeforeBraces: Attach`)
|
|
||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
|
| Type | Convention | Example |
|
||||||
- Classes: `PascalCase` (e.g., `DisplayManager`, `IDisplayDriver`)
|
|------|------------|---------|
|
||||||
- Constants/enums: `SCREAMING_SNAKE` (e.g., `POLL_INTERVAL_MS`, `ScreenState::DASHBOARD`)
|
| Classes | PascalCase | `DisplayManager`, `IDisplayDriver` |
|
||||||
- Variables/functions: `camelCase` (e.g., `currentState`, `updateDisplay`)
|
| Constants/enums | SCREAMING_SNAKE | `POLL_INTERVAL_MS`, `ScreenState::DASHBOARD` |
|
||||||
- Member variables: prefix with `_` (e.g., `_screenWidth`, `_isConnected`)
|
| Variables/functions | camelCase | `currentState`, `updateDisplay` |
|
||||||
|
| Member variables | `_` prefix | `_screenWidth`, `_isConnected` |
|
||||||
|
|
||||||
### Types
|
### Types
|
||||||
|
|
||||||
- Arduino types: `int`, `uint8_t`, `uint16_t`, `size_t`
|
|
||||||
- Use fixed-width types for protocol/serialization (`uint8_t`, not `byte`)
|
- Use fixed-width types for protocol/serialization (`uint8_t`, not `byte`)
|
||||||
- Avoid `bool` for pin states - use `uint8_t` or `int`
|
|
||||||
- Use `size_t` for sizes and array indices
|
- Use `size_t` for sizes and array indices
|
||||||
|
- Avoid `bool` for pin states — use `uint8_t` or `int`
|
||||||
|
|
||||||
### Imports Organization
|
### Imports Organization (in order)
|
||||||
|
1. Arduino core (`Arduino.h`)
|
||||||
- Arduino core headers first (`Arduino.h`)
|
2. Standard C/C++ (`<cstdint>`, `<String>`, `<vector>`)
|
||||||
- Standard C/C++ library (`<cstdint>`, `<String>`, `<vector>`)
|
3. Third-party libs (`TFT_eSPI.h`, `ArduinoJson.h`)
|
||||||
- Third-party libraries (e.g., `TFT_eSPI.h`, `ArduinoJson.h`)
|
4. Local project (`"Config.h"`, `"ScreenState.h"`)
|
||||||
- Local project headers (e.g., `"Config.h"`, `"ScreenState.h"`)
|
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
- Serial logging: `Serial.println("[ERROR] message")`
|
||||||
|
- Use `Serial.printf()` for formatted debug
|
||||||
|
- Return error codes, not exceptions
|
||||||
|
- Log state: `[STATE] → DASHBOARD`
|
||||||
|
|
||||||
- Serial logging pattern: `Serial.println("[ERROR] message")`
|
## Architecture Patterns
|
||||||
- Use `Serial.printf()` for formatted debug output
|
|
||||||
- Return error codes, not exceptions (exceptions not available in Arduino)
|
|
||||||
- Log state transitions with `[STATE] → STATE` tags
|
|
||||||
|
|
||||||
### Class Design
|
### Display Driver Interface
|
||||||
|
- Pure virtual `IDisplayDriver` in shared `KlubhausCore`
|
||||||
- Pure virtual `IDisplayDriver` interface in shared library
|
- Each board implements concrete driver (`DisplayDriverTFT`, `DisplayDriverGFX`)
|
||||||
- Each board implements a concrete driver (e.g., `DisplayDriverTFT`, `DisplayDriverGFX`)
|
|
||||||
- `DisplayManager` delegates to `IDisplayDriver` — no display-lib coupling in shared code
|
- `DisplayManager` delegates to `IDisplayDriver` — no display-lib coupling in shared code
|
||||||
|
|
||||||
### Arduino Patterns
|
### Arduino Patterns
|
||||||
|
- `setup()` — call `begin()` on managers
|
||||||
- `setup()` runs once at boot — call `begin()` on managers
|
- `loop()` — call `update()` on managers
|
||||||
- `loop()` runs continuously — call `update()` on managers
|
|
||||||
- Use `millis()` for timing (not `delay()`)
|
- Use `millis()` for timing (not `delay()`)
|
||||||
- Serial console at 115200 baud for debug commands
|
- Serial baud: 115200
|
||||||
|
|
||||||
### Style System
|
### Style System
|
||||||
|
- Style constants in board's `board_config.h`: `STYLE_SPACING_X`, `STYLE_COLOR_BG`, etc.
|
||||||
|
- Font abstraction via `IDisplayDriver`: `setTitleFont()`, `setBodyFont()`, etc.
|
||||||
|
- Layout helpers in `KlubhausCore/src/Style.h`
|
||||||
|
|
||||||
The project uses a CSS-like styling system for consistent UI across different display sizes:
|
## Key Files
|
||||||
|
|
||||||
- **Style constants** in each board's `board_config.h`: `STYLE_SPACING_X`, `STYLE_HEADER_HEIGHT`, `STYLE_COLOR_BG`, etc.
|
|
||||||
- **Font abstraction** via `IDisplayDriver` methods: `setTitleFont()`, `setBodyFont()`, `setLabelFont()`, `setDefaultFont()`
|
|
||||||
- **Layout helpers** in `KlubhausCore/src/Style.h`: `Layout` and `TileMetrics` structs
|
|
||||||
|
|
||||||
## Testing/Debugging
|
|
||||||
|
|
||||||
**No unit tests exist** - This is an embedded Arduino sketch. Verify changes by building and deploying to hardware:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mise set BOARD=esp32-s3-lcd-43 # compile for esp32-s3-lcd-43
|
|
||||||
mise set BOARD=esp32-32e-4 # compile for ESP32-32E-4"
|
|
||||||
```
|
```
|
||||||
|
libraries/KlubhausCore/src/
|
||||||
|
├── KlubhausCore.h # Umbrella include
|
||||||
|
├── Config.h # Timing, WiFiCred struct
|
||||||
|
├── ScreenState.h # State enums/structs
|
||||||
|
├── IDisplayDriver.h # Pure virtual interface
|
||||||
|
├── DisplayManager.h # Delegates to IDisplayDriver
|
||||||
|
├── NetManager.* # WiFi, HTTP, NTP
|
||||||
|
└── DoorbellLogic.* # State machine, ntfy polling
|
||||||
|
|
||||||
**Serial commands** (type into serial monitor at 115200 baud):
|
boards/{BOARD}/
|
||||||
|
├── {BOARD}.ino # Main sketch
|
||||||
| Command | Action |
|
├── board_config.h # Board-specific config
|
||||||
|---------|--------|
|
├── secrets.h # WiFi credentials
|
||||||
| `alert` | Trigger a test alert |
|
├── tft_user_setup.h # TFT_eSPI config (TFT boards)
|
||||||
| `silence` | Silence current alert |
|
└── DisplayDriver*.{h,cpp} # Concrete IDisplayDriver
|
||||||
| `dashboard` | Show dashboard screen |
|
|
||||||
| `off` | Turn off display |
|
|
||||||
| `status` | Print state + memory info |
|
|
||||||
| `reboot` | Restart device |
|
|
||||||
|
|
||||||
**Touch Test Commands** (ESP32-S3-LCD-4.3 only):
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
|---------|--------|
|
|
||||||
| `TEST:touch X Y press` | Inject synthetic press at raw panel coords (X,Y) |
|
|
||||||
| `TEST:touch X Y release` | Inject synthetic release at raw panel coords (X,Y) |
|
|
||||||
| `TEST:touch clear` | Clear test mode (required between touch sequences) |
|
|
||||||
|
|
||||||
**Debug Features** (only when `DEBUG_MODE` is enabled in board-config.sh):
|
|
||||||
|
|
||||||
| Feature | Description |
|
|
||||||
|---------|-------------|
|
|
||||||
| Red crosshair | Draws a red crosshair at exact touch coordinates on tap (DEBUG_MODE only) |
|
|
||||||
|
|
||||||
Note: The S3 touch panel is rotated 180° relative to the display. Use raw panel coordinates:
|
|
||||||
|
|
||||||
- Display (100,140) → Raw (700, 340)
|
|
||||||
- Display (700,140) → Raw (100, 340)
|
|
||||||
|
|
||||||
## Monitor Daemon and Logging
|
|
||||||
|
|
||||||
The build system includes a Python-based monitor agent that provides JSON logging and command pipes.
|
|
||||||
|
|
||||||
- **JSON log**: `/tmp/doorbell-$BOARD.jsonl` – each line is `{"ts":123.456,"line":"..."}`
|
|
||||||
- **State file**: `/tmp/doorbell-$BOARD-state.json` – current device state (screen, alert status, etc.)
|
|
||||||
- **Command FIFO**: `/tmp/doorbell-$BOARD-cmd.fifo` – send commands via `echo 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo`
|
|
||||||
|
|
||||||
Commands to interact with the daemon:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mise set BOARD=esp32-32e
|
|
||||||
mise run monitor # Start monitor daemon (background)
|
|
||||||
mise run log-tail # Tail colored logs
|
|
||||||
mise run cmd COMMAND=dashboard # Send command
|
|
||||||
mise run state # Show current device state
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The monitor daemon automatically starts after upload and is killed before upload to avoid serial port conflicts.
|
|
||||||
|
|
||||||
## Gotchas
|
## Gotchas
|
||||||
|
|
||||||
1. **secrets.h can be shared**: KlubhausCore/src/secrets.h provides default credentials. Boards with `LOCAL_SECRETS` defined in board-config.sh will use their local `secrets.h` instead.
|
1. **secrets.h**: Boards with `-DLOCAL_SECRETS` use local `secrets.h`; others use `KlubhausCore/src/secrets.h`
|
||||||
|
2. **Vendored libs**: Each board links only its display lib — never TFT_eSPI + LovyanGFX together
|
||||||
|
3. **LSP errors**: Run `mise run gen-compile-commands` then restart LSP; build works regardless
|
||||||
|
4. **Serial port**: `upload`/`monitor` auto-depend on `kill` to release port
|
||||||
|
5. **State tags**: Use `[STATE] → DASHBOARD`, `[ADMIN]`, `[TOUCH]`, `[ALERT]` for monitor parsing
|
||||||
|
|
||||||
```bash
|
## Config Constants (Config.h)
|
||||||
# For boards without local secrets.h, the defaults will be used
|
|
||||||
# To use board-specific credentials, add -DLOCAL_SECRETS to OPTS in board-config.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Display libs are vendored**: Each board uses a different display library. The build system uses `--libraries` to link only the board's vendored lib — never link both TFT_eSPI and LovyanGFX in the same build.
|
|
||||||
|
|
||||||
3. **No unit tests**: This is an embedded Arduino sketch — no test suite exists. Verify changes by building and deploying to hardware.
|
|
||||||
|
|
||||||
4. **LSP errors are expected**: The LSP may show errors about missing Arduino types until `compile_commands.json` is generated. Run `mise run gen-compile-commands` first, then restart the LSP. The build works correctly via arduino-cli.
|
|
||||||
|
|
||||||
5. **Build artifacts in board dirs**: Build output goes to `boards/[board]/build/` — cleaned by `mise run clean`.
|
|
||||||
|
|
||||||
6. **WiFi credentials are per-board**: Each board directory has its own `secrets.h` because boards may be on different networks.
|
|
||||||
|
|
||||||
7. **Use BOARD environment variable**: All build commands require the `BOARD` environment variable. The default BOARD is `esp32-s3-lcd-43` (set in mise.toml). Use `mise set BOARD=xxx` to switch boards.
|
|
||||||
|
|
||||||
8. **Serial port contention**: The `upload`, `monitor`, and `monitor-raw` tasks automatically depend on `kill` which uses `fuser` to terminate any process using the serial port before starting.
|
|
||||||
|
|
||||||
9. **Monitor state parsing**: The monitor-agent parses serial output for state updates:
|
|
||||||
- `[STATE] → DASHBOARD` → updates state file to `"screen":"DASHBOARD"`
|
|
||||||
- `[STATE] → ALERT` → updates state file to `"screen":"ALERT"`
|
|
||||||
- `[STATE] → OFF` → updates state file to `"screen":"OFF"`
|
|
||||||
- `[STATE] → BOOT` → updates state file to `"screen":"BOOT"`
|
|
||||||
- `[ADMIN]` commands also update state accordingly
|
|
||||||
|
|
||||||
10. **Serial baud rate**: All serial communication uses 115200 baud.
|
|
||||||
|
|
||||||
11. **Serial port contention**: The `kill` task uses `fuser` to release the serial port. Both `upload` and `monitor` tasks depend on `kill` to ensure the port is free.
|
|
||||||
|
|
||||||
## Config Constants
|
|
||||||
|
|
||||||
Key timing and configuration values in `Config.h`:
|
|
||||||
|
|
||||||
| Constant | Default | Description |
|
| Constant | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `FW_VERSION` | "5.1" | Firmware version string |
|
| `FW_VERSION` | "5.1" | Firmware version |
|
||||||
| `POLL_INTERVAL_MS` | 15000 | How often to poll ntfy.sh for new messages |
|
| `POLL_INTERVAL_MS` | 15000 | ntfy.sh poll interval |
|
||||||
| `HEARTBEAT_INTERVAL_MS` | 300000 | NTP sync interval (5 min) |
|
| `ALERT_TIMEOUT_MS` | 120000 | Auto-clear alert |
|
||||||
| `ALERT_TIMEOUT_MS` | 120000 | Auto-clear alert after 2 min |
|
| `INACTIVITY_TIMEOUT_MS` | 30000 | Display off timeout |
|
||||||
| `INACTIVITY_TIMEOUT_MS` | 30000 | Turn off display after 30s of inactivity |
|
| `HOLD_TO_SILENCE_MS` | 3000 | Hold to silence |
|
||||||
| `HOLD_TO_SILENCE_MS` | 3000 | Hold duration to silence alert |
|
| `WIFI_CONNECT_TIMEOUT_MS` | 15000 | WiFi timeout |
|
||||||
| `LOOP_YIELD_MS` | 10 | Yield to prevent Task Watchdog (configurable) |
|
|
||||||
| `BOOT_GRACE_MS` | 5000 | Grace period at boot before polling starts |
|
|
||||||
| `SILENCE_DISPLAY_MS` | 10000 | How long to show silence confirmation |
|
|
||||||
| `WIFI_CONNECT_TIMEOUT_MS` | 15000 | WiFi connection timeout |
|
|
||||||
| `HTTP_TIMEOUT_MS` | 10000 | HTTP request timeout |
|
| `HTTP_TIMEOUT_MS` | 10000 | HTTP request timeout |
|
||||||
| `HINT_ANIMATION_MS` | 2000 | Hint animation duration |
|
|
||||||
| `HINT_MIN_BRIGHTNESS` | 30 | Minimum brightness for hints |
|
|
||||||
| `HINT_MAX_BRIGHTNESS` | 60 | Maximum brightness for hints |
|
|
||||||
| `TOUCH_DEBOUNCE_MS` | 100 | Touch debounce delay |
|
|
||||||
|
|
||||||
## Screen States
|
## Screen States
|
||||||
|
|
||||||
The device operates in these states (defined in `ScreenState.h`):
|
|
||||||
|
|
||||||
- **BOOT** — Initializing
|
- **BOOT** — Initializing
|
||||||
- **DASHBOARD** — Normal operation, showing status
|
- **DASHBOARD** — Normal operation
|
||||||
- **ALERT** — Doorbell ring detected, display on
|
- **ALERT** — Doorbell ring detected
|
||||||
- **OFF** — Display backlight off (but polling continues)
|
- **OFF** — Display backlight off (polling continues)
|
||||||
|
|
||||||
## Serial Output Tags
|
|
||||||
|
|
||||||
The firmware outputs structured tags for the monitor agent:
|
|
||||||
|
|
||||||
- `[STATE] → DASHBOARD/ALERT/OFF/BOOT` — State transitions
|
|
||||||
- `[ADMIN]` — Admin commands received (dashboard, off, alert, silence, status, reboot)
|
|
||||||
- `[TOUCH]` — Touch events (x, y, pressed/released)
|
|
||||||
- `[ALERT]` — Alert triggered
|
|
||||||
|
|
||||||
## Reverted Changes Log
|
|
||||||
|
|
||||||
Track changes that were reverted to avoid flapping:
|
|
||||||
|
|
||||||
- 2025-02-18: Initially configured LSP via neovim/mason (`.config/nvim/lua/plugins/arduino.lua`) — user clarified they wanted Crush-native LSP config instead
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
| Issue | Solution |
|
|
||||||
|-------|----------|
|
|
||||||
| "Another instance is running" error | Run `mise run kill` to stop monitor daemon and release port |
|
|
||||||
| Upload fails - port in use | Run `mise run kill` to stop monitor daemon and release port |
|
|
||||||
| Build fails - missing libraries | Run `mise run install-libs-shared` then `mise run install` |
|
|
||||||
| LSP shows errors but build works | Run `mise run gen-compile-commands` to generate compile_commands.json for your BOARD |
|
|
||||||
| No serial output | Check baud rate is set to 115200 in serial monitor |
|
|
||||||
| State file not updating | Ensure serial output contains `[STATE]` or `[ADMIN]` tags |
|
|
||||||
|
|
||||||
## Known Fixes
|
|
||||||
|
|
||||||
- **dashboard/off admin commands reset inactivity timer** (DoorbellLogic.cpp:234,240) — Admin commands like `dashboard` and `off` now reset the inactivity timer so the display doesn't turn off immediately after switching screens.
|
|
||||||
|
|
||||||
## Documentation Lookup Rule
|
|
||||||
|
|
||||||
When uncertain about CLI tool flags or argument syntax:
|
|
||||||
|
|
||||||
1. Run the tool with `-h` or `--help` first
|
|
||||||
2. If unclear, search for official documentation matching the tool version
|
|
||||||
3. Prefer checking the tool's own help/docs over guessing
|
|
||||||
4. For Arduino CLI, check `arduino-cli <command> --help` or official Arduino CLI docs
|
|
||||||
|
|
||||||
## LSP / IDE Configuration
|
|
||||||
|
|
||||||
The project uses **clangd** for C++ via the `compile_commands.json` generated by arduino-cli:
|
|
||||||
|
|
||||||
- `mise run gen-compile-commands` regenerates the compile database (automatically run as part of `compile`)
|
|
||||||
- The generated `compile_commands.json` includes all necessary includes, defines, and library paths for your BOARD
|
|
||||||
|
|
||||||
**Generate compile_commands.json** for accurate IDE diagnostics:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mise set BOARD=esp32-32e-4
|
|
||||||
mise run gen-compile-commands
|
|
||||||
```
|
|
||||||
|
|
||||||
**Generate static .crush.json** for a specific board:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mise set BOARD=esp32-32e-4
|
|
||||||
mise run gen-crush-config
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hardware Research Log
|
|
||||||
|
|
||||||
### Hosyond ESP32-32E 4" (320x480) - Planned
|
|
||||||
|
|
||||||
**Source**: <https://www.lcdwiki.com/4.0inch_ESP32-32E_Display>
|
|
||||||
|
|
||||||
| Spec | Value |
|
|
||||||
|------|-------|
|
|
||||||
| Display Controller | ST7796S |
|
|
||||||
| Resolution | 320x480 |
|
|
||||||
| Touch | XPT2046 (resistive) |
|
|
||||||
| Library | TFT_eSPI V2.5.43 (same as 32E) |
|
|
||||||
|
|
||||||
**GPIO Pinout**:
|
|
||||||
|
|
||||||
| Function | GPIO |
|
|
||||||
|----------|------|
|
|
||||||
| LCD CS | 15 |
|
|
||||||
| LCD DC | 2 |
|
|
||||||
| LCD MOSI | 13 |
|
|
||||||
| LCD SCLK | 14 |
|
|
||||||
| LCD RST | EN |
|
|
||||||
| LCD BL | 27 |
|
|
||||||
| Touch CS | 33 |
|
|
||||||
| Touch IRQ | 36 |
|
|
||||||
|
|
||||||
**Quirks**:
|
|
||||||
|
|
||||||
- SPI pins shared between LCD and touch
|
|
||||||
- Touch IRQ on IO36 (input-only) triggers LOW on touch
|
|
||||||
- Backlight on IO27 (HIGH = on)
|
|
||||||
- Common anode RGB LEDs on IO16, IO17, IO22 (LOW = on)
|
|
||||||
<!-- rtk-instructions v2 -->
|
|
||||||
# RTK (Rust Token Killer)
|
|
||||||
|
|
||||||
RTK filters command output to reduce tokens. It's optional but recommended for saving tokens.
|
|
||||||
|
|
||||||
## Installation Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
which rtk # Check if installed
|
|
||||||
```
|
|
||||||
|
|
||||||
If installed, use it before git commands:
|
|
||||||
```bash
|
|
||||||
rtk git status
|
|
||||||
rtk git diff
|
|
||||||
rtk git add .
|
|
||||||
rtk git commit -m "message"
|
|
||||||
rtk git push
|
|
||||||
```
|
|
||||||
|
|
||||||
RTK passes through unchanged if no filter exists, so it's always safe to use.
|
|
||||||
| Package Managers | pnpm, npm, npx | 70-90% |
|
|
||||||
| Files | ls, read, grep, find | 60-75% |
|
|
||||||
| Infrastructure | docker, kubectl | 85% |
|
|
||||||
| Network | curl, wget | 65-70% |
|
|
||||||
|
|
||||||
Overall average: **60-90% token reduction** on common development operations.
|
|
||||||
<!-- /rtk-instructions -->
|
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ void DisplayDriverTFT::begin() {
|
|||||||
_tft.setRotation(DISPLAY_ROTATION);
|
_tft.setRotation(DISPLAY_ROTATION);
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
Serial.printf("[GFX] Display OK: const %dx%d, tft %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT,
|
||||||
|
_tft.width(), _tft.height());
|
||||||
Serial.flush();
|
Serial.flush();
|
||||||
|
|
||||||
// Debug: check if touch controller is responding
|
// Debug: check if touch controller is responding
|
||||||
@@ -137,6 +138,12 @@ void DisplayDriverTFT::render(const ScreenState& st) {
|
|||||||
_needsRedraw = false;
|
_needsRedraw = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case ScreenID::STATUS:
|
||||||
|
if(_needsRedraw) {
|
||||||
|
drawStatus(st);
|
||||||
|
_needsRedraw = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case ScreenID::OFF:
|
case ScreenID::OFF:
|
||||||
if(_needsRedraw) {
|
if(_needsRedraw) {
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
@@ -153,16 +160,16 @@ void DisplayDriverTFT::drawBoot(const ScreenState& st) {
|
|||||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
|
||||||
setTitleFont();
|
setTitleFont();
|
||||||
_tft.setCursor(10, 10);
|
_tft.setCursor(10, 28); // y=28 baseline accounts for ~18px font height above baseline
|
||||||
_tft.print("KLUBHAUS");
|
_tft.print("KLUBHAUS");
|
||||||
|
|
||||||
setBodyFont();
|
setBodyFont();
|
||||||
_tft.setCursor(10, 40);
|
_tft.setCursor(10, 55); // y adjusted for ~12px font
|
||||||
_tft.print(BOARD_NAME);
|
_tft.print(BOARD_NAME);
|
||||||
|
|
||||||
// Show boot stage status
|
// Show boot stage status
|
||||||
setLabelFont();
|
setLabelFont();
|
||||||
_tft.setCursor(10, 70);
|
_tft.setCursor(10, 85); // y adjusted for ~9px label font
|
||||||
switch(stage) {
|
switch(stage) {
|
||||||
case BootStage::SPLASH:
|
case BootStage::SPLASH:
|
||||||
_tft.print("Initializing...");
|
_tft.print("Initializing...");
|
||||||
@@ -186,42 +193,73 @@ void DisplayDriverTFT::drawBoot(const ScreenState& st) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
||||||
|
// Static 2-color pulse - alternate every ~2 seconds
|
||||||
uint32_t elapsed = millis() - st.alertStartMs;
|
uint32_t elapsed = millis() - st.alertStartMs;
|
||||||
uint8_t pulse = 180 + (uint8_t)(75.0f * sinf(elapsed / 300.0f));
|
bool brightPhase = (elapsed / 2000) % 2 == 0;
|
||||||
uint16_t bg = _tft.color565(pulse, 0, 0);
|
|
||||||
|
|
||||||
_tft.fillScreen(bg);
|
// Redraw when phase changes OR when touch was released (to clear fill)
|
||||||
_tft.setTextColor(TFT_WHITE, bg);
|
bool needsRedraw = (brightPhase != _lastAlertPhase) || _alertNeedsRedraw;
|
||||||
|
|
||||||
setTitleFont();
|
if(needsRedraw) {
|
||||||
_tft.setCursor(10, 20);
|
_lastAlertPhase = brightPhase;
|
||||||
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
|
_alertNeedsRedraw = false; // clear the flag
|
||||||
|
uint16_t bg = brightPhase ? TFT_RED : _tft.color565(180, 0, 0);
|
||||||
|
|
||||||
setBodyFont();
|
_tft.fillScreen(bg);
|
||||||
_tft.setCursor(10, 80);
|
_tft.setTextColor(TFT_WHITE, bg);
|
||||||
_tft.print(st.alertBody);
|
|
||||||
|
|
||||||
setLabelFont();
|
setTitleFont();
|
||||||
_tft.setCursor(10, DISPLAY_HEIGHT - 20);
|
_tft.setCursor(10, 28);
|
||||||
_tft.print("Hold to silence...");
|
_tft.print(st.alertTitle.length() > 0 ? st.alertTitle : "ALERT");
|
||||||
|
|
||||||
|
setBodyFont();
|
||||||
|
_tft.setCursor(10, 70);
|
||||||
|
_tft.print(st.alertBody);
|
||||||
|
|
||||||
|
setLabelFont();
|
||||||
|
_tft.setCursor(10, _tft.height() - 10);
|
||||||
|
_tft.print("Hold to silence...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progressive fill hint - while touch is held
|
||||||
|
if(_alertTouchDown) {
|
||||||
|
uint32_t touchElapsed = millis() - _alertTouchStartMs;
|
||||||
|
float progress = (float)touchElapsed / (float)ALERT_FILL_DURATION_MS;
|
||||||
|
if(progress > 1.0f)
|
||||||
|
progress = 1.0f;
|
||||||
|
|
||||||
|
int dispH = _tft.height();
|
||||||
|
int fillHeight = (int)(dispH * progress);
|
||||||
|
if(fillHeight > 0) {
|
||||||
|
uint16_t overlay = _tft.color565(80, 0, 0);
|
||||||
|
_tft.fillRect(0, dispH - fillHeight, _tft.width(), fillHeight, overlay);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
// Header
|
// Use actual display dimensions (after rotation)
|
||||||
_tft.fillRect(0, 0, DISPLAY_WIDTH, 30, 0x1A1A); // Dark gray header
|
int dispW = _tft.width();
|
||||||
setBodyFont();
|
int dispH = _tft.height();
|
||||||
|
|
||||||
|
// Header - using standard bitmap font for reliable positioning
|
||||||
|
_tft.fillRect(0, 0, dispW, STYLE_HEADER_HEIGHT, 0x1A1A); // Dark gray header
|
||||||
|
_tft.setTextSize(1);
|
||||||
_tft.setTextColor(TFT_WHITE);
|
_tft.setTextColor(TFT_WHITE);
|
||||||
_tft.setCursor(5, 10);
|
_tft.setCursor(5, 20); // y=28 is baseline, text sits above this
|
||||||
_tft.print("KLUBHAUS");
|
_tft.print("KLUBHAUS");
|
||||||
|
|
||||||
// WiFi indicator
|
// WiFi indicator - right aligned in header
|
||||||
_tft.setCursor(DISPLAY_WIDTH - 60, 10);
|
const char* wifiText = st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF";
|
||||||
_tft.print(st.wifiSsid.length() > 0 ? "WiFi:ON" : "WiFi:OFF");
|
int wifiW = _tft.textWidth(wifiText);
|
||||||
|
_tft.setCursor(dispW - wifiW - 10, 20);
|
||||||
|
_tft.print(wifiText);
|
||||||
|
|
||||||
// Get tile layouts from library helper
|
// Get tile layouts from library helper
|
||||||
int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
|
int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
|
||||||
|
display.setHeaderHeight(STYLE_HEADER_HEIGHT);
|
||||||
const TileLayout* layouts = display.getTileLayouts();
|
const TileLayout* layouts = display.getTileLayouts();
|
||||||
|
|
||||||
const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" };
|
const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" };
|
||||||
@@ -250,6 +288,61 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DisplayDriverTFT::drawStatus(const ScreenState& st) {
|
||||||
|
int dispW = _tft.width();
|
||||||
|
int dispH = _tft.height();
|
||||||
|
|
||||||
|
_tft.fillScreen(TFT_BLACK);
|
||||||
|
|
||||||
|
// Header
|
||||||
|
_tft.fillRect(0, 0, dispW, STYLE_HEADER_HEIGHT, 0x1A1A);
|
||||||
|
_tft.setTextSize(1);
|
||||||
|
_tft.setTextColor(TFT_WHITE);
|
||||||
|
_tft.setCursor(5, 20);
|
||||||
|
_tft.print("STATUS");
|
||||||
|
|
||||||
|
// Back button in lower right
|
||||||
|
_tft.setCursor(dispW - 60, dispH - 20);
|
||||||
|
_tft.print("[BACK]");
|
||||||
|
|
||||||
|
// Status info
|
||||||
|
setBodyFont();
|
||||||
|
int y = STYLE_HEADER_HEIGHT + 20;
|
||||||
|
|
||||||
|
// WiFi
|
||||||
|
_tft.setCursor(10, y);
|
||||||
|
_tft.printf("WiFi: %s", st.wifiSsid.length() > 0 ? st.wifiSsid.c_str() : "N/A");
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
_tft.setCursor(10, y);
|
||||||
|
_tft.printf("RSSI: %d dBm", st.wifiRssi);
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
_tft.setCursor(10, y);
|
||||||
|
_tft.printf("IP: %s", st.ipAddr.length() > 0 ? st.ipAddr.c_str() : "N/A");
|
||||||
|
y += 30;
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
uint32_t upSec = st.uptimeMs / 1000;
|
||||||
|
uint32_t upMin = upSec / 60;
|
||||||
|
uint32_t upHr = upMin / 60;
|
||||||
|
upSec = upSec % 60;
|
||||||
|
upMin = upMin % 60;
|
||||||
|
_tft.setCursor(10, y);
|
||||||
|
_tft.printf("Uptime: %02lu:%02lu:%02lu", upHr, upMin, upSec);
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
// Heap
|
||||||
|
_tft.setCursor(10, y);
|
||||||
|
_tft.printf("Heap: %d bytes", ESP.getFreeHeap());
|
||||||
|
y += 20;
|
||||||
|
|
||||||
|
// Last poll
|
||||||
|
uint32_t pollAgo = (millis() - st.lastPollMs) / 1000;
|
||||||
|
_tft.setCursor(10, y);
|
||||||
|
_tft.printf("Last poll: %lu sec ago", pollAgo);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Touch ───────────────────────────────────────────────────
|
// ── Touch ───────────────────────────────────────────────────
|
||||||
|
|
||||||
TouchEvent DisplayDriverTFT::readTouch() {
|
TouchEvent DisplayDriverTFT::readTouch() {
|
||||||
@@ -285,6 +378,14 @@ TouchEvent DisplayDriverTFT::readTouch() {
|
|||||||
uint16_t tx, ty;
|
uint16_t tx, ty;
|
||||||
uint8_t touched = _tft.getTouch(&tx, &ty, 100);
|
uint8_t touched = _tft.getTouch(&tx, &ty, 100);
|
||||||
|
|
||||||
|
// Debug: log touch state changes with transformed coords
|
||||||
|
if(touched != _touchWasPressed) {
|
||||||
|
int tx_form = tx, ty_form = ty;
|
||||||
|
transformTouch(&tx_form, &ty_form);
|
||||||
|
Serial.printf("[TOUCH] raw touched=%d wasPressed=%d (raw=%d,%d trans=%d,%d)\n", touched,
|
||||||
|
_touchWasPressed, tx, ty, tx_form, ty_form);
|
||||||
|
}
|
||||||
|
|
||||||
// Detect transitions (press/release)
|
// Detect transitions (press/release)
|
||||||
if(touched && !_touchWasPressed) {
|
if(touched && !_touchWasPressed) {
|
||||||
// Press transition: finger just touched down
|
// Press transition: finger just touched down
|
||||||
@@ -310,21 +411,32 @@ TouchEvent DisplayDriverTFT::readTouch() {
|
|||||||
|
|
||||||
// Track previous state for next call
|
// Track previous state for next call
|
||||||
_touchWasPressed = touched;
|
_touchWasPressed = touched;
|
||||||
|
|
||||||
|
// Track alert touch for progressive hint
|
||||||
|
if(evt.pressed) {
|
||||||
|
_alertTouchDown = true;
|
||||||
|
_alertTouchStartMs = millis();
|
||||||
|
} else if(evt.released) {
|
||||||
|
_alertTouchDown = false;
|
||||||
|
_alertNeedsRedraw = true; // force redraw to clear fill
|
||||||
|
}
|
||||||
|
|
||||||
return evt;
|
return evt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::transformTouch(int* x, int* y) {
|
void DisplayDriverTFT::transformTouch(int* x, int* y) {
|
||||||
// Resistive touch panel is rotated 90° vs display - swap coordinates
|
// Resistive touch panel is rotated 90° vs display - swap and adjust
|
||||||
|
// Touch panel: 320x480 (portrait), Display: 480x320 (landscape)
|
||||||
|
// This was the original working transform
|
||||||
int temp = *x;
|
int temp = *x;
|
||||||
*x = *y;
|
*x = *y;
|
||||||
*y = temp;
|
*y = temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
|
HoldState DisplayDriverTFT::updateHold(const TouchEvent& evt, unsigned long holdMs) {
|
||||||
HoldState h;
|
HoldState h;
|
||||||
TouchEvent t = readTouch();
|
|
||||||
|
|
||||||
if(t.pressed) {
|
if(evt.pressed) {
|
||||||
if(!_holdActive) {
|
if(!_holdActive) {
|
||||||
_holdActive = true;
|
_holdActive = true;
|
||||||
_holdStartMs = millis();
|
_holdStartMs = millis();
|
||||||
@@ -336,13 +448,15 @@ HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
|
|||||||
h.completed = (held >= holdMs);
|
h.completed = (held >= holdMs);
|
||||||
|
|
||||||
// Simple progress bar at bottom of screen
|
// Simple progress bar at bottom of screen
|
||||||
int barW = (int)(DISPLAY_WIDTH * h.progress);
|
int dispW = _tft.width();
|
||||||
_tft.fillRect(0, DISPLAY_HEIGHT - 8, barW, 8, TFT_WHITE);
|
int dispH = _tft.height();
|
||||||
_tft.fillRect(barW, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH - barW, 8, TFT_DARKGREY);
|
int barW = (int)(dispW * h.progress);
|
||||||
|
_tft.fillRect(0, dispH - 8, barW, 8, TFT_WHITE);
|
||||||
|
_tft.fillRect(barW, dispH - 8, dispW - barW, 8, TFT_DARKGREY);
|
||||||
} else {
|
} else {
|
||||||
if(_holdActive) {
|
if(_holdActive) {
|
||||||
// Clear the progress bar when released
|
// Clear the progress bar when released
|
||||||
_tft.fillRect(0, DISPLAY_HEIGHT - 8, DISPLAY_WIDTH, 8, TFT_DARKGREY);
|
_tft.fillRect(0, _tft.height() - 8, _tft.width(), 8, TFT_DARKGREY);
|
||||||
}
|
}
|
||||||
_holdActive = false;
|
_holdActive = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,11 @@ public:
|
|||||||
void setBacklight(bool on) override;
|
void setBacklight(bool on) override;
|
||||||
void render(const ScreenState& state) override;
|
void render(const ScreenState& state) override;
|
||||||
TouchEvent readTouch() override;
|
TouchEvent readTouch() override;
|
||||||
HoldState updateHold(unsigned long holdMs) override;
|
HoldState updateHold(const TouchEvent& evt, unsigned long holdMs) override;
|
||||||
int width() override { return _tft.width(); }
|
int width() override {
|
||||||
|
// Use TFT_eSPI's dimensions after rotation - it's more reliable
|
||||||
|
return _tft.width();
|
||||||
|
}
|
||||||
int height() override { return _tft.height(); }
|
int height() override { return _tft.height(); }
|
||||||
|
|
||||||
// Dashboard - uses transform for touch coordinate correction
|
// Dashboard - uses transform for touch coordinate correction
|
||||||
@@ -28,6 +31,7 @@ private:
|
|||||||
void drawBoot(const ScreenState& st);
|
void drawBoot(const ScreenState& st);
|
||||||
void drawAlert(const ScreenState& st);
|
void drawAlert(const ScreenState& st);
|
||||||
void drawDashboard(const ScreenState& st);
|
void drawDashboard(const ScreenState& st);
|
||||||
|
void drawStatus(const ScreenState& st);
|
||||||
|
|
||||||
TFT_eSPI _tft;
|
TFT_eSPI _tft;
|
||||||
|
|
||||||
@@ -37,6 +41,13 @@ private:
|
|||||||
BootStage _lastBootStage = BootStage::SPLASH;
|
BootStage _lastBootStage = BootStage::SPLASH;
|
||||||
bool _needsRedraw = true;
|
bool _needsRedraw = true;
|
||||||
|
|
||||||
|
// Touch hint for alert - progressive fill from bottom
|
||||||
|
bool _alertTouchDown = false;
|
||||||
|
bool _alertNeedsRedraw = false; // force redraw after touch release
|
||||||
|
uint32_t _alertTouchStartMs = 0;
|
||||||
|
bool _lastAlertPhase = false; // tracks bright/dark phase for 2-color alert
|
||||||
|
static constexpr uint32_t ALERT_FILL_DURATION_MS = 3000;
|
||||||
|
|
||||||
// Touch tracking for press/release detection
|
// Touch tracking for press/release detection
|
||||||
bool _touchWasPressed = false;
|
bool _touchWasPressed = false;
|
||||||
int _touchDownX = -1;
|
int _touchDownX = -1;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
||||||
PORT="/dev/ttyUSB0"
|
PORT="/dev/ttyUSB0"
|
||||||
LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI"
|
LIBS="--library ./vendor/esp32-32e-4/TFT_eSPI"
|
||||||
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOAD_GFXFF=1"
|
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOAD_GFXFF=1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define BOARD_NAME "WS_32E_4"
|
#define BOARD_NAME "esp32-32e-4"
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════
|
||||||
// Hosyond ESP32-32E 4" (320x480) with ST7796 + XPT2046
|
// Hosyond ESP32-32E 4" (320x480) with ST7796 + XPT2046
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ void DisplayDriverTFT::drawDashboard(const ScreenState& st) {
|
|||||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
|
|
||||||
_tft.setTextSize(1);
|
_tft.setTextSize(1);
|
||||||
_tft.setCursor(5, 5);
|
_tft.setCursor(5, 10);
|
||||||
_tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState));
|
_tft.printf("KLUBHAUS — %s", deviceStateStr(st.deviceState));
|
||||||
|
|
||||||
int y = 30;
|
int y = 30;
|
||||||
@@ -298,11 +298,10 @@ int DisplayDriverTFT::dashboardTouch(int x, int y) {
|
|||||||
return row * 2 + col; // 0, 1, 2, or 3
|
return row * 2 + col; // 0, 1, 2, or 3
|
||||||
}
|
}
|
||||||
|
|
||||||
HoldState DisplayDriverTFT::updateHold(unsigned long holdMs) {
|
HoldState DisplayDriverTFT::updateHold(const TouchEvent& evt, unsigned long holdMs) {
|
||||||
HoldState h;
|
HoldState h;
|
||||||
TouchEvent t = readTouch();
|
|
||||||
|
|
||||||
if(t.pressed) {
|
if(evt.pressed) {
|
||||||
if(!_holdActive) {
|
if(!_holdActive) {
|
||||||
_holdActive = true;
|
_holdActive = true;
|
||||||
_holdStartMs = millis();
|
_holdStartMs = millis();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public:
|
|||||||
void render(const ScreenState& state) override;
|
void render(const ScreenState& state) override;
|
||||||
TouchEvent readTouch() override;
|
TouchEvent readTouch() override;
|
||||||
int dashboardTouch(int x, int y);
|
int dashboardTouch(int x, int y);
|
||||||
HoldState updateHold(unsigned long holdMs) override;
|
HoldState updateHold(const TouchEvent& evt, unsigned long holdMs) override;
|
||||||
int width() override { return DISPLAY_WIDTH; }
|
int width() override { return DISPLAY_WIDTH; }
|
||||||
int height() override { return DISPLAY_HEIGHT; }
|
int height() override { return DISPLAY_HEIGHT; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
||||||
PORT="/dev/ttyUSB0"
|
PORT="/dev/ttyUSB0"
|
||||||
LIBS="--libraries ./vendor/esp32-32e/TFT_eSPI"
|
LIBS="--library ./vendor/esp32-32e/TFT_eSPI"
|
||||||
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOCAL_SECRETS -DLOAD_GFXFF=1"
|
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLOCAL_SECRETS -DLOAD_GFXFF=1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define BOARD_NAME "WS_32E"
|
#define BOARD_NAME "esp32-32e"
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════
|
||||||
// TODO: Set these to match YOUR display + wiring.
|
// TODO: Set these to match YOUR display + wiring.
|
||||||
|
|||||||
@@ -194,6 +194,15 @@ TouchEvent DisplayDriverGFX::readTouch() {
|
|||||||
int32_t x, y;
|
int32_t x, y;
|
||||||
bool pressed = _gfx->getTouch(&x, &y);
|
bool pressed = _gfx->getTouch(&x, &y);
|
||||||
|
|
||||||
|
// Filter out invalid coordinates (touch panel can return garbage on release)
|
||||||
|
// Ignore both press and release transitions when coordinates are out of bounds
|
||||||
|
bool validCoords = !(x < 0 || x > DISP_W || y < 0 || y > DISP_H);
|
||||||
|
if(!validCoords) {
|
||||||
|
pressed = false;
|
||||||
|
// Don't update _lastTouch.pressed - keep previous state to avoid false release
|
||||||
|
return evt;
|
||||||
|
}
|
||||||
|
|
||||||
// Debounce: ignore repeated press events within debounce window after release
|
// Debounce: ignore repeated press events within debounce window after release
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
if(pressed && _touchBounced) {
|
if(pressed && _touchBounced) {
|
||||||
@@ -261,10 +270,10 @@ int DisplayDriverGFX::dashboardTouch(int x, int y) {
|
|||||||
return row * cols + col;
|
return row * cols + col;
|
||||||
}
|
}
|
||||||
|
|
||||||
HoldState DisplayDriverGFX::updateHold(unsigned long holdMs) {
|
HoldState DisplayDriverGFX::updateHold(const TouchEvent& evt, unsigned long holdMs) {
|
||||||
HoldState state;
|
HoldState state;
|
||||||
|
|
||||||
if(!_lastTouch.pressed) {
|
if(!evt.pressed) {
|
||||||
_isHolding = false;
|
_isHolding = false;
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -330,6 +339,13 @@ void DisplayDriverGFX::render(const ScreenState& state) {
|
|||||||
_needsRedraw = false;
|
_needsRedraw = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ScreenID::STATUS:
|
||||||
|
if(_needsRedraw) {
|
||||||
|
drawStatus(state);
|
||||||
|
_needsRedraw = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,24 +411,47 @@ void DisplayDriverGFX::drawAlert(const ScreenState& state) {
|
|||||||
void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
||||||
_gfx->fillScreen(STYLE_COLOR_BG);
|
_gfx->fillScreen(STYLE_COLOR_BG);
|
||||||
|
|
||||||
// Header
|
// Header - use Layout for safe positioning
|
||||||
_gfx->fillRect(0, 0, DISP_W, STYLE_HEADER_HEIGHT, STYLE_COLOR_HEADER);
|
_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();
|
setBodyFont();
|
||||||
_gfx->setTextColor(STYLE_COLOR_FG);
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
_gfx->setCursor(STYLE_SPACING_X, 12);
|
|
||||||
_gfx->printf("KLUBHAUS");
|
|
||||||
|
|
||||||
// WiFi status
|
// Title with scrolling support
|
||||||
_gfx->setCursor(DISP_W - 120, 12);
|
_headerScroller.setText("KLUBHAUS");
|
||||||
_gfx->printf("WiFi:%s", state.wifiSsid.length() > 0 ? "ON" : "OFF");
|
_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
|
// Get tile layouts from library helper
|
||||||
int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
|
int tileCount = display.calculateDashboardLayouts(STYLE_HEADER_HEIGHT, STYLE_TILE_GAP);
|
||||||
|
display.setHeaderHeight(STYLE_HEADER_HEIGHT);
|
||||||
const TileLayout* layouts = display.getTileLayouts();
|
const TileLayout* layouts = display.getTileLayouts();
|
||||||
|
|
||||||
const char* tileLabels[] = { "1", "2", "3", "4", "5", "6", "7", "8" };
|
const char* tileLabels[] = { "Alert", "Silent", "Status", "Reboot" };
|
||||||
|
const uint16_t tileColors[] = { 0x0280, 0x0400, 0x0440, 0x0100 };
|
||||||
|
|
||||||
for(int i = 0; i < tileCount && i < 8; i++) {
|
for(int i = 0; i < tileCount && i < 4; i++) {
|
||||||
const TileLayout& lay = layouts[i];
|
const TileLayout& lay = layouts[i];
|
||||||
int x = lay.x;
|
int x = lay.x;
|
||||||
int y = lay.y;
|
int y = lay.y;
|
||||||
@@ -420,7 +459,7 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
|||||||
int h = lay.h;
|
int h = lay.h;
|
||||||
|
|
||||||
// Tile background
|
// Tile background
|
||||||
_gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, 0x0220);
|
_gfx->fillRoundRect(x, y, w, h, STYLE_TILE_RADIUS, tileColors[i]);
|
||||||
|
|
||||||
// Tile border
|
// Tile border
|
||||||
_gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG);
|
_gfx->drawRoundRect(x, y, w, h, STYLE_TILE_RADIUS, STYLE_COLOR_FG);
|
||||||
@@ -428,11 +467,114 @@ void DisplayDriverGFX::drawDashboard(const ScreenState& state) {
|
|||||||
// Tile label
|
// Tile label
|
||||||
_gfx->setTextColor(STYLE_COLOR_FG);
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
setBodyFont();
|
setBodyFont();
|
||||||
_gfx->setCursor(x + w / 2 - 30, y + h / 2 - 10);
|
int textLen = strlen(tileLabels[i]);
|
||||||
|
int textW = textLen * 14;
|
||||||
|
_gfx->setCursor(x + w / 2 - textW / 2, y + h / 2 - 10);
|
||||||
_gfx->print(tileLabels[i]);
|
_gfx->print(tileLabels[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::drawStatus(const ScreenState& st) {
|
||||||
|
_gfx->fillScreen(STYLE_COLOR_BG);
|
||||||
|
|
||||||
|
// Header with title and back button
|
||||||
|
_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);
|
||||||
|
|
||||||
|
setTitleFont();
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(safeText.x, safeText.y + 4);
|
||||||
|
_gfx->print("STATUS");
|
||||||
|
|
||||||
|
// Back button in lower-right
|
||||||
|
setLabelFont();
|
||||||
|
_gfx->setCursor(DISP_W - 60, DISP_H - 20);
|
||||||
|
_gfx->print("[BACK]");
|
||||||
|
|
||||||
|
// Status items in 2-column layout
|
||||||
|
setBodyFont();
|
||||||
|
int colWidth = DISP_W / 2;
|
||||||
|
int startY = STYLE_HEADER_HEIGHT + 30;
|
||||||
|
int rowHeight = 35;
|
||||||
|
int labelX = STYLE_SPACING_X + 10;
|
||||||
|
int valueX = STYLE_SPACING_X + 120;
|
||||||
|
|
||||||
|
// Column 1
|
||||||
|
int y = startY;
|
||||||
|
|
||||||
|
// WiFi SSID
|
||||||
|
_gfx->setTextColor(0x8888);
|
||||||
|
_gfx->setCursor(labelX, y);
|
||||||
|
_gfx->print("WiFi:");
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(valueX, y);
|
||||||
|
_gfx->print(st.wifiSsid.length() > 0 ? st.wifiSsid.c_str() : "N/A");
|
||||||
|
|
||||||
|
// RSSI
|
||||||
|
y += rowHeight;
|
||||||
|
_gfx->setTextColor(0x8888);
|
||||||
|
_gfx->setCursor(labelX, y);
|
||||||
|
_gfx->print("Signal:");
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(valueX, y);
|
||||||
|
_gfx->printf("%d dBm", st.wifiRssi);
|
||||||
|
|
||||||
|
// IP Address
|
||||||
|
y += rowHeight;
|
||||||
|
_gfx->setTextColor(0x8888);
|
||||||
|
_gfx->setCursor(labelX, y);
|
||||||
|
_gfx->print("IP:");
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(valueX, y);
|
||||||
|
_gfx->print(st.ipAddr.length() > 0 ? st.ipAddr.c_str() : "N/A");
|
||||||
|
|
||||||
|
// Column 2
|
||||||
|
y = startY;
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
uint32_t upSec = st.uptimeMs / 1000;
|
||||||
|
uint32_t upMin = upSec / 60;
|
||||||
|
uint32_t upHr = upMin / 60;
|
||||||
|
upSec = upSec % 60;
|
||||||
|
upMin = upMin % 60;
|
||||||
|
_gfx->setTextColor(0x8888);
|
||||||
|
_gfx->setCursor(colWidth + labelX, y);
|
||||||
|
_gfx->print("Uptime:");
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(colWidth + valueX, y);
|
||||||
|
_gfx->printf("%02lu:%02lu:%02lu", upHr, upMin, upSec);
|
||||||
|
|
||||||
|
// Heap
|
||||||
|
y += rowHeight;
|
||||||
|
_gfx->setTextColor(0x8888);
|
||||||
|
_gfx->setCursor(colWidth + labelX, y);
|
||||||
|
_gfx->print("Heap:");
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(colWidth + valueX, y);
|
||||||
|
_gfx->printf("%d KB", ESP.getFreeHeap() / 1024);
|
||||||
|
|
||||||
|
// Last Poll
|
||||||
|
y += rowHeight;
|
||||||
|
_gfx->setTextColor(0x8888);
|
||||||
|
_gfx->setCursor(colWidth + labelX, y);
|
||||||
|
_gfx->print("Last Poll:");
|
||||||
|
_gfx->setTextColor(STYLE_COLOR_FG);
|
||||||
|
_gfx->setCursor(colWidth + valueX, y);
|
||||||
|
uint32_t pollAgo = (millis() - st.lastPollMs) / 1000;
|
||||||
|
if(pollAgo < 60) {
|
||||||
|
_gfx->printf("%lu sec", pollAgo);
|
||||||
|
} else {
|
||||||
|
_gfx->printf("%lu min", pollAgo / 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Footer with firmware version
|
||||||
|
setLabelFont();
|
||||||
|
_gfx->setTextColor(0x6666);
|
||||||
|
_gfx->setCursor(STYLE_SPACING_X, DISP_H - STYLE_SPACING_Y);
|
||||||
|
_gfx->print("v" FW_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
void DisplayDriverGFX::drawDebugTouch(int x, int y) {
|
void DisplayDriverGFX::drawDebugTouch(int x, int y) {
|
||||||
if(!_gfx)
|
if(!_gfx)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "IDisplayDriver.h"
|
#include "IDisplayDriver.h"
|
||||||
|
#include "Style.h"
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@ public:
|
|||||||
void render(const ScreenState& state) override;
|
void render(const ScreenState& state) override;
|
||||||
|
|
||||||
TouchEvent readTouch() override;
|
TouchEvent readTouch() override;
|
||||||
HoldState updateHold(unsigned long holdMs) override;
|
HoldState updateHold(const TouchEvent& evt, unsigned long holdMs) override;
|
||||||
void drawDebugTouch(int x, int y) override;
|
void drawDebugTouch(int x, int y) override;
|
||||||
|
|
||||||
int width() override;
|
int width() override;
|
||||||
@@ -41,6 +42,7 @@ private:
|
|||||||
void drawBoot(const ScreenState& state);
|
void drawBoot(const ScreenState& state);
|
||||||
void drawAlert(const ScreenState& state);
|
void drawAlert(const ScreenState& state);
|
||||||
void drawDashboard(const ScreenState& state);
|
void drawDashboard(const ScreenState& state);
|
||||||
|
void drawStatus(const ScreenState& state);
|
||||||
|
|
||||||
// Touch handling
|
// Touch handling
|
||||||
TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 };
|
TouchEvent _lastTouch = { false, false, 0, 0, -1, -1 };
|
||||||
@@ -54,4 +56,8 @@ private:
|
|||||||
ScreenID _lastScreen = ScreenID::BOOT;
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
BootStage _lastBootStage = BootStage::SPLASH;
|
BootStage _lastBootStage = BootStage::SPLASH;
|
||||||
bool _needsRedraw = true;
|
bool _needsRedraw = true;
|
||||||
|
|
||||||
|
// Text scrollers for scrolling elements
|
||||||
|
TextScroller _headerScroller;
|
||||||
|
TextScroller _wifiScroller;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB"
|
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="--libraries ~/Arduino/libraries/LovyanGFX"
|
LIBS="--library ./vendor/esp32-s3-lcd-43/LovyanGFX"
|
||||||
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1 -DLOCAL_SECRETS"
|
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1 -DLOCAL_SECRETS"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#define BOARD_NAME "WS_S3_43"
|
#define BOARD_NAME "esp32-s3-lcd-43"
|
||||||
#define DISPLAY_WIDTH 800
|
#define DISPLAY_WIDTH 800
|
||||||
#define DISPLAY_HEIGHT 480
|
#define DISPLAY_HEIGHT 480
|
||||||
#define DISPLAY_ROTATION 0
|
#define DISPLAY_ROTATION 0
|
||||||
|
|||||||
189
justfile
Normal file
189
justfile
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
# Klubhaus Doorbell - justfile
|
||||||
|
# Run with: just <recipe>
|
||||||
|
# Set BOARD: just BOARD=esp32-32e-4 compile
|
||||||
|
|
||||||
|
# Default recipe - show help
|
||||||
|
default:
|
||||||
|
@just --list
|
||||||
|
|
||||||
|
# Set default BOARD
|
||||||
|
BOARD := "esp32-32e-4"
|
||||||
|
|
||||||
|
# Helper to source board config
|
||||||
|
# just passes args as positional, so we source in each recipe
|
||||||
|
|
||||||
|
# Compile firmware
|
||||||
|
compile:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
source ./boards/{{BOARD}}/board-config.sh
|
||||||
|
|
||||||
|
# Only regenerate compile_commands.json if needed (board changed or first run)
|
||||||
|
NEED_GEN=false
|
||||||
|
if [ ! -f compile_commands.json ]; then
|
||||||
|
NEED_GEN=true
|
||||||
|
elif [ ! -f .board-last ] || [ "$(cat .board-last)" != "{{BOARD}}" ]; then
|
||||||
|
NEED_GEN=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$NEED_GEN" = "true" ]; then
|
||||||
|
rm -rf /tmp/arduino-build
|
||||||
|
arduino-cli compile --only-compilation-database --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --build-path /tmp/arduino-build ./boards/{{BOARD}}
|
||||||
|
cp /tmp/arduino-build/compile_commands.json .
|
||||||
|
echo "{{BOARD}}" > .board-last
|
||||||
|
echo "[OK] Generated compile_commands.json for {{BOARD}}"
|
||||||
|
else
|
||||||
|
echo "[SKIP] compile_commands.json already up to date for {{BOARD}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --warnings default ./boards/{{BOARD}}
|
||||||
|
|
||||||
|
# Upload firmware
|
||||||
|
upload: kill
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
source ./boards/{{BOARD}}/board-config.sh
|
||||||
|
arduino-cli upload --fqbn "$FQBN" --port "$PORT" ./boards/{{BOARD}}
|
||||||
|
|
||||||
|
# Kill processes using serial port
|
||||||
|
kill:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set +e
|
||||||
|
source ./boards/{{BOARD}}/board-config.sh
|
||||||
|
PORT="${PORT:-$PORT}"
|
||||||
|
echo "Killing processes on $PORT..."
|
||||||
|
fuser -k "$PORT" 2>/dev/null || true
|
||||||
|
for pid in $(pgrep -f "monitor-agent.py.*{{BOARD}}" 2>/dev/null || true); do
|
||||||
|
echo "Killing monitor-agent.py (PID: $pid)..."
|
||||||
|
kill "$pid" 2>/dev/null || true
|
||||||
|
done
|
||||||
|
rm -f "/tmp/doorbell-{{BOARD}}.lock" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
echo "[OK] Killed processes for {{BOARD}}"
|
||||||
|
|
||||||
|
# Monitor raw serial
|
||||||
|
monitor-raw: kill
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
source ./boards/{{BOARD}}/board-config.sh
|
||||||
|
PORT="${PORT:-$PORT}"
|
||||||
|
TARGET="$(readlink -f "$PORT" 2>/dev/null || echo "$PORT")"
|
||||||
|
arduino-cli monitor -p "$TARGET" --config baudrate=115200
|
||||||
|
|
||||||
|
# Show tio command
|
||||||
|
monitor-tio: kill
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
source ./boards/{{BOARD}}/board-config.sh
|
||||||
|
PORT="${PORT:-$PORT}"
|
||||||
|
TARGET="$(readlink -f "$PORT" 2>/dev/null || echo "$PORT")"
|
||||||
|
echo "Run: tio --map INLCRNL $TARGET -e"
|
||||||
|
|
||||||
|
# Monitor with JSON logging
|
||||||
|
monitor: kill
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
python3 ./scripts/monitor-agent.py "{{BOARD}}" &
|
||||||
|
|
||||||
|
# Tail colored logs
|
||||||
|
watch:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
tail -f "/tmp/doorbell-{{BOARD}}.jsonl" | while read -r line; do
|
||||||
|
ts=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin)['ts'])" 2>/dev/null)
|
||||||
|
txt=$(echo "$line" | python3 -c "import sys,json; print(json.load(sys.stdin)['line'])" 2>/dev/null)
|
||||||
|
if [[ "$txt" == *"[STATE]"* ]]; then
|
||||||
|
echo -e "\033[1;35m[$ts]\033[0m $txt"
|
||||||
|
elif [[ "$txt" == *"[ADMIN]"* ]]; then
|
||||||
|
echo -e "\033[1;36m[$ts]\033[0m $txt"
|
||||||
|
elif [[ "$txt" == *"[TOUCH]"* ]]; then
|
||||||
|
echo -e "\033[1;33m[$ts]\033[0m $txt"
|
||||||
|
elif [[ "$txt" == *"ALERT"* ]]; then
|
||||||
|
echo -e "\033[1;31m[$ts]\033[0m $txt"
|
||||||
|
else
|
||||||
|
echo "[$ts] $txt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Send command to device
|
||||||
|
cmd command:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo -n "{{command}}" > "/tmp/doorbell-{{BOARD}}-cmd.fifo"
|
||||||
|
echo "[SENT] {{command}}"
|
||||||
|
|
||||||
|
# Show device state
|
||||||
|
state:
|
||||||
|
@cat "/tmp/doorbell-{{BOARD}}-state.json"
|
||||||
|
|
||||||
|
# Detect connected board
|
||||||
|
detect:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
bash ./scripts/detect-device.sh
|
||||||
|
|
||||||
|
# Install shared libraries
|
||||||
|
install-libs-shared:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
./scripts/install-shared.sh
|
||||||
|
|
||||||
|
# Install all libraries
|
||||||
|
install: install-libs-shared
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
./boards/{{BOARD}}/install.sh
|
||||||
|
arduino-cli core install esp32:esp32
|
||||||
|
|
||||||
|
# Clean build artifacts
|
||||||
|
clean:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
rm -rf vendor/
|
||||||
|
rm -rf .cache/
|
||||||
|
rm -rf boards/esp32-32e/build
|
||||||
|
rm -rf boards/esp32-32e-4/build
|
||||||
|
rm -rf boards/esp32-s3-lcd-43/build
|
||||||
|
rm -f .board-last
|
||||||
|
echo "[OK] Build artifacts cleaned"
|
||||||
|
|
||||||
|
# Clean temporary files (monitor logs, FIFOs, state files)
|
||||||
|
clean-temp:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
rm -f /tmp/doorbell-esp32-32e.jsonl /tmp/doorbell-esp32-32e-state.json /tmp/doorbell-esp32-32e-cmd.fifo
|
||||||
|
rm -f /tmp/doorbell-esp32-32e-4.jsonl /tmp/doorbell-esp32-32e-4-state.json /tmp/doorbell-esp32-32e-4-cmd.fifo
|
||||||
|
rm -f /tmp/doorbell-esp32-s3-lcd-43.jsonl /tmp/doorbell-esp32-s3-lcd-43-state.json /tmp/doorbell-esp32-s3-lcd-43-cmd.fifo
|
||||||
|
rm -f .board-last
|
||||||
|
echo "[OK] Temp files cleaned"
|
||||||
|
|
||||||
|
# Clean Arduino cache
|
||||||
|
arduino-clean:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "Checking ~/.arduino15..."
|
||||||
|
du -sh ~/.arduino15/staging 2>/dev/null || echo "No staging folder"
|
||||||
|
du -sh ~/.arduino15/packages 2>/dev/null || echo "No packages folder"
|
||||||
|
read -p "Delete staging + packages folders? [y/N] " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
rm -rf ~/.arduino15/staging
|
||||||
|
rm -rf ~/.arduino15/packages
|
||||||
|
echo "[OK] Arduino staging + packages cleared"
|
||||||
|
else
|
||||||
|
echo "Aborted"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
format:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
clang-format -i --style=file \
|
||||||
|
boards/esp32-32e/*.cpp \
|
||||||
|
boards/esp32-32e/*.h \
|
||||||
|
boards/esp32-32e/*.ino \
|
||||||
|
boards/esp32-32e-4/*.cpp \
|
||||||
|
boards/esp32-32e-4/*.h \
|
||||||
|
boards/esp32-32e-4/*.ino \
|
||||||
|
boards/esp32-s3-lcd-43/*.cpp \
|
||||||
|
boards/esp32-s3-lcd-43/*.h \
|
||||||
|
boards/esp32-s3-lcd-43/*.ino \
|
||||||
|
libraries/KlubhausCore/src/*.cpp \
|
||||||
|
libraries/KlubhausCore/src/*.h \
|
||||||
|
libraries/KlubhausCore/*.properties
|
||||||
|
|
||||||
|
# Generate crush config
|
||||||
|
gen-crush-config:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
source ./boards/{{BOARD}}/board-config.sh
|
||||||
|
printf '{\n "lsp": {\n "arduino": {\n "command": "arduino-language-server",\n "args": ["-fqbn", "%s"]\n },\n "cpp": {\n "command": "clangd"\n }\n }\n}\n' "$FQBN" > .crush.json
|
||||||
|
echo "[OK] Generated .crush.json with FQBN: $FQBN"
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
#define POLL_INTERVAL_MS 15000
|
#define POLL_INTERVAL_MS 15000
|
||||||
#define HEARTBEAT_INTERVAL_MS 300000
|
#define HEARTBEAT_INTERVAL_MS 300000
|
||||||
#define BOOT_GRACE_MS 5000
|
#define BOOT_GRACE_MS 5000
|
||||||
#define HOLD_TO_SILENCE_MS 3000
|
#define HOLD_TO_SILENCE_MS 2000
|
||||||
#define ALERT_TIMEOUT_MS 120000
|
#define ALERT_TIMEOUT_MS 120000
|
||||||
#define SILENCE_DISPLAY_MS 10000
|
#define SILENCE_DISPLAY_MS 10000
|
||||||
#define INACTIVITY_TIMEOUT_MS 30000
|
#define INACTIVITY_TIMEOUT_MS 30000
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "IDisplayDriver.h"
|
#include "IDisplayDriver.h"
|
||||||
#include "ScreenState.h"
|
#include "ScreenState.h"
|
||||||
|
#include "Style.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@@ -59,6 +60,14 @@ private:
|
|||||||
/// Calculate optimal grid dimensions based on display and tile constraints
|
/// Calculate optimal grid dimensions based on display and tile constraints
|
||||||
static void calculateGrid(
|
static void calculateGrid(
|
||||||
int tileCount, int displayW, int contentH, int* outCols, int* outRows) {
|
int tileCount, int displayW, int contentH, int* outCols, int* outRows) {
|
||||||
|
// Use 2x2 grid for 4 tiles regardless of aspect ratio
|
||||||
|
// This provides better visual balance than a single row
|
||||||
|
if(tileCount == 4) {
|
||||||
|
*outCols = 2;
|
||||||
|
*outRows = 2;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate aspect ratio to determine preferred layout
|
// Calculate aspect ratio to determine preferred layout
|
||||||
float aspectRatio = (float)displayW / contentH;
|
float aspectRatio = (float)displayW / contentH;
|
||||||
|
|
||||||
@@ -137,7 +146,9 @@ public:
|
|||||||
|
|
||||||
TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent {}; }
|
TouchEvent readTouch() { return _drv ? _drv->readTouch() : TouchEvent {}; }
|
||||||
|
|
||||||
HoldState updateHold(unsigned long ms) { return _drv ? _drv->updateHold(ms) : HoldState {}; }
|
HoldState updateHold(const TouchEvent& evt, unsigned long ms) {
|
||||||
|
return _drv ? _drv->updateHold(evt, ms) : HoldState {};
|
||||||
|
}
|
||||||
|
|
||||||
void drawDebugTouch(int x, int y) {
|
void drawDebugTouch(int x, int y) {
|
||||||
if(_drv)
|
if(_drv)
|
||||||
@@ -155,7 +166,7 @@ public:
|
|||||||
_drv->transformTouch(&dx, &dy);
|
_drv->transformTouch(&dx, &dy);
|
||||||
_drv->transformTouch(&cx, &cy);
|
_drv->transformTouch(&cx, &cy);
|
||||||
|
|
||||||
int headerH = 30;
|
int headerH = _headerHeight;
|
||||||
int cellW = _drv->width() / _gridCols;
|
int cellW = _drv->width() / _gridCols;
|
||||||
int cellH = (_drv->height() - headerH) / _gridRows;
|
int cellH = (_drv->height() - headerH) / _gridRows;
|
||||||
|
|
||||||
@@ -171,51 +182,83 @@ public:
|
|||||||
int height() { return _drv ? _drv->height() : 0; }
|
int height() { return _drv ? _drv->height() : 0; }
|
||||||
|
|
||||||
/// Handle dashboard touch - returns action for tapped tile, or NONE
|
/// Handle dashboard touch - returns action for tapped tile, or NONE
|
||||||
|
/// Note: x,y are already in display coordinates (transformed by driver)
|
||||||
TileAction handleDashboardTouch(int x, int y) const {
|
TileAction handleDashboardTouch(int x, int y) const {
|
||||||
if(!_drv || _gridCols <= 0)
|
HitResult hr = hitTestRaw(x, y);
|
||||||
return TileAction::NONE;
|
if(hr.type == UIElementType::TILE && hr.index >= 0 && hr.index < DASHBOARD_TILE_COUNT) {
|
||||||
|
return DASHBOARD_TILES[hr.index].action;
|
||||||
|
}
|
||||||
|
return TileAction::NONE;
|
||||||
|
}
|
||||||
|
|
||||||
// Transform touch coordinates (handles rotated touch panels)
|
/// Perform hit test at coordinates (already in display space, no transform)
|
||||||
_drv->transformTouch(&x, &y);
|
HitResult hitTestRaw(int x, int y) const {
|
||||||
|
if(!_drv)
|
||||||
|
return HitResult();
|
||||||
|
|
||||||
int dispW = _drv->width();
|
int dispW = _drv->width();
|
||||||
int dispH = _drv->height();
|
int dispH = _drv->height();
|
||||||
int headerH = 30;
|
int headerH = _headerHeight;
|
||||||
|
|
||||||
// Check if in header area
|
// Check header
|
||||||
if(y < headerH)
|
Rect headerRect = UIElements::header(dispW, headerH);
|
||||||
return TileAction::NONE;
|
if(headerRect.contains(x, y)) {
|
||||||
|
return HitResult(UIElementType::HEADER, 0, headerRect);
|
||||||
// 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
|
// Check tiles
|
||||||
for(int i = 0; i < _tileCount; i++) {
|
for(int i = 0; i < _tileCount; i++) {
|
||||||
const TileLayout& layout = _layouts[i];
|
const TileLayout& lay = _layouts[i];
|
||||||
if(layout.col <= col && col < layout.col + layout.cols && layout.row <= row
|
Rect tileRect(lay.x, lay.y, lay.w, lay.h);
|
||||||
&& row < layout.row + layout.rows) {
|
if(tileRect.contains(x, y)) {
|
||||||
return DASHBOARD_TILES[i].action;
|
return HitResult(UIElementType::TILE, i, tileRect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TileAction::NONE;
|
return HitResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Perform hit test at coordinates - returns element type, index, and bounds
|
||||||
|
HitResult hitTest(int x, int y) const {
|
||||||
|
if(!_drv)
|
||||||
|
return HitResult();
|
||||||
|
|
||||||
|
int tx = x, ty = y;
|
||||||
|
_drv->transformTouch(&tx, &ty);
|
||||||
|
|
||||||
|
int dispW = _drv->width();
|
||||||
|
int dispH = _drv->height();
|
||||||
|
int headerH = _headerHeight;
|
||||||
|
|
||||||
|
// Check header
|
||||||
|
Rect headerRect = UIElements::header(dispW, headerH);
|
||||||
|
if(headerRect.contains(tx, ty)) {
|
||||||
|
return HitResult(UIElementType::HEADER, 0, headerRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check tiles
|
||||||
|
for(int i = 0; i < _tileCount; i++) {
|
||||||
|
const TileLayout& lay = _layouts[i];
|
||||||
|
Rect tileRect(lay.x, lay.y, lay.w, lay.h);
|
||||||
|
if(tileRect.contains(tx, ty)) {
|
||||||
|
return HitResult(UIElementType::TILE, i, tileRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return HitResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set header height for hit testing (call after calculateDashboardLayouts or manually)
|
||||||
|
void setHeaderHeight(int h) { _headerHeight = h; }
|
||||||
|
int getHeaderHeight() const { return _headerHeight; }
|
||||||
|
|
||||||
/// Calculate and store layouts for dashboard tiles
|
/// Calculate and store layouts for dashboard tiles
|
||||||
/// Called by drivers who want to use the layout helper
|
/// Called by drivers who want to use the layout helper
|
||||||
int calculateDashboardLayouts(int headerH = 30, int margin = 8) {
|
int calculateDashboardLayouts(int headerH = 30, int margin = 8) {
|
||||||
if(!_drv)
|
if(!_drv)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
_headerHeight = headerH;
|
||||||
_tileCount = TileLayoutHelper::calculateLayouts(
|
_tileCount = TileLayoutHelper::calculateLayouts(
|
||||||
_drv->width(), _drv->height(), headerH, margin, _layouts, &_gridCols, &_gridRows);
|
_drv->width(), _drv->height(), headerH, margin, _layouts, &_gridCols, &_gridRows);
|
||||||
return _tileCount;
|
return _tileCount;
|
||||||
@@ -232,4 +275,5 @@ private:
|
|||||||
int _tileCount = 0;
|
int _tileCount = 0;
|
||||||
int _gridCols = 0;
|
int _gridCols = 0;
|
||||||
int _gridRows = 0;
|
int _gridRows = 0;
|
||||||
|
int _headerHeight = 30;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -304,8 +304,24 @@ void DoorbellLogic::onSerialCommand(const String& cmd) {
|
|||||||
Serial.println();
|
Serial.println();
|
||||||
Serial.printf("[NET] %s RSSI:%d IP:%s\n", _state.wifiSsid.c_str(), _state.wifiRssi,
|
Serial.printf("[NET] %s RSSI:%d IP:%s\n", _state.wifiSsid.c_str(), _state.wifiRssi,
|
||||||
_state.ipAddr.c_str());
|
_state.ipAddr.c_str());
|
||||||
|
} else if(cmd == "board") {
|
||||||
|
Serial.printf("[BOARD] %s\n", _board);
|
||||||
|
} else if(cmd.startsWith("hittest ")) {
|
||||||
|
String args = cmd.substring(8);
|
||||||
|
int comma = args.indexOf(',');
|
||||||
|
if(comma > 0) {
|
||||||
|
int x = args.substring(0, comma).toInt();
|
||||||
|
int y = args.substring(comma + 1).toInt();
|
||||||
|
HitResult hr = _display->hitTestRaw(x, y);
|
||||||
|
Serial.printf("[Hittest] raw:(%d,%d) type:%d index:%d bounds:(%d,%d,%d,%d)\n", x, y,
|
||||||
|
(int)hr.type, hr.index, hr.bounds.x, hr.bounds.y, hr.bounds.w, hr.bounds.h);
|
||||||
|
Serial.printf("[Display] w:%d h:%d headerH:%d\n", _display->width(), _display->height(),
|
||||||
|
_display->getHeaderHeight());
|
||||||
|
} else {
|
||||||
|
Serial.println("[Hittest] Usage: hittest x,y");
|
||||||
|
}
|
||||||
} else
|
} 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) {
|
void DoorbellLogic::setScreen(ScreenID s) {
|
||||||
@@ -319,8 +335,19 @@ void DoorbellLogic::setScreen(ScreenID s) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
||||||
|
// Check hold completion FIRST - before any release handling
|
||||||
|
// This ensures hold-to-silence works on ALERT screen
|
||||||
|
if(evt.released && _state.deviceState == DeviceState::ALERTING) {
|
||||||
|
Serial.printf("[TOUCH] checking hold: deviceState=ALERTING\n");
|
||||||
|
if(updateHold(evt)) {
|
||||||
|
return (int)TileAction::SILENCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle press - show visual feedback
|
// Handle press - show visual feedback
|
||||||
if(evt.pressed) {
|
if(evt.pressed) {
|
||||||
|
Serial.printf("[TOUCH] pressed screen=%d deviceState=%d\n", (int)_state.screen,
|
||||||
|
(int)_state.deviceState);
|
||||||
// Reset inactivity timer on any touch
|
// Reset inactivity timer on any touch
|
||||||
_lastActivityMs = millis();
|
_lastActivityMs = millis();
|
||||||
|
|
||||||
@@ -330,6 +357,18 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle STATUS screen back button (lower right corner)
|
||||||
|
if(_state.screen == ScreenID::STATUS) {
|
||||||
|
int dispW = _display->width();
|
||||||
|
int dispH = _display->height();
|
||||||
|
// Back button area: lower right (dispW-60 to dispW, dispH-30 to dispH)
|
||||||
|
if(evt.x >= dispW - 60 && evt.y >= dispH - 30) {
|
||||||
|
Serial.printf("[%lu] [TOUCH] STATUS → DASHBOARD (back)\n", millis());
|
||||||
|
setScreen(ScreenID::DASHBOARD);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw debug crosshair at touch point
|
// Draw debug crosshair at touch point
|
||||||
#ifdef DEBUG_MODE
|
#ifdef DEBUG_MODE
|
||||||
if(_state.screen == ScreenID::DASHBOARD) {
|
if(_state.screen == ScreenID::DASHBOARD) {
|
||||||
@@ -341,6 +380,9 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
|||||||
|
|
||||||
// Handle release - fire action if same tile
|
// Handle release - fire action if same tile
|
||||||
if(evt.released) {
|
if(evt.released) {
|
||||||
|
Serial.printf("[TOUCH] released screen=%d deviceState=%d\n", (int)_state.screen,
|
||||||
|
(int)_state.deviceState);
|
||||||
|
|
||||||
if(_state.screen == ScreenID::DASHBOARD) {
|
if(_state.screen == ScreenID::DASHBOARD) {
|
||||||
// Only fire action if finger stayed on same tile
|
// Only fire action if finger stayed on same tile
|
||||||
if(evt.downX >= 0 && _display->isSameTile(evt.downX, evt.downY, evt.x, evt.y)) {
|
if(evt.downX >= 0 && _display->isSameTile(evt.downX, evt.downY, evt.x, evt.y)) {
|
||||||
@@ -356,7 +398,7 @@ int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
|||||||
silenceAlert();
|
silenceAlert();
|
||||||
break;
|
break;
|
||||||
case TileAction::STATUS:
|
case TileAction::STATUS:
|
||||||
heartbeat();
|
setScreen(ScreenID::STATUS);
|
||||||
break;
|
break;
|
||||||
case TileAction::REBOOT:
|
case TileAction::REBOOT:
|
||||||
flushStatus("REBOOT (tile)");
|
flushStatus("REBOOT (tile)");
|
||||||
@@ -390,7 +432,7 @@ bool DoorbellLogic::updateHold(const TouchEvent& evt) {
|
|||||||
static int holdStartX = -1;
|
static int holdStartX = -1;
|
||||||
static int holdStartY = -1;
|
static int holdStartY = -1;
|
||||||
|
|
||||||
HoldState h = _display->updateHold(HOLD_TO_SILENCE_MS);
|
HoldState h = _display->updateHold(evt, HOLD_TO_SILENCE_MS);
|
||||||
|
|
||||||
if(h.completed) {
|
if(h.completed) {
|
||||||
silenceAlert();
|
silenceAlert();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ public:
|
|||||||
void processSerial();
|
void processSerial();
|
||||||
|
|
||||||
const ScreenState& getScreenState() const { return _state; }
|
const ScreenState& getScreenState() const { return _state; }
|
||||||
|
const char* getBoard() const { return _board; }
|
||||||
|
|
||||||
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
||||||
void silenceAlert();
|
void silenceAlert();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public:
|
|||||||
|
|
||||||
// ── Touch ──
|
// ── Touch ──
|
||||||
virtual TouchEvent readTouch() = 0;
|
virtual TouchEvent readTouch() = 0;
|
||||||
virtual HoldState updateHold(unsigned long holdMs) = 0;
|
virtual HoldState updateHold(const TouchEvent& evt, unsigned long holdMs) = 0;
|
||||||
virtual void drawDebugTouch(int x, int y) { /* default: no-op */ }
|
virtual void drawDebugTouch(int x, int y) { /* default: no-op */ }
|
||||||
virtual int width() = 0;
|
virtual int width() = 0;
|
||||||
virtual int height() = 0;
|
virtual int height() = 0;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
enum class DeviceState { BOOTED, SILENT, ALERTING, SILENCED };
|
enum class DeviceState { BOOTED, SILENT, ALERTING, SILENCED };
|
||||||
|
|
||||||
enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD };
|
enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD, STATUS };
|
||||||
|
|
||||||
enum class BootStage { SPLASH, INIT_DISPLAY, INIT_NETWORK, CONNECTING_WIFI, READY, DONE };
|
enum class BootStage { SPLASH, INIT_DISPLAY, INIT_NETWORK, CONNECTING_WIFI, READY, DONE };
|
||||||
|
|
||||||
@@ -94,6 +94,8 @@ inline const char* screenIdStr(ScreenID s) {
|
|||||||
return "ALERT";
|
return "ALERT";
|
||||||
case ScreenID::DASHBOARD:
|
case ScreenID::DASHBOARD:
|
||||||
return "DASHBOARD";
|
return "DASHBOARD";
|
||||||
|
case ScreenID::STATUS:
|
||||||
|
return "STATUS";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
|
||||||
struct Layout {
|
struct Layout {
|
||||||
@@ -36,6 +37,89 @@ struct Layout {
|
|||||||
uint16_t y = row * (tileH + gap);
|
uint16_t y = row * (tileH + gap);
|
||||||
return Layout(x, y, tileW, tileH);
|
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 <typename DrawFn> 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 {
|
struct TileMetrics {
|
||||||
@@ -60,3 +144,86 @@ struct TileMetrics {
|
|||||||
return Layout(col * (tileW + gap), row * (tileH + gap), tileW, tileH);
|
return Layout(col * (tileW + gap), row * (tileH + gap), tileW, tileH);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Rect {
|
||||||
|
int16_t x, y;
|
||||||
|
int16_t w, h;
|
||||||
|
|
||||||
|
Rect()
|
||||||
|
: x(0)
|
||||||
|
, y(0)
|
||||||
|
, w(0)
|
||||||
|
, h(0) { }
|
||||||
|
Rect(int16_t x_, int16_t y_, int16_t w_, int16_t h_)
|
||||||
|
: x(x_)
|
||||||
|
, y(y_)
|
||||||
|
, w(w_)
|
||||||
|
, h(h_) { }
|
||||||
|
|
||||||
|
int16_t left() const { return x; }
|
||||||
|
int16_t top() const { return y; }
|
||||||
|
int16_t right() const { return x + w; }
|
||||||
|
int16_t bottom() const { return y + h; }
|
||||||
|
|
||||||
|
bool contains(int16_t px, int16_t py) const {
|
||||||
|
return px >= x && px < x + w && py >= y && py < y + h;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool intersects(const Rect& other) const {
|
||||||
|
return !(
|
||||||
|
right() <= other.x || other.right() <= x || bottom() <= other.y || other.bottom() <= y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect expanded(int16_t dx, int16_t dy) const {
|
||||||
|
return Rect(x - dx, y - dy, w + 2 * dx, h + 2 * dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect translated(int16_t dx, int16_t dy) const { return Rect(x + dx, y + dy, w, h); }
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class UIElementType { NONE, HEADER, TILE, BUTTON, ICON };
|
||||||
|
|
||||||
|
struct HitResult {
|
||||||
|
UIElementType type;
|
||||||
|
int16_t index;
|
||||||
|
Rect bounds;
|
||||||
|
|
||||||
|
HitResult()
|
||||||
|
: type(UIElementType::NONE)
|
||||||
|
, index(-1)
|
||||||
|
, bounds() { }
|
||||||
|
|
||||||
|
HitResult(UIElementType t, int16_t i, const Rect& b)
|
||||||
|
: type(t)
|
||||||
|
, index(i)
|
||||||
|
, bounds(b) { }
|
||||||
|
|
||||||
|
bool isValid() const { return type != UIElementType::NONE; }
|
||||||
|
operator bool() const { return isValid(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
class UIElements {
|
||||||
|
public:
|
||||||
|
static Rect header(uint16_t screenW, uint16_t headerH) { return Rect(0, 0, screenW, headerH); }
|
||||||
|
|
||||||
|
static Rect contentArea(
|
||||||
|
uint16_t screenW, uint16_t screenH, uint16_t headerH, uint16_t padding = 10) {
|
||||||
|
return Rect(
|
||||||
|
padding, headerH + padding, screenW - 2 * padding, screenH - headerH - 2 * padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect tile(uint8_t col, uint8_t row, uint8_t cols, uint8_t rows, uint16_t contentX,
|
||||||
|
uint16_t contentY, uint16_t contentW, uint16_t contentH, uint8_t gap = 8) {
|
||||||
|
uint16_t tileW = (contentW - (cols - 1) * gap) / cols;
|
||||||
|
uint16_t tileH = (contentH - (rows - 1) * gap) / rows;
|
||||||
|
int16_t x = contentX + col * (tileW + gap);
|
||||||
|
int16_t y = contentY + row * (tileH + gap);
|
||||||
|
return Rect(x, y, tileW, tileH);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect button(int16_t x, int16_t y, int16_t w, int16_t h, int16_t padding = 8) {
|
||||||
|
return Rect(x - padding, y - padding, w + 2 * padding, h + 2 * padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Rect icon(int16_t x, int16_t y, int16_t size) { return Rect(x, y, size, size); }
|
||||||
|
};
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ run = """
|
|||||||
./boards/$BOARD/install.sh
|
./boards/$BOARD/install.sh
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[tasks.detect]
|
||||||
|
description = "Detect connected doorbell board"
|
||||||
|
run = "bash ./scripts/detect-device.sh"
|
||||||
|
|
||||||
# Convenience
|
# Convenience
|
||||||
|
|
||||||
[tasks.clean]
|
[tasks.clean]
|
||||||
|
|||||||
160
scripts/detect-device.sh
Executable file
160
scripts/detect-device.sh
Executable file
@@ -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 "$@"
|
||||||
1
vendor/esp32-s3-lcd-43/LovyanGFX
vendored
1
vendor/esp32-s3-lcd-43/LovyanGFX
vendored
Submodule vendor/esp32-s3-lcd-43/LovyanGFX deleted from 42998359d8
Reference in New Issue
Block a user