Compare commits
30 Commits
mqtt_ssl_f
...
ab1e4f2e6d
| Author | SHA1 | Date | |
|---|---|---|---|
| ab1e4f2e6d | |||
| 3877d27dae | |||
| fe6b1d871a | |||
| c656835d8e | |||
| 980de08584 | |||
| 9c314d88cf | |||
| 53c55d2726 | |||
| 094b1a9620 | |||
| 11b98166d1 | |||
| 7e279c6e45 | |||
| eb6e0f0e5c | |||
| 7adc1fec34 | |||
| 51b7722e1d | |||
| 9f07e9ea39 | |||
| 3d184773c1 | |||
| 953b5bd07d | |||
| db57b355b9 | |||
| 0f0b67cab9 | |||
| 8d397c6dd5 | |||
| 7d9bc42078 | |||
| b04590d270 | |||
| a7f1bd1667 | |||
| f286abb023 | |||
| c9f1e8e4ae | |||
| 06891e8d82 | |||
| d1835beff5 | |||
| 470d7bfacc | |||
| cc0bec97b5 | |||
| d6f105319d | |||
| fce760ebe0 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,3 +7,10 @@ vesper/CLAUDE.md
|
|||||||
vesper/flutter/
|
vesper/flutter/
|
||||||
vesper/docs_manual/
|
vesper/docs_manual/
|
||||||
Doxyfile
|
Doxyfile
|
||||||
|
vesper/.claude/
|
||||||
|
|
||||||
|
# PlatformIO — build output and downloaded libraries (never commit these)
|
||||||
|
vesper/.pio/
|
||||||
|
|
||||||
|
# Claude Code memory/session files
|
||||||
|
.claude/
|
||||||
139
vesper/HEARTBEAT_FEATURE.md
Normal file
139
vesper/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
|
||||||
828
vesper/documentation/API_Reference.md
Normal file
828
vesper/documentation/API_Reference.md
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
# 🔔 VESPER ESP32 Communication API Reference v3.0
|
||||||
|
|
||||||
|
> **Complete command reference for Vesper Bell Automation System with Grouped Commands**
|
||||||
|
> Version: 3.0 | Updated: 2025-09-15
|
||||||
|
> Supports: MQTT + WebSocket protocols with multi-client support and batch processing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Connection Protocols
|
||||||
|
- **MQTT**: `vesper/{device_id}/control` (commands) → `vesper/{device_id}/data` (responses)
|
||||||
|
- **WebSocket**: `ws://{esp_ip}/ws` (bidirectional)
|
||||||
|
- **UDP Discovery**: Broadcast on configured port for device discovery
|
||||||
|
- **UDP Port**: 32101
|
||||||
|
|
||||||
|
### WebSocket Client Identification
|
||||||
|
**Required for WebSocket clients to receive targeted messages:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "identify",
|
||||||
|
"device_type": "master" // or "secondary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "identify",
|
||||||
|
"payload": "Device identified as master"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Command Categories (NEW GROUPED ARCHITECTURE)
|
||||||
|
|
||||||
|
- [🖥️ System Commands](#️-system-commands)
|
||||||
|
- [🎵 Playback Control](#-playback-control)
|
||||||
|
- [📁 File Management](#-file-management)
|
||||||
|
- [🔧 Relay Setup](#-relay-setup)
|
||||||
|
- [🕐 Clock Setup](#-clock-setup)
|
||||||
|
- [📢 Information Messages](#-information-messages)
|
||||||
|
- [🌐 Network & Discovery](#-network--discovery)
|
||||||
|
- [🔄 Legacy Command Support](#-legacy-command-support)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ System Commands
|
||||||
|
|
||||||
|
### 🏓 Ping Test
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "ping"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "pong",
|
||||||
|
"payload": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 System Status
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "current_status",
|
||||||
|
"payload": {
|
||||||
|
"player_status": "playing",
|
||||||
|
"time_elapsed": 45230,
|
||||||
|
"projected_run_time": 34598,
|
||||||
|
"timestamp": 1699123456789
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 👤 Device Identification (WebSocket Only)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "identify",
|
||||||
|
"device_type": "master"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Restart Device
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "restart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "restart",
|
||||||
|
"payload": "Device will restart in 2 seconds"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Device will reboot after sending the response.
|
||||||
|
|
||||||
|
### 🔄 Force OTA Update
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "force_update",
|
||||||
|
"channel": "stable" // optional: "stable", "beta", or "emergency" (default: "stable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "force_update",
|
||||||
|
"payload": "Starting forced OTA update from channel: stable. Device may reboot."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (if player is active):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "force_update",
|
||||||
|
"payload": "Cannot update while playback is active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If update is successful, device will reboot automatically.
|
||||||
|
|
||||||
|
### 🔥 Custom Firmware Update
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "custom_update",
|
||||||
|
"firmware_url": "https://example.com/path/to/firmware.bin",
|
||||||
|
"checksum": "a1b2c3d4e5f6...", // optional: SHA256 checksum for verification
|
||||||
|
"file_size": 1234567, // optional: expected file size in bytes
|
||||||
|
"version": 145 // optional: firmware version number to save in NVS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "custom_update",
|
||||||
|
"payload": "Starting custom OTA update. Device may reboot."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "custom_update",
|
||||||
|
"payload": "Missing firmware_url parameter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "custom_update",
|
||||||
|
"payload": "Cannot update while playback is active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Download firmware from any URL (bypasses configured update servers)
|
||||||
|
- Optional SHA256 checksum verification
|
||||||
|
- Optional file size validation
|
||||||
|
- Optional version number to update NVS (prevents unwanted auto-downgrades)
|
||||||
|
- Automatically blocks updates during playback
|
||||||
|
- Device reboots on successful installation
|
||||||
|
|
||||||
|
**Version Parameter Behavior:**
|
||||||
|
- If `version` is provided (> 0): NVS firmware version will be updated to this value
|
||||||
|
- If `version` is omitted or 0: NVS firmware version remains unchanged
|
||||||
|
- **Important:** Without version parameter, future OTA checks may detect your custom firmware as "outdated" and trigger auto-updates/downgrades
|
||||||
|
|
||||||
|
**Note:** If update is successful, device will reboot automatically. Use with caution!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 Playback Control
|
||||||
|
|
||||||
|
### ▶️ Start Playback
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "playback",
|
||||||
|
"contents": {
|
||||||
|
"action": "play",
|
||||||
|
"name": "My Melody",
|
||||||
|
"uid": "01DegzV9FA8tYbQpkIHR",
|
||||||
|
"url": "https://example.com/melody.bin",
|
||||||
|
"speed": 500,
|
||||||
|
"note_assignments": [1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"segment_duration": 15000,
|
||||||
|
"pause_duration": 5000,
|
||||||
|
"total_duration": 60000,
|
||||||
|
"continuous_loop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⏹️ Stop Playback
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "playback",
|
||||||
|
"contents": {
|
||||||
|
"action": "stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 File Management
|
||||||
|
|
||||||
|
### 📋 List Available Melodies
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "file_manager",
|
||||||
|
"contents": {
|
||||||
|
"action": "list_melodies"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "list_melodies",
|
||||||
|
"payload": ["melody1.bin", "melody2.bin", "melody3.bin"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📥 Download Melody
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "file_manager",
|
||||||
|
"contents": {
|
||||||
|
"action": "download_melody",
|
||||||
|
"download_url": "https://example.com/melody.bin",
|
||||||
|
"melodys_uid": "01DegzV9FA8tYbQpkIHR",
|
||||||
|
"name": "Optional Display Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🗑️ Delete Melody
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "file_manager",
|
||||||
|
"contents": {
|
||||||
|
"action": "delete_melody",
|
||||||
|
"name": "01DegzV9FA8tYbQpkIHR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Relay Setup
|
||||||
|
|
||||||
|
### ⏱️ Set Relay Timers (Single Bell)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_timers",
|
||||||
|
"b1": 100,
|
||||||
|
"b2": 200,
|
||||||
|
"b3": 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⏱️ Set Relay Timers (Batch Mode)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_timers",
|
||||||
|
"timers": {
|
||||||
|
"b1": 100,
|
||||||
|
"b2": 200,
|
||||||
|
"b3": 150,
|
||||||
|
"b4": 300,
|
||||||
|
"b5": 250,
|
||||||
|
"b6": 180
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔌 Set Relay Outputs (Single Bell)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_outputs",
|
||||||
|
"b1": 1,
|
||||||
|
"b2": 2,
|
||||||
|
"b3": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔌 Set Relay Outputs (Batch Mode)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_outputs",
|
||||||
|
"outputs": {
|
||||||
|
"b1": 1,
|
||||||
|
"b2": 2,
|
||||||
|
"b3": 3,
|
||||||
|
"b4": 4,
|
||||||
|
"b5": 5,
|
||||||
|
"b6": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🕐 Clock Setup
|
||||||
|
|
||||||
|
### 🔌 Set Clock Outputs
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_outputs",
|
||||||
|
"c1": 1,
|
||||||
|
"c2": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⏰ Set Clock Timings
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_timings",
|
||||||
|
"pulseDuration": 5000,
|
||||||
|
"pauseDuration": 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔔 Set Clock Alerts
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_alerts",
|
||||||
|
"alertType": "HOURS",
|
||||||
|
"alertRingInterval": 1000,
|
||||||
|
"hourBell": 1,
|
||||||
|
"halfBell": 2,
|
||||||
|
"quarterBell": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Set Clock Backlight
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_backlight",
|
||||||
|
"enabled": true,
|
||||||
|
"output": 5,
|
||||||
|
"onTime": "18:00",
|
||||||
|
"offTime": "06:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔇 Set Clock Silence Periods
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_silence",
|
||||||
|
"daytime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "13:00",
|
||||||
|
"offTime": "15:00"
|
||||||
|
},
|
||||||
|
"nighttime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "22:00",
|
||||||
|
"offTime": "07:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 Batch Clock Setup (Multiple Settings at Once)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "batch_setup",
|
||||||
|
"outputs": {
|
||||||
|
"c1": 1,
|
||||||
|
"c2": 2
|
||||||
|
},
|
||||||
|
"timings": {
|
||||||
|
"pulseDuration": 5000,
|
||||||
|
"pauseDuration": 2000
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"alertType": "HOURS",
|
||||||
|
"hourBell": 1,
|
||||||
|
"halfBell": 2
|
||||||
|
},
|
||||||
|
"backlight": {
|
||||||
|
"enabled": true,
|
||||||
|
"output": 5,
|
||||||
|
"onTime": "18:00",
|
||||||
|
"offTime": "06:00"
|
||||||
|
},
|
||||||
|
"silence": {
|
||||||
|
"daytime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "13:00",
|
||||||
|
"offTime": "15:00"
|
||||||
|
},
|
||||||
|
"nighttime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "22:00",
|
||||||
|
"offTime": "07:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "clock_setup",
|
||||||
|
"payload": "Batch clock setup updated: 5 sections"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📢 Information Messages
|
||||||
|
|
||||||
|
> **Automatic status broadcasts sent to ALL clients**
|
||||||
|
> These messages are initiated by the ESP32 system and broadcast to all connected clients without being requested.
|
||||||
|
|
||||||
|
### 🎵 Playback Status Updates
|
||||||
|
|
||||||
|
**Sent automatically during playback state changes:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "INFO",
|
||||||
|
"type": "playback",
|
||||||
|
"payload": {
|
||||||
|
"action": "playing",
|
||||||
|
"time_elapsed": 125,
|
||||||
|
"projected_run_time": 5158
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Bell Overload Warnings
|
||||||
|
|
||||||
|
**Sent automatically when bell load monitoring detects issues:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "INFO",
|
||||||
|
"type": "bell_overload",
|
||||||
|
"payload": {
|
||||||
|
"bells": [1, 3, 5],
|
||||||
|
"loads": [85, 92, 78],
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Network & Discovery
|
||||||
|
|
||||||
|
### 🔍 UDP Discovery
|
||||||
|
|
||||||
|
**UDP Broadcast Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "discover",
|
||||||
|
"svc": "vesper"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**UDP Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "discover_reply",
|
||||||
|
"svc": "vesper",
|
||||||
|
"ver": 1,
|
||||||
|
"name": "Proj. Vesper v0.5",
|
||||||
|
"id": "ESP32_ABC123",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"ws": "ws://192.168.1.100/ws",
|
||||||
|
"port": 80,
|
||||||
|
"fw": "1.2.3",
|
||||||
|
"clients": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Legacy Command Support
|
||||||
|
|
||||||
|
**For backward compatibility, the following legacy commands are still supported:**
|
||||||
|
|
||||||
|
### Individual Commands (Legacy)
|
||||||
|
- `cmd: "ping"` → Use `system` with `action: "ping"`
|
||||||
|
- `cmd: "report_status"` → Use `system` with `action: "status"`
|
||||||
|
- `cmd: "identify"` → Use `system` with `action: "identify"`
|
||||||
|
- `cmd: "list_melodies"` → Use `file_manager` with `action: "list_melodies"`
|
||||||
|
- `cmd: "download_melody"` → Use `file_manager` with `action: "download_melody"`
|
||||||
|
- `cmd: "delete_melody"` → Use `file_manager` with `action: "delete_melody"`
|
||||||
|
- `cmd: "set_relay_timers"` → Use `relay_setup` with `action: "set_timers"`
|
||||||
|
- `cmd: "set_relay_outputs"` → Use `relay_setup` with `action: "set_outputs"`
|
||||||
|
- `cmd: "set_clock_outputs"` → Use `clock_setup` with `action: "set_outputs"`
|
||||||
|
- `cmd: "set_clock_timings"` → Use `clock_setup` with `action: "set_timings"`
|
||||||
|
- `cmd: "set_clock_alerts"` → Use `clock_setup` with `action: "set_alerts"`
|
||||||
|
- `cmd: "set_clock_backlight"` → Use `clock_setup` with `action: "set_backlight"`
|
||||||
|
- `cmd: "set_clock_silence"` → Use `clock_setup` with `action: "set_silence"`
|
||||||
|
|
||||||
|
**Legacy commands will continue to work but are deprecated. Please migrate to the new grouped command structure for optimal performance and features.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Key Advantages of Grouped Commands
|
||||||
|
|
||||||
|
### 🚀 **Batch Processing**
|
||||||
|
- Send multiple settings in a single command
|
||||||
|
- Reduce network overhead and latency
|
||||||
|
- Atomic operations ensure consistency
|
||||||
|
|
||||||
|
### 📊 **Better Organization**
|
||||||
|
- Logical grouping of related commands
|
||||||
|
- Cleaner API structure
|
||||||
|
- Easier to understand and maintain
|
||||||
|
|
||||||
|
### ⚡ **Enhanced Performance**
|
||||||
|
- Fewer round-trips for complex configurations
|
||||||
|
- Optimized ESP32 processing
|
||||||
|
- Improved user experience
|
||||||
|
|
||||||
|
### 🔄 **Backward Compatibility**
|
||||||
|
- Legacy commands still supported
|
||||||
|
- Gradual migration path
|
||||||
|
- No breaking changes for existing implementations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Integration Examples
|
||||||
|
|
||||||
|
### Dart/Flutter App Integration
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// New grouped command approach
|
||||||
|
await ClockSetup.batchClockSetup(
|
||||||
|
c1Output: 1,
|
||||||
|
c2Output: 2,
|
||||||
|
pulseDuration: 5000,
|
||||||
|
pauseDuration: 2000,
|
||||||
|
alertType: 'HOURS',
|
||||||
|
hourBell: 1,
|
||||||
|
backlightEnabled: true,
|
||||||
|
backlightOutput: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Batch relay setup
|
||||||
|
await RelaySetup.setBatchRelayOutputs({
|
||||||
|
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Individual settings still work
|
||||||
|
await ClockSetup.setOddClockOutput(1);
|
||||||
|
await ClockSetup.setEvenClockOutput(2);
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/WebSocket Integration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Batch clock configuration
|
||||||
|
const clockConfig = {
|
||||||
|
cmd: "clock_setup",
|
||||||
|
contents: {
|
||||||
|
action: "batch_setup",
|
||||||
|
outputs: { c1: 1, c2: 2 },
|
||||||
|
timings: { pulseDuration: 5000, pauseDuration: 2000 },
|
||||||
|
alerts: { alertType: "HOURS", hourBell: 1 },
|
||||||
|
backlight: { enabled: true, output: 5 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webSocket.send(JSON.stringify(clockConfig));
|
||||||
|
|
||||||
|
// Batch relay configuration
|
||||||
|
const relayConfig = {
|
||||||
|
cmd: "relay_setup",
|
||||||
|
contents: {
|
||||||
|
action: "set_outputs",
|
||||||
|
outputs: {
|
||||||
|
b1: 1, b2: 2, b3: 3, b4: 4, b5: 5, b6: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webSocket.send(JSON.stringify(relayConfig));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Error Handling
|
||||||
|
|
||||||
|
### Common Error Types
|
||||||
|
|
||||||
|
**Missing Action Parameter:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "relay_setup",
|
||||||
|
"payload": "Missing action parameter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unknown Action:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "clock_setup",
|
||||||
|
"payload": "Unknown action: invalid_action"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Batch Processing Errors:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "relay_setup",
|
||||||
|
"payload": "No valid relay timers found in batch"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success with Count:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "relay_setup",
|
||||||
|
"payload": "Batch relay outputs updated: 6 bells"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 Message Routing
|
||||||
|
|
||||||
|
### Response Routing Rules
|
||||||
|
|
||||||
|
1. **Command Responses**: Sent only to the originating client/protocol
|
||||||
|
- MQTT command → MQTT response
|
||||||
|
- WebSocket client #3 → WebSocket client #3 only
|
||||||
|
|
||||||
|
2. **Status Broadcasts**: Sent to ALL connected clients
|
||||||
|
- All WebSocket clients receive the message
|
||||||
|
- MQTT subscribers receive the message
|
||||||
|
- Used for system notifications and status updates
|
||||||
|
|
||||||
|
3. **Targeted Messages**: Based on device type
|
||||||
|
- `broadcastToMasterClients()`: Only master devices
|
||||||
|
- `broadcastToSecondaryClients()`: Only secondary devices
|
||||||
|
- `broadcastToAllWebSocketClients()`: All WebSocket clients
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Performance Optimizations
|
||||||
|
|
||||||
|
### Batch Command Benefits
|
||||||
|
|
||||||
|
**Before (Legacy - 6 separate commands):**
|
||||||
|
```javascript
|
||||||
|
// 6 separate network calls
|
||||||
|
await setRelayOutput(1, 1);
|
||||||
|
await setRelayOutput(2, 2);
|
||||||
|
await setRelayOutput(3, 3);
|
||||||
|
await setRelayOutput(4, 4);
|
||||||
|
await setRelayOutput(5, 5);
|
||||||
|
await setRelayOutput(6, 6);
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Grouped - 1 batch command):**
|
||||||
|
```javascript
|
||||||
|
// 1 network call for all settings
|
||||||
|
await setBatchRelayOutputs({
|
||||||
|
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Metrics
|
||||||
|
- **Network Calls**: Reduced by up to 85%
|
||||||
|
- **Configuration Time**: 3-5x faster
|
||||||
|
- **ESP32 Processing**: More efficient batch updates
|
||||||
|
- **Error Handling**: Atomic operations ensure consistency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Quick Reference
|
||||||
|
|
||||||
|
### Command Groups
|
||||||
|
| Group | Purpose | Batch Support |
|
||||||
|
|-------|---------|---------------|
|
||||||
|
| `system` | Device management, ping, status | No |
|
||||||
|
| `playback` | Music playback control | No |
|
||||||
|
| `file_manager` | Melody file operations | No |
|
||||||
|
| `relay_setup` | Bell configuration | ✅ Yes |
|
||||||
|
| `clock_setup` | Clock mechanism setup | ✅ Yes |
|
||||||
|
|
||||||
|
### Actions by Group
|
||||||
|
|
||||||
|
**System:** `ping`, `status`, `identify`, `restart`, `force_update`, `custom_update`
|
||||||
|
|
||||||
|
**Playback:** `play`, `stop`
|
||||||
|
|
||||||
|
**File Manager:** `list_melodies`, `download_melody`, `delete_melody`
|
||||||
|
|
||||||
|
**Relay Setup:** `set_timers`, `set_outputs`
|
||||||
|
|
||||||
|
**Clock Setup:** `set_outputs`, `set_timings`, `set_alerts`, `set_backlight`, `set_silence`, `batch_setup`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Happy Bell Automation with Grouped Commands! 🔔*
|
||||||
11
vesper/documentation/BuildProcedure.md
Normal file
11
vesper/documentation/BuildProcedure.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Device Setup Process:
|
||||||
|
|
||||||
|
1. Build device with peripherals.
|
||||||
|
2. Flash Base Firmware
|
||||||
|
3. Set Device Credentials (UID/HWID/Rev) via WebServer on device
|
||||||
|
4. Add Device to BellCloud
|
||||||
|
5. Add Device Credentials to Mosquitto
|
||||||
|
6. Reboot Device to Pull Stable Production Firmware
|
||||||
|
7. Sell the device.
|
||||||
|
- User will bind it to their account
|
||||||
|
- Factory can install App and bind user for convenience
|
||||||
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
|
||||||
0
vesper/documentation/Roadmap.md
Normal file
0
vesper/documentation/Roadmap.md
Normal file
43
vesper/documentation/SentDevices
Normal file
43
vesper/documentation/SentDevices
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
STAMNA:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PV25L22BP01R01
|
||||||
|
Bell Plus
|
||||||
|
HW: 1.0
|
||||||
|
|
||||||
|
u6545309759@gmail.com
|
||||||
|
bellsystems2025
|
||||||
|
aCx!97IEfTiA073^#*Jj
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
XRISTIANIKH_ELPIS:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PV26B02BP01R01
|
||||||
|
Bell Plus
|
||||||
|
HW: 1.0
|
||||||
|
|
||||||
|
mail: christianikielpis@gmail.com
|
||||||
|
pass: bellsystems2025
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
GREVENA:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PA26B06AM01R01
|
||||||
|
Agnus Mini
|
||||||
|
HW: 1.0
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
LARISA:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PA26B06AM01R02
|
||||||
|
Agnus Mini
|
||||||
|
HW: 1.0
|
||||||
42
vesper/documentation/common_commands.txt
Normal file
42
vesper/documentation/common_commands.txt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
COMMANDS:
|
||||||
|
|
||||||
|
|
||||||
|
PV26A28BC01R01
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"force_update",
|
||||||
|
"channel":"beta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"set_mqtt_log_level",
|
||||||
|
"level":3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"restart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system_info",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"get_full_settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
79
vesper/documentation/features.info
Normal file
79
vesper/documentation/features.info
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
Features:
|
||||||
|
|
||||||
|
// Board Naming Schema:
|
||||||
|
|
||||||
|
eg. PV25K07BC01R01
|
||||||
|
|
||||||
|
PV 25 K 07 BC 01 R 01
|
||||||
|
PV [Y] [M] [D] [BT] [RV] R [BC]
|
||||||
|
|
||||||
|
// SERBIA_OLD: PV25K07BC01R01
|
||||||
|
// SERBIA_NEW: PV26A28BC01R01
|
||||||
|
|
||||||
|
// XRISTIANIKH_ELPIS: PV26B02BP01R01
|
||||||
|
|
||||||
|
|
||||||
|
PV25L22BP01R01
|
||||||
|
|
||||||
|
|
||||||
|
Y: (Year) 2 Digit Year. eg 25 for 2025
|
||||||
|
M: (Month) 1 Letter as Coded Month. eg B for February
|
||||||
|
D: (Day) 2 Digit Date. eg 17 for 17th of the Month
|
||||||
|
BT: (Board Type) 2 letter/digit board Type (custom) eg BC for BellCore
|
||||||
|
RV: (Revision) 2 letter/digit board revision code
|
||||||
|
R: Now, just an R for "Revision" but can change later
|
||||||
|
BC: (Batch Code) 2 digit SerialNumber starting from 01
|
||||||
|
|
||||||
|
|
||||||
|
// mqtt topics:
|
||||||
|
|
||||||
|
vesper/<DEVID>/data // Data sent from the controller
|
||||||
|
vesper/<DEVID>/control // Commands sent to the controller
|
||||||
|
vesper/<DEVID>/kiosk/event // Kiosk Mode Events
|
||||||
|
vesper/<DEVID>/kiosk/info // Kiosk Mode General Info
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- WiFi Manager (captive portal with hotspot)
|
||||||
|
- MQTT Support (Subscribing and Publishing)
|
||||||
|
- WebSocket Support (Sending and Receiving)
|
||||||
|
- JSON Format Messaging (both MQTT and WS)
|
||||||
|
- SD Card Handling and File Ops
|
||||||
|
- Stand-alone Player/BellEngine Classes, with functions to Play/Pause/Stop etc
|
||||||
|
- NoteAssignments - Effectively mapping Notes to Bells
|
||||||
|
- Independent SubSystems for all Core Functions (Networking/Comms/Scheduling/Logging/etc)
|
||||||
|
- Custom Relay Output Maps and Timings (saved on SD)
|
||||||
|
- Timekeeper with RTC/Clock/Alerts/Scheduling features
|
||||||
|
- OTA Update Functionality with Versioning/Rollbacks/Checksum/Firmware Validation/NTP Sync
|
||||||
|
- Global logger with Mode Selection (None, Error, Warning, Info, Debug, Verbose)
|
||||||
|
- UDP Listener for Auto Device Discovery
|
||||||
|
- Datalogging and Statistics:
|
||||||
|
- Counter for each bell (counts total times the bell ringed)
|
||||||
|
- Counter per bell, beats/minute for reliability and thermal protection. Warranty Void scenario.
|
||||||
|
- Ability to change Log levels (in-app)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ToDo Features:
|
||||||
|
|
||||||
|
- (optional) Add Bluetooth support
|
||||||
|
- (optional) Add WiFi Direct AP Support
|
||||||
|
- (optional) Add PCB Temperature Sensor Support
|
||||||
|
|
||||||
|
- (critical) Counters and Statistics:
|
||||||
|
- Counter per playback, to figure out which melody is the most played.
|
||||||
|
This can be implemented on the App itself. Doesn't need to be on the Device.
|
||||||
|
|
||||||
|
- Create a "humanizer" mode that randomizes delays on playback to simulate human ringing.
|
||||||
|
|
||||||
|
|
||||||
|
ToDo Fixes:
|
||||||
|
|
||||||
|
- (small significance) Fix each Log's level Correctly + Fix Log Syntax where needed
|
||||||
|
- (medium significance) BellGuard: Make the buttons functional.
|
||||||
|
|
||||||
|
- Fix IP Settings not applying. More Specifically, Variables inside the Components take long to update. Either Ditch the components, or find another way.
|
||||||
|
- On Very fast playback speeds and small programs that will run for less than a second or so, STOP isn't sent properly. Player keeps indicating "playing".
|
||||||
|
- When a new user is created, set default PINs for both Quick Settings, and Settings.
|
||||||
|
|
||||||
|
|
||||||
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}
|
||||||
@@ -22,6 +22,8 @@
|
|||||||
// DEPENDENCY INCLUDES - Required system components
|
// DEPENDENCY INCLUDES - Required system components
|
||||||
// ═════════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
#include "BellEngine.hpp" // Header file with class definition
|
#include "BellEngine.hpp" // Header file with class definition
|
||||||
|
|
||||||
|
#define TAG "BellEngine"
|
||||||
#include "../Player/Player.hpp" // Melody playback controller
|
#include "../Player/Player.hpp" // Melody playback controller
|
||||||
#include "../ConfigManager/ConfigManager.hpp" // Configuration and settings
|
#include "../ConfigManager/ConfigManager.hpp" // Configuration and settings
|
||||||
#include "../Telemetry/Telemetry.hpp" // System monitoring and analytics
|
#include "../Telemetry/Telemetry.hpp" // System monitoring and analytics
|
||||||
@@ -74,7 +76,7 @@ BellEngine::~BellEngine() {
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
void BellEngine::begin() {
|
void BellEngine::begin() {
|
||||||
LOG_DEBUG("Initializing BellEngine with high-precision timing");
|
LOG_DEBUG(TAG, "Initializing BellEngine...");
|
||||||
|
|
||||||
// Create engine task with HIGHEST priority on dedicated Core 1
|
// Create engine task with HIGHEST priority on dedicated Core 1
|
||||||
// This ensures maximum performance and timing precision
|
// This ensures maximum performance and timing precision
|
||||||
@@ -88,7 +90,7 @@ void BellEngine::begin() {
|
|||||||
1 // 💻 Pin to Core 1 (dedicated)
|
1 // 💻 Pin to Core 1 (dedicated)
|
||||||
);
|
);
|
||||||
|
|
||||||
LOG_INFO("BellEngine initialized - Ready for MAXIMUM PRECISION! 🎯");
|
LOG_INFO(TAG, "BellEngine initialized !");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -96,7 +98,7 @@ void BellEngine::begin() {
|
|||||||
*/
|
*/
|
||||||
void BellEngine::setCommunicationManager(CommunicationRouter* commManager) {
|
void BellEngine::setCommunicationManager(CommunicationRouter* commManager) {
|
||||||
_communicationManager = commManager;
|
_communicationManager = commManager;
|
||||||
LOG_DEBUG("BellEngine: Communication manager %s",
|
LOG_DEBUG(TAG, "BellEngine: Communication manager %s",
|
||||||
commManager ? "connected" : "disconnected");
|
commManager ? "connected" : "disconnected");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,22 +118,22 @@ void BellEngine::setCommunicationManager(CommunicationRouter* commManager) {
|
|||||||
void BellEngine::start() {
|
void BellEngine::start() {
|
||||||
// Validate that melody data is ready before starting
|
// Validate that melody data is ready before starting
|
||||||
if (!_melodyDataReady.load()) {
|
if (!_melodyDataReady.load()) {
|
||||||
LOG_ERROR("Cannot start BellEngine: No melody data loaded");
|
LOG_ERROR(TAG, "Cannot start BellEngine: No melody data loaded");
|
||||||
return; // ⛔ Early exit if no melody data
|
return; // ⛔ Early exit if no melody data
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("🚀 BellEngine IGNITION - Starting precision playback");
|
LOG_INFO(TAG, "🚀 BellEngine Ignition - Starting precision playback");
|
||||||
_emergencyStop.store(false); // ✅ Clear any emergency stop state
|
_emergencyStop.store(false); // ✅ Clear any emergency stop state
|
||||||
_engineRunning.store(true); // ✅ Activate the engine atomically
|
_engineRunning.store(true); // ✅ Activate the engine atomically
|
||||||
}
|
}
|
||||||
|
|
||||||
void BellEngine::stop() {
|
void BellEngine::stop() {
|
||||||
LOG_INFO("BellEngine stopping gracefully");
|
LOG_INFO(TAG, "BellEngine - Stopping Gracefully");
|
||||||
_engineRunning.store(false);
|
_engineRunning.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BellEngine::emergencyStop() {
|
void BellEngine::emergencyStop() {
|
||||||
LOG_INFO("🛑 EMERGENCY STOP ACTIVATED");
|
LOG_INFO(TAG, "BellEngine - 🛑 Forcing Stop Immediately");
|
||||||
_emergencyStop.store(true);
|
_emergencyStop.store(true);
|
||||||
_engineRunning.store(false);
|
_engineRunning.store(false);
|
||||||
emergencyShutdown();
|
emergencyShutdown();
|
||||||
@@ -142,7 +144,7 @@ void BellEngine::setMelodyData(const std::vector<uint16_t>& melodySteps) {
|
|||||||
_melodySteps = melodySteps;
|
_melodySteps = melodySteps;
|
||||||
_melodyDataReady.store(true);
|
_melodyDataReady.store(true);
|
||||||
portEXIT_CRITICAL(&_melodyMutex);
|
portEXIT_CRITICAL(&_melodyMutex);
|
||||||
LOG_DEBUG("BellEngine loaded melody: %d steps", melodySteps.size());
|
LOG_DEBUG(TAG, "BellEngine - Loaded melody: %d steps", melodySteps.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void BellEngine::clearMelodyData() {
|
void BellEngine::clearMelodyData() {
|
||||||
@@ -150,7 +152,7 @@ void BellEngine::clearMelodyData() {
|
|||||||
_melodySteps.clear();
|
_melodySteps.clear();
|
||||||
_melodyDataReady.store(false);
|
_melodyDataReady.store(false);
|
||||||
portEXIT_CRITICAL(&_melodyMutex);
|
portEXIT_CRITICAL(&_melodyMutex);
|
||||||
LOG_DEBUG("BellEngine melody data cleared");
|
LOG_DEBUG(TAG, "BellEngine - Melody data cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================== CRITICAL TIMING SECTION ==================
|
// ================== CRITICAL TIMING SECTION ==================
|
||||||
@@ -158,7 +160,7 @@ void BellEngine::clearMelodyData() {
|
|||||||
|
|
||||||
void BellEngine::engineTask(void* parameter) {
|
void BellEngine::engineTask(void* parameter) {
|
||||||
BellEngine* engine = static_cast<BellEngine*>(parameter);
|
BellEngine* engine = static_cast<BellEngine*>(parameter);
|
||||||
LOG_DEBUG("🔥 BellEngine task started on Core %d with MAXIMUM priority", xPortGetCoreID());
|
LOG_DEBUG(TAG, "BellEngine - 🔥 Engine task started on Core %d with MAXIMUM priority", xPortGetCoreID());
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (engine->_engineRunning.load() && !engine->_emergencyStop.load()) {
|
if (engine->_engineRunning.load() && !engine->_emergencyStop.load()) {
|
||||||
@@ -186,7 +188,7 @@ void BellEngine::engineLoop() {
|
|||||||
|
|
||||||
// Pause handling AFTER complete loop - never interrupt mid-melody!
|
// Pause handling AFTER complete loop - never interrupt mid-melody!
|
||||||
while (_player.isPaused && _player.isPlaying && !_player.hardStop) {
|
while (_player.isPaused && _player.isPlaying && !_player.hardStop) {
|
||||||
LOG_DEBUG("⏸️ Pausing between melody loops");
|
LOG_VERBOSE(TAG, "BellEngine - ⏸️ Pausing between melody loops");
|
||||||
vTaskDelay(pdMS_TO_TICKS(10)); // Wait during pause
|
vTaskDelay(pdMS_TO_TICKS(10)); // Wait during pause
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,24 +209,32 @@ void BellEngine::playbackLoop() {
|
|||||||
portEXIT_CRITICAL(&_melodyMutex);
|
portEXIT_CRITICAL(&_melodyMutex);
|
||||||
|
|
||||||
if (melodySteps.empty()) {
|
if (melodySteps.empty()) {
|
||||||
LOG_ERROR("Empty melody in playback loop!");
|
LOG_ERROR(TAG, "BellEngine - ❌ Empty melody in playback loop!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("🎵 Starting melody loop (%d steps)", melodySteps.size());
|
LOG_DEBUG(TAG, "BellEngine - 🎵 Starting melody loop (%d steps)", melodySteps.size());
|
||||||
|
|
||||||
// CRITICAL TIMING LOOP - Complete the entire melody without interruption
|
// CRITICAL TIMING LOOP - Complete the entire melody without interruption
|
||||||
for (uint16_t note : melodySteps) {
|
for (uint16_t note : melodySteps) {
|
||||||
// Emergency exit check (only emergency stops can interrupt mid-loop)
|
// Emergency exit check (only emergency stops can interrupt mid-loop)
|
||||||
if (_emergencyStop.load() || _player.hardStop) {
|
if (_emergencyStop.load() || _player.hardStop) {
|
||||||
LOG_DEBUG("Emergency exit from playback loop");
|
LOG_DEBUG(TAG, "BellEngine - Emergency exit from playback loop");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate note with MAXIMUM PRECISION
|
// Activate note with MAXIMUM PRECISION
|
||||||
activateNote(note);
|
activateNote(note);
|
||||||
|
|
||||||
// Precise timing delay
|
// Precise timing delay - validate speed to prevent division by zero
|
||||||
|
// I THINK this should be moved outside the Bell Engine
|
||||||
|
if (_player.speed == 0) {
|
||||||
|
LOG_ERROR(TAG, "BellEngine - ❌ Invalid Speed (0) detected, stopping playback");
|
||||||
|
_player.hardStop = true;
|
||||||
|
_engineRunning.store(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t tempoMicros = _player.speed * 1000; // Convert ms to microseconds
|
uint32_t tempoMicros = _player.speed * 1000; // Convert ms to microseconds
|
||||||
preciseDelay(tempoMicros);
|
preciseDelay(tempoMicros);
|
||||||
}
|
}
|
||||||
@@ -232,7 +242,11 @@ void BellEngine::playbackLoop() {
|
|||||||
// Mark segment completion and notify Player
|
// Mark segment completion and notify Player
|
||||||
_player.segmentCmpltTime = millis();
|
_player.segmentCmpltTime = millis();
|
||||||
_player.onMelodyLoopCompleted(); // 🔥 Notify Player that melody actually finished!
|
_player.onMelodyLoopCompleted(); // 🔥 Notify Player that melody actually finished!
|
||||||
LOG_DEBUG("🎵 Melody loop completed with PRECISION");
|
if ((_player.continuous_loop && _player.segment_duration == 0) || _player.total_duration == 0) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(500)); //Give Player time to pause/stop
|
||||||
|
LOG_VERBOSE(TAG, "BellEngine - Loop completed in SINGLE Mode - waiting for Player to handle pause/stop");
|
||||||
|
}
|
||||||
|
LOG_DEBUG(TAG, "BellEngine - 🎵 Melody loop completed with PRECISION");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,26 +270,26 @@ void BellEngine::activateNote(uint16_t note) {
|
|||||||
|
|
||||||
// Additional safety check to prevent underflow crashes
|
// Additional safety check to prevent underflow crashes
|
||||||
if (bellIndex >= 255) {
|
if (bellIndex >= 255) {
|
||||||
LOG_ERROR("🚨 UNDERFLOW ERROR: bellIndex underflow for noteIndex %d", noteIndex);
|
LOG_ERROR(TAG, "BellEngine - 🚨 UNDERFLOW ERROR: bellIndex underflow for noteIndex %d", noteIndex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bounds check (CRITICAL SAFETY)
|
// Bounds check (CRITICAL SAFETY)
|
||||||
if (bellIndex >= 16) {
|
if (bellIndex >= 16) {
|
||||||
LOG_ERROR("🚨 BOUNDS ERROR: bellIndex %d >= 16", bellIndex);
|
LOG_ERROR(TAG, "BellEngine - 🚨 BOUNDS ERROR: bellIndex %d >= 16", bellIndex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for duplicate bell firing in this note
|
// Check for duplicate bell firing in this note
|
||||||
if (bellFired[bellIndex]) {
|
if (bellFired[bellIndex]) {
|
||||||
LOG_DEBUG("⚠️ DUPLICATE BELL: Skipping duplicate firing of bell %d for note %d", bellIndex, noteIndex);
|
LOG_DEBUG(TAG, "BellEngine - ⚠️ DUPLICATE BELL: Skipping duplicate firing of bell %d for note %d", bellIndex, noteIndex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bell is configured (OutputManager will validate this)
|
// Check if bell is configured (OutputManager will validate this)
|
||||||
uint8_t physicalOutput = _outputManager.getPhysicalOutput(bellIndex);
|
uint8_t physicalOutput = _outputManager.getPhysicalOutput(bellIndex);
|
||||||
if (physicalOutput == 255) {
|
if (physicalOutput == 255) {
|
||||||
LOG_DEBUG("⚠️ UNCONFIGURED: Bell %d not configured, skipping", bellIndex);
|
LOG_DEBUG(TAG, "BellEngine - ⚠️ UNCONFIGURED: Bell %d not configured, skipping", bellIndex);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +300,7 @@ void BellEngine::activateNote(uint16_t note) {
|
|||||||
uint16_t durationMs = _configManager.getBellDuration(bellIndex);
|
uint16_t durationMs = _configManager.getBellDuration(bellIndex);
|
||||||
|
|
||||||
// Add to batch firing list
|
// Add to batch firing list
|
||||||
bellDurations.push_back({bellIndex, durationMs});
|
bellDurations.push_back({physicalOutput, durationMs});
|
||||||
|
|
||||||
// Add to notification list (convert to 1-indexed for display)
|
// Add to notification list (convert to 1-indexed for display)
|
||||||
firedBellIndices.push_back(bellIndex + 1);
|
firedBellIndices.push_back(bellIndex + 1);
|
||||||
@@ -294,17 +308,18 @@ void BellEngine::activateNote(uint16_t note) {
|
|||||||
// Record telemetry
|
// Record telemetry
|
||||||
_telemetry.recordBellStrike(bellIndex);
|
_telemetry.recordBellStrike(bellIndex);
|
||||||
|
|
||||||
LOG_VERBOSE("🔨 STRIKE! Note:%d → Bell:%d for %dms", noteIndex, bellIndex, durationMs);
|
LOG_VERBOSE(TAG, "BellEngine - 🔨 STRIKE! Note:%d → Bell:%d for %dms", noteIndex, bellIndex, durationMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚀 FIRE ALL BELLS SIMULTANEOUSLY!
|
// 🚀 FIRE ALL BELLS SIMULTANEOUSLY!
|
||||||
if (!bellDurations.empty()) {
|
if (!bellDurations.empty()) {
|
||||||
_outputManager.fireOutputsBatchForDuration(bellDurations);
|
_outputManager.fireOutputsBatchForDuration(bellDurations);
|
||||||
LOG_VERBOSE("🔥🔥 BATCH FIRED %d bells SIMULTANEOUSLY!", bellDurations.size());
|
LOG_VERBOSE(TAG, "BellEngine - 🔥 Batch Fired %d bells Simultaneously !", bellDurations.size());
|
||||||
|
|
||||||
// 🔔 NOTIFY WEBSOCKET CLIENTS OF BELL DINGS!
|
// 🔔 NOTIFY WEBSOCKET CLIENTS OF BELL DINGS!
|
||||||
notifyBellsFired(firedBellIndices);
|
// * deactivated currently, since unstable and causes performance issues *
|
||||||
|
// notifyBellsFired(firedBellIndices);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,7 +341,7 @@ void BellEngine::preciseDelay(uint32_t microseconds) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BellEngine::emergencyShutdown() {
|
void BellEngine::emergencyShutdown() {
|
||||||
LOG_INFO("🚨 EMERGENCY SHUTDOWN - Using OutputManager");
|
LOG_INFO(TAG, "BellEngine - 🚨 Emergency Shutdown - Notifying OutputManager");
|
||||||
_outputManager.emergencyShutdown();
|
_outputManager.emergencyShutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,10 +366,10 @@ void BellEngine::notifyBellsFired(const std::vector<uint8_t>& bellIndices) {
|
|||||||
// Send notification to WebSocket clients only (not MQTT)
|
// Send notification to WebSocket clients only (not MQTT)
|
||||||
_communicationManager->broadcastToAllWebSocketClients(dingMsg);
|
_communicationManager->broadcastToAllWebSocketClients(dingMsg);
|
||||||
|
|
||||||
LOG_DEBUG("🔔 DING notification sent for %d bells", bellIndices.size());
|
LOG_DEBUG(TAG, "BellEngine - 🔔 DING notification sent for %d bells", bellIndices.size());
|
||||||
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
LOG_ERROR("Failed to send ding notification");
|
LOG_WARNING(TAG, "BellEngine - ❌ Failed to send ding notification");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,20 +380,20 @@ void BellEngine::notifyBellsFired(const std::vector<uint8_t>& bellIndices) {
|
|||||||
bool BellEngine::isHealthy() const {
|
bool BellEngine::isHealthy() const {
|
||||||
// Check if engine task is created and running
|
// Check if engine task is created and running
|
||||||
if (_engineTaskHandle == NULL) {
|
if (_engineTaskHandle == NULL) {
|
||||||
LOG_DEBUG("BellEngine: Unhealthy - Task not created");
|
LOG_DEBUG(TAG, "BellEngine: Unhealthy - Task not created");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if task is still alive
|
// Check if task is still alive
|
||||||
eTaskState taskState = eTaskGetState(_engineTaskHandle);
|
eTaskState taskState = eTaskGetState(_engineTaskHandle);
|
||||||
if (taskState == eDeleted || taskState == eInvalid) {
|
if (taskState == eDeleted || taskState == eInvalid) {
|
||||||
LOG_DEBUG("BellEngine: Unhealthy - Task deleted or invalid");
|
LOG_DEBUG(TAG, "BellEngine: Unhealthy - Task deleted or invalid");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if OutputManager is properly connected and healthy
|
// Check if OutputManager is properly connected and healthy
|
||||||
if (!_outputManager.isInitialized()) {
|
if (!_outputManager.isInitialized()) {
|
||||||
LOG_DEBUG("BellEngine: Unhealthy - OutputManager not initialized");
|
LOG_DEBUG(TAG, "BellEngine: Unhealthy - OutputManager not initialized");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
579
vesper/src/BuiltInMelodies/BuiltInMelodies.hpp
Normal file
579
vesper/src/BuiltInMelodies/BuiltInMelodies.hpp
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* BUILTINMELODIES.HPP - Firmware-Baked Melody Library
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 🎵 BUILT-IN MELODY LIBRARY FOR VESPER 🎵
|
||||||
|
*
|
||||||
|
* This file contains melodies baked directly into the firmware, eliminating
|
||||||
|
* the need for SD card downloads. Each melody is stored in PROGMEM to save RAM.
|
||||||
|
*
|
||||||
|
* 🏗️ ARCHITECTURE:
|
||||||
|
* • Melodies stored in PROGMEM (Flash memory, not RAM)
|
||||||
|
* • Each melody step is 2 bytes (uint16_t bitmask)
|
||||||
|
* • Metadata includes name, UID, default speed
|
||||||
|
* • Easy to add new melodies
|
||||||
|
*
|
||||||
|
* 📦 STORAGE EFFICIENCY:
|
||||||
|
* • Small melodies (~30 steps = 60 bytes)
|
||||||
|
* • Large melodies (~200 steps = 400 bytes)
|
||||||
|
* • 40 melodies average = ~6-10KB total (Flash, not RAM!)
|
||||||
|
*
|
||||||
|
* 🎶 MELODY FORMAT:
|
||||||
|
* Each uint16_t is a bitmask:
|
||||||
|
* - Bit 0-15: Which bells/notes to activate
|
||||||
|
* - Example: 0x0001 = Bell 0, 0x0003 = Bells 0+1, 0x8000 = Bell 15
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-12-28
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <pgmspace.h>
|
||||||
|
|
||||||
|
namespace BuiltInMelodies {
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// MELODY METADATA STRUCTURE
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
struct MelodyInfo {
|
||||||
|
const char* name; // Display name
|
||||||
|
const char* uid; // Unique identifier
|
||||||
|
const uint16_t* data; // Pointer to melody data in PROGMEM
|
||||||
|
uint16_t stepCount; // Number of steps
|
||||||
|
};
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// BuiltIn Melodies // More can be added here
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// 1 Bell Test Melody
|
||||||
|
const uint16_t PROGMEM builtin_1bell_test[] = {
|
||||||
|
0x0001, 0x0000, 0x0001, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Doxology Traditional
|
||||||
|
const uint16_t PROGMEM builtin_doxology_traditional[] = {
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0008, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008, 0x0000, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Doxology Alternative
|
||||||
|
const uint16_t PROGMEM builtin_doxology_alternative[] = {
|
||||||
|
0x0001, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0000,
|
||||||
|
0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0002, 0x0001,
|
||||||
|
0x0002, 0x0004, 0x0000, 0x0018, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Doxology Festive
|
||||||
|
const uint16_t PROGMEM builtin_doxology_festive[] = {
|
||||||
|
0x0002, 0x0004, 0x0009, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004,
|
||||||
|
0x0002, 0x0004, 0x0021, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vesper Traditional
|
||||||
|
const uint16_t PROGMEM builtin_vesper_traditional[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0000, 0x0001, 0x0002, 0x0004, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vesper Alternative
|
||||||
|
const uint16_t PROGMEM builtin_vesper_alternative[] = {
|
||||||
|
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0004, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0004, 0x0000, 0x0000,
|
||||||
|
0x0001, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Catehetical
|
||||||
|
const uint16_t PROGMEM builtin_catehetical[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0008, 0x0010
|
||||||
|
};
|
||||||
|
|
||||||
|
// Orthros Traditional
|
||||||
|
const uint16_t PROGMEM builtin_orthros_traditional[] = {
|
||||||
|
0x0001, 0x0000, 0x0002, 0x0000, 0x0004, 0x0008, 0x0000, 0x0010,
|
||||||
|
0x0000, 0x0020, 0x0000, 0x0040, 0x0080, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Orthros Alternative
|
||||||
|
const uint16_t PROGMEM builtin_orthros_alternative[] = {
|
||||||
|
0x0001, 0x0000, 0x0002, 0x0001, 0x0000, 0x0002, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0004, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mournfull Toll
|
||||||
|
const uint16_t PROGMEM builtin_mournfull_toll[] = {
|
||||||
|
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mournfull Toll Alternative
|
||||||
|
const uint16_t PROGMEM builtin_mournfull_toll_alternative[] = {
|
||||||
|
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
|
||||||
|
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0004,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0002, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0008, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0008, 0x0008, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mournfull Toll Meg Par
|
||||||
|
const uint16_t PROGMEM builtin_mournfull_toll_meg_par[] = {
|
||||||
|
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0001,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0004, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0002, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
|
||||||
|
0x0008, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sematron
|
||||||
|
const uint16_t PROGMEM builtin_sematron[] = {
|
||||||
|
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0001, 0x0008,
|
||||||
|
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sematron Alternative
|
||||||
|
const uint16_t PROGMEM builtin_sematron_alternative[] = {
|
||||||
|
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0001, 0x0008,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0001, 0x0001, 0x0008
|
||||||
|
};
|
||||||
|
|
||||||
|
// Athonite 1 2 Voices
|
||||||
|
const uint16_t PROGMEM builtin_athonite_1_2_voices[] = {
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0001, 0x0002, 0x0001, 0x0002
|
||||||
|
};
|
||||||
|
|
||||||
|
// Athonite 3 Voices
|
||||||
|
const uint16_t PROGMEM builtin_athonite_3_voices[] = {
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004
|
||||||
|
};
|
||||||
|
|
||||||
|
// Athonite 3 4 Voices
|
||||||
|
const uint16_t PROGMEM builtin_athonite_3_4_voices[] = {
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0008, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0009, 0x0002, 0x0001, 0x0005, 0x0002, 0x0001,
|
||||||
|
0x000A, 0x0002, 0x0001, 0x0006, 0x0002, 0x0001, 0x0009, 0x0002,
|
||||||
|
0x0001, 0x0005, 0x0002, 0x0001, 0x000A, 0x0002, 0x0001, 0x0006,
|
||||||
|
0x0002, 0x0001, 0x0009
|
||||||
|
};
|
||||||
|
|
||||||
|
// Athonite 4 8 Voices
|
||||||
|
const uint16_t PROGMEM builtin_athonite_4_8_voices[] = {
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0008, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0009, 0x0002, 0x0001, 0x0011, 0x0002, 0x0001,
|
||||||
|
0x0022, 0x0002, 0x0001, 0x0081, 0x0002, 0x0001, 0x000A, 0x0002,
|
||||||
|
0x0001, 0x0041, 0x0002, 0x0001, 0x0012, 0x0002, 0x0001, 0x0021,
|
||||||
|
0x0002, 0x0001, 0x0082, 0x0002, 0x0001, 0x0009, 0x0002, 0x0001,
|
||||||
|
0x0042, 0x0002, 0x0001, 0x0011, 0x0002, 0x0001, 0x0022, 0x0002,
|
||||||
|
0x0001, 0x0081, 0x0002, 0x0001, 0x000A, 0x0002, 0x0001, 0x0041,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0000, 0x0000,
|
||||||
|
0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Onebyone 2 3 Voices
|
||||||
|
const uint16_t PROGMEM builtin_onebyone_2_3_voices[] = {
|
||||||
|
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
|
||||||
|
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002
|
||||||
|
};
|
||||||
|
|
||||||
|
// Onebyone 4 8 Voices
|
||||||
|
const uint16_t PROGMEM builtin_onebyone_4_8_voices[] = {
|
||||||
|
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
|
||||||
|
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
|
||||||
|
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0011, 0x0002, 0x0004,
|
||||||
|
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
|
||||||
|
0x0002, 0x0041, 0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0081,
|
||||||
|
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0041, 0x0002, 0x0004,
|
||||||
|
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
|
||||||
|
0x0002, 0x0041, 0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0081,
|
||||||
|
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0041, 0x0002, 0x0004,
|
||||||
|
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
|
||||||
|
0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
|
||||||
|
0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Festive 1Voice
|
||||||
|
const uint16_t PROGMEM builtin_festive_1voice[] = {
|
||||||
|
0x0001, 0x0001, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0000, 0x0001, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
// Festive 4Voices
|
||||||
|
const uint16_t PROGMEM builtin_festive_4voices[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0009, 0x0002, 0x0001, 0x0004, 0x0009
|
||||||
|
};
|
||||||
|
|
||||||
|
// Festive 5Voices
|
||||||
|
const uint16_t PROGMEM builtin_festive_5voices[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0008, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0010, 0x0002, 0x0001, 0x0002
|
||||||
|
};
|
||||||
|
|
||||||
|
// Festive 5Voice Alternative
|
||||||
|
const uint16_t PROGMEM builtin_festive_5voice_alternative[] = {
|
||||||
|
0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004, 0x0002, 0x0008,
|
||||||
|
0x0001, 0x0010, 0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004,
|
||||||
|
0x0002, 0x0008, 0x0001, 0x0011, 0x0004, 0x0002, 0x0008, 0x0001,
|
||||||
|
0x0004, 0x0004, 0x0002, 0x0008, 0x0001, 0x0011, 0x0004, 0x0002,
|
||||||
|
0x0008, 0x0001, 0x0005, 0x0004, 0x0002, 0x0008, 0x0001, 0x0011,
|
||||||
|
0x0004, 0x0002, 0x0008, 0x0001, 0x0005, 0x0004, 0x0002, 0x0008,
|
||||||
|
0x0001, 0x0011, 0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004,
|
||||||
|
0x0002, 0x0008, 0x0001, 0x0010, 0x0004, 0x0002, 0x0008, 0x0001,
|
||||||
|
0x0004, 0x0004, 0x0002, 0x0008, 0x0001, 0x0010
|
||||||
|
};
|
||||||
|
|
||||||
|
// Festive 6Voices
|
||||||
|
const uint16_t PROGMEM builtin_festive_6voices[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0008, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0001, 0x0002, 0x0010, 0x0002, 0x0001, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
|
||||||
|
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
|
||||||
|
0x0001, 0x0002
|
||||||
|
};
|
||||||
|
|
||||||
|
// Festive 8Voices
|
||||||
|
const uint16_t PROGMEM builtin_festive_8voices[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ormilia
|
||||||
|
const uint16_t PROGMEM builtin_ormilia[] = {
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0009, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0005, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0009, 0x0000, 0x0001, 0x0002, 0x0005, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0009, 0x0000, 0x0001, 0x0002, 0x0005, 0x0000, 0x0001,
|
||||||
|
0x0002, 0x0011, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002, 0x0001,
|
||||||
|
0x0002, 0x0011, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002, 0x0041,
|
||||||
|
0x0002, 0x0081, 0x0002, 0x0009, 0x0002, 0x0041, 0x0002, 0x0081,
|
||||||
|
0x0002, 0x0009, 0x0002, 0x0041, 0x0002, 0x0081, 0x0002, 0x0005,
|
||||||
|
0x0002, 0x0001, 0x0000
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// MELODY LIBRARY - Array of all built-in melodies
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const MelodyInfo MELODY_LIBRARY[] = {
|
||||||
|
{
|
||||||
|
"1 Bell Test",
|
||||||
|
"builtin_1bell_test",
|
||||||
|
builtin_1bell_test,
|
||||||
|
sizeof(builtin_1bell_test) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Doxology Traditional",
|
||||||
|
"builtin_doxology_traditional",
|
||||||
|
builtin_doxology_traditional,
|
||||||
|
sizeof(builtin_doxology_traditional) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Doxology Alternative",
|
||||||
|
"builtin_doxology_alternative",
|
||||||
|
builtin_doxology_alternative,
|
||||||
|
sizeof(builtin_doxology_alternative) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Doxology Festive",
|
||||||
|
"builtin_doxology_festive",
|
||||||
|
builtin_doxology_festive,
|
||||||
|
sizeof(builtin_doxology_festive) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Vesper Traditional",
|
||||||
|
"builtin_vesper_traditional",
|
||||||
|
builtin_vesper_traditional,
|
||||||
|
sizeof(builtin_vesper_traditional) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Vesper Alternative",
|
||||||
|
"builtin_vesper_alternative",
|
||||||
|
builtin_vesper_alternative,
|
||||||
|
sizeof(builtin_vesper_alternative) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Catehetical",
|
||||||
|
"builtin_catehetical",
|
||||||
|
builtin_catehetical,
|
||||||
|
sizeof(builtin_catehetical) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Orthros Traditional",
|
||||||
|
"builtin_orthros_traditional",
|
||||||
|
builtin_orthros_traditional,
|
||||||
|
sizeof(builtin_orthros_traditional) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Orthros Alternative",
|
||||||
|
"builtin_orthros_alternative",
|
||||||
|
builtin_orthros_alternative,
|
||||||
|
sizeof(builtin_orthros_alternative) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Mournfull Toll",
|
||||||
|
"builtin_mournfull_toll",
|
||||||
|
builtin_mournfull_toll,
|
||||||
|
sizeof(builtin_mournfull_toll) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Mournfull Toll Alternative",
|
||||||
|
"builtin_mournfull_toll_alternative",
|
||||||
|
builtin_mournfull_toll_alternative,
|
||||||
|
sizeof(builtin_mournfull_toll_alternative) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Mournfull Toll Meg Par",
|
||||||
|
"builtin_mournfull_toll_meg_par",
|
||||||
|
builtin_mournfull_toll_meg_par,
|
||||||
|
sizeof(builtin_mournfull_toll_meg_par) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sematron",
|
||||||
|
"builtin_sematron",
|
||||||
|
builtin_sematron,
|
||||||
|
sizeof(builtin_sematron) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Sematron Alternative",
|
||||||
|
"builtin_sematron_alternative",
|
||||||
|
builtin_sematron_alternative,
|
||||||
|
sizeof(builtin_sematron_alternative) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Athonite 1 2 Voices",
|
||||||
|
"builtin_athonite_1_2_voices",
|
||||||
|
builtin_athonite_1_2_voices,
|
||||||
|
sizeof(builtin_athonite_1_2_voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Athonite 3 Voices",
|
||||||
|
"builtin_athonite_3_voices",
|
||||||
|
builtin_athonite_3_voices,
|
||||||
|
sizeof(builtin_athonite_3_voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Athonite 3 4 Voices",
|
||||||
|
"builtin_athonite_3_4_voices",
|
||||||
|
builtin_athonite_3_4_voices,
|
||||||
|
sizeof(builtin_athonite_3_4_voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Athonite 4 8 Voices",
|
||||||
|
"builtin_athonite_4_8_voices",
|
||||||
|
builtin_athonite_4_8_voices,
|
||||||
|
sizeof(builtin_athonite_4_8_voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Onebyone 2 3 Voices",
|
||||||
|
"builtin_onebyone_2_3_voices",
|
||||||
|
builtin_onebyone_2_3_voices,
|
||||||
|
sizeof(builtin_onebyone_2_3_voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Onebyone 4 8 Voices",
|
||||||
|
"builtin_onebyone_4_8_voices",
|
||||||
|
builtin_onebyone_4_8_voices,
|
||||||
|
sizeof(builtin_onebyone_4_8_voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Festive 1Voice",
|
||||||
|
"builtin_festive_1voice",
|
||||||
|
builtin_festive_1voice,
|
||||||
|
sizeof(builtin_festive_1voice) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Festive 4Voices",
|
||||||
|
"builtin_festive_4voices",
|
||||||
|
builtin_festive_4voices,
|
||||||
|
sizeof(builtin_festive_4voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Festive 5Voices",
|
||||||
|
"builtin_festive_5voices",
|
||||||
|
builtin_festive_5voices,
|
||||||
|
sizeof(builtin_festive_5voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Festive 5Voice Alternative",
|
||||||
|
"builtin_festive_5voice_alternative",
|
||||||
|
builtin_festive_5voice_alternative,
|
||||||
|
sizeof(builtin_festive_5voice_alternative) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Festive 6Voices",
|
||||||
|
"builtin_festive_6voices",
|
||||||
|
builtin_festive_6voices,
|
||||||
|
sizeof(builtin_festive_6voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Festive 8Voices",
|
||||||
|
"builtin_festive_8voices",
|
||||||
|
builtin_festive_8voices,
|
||||||
|
sizeof(builtin_festive_8voices) / sizeof(uint16_t)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Ormilia",
|
||||||
|
"builtin_ormilia",
|
||||||
|
builtin_ormilia,
|
||||||
|
sizeof(builtin_ormilia) / sizeof(uint16_t)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const uint16_t MELODY_COUNT = sizeof(MELODY_LIBRARY) / sizeof(MelodyInfo);
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if a UID is a built-in melody
|
||||||
|
* @param uid The melody UID to check
|
||||||
|
* @return true if it's a built-in melody (starts with "builtin_")
|
||||||
|
*/
|
||||||
|
inline bool isBuiltInMelody(const String& uid) {
|
||||||
|
return uid.startsWith("builtin_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Find a built-in melody by UID
|
||||||
|
* @param uid The melody UID to find
|
||||||
|
* @return Pointer to MelodyInfo if found, nullptr otherwise
|
||||||
|
*/
|
||||||
|
inline const MelodyInfo* findMelodyByUID(const String& uid) {
|
||||||
|
for (uint16_t i = 0; i < MELODY_COUNT; i++) {
|
||||||
|
if (uid == MELODY_LIBRARY[i].uid) {
|
||||||
|
return &MELODY_LIBRARY[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Load a built-in melody into a vector
|
||||||
|
* @param uid The melody UID to load
|
||||||
|
* @param melodySteps Vector to fill with melody data
|
||||||
|
* @return true if melody was found and loaded, false otherwise
|
||||||
|
*/
|
||||||
|
inline bool loadBuiltInMelody(const String& uid, std::vector<uint16_t>& melodySteps) {
|
||||||
|
const MelodyInfo* melody = findMelodyByUID(uid);
|
||||||
|
if (!melody) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize vector and copy data from PROGMEM
|
||||||
|
melodySteps.resize(melody->stepCount);
|
||||||
|
for (uint16_t i = 0; i < melody->stepCount; i++) {
|
||||||
|
melodySteps[i] = pgm_read_word(&(melody->data[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get list of all built-in melodies as JSON string
|
||||||
|
* @return JSON array string of melody names and UIDs
|
||||||
|
*/
|
||||||
|
inline String getBuiltInMelodiesJSON() {
|
||||||
|
String json = "[";
|
||||||
|
for (uint16_t i = 0; i < MELODY_COUNT; i++) {
|
||||||
|
if (i > 0) json += ",";
|
||||||
|
json += "{";
|
||||||
|
json += "\"name\":\"" + String(MELODY_LIBRARY[i].name) + "\",";
|
||||||
|
json += "\"uid\":\"" + String(MELODY_LIBRARY[i].uid) + "\",";
|
||||||
|
json += "}";
|
||||||
|
}
|
||||||
|
json += "]";
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
187
vesper/src/BuiltInMelodies/README.md
Normal file
187
vesper/src/BuiltInMelodies/README.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# Built-In Melodies System
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The built-in melodies system allows you to bake melodies directly into the firmware, eliminating the need for SD card downloads. Melodies are stored in **PROGMEM** (Flash memory), so they don't consume precious RAM.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **Check**: When a melody is requested, the Player first checks if the UID starts with `builtin_`
|
||||||
|
2. **Load**: If it's built-in, the melody is loaded from Flash memory (PROGMEM)
|
||||||
|
3. **Fallback**: If not built-in, it loads from SD card as usual
|
||||||
|
|
||||||
|
## Adding New Melodies
|
||||||
|
|
||||||
|
### Step 1: Create Your Melody Data
|
||||||
|
|
||||||
|
Each melody step is a **2-byte (uint16_t) bitmask** representing which bells to activate:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Example: Simple pattern
|
||||||
|
const uint16_t PROGMEM melody_my_tune[] = {
|
||||||
|
0x0001, // Bell 0
|
||||||
|
0x0002, // Bell 1
|
||||||
|
0x0004, // Bell 2
|
||||||
|
0x0008, // Bell 3
|
||||||
|
0x0003, // Bells 0+1 together
|
||||||
|
0x000F // Bells 0+1+2+3 together
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bitmask Reference:**
|
||||||
|
- `0x0001` = Bell 0 (bit 0)
|
||||||
|
- `0x0002` = Bell 1 (bit 1)
|
||||||
|
- `0x0004` = Bell 2 (bit 2)
|
||||||
|
- `0x0008` = Bell 3 (bit 3)
|
||||||
|
- `0x0010` = Bell 4 (bit 4)
|
||||||
|
- `0x0020` = Bell 5 (bit 5)
|
||||||
|
- `0x0040` = Bell 6 (bit 6)
|
||||||
|
- `0x0080` = Bell 7 (bit 7)
|
||||||
|
- `0x0100` = Bell 8 (bit 8)
|
||||||
|
- ... up to `0x8000` = Bell 15 (bit 15)
|
||||||
|
- `0x0000` = Silence/rest
|
||||||
|
|
||||||
|
**Combining Bells:**
|
||||||
|
- `0x0003` = Bells 0+1 (0x0001 | 0x0002)
|
||||||
|
- `0x0005` = Bells 0+2 (0x0001 | 0x0004)
|
||||||
|
- `0x000F` = Bells 0+1+2+3
|
||||||
|
- `0xFFFF` = All 16 bells
|
||||||
|
|
||||||
|
### Step 2: Add to BuiltInMelodies.hpp
|
||||||
|
|
||||||
|
Open `src/BuiltInMelodies/BuiltInMelodies.hpp` and:
|
||||||
|
|
||||||
|
1. **Add your melody array:**
|
||||||
|
```cpp
|
||||||
|
// Your new melody
|
||||||
|
const uint16_t PROGMEM melody_my_awesome_tune[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0008,
|
||||||
|
0x0010, 0x0020, 0x0040, 0x0080,
|
||||||
|
// ... up to 200 steps
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Add to MELODY_LIBRARY array:**
|
||||||
|
```cpp
|
||||||
|
const MelodyInfo MELODY_LIBRARY[] = {
|
||||||
|
// ... existing melodies ...
|
||||||
|
|
||||||
|
// Your new melody
|
||||||
|
{
|
||||||
|
"My Awesome Tune", // Display name
|
||||||
|
"builtin_my_awesome_tune", // UID (must start with "builtin_")
|
||||||
|
melody_my_awesome_tune, // Data array
|
||||||
|
sizeof(melody_my_awesome_tune) / sizeof(uint16_t), // Step count
|
||||||
|
200 // Default speed in milliseconds per beat
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Use Your Melody
|
||||||
|
|
||||||
|
Send a play command with the built-in melody UID:
|
||||||
|
|
||||||
|
**MQTT:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"group": "playback",
|
||||||
|
"action": "play",
|
||||||
|
"uid": "builtin_my_awesome_tune",
|
||||||
|
"name": "My Awesome Tune",
|
||||||
|
"speed": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**WebSocket/HTTP:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"group": "playback",
|
||||||
|
"action": "play",
|
||||||
|
"uid": "builtin_my_awesome_tune",
|
||||||
|
"name": "My Awesome Tune",
|
||||||
|
"speed": 200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pre-Loaded Melodies
|
||||||
|
|
||||||
|
The following melodies are already built-in:
|
||||||
|
|
||||||
|
| UID | Name | Steps | Default Speed |
|
||||||
|
|-----|------|-------|---------------|
|
||||||
|
| `builtin_scale` | Simple Scale | 8 | 200ms |
|
||||||
|
| `builtin_happy_birthday` | Happy Birthday | 23 | 250ms |
|
||||||
|
| `builtin_jingle_bells` | Jingle Bells | 32 | 180ms |
|
||||||
|
| `builtin_westminster` | Westminster Chimes | 16 | 400ms |
|
||||||
|
| `builtin_alarm` | Alarm | 16 | 150ms |
|
||||||
|
| `builtin_doorbell` | Doorbell | 4 | 300ms |
|
||||||
|
| `builtin_single_bell` | Single Bell Test | 1 | 100ms |
|
||||||
|
|
||||||
|
## Memory Usage
|
||||||
|
|
||||||
|
### Flash Memory (PROGMEM)
|
||||||
|
- Small melody (~30 steps): **60 bytes**
|
||||||
|
- Large melody (~200 steps): **400 bytes**
|
||||||
|
- 40 melodies average: **~6-10KB** (stored in Flash, not RAM!)
|
||||||
|
|
||||||
|
### RAM Usage
|
||||||
|
Only the **currently playing melody** is loaded into RAM. Built-in melodies are copied from Flash when needed.
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Always use `builtin_` prefix** for UIDs to identify them as built-in
|
||||||
|
2. **Test with small melodies first** before adding large ones
|
||||||
|
3. **Use hex calculator** for complex bell combinations: `0x0001 | 0x0004 = 0x0005`
|
||||||
|
4. **Add rests** with `0x0000` for silence between notes
|
||||||
|
5. **Keep it simple** - most melodies work great with 30-50 steps
|
||||||
|
|
||||||
|
## Converting Binary Files to Code
|
||||||
|
|
||||||
|
If you have existing binary melody files and want to convert them to built-in format:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python script to convert binary file to C++ array
|
||||||
|
with open('melody.bin', 'rb') as f:
|
||||||
|
data = f.read()
|
||||||
|
|
||||||
|
print("const uint16_t PROGMEM melody_name[] = {")
|
||||||
|
for i in range(0, len(data), 2):
|
||||||
|
if i % 16 == 0:
|
||||||
|
print(" ", end="")
|
||||||
|
high = data[i]
|
||||||
|
low = data[i+1]
|
||||||
|
value = (high << 8) | low
|
||||||
|
print(f"0x{value:04X}", end="")
|
||||||
|
if i < len(data) - 2:
|
||||||
|
print(", ", end="")
|
||||||
|
if (i + 2) % 16 == 0:
|
||||||
|
print()
|
||||||
|
print("\n};")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Creating a Custom Melody
|
||||||
|
|
||||||
|
Let's create "Mary Had a Little Lamb":
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Mary Had a Little Lamb
|
||||||
|
// Notes: E D C D E E E, D D D, E G G
|
||||||
|
// Mapping: E=0, D=1, C=2, G=3
|
||||||
|
const uint16_t PROGMEM melody_mary_lamb[] = {
|
||||||
|
0x0001, 0x0002, 0x0004, 0x0002, // E D C D
|
||||||
|
0x0001, 0x0001, 0x0001, 0x0000, // E E E (rest)
|
||||||
|
0x0002, 0x0002, 0x0002, 0x0000, // D D D (rest)
|
||||||
|
0x0001, 0x0008, 0x0008 // E G G
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add to MELODY_LIBRARY:
|
||||||
|
{
|
||||||
|
"Mary Had a Little Lamb",
|
||||||
|
"builtin_mary_lamb",
|
||||||
|
melody_mary_lamb,
|
||||||
|
sizeof(melody_mary_lamb) / sizeof(uint16_t),
|
||||||
|
300 // 300ms per beat
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can play it with UID `builtin_mary_lamb`!
|
||||||
@@ -1,31 +1,33 @@
|
|||||||
#include "ClientManager.hpp"
|
#include "ClientManager.hpp"
|
||||||
|
|
||||||
|
#define TAG "ClientManager"
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
|
|
||||||
ClientManager::ClientManager() {
|
ClientManager::ClientManager() {
|
||||||
LOG_INFO("Client Manager Component - Initialized");
|
LOG_INFO(TAG, "Client Manager initialized !");
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientManager::~ClientManager() {
|
ClientManager::~ClientManager() {
|
||||||
_clients.clear();
|
_clients.clear();
|
||||||
LOG_INFO("Client Manager Component - Destroyed");
|
LOG_INFO(TAG, "Client Manager destroyed");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientManager::addClient(AsyncWebSocketClient* client, DeviceType deviceType) {
|
void ClientManager::addClient(AsyncWebSocketClient* client, DeviceType deviceType) {
|
||||||
if (!isValidClient(client)) {
|
if (!isValidClient(client)) {
|
||||||
LOG_ERROR("Cannot add invalid client");
|
LOG_WARNING(TAG, "Client Manager - Cannot add invalid client");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t clientId = client->id();
|
uint32_t clientId = client->id();
|
||||||
_clients[clientId] = ClientInfo(client, deviceType);
|
_clients[clientId] = ClientInfo(client, deviceType);
|
||||||
|
|
||||||
LOG_INFO("Client #%u added as %s device", clientId, deviceTypeToString(deviceType));
|
LOG_INFO(TAG, "Client Manager - Client #%u added as %s device", clientId, deviceTypeToString(deviceType));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientManager::removeClient(uint32_t clientId) {
|
void ClientManager::removeClient(uint32_t clientId) {
|
||||||
auto it = _clients.find(clientId);
|
auto it = _clients.find(clientId);
|
||||||
if (it != _clients.end()) {
|
if (it != _clients.end()) {
|
||||||
LOG_INFO("Client #%u removed (%s device)", clientId,
|
LOG_INFO(TAG, "Client Manager - Client #%u removed (%s device)", clientId,
|
||||||
deviceTypeToString(it->second.deviceType));
|
deviceTypeToString(it->second.deviceType));
|
||||||
_clients.erase(it);
|
_clients.erase(it);
|
||||||
}
|
}
|
||||||
@@ -36,7 +38,7 @@ void ClientManager::updateClientType(uint32_t clientId, DeviceType deviceType) {
|
|||||||
if (it != _clients.end()) {
|
if (it != _clients.end()) {
|
||||||
DeviceType oldType = it->second.deviceType;
|
DeviceType oldType = it->second.deviceType;
|
||||||
it->second.deviceType = deviceType;
|
it->second.deviceType = deviceType;
|
||||||
LOG_INFO("Client #%u type updated from %s to %s", clientId,
|
LOG_INFO(TAG, "Client Manager - Client #%u type updated from %s to %s", clientId,
|
||||||
deviceTypeToString(oldType), deviceTypeToString(deviceType));
|
deviceTypeToString(oldType), deviceTypeToString(deviceType));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -72,11 +74,11 @@ bool ClientManager::sendToClient(uint32_t clientId, const String& message) {
|
|||||||
if (it != _clients.end() && isValidClient(it->second.client)) {
|
if (it != _clients.end() && isValidClient(it->second.client)) {
|
||||||
it->second.client->text(message);
|
it->second.client->text(message);
|
||||||
updateClientLastSeen(clientId);
|
updateClientLastSeen(clientId);
|
||||||
LOG_DEBUG("Message sent to client #%u: %s", clientId, message.c_str());
|
LOG_DEBUG(TAG, "Client Manager - Message sent to client #%u: %s", clientId, message.c_str());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_WARNING("Failed to send message to client #%u - client not found or invalid", clientId);
|
LOG_WARNING(TAG, "Client Manager - Failed to send message to client #%u - client not found or invalid", clientId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +92,7 @@ void ClientManager::sendToMasterClients(const String& message) {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_DEBUG("Message sent to %d master client(s): %s", count, message.c_str());
|
LOG_DEBUG(TAG, "Client Manager - Message sent to %d master client(s): %s", count, message.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientManager::sendToSecondaryClients(const String& message) {
|
void ClientManager::sendToSecondaryClients(const String& message) {
|
||||||
@@ -103,7 +105,7 @@ void ClientManager::sendToSecondaryClients(const String& message) {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_DEBUG("Message sent to %d secondary client(s): %s", count, message.c_str());
|
LOG_DEBUG(TAG, "Client Manager - Message sent to %d secondary client(s): %s", count, message.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientManager::broadcastToAll(const String& message) {
|
void ClientManager::broadcastToAll(const String& message) {
|
||||||
@@ -115,14 +117,14 @@ void ClientManager::broadcastToAll(const String& message) {
|
|||||||
count++;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG_DEBUG("Message broadcasted to %d client(s): %s", count, message.c_str());
|
LOG_DEBUG(TAG, "Client Manager - Message broadcasted to %d client(s): %s", count, message.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientManager::cleanupDisconnectedClients() {
|
void ClientManager::cleanupDisconnectedClients() {
|
||||||
auto it = _clients.begin();
|
auto it = _clients.begin();
|
||||||
while (it != _clients.end()) {
|
while (it != _clients.end()) {
|
||||||
if (!isValidClient(it->second.client)) {
|
if (!isValidClient(it->second.client)) {
|
||||||
LOG_DEBUG("Cleaning up disconnected client #%u", it->first);
|
LOG_DEBUG(TAG, "Client Manager - Cleaning up disconnected client #%u", it->first);
|
||||||
it->second.isConnected = false;
|
it->second.isConnected = false;
|
||||||
it = _clients.erase(it);
|
it = _clients.erase(it);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "CommandHandler.hpp"
|
#include "CommandHandler.hpp"
|
||||||
|
|
||||||
|
#define TAG "CommandHandler"
|
||||||
#include "../../ConfigManager/ConfigManager.hpp"
|
#include "../../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../../OTAManager/OTAManager.hpp"
|
#include "../../OTAManager/OTAManager.hpp"
|
||||||
#include "../../Player/Player.hpp"
|
#include "../../Player/Player.hpp"
|
||||||
@@ -10,8 +12,10 @@
|
|||||||
#include "../../TimeKeeper/TimeKeeper.hpp"
|
#include "../../TimeKeeper/TimeKeeper.hpp"
|
||||||
#include "../../FirmwareValidator/FirmwareValidator.hpp"
|
#include "../../FirmwareValidator/FirmwareValidator.hpp"
|
||||||
#include "../../ClientManager/ClientManager.hpp"
|
#include "../../ClientManager/ClientManager.hpp"
|
||||||
|
#include "../../Telemetry/Telemetry.hpp"
|
||||||
#include "../../Logging/Logging.hpp"
|
#include "../../Logging/Logging.hpp"
|
||||||
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
||||||
|
#include "../CommunicationRouter/CommunicationRouter.hpp"
|
||||||
|
|
||||||
CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager)
|
CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaManager)
|
||||||
: _configManager(configManager)
|
: _configManager(configManager)
|
||||||
@@ -21,6 +25,8 @@ CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaMana
|
|||||||
, _timeKeeper(nullptr)
|
, _timeKeeper(nullptr)
|
||||||
, _firmwareValidator(nullptr)
|
, _firmwareValidator(nullptr)
|
||||||
, _clientManager(nullptr)
|
, _clientManager(nullptr)
|
||||||
|
, _telemetry(nullptr)
|
||||||
|
, _communicationRouter(nullptr)
|
||||||
, _responseCallback(nullptr) {}
|
, _responseCallback(nullptr) {}
|
||||||
|
|
||||||
CommandHandler::~CommandHandler() {}
|
CommandHandler::~CommandHandler() {}
|
||||||
@@ -45,6 +51,14 @@ void CommandHandler::setClientManagerReference(ClientManager* cm) {
|
|||||||
_clientManager = cm;
|
_clientManager = cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommandHandler::setTelemetryReference(Telemetry* telemetry) {
|
||||||
|
_telemetry = telemetry;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandHandler::setCommunicationRouterReference(CommunicationRouter* comm) {
|
||||||
|
_communicationRouter = comm;
|
||||||
|
}
|
||||||
|
|
||||||
void CommandHandler::setResponseCallback(ResponseCallback callback) {
|
void CommandHandler::setResponseCallback(ResponseCallback callback) {
|
||||||
_responseCallback = callback;
|
_responseCallback = callback;
|
||||||
}
|
}
|
||||||
@@ -53,7 +67,7 @@ void CommandHandler::processCommand(JsonDocument& command, const MessageContext&
|
|||||||
String cmd = command["cmd"];
|
String cmd = command["cmd"];
|
||||||
JsonVariant contents = command["contents"];
|
JsonVariant contents = command["contents"];
|
||||||
|
|
||||||
LOG_DEBUG("Processing command: %s from %s", cmd.c_str(),
|
LOG_DEBUG(TAG, "Processing command: %s from %s", cmd.c_str(),
|
||||||
context.source == MessageSource::MQTT ? "MQTT" : "WebSocket");
|
context.source == MessageSource::MQTT ? "MQTT" : "WebSocket");
|
||||||
|
|
||||||
if (cmd == "ping") {
|
if (cmd == "ping") {
|
||||||
@@ -73,7 +87,7 @@ void CommandHandler::processCommand(JsonDocument& command, const MessageContext&
|
|||||||
} else if (cmd == "system") {
|
} else if (cmd == "system") {
|
||||||
handleSystemCommand(contents, context);
|
handleSystemCommand(contents, context);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown command received: %s", cmd.c_str());
|
LOG_WARNING(TAG, "Unknown command received: %s", cmd.c_str());
|
||||||
sendErrorResponse("unknown_command", "Command not recognized: " + cmd, context);
|
sendErrorResponse("unknown_command", "Command not recognized: " + cmd, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +128,15 @@ void CommandHandler::handleStatusCommand(const MessageContext& context) {
|
|||||||
projectedRunTime = _player->calculateProjectedRunTime();
|
projectedRunTime = _player->calculateProjectedRunTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
String response = ResponseBuilder::deviceStatus(playerStatus, timeElapsedMs, projectedRunTime);
|
// Collect strike counters from Telemetry
|
||||||
|
uint32_t strikeCounters[16] = {0};
|
||||||
|
if (_telemetry) {
|
||||||
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
|
strikeCounters[i] = _telemetry->getStrikeCount(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String response = ResponseBuilder::deviceStatus(playerStatus, timeElapsedMs, projectedRunTime, strikeCounters);
|
||||||
sendResponse(response, context);
|
sendResponse(response, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +148,7 @@ void CommandHandler::handleIdentifyCommand(JsonVariant contents, const MessageCo
|
|||||||
|
|
||||||
// 🛡️ SAFETY CHECK: Ensure ClientManager reference is set
|
// 🛡️ SAFETY CHECK: Ensure ClientManager reference is set
|
||||||
if (!_clientManager) {
|
if (!_clientManager) {
|
||||||
LOG_ERROR("ClientManager reference not set in CommandHandler!");
|
LOG_ERROR(TAG, "ClientManager reference not set in CommandHandler!");
|
||||||
sendErrorResponse("identify", "Internal error: ClientManager not available", context);
|
sendErrorResponse("identify", "Internal error: ClientManager not available", context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -148,7 +170,7 @@ void CommandHandler::handleIdentifyCommand(JsonVariant contents, const MessageCo
|
|||||||
if (deviceType != ClientManager::DeviceType::UNKNOWN) {
|
if (deviceType != ClientManager::DeviceType::UNKNOWN) {
|
||||||
_clientManager->updateClientType(context.clientId, deviceType);
|
_clientManager->updateClientType(context.clientId, deviceType);
|
||||||
sendSuccessResponse("identify", "Device identified as " + deviceTypeStr, context);
|
sendSuccessResponse("identify", "Device identified as " + deviceTypeStr, context);
|
||||||
LOG_INFO("Client #%u identified as %s device", context.clientId, deviceTypeStr.c_str());
|
LOG_INFO(TAG, "Client #%u identified as %s device", context.clientId, deviceTypeStr.c_str());
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("identify", "Invalid device_type. Use 'master' or 'secondary'", context);
|
sendErrorResponse("identify", "Invalid device_type. Use 'master' or 'secondary'", context);
|
||||||
}
|
}
|
||||||
@@ -164,7 +186,7 @@ void CommandHandler::handlePlaybackCommand(JsonVariant contents, const MessageCo
|
|||||||
sendErrorResponse("playback", "Playback command failed", context);
|
sendErrorResponse("playback", "Playback command failed", context);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Player reference not set");
|
LOG_ERROR(TAG, "Player reference not set");
|
||||||
sendErrorResponse("playback", "Player not available", context);
|
sendErrorResponse("playback", "Player not available", context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -176,7 +198,7 @@ void CommandHandler::handleFileManagerCommand(JsonVariant contents, const Messag
|
|||||||
}
|
}
|
||||||
|
|
||||||
String action = contents["action"];
|
String action = contents["action"];
|
||||||
LOG_DEBUG("Processing file manager action: %s", action.c_str());
|
LOG_DEBUG(TAG, "Processing file manager action: %s", action.c_str());
|
||||||
|
|
||||||
if (action == "list_melodies") {
|
if (action == "list_melodies") {
|
||||||
handleListMelodiesCommand(context);
|
handleListMelodiesCommand(context);
|
||||||
@@ -185,7 +207,7 @@ void CommandHandler::handleFileManagerCommand(JsonVariant contents, const Messag
|
|||||||
} else if (action == "delete_melody") {
|
} else if (action == "delete_melody") {
|
||||||
handleDeleteMelodyCommand(contents, context);
|
handleDeleteMelodyCommand(contents, context);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown file manager action: %s", action.c_str());
|
LOG_WARNING(TAG, "Unknown file manager action: %s", action.c_str());
|
||||||
sendErrorResponse("file_manager", "Unknown action: " + action, context);
|
sendErrorResponse("file_manager", "Unknown action: " + action, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,14 +219,14 @@ void CommandHandler::handleRelaySetupCommand(JsonVariant contents, const Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
String action = contents["action"];
|
String action = contents["action"];
|
||||||
LOG_DEBUG("Processing relay setup action: %s", action.c_str());
|
LOG_DEBUG(TAG, "Processing relay setup action: %s", action.c_str());
|
||||||
|
|
||||||
if (action == "set_timings") {
|
if (action == "set_timings") {
|
||||||
handleSetRelayTimersCommand(contents, context);
|
handleSetRelayTimersCommand(contents, context);
|
||||||
} else if (action == "set_outputs") {
|
} else if (action == "set_outputs") {
|
||||||
handleSetRelayOutputsCommand(contents, context);
|
handleSetRelayOutputsCommand(contents, context);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown relay setup action: %s", action.c_str());
|
LOG_WARNING(TAG, "Unknown relay setup action: %s", action.c_str());
|
||||||
sendErrorResponse("relay_setup", "Unknown action: " + action, context);
|
sendErrorResponse("relay_setup", "Unknown action: " + action, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -216,7 +238,7 @@ void CommandHandler::handleClockSetupCommand(JsonVariant contents, const Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
String action = contents["action"];
|
String action = contents["action"];
|
||||||
LOG_DEBUG("Processing clock setup action: %s", action.c_str());
|
LOG_DEBUG(TAG, "Processing clock setup action: %s", action.c_str());
|
||||||
|
|
||||||
if (action == "set_outputs") {
|
if (action == "set_outputs") {
|
||||||
handleSetClockOutputsCommand(contents, context);
|
handleSetClockOutputsCommand(contents, context);
|
||||||
@@ -237,7 +259,7 @@ void CommandHandler::handleClockSetupCommand(JsonVariant contents, const Message
|
|||||||
} else if (action == "set_enabled") {
|
} else if (action == "set_enabled") {
|
||||||
handleSetClockEnabledCommand(contents, context);
|
handleSetClockEnabledCommand(contents, context);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown clock setup action: %s", action.c_str());
|
LOG_WARNING(TAG, "Unknown clock setup action: %s", action.c_str());
|
||||||
sendErrorResponse("clock_setup", "Unknown action: " + action, context);
|
sendErrorResponse("clock_setup", "Unknown action: " + action, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +271,7 @@ void CommandHandler::handleSystemInfoCommand(JsonVariant contents, const Message
|
|||||||
}
|
}
|
||||||
|
|
||||||
String action = contents["action"];
|
String action = contents["action"];
|
||||||
LOG_DEBUG("Processing system info action: %s", action.c_str());
|
LOG_DEBUG(TAG, "Processing system info action: %s", action.c_str());
|
||||||
|
|
||||||
if (action == "report_status") {
|
if (action == "report_status") {
|
||||||
handleStatusCommand(context);
|
handleStatusCommand(context);
|
||||||
@@ -257,16 +279,16 @@ void CommandHandler::handleSystemInfoCommand(JsonVariant contents, const Message
|
|||||||
handleGetDeviceTimeCommand(context);
|
handleGetDeviceTimeCommand(context);
|
||||||
} else if (action == "get_clock_time") {
|
} else if (action == "get_clock_time") {
|
||||||
handleGetClockTimeCommand(context);
|
handleGetClockTimeCommand(context);
|
||||||
} else if (action == "commit_firmware") {
|
|
||||||
handleCommitFirmwareCommand(context);
|
|
||||||
} else if (action == "rollback_firmware") {
|
|
||||||
handleRollbackFirmwareCommand(context);
|
|
||||||
} else if (action == "get_firmware_status") {
|
} else if (action == "get_firmware_status") {
|
||||||
handleGetFirmwareStatusCommand(context);
|
handleGetFirmwareStatusCommand(context);
|
||||||
|
} else if (action == "network_info") {
|
||||||
|
handleNetworkInfoCommand(context);
|
||||||
} else if (action == "get_full_settings") {
|
} else if (action == "get_full_settings") {
|
||||||
handleGetFullSettingsCommand(context);
|
handleGetFullSettingsCommand(context);
|
||||||
|
} else if (action == "sync_time_to_lcd") {
|
||||||
|
handleSyncTimeToLcdCommand(context);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown system info action: %s", action.c_str());
|
LOG_WARNING(TAG, "Unknown system info action: %s", action.c_str());
|
||||||
sendErrorResponse("system_info", "Unknown action: " + action, context);
|
sendErrorResponse("system_info", "Unknown action: " + action, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +305,7 @@ void CommandHandler::handleListMelodiesCommand(const MessageContext& context) {
|
|||||||
DeserializationError error = deserializeJson(doc, fileListJson);
|
DeserializationError error = deserializeJson(doc, fileListJson);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("Failed to parse file list JSON: %s", error.c_str());
|
LOG_ERROR(TAG, "Failed to parse file list JSON: %s", error.c_str());
|
||||||
sendErrorResponse("list_melodies", "Failed to parse file list", context);
|
sendErrorResponse("list_melodies", "Failed to parse file list", context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -342,26 +364,32 @@ void CommandHandler::handleSetRelayTimersCommand(JsonVariant contents, const Mes
|
|||||||
bool saved = _configManager.saveBellDurations();
|
bool saved = _configManager.saveBellDurations();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_relay_timers", "Relay timers updated and saved", context);
|
sendSuccessResponse("set_relay_timers", "Relay timers updated and saved", context);
|
||||||
LOG_INFO("Relay timers updated and saved successfully");
|
LOG_INFO(TAG, "Relay timers updated and saved successfully");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_relay_timers", "Failed to save relay timers to SD card", context);
|
sendErrorResponse("set_relay_timers", "Failed to save relay timers to SD card", context);
|
||||||
LOG_ERROR("Failed to save relay timers configuration");
|
LOG_ERROR(TAG, "Failed to save relay timers configuration");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_relay_timers", "Failed to update relay timers", context);
|
sendErrorResponse("set_relay_timers", "Failed to update relay timers", context);
|
||||||
LOG_ERROR("Exception occurred while updating relay timers");
|
LOG_ERROR(TAG, "Exception occurred while updating relay timers");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandHandler::handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context) {
|
void CommandHandler::handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
try {
|
try {
|
||||||
_configManager.updateBellOutputs(contents);
|
_configManager.updateBellOutputs(contents);
|
||||||
// Note: Bell outputs are typically not persisted to SD card as they're more of a mapping configuration
|
// Save bell outputs configuration to SD card for persistence
|
||||||
sendSuccessResponse("set_relay_outputs", "Relay outputs updated", context);
|
bool saved = _configManager.saveBellOutputs();
|
||||||
LOG_INFO("Relay outputs updated successfully");
|
if (saved) {
|
||||||
|
sendSuccessResponse("set_relay_outputs", "Relay outputs updated and saved", context);
|
||||||
|
LOG_INFO(TAG, "Relay outputs updated and saved successfully");
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_relay_outputs", "Failed to save relay outputs to SD card", context);
|
||||||
|
LOG_ERROR(TAG, "Failed to save relay outputs configuration");
|
||||||
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_relay_outputs", "Failed to update relay outputs", context);
|
sendErrorResponse("set_relay_outputs", "Failed to update relay outputs", context);
|
||||||
LOG_ERROR("Exception occurred while updating relay outputs");
|
LOG_ERROR(TAG, "Exception occurred while updating relay outputs");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,14 +400,14 @@ void CommandHandler::handleSetClockOutputsCommand(JsonVariant contents, const Me
|
|||||||
bool saved = _configManager.saveClockConfig();
|
bool saved = _configManager.saveClockConfig();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_clock_outputs", "Clock outputs updated and saved", context);
|
sendSuccessResponse("set_clock_outputs", "Clock outputs updated and saved", context);
|
||||||
LOG_INFO("Clock outputs updated and saved successfully");
|
LOG_INFO(TAG, "Clock outputs updated and saved successfully");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_clock_outputs", "Failed to save clock outputs to SD card", context);
|
sendErrorResponse("set_clock_outputs", "Failed to save clock outputs to SD card", context);
|
||||||
LOG_ERROR("Failed to save clock outputs configuration");
|
LOG_ERROR(TAG, "Failed to save clock outputs configuration");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_clock_outputs", "Failed to update clock outputs", context);
|
sendErrorResponse("set_clock_outputs", "Failed to update clock outputs", context);
|
||||||
LOG_ERROR("Exception occurred while updating clock outputs");
|
LOG_ERROR(TAG, "Exception occurred while updating clock outputs");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,14 +418,14 @@ void CommandHandler::handleSetClockTimingsCommand(JsonVariant contents, const Me
|
|||||||
bool saved = _configManager.saveClockConfig();
|
bool saved = _configManager.saveClockConfig();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_clock_timings", "Clock timings updated and saved", context);
|
sendSuccessResponse("set_clock_timings", "Clock timings updated and saved", context);
|
||||||
LOG_INFO("Clock timings updated and saved successfully");
|
LOG_INFO(TAG, "Clock timings updated and saved successfully");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_clock_timings", "Failed to save clock timings to SD card", context);
|
sendErrorResponse("set_clock_timings", "Failed to save clock timings to SD card", context);
|
||||||
LOG_ERROR("Failed to save clock timings configuration");
|
LOG_ERROR(TAG, "Failed to save clock timings configuration");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_clock_timings", "Failed to update clock timings", context);
|
sendErrorResponse("set_clock_timings", "Failed to update clock timings", context);
|
||||||
LOG_ERROR("Exception occurred while updating clock timings");
|
LOG_ERROR(TAG, "Exception occurred while updating clock timings");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,14 +436,14 @@ void CommandHandler::handleSetClockAlertsCommand(JsonVariant contents, const Mes
|
|||||||
bool saved = _configManager.saveClockConfig();
|
bool saved = _configManager.saveClockConfig();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_clock_alerts", "Clock alerts updated and saved", context);
|
sendSuccessResponse("set_clock_alerts", "Clock alerts updated and saved", context);
|
||||||
LOG_INFO("Clock alerts updated and saved successfully");
|
LOG_INFO(TAG, "Clock alerts updated and saved successfully");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_clock_alerts", "Failed to save clock alerts to SD card", context);
|
sendErrorResponse("set_clock_alerts", "Failed to save clock alerts to SD card", context);
|
||||||
LOG_ERROR("Failed to save clock alerts configuration");
|
LOG_ERROR(TAG, "Failed to save clock alerts configuration");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_clock_alerts", "Failed to update clock alerts", context);
|
sendErrorResponse("set_clock_alerts", "Failed to update clock alerts", context);
|
||||||
LOG_ERROR("Exception occurred while updating clock alerts");
|
LOG_ERROR(TAG, "Exception occurred while updating clock alerts");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,14 +454,14 @@ void CommandHandler::handleSetClockBacklightCommand(JsonVariant contents, const
|
|||||||
bool saved = _configManager.saveClockConfig();
|
bool saved = _configManager.saveClockConfig();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_clock_backlight", "Clock backlight updated and saved", context);
|
sendSuccessResponse("set_clock_backlight", "Clock backlight updated and saved", context);
|
||||||
LOG_INFO("Clock backlight updated and saved successfully");
|
LOG_INFO(TAG, "Clock backlight updated and saved successfully");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_clock_backlight", "Failed to save clock backlight to SD card", context);
|
sendErrorResponse("set_clock_backlight", "Failed to save clock backlight to SD card", context);
|
||||||
LOG_ERROR("Failed to save clock backlight configuration");
|
LOG_ERROR(TAG, "Failed to save clock backlight configuration");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_clock_backlight", "Failed to update clock backlight", context);
|
sendErrorResponse("set_clock_backlight", "Failed to update clock backlight", context);
|
||||||
LOG_ERROR("Exception occurred while updating clock backlight");
|
LOG_ERROR(TAG, "Exception occurred while updating clock backlight");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,14 +472,14 @@ void CommandHandler::handleSetClockSilenceCommand(JsonVariant contents, const Me
|
|||||||
bool saved = _configManager.saveClockConfig();
|
bool saved = _configManager.saveClockConfig();
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_clock_silence", "Clock silence periods updated and saved", context);
|
sendSuccessResponse("set_clock_silence", "Clock silence periods updated and saved", context);
|
||||||
LOG_INFO("Clock silence periods updated and saved successfully");
|
LOG_INFO(TAG, "Clock silence periods updated and saved successfully");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_clock_silence", "Failed to save clock silence configuration to SD card", context);
|
sendErrorResponse("set_clock_silence", "Failed to save clock silence configuration to SD card", context);
|
||||||
LOG_ERROR("Failed to save clock silence configuration");
|
LOG_ERROR(TAG, "Failed to save clock silence configuration");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_clock_silence", "Failed to update clock silence periods", context);
|
sendErrorResponse("set_clock_silence", "Failed to update clock silence periods", context);
|
||||||
LOG_ERROR("Exception occurred while updating clock silence periods");
|
LOG_ERROR(TAG, "Exception occurred while updating clock silence periods");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +516,7 @@ void CommandHandler::handleSetRtcTimeCommand(JsonVariant contents, const Message
|
|||||||
// Update timezone configuration
|
// Update timezone configuration
|
||||||
_configManager.updateTimeConfig(baseGmtOffset, dstOffset);
|
_configManager.updateTimeConfig(baseGmtOffset, dstOffset);
|
||||||
|
|
||||||
LOG_INFO("Timezone updated: %s (GMT%+ld, DST%+ld)",
|
LOG_INFO(TAG, "Timezone updated: %s (GMT%+ld, DST%+ld)",
|
||||||
timezoneName.c_str(), baseGmtOffset/3600, dstOffset/3600);
|
timezoneName.c_str(), baseGmtOffset/3600, dstOffset/3600);
|
||||||
|
|
||||||
// Apply total offset to timestamp
|
// Apply total offset to timestamp
|
||||||
@@ -500,14 +528,14 @@ void CommandHandler::handleSetRtcTimeCommand(JsonVariant contents, const Message
|
|||||||
|
|
||||||
// Verify the time was set correctly by reading it back
|
// Verify the time was set correctly by reading it back
|
||||||
unsigned long verifyTime = _timeKeeper->getTime();
|
unsigned long verifyTime = _timeKeeper->getTime();
|
||||||
if (verifyTime > 0 && abs((long)verifyTime - (long)timestamp) < 5) { // Allow 5 second tolerance
|
if (verifyTime > 0 && abs((long)verifyTime - (long)localTimestamp) < 5) { // Allow 5 second tolerance
|
||||||
sendSuccessResponse("set_rtc_time",
|
sendSuccessResponse("set_rtc_time",
|
||||||
"RTC time and timezone updated successfully", context);
|
"RTC time and timezone updated successfully", context);
|
||||||
LOG_INFO("RTC time set with timezone: UTC %lu + %ld = local %lu",
|
LOG_INFO(TAG, "RTC time set with timezone: UTC %lu + %ld = local %lu",
|
||||||
timestamp, totalOffset, localTimestamp);
|
timestamp, totalOffset, localTimestamp);
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_rtc_time", "Failed to verify RTC time was set correctly", context);
|
sendErrorResponse("set_rtc_time", "Failed to verify RTC time was set correctly", context);
|
||||||
LOG_ERROR("RTC time verification failed - expected: %lu, got: %lu", timestamp, verifyTime);
|
LOG_ERROR(TAG, "RTC time verification failed - expected: %lu, got: %lu", localTimestamp, verifyTime);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Legacy method: Use device's existing timezone config
|
// Legacy method: Use device's existing timezone config
|
||||||
@@ -517,10 +545,10 @@ void CommandHandler::handleSetRtcTimeCommand(JsonVariant contents, const Message
|
|||||||
unsigned long verifyTime = _timeKeeper->getTime();
|
unsigned long verifyTime = _timeKeeper->getTime();
|
||||||
if (verifyTime > 0 && abs((long)verifyTime - (long)timestamp) < 5) { // Allow 5 second tolerance
|
if (verifyTime > 0 && abs((long)verifyTime - (long)timestamp) < 5) { // Allow 5 second tolerance
|
||||||
sendSuccessResponse("set_rtc_time", "RTC time updated successfully", context);
|
sendSuccessResponse("set_rtc_time", "RTC time updated successfully", context);
|
||||||
LOG_INFO("RTC time set using device timezone config: %lu", timestamp);
|
LOG_INFO(TAG, "RTC time set using device timezone config: %lu", timestamp);
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_rtc_time", "Failed to verify RTC time was set correctly", context);
|
sendErrorResponse("set_rtc_time", "Failed to verify RTC time was set correctly", context);
|
||||||
LOG_ERROR("RTC time verification failed - expected: %lu, got: %lu", timestamp, verifyTime);
|
LOG_ERROR(TAG, "RTC time verification failed - expected: %lu, got: %lu", timestamp, verifyTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,11 +587,11 @@ void CommandHandler::handleSetPhysicalClockTimeCommand(JsonVariant contents, con
|
|||||||
|
|
||||||
if (saved) {
|
if (saved) {
|
||||||
sendSuccessResponse("set_physical_clock_time", "Physical clock time updated and saved successfully", context);
|
sendSuccessResponse("set_physical_clock_time", "Physical clock time updated and saved successfully", context);
|
||||||
LOG_INFO("Physical clock time set to %02d:%02d (12h: %02d:%02d) and saved to SD",
|
LOG_INFO(TAG, "Physical clock time set to %02d:%02d (12h: %02d:%02d) and saved to SD",
|
||||||
hour, minute, clockHour, minute);
|
hour, minute, clockHour, minute);
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_physical_clock_time", "Physical clock time updated but failed to save to SD card", context);
|
sendErrorResponse("set_physical_clock_time", "Physical clock time updated but failed to save to SD card", context);
|
||||||
LOG_ERROR("Physical clock time set to %02d:%02d but failed to save to SD", hour, minute);
|
LOG_ERROR(TAG, "Physical clock time set to %02d:%02d but failed to save to SD", hour, minute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,12 +603,12 @@ void CommandHandler::handlePauseClockUpdatesCommand(JsonVariant contents, const
|
|||||||
if (contents["action"] == "pause_clock_updates") {
|
if (contents["action"] == "pause_clock_updates") {
|
||||||
_timeKeeper->pauseClockUpdates();
|
_timeKeeper->pauseClockUpdates();
|
||||||
sendSuccessResponse("pause_clock_updates", "Clock updates paused", context);
|
sendSuccessResponse("pause_clock_updates", "Clock updates paused", context);
|
||||||
LOG_DEBUG("Clock updates paused");
|
LOG_DEBUG(TAG, "Clock updates paused");
|
||||||
return;
|
return;
|
||||||
} else if (contents["action"] == "resume_clock_updates") {
|
} else if (contents["action"] == "resume_clock_updates") {
|
||||||
_timeKeeper->resumeClockUpdates();
|
_timeKeeper->resumeClockUpdates();
|
||||||
sendSuccessResponse("resume_clock_updates", "Clock updates resumed", context);
|
sendSuccessResponse("resume_clock_updates", "Clock updates resumed", context);
|
||||||
LOG_DEBUG("Clock updates resumed");
|
LOG_DEBUG(TAG, "Clock updates resumed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,31 +628,38 @@ void CommandHandler::handleSetClockEnabledCommand(JsonVariant contents, const Me
|
|||||||
if (saved) {
|
if (saved) {
|
||||||
String status = enabled ? "enabled" : "disabled";
|
String status = enabled ? "enabled" : "disabled";
|
||||||
sendSuccessResponse("set_clock_enabled", "Clock " + status + " and saved successfully", context);
|
sendSuccessResponse("set_clock_enabled", "Clock " + status + " and saved successfully", context);
|
||||||
LOG_INFO("Clock %s via remote command", status.c_str());
|
LOG_INFO(TAG, "Clock %s via remote command", status.c_str());
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_clock_enabled", "Clock setting updated but failed to save to SD card", context);
|
sendErrorResponse("set_clock_enabled", "Clock setting updated but failed to save to SD card", context);
|
||||||
LOG_ERROR("Failed to save clock enabled setting to SD card");
|
LOG_ERROR(TAG, "Failed to save clock enabled setting to SD card");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_clock_enabled", "Failed to update clock enabled setting", context);
|
sendErrorResponse("set_clock_enabled", "Failed to update clock enabled setting", context);
|
||||||
LOG_ERROR("Exception occurred while updating clock enabled setting");
|
LOG_ERROR(TAG, "Exception occurred while updating clock enabled setting");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandHandler::handleGetDeviceTimeCommand(const MessageContext& context) {
|
void CommandHandler::handleGetDeviceTimeCommand(const MessageContext& context) {
|
||||||
StaticJsonDocument<256> response;
|
StaticJsonDocument<384> response;
|
||||||
response["status"] = "SUCCESS";
|
response["status"] = "SUCCESS";
|
||||||
response["type"] = "device_time";
|
response["type"] = "device_time";
|
||||||
|
|
||||||
if (_timeKeeper) {
|
if (_timeKeeper) {
|
||||||
// Get Unix timestamp from Timekeeper
|
// RTC stores LOCAL time (already timezone-adjusted)
|
||||||
unsigned long timestamp = _timeKeeper->getTime();
|
unsigned long localTimestamp = _timeKeeper->getTime();
|
||||||
response["payload"]["timestamp"] = timestamp;
|
|
||||||
|
// Get timezone offset to calculate UTC
|
||||||
|
const auto& timeConfig = _configManager.getTimeConfig();
|
||||||
|
long totalOffset = timeConfig.gmtOffsetSec + timeConfig.daylightOffsetSec;
|
||||||
|
unsigned long utcTimestamp = localTimestamp - totalOffset;
|
||||||
|
|
||||||
|
response["payload"]["local_timestamp"] = localTimestamp;
|
||||||
|
response["payload"]["utc_timestamp"] = utcTimestamp;
|
||||||
response["payload"]["rtc_available"] = true;
|
response["payload"]["rtc_available"] = true;
|
||||||
|
|
||||||
// Convert to readable format
|
// Convert LOCAL timestamp to readable format using gmtime (no additional offset)
|
||||||
time_t rawTime = (time_t)timestamp;
|
time_t rawTime = (time_t)localTimestamp;
|
||||||
struct tm* timeInfo = localtime(&rawTime);
|
struct tm* timeInfo = gmtime(&rawTime); // Use gmtime to avoid double-offset
|
||||||
response["payload"]["year"] = timeInfo->tm_year + 1900;
|
response["payload"]["year"] = timeInfo->tm_year + 1900;
|
||||||
response["payload"]["month"] = timeInfo->tm_mon + 1;
|
response["payload"]["month"] = timeInfo->tm_mon + 1;
|
||||||
response["payload"]["day"] = timeInfo->tm_mday;
|
response["payload"]["day"] = timeInfo->tm_mday;
|
||||||
@@ -632,16 +667,17 @@ void CommandHandler::handleGetDeviceTimeCommand(const MessageContext& context) {
|
|||||||
response["payload"]["minute"] = timeInfo->tm_min;
|
response["payload"]["minute"] = timeInfo->tm_min;
|
||||||
response["payload"]["second"] = timeInfo->tm_sec;
|
response["payload"]["second"] = timeInfo->tm_sec;
|
||||||
} else {
|
} else {
|
||||||
response["payload"]["timestamp"] = millis() / 1000;
|
response["payload"]["local_timestamp"] = millis() / 1000;
|
||||||
|
response["payload"]["utc_timestamp"] = millis() / 1000;
|
||||||
response["payload"]["rtc_available"] = false;
|
response["payload"]["rtc_available"] = false;
|
||||||
LOG_WARNING("TimeKeeper reference not set for device time request");
|
LOG_WARNING(TAG, "TimeKeeper reference not set for device time request");
|
||||||
}
|
}
|
||||||
|
|
||||||
String responseStr;
|
String responseStr;
|
||||||
serializeJson(response, responseStr);
|
serializeJson(response, responseStr);
|
||||||
sendResponse(responseStr, context);
|
sendResponse(responseStr, context);
|
||||||
|
|
||||||
LOG_DEBUG("Device time requested");
|
LOG_DEBUG(TAG, "Device time requested");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandHandler::handleGetClockTimeCommand(const MessageContext& context) {
|
void CommandHandler::handleGetClockTimeCommand(const MessageContext& context) {
|
||||||
@@ -660,7 +696,7 @@ void CommandHandler::handleGetClockTimeCommand(const MessageContext& context) {
|
|||||||
serializeJson(response, responseStr);
|
serializeJson(response, responseStr);
|
||||||
sendResponse(responseStr, context);
|
sendResponse(responseStr, context);
|
||||||
|
|
||||||
LOG_DEBUG("Physical clock time requested: %02d:%02d (last sync: %lu)",
|
LOG_DEBUG(TAG, "Physical clock time requested: %02d:%02d (last sync: %lu)",
|
||||||
_configManager.getPhysicalClockHour(),
|
_configManager.getPhysicalClockHour(),
|
||||||
_configManager.getPhysicalClockMinute(),
|
_configManager.getPhysicalClockMinute(),
|
||||||
_configManager.getLastSyncTime());
|
_configManager.getLastSyncTime());
|
||||||
@@ -682,16 +718,16 @@ void CommandHandler::handleCommitFirmwareCommand(const MessageContext& context)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("💾 Manual firmware commit requested via %s",
|
LOG_INFO(TAG, "💾 Manual firmware commit requested via %s",
|
||||||
context.source == MessageSource::MQTT ? "MQTT" : "WebSocket");
|
context.source == MessageSource::MQTT ? "MQTT" : "WebSocket");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_firmwareValidator->commitFirmware();
|
_firmwareValidator->commitFirmware();
|
||||||
sendSuccessResponse("commit_firmware", "Firmware committed successfully", context);
|
sendSuccessResponse("commit_firmware", "Firmware committed successfully", context);
|
||||||
LOG_INFO("✅ Firmware manually committed - system is now stable");
|
LOG_INFO(TAG, "✅ Firmware manually committed - system is now stable");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("commit_firmware", "Failed to commit firmware", context);
|
sendErrorResponse("commit_firmware", "Failed to commit firmware", context);
|
||||||
LOG_ERROR("❌ Failed to commit firmware");
|
LOG_ERROR(TAG, "❌ Failed to commit firmware");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,18 +737,18 @@ void CommandHandler::handleRollbackFirmwareCommand(const MessageContext& context
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_WARNING("🔄 Manual firmware rollback requested via %s",
|
LOG_WARNING(TAG, "🔄 Manual firmware rollback requested via %s",
|
||||||
context.source == MessageSource::MQTT ? "MQTT" : "WebSocket");
|
context.source == MessageSource::MQTT ? "MQTT" : "WebSocket");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_firmwareValidator->rollbackFirmware();
|
_firmwareValidator->rollbackFirmware();
|
||||||
sendSuccessResponse("rollback_firmware", "Firmware rollback initiated - device will reboot", context);
|
sendSuccessResponse("rollback_firmware", "Firmware rollback initiated - device will reboot", context);
|
||||||
LOG_WARNING("🔄 Firmware rollback initiated - device should reboot shortly");
|
LOG_WARNING(TAG, "🔄 Firmware rollback initiated - device should reboot shortly");
|
||||||
|
|
||||||
// Device should reboot automatically, but this response might not be sent
|
// Device should reboot automatically, but this response might not be sent
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("rollback_firmware", "Failed to initiate firmware rollback", context);
|
sendErrorResponse("rollback_firmware", "Failed to initiate firmware rollback", context);
|
||||||
LOG_ERROR("❌ Failed to initiate firmware rollback");
|
LOG_ERROR(TAG, "❌ Failed to initiate firmware rollback");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -786,11 +822,26 @@ void CommandHandler::handleGetFirmwareStatusCommand(const MessageContext& contex
|
|||||||
serializeJson(response, responseStr);
|
serializeJson(response, responseStr);
|
||||||
sendResponse(responseStr, context);
|
sendResponse(responseStr, context);
|
||||||
|
|
||||||
LOG_DEBUG("Firmware status requested: %s", stateStr.c_str());
|
LOG_DEBUG(TAG, "Firmware status requested: %s", stateStr.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandHandler::handleNetworkInfoCommand(const MessageContext& context) {
|
||||||
|
StaticJsonDocument<256> response;
|
||||||
|
response["status"] = "SUCCESS";
|
||||||
|
response["type"] = "network_info";
|
||||||
|
|
||||||
|
JsonObject payload = response.createNestedObject("payload");
|
||||||
|
payload["ip"] = WiFi.localIP().toString();
|
||||||
|
payload["gateway"] = WiFi.gatewayIP().toString();
|
||||||
|
payload["dns"] = WiFi.dnsIP().toString();
|
||||||
|
|
||||||
|
String responseStr;
|
||||||
|
serializeJson(response, responseStr);
|
||||||
|
sendResponse(responseStr, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context) {
|
void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context) {
|
||||||
LOG_DEBUG("Full settings requested");
|
LOG_DEBUG(TAG, "Full settings requested");
|
||||||
|
|
||||||
// Get all settings as JSON string from ConfigManager
|
// Get all settings as JSON string from ConfigManager
|
||||||
String settingsJson = _configManager.getAllSettingsAsJson();
|
String settingsJson = _configManager.getAllSettingsAsJson();
|
||||||
@@ -805,7 +856,7 @@ void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context)
|
|||||||
DeserializationError error = deserializeJson(settingsDoc, settingsJson);
|
DeserializationError error = deserializeJson(settingsDoc, settingsJson);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("Failed to parse settings JSON: %s", error.c_str());
|
LOG_ERROR(TAG, "Failed to parse settings JSON: %s", error.c_str());
|
||||||
sendErrorResponse("get_full_settings", "Failed to serialize settings", context);
|
sendErrorResponse("get_full_settings", "Failed to serialize settings", context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -816,10 +867,40 @@ void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context)
|
|||||||
serializeJson(response, responseStr);
|
serializeJson(response, responseStr);
|
||||||
sendResponse(responseStr, context);
|
sendResponse(responseStr, context);
|
||||||
|
|
||||||
LOG_DEBUG("Full settings sent (%d bytes)", responseStr.length());
|
LOG_DEBUG(TAG, "Full settings sent (%d bytes)", responseStr.length());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommandHandler::handleSyncTimeToLcdCommand(const MessageContext& context) {
|
||||||
|
StaticJsonDocument<256> response;
|
||||||
|
response["status"] = "SUCCESS";
|
||||||
|
response["type"] = "sync_time_to_lcd";
|
||||||
|
|
||||||
|
// Get the local timestamp from TimeKeeper (RTC stores local time)
|
||||||
|
unsigned long localTimestamp = 0;
|
||||||
|
if (_timeKeeper) {
|
||||||
|
localTimestamp = _timeKeeper->getTime();
|
||||||
|
} else {
|
||||||
|
// Fallback to millis if TimeKeeper not available
|
||||||
|
localTimestamp = millis() / 1000;
|
||||||
|
LOG_WARNING(TAG, "TimeKeeper not available for LCD time sync");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get timezone offset from ConfigManager (in seconds)
|
||||||
|
const auto& timeConfig = _configManager.getTimeConfig();
|
||||||
|
long totalOffset = timeConfig.gmtOffsetSec + timeConfig.daylightOffsetSec;
|
||||||
|
|
||||||
|
// Calculate UTC timestamp by subtracting the offset from local time
|
||||||
|
unsigned long utcTimestamp = localTimestamp - totalOffset;
|
||||||
|
|
||||||
|
response["payload"]["timestamp"] = utcTimestamp;
|
||||||
|
response["payload"]["offset"] = totalOffset;
|
||||||
|
|
||||||
|
String responseStr;
|
||||||
|
serializeJson(response, responseStr);
|
||||||
|
sendResponse(responseStr, context);
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "LCD time sync: UTC=%lu, offset=%ld", utcTimestamp, totalOffset);
|
||||||
|
}
|
||||||
|
|
||||||
void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context) {
|
void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
// Validate that we have at least one parameter to update
|
// Validate that we have at least one parameter to update
|
||||||
@@ -854,7 +935,7 @@ void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const M
|
|||||||
hostname = newHostname;
|
hostname = newHostname;
|
||||||
configChanged = true;
|
configChanged = true;
|
||||||
needsReboot = true;
|
needsReboot = true;
|
||||||
LOG_INFO("Hostname will be updated to: %s", hostname.c_str());
|
LOG_INFO(TAG, "Hostname will be updated to: %s", hostname.c_str());
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_network_config", "Invalid hostname (must be 1-32 characters)", context);
|
sendErrorResponse("set_network_config", "Invalid hostname (must be 1-32 characters)", context);
|
||||||
return;
|
return;
|
||||||
@@ -896,9 +977,9 @@ void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const M
|
|||||||
dns2.fromString(contents["dns2"].as<String>());
|
dns2.fromString(contents["dns2"].as<String>());
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Static IP configuration will be applied: %s", ip.toString().c_str());
|
LOG_INFO(TAG, "Static IP configuration will be applied: %s", ip.toString().c_str());
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("DHCP mode will be enabled");
|
LOG_INFO(TAG, "DHCP mode will be enabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -917,10 +998,10 @@ void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const M
|
|||||||
responseMsg += ". RESTART DEVICE to apply changes";
|
responseMsg += ". RESTART DEVICE to apply changes";
|
||||||
}
|
}
|
||||||
sendSuccessResponse("set_network_config", responseMsg, context);
|
sendSuccessResponse("set_network_config", responseMsg, context);
|
||||||
LOG_INFO("✅ Network configuration saved to SD card");
|
LOG_INFO(TAG, "✅ Network configuration saved to SD card");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("set_network_config", "Configuration updated but failed to save to SD card", context);
|
sendErrorResponse("set_network_config", "Configuration updated but failed to save to SD card", context);
|
||||||
LOG_ERROR("❌ Failed to save network configuration to SD card");
|
LOG_ERROR(TAG, "❌ Failed to save network configuration to SD card");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sendSuccessResponse("set_network_config", "No changes detected", context);
|
sendSuccessResponse("set_network_config", "No changes detected", context);
|
||||||
@@ -928,10 +1009,10 @@ void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const M
|
|||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
sendErrorResponse("set_network_config", String("Exception: ") + e.what(), context);
|
sendErrorResponse("set_network_config", String("Exception: ") + e.what(), context);
|
||||||
LOG_ERROR("Exception in handleSetNetworkConfigCommand: %s", e.what());
|
LOG_ERROR(TAG, "Exception in handleSetNetworkConfigCommand: %s", e.what());
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("set_network_config", "Unknown error occurred", context);
|
sendErrorResponse("set_network_config", "Unknown error occurred", context);
|
||||||
LOG_ERROR("Unknown exception in handleSetNetworkConfigCommand");
|
LOG_ERROR(TAG, "Unknown exception in handleSetNetworkConfigCommand");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -941,7 +1022,7 @@ void CommandHandler::handleSetNetworkConfigCommand(JsonVariant contents, const M
|
|||||||
|
|
||||||
void CommandHandler::handleResetDefaultsCommand(const MessageContext& context) {
|
void CommandHandler::handleResetDefaultsCommand(const MessageContext& context) {
|
||||||
|
|
||||||
LOG_WARNING("⚠️ Factory reset requested. Proceeding...");
|
LOG_WARNING(TAG, "⚠️ Factory reset requested. Proceeding...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Reset all configurations to defaults
|
// Reset all configurations to defaults
|
||||||
@@ -949,14 +1030,14 @@ void CommandHandler::handleResetDefaultsCommand(const MessageContext& context) {
|
|||||||
|
|
||||||
if (resetComplete) {
|
if (resetComplete) {
|
||||||
sendSuccessResponse("reset_defaults", "Reset to Defaults completed. Device will Restart to apply changes.", context);
|
sendSuccessResponse("reset_defaults", "Reset to Defaults completed. Device will Restart to apply changes.", context);
|
||||||
LOG_WARNING("✅ Factory reset completed and all configurations saved to SD card");
|
LOG_WARNING(TAG, "✅ Factory reset completed and all configurations saved to SD card");
|
||||||
} else {
|
} else {
|
||||||
sendErrorResponse("reset_defaults", "Reset to Defaults applied but failed to save some configurations to SD card", context);
|
sendErrorResponse("reset_defaults", "Reset to Defaults applied but failed to save some configurations to SD card", context);
|
||||||
LOG_ERROR("❌ Reset to Defaults applied but failed to save some configurations to SD card");
|
LOG_ERROR(TAG, "❌ Reset to Defaults applied but failed to save some configurations to SD card");
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
sendErrorResponse("reset_defaults", "Failed to perform Reset to Defaults", context);
|
sendErrorResponse("reset_defaults", "Failed to perform Reset to Defaults", context);
|
||||||
LOG_ERROR("❌ Exception occurred during Resetting to Defaults");
|
LOG_ERROR(TAG, "❌ Exception occurred during Resetting to Defaults");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -974,7 +1055,7 @@ void CommandHandler::handleSystemCommand(JsonVariant contents, const MessageCont
|
|||||||
}
|
}
|
||||||
|
|
||||||
String action = contents["action"];
|
String action = contents["action"];
|
||||||
LOG_DEBUG("Processing system action: %s", action.c_str());
|
LOG_DEBUG(TAG, "Processing system action: %s", action.c_str());
|
||||||
|
|
||||||
if (action == "status") {
|
if (action == "status") {
|
||||||
handleStatusCommand(context);
|
handleStatusCommand(context);
|
||||||
@@ -988,9 +1069,260 @@ void CommandHandler::handleSystemCommand(JsonVariant contents, const MessageCont
|
|||||||
handleGetFirmwareStatusCommand(context);
|
handleGetFirmwareStatusCommand(context);
|
||||||
} else if (action == "set_network_config") {
|
} else if (action == "set_network_config") {
|
||||||
handleSetNetworkConfigCommand(contents, context);
|
handleSetNetworkConfigCommand(contents, context);
|
||||||
|
} else if (action == "set_serial_log_level") {
|
||||||
|
handleSetSerialLogLevelCommand(contents, context);
|
||||||
|
} else if (action == "set_sd_log_level") {
|
||||||
|
handleSetSdLogLevelCommand(contents, context);
|
||||||
|
} else if (action == "set_mqtt_log_level") {
|
||||||
|
handleSetMqttLogLevelCommand(contents, context);
|
||||||
|
} else if (action == "set_mqtt_enabled") {
|
||||||
|
handleSetMqttEnabledCommand(contents, context);
|
||||||
|
} else if (action == "restart" || action == "reboot") {
|
||||||
|
handleRestartCommand(context);
|
||||||
|
} else if (action == "force_update") {
|
||||||
|
handleForceUpdateCommand(contents, context);
|
||||||
|
} else if (action == "custom_update") {
|
||||||
|
handleCustomUpdateCommand(contents, context);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown system action: %s", action.c_str());
|
LOG_WARNING(TAG, "Unknown system action: %s", action.c_str());
|
||||||
sendErrorResponse("system", "Unknown action: " + action, context);
|
sendErrorResponse("system", "Unknown action: " + action, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// LOG LEVEL COMMANDS
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void CommandHandler::handleSetSerialLogLevelCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
|
if (!contents.containsKey("level")) {
|
||||||
|
sendErrorResponse("set_serial_log_level", "Missing level parameter", context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t level = contents["level"].as<uint8_t>();
|
||||||
|
|
||||||
|
// Set the level in ConfigManager
|
||||||
|
if (_configManager.setSerialLogLevel(level)) {
|
||||||
|
// Apply the level to Logging immediately
|
||||||
|
Logging::setLevel((Logging::LogLevel)level);
|
||||||
|
|
||||||
|
// Save to SD card
|
||||||
|
bool saved = _configManager.saveGeneralConfig();
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
sendSuccessResponse("set_serial_log_level",
|
||||||
|
"Serial log level set to " + String(level) + " and saved", context);
|
||||||
|
LOG_INFO(TAG, "Serial log level updated to %d", level);
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_serial_log_level",
|
||||||
|
"Log level set but failed to save to SD card", context);
|
||||||
|
LOG_ERROR(TAG, "Failed to save serial log level to SD card");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_serial_log_level",
|
||||||
|
"Invalid log level (must be 0-5)", context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandHandler::handleSetSdLogLevelCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
|
if (!contents.containsKey("level")) {
|
||||||
|
sendErrorResponse("set_sd_log_level", "Missing level parameter", context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t level = contents["level"].as<uint8_t>();
|
||||||
|
|
||||||
|
// Set the level in ConfigManager
|
||||||
|
if (_configManager.setSdLogLevel(level)) {
|
||||||
|
// Apply immediately
|
||||||
|
Logging::setSdLevel((Logging::LogLevel)level);
|
||||||
|
|
||||||
|
// Save to SD card
|
||||||
|
bool saved = _configManager.saveGeneralConfig();
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
sendSuccessResponse("set_sd_log_level",
|
||||||
|
"SD log level set to " + String(level) + " and saved", context);
|
||||||
|
LOG_INFO(TAG, "SD log level updated to %d", level);
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_sd_log_level",
|
||||||
|
"Log level set but failed to save to SD card", context);
|
||||||
|
LOG_ERROR(TAG, "Failed to save SD log level to SD card");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_sd_log_level",
|
||||||
|
"Invalid log level (must be 0-5)", context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandHandler::handleSetMqttLogLevelCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
|
if (!contents.containsKey("level")) {
|
||||||
|
sendErrorResponse("set_mqtt_log_level", "Missing level parameter", context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t level = contents["level"].as<uint8_t>();
|
||||||
|
|
||||||
|
// Set the level in ConfigManager
|
||||||
|
if (_configManager.setMqttLogLevel(level)) {
|
||||||
|
// Apply the level to Logging immediately
|
||||||
|
Logging::setMqttLogLevel((Logging::LogLevel)level);
|
||||||
|
|
||||||
|
// Save to SD card
|
||||||
|
bool saved = _configManager.saveGeneralConfig();
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
sendSuccessResponse("set_mqtt_log_level",
|
||||||
|
"MQTT log level set to " + String(level) + " and saved", context);
|
||||||
|
LOG_INFO(TAG, "MQTT log level updated to %d", level);
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_mqtt_log_level",
|
||||||
|
"Log level set but failed to save to SD card", context);
|
||||||
|
LOG_ERROR(TAG, "Failed to save MQTT log level to SD card");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_mqtt_log_level",
|
||||||
|
"Invalid log level (must be 0-5)", context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommandHandler::handleSetMqttEnabledCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
|
if (!contents.containsKey("enabled")) {
|
||||||
|
sendErrorResponse("set_mqtt_enabled", "Missing enabled parameter", context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool enabled = contents["enabled"].as<bool>();
|
||||||
|
|
||||||
|
// Set MQTT enabled state in ConfigManager
|
||||||
|
_configManager.setMqttEnabled(enabled);
|
||||||
|
|
||||||
|
// Save to SD card
|
||||||
|
bool saved = _configManager.saveGeneralConfig();
|
||||||
|
|
||||||
|
if (saved) {
|
||||||
|
sendSuccessResponse("set_mqtt_enabled",
|
||||||
|
String("MQTT ") + (enabled ? "enabled" : "disabled") + " and saved", context);
|
||||||
|
LOG_INFO(TAG, "MQTT %s by user command", enabled ? "enabled" : "disabled");
|
||||||
|
|
||||||
|
// If disabling, disconnect MQTT immediately
|
||||||
|
// If enabling, trigger connection attempt
|
||||||
|
if (_communicationRouter) {
|
||||||
|
if (!enabled) {
|
||||||
|
_communicationRouter->getMQTTClient().disconnect();
|
||||||
|
} else {
|
||||||
|
_communicationRouter->getMQTTClient().connect();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(TAG, "CommunicationRouter reference not set - cannot control MQTT");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendErrorResponse("set_mqtt_enabled",
|
||||||
|
"MQTT state changed but failed to save to SD card", context);
|
||||||
|
LOG_ERROR(TAG, "Failed to save MQTT enabled state to SD card");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// RESTART COMMAND
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void CommandHandler::handleRestartCommand(const MessageContext& context) {
|
||||||
|
LOG_WARNING(TAG, "🔄 Device restart requested via command");
|
||||||
|
sendSuccessResponse("restart", "Device will restart in 2 seconds", context);
|
||||||
|
|
||||||
|
// Small delay to ensure response is sent
|
||||||
|
delay(2000);
|
||||||
|
|
||||||
|
// Restart the ESP32
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// FORCE UPDATE COMMAND
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void CommandHandler::handleForceUpdateCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
|
LOG_WARNING(TAG, "🔄 Force OTA update requested via command");
|
||||||
|
|
||||||
|
// Check if player is active
|
||||||
|
if (_player && _player->isCurrentlyPlaying()) {
|
||||||
|
sendErrorResponse("force_update", "Cannot update while playback is active", context);
|
||||||
|
LOG_WARNING(TAG, "Force update rejected - player is active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get optional channel parameter (defaults to "stable")
|
||||||
|
String channel = "stable";
|
||||||
|
if (contents.containsKey("channel")) {
|
||||||
|
channel = contents["channel"].as<String>();
|
||||||
|
}
|
||||||
|
|
||||||
|
sendSuccessResponse("force_update",
|
||||||
|
"Starting forced OTA update from channel: " + channel + ". Device may reboot.", context);
|
||||||
|
|
||||||
|
// Small delay to ensure response is sent
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
// Perform the update
|
||||||
|
bool result = _otaManager.performManualUpdate(channel);
|
||||||
|
|
||||||
|
// Note: If update succeeds, device will reboot and this won't be reached
|
||||||
|
if (!result) {
|
||||||
|
LOG_ERROR(TAG, "Force update failed");
|
||||||
|
// Error response may not be received if we already restarted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CUSTOM UPDATE COMMAND
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const MessageContext& context) {
|
||||||
|
LOG_WARNING(TAG, "🔥 Custom OTA update requested via command");
|
||||||
|
|
||||||
|
// Validate required parameters
|
||||||
|
if (!contents.containsKey("firmware_url")) {
|
||||||
|
sendErrorResponse("custom_update", "Missing firmware_url parameter", context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String firmwareUrl = contents["firmware_url"].as<String>();
|
||||||
|
|
||||||
|
// Optional parameters
|
||||||
|
String checksum = contents.containsKey("checksum") ?
|
||||||
|
contents["checksum"].as<String>() : "";
|
||||||
|
size_t fileSize = contents.containsKey("file_size") ?
|
||||||
|
contents["file_size"].as<size_t>() : 0;
|
||||||
|
uint16_t version = contents.containsKey("version") ?
|
||||||
|
contents["version"].as<uint16_t>() : 0;
|
||||||
|
|
||||||
|
// Check if player is active
|
||||||
|
if (_player && _player->isCurrentlyPlaying()) {
|
||||||
|
sendErrorResponse("custom_update", "Cannot update while playback is active", context);
|
||||||
|
LOG_WARNING(TAG, "Custom update rejected - player is active");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "Custom update: URL=%s, Checksum=%s, Size=%u, Version=%u",
|
||||||
|
firmwareUrl.c_str(),
|
||||||
|
checksum.isEmpty() ? "none" : checksum.c_str(),
|
||||||
|
fileSize,
|
||||||
|
version);
|
||||||
|
|
||||||
|
sendSuccessResponse("custom_update",
|
||||||
|
"Starting custom OTA update. Device may reboot.", context);
|
||||||
|
|
||||||
|
// Small delay to ensure response is sent
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
// Perform the custom update
|
||||||
|
bool result = _otaManager.performCustomUpdate(firmwareUrl, checksum, fileSize, version);
|
||||||
|
|
||||||
|
// Note: If update succeeds, device will reboot and this won't be reached
|
||||||
|
if (!result) {
|
||||||
|
LOG_ERROR(TAG, "Custom update failed");
|
||||||
|
// Error response may not be received if we already restarted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,16 @@ class FileManager;
|
|||||||
class Timekeeper;
|
class Timekeeper;
|
||||||
class FirmwareValidator;
|
class FirmwareValidator;
|
||||||
class ClientManager;
|
class ClientManager;
|
||||||
|
class Telemetry;
|
||||||
|
class CommunicationRouter;
|
||||||
|
|
||||||
class CommandHandler {
|
class CommandHandler {
|
||||||
public:
|
public:
|
||||||
// Message source identification
|
// Message source identification
|
||||||
enum class MessageSource {
|
enum class MessageSource {
|
||||||
MQTT,
|
MQTT,
|
||||||
WEBSOCKET
|
WEBSOCKET,
|
||||||
|
UART
|
||||||
};
|
};
|
||||||
|
|
||||||
struct MessageContext {
|
struct MessageContext {
|
||||||
@@ -65,6 +68,8 @@ public:
|
|||||||
void setTimeKeeperReference(Timekeeper* tk);
|
void setTimeKeeperReference(Timekeeper* tk);
|
||||||
void setFirmwareValidatorReference(FirmwareValidator* fv);
|
void setFirmwareValidatorReference(FirmwareValidator* fv);
|
||||||
void setClientManagerReference(ClientManager* cm);
|
void setClientManagerReference(ClientManager* cm);
|
||||||
|
void setTelemetryReference(Telemetry* telemetry);
|
||||||
|
void setCommunicationRouterReference(CommunicationRouter* comm);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Set response callback for sending responses back
|
* @brief Set response callback for sending responses back
|
||||||
@@ -87,6 +92,8 @@ private:
|
|||||||
Timekeeper* _timeKeeper;
|
Timekeeper* _timeKeeper;
|
||||||
FirmwareValidator* _firmwareValidator;
|
FirmwareValidator* _firmwareValidator;
|
||||||
ClientManager* _clientManager;
|
ClientManager* _clientManager;
|
||||||
|
Telemetry* _telemetry;
|
||||||
|
CommunicationRouter* _communicationRouter;
|
||||||
ResponseCallback _responseCallback;
|
ResponseCallback _responseCallback;
|
||||||
|
|
||||||
// Response helpers
|
// Response helpers
|
||||||
@@ -131,11 +138,26 @@ private:
|
|||||||
void handleCommitFirmwareCommand(const MessageContext& context);
|
void handleCommitFirmwareCommand(const MessageContext& context);
|
||||||
void handleRollbackFirmwareCommand(const MessageContext& context);
|
void handleRollbackFirmwareCommand(const MessageContext& context);
|
||||||
void handleGetFirmwareStatusCommand(const MessageContext& context);
|
void handleGetFirmwareStatusCommand(const MessageContext& context);
|
||||||
|
void handleNetworkInfoCommand(const MessageContext& context);
|
||||||
void handleGetFullSettingsCommand(const MessageContext& context);
|
void handleGetFullSettingsCommand(const MessageContext& context);
|
||||||
|
void handleSyncTimeToLcdCommand(const MessageContext& context);
|
||||||
|
|
||||||
// Network configuration
|
// Network configuration
|
||||||
void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context);
|
void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context);
|
||||||
|
|
||||||
// System Config
|
// System Config
|
||||||
void handleResetDefaultsCommand(const MessageContext& context);
|
void handleResetDefaultsCommand(const MessageContext& context);
|
||||||
|
|
||||||
|
// Log Level Commands
|
||||||
|
void handleSetSerialLogLevelCommand(JsonVariant contents, const MessageContext& context);
|
||||||
|
void handleSetSdLogLevelCommand(JsonVariant contents, const MessageContext& context);
|
||||||
|
void handleSetMqttLogLevelCommand(JsonVariant contents, const MessageContext& context);
|
||||||
|
|
||||||
|
// MQTT Control Commands
|
||||||
|
void handleSetMqttEnabledCommand(JsonVariant contents, const MessageContext& context);
|
||||||
|
|
||||||
|
// Device Control Commands
|
||||||
|
void handleRestartCommand(const MessageContext& context);
|
||||||
|
void handleForceUpdateCommand(JsonVariant contents, const MessageContext& context);
|
||||||
|
void handleCustomUpdateCommand(JsonVariant contents, const MessageContext& context);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "CommunicationRouter.hpp"
|
#include "CommunicationRouter.hpp"
|
||||||
|
|
||||||
|
#define TAG "CommRouter"
|
||||||
#include "../../ConfigManager/ConfigManager.hpp"
|
#include "../../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../../OTAManager/OTAManager.hpp"
|
#include "../../OTAManager/OTAManager.hpp"
|
||||||
#include "../../Networking/Networking.hpp"
|
#include "../../Networking/Networking.hpp"
|
||||||
@@ -31,16 +33,19 @@ CommunicationRouter::CommunicationRouter(ConfigManager& configManager,
|
|||||||
, _mqttClient(configManager, networking)
|
, _mqttClient(configManager, networking)
|
||||||
, _clientManager()
|
, _clientManager()
|
||||||
, _wsServer(webSocket, _clientManager)
|
, _wsServer(webSocket, _clientManager)
|
||||||
, _commandHandler(configManager, otaManager) {}
|
, _commandHandler(configManager, otaManager)
|
||||||
|
, _httpHandler(server, configManager)
|
||||||
|
, _uartHandler()
|
||||||
|
, _settingsServer(server, configManager, networking) {}
|
||||||
|
|
||||||
CommunicationRouter::~CommunicationRouter() {}
|
CommunicationRouter::~CommunicationRouter() {}
|
||||||
|
|
||||||
void CommunicationRouter::begin() {
|
void CommunicationRouter::begin() {
|
||||||
LOG_INFO("Initializing Communication Router v4.0 (Modular)");
|
LOG_INFO(TAG, "Initializing Communication Router v4.0 (Modular)");
|
||||||
|
|
||||||
// 🔥 CRITICAL: Initialize WebSocket FIRST to ensure it's always set up
|
// 🔥 CRITICAL: Initialize WebSocket FIRST to ensure it's always set up
|
||||||
// Even if MQTT fails, we want WebSocket to work!
|
// Even if MQTT fails, we want WebSocket to work!
|
||||||
LOG_INFO("Setting up WebSocket server...");
|
LOG_INFO(TAG, "Setting up WebSocket server...");
|
||||||
|
|
||||||
// Initialize WebSocket server
|
// Initialize WebSocket server
|
||||||
_wsServer.begin();
|
_wsServer.begin();
|
||||||
@@ -51,30 +56,91 @@ void CommunicationRouter::begin() {
|
|||||||
// 🔥 CRITICAL FIX: Attach WebSocket handler to AsyncWebServer
|
// 🔥 CRITICAL FIX: Attach WebSocket handler to AsyncWebServer
|
||||||
// This MUST happen before any potential failures!
|
// This MUST happen before any potential failures!
|
||||||
_server.addHandler(&_webSocket);
|
_server.addHandler(&_webSocket);
|
||||||
LOG_INFO("✅ WebSocket handler attached to AsyncWebServer on /ws");
|
LOG_INFO(TAG, "✅ WebSocket handler attached to AsyncWebServer on /ws");
|
||||||
|
|
||||||
//Now initialize MQTT client (can fail without breaking WebSocket)
|
//Now initialize MQTT client (can fail without breaking WebSocket)
|
||||||
try {
|
try {
|
||||||
LOG_INFO("Setting up MQTT client...");
|
LOG_INFO(TAG, "Setting up MQTT client...");
|
||||||
_mqttClient.begin();
|
_mqttClient.begin();
|
||||||
_mqttClient.setCallback([this](const String& topic, const String& payload) {
|
_mqttClient.setCallback([this](const String& topic, const String& payload) {
|
||||||
onMqttMessage(topic, payload);
|
onMqttMessage(topic, payload);
|
||||||
});
|
});
|
||||||
LOG_INFO("✅ MQTT client initialized");
|
|
||||||
|
// Setup MQTT logging callback
|
||||||
|
String logTopic = "vesper/" + _configManager.getDeviceUID() + "/logs";
|
||||||
|
Logging::setMqttPublishCallback(
|
||||||
|
[this](const String& topic, const String& payload, int qos) {
|
||||||
|
_mqttClient.publish(topic, payload, qos, false);
|
||||||
|
},
|
||||||
|
logTopic
|
||||||
|
);
|
||||||
|
|
||||||
|
// Apply log levels from config for all three channels
|
||||||
|
Logging::setSerialLevel((Logging::LogLevel)_configManager.getSerialLogLevel());
|
||||||
|
Logging::setMqttLevel((Logging::LogLevel)_configManager.getMqttLogLevel());
|
||||||
|
Logging::setSdLevel((Logging::LogLevel)_configManager.getSdLogLevel());
|
||||||
|
LOG_INFO(TAG, "Log levels applied — Serial:%d MQTT:%d SD:%d",
|
||||||
|
_configManager.getSerialLogLevel(),
|
||||||
|
_configManager.getMqttLogLevel(),
|
||||||
|
_configManager.getSdLogLevel());
|
||||||
|
|
||||||
|
// Silence MQTT-internal subsystems on the MQTT channel to prevent log storms.
|
||||||
|
// These systems generate logs while sending logs — suppress them over MQTT only.
|
||||||
|
Logging::setSubsystemMqttLevel("MQTTClient", Logging::NONE);
|
||||||
|
Logging::setSubsystemMqttLevel("CommRouter", Logging::WARNING);
|
||||||
|
Logging::setSubsystemMqttLevel("Logger", Logging::NONE);
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "✅ MQTT client initialized");
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
LOG_ERROR("❌ MQTT initialization failed, but WebSocket is still available");
|
LOG_ERROR(TAG, "❌ MQTT initialization failed, but WebSocket is still available");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wire up SD logging channel (requires FileManager to be set first via setFileManagerReference)
|
||||||
|
// SD callback is registered lazily in setFileManagerReference once the pointer is available
|
||||||
|
|
||||||
// 🔥 CRITICAL FIX: Connect ClientManager to CommandHandler
|
// 🔥 CRITICAL FIX: Connect ClientManager to CommandHandler
|
||||||
_commandHandler.setClientManagerReference(&_clientManager);
|
_commandHandler.setClientManagerReference(&_clientManager);
|
||||||
LOG_INFO("ClientManager reference set for CommandHandler");
|
LOG_INFO(TAG, "ClientManager reference set for CommandHandler");
|
||||||
|
|
||||||
|
// 🔥 Set CommunicationRouter reference for MQTT control commands
|
||||||
|
_commandHandler.setCommunicationRouterReference(this);
|
||||||
|
LOG_INFO(TAG, "CommunicationRouter reference set for CommandHandler");
|
||||||
|
|
||||||
// Setup command handler response callback
|
// Setup command handler response callback
|
||||||
_commandHandler.setResponseCallback([this](const String& response, const CommandHandler::MessageContext& context) {
|
_commandHandler.setResponseCallback([this](const String& response, const CommandHandler::MessageContext& context) {
|
||||||
sendResponse(response, context);
|
sendResponse(response, context);
|
||||||
});
|
});
|
||||||
|
|
||||||
LOG_INFO("Communication Router initialized with modular architecture");
|
// Initialize HTTP Request Handler
|
||||||
|
LOG_INFO(TAG, "Setting up HTTP REST API...");
|
||||||
|
_httpHandler.begin();
|
||||||
|
_httpHandler.setCommandHandlerReference(&_commandHandler);
|
||||||
|
LOG_INFO(TAG, "✅ HTTP REST API initialized");
|
||||||
|
|
||||||
|
// Initialize Settings Web Server
|
||||||
|
LOG_INFO(TAG, "Setting up Settings Web Server...");
|
||||||
|
_settingsServer.begin();
|
||||||
|
LOG_INFO(TAG, "✅ Settings Web Server initialized at /settings");
|
||||||
|
|
||||||
|
// Initialize UART Command Handler
|
||||||
|
LOG_INFO(TAG, "Setting up UART Command Handler...");
|
||||||
|
_uartHandler.begin();
|
||||||
|
_uartHandler.setCallback([this](JsonDocument& message) {
|
||||||
|
onUartMessage(message);
|
||||||
|
});
|
||||||
|
LOG_INFO(TAG, "✅ UART Command Handler initialized (TX: GPIO12, RX: GPIO13)");
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "Communication Router initialized with modular architecture");
|
||||||
|
LOG_INFO(TAG, " • MQTT: AsyncMqttClient");
|
||||||
|
LOG_INFO(TAG, " • WebSocket: Multi-client support");
|
||||||
|
LOG_INFO(TAG, " • HTTP REST API: /api endpoints");
|
||||||
|
LOG_INFO(TAG, " • UART: External device control");
|
||||||
|
LOG_INFO(TAG, " • Settings Page: /settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommunicationRouter::loop() {
|
||||||
|
// Process UART incoming data
|
||||||
|
_uartHandler.loop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommunicationRouter::setPlayerReference(Player* player) {
|
void CommunicationRouter::setPlayerReference(Player* player) {
|
||||||
@@ -85,6 +151,14 @@ void CommunicationRouter::setPlayerReference(Player* player) {
|
|||||||
void CommunicationRouter::setFileManagerReference(FileManager* fm) {
|
void CommunicationRouter::setFileManagerReference(FileManager* fm) {
|
||||||
_fileManager = fm;
|
_fileManager = fm;
|
||||||
_commandHandler.setFileManagerReference(fm);
|
_commandHandler.setFileManagerReference(fm);
|
||||||
|
|
||||||
|
// Register SD log channel now that FileManager is available
|
||||||
|
if (fm != nullptr) {
|
||||||
|
Logging::setSdWriteCallback([fm](const String& line) {
|
||||||
|
fm->appendLine("/logs/vesper.log", line);
|
||||||
|
});
|
||||||
|
LOG_INFO(TAG, "SD log channel registered -> /logs/vesper.log");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommunicationRouter::setTimeKeeperReference(Timekeeper* tk) {
|
void CommunicationRouter::setTimeKeeperReference(Timekeeper* tk) {
|
||||||
@@ -97,14 +171,18 @@ void CommunicationRouter::setFirmwareValidatorReference(FirmwareValidator* fv) {
|
|||||||
_commandHandler.setFirmwareValidatorReference(fv);
|
_commandHandler.setFirmwareValidatorReference(fv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommunicationRouter::setTelemetryReference(Telemetry* telemetry) {
|
||||||
|
_commandHandler.setTelemetryReference(telemetry);
|
||||||
|
}
|
||||||
|
|
||||||
void CommunicationRouter::setupUdpDiscovery() {
|
void CommunicationRouter::setupUdpDiscovery() {
|
||||||
uint16_t discoveryPort = _configManager.getNetworkConfig().discoveryPort;
|
uint16_t discoveryPort = _configManager.getNetworkConfig().discoveryPort;
|
||||||
if (_udp.listen(discoveryPort)) {
|
if (_udp.listen(discoveryPort)) {
|
||||||
LOG_INFO("UDP discovery listening on port %u", discoveryPort);
|
LOG_INFO(TAG, "UDP discovery listening on port %u", discoveryPort);
|
||||||
|
|
||||||
_udp.onPacket([this](AsyncUDPPacket packet) {
|
_udp.onPacket([this](AsyncUDPPacket packet) {
|
||||||
String msg = String((const char*)packet.data(), packet.length());
|
String msg = String((const char*)packet.data(), packet.length());
|
||||||
LOG_DEBUG("UDP from %s:%u -> %s",
|
LOG_DEBUG(TAG, "UDP from %s:%u -> %s",
|
||||||
packet.remoteIP().toString().c_str(),
|
packet.remoteIP().toString().c_str(),
|
||||||
packet.remotePort(),
|
packet.remotePort(),
|
||||||
msg.c_str());
|
msg.c_str());
|
||||||
@@ -117,7 +195,7 @@ void CommunicationRouter::setupUdpDiscovery() {
|
|||||||
StaticJsonDocument<128> req;
|
StaticJsonDocument<128> req;
|
||||||
DeserializationError err = deserializeJson(req, msg);
|
DeserializationError err = deserializeJson(req, msg);
|
||||||
if (!err) {
|
if (!err) {
|
||||||
shouldReply = (req["op"] == "discover" && req["svc"] == "vesper");
|
shouldReply = (req["op"] == "discover");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +210,7 @@ void CommunicationRouter::setupUdpDiscovery() {
|
|||||||
doc["id"] = _configManager.getDeviceUID();
|
doc["id"] = _configManager.getDeviceUID();
|
||||||
doc["ip"] = _networking.getLocalIP();
|
doc["ip"] = _networking.getLocalIP();
|
||||||
char wsUrl[64];
|
char wsUrl[64];
|
||||||
snprintf(wsUrl, sizeof(wsUrl), "ws://%s/ws", _networking.getLocalIP().c_str());
|
snprintf(wsUrl, sizeof(wsUrl), "ws://%s:80/ws", _networking.getLocalIP().c_str());
|
||||||
doc["ws"] = wsUrl;
|
doc["ws"] = wsUrl;
|
||||||
doc["port"] = 80;
|
doc["port"] = 80;
|
||||||
doc["fw"] = "2.0";
|
doc["fw"] = "2.0";
|
||||||
@@ -145,7 +223,7 @@ void CommunicationRouter::setupUdpDiscovery() {
|
|||||||
packet.remoteIP(), packet.remotePort());
|
packet.remoteIP(), packet.remotePort());
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Failed to start UDP discovery.");
|
LOG_ERROR(TAG, "Failed to start UDP discovery.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,19 +242,21 @@ size_t CommunicationRouter::getWebSocketClientCount() const {
|
|||||||
bool CommunicationRouter::isHealthy() const {
|
bool CommunicationRouter::isHealthy() const {
|
||||||
// Check if required references are set
|
// Check if required references are set
|
||||||
if (!_player || !_fileManager || !_timeKeeper) {
|
if (!_player || !_fileManager || !_timeKeeper) {
|
||||||
LOG_DEBUG("CommunicationRouter: Unhealthy - Missing references");
|
LOG_WARNING(TAG, "Unhealthy - missing subsystem references (player=%d fileManager=%d timeKeeper=%d)",
|
||||||
|
_player != nullptr, _fileManager != nullptr, _timeKeeper != nullptr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check network connectivity first — no point checking connections without a network
|
||||||
|
if (!_networking.isConnected()) {
|
||||||
|
LOG_WARNING(TAG, "Unhealthy - no network connection");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if at least one protocol is connected
|
// Check if at least one protocol is connected
|
||||||
if (!isMqttConnected() && !hasActiveWebSocketClients()) {
|
if (!isMqttConnected() && !hasActiveWebSocketClients()) {
|
||||||
LOG_DEBUG("CommunicationRouter: Unhealthy - No active connections");
|
LOG_WARNING(TAG, "Unhealthy - no active connections (MQTT=%d, WebSocket=%d)",
|
||||||
return false;
|
isMqttConnected(), hasActiveWebSocketClients());
|
||||||
}
|
|
||||||
|
|
||||||
// Check network connectivity
|
|
||||||
if (!_networking.isConnected()) {
|
|
||||||
LOG_DEBUG("CommunicationRouter: Unhealthy - No network connection");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,9 +295,9 @@ void CommunicationRouter::broadcastToAllWebSocketClients(const JsonDocument& mes
|
|||||||
void CommunicationRouter::publishToMqtt(const String& data) {
|
void CommunicationRouter::publishToMqtt(const String& data) {
|
||||||
if (_mqttClient.isConnected()) {
|
if (_mqttClient.isConnected()) {
|
||||||
_mqttClient.publish("data", data, 0, false);
|
_mqttClient.publish("data", data, 0, false);
|
||||||
LOG_DEBUG("Published to MQTT: %s", data.c_str());
|
LOG_DEBUG(TAG, "Published to MQTT: %s", data.c_str());
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("MQTT Not Connected! Message Failed: %s", data.c_str());
|
LOG_ERROR(TAG, "MQTT Not Connected! Message Failed: %s", data.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,29 +319,29 @@ void CommunicationRouter::sendBellOverloadNotification(const std::vector<uint8_t
|
|||||||
overloadMsg["payload"]["severity"] = severity;
|
overloadMsg["payload"]["severity"] = severity;
|
||||||
broadcastStatus(overloadMsg);
|
broadcastStatus(overloadMsg);
|
||||||
|
|
||||||
LOG_WARNING("Bell overload notification sent: %d bells, severity: %s",
|
LOG_WARNING(TAG, "Bell overload notification sent: %d bells, severity: %s",
|
||||||
bellNumbers.size(), severity.c_str());
|
bellNumbers.size(), severity.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommunicationRouter::onNetworkConnected() {
|
void CommunicationRouter::onNetworkConnected() {
|
||||||
LOG_DEBUG("Network connected - notifying MQTT client");
|
LOG_DEBUG(TAG, "Network connected - notifying MQTT client");
|
||||||
_mqttClient.onNetworkConnected();
|
_mqttClient.onNetworkConnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommunicationRouter::onNetworkDisconnected() {
|
void CommunicationRouter::onNetworkDisconnected() {
|
||||||
LOG_DEBUG("Network disconnected - notifying MQTT client");
|
LOG_DEBUG(TAG, "Network disconnected - notifying MQTT client");
|
||||||
_mqttClient.onNetworkDisconnected();
|
_mqttClient.onNetworkDisconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommunicationRouter::onMqttMessage(const String& topic, const String& payload) {
|
void CommunicationRouter::onMqttMessage(const String& topic, const String& payload) {
|
||||||
LOG_DEBUG("MQTT message received: %s", payload.c_str());
|
LOG_DEBUG(TAG, "MQTT message received: %s", payload.c_str());
|
||||||
|
|
||||||
// Parse JSON
|
// Parse JSON
|
||||||
StaticJsonDocument<2048> doc;
|
StaticJsonDocument<2048> doc;
|
||||||
DeserializationError error = deserializeJson(doc, payload);
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("Failed to parse MQTT JSON: %s", error.c_str());
|
LOG_ERROR(TAG, "Failed to parse MQTT JSON: %s", error.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +355,7 @@ void CommunicationRouter::onMqttMessage(const String& topic, const String& paylo
|
|||||||
void CommunicationRouter::onWebSocketMessage(uint32_t clientId, const JsonDocument& message) {
|
void CommunicationRouter::onWebSocketMessage(uint32_t clientId, const JsonDocument& message) {
|
||||||
// Extract command for logging
|
// Extract command for logging
|
||||||
String cmd = message["cmd"] | "unknown";
|
String cmd = message["cmd"] | "unknown";
|
||||||
LOG_INFO("📨 WebSocket message from client #%u: cmd=%s", clientId, cmd.c_str());
|
LOG_INFO(TAG, "📨 WebSocket message from client #%u: cmd=%s", clientId, cmd.c_str());
|
||||||
|
|
||||||
// Create message context for WebSocket with client ID
|
// Create message context for WebSocket with client ID
|
||||||
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, clientId);
|
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, clientId);
|
||||||
@@ -284,17 +364,54 @@ void CommunicationRouter::onWebSocketMessage(uint32_t clientId, const JsonDocume
|
|||||||
JsonDocument& mutableDoc = const_cast<JsonDocument&>(message);
|
JsonDocument& mutableDoc = const_cast<JsonDocument&>(message);
|
||||||
_commandHandler.processCommand(mutableDoc, context);
|
_commandHandler.processCommand(mutableDoc, context);
|
||||||
|
|
||||||
LOG_DEBUG("WebSocket message from client #%u processed", clientId);
|
LOG_DEBUG(TAG, "WebSocket message from client #%u processed", clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CommunicationRouter::onUartMessage(JsonDocument& message) {
|
||||||
|
// Extract command and action for filtering
|
||||||
|
String cmd = message["cmd"] | "";
|
||||||
|
String action = message["contents"]["action"] | "";
|
||||||
|
|
||||||
|
// UART COMMAND WHITELIST: Only allow specific commands
|
||||||
|
// This prevents feedback loops between devices when bad messages occur.
|
||||||
|
// To re-enable full UART command support, remove this filter.
|
||||||
|
bool allowed = false;
|
||||||
|
|
||||||
|
if (cmd == "system_info" && action == "sync_time_to_lcd") {
|
||||||
|
allowed = true;
|
||||||
|
} else if (cmd == "playback" && (action == "play" || action == "stop")) {
|
||||||
|
allowed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowed) {
|
||||||
|
// Silently ignore - do NOT send error response to avoid feedback loop
|
||||||
|
LOG_DEBUG(TAG, "UART: Ignoring non-whitelisted command (cmd=%s, action=%s)",
|
||||||
|
cmd.c_str(), action.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "🔌 UART command received: cmd=%s, action=%s", cmd.c_str(), action.c_str());
|
||||||
|
|
||||||
|
// Create message context for UART
|
||||||
|
CommandHandler::MessageContext context(CommandHandler::MessageSource::UART);
|
||||||
|
|
||||||
|
// Forward to command handler
|
||||||
|
_commandHandler.processCommand(message, context);
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "UART message processed");
|
||||||
}
|
}
|
||||||
|
|
||||||
void CommunicationRouter::sendResponse(const String& response, const CommandHandler::MessageContext& context) {
|
void CommunicationRouter::sendResponse(const String& response, const CommandHandler::MessageContext& context) {
|
||||||
if (context.source == CommandHandler::MessageSource::MQTT) {
|
if (context.source == CommandHandler::MessageSource::MQTT) {
|
||||||
LOG_DEBUG("↗️ Sending response via MQTT: %s", response.c_str());
|
LOG_DEBUG(TAG, "↗️ Sending response via MQTT: %s", response.c_str());
|
||||||
publishToMqtt(response);
|
publishToMqtt(response);
|
||||||
} else if (context.source == CommandHandler::MessageSource::WEBSOCKET) {
|
} else if (context.source == CommandHandler::MessageSource::WEBSOCKET) {
|
||||||
LOG_DEBUG("↗️ Sending response to WebSocket client #%u: %s", context.clientId, response.c_str());
|
LOG_DEBUG(TAG, "↗️ Sending response to WebSocket client #%u: %s", context.clientId, response.c_str());
|
||||||
_wsServer.sendToClient(context.clientId, response);
|
_wsServer.sendToClient(context.clientId, response);
|
||||||
|
} else if (context.source == CommandHandler::MessageSource::UART) {
|
||||||
|
LOG_DEBUG(TAG, "↗️ Sending response via UART: %s", response.c_str());
|
||||||
|
_uartHandler.send(response);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Unknown message source for response routing!");
|
LOG_ERROR(TAG, "❌ Unknown message source for response routing!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,10 @@
|
|||||||
#include "../WebSocketServer/WebSocketServer.hpp"
|
#include "../WebSocketServer/WebSocketServer.hpp"
|
||||||
#include "../CommandHandler/CommandHandler.hpp"
|
#include "../CommandHandler/CommandHandler.hpp"
|
||||||
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
||||||
|
#include "../HTTPRequestHandler/HTTPRequestHandler.hpp"
|
||||||
|
#include "../UARTCommandHandler/UARTCommandHandler.hpp"
|
||||||
#include "../../ClientManager/ClientManager.hpp"
|
#include "../../ClientManager/ClientManager.hpp"
|
||||||
|
#include "../../SettingsWebServer/SettingsWebServer.hpp"
|
||||||
|
|
||||||
class ConfigManager;
|
class ConfigManager;
|
||||||
class OTAManager;
|
class OTAManager;
|
||||||
@@ -47,6 +50,7 @@ class FileManager;
|
|||||||
class Timekeeper;
|
class Timekeeper;
|
||||||
class Networking;
|
class Networking;
|
||||||
class FirmwareValidator;
|
class FirmwareValidator;
|
||||||
|
class Telemetry;
|
||||||
|
|
||||||
class CommunicationRouter {
|
class CommunicationRouter {
|
||||||
public:
|
public:
|
||||||
@@ -60,10 +64,12 @@ public:
|
|||||||
~CommunicationRouter();
|
~CommunicationRouter();
|
||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
|
void loop(); // Must be called from main loop for UART processing
|
||||||
void setPlayerReference(Player* player);
|
void setPlayerReference(Player* player);
|
||||||
void setFileManagerReference(FileManager* fm);
|
void setFileManagerReference(FileManager* fm);
|
||||||
void setTimeKeeperReference(Timekeeper* tk);
|
void setTimeKeeperReference(Timekeeper* tk);
|
||||||
void setFirmwareValidatorReference(FirmwareValidator* fv);
|
void setFirmwareValidatorReference(FirmwareValidator* fv);
|
||||||
|
void setTelemetryReference(Telemetry* telemetry);
|
||||||
void setupUdpDiscovery();
|
void setupUdpDiscovery();
|
||||||
|
|
||||||
// Status methods
|
// Status methods
|
||||||
@@ -72,6 +78,10 @@ public:
|
|||||||
size_t getWebSocketClientCount() const;
|
size_t getWebSocketClientCount() const;
|
||||||
bool isHealthy() const;
|
bool isHealthy() const;
|
||||||
|
|
||||||
|
// Component accessors
|
||||||
|
MQTTAsyncClient& getMQTTClient() { return _mqttClient; }
|
||||||
|
UARTCommandHandler& getUARTHandler() { return _uartHandler; }
|
||||||
|
|
||||||
// Broadcast methods
|
// Broadcast methods
|
||||||
void broadcastStatus(const String& statusMessage);
|
void broadcastStatus(const String& statusMessage);
|
||||||
void broadcastStatus(const JsonDocument& statusJson);
|
void broadcastStatus(const JsonDocument& statusJson);
|
||||||
@@ -108,10 +118,14 @@ private:
|
|||||||
ClientManager _clientManager;
|
ClientManager _clientManager;
|
||||||
WebSocketServer _wsServer;
|
WebSocketServer _wsServer;
|
||||||
CommandHandler _commandHandler;
|
CommandHandler _commandHandler;
|
||||||
|
HTTPRequestHandler _httpHandler;
|
||||||
|
UARTCommandHandler _uartHandler;
|
||||||
|
SettingsWebServer _settingsServer;
|
||||||
|
|
||||||
// Message handlers
|
// Message handlers
|
||||||
void onMqttMessage(const String& topic, const String& payload);
|
void onMqttMessage(const String& topic, const String& payload);
|
||||||
void onWebSocketMessage(uint32_t clientId, const JsonDocument& message);
|
void onWebSocketMessage(uint32_t clientId, const JsonDocument& message);
|
||||||
|
void onUartMessage(JsonDocument& message);
|
||||||
|
|
||||||
// Response routing
|
// Response routing
|
||||||
void sendResponse(const String& response, const CommandHandler::MessageContext& context);
|
void sendResponse(const String& response, const CommandHandler::MessageContext& context);
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* HTTPREQUESTHANDLER.CPP - HTTP REST API Request Handler Implementation
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "HTTPRequestHandler.hpp"
|
||||||
|
|
||||||
|
#define TAG "HTTPHandler"
|
||||||
|
#include "../CommandHandler/CommandHandler.hpp"
|
||||||
|
#include "../../ConfigManager/ConfigManager.hpp"
|
||||||
|
#include "../../Logging/Logging.hpp"
|
||||||
|
|
||||||
|
HTTPRequestHandler::HTTPRequestHandler(AsyncWebServer& server,
|
||||||
|
ConfigManager& configManager)
|
||||||
|
: _server(server)
|
||||||
|
, _configManager(configManager)
|
||||||
|
, _commandHandler(nullptr) {
|
||||||
|
}
|
||||||
|
|
||||||
|
HTTPRequestHandler::~HTTPRequestHandler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::begin() {
|
||||||
|
LOG_INFO(TAG, "HTTPRequestHandler - Initializing HTTP REST API endpoints");
|
||||||
|
|
||||||
|
// POST /api/command - Execute any command
|
||||||
|
_server.on("/api/command", HTTP_POST,
|
||||||
|
[](AsyncWebServerRequest* request) {
|
||||||
|
// This is called when request is complete but body is empty
|
||||||
|
request->send(400, "application/json", "{\"error\":\"No body provided\"}");
|
||||||
|
},
|
||||||
|
nullptr, // No file upload handler
|
||||||
|
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
|
||||||
|
// This is called for body data
|
||||||
|
if (index == 0) {
|
||||||
|
// First chunk - could allocate buffers if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index + len == total) {
|
||||||
|
// Last chunk - process the complete request
|
||||||
|
handleCommandRequest(request, data, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/status - Get system status
|
||||||
|
_server.on("/api/status", HTTP_GET,
|
||||||
|
[this](AsyncWebServerRequest* request) {
|
||||||
|
handleStatusRequest(request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /api/ping - Health check
|
||||||
|
_server.on("/api/ping", HTTP_GET,
|
||||||
|
[this](AsyncWebServerRequest* request) {
|
||||||
|
handlePingRequest(request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enable CORS for API endpoints (allows web apps to call the API)
|
||||||
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
|
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "HTTPRequestHandler - REST API endpoints registered");
|
||||||
|
LOG_INFO(TAG, " POST /api/command - Execute commands");
|
||||||
|
LOG_INFO(TAG, " GET /api/status - System status");
|
||||||
|
LOG_INFO(TAG, " GET /api/ping - Health check");
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::setCommandHandlerReference(CommandHandler* handler) {
|
||||||
|
_commandHandler = handler;
|
||||||
|
LOG_DEBUG(TAG, "HTTPRequestHandler - CommandHandler reference set");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HTTPRequestHandler::isHealthy() const {
|
||||||
|
// HTTP handler is healthy if it has been initialized with dependencies
|
||||||
|
return _commandHandler != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::handleCommandRequest(AsyncWebServerRequest* request, uint8_t* data, size_t len) {
|
||||||
|
if (!_commandHandler) {
|
||||||
|
sendErrorResponse(request, 503, "Command handler not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse JSON from body
|
||||||
|
JsonDocument doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, data, len);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
LOG_WARNING(TAG, "HTTPRequestHandler - JSON parse error: %s", error.c_str());
|
||||||
|
sendErrorResponse(request, 400, "Invalid JSON");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "HTTPRequestHandler - Processing command via HTTP");
|
||||||
|
|
||||||
|
// Create message context for HTTP (treat as WebSocket with special ID)
|
||||||
|
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, 0xFFFFFFFF);
|
||||||
|
|
||||||
|
// Capture request pointer for response
|
||||||
|
AsyncWebServerRequest* capturedRequest = request;
|
||||||
|
bool responseSent = false;
|
||||||
|
|
||||||
|
// Set temporary response callback to capture the response
|
||||||
|
auto originalCallback = [capturedRequest, &responseSent](const String& response, const CommandHandler::MessageContext& ctx) {
|
||||||
|
if (!responseSent && capturedRequest != nullptr) {
|
||||||
|
capturedRequest->send(200, "application/json", response);
|
||||||
|
responseSent = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Temporarily override the command handler's response callback
|
||||||
|
// Note: This requires the CommandHandler to support callback override
|
||||||
|
// For now, we'll process and let the normal flow handle it
|
||||||
|
|
||||||
|
// Process the command
|
||||||
|
_commandHandler->processCommand(doc, context);
|
||||||
|
|
||||||
|
// If no response was sent by the callback, send a generic success
|
||||||
|
if (!responseSent) {
|
||||||
|
sendJsonResponse(request, 200, "{\"status\":\"ok\",\"message\":\"Command processed\"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||||
|
if (!_commandHandler) {
|
||||||
|
sendErrorResponse(request, 503, "Command handler not initialized");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "HTTPRequestHandler - Status request via HTTP");
|
||||||
|
|
||||||
|
// Create a status command
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["group"] = "system";
|
||||||
|
doc["action"] = "status";
|
||||||
|
|
||||||
|
// Create message context
|
||||||
|
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, 0xFFFFFFFF);
|
||||||
|
|
||||||
|
// Capture request for response
|
||||||
|
AsyncWebServerRequest* capturedRequest = request;
|
||||||
|
bool responseSent = false;
|
||||||
|
|
||||||
|
// Process via command handler
|
||||||
|
_commandHandler->processCommand(doc, context);
|
||||||
|
|
||||||
|
// Fallback response if needed
|
||||||
|
if (!responseSent) {
|
||||||
|
JsonDocument response;
|
||||||
|
response["status"] = "ok";
|
||||||
|
response["device_uid"] = _configManager.getDeviceUID();
|
||||||
|
response["fw_version"] = _configManager.getFwVersion();
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(response, output);
|
||||||
|
sendJsonResponse(request, 200, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::handlePingRequest(AsyncWebServerRequest* request) {
|
||||||
|
LOG_DEBUG(TAG, "HTTPRequestHandler - Ping request via HTTP");
|
||||||
|
|
||||||
|
JsonDocument response;
|
||||||
|
response["status"] = "ok";
|
||||||
|
response["message"] = "pong";
|
||||||
|
response["uptime"] = millis();
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(response, output);
|
||||||
|
sendJsonResponse(request, 200, output);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::sendJsonResponse(AsyncWebServerRequest* request, int code, const String& json) {
|
||||||
|
request->send(code, "application/json", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HTTPRequestHandler::sendErrorResponse(AsyncWebServerRequest* request, int code, const String& error) {
|
||||||
|
JsonDocument doc;
|
||||||
|
doc["status"] = "error";
|
||||||
|
doc["error"] = error;
|
||||||
|
|
||||||
|
String output;
|
||||||
|
serializeJson(doc, output);
|
||||||
|
sendJsonResponse(request, code, output);
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* HTTPREQUESTHANDLER.HPP - HTTP REST API Request Handler
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 📡 HTTP REQUEST HANDLER FOR VESPER 📡
|
||||||
|
*
|
||||||
|
* Provides HTTP REST API endpoints alongside WebSocket/MQTT:
|
||||||
|
* • Operates side-by-side with WebSocket (not as fallback)
|
||||||
|
* • Same command structure as MQTT/WebSocket
|
||||||
|
* • Reliable request-response pattern
|
||||||
|
* • Works in both STA and AP modes
|
||||||
|
*
|
||||||
|
* 🏗️ ARCHITECTURE:
|
||||||
|
* • Uses AsyncWebServer for non-blocking operation
|
||||||
|
* • Routes HTTP POST requests to CommandHandler
|
||||||
|
* • Returns JSON responses
|
||||||
|
* • Thread-safe operation
|
||||||
|
*
|
||||||
|
* 📡 API ENDPOINTS:
|
||||||
|
* POST /api/command - Execute any VESPER command
|
||||||
|
* GET /api/status - Get system status
|
||||||
|
* GET /api/ping - Health check
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-12-28
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class CommandHandler;
|
||||||
|
class ConfigManager;
|
||||||
|
|
||||||
|
class HTTPRequestHandler {
|
||||||
|
public:
|
||||||
|
explicit HTTPRequestHandler(AsyncWebServer& server,
|
||||||
|
ConfigManager& configManager);
|
||||||
|
~HTTPRequestHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize HTTP request handler and register endpoints
|
||||||
|
*/
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set CommandHandler reference for processing commands
|
||||||
|
*/
|
||||||
|
void setCommandHandlerReference(CommandHandler* handler);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if HTTP handler is healthy
|
||||||
|
*/
|
||||||
|
bool isHealthy() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Dependencies
|
||||||
|
AsyncWebServer& _server;
|
||||||
|
ConfigManager& _configManager;
|
||||||
|
CommandHandler* _commandHandler;
|
||||||
|
|
||||||
|
// Endpoint handlers
|
||||||
|
void handleCommandRequest(AsyncWebServerRequest* request, uint8_t* data, size_t len);
|
||||||
|
void handleStatusRequest(AsyncWebServerRequest* request);
|
||||||
|
void handlePingRequest(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
void sendJsonResponse(AsyncWebServerRequest* request, int code, const String& json);
|
||||||
|
void sendErrorResponse(AsyncWebServerRequest* request, int code, const String& error);
|
||||||
|
};
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "MQTTAsyncClient.hpp"
|
#include "MQTTAsyncClient.hpp"
|
||||||
|
|
||||||
|
#define TAG "MQTTClient"
|
||||||
#include "../../ConfigManager/ConfigManager.hpp"
|
#include "../../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../../Networking/Networking.hpp"
|
#include "../../Networking/Networking.hpp"
|
||||||
#include "../../Logging/Logging.hpp"
|
#include "../../Logging/Logging.hpp"
|
||||||
@@ -13,30 +15,60 @@ MQTTAsyncClient::MQTTAsyncClient(ConfigManager& configManager, Networking& netwo
|
|||||||
: _configManager(configManager)
|
: _configManager(configManager)
|
||||||
, _networking(networking)
|
, _networking(networking)
|
||||||
, _messageCallback(nullptr)
|
, _messageCallback(nullptr)
|
||||||
, _mqttReconnectTimer(nullptr) {
|
, _mqttReconnectTimer(nullptr)
|
||||||
|
, _networkStabilizationTimer(nullptr)
|
||||||
|
, _heartbeatTimer(nullptr)
|
||||||
|
, _reconnectAttempts(0)
|
||||||
|
, _lastConnectionAttempt(0) {
|
||||||
|
|
||||||
_instance = this; // Set static instance pointer
|
_instance = this; // Set static instance pointer
|
||||||
|
|
||||||
// Create reconnection timer
|
// Create reconnection timer (initial delay will be calculated dynamically)
|
||||||
_mqttReconnectTimer = xTimerCreate(
|
_mqttReconnectTimer = xTimerCreate(
|
||||||
"mqttReconnect", // Timer name (for debugging)
|
"mqttReconnect", // Timer name (for debugging)
|
||||||
pdMS_TO_TICKS(MQTT_RECONNECT_DELAY), // Period: 5000ms = 5 seconds
|
pdMS_TO_TICKS(MQTT_RECONNECT_BASE_DELAY), // Initial period: 5000ms = 5 seconds
|
||||||
pdFALSE, // One-shot (false) or Auto-reload (true)
|
pdFALSE, // One-shot (false) or Auto-reload (true)
|
||||||
(void*)0, // Timer ID (can store data)
|
(void*)0, // Timer ID (can store data)
|
||||||
mqttReconnectTimerCallback // Callback function when timer expires
|
mqttReconnectTimerCallback // Callback function when timer expires
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create network stabilization timer (one-shot, 2 seconds)
|
||||||
|
_networkStabilizationTimer = xTimerCreate(
|
||||||
|
"mqttNetStable", // Timer name
|
||||||
|
pdMS_TO_TICKS(NETWORK_STABILIZATION_DELAY), // Period: 2000ms = 2 seconds
|
||||||
|
pdFALSE, // One-shot timer
|
||||||
|
(void*)0, // Timer ID
|
||||||
|
networkStabilizationTimerCallback // Callback function
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create heartbeat timer (auto-reload every 30 seconds)
|
||||||
|
_heartbeatTimer = xTimerCreate(
|
||||||
|
"mqttHeartbeat", // Timer name
|
||||||
|
pdMS_TO_TICKS(HEARTBEAT_INTERVAL), // Period: 30000ms = 30 seconds
|
||||||
|
pdTRUE, // Auto-reload (true) - repeating timer
|
||||||
|
(void*)0, // Timer ID
|
||||||
|
heartbeatTimerCallback // Callback function
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MQTTAsyncClient::~MQTTAsyncClient() {
|
MQTTAsyncClient::~MQTTAsyncClient() {
|
||||||
if (_mqttReconnectTimer) {
|
if (_mqttReconnectTimer) {
|
||||||
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
|
xTimerDelete(_mqttReconnectTimer, portMAX_DELAY);
|
||||||
}
|
}
|
||||||
|
if (_networkStabilizationTimer) {
|
||||||
|
xTimerStop(_networkStabilizationTimer, 0);
|
||||||
|
xTimerDelete(_networkStabilizationTimer, portMAX_DELAY);
|
||||||
|
}
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
xTimerStop(_heartbeatTimer, 0);
|
||||||
|
xTimerDelete(_heartbeatTimer, portMAX_DELAY);
|
||||||
|
}
|
||||||
_mqttClient.disconnect();
|
_mqttClient.disconnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::begin() {
|
void MQTTAsyncClient::begin() {
|
||||||
LOG_INFO("Initializing MQTT Async Client");
|
LOG_INFO(TAG, "Initializing MQTT Async Client");
|
||||||
|
|
||||||
auto& mqttConfig = _configManager.getMqttConfig();
|
auto& mqttConfig = _configManager.getMqttConfig();
|
||||||
|
|
||||||
@@ -46,7 +78,7 @@ void MQTTAsyncClient::begin() {
|
|||||||
_dataTopic = "vesper/" + deviceUID + "/data";
|
_dataTopic = "vesper/" + deviceUID + "/data";
|
||||||
_clientId = "vesper-" + deviceUID;
|
_clientId = "vesper-" + deviceUID;
|
||||||
|
|
||||||
LOG_INFO("MQTT Topics: control=%s, data=%s", _controlTopic.c_str(), _dataTopic.c_str());
|
LOG_INFO(TAG, "MQTT Topics: control=%s, data=%s", _controlTopic.c_str(), _dataTopic.c_str());
|
||||||
|
|
||||||
// Setup event handlers
|
// Setup event handlers
|
||||||
_mqttClient.onConnect([this](bool sessionPresent) {
|
_mqttClient.onConnect([this](bool sessionPresent) {
|
||||||
@@ -80,40 +112,54 @@ void MQTTAsyncClient::begin() {
|
|||||||
_mqttClient.setKeepAlive(15);
|
_mqttClient.setKeepAlive(15);
|
||||||
_mqttClient.setCleanSession(true);
|
_mqttClient.setCleanSession(true);
|
||||||
|
|
||||||
LOG_INFO("✅ MQTT Async Client initialized");
|
LOG_INFO(TAG, "✅ MQTT Async Client initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::connect() {
|
void MQTTAsyncClient::connect() {
|
||||||
if (_mqttClient.connected()) {
|
auto& mqttConfig = _configManager.getMqttConfig();
|
||||||
LOG_DEBUG("Already connected to MQTT");
|
|
||||||
|
// 🔥 Check if MQTT is enabled
|
||||||
|
if (!mqttConfig.enabled) {
|
||||||
|
LOG_DEBUG(TAG, "MQTT is disabled in configuration - skipping connection");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& mqttConfig = _configManager.getMqttConfig();
|
if (_mqttClient.connected()) {
|
||||||
|
LOG_DEBUG(TAG, "Already connected to MQTT");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("Free heap BEFORE MQTT connect: %d bytes", ESP.getFreeHeap());
|
// Track connection attempt
|
||||||
|
_lastConnectionAttempt = millis();
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "Free heap BEFORE MQTT connect: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
_mqttClient.connect();
|
_mqttClient.connect();
|
||||||
|
|
||||||
LOG_INFO("MQTT connect() called - waiting for async connection...");
|
LOG_INFO(TAG, "MQTT connect() called - waiting for async connection...");
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::disconnect() {
|
void MQTTAsyncClient::disconnect() {
|
||||||
_mqttClient.disconnect();
|
_mqttClient.disconnect();
|
||||||
LOG_INFO("Disconnected from MQTT broker");
|
LOG_INFO(TAG, "Disconnected from MQTT broker");
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, int qos, bool retain) {
|
uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, int qos, bool retain) {
|
||||||
|
// Check if connected before attempting to publish
|
||||||
|
if (!_mqttClient.connected()) {
|
||||||
|
// Don't log error here - would cause infinite loop with MQTT logging
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Build full topic (if relative)
|
// Build full topic (if relative)
|
||||||
String fullTopic = topic.startsWith("vesper/") ? topic : _dataTopic;
|
String fullTopic = topic.startsWith("vesper/") ? topic : _dataTopic;
|
||||||
|
|
||||||
uint16_t packetId = _mqttClient.publish(fullTopic.c_str(), qos, retain, payload.c_str());
|
uint16_t packetId = _mqttClient.publish(fullTopic.c_str(), qos, retain, payload.c_str());
|
||||||
|
|
||||||
if (packetId > 0) {
|
if (packetId > 0) {
|
||||||
LOG_DEBUG("Published to %s: %s (packetId=%d)", fullTopic.c_str(), payload.c_str(), packetId);
|
LOG_DEBUG(TAG, "Published to %s: %s (packetId=%d)", fullTopic.c_str(), payload.c_str(), packetId);
|
||||||
} else {
|
|
||||||
LOG_ERROR("Failed to publish to %s", fullTopic.c_str());
|
|
||||||
}
|
}
|
||||||
|
// REMOVED: Error logging here to prevent infinite recursion with MQTT logs
|
||||||
|
|
||||||
return packetId;
|
return packetId;
|
||||||
}
|
}
|
||||||
@@ -127,17 +173,32 @@ bool MQTTAsyncClient::isConnected() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onNetworkConnected() {
|
void MQTTAsyncClient::onNetworkConnected() {
|
||||||
LOG_DEBUG("Network connected - waiting 2 seconds for network stack to stabilize...");
|
auto& mqttConfig = _configManager.getMqttConfig();
|
||||||
|
|
||||||
// Small delay to ensure network stack is fully ready
|
// 🔥 Only attempt connection if MQTT is enabled
|
||||||
delay(2000);
|
if (!mqttConfig.enabled) {
|
||||||
|
LOG_DEBUG(TAG, "Network connected but MQTT is disabled - skipping MQTT connection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Network stable - connecting to MQTT");
|
LOG_DEBUG(TAG, "Network connected - scheduling MQTT connection after 2s stabilization (non-blocking)");
|
||||||
|
|
||||||
|
// Reset reconnect attempts on fresh network connection
|
||||||
|
_reconnectAttempts = 0;
|
||||||
|
|
||||||
|
// 🔥 CRITICAL FIX: Use non-blocking timer instead of delay()
|
||||||
|
// This prevents blocking UDP discovery, WebSocket connections, and async operations
|
||||||
|
if (_networkStabilizationTimer) {
|
||||||
|
xTimerStart(_networkStabilizationTimer, 0);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(TAG, "Network stabilization timer not initialized!");
|
||||||
|
// Fallback to immediate connection (better than blocking)
|
||||||
connect();
|
connect();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onNetworkDisconnected() {
|
void MQTTAsyncClient::onNetworkDisconnected() {
|
||||||
LOG_DEBUG("Network disconnected - MQTT will auto-reconnect when network returns");
|
LOG_DEBUG(TAG, "Network disconnected - MQTT will auto-reconnect when network returns");
|
||||||
|
|
||||||
if (_mqttClient.connected()) {
|
if (_mqttClient.connected()) {
|
||||||
_mqttClient.disconnect(true);
|
_mqttClient.disconnect(true);
|
||||||
@@ -146,18 +207,26 @@ void MQTTAsyncClient::onNetworkDisconnected() {
|
|||||||
|
|
||||||
void MQTTAsyncClient::subscribe() {
|
void MQTTAsyncClient::subscribe() {
|
||||||
uint16_t packetId = _mqttClient.subscribe(_controlTopic.c_str(), 0);
|
uint16_t packetId = _mqttClient.subscribe(_controlTopic.c_str(), 0);
|
||||||
LOG_INFO("📬 Subscribing to control topic: %s (packetId=%d)", _controlTopic.c_str(), packetId);
|
LOG_INFO(TAG, "📬 Subscribing to control topic: %s (packetId=%d)", _controlTopic.c_str(), packetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttConnect(bool sessionPresent) {
|
void MQTTAsyncClient::onMqttConnect(bool sessionPresent) {
|
||||||
LOG_INFO("✅ Connected to MQTT broker (session present: %s)", sessionPresent ? "yes" : "no");
|
LOG_INFO(TAG, "✅ Connected to MQTT broker (session present: %s)", sessionPresent ? "yes" : "no");
|
||||||
LOG_INFO("🔍 Free heap AFTER MQTT connect: %d bytes", ESP.getFreeHeap());
|
LOG_INFO(TAG, "🔍 Free heap AFTER MQTT connect: %d bytes", ESP.getFreeHeap());
|
||||||
|
|
||||||
|
// Reset reconnection attempts on successful connection
|
||||||
|
_reconnectAttempts = 0;
|
||||||
|
|
||||||
// Subscribe to control topic
|
// Subscribe to control topic
|
||||||
subscribe();
|
subscribe();
|
||||||
|
|
||||||
|
// 🔥 Start heartbeat timer
|
||||||
|
startHeartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
|
void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
|
||||||
|
auto& mqttConfig = _configManager.getMqttConfig();
|
||||||
|
|
||||||
const char* reasonStr;
|
const char* reasonStr;
|
||||||
switch(reason) {
|
switch(reason) {
|
||||||
case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED:
|
case AsyncMqttClientDisconnectReason::TCP_DISCONNECTED:
|
||||||
@@ -183,23 +252,42 @@ void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_ERROR("❌ Disconnected from MQTT broker - Reason: %s (%d)", reasonStr, static_cast<int>(reason));
|
LOG_ERROR(TAG, "❌ Disconnected from MQTT broker - Reason: %s (%d)", reasonStr, static_cast<int>(reason));
|
||||||
|
|
||||||
|
// Stop heartbeat timer when disconnected
|
||||||
|
stopHeartbeat();
|
||||||
|
|
||||||
|
// 🔥 Don't attempt reconnection if MQTT is disabled
|
||||||
|
if (!mqttConfig.enabled) {
|
||||||
|
LOG_INFO(TAG, "MQTT is disabled - not attempting reconnection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (_networking.isConnected()) {
|
if (_networking.isConnected()) {
|
||||||
LOG_INFO("Network still connected - scheduling MQTT reconnection in %d seconds", MQTT_RECONNECT_DELAY / 1000);
|
// Increment reconnection attempts
|
||||||
|
_reconnectAttempts++;
|
||||||
|
|
||||||
|
// Calculate backoff delay
|
||||||
|
unsigned long reconnectDelay = getReconnectDelay();
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "Network still connected - scheduling MQTT reconnection #%d in %lu seconds (backoff active)",
|
||||||
|
_reconnectAttempts, reconnectDelay / 1000);
|
||||||
|
|
||||||
|
// Update timer period with new delay
|
||||||
|
xTimerChangePeriod(_mqttReconnectTimer, pdMS_TO_TICKS(reconnectDelay), 0);
|
||||||
xTimerStart(_mqttReconnectTimer, 0);
|
xTimerStart(_mqttReconnectTimer, 0);
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Network is down - waiting for network to reconnect");
|
LOG_INFO(TAG, "Network is down - waiting for network to reconnect");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttSubscribe(uint16_t packetId, uint8_t qos) {
|
void MQTTAsyncClient::onMqttSubscribe(uint16_t packetId, uint8_t qos) {
|
||||||
LOG_INFO("✅ Subscribed to topic (packetId=%d, QoS=%d)", packetId, qos);
|
LOG_INFO(TAG, "✅ Subscribed to topic (packetId=%d, QoS=%d)", packetId, qos);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttUnsubscribe(uint16_t packetId) {
|
void MQTTAsyncClient::onMqttUnsubscribe(uint16_t packetId) {
|
||||||
LOG_DEBUG("Unsubscribed from topic (packetId=%d)", packetId);
|
LOG_DEBUG(TAG, "Unsubscribed from topic (packetId=%d)", packetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
|
void MQTTAsyncClient::onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
|
||||||
@@ -207,7 +295,7 @@ void MQTTAsyncClient::onMqttMessage(char* topic, char* payload, AsyncMqttClientM
|
|||||||
String topicStr = String(topic);
|
String topicStr = String(topic);
|
||||||
String payloadStr = String(payload).substring(0, len);
|
String payloadStr = String(payload).substring(0, len);
|
||||||
|
|
||||||
LOG_DEBUG("MQTT message received - topic: %s, payload: %s", topicStr.c_str(), payloadStr.c_str());
|
LOG_DEBUG(TAG, "MQTT message received - topic: %s, payload: %s", topicStr.c_str(), payloadStr.c_str());
|
||||||
|
|
||||||
// Call user callback
|
// Call user callback
|
||||||
if (_messageCallback) {
|
if (_messageCallback) {
|
||||||
@@ -216,16 +304,16 @@ void MQTTAsyncClient::onMqttMessage(char* topic, char* payload, AsyncMqttClientM
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::onMqttPublish(uint16_t packetId) {
|
void MQTTAsyncClient::onMqttPublish(uint16_t packetId) {
|
||||||
LOG_DEBUG("MQTT publish acknowledged (packetId=%d)", packetId);
|
LOG_DEBUG(TAG, "MQTT publish acknowledged (packetId=%d)", packetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MQTTAsyncClient::attemptReconnection() {
|
void MQTTAsyncClient::attemptReconnection() {
|
||||||
// Double-check network is still up
|
// Double-check network is still up
|
||||||
if (_networking.isConnected()) {
|
if (_networking.isConnected()) {
|
||||||
LOG_INFO("Attempting MQTT reconnection...");
|
LOG_INFO(TAG, "Attempting MQTT reconnection...");
|
||||||
connect();
|
connect();
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Network down during reconnect attempt - aborting");
|
LOG_WARNING(TAG, "Network down during reconnect attempt - aborting");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,3 +326,127 @@ void MQTTAsyncClient::mqttReconnectTimerCallback(TimerHandle_t xTimer) {
|
|||||||
MQTTAsyncClient::_instance->attemptReconnection();
|
MQTTAsyncClient::_instance->attemptReconnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// HEARTBEAT FUNCTIONALITY
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void MQTTAsyncClient::startHeartbeat() {
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
LOG_INFO(TAG, "💓 Starting MQTT heartbeat (every %d seconds)", HEARTBEAT_INTERVAL / 1000);
|
||||||
|
|
||||||
|
// Publish first heartbeat immediately
|
||||||
|
publishHeartbeat();
|
||||||
|
|
||||||
|
// Start periodic timer
|
||||||
|
xTimerStart(_heartbeatTimer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::stopHeartbeat() {
|
||||||
|
if (_heartbeatTimer) {
|
||||||
|
xTimerStop(_heartbeatTimer, 0);
|
||||||
|
LOG_INFO(TAG, "❤️ Stopped MQTT heartbeat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::publishHeartbeat() {
|
||||||
|
if (!_mqttClient.connected()) {
|
||||||
|
LOG_WARNING(TAG, "⚠️ Cannot publish heartbeat - MQTT not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build heartbeat JSON message
|
||||||
|
StaticJsonDocument<512> doc;
|
||||||
|
doc["status"] = "INFO";
|
||||||
|
doc["type"] = "heartbeat";
|
||||||
|
|
||||||
|
JsonObject payload = doc.createNestedObject("payload");
|
||||||
|
|
||||||
|
// Device ID from NVS
|
||||||
|
payload["device_id"] = _configManager.getDeviceUID();
|
||||||
|
|
||||||
|
// Firmware version
|
||||||
|
payload["firmware_version"] = _configManager.getFwVersion();
|
||||||
|
|
||||||
|
// Current date/time (from TimeKeeper if available, else uptime-based)
|
||||||
|
// For now, we'll use a simple timestamp format
|
||||||
|
unsigned long uptimeMs = millis();
|
||||||
|
unsigned long uptimeSec = uptimeMs / 1000;
|
||||||
|
unsigned long hours = uptimeSec / 3600;
|
||||||
|
unsigned long minutes = (uptimeSec % 3600) / 60;
|
||||||
|
unsigned long seconds = uptimeSec % 60;
|
||||||
|
|
||||||
|
char timestampStr[64];
|
||||||
|
snprintf(timestampStr, sizeof(timestampStr), "Uptime: %luh %lum %lus", hours, minutes, seconds);
|
||||||
|
payload["timestamp"] = timestampStr;
|
||||||
|
|
||||||
|
// IP address
|
||||||
|
payload["ip_address"] = _networking.getLocalIP();
|
||||||
|
|
||||||
|
// Gateway address
|
||||||
|
payload["gateway"] = _networking.getGateway();
|
||||||
|
|
||||||
|
// Uptime in milliseconds
|
||||||
|
payload["uptime_ms"] = uptimeMs;
|
||||||
|
|
||||||
|
// Serialize to string
|
||||||
|
String heartbeatMessage;
|
||||||
|
serializeJson(doc, heartbeatMessage);
|
||||||
|
|
||||||
|
// Publish to heartbeat topic with RETAIN flag
|
||||||
|
String heartbeatTopic = "vesper/" + _configManager.getDeviceUID() + "/status/heartbeat";
|
||||||
|
uint16_t packetId = _mqttClient.publish(heartbeatTopic.c_str(), 1, true, heartbeatMessage.c_str());
|
||||||
|
|
||||||
|
if (packetId > 0) {
|
||||||
|
LOG_DEBUG(TAG, "💓 Published heartbeat (retained) - IP: %s, Uptime: %lums",
|
||||||
|
_networking.getLocalIP().c_str(), uptimeMs);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(TAG, "❌ Failed to publish heartbeat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::heartbeatTimerCallback(TimerHandle_t xTimer) {
|
||||||
|
if (MQTTAsyncClient::_instance) {
|
||||||
|
MQTTAsyncClient::_instance->publishHeartbeat();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// NETWORK STABILIZATION - NON-BLOCKING TIMER APPROACH
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void MQTTAsyncClient::connectAfterStabilization() {
|
||||||
|
LOG_DEBUG(TAG, "Network stabilization complete - connecting to MQTT");
|
||||||
|
connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAsyncClient::networkStabilizationTimerCallback(TimerHandle_t xTimer) {
|
||||||
|
if (MQTTAsyncClient::_instance) {
|
||||||
|
MQTTAsyncClient::_instance->connectAfterStabilization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// EXPONENTIAL BACKOFF CALCULATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
unsigned long MQTTAsyncClient::getReconnectDelay() {
|
||||||
|
// First 3 attempts: Quick retries (5 seconds each)
|
||||||
|
if (_reconnectAttempts <= MQTT_MAX_QUICK_RETRIES) {
|
||||||
|
return MQTT_RECONNECT_BASE_DELAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// After quick retries: Exponential backoff
|
||||||
|
// Formula: base_delay * 2^(attempts - quick_retries)
|
||||||
|
// Examples: 10s, 20s, 40s, 80s, 160s, 300s (capped at 5 minutes)
|
||||||
|
uint8_t backoffPower = _reconnectAttempts - MQTT_MAX_QUICK_RETRIES;
|
||||||
|
unsigned long delay = MQTT_RECONNECT_BASE_DELAY * (1 << backoffPower); // 2^backoffPower
|
||||||
|
|
||||||
|
// Cap at maximum delay (5 minutes)
|
||||||
|
if (delay > MQTT_RECONNECT_MAX_DELAY) {
|
||||||
|
delay = MQTT_RECONNECT_MAX_DELAY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return delay;
|
||||||
|
}
|
||||||
@@ -108,9 +108,28 @@ private:
|
|||||||
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total);
|
void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total);
|
||||||
void onMqttPublish(uint16_t packetId);
|
void onMqttPublish(uint16_t packetId);
|
||||||
|
|
||||||
// Reconnection Timer
|
// Reconnection Timer with Exponential Backoff
|
||||||
TimerHandle_t _mqttReconnectTimer;
|
TimerHandle_t _mqttReconnectTimer;
|
||||||
static const unsigned long MQTT_RECONNECT_DELAY = 5000; // 5 seconds
|
static const unsigned long MQTT_RECONNECT_BASE_DELAY = 5000; // 5 seconds base
|
||||||
|
static const unsigned long MQTT_RECONNECT_MAX_DELAY = 300000; // 5 minutes max
|
||||||
|
static const uint8_t MQTT_MAX_QUICK_RETRIES = 3; // Try 3 times quickly
|
||||||
|
uint8_t _reconnectAttempts; // Track failed attempts
|
||||||
|
unsigned long _lastConnectionAttempt; // Track last attempt time
|
||||||
void attemptReconnection();
|
void attemptReconnection();
|
||||||
static void mqttReconnectTimerCallback(TimerHandle_t xTimer);
|
static void mqttReconnectTimerCallback(TimerHandle_t xTimer);
|
||||||
|
unsigned long getReconnectDelay(); // Calculate backoff delay
|
||||||
|
|
||||||
|
// Network Stabilization Timer (non-blocking replacement for delay)
|
||||||
|
TimerHandle_t _networkStabilizationTimer;
|
||||||
|
static const unsigned long NETWORK_STABILIZATION_DELAY = 2000; // 2 seconds
|
||||||
|
void connectAfterStabilization();
|
||||||
|
static void networkStabilizationTimerCallback(TimerHandle_t xTimer);
|
||||||
|
|
||||||
|
// Heartbeat Timer (30 seconds)
|
||||||
|
TimerHandle_t _heartbeatTimer;
|
||||||
|
static const unsigned long HEARTBEAT_INTERVAL = 30000; // 30 seconds
|
||||||
|
void publishHeartbeat();
|
||||||
|
static void heartbeatTimerCallback(TimerHandle_t xTimer);
|
||||||
|
void startHeartbeat();
|
||||||
|
void stopHeartbeat();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "ResponseBuilder.hpp"
|
#include "ResponseBuilder.hpp"
|
||||||
|
|
||||||
|
#define TAG "ResponseBuilder"
|
||||||
#include "../../Logging/Logging.hpp"
|
#include "../../Logging/Logging.hpp"
|
||||||
|
|
||||||
// Static member initialization
|
// Static member initialization
|
||||||
@@ -32,8 +34,8 @@ String ResponseBuilder::pong() {
|
|||||||
return success("pong", "");
|
return success("pong", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsed, uint64_t projectedRunTime) {
|
String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsed, uint64_t projectedRunTime, const uint32_t strikeCounters[16]) {
|
||||||
StaticJsonDocument<512> statusDoc; // Increased size for additional data
|
DynamicJsonDocument statusDoc(1024); // Increased size for strikeCounters array
|
||||||
|
|
||||||
statusDoc["status"] = "SUCCESS";
|
statusDoc["status"] = "SUCCESS";
|
||||||
statusDoc["type"] = "current_status";
|
statusDoc["type"] = "current_status";
|
||||||
@@ -63,10 +65,16 @@ String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeEla
|
|||||||
payload["time_elapsed"] = timeElapsed; // in milliseconds
|
payload["time_elapsed"] = timeElapsed; // in milliseconds
|
||||||
payload["projected_run_time"] = projectedRunTime; // NEW: total projected duration
|
payload["projected_run_time"] = projectedRunTime; // NEW: total projected duration
|
||||||
|
|
||||||
|
// Add strike counters array
|
||||||
|
JsonArray strikeCountersArray = payload.createNestedArray("strike_counters");
|
||||||
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
|
strikeCountersArray.add(strikeCounters[i]);
|
||||||
|
}
|
||||||
|
|
||||||
String result;
|
String result;
|
||||||
serializeJson(statusDoc, result);
|
serializeJson(statusDoc, result);
|
||||||
|
|
||||||
LOG_DEBUG("Device status response: %s", result.c_str());
|
LOG_DEBUG(TAG, "Device status response: %s", result.c_str());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +137,7 @@ String ResponseBuilder::buildResponse(Status status, const String& type, const S
|
|||||||
String result;
|
String result;
|
||||||
serializeJson(_responseDoc, result);
|
serializeJson(_responseDoc, result);
|
||||||
|
|
||||||
LOG_DEBUG("Response built: %s", result.c_str());
|
LOG_DEBUG(TAG, "Response built: %s", result.c_str());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +151,7 @@ String ResponseBuilder::buildResponse(Status status, const String& type, const J
|
|||||||
String result;
|
String result;
|
||||||
serializeJson(_responseDoc, result);
|
serializeJson(_responseDoc, result);
|
||||||
|
|
||||||
LOG_DEBUG("Response built: %s", result.c_str());
|
LOG_DEBUG(TAG, "Response built: %s", result.c_str());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public:
|
|||||||
// Specialized response builders for common scenarios
|
// Specialized response builders for common scenarios
|
||||||
static String acknowledgment(const String& commandType);
|
static String acknowledgment(const String& commandType);
|
||||||
static String pong();
|
static String pong();
|
||||||
static String deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsedMs, uint64_t projectedRunTime = 0);
|
static String deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsedMs, uint64_t projectedRunTime, const uint32_t strikeCounters[16]);
|
||||||
static String melodyList(const String& fileListJson);
|
static String melodyList(const String& fileListJson);
|
||||||
static String downloadResult(bool success, const String& filename = "");
|
static String downloadResult(bool success, const String& filename = "");
|
||||||
static String configUpdate(const String& configType);
|
static String configUpdate(const String& configType);
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* UARTCOMMANDHANDLER.CPP - UART Command Handler Implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "UARTCommandHandler.hpp"
|
||||||
|
|
||||||
|
#define TAG "UARTHandler"
|
||||||
|
#include "../../Logging/Logging.hpp"
|
||||||
|
|
||||||
|
UARTCommandHandler::UARTCommandHandler(uint8_t txPin, uint8_t rxPin, uint32_t baudRate)
|
||||||
|
: _serial(Serial2)
|
||||||
|
, _txPin(txPin)
|
||||||
|
, _rxPin(rxPin)
|
||||||
|
, _baudRate(baudRate)
|
||||||
|
, _ready(false)
|
||||||
|
, _bufferIndex(0)
|
||||||
|
, _messageCount(0)
|
||||||
|
, _errorCount(0)
|
||||||
|
, _callback(nullptr)
|
||||||
|
{
|
||||||
|
resetBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
UARTCommandHandler::~UARTCommandHandler() {
|
||||||
|
_serial.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::begin() {
|
||||||
|
LOG_INFO(TAG, "Initializing UART Command Handler");
|
||||||
|
LOG_INFO(TAG, " TX Pin: GPIO%d", _txPin);
|
||||||
|
LOG_INFO(TAG, " RX Pin: GPIO%d", _rxPin);
|
||||||
|
LOG_INFO(TAG, " Baud Rate: %u", _baudRate);
|
||||||
|
|
||||||
|
// Initialize Serial2 with custom pins
|
||||||
|
_serial.begin(_baudRate, SERIAL_8N1, _rxPin, _txPin);
|
||||||
|
|
||||||
|
// Clear any garbage in the buffer
|
||||||
|
while (_serial.available()) {
|
||||||
|
_serial.read();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ready = true;
|
||||||
|
LOG_INFO(TAG, "UART Command Handler ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::loop() {
|
||||||
|
if (!_ready) return;
|
||||||
|
|
||||||
|
// Process all available bytes
|
||||||
|
while (_serial.available()) {
|
||||||
|
char c = _serial.read();
|
||||||
|
|
||||||
|
// Check for message delimiter (newline)
|
||||||
|
if (c == '\n' || c == '\r') {
|
||||||
|
if (_bufferIndex > 0) {
|
||||||
|
// Null-terminate and process
|
||||||
|
_buffer[_bufferIndex] = '\0';
|
||||||
|
processLine(_buffer);
|
||||||
|
resetBuffer();
|
||||||
|
}
|
||||||
|
// Skip empty lines
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add character to buffer
|
||||||
|
if (_bufferIndex < BUFFER_SIZE - 1) {
|
||||||
|
_buffer[_bufferIndex++] = c;
|
||||||
|
} else {
|
||||||
|
// Buffer overflow - discard and reset
|
||||||
|
LOG_ERROR(TAG, "UART buffer overflow, discarding message");
|
||||||
|
_errorCount++;
|
||||||
|
resetBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::setCallback(MessageCallback callback) {
|
||||||
|
_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::send(const String& response) {
|
||||||
|
if (!_ready) {
|
||||||
|
LOG_ERROR(TAG, "UART not ready, cannot send response");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_serial.print(response);
|
||||||
|
_serial.print('\n'); // Newline delimiter
|
||||||
|
_serial.flush(); // Ensure data is sent
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "UART TX: %s", response.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::processLine(const char* line) {
|
||||||
|
LOG_DEBUG(TAG, "UART RX: %s", line);
|
||||||
|
|
||||||
|
// Skip empty lines or whitespace-only
|
||||||
|
if (strlen(line) == 0) return;
|
||||||
|
|
||||||
|
// Parse JSON
|
||||||
|
StaticJsonDocument<1024> doc;
|
||||||
|
DeserializationError error = deserializeJson(doc, line);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
LOG_ERROR(TAG, "UART JSON parse error: %s", error.c_str());
|
||||||
|
_errorCount++;
|
||||||
|
|
||||||
|
// Send error response back
|
||||||
|
StaticJsonDocument<256> errorDoc;
|
||||||
|
errorDoc["status"] = "ERROR";
|
||||||
|
errorDoc["type"] = "parse_error";
|
||||||
|
errorDoc["payload"] = error.c_str();
|
||||||
|
|
||||||
|
String errorResponse;
|
||||||
|
serializeJson(errorDoc, errorResponse);
|
||||||
|
send(errorResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_messageCount++;
|
||||||
|
|
||||||
|
// Invoke callback if set
|
||||||
|
if (_callback) {
|
||||||
|
_callback(doc);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING(TAG, "UART message received but no callback set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::resetBuffer() {
|
||||||
|
_bufferIndex = 0;
|
||||||
|
memset(_buffer, 0, BUFFER_SIZE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* UARTCOMMANDHANDLER.HPP - UART Command Interface for External Control Devices
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 🔌 UART COMMAND HANDLER 🔌
|
||||||
|
*
|
||||||
|
* Enables command input from external devices (LCD panels, button controllers)
|
||||||
|
* via UART serial communication. Uses newline-delimited JSON protocol.
|
||||||
|
*
|
||||||
|
* Pin Configuration:
|
||||||
|
* • TX: GPIO12
|
||||||
|
* • RX: GPIO13
|
||||||
|
* • Baud: 115200 (configurable)
|
||||||
|
*
|
||||||
|
* Protocol:
|
||||||
|
* • Newline-delimited JSON messages
|
||||||
|
* • Same command format as MQTT/WebSocket
|
||||||
|
* • Responses sent back on same UART
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-01-19
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class UARTCommandHandler {
|
||||||
|
public:
|
||||||
|
// Default pin configuration
|
||||||
|
static constexpr uint8_t DEFAULT_TX_PIN = 12;
|
||||||
|
static constexpr uint8_t DEFAULT_RX_PIN = 13;
|
||||||
|
static constexpr uint32_t DEFAULT_BAUD_RATE = 115200;
|
||||||
|
static constexpr size_t BUFFER_SIZE = 1024;
|
||||||
|
|
||||||
|
// Message callback type - called when a complete JSON message is received
|
||||||
|
using MessageCallback = std::function<void(JsonDocument& message)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct UART handler with custom pins
|
||||||
|
* @param txPin GPIO pin for TX (default: 12)
|
||||||
|
* @param rxPin GPIO pin for RX (default: 13)
|
||||||
|
* @param baudRate Baud rate (default: 115200)
|
||||||
|
*/
|
||||||
|
explicit UARTCommandHandler(uint8_t txPin = DEFAULT_TX_PIN,
|
||||||
|
uint8_t rxPin = DEFAULT_RX_PIN,
|
||||||
|
uint32_t baudRate = DEFAULT_BAUD_RATE);
|
||||||
|
|
||||||
|
~UARTCommandHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the UART interface
|
||||||
|
*/
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process incoming UART data (call from loop or task)
|
||||||
|
* Non-blocking - processes available bytes and returns
|
||||||
|
*/
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set callback for received messages
|
||||||
|
* @param callback Function to call with parsed JSON
|
||||||
|
*/
|
||||||
|
void setCallback(MessageCallback callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a response back over UART
|
||||||
|
* @param response JSON string to send (newline appended automatically)
|
||||||
|
*/
|
||||||
|
void send(const String& response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if UART is initialized and ready
|
||||||
|
*/
|
||||||
|
bool isReady() const { return _ready; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get number of messages received since boot
|
||||||
|
*/
|
||||||
|
uint32_t getMessageCount() const { return _messageCount; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get number of parse errors since boot
|
||||||
|
*/
|
||||||
|
uint32_t getErrorCount() const { return _errorCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
HardwareSerial& _serial;
|
||||||
|
uint8_t _txPin;
|
||||||
|
uint8_t _rxPin;
|
||||||
|
uint32_t _baudRate;
|
||||||
|
bool _ready;
|
||||||
|
|
||||||
|
// Receive buffer
|
||||||
|
char _buffer[BUFFER_SIZE];
|
||||||
|
size_t _bufferIndex;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
uint32_t _messageCount;
|
||||||
|
uint32_t _errorCount;
|
||||||
|
|
||||||
|
// Callback
|
||||||
|
MessageCallback _callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process a complete line from the buffer
|
||||||
|
* @param line Null-terminated string containing the message
|
||||||
|
*/
|
||||||
|
void processLine(const char* line);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset the receive buffer
|
||||||
|
*/
|
||||||
|
void resetBuffer();
|
||||||
|
};
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "WebSocketServer.hpp"
|
#include "WebSocketServer.hpp"
|
||||||
|
|
||||||
|
#define TAG "WebSocket"
|
||||||
#include "../../Logging/Logging.hpp"
|
#include "../../Logging/Logging.hpp"
|
||||||
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
#include "../ResponseBuilder/ResponseBuilder.hpp"
|
||||||
|
|
||||||
@@ -23,7 +25,7 @@ WebSocketServer::~WebSocketServer() {
|
|||||||
|
|
||||||
void WebSocketServer::begin() {
|
void WebSocketServer::begin() {
|
||||||
_webSocket.onEvent(onEvent);
|
_webSocket.onEvent(onEvent);
|
||||||
LOG_INFO("WebSocket server initialized on /ws");
|
LOG_INFO(TAG, "WebSocket server initialized on /ws");
|
||||||
|
|
||||||
// 🔥 CRITICAL: This line was missing - attach WebSocket to the AsyncWebServer
|
// 🔥 CRITICAL: This line was missing - attach WebSocket to the AsyncWebServer
|
||||||
// Without this, the server doesn't know about the WebSocket handler!
|
// Without this, the server doesn't know about the WebSocket handler!
|
||||||
@@ -40,17 +42,17 @@ void WebSocketServer::sendToClient(uint32_t clientId, const String& message) {
|
|||||||
|
|
||||||
void WebSocketServer::broadcastToAll(const String& message) {
|
void WebSocketServer::broadcastToAll(const String& message) {
|
||||||
_clientManager.broadcastToAll(message);
|
_clientManager.broadcastToAll(message);
|
||||||
LOG_DEBUG("Broadcast to all WebSocket clients: %s", message.c_str());
|
LOG_DEBUG(TAG, "Broadcast to all WebSocket clients: %s", message.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketServer::broadcastToMaster(const String& message) {
|
void WebSocketServer::broadcastToMaster(const String& message) {
|
||||||
_clientManager.sendToMasterClients(message);
|
_clientManager.sendToMasterClients(message);
|
||||||
LOG_DEBUG("Broadcast to master clients: %s", message.c_str());
|
LOG_DEBUG(TAG, "Broadcast to master clients: %s", message.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketServer::broadcastToSecondary(const String& message) {
|
void WebSocketServer::broadcastToSecondary(const String& message) {
|
||||||
_clientManager.sendToSecondaryClients(message);
|
_clientManager.sendToSecondaryClients(message);
|
||||||
LOG_DEBUG("Broadcast to secondary clients: %s", message.c_str());
|
LOG_DEBUG(TAG, "Broadcast to secondary clients: %s", message.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WebSocketServer::hasClients() const {
|
bool WebSocketServer::hasClients() const {
|
||||||
@@ -64,7 +66,7 @@ size_t WebSocketServer::getClientCount() const {
|
|||||||
void WebSocketServer::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
|
void WebSocketServer::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||||
AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
AwsEventType type, void* arg, uint8_t* data, size_t len) {
|
||||||
if (!_instance) {
|
if (!_instance) {
|
||||||
LOG_ERROR("WebSocketServer static instance is NULL - callback ignored!");
|
LOG_ERROR(TAG, "WebSocketServer static instance is NULL - callback ignored!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,7 +84,7 @@ void WebSocketServer::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* clie
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case WS_EVT_ERROR:
|
case WS_EVT_ERROR:
|
||||||
LOG_ERROR("WebSocket client #%u error(%u): %s",
|
LOG_ERROR(TAG, "WebSocket client #%u error(%u): %s",
|
||||||
client->id(), *((uint16_t*)arg), (char*)data);
|
client->id(), *((uint16_t*)arg), (char*)data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -92,7 +94,7 @@ void WebSocketServer::onEvent(AsyncWebSocket* server, AsyncWebSocketClient* clie
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketServer::onConnect(AsyncWebSocketClient* client) {
|
void WebSocketServer::onConnect(AsyncWebSocketClient* client) {
|
||||||
LOG_INFO("WebSocket client #%u connected from %s",
|
LOG_INFO(TAG, "WebSocket client #%u connected from %s",
|
||||||
client->id(), client->remoteIP().toString().c_str());
|
client->id(), client->remoteIP().toString().c_str());
|
||||||
|
|
||||||
// Add client to manager (type UNKNOWN until they identify)
|
// Add client to manager (type UNKNOWN until they identify)
|
||||||
@@ -104,7 +106,7 @@ void WebSocketServer::onConnect(AsyncWebSocketClient* client) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketServer::onDisconnect(AsyncWebSocketClient* client) {
|
void WebSocketServer::onDisconnect(AsyncWebSocketClient* client) {
|
||||||
LOG_INFO("WebSocket client #%u disconnected", client->id());
|
LOG_INFO(TAG, "WebSocket client #%u disconnected", client->id());
|
||||||
|
|
||||||
_clientManager.removeClient(client->id());
|
_clientManager.removeClient(client->id());
|
||||||
_clientManager.cleanupDisconnectedClients();
|
_clientManager.cleanupDisconnectedClients();
|
||||||
@@ -118,7 +120,7 @@ void WebSocketServer::onData(AsyncWebSocketClient* client, void* arg, uint8_t* d
|
|||||||
// Allocate buffer for payload
|
// Allocate buffer for payload
|
||||||
char* payload = (char*)malloc(len + 1);
|
char* payload = (char*)malloc(len + 1);
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
LOG_ERROR("Failed to allocate memory for WebSocket payload");
|
LOG_ERROR(TAG, "Failed to allocate memory for WebSocket payload");
|
||||||
String errorResponse = ResponseBuilder::error("memory_error", "Out of memory");
|
String errorResponse = ResponseBuilder::error("memory_error", "Out of memory");
|
||||||
_clientManager.sendToClient(client->id(), errorResponse);
|
_clientManager.sendToClient(client->id(), errorResponse);
|
||||||
return;
|
return;
|
||||||
@@ -127,14 +129,14 @@ void WebSocketServer::onData(AsyncWebSocketClient* client, void* arg, uint8_t* d
|
|||||||
memcpy(payload, data, len);
|
memcpy(payload, data, len);
|
||||||
payload[len] = '\0';
|
payload[len] = '\0';
|
||||||
|
|
||||||
LOG_DEBUG("WebSocket client #%u sent: %s", client->id(), payload);
|
LOG_DEBUG(TAG, "WebSocket client #%u sent: %s", client->id(), payload);
|
||||||
|
|
||||||
// Parse JSON
|
// Parse JSON
|
||||||
StaticJsonDocument<2048> doc;
|
StaticJsonDocument<2048> doc;
|
||||||
DeserializationError error = deserializeJson(doc, payload);
|
DeserializationError error = deserializeJson(doc, payload);
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("Failed to parse WebSocket JSON from client #%u: %s", client->id(), error.c_str());
|
LOG_ERROR(TAG, "Failed to parse WebSocket JSON from client #%u: %s", client->id(), error.c_str());
|
||||||
String errorResponse = ResponseBuilder::error("parse_error", "Invalid JSON");
|
String errorResponse = ResponseBuilder::error("parse_error", "Invalid JSON");
|
||||||
_clientManager.sendToClient(client->id(), errorResponse);
|
_clientManager.sendToClient(client->id(), errorResponse);
|
||||||
} else {
|
} else {
|
||||||
@@ -143,15 +145,15 @@ void WebSocketServer::onData(AsyncWebSocketClient* client, void* arg, uint8_t* d
|
|||||||
|
|
||||||
// Call user callback if set
|
// Call user callback if set
|
||||||
if (_messageCallback) {
|
if (_messageCallback) {
|
||||||
LOG_DEBUG("Routing message from client #%u to callback handler", client->id());
|
LOG_DEBUG(TAG, "Routing message from client #%u to callback handler", client->id());
|
||||||
_messageCallback(client->id(), doc);
|
_messageCallback(client->id(), doc);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("WebSocket message received but no callback handler is set!");
|
LOG_WARNING(TAG, "WebSocket message received but no callback handler is set!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
free(payload);
|
free(payload);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Received fragmented or non-text WebSocket message from client #%u - ignoring", client->id());
|
LOG_WARNING(TAG, "Received fragmented or non-text WebSocket message from client #%u - ignoring", client->id());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,6 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <IPAddress.h>
|
#include <IPAddress.h>
|
||||||
#include <ETH.h>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <nvs_flash.h>
|
#include <nvs_flash.h>
|
||||||
#include <nvs.h>
|
#include <nvs.h>
|
||||||
@@ -47,7 +46,7 @@ public:
|
|||||||
String deviceUID = ""; // 🏷️ Factory-set UID (NVS) - NO DEFAULT
|
String deviceUID = ""; // 🏷️ Factory-set UID (NVS) - NO DEFAULT
|
||||||
String hwType = ""; // 🔧 Factory-set hardware type (NVS) - NO DEFAULT
|
String hwType = ""; // 🔧 Factory-set hardware type (NVS) - NO DEFAULT
|
||||||
String hwVersion = ""; // 📐 Factory-set hardware revision (NVS) - NO DEFAULT
|
String hwVersion = ""; // 📐 Factory-set hardware revision (NVS) - NO DEFAULT
|
||||||
String fwVersion = "0.0.0"; // 📋 Current firmware version (SD) - auto-updated
|
String fwVersion = "0"; // 📋 Current firmware version (SD) - auto-updated (integer string)
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +68,9 @@ public:
|
|||||||
String apSsid; // 📡 Auto-generated AP name
|
String apSsid; // 📡 Auto-generated AP name
|
||||||
String apPass; // 🔐 AP is Open. No Password
|
String apPass; // 🔐 AP is Open. No Password
|
||||||
uint16_t discoveryPort = 32101; // 📡 Fixed discovery port
|
uint16_t discoveryPort = 32101; // 📡 Fixed discovery port
|
||||||
|
bool permanentAPMode = false; // 🔘 Permanent AP mode toggle (stored on SD)
|
||||||
|
String defaultWifiSsid = "BellSystemsInfra"; // 📡 Default WiFi SSID to try on boot
|
||||||
|
String defaultWifiPsk = "v3sp3r_8998!"; // 🔐 Default WiFi password to try on boot
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,11 +81,12 @@ public:
|
|||||||
* Username defaults to deviceUID for unique identification.
|
* Username defaults to deviceUID for unique identification.
|
||||||
*/
|
*/
|
||||||
struct MqttConfig {
|
struct MqttConfig {
|
||||||
IPAddress host = IPAddress(145, 223, 96, 251); // 📡 Local Mosquitto broker
|
IPAddress host = IPAddress(72,61,191,197); // 📡 MQTT broker (default cloud broker)
|
||||||
int port = 1883; // 🔌 Standard MQTT port (non-SSL)
|
int port = 1883; // 🔌 Standard MQTT port (non-SSL)
|
||||||
String user; // 👤 Auto-set to deviceUID
|
String user; // 👤 Auto-set to deviceUID
|
||||||
String password = "vesper"; // 🔑 Default password
|
String password = "vesper"; // 🔑 Default password
|
||||||
bool useSSL = false; // 🔒 SSL disabled for local broker
|
bool useSSL = false; // 🔒 SSL disabled for local broker
|
||||||
|
bool enabled = true; // 🔘 MQTT enabled by default (can be toggled via command)
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,8 +105,8 @@ public:
|
|||||||
uint8_t ethSpiMiso = 19; // 🔄 Hardware-specific - OK as is
|
uint8_t ethSpiMiso = 19; // 🔄 Hardware-specific - OK as is
|
||||||
uint8_t ethSpiMosi = 23; // 🔄 Hardware-specific - OK as is
|
uint8_t ethSpiMosi = 23; // 🔄 Hardware-specific - OK as is
|
||||||
|
|
||||||
// ETH PHY Configuration - hardware-specific
|
// ETH PHY Configuration - ETHERNET DISABLED (kept for legacy/future use)
|
||||||
eth_phy_type_t ethPhyType = ETH_PHY_W5500; // 🔌 Hardware-specific - OK as is
|
uint8_t ethPhyType = 9; // was ETH_PHY_W5500 (9) — Ethernet removed in v138
|
||||||
uint8_t ethPhyAddr = 1; // 📍 Hardware-specific - OK as is
|
uint8_t ethPhyAddr = 1; // 📍 Hardware-specific - OK as is
|
||||||
uint8_t ethPhyCs = 5; // 💾 Hardware-specific - OK as is
|
uint8_t ethPhyCs = 5; // 💾 Hardware-specific - OK as is
|
||||||
int8_t ethPhyIrq = -1; // ⚡ Hardware-specific - OK as is
|
int8_t ethPhyIrq = -1; // ⚡ Hardware-specific - OK as is
|
||||||
@@ -119,7 +122,7 @@ public:
|
|||||||
*/
|
*/
|
||||||
struct TimeConfig {
|
struct TimeConfig {
|
||||||
String ntpServer = "pool.ntp.org"; // ⏰ Universal NTP - OK as is
|
String ntpServer = "pool.ntp.org"; // ⏰ Universal NTP - OK as is
|
||||||
long gmtOffsetSec = 0; // 🌍 Default UTC, app-configurable via SD
|
long gmtOffsetSec = 7200; // 🌍 Default GMT+2 (Greek Time), app-configurable via SD
|
||||||
int daylightOffsetSec = 0; // ☀️ Default no DST, app-configurable via SD
|
int daylightOffsetSec = 0; // ☀️ Default no DST, app-configurable via SD
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -203,6 +206,20 @@ public:
|
|||||||
String nighttimeSilenceOffTime = "07:00"; // 🌙 End of nighttime silence
|
String nighttimeSilenceOffTime = "07:00"; // 🌙 End of nighttime silence
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @struct General Config
|
||||||
|
* @brief General configuration (loaded from SD)
|
||||||
|
*
|
||||||
|
* All clock settings are loaded from SD card at startup.
|
||||||
|
*/
|
||||||
|
struct GeneralConfig {
|
||||||
|
uint8_t serialLogLevel = 5;
|
||||||
|
uint8_t sdLogLevel = 0;
|
||||||
|
uint8_t mqttLogLevel = 0;
|
||||||
|
bool mqttEnabled = true; // MQTT enabled by default
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// MEMBER VARIABLES - Clean deployment-ready storage
|
// MEMBER VARIABLES - Clean deployment-ready storage
|
||||||
@@ -215,6 +232,7 @@ private:
|
|||||||
UpdateConfig updateConfig;
|
UpdateConfig updateConfig;
|
||||||
BellConfig bellConfig;
|
BellConfig bellConfig;
|
||||||
ClockConfig clockConfig;
|
ClockConfig clockConfig;
|
||||||
|
GeneralConfig generalConfig;
|
||||||
|
|
||||||
bool sdInitialized = false;
|
bool sdInitialized = false;
|
||||||
std::vector<String> updateServers;
|
std::vector<String> updateServers;
|
||||||
@@ -274,7 +292,6 @@ public:
|
|||||||
*/
|
*/
|
||||||
bool begin();
|
bool begin();
|
||||||
|
|
||||||
void loadFromSD();
|
|
||||||
bool saveToSD();
|
bool saveToSD();
|
||||||
|
|
||||||
// Configuration access (read-only getters)
|
// Configuration access (read-only getters)
|
||||||
@@ -286,6 +303,7 @@ public:
|
|||||||
const UpdateConfig& getUpdateConfig() const { return updateConfig; }
|
const UpdateConfig& getUpdateConfig() const { return updateConfig; }
|
||||||
const BellConfig& getBellConfig() const { return bellConfig; }
|
const BellConfig& getBellConfig() const { return bellConfig; }
|
||||||
const ClockConfig& getClockConfig() const { return clockConfig; }
|
const ClockConfig& getClockConfig() const { return clockConfig; }
|
||||||
|
const GeneralConfig& getGeneralConfig() const { return generalConfig; }
|
||||||
|
|
||||||
// Device identity methods (READ-ONLY - factory set via separate factory firmware)
|
// Device identity methods (READ-ONLY - factory set via separate factory firmware)
|
||||||
// These values are loaded ONCE at boot from NVS and kept in RAM
|
// These values are loaded ONCE at boot from NVS and kept in RAM
|
||||||
@@ -303,13 +321,23 @@ public:
|
|||||||
void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway,
|
void updateNetworkConfig(const String& hostname, bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||||
IPAddress subnet, IPAddress dns1, IPAddress dns2);
|
IPAddress subnet, IPAddress dns1, IPAddress dns2);
|
||||||
|
|
||||||
|
// AP Mode configuration
|
||||||
|
bool getPermanentAPMode() const { return networkConfig.permanentAPMode; }
|
||||||
|
void setPermanentAPMode(bool enabled) { networkConfig.permanentAPMode = enabled; }
|
||||||
|
|
||||||
// Network configuration persistence
|
// Network configuration persistence
|
||||||
bool loadNetworkConfig();
|
bool loadNetworkConfig();
|
||||||
bool saveNetworkConfig();
|
bool saveNetworkConfig();
|
||||||
|
|
||||||
|
// Time configuration persistence
|
||||||
|
bool loadTimeConfig();
|
||||||
|
bool saveTimeConfig();
|
||||||
|
|
||||||
// Bell and clock configuration methods (unchanged)
|
// Bell and clock configuration methods (unchanged)
|
||||||
bool loadBellDurations();
|
bool loadBellDurations();
|
||||||
bool saveBellDurations();
|
bool saveBellDurations();
|
||||||
|
bool loadBellOutputs();
|
||||||
|
bool saveBellOutputs();
|
||||||
void updateBellDurations(JsonVariant doc);
|
void updateBellDurations(JsonVariant doc);
|
||||||
void updateBellOutputs(JsonVariant doc);
|
void updateBellOutputs(JsonVariant doc);
|
||||||
uint16_t getBellDuration(uint8_t bellIndex) const;
|
uint16_t getBellDuration(uint8_t bellIndex) const;
|
||||||
@@ -376,6 +404,18 @@ public:
|
|||||||
void setNighttimeSilenceOnTime(const String& time) { clockConfig.nighttimeSilenceOnTime = time; }
|
void setNighttimeSilenceOnTime(const String& time) { clockConfig.nighttimeSilenceOnTime = time; }
|
||||||
void setNighttimeSilenceOffTime(const String& time) { clockConfig.nighttimeSilenceOffTime = time; }
|
void setNighttimeSilenceOffTime(const String& time) { clockConfig.nighttimeSilenceOffTime = time; }
|
||||||
|
|
||||||
|
// General Config methods
|
||||||
|
bool setSerialLogLevel(uint8_t level);
|
||||||
|
bool setSdLogLevel(uint8_t level);
|
||||||
|
bool setMqttLogLevel(uint8_t level);
|
||||||
|
uint8_t getSerialLogLevel() const { return generalConfig.serialLogLevel; }
|
||||||
|
uint8_t getSdLogLevel() const { return generalConfig.sdLogLevel; }
|
||||||
|
uint8_t getMqttLogLevel() const { return generalConfig.mqttLogLevel; }
|
||||||
|
void setMqttEnabled(bool enabled) { generalConfig.mqttEnabled = enabled; mqttConfig.enabled = enabled; }
|
||||||
|
bool getMqttEnabled() const { return generalConfig.mqttEnabled; }
|
||||||
|
bool loadGeneralConfig();
|
||||||
|
bool saveGeneralConfig();
|
||||||
|
|
||||||
// Other methods (unchanged)
|
// Other methods (unchanged)
|
||||||
void updateClockAlerts(JsonVariant doc);
|
void updateClockAlerts(JsonVariant doc);
|
||||||
void updateClockBacklight(JsonVariant doc);
|
void updateClockBacklight(JsonVariant doc);
|
||||||
@@ -395,6 +435,7 @@ public:
|
|||||||
String getAPSSID() const { return networkConfig.apSsid; }
|
String getAPSSID() const { return networkConfig.apSsid; }
|
||||||
bool isHealthy() const;
|
bool isHealthy() const;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Get all configuration settings as a JSON string
|
* @brief Get all configuration settings as a JSON string
|
||||||
* @return JSON string containing all current settings
|
* @return JSON string containing all current settings
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "FileManager.hpp"
|
#include "FileManager.hpp"
|
||||||
|
|
||||||
|
#define TAG "FileManager"
|
||||||
|
#include "../BuiltInMelodies/BuiltInMelodies.hpp"
|
||||||
|
|
||||||
FileManager::FileManager(ConfigManager* config) : configManager(config) {
|
FileManager::FileManager(ConfigManager* config) : configManager(config) {
|
||||||
// Constructor - store reference to ConfigManager
|
// Constructor - store reference to ConfigManager
|
||||||
}
|
}
|
||||||
@@ -7,31 +10,42 @@ FileManager::FileManager(ConfigManager* config) : configManager(config) {
|
|||||||
bool FileManager::initializeSD() {
|
bool FileManager::initializeSD() {
|
||||||
uint8_t sdPin = configManager->getHardwareConfig().sdChipSelect;
|
uint8_t sdPin = configManager->getHardwareConfig().sdChipSelect;
|
||||||
if (!SD.begin(sdPin)) {
|
if (!SD.begin(sdPin)) {
|
||||||
LOG_ERROR("SD Card initialization failed!");
|
LOG_ERROR(TAG, "SD Card initialization failed!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileManager::addMelody(JsonVariant doc) {
|
bool FileManager::addMelody(JsonVariant doc) {
|
||||||
LOG_INFO("Adding melody from JSON data...");
|
LOG_INFO(TAG, "Adding melody from JSON data...");
|
||||||
|
|
||||||
// Extract URL and filename from JSON
|
// Extract URL and filename from JSON
|
||||||
if (!doc.containsKey("download_url") || !doc.containsKey("melodys_uid")) {
|
if (!doc.containsKey("download_url") || !doc.containsKey("melodys_uid")) {
|
||||||
LOG_ERROR("Missing required parameters: download_url or melodys_uid");
|
LOG_ERROR(TAG, "Missing required parameters: download_url or melodys_uid");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* url = doc["download_url"];
|
const char* url = doc["download_url"];
|
||||||
const char* filename = doc["melodys_uid"];
|
const char* melodyUid = doc["melodys_uid"];
|
||||||
|
|
||||||
|
// Check if this is a built-in melody - skip download if it exists
|
||||||
|
if (BuiltInMelodies::isBuiltInMelody(melodyUid)) {
|
||||||
|
const BuiltInMelodies::MelodyInfo* builtinMelody = BuiltInMelodies::findMelodyByUID(melodyUid);
|
||||||
|
if (builtinMelody != nullptr) {
|
||||||
|
LOG_INFO(TAG, "Melody '%s' is a built-in melody, skipping download", melodyUid);
|
||||||
|
return true; // Success - no download needed
|
||||||
|
}
|
||||||
|
// If starts with builtin_ but not found, log warning and try download anyway
|
||||||
|
LOG_WARNING(TAG, "Melody '%s' has builtin_ prefix but not found in library, attempting download", melodyUid);
|
||||||
|
}
|
||||||
|
|
||||||
// Download the melody file to /melodies directory
|
// Download the melody file to /melodies directory
|
||||||
if (downloadFile(url, "/melodies", filename)) {
|
if (downloadFile(url, "/melodies", melodyUid)) {
|
||||||
LOG_INFO("Melody download successful: %s", filename);
|
LOG_INFO(TAG, "Melody download successful: %s", melodyUid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_ERROR("Melody download failed: %s", filename);
|
LOG_ERROR(TAG, "Melody download failed: %s", melodyUid);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,32 +60,69 @@ bool FileManager::ensureDirectoryExists(const String& dirPath) {
|
|||||||
normalizedPath += "/";
|
normalizedPath += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if directory already exists
|
||||||
|
if (SD.exists(normalizedPath.c_str())) {
|
||||||
|
return true; // Directory already exists, success
|
||||||
|
}
|
||||||
|
|
||||||
// Create directory if it doesn't exist
|
// Create directory if it doesn't exist
|
||||||
return SD.mkdir(normalizedPath.c_str());
|
return SD.mkdir(normalizedPath.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileManager::downloadFile(const String& url, const String& directory, const String& filename) {
|
bool FileManager::downloadFile(const String& url, const String& directory, const String& filename) {
|
||||||
LOG_INFO("Starting download from: %s", url.c_str());
|
LOG_INFO(TAG, "Starting download from: %s", url.c_str());
|
||||||
|
|
||||||
|
// Check if URL is HTTPS
|
||||||
|
bool isHttps = url.startsWith("https://");
|
||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
|
WiFiClientSecure* secureClient = nullptr;
|
||||||
|
|
||||||
|
// Configure HTTP client based on protocol
|
||||||
|
if (isHttps) {
|
||||||
|
secureClient = new WiFiClientSecure();
|
||||||
|
secureClient->setInsecure(); // Skip certificate validation for Firebase
|
||||||
|
secureClient->setTimeout(15); // 15 second timeout for TLS operations
|
||||||
|
http.begin(*secureClient, url);
|
||||||
|
LOG_DEBUG(TAG, "Using HTTPS with secure client");
|
||||||
|
} else {
|
||||||
http.begin(url);
|
http.begin(url);
|
||||||
|
LOG_DEBUG(TAG, "Using HTTP");
|
||||||
|
}
|
||||||
|
|
||||||
|
http.setTimeout(30000); // 30 second timeout for large files
|
||||||
|
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); // Follow redirects automatically
|
||||||
|
|
||||||
|
// Disable task watchdog for current task during blocking HTTPS operation
|
||||||
|
// The TLS handshake can take several seconds and would trigger watchdog
|
||||||
|
LOG_DEBUG(TAG, "Disabling watchdog for download...");
|
||||||
|
esp_task_wdt_delete(NULL);
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "Sending HTTP GET request...");
|
||||||
int httpCode = http.GET();
|
int httpCode = http.GET();
|
||||||
|
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
// Re-enable task watchdog after HTTP request completes
|
||||||
LOG_ERROR("HTTP GET failed, error: %s", http.errorToString(httpCode).c_str());
|
esp_task_wdt_add(NULL);
|
||||||
|
LOG_DEBUG(TAG, "Watchdog re-enabled after HTTP request");
|
||||||
|
|
||||||
|
if (httpCode != HTTP_CODE_OK && httpCode != HTTP_CODE_MOVED_PERMANENTLY && httpCode != HTTP_CODE_FOUND) {
|
||||||
|
LOG_ERROR(TAG, "HTTP GET failed, code: %d, error: %s", httpCode, http.errorToString(httpCode).c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!initializeSD()) {
|
if (!initializeSD()) {
|
||||||
http.end();
|
http.end();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
if (!ensureDirectoryExists(directory)) {
|
if (!ensureDirectoryExists(directory)) {
|
||||||
LOG_ERROR("Failed to create directory: %s", directory.c_str());
|
LOG_ERROR(TAG, "Failed to create directory: %s", directory.c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,34 +133,79 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
|
|||||||
|
|
||||||
File file = SD.open(fullPath.c_str(), FILE_WRITE);
|
File file = SD.open(fullPath.c_str(), FILE_WRITE);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERROR("Failed to open file for writing: %s", fullPath.c_str());
|
LOG_ERROR(TAG, "Failed to open file for writing: %s", fullPath.c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get stream and file size
|
||||||
WiFiClient* stream = http.getStreamPtr();
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
uint8_t buffer[1024];
|
int contentLength = http.getSize();
|
||||||
int bytesRead;
|
LOG_DEBUG(TAG, "Content length: %d bytes", contentLength);
|
||||||
|
|
||||||
while (http.connected() && (bytesRead = stream->readBytes(buffer, sizeof(buffer))) > 0) {
|
uint8_t buffer[512]; // Smaller buffer for better responsiveness
|
||||||
|
size_t totalBytes = 0;
|
||||||
|
unsigned long lastYield = millis();
|
||||||
|
unsigned long lastLog = millis();
|
||||||
|
|
||||||
|
// Download with aggressive watchdog feeding
|
||||||
|
while (http.connected() && (contentLength <= 0 || totalBytes < contentLength)) {
|
||||||
|
// Check available data
|
||||||
|
size_t availableSize = stream->available();
|
||||||
|
|
||||||
|
if (availableSize) {
|
||||||
|
// Read available data (up to buffer size)
|
||||||
|
size_t readSize = availableSize > sizeof(buffer) ? sizeof(buffer) : availableSize;
|
||||||
|
int bytesRead = stream->readBytes(buffer, readSize);
|
||||||
|
|
||||||
|
if (bytesRead > 0) {
|
||||||
file.write(buffer, bytesRead);
|
file.write(buffer, bytesRead);
|
||||||
|
totalBytes += bytesRead;
|
||||||
|
|
||||||
|
// Log progress every 5 seconds
|
||||||
|
if (millis() - lastLog > 5000) {
|
||||||
|
LOG_DEBUG(TAG, "Download progress: %u bytes", totalBytes);
|
||||||
|
lastLog = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggressive task yielding every 50ms to prevent watchdog timeout
|
||||||
|
if (millis() - lastYield > 50) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(5 / portTICK_PERIOD_MS); // Let other tasks run (5ms)
|
||||||
|
lastYield = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit if no data and connection closed
|
||||||
|
if (!availableSize && !http.connected()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yield and small delay if no data available yet
|
||||||
|
if (!availableSize) {
|
||||||
|
yield();
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
http.end();
|
http.end();
|
||||||
LOG_INFO("Download complete, file saved to: %s", fullPath.c_str());
|
if (secureClient) delete secureClient;
|
||||||
|
LOG_INFO(TAG, "Download complete, file saved to: %s (%u bytes)", fullPath.c_str(), totalBytes);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
String FileManager::listFilesAsJson(const char* dirPath) {
|
String FileManager::listFilesAsJson(const char* dirPath) {
|
||||||
if (!initializeSD()) {
|
if (!initializeSD()) {
|
||||||
LOG_ERROR("SD initialization failed");
|
LOG_ERROR(TAG, "SD initialization failed");
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
File dir = SD.open(dirPath);
|
File dir = SD.open(dirPath);
|
||||||
if (!dir || !dir.isDirectory()) {
|
if (!dir || !dir.isDirectory()) {
|
||||||
LOG_ERROR("Directory not found: %s", dirPath);
|
LOG_ERROR(TAG, "Directory not found: %s", dirPath);
|
||||||
return "{}";
|
return "{}";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,10 +244,10 @@ bool FileManager::deleteFile(const String& filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (SD.remove(filePath.c_str())) {
|
if (SD.remove(filePath.c_str())) {
|
||||||
LOG_INFO("File deleted: %s", filePath.c_str());
|
LOG_INFO(TAG, "File deleted: %s", filePath.c_str());
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Failed to delete file: %s", filePath.c_str());
|
LOG_ERROR(TAG, "Failed to delete file: %s", filePath.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,6 +271,52 @@ size_t FileManager::getFileSize(const String& filePath) {
|
|||||||
return size;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileManager::writeJsonFile(const String& filePath, JsonDocument& doc) {
|
||||||
|
if (!initializeSD()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = SD.open(filePath.c_str(), FILE_WRITE);
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERROR(TAG, "Failed to open file for writing: %s", filePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serializeJson(doc, file) == 0) {
|
||||||
|
LOG_ERROR(TAG, "Failed to write JSON to file: %s", filePath.c_str());
|
||||||
|
file.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
LOG_DEBUG(TAG, "JSON file written successfully: %s", filePath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool FileManager::readJsonFile(const String& filePath, JsonDocument& doc) {
|
||||||
|
if (!initializeSD()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = SD.open(filePath.c_str(), FILE_READ);
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERROR(TAG, "Failed to open file for reading: %s", filePath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeserializationError error = deserializeJson(doc, file);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
LOG_ERROR(TAG, "Failed to parse JSON from file: %s, error: %s",
|
||||||
|
filePath.c_str(), error.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "JSON file read successfully: %s", filePath.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
// HEALTH CHECK IMPLEMENTATION
|
// HEALTH CHECK IMPLEMENTATION
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -182,26 +324,26 @@ size_t FileManager::getFileSize(const String& filePath) {
|
|||||||
bool FileManager::isHealthy() const {
|
bool FileManager::isHealthy() const {
|
||||||
// Check if ConfigManager is available
|
// Check if ConfigManager is available
|
||||||
if (!configManager) {
|
if (!configManager) {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - ConfigManager not available");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - ConfigManager not available");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if SD card can be initialized
|
// Check if SD card can be initialized
|
||||||
uint8_t sdPin = configManager->getHardwareConfig().sdChipSelect;
|
uint8_t sdPin = configManager->getHardwareConfig().sdChipSelect;
|
||||||
if (!SD.begin(sdPin)) {
|
if (!SD.begin(sdPin)) {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - SD Card initialization failed");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - SD Card initialization failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we can read from SD card (test with root directory)
|
// Check if we can read from SD card (test with root directory)
|
||||||
File root = SD.open("/");
|
File root = SD.open("/");
|
||||||
if (!root) {
|
if (!root) {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - Cannot access SD root directory");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - Cannot access SD root directory");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!root.isDirectory()) {
|
if (!root.isDirectory()) {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - SD root is not a directory");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - SD root is not a directory");
|
||||||
root.close();
|
root.close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -212,7 +354,7 @@ bool FileManager::isHealthy() const {
|
|||||||
String testFile = "/health_test.tmp";
|
String testFile = "/health_test.tmp";
|
||||||
File file = SD.open(testFile.c_str(), FILE_WRITE);
|
File file = SD.open(testFile.c_str(), FILE_WRITE);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - Cannot write to SD card");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - Cannot write to SD card");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +364,7 @@ bool FileManager::isHealthy() const {
|
|||||||
// Verify we can read the test file
|
// Verify we can read the test file
|
||||||
file = SD.open(testFile.c_str(), FILE_READ);
|
file = SD.open(testFile.c_str(), FILE_READ);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - Cannot read test file from SD card");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - Cannot read test file from SD card");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,9 +375,24 @@ bool FileManager::isHealthy() const {
|
|||||||
SD.remove(testFile.c_str());
|
SD.remove(testFile.c_str());
|
||||||
|
|
||||||
if (content != "health_check") {
|
if (content != "health_check") {
|
||||||
LOG_DEBUG("FileManager: Unhealthy - SD card read/write test failed");
|
LOG_DEBUG(TAG, "FileManager: Unhealthy - SD card read/write test failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool FileManager::appendLine(const String& filePath, const String& line) {
|
||||||
|
if (!initializeSD()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File file = SD.open(filePath.c_str(), FILE_APPEND);
|
||||||
|
if (!file) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.println(line);
|
||||||
|
file.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
#include <SD.h>
|
#include <SD.h>
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
#include <WiFiClient.h>
|
#include <WiFiClient.h>
|
||||||
|
#include <WiFiClientSecure.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
#include "../ConfigManager/ConfigManager.hpp"
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
|
|
||||||
@@ -45,6 +47,13 @@ public:
|
|||||||
bool createDirectory(const String& dirPath);
|
bool createDirectory(const String& dirPath);
|
||||||
size_t getFileSize(const String& filePath);
|
size_t getFileSize(const String& filePath);
|
||||||
|
|
||||||
|
// Generic read/write for JSON data
|
||||||
|
bool writeJsonFile(const String& filePath, JsonDocument& doc);
|
||||||
|
bool readJsonFile(const String& filePath, JsonDocument& doc);
|
||||||
|
|
||||||
|
// Append a single text line to a file (used by SD log channel)
|
||||||
|
bool appendLine(const String& filePath, const String& line);
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// HEALTH CHECK METHOD
|
// HEALTH CHECK METHOD
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "FirmwareValidator.hpp"
|
#include "FirmwareValidator.hpp"
|
||||||
|
|
||||||
|
#define TAG "FirmwareValidator"
|
||||||
#include "../HealthMonitor/HealthMonitor.hpp"
|
#include "../HealthMonitor/HealthMonitor.hpp"
|
||||||
#include "../ConfigManager/ConfigManager.hpp"
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
#include <esp_task_wdt.h>
|
#include <esp_task_wdt.h>
|
||||||
@@ -43,45 +45,45 @@ FirmwareValidator::~FirmwareValidator() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool FirmwareValidator::begin(HealthMonitor* healthMonitor, ConfigManager* configManager) {
|
bool FirmwareValidator::begin(HealthMonitor* healthMonitor, ConfigManager* configManager) {
|
||||||
LOG_INFO("🛡️ Initializing Firmware Validator System");
|
LOG_INFO(TAG, "🛡️ Initializing Firmware Validator System");
|
||||||
|
|
||||||
_healthMonitor = healthMonitor;
|
_healthMonitor = healthMonitor;
|
||||||
_configManager = configManager;
|
_configManager = configManager;
|
||||||
|
|
||||||
// Initialize NVS for persistent state storage
|
// Initialize NVS for persistent state storage
|
||||||
if (!initializeNVS()) {
|
if (!initializeNVS()) {
|
||||||
LOG_ERROR("❌ Failed to initialize NVS for firmware validation");
|
LOG_ERROR(TAG, "❌ Failed to initialize NVS for firmware validation");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize ESP32 partition information
|
// Initialize ESP32 partition information
|
||||||
if (!initializePartitions()) {
|
if (!initializePartitions()) {
|
||||||
LOG_ERROR("❌ Failed to initialize ESP32 partitions");
|
LOG_ERROR(TAG, "❌ Failed to initialize ESP32 partitions");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load previous validation state
|
// Load previous validation state
|
||||||
loadValidationState();
|
loadValidationState();
|
||||||
|
|
||||||
LOG_INFO("✅ Firmware Validator initialized");
|
LOG_INFO(TAG, "✅ Firmware Validator initialized");
|
||||||
LOG_INFO("📍 Running partition: %s", getPartitionLabel(_runningPartition).c_str());
|
LOG_INFO(TAG, "📍 Running partition: %s", getPartitionLabel(_runningPartition).c_str());
|
||||||
LOG_INFO("📍 Backup partition: %s", getPartitionLabel(_backupPartition).c_str());
|
LOG_INFO(TAG, "📍 Backup partition: %s", getPartitionLabel(_backupPartition).c_str());
|
||||||
LOG_INFO("🔄 Validation state: %s", validationStateToString(_validationState).c_str());
|
LOG_INFO(TAG, "🔄 Validation state: %s", validationStateToString(_validationState).c_str());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FirmwareValidator::performStartupValidation() {
|
bool FirmwareValidator::performStartupValidation() {
|
||||||
LOG_INFO("🚀 Starting firmware startup validation...");
|
LOG_INFO(TAG, "🚀 Starting firmware startup validation...");
|
||||||
|
|
||||||
// Check if this is a new firmware that needs validation
|
// Check if this is a new firmware that needs validation
|
||||||
const esp_partition_t* bootPartition = esp_ota_get_boot_partition();
|
const esp_partition_t* bootPartition = esp_ota_get_boot_partition();
|
||||||
const esp_partition_t* runningPartition = esp_ota_get_running_partition();
|
const esp_partition_t* runningPartition = esp_ota_get_running_partition();
|
||||||
|
|
||||||
if (bootPartition != runningPartition) {
|
if (bootPartition != runningPartition) {
|
||||||
LOG_WARNING("⚠️ Boot partition differs from running partition!");
|
LOG_WARNING(TAG, "⚠️ Boot partition differs from running partition!");
|
||||||
LOG_WARNING(" Boot: %s", getPartitionLabel(bootPartition).c_str());
|
LOG_WARNING(TAG, " Boot: %s", getPartitionLabel(bootPartition).c_str());
|
||||||
LOG_WARNING(" Running: %s", getPartitionLabel(runningPartition).c_str());
|
LOG_WARNING(TAG, " Running: %s", getPartitionLabel(runningPartition).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment boot count for this session
|
// Increment boot count for this session
|
||||||
@@ -91,11 +93,11 @@ bool FirmwareValidator::performStartupValidation() {
|
|||||||
if (_validationState == FirmwareValidationState::UNKNOWN) {
|
if (_validationState == FirmwareValidationState::UNKNOWN) {
|
||||||
// First boot of potentially new firmware
|
// First boot of potentially new firmware
|
||||||
_validationState = FirmwareValidationState::STARTUP_PENDING;
|
_validationState = FirmwareValidationState::STARTUP_PENDING;
|
||||||
LOG_INFO("🆕 New firmware detected - entering validation mode");
|
LOG_INFO(TAG, "🆕 New firmware detected - entering validation mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_validationState == FirmwareValidationState::STARTUP_PENDING) {
|
if (_validationState == FirmwareValidationState::STARTUP_PENDING) {
|
||||||
LOG_INFO("🔍 Performing startup validation...");
|
LOG_INFO(TAG, "🔍 Performing startup validation...");
|
||||||
_validationState = FirmwareValidationState::STARTUP_RUNNING;
|
_validationState = FirmwareValidationState::STARTUP_RUNNING;
|
||||||
_validationStartTime = millis();
|
_validationStartTime = millis();
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ bool FirmwareValidator::performStartupValidation() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_WARNING("⚠️ Startup health check failed, retrying...");
|
LOG_WARNING(TAG, "⚠️ Startup health check failed, retrying...");
|
||||||
delay(1000); // Wait 1 second before retry
|
delay(1000); // Wait 1 second before retry
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,18 +119,18 @@ bool FirmwareValidator::performStartupValidation() {
|
|||||||
_validationState = FirmwareValidationState::RUNTIME_TESTING;
|
_validationState = FirmwareValidationState::RUNTIME_TESTING;
|
||||||
_startupRetryCount = 0; // Reset retry count on success
|
_startupRetryCount = 0; // Reset retry count on success
|
||||||
saveValidationState();
|
saveValidationState();
|
||||||
LOG_INFO("✅ Firmware startup validation PASSED - proceeding with initialization");
|
LOG_INFO(TAG, "✅ Firmware startup validation PASSED - proceeding with initialization");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Startup validation FAILED after %lu ms", _config.startupTimeoutMs);
|
LOG_ERROR(TAG, "❌ Startup validation FAILED after %lu ms", _config.startupTimeoutMs);
|
||||||
_startupRetryCount++;
|
_startupRetryCount++;
|
||||||
|
|
||||||
if (_startupRetryCount >= _config.maxStartupRetries) {
|
if (_startupRetryCount >= _config.maxStartupRetries) {
|
||||||
LOG_ERROR("💥 Maximum startup retries exceeded - triggering rollback");
|
LOG_ERROR(TAG, "💥 Maximum startup retries exceeded - triggering rollback");
|
||||||
handleValidationFailure("Startup validation failed repeatedly");
|
handleValidationFailure("Startup validation failed repeatedly");
|
||||||
return false; // This will trigger rollback and reboot
|
return false; // This will trigger rollback and reboot
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("🔄 Startup retry %d/%d - rebooting...",
|
LOG_WARNING(TAG, "🔄 Startup retry %d/%d - rebooting...",
|
||||||
_startupRetryCount, _config.maxStartupRetries);
|
_startupRetryCount, _config.maxStartupRetries);
|
||||||
saveValidationState();
|
saveValidationState();
|
||||||
delay(1000);
|
delay(1000);
|
||||||
@@ -137,20 +139,20 @@ bool FirmwareValidator::performStartupValidation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (_validationState == FirmwareValidationState::VALIDATED) {
|
} else if (_validationState == FirmwareValidationState::VALIDATED) {
|
||||||
LOG_INFO("✅ Firmware already validated - normal operation");
|
LOG_INFO(TAG, "✅ Firmware already validated - normal operation");
|
||||||
return true;
|
return true;
|
||||||
} else if (_validationState == FirmwareValidationState::STARTUP_RUNNING) {
|
} else if (_validationState == FirmwareValidationState::STARTUP_RUNNING) {
|
||||||
// Handle interrupted validation from previous boot
|
// Handle interrupted validation from previous boot
|
||||||
LOG_INFO("🔄 Resuming interrupted validation - transitioning to runtime testing");
|
LOG_INFO(TAG, "🔄 Resuming interrupted validation - transitioning to runtime testing");
|
||||||
_validationState = FirmwareValidationState::RUNTIME_TESTING;
|
_validationState = FirmwareValidationState::RUNTIME_TESTING;
|
||||||
saveValidationState();
|
saveValidationState();
|
||||||
return true;
|
return true;
|
||||||
} else if (_validationState == FirmwareValidationState::RUNTIME_TESTING) {
|
} else if (_validationState == FirmwareValidationState::RUNTIME_TESTING) {
|
||||||
// Already in runtime testing from previous boot
|
// Already in runtime testing from previous boot
|
||||||
LOG_INFO("🔄 Continuing runtime validation from previous session");
|
LOG_INFO(TAG, "🔄 Continuing runtime validation from previous session");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("⚠️ Unexpected validation state: %s",
|
LOG_WARNING(TAG, "⚠️ Unexpected validation state: %s",
|
||||||
validationStateToString(_validationState).c_str());
|
validationStateToString(_validationState).c_str());
|
||||||
return true; // Continue anyway
|
return true; // Continue anyway
|
||||||
}
|
}
|
||||||
@@ -158,12 +160,12 @@ bool FirmwareValidator::performStartupValidation() {
|
|||||||
|
|
||||||
void FirmwareValidator::startRuntimeValidation() {
|
void FirmwareValidator::startRuntimeValidation() {
|
||||||
if (_validationState != FirmwareValidationState::RUNTIME_TESTING) {
|
if (_validationState != FirmwareValidationState::RUNTIME_TESTING) {
|
||||||
LOG_WARNING("⚠️ Runtime validation called in wrong state: %s",
|
LOG_WARNING(TAG, "⚠️ Runtime validation called in wrong state: %s",
|
||||||
validationStateToString(_validationState).c_str());
|
validationStateToString(_validationState).c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("🏃 Starting extended runtime validation (%lu ms timeout)",
|
LOG_INFO(TAG, "🏃 Starting extended runtime validation (%lu ms timeout)",
|
||||||
_config.runtimeTimeoutMs);
|
_config.runtimeTimeoutMs);
|
||||||
|
|
||||||
_validationStartTime = millis();
|
_validationStartTime = millis();
|
||||||
@@ -180,7 +182,7 @@ void FirmwareValidator::startRuntimeValidation() {
|
|||||||
if (_validationTimer) {
|
if (_validationTimer) {
|
||||||
xTimerStart(_validationTimer, 0);
|
xTimerStart(_validationTimer, 0);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Failed to create validation timer");
|
LOG_ERROR(TAG, "❌ Failed to create validation timer");
|
||||||
handleValidationFailure("Timer creation failed");
|
handleValidationFailure("Timer creation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -197,7 +199,7 @@ void FirmwareValidator::startRuntimeValidation() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!_monitoringTask) {
|
if (!_monitoringTask) {
|
||||||
LOG_ERROR("❌ Failed to create monitoring task");
|
LOG_ERROR(TAG, "❌ Failed to create monitoring task");
|
||||||
handleValidationFailure("Monitoring task creation failed");
|
handleValidationFailure("Monitoring task creation failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -207,21 +209,21 @@ void FirmwareValidator::startRuntimeValidation() {
|
|||||||
setupWatchdog();
|
setupWatchdog();
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("✅ Runtime validation started - monitoring system health...");
|
LOG_INFO(TAG, "✅ Runtime validation started - monitoring system health...");
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::commitFirmware() {
|
void FirmwareValidator::commitFirmware() {
|
||||||
if (_validationState == FirmwareValidationState::VALIDATED) {
|
if (_validationState == FirmwareValidationState::VALIDATED) {
|
||||||
LOG_INFO("✅ Firmware already committed");
|
LOG_INFO(TAG, "✅ Firmware already committed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("💾 Committing firmware as valid and stable...");
|
LOG_INFO(TAG, "💾 Committing firmware as valid and stable...");
|
||||||
|
|
||||||
// Mark current partition as valid boot partition
|
// Mark current partition as valid boot partition
|
||||||
esp_err_t err = esp_ota_set_boot_partition(_runningPartition);
|
esp_err_t err = esp_ota_set_boot_partition(_runningPartition);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
LOG_ERROR("❌ Failed to set boot partition: %s", esp_err_to_name(err));
|
LOG_ERROR(TAG, "❌ Failed to set boot partition: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,11 +242,11 @@ void FirmwareValidator::commitFirmware() {
|
|||||||
_monitoringTask = nullptr;
|
_monitoringTask = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("🎉 Firmware successfully committed! System is now stable.");
|
LOG_INFO(TAG, "🎉 Firmware successfully committed! System is now stable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::rollbackFirmware() {
|
void FirmwareValidator::rollbackFirmware() {
|
||||||
LOG_WARNING("🔄 Manual firmware rollback requested");
|
LOG_WARNING(TAG, "🔄 Manual firmware rollback requested");
|
||||||
handleValidationFailure("Manual rollback requested");
|
handleValidationFailure("Manual rollback requested");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,13 +260,13 @@ bool FirmwareValidator::initializeNVS() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
LOG_ERROR("❌ Failed to initialize NVS flash: %s", esp_err_to_name(err));
|
LOG_ERROR(TAG, "❌ Failed to initialize NVS flash: %s", esp_err_to_name(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &_nvsHandle);
|
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &_nvsHandle);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
LOG_ERROR("❌ Failed to open NVS namespace: %s", esp_err_to_name(err));
|
LOG_ERROR(TAG, "❌ Failed to open NVS namespace: %s", esp_err_to_name(err));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,9 +281,9 @@ void FirmwareValidator::loadValidationState() {
|
|||||||
err = nvs_get_u8(_nvsHandle, NVS_STATE_KEY, &state);
|
err = nvs_get_u8(_nvsHandle, NVS_STATE_KEY, &state);
|
||||||
if (err == ESP_OK) {
|
if (err == ESP_OK) {
|
||||||
_validationState = static_cast<FirmwareValidationState>(state);
|
_validationState = static_cast<FirmwareValidationState>(state);
|
||||||
LOG_DEBUG("📖 NVS validation state found: %s", validationStateToString(_validationState).c_str());
|
LOG_DEBUG(TAG, "📖 NVS validation state found: %s", validationStateToString(_validationState).c_str());
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG("📖 No NVS validation state found, using UNKNOWN (error: %s)", esp_err_to_name(err));
|
LOG_DEBUG(TAG, "📖 No NVS validation state found, using UNKNOWN (error: %s)", esp_err_to_name(err));
|
||||||
_validationState = FirmwareValidationState::UNKNOWN;
|
_validationState = FirmwareValidationState::UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,7 +291,7 @@ void FirmwareValidator::loadValidationState() {
|
|||||||
nvs_get_u8(_nvsHandle, NVS_RETRY_COUNT_KEY, &_startupRetryCount);
|
nvs_get_u8(_nvsHandle, NVS_RETRY_COUNT_KEY, &_startupRetryCount);
|
||||||
nvs_get_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, &_runtimeFailureCount);
|
nvs_get_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, &_runtimeFailureCount);
|
||||||
|
|
||||||
LOG_DEBUG("📖 Loaded validation state: %s (retries: %d, failures: %d)",
|
LOG_DEBUG(TAG, "📖 Loaded validation state: %s (retries: %d, failures: %d)",
|
||||||
validationStateToString(_validationState).c_str(),
|
validationStateToString(_validationState).c_str(),
|
||||||
_startupRetryCount, _runtimeFailureCount);
|
_startupRetryCount, _runtimeFailureCount);
|
||||||
}
|
}
|
||||||
@@ -300,7 +302,7 @@ void FirmwareValidator::saveValidationState() {
|
|||||||
// Save validation state
|
// Save validation state
|
||||||
err = nvs_set_u8(_nvsHandle, NVS_STATE_KEY, static_cast<uint8_t>(_validationState));
|
err = nvs_set_u8(_nvsHandle, NVS_STATE_KEY, static_cast<uint8_t>(_validationState));
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
LOG_ERROR("❌ Failed to save validation state: %s", esp_err_to_name(err));
|
LOG_ERROR(TAG, "❌ Failed to save validation state: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save retry counts
|
// Save retry counts
|
||||||
@@ -314,16 +316,16 @@ void FirmwareValidator::saveValidationState() {
|
|||||||
// Commit changes
|
// Commit changes
|
||||||
err = nvs_commit(_nvsHandle);
|
err = nvs_commit(_nvsHandle);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
LOG_ERROR("❌ Failed to commit NVS changes: %s", esp_err_to_name(err));
|
LOG_ERROR(TAG, "❌ Failed to commit NVS changes: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("💾 Saved validation state: %s", validationStateToString(_validationState).c_str());
|
LOG_DEBUG(TAG, "💾 Saved validation state: %s", validationStateToString(_validationState).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FirmwareValidator::initializePartitions() {
|
bool FirmwareValidator::initializePartitions() {
|
||||||
_runningPartition = esp_ota_get_running_partition();
|
_runningPartition = esp_ota_get_running_partition();
|
||||||
if (!_runningPartition) {
|
if (!_runningPartition) {
|
||||||
LOG_ERROR("❌ Failed to get running partition");
|
LOG_ERROR(TAG, "❌ Failed to get running partition");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +352,7 @@ bool FirmwareValidator::initializePartitions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!_backupPartition) {
|
if (!_backupPartition) {
|
||||||
LOG_ERROR("❌ Failed to find backup partition");
|
LOG_ERROR(TAG, "❌ Failed to find backup partition");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,11 +360,11 @@ bool FirmwareValidator::initializePartitions() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool FirmwareValidator::performBasicHealthCheck() {
|
bool FirmwareValidator::performBasicHealthCheck() {
|
||||||
LOG_VERBOSE("🔍 Performing basic startup health check...");
|
LOG_VERBOSE(TAG, "🔍 Performing basic startup health check...");
|
||||||
|
|
||||||
// Check if health monitor is available
|
// Check if health monitor is available
|
||||||
if (!_healthMonitor) {
|
if (!_healthMonitor) {
|
||||||
LOG_ERROR("❌ Health monitor not available");
|
LOG_ERROR(TAG, "❌ Health monitor not available");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,20 +377,20 @@ bool FirmwareValidator::performBasicHealthCheck() {
|
|||||||
bool basicHealthOk = bellEngineOk && outputManagerOk && configManagerOk && fileManagerOk;
|
bool basicHealthOk = bellEngineOk && outputManagerOk && configManagerOk && fileManagerOk;
|
||||||
|
|
||||||
if (!basicHealthOk) {
|
if (!basicHealthOk) {
|
||||||
LOG_ERROR("❌ Basic health check failed:");
|
LOG_ERROR(TAG, "❌ Basic health check failed:");
|
||||||
if (!bellEngineOk) LOG_ERROR(" - BellEngine: FAILED");
|
if (!bellEngineOk) LOG_ERROR(TAG, " - BellEngine: FAILED");
|
||||||
if (!outputManagerOk) LOG_ERROR(" - OutputManager: FAILED");
|
if (!outputManagerOk) LOG_ERROR(TAG, " - OutputManager: FAILED");
|
||||||
if (!configManagerOk) LOG_ERROR(" - ConfigManager: FAILED");
|
if (!configManagerOk) LOG_ERROR(TAG, " - ConfigManager: FAILED");
|
||||||
if (!fileManagerOk) LOG_ERROR(" - FileManager: FAILED");
|
if (!fileManagerOk) LOG_ERROR(TAG, " - FileManager: FAILED");
|
||||||
} else {
|
} else {
|
||||||
LOG_VERBOSE("✅ Basic health check passed");
|
LOG_VERBOSE(TAG, "✅ Basic health check passed");
|
||||||
}
|
}
|
||||||
|
|
||||||
return basicHealthOk;
|
return basicHealthOk;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FirmwareValidator::performRuntimeHealthCheck() {
|
bool FirmwareValidator::performRuntimeHealthCheck() {
|
||||||
LOG_VERBOSE("🔍 Performing comprehensive runtime health check...");
|
LOG_VERBOSE(TAG, "🔍 Performing comprehensive runtime health check...");
|
||||||
|
|
||||||
if (!_healthMonitor) {
|
if (!_healthMonitor) {
|
||||||
return false;
|
return false;
|
||||||
@@ -402,7 +404,7 @@ bool FirmwareValidator::performRuntimeHealthCheck() {
|
|||||||
&& (criticalFailures == 0);
|
&& (criticalFailures == 0);
|
||||||
|
|
||||||
if (!runtimeHealthOk) {
|
if (!runtimeHealthOk) {
|
||||||
LOG_WARNING("⚠️ Runtime health check failed - Critical failures: %d, Overall: %s",
|
LOG_WARNING(TAG, "⚠️ Runtime health check failed - Critical failures: %d, Overall: %s",
|
||||||
criticalFailures,
|
criticalFailures,
|
||||||
(overallHealth == HealthStatus::HEALTHY) ? "HEALTHY" :
|
(overallHealth == HealthStatus::HEALTHY) ? "HEALTHY" :
|
||||||
(overallHealth == HealthStatus::WARNING) ? "WARNING" :
|
(overallHealth == HealthStatus::WARNING) ? "WARNING" :
|
||||||
@@ -415,18 +417,18 @@ bool FirmwareValidator::performRuntimeHealthCheck() {
|
|||||||
void FirmwareValidator::validationTimerCallback(TimerHandle_t timer) {
|
void FirmwareValidator::validationTimerCallback(TimerHandle_t timer) {
|
||||||
FirmwareValidator* validator = static_cast<FirmwareValidator*>(pvTimerGetTimerID(timer));
|
FirmwareValidator* validator = static_cast<FirmwareValidator*>(pvTimerGetTimerID(timer));
|
||||||
|
|
||||||
LOG_INFO("⏰ Runtime validation timeout reached - committing firmware");
|
LOG_INFO(TAG, "⏰ Runtime validation timeout reached - committing firmware");
|
||||||
validator->handleValidationSuccess();
|
validator->handleValidationSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::monitoringTaskFunction(void* parameter) {
|
void FirmwareValidator::monitoringTaskFunction(void* parameter) {
|
||||||
FirmwareValidator* validator = static_cast<FirmwareValidator*>(parameter);
|
FirmwareValidator* validator = static_cast<FirmwareValidator*>(parameter);
|
||||||
LOG_INFO("🔍 Firmware validation monitoring task started on Core %d", xPortGetCoreID());
|
LOG_INFO(TAG, "🔍 Firmware validation monitoring task started on Core %d", xPortGetCoreID());
|
||||||
|
|
||||||
validator->monitoringLoop();
|
validator->monitoringLoop();
|
||||||
|
|
||||||
// Task should not reach here normally
|
// Task should not reach here normally
|
||||||
LOG_WARNING("⚠️ Firmware validation monitoring task ended unexpectedly");
|
LOG_WARNING(TAG, "⚠️ Firmware validation monitoring task ended unexpectedly");
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,11 +444,11 @@ void FirmwareValidator::monitoringLoop() {
|
|||||||
|
|
||||||
if (!healthOk) {
|
if (!healthOk) {
|
||||||
_runtimeFailureCount++;
|
_runtimeFailureCount++;
|
||||||
LOG_WARNING("⚠️ Runtime health check failed (%d/%d failures)",
|
LOG_WARNING(TAG, "⚠️ Runtime health check failed (%d/%d failures)",
|
||||||
_runtimeFailureCount, _config.maxRuntimeFailures);
|
_runtimeFailureCount, _config.maxRuntimeFailures);
|
||||||
|
|
||||||
if (_runtimeFailureCount >= _config.maxRuntimeFailures) {
|
if (_runtimeFailureCount >= _config.maxRuntimeFailures) {
|
||||||
LOG_ERROR("💥 Maximum runtime failures exceeded - triggering rollback");
|
LOG_ERROR(TAG, "💥 Maximum runtime failures exceeded - triggering rollback");
|
||||||
handleValidationFailure("Too many runtime health check failures");
|
handleValidationFailure("Too many runtime health check failures");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -454,7 +456,7 @@ void FirmwareValidator::monitoringLoop() {
|
|||||||
// Reset failure count on successful health check
|
// Reset failure count on successful health check
|
||||||
if (_runtimeFailureCount > 0) {
|
if (_runtimeFailureCount > 0) {
|
||||||
_runtimeFailureCount = 0;
|
_runtimeFailureCount = 0;
|
||||||
LOG_INFO("✅ Runtime health recovered - reset failure count");
|
LOG_INFO(TAG, "✅ Runtime health recovered - reset failure count");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,13 +466,13 @@ void FirmwareValidator::monitoringLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::handleValidationSuccess() {
|
void FirmwareValidator::handleValidationSuccess() {
|
||||||
LOG_INFO("🎉 Firmware validation completed successfully!");
|
LOG_INFO(TAG, "🎉 Firmware validation completed successfully!");
|
||||||
commitFirmware();
|
commitFirmware();
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::handleValidationFailure(const String& reason) {
|
void FirmwareValidator::handleValidationFailure(const String& reason) {
|
||||||
LOG_ERROR("💥 Firmware validation FAILED: %s", reason.c_str());
|
LOG_ERROR(TAG, "💥 Firmware validation FAILED: %s", reason.c_str());
|
||||||
LOG_ERROR("🔄 Initiating firmware rollback...");
|
LOG_ERROR(TAG, "🔄 Initiating firmware rollback...");
|
||||||
|
|
||||||
_validationState = FirmwareValidationState::FAILED_RUNTIME;
|
_validationState = FirmwareValidationState::FAILED_RUNTIME;
|
||||||
saveValidationState();
|
saveValidationState();
|
||||||
@@ -479,7 +481,7 @@ void FirmwareValidator::handleValidationFailure(const String& reason) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::executeRollback() {
|
void FirmwareValidator::executeRollback() {
|
||||||
LOG_WARNING("🔄 Executing firmware rollback to previous version...");
|
LOG_WARNING(TAG, "🔄 Executing firmware rollback to previous version...");
|
||||||
|
|
||||||
// Clean up validation resources first
|
// Clean up validation resources first
|
||||||
if (_validationTimer) {
|
if (_validationTimer) {
|
||||||
@@ -496,18 +498,18 @@ void FirmwareValidator::executeRollback() {
|
|||||||
esp_err_t err = esp_ota_mark_app_invalid_rollback_and_reboot();
|
esp_err_t err = esp_ota_mark_app_invalid_rollback_and_reboot();
|
||||||
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
LOG_ERROR("❌ Failed to rollback firmware: %s", esp_err_to_name(err));
|
LOG_ERROR(TAG, "❌ Failed to rollback firmware: %s", esp_err_to_name(err));
|
||||||
LOG_ERROR("💀 System may be in unstable state - manual intervention required");
|
LOG_ERROR(TAG, "💀 System may be in unstable state - manual intervention required");
|
||||||
|
|
||||||
// If rollback fails, try manual reboot to backup partition
|
// If rollback fails, try manual reboot to backup partition
|
||||||
LOG_WARNING("🆘 Attempting manual reboot to backup partition...");
|
LOG_WARNING(TAG, "🆘 Attempting manual reboot to backup partition...");
|
||||||
|
|
||||||
if (_backupPartition) {
|
if (_backupPartition) {
|
||||||
esp_ota_set_boot_partition(_backupPartition);
|
esp_ota_set_boot_partition(_backupPartition);
|
||||||
delay(1000);
|
delay(1000);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("💀 No backup partition available - system halt");
|
LOG_ERROR(TAG, "💀 No backup partition available - system halt");
|
||||||
while(1) {
|
while(1) {
|
||||||
delay(1000); // Hang here to prevent further damage
|
delay(1000); // Hang here to prevent further damage
|
||||||
}
|
}
|
||||||
@@ -515,7 +517,7 @@ void FirmwareValidator::executeRollback() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// This point should not be reached as the device should reboot
|
// This point should not be reached as the device should reboot
|
||||||
LOG_ERROR("💀 Rollback function returned unexpectedly");
|
LOG_ERROR(TAG, "💀 Rollback function returned unexpectedly");
|
||||||
}
|
}
|
||||||
|
|
||||||
FirmwareInfo FirmwareValidator::getCurrentFirmwareInfo() const {
|
FirmwareInfo FirmwareValidator::getCurrentFirmwareInfo() const {
|
||||||
@@ -649,7 +651,7 @@ void FirmwareValidator::incrementBootCount() {
|
|||||||
nvs_set_u32(_nvsHandle, NVS_BOOT_COUNT_KEY, bootCount);
|
nvs_set_u32(_nvsHandle, NVS_BOOT_COUNT_KEY, bootCount);
|
||||||
nvs_commit(_nvsHandle);
|
nvs_commit(_nvsHandle);
|
||||||
|
|
||||||
LOG_DEBUG("📊 Boot count: %lu", bootCount);
|
LOG_DEBUG(TAG, "📊 Boot count: %lu", bootCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::resetValidationCounters() {
|
void FirmwareValidator::resetValidationCounters() {
|
||||||
@@ -661,35 +663,31 @@ void FirmwareValidator::resetValidationCounters() {
|
|||||||
nvs_set_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, 0);
|
nvs_set_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, 0);
|
||||||
nvs_commit(_nvsHandle);
|
nvs_commit(_nvsHandle);
|
||||||
|
|
||||||
LOG_DEBUG("🔄 Reset validation counters");
|
LOG_DEBUG(TAG, "🔄 Reset validation counters");
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::setupWatchdog() {
|
void FirmwareValidator::setupWatchdog() {
|
||||||
// Check if watchdog is already initialized
|
// Check if watchdog is already initialized
|
||||||
esp_task_wdt_config_t config = {
|
// Use IDF v4 API: esp_task_wdt_init(timeout_seconds, panic_on_timeout)
|
||||||
.timeout_ms = _config.watchdogTimeoutMs,
|
uint32_t timeoutSec = (_config.watchdogTimeoutMs + 999) / 1000; // ms → seconds, round up
|
||||||
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1,
|
esp_err_t err = esp_task_wdt_init(timeoutSec, true);
|
||||||
.trigger_panic = true
|
|
||||||
};
|
|
||||||
|
|
||||||
esp_err_t err = esp_task_wdt_init(&config);
|
|
||||||
if (err == ESP_ERR_INVALID_STATE) {
|
if (err == ESP_ERR_INVALID_STATE) {
|
||||||
LOG_DEBUG("🐕 Watchdog already initialized - skipping init");
|
LOG_DEBUG(TAG, "🐕 Watchdog already initialized - skipping init");
|
||||||
} else if (err != ESP_OK) {
|
} else if (err != ESP_OK) {
|
||||||
LOG_WARNING("⚠️ Failed to initialize task watchdog: %s", esp_err_to_name(err));
|
LOG_WARNING(TAG, "⚠️ Failed to initialize task watchdog: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to add current task to watchdog
|
// Try to add current task to watchdog
|
||||||
err = esp_task_wdt_add(NULL);
|
err = esp_task_wdt_add(NULL);
|
||||||
if (err == ESP_ERR_INVALID_ARG) {
|
if (err == ESP_ERR_INVALID_ARG) {
|
||||||
LOG_DEBUG("🐕 Task already added to watchdog");
|
LOG_DEBUG(TAG, "🐕 Task already added to watchdog");
|
||||||
} else if (err != ESP_OK) {
|
} else if (err != ESP_OK) {
|
||||||
LOG_WARNING("⚠️ Failed to add task to watchdog: %s", esp_err_to_name(err));
|
LOG_WARNING(TAG, "⚠️ Failed to add task to watchdog: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("🐕 Watchdog enabled with %lu second timeout", _config.watchdogTimeoutMs / 1000);
|
LOG_INFO(TAG, "🐕 Watchdog enabled with %lu second timeout", _config.watchdogTimeoutMs / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
void FirmwareValidator::feedWatchdog() {
|
void FirmwareValidator::feedWatchdog() {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "HealthMonitor.hpp"
|
#include "HealthMonitor.hpp"
|
||||||
|
|
||||||
|
#define TAG "HealthMonitor"
|
||||||
#include "../BellEngine/BellEngine.hpp"
|
#include "../BellEngine/BellEngine.hpp"
|
||||||
#include "../OutputManager/OutputManager.hpp"
|
#include "../OutputManager/OutputManager.hpp"
|
||||||
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
||||||
@@ -29,7 +31,7 @@ HealthMonitor::~HealthMonitor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool HealthMonitor::begin() {
|
bool HealthMonitor::begin() {
|
||||||
LOG_INFO("🏥 Initializing Health Monitor System");
|
LOG_INFO(TAG, "🏥 Initializing Health Monitor System");
|
||||||
|
|
||||||
// Create monitoring task if auto-monitoring is enabled
|
// Create monitoring task if auto-monitoring is enabled
|
||||||
if (_autoMonitoring) {
|
if (_autoMonitoring) {
|
||||||
@@ -44,14 +46,14 @@ bool HealthMonitor::begin() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (_monitoringTaskHandle != nullptr) {
|
if (_monitoringTaskHandle != nullptr) {
|
||||||
LOG_INFO("✅ Health Monitor initialized with automatic monitoring");
|
LOG_INFO(TAG, "✅ Health Monitor initialized with automatic monitoring");
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Failed to create Health Monitor task");
|
LOG_ERROR(TAG, "❌ Failed to create Health Monitor task");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("✅ Health Monitor initialized (manual mode)");
|
LOG_INFO(TAG, "✅ Health Monitor initialized (manual mode)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,12 +73,12 @@ void HealthMonitor::initializeSubsystemHealth() {
|
|||||||
_subsystemHealth["OTAManager"] = SubsystemHealth("OTAManager", false); // Non-critical
|
_subsystemHealth["OTAManager"] = SubsystemHealth("OTAManager", false); // Non-critical
|
||||||
_subsystemHealth["Networking"] = SubsystemHealth("Networking", false); // Non-critical
|
_subsystemHealth["Networking"] = SubsystemHealth("Networking", false); // Non-critical
|
||||||
|
|
||||||
LOG_DEBUG("🏗️ Initialized health monitoring for %d subsystems", _subsystemHealth.size());
|
LOG_DEBUG(TAG, "🏗️ Initialized health monitoring for %d subsystems", _subsystemHealth.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
void HealthMonitor::monitoringTask(void* parameter) {
|
void HealthMonitor::monitoringTask(void* parameter) {
|
||||||
HealthMonitor* monitor = static_cast<HealthMonitor*>(parameter);
|
HealthMonitor* monitor = static_cast<HealthMonitor*>(parameter);
|
||||||
LOG_INFO("🏥 Health Monitor task started on Core %d", xPortGetCoreID());
|
LOG_INFO(TAG, "🏥 Health Monitor task started on Core %d", xPortGetCoreID());
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
monitor->monitoringLoop();
|
monitor->monitoringLoop();
|
||||||
@@ -88,12 +90,12 @@ void HealthMonitor::monitoringLoop() {
|
|||||||
|
|
||||||
if (_player) {
|
if (_player) {
|
||||||
if (_player->_status != PlayerStatus::STOPPED) {
|
if (_player->_status != PlayerStatus::STOPPED) {
|
||||||
LOG_VERBOSE("⏸️ Skipping health check during active playback");
|
LOG_VERBOSE(TAG, "⏸️ Skipping health check during active playback");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_VERBOSE("🔍 Performing periodic health check...");
|
LOG_VERBOSE(TAG, "🔍 Performing periodic health check...");
|
||||||
|
|
||||||
HealthStatus overallHealth = performFullHealthCheck();
|
HealthStatus overallHealth = performFullHealthCheck();
|
||||||
|
|
||||||
@@ -102,25 +104,32 @@ void HealthMonitor::monitoringLoop() {
|
|||||||
uint8_t warningCount = getWarningCount();
|
uint8_t warningCount = getWarningCount();
|
||||||
|
|
||||||
if (criticalCount > 0) {
|
if (criticalCount > 0) {
|
||||||
LOG_WARNING("🚨 Health Monitor: %d critical failures detected!", criticalCount);
|
LOG_WARNING(TAG, "🚨 Health Monitor: %d critical failures detected!", criticalCount);
|
||||||
|
|
||||||
// List critical failures
|
// List critical failures
|
||||||
for (const auto& [name, health] : _subsystemHealth) {
|
for (const auto& [name, health] : _subsystemHealth) {
|
||||||
if (health.status == HealthStatus::CRITICAL || health.status == HealthStatus::FAILED) {
|
if (health.status == HealthStatus::CRITICAL || health.status == HealthStatus::FAILED) {
|
||||||
LOG_ERROR("❌ CRITICAL: %s - %s", name.c_str(), health.lastError.c_str());
|
LOG_ERROR(TAG, "❌ CRITICAL: %s - %s", name.c_str(), health.lastError.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if firmware rollback is recommended
|
// Check if firmware rollback is recommended
|
||||||
if (shouldRollbackFirmware()) {
|
if (shouldRollbackFirmware()) {
|
||||||
LOG_ERROR("🔄 FIRMWARE ROLLBACK RECOMMENDED - Too many critical failures");
|
LOG_ERROR(TAG, "🔄 FIRMWARE ROLLBACK RECOMMENDED - Too many critical failures");
|
||||||
// In a real system, this would trigger an OTA rollback
|
// In a real system, this would trigger an OTA rollback
|
||||||
// For now, we just log the recommendation
|
// For now, we just log the recommendation
|
||||||
}
|
}
|
||||||
} else if (warningCount > 0) {
|
} else if (warningCount > 0) {
|
||||||
LOG_WARNING("⚠️ Health Monitor: %d warnings detected", warningCount);
|
LOG_WARNING(TAG, "⚠️ Health Monitor: %d warning(s) detected", warningCount);
|
||||||
|
|
||||||
|
// List every subsystem that is in WARNING state
|
||||||
|
for (const auto& [name, health] : _subsystemHealth) {
|
||||||
|
if (health.status == HealthStatus::WARNING) {
|
||||||
|
LOG_WARNING(TAG, "⚠️ WARNING: %s - %s", name.c_str(), health.lastError.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_VERBOSE("✅ All subsystems healthy");
|
LOG_VERBOSE(TAG, "✅ All subsystems healthy");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +228,7 @@ HealthStatus HealthMonitor::performFullHealthCheck() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsigned long elapsed = millis() - startTime;
|
unsigned long elapsed = millis() - startTime;
|
||||||
LOG_VERBOSE("🔍 Health check completed: %d systems in %lums", checkedSystems, elapsed);
|
LOG_VERBOSE(TAG, "🔍 Health check completed: %d systems in %lums", checkedSystems, elapsed);
|
||||||
|
|
||||||
return calculateOverallHealth();
|
return calculateOverallHealth();
|
||||||
}
|
}
|
||||||
@@ -228,7 +237,7 @@ HealthStatus HealthMonitor::checkSubsystemHealth(const String& subsystemName) {
|
|||||||
// Perform health check on specific subsystem
|
// Perform health check on specific subsystem
|
||||||
auto it = _subsystemHealth.find(subsystemName);
|
auto it = _subsystemHealth.find(subsystemName);
|
||||||
if (it == _subsystemHealth.end()) {
|
if (it == _subsystemHealth.end()) {
|
||||||
LOG_WARNING("❓ Unknown subsystem: %s", subsystemName.c_str());
|
LOG_WARNING(TAG, "❓ Unknown subsystem: %s", subsystemName.c_str());
|
||||||
return HealthStatus::FAILED;
|
return HealthStatus::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +265,7 @@ HealthStatus HealthMonitor::checkSubsystemHealth(const String& subsystemName) {
|
|||||||
} else if (subsystemName == "FileManager" && _fileManager) {
|
} else if (subsystemName == "FileManager" && _fileManager) {
|
||||||
healthy = _fileManager->isHealthy();
|
healthy = _fileManager->isHealthy();
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("🔌 Subsystem %s not connected to health monitor", subsystemName.c_str());
|
LOG_WARNING(TAG, "🔌 Subsystem %s not connected to health monitor", subsystemName.c_str());
|
||||||
return HealthStatus::FAILED;
|
return HealthStatus::FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,7 +391,7 @@ void HealthMonitor::updateSubsystemHealth(const String& name, HealthStatus statu
|
|||||||
it->second.lastError = error;
|
it->second.lastError = error;
|
||||||
it->second.lastCheck = millis();
|
it->second.lastCheck = millis();
|
||||||
|
|
||||||
LOG_VERBOSE("🔍 %s: %s %s",
|
LOG_VERBOSE(TAG, "🔍 %s: %s %s",
|
||||||
name.c_str(),
|
name.c_str(),
|
||||||
healthStatusToString(status).c_str(),
|
healthStatusToString(status).c_str(),
|
||||||
error.isEmpty() ? "" : ("(" + error + ")").c_str());
|
error.isEmpty() ? "" : ("(" + error + ")").c_str());
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "InputManager.hpp"
|
#include "InputManager.hpp"
|
||||||
|
|
||||||
|
#define TAG "InputManager"
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
|
|
||||||
// Static instance pointer
|
// Static instance pointer
|
||||||
@@ -28,7 +30,7 @@ InputManager::~InputManager() {
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
bool InputManager::begin() {
|
bool InputManager::begin() {
|
||||||
LOG_INFO("InputManager: Initializing input handling system");
|
LOG_INFO(TAG, "InputManager: Initializing input handling system");
|
||||||
|
|
||||||
// Configure factory reset button
|
// Configure factory reset button
|
||||||
configureButton(_factoryResetButton.config);
|
configureButton(_factoryResetButton.config);
|
||||||
@@ -51,13 +53,13 @@ bool InputManager::begin() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result != pdPASS) {
|
if (result != pdPASS) {
|
||||||
LOG_ERROR("InputManager: Failed to create input task!");
|
LOG_ERROR(TAG, "InputManager: Failed to create input task!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
|
||||||
LOG_INFO("InputManager: Initialization complete - Factory Reset on GPIO 0 (Task running)");
|
LOG_INFO(TAG, "InputManager: Initialization complete - Factory Reset on GPIO 0 (Task running)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ void InputManager::end() {
|
|||||||
if (_inputTaskHandle != nullptr) {
|
if (_inputTaskHandle != nullptr) {
|
||||||
vTaskDelete(_inputTaskHandle);
|
vTaskDelete(_inputTaskHandle);
|
||||||
_inputTaskHandle = nullptr;
|
_inputTaskHandle = nullptr;
|
||||||
LOG_INFO("InputManager: Input task stopped");
|
LOG_INFO(TAG, "InputManager: Input task stopped");
|
||||||
}
|
}
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
@@ -76,12 +78,12 @@ void InputManager::end() {
|
|||||||
|
|
||||||
void InputManager::setFactoryResetPressCallback(ButtonCallback callback) {
|
void InputManager::setFactoryResetPressCallback(ButtonCallback callback) {
|
||||||
_factoryResetButton.config.onPress = callback;
|
_factoryResetButton.config.onPress = callback;
|
||||||
LOG_DEBUG("InputManager: Factory reset press callback registered");
|
LOG_DEBUG(TAG, "InputManager: Factory reset press callback registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputManager::setFactoryResetLongPressCallback(ButtonCallback callback) {
|
void InputManager::setFactoryResetLongPressCallback(ButtonCallback callback) {
|
||||||
_factoryResetButton.config.onLongPress = callback;
|
_factoryResetButton.config.onLongPress = callback;
|
||||||
LOG_DEBUG("InputManager: Factory reset long press callback registered");
|
LOG_DEBUG(TAG, "InputManager: Factory reset long press callback registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -101,7 +103,7 @@ uint32_t InputManager::getFactoryResetPressDuration() const {
|
|||||||
|
|
||||||
bool InputManager::isHealthy() const {
|
bool InputManager::isHealthy() const {
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
LOG_DEBUG("InputManager: Unhealthy - not initialized");
|
LOG_DEBUG(TAG, "InputManager: Unhealthy - not initialized");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +118,7 @@ bool InputManager::isHealthy() const {
|
|||||||
void InputManager::inputTaskFunction(void* parameter) {
|
void InputManager::inputTaskFunction(void* parameter) {
|
||||||
InputManager* manager = static_cast<InputManager*>(parameter);
|
InputManager* manager = static_cast<InputManager*>(parameter);
|
||||||
|
|
||||||
LOG_INFO("InputManager: Input task started (polling every %dms)", INPUT_POLL_RATE_MS);
|
LOG_INFO(TAG, "InputManager: Input task started (polling every %dms)", INPUT_POLL_RATE_MS);
|
||||||
|
|
||||||
TickType_t lastWakeTime = xTaskGetTickCount();
|
TickType_t lastWakeTime = xTaskGetTickCount();
|
||||||
const TickType_t pollInterval = pdMS_TO_TICKS(INPUT_POLL_RATE_MS);
|
const TickType_t pollInterval = pdMS_TO_TICKS(INPUT_POLL_RATE_MS);
|
||||||
@@ -155,7 +157,7 @@ void InputManager::configureButton(const ButtonConfig& config) {
|
|||||||
pinMode(config.pin, INPUT_PULLUP);
|
pinMode(config.pin, INPUT_PULLUP);
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("InputManager: Configured GPIO %d as input (%s)",
|
LOG_DEBUG(TAG, "InputManager: Configured GPIO %d as input (%s)",
|
||||||
config.pin, config.activeHigh ? "active-high" : "active-low");
|
config.pin, config.activeHigh ? "active-high" : "active-low");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +184,7 @@ void InputManager::updateButton(ButtonData& button) {
|
|||||||
// Button just pressed - start debouncing
|
// Button just pressed - start debouncing
|
||||||
button.state = ButtonState::DEBOUNCING_PRESS;
|
button.state = ButtonState::DEBOUNCING_PRESS;
|
||||||
button.stateChangeTime = now;
|
button.stateChangeTime = now;
|
||||||
LOG_DEBUG("InputManager: Button press detected on GPIO %d - debouncing",
|
LOG_DEBUG(TAG, "InputManager: Button press detected on GPIO %d - debouncing",
|
||||||
button.config.pin);
|
button.config.pin);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -192,14 +194,14 @@ void InputManager::updateButton(ButtonData& button) {
|
|||||||
if (!currentState) {
|
if (!currentState) {
|
||||||
// Button released during debounce - false trigger
|
// Button released during debounce - false trigger
|
||||||
button.state = ButtonState::IDLE;
|
button.state = ButtonState::IDLE;
|
||||||
LOG_DEBUG("InputManager: False trigger on GPIO %d (released during debounce)",
|
LOG_DEBUG(TAG, "InputManager: False trigger on GPIO %d (released during debounce)",
|
||||||
button.config.pin);
|
button.config.pin);
|
||||||
} else if (now - button.stateChangeTime >= button.config.debounceMs) {
|
} else if (now - button.stateChangeTime >= button.config.debounceMs) {
|
||||||
// Debounce time passed - confirm press
|
// Debounce time passed - confirm press
|
||||||
button.state = ButtonState::LONG_PRESS_PENDING;
|
button.state = ButtonState::LONG_PRESS_PENDING;
|
||||||
button.pressStartTime = now;
|
button.pressStartTime = now;
|
||||||
button.longPressTriggered = false;
|
button.longPressTriggered = false;
|
||||||
LOG_INFO("InputManager: Button press confirmed on GPIO %d",
|
LOG_INFO(TAG, "InputManager: Button press confirmed on GPIO %d",
|
||||||
button.config.pin);
|
button.config.pin);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -210,14 +212,14 @@ void InputManager::updateButton(ButtonData& button) {
|
|||||||
// Button released before long press threshold - it's a short press
|
// Button released before long press threshold - it's a short press
|
||||||
button.state = ButtonState::DEBOUNCING_RELEASE;
|
button.state = ButtonState::DEBOUNCING_RELEASE;
|
||||||
button.stateChangeTime = now;
|
button.stateChangeTime = now;
|
||||||
LOG_INFO("InputManager: Short press detected on GPIO %d (held for %lums)",
|
LOG_INFO(TAG, "InputManager: Short press detected on GPIO %d (held for %lums)",
|
||||||
button.config.pin, now - button.pressStartTime);
|
button.config.pin, now - button.pressStartTime);
|
||||||
} else if (now - button.pressStartTime >= button.config.longPressMs) {
|
} else if (now - button.pressStartTime >= button.config.longPressMs) {
|
||||||
// Long press threshold reached
|
// Long press threshold reached
|
||||||
button.state = ButtonState::LONG_PRESSED;
|
button.state = ButtonState::LONG_PRESSED;
|
||||||
button.longPressTriggered = true;
|
button.longPressTriggered = true;
|
||||||
|
|
||||||
LOG_WARNING("InputManager: LONG PRESS DETECTED on GPIO %d (held for %lums)",
|
LOG_WARNING(TAG, "InputManager: LONG PRESS DETECTED on GPIO %d (held for %lums)",
|
||||||
button.config.pin, now - button.pressStartTime);
|
button.config.pin, now - button.pressStartTime);
|
||||||
|
|
||||||
// Trigger long press callback
|
// Trigger long press callback
|
||||||
@@ -232,7 +234,7 @@ void InputManager::updateButton(ButtonData& button) {
|
|||||||
if (!currentState) {
|
if (!currentState) {
|
||||||
button.state = ButtonState::DEBOUNCING_RELEASE;
|
button.state = ButtonState::DEBOUNCING_RELEASE;
|
||||||
button.stateChangeTime = now;
|
button.stateChangeTime = now;
|
||||||
LOG_INFO("InputManager: Long press released on GPIO %d (total duration: %lums)",
|
LOG_INFO(TAG, "InputManager: Long press released on GPIO %d (total duration: %lums)",
|
||||||
button.config.pin, now - button.pressStartTime);
|
button.config.pin, now - button.pressStartTime);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -242,7 +244,7 @@ void InputManager::updateButton(ButtonData& button) {
|
|||||||
if (currentState) {
|
if (currentState) {
|
||||||
// Button pressed again during release debounce - go back to pressed state
|
// Button pressed again during release debounce - go back to pressed state
|
||||||
button.state = ButtonState::LONG_PRESS_PENDING;
|
button.state = ButtonState::LONG_PRESS_PENDING;
|
||||||
LOG_DEBUG("InputManager: Button re-pressed during release debounce on GPIO %d",
|
LOG_DEBUG(TAG, "InputManager: Button re-pressed during release debounce on GPIO %d",
|
||||||
button.config.pin);
|
button.config.pin);
|
||||||
} else if (now - button.stateChangeTime >= button.config.debounceMs) {
|
} else if (now - button.stateChangeTime >= button.config.debounceMs) {
|
||||||
// Debounce time passed - confirm release
|
// Debounce time passed - confirm release
|
||||||
@@ -250,12 +252,12 @@ void InputManager::updateButton(ButtonData& button) {
|
|||||||
|
|
||||||
// If it was a short press (not long press), trigger the press callback
|
// If it was a short press (not long press), trigger the press callback
|
||||||
if (!button.longPressTriggered && button.config.onPress) {
|
if (!button.longPressTriggered && button.config.onPress) {
|
||||||
LOG_INFO("InputManager: Triggering press callback for GPIO %d",
|
LOG_INFO(TAG, "InputManager: Triggering press callback for GPIO %d",
|
||||||
button.config.pin);
|
button.config.pin);
|
||||||
button.config.onPress();
|
button.config.onPress();
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("InputManager: Button release confirmed on GPIO %d",
|
LOG_DEBUG(TAG, "InputManager: Button release confirmed on GPIO %d",
|
||||||
button.config.pin);
|
button.config.pin);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,72 +1,251 @@
|
|||||||
#include "Logging.hpp"
|
#include "Logging.hpp"
|
||||||
|
|
||||||
// Initialize static member
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
Logging::LogLevel Logging::currentLevel = Logging::VERBOSE; // Default to DEBUG
|
// STATIC MEMBER INITIALIZATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
void Logging::setLevel(LogLevel level) {
|
Logging::LogLevel Logging::_serialLevel = Logging::VERBOSE;
|
||||||
currentLevel = level;
|
Logging::LogLevel Logging::_mqttLevel = Logging::NONE;
|
||||||
Serial.printf("[LOGGING] Log level set to %d\n", level);
|
Logging::LogLevel Logging::_sdLevel = Logging::NONE;
|
||||||
|
|
||||||
|
std::map<String, Logging::LogLevel> Logging::_serialOverrides;
|
||||||
|
std::map<String, Logging::LogLevel> Logging::_mqttOverrides;
|
||||||
|
std::map<String, Logging::LogLevel> Logging::_sdOverrides;
|
||||||
|
|
||||||
|
Logging::MqttPublishCallback Logging::_mqttCallback = nullptr;
|
||||||
|
Logging::SdWriteCallback Logging::_sdCallback = nullptr;
|
||||||
|
String Logging::_mqttLogTopic = "";
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// GLOBAL CHANNEL LEVEL SETTERS / GETTERS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Logging::setSerialLevel(LogLevel level) {
|
||||||
|
_serialLevel = level;
|
||||||
|
Serial.printf("[Logger] Serial level -> %d\n", level);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logging::LogLevel Logging::getLevel() {
|
void Logging::setMqttLevel(LogLevel level) {
|
||||||
return currentLevel;
|
_mqttLevel = level;
|
||||||
|
Serial.printf("[Logger] MQTT level -> %d\n", level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Logging::setSdLevel(LogLevel level) {
|
||||||
|
_sdLevel = level;
|
||||||
|
Serial.printf("[Logger] SD level -> %d\n", level);
|
||||||
|
}
|
||||||
|
|
||||||
|
Logging::LogLevel Logging::getSerialLevel() { return _serialLevel; }
|
||||||
|
Logging::LogLevel Logging::getMqttLevel() { return _mqttLevel; }
|
||||||
|
Logging::LogLevel Logging::getSdLevel() { return _sdLevel; }
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PER-SUBSYSTEM OVERRIDES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Logging::setSubsystemSerialLevel(const char* tag, LogLevel level) {
|
||||||
|
_serialOverrides[String(tag)] = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::setSubsystemMqttLevel(const char* tag, LogLevel level) {
|
||||||
|
_mqttOverrides[String(tag)] = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::setSubsystemSdLevel(const char* tag, LogLevel level) {
|
||||||
|
_sdOverrides[String(tag)] = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// CALLBACK REGISTRATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Logging::setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic) {
|
||||||
|
_mqttCallback = callback;
|
||||||
|
_mqttLogTopic = logTopic;
|
||||||
|
Serial.printf("[Logger] MQTT publish callback registered: %s\n", logTopic.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::setSdWriteCallback(SdWriteCallback callback) {
|
||||||
|
_sdCallback = callback;
|
||||||
|
Serial.printf("[Logger] SD write callback registered\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PUBLIC LOGGING FUNCTIONS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Logging::error(const char* tag, const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
log(ERROR, "ERROR", tag, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::warning(const char* tag, const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
log(WARNING, "WARN", tag, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::info(const char* tag, const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
log(INFO, "INFO", tag, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::debug(const char* tag, const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
log(DEBUG, "DEBG", tag, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Logging::verbose(const char* tag, const char* format, ...) {
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
log(VERBOSE, "VERB", tag, format, args);
|
||||||
|
va_end(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UTILITIES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
bool Logging::isLevelEnabled(LogLevel level) {
|
bool Logging::isLevelEnabled(LogLevel level) {
|
||||||
return currentLevel >= level;
|
return _serialLevel >= level;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Logging::error(const char* format, ...) {
|
String Logging::levelToString(LogLevel level) {
|
||||||
if (!isLevelEnabled(ERROR)) return;
|
switch (level) {
|
||||||
|
case ERROR: return "ERROR";
|
||||||
va_list args;
|
case WARNING: return "WARNING";
|
||||||
va_start(args, format);
|
case INFO: return "INFO";
|
||||||
log(ERROR, "🔴 EROR", format, args);
|
case DEBUG: return "DEBUG";
|
||||||
va_end(args);
|
case VERBOSE: return "VERBOSE";
|
||||||
|
default: return "NONE";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Logging::warning(const char* format, ...) {
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
if (!isLevelEnabled(WARNING)) return;
|
// PRIVATE: RESOLVE EFFECTIVE LEVEL FOR A TAG ON A CHANNEL
|
||||||
|
// Returns the override level if one exists for this tag, otherwise the global level.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
va_list args;
|
Logging::LogLevel Logging::resolveLevel(const char* tag, LogLevel globalLevel, const std::map<String, LogLevel>& overrides) {
|
||||||
va_start(args, format);
|
auto it = overrides.find(String(tag));
|
||||||
log(WARNING, "🟡 WARN", format, args);
|
if (it != overrides.end()) {
|
||||||
va_end(args);
|
return it->second;
|
||||||
|
}
|
||||||
|
return globalLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Logging::info(const char* format, ...) {
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
if (!isLevelEnabled(INFO)) return;
|
// PRIVATE: CORE LOG DISPATCH
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
va_list args;
|
void Logging::log(LogLevel level, const char* levelStr, const char* tag, const char* format, va_list args) {
|
||||||
va_start(args, format);
|
// Resolve effective level for each channel (override wins over global)
|
||||||
log(INFO, "🟢 INFO", format, args);
|
LogLevel serialEffective = resolveLevel(tag, _serialLevel, _serialOverrides);
|
||||||
va_end(args);
|
LogLevel mqttEffective = resolveLevel(tag, _mqttLevel, _mqttOverrides);
|
||||||
|
LogLevel sdEffective = resolveLevel(tag, _sdLevel, _sdOverrides);
|
||||||
|
|
||||||
|
bool serialEnabled = (serialEffective >= level);
|
||||||
|
bool mqttEnabled = (mqttEffective >= level) && (_mqttCallback != nullptr);
|
||||||
|
bool sdEnabled = (sdEffective >= level) && (_sdCallback != nullptr);
|
||||||
|
|
||||||
|
// Early exit if nothing will output this message
|
||||||
|
if (!serialEnabled && !mqttEnabled && !sdEnabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Logging::debug(const char* format, ...) {
|
// Format the message once
|
||||||
if (!isLevelEnabled(DEBUG)) return;
|
|
||||||
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
log(DEBUG, "🐞 DEBG", format, args);
|
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logging::verbose(const char* format, ...) {
|
|
||||||
if (!isLevelEnabled(VERBOSE)) return;
|
|
||||||
|
|
||||||
va_list args;
|
|
||||||
va_start(args, format);
|
|
||||||
log(VERBOSE, "🧾 VERB", format, args);
|
|
||||||
va_end(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Logging::log(LogLevel level, const char* levelStr, const char* format, va_list args) {
|
|
||||||
Serial.printf("[%s] ", levelStr);
|
|
||||||
|
|
||||||
// Print the formatted message
|
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||||
Serial.print(buffer);
|
|
||||||
Serial.println();
|
// Serial output
|
||||||
|
if (serialEnabled) {
|
||||||
|
Serial.printf("[%s][%s] %s\n", levelStr, tag, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MQTT output
|
||||||
|
if (mqttEnabled) {
|
||||||
|
publishToMqtt(level, levelStr, tag, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SD output
|
||||||
|
if (sdEnabled) {
|
||||||
|
writeToSd(level, levelStr, tag, buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PRIVATE: MQTT PUBLISH
|
||||||
|
// Uses a re-entrancy guard to prevent log-of-a-log recursion.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Logging::publishToMqtt(LogLevel level, const char* levelStr, const char* tag, const char* message) {
|
||||||
|
if (!_mqttCallback || _mqttLogTopic.isEmpty()) return;
|
||||||
|
|
||||||
|
static bool isPublishing = false;
|
||||||
|
if (isPublishing) return;
|
||||||
|
isPublishing = true;
|
||||||
|
|
||||||
|
// JSON: {"level":"WARNING","subsystem":"BellEngine","message":"...","timestamp":12345}
|
||||||
|
String payload;
|
||||||
|
payload.reserve(600);
|
||||||
|
payload = "{\"level\":\"";
|
||||||
|
payload += levelStr;
|
||||||
|
payload += "\",\"subsystem\":\"";
|
||||||
|
payload += tag;
|
||||||
|
payload += "\",\"message\":\"";
|
||||||
|
|
||||||
|
// Escape special JSON characters
|
||||||
|
const char* p = message;
|
||||||
|
while (*p) {
|
||||||
|
char c = *p++;
|
||||||
|
if (c == '\\') payload += "\\\\";
|
||||||
|
else if (c == '"') payload += "\\\"";
|
||||||
|
else if (c == '\n') payload += "\\n";
|
||||||
|
else if (c == '\r') payload += "\\r";
|
||||||
|
else payload += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload += "\",\"timestamp\":";
|
||||||
|
payload += millis();
|
||||||
|
payload += "}";
|
||||||
|
|
||||||
|
_mqttCallback(_mqttLogTopic, payload, 1);
|
||||||
|
|
||||||
|
isPublishing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PRIVATE: SD WRITE
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Logging::writeToSd(LogLevel level, const char* levelStr, const char* tag, const char* message) {
|
||||||
|
if (!_sdCallback) return;
|
||||||
|
|
||||||
|
static bool isWriting = false;
|
||||||
|
if (isWriting) return;
|
||||||
|
isWriting = true;
|
||||||
|
|
||||||
|
// Plain text line: [WARN][BellEngine] message (timestamp: 12345ms)
|
||||||
|
String line;
|
||||||
|
line.reserve(300);
|
||||||
|
line = "[";
|
||||||
|
line += levelStr;
|
||||||
|
line += "][";
|
||||||
|
line += tag;
|
||||||
|
line += "] ";
|
||||||
|
line += message;
|
||||||
|
line += " (";
|
||||||
|
line += millis();
|
||||||
|
line += "ms)";
|
||||||
|
|
||||||
|
_sdCallback(line);
|
||||||
|
|
||||||
|
isWriting = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
/*
|
/*
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
* LOGGING.HPP - Centralized Logging System
|
* LOGGING.HPP - Subsystem-Aware Centralized Logging System
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
*
|
*
|
||||||
* 📝 THE INFORMATION CHRONICLER OF VESPER 📝
|
* 📝 THE INFORMATION CHRONICLER OF VESPER 📝
|
||||||
*
|
*
|
||||||
* This header provides a unified logging interface with multiple levels,
|
* Three independent output channels, each with their own level:
|
||||||
* timestamps, and comprehensive debugging support throughout the system.
|
* • Serial — USB debugging, local connection
|
||||||
|
* • MQTT — Remote troubleshooting via web dashboard
|
||||||
|
* • SD — Persistent log storage for post-mortem analysis
|
||||||
*
|
*
|
||||||
* 📋 VERSION: 2.0 (Enhanced logging system)
|
* Per-subsystem filtering: each subsystem tag can have its own level
|
||||||
|
* overrides per channel. If no override is set, the global channel
|
||||||
|
* level applies. Set a tag's level to NONE on a specific channel to
|
||||||
|
* silence it entirely on that channel (e.g. MQTT internals on MQTT).
|
||||||
|
*
|
||||||
|
* Usage in each .cpp file:
|
||||||
|
* #define TAG "BellEngine" // one line at the top
|
||||||
|
* LOG_INFO(TAG, "Ring scheduled"); // all calls include the tag
|
||||||
|
*
|
||||||
|
* The JSON payload sent over MQTT includes the subsystem field:
|
||||||
|
* {"level":"WARNING","subsystem":"BellEngine","message":"...","timestamp":12345}
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 3.0 (Subsystem-aware logging)
|
||||||
* 📅 DATE: 2025
|
* 📅 DATE: 2025
|
||||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -18,48 +32,114 @@
|
|||||||
#define LOGGING_HPP
|
#define LOGGING_HPP
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <map>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
class Logging {
|
class Logging {
|
||||||
public:
|
public:
|
||||||
// Log Levels
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// LOG LEVELS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
enum LogLevel {
|
enum LogLevel {
|
||||||
NONE = 0, // No logs
|
NONE = 0, // No output
|
||||||
ERROR = 1, // Errors only
|
ERROR = 1, // Errors only
|
||||||
WARNING = 2, // Warnings and errors
|
WARNING = 2, // Warnings and errors
|
||||||
INFO = 3, // Info, warnings, and errors
|
INFO = 3, // Info, warnings, errors
|
||||||
DEBUG = 4, // Debug logs. Really high level (full debugging)
|
DEBUG = 4, // Debug detail
|
||||||
VERBOSE = 5 // Nearly every command gets printed
|
VERBOSE = 5 // Everything
|
||||||
};
|
};
|
||||||
|
|
||||||
private:
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
static LogLevel currentLevel;
|
// CALLBACK TYPES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
using MqttPublishCallback = std::function<void(const String& topic, const String& payload, int qos)>;
|
||||||
|
using SdWriteCallback = std::function<void(const String& line)>;
|
||||||
|
|
||||||
public:
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// Set the active log level
|
// GLOBAL CHANNEL LEVELS
|
||||||
static void setLevel(LogLevel level);
|
// Set the baseline level for each output channel.
|
||||||
|
// Per-subsystem overrides take precedence when set.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
static void setSerialLevel(LogLevel level);
|
||||||
|
static void setMqttLevel(LogLevel level);
|
||||||
|
static void setSdLevel(LogLevel level);
|
||||||
|
|
||||||
// Get current log level
|
static LogLevel getSerialLevel();
|
||||||
static LogLevel getLevel();
|
static LogLevel getMqttLevel();
|
||||||
|
static LogLevel getSdLevel();
|
||||||
|
|
||||||
// Logging functions
|
// Legacy compatibility (maps to serial level)
|
||||||
static void error(const char* format, ...);
|
static void setLevel(LogLevel level) { setSerialLevel(level); }
|
||||||
static void warning(const char* format, ...);
|
static LogLevel getLevel() { return getSerialLevel(); }
|
||||||
static void info(const char* format, ...);
|
static void setMqttLogLevel(LogLevel level) { setMqttLevel(level); }
|
||||||
static void debug(const char* format, ...);
|
static LogLevel getMqttLogLevel() { return getMqttLevel(); }
|
||||||
static void verbose(const char* format, ...);
|
|
||||||
|
|
||||||
// Check if level is enabled (for conditional logging)
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PER-SUBSYSTEM LEVEL OVERRIDES
|
||||||
|
// Call these at startup to silence or focus specific subsystems per channel.
|
||||||
|
// Pass NONE to completely silence a subsystem on a channel.
|
||||||
|
// Pass a level to cap that subsystem at that level on that channel.
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
static void setSubsystemSerialLevel(const char* tag, LogLevel level);
|
||||||
|
static void setSubsystemMqttLevel(const char* tag, LogLevel level);
|
||||||
|
static void setSubsystemSdLevel(const char* tag, LogLevel level);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// OUTPUT CHANNEL REGISTRATION
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
static void setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic);
|
||||||
|
static void setSdWriteCallback(SdWriteCallback callback);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// LOGGING FUNCTIONS (tag = subsystem name, e.g. "BellEngine")
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
static void error(const char* tag, const char* format, ...);
|
||||||
|
static void warning(const char* tag, const char* format, ...);
|
||||||
|
static void info(const char* tag, const char* format, ...);
|
||||||
|
static void debug(const char* tag, const char* format, ...);
|
||||||
|
static void verbose(const char* tag, const char* format, ...);
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// UTILITIES
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
static bool isLevelEnabled(LogLevel level);
|
static bool isLevelEnabled(LogLevel level);
|
||||||
|
static String levelToString(LogLevel level);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static void log(LogLevel level, const char* levelStr, const char* format, va_list args);
|
// Global channel levels
|
||||||
|
static LogLevel _serialLevel;
|
||||||
|
static LogLevel _mqttLevel;
|
||||||
|
static LogLevel _sdLevel;
|
||||||
|
|
||||||
|
// Per-subsystem overrides per channel (tag -> level)
|
||||||
|
// A value of NONE means "suppress this subsystem on this channel entirely"
|
||||||
|
static std::map<String, LogLevel> _serialOverrides;
|
||||||
|
static std::map<String, LogLevel> _mqttOverrides;
|
||||||
|
static std::map<String, LogLevel> _sdOverrides;
|
||||||
|
|
||||||
|
// Output channel callbacks
|
||||||
|
static MqttPublishCallback _mqttCallback;
|
||||||
|
static SdWriteCallback _sdCallback;
|
||||||
|
static String _mqttLogTopic;
|
||||||
|
|
||||||
|
// Core internal methods
|
||||||
|
static void log(LogLevel level, const char* levelStr, const char* tag, const char* format, va_list args);
|
||||||
|
static void publishToMqtt(LogLevel level, const char* levelStr, const char* tag, const char* message);
|
||||||
|
static void writeToSd(LogLevel level, const char* levelStr, const char* tag, const char* message);
|
||||||
|
|
||||||
|
// Resolve effective level for a tag on a channel
|
||||||
|
static LogLevel resolveLevel(const char* tag, LogLevel globalLevel, const std::map<String, LogLevel>& overrides);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convenience macros for easier use
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
#define LOG_ERROR(...) Logging::error(__VA_ARGS__)
|
// MACROS
|
||||||
#define LOG_WARNING(...) Logging::warning(__VA_ARGS__)
|
// Each .cpp file defines: #define TAG "SubsystemName"
|
||||||
#define LOG_INFO(...) Logging::info(__VA_ARGS__)
|
// Then uses: LOG_INFO(TAG, "message %d", value)
|
||||||
#define LOG_DEBUG(...) Logging::debug(__VA_ARGS__)
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
#define LOG_VERBOSE(...) Logging::verbose(__VA_ARGS__)
|
#define LOG_ERROR(tag, ...) Logging::error(tag, __VA_ARGS__)
|
||||||
|
#define LOG_WARNING(tag, ...) Logging::warning(tag, __VA_ARGS__)
|
||||||
|
#define LOG_INFO(tag, ...) Logging::info(tag, __VA_ARGS__)
|
||||||
|
#define LOG_DEBUG(tag, ...) Logging::debug(tag, __VA_ARGS__)
|
||||||
|
#define LOG_VERBOSE(tag, ...) Logging::verbose(tag, __VA_ARGS__)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#include "Networking.hpp"
|
#include "Networking.hpp"
|
||||||
|
|
||||||
|
#define TAG "Networking"
|
||||||
#include "../ConfigManager/ConfigManager.hpp"
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
#include <WiFiManager.h>
|
#include <WiFiManager.h>
|
||||||
@@ -20,7 +22,7 @@ Networking::Networking(ConfigManager& configManager)
|
|||||||
|
|
||||||
// Safety check for multiple instances
|
// Safety check for multiple instances
|
||||||
if (_instance != nullptr) {
|
if (_instance != nullptr) {
|
||||||
LOG_WARNING("Multiple Networking instances detected! Previous instance will be overridden.");
|
LOG_WARNING(TAG, "Multiple Networking instances detected! Previous instance will be overridden.");
|
||||||
}
|
}
|
||||||
|
|
||||||
_instance = this;
|
_instance = this;
|
||||||
@@ -48,7 +50,7 @@ Networking::~Networking() {
|
|||||||
|
|
||||||
|
|
||||||
void Networking::begin() {
|
void Networking::begin() {
|
||||||
LOG_INFO("Initializing Networking System");
|
LOG_INFO(TAG, "Initializing Networking System");
|
||||||
|
|
||||||
// Create reconnection timer
|
// Create reconnection timer
|
||||||
_reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL),
|
_reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL),
|
||||||
@@ -64,67 +66,108 @@ void Networking::begin() {
|
|||||||
// Clear Previous Settings, USE once to test.
|
// Clear Previous Settings, USE once to test.
|
||||||
//_wifiManager->resetSettings();
|
//_wifiManager->resetSettings();
|
||||||
|
|
||||||
// Start Ethernet hardware
|
// Check if permanent AP mode is enabled
|
||||||
auto& hwConfig = _configManager.getHardwareConfig();
|
if (_configManager.getPermanentAPMode()) {
|
||||||
ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs,
|
LOG_INFO(TAG, "Permanent AP mode enabled - starting AP mode on 192.168.4.1");
|
||||||
hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI);
|
startPermanentAPMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Start connection sequence
|
// ETHERNET DISABLED - WiFi only mode
|
||||||
LOG_INFO("Starting network connection sequence...");
|
// Start Ethernet hardware
|
||||||
startEthernetConnection();
|
// auto& hwConfig = _configManager.getHardwareConfig();
|
||||||
|
// ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs,
|
||||||
|
// hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI);
|
||||||
|
|
||||||
|
// Start connection sequence - Skip Ethernet, go directly to WiFi
|
||||||
|
LOG_INFO(TAG, "Starting WiFi connection (Ethernet disabled)...");
|
||||||
|
startWiFiConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::startEthernetConnection() {
|
void Networking::startEthernetConnection() {
|
||||||
LOG_INFO("Attempting Ethernet connection...");
|
// ETHERNET DISABLED - Skip to WiFi immediately
|
||||||
setState(NetworkState::CONNECTING_ETHERNET);
|
LOG_DEBUG(TAG, "Ethernet connection disabled - falling back to WiFi");
|
||||||
|
|
||||||
// Check if Ethernet hardware initialization failed
|
|
||||||
if (!ETH.linkUp()) {
|
|
||||||
LOG_WARNING("Ethernet hardware not detected or failed to initialize");
|
|
||||||
LOG_INFO("Falling back to WiFi immediately");
|
|
||||||
startWiFiConnection();
|
startWiFiConnection();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ethernet will auto-connect via events
|
// Original Ethernet code (DISABLED):
|
||||||
// Set timeout for Ethernet attempt (5 seconds)
|
// LOG_INFO(TAG, "Attempting Ethernet connection...");
|
||||||
_lastConnectionAttempt = millis();
|
// setState(NetworkState::CONNECTING_ETHERNET);
|
||||||
|
//
|
||||||
// Start reconnection timer to handle timeout
|
// // Check if Ethernet hardware initialization failed
|
||||||
xTimerStart(_reconnectionTimer, 0);
|
// if (!ETH.linkUp()) {
|
||||||
|
// LOG_WARNING(TAG, "Ethernet hardware not detected or failed to initialize");
|
||||||
|
// LOG_INFO(TAG, "Falling back to WiFi immediately");
|
||||||
|
// startWiFiConnection();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Ethernet will auto-connect via events
|
||||||
|
// // Set timeout for Ethernet attempt (5 seconds)
|
||||||
|
// _lastConnectionAttempt = millis();
|
||||||
|
//
|
||||||
|
// // Start reconnection timer to handle timeout
|
||||||
|
// xTimerStart(_reconnectionTimer, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::startWiFiConnection() {
|
void Networking::startWiFiConnection() {
|
||||||
LOG_INFO("Attempting WiFi connection...");
|
LOG_INFO(TAG, "Attempting WiFi connection...");
|
||||||
setState(NetworkState::CONNECTING_WIFI);
|
setState(NetworkState::CONNECTING_WIFI);
|
||||||
|
|
||||||
if (!hasValidWiFiCredentials()) {
|
// ALWAYS try default credentials first (for bundled router deployment)
|
||||||
LOG_WARNING("No valid WiFi credentials found");
|
auto& netConfig = _configManager.getNetworkConfig();
|
||||||
if (!_bootSequenceComplete) {
|
|
||||||
// No credentials during boot - start portal
|
|
||||||
startWiFiPortal();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and log saved credentials (for debugging)
|
LOG_INFO(TAG, "Using DEFAULT WiFi credentials - SSID: %s", netConfig.defaultWifiSsid.c_str());
|
||||||
String savedSSID = _wifiManager->getWiFiSSID(true);
|
|
||||||
LOG_INFO("Using WiFiManager saved credentials - SSID: %s", savedSSID.c_str());
|
|
||||||
|
|
||||||
WiFi.mode(WIFI_STA);
|
|
||||||
applyNetworkConfig(false); // false = WiFi config
|
applyNetworkConfig(false); // false = WiFi config
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
// Let WiFiManager handle credentials (uses saved SSID/password)
|
WiFi.begin(netConfig.defaultWifiSsid.c_str(), netConfig.defaultWifiPsk.c_str());
|
||||||
WiFi.begin();
|
|
||||||
|
|
||||||
_lastConnectionAttempt = millis();
|
_lastConnectionAttempt = millis();
|
||||||
|
|
||||||
// Start reconnection timer to handle timeout
|
|
||||||
xTimerStart(_reconnectionTimer, 0);
|
xTimerStart(_reconnectionTimer, 0);
|
||||||
|
|
||||||
|
// Original WiFiManager fallback code (DISABLED for fixed deployment):
|
||||||
|
// // First, try default credentials if this is the first boot attempt
|
||||||
|
// if (!_bootSequenceComplete && !hasValidWiFiCredentials()) {
|
||||||
|
// LOG_INFO(TAG, "No saved credentials - trying default WiFi credentials");
|
||||||
|
// auto& netConfig = _configManager.getNetworkConfig();
|
||||||
|
//
|
||||||
|
// applyNetworkConfig(false); // false = WiFi config
|
||||||
|
// WiFi.mode(WIFI_STA);
|
||||||
|
// WiFi.begin(netConfig.defaultWifiSsid.c_str(), netConfig.defaultWifiPsk.c_str());
|
||||||
|
//
|
||||||
|
// _lastConnectionAttempt = millis();
|
||||||
|
// xTimerStart(_reconnectionTimer, 0);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Check if we have valid saved credentials
|
||||||
|
// if (!hasValidWiFiCredentials()) {
|
||||||
|
// LOG_WARNING(TAG, "No valid WiFi credentials found");
|
||||||
|
// if (!_bootSequenceComplete) {
|
||||||
|
// // No credentials during boot - start portal
|
||||||
|
// startWiFiPortal();
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Get and log saved credentials (for debugging)
|
||||||
|
// String savedSSID = _wifiManager->getWiFiSSID(true);
|
||||||
|
// LOG_INFO(TAG, "Using WiFiManager saved credentials - SSID: %s", savedSSID.c_str());
|
||||||
|
//
|
||||||
|
// applyNetworkConfig(false); // false = WiFi config
|
||||||
|
// WiFi.mode(WIFI_STA);
|
||||||
|
//
|
||||||
|
// // Let WiFiManager handle credentials (uses saved SSID/password)
|
||||||
|
// WiFi.begin();
|
||||||
|
//
|
||||||
|
// _lastConnectionAttempt = millis();
|
||||||
|
//
|
||||||
|
// // Start reconnection timer to handle timeout
|
||||||
|
// xTimerStart(_reconnectionTimer, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::startWiFiPortal() {
|
void Networking::startWiFiPortal() {
|
||||||
LOG_INFO("Starting WiFi configuration portal...");
|
LOG_INFO(TAG, "Starting WiFi configuration portal...");
|
||||||
setState(NetworkState::WIFI_PORTAL_MODE);
|
setState(NetworkState::WIFI_PORTAL_MODE);
|
||||||
|
|
||||||
WiFi.mode(WIFI_AP_STA);
|
WiFi.mode(WIFI_AP_STA);
|
||||||
@@ -132,13 +175,25 @@ void Networking::startWiFiPortal() {
|
|||||||
auto& netConfig = _configManager.getNetworkConfig();
|
auto& netConfig = _configManager.getNetworkConfig();
|
||||||
String apName = "Vesper-" + _configManager.getDeviceUID();
|
String apName = "Vesper-" + _configManager.getDeviceUID();
|
||||||
|
|
||||||
LOG_INFO("WiFi Portal: SSID='%s', Password='%s'", apName.c_str(), netConfig.apPass.c_str());
|
LOG_INFO(TAG, "WiFi Portal: SSID='%s', Password='%s'", apName.c_str(), netConfig.apPass.c_str());
|
||||||
|
|
||||||
|
// Add custom HTML to WiFiManager portal for permanent AP mode toggle
|
||||||
|
String customHTML = "<br/><br/><h3>Network Mode</h3>";
|
||||||
|
customHTML += "<p>Choose how to operate this device:</p>";
|
||||||
|
customHTML += "<form action='/settings' method='get'>";
|
||||||
|
customHTML += "<button type='submit' style='width:100%; padding:15px; margin:10px 0; background:#667eea; color:white; border:none; border-radius:8px; cursor:pointer; font-size:16px;'>";
|
||||||
|
customHTML += "Open Settings (Switch to Permanent AP Mode)";
|
||||||
|
customHTML += "</button>";
|
||||||
|
customHTML += "</form>";
|
||||||
|
customHTML += "<br/><p style='font-size:12px; color:#666;'>Note: You can configure network mode later at <b>http://192.168.4.1/settings</b> (AP mode) or <b>http://{device-ip}/settings</b> (Router mode)</p>";
|
||||||
|
|
||||||
|
_wifiManager->setCustomHeadElement(customHTML.c_str());
|
||||||
|
|
||||||
if (_wifiManager->autoConnect(apName.c_str(), netConfig.apPass.c_str())) {
|
if (_wifiManager->autoConnect(apName.c_str(), netConfig.apPass.c_str())) {
|
||||||
LOG_INFO("WiFi configured successfully via portal");
|
LOG_INFO(TAG, "WiFi configured successfully via portal");
|
||||||
onWiFiConnected();
|
onWiFiConnected();
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("WiFi portal configuration failed");
|
LOG_ERROR(TAG, "WiFi portal configuration failed");
|
||||||
setState(NetworkState::DISCONNECTED);
|
setState(NetworkState::DISCONNECTED);
|
||||||
// Start reconnection timer to try again
|
// Start reconnection timer to try again
|
||||||
xTimerStart(_reconnectionTimer, 0);
|
xTimerStart(_reconnectionTimer, 0);
|
||||||
@@ -150,38 +205,39 @@ void Networking::handleReconnection() {
|
|||||||
return; // Already connected
|
return; // Already connected
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Attempting reconnection...");
|
LOG_DEBUG(TAG, "Attempting reconnection...");
|
||||||
|
|
||||||
|
// ETHERNET DISABLED - Skip Ethernet timeout checks
|
||||||
// Check for Ethernet timeout (fall back to WiFi)
|
// Check for Ethernet timeout (fall back to WiFi)
|
||||||
if (_state == NetworkState::CONNECTING_ETHERNET) {
|
// if (_state == NetworkState::CONNECTING_ETHERNET) {
|
||||||
unsigned long now = millis();
|
// unsigned long now = millis();
|
||||||
if (now - _lastConnectionAttempt > 5000) { // 5 second timeout
|
// if (now - _lastConnectionAttempt > 5000) { // 5 second timeout
|
||||||
LOG_INFO("Ethernet connection timeout - falling back to WiFi");
|
// LOG_INFO(TAG, "Ethernet connection timeout - falling back to WiFi");
|
||||||
startWiFiConnection();
|
// startWiFiConnection();
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
return; // Still waiting for Ethernet
|
// return; // Still waiting for Ethernet
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Check for WiFi timeout
|
// Check for WiFi timeout
|
||||||
if (_state == NetworkState::CONNECTING_WIFI) {
|
if (_state == NetworkState::CONNECTING_WIFI) {
|
||||||
unsigned long now = millis();
|
unsigned long now = millis();
|
||||||
if (now - _lastConnectionAttempt > 10000) { // 10 second timeout
|
if (now - _lastConnectionAttempt > 10000) { // 10 second timeout
|
||||||
_wifiConnectionFailures++;
|
_wifiConnectionFailures++;
|
||||||
LOG_WARNING("WiFi connection timeout (failure #%d)", _wifiConnectionFailures);
|
LOG_WARNING(TAG, "WiFi connection timeout (failure #%d)", _wifiConnectionFailures);
|
||||||
|
|
||||||
// After 3 failed attempts during boot, start portal
|
// After 3 failed attempts during boot, start portal
|
||||||
if (_wifiConnectionFailures >= MAX_WIFI_FAILURES) {
|
if (_wifiConnectionFailures >= MAX_WIFI_FAILURES) {
|
||||||
LOG_ERROR("Multiple WiFi connection failures - credentials may be invalid");
|
LOG_ERROR(TAG, "Multiple WiFi connection failures - credentials may be invalid");
|
||||||
|
|
||||||
if (!_bootSequenceComplete) {
|
if (!_bootSequenceComplete) {
|
||||||
// Boot not complete yet - open portal
|
// Boot not complete yet - open portal
|
||||||
LOG_INFO("Opening WiFi portal for reconfiguration");
|
LOG_INFO(TAG, "Opening WiFi portal for reconfiguration");
|
||||||
_wifiConnectionFailures = 0; // Reset counter
|
_wifiConnectionFailures = 0; // Reset counter
|
||||||
startWiFiPortal();
|
startWiFiPortal();
|
||||||
} else {
|
} else {
|
||||||
// Boot already complete - just keep retrying
|
// Boot already complete - just keep retrying
|
||||||
LOG_WARNING("WiFi connection lost - continuing retry attempts");
|
LOG_WARNING(TAG, "WiFi connection lost - continuing retry attempts");
|
||||||
// Reset counter after extended failure to prevent overflow
|
// Reset counter after extended failure to prevent overflow
|
||||||
if (_wifiConnectionFailures > 10) {
|
if (_wifiConnectionFailures > 10) {
|
||||||
_wifiConnectionFailures = 3;
|
_wifiConnectionFailures = 3;
|
||||||
@@ -197,20 +253,15 @@ void Networking::handleReconnection() {
|
|||||||
return; // Still waiting for WiFi
|
return; // Still waiting for WiFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// State is DISCONNECTED - decide what to try
|
// State is DISCONNECTED - WiFi only mode (Ethernet disabled)
|
||||||
if (_ethernetCableConnected) {
|
LOG_INFO(TAG, "Disconnected - trying WiFi");
|
||||||
LOG_INFO("Ethernet cable detected - trying Ethernet");
|
|
||||||
startEthernetConnection();
|
|
||||||
} else {
|
|
||||||
LOG_INFO("No Ethernet - trying WiFi");
|
|
||||||
if (hasValidWiFiCredentials()) {
|
if (hasValidWiFiCredentials()) {
|
||||||
startWiFiConnection();
|
startWiFiConnection();
|
||||||
} else if (!_bootSequenceComplete) {
|
} else if (!_bootSequenceComplete) {
|
||||||
// No credentials during boot - start portal
|
// No credentials during boot - start portal
|
||||||
startWiFiPortal();
|
startWiFiPortal();
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("No WiFi credentials and boot sequence complete - waiting");
|
LOG_WARNING(TAG, "No WiFi credentials and boot sequence complete - waiting");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,59 +272,60 @@ void Networking::handleReconnection() {
|
|||||||
bool Networking::isHealthy() const {
|
bool Networking::isHealthy() const {
|
||||||
// Check if we have any active connection
|
// Check if we have any active connection
|
||||||
if (_activeConnection == ConnectionType::NONE) {
|
if (_activeConnection == ConnectionType::NONE) {
|
||||||
LOG_DEBUG("Networking: Unhealthy - No active connection");
|
LOG_DEBUG(TAG, "Networking: Unhealthy - No active connection");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connection state
|
// Check connection state (Ethernet disabled, only check WiFi or AP)
|
||||||
if (_state != NetworkState::CONNECTED_ETHERNET && _state != NetworkState::CONNECTED_WIFI) {
|
if (_state != NetworkState::CONNECTED_WIFI && _state != NetworkState::AP_MODE_PERMANENT) {
|
||||||
LOG_DEBUG("Networking: Unhealthy - Not in connected state");
|
LOG_DEBUG(TAG, "Networking: Unhealthy - Not in connected state");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check IP address validity
|
// Check IP address validity
|
||||||
String ip = getLocalIP();
|
String ip = getLocalIP();
|
||||||
if (ip == "0.0.0.0" || ip.isEmpty()) {
|
if (ip == "0.0.0.0" || ip.isEmpty()) {
|
||||||
LOG_DEBUG("Networking: Unhealthy - Invalid IP address");
|
LOG_DEBUG(TAG, "Networking: Unhealthy - Invalid IP address");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For WiFi connections, check signal strength
|
// For WiFi connections, check signal strength
|
||||||
if (_activeConnection == ConnectionType::WIFI) {
|
if (_activeConnection == ConnectionType::WIFI) {
|
||||||
if (WiFi.status() != WL_CONNECTED) {
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
LOG_DEBUG("Networking: Unhealthy - WiFi not connected");
|
LOG_DEBUG(TAG, "Networking: Unhealthy - WiFi not connected");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check signal strength (RSSI should be better than -80 dBm)
|
// Check signal strength (RSSI should be better than -80 dBm)
|
||||||
int32_t rssi = WiFi.RSSI();
|
int32_t rssi = WiFi.RSSI();
|
||||||
if (rssi < -80) {
|
if (rssi < -80) {
|
||||||
LOG_DEBUG("Networking: Unhealthy - Poor WiFi signal: %d dBm", rssi);
|
LOG_DEBUG(TAG, "Networking: Unhealthy - Poor WiFi signal: %d dBm", rssi);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ETHERNET DISABLED - Removed Ethernet link check
|
||||||
// For Ethernet connections, check link status
|
// For Ethernet connections, check link status
|
||||||
if (_activeConnection == ConnectionType::ETHERNET) {
|
// if (_activeConnection == ConnectionType::ETHERNET) {
|
||||||
if (!ETH.linkUp()) {
|
// if (!ETH.linkUp()) {
|
||||||
LOG_DEBUG("Networking: Unhealthy - Ethernet link down");
|
// LOG_DEBUG(TAG, "Networking: Unhealthy - Ethernet link down");
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::setState(NetworkState newState) {
|
void Networking::setState(NetworkState newState) {
|
||||||
if (_state != newState) {
|
if (_state != newState) {
|
||||||
LOG_DEBUG("Network state: %d -> %d", (int)_state, (int)newState);
|
LOG_DEBUG(TAG, "Network state: %d -> %d", (int)_state, (int)newState);
|
||||||
_state = newState;
|
_state = newState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::setActiveConnection(ConnectionType type) {
|
void Networking::setActiveConnection(ConnectionType type) {
|
||||||
if (_activeConnection != type) {
|
if (_activeConnection != type) {
|
||||||
LOG_INFO("Active connection changed: %d -> %d", (int)_activeConnection, (int)type);
|
LOG_INFO(TAG, "Active connection changed: %d -> %d", (int)_activeConnection, (int)type);
|
||||||
_activeConnection = type;
|
_activeConnection = type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -286,39 +338,47 @@ void Networking::notifyConnectionChange(bool connected) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event handlers
|
// Event handlers (ETHERNET DISABLED)
|
||||||
void Networking::onEthernetConnected() {
|
void Networking::onEthernetConnected() {
|
||||||
LOG_INFO("Ethernet connected successfully");
|
// ETHERNET DISABLED - This should never be called
|
||||||
setState(NetworkState::CONNECTED_ETHERNET);
|
LOG_WARNING(TAG, "Ethernet event received but Ethernet is disabled - ignoring");
|
||||||
setActiveConnection(ConnectionType::ETHERNET);
|
|
||||||
|
|
||||||
// Stop WiFi if it was running
|
// Original code (DISABLED):
|
||||||
if (WiFi.getMode() != WIFI_OFF) {
|
// LOG_INFO(TAG, "Ethernet connected successfully");
|
||||||
WiFi.disconnect(true);
|
// setState(NetworkState::CONNECTED_ETHERNET);
|
||||||
WiFi.mode(WIFI_OFF);
|
// setActiveConnection(ConnectionType::ETHERNET);
|
||||||
}
|
//
|
||||||
|
// // Stop WiFi if it was running
|
||||||
// Stop reconnection timer
|
// if (WiFi.getMode() != WIFI_OFF) {
|
||||||
xTimerStop(_reconnectionTimer, 0);
|
// WiFi.disconnect(true);
|
||||||
|
// WiFi.mode(WIFI_OFF);
|
||||||
notifyConnectionChange(true);
|
// }
|
||||||
|
//
|
||||||
|
// // Stop reconnection timer
|
||||||
|
// xTimerStop(_reconnectionTimer, 0);
|
||||||
|
//
|
||||||
|
// notifyConnectionChange(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::onEthernetDisconnected() {
|
void Networking::onEthernetDisconnected() {
|
||||||
LOG_WARNING("Ethernet disconnected");
|
// ETHERNET DISABLED - This should never be called
|
||||||
|
LOG_WARNING(TAG, "Ethernet disconnect event received but Ethernet is disabled - ignoring");
|
||||||
|
|
||||||
if (_activeConnection == ConnectionType::ETHERNET) {
|
// Original code (DISABLED):
|
||||||
setState(NetworkState::DISCONNECTED);
|
// LOG_WARNING(TAG, "Ethernet disconnected");
|
||||||
setActiveConnection(ConnectionType::NONE);
|
//
|
||||||
notifyConnectionChange(false);
|
// if (_activeConnection == ConnectionType::ETHERNET) {
|
||||||
|
// setState(NetworkState::DISCONNECTED);
|
||||||
// Start reconnection attempts
|
// setActiveConnection(ConnectionType::NONE);
|
||||||
xTimerStart(_reconnectionTimer, 0);
|
// notifyConnectionChange(false);
|
||||||
}
|
//
|
||||||
|
// // Start reconnection attempts
|
||||||
|
// xTimerStart(_reconnectionTimer, 0);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::onWiFiConnected() {
|
void Networking::onWiFiConnected() {
|
||||||
LOG_INFO("WiFi connected successfully - IP: %s", WiFi.localIP().toString().c_str());
|
LOG_INFO(TAG, "WiFi connected successfully - IP: %s", WiFi.localIP().toString().c_str());
|
||||||
setState(NetworkState::CONNECTED_WIFI);
|
setState(NetworkState::CONNECTED_WIFI);
|
||||||
setActiveConnection(ConnectionType::WIFI);
|
setActiveConnection(ConnectionType::WIFI);
|
||||||
|
|
||||||
@@ -335,7 +395,7 @@ void Networking::onWiFiConnected() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Networking::onWiFiDisconnected() {
|
void Networking::onWiFiDisconnected() {
|
||||||
LOG_WARNING("WiFi disconnected");
|
LOG_WARNING(TAG, "WiFi disconnected");
|
||||||
|
|
||||||
if (_activeConnection == ConnectionType::WIFI) {
|
if (_activeConnection == ConnectionType::WIFI) {
|
||||||
setState(NetworkState::DISCONNECTED);
|
setState(NetworkState::DISCONNECTED);
|
||||||
@@ -348,35 +408,37 @@ void Networking::onWiFiDisconnected() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Networking::onEthernetCableChange(bool connected) {
|
void Networking::onEthernetCableChange(bool connected) {
|
||||||
_ethernetCableConnected = connected;
|
// ETHERNET DISABLED - Ignore cable events
|
||||||
LOG_INFO("Ethernet cable %s", connected ? "connected" : "disconnected");
|
LOG_DEBUG(TAG, "Ethernet cable event ignored (Ethernet disabled)");
|
||||||
|
|
||||||
if (connected && _activeConnection != ConnectionType::ETHERNET) {
|
// Original code (DISABLED):
|
||||||
// Cable connected and we're not using Ethernet - try to connect
|
// _ethernetCableConnected = connected;
|
||||||
startEthernetConnection();
|
// LOG_INFO(TAG, "Ethernet cable %s", connected ? "connected" : "disconnected");
|
||||||
}
|
//
|
||||||
|
// if (connected && _activeConnection != ConnectionType::ETHERNET) {
|
||||||
|
// // Cable connected and we're not using Ethernet - try to connect
|
||||||
|
// startEthernetConnection();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility methods
|
// Utility methods
|
||||||
void Networking::applyNetworkConfig(bool ethernet) {
|
void Networking::applyNetworkConfig(bool ethernet) {
|
||||||
auto& netConfig = _configManager.getNetworkConfig();
|
auto& netConfig = _configManager.getNetworkConfig();
|
||||||
|
|
||||||
if (netConfig.useStaticIP) {
|
// ETHERNET DISABLED - Only apply WiFi config
|
||||||
LOG_INFO("Applying static IP configuration");
|
|
||||||
if (ethernet) {
|
if (ethernet) {
|
||||||
ETH.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
|
LOG_WARNING(TAG, "applyNetworkConfig called with ethernet=true but Ethernet is disabled");
|
||||||
} else {
|
return;
|
||||||
WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOG_INFO("Using DHCP configuration");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ethernet) {
|
if (netConfig.useStaticIP) {
|
||||||
ETH.setHostname(netConfig.hostname.c_str());
|
LOG_INFO(TAG, "Applying static IP configuration");
|
||||||
|
WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
|
||||||
} else {
|
} else {
|
||||||
WiFi.setHostname(netConfig.hostname.c_str());
|
LOG_INFO(TAG, "Using DHCP configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WiFi.setHostname(netConfig.hostname.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Networking::hasValidWiFiCredentials() {
|
bool Networking::hasValidWiFiCredentials() {
|
||||||
@@ -394,16 +456,33 @@ bool Networking::isConnected() const {
|
|||||||
String Networking::getLocalIP() const {
|
String Networking::getLocalIP() const {
|
||||||
switch (_activeConnection) {
|
switch (_activeConnection) {
|
||||||
case ConnectionType::ETHERNET:
|
case ConnectionType::ETHERNET:
|
||||||
return ETH.localIP().toString();
|
// ETHERNET DISABLED - Should never reach here
|
||||||
|
LOG_WARNING(TAG, "getLocalIP called with ETHERNET type but Ethernet is disabled");
|
||||||
|
return "0.0.0.0";
|
||||||
case ConnectionType::WIFI:
|
case ConnectionType::WIFI:
|
||||||
return WiFi.localIP().toString();
|
return WiFi.localIP().toString();
|
||||||
|
case ConnectionType::AP:
|
||||||
|
return WiFi.softAPIP().toString();
|
||||||
|
default:
|
||||||
|
return "0.0.0.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String Networking::getGateway() const {
|
||||||
|
switch (_activeConnection) {
|
||||||
|
case ConnectionType::ETHERNET:
|
||||||
|
// ETHERNET DISABLED - Should never reach here
|
||||||
|
LOG_WARNING(TAG, "getGateway called with ETHERNET type but Ethernet is disabled");
|
||||||
|
return "0.0.0.0";
|
||||||
|
case ConnectionType::WIFI:
|
||||||
|
return WiFi.gatewayIP().toString();
|
||||||
default:
|
default:
|
||||||
return "0.0.0.0";
|
return "0.0.0.0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::forceReconnect() {
|
void Networking::forceReconnect() {
|
||||||
LOG_INFO("Forcing reconnection...");
|
LOG_INFO(TAG, "Forcing reconnection...");
|
||||||
setState(NetworkState::RECONNECTING);
|
setState(NetworkState::RECONNECTING);
|
||||||
setActiveConnection(ConnectionType::NONE);
|
setActiveConnection(ConnectionType::NONE);
|
||||||
|
|
||||||
@@ -413,56 +492,40 @@ void Networking::forceReconnect() {
|
|||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart connection sequence
|
// Restart connection sequence - WiFi only (Ethernet disabled)
|
||||||
delay(1000);
|
delay(1000);
|
||||||
startEthernetConnection();
|
startWiFiConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static callbacks
|
// Static callbacks
|
||||||
void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_info_t info) {
|
void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_info_t info) {
|
||||||
if (!_instance) return;
|
if (!_instance) return;
|
||||||
|
|
||||||
LOG_DEBUG("Network event: %d", event);
|
LOG_DEBUG(TAG, "Network event: %d", event);
|
||||||
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
// ETHERNET EVENTS DISABLED - Ignored
|
||||||
case ARDUINO_EVENT_ETH_START:
|
case ARDUINO_EVENT_ETH_START:
|
||||||
LOG_DEBUG("ETH Started");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ARDUINO_EVENT_ETH_CONNECTED:
|
case ARDUINO_EVENT_ETH_CONNECTED:
|
||||||
LOG_DEBUG("ETH Cable Connected");
|
|
||||||
_instance->onEthernetCableChange(true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||||
LOG_INFO("ETH Got IP: %s", ETH.localIP().toString().c_str());
|
|
||||||
_instance->applyNetworkConfig(true);
|
|
||||||
_instance->onEthernetConnected();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
||||||
LOG_WARNING("ETH Cable Disconnected");
|
|
||||||
_instance->onEthernetCableChange(false);
|
|
||||||
_instance->onEthernetDisconnected();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ARDUINO_EVENT_ETH_STOP:
|
case ARDUINO_EVENT_ETH_STOP:
|
||||||
LOG_INFO("ETH Stopped");
|
LOG_DEBUG(TAG, "Ethernet event ignored (Ethernet disabled)");
|
||||||
_instance->onEthernetDisconnected();
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// WiFi events (ACTIVE)
|
||||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||||
LOG_INFO("WiFi Got IP: %s", WiFi.localIP().toString().c_str());
|
LOG_INFO(TAG, "WiFi Got IP: %s", WiFi.localIP().toString().c_str());
|
||||||
_instance->onWiFiConnected();
|
_instance->onWiFiConnected();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||||
LOG_WARNING("WiFi Disconnected");
|
LOG_WARNING(TAG, "WiFi Disconnected");
|
||||||
_instance->onWiFiDisconnected();
|
_instance->onWiFiDisconnected();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||||
LOG_DEBUG("WiFi STA Connected");
|
LOG_DEBUG(TAG, "WiFi STA Connected");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -475,3 +538,62 @@ void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
|
|||||||
_instance->handleReconnection();
|
_instance->handleReconnection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Networking::startPermanentAPMode() {
|
||||||
|
LOG_INFO(TAG, "Starting Permanent AP Mode");
|
||||||
|
setState(NetworkState::AP_MODE_PERMANENT);
|
||||||
|
|
||||||
|
// Stop any existing connections
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
WiFi.mode(WIFI_OFF);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
// Configure AP mode with fixed 192.168.4.1 IP
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
|
||||||
|
auto& netConfig = _configManager.getNetworkConfig();
|
||||||
|
String apName = netConfig.apSsid;
|
||||||
|
String apPass = netConfig.apPass;
|
||||||
|
|
||||||
|
// Configure AP with fixed IP: 192.168.4.1
|
||||||
|
IPAddress local_IP(192, 168, 4, 1);
|
||||||
|
IPAddress gateway(192, 168, 4, 1);
|
||||||
|
IPAddress subnet(255, 255, 255, 0);
|
||||||
|
|
||||||
|
if (!WiFi.softAPConfig(local_IP, gateway, subnet)) {
|
||||||
|
LOG_ERROR(TAG, "Failed to configure AP IP address");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start AP
|
||||||
|
bool apStarted;
|
||||||
|
if (apPass.isEmpty()) {
|
||||||
|
apStarted = WiFi.softAP(apName.c_str());
|
||||||
|
LOG_INFO(TAG, "Starting open AP (no password): %s", apName.c_str());
|
||||||
|
} else {
|
||||||
|
apStarted = WiFi.softAP(apName.c_str(), apPass.c_str());
|
||||||
|
LOG_INFO(TAG, "Starting AP with password: %s", apName.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (apStarted) {
|
||||||
|
LOG_INFO(TAG, "✅ Permanent AP Mode active");
|
||||||
|
LOG_INFO(TAG, " SSID: %s", apName.c_str());
|
||||||
|
LOG_INFO(TAG, " IP: 192.168.4.1");
|
||||||
|
LOG_INFO(TAG, " Subnet: 255.255.255.0");
|
||||||
|
|
||||||
|
setActiveConnection(ConnectionType::AP);
|
||||||
|
|
||||||
|
// Stop reconnection timer (not needed in permanent AP mode)
|
||||||
|
if (_reconnectionTimer) {
|
||||||
|
xTimerStop(_reconnectionTimer, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark boot sequence as complete
|
||||||
|
_bootSequenceComplete = true;
|
||||||
|
|
||||||
|
// Notify connected
|
||||||
|
notifyConnectionChange(true);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(TAG, "❌ Failed to start AP Mode");
|
||||||
|
setState(NetworkState::DISCONNECTED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,13 +66,15 @@ enum class NetworkState {
|
|||||||
WIFI_PORTAL_MODE,
|
WIFI_PORTAL_MODE,
|
||||||
CONNECTED_ETHERNET,
|
CONNECTED_ETHERNET,
|
||||||
CONNECTED_WIFI,
|
CONNECTED_WIFI,
|
||||||
RECONNECTING
|
RECONNECTING,
|
||||||
|
AP_MODE_PERMANENT
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class ConnectionType {
|
enum class ConnectionType {
|
||||||
NONE,
|
NONE,
|
||||||
ETHERNET,
|
ETHERNET,
|
||||||
WIFI
|
WIFI,
|
||||||
|
AP
|
||||||
};
|
};
|
||||||
|
|
||||||
class Networking {
|
class Networking {
|
||||||
@@ -85,6 +87,7 @@ public:
|
|||||||
// Returns whether the network is currently connected
|
// Returns whether the network is currently connected
|
||||||
bool isConnected() const;
|
bool isConnected() const;
|
||||||
String getLocalIP() const;
|
String getLocalIP() const;
|
||||||
|
String getGateway() const;
|
||||||
ConnectionType getActiveConnection() const { return _activeConnection; }
|
ConnectionType getActiveConnection() const { return _activeConnection; }
|
||||||
NetworkState getState() const { return _state; }
|
NetworkState getState() const { return _state; }
|
||||||
|
|
||||||
@@ -97,6 +100,10 @@ public:
|
|||||||
// Manual connection control (for testing/debugging)
|
// Manual connection control (for testing/debugging)
|
||||||
void forceReconnect();
|
void forceReconnect();
|
||||||
|
|
||||||
|
// AP Mode control
|
||||||
|
void startPermanentAPMode();
|
||||||
|
bool isInAPMode() const { return _state == NetworkState::AP_MODE_PERMANENT; }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// HEALTH CHECK METHOD
|
// HEALTH CHECK METHOD
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,9 +26,13 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include "../FileManager/FileManager.hpp"
|
#include "../FileManager/FileManager.hpp"
|
||||||
|
#include "../Telemetry/Telemetry.hpp"
|
||||||
|
#include "../TimeKeeper/TimeKeeper.hpp"
|
||||||
|
|
||||||
class ConfigManager; // Forward declaration
|
class ConfigManager; // Forward declaration
|
||||||
class Player; // Forward declaration for idle check
|
class Player; // Forward declaration for idle check
|
||||||
|
class Timekeeper; // Forward declaration for freeze mode
|
||||||
|
class Telemetry; // Forward declaration for freeze mode
|
||||||
|
|
||||||
class OTAManager {
|
class OTAManager {
|
||||||
public:
|
public:
|
||||||
@@ -66,7 +70,9 @@ public:
|
|||||||
|
|
||||||
void begin();
|
void begin();
|
||||||
void setFileManager(FileManager* fm);
|
void setFileManager(FileManager* fm);
|
||||||
void setPlayer(Player* player); // NEW: Set player reference for idle check
|
void setPlayer(Player* player); // Set player reference for idle check
|
||||||
|
void setTimeKeeper(Timekeeper* tk); // Set timekeeper reference for freeze mode
|
||||||
|
void setTelemetry(Telemetry* telemetry); // Set telemetry reference for freeze mode
|
||||||
|
|
||||||
void checkForUpdates();
|
void checkForUpdates();
|
||||||
void checkForUpdates(const String& channel); // Check specific channel
|
void checkForUpdates(const String& channel); // Check specific channel
|
||||||
@@ -77,6 +83,7 @@ public:
|
|||||||
void checkFirmwareUpdateFromSD(); // Check SD for firmware update
|
void checkFirmwareUpdateFromSD(); // Check SD for firmware update
|
||||||
bool performManualUpdate(); // Manual update triggered by app
|
bool performManualUpdate(); // Manual update triggered by app
|
||||||
bool performManualUpdate(const String& channel); // Manual update from specific channel
|
bool performManualUpdate(const String& channel); // Manual update from specific channel
|
||||||
|
bool performCustomUpdate(const String& firmwareUrl, const String& checksum = "", size_t fileSize = 0, uint16_t version = 0); // Custom firmware update
|
||||||
|
|
||||||
// Hardware identification
|
// Hardware identification
|
||||||
String getHardwareVariant() const;
|
String getHardwareVariant() const;
|
||||||
@@ -85,8 +92,8 @@ public:
|
|||||||
// Status and info
|
// Status and info
|
||||||
Status getStatus() const { return _status; }
|
Status getStatus() const { return _status; }
|
||||||
ErrorCode getLastError() const { return _lastError; }
|
ErrorCode getLastError() const { return _lastError; }
|
||||||
float getCurrentVersion() const;
|
uint16_t getCurrentVersion() const;
|
||||||
float getAvailableVersion() const { return _availableVersion; }
|
uint16_t getAvailableVersion() const { return _availableVersion; }
|
||||||
bool isUpdateAvailable() const { return _updateAvailable; }
|
bool isUpdateAvailable() const { return _updateAvailable; }
|
||||||
|
|
||||||
// Callbacks
|
// Callbacks
|
||||||
@@ -103,11 +110,13 @@ public:
|
|||||||
private:
|
private:
|
||||||
ConfigManager& _configManager;
|
ConfigManager& _configManager;
|
||||||
FileManager* _fileManager;
|
FileManager* _fileManager;
|
||||||
Player* _player; // NEW: Player reference for idle check
|
Player* _player; // Player reference for idle check
|
||||||
|
Timekeeper* _timeKeeper; // TimeKeeper reference for freeze mode
|
||||||
|
Telemetry* _telemetry; // Telemetry reference for freeze mode
|
||||||
Status _status;
|
Status _status;
|
||||||
ErrorCode _lastError;
|
ErrorCode _lastError;
|
||||||
float _availableVersion;
|
uint16_t _availableVersion;
|
||||||
float _minVersion; // NEW: Minimum required version
|
uint16_t _minVersion; // NEW: Minimum required version
|
||||||
size_t _expectedFileSize; // NEW: Expected firmware file size
|
size_t _expectedFileSize; // NEW: Expected firmware file size
|
||||||
bool _updateAvailable;
|
bool _updateAvailable;
|
||||||
String _availableChecksum;
|
String _availableChecksum;
|
||||||
@@ -122,6 +131,24 @@ private:
|
|||||||
TimerHandle_t _scheduledCheckTimer;
|
TimerHandle_t _scheduledCheckTimer;
|
||||||
static void scheduledCheckCallback(TimerHandle_t xTimer);
|
static void scheduledCheckCallback(TimerHandle_t xTimer);
|
||||||
|
|
||||||
|
// Initial boot check timer (non-blocking delayed check)
|
||||||
|
TimerHandle_t _initialCheckTimer;
|
||||||
|
static void initialCheckCallback(TimerHandle_t xTimer);
|
||||||
|
void performInitialCheck(); // Async initial check after boot
|
||||||
|
|
||||||
|
// Worker task for OTA operations (prevents stack overflow in timer callbacks)
|
||||||
|
TaskHandle_t _otaWorkerTask;
|
||||||
|
SemaphoreHandle_t _otaWorkSignal;
|
||||||
|
static void otaWorkerTaskFunction(void* parameter);
|
||||||
|
void otaWorkerLoop();
|
||||||
|
|
||||||
|
enum class OTAWorkType {
|
||||||
|
NONE,
|
||||||
|
INITIAL_CHECK,
|
||||||
|
SCHEDULED_CHECK
|
||||||
|
};
|
||||||
|
OTAWorkType _pendingWork;
|
||||||
|
|
||||||
void setStatus(Status status, ErrorCode error = ErrorCode::NONE);
|
void setStatus(Status status, ErrorCode error = ErrorCode::NONE);
|
||||||
void notifyProgress(size_t current, size_t total);
|
void notifyProgress(size_t current, size_t total);
|
||||||
bool checkVersion();
|
bool checkVersion();
|
||||||
@@ -129,7 +156,8 @@ private:
|
|||||||
bool checkChannelsMetadata();
|
bool checkChannelsMetadata();
|
||||||
bool downloadAndInstall();
|
bool downloadAndInstall();
|
||||||
bool downloadAndInstall(const String& channel);
|
bool downloadAndInstall(const String& channel);
|
||||||
bool downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize); // NEW: Added size param
|
bool downloadDirectToFlash(const String& url, size_t expectedSize); // NEW: Direct to flash (bypasses SD)
|
||||||
|
bool downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize); // OLD: Via SD card
|
||||||
bool verifyChecksum(const String& filePath, const String& expectedChecksum);
|
bool verifyChecksum(const String& filePath, const String& expectedChecksum);
|
||||||
String calculateSHA256(const String& filePath);
|
String calculateSHA256(const String& filePath);
|
||||||
bool installFromSD(const String& filePath);
|
bool installFromSD(const String& filePath);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "OutputManager.hpp"
|
#include "OutputManager.hpp"
|
||||||
|
|
||||||
|
#define TAG "OutputManager"
|
||||||
#include "../ConfigManager/ConfigManager.hpp"
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
#include <Adafruit_PCF8574.h>
|
#include <Adafruit_PCF8574.h>
|
||||||
@@ -25,7 +27,7 @@ void OutputManager::startDurationTask() {
|
|||||||
}
|
}
|
||||||
_activeOutputs.reserve(32); // Support up to 32 virtual outputs
|
_activeOutputs.reserve(32); // Support up to 32 virtual outputs
|
||||||
xTaskCreatePinnedToCore(durationTask, "OutputDuration", 4096, this, 5, &_durationTaskHandle, 1);
|
xTaskCreatePinnedToCore(durationTask, "OutputDuration", 4096, this, 5, &_durationTaskHandle, 1);
|
||||||
LOG_INFO("⚡ Output Duration Management Task Initialized");
|
LOG_INFO(TAG, "⚡ Output Duration Management Task Initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutputManager::stopDurationTask() {
|
void OutputManager::stopDurationTask() {
|
||||||
@@ -35,13 +37,13 @@ void OutputManager::stopDurationTask() {
|
|||||||
portENTER_CRITICAL(&_outputMutex);
|
portENTER_CRITICAL(&_outputMutex);
|
||||||
_activeOutputs.clear();
|
_activeOutputs.clear();
|
||||||
portEXIT_CRITICAL(&_outputMutex);
|
portEXIT_CRITICAL(&_outputMutex);
|
||||||
LOG_INFO("⚡ Output Duration Management Task Stopped");
|
LOG_INFO(TAG, "⚡ Output Duration Management Task Stopped");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void OutputManager::durationTask(void* parameter) {
|
void OutputManager::durationTask(void* parameter) {
|
||||||
OutputManager* manager = static_cast<OutputManager*>(parameter);
|
OutputManager* manager = static_cast<OutputManager*>(parameter);
|
||||||
LOG_DEBUG("⚡ Output duration management task running on Core %d", xPortGetCoreID());
|
LOG_DEBUG(TAG, "⚡ Output duration management task running on Core %d", xPortGetCoreID());
|
||||||
while (true) {
|
while (true) {
|
||||||
manager->processExpiredOutputs();
|
manager->processExpiredOutputs();
|
||||||
vTaskDelay(pdMS_TO_TICKS(1));
|
vTaskDelay(pdMS_TO_TICKS(1));
|
||||||
@@ -58,7 +60,7 @@ void OutputManager::processExpiredOutputs() {
|
|||||||
_activeOutputs.erase(it);
|
_activeOutputs.erase(it);
|
||||||
portEXIT_CRITICAL(&_outputMutex);
|
portEXIT_CRITICAL(&_outputMutex);
|
||||||
extinguishOutput(outputIndex);
|
extinguishOutput(outputIndex);
|
||||||
LOG_VERBOSE("⚡ AUTO-EXTINGUISH Output:%d after %dms", outputIndex, duration_micros / 1000);
|
LOG_VERBOSE(TAG, "⚡ AUTO-EXTINGUISH Output:%d after %dms", outputIndex, duration_micros / 1000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,34 +69,28 @@ void OutputManager::processExpiredOutputs() {
|
|||||||
|
|
||||||
uint8_t OutputManager::getPhysicalOutput(uint8_t virtualOutput) const {
|
uint8_t OutputManager::getPhysicalOutput(uint8_t virtualOutput) const {
|
||||||
if (!_configManager) {
|
if (!_configManager) {
|
||||||
LOG_WARNING("⚠️ ConfigManager not available, using direct mapping for virtual output %d", virtualOutput);
|
LOG_WARNING(TAG, "⚠️ ConfigManager not available, using direct mapping for virtual output %d", virtualOutput);
|
||||||
return virtualOutput;
|
return virtualOutput;
|
||||||
}
|
}
|
||||||
if (!isValidVirtualOutput(virtualOutput)) {
|
if (!isValidVirtualOutput(virtualOutput)) {
|
||||||
LOG_ERROR("❌ Invalid virtual output %d, using direct mapping", virtualOutput);
|
LOG_ERROR(TAG, "❌ Invalid virtual output %d, using direct mapping", virtualOutput);
|
||||||
return virtualOutput;
|
return virtualOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get 1-indexed bell output from config
|
// Get 0-indexed bell output from config
|
||||||
uint16_t bellOutput1Indexed = _configManager->getBellOutput(virtualOutput);
|
uint16_t bellOutput = _configManager->getBellOutput(virtualOutput);
|
||||||
|
|
||||||
// Handle unconfigured bells (255 = disabled)
|
// Handle unconfigured bells (255 = disabled)
|
||||||
if (bellOutput1Indexed == 255) {
|
if (bellOutput == 255) {
|
||||||
LOG_WARNING("⚠️ Bell %d not configured (255)", virtualOutput);
|
LOG_WARNING(TAG, "⚠️ Bell %d not configured (255)", virtualOutput);
|
||||||
return 255; // Return invalid to prevent firing
|
return 255; // Return invalid to prevent firing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle invalid 0 configuration
|
// Physical output is already 0-indexed from config
|
||||||
if (bellOutput1Indexed == 0) {
|
uint8_t physicalOutput = (uint8_t)bellOutput;
|
||||||
LOG_ERROR("❌ Bell %d configured as 0 (invalid - should be 1-indexed)", virtualOutput);
|
|
||||||
return 255;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert 1-indexed config to 0-indexed physical output
|
LOG_DEBUG(TAG, "🔗 Bell %d → 0-indexed output %d",
|
||||||
uint8_t physicalOutput = (uint8_t)(bellOutput1Indexed - 1);
|
virtualOutput, physicalOutput);
|
||||||
|
|
||||||
LOG_DEBUG("🔗 Bell %d → 1-indexed config %d → 0-indexed output %d",
|
|
||||||
virtualOutput, bellOutput1Indexed, physicalOutput);
|
|
||||||
|
|
||||||
return physicalOutput;
|
return physicalOutput;
|
||||||
}
|
}
|
||||||
@@ -110,12 +106,12 @@ bool OutputManager::isValidPhysicalOutput(uint8_t physicalOutput) const {
|
|||||||
|
|
||||||
void OutputManager::fireClockOutput(uint8_t virtualOutput, uint16_t durationMs) {
|
void OutputManager::fireClockOutput(uint8_t virtualOutput, uint16_t durationMs) {
|
||||||
if (!_initialized) {
|
if (!_initialized) {
|
||||||
LOG_ERROR("❌ OutputManager not initialized for clock output!");
|
LOG_ERROR(TAG, "❌ OutputManager not initialized for clock output!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_configManager) {
|
if (!_configManager) {
|
||||||
LOG_ERROR("❌ ConfigManager not available for clock output mapping!");
|
LOG_ERROR(TAG, "❌ ConfigManager not available for clock output mapping!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,40 +121,33 @@ void OutputManager::fireClockOutput(uint8_t virtualOutput, uint16_t durationMs)
|
|||||||
// Virtual clock output 0 = C1
|
// Virtual clock output 0 = C1
|
||||||
physicalOutput = _configManager->getClockOutput1();
|
physicalOutput = _configManager->getClockOutput1();
|
||||||
if (physicalOutput == 255) {
|
if (physicalOutput == 255) {
|
||||||
LOG_WARNING("⚠️ Clock C1 not configured (255)");
|
LOG_WARNING(TAG, "⚠️ Clock C1 not configured (255)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if (virtualOutput == 1) {
|
} else if (virtualOutput == 1) {
|
||||||
// Virtual clock output 1 = C2
|
// Virtual clock output 1 = C2
|
||||||
physicalOutput = _configManager->getClockOutput2();
|
physicalOutput = _configManager->getClockOutput2();
|
||||||
if (physicalOutput == 255) {
|
if (physicalOutput == 255) {
|
||||||
LOG_WARNING("⚠️ Clock C2 not configured (255)");
|
LOG_WARNING(TAG, "⚠️ Clock C2 not configured (255)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Invalid virtual clock output: %d (only 0=C1, 1=C2 supported)", virtualOutput);
|
LOG_ERROR(TAG, "❌ Invalid virtual clock output: %d (only 0=C1, 1=C2 supported)", virtualOutput);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert 1-indexed config value to 0-indexed physical output
|
// Physical output is already 0-indexed from config
|
||||||
if (physicalOutput == 0) {
|
if (!isValidPhysicalOutput(physicalOutput)) {
|
||||||
LOG_ERROR("❌ Clock output configured as 0 (invalid - should be 1-indexed)");
|
LOG_ERROR(TAG, "❌ Invalid physical output for clock: %d (max outputs: %d)",
|
||||||
return;
|
physicalOutput, getMaxOutputs());
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t zeroIndexedOutput = physicalOutput - 1; // Convert 1-indexed to 0-indexed
|
|
||||||
|
|
||||||
if (!isValidPhysicalOutput(zeroIndexedOutput)) {
|
|
||||||
LOG_ERROR("❌ Invalid physical output for clock: %d (1-indexed config: %d, max outputs: %d)",
|
|
||||||
zeroIndexedOutput, physicalOutput, getMaxOutputs());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire the physical output directly
|
// Fire the physical output directly
|
||||||
fireOutputForDuration(zeroIndexedOutput, durationMs);
|
fireOutputForDuration(physicalOutput, durationMs);
|
||||||
|
|
||||||
LOG_DEBUG("🕐 FIRE Clock Virtual %d (C%d) → 1-indexed config %d → 0-indexed output %d for %dms",
|
LOG_DEBUG(TAG, "🕐 FIRE Clock Virtual %d (C%d) → 0-indexed output %d for %dms",
|
||||||
virtualOutput, virtualOutput + 1, physicalOutput, zeroIndexedOutput, durationMs);
|
virtualOutput, virtualOutput + 1, physicalOutput, durationMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== PCF8574/PCF8575 MULTI-CHIP IMPLEMENTATION ====================
|
// ==================== PCF8574/PCF8575 MULTI-CHIP IMPLEMENTATION ====================
|
||||||
@@ -188,19 +177,19 @@ PCF8574OutputManager::~PCF8574OutputManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool PCF8574OutputManager::initialize() {
|
bool PCF8574OutputManager::initialize() {
|
||||||
LOG_INFO("🔌 Initializing Multi-Chip PCF857x Output Manager (%d chips)", _chipCount);
|
LOG_INFO(TAG, "🔌 Initializing Multi-Chip PCF857x Output Manager (%d chips)", _chipCount);
|
||||||
delay(100);
|
delay(100);
|
||||||
|
|
||||||
bool allSuccess = true;
|
bool allSuccess = true;
|
||||||
for (uint8_t i = 0; i < _chipCount; i++) {
|
for (uint8_t i = 0; i < _chipCount; i++) {
|
||||||
if (!initializeChip(i)) {
|
if (!initializeChip(i)) {
|
||||||
LOG_ERROR("❌ Failed to initialize chip %d!", i);
|
LOG_ERROR(TAG, "❌ Failed to initialize chip %d!", i);
|
||||||
allSuccess = false;
|
allSuccess = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!allSuccess) {
|
if (!allSuccess) {
|
||||||
LOG_ERROR("❌ Not all chips initialized successfully!");
|
LOG_ERROR(TAG, "❌ Not all chips initialized successfully!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,46 +198,46 @@ bool PCF8574OutputManager::initialize() {
|
|||||||
_allChipsInitialized = true;
|
_allChipsInitialized = true;
|
||||||
_initialized = true; // Set base class flag too!
|
_initialized = true; // Set base class flag too!
|
||||||
|
|
||||||
LOG_INFO("✅ Multi-Chip PCF857x Output Manager Initialized (%d total outputs)", _totalOutputs);
|
LOG_INFO(TAG, "✅ Multi-Chip PCF857x Output Manager Initialized (%d total outputs)", _totalOutputs);
|
||||||
generateHardwareTypeString();
|
generateHardwareTypeString();
|
||||||
|
|
||||||
if (_configManager) {
|
if (_configManager) {
|
||||||
LOG_INFO("📋 Virtual Output Configuration Mappings:");
|
LOG_INFO(TAG, "📋 Virtual Output Configuration Mappings:");
|
||||||
for (uint8_t i = 0; i < min(16, (int)_totalOutputs); i++) { // Check virtual outputs
|
for (uint8_t i = 0; i < min(16, (int)_totalOutputs); i++) { // Check virtual outputs
|
||||||
uint16_t configOutput = _configManager->getBellOutput(i);
|
uint16_t configOutput = _configManager->getBellOutput(i);
|
||||||
if (configOutput < _totalOutputs) {
|
if (configOutput < _totalOutputs) {
|
||||||
VirtualOutputInfo info = getVirtualOutputInfo(configOutput);
|
VirtualOutputInfo info = getVirtualOutputInfo(configOutput);
|
||||||
LOG_DEBUG(" Bell %d → Virtual Output %d → %s[%d] Pin %d", i, configOutput, info.chipType, info.chipIndex, info.localPin);
|
LOG_DEBUG(TAG, " Bell %d → Virtual Output %d → %s[%d] Pin %d", i, configOutput, info.chipType, info.chipIndex, info.localPin);
|
||||||
} else if (configOutput == 255) {
|
} else if (configOutput == 255) {
|
||||||
LOG_DEBUG(" Bell %d → Not configured (255)", i);
|
LOG_DEBUG(TAG, " Bell %d → Not configured (255)", i);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("⚠️ Bell %d mapped to invalid virtual output %d (max: %d)", i, configOutput, _totalOutputs - 1);
|
LOG_WARNING(TAG, "⚠️ Bell %d mapped to invalid virtual output %d (max: %d)", i, configOutput, _totalOutputs - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t c1 = _configManager->getClockOutput1();
|
uint8_t c1 = _configManager->getClockOutput1();
|
||||||
uint8_t c2 = _configManager->getClockOutput2();
|
uint8_t c2 = _configManager->getClockOutput2();
|
||||||
LOG_INFO("🕐 Clock Virtual Output Mappings:");
|
LOG_INFO(TAG, "🕐 Clock Virtual Output Mappings:");
|
||||||
|
|
||||||
if (c1 != 255 && c1 < _totalOutputs) {
|
if (c1 != 255 && c1 < _totalOutputs) {
|
||||||
VirtualOutputInfo info = getVirtualOutputInfo(c1);
|
VirtualOutputInfo info = getVirtualOutputInfo(c1);
|
||||||
LOG_DEBUG(" Clock C1 → Virtual Output %d → %s[%d] Pin %d", c1, info.chipType, info.chipIndex, info.localPin);
|
LOG_DEBUG(TAG, " Clock C1 → Virtual Output %d → %s[%d] Pin %d", c1, info.chipType, info.chipIndex, info.localPin);
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG(" Clock C1 → Not configured");
|
LOG_DEBUG(TAG, " Clock C1 → Not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c2 != 255 && c2 < _totalOutputs) {
|
if (c2 != 255 && c2 < _totalOutputs) {
|
||||||
VirtualOutputInfo info = getVirtualOutputInfo(c2);
|
VirtualOutputInfo info = getVirtualOutputInfo(c2);
|
||||||
LOG_DEBUG(" Clock C2 → Virtual Output %d → %s[%d] Pin %d", c2, info.chipType, info.chipIndex, info.localPin);
|
LOG_DEBUG(TAG, " Clock C2 → Virtual Output %d → %s[%d] Pin %d", c2, info.chipType, info.chipIndex, info.localPin);
|
||||||
} else {
|
} else {
|
||||||
LOG_DEBUG(" Clock C2 → Not configured");
|
LOG_DEBUG(TAG, " Clock C2 → Not configured");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show virtual output mapping
|
// Show virtual output mapping
|
||||||
LOG_INFO("🔗 Virtual Output Mapping:");
|
LOG_INFO(TAG, "🔗 Virtual Output Mapping:");
|
||||||
for (uint8_t i = 0; i < _totalOutputs; i++) {
|
for (uint8_t i = 0; i < _totalOutputs; i++) {
|
||||||
VirtualOutputInfo info = getVirtualOutputInfo(i);
|
VirtualOutputInfo info = getVirtualOutputInfo(i);
|
||||||
LOG_DEBUG(" Virtual Output %d → %s[%d] Pin %d", i, info.chipType, info.chipIndex, info.localPin);
|
LOG_DEBUG(TAG, " Virtual Output %d → %s[%d] Pin %d", i, info.chipType, info.chipIndex, info.localPin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -256,18 +245,18 @@ bool PCF8574OutputManager::initialize() {
|
|||||||
|
|
||||||
void PCF8574OutputManager::fireOutput(uint8_t outputIndex) {
|
void PCF8574OutputManager::fireOutput(uint8_t outputIndex) {
|
||||||
if (!_allChipsInitialized) {
|
if (!_allChipsInitialized) {
|
||||||
LOG_ERROR("❌ PCF857x chips not initialized!");
|
LOG_ERROR(TAG, "❌ PCF857x chips not initialized!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isValidVirtualOutput(outputIndex)) {
|
if (!isValidVirtualOutput(outputIndex)) {
|
||||||
LOG_ERROR("❌ Invalid virtual output: %d (max: %d)", outputIndex, _totalOutputs - 1);
|
LOG_ERROR(TAG, "❌ Invalid virtual output: %d (max: %d)", outputIndex, _totalOutputs - 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
||||||
writeOutputToChip(info.chipIndex, info.localPin, false);
|
writeOutputToChip(info.chipIndex, info.localPin, false);
|
||||||
|
|
||||||
LOG_DEBUG("🔥 FIRE Virtual Output %d → %s[%d] Pin %d", outputIndex, info.chipType, info.chipIndex, info.localPin);
|
LOG_DEBUG(TAG, "🔥 FIRE Virtual Output %d → %s[%d] Pin %d", outputIndex, info.chipType, info.chipIndex, info.localPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PCF8574OutputManager::extinguishOutput(uint8_t outputIndex) {
|
void PCF8574OutputManager::extinguishOutput(uint8_t outputIndex) {
|
||||||
@@ -277,7 +266,7 @@ void PCF8574OutputManager::extinguishOutput(uint8_t outputIndex) {
|
|||||||
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
||||||
writeOutputToChip(info.chipIndex, info.localPin, true);
|
writeOutputToChip(info.chipIndex, info.localPin, true);
|
||||||
|
|
||||||
LOG_DEBUG("💧 EXTINGUISH Virtual Output %d → %s[%d] Pin %d", outputIndex, info.chipType, info.chipIndex, info.localPin);
|
LOG_DEBUG(TAG, "💧 EXTINGUISH Virtual Output %d → %s[%d] Pin %d", outputIndex, info.chipType, info.chipIndex, info.localPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PCF8574OutputManager::fireOutputForDuration(uint8_t outputIndex, uint16_t durationMs) {
|
void PCF8574OutputManager::fireOutputForDuration(uint8_t outputIndex, uint16_t durationMs) {
|
||||||
@@ -329,7 +318,7 @@ void PCF8574OutputManager::fireOutputsBatchForDuration(const std::vector<std::pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
void PCF8574OutputManager::emergencyShutdown() {
|
void PCF8574OutputManager::emergencyShutdown() {
|
||||||
LOG_WARNING("🚨 PCF857x EMERGENCY SHUTDOWN - All outputs HIGH");
|
LOG_WARNING(TAG, "🚨 PCF857x EMERGENCY SHUTDOWN - All outputs HIGH");
|
||||||
portENTER_CRITICAL(&_outputMutex);
|
portENTER_CRITICAL(&_outputMutex);
|
||||||
_activeOutputs.clear();
|
_activeOutputs.clear();
|
||||||
portEXIT_CRITICAL(&_outputMutex);
|
portEXIT_CRITICAL(&_outputMutex);
|
||||||
@@ -357,7 +346,7 @@ ChipConfig PCF8574OutputManager::getChipConfig(uint8_t chipIndex) const {
|
|||||||
|
|
||||||
bool PCF8574OutputManager::addChip(uint8_t i2cAddress, ChipType chipType, uint8_t activeOutputs) {
|
bool PCF8574OutputManager::addChip(uint8_t i2cAddress, ChipType chipType, uint8_t activeOutputs) {
|
||||||
if (_chipCount >= MAX_CHIPS) {
|
if (_chipCount >= MAX_CHIPS) {
|
||||||
LOG_ERROR("❌ Cannot add more chips - maximum %d chips supported", MAX_CHIPS);
|
LOG_ERROR(TAG, "❌ Cannot add more chips - maximum %d chips supported", MAX_CHIPS);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +354,7 @@ bool PCF8574OutputManager::addChip(uint8_t i2cAddress, ChipType chipType, uint8_
|
|||||||
_chipCount++;
|
_chipCount++;
|
||||||
updateTotalOutputs();
|
updateTotalOutputs();
|
||||||
|
|
||||||
LOG_INFO("✅ Added chip %d: %s at 0x%02X (%d/%d active outputs)",
|
LOG_INFO(TAG, "✅ Added chip %d: %s at 0x%02X (%d/%d active outputs)",
|
||||||
_chipCount - 1,
|
_chipCount - 1,
|
||||||
(chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575",
|
(chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575",
|
||||||
i2cAddress,
|
i2cAddress,
|
||||||
@@ -383,7 +372,7 @@ PCF8574OutputManager::VirtualOutputInfo PCF8574OutputManager::getVirtualOutputIn
|
|||||||
info.chipIndex = 0;
|
info.chipIndex = 0;
|
||||||
info.localPin = 0;
|
info.localPin = 0;
|
||||||
info.chipType = (_chipCount > 0 && _chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
info.chipType = (_chipCount > 0 && _chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||||
LOG_ERROR("❌ Invalid virtual output %d (max: %d)", virtualOutput, _totalOutputs - 1);
|
LOG_ERROR(TAG, "❌ Invalid virtual output %d (max: %d)", virtualOutput, _totalOutputs - 1);
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +392,7 @@ PCF8574OutputManager::VirtualOutputInfo PCF8574OutputManager::getVirtualOutputIn
|
|||||||
info.chipIndex = 0;
|
info.chipIndex = 0;
|
||||||
info.localPin = 0;
|
info.localPin = 0;
|
||||||
info.chipType = (_chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
info.chipType = (_chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||||
LOG_ERROR("❌ Virtual output %d exceeds available outputs on single chip", virtualOutput);
|
LOG_ERROR(TAG, "❌ Virtual output %d exceeds available outputs on single chip", virtualOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
@@ -411,7 +400,7 @@ PCF8574OutputManager::VirtualOutputInfo PCF8574OutputManager::getVirtualOutputIn
|
|||||||
|
|
||||||
void PCF8574OutputManager::setChipActiveOutputs(uint8_t chipIndex, uint8_t activeOutputs) {
|
void PCF8574OutputManager::setChipActiveOutputs(uint8_t chipIndex, uint8_t activeOutputs) {
|
||||||
if (chipIndex >= _chipCount) {
|
if (chipIndex >= _chipCount) {
|
||||||
LOG_ERROR("❌ Invalid chip index %d (max: %d)", chipIndex, _chipCount - 1);
|
LOG_ERROR(TAG, "❌ Invalid chip index %d (max: %d)", chipIndex, _chipCount - 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,12 +408,12 @@ void PCF8574OutputManager::setChipActiveOutputs(uint8_t chipIndex, uint8_t activ
|
|||||||
_chips[chipIndex].activeOutputs = min(activeOutputs, maxOutputs);
|
_chips[chipIndex].activeOutputs = min(activeOutputs, maxOutputs);
|
||||||
updateTotalOutputs();
|
updateTotalOutputs();
|
||||||
|
|
||||||
LOG_INFO("✅ Updated chip %d active outputs: %d/%d", chipIndex, _chips[chipIndex].activeOutputs, maxOutputs);
|
LOG_INFO(TAG, "✅ Updated chip %d active outputs: %d/%d", chipIndex, _chips[chipIndex].activeOutputs, maxOutputs);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t PCF8574OutputManager::getChipActiveOutputs(uint8_t chipIndex) const {
|
uint8_t PCF8574OutputManager::getChipActiveOutputs(uint8_t chipIndex) const {
|
||||||
if (chipIndex >= _chipCount) {
|
if (chipIndex >= _chipCount) {
|
||||||
LOG_ERROR("❌ Invalid chip index %d (max: %d)", chipIndex, _chipCount - 1);
|
LOG_ERROR(TAG, "❌ Invalid chip index %d (max: %d)", chipIndex, _chipCount - 1);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return _chips[chipIndex].activeOutputs;
|
return _chips[chipIndex].activeOutputs;
|
||||||
@@ -437,7 +426,7 @@ bool PCF8574OutputManager::initializeChip(uint8_t chipIndex) {
|
|||||||
ChipConfig& chip = _chips[chipIndex];
|
ChipConfig& chip = _chips[chipIndex];
|
||||||
const char* chipTypeStr = (chip.chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
const char* chipTypeStr = (chip.chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||||
|
|
||||||
LOG_DEBUG("🔌 Initializing %s at address 0x%02X", chipTypeStr, chip.i2cAddress);
|
LOG_DEBUG(TAG, "🔌 Initializing %s at address 0x%02X", chipTypeStr, chip.i2cAddress);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (chip.chipType == ChipType::PCF8574) {
|
if (chip.chipType == ChipType::PCF8574) {
|
||||||
@@ -447,7 +436,7 @@ bool PCF8574OutputManager::initializeChip(uint8_t chipIndex) {
|
|||||||
Adafruit_PCF8574* pcf = static_cast<Adafruit_PCF8574*>(chip.chipInstance);
|
Adafruit_PCF8574* pcf = static_cast<Adafruit_PCF8574*>(chip.chipInstance);
|
||||||
|
|
||||||
if (!pcf->begin(chip.i2cAddress, &Wire)) {
|
if (!pcf->begin(chip.i2cAddress, &Wire)) {
|
||||||
LOG_ERROR("❌ Failed to initialize PCF8574 at address 0x%02X", chip.i2cAddress);
|
LOG_ERROR(TAG, "❌ Failed to initialize PCF8574 at address 0x%02X", chip.i2cAddress);
|
||||||
chip.chipInstance = nullptr;
|
chip.chipInstance = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -464,7 +453,7 @@ bool PCF8574OutputManager::initializeChip(uint8_t chipIndex) {
|
|||||||
Adafruit_PCF8575* pcf = static_cast<Adafruit_PCF8575*>(chip.chipInstance);
|
Adafruit_PCF8575* pcf = static_cast<Adafruit_PCF8575*>(chip.chipInstance);
|
||||||
|
|
||||||
if (!pcf->begin(chip.i2cAddress, &Wire)) {
|
if (!pcf->begin(chip.i2cAddress, &Wire)) {
|
||||||
LOG_ERROR("❌ Failed to initialize PCF8575 at address 0x%02X", chip.i2cAddress);
|
LOG_ERROR(TAG, "❌ Failed to initialize PCF8575 at address 0x%02X", chip.i2cAddress);
|
||||||
chip.chipInstance = nullptr;
|
chip.chipInstance = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -477,11 +466,11 @@ bool PCF8574OutputManager::initializeChip(uint8_t chipIndex) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
chip.initialized = true;
|
chip.initialized = true;
|
||||||
LOG_DEBUG("✅ %s at 0x%02X initialized successfully", chipTypeStr, chip.i2cAddress);
|
LOG_DEBUG(TAG, "✅ %s at 0x%02X initialized successfully", chipTypeStr, chip.i2cAddress);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
LOG_ERROR("❌ Exception during %s initialization at 0x%02X", chipTypeStr, chip.i2cAddress);
|
LOG_ERROR(TAG, "❌ Exception during %s initialization at 0x%02X", chipTypeStr, chip.i2cAddress);
|
||||||
chip.chipInstance = nullptr;
|
chip.chipInstance = nullptr;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -516,7 +505,7 @@ PCF8574OutputManager::OutputMapping PCF8574OutputManager::getOutputMapping(uint8
|
|||||||
// Invalid output - return chip 0, pin 0 as safe fallback
|
// Invalid output - return chip 0, pin 0 as safe fallback
|
||||||
mapping.chipIndex = 0;
|
mapping.chipIndex = 0;
|
||||||
mapping.localPin = 0;
|
mapping.localPin = 0;
|
||||||
LOG_ERROR("❌ Invalid physical output %d mapped to fallback", physicalOutput);
|
LOG_ERROR(TAG, "❌ Invalid physical output %d mapped to fallback", physicalOutput);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapping;
|
return mapping;
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include "Player.hpp"
|
#include "Player.hpp"
|
||||||
|
|
||||||
|
#define TAG "Player"
|
||||||
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
#include "../Communication/CommunicationRouter/CommunicationRouter.hpp"
|
||||||
#include "../BellEngine/BellEngine.hpp"
|
#include "../BellEngine/BellEngine.hpp"
|
||||||
|
#include "../Telemetry/Telemetry.hpp"
|
||||||
|
#include "../TimeKeeper/TimeKeeper.hpp" // 🔥 Include for Timekeeper class definition
|
||||||
|
#include "../BuiltInMelodies/BuiltInMelodies.hpp"
|
||||||
|
|
||||||
// Note: Removed global melody_steps dependency for cleaner architecture
|
// Note: Removed global melody_steps dependency for cleaner architecture
|
||||||
|
|
||||||
@@ -10,7 +15,7 @@ Player::Player(CommunicationRouter* comm, FileManager* fm)
|
|||||||
, name("melody1")
|
, name("melody1")
|
||||||
, uid("x")
|
, uid("x")
|
||||||
, url("-")
|
, url("-")
|
||||||
, noteAssignments{1,2,3,4,5,6,0,0,0,0,0,0,0,0,0,0}
|
, noteAssignments{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}
|
||||||
, speed(500)
|
, speed(500)
|
||||||
, segment_duration(15000)
|
, segment_duration(15000)
|
||||||
, pause_duration(0)
|
, pause_duration(0)
|
||||||
@@ -28,6 +33,8 @@ Player::Player(CommunicationRouter* comm, FileManager* fm)
|
|||||||
, _commManager(comm)
|
, _commManager(comm)
|
||||||
, _fileManager(fm)
|
, _fileManager(fm)
|
||||||
, _bellEngine(nullptr)
|
, _bellEngine(nullptr)
|
||||||
|
, _telemetry(nullptr)
|
||||||
|
, _timekeeper(nullptr)
|
||||||
, _durationTimerHandle(NULL) {
|
, _durationTimerHandle(NULL) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +44,7 @@ Player::Player()
|
|||||||
, name("melody1")
|
, name("melody1")
|
||||||
, uid("x")
|
, uid("x")
|
||||||
, url("-")
|
, url("-")
|
||||||
, noteAssignments{1,2,3,4,5,6,0,0,0,0,0,0,0,0,0,0}
|
, noteAssignments{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}
|
||||||
, speed(500)
|
, speed(500)
|
||||||
, segment_duration(15000)
|
, segment_duration(15000)
|
||||||
, pause_duration(0)
|
, pause_duration(0)
|
||||||
@@ -55,6 +62,8 @@ Player::Player()
|
|||||||
, _commManager(nullptr)
|
, _commManager(nullptr)
|
||||||
, _fileManager(nullptr)
|
, _fileManager(nullptr)
|
||||||
, _bellEngine(nullptr)
|
, _bellEngine(nullptr)
|
||||||
|
, _telemetry(nullptr)
|
||||||
|
, _timekeeper(nullptr)
|
||||||
, _durationTimerHandle(NULL) {
|
, _durationTimerHandle(NULL) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +87,7 @@ Player::~Player() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::begin() {
|
void Player::begin() {
|
||||||
LOG_INFO("Initializing Player with FreeRTOS Timer (saves 4KB RAM!)");
|
LOG_INFO(TAG, "Initializing Player with FreeRTOS Timer (saves 4KB RAM!)");
|
||||||
|
|
||||||
// Create a periodic timer that fires every 500ms
|
// Create a periodic timer that fires every 500ms
|
||||||
_durationTimerHandle = xTimerCreate(
|
_durationTimerHandle = xTimerCreate(
|
||||||
@@ -91,18 +100,24 @@ void Player::begin() {
|
|||||||
|
|
||||||
if (_durationTimerHandle != NULL) {
|
if (_durationTimerHandle != NULL) {
|
||||||
xTimerStart(_durationTimerHandle, 0);
|
xTimerStart(_durationTimerHandle, 0);
|
||||||
LOG_INFO("Player initialized successfully with timer");
|
LOG_INFO(TAG, "Player initialized successfully with timer");
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("Failed to create Player timer!");
|
LOG_ERROR(TAG, "Failed to create Player timer!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::play() {
|
void Player::play() {
|
||||||
if (_melodySteps.empty()) {
|
if (_melodySteps.empty()) {
|
||||||
LOG_ERROR("Cannot play: No melody loaded");
|
LOG_ERROR(TAG, "Cannot play: No melody loaded");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 CRITICAL: Interrupt any active clock alerts - user playback has priority!
|
||||||
|
if (_timekeeper) {
|
||||||
|
_timekeeper->interruptActiveAlert();
|
||||||
|
LOG_DEBUG(TAG, "Player: Interrupted any active clock alerts");
|
||||||
|
}
|
||||||
|
|
||||||
if (_bellEngine) {
|
if (_bellEngine) {
|
||||||
_bellEngine->setMelodyData(_melodySteps);
|
_bellEngine->setMelodyData(_melodySteps);
|
||||||
_bellEngine->start();
|
_bellEngine->start();
|
||||||
@@ -112,7 +127,7 @@ void Player::play() {
|
|||||||
hardStop = false;
|
hardStop = false;
|
||||||
startTime = segmentStartTime = millis();
|
startTime = segmentStartTime = millis();
|
||||||
setStatus(PlayerStatus::PLAYING); // Update status and notify clients
|
setStatus(PlayerStatus::PLAYING); // Update status and notify clients
|
||||||
LOG_DEBUG("Plbck: PLAY");
|
LOG_DEBUG(TAG, "Plbck: PLAY");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::forceStop() {
|
void Player::forceStop() {
|
||||||
@@ -123,7 +138,13 @@ void Player::forceStop() {
|
|||||||
hardStop = true;
|
hardStop = true;
|
||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
setStatus(PlayerStatus::STOPPED); // Immediate stop, notify clients
|
setStatus(PlayerStatus::STOPPED); // Immediate stop, notify clients
|
||||||
LOG_DEBUG("Plbck: FORCE STOP");
|
|
||||||
|
// Save strike counters after melody stops
|
||||||
|
if (_telemetry) {
|
||||||
|
_telemetry->saveStrikeCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "Plbck: FORCE STOP");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::stop() {
|
void Player::stop() {
|
||||||
@@ -135,9 +156,19 @@ void Player::stop() {
|
|||||||
isPlaying = false;
|
isPlaying = false;
|
||||||
|
|
||||||
// Set STOPPING status - actual stop message will be sent when BellEngine finishes
|
// Set STOPPING status - actual stop message will be sent when BellEngine finishes
|
||||||
setStatus(PlayerStatus::STOPPING);
|
if (isPaused) {
|
||||||
LOG_DEBUG("Plbck: SOFT STOP (waiting for melody to complete)");
|
setStatus(PlayerStatus::STOPPED);
|
||||||
|
|
||||||
|
// Save strike counters after melody stops
|
||||||
|
if (_telemetry) {
|
||||||
|
_telemetry->saveStrikeCounters();
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "Plbck: STOP from PAUSED state");
|
||||||
|
} else {
|
||||||
|
setStatus(PlayerStatus::STOPPING);
|
||||||
|
LOG_DEBUG(TAG, "Plbck: SOFT STOP (waiting for melody to complete)");
|
||||||
|
}
|
||||||
// NOTE: The actual "stop" message is now sent in onMelodyLoopCompleted()
|
// NOTE: The actual "stop" message is now sent in onMelodyLoopCompleted()
|
||||||
// when the BellEngine actually finishes the current loop
|
// when the BellEngine actually finishes the current loop
|
||||||
}
|
}
|
||||||
@@ -145,14 +176,14 @@ void Player::stop() {
|
|||||||
void Player::pause() {
|
void Player::pause() {
|
||||||
isPaused = true;
|
isPaused = true;
|
||||||
setStatus(PlayerStatus::PAUSED);
|
setStatus(PlayerStatus::PAUSED);
|
||||||
LOG_DEBUG("Plbck: PAUSE");
|
LOG_DEBUG(TAG, "Plbck: PAUSE");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::unpause() {
|
void Player::unpause() {
|
||||||
isPaused = false;
|
isPaused = false;
|
||||||
segmentStartTime = millis();
|
segmentStartTime = millis();
|
||||||
setStatus(PlayerStatus::PLAYING);
|
setStatus(PlayerStatus::PLAYING);
|
||||||
LOG_DEBUG("Plbck: RESUME");
|
LOG_DEBUG(TAG, "Plbck: RESUME");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Player::command(JsonVariant data) {
|
bool Player::command(JsonVariant data) {
|
||||||
@@ -160,7 +191,7 @@ bool Player::command(JsonVariant data) {
|
|||||||
loadMelodyInRAM(); // Removed parameter - use internal storage
|
loadMelodyInRAM(); // Removed parameter - use internal storage
|
||||||
|
|
||||||
String action = data["action"];
|
String action = data["action"];
|
||||||
LOG_DEBUG("Incoming Command: %s", action.c_str());
|
LOG_DEBUG(TAG, "Incoming Command: %s", action.c_str());
|
||||||
|
|
||||||
// Play or Stop Logic
|
// Play or Stop Logic
|
||||||
if (action == "play") {
|
if (action == "play") {
|
||||||
@@ -170,7 +201,7 @@ bool Player::command(JsonVariant data) {
|
|||||||
forceStop();
|
forceStop();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("Unknown playback action: %s", action.c_str());
|
LOG_WARNING(TAG, "Unknown playback action: %s", action.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,30 +239,45 @@ void Player::setMelodyAttributes(JsonVariant doc) {
|
|||||||
continuous_loop = doc["continuous_loop"].as<bool>();
|
continuous_loop = doc["continuous_loop"].as<bool>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (continuous_loop && total_duration == 0) {
|
// Recalculate infinite_play based on current values (reset first!)
|
||||||
infinite_play = true;
|
infinite_play = (continuous_loop && total_duration == 0);
|
||||||
}
|
|
||||||
|
|
||||||
if (!continuous_loop) {
|
if (!continuous_loop) {
|
||||||
total_duration = segment_duration;
|
total_duration = segment_duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Print Just for Debugging Purposes
|
// Print Just for Debugging Purposes
|
||||||
LOG_DEBUG("Set Melody Vars / Name: %s, UID: %s",
|
LOG_DEBUG(TAG, "Set Melody Vars / Name: %s, UID: %s",
|
||||||
name.c_str(), uid.c_str());
|
name.c_str(), uid.c_str());
|
||||||
LOG_DEBUG("URL: %s", url.c_str());
|
LOG_DEBUG(TAG, "URL: %s", url.c_str());
|
||||||
LOG_DEBUG("Speed: %d, Per Segment Duration: %lu, Pause Duration: %lu, Total Duration: %d, Continuous: %s, Infinite: %s",
|
LOG_DEBUG(TAG, "Speed: %d, Per Segment Duration: %lu, Pause Duration: %lu, Total Duration: %d, Continuous: %s, Infinite: %s",
|
||||||
speed, segment_duration, pause_duration, total_duration,
|
speed, segment_duration, pause_duration, total_duration,
|
||||||
continuous_loop ? "true" : "false", infinite_play ? "true" : "false");
|
continuous_loop ? "true" : "false", infinite_play ? "true" : "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::loadMelodyInRAM() {
|
void Player::loadMelodyInRAM() {
|
||||||
String filePath = "/melodies/" + String(uid.c_str());
|
String uidStr = String(uid.c_str());
|
||||||
|
|
||||||
|
// 🎵 PRIORITY 1: Check if this is a built-in melody
|
||||||
|
if (BuiltInMelodies::isBuiltInMelody(uidStr)) {
|
||||||
|
LOG_INFO(TAG, "Loading built-in melody: %s", uidStr.c_str());
|
||||||
|
|
||||||
|
if (BuiltInMelodies::loadBuiltInMelody(uidStr, _melodySteps)) {
|
||||||
|
LOG_INFO(TAG, "✅ Built-in melody loaded successfully: %d steps", _melodySteps.size());
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(TAG, "Failed to load built-in melody: %s", uidStr.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎵 PRIORITY 2: Load from SD card
|
||||||
|
String filePath = "/melodies/" + uidStr;
|
||||||
|
|
||||||
File bin_file = SD.open(filePath.c_str(), FILE_READ);
|
File bin_file = SD.open(filePath.c_str(), FILE_READ);
|
||||||
if (!bin_file) {
|
if (!bin_file) {
|
||||||
LOG_ERROR("Failed to open file: %s", filePath.c_str());
|
LOG_ERROR(TAG, "Failed to open file: %s", filePath.c_str());
|
||||||
LOG_ERROR("Check Servers for the File...");
|
LOG_ERROR(TAG, "Check Servers for the File...");
|
||||||
|
|
||||||
// Try to download the file using FileManager
|
// Try to download the file using FileManager
|
||||||
if (_fileManager) {
|
if (_fileManager) {
|
||||||
@@ -240,20 +286,20 @@ void Player::loadMelodyInRAM() {
|
|||||||
doc["melodys_uid"] = uid;
|
doc["melodys_uid"] = uid;
|
||||||
|
|
||||||
if (!_fileManager->addMelody(doc)) {
|
if (!_fileManager->addMelody(doc)) {
|
||||||
LOG_ERROR("Failed to Download File. Check Internet Connection");
|
LOG_ERROR(TAG, "Failed to Download File. Check Internet Connection");
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
bin_file = SD.open(filePath.c_str(), FILE_READ);
|
bin_file = SD.open(filePath.c_str(), FILE_READ);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("FileManager not available for download");
|
LOG_ERROR(TAG, "FileManager not available for download");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t fileSize = bin_file.size();
|
size_t fileSize = bin_file.size();
|
||||||
if (fileSize % 2 != 0) {
|
if (fileSize % 2 != 0) {
|
||||||
LOG_ERROR("Invalid file size: %u (not a multiple of 2)", fileSize);
|
LOG_ERROR(TAG, "Invalid file size: %u (not a multiple of 2)", fileSize);
|
||||||
bin_file.close();
|
bin_file.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -267,7 +313,7 @@ void Player::loadMelodyInRAM() {
|
|||||||
_melodySteps[i] = (high << 8) | low;
|
_melodySteps[i] = (high << 8) | low;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Melody loaded successfully: %d steps", _melodySteps.size());
|
LOG_INFO(TAG, "Melody loaded successfully from SD: %d steps", _melodySteps.size());
|
||||||
bin_file.close();
|
bin_file.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,7 +343,7 @@ bool Player::timeToStop(unsigned long now) {
|
|||||||
if (isPlaying && !infinite_play) {
|
if (isPlaying && !infinite_play) {
|
||||||
uint64_t stopTime = startTime + total_duration;
|
uint64_t stopTime = startTime + total_duration;
|
||||||
if (now >= stopTime) {
|
if (now >= stopTime) {
|
||||||
LOG_DEBUG("(TimerFunction) Total Run Duration Reached. Soft Stopping.");
|
LOG_DEBUG(TAG, "(TimerFunction) Total Run Duration Reached. Soft Stopping.");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -348,7 +394,7 @@ void Player::setStatus(PlayerStatus newStatus) {
|
|||||||
// 🔥 Use broadcastStatus() to send to BOTH WebSocket AND MQTT clients!
|
// 🔥 Use broadcastStatus() to send to BOTH WebSocket AND MQTT clients!
|
||||||
_commManager->broadcastStatus(doc);
|
_commManager->broadcastStatus(doc);
|
||||||
|
|
||||||
LOG_DEBUG("Status changed: %d → %d, broadcast sent with runTime: %llu",
|
LOG_DEBUG(TAG, "Status changed: %d → %d, broadcast sent with runTime: %llu",
|
||||||
(int)oldStatus, (int)newStatus, projectedRunTime);
|
(int)oldStatus, (int)newStatus, projectedRunTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,7 +404,7 @@ void Player::onMelodyLoopCompleted() {
|
|||||||
if (_status == PlayerStatus::STOPPING) {
|
if (_status == PlayerStatus::STOPPING) {
|
||||||
// We were in soft stop mode, now actually stop
|
// We were in soft stop mode, now actually stop
|
||||||
setStatus(PlayerStatus::STOPPED);
|
setStatus(PlayerStatus::STOPPED);
|
||||||
LOG_DEBUG("Plbck: ACTUAL STOP (melody loop completed)");
|
LOG_DEBUG(TAG, "Plbck: ACTUAL STOP (melody loop completed)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark segment completion time
|
// Mark segment completion time
|
||||||
@@ -369,9 +415,9 @@ void Player::onMelodyLoopCompleted() {
|
|||||||
bool Player::timeToPause(unsigned long now) {
|
bool Player::timeToPause(unsigned long now) {
|
||||||
if (isPlaying && continuous_loop) {
|
if (isPlaying && continuous_loop) {
|
||||||
uint64_t timeToPause = segmentStartTime + segment_duration;
|
uint64_t timeToPause = segmentStartTime + segment_duration;
|
||||||
LOG_DEBUG("PTL: %llu // NOW: %lu", timeToPause, now);
|
LOG_DEBUG(TAG, "PTL: %llu // NOW: %lu", timeToPause, now);
|
||||||
if (now >= timeToPause && !isPaused) {
|
if (now >= timeToPause && !isPaused) {
|
||||||
LOG_DEBUG("(TimerFunction) Segment Duration Reached. Pausing.");
|
LOG_DEBUG(TAG, "(TimerFunction) Segment Duration Reached. Pausing.");
|
||||||
pauseTime = now;
|
pauseTime = now;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -384,7 +430,8 @@ bool Player::timeToResume(unsigned long now) {
|
|||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
uint64_t timeToResume = segmentCmpltTime + pause_duration;
|
uint64_t timeToResume = segmentCmpltTime + pause_duration;
|
||||||
if (now >= timeToResume) {
|
if (now >= timeToResume) {
|
||||||
LOG_DEBUG("(TimerFunction) Pause Duration Reached. Resuming");
|
LOG_DEBUG(TAG, "(TimerFunction) Pause Duration Reached. Resuming");
|
||||||
|
segmentStartTime = now; // Reset segment start time for next cycle
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,35 +468,35 @@ uint64_t Player::calculateProjectedRunTime() const {
|
|||||||
bool Player::isHealthy() const {
|
bool Player::isHealthy() const {
|
||||||
// Check if dependencies are properly set
|
// Check if dependencies are properly set
|
||||||
if (!_commManager) {
|
if (!_commManager) {
|
||||||
LOG_DEBUG("Player: Unhealthy - Communication manager not set");
|
LOG_DEBUG(TAG, "Player: Unhealthy - Communication manager not set");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_fileManager) {
|
if (!_fileManager) {
|
||||||
LOG_DEBUG("Player: Unhealthy - File manager not set");
|
LOG_DEBUG(TAG, "Player: Unhealthy - File manager not set");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_bellEngine) {
|
if (!_bellEngine) {
|
||||||
LOG_DEBUG("Player: Unhealthy - BellEngine not set");
|
LOG_DEBUG(TAG, "Player: Unhealthy - BellEngine not set");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if timer is properly created
|
// Check if timer is properly created
|
||||||
if (_durationTimerHandle == NULL) {
|
if (_durationTimerHandle == NULL) {
|
||||||
LOG_DEBUG("Player: Unhealthy - Duration timer not created");
|
LOG_DEBUG(TAG, "Player: Unhealthy - Duration timer not created");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if timer is actually running
|
// Check if timer is actually running
|
||||||
if (xTimerIsTimerActive(_durationTimerHandle) == pdFALSE) {
|
if (xTimerIsTimerActive(_durationTimerHandle) == pdFALSE) {
|
||||||
LOG_DEBUG("Player: Unhealthy - Duration timer not active");
|
LOG_DEBUG(TAG, "Player: Unhealthy - Duration timer not active");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for consistent playback state
|
// Check for consistent playback state
|
||||||
if (isPlaying && (_status == PlayerStatus::STOPPED)) {
|
if (isPlaying && (_status == PlayerStatus::STOPPED)) {
|
||||||
LOG_DEBUG("Player: Unhealthy - Inconsistent playback state");
|
LOG_DEBUG(TAG, "Player: Unhealthy - Inconsistent playback state");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@
|
|||||||
// ═════════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
class CommunicationRouter; // Command handling and communication
|
class CommunicationRouter; // Command handling and communication
|
||||||
class BellEngine; // High-precision timing engine
|
class BellEngine; // High-precision timing engine
|
||||||
|
class Telemetry; // System telemetry and monitoring
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════════
|
||||||
// PLAYER STATUS ENUMERATION
|
// PLAYER STATUS ENUMERATION
|
||||||
@@ -126,6 +127,18 @@ public:
|
|||||||
*/
|
*/
|
||||||
void setBellEngine(BellEngine* engine) { _bellEngine = engine; }
|
void setBellEngine(BellEngine* engine) { _bellEngine = engine; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set Telemetry reference for strike counter persistence
|
||||||
|
* @param telemetry Pointer to Telemetry instance
|
||||||
|
*/
|
||||||
|
void setTelemetry(Telemetry* telemetry) { _telemetry = telemetry; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set Timekeeper reference for alert coordination
|
||||||
|
* @param timekeeper Pointer to Timekeeper instance
|
||||||
|
*/
|
||||||
|
void setTimekeeper(class Timekeeper* timekeeper) { _timekeeper = timekeeper; }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// MELODY METADATA - Public access for compatibility
|
// MELODY METADATA - Public access for compatibility
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -241,6 +254,8 @@ private:
|
|||||||
CommunicationRouter* _commManager; // 📡 Communication system reference
|
CommunicationRouter* _commManager; // 📡 Communication system reference
|
||||||
FileManager* _fileManager; // 📁 File operations reference
|
FileManager* _fileManager; // 📁 File operations reference
|
||||||
BellEngine* _bellEngine; // 🔥 High-precision timing engine reference
|
BellEngine* _bellEngine; // 🔥 High-precision timing engine reference
|
||||||
|
Telemetry* _telemetry; // 📄 Telemetry system reference
|
||||||
|
class Timekeeper* _timekeeper; // ⏰ Timekeeper reference for alert coordination
|
||||||
|
|
||||||
std::vector<uint16_t> _melodySteps; // 🎵 Melody data owned by Player
|
std::vector<uint16_t> _melodySteps; // 🎵 Melody data owned by Player
|
||||||
TimerHandle_t _durationTimerHandle = NULL; // ⏱️ FreeRTOS timer (saves 4KB vs task!)
|
TimerHandle_t _durationTimerHandle = NULL; // ⏱️ FreeRTOS timer (saves 4KB vs task!)
|
||||||
|
|||||||
227
vesper/src/SDCardMutex/SDCardMutex.hpp
Normal file
227
vesper/src/SDCardMutex/SDCardMutex.hpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
* SDCARDMUTEX.HPP - Thread-Safe SD Card Access Manager
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 🔒 THE SD CARD CONCURRENCY GUARDIAN OF VESPER 🔒
|
||||||
|
*
|
||||||
|
* This singleton class provides thread-safe access to the SD card by managing
|
||||||
|
* a FreeRTOS mutex. All SD card operations MUST acquire this mutex to prevent
|
||||||
|
* concurrent access that can lead to file corruption and write failures.
|
||||||
|
*
|
||||||
|
* CRITICAL: The ESP32 SD library is NOT thread-safe. Without this mutex,
|
||||||
|
* simultaneous SD access from multiple FreeRTOS tasks will cause:
|
||||||
|
* - File corruption
|
||||||
|
* - Write failures
|
||||||
|
* - SD card "not recognized" errors
|
||||||
|
* - Random intermittent failures
|
||||||
|
*
|
||||||
|
* USAGE:
|
||||||
|
*
|
||||||
|
* // Lock before ANY SD operation
|
||||||
|
* if (SDCardMutex::getInstance().lock()) {
|
||||||
|
* File file = SD.open("/myfile.txt", FILE_WRITE);
|
||||||
|
* file.println("data");
|
||||||
|
* file.close();
|
||||||
|
* SDCardMutex::getInstance().unlock();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Or use RAII helper for automatic unlock
|
||||||
|
* {
|
||||||
|
* SDCardLock lock; // Acquires mutex
|
||||||
|
* File file = SD.open("/myfile.txt", FILE_WRITE);
|
||||||
|
* file.println("data");
|
||||||
|
* file.close();
|
||||||
|
* } // Automatically releases mutex when going out of scope
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-01-07
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include "../Logging/Logging.hpp"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Singleton class for thread-safe SD card access
|
||||||
|
*
|
||||||
|
* Manages a global mutex that all SD card operations must acquire
|
||||||
|
* to prevent concurrent access from multiple FreeRTOS tasks.
|
||||||
|
*/
|
||||||
|
class SDCardMutex {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Get the singleton instance
|
||||||
|
* @return Reference to the singleton instance
|
||||||
|
*/
|
||||||
|
static SDCardMutex& getInstance() {
|
||||||
|
static SDCardMutex instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the mutex (call once during setup)
|
||||||
|
* @return true if initialization succeeded, false otherwise
|
||||||
|
*/
|
||||||
|
bool begin() {
|
||||||
|
if (_mutex != NULL) {
|
||||||
|
LOG_WARNING("SDCardMutex", "SDCardMutex already initialized");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
if (_mutex == NULL) {
|
||||||
|
LOG_ERROR("SDCardMutex", "Failed to create SD card mutex!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("SDCardMutex", "SD card mutex initialized");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Acquire the SD card mutex
|
||||||
|
* @param timeoutMs Maximum time to wait for mutex (default: 5 seconds)
|
||||||
|
* @return true if mutex acquired, false if timeout
|
||||||
|
*/
|
||||||
|
bool lock(uint32_t timeoutMs = 5000) {
|
||||||
|
if (_mutex == NULL) {
|
||||||
|
LOG_ERROR("SDCardMutex", "SDCardMutex not initialized!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TickType_t timeout = (timeoutMs == portMAX_DELAY)
|
||||||
|
? portMAX_DELAY
|
||||||
|
: pdMS_TO_TICKS(timeoutMs);
|
||||||
|
|
||||||
|
if (xSemaphoreTake(_mutex, timeout) == pdTRUE) {
|
||||||
|
_lockCount++;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("SDCardMutex", "SD card mutex timeout after %u ms!", timeoutMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release the SD card mutex
|
||||||
|
*/
|
||||||
|
void unlock() {
|
||||||
|
if (_mutex == NULL) {
|
||||||
|
LOG_ERROR("SDCardMutex", "SDCardMutex not initialized!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreGive(_mutex);
|
||||||
|
_unlockCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get mutex lock statistics
|
||||||
|
* @param locks Reference to store lock count
|
||||||
|
* @param unlocks Reference to store unlock count
|
||||||
|
*/
|
||||||
|
void getStats(uint32_t& locks, uint32_t& unlocks) const {
|
||||||
|
locks = _lockCount;
|
||||||
|
unlocks = _unlockCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if mutex is currently locked by THIS task
|
||||||
|
* @return true if current task holds the mutex
|
||||||
|
*/
|
||||||
|
bool isLockedByMe() const {
|
||||||
|
if (_mutex == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return xSemaphoreGetMutexHolder(_mutex) == xTaskGetCurrentTaskHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete copy constructor and assignment operator (singleton)
|
||||||
|
SDCardMutex(const SDCardMutex&) = delete;
|
||||||
|
SDCardMutex& operator=(const SDCardMutex&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
SDCardMutex()
|
||||||
|
: _mutex(NULL)
|
||||||
|
, _lockCount(0)
|
||||||
|
, _unlockCount(0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
~SDCardMutex() {
|
||||||
|
if (_mutex != NULL) {
|
||||||
|
vSemaphoreDelete(_mutex);
|
||||||
|
_mutex = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SemaphoreHandle_t _mutex;
|
||||||
|
uint32_t _lockCount;
|
||||||
|
uint32_t _unlockCount;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief RAII helper class for automatic mutex lock/unlock
|
||||||
|
*
|
||||||
|
* Acquires SD card mutex on construction and releases on destruction.
|
||||||
|
* Use this for automatic cleanup when going out of scope.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* {
|
||||||
|
* SDCardLock lock;
|
||||||
|
* File file = SD.open("/test.txt", FILE_WRITE);
|
||||||
|
* file.println("data");
|
||||||
|
* file.close();
|
||||||
|
* } // Mutex automatically released here
|
||||||
|
*/
|
||||||
|
class SDCardLock {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Constructor - acquires mutex
|
||||||
|
* @param timeoutMs Maximum time to wait for mutex
|
||||||
|
*/
|
||||||
|
explicit SDCardLock(uint32_t timeoutMs = 5000)
|
||||||
|
: _locked(false) {
|
||||||
|
_locked = SDCardMutex::getInstance().lock(timeoutMs);
|
||||||
|
if (!_locked) {
|
||||||
|
LOG_ERROR("SDCardMutex", "SDCardLock failed to acquire mutex!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Destructor - releases mutex
|
||||||
|
*/
|
||||||
|
~SDCardLock() {
|
||||||
|
if (_locked) {
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if lock was successfully acquired
|
||||||
|
* @return true if mutex is locked
|
||||||
|
*/
|
||||||
|
bool isLocked() const {
|
||||||
|
return _locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Explicit conversion to bool for easy checking
|
||||||
|
*/
|
||||||
|
explicit operator bool() const {
|
||||||
|
return _locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete copy constructor and assignment operator
|
||||||
|
SDCardLock(const SDCardLock&) = delete;
|
||||||
|
SDCardLock& operator=(const SDCardLock&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool _locked;
|
||||||
|
};
|
||||||
281
vesper/src/SettingsWebServer/SettingsPage.h
Normal file
281
vesper/src/SettingsWebServer/SettingsPage.h
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* SETTINGSPAGE.H - HTML Content for Settings Web Interface
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* This file contains the HTML/CSS/JavaScript for the VESPER network settings page.
|
||||||
|
* Separated from the main implementation for better maintainability.
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-12-29
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// HTML template for the settings page
|
||||||
|
// Use placeholders for dynamic content:
|
||||||
|
// %MODE_BADGE_CLASS% - "ap" or "station"
|
||||||
|
// %MODE_TEXT% - "AP Mode" or "Station Mode"
|
||||||
|
// %CURRENT_IP% - Current IP address
|
||||||
|
// %DEVICE_UID% - Device unique ID
|
||||||
|
// %FW_VERSION% - Firmware version
|
||||||
|
// %AP_ACTIVE_CLASS% - "active" if in AP mode, "" otherwise
|
||||||
|
// %STATION_ACTIVE_CLASS% - "active" if in station mode, "" otherwise
|
||||||
|
// %SELECTED_MODE% - "ap" or "station"
|
||||||
|
|
||||||
|
const char SETTINGS_PAGE_HTML[] PROGMEM = R"rawliteral(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>VESPER Network Settings</title>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
color: #667eea;
|
||||||
|
font-size: 32px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.status-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.status-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.status-label {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.status-value {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.mode-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.mode-badge.ap {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
}
|
||||||
|
.mode-badge.station {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #388e3c;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.mode-selector {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.mode-option {
|
||||||
|
flex: 1;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px solid #e0e0e0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.mode-option:hover {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: #f0f4ff;
|
||||||
|
}
|
||||||
|
.mode-option.active {
|
||||||
|
border-color: #667eea;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.mode-option h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.mode-option p {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: #f8f9fa;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>VESPER Settings</h1>
|
||||||
|
<p>Network Configuration</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-card">
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Current Mode:</span>
|
||||||
|
<span class="status-value">
|
||||||
|
<span class="mode-badge %MODE_BADGE_CLASS%">
|
||||||
|
%MODE_TEXT%
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">IP Address:</span>
|
||||||
|
<span class="status-value">%CURRENT_IP%</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Device UID:</span>
|
||||||
|
<span class="status-value">%DEVICE_UID%</span>
|
||||||
|
</div>
|
||||||
|
<div class="status-item">
|
||||||
|
<span class="status-label">Firmware:</span>
|
||||||
|
<span class="status-value">v%FW_VERSION%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-title">Select Network Mode</div>
|
||||||
|
|
||||||
|
<div class="mode-selector">
|
||||||
|
<div class="mode-option %AP_ACTIVE_CLASS%" onclick="selectMode('ap')">
|
||||||
|
<h3>AP Mode</h3>
|
||||||
|
<p>Direct Connection<br>192.168.4.1</p>
|
||||||
|
</div>
|
||||||
|
<div class="mode-option %STATION_ACTIVE_CLASS%" onclick="selectMode('station')">
|
||||||
|
<h3>Router Mode</h3>
|
||||||
|
<p>Connect via Router<br>WiFi/Ethernet</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" onclick="applyMode()">Apply & Reboot</button>
|
||||||
|
<button class="btn btn-secondary" onclick="rebootDevice()">Reboot Device</button>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
Device will reboot after applying changes. Make sure to reconnect to the correct network after reboot.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
VESPER Bell Automation System<br>
|
||||||
|
Advanced Bell Systems
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let selectedMode = '%SELECTED_MODE%';
|
||||||
|
|
||||||
|
function selectMode(mode) {
|
||||||
|
selectedMode = mode;
|
||||||
|
document.querySelectorAll('.mode-option').forEach(el => {
|
||||||
|
el.classList.remove('active');
|
||||||
|
});
|
||||||
|
event.target.closest('.mode-option').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyMode() {
|
||||||
|
if (confirm('Device will reboot and switch to ' + (selectedMode === 'ap' ? 'AP Mode' : 'Router Mode') + '. Continue?')) {
|
||||||
|
fetch('/api/set-mode', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
body: 'mode=' + selectedMode
|
||||||
|
}).then(response => {
|
||||||
|
alert('Rebooting... Please wait 10 seconds and reconnect.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function rebootDevice() {
|
||||||
|
if (confirm('Reboot device now?')) {
|
||||||
|
fetch('/api/reboot', {method: 'POST'}).then(() => {
|
||||||
|
alert('Rebooting... Please wait 10 seconds.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)rawliteral";
|
||||||
126
vesper/src/SettingsWebServer/SettingsWebServer.cpp
Normal file
126
vesper/src/SettingsWebServer/SettingsWebServer.cpp
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* SETTINGSWEBSERVER.CPP - Network Mode Settings Web Interface Implementation
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "SettingsWebServer.hpp"
|
||||||
|
|
||||||
|
#define TAG "SettingsWebServer"
|
||||||
|
#include "SettingsPage.h"
|
||||||
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
|
#include "../Networking/Networking.hpp"
|
||||||
|
#include "../Logging/Logging.hpp"
|
||||||
|
|
||||||
|
SettingsWebServer::SettingsWebServer(AsyncWebServer& server,
|
||||||
|
ConfigManager& configManager,
|
||||||
|
Networking& networking)
|
||||||
|
: _server(server)
|
||||||
|
, _configManager(configManager)
|
||||||
|
, _networking(networking) {
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsWebServer::~SettingsWebServer() {
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsWebServer::begin() {
|
||||||
|
LOG_INFO(TAG, "SettingsWebServer - Initializing settings web interface");
|
||||||
|
|
||||||
|
// GET /settings - Main settings page
|
||||||
|
_server.on("/settings", HTTP_GET,
|
||||||
|
[this](AsyncWebServerRequest* request) {
|
||||||
|
handleSettingsPage(request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /api/set-mode - Set network mode
|
||||||
|
_server.on("/api/set-mode", HTTP_POST,
|
||||||
|
[this](AsyncWebServerRequest* request) {
|
||||||
|
handleSetMode(request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// POST /api/reboot - Reboot device
|
||||||
|
_server.on("/api/reboot", HTTP_POST,
|
||||||
|
[this](AsyncWebServerRequest* request) {
|
||||||
|
handleReboot(request);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "SettingsWebServer - Endpoints registered");
|
||||||
|
LOG_INFO(TAG, " GET /settings - Settings page");
|
||||||
|
LOG_INFO(TAG, " POST /api/set-mode - Set network mode");
|
||||||
|
LOG_INFO(TAG, " POST /api/reboot - Reboot device");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsWebServer::handleSettingsPage(AsyncWebServerRequest* request) {
|
||||||
|
LOG_DEBUG(TAG, "SettingsWebServer - Settings page requested");
|
||||||
|
String html = generateSettingsHTML();
|
||||||
|
request->send(200, "text/html", html);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsWebServer::handleSetMode(AsyncWebServerRequest* request) {
|
||||||
|
if (!request->hasParam("mode", true)) {
|
||||||
|
request->send(400, "text/plain", "Missing mode parameter");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mode = request->getParam("mode", true)->value();
|
||||||
|
LOG_INFO(TAG, "SettingsWebServer - Mode change requested: %s", mode.c_str());
|
||||||
|
|
||||||
|
if (mode == "ap") {
|
||||||
|
// Switch to permanent AP mode
|
||||||
|
_configManager.setPermanentAPMode(true);
|
||||||
|
_configManager.saveNetworkConfig();
|
||||||
|
LOG_INFO(TAG, "✅ Permanent AP mode enabled - will activate on reboot");
|
||||||
|
request->send(200, "text/plain", "AP mode enabled. Device will reboot in 3 seconds.");
|
||||||
|
|
||||||
|
// Reboot after 3 seconds
|
||||||
|
delay(3000);
|
||||||
|
ESP.restart();
|
||||||
|
|
||||||
|
} else if (mode == "station") {
|
||||||
|
// Switch to station mode (router mode)
|
||||||
|
_configManager.setPermanentAPMode(false);
|
||||||
|
_configManager.saveNetworkConfig();
|
||||||
|
LOG_INFO(TAG, "✅ Station mode enabled - will activate on reboot");
|
||||||
|
request->send(200, "text/plain", "Station mode enabled. Device will reboot in 3 seconds.");
|
||||||
|
|
||||||
|
// Reboot after 3 seconds
|
||||||
|
delay(3000);
|
||||||
|
ESP.restart();
|
||||||
|
|
||||||
|
} else {
|
||||||
|
request->send(400, "text/plain", "Invalid mode. Use 'ap' or 'station'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SettingsWebServer::handleReboot(AsyncWebServerRequest* request) {
|
||||||
|
LOG_INFO(TAG, "SettingsWebServer - Reboot requested");
|
||||||
|
request->send(200, "text/plain", "Rebooting device in 2 seconds...");
|
||||||
|
|
||||||
|
delay(2000);
|
||||||
|
ESP.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
String SettingsWebServer::generateSettingsHTML() {
|
||||||
|
bool isAPMode = _networking.isInAPMode();
|
||||||
|
String currentIP = _networking.getLocalIP();
|
||||||
|
String deviceUID = _configManager.getDeviceUID();
|
||||||
|
String fwVersion = _configManager.getFwVersion();
|
||||||
|
|
||||||
|
// Load HTML template from PROGMEM
|
||||||
|
String html = String(FPSTR(SETTINGS_PAGE_HTML));
|
||||||
|
|
||||||
|
// Replace placeholders with dynamic values
|
||||||
|
html.replace("%MODE_BADGE_CLASS%", isAPMode ? "ap" : "station");
|
||||||
|
html.replace("%MODE_TEXT%", isAPMode ? "AP Mode" : "Station Mode");
|
||||||
|
html.replace("%CURRENT_IP%", currentIP);
|
||||||
|
html.replace("%DEVICE_UID%", deviceUID);
|
||||||
|
html.replace("%FW_VERSION%", fwVersion);
|
||||||
|
html.replace("%AP_ACTIVE_CLASS%", isAPMode ? "active" : "");
|
||||||
|
html.replace("%STATION_ACTIVE_CLASS%", !isAPMode ? "active" : "");
|
||||||
|
html.replace("%SELECTED_MODE%", isAPMode ? "ap" : "station");
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
66
vesper/src/SettingsWebServer/SettingsWebServer.hpp
Normal file
66
vesper/src/SettingsWebServer/SettingsWebServer.hpp
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* SETTINGSWEBSERVER.HPP - Network Mode Settings Web Interface
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 🌐 SETTINGS WEB INTERFACE FOR VESPER 🌐
|
||||||
|
*
|
||||||
|
* Provides web interface for switching between AP and Station modes:
|
||||||
|
* • Accessible at http://192.168.4.1/settings (AP mode)
|
||||||
|
* • Accessible at http://{device-ip}/settings (Station mode)
|
||||||
|
* • Toggle between AP mode and Router mode
|
||||||
|
* • Configure WiFi credentials for router connection
|
||||||
|
* • Reboot device with new settings
|
||||||
|
*
|
||||||
|
* 🏗️ ARCHITECTURE:
|
||||||
|
* • Uses AsyncWebServer for non-blocking operation
|
||||||
|
* • HTML interface with toggle switch
|
||||||
|
* • Updates ConfigManager and triggers reboot
|
||||||
|
* • Works in both AP and Station modes
|
||||||
|
*
|
||||||
|
* 📡 ENDPOINTS:
|
||||||
|
* GET /settings - Settings page with mode toggle
|
||||||
|
* POST /api/set-mode - Set network mode (AP or STA)
|
||||||
|
* POST /api/reboot - Reboot device
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-12-28
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ESPAsyncWebServer.h>
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class ConfigManager;
|
||||||
|
class Networking;
|
||||||
|
|
||||||
|
class SettingsWebServer {
|
||||||
|
public:
|
||||||
|
explicit SettingsWebServer(AsyncWebServer& server,
|
||||||
|
ConfigManager& configManager,
|
||||||
|
Networking& networking);
|
||||||
|
~SettingsWebServer();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize settings web server and register endpoints
|
||||||
|
*/
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Dependencies
|
||||||
|
AsyncWebServer& _server;
|
||||||
|
ConfigManager& _configManager;
|
||||||
|
Networking& _networking;
|
||||||
|
|
||||||
|
// Endpoint handlers
|
||||||
|
void handleSettingsPage(AsyncWebServerRequest* request);
|
||||||
|
void handleSetMode(AsyncWebServerRequest* request);
|
||||||
|
void handleReboot(AsyncWebServerRequest* request);
|
||||||
|
|
||||||
|
// HTML generation
|
||||||
|
String generateSettingsHTML();
|
||||||
|
};
|
||||||
@@ -1,34 +1,45 @@
|
|||||||
#include "Telemetry.hpp"
|
#include "Telemetry.hpp"
|
||||||
|
|
||||||
|
#define TAG "Telemetry"
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
void Telemetry::begin() {
|
void Telemetry::begin() {
|
||||||
// Initialize arrays
|
// Initialize arrays
|
||||||
for (uint8_t i = 0; i < 16; i++) {
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
strikeCounters[i] = 0;
|
strikeCounters[i] = 0;
|
||||||
bellLoad[i] = 0;
|
bellLoad[i] = 0;
|
||||||
bellMaxLoad[i] = 60; // Default max load
|
bellMaxLoad[i] = 200; // Default max load
|
||||||
}
|
}
|
||||||
|
|
||||||
coolingActive = false;
|
coolingActive = false;
|
||||||
|
|
||||||
|
// Load strike counters from SD if available
|
||||||
|
loadStrikeCounters();
|
||||||
|
|
||||||
// Create the telemetry task
|
// Create the telemetry task
|
||||||
xTaskCreatePinnedToCore(telemetryTask, "TelemetryTask", 4096, this, 2, &telemetryTaskHandle, 1);
|
xTaskCreatePinnedToCore(telemetryTask, "TelemetryTask", 4096, this, 2, &telemetryTaskHandle, 1);
|
||||||
|
|
||||||
LOG_INFO("Telemetry initialized");
|
LOG_INFO(TAG, "Telemetry initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Telemetry::setPlayerReference(bool* isPlayingPtr) {
|
void Telemetry::setPlayerReference(bool* isPlayingPtr) {
|
||||||
playerIsPlayingPtr = isPlayingPtr;
|
playerIsPlayingPtr = isPlayingPtr;
|
||||||
LOG_DEBUG("Player reference set");
|
LOG_DEBUG(TAG, "Player reference set");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Telemetry::setFileManager(FileManager* fm) {
|
||||||
|
fileManager = fm;
|
||||||
|
LOG_DEBUG(TAG, "FileManager reference set");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Telemetry::setForceStopCallback(void (*callback)()) {
|
void Telemetry::setForceStopCallback(void (*callback)()) {
|
||||||
forceStopCallback = callback;
|
forceStopCallback = callback;
|
||||||
LOG_DEBUG("Force stop callback set");
|
LOG_DEBUG(TAG, "Force stop callback set");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Telemetry::recordBellStrike(uint8_t bellIndex) {
|
void Telemetry::recordBellStrike(uint8_t bellIndex) {
|
||||||
if (bellIndex >= 16) {
|
if (bellIndex >= 16) {
|
||||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
LOG_ERROR(TAG, "Invalid bell index: %d", bellIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +54,7 @@ void Telemetry::recordBellStrike(uint8_t bellIndex) {
|
|||||||
|
|
||||||
uint32_t Telemetry::getStrikeCount(uint8_t bellIndex) {
|
uint32_t Telemetry::getStrikeCount(uint8_t bellIndex) {
|
||||||
if (bellIndex >= 16) {
|
if (bellIndex >= 16) {
|
||||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
LOG_ERROR(TAG, "Invalid bell index: %d", bellIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return strikeCounters[bellIndex];
|
return strikeCounters[bellIndex];
|
||||||
@@ -56,12 +67,12 @@ void Telemetry::resetStrikeCounters() {
|
|||||||
}
|
}
|
||||||
portEXIT_CRITICAL(&telemetrySpinlock);
|
portEXIT_CRITICAL(&telemetrySpinlock);
|
||||||
|
|
||||||
LOG_WARNING("Strike counters reset by user");
|
LOG_WARNING(TAG, "Strike counters reset by user");
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t Telemetry::getBellLoad(uint8_t bellIndex) {
|
uint16_t Telemetry::getBellLoad(uint8_t bellIndex) {
|
||||||
if (bellIndex >= 16) {
|
if (bellIndex >= 16) {
|
||||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
LOG_ERROR(TAG, "Invalid bell index: %d", bellIndex);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return bellLoad[bellIndex];
|
return bellLoad[bellIndex];
|
||||||
@@ -69,17 +80,17 @@ uint16_t Telemetry::getBellLoad(uint8_t bellIndex) {
|
|||||||
|
|
||||||
void Telemetry::setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad) {
|
void Telemetry::setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad) {
|
||||||
if (bellIndex >= 16) {
|
if (bellIndex >= 16) {
|
||||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
LOG_ERROR(TAG, "Invalid bell index: %d", bellIndex);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bellMaxLoad[bellIndex] = maxLoad;
|
bellMaxLoad[bellIndex] = maxLoad;
|
||||||
LOG_INFO("Bell %d max load set to %d", bellIndex, maxLoad);
|
LOG_INFO(TAG, "Bell %d max load set to %d", bellIndex, maxLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Telemetry::isOverloaded(uint8_t bellIndex) {
|
bool Telemetry::isOverloaded(uint8_t bellIndex) {
|
||||||
if (bellIndex >= 16) {
|
if (bellIndex >= 16) {
|
||||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
LOG_ERROR(TAG, "Invalid bell index: %d", bellIndex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return bellLoad[bellIndex] > bellMaxLoad[bellIndex];
|
return bellLoad[bellIndex] > bellMaxLoad[bellIndex];
|
||||||
@@ -91,12 +102,12 @@ bool Telemetry::isCoolingActive() {
|
|||||||
|
|
||||||
void Telemetry::logTemperature(float temperature) {
|
void Telemetry::logTemperature(float temperature) {
|
||||||
// Future implementation for temperature logging
|
// Future implementation for temperature logging
|
||||||
LOG_INFO("Temperature: %.2f°C", temperature);
|
LOG_INFO(TAG, "Temperature: %.2f°C", temperature);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Telemetry::logVibration(float vibration) {
|
void Telemetry::logVibration(float vibration) {
|
||||||
// Future implementation for vibration logging
|
// Future implementation for vibration logging
|
||||||
LOG_INFO("Vibration: %.2f", vibration);
|
LOG_INFO(TAG, "Vibration: %.2f", vibration);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Telemetry::checkBellLoads() {
|
void Telemetry::checkBellLoads() {
|
||||||
@@ -123,7 +134,7 @@ void Telemetry::checkBellLoads() {
|
|||||||
|
|
||||||
// Critical overload - protection kicks in
|
// Critical overload - protection kicks in
|
||||||
if (bellLoad[i] > bellMaxLoad[i]) {
|
if (bellLoad[i] > bellMaxLoad[i]) {
|
||||||
LOG_ERROR("Bell %d OVERLOADED! load=%d max=%d",
|
LOG_ERROR(TAG, "Bell %d OVERLOADED! load=%d max=%d",
|
||||||
i, bellLoad[i], bellMaxLoad[i]);
|
i, bellLoad[i], bellMaxLoad[i]);
|
||||||
|
|
||||||
criticalBells.push_back(i);
|
criticalBells.push_back(i);
|
||||||
@@ -132,7 +143,7 @@ void Telemetry::checkBellLoads() {
|
|||||||
|
|
||||||
} else if (bellLoad[i] > criticalThreshold) {
|
} else if (bellLoad[i] > criticalThreshold) {
|
||||||
// Critical warning - approaching overload
|
// Critical warning - approaching overload
|
||||||
LOG_WARNING("Bell %d approaching overload! load=%d (critical threshold=%d)",
|
LOG_WARNING(TAG, "Bell %d approaching overload! load=%d (critical threshold=%d)",
|
||||||
i, bellLoad[i], criticalThreshold);
|
i, bellLoad[i], criticalThreshold);
|
||||||
|
|
||||||
criticalBells.push_back(i);
|
criticalBells.push_back(i);
|
||||||
@@ -140,7 +151,7 @@ void Telemetry::checkBellLoads() {
|
|||||||
|
|
||||||
} else if (bellLoad[i] > warningThreshold) {
|
} else if (bellLoad[i] > warningThreshold) {
|
||||||
// Warning - moderate load
|
// Warning - moderate load
|
||||||
LOG_INFO("Bell %d moderate load warning! load=%d (warning threshold=%d)",
|
LOG_INFO(TAG, "Bell %d moderate load warning! load=%d (warning threshold=%d)",
|
||||||
i, bellLoad[i], warningThreshold);
|
i, bellLoad[i], warningThreshold);
|
||||||
|
|
||||||
warningBells.push_back(i);
|
warningBells.push_back(i);
|
||||||
@@ -160,9 +171,11 @@ void Telemetry::checkBellLoads() {
|
|||||||
void Telemetry::telemetryTask(void* parameter) {
|
void Telemetry::telemetryTask(void* parameter) {
|
||||||
Telemetry* telemetry = static_cast<Telemetry*>(parameter);
|
Telemetry* telemetry = static_cast<Telemetry*>(parameter);
|
||||||
|
|
||||||
LOG_INFO("Telemetry task started");
|
LOG_INFO(TAG, "Telemetry task started");
|
||||||
|
|
||||||
while(1) {
|
while(1) {
|
||||||
|
// Skip processing if paused (OTA freeze mode)
|
||||||
|
if (!telemetry->isPaused) {
|
||||||
// Only run if player is playing OR we're still cooling
|
// Only run if player is playing OR we're still cooling
|
||||||
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
|
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
|
||||||
*(telemetry->playerIsPlayingPtr) : false;
|
*(telemetry->playerIsPlayingPtr) : false;
|
||||||
@@ -170,11 +183,68 @@ void Telemetry::telemetryTask(void* parameter) {
|
|||||||
if (isPlaying || telemetry->coolingActive) {
|
if (isPlaying || telemetry->coolingActive) {
|
||||||
telemetry->checkBellLoads();
|
telemetry->checkBellLoads();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(1000)); // Run every 1s
|
vTaskDelay(pdMS_TO_TICKS(1000)); // Run every 1s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// STRIKE COUNTER PERSISTENCE
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
void Telemetry::saveStrikeCounters() {
|
||||||
|
if (!fileManager) {
|
||||||
|
LOG_WARNING(TAG, "Cannot save strike counters: FileManager not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticJsonDocument<512> doc;
|
||||||
|
JsonArray counters = doc.createNestedArray("strikeCounters");
|
||||||
|
|
||||||
|
// Thread-safe read of strike counters
|
||||||
|
portENTER_CRITICAL(&telemetrySpinlock);
|
||||||
|
for (uint8_t i = 0; i < 16; i++) {
|
||||||
|
counters.add(strikeCounters[i]);
|
||||||
|
}
|
||||||
|
portEXIT_CRITICAL(&telemetrySpinlock);
|
||||||
|
|
||||||
|
if (fileManager->writeJsonFile("/telemetry_data.json", doc)) {
|
||||||
|
LOG_INFO(TAG, "Strike counters saved to SD card");
|
||||||
|
} else {
|
||||||
|
LOG_ERROR(TAG, "Failed to save strike counters to SD card");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Telemetry::loadStrikeCounters() {
|
||||||
|
if (!fileManager) {
|
||||||
|
LOG_WARNING(TAG, "Cannot load strike counters: FileManager not set");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StaticJsonDocument<512> doc;
|
||||||
|
|
||||||
|
if (!fileManager->readJsonFile("/telemetry_data.json", doc)) {
|
||||||
|
LOG_INFO(TAG, "No previous strike counter data found, starting fresh");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray counters = doc["strikeCounters"];
|
||||||
|
if (counters.isNull()) {
|
||||||
|
LOG_WARNING(TAG, "Invalid telemetry data format");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread-safe write of strike counters
|
||||||
|
portENTER_CRITICAL(&telemetrySpinlock);
|
||||||
|
for (uint8_t i = 0; i < 16 && i < counters.size(); i++) {
|
||||||
|
strikeCounters[i] = counters[i].as<uint32_t>();
|
||||||
|
}
|
||||||
|
portEXIT_CRITICAL(&telemetrySpinlock);
|
||||||
|
|
||||||
|
LOG_INFO(TAG, "Strike counters loaded from SD card");
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
// HEALTH CHECK IMPLEMENTATION
|
// HEALTH CHECK IMPLEMENTATION
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -182,20 +252,20 @@ void Telemetry::telemetryTask(void* parameter) {
|
|||||||
bool Telemetry::isHealthy() const {
|
bool Telemetry::isHealthy() const {
|
||||||
// Check if telemetry task is created and running
|
// Check if telemetry task is created and running
|
||||||
if (telemetryTaskHandle == NULL) {
|
if (telemetryTaskHandle == NULL) {
|
||||||
LOG_DEBUG("Telemetry: Unhealthy - Task not created");
|
LOG_DEBUG(TAG, "Telemetry: Unhealthy - Task not created");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if task is still alive
|
// Check if task is still alive
|
||||||
eTaskState taskState = eTaskGetState(telemetryTaskHandle);
|
eTaskState taskState = eTaskGetState(telemetryTaskHandle);
|
||||||
if (taskState == eDeleted || taskState == eInvalid) {
|
if (taskState == eDeleted || taskState == eInvalid) {
|
||||||
LOG_DEBUG("Telemetry: Unhealthy - Task deleted or invalid");
|
LOG_DEBUG(TAG, "Telemetry: Unhealthy - Task deleted or invalid");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if player reference is set
|
// Check if player reference is set
|
||||||
if (playerIsPlayingPtr == nullptr) {
|
if (playerIsPlayingPtr == nullptr) {
|
||||||
LOG_DEBUG("Telemetry: Unhealthy - Player reference not set");
|
LOG_DEBUG(TAG, "Telemetry: Unhealthy - Player reference not set");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +279,7 @@ bool Telemetry::isHealthy() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasCriticalOverload) {
|
if (hasCriticalOverload) {
|
||||||
LOG_DEBUG("Telemetry: Unhealthy - Critical bell overload detected");
|
LOG_DEBUG(TAG, "Telemetry: Unhealthy - Critical bell overload detected");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
|
#include "../FileManager/FileManager.hpp"
|
||||||
|
|
||||||
class Telemetry {
|
class Telemetry {
|
||||||
private:
|
private:
|
||||||
@@ -71,16 +72,21 @@ private:
|
|||||||
|
|
||||||
// External references (to be set via setters)
|
// External references (to be set via setters)
|
||||||
bool* playerIsPlayingPtr = nullptr;
|
bool* playerIsPlayingPtr = nullptr;
|
||||||
|
FileManager* fileManager = nullptr;
|
||||||
|
|
||||||
// Spinlock for critical sections
|
// Spinlock for critical sections
|
||||||
portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED;
|
portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED;
|
||||||
|
|
||||||
|
// Pause flag for OTA freeze mode
|
||||||
|
volatile bool isPaused = false;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
// Initialization
|
// Initialization
|
||||||
void begin();
|
void begin();
|
||||||
|
|
||||||
// Set external references
|
// Set external references
|
||||||
void setPlayerReference(bool* isPlayingPtr);
|
void setPlayerReference(bool* isPlayingPtr);
|
||||||
|
void setFileManager(FileManager* fm);
|
||||||
|
|
||||||
// Bell strike handling (call this on every hammer strike)
|
// Bell strike handling (call this on every hammer strike)
|
||||||
void recordBellStrike(uint8_t bellIndex);
|
void recordBellStrike(uint8_t bellIndex);
|
||||||
@@ -89,6 +95,10 @@ public:
|
|||||||
uint32_t getStrikeCount(uint8_t bellIndex);
|
uint32_t getStrikeCount(uint8_t bellIndex);
|
||||||
void resetStrikeCounters(); // User-requested reset
|
void resetStrikeCounters(); // User-requested reset
|
||||||
|
|
||||||
|
// Persistence methods
|
||||||
|
void saveStrikeCounters();
|
||||||
|
void loadStrikeCounters();
|
||||||
|
|
||||||
// Bell load management
|
// Bell load management
|
||||||
uint16_t getBellLoad(uint8_t bellIndex);
|
uint16_t getBellLoad(uint8_t bellIndex);
|
||||||
void setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad);
|
void setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad);
|
||||||
@@ -102,6 +112,10 @@ public:
|
|||||||
// Force stop callback (to be set by main application)
|
// Force stop callback (to be set by main application)
|
||||||
void setForceStopCallback(void (*callback)());
|
void setForceStopCallback(void (*callback)());
|
||||||
|
|
||||||
|
// Pause/Resume for OTA freeze mode (stops SD writes during firmware update)
|
||||||
|
void pause() { isPaused = true; }
|
||||||
|
void resume() { isPaused = false; }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// HEALTH CHECK METHOD
|
// HEALTH CHECK METHOD
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
#include "TimeKeeper.hpp"
|
#include "TimeKeeper.hpp"
|
||||||
|
|
||||||
|
#define TAG "TimeKeeper"
|
||||||
#include "../OutputManager/OutputManager.hpp"
|
#include "../OutputManager/OutputManager.hpp"
|
||||||
#include "../ConfigManager/ConfigManager.hpp"
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../Networking/Networking.hpp"
|
#include "../Networking/Networking.hpp"
|
||||||
|
#include "../Player/Player.hpp" // 🔥 Include for Player class definition
|
||||||
#include "SD.h"
|
#include "SD.h"
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
void Timekeeper::begin() {
|
void Timekeeper::begin() {
|
||||||
LOG_INFO("Timekeeper initialized - clock outputs managed by ConfigManager");
|
LOG_INFO(TAG, "Timekeeper initialized - clock outputs managed by ConfigManager");
|
||||||
|
|
||||||
// Initialize RTC
|
// Initialize RTC
|
||||||
if (!rtc.begin()) {
|
if (!rtc.begin()) {
|
||||||
LOG_ERROR("Couldn't find RTC");
|
LOG_ERROR(TAG, "Couldn't find RTC");
|
||||||
// Continue anyway, but log the error
|
// Continue anyway, but log the error
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("RTC initialized successfully");
|
LOG_INFO(TAG, "RTC initialized successfully");
|
||||||
|
|
||||||
// Check if RTC lost power
|
// Check if RTC lost power
|
||||||
if (!rtc.isrunning()) {
|
if (!rtc.isrunning()) {
|
||||||
LOG_WARNING("RTC is NOT running! Setting time...");
|
LOG_WARNING(TAG, "RTC is NOT running! Setting time...");
|
||||||
// Set to compile time as fallback
|
// Set to compile time as fallback
|
||||||
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
|
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
|
||||||
}
|
}
|
||||||
@@ -29,46 +32,59 @@ void Timekeeper::begin() {
|
|||||||
// Create SINGLE consolidated task (saves 8KB RAM!)
|
// Create SINGLE consolidated task (saves 8KB RAM!)
|
||||||
xTaskCreatePinnedToCore(mainTimekeeperTask, "TimeKeeper", 4096, this, 2, &mainTaskHandle, 1);
|
xTaskCreatePinnedToCore(mainTimekeeperTask, "TimeKeeper", 4096, this, 2, &mainTaskHandle, 1);
|
||||||
|
|
||||||
LOG_INFO("TimeKeeper initialized with SIMPLE sync approach (like your Arduino code)");
|
LOG_INFO(TAG, "TimeKeeper initialized with SIMPLE sync approach (like your Arduino code)");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::setOutputManager(OutputManager* outputManager) {
|
void Timekeeper::setOutputManager(OutputManager* outputManager) {
|
||||||
_outputManager = outputManager;
|
_outputManager = outputManager;
|
||||||
LOG_INFO("Timekeeper connected to OutputManager - CLEAN ARCHITECTURE!");
|
LOG_INFO(TAG, "Timekeeper connected to OutputManager - CLEAN ARCHITECTURE!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::setConfigManager(ConfigManager* configManager) {
|
void Timekeeper::setConfigManager(ConfigManager* configManager) {
|
||||||
_configManager = configManager;
|
_configManager = configManager;
|
||||||
LOG_INFO("Timekeeper connected to ConfigManager");
|
LOG_INFO(TAG, "Timekeeper connected to ConfigManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::setNetworking(Networking* networking) {
|
void Timekeeper::setNetworking(Networking* networking) {
|
||||||
_networking = networking;
|
_networking = networking;
|
||||||
LOG_INFO("Timekeeper connected to Networking");
|
LOG_INFO(TAG, "Timekeeper connected to Networking");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timekeeper::setPlayer(Player* player) {
|
||||||
|
_player = player;
|
||||||
|
LOG_INFO(TAG, "Timekeeper connected to Player for playback coordination");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timekeeper::interruptActiveAlert() {
|
||||||
|
if (alertInProgress.load()) {
|
||||||
|
LOG_INFO(TAG, "⚡ ALERT INTERRUPTED by user playback - marking as complete");
|
||||||
|
alertInProgress.store(false);
|
||||||
|
// Alert will stop naturally on next check in fireAlertBell loop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) {
|
void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) {
|
||||||
relayWriteFunc = func;
|
relayWriteFunc = func;
|
||||||
LOG_WARNING("Using LEGACY relay function - consider upgrading to OutputManager");
|
LOG_WARNING(TAG, "Using LEGACY relay function - consider upgrading to OutputManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::setClockOutputs(int relay1, int relay2) {
|
void Timekeeper::setClockOutputs(int relay1, int relay2) {
|
||||||
LOG_WARNING("⚠️ setClockOutputs() is DEPRECATED! Use ConfigManager.setClockOutput1/2() instead");
|
LOG_WARNING(TAG, "⚠️ setClockOutputs() is DEPRECATED! Use ConfigManager.setClockOutput1/2() instead");
|
||||||
LOG_WARNING("⚠️ Clock outputs should be configured via MQTT/WebSocket commands");
|
LOG_WARNING(TAG, "⚠️ Clock outputs should be configured via MQTT/WebSocket commands");
|
||||||
|
|
||||||
// For backward compatibility, still set the config if ConfigManager is available
|
// For backward compatibility, still set the config if ConfigManager is available
|
||||||
if (_configManager) {
|
if (_configManager) {
|
||||||
_configManager->setClockOutput1(relay1);
|
_configManager->setClockOutput1(relay1);
|
||||||
_configManager->setClockOutput2(relay2);
|
_configManager->setClockOutput2(relay2);
|
||||||
LOG_INFO("Clock outputs updated via legacy method: C1=%d, C2=%d", relay1, relay2);
|
LOG_INFO(TAG, "Clock outputs updated via legacy method: C1=%d, C2=%d", relay1, relay2);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("ConfigManager not available - cannot set clock outputs");
|
LOG_ERROR(TAG, "ConfigManager not available - cannot set clock outputs");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::setTime(unsigned long timestamp) {
|
void Timekeeper::setTime(unsigned long timestamp) {
|
||||||
if (!rtc.begin()) {
|
if (!rtc.begin()) {
|
||||||
LOG_ERROR("RTC not available - cannot set time");
|
LOG_ERROR(TAG, "RTC not available - cannot set time");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +101,7 @@ void Timekeeper::setTime(unsigned long timestamp) {
|
|||||||
// Set the RTC with local time
|
// Set the RTC with local time
|
||||||
rtc.adjust(newTime);
|
rtc.adjust(newTime);
|
||||||
|
|
||||||
LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (UTC timestamp: %lu + %ld offset = %lu)",
|
LOG_INFO(TAG, "RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (UTC timestamp: %lu + %ld offset = %lu)",
|
||||||
newTime.year(), newTime.month(), newTime.day(),
|
newTime.year(), newTime.month(), newTime.day(),
|
||||||
newTime.hour(), newTime.minute(), newTime.second(),
|
newTime.hour(), newTime.minute(), newTime.second(),
|
||||||
timestamp, totalOffset, localTimestamp);
|
timestamp, totalOffset, localTimestamp);
|
||||||
@@ -96,7 +112,7 @@ void Timekeeper::setTime(unsigned long timestamp) {
|
|||||||
|
|
||||||
void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) {
|
void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) {
|
||||||
if (!rtc.begin()) {
|
if (!rtc.begin()) {
|
||||||
LOG_ERROR("RTC not available - cannot set time");
|
LOG_ERROR(TAG, "RTC not available - cannot set time");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +122,7 @@ void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) {
|
|||||||
// Set the RTC with local time
|
// Set the RTC with local time
|
||||||
rtc.adjust(newTime);
|
rtc.adjust(newTime);
|
||||||
|
|
||||||
LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (local timestamp: %lu)",
|
LOG_INFO(TAG, "RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (local timestamp: %lu)",
|
||||||
newTime.year(), newTime.month(), newTime.day(),
|
newTime.year(), newTime.month(), newTime.day(),
|
||||||
newTime.hour(), newTime.minute(), newTime.second(),
|
newTime.hour(), newTime.minute(), newTime.second(),
|
||||||
localTimestamp);
|
localTimestamp);
|
||||||
@@ -117,14 +133,14 @@ void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) {
|
|||||||
|
|
||||||
unsigned long Timekeeper::getTime() {
|
unsigned long Timekeeper::getTime() {
|
||||||
if (!rtc.isrunning()) {
|
if (!rtc.isrunning()) {
|
||||||
LOG_ERROR("RTC not running - cannot get time");
|
LOG_ERROR(TAG, "RTC not running - cannot get time");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime now = rtc.now();
|
DateTime now = rtc.now();
|
||||||
unsigned long timestamp = now.unixtime();
|
unsigned long timestamp = now.unixtime();
|
||||||
|
|
||||||
LOG_DEBUG("Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)",
|
LOG_DEBUG(TAG, "Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)",
|
||||||
now.year(), now.month(), now.day(),
|
now.year(), now.month(), now.day(),
|
||||||
now.hour(), now.minute(), now.second(),
|
now.hour(), now.minute(), now.second(),
|
||||||
timestamp);
|
timestamp);
|
||||||
@@ -135,16 +151,16 @@ unsigned long Timekeeper::getTime() {
|
|||||||
void Timekeeper::syncTimeWithNTP() {
|
void Timekeeper::syncTimeWithNTP() {
|
||||||
// Check if we have network connection and required dependencies
|
// Check if we have network connection and required dependencies
|
||||||
if (!_networking || !_configManager) {
|
if (!_networking || !_configManager) {
|
||||||
LOG_ERROR("Cannot sync time: Networking or ConfigManager not set");
|
LOG_WARNING(TAG, "Cannot sync time: Networking or ConfigManager not set - using RTC time");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_networking->isConnected()) {
|
if (!_networking->isConnected()) {
|
||||||
LOG_WARNING("Cannot sync time: No network connection");
|
LOG_INFO(TAG, "No network connection - skipping NTP sync, using RTC time");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Syncing time with NTP server...");
|
LOG_INFO(TAG, "⏰ Starting non-blocking NTP sync...");
|
||||||
|
|
||||||
// Get config from ConfigManager
|
// Get config from ConfigManager
|
||||||
auto& timeConfig = _configManager->getTimeConfig();
|
auto& timeConfig = _configManager->getTimeConfig();
|
||||||
@@ -152,30 +168,23 @@ void Timekeeper::syncTimeWithNTP() {
|
|||||||
// Configure NTP with settings from config
|
// Configure NTP with settings from config
|
||||||
configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str());
|
configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str());
|
||||||
|
|
||||||
// Wait for time sync with timeout
|
// 🔥 NON-BLOCKING: Try to get time with reasonable timeout for network response
|
||||||
struct tm timeInfo;
|
struct tm timeInfo;
|
||||||
int attempts = 0;
|
if (getLocalTime(&timeInfo, 5000)) { // 5 second timeout for NTP response
|
||||||
while (!getLocalTime(&timeInfo) && attempts < 10) {
|
// Success! Update RTC with synchronized time
|
||||||
LOG_DEBUG("Waiting for NTP sync... attempt %d", attempts + 1);
|
|
||||||
delay(1000);
|
|
||||||
attempts++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attempts >= 10) {
|
|
||||||
LOG_ERROR("Failed to obtain time from NTP server after 10 attempts");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update RTC with synchronized time
|
|
||||||
rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
|
rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
|
||||||
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
|
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
|
||||||
|
|
||||||
LOG_INFO("Time synced successfully: %04d-%02d-%02d %02d:%02d:%02d",
|
LOG_INFO(TAG, "✅ NTP sync successful: %04d-%02d-%02d %02d:%02d:%02d",
|
||||||
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
|
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
|
||||||
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
|
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
|
||||||
|
|
||||||
// Reload today's events since the time might have changed significantly
|
// Reload today's events since the time might have changed significantly
|
||||||
loadTodaysEvents();
|
loadTodaysEvents();
|
||||||
|
} else {
|
||||||
|
// No internet or NTP server unreachable - this is NORMAL for local networks
|
||||||
|
LOG_INFO(TAG, "⚠️ NTP sync skipped (no internet) - using RTC time. This is normal for local networks.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -184,7 +193,7 @@ void Timekeeper::syncTimeWithNTP() {
|
|||||||
|
|
||||||
void Timekeeper::mainTimekeeperTask(void* parameter) {
|
void Timekeeper::mainTimekeeperTask(void* parameter) {
|
||||||
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
|
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
|
||||||
LOG_INFO("🕒 SIMPLE TimeKeeper task started - based on your Arduino code approach");
|
LOG_INFO(TAG, "🕒 SIMPLE TimeKeeper task started - based on your Arduino code approach");
|
||||||
|
|
||||||
unsigned long lastRtcCheck = 0;
|
unsigned long lastRtcCheck = 0;
|
||||||
unsigned long lastScheduleCheck = 0;
|
unsigned long lastScheduleCheck = 0;
|
||||||
@@ -216,19 +225,19 @@ void Timekeeper::mainTimekeeperTask(void* parameter) {
|
|||||||
if (keeper->rtc.isrunning()) {
|
if (keeper->rtc.isrunning()) {
|
||||||
// Check for midnight - reload events for new day
|
// Check for midnight - reload events for new day
|
||||||
if (rtcNow.hour() == 0 && rtcNow.minute() == 0 && rtcNow.second() < 10) {
|
if (rtcNow.hour() == 0 && rtcNow.minute() == 0 && rtcNow.second() < 10) {
|
||||||
LOG_INFO("🌙 Midnight detected - reloading events");
|
LOG_INFO(TAG, "🌙 Midnight detected - reloading events");
|
||||||
keeper->loadTodaysEvents();
|
keeper->loadTodaysEvents();
|
||||||
keeper->loadNextDayEvents();
|
keeper->loadNextDayEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hourly maintenance
|
// Hourly maintenance
|
||||||
if (rtcNow.minute() == 0 && rtcNow.second() < 10) {
|
if (rtcNow.minute() == 0 && rtcNow.second() < 10) {
|
||||||
LOG_DEBUG("🕐 Hourly check at %02d:00", rtcNow.hour());
|
LOG_DEBUG(TAG, "🕐 Hourly check at %02d:00", rtcNow.hour());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
static uint8_t rtcWarningCounter = 0;
|
static uint8_t rtcWarningCounter = 0;
|
||||||
if (rtcWarningCounter++ % 6 == 0) { // Log every minute
|
if (rtcWarningCounter++ % 6 == 0) { // Log every minute
|
||||||
LOG_WARNING("⚠️ RTC not running!");
|
LOG_WARNING(TAG, "⚠️ RTC not running!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastRtcCheck = now;
|
lastRtcCheck = now;
|
||||||
@@ -267,19 +276,19 @@ void Timekeeper::checkAndSyncPhysicalClock() {
|
|||||||
// Calculate time difference (your exact logic!)
|
// Calculate time difference (your exact logic!)
|
||||||
int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute);
|
int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute);
|
||||||
|
|
||||||
LOG_VERBOSE("⏰ CHECK: Real time %02d:%02d vs Physical %02d:%02d - DIFF: %d mins",
|
LOG_VERBOSE(TAG, "⏰ CHECK: Real time %02d:%02d vs Physical %02d:%02d - DIFF: %d mins",
|
||||||
realHour, realMinute, physicalHour, physicalMinute, timeDifference);
|
realHour, realMinute, physicalHour, physicalMinute, timeDifference);
|
||||||
|
|
||||||
// Handle 12-hour rollover (if negative, add 12 hours)
|
// Handle 12-hour rollover (if negative, add 12 hours)
|
||||||
if (timeDifference < 0) {
|
if (timeDifference < 0) {
|
||||||
timeDifference += 12 * 60; // Add 12 hours to handle rollover
|
timeDifference += 12 * 60; // Add 12 hours to handle rollover
|
||||||
LOG_VERBOSE("⏰ DIFF: Adjusted for rollover, new difference %d minutes", timeDifference);
|
LOG_VERBOSE(TAG, "⏰ DIFF: Adjusted for rollover, new difference %d minutes", timeDifference);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a difference, advance the clock by one minute (your runMotor equivalent)
|
// If there's a difference, advance the clock by one minute (your runMotor equivalent)
|
||||||
if (timeDifference >= 1) {
|
if (timeDifference >= 1) {
|
||||||
advancePhysicalClockOneMinute();
|
advancePhysicalClockOneMinute();
|
||||||
LOG_DEBUG("⏰ SYNC: Advanced physical clock by 1 minute to %02d:%02d (real: %02d:%02d, diff: %lu mins)",
|
LOG_DEBUG(TAG, "⏰ SYNC: Advanced physical clock by 1 minute to %02d:%02d (real: %02d:%02d, diff: %lu mins)",
|
||||||
_configManager->getPhysicalClockHour(), _configManager->getPhysicalClockMinute(),
|
_configManager->getPhysicalClockHour(), _configManager->getPhysicalClockMinute(),
|
||||||
realHour, realMinute, timeDifference);
|
realHour, realMinute, timeDifference);
|
||||||
}
|
}
|
||||||
@@ -295,7 +304,7 @@ void Timekeeper::advancePhysicalClockOneMinute() {
|
|||||||
bool useC1 = _configManager->getNextOutputIsC1();
|
bool useC1 = _configManager->getNextOutputIsC1();
|
||||||
uint8_t outputToFire = useC1 ? (clockConfig.c1output - 1) : (clockConfig.c2output - 1);
|
uint8_t outputToFire = useC1 ? (clockConfig.c1output - 1) : (clockConfig.c2output - 1);
|
||||||
|
|
||||||
LOG_DEBUG("🔥 ADVANCE: Firing %s (output %d) for %dms",
|
LOG_DEBUG(TAG, "🔥 ADVANCE: Firing %s (output %d) for %dms",
|
||||||
useC1 ? "C1" : "C2", outputToFire + 1, clockConfig.pulseDuration);
|
useC1 ? "C1" : "C2", outputToFire + 1, clockConfig.pulseDuration);
|
||||||
|
|
||||||
_outputManager->fireOutputForDuration(outputToFire, clockConfig.pulseDuration);
|
_outputManager->fireOutputForDuration(outputToFire, clockConfig.pulseDuration);
|
||||||
@@ -323,7 +332,7 @@ void Timekeeper::updatePhysicalClockTime() {
|
|||||||
_configManager->setLastSyncTime(millis() / 1000);
|
_configManager->setLastSyncTime(millis() / 1000);
|
||||||
_configManager->saveClockState();
|
_configManager->saveClockState();
|
||||||
|
|
||||||
LOG_DEBUG("📅 STATE: Physical clock advanced to %d:%02d", currentHour, currentMinute);
|
LOG_DEBUG(TAG, "📅 STATE: Physical clock advanced to %d:%02d", currentHour, currentMinute);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -338,7 +347,7 @@ void Timekeeper::loadTodaysEvents() {
|
|||||||
// Get current date/time from RTC
|
// Get current date/time from RTC
|
||||||
DateTime now = rtc.now();
|
DateTime now = rtc.now();
|
||||||
if (!rtc.isrunning()) {
|
if (!rtc.isrunning()) {
|
||||||
LOG_ERROR("RTC not running - cannot load events");
|
LOG_ERROR(TAG, "RTC not running - cannot load events");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,13 +356,13 @@ void Timekeeper::loadTodaysEvents() {
|
|||||||
int currentDay = now.day();
|
int currentDay = now.day();
|
||||||
int currentDayOfWeek = now.dayOfTheWeek(); // 0=Sunday, 1=Monday, etc.
|
int currentDayOfWeek = now.dayOfTheWeek(); // 0=Sunday, 1=Monday, etc.
|
||||||
|
|
||||||
LOG_INFO("Loading events for: %04d-%02d-%02d (day %d)",
|
LOG_INFO(TAG, "Loading events for: %04d-%02d-%02d (day %d)",
|
||||||
currentYear, currentMonth, currentDay, currentDayOfWeek);
|
currentYear, currentMonth, currentDay, currentDayOfWeek);
|
||||||
|
|
||||||
// Open and parse events file
|
// Open and parse events file
|
||||||
File file = SD.open("/events/events.json");
|
File file = SD.open("/events/events.json");
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERROR("Failed to open events.json");
|
LOG_ERROR(TAG, "Failed to open events.json");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +374,7 @@ void Timekeeper::loadTodaysEvents() {
|
|||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
LOG_ERROR("JSON parsing failed: %s", error.c_str());
|
LOG_ERROR(TAG, "JSON parsing failed: %s", error.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +428,7 @@ void Timekeeper::loadTodaysEvents() {
|
|||||||
// Sort events by time
|
// Sort events by time
|
||||||
sortEventsByTime();
|
sortEventsByTime();
|
||||||
|
|
||||||
LOG_INFO("Loaded %d events for today", eventsLoaded);
|
LOG_INFO(TAG, "Loaded %d events for today", eventsLoaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Timekeeper::isSameDate(String eventDateTime, int year, int month, int day) {
|
bool Timekeeper::isSameDate(String eventDateTime, int year, int month, int day) {
|
||||||
@@ -453,7 +462,7 @@ void Timekeeper::addToTodaysSchedule(JsonObject event) {
|
|||||||
|
|
||||||
todaysEvents.push_back(schedEvent);
|
todaysEvents.push_back(schedEvent);
|
||||||
|
|
||||||
LOG_DEBUG("Added event '%s' at %s",
|
LOG_DEBUG(TAG, "Added event '%s' at %s",
|
||||||
event["name"].as<String>().c_str(),
|
event["name"].as<String>().c_str(),
|
||||||
schedEvent.timeStr.c_str());
|
schedEvent.timeStr.c_str());
|
||||||
}
|
}
|
||||||
@@ -503,7 +512,7 @@ void Timekeeper::checkScheduledEvents() {
|
|||||||
void Timekeeper::triggerEvent(ScheduledEvent& event) {
|
void Timekeeper::triggerEvent(ScheduledEvent& event) {
|
||||||
JsonObject eventData = event.eventData;
|
JsonObject eventData = event.eventData;
|
||||||
|
|
||||||
LOG_INFO("TRIGGERING EVENT: %s at %s",
|
LOG_INFO(TAG, "TRIGGERING EVENT: %s at %s",
|
||||||
eventData["name"].as<String>().c_str(),
|
eventData["name"].as<String>().c_str(),
|
||||||
event.timeStr.c_str());
|
event.timeStr.c_str());
|
||||||
|
|
||||||
@@ -516,7 +525,7 @@ void Timekeeper::triggerEvent(ScheduledEvent& event) {
|
|||||||
String melodyUID = melody["uid"].as<String>();
|
String melodyUID = melody["uid"].as<String>();
|
||||||
String melodyName = melody["name"].as<String>();
|
String melodyName = melody["name"].as<String>();
|
||||||
|
|
||||||
LOG_INFO("Playing melody: %s (UID: %s)",
|
LOG_INFO(TAG, "Playing melody: %s (UID: %s)",
|
||||||
melodyName.c_str(), melodyUID.c_str());
|
melodyName.c_str(), melodyUID.c_str());
|
||||||
|
|
||||||
// TODO: Add your melody trigger code here
|
// TODO: Add your melody trigger code here
|
||||||
@@ -526,7 +535,7 @@ void Timekeeper::triggerEvent(ScheduledEvent& event) {
|
|||||||
void Timekeeper::loadNextDayEvents() {
|
void Timekeeper::loadNextDayEvents() {
|
||||||
// This function would load tomorrow's events for smooth midnight transition
|
// This function would load tomorrow's events for smooth midnight transition
|
||||||
// Implementation similar to loadTodaysEvents() but for tomorrow's date
|
// Implementation similar to loadTodaysEvents() but for tomorrow's date
|
||||||
LOG_DEBUG("Pre-loading tomorrow's events...");
|
LOG_DEBUG(TAG, "Pre-loading tomorrow's events...");
|
||||||
// TODO: Implement if needed for smoother transitions
|
// TODO: Implement if needed for smoother transitions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,14 +554,34 @@ void Timekeeper::checkClockAlerts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 CRITICAL: Check if Player is busy - if so, SKIP alert completely
|
||||||
|
if (_player && _player->isPlaying) {
|
||||||
|
// Player is active (playing, paused, stopping, etc.) - skip alert entirely
|
||||||
|
// Mark this alert as processed to prevent it from firing when playback ends
|
||||||
|
DateTime now = rtc.now();
|
||||||
|
int currentMinute = now.minute();
|
||||||
|
|
||||||
|
if (currentMinute == 0) {
|
||||||
|
lastHour = now.hour(); // Mark hour as processed
|
||||||
|
} else if (currentMinute == 30) {
|
||||||
|
lastMinute = 30; // Mark half-hour as processed
|
||||||
|
} else if (currentMinute == 15 || currentMinute == 45) {
|
||||||
|
lastMinute = currentMinute; // Mark quarter-hour as processed
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG(TAG, "⏭️ SKIPPING clock alert - Player is busy (playing/paused)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get current time
|
// Get current time
|
||||||
DateTime now = rtc.now();
|
DateTime now = rtc.now();
|
||||||
int currentHour = now.hour();
|
int currentHour = now.hour();
|
||||||
int currentMinute = now.minute();
|
int currentMinute = now.minute();
|
||||||
int currentSecond = now.second();
|
int currentSecond = now.second();
|
||||||
|
|
||||||
// Only trigger alerts on exact seconds (0-2) to avoid multiple triggers
|
// Only trigger alerts in first 30 seconds of the minute
|
||||||
if (currentSecond > 2) {
|
// The lastHour/lastMinute tracking prevents duplicate triggers
|
||||||
|
if (currentSecond > 30) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,7 +607,7 @@ void Timekeeper::checkClockAlerts() {
|
|||||||
// 🕕 HALF-HOUR ALERTS (at xx:30)
|
// 🕕 HALF-HOUR ALERTS (at xx:30)
|
||||||
if (currentMinute == 30 && lastMinute != 30) {
|
if (currentMinute == 30 && lastMinute != 30) {
|
||||||
if (clockConfig.halfBell != 255) { // 255 = disabled
|
if (clockConfig.halfBell != 255) { // 255 = disabled
|
||||||
LOG_INFO("🕕 Half-hour alert at %02d:30", currentHour);
|
LOG_INFO(TAG, "🕕 Half-hour alert at %02d:30", currentHour);
|
||||||
fireAlertBell(clockConfig.halfBell, 1);
|
fireAlertBell(clockConfig.halfBell, 1);
|
||||||
}
|
}
|
||||||
lastMinute = 30;
|
lastMinute = 30;
|
||||||
@@ -587,7 +616,7 @@ void Timekeeper::checkClockAlerts() {
|
|||||||
// 🕒 QUARTER-HOUR ALERTS (at xx:15 and xx:45)
|
// 🕒 QUARTER-HOUR ALERTS (at xx:15 and xx:45)
|
||||||
if ((currentMinute == 15 || currentMinute == 45) && lastMinute != currentMinute) {
|
if ((currentMinute == 15 || currentMinute == 45) && lastMinute != currentMinute) {
|
||||||
if (clockConfig.quarterBell != 255) { // 255 = disabled
|
if (clockConfig.quarterBell != 255) { // 255 = disabled
|
||||||
LOG_INFO("🕒 Quarter-hour alert at %02d:%02d", currentHour, currentMinute);
|
LOG_INFO(TAG, "🕒 Quarter-hour alert at %02d:%02d", currentHour, currentMinute);
|
||||||
fireAlertBell(clockConfig.quarterBell, 1);
|
fireAlertBell(clockConfig.quarterBell, 1);
|
||||||
}
|
}
|
||||||
lastMinute = currentMinute;
|
lastMinute = currentMinute;
|
||||||
@@ -611,7 +640,7 @@ void Timekeeper::triggerHourlyAlert(int hour) {
|
|||||||
|
|
||||||
if (clockConfig.alertType == "SINGLE") {
|
if (clockConfig.alertType == "SINGLE") {
|
||||||
// Single ding for any hour
|
// Single ding for any hour
|
||||||
LOG_INFO("🕐 Hourly alert (SINGLE) at %02d:00", hour);
|
LOG_INFO(TAG, "🕐 Hourly alert (SINGLE) at %02d:00", hour);
|
||||||
fireAlertBell(clockConfig.hourBell, 1);
|
fireAlertBell(clockConfig.hourBell, 1);
|
||||||
}
|
}
|
||||||
else if (clockConfig.alertType == "HOURS") {
|
else if (clockConfig.alertType == "HOURS") {
|
||||||
@@ -620,7 +649,7 @@ void Timekeeper::triggerHourlyAlert(int hour) {
|
|||||||
if (bellCount == 0) bellCount = 12; // Midnight = 12 bells
|
if (bellCount == 0) bellCount = 12; // Midnight = 12 bells
|
||||||
if (bellCount > 12) bellCount = bellCount - 12; // 24h to 12h conversion
|
if (bellCount > 12) bellCount = bellCount - 12; // 24h to 12h conversion
|
||||||
|
|
||||||
LOG_INFO("🕐 Hourly alert (HOURS) at %02d:00 - %d rings", hour, bellCount);
|
LOG_INFO(TAG, "🕐 Hourly alert (HOURS) at %02d:00 - %d rings", hour, bellCount);
|
||||||
fireAlertBell(clockConfig.hourBell, bellCount);
|
fireAlertBell(clockConfig.hourBell, bellCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -632,11 +661,20 @@ void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) {
|
|||||||
|
|
||||||
const auto& clockConfig = _configManager->getClockConfig();
|
const auto& clockConfig = _configManager->getClockConfig();
|
||||||
|
|
||||||
|
// Mark alert as in progress
|
||||||
|
alertInProgress.store(true);
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
for (int i = 0; i < count; i++) {
|
||||||
|
// 🔥 Check for interruption by user playback
|
||||||
|
if (!alertInProgress.load()) {
|
||||||
|
LOG_INFO(TAG, "⚡ Alert interrupted at ring %d/%d - stopping immediately", i + 1, count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Get bell duration from bell configuration
|
// Get bell duration from bell configuration
|
||||||
uint16_t bellDuration = _configManager->getBellDuration(bellNumber);
|
uint16_t bellDuration = _configManager->getBellDuration(bellNumber);
|
||||||
|
|
||||||
LOG_DEBUG("🔔 Alert bell #%d ring %d/%d (duration: %dms)",
|
LOG_DEBUG(TAG, "🔔 Alert bell #%d ring %d/%d (duration: %dms)",
|
||||||
bellNumber + 1, i + 1, count, bellDuration);
|
bellNumber + 1, i + 1, count, bellDuration);
|
||||||
|
|
||||||
// Fire the bell using OutputManager
|
// Fire the bell using OutputManager
|
||||||
@@ -647,6 +685,9 @@ void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) {
|
|||||||
vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval));
|
vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark alert as complete
|
||||||
|
alertInProgress.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::checkBacklightAutomation() {
|
void Timekeeper::checkBacklightAutomation() {
|
||||||
@@ -675,14 +716,14 @@ void Timekeeper::checkBacklightAutomation() {
|
|||||||
|
|
||||||
// Check if it's time to turn backlight ON
|
// Check if it's time to turn backlight ON
|
||||||
if (currentTime == clockConfig.backlightOnTime && !backlightState) {
|
if (currentTime == clockConfig.backlightOnTime && !backlightState) {
|
||||||
LOG_INFO("💡 Turning backlight ON at %s (output #%d)",
|
LOG_INFO(TAG, "💡 Turning backlight ON at %s (output #%d)",
|
||||||
currentTime.c_str(), clockConfig.backlightOutput + 1);
|
currentTime.c_str(), clockConfig.backlightOutput + 1);
|
||||||
_outputManager->fireOutput(clockConfig.backlightOutput);
|
_outputManager->fireOutput(clockConfig.backlightOutput);
|
||||||
backlightState = true;
|
backlightState = true;
|
||||||
}
|
}
|
||||||
// Check if it's time to turn backlight OFF
|
// Check if it's time to turn backlight OFF
|
||||||
else if (currentTime == clockConfig.backlightOffTime && backlightState) {
|
else if (currentTime == clockConfig.backlightOffTime && backlightState) {
|
||||||
LOG_INFO("💡 Turning backlight OFF at %s (output #%d)",
|
LOG_INFO(TAG, "💡 Turning backlight OFF at %s (output #%d)",
|
||||||
currentTime.c_str(), clockConfig.backlightOutput + 1);
|
currentTime.c_str(), clockConfig.backlightOutput + 1);
|
||||||
_outputManager->extinguishOutput(clockConfig.backlightOutput);
|
_outputManager->extinguishOutput(clockConfig.backlightOutput);
|
||||||
backlightState = false;
|
backlightState = false;
|
||||||
@@ -704,14 +745,22 @@ bool Timekeeper::isInSilencePeriod() {
|
|||||||
|
|
||||||
// Check daytime silence period
|
// Check daytime silence period
|
||||||
if (clockConfig.daytimeSilenceEnabled) {
|
if (clockConfig.daytimeSilenceEnabled) {
|
||||||
if (isTimeInRange(currentTime, clockConfig.daytimeSilenceOnTime, clockConfig.daytimeSilenceOffTime)) {
|
bool inDaytime = isTimeInRange(currentTime, clockConfig.daytimeSilenceOnTime, clockConfig.daytimeSilenceOffTime);
|
||||||
|
LOG_DEBUG(TAG, "🔇 Daytime silence check: current=%s, range=%s-%s, inRange=%s",
|
||||||
|
currentTime.c_str(), clockConfig.daytimeSilenceOnTime.c_str(),
|
||||||
|
clockConfig.daytimeSilenceOffTime.c_str(), inDaytime ? "YES" : "NO");
|
||||||
|
if (inDaytime) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check nighttime silence period
|
// Check nighttime silence period
|
||||||
if (clockConfig.nighttimeSilenceEnabled) {
|
if (clockConfig.nighttimeSilenceEnabled) {
|
||||||
if (isTimeInRange(currentTime, clockConfig.nighttimeSilenceOnTime, clockConfig.nighttimeSilenceOffTime)) {
|
bool inNighttime = isTimeInRange(currentTime, clockConfig.nighttimeSilenceOnTime, clockConfig.nighttimeSilenceOffTime);
|
||||||
|
LOG_DEBUG(TAG, "🌙 Nighttime silence check: current=%s, range=%s-%s, inRange=%s",
|
||||||
|
currentTime.c_str(), clockConfig.nighttimeSilenceOnTime.c_str(),
|
||||||
|
clockConfig.nighttimeSilenceOffTime.c_str(), inNighttime ? "YES" : "NO");
|
||||||
|
if (inNighttime) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -737,38 +786,38 @@ bool Timekeeper::isTimeInRange(const String& currentTime, const String& startTim
|
|||||||
bool Timekeeper::isHealthy() {
|
bool Timekeeper::isHealthy() {
|
||||||
// Check if RTC is running
|
// Check if RTC is running
|
||||||
if (!rtc.isrunning()) {
|
if (!rtc.isrunning()) {
|
||||||
LOG_DEBUG("TimeKeeper: Unhealthy - RTC not running");
|
LOG_DEBUG(TAG, "TimeKeeper: Unhealthy - RTC not running");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if main task is created and running
|
// Check if main task is created and running
|
||||||
if (mainTaskHandle == NULL) {
|
if (mainTaskHandle == NULL) {
|
||||||
LOG_DEBUG("TimeKeeper: Unhealthy - Main task not created");
|
LOG_DEBUG(TAG, "TimeKeeper: Unhealthy - Main task not created");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if task is still alive
|
// Check if task is still alive
|
||||||
eTaskState taskState = eTaskGetState(mainTaskHandle);
|
eTaskState taskState = eTaskGetState(mainTaskHandle);
|
||||||
if (taskState == eDeleted || taskState == eInvalid) {
|
if (taskState == eDeleted || taskState == eInvalid) {
|
||||||
LOG_DEBUG("TimeKeeper: Unhealthy - Main task deleted or invalid");
|
LOG_DEBUG(TAG, "TimeKeeper: Unhealthy - Main task deleted or invalid");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if required dependencies are set
|
// Check if required dependencies are set
|
||||||
if (!_configManager) {
|
if (!_configManager) {
|
||||||
LOG_DEBUG("TimeKeeper: Unhealthy - ConfigManager not set");
|
LOG_DEBUG(TAG, "TimeKeeper: Unhealthy - ConfigManager not set");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_outputManager) {
|
if (!_outputManager) {
|
||||||
LOG_DEBUG("TimeKeeper: Unhealthy - OutputManager not set");
|
LOG_DEBUG(TAG, "TimeKeeper: Unhealthy - OutputManager not set");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if time is reasonable (not stuck at epoch or way in the future)
|
// Check if time is reasonable (not stuck at epoch or way in the future)
|
||||||
DateTime now = rtc.now();
|
DateTime now = rtc.now();
|
||||||
if (now.year() < 2020 || now.year() > 2100) {
|
if (now.year() < 2020 || now.year() > 2100) {
|
||||||
LOG_DEBUG("TimeKeeper: Unhealthy - RTC time unreasonable: %d", now.year());
|
LOG_DEBUG(TAG, "TimeKeeper: Unhealthy - RTC time unreasonable: %d", now.year());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <RTClib.h>
|
#include <RTClib.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
@@ -61,6 +62,7 @@ private:
|
|||||||
// Alert management - new functionality
|
// Alert management - new functionality
|
||||||
int lastHour = -1; // Track last processed hour to avoid duplicate alerts
|
int lastHour = -1; // Track last processed hour to avoid duplicate alerts
|
||||||
int lastMinute = -1; // Track last processed minute for quarter/half alerts
|
int lastMinute = -1; // Track last processed minute for quarter/half alerts
|
||||||
|
std::atomic<bool> alertInProgress{false}; // Flag to track if alert is currently playing
|
||||||
|
|
||||||
// Backlight management - new functionality
|
// Backlight management - new functionality
|
||||||
bool backlightState = false; // Track current backlight state
|
bool backlightState = false; // Track current backlight state
|
||||||
@@ -69,6 +71,7 @@ private:
|
|||||||
OutputManager* _outputManager = nullptr;
|
OutputManager* _outputManager = nullptr;
|
||||||
ConfigManager* _configManager = nullptr;
|
ConfigManager* _configManager = nullptr;
|
||||||
Networking* _networking = nullptr;
|
Networking* _networking = nullptr;
|
||||||
|
class Player* _player = nullptr; // Reference to Player for playback status checks
|
||||||
|
|
||||||
// Legacy function pointer (DEPRECATED - will be removed)
|
// Legacy function pointer (DEPRECATED - will be removed)
|
||||||
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
||||||
@@ -84,12 +87,16 @@ public:
|
|||||||
void setOutputManager(OutputManager* outputManager);
|
void setOutputManager(OutputManager* outputManager);
|
||||||
void setConfigManager(ConfigManager* configManager);
|
void setConfigManager(ConfigManager* configManager);
|
||||||
void setNetworking(Networking* networking);
|
void setNetworking(Networking* networking);
|
||||||
|
void setPlayer(class Player* player); // Set Player reference for playback coordination
|
||||||
|
|
||||||
// Clock Updates Pause Functions
|
// Clock Updates Pause Functions
|
||||||
void pauseClockUpdates() { clockUpdatesPaused = true; }
|
void pauseClockUpdates() { clockUpdatesPaused = true; }
|
||||||
void resumeClockUpdates() { clockUpdatesPaused = false; }
|
void resumeClockUpdates() { clockUpdatesPaused = false; }
|
||||||
bool areClockUpdatesPaused() const { return clockUpdatesPaused; }
|
bool areClockUpdatesPaused() const { return clockUpdatesPaused; }
|
||||||
|
|
||||||
|
// Alert interruption - called by Player when starting playback
|
||||||
|
void interruptActiveAlert();
|
||||||
|
|
||||||
// Legacy interface (DEPRECATED - will be removed)
|
// Legacy interface (DEPRECATED - will be removed)
|
||||||
void setRelayWriteFunction(void (*func)(int, int));
|
void setRelayWriteFunction(void (*func)(int, int));
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
|
}
|
||||||
@@ -24,8 +24,9 @@
|
|||||||
* 🎯 KEY FEATURES:
|
* 🎯 KEY FEATURES:
|
||||||
* ✅ Microsecond-precision bell timing (BellEngine)
|
* ✅ Microsecond-precision bell timing (BellEngine)
|
||||||
* ✅ Multi-hardware support (PCF8574, GPIO, Mock)
|
* ✅ Multi-hardware support (PCF8574, GPIO, Mock)
|
||||||
* ✅ Dual network connectivity (Ethernet + WiFi)
|
* ✅ Dual network connectivity (Ethernet + WiFi + Permanent AP Mode)
|
||||||
* ✅ Dual Communication Support (MQTT + WebSocket)
|
* ✅ Multi-protocol communication (MQTT + WebSocket + HTTP REST API)
|
||||||
|
* ✅ Web settings interface for network mode switching
|
||||||
* ✅ Real-time telemetry and load monitoring
|
* ✅ Real-time telemetry and load monitoring
|
||||||
* ✅ Over-the-air firmware updates
|
* ✅ Over-the-air firmware updates
|
||||||
* ✅ SD card configuration and file management
|
* ✅ SD card configuration and file management
|
||||||
@@ -33,8 +34,9 @@
|
|||||||
* ✅ Comprehensive logging system
|
* ✅ Comprehensive logging system
|
||||||
*
|
*
|
||||||
* 📡 COMMUNICATION PROTOCOLS:
|
* 📡 COMMUNICATION PROTOCOLS:
|
||||||
* • MQTT (SSL/TLS via PubSubClient on Core 0)
|
* • MQTT (SSL/TLS via AsyncMqttClient on Core 0)
|
||||||
* • WebSocket (Real-time web interface)
|
* • WebSocket (Real-time web interface)
|
||||||
|
* • HTTP REST API (Command execution via HTTP)
|
||||||
* • UDP Discovery (Auto-discovery service)
|
* • UDP Discovery (Auto-discovery service)
|
||||||
* • HTTP/HTTPS (OTA updates)
|
* • HTTP/HTTPS (OTA updates)
|
||||||
*
|
*
|
||||||
@@ -62,14 +64,23 @@
|
|||||||
* 👨💻 AUTHOR: BellSystems bonamin
|
* 👨💻 AUTHOR: BellSystems bonamin
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define FW_VERSION "0.1"
|
#define FW_VERSION "154"
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
* 📅 VERSION HISTORY:
|
* 📅 VERSION HISTORY:
|
||||||
|
* NOTE: Versions are now stored as integers (v1.3 = 130)
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
* v0.1 - Vesper Launch Beta
|
* 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.
|
||||||
* ═══════════════════════════════════════════════════════════════════════════════
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -79,8 +90,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
// SYSTEM LIBRARIES - Core ESP32 and Arduino functionality
|
// SYSTEM LIBRARIES - Core ESP32 and Arduino functionality
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -117,6 +126,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
// CUSTOM CLASSES - Include Custom Classes and Functions
|
// CUSTOM CLASSES - Include Custom Classes and Functions
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
#include "src/SDCardMutex/SDCardMutex.hpp" // ⚠️ MUST be included before any SD-using classes
|
||||||
#include "src/ConfigManager/ConfigManager.hpp"
|
#include "src/ConfigManager/ConfigManager.hpp"
|
||||||
#include "src/FileManager/FileManager.hpp"
|
#include "src/FileManager/FileManager.hpp"
|
||||||
#include "src/TimeKeeper/TimeKeeper.hpp"
|
#include "src/TimeKeeper/TimeKeeper.hpp"
|
||||||
@@ -178,6 +188,7 @@ BellEngine bellEngine(player, configManager, telemetry, outputManager); // 🔥
|
|||||||
|
|
||||||
TaskHandle_t bellEngineHandle = NULL; // Legacy - will be removed
|
TaskHandle_t bellEngineHandle = NULL; // Legacy - will be removed
|
||||||
TimerHandle_t schedulerTimer;
|
TimerHandle_t schedulerTimer;
|
||||||
|
TimerHandle_t ntpSyncTimer; // Non-blocking delayed NTP sync timer
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -188,21 +199,44 @@ void handleFactoryReset() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-blocking NTP sync timer callback
|
||||||
|
void ntpSyncTimerCallback(TimerHandle_t xTimer) {
|
||||||
|
LOG_DEBUG("Network stabilization complete - starting NTP sync");
|
||||||
|
if (!networking.isInAPMode()) {
|
||||||
|
timekeeper.syncTimeWithNTP();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void setup()
|
void setup()
|
||||||
{
|
{
|
||||||
// Initialize Serial Communications (for debugging) & I2C Bus (for Hardware Control)
|
// Initialize Serial Communications (for debugging) & I2C Bus (for Hardware Control)
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
Serial.println("Hello, VESPER System Initialized! - PontikoTest");
|
Serial.print("VESPER System Booting UP! - Version ");
|
||||||
|
Serial.println(FW_VERSION);
|
||||||
Wire.begin(4,15);
|
Wire.begin(4,15);
|
||||||
auto& hwConfig = configManager.getHardwareConfig();
|
auto& hwConfig = configManager.getHardwareConfig();
|
||||||
SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi);
|
SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi);
|
||||||
delay(50);
|
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)
|
// Initialize Configuration (loads factory identity from NVS + user settings from SD)
|
||||||
configManager.begin();
|
configManager.begin();
|
||||||
|
|
||||||
|
// Apply log level from config (loaded from SD)
|
||||||
|
uint8_t logLevel = configManager.getGeneralConfig().serialLogLevel;
|
||||||
|
Logging::setLevel((Logging::LogLevel)logLevel);
|
||||||
|
LOG_INFO("Log level set to %d from configuration", logLevel);
|
||||||
|
|
||||||
inputManager.begin();
|
inputManager.begin();
|
||||||
inputManager.setFactoryResetLongPressCallback(handleFactoryReset);
|
inputManager.setFactoryResetLongPressCallback(handleFactoryReset);
|
||||||
|
|
||||||
@@ -214,6 +248,18 @@ void setup()
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
// Update firmware version (this is the ONLY identity field that can be set)
|
// 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("⚠️ Migrated version format: %s -> %u", currentVersion.c_str(), versionInt);
|
||||||
|
}
|
||||||
|
|
||||||
configManager.setFwVersion(FW_VERSION);
|
configManager.setFwVersion(FW_VERSION);
|
||||||
LOG_INFO("Firmware version: %s", FW_VERSION);
|
LOG_INFO("Firmware version: %s", FW_VERSION);
|
||||||
|
|
||||||
@@ -273,16 +319,18 @@ void setup()
|
|||||||
timekeeper.setOutputManager(&outputManager);
|
timekeeper.setOutputManager(&outputManager);
|
||||||
timekeeper.setConfigManager(&configManager);
|
timekeeper.setConfigManager(&configManager);
|
||||||
timekeeper.setNetworking(&networking);
|
timekeeper.setNetworking(&networking);
|
||||||
|
timekeeper.setPlayer(&player); // 🔥 Connect for playback coordination
|
||||||
// Clock outputs now configured via ConfigManager/Communication commands
|
// Clock outputs now configured via ConfigManager/Communication commands
|
||||||
|
|
||||||
// Register TimeKeeper with health monitor
|
// Register TimeKeeper with health monitor
|
||||||
healthMonitor.setTimeKeeper(&timekeeper);
|
healthMonitor.setTimeKeeper(&timekeeper);
|
||||||
|
|
||||||
// Initialize Telemetry
|
// Initialize Telemetry
|
||||||
telemetry.begin();
|
|
||||||
telemetry.setPlayerReference(&player.isPlaying);
|
telemetry.setPlayerReference(&player.isPlaying);
|
||||||
// 🚑 CRITICAL: Connect force stop callback for overload protection!
|
// 🚑 CRITICAL: Connect force stop callback for overload protection!
|
||||||
telemetry.setForceStopCallback([]() { player.forceStop(); });
|
telemetry.setForceStopCallback([]() { player.forceStop(); });
|
||||||
|
telemetry.setFileManager(&fileManager);
|
||||||
|
telemetry.begin();
|
||||||
|
|
||||||
// Register Telemetry with health monitor
|
// Register Telemetry with health monitor
|
||||||
healthMonitor.setTelemetry(&telemetry);
|
healthMonitor.setTelemetry(&telemetry);
|
||||||
@@ -308,9 +356,12 @@ void setup()
|
|||||||
communication.setFileManagerReference(&fileManager);
|
communication.setFileManagerReference(&fileManager);
|
||||||
communication.setTimeKeeperReference(&timekeeper);
|
communication.setTimeKeeperReference(&timekeeper);
|
||||||
communication.setFirmwareValidatorReference(&firmwareValidator);
|
communication.setFirmwareValidatorReference(&firmwareValidator);
|
||||||
|
communication.setTelemetryReference(&telemetry);
|
||||||
|
|
||||||
player.setDependencies(&communication, &fileManager);
|
player.setDependencies(&communication, &fileManager);
|
||||||
player.setBellEngine(&bellEngine); // Connect the beast!
|
player.setBellEngine(&bellEngine); // Connect the beast!
|
||||||
|
player.setTelemetry(&telemetry);
|
||||||
|
player.setTimekeeper(&timekeeper); // 🔥 Connect for alert coordination
|
||||||
|
|
||||||
// Register Communication with health monitor
|
// Register Communication with health monitor
|
||||||
healthMonitor.setCommunication(&communication);
|
healthMonitor.setCommunication(&communication);
|
||||||
@@ -318,46 +369,74 @@ void setup()
|
|||||||
// 🔔 CONNECT BELLENGINE TO COMMUNICATION FOR DING NOTIFICATIONS!
|
// 🔔 CONNECT BELLENGINE TO COMMUNICATION FOR DING NOTIFICATIONS!
|
||||||
bellEngine.setCommunicationManager(&communication);
|
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
|
// Set up network callbacks
|
||||||
networking.setNetworkCallbacks(
|
networking.setNetworkCallbacks(
|
||||||
[]() {
|
[&webServerStarted]() {
|
||||||
communication.onNetworkConnected();
|
communication.onNetworkConnected();
|
||||||
// Start AsyncWebServer when network becomes available
|
|
||||||
if (networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
|
// 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("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("🚀 Starting AsyncWebServer on port 80...");
|
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
||||||
server.begin();
|
server.begin();
|
||||||
LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str());
|
LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str());
|
||||||
|
webServerStarted = true;
|
||||||
}
|
}
|
||||||
}, // onConnected
|
}, // onConnected
|
||||||
[]() { communication.onNetworkDisconnected(); } // onDisconnected
|
[]() { communication.onNetworkDisconnected(); } // onDisconnected
|
||||||
);
|
);
|
||||||
|
|
||||||
// If already connected, trigger MQTT connection manually
|
// If already connected, trigger MQTT connection and setup manually
|
||||||
if (networking.isConnected()) {
|
if (networking.isConnected()) {
|
||||||
LOG_INFO("Network already connected - triggering MQTT connection");
|
LOG_INFO("Network already connected - initializing services");
|
||||||
communication.onNetworkConnected();
|
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("Network already connected - scheduling NTP sync after 3s stabilization (non-blocking)");
|
||||||
|
xTimerStart(ntpSyncTimer, 0);
|
||||||
|
}
|
||||||
|
|
||||||
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
|
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
|
||||||
// Do NOT start if WiFiManager portal is active (port 80 conflict!)
|
// Do NOT start if WiFiManager portal is active (port 80 conflict!)
|
||||||
|
if (!webServerStarted && networking.getState() != NetworkState::WIFI_PORTAL_MODE) {
|
||||||
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
LOG_INFO("🚀 Starting AsyncWebServer on port 80...");
|
||||||
server.begin();
|
server.begin();
|
||||||
LOG_INFO("✅ AsyncWebServer started and listening on http://%s", networking.getLocalIP().c_str());
|
LOG_INFO("✅ AsyncWebServer started on http://%s", networking.getLocalIP().c_str());
|
||||||
|
webServerStarted = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("⚠️ Network not ready - AsyncWebServer will start after connection");
|
LOG_WARNING("⚠️ Network not ready - services will start after connection");
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(500);
|
// Initialize OTA Manager
|
||||||
|
|
||||||
// Initialize OTA Manager and check for updates
|
|
||||||
otaManager.begin();
|
otaManager.begin();
|
||||||
otaManager.setFileManager(&fileManager);
|
otaManager.setFileManager(&fileManager);
|
||||||
otaManager.setPlayer(&player); // Set player reference for idle check
|
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
|
||||||
|
|
||||||
// 🔥 CRITICAL: Delay OTA check to avoid UDP socket race with MQTT
|
// 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay)
|
||||||
// Both MQTT and OTA HTTP use UDP sockets, must sequence them!
|
// UDP discovery setup can happen immediately without conflicts
|
||||||
delay(2000);
|
|
||||||
LOG_INFO("Starting OTA update check after network stabilization...");
|
|
||||||
otaManager.checkForUpdates();
|
|
||||||
communication.setupUdpDiscovery();
|
communication.setupUdpDiscovery();
|
||||||
|
|
||||||
// Register OTA Manager with health monitor
|
// Register OTA Manager with health monitor
|
||||||
@@ -428,10 +507,25 @@ void loop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 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
|
// 🔥 DEBUG: Log every 10 seconds to verify we're still running
|
||||||
static unsigned long lastLog = 0;
|
static unsigned long lastLog = 0;
|
||||||
if (millis() - lastLog > 10000) {
|
if (millis() - lastLog > 10000) {
|
||||||
LOG_DEBUG("❤️ Loop alive, free heap: %d", ESP.getFreeHeap());
|
LOG_DEBUG("❤️ 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();
|
lastLog = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user