diff --git a/.gitignore b/.gitignore index 381fa0e..a60ac9a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,10 @@ vesper/CLAUDE.md vesper/flutter/ vesper/docs_manual/ Doxyfile -vesper/.claude/ \ No newline at end of file +vesper/.claude/ + +# PlatformIO — build output and downloaded libraries (never commit these) +vesper/.pio/ + +# Claude Code memory/session files +.claude/ \ No newline at end of file diff --git a/vesper/documentation/HEARTBEAT_FEATURE.md b/vesper/documentation/HEARTBEAT_FEATURE.md new file mode 100644 index 0000000..f9abfe6 --- /dev/null +++ b/vesper/documentation/HEARTBEAT_FEATURE.md @@ -0,0 +1,139 @@ +# 💓 MQTT Heartbeat Feature + +## Overview +Implemented a **retained MQTT heartbeat** system that sends periodic status updates every 30 seconds when the controller is connected to MQTT. + +## What It Does + +### Heartbeat Message +Every 30 seconds, the controller publishes a **retained** message to: +``` +vesper/{deviceID}/status/heartbeat +``` + +### Message Format +```json +{ + "status": "INFO", + "type": "heartbeat", + "payload": { + "device_id": "VESPER-ABC123", + "firmware_version": "130", + "timestamp": "Uptime: 5h 23m 45s", + "ip_address": "192.168.1.100", + "gateway": "192.168.1.1", + "uptime_ms": 19425000 + } +} +``` + +### Key Features +✅ **Retained Message** - Only the LAST heartbeat stays on the broker +✅ **Auto-Start** - Begins when MQTT connects +✅ **Auto-Stop** - Stops when MQTT disconnects +✅ **30-Second Interval** - Periodic updates +✅ **First Beat Immediate** - Sends first heartbeat right after connecting +✅ **QoS 1** - Reliable delivery + +## Why This is Awesome + +### For Your Flutter App +1. **Immediate Status** - Any new connection gets the last known status instantly +2. **Stale Detection** - Can detect if controller went offline (timestamp too old) +3. **Device Discovery** - Apps can subscribe to `vesper/+/status/heartbeat` to find all controllers +4. **No Polling** - Just subscribe once and get automatic updates + +### Example App Logic +```dart +// Subscribe to heartbeat +mqtt.subscribe('vesper/DEVICE-123/status/heartbeat'); + +// On message received +if (heartbeat.uptime_ms > lastSeen.uptime_ms + 120000) { + // No heartbeat for 2+ minutes = controller offline + showOfflineWarning(); +} +``` + +## Implementation Details + +### Files Modified +1. **MQTTAsyncClient.hpp** - Added heartbeat timer and methods +2. **MQTTAsyncClient.cpp** - Implemented heartbeat logic +3. **Networking.hpp** - Added `getGateway()` method +4. **Networking.cpp** - Implemented `getGateway()` method + +### New Methods Added +```cpp +void startHeartbeat(); // Start 30s periodic timer +void stopHeartbeat(); // Stop timer +void publishHeartbeat(); // Build and publish message +void heartbeatTimerCallback(); // Timer callback handler +``` + +### Timer Configuration +- **Type**: FreeRTOS Software Timer +- **Mode**: Auto-reload (repeating) +- **Period**: 30,000 ms (30 seconds) +- **Core**: Runs on Core 0 (MQTT task core) + +## Testing + +### How to Test +1. Flash the firmware +2. Subscribe to the heartbeat topic: + ```bash + mosquitto_sub -h YOUR_BROKER -t "vesper/+/status/heartbeat" -v + ``` +3. You should see heartbeats every 30 seconds +4. Disconnect the controller - the last message stays retained +5. Reconnect - you'll immediately see the last retained message, then new ones every 30s + +### Expected Serial Output +``` +💓 Starting MQTT heartbeat (every 30 seconds) +💓 Published heartbeat (retained) - IP: 192.168.1.100, Uptime: 45000ms +💓 Published heartbeat (retained) - IP: 192.168.1.100, Uptime: 75000ms +❤️ Stopped MQTT heartbeat (when MQTT disconnects) +``` + +## Future Enhancements (Optional) + +### Possible Additions: +- Add actual RTC timestamp (instead of just uptime) +- Add WiFi signal strength (RSSI) for WiFi connections +- Add free heap memory +- Add current playback status +- Add bell configuration version/hash + +### Implementation Example: +```cpp +// In publishHeartbeat() +payload["rssi"] = WiFi.RSSI(); // WiFi signal strength +payload["free_heap"] = ESP.getFreeHeap(); +payload["playback_active"] = player.isPlaying; +``` + +## Configuration + +### Current Settings (can be changed in MQTTAsyncClient.hpp): +```cpp +static const unsigned long HEARTBEAT_INTERVAL = 30000; // 30 seconds +``` + +To change interval to 60 seconds: +```cpp +static const unsigned long HEARTBEAT_INTERVAL = 60000; // 60 seconds +``` + +## Notes +- Message is published with **QoS 1** (at least once delivery) +- Message is **retained** (broker keeps last message) +- Timer starts automatically when MQTT connects +- Timer stops automatically when MQTT disconnects +- First heartbeat is sent immediately upon connection (no 30s wait) + +--- +**Feature Implemented**: January 2025 +**Version**: Firmware v130+ +**Status**: ✅ Production Ready diff --git a/vesper/documentation/project-vesper-plan.md b/vesper/documentation/project-vesper-plan.md new file mode 100644 index 0000000..18ecfa2 --- /dev/null +++ b/vesper/documentation/project-vesper-plan.md @@ -0,0 +1,463 @@ +# Project Vesper — Manufacturing Automation Master Plan + +> **How to use this document:** Work through each Phase in order. Each Phase has self-contained tasks you can hand directly to Claude Code. Phases 1–3 are foundational; don't skip ahead. Phases 4–6 build on top of them. + +--- + +## Current Stack (Reference) + +| Layer | Technology | +|---|---| +| Microcontroller | ESP32 (ESP8266 on older models, STM32 possible future) | +| MCU Firmware | Arduino / C++ (moving to PlatformIO) | +| Tablet App | Flutter / FlutterFlow (Android) | +| Phone App | Flutter / FlutterFlow (Android + iOS) | +| Admin Console Backend | Python 3.11+ / FastAPI | +| Admin Console Frontend | React + Tailwind CSS | +| Primary Database | Firebase Firestore (via Admin SDK) | +| Secondary Database | Local SQLite (MQTT logs, etc.) | +| File Storage | Firebase Storage | +| MQTT Broker | Mosquitto on VPS | +| Web Server | NGINX on VPS (OTA files, reverse proxy) | +| Deployment | Gitea → git pull on VPS, Docker Compose locally | + +--- + +## Serial Number Format (Locked In) + +``` +PV-YYMMM-BBTTR-XXXXX + +PV = Project Vesper prefix +YY = 2-digit year (e.g. 26) +MMM = 3-char date block = 2-digit month letter + 2-digit day (e.g. A18 = Jan 18) +BB = Board Type code (e.g. BC = BellCore, BP = BellPRO) +TT = Board Type revision (e.g. 02) +R = Literal "R" +XXXXX = 5-char random suffix (A-Z, 0-9, excluding 0/O/1/I for label clarity) + +Example: PV-26A18-BC02R-X7KQA +``` + +**Month Letter Codes:** +A=Jan, B=Feb, C=Mar, D=Apr, E=May, F=Jun, G=Jul, H=Aug, I=Sep, J=Oct, K=Nov, L=Dec + +--- + +## Phase 1 — PlatformIO Migration (Firmware Side) + +> **Goal:** Replace Arduino IDE with PlatformIO. All future firmware work happens here. This unlocks scripted builds and the ability to produce `.bin` files on demand. +> +> **Tell Claude Code:** *"Help me migrate my existing Arduino IDE ESP32 project to PlatformIO with multiple board environments."* + +### Tasks + +- [ ] Install PlatformIO extension in VS Code +- [ ] Create `platformio.ini` with one `[env]` block per hardware variant. Example: + +```ini +[env:bellcore-v2] +platform = espressif32 +board = esp32dev +board_build.partitions = partitions/custom_4mb.csv +board_build.flash_mode = dio +board_upload.flash_size = 4MB +build_flags = + -DBOARD_TYPE="BC" + -DBOARD_VERSION="02" + -DPSRAM_ENABLED=1 + +[env:bellcore-v1] +platform = espressif32 +board = esp32dev +board_build.partitions = partitions/custom_2mb.csv +board_build.flash_mode = dout +board_upload.flash_size = 2MB +build_flags = + -DBOARD_TYPE="BC" + -DBOARD_VERSION="01" + -DPSRAM_ENABLED=0 +``` + +- [ ] Move all `#include` library dependencies into `lib_deps` in `platformio.ini` (no more manual library manager) +- [ ] Verify `pio run -e bellcore-v2` compiles clean +- [ ] Confirm `.pio/build/bellcore-v2/firmware.bin` is produced +- [ ] Create a `/firmware` directory structure on the server (NGINX already serves this): + +``` +/srv/ota/ + bellcore-v1/ + latest.bin + v1.0.0.bin + v1.0.1.bin + bellcore-v2/ + latest.bin + ... +``` + +- [ ] Update your OTA logic in firmware to pull from `/ota/{board_type}/latest.bin` +- [ ] Add a `scripts/build_and_upload.sh` that compiles + copies the new `.bin` to the right NGINX folder (run this after every release) + +### NVS Partition Generator Setup + +- [ ] Install `esptool` and `esp-idf` NVS tool: `pip install esptool` +- [ ] Grab `nvs_partition_gen.py` from ESP-IDF tools (or install via `idf-component-manager`) +- [ ] Test generating a `.bin` from a CSV manually: + +```csv +key,type,encoding,value +serial_number,data,string,PV-26A18-BC02R-X7KQA +hw_type,data,string,BC +hw_version,data,string,02 +``` + +```bash +python nvs_partition_gen.py generate nvs_data.csv nvs_data.bin 0x6000 +``` + +- [ ] Confirm the ESP32 reads NVS values correctly on boot with this pre-flashed partition +- [ ] Note the NVS partition address for your board (check your partition table CSV — typically `0x9000`) + +--- + +## Phase 2 — MQTT Dynamic Auth (Backend Side) + +> **Goal:** Replace `mosquitto_passwd` manual SSH with automatic credential management. New devices are live on MQTT the moment they exist in your database. Per-device topic isolation enforced automatically. +> +> **Tell Claude Code:** *"Help me set up mosquitto-go-auth on my VPS with a FastAPI backend for dynamic MQTT authentication and ACL enforcement."* + +### Tasks + +#### 2a. Install mosquitto-go-auth on VPS + +- [ ] Install Go on VPS (required to build the plugin) +- [ ] Clone and build `mosquitto-go-auth`: + ```bash + git clone https://github.com/iegomez/mosquitto-go-auth + cd mosquitto-go-auth && make + ``` +- [ ] Update `mosquitto.conf` to load the plugin: + ``` + auth_plugin /path/to/go-auth.so + auth_opt_backends http + auth_opt_http_host localhost + auth_opt_http_port 8000 + auth_opt_http_getuser_uri /mqtt/auth/user + auth_opt_http_aclcheck_uri /mqtt/auth/acl + auth_opt_cache true + auth_opt_cache_host localhost + auth_opt_cache_reset true + auth_opt_auth_cache_seconds 300 + auth_opt_acl_cache_seconds 300 + ``` + +#### 2b. Add MQTT Auth Endpoints to FastAPI + +- [ ] Create `/mqtt/auth/user` endpoint — Mosquitto calls this on CONNECT: + - Receives: `username` (= device SN), `password` + - Checks Firestore/SQLite for device record + hashed password + - Returns: `200` (allow) or `403` (deny) + +- [ ] Create `/mqtt/auth/acl` endpoint — Mosquitto calls this on SUBSCRIBE/PUBLISH: + - Receives: `username`, `topic`, `acc` (1=sub, 2=pub) + - Rule: username must match the SN segment in the topic + - Topic pattern: `/vesper/{SN}/data` or `/vesper/{SN}/control` + - Extract `{SN}` from topic, compare to `username` + - Returns: `200` or `403` + +- [ ] **For user phone app clients:** Add a separate user auth flow + - Users authenticate with their Firebase UID as MQTT username + - ACL check: look up which devices this UID owns in Firestore, permit only those SNs in topics + +#### 2c. MQTT Password Strategy + +Use **HMAC-derived passwords** so you never have to store or manually set them: + +```python +import hmac, hashlib + +MQTT_SECRET = os.getenv("MQTT_SECRET") # Keep in .env, never commit + +def derive_mqtt_password(serial_number: str) -> str: + return hmac.new( + MQTT_SECRET.encode(), + serial_number.encode(), + hashlib.sha256 + ).hexdigest()[:32] +``` + +- Device SN is known → password is deterministic → firmware can compute it at boot +- No password storage needed in DB (just re-derive on auth check) +- Changing `MQTT_SECRET` rotates all passwords at once if ever needed + +- [ ] Add `MQTT_SECRET` to your `.env` and Docker Compose secrets +- [ ] Update firmware to derive its own MQTT password using the same HMAC logic (port to C++) +- [ ] Remove all existing `mosquitto_passwd` file entries and disable static auth + +#### 2d. Test + +- [ ] New device connects with correct SN + derived password → allowed +- [ ] Device tries to sub/pub on another device's topic → denied (403) +- [ ] Wrong password → denied +- [ ] Confirm cache is working (check logs, only 1-2 auth calls per session) + +--- + +## Phase 3 — Serial Number & Batch Management in Admin Console + +> **Goal:** SN generation, DB registration, and MQTT credential provisioning all happen in one flow in the React Console. The Flutter admin app is retired. +> +> **Tell Claude Code:** *"Add a Manufacturing / Batch Management section to our React+FastAPI admin console with the following features..."* + +### Tasks + +#### 3a. Backend — New API Routes in FastAPI + +- [ ] `POST /manufacturing/batch` — Create a new batch: + - Input: `board_type`, `board_version`, `quantity`, `subscription_plan`, `available_outputs` + - Generate N serial numbers using the `PV-YYMMM-BBTTR-XXXXX` format + - Check Firestore for collisions, regenerate if collision found + - Write N device documents to Firestore collection `devices`: + ```json + { + "serial_number": "PV-26A18-BC02R-X7KQA", + "hw_type": "BC", + "hw_version": "02", + "status": "manufactured", + "subscription_plan": "standard", + "available_outputs": 8, + "created_at": "...", + "owner": null, + "users_list": [] + } + ``` + - Returns: list of created SNs + +- [ ] `GET /manufacturing/batch/{batch_id}` — List devices in a batch with status +- [ ] `GET /manufacturing/devices` — List all devices with filters (status, hw_type, date range) +- [ ] `POST /manufacturing/devices/{sn}/assign` — Pre-assign device to a customer email +- [ ] `GET /manufacturing/firmware/{hw_type}/{hw_version}` — Return download URL for the correct `.bin` + +#### 3b. SN Generator Utility (Python) + +```python +# utils/serial_number.py +import random, string +from datetime import datetime + +MONTH_CODES = "ABCDEFGHIJKL" +SAFE_CHARS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No 0,O,1,I + +def generate_serial(board_type: str, board_version: str) -> str: + now = datetime.utcnow() + year = now.strftime("%y") + month = MONTH_CODES[now.month - 1] + day = now.strftime("%d") + random_suffix = "".join(random.choices(SAFE_CHARS, k=5)) + return f"PV-{year}{month}{day}-{board_type}{board_version}R-{random_suffix}" +``` + +#### 3c. NVS Binary Generation in FastAPI + +- [ ] Copy `nvs_partition_gen.py` into your FastAPI project +- [ ] Add endpoint `GET /manufacturing/devices/{sn}/nvs.bin`: + - Generates a temp CSV for this SN + - Runs `nvs_partition_gen.py` to produce the `.bin` + - Returns the binary file as a download +- [ ] Add endpoint `GET /manufacturing/devices/{sn}/firmware.bin`: + - Looks up device's `hw_type` and `hw_version` from Firestore + - Returns the correct firmware `.bin` from the NGINX folder (or redirects to NGINX URL) + +#### 3d. Label Sheet Generation + +- [ ] Add `POST /manufacturing/batch/{batch_id}/labels` endpoint + - Returns a PDF with one label per device + - Each label contains: SN (human readable), QR code of SN, HW Type, HW Version + - Use `reportlab` or `fpdf2` Python library for PDF generation + - QR code: use `qrcode` Python library + +#### 3e. Frontend — Manufacturing Section in React Console + +- [ ] New route: `/manufacturing` +- [ ] **Batch Creator:** Form with board type selector, quantity, subscription plan → calls `POST /manufacturing/batch` → shows created SNs + download label PDF button +- [ ] **Device List:** Filterable table of all devices with status badges (manufactured / sold / claimed / active) +- [ ] **Device Detail Page:** Shows all fields, allows status update, shows assignment history + +--- + +## Phase 4 — Browser-Based Flashing (The Provisioning Wizard) + +> **Goal:** A single browser tab handles the entire provisioning flow. Plug in ESP32, click through wizard, done. No Arduino IDE, no esptool CLI, no SSH. +> +> **Tell Claude Code:** *"Add a Device Provisioning Wizard to our React admin console using esptool-js and the Web Serial API."* +> +> **Browser requirement:** Chrome or Edge only (Web Serial API). Firefox not supported. This is fine for an internal manufacturing tool. + +### Tasks + +#### 4a. Add esptool-js to React Console + +- [ ] `npm install esptool-js` (or use the CDN build) +- [ ] Confirm Chrome is used on the manufacturing bench laptop + +#### 4b. Provisioning Wizard UI (React Component) + +Build a step-by-step wizard. Steps: + +**Step 1 — Select or Create Device** +- Search existing unprovisioned device by SN, OR +- Quick-create single device (calls `POST /manufacturing/batch` with qty=1) +- Displays SN, HW Type, HW Version + +**Step 2 — Flash Device** +- "Connect Device" button → triggers Web Serial port picker +- Fetches `nvs.bin` and `firmware.bin` from your FastAPI backend for this SN +- Shows two progress bars: NVS partition flash + Firmware flash +- Flash addresses (example for standard ESP32): + - NVS: `0x9000` (verify against your partition table) + - Firmware: `0x10000` +- On completion: updates device status to `flashed` in Firestore via API call + +**Step 3 — Verify** +- Prompt: "Power cycle device and wait for it to connect" +- Poll Firestore (or MQTT) for first heartbeat/connection from this SN +- Show green checkmark when device phone home +- Updates status to `provisioned` + +**Step 4 — Done** +- Show summary +- Option: "Provision next device" (loops back to Step 1 with same batch settings) +- Option: "Print label" (downloads single-device PDF label) + +#### 4c. esptool-js Flash Logic (Skeleton) + +```javascript +import { ESPLoader, Transport } from "esptool-js"; + +async function flashDevice(serialPort, nvsArrayBuffer, firmwareArrayBuffer) { + const transport = new Transport(serialPort); + const loader = new ESPLoader({ transport, baudrate: 460800 }); + + await loader.main_fn(); + await loader.flash_id(); + + await loader.write_flash({ + fileArray: [ + { data: nvsArrayBuffer, address: 0x9000 }, + { data: firmwareArrayBuffer, address: 0x10000 }, + ], + flashSize: "keep", + flashMode: "keep", + flashFreq: "keep", + eraseAll: false, + compress: true, + }); + + await transport.disconnect(); +} +``` + +#### 4d. NGINX CORS Headers + +Add to your NGINX config so the browser can fetch `.bin` files: + +```nginx +location /ota/ { + add_header Access-Control-Allow-Origin "https://your-console-domain.com"; + add_header Access-Control-Allow-Methods "GET"; +} +``` + +--- + +## Phase 5 — Email Notifications + +> **Goal:** Admin Console can send transactional emails (device assignment invites, alerts, etc.) +> +> **Tell Claude Code:** *"Add email sending capability to our FastAPI backend using Resend (or SMTP)."* + +### Tasks + +- [ ] Sign up for [Resend](https://resend.com) (free tier: 3000 emails/month, 100/day) +- [ ] Add `RESEND_API_KEY` to `.env` +- [ ] Install: `pip install resend` +- [ ] Create `utils/email.py`: + +```python +import resend +import os + +resend.api_key = os.getenv("RESEND_API_KEY") + +def send_device_invite(customer_email: str, serial_number: str, customer_name: str = None): + resend.Emails.send({ + "from": "noreply@yourcompany.com", + "to": customer_email, + "subject": "Your Vesper device is ready", + "html": f""" +

