Compare commits
9 Commits
c9232ee477
...
deda456d35
| Author | SHA1 | Date | |
|---|---|---|---|
| deda456d35 | |||
| 00989f1d8b | |||
| b296c56f80 | |||
| 999e0b7e60 | |||
| 2122611a34 | |||
| 07e6503520 | |||
| 8b17049620 | |||
| cd74b501db | |||
| 6cff6d4bf4 |
30
hk.pkl
Normal file
30
hk.pkl
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
amends "package://github.com/jdx/hk/releases/download/v1.36.0/hk@1.36.0#/Config.pkl"
|
||||||
|
import "package://github.com/jdx/hk/releases/download/v1.36.0/hk@1.36.0#/Builtins.pkl"
|
||||||
|
|
||||||
|
// Only process files in sketches/ (excluding vendor/)
|
||||||
|
exclude = List("**", "!sketches/**", "sketches/**/vendor/**")
|
||||||
|
|
||||||
|
local linters = new Mapping<String, Step> {
|
||||||
|
["format"] = Builtins.clang_format
|
||||||
|
|
||||||
|
["trailing-whitespace"] = Builtins.trailing_whitespace
|
||||||
|
|
||||||
|
["mixed-line-ending"] = Builtins.mixed_line_ending
|
||||||
|
|
||||||
|
["check-merge-conflict"] = Builtins.check_merge_conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
hooks {
|
||||||
|
["pre-commit"] {
|
||||||
|
fix = true
|
||||||
|
stash = "git"
|
||||||
|
steps = linters
|
||||||
|
}
|
||||||
|
["fix"] {
|
||||||
|
fix = true
|
||||||
|
steps = linters
|
||||||
|
}
|
||||||
|
["check"] {
|
||||||
|
steps = linters
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
M.board = 'arduino:avr:uno'
|
M.board = 'esp32:esp32:esp32'
|
||||||
M.port = '/dev/ttyUSB0'
|
M.port = '/dev/ttyUSB0'
|
||||||
M.baudrate =115200
|
M.baudrate = 115200
|
||||||
return M
|
return M
|
||||||
|
|||||||
32
sketches/doorbell-touch/.crushmemory
Normal file
32
sketches/doorbell-touch/.crushmemory
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Doorbell Touch - Build Harness
|
||||||
|
|
||||||
|
## Quick Commands
|
||||||
|
```bash
|
||||||
|
BOARD=esp32-32e-4 mise run compile # Compile
|
||||||
|
BOARD=esp32-32e-4 mise run upload # Upload (auto-kills monitor)
|
||||||
|
BOARD=esp32-32e-4 mise run monitor # Start JSON monitor daemon
|
||||||
|
BOARD=esp32-32e-4 mise run log-tail # Watch colored logs
|
||||||
|
BOARD=esp32-32e-4 mise run cmd COMMAND=dashboard # Send command
|
||||||
|
BOARD=esp32-32e-4 mise run state # Show device state
|
||||||
|
BOARD=esp32-32e-4 mise run install # Install libs (shared + board)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `mise.toml` - Tasks (compile, upload, monitor, log-tail, cmd, state, install, clean)
|
||||||
|
- `scripts/lockfile.sh` - Lockfile functions (acquire_lock, kill_locked)
|
||||||
|
- `scripts/monitor-agent.py` - Serial monitor with JSON log + command FIFO
|
||||||
|
- `scripts/install-shared.sh` - Shared Arduino libs (ArduinoJson, NTPClient)
|
||||||
|
- `boards/{BOARD}/install.sh` - Board-specific vendor libs
|
||||||
|
|
||||||
|
## Monitor Daemon
|
||||||
|
- JSON log: `/tmp/doorbell-$BOARD.jsonl` - `{"ts":123.456,"line":"..."}`
|
||||||
|
- State: `/tmp/doorbell-$BOARD-state.json` - `{"screen":"DASHBOARD",...}`
|
||||||
|
- Cmd: `echo 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo`
|
||||||
|
|
||||||
|
## Boards
|
||||||
|
- esp32-32e (original 2.8" ILI9341)
|
||||||
|
- esp32-32e-4 (4" ST7796)
|
||||||
|
- esp32-s3-lcd-43 (4.3" S3 display)
|
||||||
|
|
||||||
|
## Known Fixes
|
||||||
|
- dashboard/off admin commands now reset inactivity timer (DoorbellLogic.cpp:234,240)
|
||||||
@@ -17,7 +17,10 @@ void DisplayDriverTFT::begin() {
|
|||||||
Serial.printf("[TOUCH] Raw Z=%d (non-zero = controller detected)\n", z);
|
Serial.printf("[TOUCH] Raw Z=%d (non-zero = controller detected)\n", z);
|
||||||
Serial.flush();
|
Serial.flush();
|
||||||
|
|
||||||
drawBoot();
|
ScreenState st;
|
||||||
|
st.screen = ScreenID::BOOT;
|
||||||
|
st.bootStage = BootStage::SPLASH;
|
||||||
|
drawBoot(st);
|
||||||
|
|
||||||
digitalWrite(PIN_LCD_BL, HIGH);
|
digitalWrite(PIN_LCD_BL, HIGH);
|
||||||
Serial.println("[GFX] Backlight ON");
|
Serial.println("[GFX] Backlight ON");
|
||||||
@@ -29,15 +32,16 @@ void DisplayDriverTFT::setBacklight(bool on) { digitalWrite(PIN_LCD_BL, on ? HIG
|
|||||||
// ── Rendering ───────────────────────────────────────────────
|
// ── Rendering ───────────────────────────────────────────────
|
||||||
|
|
||||||
void DisplayDriverTFT::render(const ScreenState& st) {
|
void DisplayDriverTFT::render(const ScreenState& st) {
|
||||||
if(st.screen != _lastScreen) {
|
if(st.screen != _lastScreen || (st.screen == ScreenID::BOOT && st.bootStage != _lastBootStage)) {
|
||||||
_needsRedraw = true;
|
_needsRedraw = true;
|
||||||
_lastScreen = st.screen;
|
_lastScreen = st.screen;
|
||||||
|
_lastBootStage = st.bootStage;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(st.screen) {
|
switch(st.screen) {
|
||||||
case ScreenID::BOOT:
|
case ScreenID::BOOT:
|
||||||
if(_needsRedraw) {
|
if(_needsRedraw) {
|
||||||
drawBoot();
|
drawBoot(st);
|
||||||
_needsRedraw = false;
|
_needsRedraw = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -60,7 +64,9 @@ void DisplayDriverTFT::render(const ScreenState& st) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawBoot() {
|
void DisplayDriverTFT::drawBoot(const ScreenState& st) {
|
||||||
|
BootStage stage = st.bootStage;
|
||||||
|
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
_tft.setTextSize(2);
|
_tft.setTextSize(2);
|
||||||
@@ -69,8 +75,29 @@ void DisplayDriverTFT::drawBoot() {
|
|||||||
_tft.setTextSize(1);
|
_tft.setTextSize(1);
|
||||||
_tft.setCursor(10, 40);
|
_tft.setCursor(10, 40);
|
||||||
_tft.print(BOARD_NAME);
|
_tft.print(BOARD_NAME);
|
||||||
_tft.setCursor(10, 60);
|
|
||||||
_tft.print("Booting...");
|
// Show boot stage status
|
||||||
|
_tft.setCursor(10, 70);
|
||||||
|
switch(stage) {
|
||||||
|
case BootStage::SPLASH:
|
||||||
|
_tft.print("Initializing...");
|
||||||
|
break;
|
||||||
|
case BootStage::INIT_DISPLAY:
|
||||||
|
_tft.print("Display OK");
|
||||||
|
break;
|
||||||
|
case BootStage::INIT_NETWORK:
|
||||||
|
_tft.print("Network init...");
|
||||||
|
break;
|
||||||
|
case BootStage::CONNECTING_WIFI:
|
||||||
|
_tft.print("Connecting WiFi...");
|
||||||
|
break;
|
||||||
|
case BootStage::READY:
|
||||||
|
_tft.print("All systems go!");
|
||||||
|
break;
|
||||||
|
case BootStage::DONE:
|
||||||
|
_tft.print("Ready!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ public:
|
|||||||
int height() override { return DISPLAY_HEIGHT; }
|
int height() override { return DISPLAY_HEIGHT; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void drawBoot();
|
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);
|
||||||
|
|
||||||
@@ -28,5 +28,6 @@ private:
|
|||||||
bool _holdActive = false;
|
bool _holdActive = false;
|
||||||
uint32_t _holdStartMs = 0;
|
uint32_t _holdStartMs = 0;
|
||||||
ScreenID _lastScreen = ScreenID::BOOT;
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
|
BootStage _lastBootStage = BootStage::SPLASH;
|
||||||
bool _needsRedraw = true;
|
bool _needsRedraw = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
||||||
|
PORT="/dev/ttyUSB0"
|
||||||
|
LIBS="--libraries ./vendor/esp32-32e-4/TFT_eSPI"
|
||||||
|
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM"
|
||||||
@@ -21,68 +21,29 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// ── Touch debug: poll continuously ──
|
// Read touch
|
||||||
TouchEvent evt = display.readTouch();
|
TouchEvent evt = display.readTouch();
|
||||||
|
|
||||||
|
// Touch debug (useful for new boards)
|
||||||
if(evt.pressed) {
|
if(evt.pressed) {
|
||||||
Serial.printf("[TOUCH] pressed: x=%d, y=%d\n", evt.x, evt.y);
|
Serial.printf("[TOUCH] pressed: x=%d, y=%d\n", evt.x, evt.y);
|
||||||
} else {
|
|
||||||
// Debug: print raw Z even when not touched, to see controller state
|
|
||||||
static uint32_t lastDebug = 0;
|
|
||||||
if(millis() - lastDebug > 2000) {
|
|
||||||
lastDebug = millis();
|
|
||||||
uint16_t z = tftDriver.getRawTouchZ();
|
|
||||||
Serial.printf("[TOUCH] Raw Z=%d\n", z);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── State machine tick ──
|
// State machine tick
|
||||||
logic.update();
|
logic.update();
|
||||||
|
|
||||||
// ── Render current screen ──
|
// Render current screen
|
||||||
display.render(logic.getScreenState());
|
display.render(logic.getScreenState());
|
||||||
|
|
||||||
// ── Touch handling ──
|
// Handle tap gestures
|
||||||
const ScreenState& st = logic.getScreenState();
|
logic.handleTouch(evt);
|
||||||
static int holdStartX = -1;
|
|
||||||
static int holdStartY = -1;
|
|
||||||
|
|
||||||
if(st.deviceState == DeviceState::ALERTING) {
|
// Handle hold-to-silence gesture
|
||||||
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
logic.updateHold(evt);
|
||||||
if(h.completed) {
|
|
||||||
logic.silenceAlert();
|
|
||||||
holdStartX = -1;
|
|
||||||
holdStartY = -1;
|
|
||||||
}
|
|
||||||
if(h.started) {
|
|
||||||
TouchEvent t = display.readTouch();
|
|
||||||
holdStartX = t.x;
|
|
||||||
holdStartY = t.y;
|
|
||||||
}
|
|
||||||
if(holdStartX >= 0) {
|
|
||||||
display.updateHint(holdStartX, holdStartY, h.active);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holdStartX = -1;
|
|
||||||
holdStartY = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(st.screen == ScreenID::DASHBOARD) {
|
// Serial console
|
||||||
TouchEvent evt = display.readTouch();
|
logic.processSerial();
|
||||||
if(evt.pressed) {
|
|
||||||
int tile = display.dashboardTouch(evt.x, evt.y);
|
|
||||||
if(tile >= 0)
|
|
||||||
Serial.printf("[DASH] Tile %d tapped\n", tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Serial console ──
|
// Yield to WiFi/BT stack
|
||||||
if(Serial.available()) {
|
delay(LOOP_YIELD_MS);
|
||||||
String cmd = Serial.readStringUntil('\n');
|
|
||||||
cmd.trim();
|
|
||||||
if(cmd.length() > 0)
|
|
||||||
logic.onSerialCommand(cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Yield to WiFi/BT stack (prevents Task Watchdog timeout)
|
|
||||||
delay(10);
|
|
||||||
}
|
}
|
||||||
16
sketches/doorbell-touch/boards/esp32-32e-4/install.sh
Executable file
16
sketches/doorbell-touch/boards/esp32-32e-4/install.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install TFT_eSPI for esp32-32e-4
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
VENDOR_DIR="$DIR/../../vendor/esp32-32e-4"
|
||||||
|
|
||||||
|
if [ ! -d "$VENDOR_DIR/TFT_eSPI" ]; then
|
||||||
|
echo "Cloning TFT_eSPI..."
|
||||||
|
git clone --depth 1 --branch V2.5.43 \
|
||||||
|
https://github.com/Bodmer/TFT_eSPI.git \
|
||||||
|
"$VENDOR_DIR/TFT_eSPI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$DIR/tft_user_setup.h" "$VENDOR_DIR/TFT_eSPI/User_Setup.h"
|
||||||
|
echo "[OK] TFT_eSPI vendored for esp32-32e-4"
|
||||||
@@ -11,7 +11,10 @@ void DisplayDriverTFT::begin() {
|
|||||||
|
|
||||||
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
Serial.printf("[GFX] Display OK: %dx%d\n", DISPLAY_WIDTH, DISPLAY_HEIGHT);
|
||||||
|
|
||||||
drawBoot();
|
ScreenState st;
|
||||||
|
st.screen = ScreenID::BOOT;
|
||||||
|
st.bootStage = BootStage::SPLASH;
|
||||||
|
drawBoot(st);
|
||||||
|
|
||||||
digitalWrite(PIN_LCD_BL, HIGH);
|
digitalWrite(PIN_LCD_BL, HIGH);
|
||||||
Serial.println("[GFX] Backlight ON");
|
Serial.println("[GFX] Backlight ON");
|
||||||
@@ -22,15 +25,16 @@ void DisplayDriverTFT::setBacklight(bool on) { digitalWrite(PIN_LCD_BL, on ? HIG
|
|||||||
// ── Rendering ───────────────────────────────────────────────
|
// ── Rendering ───────────────────────────────────────────────
|
||||||
|
|
||||||
void DisplayDriverTFT::render(const ScreenState& st) {
|
void DisplayDriverTFT::render(const ScreenState& st) {
|
||||||
if(st.screen != _lastScreen) {
|
if(st.screen != _lastScreen || (st.screen == ScreenID::BOOT && st.bootStage != _lastBootStage)) {
|
||||||
_needsRedraw = true;
|
_needsRedraw = true;
|
||||||
_lastScreen = st.screen;
|
_lastScreen = st.screen;
|
||||||
|
_lastBootStage = st.bootStage;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(st.screen) {
|
switch(st.screen) {
|
||||||
case ScreenID::BOOT:
|
case ScreenID::BOOT:
|
||||||
if(_needsRedraw) {
|
if(_needsRedraw) {
|
||||||
drawBoot();
|
drawBoot(st);
|
||||||
_needsRedraw = false;
|
_needsRedraw = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -53,7 +57,9 @@ void DisplayDriverTFT::render(const ScreenState& st) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawBoot() {
|
void DisplayDriverTFT::drawBoot(const ScreenState& st) {
|
||||||
|
BootStage stage = st.bootStage;
|
||||||
|
|
||||||
_tft.fillScreen(TFT_BLACK);
|
_tft.fillScreen(TFT_BLACK);
|
||||||
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
_tft.setTextColor(TFT_WHITE, TFT_BLACK);
|
||||||
_tft.setTextSize(2);
|
_tft.setTextSize(2);
|
||||||
@@ -62,8 +68,29 @@ void DisplayDriverTFT::drawBoot() {
|
|||||||
_tft.setTextSize(1);
|
_tft.setTextSize(1);
|
||||||
_tft.setCursor(10, 40);
|
_tft.setCursor(10, 40);
|
||||||
_tft.print(BOARD_NAME);
|
_tft.print(BOARD_NAME);
|
||||||
_tft.setCursor(10, 60);
|
|
||||||
_tft.print("Booting...");
|
// Show boot stage status
|
||||||
|
_tft.setCursor(10, 70);
|
||||||
|
switch(stage) {
|
||||||
|
case BootStage::SPLASH:
|
||||||
|
_tft.print("Initializing...");
|
||||||
|
break;
|
||||||
|
case BootStage::INIT_DISPLAY:
|
||||||
|
_tft.print("Display OK");
|
||||||
|
break;
|
||||||
|
case BootStage::INIT_NETWORK:
|
||||||
|
_tft.print("Network init...");
|
||||||
|
break;
|
||||||
|
case BootStage::CONNECTING_WIFI:
|
||||||
|
_tft.print("Connecting WiFi...");
|
||||||
|
break;
|
||||||
|
case BootStage::READY:
|
||||||
|
_tft.print("All systems go!");
|
||||||
|
break;
|
||||||
|
case BootStage::DONE:
|
||||||
|
_tft.print("Ready!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
void DisplayDriverTFT::drawAlert(const ScreenState& st) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public:
|
|||||||
int height() override { return DISPLAY_HEIGHT; }
|
int height() override { return DISPLAY_HEIGHT; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void drawBoot();
|
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);
|
||||||
|
|
||||||
@@ -27,5 +27,6 @@ private:
|
|||||||
bool _holdActive = false;
|
bool _holdActive = false;
|
||||||
uint32_t _holdStartMs = 0;
|
uint32_t _holdStartMs = 0;
|
||||||
ScreenID _lastScreen = ScreenID::BOOT;
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
|
BootStage _lastBootStage = BootStage::SPLASH;
|
||||||
bool _needsRedraw = true;
|
bool _needsRedraw = true;
|
||||||
};
|
};
|
||||||
|
|||||||
4
sketches/doorbell-touch/boards/esp32-32e/board-config.sh
Normal file
4
sketches/doorbell-touch/boards/esp32-32e/board-config.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
FQBN="esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default"
|
||||||
|
PORT="/dev/ttyUSB0"
|
||||||
|
LIBS="--libraries ./vendor/esp32-32e/TFT_eSPI"
|
||||||
|
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM"
|
||||||
@@ -21,52 +21,24 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// ── State machine tick ──
|
// Read touch
|
||||||
|
TouchEvent evt = display.readTouch();
|
||||||
|
|
||||||
|
// State machine tick
|
||||||
logic.update();
|
logic.update();
|
||||||
|
|
||||||
// ── Render current screen ──
|
// Render current screen
|
||||||
display.render(logic.getScreenState());
|
display.render(logic.getScreenState());
|
||||||
|
|
||||||
// ── Touch handling ──
|
// Handle tap gestures
|
||||||
const ScreenState& st = logic.getScreenState();
|
logic.handleTouch(evt);
|
||||||
static int holdStartX = -1;
|
|
||||||
static int holdStartY = -1;
|
|
||||||
|
|
||||||
if(st.deviceState == DeviceState::ALERTING) {
|
// Handle hold-to-silence gesture
|
||||||
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
logic.updateHold(evt);
|
||||||
if(h.completed) {
|
|
||||||
logic.silenceAlert();
|
|
||||||
holdStartX = -1;
|
|
||||||
holdStartY = -1;
|
|
||||||
}
|
|
||||||
if(h.started) {
|
|
||||||
TouchEvent t = display.readTouch();
|
|
||||||
holdStartX = t.x;
|
|
||||||
holdStartY = t.y;
|
|
||||||
}
|
|
||||||
// Fix for esp32-32e.ino
|
|
||||||
if(holdStartX >= 0) {
|
|
||||||
display.updateHint(holdStartX, holdStartY, h.active);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holdStartX = -1;
|
|
||||||
holdStartY = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(st.screen == ScreenID::DASHBOARD) {
|
// Serial console
|
||||||
TouchEvent evt = display.readTouch();
|
logic.processSerial();
|
||||||
if(evt.pressed) {
|
|
||||||
int tile = display.dashboardTouch(evt.x, evt.y);
|
|
||||||
if(tile >= 0)
|
|
||||||
Serial.printf("[DASH] Tile %d tapped\n", tile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Serial console ──
|
// Yield to WiFi/BT stack
|
||||||
if(Serial.available()) {
|
delay(LOOP_YIELD_MS);
|
||||||
String cmd = Serial.readStringUntil('\n');
|
|
||||||
cmd.trim();
|
|
||||||
if(cmd.length() > 0)
|
|
||||||
logic.onSerialCommand(cmd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
sketches/doorbell-touch/boards/esp32-32e/install.sh
Executable file
16
sketches/doorbell-touch/boards/esp32-32e/install.sh
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install TFT_eSPI for esp32-32e
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
VENDOR_DIR="$DIR/../../vendor/esp32-32e"
|
||||||
|
|
||||||
|
if [ ! -d "$VENDOR_DIR/TFT_eSPI" ]; then
|
||||||
|
echo "Cloning TFT_eSPI..."
|
||||||
|
git clone --depth 1 --branch V2.5.43 \
|
||||||
|
https://github.com/Bodmer/TFT_eSPI.git \
|
||||||
|
"$VENDOR_DIR/TFT_eSPI"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp "$DIR/tft_user_setup.h" "$VENDOR_DIR/TFT_eSPI/User_Setup.h"
|
||||||
|
echo "[OK] TFT_eSPI vendored for esp32-32e"
|
||||||
@@ -129,19 +129,16 @@ void DisplayDriverGFX::render(const ScreenState& state) {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// Check if we need full redraw
|
// Check if we need full redraw
|
||||||
if(state.screen != _lastScreen) {
|
if(state.screen != _lastScreen || (state.screen == ScreenID::BOOT && state.bootStage != _lastBootStage)) {
|
||||||
_needsRedraw = true;
|
_needsRedraw = true;
|
||||||
_lastScreen = state.screen;
|
_lastScreen = state.screen;
|
||||||
|
_lastBootStage = state.bootStage;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch(state.screen) {
|
switch(state.screen) {
|
||||||
case ScreenID::BOOT:
|
case ScreenID::BOOT:
|
||||||
if(_needsRedraw) {
|
if(_needsRedraw) {
|
||||||
_gfx->fillScreen(0x000000);
|
drawBoot(state);
|
||||||
_gfx->setTextColor(0xFFFF);
|
|
||||||
_gfx->setTextSize(2);
|
|
||||||
_gfx->setCursor(10, 10);
|
|
||||||
_gfx->print("KLUBHAUS BOOT");
|
|
||||||
_needsRedraw = false;
|
_needsRedraw = false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -170,6 +167,43 @@ void DisplayDriverGFX::render(const ScreenState& state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void DisplayDriverGFX::drawBoot(const ScreenState& state) {
|
||||||
|
BootStage stage = state.bootStage;
|
||||||
|
|
||||||
|
_gfx->fillScreen(0x000000);
|
||||||
|
_gfx->setTextColor(0xFFFF);
|
||||||
|
_gfx->setTextSize(2);
|
||||||
|
_gfx->setCursor(10, 10);
|
||||||
|
_gfx->print("KLUBHAUS");
|
||||||
|
|
||||||
|
_gfx->setTextSize(1);
|
||||||
|
_gfx->setCursor(10, 50);
|
||||||
|
_gfx->print(BOARD_NAME);
|
||||||
|
|
||||||
|
// Show boot stage status
|
||||||
|
_gfx->setCursor(10, 80);
|
||||||
|
switch(stage) {
|
||||||
|
case BootStage::SPLASH:
|
||||||
|
_gfx->print("Initializing...");
|
||||||
|
break;
|
||||||
|
case BootStage::INIT_DISPLAY:
|
||||||
|
_gfx->print("Display OK");
|
||||||
|
break;
|
||||||
|
case BootStage::INIT_NETWORK:
|
||||||
|
_gfx->print("Network init...");
|
||||||
|
break;
|
||||||
|
case BootStage::CONNECTING_WIFI:
|
||||||
|
_gfx->print("Connecting WiFi...");
|
||||||
|
break;
|
||||||
|
case BootStage::READY:
|
||||||
|
_gfx->print("All systems go!");
|
||||||
|
break;
|
||||||
|
case BootStage::DONE:
|
||||||
|
_gfx->print("Ready!");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void DisplayDriverGFX::drawAlert(const ScreenState& state) {
|
void DisplayDriverGFX::drawAlert(const ScreenState& state) {
|
||||||
uint32_t elapsed = millis() - state.alertStartMs;
|
uint32_t elapsed = millis() - state.alertStartMs;
|
||||||
uint8_t pulse = static_cast<uint8_t>(180.0f + 75.0f * sinf(elapsed / 300.0f));
|
uint8_t pulse = static_cast<uint8_t>(180.0f + 75.0f * sinf(elapsed / 300.0f));
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
// Helper rendering functions
|
// Helper rendering functions
|
||||||
|
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);
|
||||||
|
|
||||||
@@ -34,5 +35,6 @@ private:
|
|||||||
|
|
||||||
// Screen tracking
|
// Screen tracking
|
||||||
ScreenID _lastScreen = ScreenID::BOOT;
|
ScreenID _lastScreen = ScreenID::BOOT;
|
||||||
|
BootStage _lastBootStage = BootStage::SPLASH;
|
||||||
bool _needsRedraw = true;
|
bool _needsRedraw = true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FQBN="esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB"
|
||||||
|
PORT="/dev/ttyACM0"
|
||||||
|
LIBS="--libraries ./vendor/esp32-s3-lcd-43/LovyanGFX"
|
||||||
|
OPTS="-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1"
|
||||||
@@ -21,60 +21,21 @@ void setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
// Read touch
|
||||||
TouchEvent evt = display.readTouch();
|
TouchEvent evt = display.readTouch();
|
||||||
|
|
||||||
|
// State machine tick
|
||||||
logic.update();
|
logic.update();
|
||||||
|
|
||||||
|
// Render current screen
|
||||||
display.render(logic.getScreenState());
|
display.render(logic.getScreenState());
|
||||||
|
|
||||||
const ScreenState& st = logic.getScreenState();
|
// Handle tap gestures
|
||||||
|
logic.handleTouch(evt);
|
||||||
|
|
||||||
// Track initial hold position for hint
|
// Handle hold-to-silence gesture
|
||||||
static int holdStartX = -1;
|
logic.updateHold(evt);
|
||||||
static int holdStartY = -1;
|
|
||||||
|
|
||||||
if(st.deviceState == DeviceState::ALERTING) {
|
// Serial console
|
||||||
HoldState h = display.updateHold(HOLD_TO_SILENCE_MS);
|
logic.processSerial();
|
||||||
if(h.completed) {
|
|
||||||
logic.silenceAlert();
|
|
||||||
holdStartX = -1;
|
|
||||||
holdStartY = -1;
|
|
||||||
}
|
|
||||||
if(h.started) {
|
|
||||||
holdStartX = evt.x;
|
|
||||||
holdStartY = evt.y;
|
|
||||||
}
|
|
||||||
// Draw hint during hold (ACTIVE) or idle (IDLE)
|
|
||||||
if(holdStartX >= 0) {
|
|
||||||
if(h.active) {
|
|
||||||
display.updateHint(holdStartX, holdStartY, true);
|
|
||||||
} else {
|
|
||||||
display.updateHint(holdStartX, holdStartY, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holdStartX = -1;
|
|
||||||
holdStartY = -1;
|
|
||||||
}
|
|
||||||
if(evt.pressed) {
|
|
||||||
if(st.screen == ScreenID::OFF) {
|
|
||||||
// Tap in OFF mode → wake to DASHBOARD
|
|
||||||
Serial.println("[TOUCH] OFF → DASHBOARD");
|
|
||||||
logic.setScreen(ScreenID::DASHBOARD);
|
|
||||||
display.setBacklight(true);
|
|
||||||
} else if(st.screen == ScreenID::DASHBOARD) {
|
|
||||||
int tile = display.dashboardTouch(evt.x, evt.y);
|
|
||||||
if(tile >= 0) {
|
|
||||||
Serial.printf("[DASH] Tile %d tapped\n", tile);
|
|
||||||
}
|
|
||||||
} else if(st.screen == ScreenID::ALERT) {
|
|
||||||
Serial.println("[TOUCH] ALERT tap");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(Serial.available()) {
|
|
||||||
String cmd = Serial.readStringUntil('\n');
|
|
||||||
cmd.trim();
|
|
||||||
if(cmd.length() > 0)
|
|
||||||
logic.onSerialCommand(cmd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
sketches/doorbell-touch/boards/esp32-s3-lcd-43/install.sh
Executable file
31
sketches/doorbell-touch/boards/esp32-s3-lcd-43/install.sh
Executable file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install LovyanGFX for esp32-s3-lcd-43
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
VENDOR_DIR="$DIR/../../vendor/esp32-s3-lcd-43"
|
||||||
|
LOVGFX_DIR="$VENDOR_DIR/LovyanGFX"
|
||||||
|
|
||||||
|
if [ ! -d "$LOVGFX_DIR" ]; then
|
||||||
|
echo "Cloning LovyanGFX..."
|
||||||
|
git clone --depth 1 \
|
||||||
|
https://github.com/lovyan03/LovyanGFX.git \
|
||||||
|
"$LOVGFX_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create library.properties
|
||||||
|
cat > "$LOVGFX_DIR/library.properties" << 'EOF'
|
||||||
|
name=LovyanGFX
|
||||||
|
version=1.2.0
|
||||||
|
author=lovyan03
|
||||||
|
maintainer=lovyan03
|
||||||
|
sentence=Display and touch driver library for ESP32
|
||||||
|
paragraph=Universal graphics library for ESP32 with support for various displays and touch controllers
|
||||||
|
category=Display
|
||||||
|
url=https://github.com/lovyan03/LovyanGFX
|
||||||
|
architectures=esp32
|
||||||
|
includes=LovyanGFX.hpp
|
||||||
|
EOF
|
||||||
|
|
||||||
|
mkdir -p "$LOVGFX_DIR/src"
|
||||||
|
echo "[OK] LovyanGFX vendored for esp32-s3-lcd-43"
|
||||||
@@ -16,12 +16,18 @@
|
|||||||
#define HOLD_TO_SILENCE_MS 3000
|
#define HOLD_TO_SILENCE_MS 3000
|
||||||
#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 WIFI_CONNECT_TIMEOUT_MS 15000
|
#define WIFI_CONNECT_TIMEOUT_MS 15000
|
||||||
#define HTTP_TIMEOUT_MS 10000
|
#define HTTP_TIMEOUT_MS 10000
|
||||||
#define HINT_ANIMATION_MS 2000
|
#define HINT_ANIMATION_MS 2000
|
||||||
#define HINT_MIN_BRIGHTNESS 30
|
#define HINT_MIN_BRIGHTNESS 30
|
||||||
#define HINT_MAX_BRIGHTNESS 60
|
#define HINT_MAX_BRIGHTNESS 60
|
||||||
|
|
||||||
|
// ── Loop yield (prevents Task Watchdog on ESP32) ──
|
||||||
|
#ifndef LOOP_YIELD_MS
|
||||||
|
#define LOOP_YIELD_MS 10
|
||||||
|
#endif
|
||||||
|
|
||||||
// ── WiFi credential struct (populated in each board's secrets.h) ──
|
// ── WiFi credential struct (populated in each board's secrets.h) ──
|
||||||
struct WiFiCred {
|
struct WiFiCred {
|
||||||
const char* ssid;
|
const char* ssid;
|
||||||
|
|||||||
@@ -26,12 +26,22 @@ void DoorbellLogic::begin(
|
|||||||
Serial.println(F(" *** DEBUG MODE — _test topics ***"));
|
Serial.println(F(" *** DEBUG MODE — _test topics ***"));
|
||||||
Serial.println(F("========================================\n"));
|
Serial.println(F("========================================\n"));
|
||||||
|
|
||||||
// Display
|
// Stage 1: Display init
|
||||||
|
setBootStage(BootStage::INIT_DISPLAY);
|
||||||
|
{
|
||||||
|
ScreenState temp;
|
||||||
|
temp.bootStage = BootStage::SPLASH;
|
||||||
|
_display->render(temp);
|
||||||
|
}
|
||||||
_display->begin();
|
_display->begin();
|
||||||
|
delay(LOOP_YIELD_MS);
|
||||||
|
|
||||||
// Network
|
// Stage 2: Network init
|
||||||
|
setBootStage(BootStage::INIT_NETWORK);
|
||||||
_net.begin(creds, credCount);
|
_net.begin(creds, credCount);
|
||||||
|
|
||||||
|
// Stage 3: WiFi connect
|
||||||
|
setBootStage(BootStage::CONNECTING_WIFI);
|
||||||
if(_net.isConnected()) {
|
if(_net.isConnected()) {
|
||||||
_net.syncNTP();
|
_net.syncNTP();
|
||||||
Serial.printf("[NET] WiFi:%s RSSI:%d IP:%s\n", _net.getSSID().c_str(), _net.getRSSI(),
|
Serial.printf("[NET] WiFi:%s RSSI:%d IP:%s\n", _net.getSSID().c_str(), _net.getRSSI(),
|
||||||
@@ -51,9 +61,16 @@ void DoorbellLogic::begin(
|
|||||||
Serial.printf("[CONFIG] SILENCE_URL: %s\n", _silenceUrl.c_str());
|
Serial.printf("[CONFIG] SILENCE_URL: %s\n", _silenceUrl.c_str());
|
||||||
Serial.printf("[CONFIG] ADMIN_URL: %s\n", _adminUrl.c_str());
|
Serial.printf("[CONFIG] ADMIN_URL: %s\n", _adminUrl.c_str());
|
||||||
|
|
||||||
|
// Stage 4: Ready
|
||||||
|
setBootStage(BootStage::READY);
|
||||||
|
delay(LOOP_YIELD_MS);
|
||||||
|
|
||||||
// Boot status
|
// Boot status
|
||||||
flushStatus(String("BOOTED — ") + _net.getSSID() + " " + _net.getIP()
|
flushStatus(String("BOOTED — ") + _net.getSSID() + " " + _net.getIP()
|
||||||
+ " RSSI:" + String(_net.getRSSI()));
|
+ " RSSI:" + String(_net.getRSSI()));
|
||||||
|
|
||||||
|
// Stage 5: Done (transition to OFF happens in finishBoot)
|
||||||
|
setBootStage(BootStage::DONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void DoorbellLogic::finishBoot() {
|
void DoorbellLogic::finishBoot() {
|
||||||
@@ -62,6 +79,7 @@ void DoorbellLogic::finishBoot() {
|
|||||||
_display->setBacklight(false);
|
_display->setBacklight(false);
|
||||||
_state.backlightOn = false;
|
_state.backlightOn = false;
|
||||||
_bootGraceEnd = millis() + BOOT_GRACE_MS;
|
_bootGraceEnd = millis() + BOOT_GRACE_MS;
|
||||||
|
_lastActivityMs = millis();
|
||||||
Serial.printf("[BOOT] Grace period: %d ms\n", BOOT_GRACE_MS);
|
Serial.printf("[BOOT] Grace period: %d ms\n", BOOT_GRACE_MS);
|
||||||
Serial.printf("[BOOT] Ready — monitoring %s\n", NTFY_SERVER);
|
Serial.printf("[BOOT] Ready — monitoring %s\n", NTFY_SERVER);
|
||||||
}
|
}
|
||||||
@@ -113,6 +131,14 @@ void DoorbellLogic::update() {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
// Inactivity timeout: turn off screen after no activity
|
||||||
|
if(_state.screen != ScreenID::OFF && now - _lastActivityMs > INACTIVITY_TIMEOUT_MS) {
|
||||||
|
Serial.printf("[%lu] [STATE] Inactivity timeout (%lu ms) → OFF\n",
|
||||||
|
millis(), now - _lastActivityMs);
|
||||||
|
_state.screen = ScreenID::OFF;
|
||||||
|
_display->setBacklight(false);
|
||||||
|
_state.backlightOn = false;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,10 +230,14 @@ void DoorbellLogic::onAdmin(const String& cmd) {
|
|||||||
delay(500);
|
delay(500);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
} else if(cmd == "dashboard") {
|
} else if(cmd == "dashboard") {
|
||||||
|
Serial.printf("[%lu] [ADMIN] dashboard\n", millis());
|
||||||
|
_lastActivityMs = millis(); // Reset inactivity timer
|
||||||
_state.screen = ScreenID::DASHBOARD;
|
_state.screen = ScreenID::DASHBOARD;
|
||||||
_display->setBacklight(true);
|
_display->setBacklight(true);
|
||||||
_state.backlightOn = true;
|
_state.backlightOn = true;
|
||||||
} else if(cmd == "off") {
|
} else if(cmd == "off") {
|
||||||
|
Serial.printf("[%lu] [ADMIN] off\n", millis());
|
||||||
|
_lastActivityMs = millis(); // Reset inactivity timer
|
||||||
_state.screen = ScreenID::OFF;
|
_state.screen = ScreenID::OFF;
|
||||||
_display->setBacklight(false);
|
_display->setBacklight(false);
|
||||||
_state.backlightOn = false;
|
_state.backlightOn = false;
|
||||||
@@ -278,3 +308,78 @@ void DoorbellLogic::setScreen(ScreenID s) {
|
|||||||
_display->setBacklight(needsBacklight);
|
_display->setBacklight(needsBacklight);
|
||||||
_state.backlightOn = needsBacklight;
|
_state.backlightOn = needsBacklight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int DoorbellLogic::handleTouch(const TouchEvent& evt) {
|
||||||
|
if(!evt.pressed)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// Reset inactivity timer on any touch
|
||||||
|
_lastActivityMs = millis();
|
||||||
|
|
||||||
|
if(_state.screen == ScreenID::OFF) {
|
||||||
|
Serial.printf("[%lu] [TOUCH] OFF → DASHBOARD\n", millis());
|
||||||
|
setScreen(ScreenID::DASHBOARD);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_state.screen == ScreenID::DASHBOARD) {
|
||||||
|
int tile = _display->dashboardTouch(evt.x, evt.y);
|
||||||
|
if(tile >= 0) {
|
||||||
|
Serial.printf("[DASH] Tile %d tapped\n", tile);
|
||||||
|
}
|
||||||
|
return tile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_state.screen == ScreenID::ALERT) {
|
||||||
|
Serial.println("[TOUCH] ALERT tap");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Hold gesture for silencing ─────────────────────────────────
|
||||||
|
|
||||||
|
bool DoorbellLogic::updateHold(const TouchEvent& evt) {
|
||||||
|
if(_state.deviceState != DeviceState::ALERTING)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
static int holdStartX = -1;
|
||||||
|
static int holdStartY = -1;
|
||||||
|
|
||||||
|
HoldState h = _display->updateHold(HOLD_TO_SILENCE_MS);
|
||||||
|
|
||||||
|
if(h.completed) {
|
||||||
|
silenceAlert();
|
||||||
|
holdStartX = -1;
|
||||||
|
holdStartY = -1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(h.started) {
|
||||||
|
holdStartX = evt.x;
|
||||||
|
holdStartY = evt.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(holdStartX >= 0) {
|
||||||
|
_display->updateHint(holdStartX, holdStartY, h.active);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Serial console helper ───────────────────────────────────────
|
||||||
|
|
||||||
|
void DoorbellLogic::processSerial() {
|
||||||
|
if(Serial.available()) {
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
if(cmd.length() > 0)
|
||||||
|
onSerialCommand(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DoorbellLogic::setBootStage(BootStage stage) {
|
||||||
|
_state.bootStage = stage;
|
||||||
|
_display->render(_state);
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,12 +19,22 @@ public:
|
|||||||
void finishBoot();
|
void finishBoot();
|
||||||
/// Serial debug console.
|
/// Serial debug console.
|
||||||
void onSerialCommand(const String& cmd);
|
void onSerialCommand(const String& cmd);
|
||||||
|
/// Process Serial input — call each loop iteration.
|
||||||
|
void processSerial();
|
||||||
|
|
||||||
const ScreenState& getScreenState() const { return _state; }
|
const ScreenState& getScreenState() const { return _state; }
|
||||||
|
|
||||||
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
/// Externally trigger silence (e.g. hold-to-silence gesture).
|
||||||
void silenceAlert();
|
void silenceAlert();
|
||||||
void setScreen(ScreenID s);
|
void setScreen(ScreenID s);
|
||||||
|
/// Handle touch input — returns dashboard tile index if tapped, or -1.
|
||||||
|
int handleTouch(const TouchEvent& evt);
|
||||||
|
/// Handle hold gesture for silencing — call each loop iteration when alerting.
|
||||||
|
/// Returns true if hold completed and alert was silenced.
|
||||||
|
bool updateHold(const TouchEvent& evt);
|
||||||
|
|
||||||
|
/// Set current boot stage (for staged boot sequence).
|
||||||
|
void setBootStage(BootStage stage);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void pollTopics();
|
void pollTopics();
|
||||||
@@ -47,6 +57,7 @@ private:
|
|||||||
|
|
||||||
uint32_t _lastPollMs = 0;
|
uint32_t _lastPollMs = 0;
|
||||||
uint32_t _lastHeartbeatMs = 0;
|
uint32_t _lastHeartbeatMs = 0;
|
||||||
|
uint32_t _lastActivityMs = 0;
|
||||||
uint32_t _bootGraceEnd = 0;
|
uint32_t _bootGraceEnd = 0;
|
||||||
|
|
||||||
String _alertUrl;
|
String _alertUrl;
|
||||||
|
|||||||
@@ -5,9 +5,12 @@ enum class DeviceState { BOOTED, SILENT, ALERTING, SILENCED };
|
|||||||
|
|
||||||
enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD };
|
enum class ScreenID { BOOT, OFF, ALERT, DASHBOARD };
|
||||||
|
|
||||||
|
enum class BootStage { SPLASH, INIT_DISPLAY, INIT_NETWORK, CONNECTING_WIFI, READY, DONE };
|
||||||
|
|
||||||
struct ScreenState {
|
struct ScreenState {
|
||||||
DeviceState deviceState = DeviceState::BOOTED;
|
DeviceState deviceState = DeviceState::BOOTED;
|
||||||
ScreenID screen = ScreenID::BOOT;
|
ScreenID screen = ScreenID::BOOT;
|
||||||
|
BootStage bootStage = BootStage::SPLASH;
|
||||||
|
|
||||||
String alertTitle;
|
String alertTitle;
|
||||||
String alertBody;
|
String alertBody;
|
||||||
@@ -52,3 +55,21 @@ inline const char* screenIdStr(ScreenID s) {
|
|||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline const char* bootStageStr(BootStage s) {
|
||||||
|
switch(s) {
|
||||||
|
case BootStage::SPLASH:
|
||||||
|
return "SPLASH";
|
||||||
|
case BootStage::INIT_DISPLAY:
|
||||||
|
return "INIT_DISPLAY";
|
||||||
|
case BootStage::INIT_NETWORK:
|
||||||
|
return "INIT_NETWORK";
|
||||||
|
case BootStage::CONNECTING_WIFI:
|
||||||
|
return "CONNECTING_WIFI";
|
||||||
|
case BootStage::READY:
|
||||||
|
return "READY";
|
||||||
|
case BootStage::DONE:
|
||||||
|
return "DONE";
|
||||||
|
}
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,181 +2,124 @@
|
|||||||
# Klubhaus Doorbell — Multi-Target Build Harness
|
# Klubhaus Doorbell — Multi-Target Build Harness
|
||||||
# ═══════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Required tools
|
||||||
|
[tools]
|
||||||
|
hk = "latest"
|
||||||
|
pkl = "latest"
|
||||||
|
# screen = "latest" # Install via system package manager (brew install screen / apt install screen)
|
||||||
|
# zellij = "latest" # Install manually if needed (brew install zellij)
|
||||||
|
|
||||||
|
# Usage:
|
||||||
|
# BOARD=esp32-32e-4 mise run compile # compile
|
||||||
|
# BOARD=esp32-32e-4 mise run upload # upload
|
||||||
|
# BOARD=esp32-32e-4 mise run monitor # monitor
|
||||||
|
# BOARD=esp32-32e-4 mise run monitor-screen # shows tio command to run manually
|
||||||
|
#
|
||||||
|
# Valid BOARD: esp32-32e, esp32-32e-4, esp32-s3-lcd-43
|
||||||
|
# Board config lives in: boards/{BOARD}/board-config.sh
|
||||||
|
|
||||||
|
[tasks.compile]
|
||||||
|
description = "Compile (uses BOARD env var)"
|
||||||
|
run = """
|
||||||
|
source ./boards/$BOARD/board-config.sh
|
||||||
|
arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --warnings default ./boards/$BOARD
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.upload]
|
||||||
|
description = "Upload (uses BOARD env var)"
|
||||||
|
run = """
|
||||||
|
source ./boards/$BOARD/board-config.sh
|
||||||
|
source ./scripts/lockfile.sh
|
||||||
|
FORCE=1 TASK_NAME=upload acquire_lock || exit 1
|
||||||
|
|
||||||
|
PORT="${PORT:-$PORT}"
|
||||||
|
arduino-cli compile --fqbn "$FQBN" --libraries ./libraries $LIBS --build-property "compiler.cpp.extra_flags=$OPTS" --warnings default ./boards/$BOARD && \
|
||||||
|
arduino-cli upload --fqbn "$FQBN" --port "$PORT" ./boards/$BOARD
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.monitor-raw]
|
||||||
|
run = """
|
||||||
|
source ./boards/$BOARD/board-config.sh
|
||||||
|
source ./scripts/lockfile.sh
|
||||||
|
acquire_lock || exit 1
|
||||||
|
|
||||||
|
PORT="${PORT:-$PORT}"
|
||||||
|
TARGET="$(readlink -f "$PORT" 2>/dev/null || echo "$PORT")"
|
||||||
|
arduino-cli monitor -p "$TARGET" --config baudrate=115200
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.monitor-tio]
|
||||||
|
description = "Show tio command to run in separate terminal"
|
||||||
|
run = """
|
||||||
|
source ./boards/$BOARD/board-config.sh
|
||||||
|
source ./scripts/lockfile.sh
|
||||||
|
acquire_lock || exit 1
|
||||||
|
|
||||||
|
PORT="${PORT:-$PORT}"
|
||||||
|
TARGET="$(readlink -f "$PORT" 2>/dev/null || echo "$PORT")"
|
||||||
|
tio --map INLCRNL "$TARGET" -e
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.kill]
|
||||||
|
description = "Kill running monitor/upload for BOARD"
|
||||||
|
run = """
|
||||||
|
source ./scripts/lockfile.sh
|
||||||
|
kill_locked
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.monitor]
|
||||||
|
description = "Monitor agent with JSON log + command pipe (Python-based)"
|
||||||
|
run = """
|
||||||
|
python3 ./scripts/monitor-agent.py "$BOARD"
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.log-tail]
|
||||||
|
description = "Tail the logs with human-readable formatting"
|
||||||
|
run = '''
|
||||||
|
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
|
||||||
|
'''
|
||||||
|
|
||||||
|
[tasks.cmd]
|
||||||
|
description = "Send command to device (COMMAND=dashboard mise run cmd)"
|
||||||
|
run = """
|
||||||
|
echo -n "$COMMAND" > "/tmp/doorbell-$BOARD-cmd.fifo"
|
||||||
|
echo "[SENT] $COMMAND"
|
||||||
|
"""
|
||||||
|
|
||||||
|
[tasks.state]
|
||||||
|
description = "Show current device state"
|
||||||
|
run = """
|
||||||
|
cat "/tmp/doorbell-$BOARD-state.json"
|
||||||
|
"""
|
||||||
|
|
||||||
[tasks.install-libs-shared]
|
[tasks.install-libs-shared]
|
||||||
description = "Install shared (platform-independent) libraries"
|
description = "Install shared Arduino libraries"
|
||||||
run = """
|
run = """
|
||||||
arduino-cli lib install "ArduinoJson@7.4.1"
|
./scripts/install-shared.sh
|
||||||
arduino-cli lib install "NTPClient@3.2.1"
|
|
||||||
echo "[OK] Shared libraries installed"
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.install-libs-32e]
|
[tasks.install]
|
||||||
description = "Vendor TFT_eSPI into vendor/esp32-32e"
|
description = "Install libraries for BOARD (shared + board-specific)"
|
||||||
run = """
|
run = """
|
||||||
#!/usr/bin/env bash
|
./scripts/install-shared.sh
|
||||||
set -euo pipefail
|
./boards/$BOARD/install.sh
|
||||||
if [ ! -d "vendor/esp32-32e/TFT_eSPI" ]; then
|
|
||||||
echo "Cloning TFT_eSPI..."
|
|
||||||
git clone --depth 1 --branch V2.5.43 \
|
|
||||||
https://github.com/Bodmer/TFT_eSPI.git \
|
|
||||||
vendor/esp32-32e/TFT_eSPI
|
|
||||||
fi
|
|
||||||
echo "Copying board-specific User_Setup.h..."
|
|
||||||
cp boards/esp32-32e/tft_user_setup.h vendor/esp32-32e/TFT_eSPI/User_Setup.h
|
|
||||||
echo "[OK] TFT_eSPI 2.5.43 vendored + configured"
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
[tasks.install-libs-32e-4]
|
# Convenience
|
||||||
description = "Vendor TFT_eSPI into vendor/esp32-32e-4 (ST7796 320x480)"
|
|
||||||
run = """
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
if [ ! -d "vendor/esp32-32e-4/TFT_eSPI" ]; then
|
|
||||||
echo "Cloning TFT_eSPI..."
|
|
||||||
git clone --depth 1 --branch V2.5.43 \
|
|
||||||
https://github.com/Bodmer/TFT_eSPI.git \
|
|
||||||
vendor/esp32-32e-4/TFT_eSPI
|
|
||||||
fi
|
|
||||||
echo "Copying board-specific User_Setup.h..."
|
|
||||||
cp boards/esp32-32e-4/tft_user_setup.h vendor/esp32-32e-4/TFT_eSPI/User_Setup.h
|
|
||||||
echo "[OK] TFT_eSPI 2.5.43 vendored + configured for 4 inch"
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.install-libs-s3-43]
|
|
||||||
description = "Vendor LovyanGFX into vendor/esp32-s3-lcd-43"
|
|
||||||
run = """
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
LOVyanGFX_DIR="vendor/esp32-s3-lcd-43/LovyanGFX"
|
|
||||||
|
|
||||||
# Clone LovyanGFX (latest)
|
|
||||||
if [ ! -d "$LOVyanGFX_DIR" ]; then
|
|
||||||
echo "Cloning LovyanGFX..."
|
|
||||||
git clone --depth 1 \
|
|
||||||
https://github.com/lovyan03/LovyanGFX.git \
|
|
||||||
"$LOVyanGFX_DIR"
|
|
||||||
else
|
|
||||||
echo "LovyanGFX already exists, skipping"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create library.properties that correctly points to source
|
|
||||||
cat > "$LOVyanGFX_DIR/library.properties" << 'EOF'
|
|
||||||
name=LovyanGFX
|
|
||||||
version=1.2.0
|
|
||||||
author=lovyan03
|
|
||||||
maintainer=lovyan03
|
|
||||||
sentence=Display and touch driver library for ESP32
|
|
||||||
paragraph=Universal graphics library for ESP32 with support for various displays and touch controllers
|
|
||||||
category=Display
|
|
||||||
url=https://github.com/lovyan03/LovyanGFX
|
|
||||||
architectures=esp32
|
|
||||||
includes=LovyanGFX.hpp
|
|
||||||
# This tells Arduino to build from src/
|
|
||||||
# Arduino will look in src/ for .cpp files
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Create a empty src to ensure sources are found
|
|
||||||
mkdir -p "$LOVyanGFX_DIR/src"
|
|
||||||
|
|
||||||
echo "[OK] LovyanGFX vendored"
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.install-libs]
|
|
||||||
description = "Install all libraries (shared + vendored)"
|
|
||||||
depends = ["install-libs-shared", "install-libs-32e", "install-libs-32e-4", "install-libs-s3-43"]
|
|
||||||
|
|
||||||
# ── ESP32-32E ────────────────────────────────────────────
|
|
||||||
|
|
||||||
[tasks.compile-32e]
|
|
||||||
description = "Compile ESP32-32E sketch"
|
|
||||||
depends = ["install-libs"]
|
|
||||||
run = """
|
|
||||||
arduino-cli compile \
|
|
||||||
--fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \
|
|
||||||
--libraries ./libraries \
|
|
||||||
--libraries ./vendor/esp32-32e/TFT_eSPI \
|
|
||||||
--build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM" \
|
|
||||||
--warnings default \
|
|
||||||
./boards/esp32-32e
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.upload-32e]
|
|
||||||
description = "Upload to ESP32-32E"
|
|
||||||
run = """
|
|
||||||
arduino-cli upload \
|
|
||||||
--fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \
|
|
||||||
--port "${PORT:-/dev/ttyUSB0}" \
|
|
||||||
./boards/esp32-32e
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.monitor-32e]
|
|
||||||
description = "Serial monitor for ESP32-32E"
|
|
||||||
run = """
|
|
||||||
arduino-cli monitor --port "${PORT:-/dev/ttyUSB0}" --config baudrate=115200
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ── ESP32-32E-4 inch ─────────────────────────────────────────
|
|
||||||
|
|
||||||
[tasks.compile-32e-4]
|
|
||||||
description = "Compile ESP32-32E-4 inch sketch (ST7796 320x480)"
|
|
||||||
depends = ["install-libs-32e-4"]
|
|
||||||
run = """
|
|
||||||
arduino-cli compile \
|
|
||||||
--fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \
|
|
||||||
--libraries ./libraries \
|
|
||||||
--libraries ./vendor/esp32-32e-4/TFT_eSPI \
|
|
||||||
--build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM" \
|
|
||||||
--warnings default \
|
|
||||||
./boards/esp32-32e-4
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.upload-32e-4]
|
|
||||||
description = "Upload to ESP32-32E-4\""
|
|
||||||
run = """
|
|
||||||
arduino-cli upload \
|
|
||||||
--fqbn "esp32:esp32:esp32:FlashSize=4M,PartitionScheme=default" \
|
|
||||||
--port "${PORT:-/dev/ttyUSB0}" \
|
|
||||||
./boards/esp32-32e-4
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.monitor-32e-4]
|
|
||||||
description = "Serial monitor for ESP32-32E 4 inch"
|
|
||||||
run = """
|
|
||||||
arduino-cli monitor -p "${PORT:-/dev/ttyUSB0}" --config 115200
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ── ESP32-S3-LCD-4.3 ────────────────────────────────────
|
|
||||||
|
|
||||||
[tasks.compile-s3-43]
|
|
||||||
description = "Compile ESP32-S3-LCD-4.3 sketch (Core 2.x)"
|
|
||||||
depends = ["install-libs"]
|
|
||||||
run = """
|
|
||||||
arduino-cli compile \
|
|
||||||
--fqbn "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" \
|
|
||||||
--libraries ./libraries \
|
|
||||||
--library ./vendor/esp32-s3-lcd-43/LovyanGFX \
|
|
||||||
--build-property "compiler.cpp.extra_flags=-DDEBUG_MODE -DBOARD_HAS_PSRAM -DLGFX_USE_V1" \
|
|
||||||
--warnings default \
|
|
||||||
./boards/esp32-s3-lcd-43
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.upload-s3-43]
|
|
||||||
description = "Upload to ESP32-S3-LCD-4.3"
|
|
||||||
run = """
|
|
||||||
arduino-cli upload \
|
|
||||||
--fqbn "esp32:esp32:waveshare_esp32_s3_touch_lcd_43:PSRAM=enabled,FlashSize=16M,USBMode=hwcdc,PartitionScheme=app3M_fat9M_16MB" \
|
|
||||||
--port "${PORT:-/dev/ttyACM0}" \
|
|
||||||
./boards/esp32-s3-lcd-43
|
|
||||||
"""
|
|
||||||
|
|
||||||
[tasks.monitor-s3-43]
|
|
||||||
description = "Serial monitor for ESP32-S3-LCD-4.3"
|
|
||||||
run = """
|
|
||||||
arduino-cli monitor --port "${PORT:-/dev/ttyACM0}" --config baudrate=115200
|
|
||||||
"""
|
|
||||||
|
|
||||||
# ── Convenience ──────────────────────────────────────────
|
|
||||||
|
|
||||||
[tasks.clean]
|
[tasks.clean]
|
||||||
description = "Remove build artifacts"
|
description = "Remove build artifacts"
|
||||||
@@ -188,6 +131,23 @@ rm -rf boards/esp32-s3-lcd-43/build
|
|||||||
echo "[OK] Build artifacts cleaned"
|
echo "[OK] Build artifacts cleaned"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[tasks.arduino-clean]
|
||||||
|
description = "Clean Arduino CLI cache (staging + packages)"
|
||||||
|
run = """
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
[tasks.format]
|
[tasks.format]
|
||||||
run = """
|
run = """
|
||||||
clang-format -i --style=file \
|
clang-format -i --style=file \
|
||||||
@@ -204,3 +164,6 @@ clang-format -i --style=file \
|
|||||||
libraries/KlubhausCore/src/*.h \
|
libraries/KlubhausCore/src/*.h \
|
||||||
libraries/KlubhausCore/*.properties
|
libraries/KlubhausCore/*.properties
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
[env]
|
||||||
|
BOARD = "esp32-32e-4"
|
||||||
|
|||||||
7
sketches/doorbell-touch/scripts/install-shared.sh
Executable file
7
sketches/doorbell-touch/scripts/install-shared.sh
Executable file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Install shared Arduino libraries
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
arduino-cli lib install "ArduinoJson@7.4.1"
|
||||||
|
arduino-cli lib install "NTPClient@3.2.1"
|
||||||
|
echo "[OK] Shared libraries installed"
|
||||||
65
sketches/doorbell-touch/scripts/lockfile.sh
Executable file
65
sketches/doorbell-touch/scripts/lockfile.sh
Executable file
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lockfile helper for doorbell build harness
|
||||||
|
# Source this from mise tasks
|
||||||
|
|
||||||
|
LOCKFILE="/tmp/doorbell-${BOARD:-esp32-32e-4}.lock"
|
||||||
|
|
||||||
|
acquire_lock() {
|
||||||
|
if [ -f "$LOCKFILE" ]; then
|
||||||
|
OLD_PID=$(cat "$LOCKFILE" 2>/dev/null)
|
||||||
|
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
|
||||||
|
if [ "${FORCE:-}" = "1" ]; then
|
||||||
|
echo "Force: Killing existing process (PID: $OLD_PID)..."
|
||||||
|
kill "$OLD_PID" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
kill -9 "$OLD_PID" 2>/dev/null
|
||||||
|
echo "Killed PID $OLD_PID"
|
||||||
|
else
|
||||||
|
echo "Error: Another instance is running (PID: $OLD_PID)"
|
||||||
|
echo "Kill with: mise run kill BOARD=$BOARD"
|
||||||
|
echo "Or force with: FORCE=1 mise run $TASK_NAME BOARD=$BOARD"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Stale lockfile found, removing..."
|
||||||
|
rm -f "$LOCKFILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
trap 'rm -f "$LOCKFILE"' EXIT
|
||||||
|
echo "$$" > "$LOCKFILE"
|
||||||
|
echo "Acquired lock: $LOCKFILE (PID: $$)"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
release_lock() {
|
||||||
|
rm -f "$LOCKFILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_locked() {
|
||||||
|
if [ ! -f "$LOCKFILE" ]; then
|
||||||
|
echo "No lockfile found: $LOCKFILE"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
OLD_PID=$(cat "$LOCKFILE" 2>/dev/null)
|
||||||
|
if [ -z "$OLD_PID" ]; then
|
||||||
|
echo "Lockfile is empty, removing..."
|
||||||
|
rm -f "$LOCKFILE"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if kill -0 "$OLD_PID" 2>/dev/null; then
|
||||||
|
echo "Killing PID $OLD_PID..."
|
||||||
|
kill "$OLD_PID" 2>/dev/null
|
||||||
|
sleep 1
|
||||||
|
if kill -0 "$OLD_PID" 2>/dev/null; then
|
||||||
|
echo "Force killing PID $OLD_PID..."
|
||||||
|
kill -9 "$OLD_PID" 2>/dev/null
|
||||||
|
fi
|
||||||
|
echo "Killed PID $OLD_PID"
|
||||||
|
else
|
||||||
|
echo "Process $OLD_PID not running (stale lockfile)"
|
||||||
|
fi
|
||||||
|
rm -f "$LOCKFILE"
|
||||||
|
echo "Lockfile removed"
|
||||||
|
}
|
||||||
104
sketches/doorbell-touch/scripts/monitor-agent.py
Normal file
104
sketches/doorbell-touch/scripts/monitor-agent.py
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Monitor agent - multiplexed serial monitor for AI interaction"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import serial
|
||||||
|
import select
|
||||||
|
|
||||||
|
BOARD = sys.argv[1] if len(sys.argv) > 1 else "esp32-32e-4"
|
||||||
|
|
||||||
|
# Load board config
|
||||||
|
exec(open(f"./boards/{BOARD}/board-config.sh").read().replace("export ", ""))
|
||||||
|
|
||||||
|
SERIAL_PORT = os.environ.get("PORT", PORT)
|
||||||
|
LOGFILE = f"/tmp/doorbell-{BOARD}.jsonl"
|
||||||
|
STATEFILE = f"/tmp/doorbell-{BOARD}-state.json"
|
||||||
|
CMDFIFO = f"/tmp/doorbell-{BOARD}-cmd.fifo"
|
||||||
|
|
||||||
|
# Initialize state
|
||||||
|
with open(STATEFILE, "w") as f:
|
||||||
|
json.dump({"screen": "BOOT", "deviceState": "BOOTED", "backlightOn": False}, f)
|
||||||
|
|
||||||
|
# Open serial port
|
||||||
|
ser = serial.Serial(SERIAL_PORT, 115200, timeout=1)
|
||||||
|
|
||||||
|
# State parsing
|
||||||
|
def parse_state(line):
|
||||||
|
updates = {}
|
||||||
|
if "[STATE]" in line and "→ OFF" in line:
|
||||||
|
updates["screen"] = "OFF"
|
||||||
|
elif "[STATE]" in line and "→ DASHBOARD" in line:
|
||||||
|
updates["screen"] = "DASHBOARD"
|
||||||
|
elif "[STATE]" in line and "→ ALERT" in line:
|
||||||
|
updates["screen"] = "ALERT"
|
||||||
|
elif "[STATE]" in line and "→ BOOT" in line:
|
||||||
|
updates["screen"] = "BOOT"
|
||||||
|
elif "[ADMIN]" in line and "dashboard" in line:
|
||||||
|
updates["screen"] = "DASHBOARD"
|
||||||
|
elif "[ADMIN]" in line and "off" in line:
|
||||||
|
updates["screen"] = "OFF"
|
||||||
|
return updates
|
||||||
|
|
||||||
|
print(f"Monitor agent started (BOARD={BOARD}):")
|
||||||
|
print(f" Log: {LOGFILE}")
|
||||||
|
print(f" State: {STATEFILE}")
|
||||||
|
print(f" Cmd: echo 'dashboard' > {CMDFIFO}")
|
||||||
|
print()
|
||||||
|
print("Press Ctrl+C to exit")
|
||||||
|
|
||||||
|
# Create FIFO for commands if not exists
|
||||||
|
if not os.path.exists(CMDFIFO):
|
||||||
|
os.mkfifo(CMDFIFO)
|
||||||
|
|
||||||
|
# Command FIFO reader thread
|
||||||
|
cmd_running = True
|
||||||
|
def cmd_reader():
|
||||||
|
global cmd_running
|
||||||
|
while cmd_running:
|
||||||
|
try:
|
||||||
|
with open(CMDFIFO, "r") as fifo:
|
||||||
|
# Use select to avoid blocking forever
|
||||||
|
ready, _, _ = select.select([fifo], [], [], 1)
|
||||||
|
if ready:
|
||||||
|
cmd = fifo.read().strip()
|
||||||
|
if cmd:
|
||||||
|
ser.write((cmd + "\r").encode())
|
||||||
|
print(f"[SENT] {cmd}")
|
||||||
|
except Exception as e:
|
||||||
|
if cmd_running:
|
||||||
|
print(f"Cmd reader error: {e}")
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
cmd_thread = threading.Thread(target=cmd_reader, daemon=True)
|
||||||
|
cmd_thread.start()
|
||||||
|
|
||||||
|
# Main loop: read serial and log
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if ser.in_waiting:
|
||||||
|
line = ser.readline().decode("utf-8", errors="replace").strip()
|
||||||
|
if line:
|
||||||
|
ts = time.time()
|
||||||
|
# Write JSON log
|
||||||
|
with open(LOGFILE, "a") as f:
|
||||||
|
json.dump({"ts": f"{ts:.3f}", "line": line}, f)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
# Update state
|
||||||
|
updates = parse_state(line)
|
||||||
|
if updates:
|
||||||
|
with open(STATEFILE, "w") as f:
|
||||||
|
json.dump(updates, f)
|
||||||
|
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
cmd_running = False
|
||||||
|
ser.close()
|
||||||
|
print("\nMonitor stopped")
|
||||||
98
sketches/doorbell-touch/scripts/monitor-agent.sh
Executable file
98
sketches/doorbell-touch/scripts/monitor-agent.sh
Executable file
@@ -0,0 +1,98 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Monitor agent - multiplexed serial monitor for AI interaction
|
||||||
|
# Usage: ./scripts/monitor-agent.sh <board>
|
||||||
|
#
|
||||||
|
# Outputs:
|
||||||
|
# - /tmp/doorbell-$BOARD.jsonl - JSON Lines log {"ts":123.456,"line":"..."}
|
||||||
|
# - /tmp/doorbell-$BOARD-state.json - Current device state
|
||||||
|
# - /tmp/doorbell-$BOARD-cmd.fifo - Command pipe (echo 'cmd' > fifo)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# tail -f /tmp/doorbell-$BOARD.jsonl # Watch logs
|
||||||
|
# echo 'dashboard' > /tmp/doorbell-$BOARD-cmd.fifo # Send command
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BOARD="${1:-esp32-32e-4}"
|
||||||
|
source "./boards/$BOARD/board-config.sh"
|
||||||
|
source ./scripts/lockfile.sh
|
||||||
|
|
||||||
|
acquire_lock || exit 1
|
||||||
|
|
||||||
|
SERIAL="$PORT"
|
||||||
|
LOGFILE="/tmp/doorbell-$BOARD.jsonl"
|
||||||
|
STATEFILE="/tmp/doorbell-$BOARD-state.json"
|
||||||
|
CMDFIFO="/tmp/doorbell-$BOARD-cmd.fifo"
|
||||||
|
SCREENSOCK="doorbell-$BOARD"
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
cleanup() {
|
||||||
|
screen -S "$SCREENSOCK" -X quit 2>/dev/null || true
|
||||||
|
rm -f "$CMDFIFO"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Create command FIFO
|
||||||
|
rm -f "$CMDFIFO"
|
||||||
|
mkfifo "$CMDFIFO"
|
||||||
|
|
||||||
|
# Initialize state
|
||||||
|
echo '{"screen":"BOOT","deviceState":"BOOTED","backlightOn":false}' > "$STATEFILE"
|
||||||
|
|
||||||
|
# Start screen in detached mode with logging
|
||||||
|
# -L: enable logging
|
||||||
|
# -Logfile: specify log file
|
||||||
|
# -d -m: create detached
|
||||||
|
# -S: session name
|
||||||
|
screen -L -Logfile "$LOGFILE.raw" -d -m -S "$SCREENSOCK" "$SERIAL" 115200
|
||||||
|
|
||||||
|
# Give screen time to start
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Reader: parse raw screen log -> JSON Lines + state
|
||||||
|
(
|
||||||
|
exec >>"$LOGFILE" 2>&1
|
||||||
|
last_screen_line=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
# Skip duplicate/corrupted lines
|
||||||
|
[[ "$line" == "$last_screen_line" ]] && continue
|
||||||
|
last_screen_line="$line"
|
||||||
|
|
||||||
|
# Only process non-empty lines
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
|
||||||
|
TS=$(date +%s.%3N)
|
||||||
|
printf '{"ts":%s,"line":"%s"}\n' "$TS" "$line"
|
||||||
|
|
||||||
|
# Parse state changes
|
||||||
|
case "$line" in
|
||||||
|
*"[STATE]"*"→ OFF"*) echo '{"screen":"OFF"}' > "$STATEFILE" ;;
|
||||||
|
*"[STATE]"*"→ DASHBOARD"*) echo '{"screen":"DASHBOARD"}' > "$STATEFILE" ;;
|
||||||
|
*"[STATE]"*"→ ALERT"*) echo '{"screen":"ALERT"}' > "$STATEFILE" ;;
|
||||||
|
*"[STATE]"*"→ BOOT"*) echo '{"screen":"BOOT"}' > "$STATEFILE" ;;
|
||||||
|
*"[ADMIN]"*"dashboard"*) echo '{"screen":"DASHBOARD"}' > "$STATEFILE" ;;
|
||||||
|
*"[ADMIN]"*"off"*) echo '{"screen":"OFF"}' > "$STATEFILE" ;;
|
||||||
|
esac
|
||||||
|
done < "$LOGFILE.raw"
|
||||||
|
) &
|
||||||
|
READER_PID=$!
|
||||||
|
|
||||||
|
# Writer: command FIFO -> screen session
|
||||||
|
(
|
||||||
|
while IFS= read -r cmd < "$CMDFIFO"; do
|
||||||
|
# Send command to screen session
|
||||||
|
screen -S "$SCREENSOCK" -X stuff "$cmd^M"
|
||||||
|
echo "[SENT] $cmd"
|
||||||
|
done
|
||||||
|
) &
|
||||||
|
WRITER_PID=$!
|
||||||
|
|
||||||
|
echo "Monitor agent started (BOARD=$BOARD):"
|
||||||
|
echo " Log: $LOGFILE"
|
||||||
|
echo " State: $STATEFILE"
|
||||||
|
echo " Cmd: echo 'dashboard' > $CMDFIFO"
|
||||||
|
echo ""
|
||||||
|
echo "Press Ctrl+C to exit"
|
||||||
|
|
||||||
|
# Wait for reader
|
||||||
|
wait "$READER_PID" || true
|
||||||
Reference in New Issue
Block a user