feat: Migrate firmware to PlatformIO (ESP32-S3, vesper-v1 env)
Replaces Arduino IDE with PlatformIO as the build system. Entry point moved from vesper.ino to src/main.cpp. All library dependencies are now declared in platformio.ini and downloaded automatically via lib_deps. Board: Kincony KC868-A6 (ESP32-S3, 4MB flash) → env:vesper-v1 Future variants Vesper+ and Vesper Pro are pre-configured but commented out. Compatibility fixes applied for this framework version: - Removed ETH.h dependency (Ethernet was disabled in v138) - Watchdog init updated to IDF v4 API (esp_task_wdt_init signature) - ETH.linkUp() check removed from OTAManager Also adds .pio/ to .gitignore and commits the manufacturing plan docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -7,4 +7,10 @@ vesper/CLAUDE.md
|
|||||||
vesper/flutter/
|
vesper/flutter/
|
||||||
vesper/docs_manual/
|
vesper/docs_manual/
|
||||||
Doxyfile
|
Doxyfile
|
||||||
vesper/.claude/
|
vesper/.claude/
|
||||||
|
|
||||||
|
# PlatformIO — build output and downloaded libraries (never commit these)
|
||||||
|
vesper/.pio/
|
||||||
|
|
||||||
|
# Claude Code memory/session files
|
||||||
|
.claude/
|
||||||
139
vesper/documentation/HEARTBEAT_FEATURE.md
Normal file
139
vesper/documentation/HEARTBEAT_FEATURE.md
Normal file
@@ -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
|
||||||
463
vesper/documentation/project-vesper-plan.md
Normal file
463
vesper/documentation/project-vesper-plan.md
Normal file
@@ -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"""
|
||||||
|
<h2>Your device has been registered</h2>
|
||||||
|
<p>Serial Number: <strong>{serial_number}</strong></p>
|
||||||
|
<p>Open the Vesper app and enter this serial number to get started.</p>
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] 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.
|
||||||
121
vesper/platformio.ini
Normal file
121
vesper/platformio.ini
Normal file
@@ -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}
|
||||||
536
vesper/src/main.cpp
Normal file
536
vesper/src/main.cpp
Normal file
@@ -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.h> // SD card file system operations
|
||||||
|
#include <FS.h> // File system base class
|
||||||
|
#include <ETH.h> // Ethernet connectivity (W5500 support)
|
||||||
|
#include <SPI.h> // SPI communication protocol
|
||||||
|
#include <Arduino.h> // Arduino core framework
|
||||||
|
#include <WiFi.h> // WiFi connectivity management
|
||||||
|
#include <HTTPClient.h> // HTTP client for OTA updates
|
||||||
|
#include <Update.h> // Firmware update utilities
|
||||||
|
#include <Wire.h> // I2C communication protocol
|
||||||
|
#include <esp_task_wdt.h> // Task watchdog timer
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// NETWORKING LIBRARIES - Advanced networking and communication
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
#include <WiFiManager.h> // WiFi configuration portal
|
||||||
|
#include <ESPAsyncWebServer.h> // Async web server for WebSocket support
|
||||||
|
#include <AsyncUDP.h> // UDP for discovery service
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// DATA PROCESSING LIBRARIES - JSON parsing and data structures
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
#include <ArduinoJson.h> // Efficient JSON processing
|
||||||
|
#include <string> // STL string support
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// HARDWARE LIBRARIES - Peripheral device control
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
#include <Adafruit_PCF8574.h> // I2C GPIO expander for relay control
|
||||||
|
#include <RTClib.h> // 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user