Your device has been registered

+

Serial Number: {serial_number}

+

Open the Vesper app and enter this serial number to get started.

+ """ + }) +``` + +- [ ] Hook into `POST /manufacturing/devices/{sn}/assign` to send invite automatically +- [ ] Add basic email templates for: device assignment, welcome, error alerts + +--- + +## Phase 6 — Polish & Retire Legacy Tools + +> **Goal:** Clean up. Everything lives in the Console. Nothing is done manually. + +### Tasks + +- [ ] **Retire Flutter admin app** — confirm every function it had is now in the React Console +- [ ] **Remove static mosquitto password file** — all auth is dynamic now +- [ ] **Add device status dashboard** to Console home: counts by status, recent provisioning activity +- [ ] **Add audit log** — every manufacturing action (batch created, device flashed, device assigned) logged to SQLite with timestamp and admin user +- [ ] **Document your `platformio.ini` environments** — add a `FIRMWARE_VARIANTS.md` to the firmware repo +- [ ] **Set up Gitea webhook** → on push to `main`, VPS auto-pulls and restarts Docker containers (replaces manual `git pull`) + +--- + +## Gitea / Docker Compose Deployment Note + +Your local Docker Compose setup and VPS production setup are the same codebase — this is correct and will continue to work fine. A few tips: + +- Use a `.env.production` and `.env.development` file, never commit either +- Your `docker-compose.yml` should reference `${ENV_VAR}` from the env file +- The Gitea webhook for auto-deploy is a simple shell script triggered by the webhook: + ```bash + #!/bin/bash + cd /path/to/project + git pull origin main + docker compose up -d --build + ``` +- Protect this webhook endpoint with a secret token + +--- + +## Summary — What Gets Killed + +| Old Way | Replaced By | +|---|---| +| Arduino IDE | PlatformIO (VS Code) | +| Manual `mosquitto_passwd` via SSH | FastAPI dynamic auth endpoints | +| Flutter admin app | React Admin Console | +| Manual SN generation | Console batch creator | +| Manual DB entry per device | Auto-provisioned on batch creation | +| Manual firmware flash + config page | Browser provisioning wizard (esptool-js) | +| Manual NVS entry via HTTP config page | Pre-flashed NVS partition | + +## Estimated Time Per Device (After All Phases Complete) + +| Task | Time | +|---|---| +| Generate 15-device batch + print labels | ~2 min | +| Flash each device (plug in, click Flash, done) | ~3 min each (parallelizable) | +| Devices self-verify on lab WiFi | passive, ~1 min each | +| **Total for 15 devices** | **~20-25 min** | + +vs. current ~20 min per device = ~5 hours for 15. diff --git a/vesper/platformio.ini b/vesper/platformio.ini new file mode 100644 index 0000000..52e2324 --- /dev/null +++ b/vesper/platformio.ini @@ -0,0 +1,121 @@ +; ═══════════════════════════════════════════════════════════════════════════════ +; Project Vesper — PlatformIO Configuration +; ═══════════════════════════════════════════════════════════════════════════════ +; +; Hardware Variants: +; vesper-v1 — Kincony KC868-A6 (ESP32-S3, 4MB flash) — current production board +; +; Future variants (not yet active): +; vesper-plus-v1 — Vesper+ with RF remote support +; vesper-pro-v1 — Vesper Pro with onboard LCD +; +; Build: pio run -e vesper-v1 +; Upload: pio run -e vesper-v1 --target upload +; Monitor: pio device monitor +; Clean: pio run -e vesper-v1 --target clean +; ═══════════════════════════════════════════════════════════════════════════════ + +; ─────────────────────────────────────────────────────────────────────────────── +; SHARED SETTINGS — inherited by all environments +; ─────────────────────────────────────────────────────────────────────────────── +[common] +platform = espressif32 +framework = arduino +monitor_speed = 115200 + +; All external library dependencies +lib_deps = + ; WiFi provisioning portal + tzapu/WiFiManager @ ^2.0.17 + + ; Async web server + WebSocket support + ; NOTE: Use the ESP32-compatible fork, not the original + https://github.com/me-no-dev/ESPAsyncWebServer.git + https://github.com/me-no-dev/AsyncTCP.git + + ; JSON parsing + bblanchon/ArduinoJson @ ^7.0.0 + + ; I2C GPIO expanders (relay control) — PCF8575 header is bundled in same library + adafruit/Adafruit PCF8574 @ ^1.1.0 + + ; Real-time clock + adafruit/RTClib @ ^2.1.4 + + ; Async MQTT client + ; NOTE: Requires AsyncTCP (already listed above) + https://github.com/marvinroger/async-mqtt-client.git + +build_flags_common = + -DCORE_DEBUG_LEVEL=0 + -DCONFIG_ASYNC_TCP_RUNNING_CORE=0 + +; ─────────────────────────────────────────────────────────────────────────────── +; VESPER v1 — Kincony KC868-A6 (ESP32-S3, 4MB Flash) +; Current production board +; ─────────────────────────────────────────────────────────────────────────────── +[env:vesper-v1] +platform = ${common.platform} +framework = ${common.framework} +board = esp32-s3-devkitc-1 + +; Serial monitor +monitor_speed = ${common.monitor_speed} + +; Upload settings +upload_speed = 921600 +upload_protocol = esptool + +; Partition table — default 4MB with OTA support +; Provides: 1.8MB app slot + 1.8MB OTA slot + 64KB NVS + SPIFFS +board_build.partitions = default_8MB.csv + +; Build flags for this variant +build_flags = + ${common.build_flags_common} + -DBOARD_TYPE=\"VS\" + -DBOARD_VERSION=\"01\" + -DBOARD_NAME=\"Vesper\" + -DPSRAM_ENABLED=0 + -DHAS_RF=0 + -DHAS_LCD=0 + +lib_deps = ${common.lib_deps} + + +; ─────────────────────────────────────────────────────────────────────────────── +; VESPER+ v1 — Future: adds RF remote support +; ─────────────────────────────────────────────────────────────────────────────── +; [env:vesper-plus-v1] +; platform = ${common.platform} +; framework = ${common.framework} +; board = esp32-s3-devkitc-1 +; monitor_speed = ${common.monitor_speed} +; build_flags = +; ${common.build_flags_common} +; -DBOARD_TYPE=\"VP\" +; -DBOARD_VERSION=\"01\" +; -DBOARD_NAME=\"Vesper+\" +; -DPSRAM_ENABLED=0 +; -DHAS_RF=1 +; -DHAS_LCD=0 +; lib_deps = ${common.lib_deps} + + +; ─────────────────────────────────────────────────────────────────────────────── +; VESPER PRO v1 — Future: adds onboard LCD +; ─────────────────────────────────────────────────────────────────────────────── +; [env:vesper-pro-v1] +; platform = ${common.platform} +; framework = ${common.framework} +; board = esp32-s3-devkitc-1 +; monitor_speed = ${common.monitor_speed} +; build_flags = +; ${common.build_flags_common} +; -DBOARD_TYPE=\"VX\" +; -DBOARD_VERSION=\"01\" +; -DBOARD_NAME=\"VesperPro\" +; -DPSRAM_ENABLED=0 +; -DHAS_RF=0 +; -DHAS_LCD=1 +; lib_deps = ${common.lib_deps} diff --git a/vesper/src/main.cpp b/vesper/src/main.cpp new file mode 100644 index 0000000..b410a83 --- /dev/null +++ b/vesper/src/main.cpp @@ -0,0 +1,536 @@ +/* + + █████ █████ ██████████ █████████ ███████████ ██████████ ███████████ +▒▒███ ▒▒███ ▒▒███▒▒▒▒▒█ ███▒▒▒▒▒███▒▒███▒▒▒▒▒███▒▒███▒▒▒▒▒█▒▒███▒▒▒▒▒███ + ▒███ ▒███ ▒███ █ ▒ ▒███ ▒▒▒ ▒███ ▒███ ▒███ █ ▒ ▒███ ▒███ + ▒███ ▒███ ▒██████ ▒▒█████████ ▒██████████ ▒██████ ▒██████████ + ▒▒███ ███ ▒███▒▒█ ▒▒▒▒▒▒▒▒███ ▒███▒▒▒▒▒▒ ▒███▒▒█ ▒███▒▒▒▒▒███ + ▒▒▒█████▒ ▒███ ▒ █ ███ ▒███ ▒███ ▒███ ▒ █ ▒███ ▒███ + ▒▒███ ██████████▒▒█████████ █████ ██████████ █████ █████ + ▒▒▒ ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒▒▒▒▒▒ ▒▒▒▒▒ ▒▒▒▒▒ + + * ═══════════════════════════════════════════════════════════════════════════════════ + * Project VESPER - BELL AUTOMATION SYSTEM - Main Firmware Entry Point + * ═══════════════════════════════════════════════════════════════════════════════════ + * + * 🔔 DESCRIPTION: + * High-precision automated bell control system with multi-protocol communication, + * real-time telemetry, OTA updates, and modular hardware abstraction. + * + * 🏗️ ARCHITECTURE: + * Clean modular design with dependency injection and proper separation of concerns. + * Each major system is encapsulated in its own class with well-defined interfaces. + * + * 🎯 KEY FEATURES: + * ✅ Microsecond-precision bell timing (BellEngine) + * ✅ Multi-hardware support (PCF8574, GPIO, Mock) + * ✅ Dual network connectivity (Ethernet + WiFi + Permanent AP Mode) + * ✅ Multi-protocol communication (MQTT + WebSocket + HTTP REST API) + * ✅ Web settings interface for network mode switching + * ✅ Real-time telemetry and load monitoring + * ✅ Over-the-air firmware updates + * ✅ SD card configuration and file management + * ✅ NTP time synchronization + * ✅ Comprehensive logging system + * + * 📡 COMMUNICATION PROTOCOLS: + * • MQTT (SSL/TLS via AsyncMqttClient on Core 0) + * • WebSocket (Real-time web interface) + * • HTTP REST API (Command execution via HTTP) + * • UDP Discovery (Auto-discovery service) + * • HTTP/HTTPS (OTA updates) + * + * 🔧 HARDWARE ABSTRACTION: + * OutputManager provides clean interface for different relay systems: + * - PCF8574OutputManager: I2C GPIO expander (8 outputs, 6 on Kincony A6 Board) + * - GPIOOutputManager: Direct ESP32 pins (for DIY projects) + * - MockOutputManager: Testing without hardware + * + * ⚡ PERFORMANCE: + * High-priority FreeRTOS tasks ensure microsecond timing precision. + * Core 1 dedicated to BellEngine for maximum performance. + * + * ═══════════════════════════════════════════════════════════════════════════════════ + */ + + + + + /* + * ═══════════════════════════════════════════════════════════════════════════════ + * 📋 VERSION CONFIGURATION + * ═══════════════════════════════════════════════════════════════════════════════ + * 📅 DATE: 2025-10-10 + * 👨‍💻 AUTHOR: BellSystems bonamin + */ + +#define FW_VERSION "154" + + +/* + * ═══════════════════════════════════════════════════════════════════════════════ + * 📅 VERSION HISTORY: + * NOTE: Versions are now stored as integers (v1.3 = 130) + * ═══════════════════════════════════════════════════════════════════════════════ + * v0.1 (100) - Vesper Launch Beta + * v1.2 (120) - Added Log Level Configuration via App/MQTT + * v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes + * v137 - Made OTA and MQTT delays Async + * v138 - Removed Ethernet, added default WiFi creds (Mikrotik AP) and fixed various Clock issues + * v140 - Changed FW Updates to Direct-to-Flash and added manual update functionality with version check + * v151 - Fixed Clock Alerts not running properly + * v152 - Fix RTC Time Reports, added sync_time_to_LCD functionality + * v153 - Fix Infinite Loop Bug and Melody Download crashes. + * ═══════════════════════════════════════════════════════════════════════════════ + */ + + + + + + + +// ═══════════════════════════════════════════════════════════════════════════════════ +// SYSTEM LIBRARIES - Core ESP32 and Arduino functionality +// ═══════════════════════════════════════════════════════════════════════════════════ +#include // SD card file system operations +#include // File system base class +#include // Ethernet connectivity (W5500 support) +#include // SPI communication protocol +#include // Arduino core framework +#include // WiFi connectivity management +#include // HTTP client for OTA updates +#include // Firmware update utilities +#include // I2C communication protocol +#include // Task watchdog timer + +// ═══════════════════════════════════════════════════════════════════════════════════ +// NETWORKING LIBRARIES - Advanced networking and communication +// ═══════════════════════════════════════════════════════════════════════════════════ +#include // WiFi configuration portal +#include // Async web server for WebSocket support +#include // UDP for discovery service + +// ═══════════════════════════════════════════════════════════════════════════════════ +// DATA PROCESSING LIBRARIES - JSON parsing and data structures +// ═══════════════════════════════════════════════════════════════════════════════════ +#include // Efficient JSON processing +#include // STL string support + +// ═══════════════════════════════════════════════════════════════════════════════════ +// HARDWARE LIBRARIES - Peripheral device control +// ═══════════════════════════════════════════════════════════════════════════════════ +#include // I2C GPIO expander for relay control +#include // Real-time clock functionality + +// ═══════════════════════════════════════════════════════════════════════════════════ +// CUSTOM CLASSES - Include Custom Classes and Functions +// ═══════════════════════════════════════════════════════════════════════════════════ +#include "SDCardMutex/SDCardMutex.hpp" // ⚠️ MUST be included before any SD-using classes +#include "ConfigManager/ConfigManager.hpp" +#include "FileManager/FileManager.hpp" +#include "TimeKeeper/TimeKeeper.hpp" +#include "Logging/Logging.hpp" +#include "Telemetry/Telemetry.hpp" +#include "OTAManager/OTAManager.hpp" +#include "Networking/Networking.hpp" +#include "Communication/CommunicationRouter/CommunicationRouter.hpp" +#include "ClientManager/ClientManager.hpp" +#include "Communication/ResponseBuilder/ResponseBuilder.hpp" +#include "Player/Player.hpp" +#include "BellEngine/BellEngine.hpp" +#include "OutputManager/OutputManager.hpp" +#include "HealthMonitor/HealthMonitor.hpp" +#include "FirmwareValidator/FirmwareValidator.hpp" +#include "InputManager/InputManager.hpp" + +#define TAG "Main" + +// Class Constructors +ConfigManager configManager; +FileManager fileManager(&configManager); +Timekeeper timekeeper; +Telemetry telemetry; +OTAManager otaManager(configManager); +Player player; +AsyncWebServer server(80); +AsyncWebSocket ws("/ws"); +AsyncUDP udp; +Networking networking(configManager); +CommunicationRouter communication(configManager, otaManager, networking, server, ws, udp); +HealthMonitor healthMonitor; +FirmwareValidator firmwareValidator; +InputManager inputManager; + + +// 🔥 OUTPUT SYSTEM - PCF8574/PCF8575 I2C Expanders Configuration +// Choose one of the following configurations (with active output counts): + +// Option 1: Single PCF8574 (6 active outputs out of 8 max) +PCF8574OutputManager outputManager(0x24, ChipType::PCF8574, 6); + +// Option 2: Single PCF8575 (8 active outputs out of 16 max) +//PCF8574OutputManager outputManager(0x24, ChipType::PCF8575, 8); + +// Option 3: PCF8574 + PCF8575 (6 + 8 = 14 total virtual outputs) +//PCF8574OutputManager outputManager(0x24, ChipType::PCF8574, 6, 0x21, ChipType::PCF8575, 8); + +// Option 4: Dual PCF8575 (8 + 8 = 16 total virtual outputs) +//PCF8574OutputManager outputManager(0x24, ChipType::PCF8575, 8, 0x21, ChipType::PCF8575, 8); + +// Virtual Output Mapping Examples: +// Option 1: Virtual outputs 0-5 → PCF8574[0x20] pins 0-5 +// Option 3: Virtual outputs 0-5 → PCF8574[0x20] pins 0-5, Virtual outputs 6-13 → PCF8575[0x21] pins 0-7 +// Option 4: Virtual outputs 0-7 → PCF8575[0x20] pins 0-7, Virtual outputs 8-15 → PCF8575[0x21] pins 0-7 + +// Legacy backward-compatible (defaults to 8 active outputs): +//PCF8574OutputManager outputManager(0x20, ChipType::PCF8574); // 8/8 active outputs + +BellEngine bellEngine(player, configManager, telemetry, outputManager); // 🔥 THE ULTIMATE BEAST! + +TaskHandle_t bellEngineHandle = NULL; // Legacy - will be removed +TimerHandle_t schedulerTimer; +TimerHandle_t ntpSyncTimer; // Non-blocking delayed NTP sync timer + + + +void handleFactoryReset() { + if (configManager.resetAllToDefaults()) { + delay(3000); + ESP.restart(); + } +} + +// Non-blocking NTP sync timer callback +void ntpSyncTimerCallback(TimerHandle_t xTimer) { + LOG_DEBUG(TAG, "Network stabilization complete - starting NTP sync"); + if (!networking.isInAPMode()) { + timekeeper.syncTimeWithNTP(); + } +} + + + +void setup() +{ + // Initialize Serial Communications (for debugging) & I2C Bus (for Hardware Control) + Serial.begin(115200); + Serial.print("VESPER System Booting UP! - Version "); + Serial.println(FW_VERSION); + Wire.begin(4,15); + auto& hwConfig = configManager.getHardwareConfig(); + SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi); + delay(50); + + // 🔒 CRITICAL: Initialize SD Card Mutex BEFORE any SD operations + // This prevents concurrent SD access from multiple FreeRTOS tasks + if (!SDCardMutex::getInstance().begin()) { + Serial.println("❌ FATAL: Failed to initialize SD card mutex!"); + Serial.println(" System cannot continue safely - entering infinite loop"); + while(1) { delay(1000); } // Halt system - unsafe to proceed + } + Serial.println("✅ SD card mutex initialized"); + + // Initialize Configuration (loads factory identity from NVS + user settings from SD) + configManager.begin(); + + // Apply log level from config (loaded from SD) + uint8_t logLevel = configManager.getGeneralConfig().serialLogLevel; + Logging::setLevel((Logging::LogLevel)logLevel); + LOG_INFO(TAG, "Log level set to %d from configuration", logLevel); + + inputManager.begin(); + inputManager.setFactoryResetLongPressCallback(handleFactoryReset); + + // ═══════════════════════════════════════════════════════════════════════════════ + // REMOVED: Manual device identity setters + // Device identity (UID, hwType, hwVersion) is now READ-ONLY in production firmware + // These values are set by factory firmware and stored permanently in NVS + // Production firmware loads them once at boot and keeps them in RAM + // ═══════════════════════════════════════════════════════════════════════════════ + + // Update firmware version (this is the ONLY identity field that can be set) + + // 🔥 MIGRATION: Convert old float-style version to integer format + String currentVersion = configManager.getFwVersion(); + if (currentVersion.indexOf('.') != -1) { + // Old format detected (e.g., "1.3"), convert to integer ("130") + float versionFloat = currentVersion.toFloat(); + uint16_t versionInt = (uint16_t)(versionFloat * 100.0f); + configManager.setFwVersion(String(versionInt)); + configManager.saveDeviceConfig(); + LOG_INFO(TAG, "⚠️ Migrated version format: %s -> %u", currentVersion.c_str(), versionInt); + } + + configManager.setFwVersion(FW_VERSION); + LOG_INFO(TAG, "Firmware version: %s", FW_VERSION); + + + // Display device information after configuration is loaded + Serial.println("\n=== DEVICE IDENTITY ==="); + Serial.printf("Device UID: %s\n", configManager.getDeviceUID().c_str()); + Serial.printf("Hardware Type: %s\n", configManager.getHwType().c_str()); + Serial.printf("Hardware Version: %s\n", configManager.getHwVersion().c_str()); + Serial.printf("Firmware Version: %s\n", configManager.getFwVersion().c_str()); + Serial.printf("AP SSID: %s\n", configManager.getAPSSID().c_str()); + Serial.println("=====================\n"); + + // 🔥 CRITICAL: Initialize Health Monitor FIRST (required for firmware validation) + healthMonitor.begin(); + // Register all subsystems with health monitor for continuous monitoring + healthMonitor.setConfigManager(&configManager); + healthMonitor.setFileManager(&fileManager); + + // Initialize Output Manager - 🔥 THE NEW WAY! + outputManager.setConfigManager(&configManager); + if (!outputManager.initialize()) { + LOG_ERROR(TAG, "Failed to initialize OutputManager!"); + // Continue anyway for now + } + // Register OutputManager with health monitor + healthMonitor.setOutputManager(&outputManager); + + // Initialize BellEngine early for health validation + bellEngine.begin(); + healthMonitor.setBellEngine(&bellEngine); + + delay(100); + + // 🔥 BULLETPROOF: Initialize Firmware Validator and perform startup validation + firmwareValidator.begin(&healthMonitor, &configManager); + delay(100); + + // 💀 CRITICAL SAFETY CHECK: Perform startup validation + // This MUST happen early before initializing other subsystems + if (!firmwareValidator.performStartupValidation()) { + // If we reach here, startup validation failed and rollback was triggered + // The system should reboot automatically to the previous firmware + LOG_ERROR(TAG, "💀 STARTUP VALIDATION FAILED - SYSTEM HALTED"); + while(1) { delay(1000); } // Should not reach here + } + + LOG_INFO(TAG, "✅ Firmware startup validation PASSED - proceeding with initialization"); + + // Initialize remaining subsystems... + + // SD Card initialization is now handled by ConfigManager + + // Initialize timekeeper with NO clock outputs + timekeeper.begin(); // No parameters needed + // Connect the timekeeper to dependencies (CLEAN!) + timekeeper.setOutputManager(&outputManager); + timekeeper.setConfigManager(&configManager); + timekeeper.setNetworking(&networking); + timekeeper.setPlayer(&player); // 🔥 Connect for playback coordination + // Clock outputs now configured via ConfigManager/Communication commands + + // Register TimeKeeper with health monitor + healthMonitor.setTimeKeeper(&timekeeper); + + // Initialize Telemetry + telemetry.setPlayerReference(&player.isPlaying); + // 🚑 CRITICAL: Connect force stop callback for overload protection! + telemetry.setForceStopCallback([]() { player.forceStop(); }); + telemetry.setFileManager(&fileManager); + telemetry.begin(); + + // Register Telemetry with health monitor + healthMonitor.setTelemetry(&telemetry); + + + // Initialize Networking (handles everything automatically) + networking.begin(); + + // Register Networking with health monitor + healthMonitor.setNetworking(&networking); + + // Initialize Player + player.begin(); + + // Register Player with health monitor + healthMonitor.setPlayer(&player); + + // BellEngine already initialized and registered earlier for health validation + + // Initialize Communication Manager (now with PubSubClient MQTT) + communication.begin(); + communication.setPlayerReference(&player); + communication.setFileManagerReference(&fileManager); + communication.setTimeKeeperReference(&timekeeper); + communication.setFirmwareValidatorReference(&firmwareValidator); + communication.setTelemetryReference(&telemetry); + + player.setDependencies(&communication, &fileManager); + player.setBellEngine(&bellEngine); // Connect the beast! + player.setTelemetry(&telemetry); + player.setTimekeeper(&timekeeper); // 🔥 Connect for alert coordination + + // Register Communication with health monitor + healthMonitor.setCommunication(&communication); + + // 🔔 CONNECT BELLENGINE TO COMMUNICATION FOR DING NOTIFICATIONS! + bellEngine.setCommunicationManager(&communication); + + // Track if AsyncWebServer has been started to prevent duplicates + static bool webServerStarted = false; + + // Create NTP sync timer (one-shot, 3 second delay for network stabilization) + ntpSyncTimer = xTimerCreate( + "NTPSync", // Timer name + pdMS_TO_TICKS(3000), // 3 second delay (network stabilization) + pdFALSE, // One-shot timer (not auto-reload) + NULL, // Timer ID (not used) + ntpSyncTimerCallback // Callback function + ); + + // Set up network callbacks + networking.setNetworkCallbacks( + [&webServerStarted]() { + communication.onNetworkConnected(); + + // Schedule non-blocking NTP sync after 3s network stabilization (like MQTT) + // Skip NTP sync in AP mode (no internet connection) + if (!networking.isInAPMode() && ntpSyncTimer) { + LOG_DEBUG(TAG, "Network connected - scheduling NTP sync after 3s stabilization (non-blocking)"); + xTimerStart(ntpSyncTimer, 0); + } + + // Start AsyncWebServer when network becomes available (only once!) + if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) { + LOG_INFO(TAG, "🚀 Starting AsyncWebServer on port 80..."); + server.begin(); + LOG_INFO(TAG, "✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str()); + webServerStarted = true; + } + }, // onConnected + []() { communication.onNetworkDisconnected(); } // onDisconnected + ); + + // If already connected, trigger MQTT connection and setup manually + if (networking.isConnected()) { + LOG_INFO(TAG, "Network already connected - initializing services"); + communication.onNetworkConnected(); + + // Schedule non-blocking NTP sync after 3s network stabilization (like MQTT) + // Skip NTP sync in AP mode (no internet connection) + if (!networking.isInAPMode() && ntpSyncTimer) { + LOG_DEBUG(TAG, "Network already connected - scheduling NTP sync after 3s stabilization (non-blocking)"); + xTimerStart(ntpSyncTimer, 0); + } + + // 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready + // Do NOT start if WiFiManager portal is active (port 80 conflict!) + if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) { + LOG_INFO(TAG, "🚀 Starting AsyncWebServer on port 80..."); + server.begin(); + LOG_INFO(TAG, "✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str()); + webServerStarted = true; + } + } else { + LOG_WARNING(TAG, "⚠️ Network not ready - services will start after connection"); + } + + // Initialize OTA Manager + otaManager.begin(); + otaManager.setFileManager(&fileManager); + otaManager.setPlayer(&player); // Set player reference for idle check + otaManager.setTimeKeeper(&timekeeper); // Set timekeeper reference for freeze mode + otaManager.setTelemetry(&telemetry); // Set telemetry reference for freeze mode + + // 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay) + // UDP discovery setup can happen immediately without conflicts + communication.setupUdpDiscovery(); + + // Register OTA Manager with health monitor + healthMonitor.setOTAManager(&otaManager); + + // Note: AsyncWebServer will be started by network callbacks when connection is ready + // This avoids port 80 conflicts with WiFiManager's captive portal + + + // 🔥 START RUNTIME VALIDATION: All subsystems are now initialized + // Begin extended runtime validation if we're in testing mode + if (firmwareValidator.isInTestingMode()) { + LOG_INFO(TAG, "🏃 Starting runtime validation - firmware will be tested for %lu seconds", + firmwareValidator.getValidationConfig().runtimeTimeoutMs / 1000); + firmwareValidator.startRuntimeValidation(); + } else { + LOG_INFO(TAG, "✅ Firmware already validated - normal operation mode"); + } + + + // ═══════════════════════════════════════════════════════════════════════════════ + // INITIALIZATION COMPLETE + // ═══════════════════════════════════════════════════════════════════════════════ + // ✅ All automatic task creation handled by individual components: + // • BellEngine creates high-priority timing task on Core 1 + // • Telemetry creates monitoring task for load tracking + // • Player creates duration timer for playback control + // • Communication creates MQTT task on Core 0 with PubSubClient + // • Networking creates connection management timers + // ✅ Bell configuration automatically loaded by ConfigManager + // ✅ System ready for MQTT commands, WebSocket connections, and UDP discovery +} + +// ███████████████████████████████████████████████████████████████████████████████████ +// █ MAIN LOOP █ +// ███████████████████████████████████████████████████████████████████████████████████ +// The main loop is intentionally kept minimal in this architecture. All critical +// functionality runs in dedicated FreeRTOS tasks for optimal performance and timing. +// This ensures the main loop doesn't interfere with precision bell timing. + +/** + * @brief Main execution loop - Minimal by design + * + * In the new modular architecture, all heavy lifting is done by dedicated tasks: + * • BellEngine: High-priority task on Core 1 for microsecond timing + * • Telemetry: Background monitoring task for system health + * • Player: Timer-based duration control for melody playback + * • Communication: MQTT task on Core 0 + Event-driven WebSocket + * • Networking: Automatic connection management + * + * The main loop only handles lightweight operations that don't require + * precise timing or could benefit from running on Core 0. + * + * @note This loop runs on Core 0 and should remain lightweight to avoid + * interfering with the precision timing on Core 1. + */ +void loop() +{ + // Feed watchdog only during firmware validation + if (firmwareValidator.isInTestingMode()) { + esp_task_wdt_reset(); + } else { + // Remove task from watchdog if validation completed + static bool taskRemoved = false; + if (!taskRemoved) { + esp_task_wdt_delete(NULL); // Remove current task + taskRemoved = true; + } + } + + // 🔥 CRITICAL: Clean up dead WebSocket connections every 2 seconds + // This prevents ghost connections from blocking new clients + static unsigned long lastWsCleanup = 0; + if (millis() - lastWsCleanup > 2000) { + ws.cleanupClients(); + lastWsCleanup = millis(); + } + + // Process UART command input from external devices (LCD panel, buttons) + communication.loop(); + + // 🔥 DEBUG: Log every 10 seconds to verify we're still running + static unsigned long lastLog = 0; + if (millis() - lastLog > 10000) { + LOG_DEBUG(TAG, "❤️ Loop alive | Free heap: %d bytes (%.1f KB) | Min free: %d | Largest block: %d", + ESP.getFreeHeap(), + ESP.getFreeHeap() / 1024.0, + ESP.getMinFreeHeap(), + ESP.getMaxAllocHeap()); + lastLog = millis(); + } + + // Keep the loop responsive but not busy + delay(100); // ⏱️ 100ms delay to prevent busy waiting +}