Compare commits

..

13 Commits

Author SHA1 Message Date
ab1e4f2e6d Merge feature/subsystem-logging into main
- Per-subsystem log tags across all firmware modules
- PlatformIO migration (ESP32-S3, env:vesper-v1)
- Manufacturing automation plan docs added
- .gitignore updated for PlatformIO build artifacts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 17:32:31 +02:00
3877d27dae feat: Migrate firmware to PlatformIO (ESP32-S3, vesper-v1 env)
Replaces Arduino IDE with PlatformIO as the build system. Entry point
moved from vesper.ino to src/main.cpp. All library dependencies are now
declared in platformio.ini and downloaded automatically via lib_deps.

Board: Kincony KC868-A6 (ESP32-S3, 4MB flash) → env:vesper-v1
Future variants Vesper+ and Vesper Pro are pre-configured but commented out.

Compatibility fixes applied for this framework version:
- Removed ETH.h dependency (Ethernet was disabled in v138)
- Watchdog init updated to IDF v4 API (esp_task_wdt_init signature)
- ETH.linkUp() check removed from OTAManager

Also adds .pio/ to .gitignore and commits the manufacturing plan docs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 17:31:53 +02:00
fe6b1d871a feat: Add per-subsystem log tags to all firmware modules
Refactored logging system to require a TAG as first argument on all
LOG_* macros, enabling per-subsystem log filtering and cleaner output.
Each subsystem now defines its own TAG (e.g. "BellEngine", "Player").
Also overhauled Logging.hpp/cpp with improved level control and output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 17:31:28 +02:00
c656835d8e Fix LCD Communicaiton infinite looping upon failed commands 2026-02-10 18:45:41 +02:00
980de08584 Fix Infinite Loop Bug, Melody Download crashes and dwnld skip if melody is builin. 2026-02-04 17:00:27 +02:00
9c314d88cf Fixed Infinite Play bug
When Infinite Play was set once, it was never reset.
2026-02-03 15:46:47 +02:00
53c55d2726 Added Sync Time to LCD, Fixed UTC Timestamp issues 2026-01-28 10:28:05 +02:00
094b1a9620 Added UART as a communication interface option. 2026-01-19 21:24:35 +02:00
11b98166d1 Fixed OTA problems, Clock Alerts and MQTT Logs. V151 2026-01-19 19:02:25 +02:00
7e279c6e45 Added delay to NTP to let WiFi catch up before time request 2026-01-07 21:46:26 +02:00
eb6e0f0e5c Added Global Mutex for SD IOPS, and changed OTA to write directly to Flash 2026-01-07 21:04:33 +02:00
7adc1fec34 Changed OTA to write Directly to flash 2026-01-07 15:46:20 +02:00
51b7722e1d Removed Ethernet, added default WiFi (mikrotik AP) and fixed various Clock bugs. 2026-01-07 01:06:45 +02:00
46 changed files with 5521 additions and 1389 deletions

8
.gitignore vendored
View File

@@ -7,4 +7,10 @@ vesper/CLAUDE.md
vesper/flutter/ vesper/flutter/
vesper/docs_manual/ vesper/docs_manual/
Doxyfile Doxyfile
vesper/.claude/ vesper/.claude/
# PlatformIO — build output and downloaded libraries (never commit these)
vesper/.pio/
# Claude Code memory/session files
.claude/

View 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! 🔔*

View 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

View 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

View File

View 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

View 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"
}
}

View 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.

View 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 13 are foundational; don't skip ahead. Phases 46 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
View 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}

View File

@@ -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..."); 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 !"); 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("BellEngine - 🛑 Forcing Stop Immediately"); 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 - 🔥 Engine 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_VERBOSE("BellEngine - ⏸️ 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,17 +209,17 @@ void BellEngine::playbackLoop() {
portEXIT_CRITICAL(&_melodyMutex); portEXIT_CRITICAL(&_melodyMutex);
if (melodySteps.empty()) { if (melodySteps.empty()) {
LOG_ERROR("BellEngine - ❌ Empty melody in playback loop!"); LOG_ERROR(TAG, "BellEngine - ❌ Empty melody in playback loop!");
return; return;
} }
LOG_DEBUG("BellEngine - 🎵 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("BellEngine - Emergency exit from playback loop"); LOG_DEBUG(TAG, "BellEngine - Emergency exit from playback loop");
return; return;
} }
@@ -227,7 +229,7 @@ void BellEngine::playbackLoop() {
// Precise timing delay - validate speed to prevent division by zero // Precise timing delay - validate speed to prevent division by zero
// I THINK this should be moved outside the Bell Engine // I THINK this should be moved outside the Bell Engine
if (_player.speed == 0) { if (_player.speed == 0) {
LOG_ERROR("BellEngine - ❌ Invalid Speed (0) detected, stopping playback"); LOG_ERROR(TAG, "BellEngine - ❌ Invalid Speed (0) detected, stopping playback");
_player.hardStop = true; _player.hardStop = true;
_engineRunning.store(false); _engineRunning.store(false);
return; return;
@@ -242,9 +244,9 @@ void BellEngine::playbackLoop() {
_player.onMelodyLoopCompleted(); // 🔥 Notify Player that melody actually finished! _player.onMelodyLoopCompleted(); // 🔥 Notify Player that melody actually finished!
if ((_player.continuous_loop && _player.segment_duration == 0) || _player.total_duration == 0) { if ((_player.continuous_loop && _player.segment_duration == 0) || _player.total_duration == 0) {
vTaskDelay(pdMS_TO_TICKS(500)); //Give Player time to pause/stop vTaskDelay(pdMS_TO_TICKS(500)); //Give Player time to pause/stop
LOG_VERBOSE("BellEngine - Loop completed in SINGLE Mode - waiting for Player to handle pause/stop"); LOG_VERBOSE(TAG, "BellEngine - Loop completed in SINGLE Mode - waiting for Player to handle pause/stop");
} }
LOG_DEBUG("BellEngine - 🎵 Melody loop completed with PRECISION"); LOG_DEBUG(TAG, "BellEngine - 🎵 Melody loop completed with PRECISION");
} }
@@ -268,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("BellEngine - 🚨 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("BellEngine - 🚨 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("BellEngine - ⚠️ 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("BellEngine - ⚠️ UNCONFIGURED: Bell %d not configured, skipping", bellIndex); LOG_DEBUG(TAG, "BellEngine - ⚠️ UNCONFIGURED: Bell %d not configured, skipping", bellIndex);
continue; continue;
} }
@@ -306,14 +308,14 @@ void BellEngine::activateNote(uint16_t note) {
// Record telemetry // Record telemetry
_telemetry.recordBellStrike(bellIndex); _telemetry.recordBellStrike(bellIndex);
LOG_VERBOSE("BellEngine - 🔨 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("BellEngine - 🔥 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!
// * deactivated currently, since unstable and causes performance issues * // * deactivated currently, since unstable and causes performance issues *
@@ -339,7 +341,7 @@ void BellEngine::preciseDelay(uint32_t microseconds) {
} }
void BellEngine::emergencyShutdown() { void BellEngine::emergencyShutdown() {
LOG_INFO("BellEngine - 🚨 Emergency Shutdown - Notifying OutputManager"); LOG_INFO(TAG, "BellEngine - 🚨 Emergency Shutdown - Notifying OutputManager");
_outputManager.emergencyShutdown(); _outputManager.emergencyShutdown();
} }
@@ -364,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("BellEngine - 🔔 DING notification sent for %d bells", bellIndices.size()); LOG_DEBUG(TAG, "BellEngine - 🔔 DING notification sent for %d bells", bellIndices.size());
} catch (...) { } catch (...) {
LOG_WARNING("BellEngine - ❌ Failed to send ding notification"); LOG_WARNING(TAG, "BellEngine - ❌ Failed to send ding notification");
} }
} }
@@ -378,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;
} }

View File

@@ -47,120 +47,465 @@ struct MelodyInfo {
const char* uid; // Unique identifier const char* uid; // Unique identifier
const uint16_t* data; // Pointer to melody data in PROGMEM const uint16_t* data; // Pointer to melody data in PROGMEM
uint16_t stepCount; // Number of steps uint16_t stepCount; // Number of steps
uint16_t defaultSpeed; // Default speed in milliseconds per beat
}; };
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// EXAMPLE MELODIES - Add your melodies here! // BuiltIn Melodies // More can be added here
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// Example: Simple Scale (C-D-E-F-G-A-B-C) // 1 Bell Test Melody
const uint16_t PROGMEM melody_simple_scale[] = { const uint16_t PROGMEM builtin_1bell_test[] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0001, 0x0000, 0x0001, 0x0000
0x0010, 0x0020, 0x0040, 0x0080
}; };
// Example: Happy Birthday (simplified) // Doxology Traditional
const uint16_t PROGMEM melody_happy_birthday[] = { const uint16_t PROGMEM builtin_doxology_traditional[] = {
0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0000, 0x0000,
0x0008, 0x0004, 0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0008, 0x0000, 0x0000,
0x0002, 0x0001, 0x0010, 0x0008, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0001, 0x0080, 0x0008, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008, 0x0000, 0x0000
0x0004, 0x0002, 0x0040, 0x0040,
0x0008, 0x0004, 0x0002
}; };
// Example: Jingle Bells (simplified) // Doxology Alternative
const uint16_t PROGMEM melody_jingle_bells[] = { const uint16_t PROGMEM builtin_doxology_alternative[] = {
0x0004, 0x0004, 0x0004, 0x0000, 0x0001, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001,
0x0004, 0x0004, 0x0004, 0x0000, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0000,
0x0004, 0x0008, 0x0001, 0x0002, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0002, 0x0001,
0x0004, 0x0000, 0x0000, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000
0x0008, 0x0008, 0x0008, 0x0008,
0x0008, 0x0004, 0x0004, 0x0004,
0x0002, 0x0002, 0x0004, 0x0002,
0x0008, 0x0000, 0x0000, 0x0000
}; };
// Example: Westminster Chimes // Doxology Festive
const uint16_t PROGMEM melody_westminster_chimes[] = { const uint16_t PROGMEM builtin_doxology_festive[] = {
0x0008, 0x0004, 0x0002, 0x0001, 0x0002, 0x0004, 0x0009, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004,
0x0001, 0x0002, 0x0008, 0x0004, 0x0002, 0x0004, 0x0021, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004
0x0008, 0x0001, 0x0002, 0x0004,
0x0002, 0x0008, 0x0004, 0x0001
}; };
// Example: Alarm Pattern // Vesper Traditional
const uint16_t PROGMEM melody_alarm[] = { const uint16_t PROGMEM builtin_vesper_traditional[] = {
0x0001, 0x0080, 0x0001, 0x0080, 0x0001, 0x0002, 0x0004, 0x0000, 0x0001, 0x0002, 0x0004, 0x0000,
0x0001, 0x0080, 0x0001, 0x0080, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0000
0x0000, 0x0000, 0x0001, 0x0080,
0x0001, 0x0080, 0x0001, 0x0080
}; };
// Example: Doorbell // Vesper Alternative
const uint16_t PROGMEM melody_doorbell[] = { const uint16_t PROGMEM builtin_vesper_alternative[] = {
0x0004, 0x0008, 0x0004, 0x0008 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
}; };
// Example: Single Bell Test // Catehetical
const uint16_t PROGMEM melody_single_bell[] = { const uint16_t PROGMEM builtin_catehetical[] = {
0x0001 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 // MELODY LIBRARY - Array of all built-in melodies
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
const MelodyInfo MELODY_LIBRARY[] = { const MelodyInfo MELODY_LIBRARY[] = {
{ {
"Simple Scale", "1 Bell Test",
"builtin_scale", "builtin_1bell_test",
melody_simple_scale, builtin_1bell_test,
sizeof(melody_simple_scale) / sizeof(uint16_t), sizeof(builtin_1bell_test) / sizeof(uint16_t)
200 // 200ms per beat
}, },
{ {
"Happy Birthday", "Doxology Traditional",
"builtin_happy_birthday", "builtin_doxology_traditional",
melody_happy_birthday, builtin_doxology_traditional,
sizeof(melody_happy_birthday) / sizeof(uint16_t), sizeof(builtin_doxology_traditional) / sizeof(uint16_t)
250
}, },
{ {
"Jingle Bells", "Doxology Alternative",
"builtin_jingle_bells", "builtin_doxology_alternative",
melody_jingle_bells, builtin_doxology_alternative,
sizeof(melody_jingle_bells) / sizeof(uint16_t), sizeof(builtin_doxology_alternative) / sizeof(uint16_t)
180
}, },
{ {
"Westminster Chimes", "Doxology Festive",
"builtin_westminster", "builtin_doxology_festive",
melody_westminster_chimes, builtin_doxology_festive,
sizeof(melody_westminster_chimes) / sizeof(uint16_t), sizeof(builtin_doxology_festive) / sizeof(uint16_t)
400
}, },
{ {
"Alarm", "Vesper Traditional",
"builtin_alarm", "builtin_vesper_traditional",
melody_alarm, builtin_vesper_traditional,
sizeof(melody_alarm) / sizeof(uint16_t), sizeof(builtin_vesper_traditional) / sizeof(uint16_t)
150
}, },
{ {
"Doorbell", "Vesper Alternative",
"builtin_doorbell", "builtin_vesper_alternative",
melody_doorbell, builtin_vesper_alternative,
sizeof(melody_doorbell) / sizeof(uint16_t), sizeof(builtin_vesper_alternative) / sizeof(uint16_t)
300
}, },
{ {
"Single Bell Test", "Catehetical",
"builtin_single_bell", "builtin_catehetical",
melody_single_bell, builtin_catehetical,
sizeof(melody_single_bell) / sizeof(uint16_t), sizeof(builtin_catehetical) / sizeof(uint16_t)
100 },
{
"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)
} }
}; };
@@ -225,29 +570,10 @@ inline String getBuiltInMelodiesJSON() {
json += "{"; json += "{";
json += "\"name\":\"" + String(MELODY_LIBRARY[i].name) + "\","; json += "\"name\":\"" + String(MELODY_LIBRARY[i].name) + "\",";
json += "\"uid\":\"" + String(MELODY_LIBRARY[i].uid) + "\","; json += "\"uid\":\"" + String(MELODY_LIBRARY[i].uid) + "\",";
json += "\"steps\":" + String(MELODY_LIBRARY[i].stepCount) + ",";
json += "\"speed\":" + String(MELODY_LIBRARY[i].defaultSpeed);
json += "}"; json += "}";
} }
json += "]"; json += "]";
return json; return json;
} }
} // namespace BuiltInMelodies }
// ═══════════════════════════════════════════════════════════════════════════════════
// USAGE EXAMPLE:
// ═══════════════════════════════════════════════════════════════════════════════════
/*
// Check if melody is built-in
if (BuiltInMelodies::isBuiltInMelody(uid)) {
// Load it from firmware
std::vector<uint16_t> melodyData;
if (BuiltInMelodies::loadBuiltInMelody(uid, melodyData)) {
// Use melodyData...
}
} else {
// Load from SD card as usual
}
*/
// ═══════════════════════════════════════════════════════════════════════════════════

View File

@@ -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 initialized !"); LOG_INFO(TAG, "Client Manager initialized !");
} }
ClientManager::~ClientManager() { ClientManager::~ClientManager() {
_clients.clear(); _clients.clear();
LOG_INFO("Client Manager 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_WARNING("Client Manager - 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 Manager - 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 Manager - 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 Manager - 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("Client Manager - 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("Client Manager - 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("Client Manager - 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("Client Manager - 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("Client Manager - 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("Client Manager - 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 {

View File

@@ -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"
@@ -65,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") {
@@ -85,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);
} }
} }
@@ -146,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;
} }
@@ -168,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);
} }
@@ -184,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);
} }
} }
@@ -196,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);
@@ -205,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);
} }
} }
@@ -217,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);
} }
} }
@@ -236,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);
@@ -257,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);
} }
} }
@@ -269,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);
@@ -283,8 +285,10 @@ void CommandHandler::handleSystemInfoCommand(JsonVariant contents, const Message
handleNetworkInfoCommand(context); 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);
} }
} }
@@ -301,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;
} }
@@ -360,14 +364,14 @@ 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");
} }
} }
@@ -378,14 +382,14 @@ void CommandHandler::handleSetRelayOutputsCommand(JsonVariant contents, const Me
bool saved = _configManager.saveBellOutputs(); bool saved = _configManager.saveBellOutputs();
if (saved) { if (saved) {
sendSuccessResponse("set_relay_outputs", "Relay outputs updated and saved", context); sendSuccessResponse("set_relay_outputs", "Relay outputs updated and saved", context);
LOG_INFO("Relay outputs updated and saved successfully"); LOG_INFO(TAG, "Relay outputs updated and saved successfully");
} else { } else {
sendErrorResponse("set_relay_outputs", "Failed to save relay outputs to SD card", context); sendErrorResponse("set_relay_outputs", "Failed to save relay outputs to SD card", context);
LOG_ERROR("Failed to save relay outputs configuration"); 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");
} }
} }
@@ -396,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");
} }
} }
@@ -414,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");
} }
} }
@@ -432,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");
} }
} }
@@ -450,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");
} }
} }
@@ -468,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");
} }
} }
@@ -512,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
@@ -524,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
@@ -541,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);
} }
} }
} }
@@ -583,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);
} }
} }
@@ -599,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;
} }
} }
@@ -624,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;
@@ -656,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) {
@@ -684,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());
@@ -706,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");
} }
} }
@@ -725,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");
} }
} }
@@ -810,7 +822,7 @@ 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) { void CommandHandler::handleNetworkInfoCommand(const MessageContext& context) {
@@ -829,7 +841,7 @@ void CommandHandler::handleNetworkInfoCommand(const MessageContext& 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();
@@ -844,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;
} }
@@ -855,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
@@ -893,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;
@@ -935,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");
} }
} }
@@ -956,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);
@@ -967,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");
} }
} }
@@ -980,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
@@ -988,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");
} }
} }
@@ -1013,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);
@@ -1042,7 +1084,7 @@ void CommandHandler::handleSystemCommand(JsonVariant contents, const MessageCont
} else if (action == "custom_update") { } else if (action == "custom_update") {
handleCustomUpdateCommand(contents, context); 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);
} }
} }
@@ -1070,11 +1112,11 @@ void CommandHandler::handleSetSerialLogLevelCommand(JsonVariant contents, const
if (saved) { if (saved) {
sendSuccessResponse("set_serial_log_level", sendSuccessResponse("set_serial_log_level",
"Serial log level set to " + String(level) + " and saved", context); "Serial log level set to " + String(level) + " and saved", context);
LOG_INFO("Serial log level updated to %d", level); LOG_INFO(TAG, "Serial log level updated to %d", level);
} else { } else {
sendErrorResponse("set_serial_log_level", sendErrorResponse("set_serial_log_level",
"Log level set but failed to save to SD card", context); "Log level set but failed to save to SD card", context);
LOG_ERROR("Failed to save serial log level to SD card"); LOG_ERROR(TAG, "Failed to save serial log level to SD card");
} }
} else { } else {
sendErrorResponse("set_serial_log_level", sendErrorResponse("set_serial_log_level",
@@ -1092,17 +1134,20 @@ void CommandHandler::handleSetSdLogLevelCommand(JsonVariant contents, const Mess
// Set the level in ConfigManager // Set the level in ConfigManager
if (_configManager.setSdLogLevel(level)) { if (_configManager.setSdLogLevel(level)) {
// Apply immediately
Logging::setSdLevel((Logging::LogLevel)level);
// Save to SD card // Save to SD card
bool saved = _configManager.saveGeneralConfig(); bool saved = _configManager.saveGeneralConfig();
if (saved) { if (saved) {
sendSuccessResponse("set_sd_log_level", sendSuccessResponse("set_sd_log_level",
"SD log level set to " + String(level) + " and saved", context); "SD log level set to " + String(level) + " and saved", context);
LOG_INFO("SD log level updated to %d (not yet implemented)", level); LOG_INFO(TAG, "SD log level updated to %d", level);
} else { } else {
sendErrorResponse("set_sd_log_level", sendErrorResponse("set_sd_log_level",
"Log level set but failed to save to SD card", context); "Log level set but failed to save to SD card", context);
LOG_ERROR("Failed to save SD log level to SD card"); LOG_ERROR(TAG, "Failed to save SD log level to SD card");
} }
} else { } else {
sendErrorResponse("set_sd_log_level", sendErrorResponse("set_sd_log_level",
@@ -1129,11 +1174,11 @@ void CommandHandler::handleSetMqttLogLevelCommand(JsonVariant contents, const Me
if (saved) { if (saved) {
sendSuccessResponse("set_mqtt_log_level", sendSuccessResponse("set_mqtt_log_level",
"MQTT log level set to " + String(level) + " and saved", context); "MQTT log level set to " + String(level) + " and saved", context);
LOG_INFO("MQTT log level updated to %d", level); LOG_INFO(TAG, "MQTT log level updated to %d", level);
} else { } else {
sendErrorResponse("set_mqtt_log_level", sendErrorResponse("set_mqtt_log_level",
"Log level set but failed to save to SD card", context); "Log level set but failed to save to SD card", context);
LOG_ERROR("Failed to save MQTT log level to SD card"); LOG_ERROR(TAG, "Failed to save MQTT log level to SD card");
} }
} else { } else {
sendErrorResponse("set_mqtt_log_level", sendErrorResponse("set_mqtt_log_level",
@@ -1158,7 +1203,7 @@ void CommandHandler::handleSetMqttEnabledCommand(JsonVariant contents, const Mes
if (saved) { if (saved) {
sendSuccessResponse("set_mqtt_enabled", sendSuccessResponse("set_mqtt_enabled",
String("MQTT ") + (enabled ? "enabled" : "disabled") + " and saved", context); String("MQTT ") + (enabled ? "enabled" : "disabled") + " and saved", context);
LOG_INFO("MQTT %s by user command", enabled ? "enabled" : "disabled"); LOG_INFO(TAG, "MQTT %s by user command", enabled ? "enabled" : "disabled");
// If disabling, disconnect MQTT immediately // If disabling, disconnect MQTT immediately
// If enabling, trigger connection attempt // If enabling, trigger connection attempt
@@ -1169,12 +1214,12 @@ void CommandHandler::handleSetMqttEnabledCommand(JsonVariant contents, const Mes
_communicationRouter->getMQTTClient().connect(); _communicationRouter->getMQTTClient().connect();
} }
} else { } else {
LOG_WARNING("CommunicationRouter reference not set - cannot control MQTT"); LOG_WARNING(TAG, "CommunicationRouter reference not set - cannot control MQTT");
} }
} else { } else {
sendErrorResponse("set_mqtt_enabled", sendErrorResponse("set_mqtt_enabled",
"MQTT state changed but failed to save to SD card", context); "MQTT state changed but failed to save to SD card", context);
LOG_ERROR("Failed to save MQTT enabled state to SD card"); LOG_ERROR(TAG, "Failed to save MQTT enabled state to SD card");
} }
} }
@@ -1183,7 +1228,7 @@ void CommandHandler::handleSetMqttEnabledCommand(JsonVariant contents, const Mes
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
void CommandHandler::handleRestartCommand(const MessageContext& context) { void CommandHandler::handleRestartCommand(const MessageContext& context) {
LOG_WARNING("🔄 Device restart requested via command"); LOG_WARNING(TAG, "🔄 Device restart requested via command");
sendSuccessResponse("restart", "Device will restart in 2 seconds", context); sendSuccessResponse("restart", "Device will restart in 2 seconds", context);
// Small delay to ensure response is sent // Small delay to ensure response is sent
@@ -1198,12 +1243,12 @@ void CommandHandler::handleRestartCommand(const MessageContext& context) {
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
void CommandHandler::handleForceUpdateCommand(JsonVariant contents, const MessageContext& context) { void CommandHandler::handleForceUpdateCommand(JsonVariant contents, const MessageContext& context) {
LOG_WARNING("🔄 Force OTA update requested via command"); LOG_WARNING(TAG, "🔄 Force OTA update requested via command");
// Check if player is active // Check if player is active
if (_player && _player->isCurrentlyPlaying()) { if (_player && _player->isCurrentlyPlaying()) {
sendErrorResponse("force_update", "Cannot update while playback is active", context); sendErrorResponse("force_update", "Cannot update while playback is active", context);
LOG_WARNING("Force update rejected - player is active"); LOG_WARNING(TAG, "Force update rejected - player is active");
return; return;
} }
@@ -1224,7 +1269,7 @@ void CommandHandler::handleForceUpdateCommand(JsonVariant contents, const Messag
// Note: If update succeeds, device will reboot and this won't be reached // Note: If update succeeds, device will reboot and this won't be reached
if (!result) { if (!result) {
LOG_ERROR("Force update failed"); LOG_ERROR(TAG, "Force update failed");
// Error response may not be received if we already restarted // Error response may not be received if we already restarted
} }
} }
@@ -1234,7 +1279,7 @@ void CommandHandler::handleForceUpdateCommand(JsonVariant contents, const Messag
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const MessageContext& context) { void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const MessageContext& context) {
LOG_WARNING("🔥 Custom OTA update requested via command"); LOG_WARNING(TAG, "🔥 Custom OTA update requested via command");
// Validate required parameters // Validate required parameters
if (!contents.containsKey("firmware_url")) { if (!contents.containsKey("firmware_url")) {
@@ -1249,18 +1294,21 @@ void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const Messa
contents["checksum"].as<String>() : ""; contents["checksum"].as<String>() : "";
size_t fileSize = contents.containsKey("file_size") ? size_t fileSize = contents.containsKey("file_size") ?
contents["file_size"].as<size_t>() : 0; contents["file_size"].as<size_t>() : 0;
uint16_t version = contents.containsKey("version") ?
contents["version"].as<uint16_t>() : 0;
// Check if player is active // Check if player is active
if (_player && _player->isCurrentlyPlaying()) { if (_player && _player->isCurrentlyPlaying()) {
sendErrorResponse("custom_update", "Cannot update while playback is active", context); sendErrorResponse("custom_update", "Cannot update while playback is active", context);
LOG_WARNING("Custom update rejected - player is active"); LOG_WARNING(TAG, "Custom update rejected - player is active");
return; return;
} }
LOG_INFO("Custom update: URL=%s, Checksum=%s, Size=%u", LOG_INFO(TAG, "Custom update: URL=%s, Checksum=%s, Size=%u, Version=%u",
firmwareUrl.c_str(), firmwareUrl.c_str(),
checksum.isEmpty() ? "none" : checksum.c_str(), checksum.isEmpty() ? "none" : checksum.c_str(),
fileSize); fileSize,
version);
sendSuccessResponse("custom_update", sendSuccessResponse("custom_update",
"Starting custom OTA update. Device may reboot.", context); "Starting custom OTA update. Device may reboot.", context);
@@ -1269,11 +1317,11 @@ void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const Messa
delay(1000); delay(1000);
// Perform the custom update // Perform the custom update
bool result = _otaManager.performCustomUpdate(firmwareUrl, checksum, fileSize); bool result = _otaManager.performCustomUpdate(firmwareUrl, checksum, fileSize, version);
// Note: If update succeeds, device will reboot and this won't be reached // Note: If update succeeds, device will reboot and this won't be reached
if (!result) { if (!result) {
LOG_ERROR("Custom update failed"); LOG_ERROR(TAG, "Custom update failed");
// Error response may not be received if we already restarted // Error response may not be received if we already restarted
} }
} }

View File

@@ -41,7 +41,8 @@ public:
// Message source identification // Message source identification
enum class MessageSource { enum class MessageSource {
MQTT, MQTT,
WEBSOCKET WEBSOCKET,
UART
}; };
struct MessageContext { struct MessageContext {
@@ -139,6 +140,7 @@ private:
void handleGetFirmwareStatusCommand(const MessageContext& context); void handleGetFirmwareStatusCommand(const MessageContext& context);
void handleNetworkInfoCommand(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);

View File

@@ -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"
@@ -33,16 +35,17 @@ CommunicationRouter::CommunicationRouter(ConfigManager& configManager,
, _wsServer(webSocket, _clientManager) , _wsServer(webSocket, _clientManager)
, _commandHandler(configManager, otaManager) , _commandHandler(configManager, otaManager)
, _httpHandler(server, configManager) , _httpHandler(server, configManager)
, _uartHandler()
, _settingsServer(server, configManager, networking) {} , _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();
@@ -53,11 +56,11 @@ 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);
@@ -72,23 +75,36 @@ void CommunicationRouter::begin() {
logTopic logTopic
); );
// Apply MQTT log level from config // Apply log levels from config for all three channels
uint8_t mqttLogLevel = _configManager.getMqttLogLevel(); Logging::setSerialLevel((Logging::LogLevel)_configManager.getSerialLogLevel());
Logging::setMqttLogLevel((Logging::LogLevel)mqttLogLevel); Logging::setMqttLevel((Logging::LogLevel)_configManager.getMqttLogLevel());
LOG_INFO("MQTT logging enabled with level %d on topic: %s", mqttLogLevel, logTopic.c_str()); Logging::setSdLevel((Logging::LogLevel)_configManager.getSdLogLevel());
LOG_INFO(TAG, "Log levels applied — Serial:%d MQTT:%d SD:%d",
_configManager.getSerialLogLevel(),
_configManager.getMqttLogLevel(),
_configManager.getSdLogLevel());
LOG_INFO("✅ MQTT client initialized"); // 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 // 🔥 Set CommunicationRouter reference for MQTT control commands
_commandHandler.setCommunicationRouterReference(this); _commandHandler.setCommunicationRouterReference(this);
LOG_INFO("CommunicationRouter reference set for CommandHandler"); 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) {
@@ -96,21 +112,35 @@ void CommunicationRouter::begin() {
}); });
// Initialize HTTP Request Handler // Initialize HTTP Request Handler
LOG_INFO("Setting up HTTP REST API..."); LOG_INFO(TAG, "Setting up HTTP REST API...");
_httpHandler.begin(); _httpHandler.begin();
_httpHandler.setCommandHandlerReference(&_commandHandler); _httpHandler.setCommandHandlerReference(&_commandHandler);
LOG_INFO("✅ HTTP REST API initialized"); LOG_INFO(TAG, "✅ HTTP REST API initialized");
// Initialize Settings Web Server // Initialize Settings Web Server
LOG_INFO("Setting up Settings Web Server..."); LOG_INFO(TAG, "Setting up Settings Web Server...");
_settingsServer.begin(); _settingsServer.begin();
LOG_INFO("✅ Settings Web Server initialized at /settings"); LOG_INFO(TAG, "✅ Settings Web Server initialized at /settings");
LOG_INFO("Communication Router initialized with modular architecture"); // Initialize UART Command Handler
LOG_INFO(" • MQTT: AsyncMqttClient"); LOG_INFO(TAG, "Setting up UART Command Handler...");
LOG_INFO(" • WebSocket: Multi-client support"); _uartHandler.begin();
LOG_INFO(" • HTTP REST API: /api endpoints"); _uartHandler.setCallback([this](JsonDocument& message) {
LOG_INFO(" • Settings Page: /settings"); 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) {
@@ -121,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) {
@@ -140,11 +178,11 @@ void CommunicationRouter::setTelemetryReference(Telemetry* 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());
@@ -185,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.");
} }
} }
@@ -204,22 +242,24 @@ 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; 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;
}
// 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)",
isMqttConnected(), hasActiveWebSocketClients());
return false; return false;
} }
// Check network connectivity
if (!_networking.isConnected()) {
LOG_DEBUG("CommunicationRouter: Unhealthy - No network connection");
return false;
}
return true; return true;
} }
@@ -255,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());
} }
} }
@@ -279,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;
} }
@@ -315,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);
@@ -324,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!");
} }
} }

View File

@@ -39,6 +39,7 @@
#include "../CommandHandler/CommandHandler.hpp" #include "../CommandHandler/CommandHandler.hpp"
#include "../ResponseBuilder/ResponseBuilder.hpp" #include "../ResponseBuilder/ResponseBuilder.hpp"
#include "../HTTPRequestHandler/HTTPRequestHandler.hpp" #include "../HTTPRequestHandler/HTTPRequestHandler.hpp"
#include "../UARTCommandHandler/UARTCommandHandler.hpp"
#include "../../ClientManager/ClientManager.hpp" #include "../../ClientManager/ClientManager.hpp"
#include "../../SettingsWebServer/SettingsWebServer.hpp" #include "../../SettingsWebServer/SettingsWebServer.hpp"
@@ -63,6 +64,7 @@ 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);
@@ -78,6 +80,7 @@ public:
// Component accessors // Component accessors
MQTTAsyncClient& getMQTTClient() { return _mqttClient; } MQTTAsyncClient& getMQTTClient() { return _mqttClient; }
UARTCommandHandler& getUARTHandler() { return _uartHandler; }
// Broadcast methods // Broadcast methods
void broadcastStatus(const String& statusMessage); void broadcastStatus(const String& statusMessage);
@@ -116,11 +119,13 @@ private:
WebSocketServer _wsServer; WebSocketServer _wsServer;
CommandHandler _commandHandler; CommandHandler _commandHandler;
HTTPRequestHandler _httpHandler; HTTPRequestHandler _httpHandler;
UARTCommandHandler _uartHandler;
SettingsWebServer _settingsServer; 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);

View File

@@ -5,6 +5,8 @@
*/ */
#include "HTTPRequestHandler.hpp" #include "HTTPRequestHandler.hpp"
#define TAG "HTTPHandler"
#include "../CommandHandler/CommandHandler.hpp" #include "../CommandHandler/CommandHandler.hpp"
#include "../../ConfigManager/ConfigManager.hpp" #include "../../ConfigManager/ConfigManager.hpp"
#include "../../Logging/Logging.hpp" #include "../../Logging/Logging.hpp"
@@ -20,7 +22,7 @@ HTTPRequestHandler::~HTTPRequestHandler() {
} }
void HTTPRequestHandler::begin() { void HTTPRequestHandler::begin() {
LOG_INFO("HTTPRequestHandler - Initializing HTTP REST API endpoints"); LOG_INFO(TAG, "HTTPRequestHandler - Initializing HTTP REST API endpoints");
// POST /api/command - Execute any command // POST /api/command - Execute any command
_server.on("/api/command", HTTP_POST, _server.on("/api/command", HTTP_POST,
@@ -61,15 +63,15 @@ void HTTPRequestHandler::begin() {
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "Content-Type");
LOG_INFO("HTTPRequestHandler - REST API endpoints registered"); LOG_INFO(TAG, "HTTPRequestHandler - REST API endpoints registered");
LOG_INFO(" POST /api/command - Execute commands"); LOG_INFO(TAG, " POST /api/command - Execute commands");
LOG_INFO(" GET /api/status - System status"); LOG_INFO(TAG, " GET /api/status - System status");
LOG_INFO(" GET /api/ping - Health check"); LOG_INFO(TAG, " GET /api/ping - Health check");
} }
void HTTPRequestHandler::setCommandHandlerReference(CommandHandler* handler) { void HTTPRequestHandler::setCommandHandlerReference(CommandHandler* handler) {
_commandHandler = handler; _commandHandler = handler;
LOG_DEBUG("HTTPRequestHandler - CommandHandler reference set"); LOG_DEBUG(TAG, "HTTPRequestHandler - CommandHandler reference set");
} }
bool HTTPRequestHandler::isHealthy() const { bool HTTPRequestHandler::isHealthy() const {
@@ -88,12 +90,12 @@ void HTTPRequestHandler::handleCommandRequest(AsyncWebServerRequest* request, ui
DeserializationError error = deserializeJson(doc, data, len); DeserializationError error = deserializeJson(doc, data, len);
if (error) { if (error) {
LOG_WARNING("HTTPRequestHandler - JSON parse error: %s", error.c_str()); LOG_WARNING(TAG, "HTTPRequestHandler - JSON parse error: %s", error.c_str());
sendErrorResponse(request, 400, "Invalid JSON"); sendErrorResponse(request, 400, "Invalid JSON");
return; return;
} }
LOG_DEBUG("HTTPRequestHandler - Processing command via HTTP"); LOG_DEBUG(TAG, "HTTPRequestHandler - Processing command via HTTP");
// Create message context for HTTP (treat as WebSocket with special ID) // Create message context for HTTP (treat as WebSocket with special ID)
CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, 0xFFFFFFFF); CommandHandler::MessageContext context(CommandHandler::MessageSource::WEBSOCKET, 0xFFFFFFFF);
@@ -129,7 +131,7 @@ void HTTPRequestHandler::handleStatusRequest(AsyncWebServerRequest* request) {
return; return;
} }
LOG_DEBUG("HTTPRequestHandler - Status request via HTTP"); LOG_DEBUG(TAG, "HTTPRequestHandler - Status request via HTTP");
// Create a status command // Create a status command
JsonDocument doc; JsonDocument doc;
@@ -160,7 +162,7 @@ void HTTPRequestHandler::handleStatusRequest(AsyncWebServerRequest* request) {
} }
void HTTPRequestHandler::handlePingRequest(AsyncWebServerRequest* request) { void HTTPRequestHandler::handlePingRequest(AsyncWebServerRequest* request) {
LOG_DEBUG("HTTPRequestHandler - Ping request via HTTP"); LOG_DEBUG(TAG, "HTTPRequestHandler - Ping request via HTTP");
JsonDocument response; JsonDocument response;
response["status"] = "ok"; response["status"] = "ok";

View File

@@ -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"
@@ -66,7 +68,7 @@ MQTTAsyncClient::~MQTTAsyncClient() {
} }
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();
@@ -76,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) {
@@ -110,7 +112,7 @@ 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() {
@@ -118,28 +120,28 @@ void MQTTAsyncClient::connect() {
// 🔥 Check if MQTT is enabled // 🔥 Check if MQTT is enabled
if (!mqttConfig.enabled) { if (!mqttConfig.enabled) {
LOG_DEBUG("MQTT is disabled in configuration - skipping connection"); LOG_DEBUG(TAG, "MQTT is disabled in configuration - skipping connection");
return; return;
} }
if (_mqttClient.connected()) { if (_mqttClient.connected()) {
LOG_DEBUG("Already connected to MQTT"); LOG_DEBUG(TAG, "Already connected to MQTT");
return; return;
} }
// Track connection attempt // Track connection attempt
_lastConnectionAttempt = millis(); _lastConnectionAttempt = millis();
LOG_INFO("Free heap BEFORE MQTT connect: %d bytes", ESP.getFreeHeap()); 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) {
@@ -155,7 +157,7 @@ uint16_t MQTTAsyncClient::publish(const String& topic, const String& payload, in
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);
} }
// REMOVED: Error logging here to prevent infinite recursion with MQTT logs // REMOVED: Error logging here to prevent infinite recursion with MQTT logs
@@ -175,11 +177,11 @@ void MQTTAsyncClient::onNetworkConnected() {
// 🔥 Only attempt connection if MQTT is enabled // 🔥 Only attempt connection if MQTT is enabled
if (!mqttConfig.enabled) { if (!mqttConfig.enabled) {
LOG_DEBUG("Network connected but MQTT is disabled - skipping MQTT connection"); LOG_DEBUG(TAG, "Network connected but MQTT is disabled - skipping MQTT connection");
return; return;
} }
LOG_DEBUG("Network connected - scheduling MQTT connection after 2s stabilization (non-blocking)"); LOG_DEBUG(TAG, "Network connected - scheduling MQTT connection after 2s stabilization (non-blocking)");
// Reset reconnect attempts on fresh network connection // Reset reconnect attempts on fresh network connection
_reconnectAttempts = 0; _reconnectAttempts = 0;
@@ -189,14 +191,14 @@ void MQTTAsyncClient::onNetworkConnected() {
if (_networkStabilizationTimer) { if (_networkStabilizationTimer) {
xTimerStart(_networkStabilizationTimer, 0); xTimerStart(_networkStabilizationTimer, 0);
} else { } else {
LOG_ERROR("Network stabilization timer not initialized!"); LOG_ERROR(TAG, "Network stabilization timer not initialized!");
// Fallback to immediate connection (better than blocking) // 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);
@@ -205,12 +207,12 @@ 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 // Reset reconnection attempts on successful connection
_reconnectAttempts = 0; _reconnectAttempts = 0;
@@ -250,14 +252,14 @@ 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 // Stop heartbeat timer when disconnected
stopHeartbeat(); stopHeartbeat();
// 🔥 Don't attempt reconnection if MQTT is disabled // 🔥 Don't attempt reconnection if MQTT is disabled
if (!mqttConfig.enabled) { if (!mqttConfig.enabled) {
LOG_INFO("MQTT is disabled - not attempting reconnection"); LOG_INFO(TAG, "MQTT is disabled - not attempting reconnection");
return; return;
} }
@@ -268,24 +270,24 @@ void MQTTAsyncClient::onMqttDisconnect(AsyncMqttClientDisconnectReason reason) {
// Calculate backoff delay // Calculate backoff delay
unsigned long reconnectDelay = getReconnectDelay(); unsigned long reconnectDelay = getReconnectDelay();
LOG_INFO("Network still connected - scheduling MQTT reconnection #%d in %lu seconds (backoff active)", LOG_INFO(TAG, "Network still connected - scheduling MQTT reconnection #%d in %lu seconds (backoff active)",
_reconnectAttempts, reconnectDelay / 1000); _reconnectAttempts, reconnectDelay / 1000);
// Update timer period with new delay // Update timer period with new delay
xTimerChangePeriod(_mqttReconnectTimer, pdMS_TO_TICKS(reconnectDelay), 0); 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) {
@@ -293,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) {
@@ -302,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");
} }
} }
@@ -331,7 +333,7 @@ void MQTTAsyncClient::mqttReconnectTimerCallback(TimerHandle_t xTimer) {
void MQTTAsyncClient::startHeartbeat() { void MQTTAsyncClient::startHeartbeat() {
if (_heartbeatTimer) { if (_heartbeatTimer) {
LOG_INFO("💓 Starting MQTT heartbeat (every %d seconds)", HEARTBEAT_INTERVAL / 1000); LOG_INFO(TAG, "💓 Starting MQTT heartbeat (every %d seconds)", HEARTBEAT_INTERVAL / 1000);
// Publish first heartbeat immediately // Publish first heartbeat immediately
publishHeartbeat(); publishHeartbeat();
@@ -344,13 +346,13 @@ void MQTTAsyncClient::startHeartbeat() {
void MQTTAsyncClient::stopHeartbeat() { void MQTTAsyncClient::stopHeartbeat() {
if (_heartbeatTimer) { if (_heartbeatTimer) {
xTimerStop(_heartbeatTimer, 0); xTimerStop(_heartbeatTimer, 0);
LOG_INFO("❤️ Stopped MQTT heartbeat"); LOG_INFO(TAG, "❤️ Stopped MQTT heartbeat");
} }
} }
void MQTTAsyncClient::publishHeartbeat() { void MQTTAsyncClient::publishHeartbeat() {
if (!_mqttClient.connected()) { if (!_mqttClient.connected()) {
LOG_WARNING("⚠️ Cannot publish heartbeat - MQTT not connected"); LOG_WARNING(TAG, "⚠️ Cannot publish heartbeat - MQTT not connected");
return; return;
} }
@@ -397,10 +399,10 @@ void MQTTAsyncClient::publishHeartbeat() {
uint16_t packetId = _mqttClient.publish(heartbeatTopic.c_str(), 1, true, heartbeatMessage.c_str()); uint16_t packetId = _mqttClient.publish(heartbeatTopic.c_str(), 1, true, heartbeatMessage.c_str());
if (packetId > 0) { if (packetId > 0) {
LOG_DEBUG("💓 Published heartbeat (retained) - IP: %s, Uptime: %lums", LOG_DEBUG(TAG, "💓 Published heartbeat (retained) - IP: %s, Uptime: %lums",
_networking.getLocalIP().c_str(), uptimeMs); _networking.getLocalIP().c_str(), uptimeMs);
} else { } else {
LOG_ERROR("❌ Failed to publish heartbeat"); LOG_ERROR(TAG, "❌ Failed to publish heartbeat");
} }
} }
@@ -415,7 +417,7 @@ void MQTTAsyncClient::heartbeatTimerCallback(TimerHandle_t xTimer) {
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════
void MQTTAsyncClient::connectAfterStabilization() { void MQTTAsyncClient::connectAfterStabilization() {
LOG_DEBUG("Network stabilization complete - connecting to MQTT"); LOG_DEBUG(TAG, "Network stabilization complete - connecting to MQTT");
connect(); connect();
} }

View File

@@ -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
@@ -72,7 +74,7 @@ String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeEla
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;
} }
@@ -135,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;
} }
@@ -149,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;
} }

View File

@@ -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);
}

View File

@@ -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();
};

View File

@@ -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());
} }
} }

View File

@@ -1,4 +1,6 @@
#include "ConfigManager.hpp" #include "ConfigManager.hpp"
#define TAG "ConfigManager"
#include "../../src/Logging/Logging.hpp" #include "../../src/Logging/Logging.hpp"
#include <WiFi.h> // For MAC address generation #include <WiFi.h> // For MAC address generation
#include <time.h> // For timestamp generation #include <time.h> // For timestamp generation
@@ -27,7 +29,7 @@ void ConfigManager::initializeCleanDefaults() {
// Set MQTT user to deviceUID for unique identification // Set MQTT user to deviceUID for unique identification
mqttConfig.user = deviceConfig.deviceUID; mqttConfig.user = deviceConfig.deviceUID;
LOG_DEBUG("ConfigManager - Clean defaults initialized with auto-generated identifiers"); LOG_DEBUG(TAG, "ConfigManager - Clean defaults initialized with auto-generated identifiers");
} }
void ConfigManager::generateNetworkIdentifiers() { void ConfigManager::generateNetworkIdentifiers() {
@@ -36,7 +38,7 @@ void ConfigManager::generateNetworkIdentifiers() {
networkConfig.apSsid = "BellSystems-Setup-" + deviceConfig.deviceUID; networkConfig.apSsid = "BellSystems-Setup-" + deviceConfig.deviceUID;
LOG_DEBUG("ConfigManager - Generated hostname: %s, AP SSID: %s", LOG_DEBUG(TAG, "ConfigManager - Generated hostname: %s, AP SSID: %s",
networkConfig.hostname.c_str(), networkConfig.apSsid.c_str()); networkConfig.hostname.c_str(), networkConfig.apSsid.c_str());
} }
@@ -49,11 +51,11 @@ void ConfigManager::createDefaultBellConfig() {
} }
bool ConfigManager::begin() { bool ConfigManager::begin() {
LOG_INFO("ConfigManager - ✅ Initializing..."); LOG_INFO(TAG, "ConfigManager - ✅ Initializing...");
// Step 1: Initialize NVS for device identity (factory-set, permanent) // Step 1: Initialize NVS for device identity (factory-set, permanent)
if (!initializeNVS()) { if (!initializeNVS()) {
LOG_ERROR("ConfigManager - ❌ NVS initialization failed, using empty defaults"); LOG_ERROR(TAG, "ConfigManager - ❌ NVS initialization failed, using empty defaults");
} else { } else {
// Load device identity from NVS (deviceUID, hwType, hwVersion) // Load device identity from NVS (deviceUID, hwType, hwVersion)
loadDeviceIdentityFromNVS(); loadDeviceIdentityFromNVS();
@@ -64,58 +66,58 @@ bool ConfigManager::begin() {
// Step 3: Initialize SD card for user-configurable settings // Step 3: Initialize SD card for user-configurable settings
if (!ensureSDCard()) { if (!ensureSDCard()) {
LOG_ERROR("ConfigManager - ❌ SD Card initialization failed, using defaults"); LOG_ERROR(TAG, "ConfigManager - ❌ SD Card initialization failed, using defaults");
return false; return false;
} }
// Step 5: Load update servers list // Step 5: Load update servers list
if (!loadUpdateServers()) { if (!loadUpdateServers()) {
LOG_WARNING("ConfigManager - ⚠️ Could not load update servers - using fallback only"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Could not load update servers - using fallback only");
} }
// Load network config, save defaults if not found // Load network config, save defaults if not found
if (!loadNetworkConfig()) { if (!loadNetworkConfig()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default network config file"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default network config file");
saveNetworkConfig(); saveNetworkConfig();
} }
// Load time config, save defaults if not found // Load time config, save defaults if not found
if (!loadTimeConfig()) { if (!loadTimeConfig()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default time config file (GMT+2)"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default time config file (GMT+2)");
saveTimeConfig(); saveTimeConfig();
} }
// Load bell durations, save defaults if not found // Load bell durations, save defaults if not found
if (!loadBellDurations()) { if (!loadBellDurations()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default bell durations file"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default bell durations file");
saveBellDurations(); saveBellDurations();
} }
// Load bell outputs, save defaults if not found // Load bell outputs, save defaults if not found
if (!loadBellOutputs()) { if (!loadBellOutputs()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default bell outputs file"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default bell outputs file");
saveBellOutputs(); saveBellOutputs();
} }
// Load clock config, save defaults if not found // Load clock config, save defaults if not found
if (!loadClockConfig()) { if (!loadClockConfig()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default clock config file"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default clock config file");
saveClockConfig(); saveClockConfig();
} }
// Load clock state, save defaults if not found // Load clock state, save defaults if not found
if (!loadClockState()) { if (!loadClockState()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default clock state file"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default clock state file");
saveClockState(); saveClockState();
} }
if (!loadGeneralConfig()) { if (!loadGeneralConfig()) {
LOG_WARNING("ConfigManager - ⚠️ Creating default general config file"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Creating default general config file");
saveGeneralConfig(); saveGeneralConfig();
} }
LOG_INFO("ConfigManager - ✅ Initialization Complete ! UID: %s, Hostname: %s", LOG_INFO(TAG, "ConfigManager - ✅ Initialization Complete ! UID: %s, Hostname: %s",
deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str()); deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str());
return true; return true;
} }
@@ -127,29 +129,29 @@ bool ConfigManager::begin() {
bool ConfigManager::initializeNVS() { bool ConfigManager::initializeNVS() {
esp_err_t err = nvs_flash_init(); esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) { if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
LOG_WARNING("ConfigManager - ⚠️ NVS partition truncated, erasing..."); LOG_WARNING(TAG, "ConfigManager - ⚠️ NVS partition truncated, erasing...");
ESP_ERROR_CHECK(nvs_flash_erase()); ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init(); err = nvs_flash_init();
} }
if (err != ESP_OK) { if (err != ESP_OK) {
LOG_ERROR("ConfigManager - ❌ Failed to initialize NVS flash: %s", esp_err_to_name(err)); LOG_ERROR(TAG, "ConfigManager - ❌ 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("ConfigManager - ❌ Failed to open NVS handle: %s", esp_err_to_name(err)); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to open NVS handle: %s", esp_err_to_name(err));
return false; return false;
} }
LOG_DEBUG("ConfigManager - NVS initialized successfully"); LOG_DEBUG(TAG, "ConfigManager - NVS initialized successfully");
return true; return true;
} }
bool ConfigManager::loadDeviceIdentityFromNVS() { bool ConfigManager::loadDeviceIdentityFromNVS() {
if (nvsHandle == 0) { if (nvsHandle == 0) {
LOG_ERROR("ConfigManager - ❌ NVS not initialized, cannot load device identity"); LOG_ERROR(TAG, "ConfigManager - ❌ NVS not initialized, cannot load device identity");
return false; return false;
} }
@@ -160,21 +162,21 @@ bool ConfigManager::loadDeviceIdentityFromNVS() {
// Validate that factory identity exists // Validate that factory identity exists
if (deviceConfig.deviceUID.isEmpty() || deviceConfig.hwType.isEmpty() || deviceConfig.hwVersion.isEmpty()) { if (deviceConfig.deviceUID.isEmpty() || deviceConfig.hwType.isEmpty() || deviceConfig.hwVersion.isEmpty()) {
LOG_ERROR("═══════════════════════════════════════════════════════════════════════════"); LOG_ERROR(TAG, "═══════════════════════════════════════════════════════════════════════════");
LOG_ERROR(" ⚠️ CRITICAL: DEVICE IDENTITY NOT FOUND IN NVS"); LOG_ERROR(TAG, " ⚠️ CRITICAL: DEVICE IDENTITY NOT FOUND IN NVS");
LOG_ERROR(" ⚠️ This device has NOT been factory-programmed!"); LOG_ERROR(TAG, " ⚠️ This device has NOT been factory-programmed!");
LOG_ERROR(" ⚠️ Please flash factory firmware to set device identity"); LOG_ERROR(TAG, " ⚠️ Please flash factory firmware to set device identity");
LOG_ERROR("═══════════════════════════════════════════════════════════════════════════"); LOG_ERROR(TAG, "═══════════════════════════════════════════════════════════════════════════");
return false; return false;
} }
LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(TAG, "═══════════════════════════════════════════════════════════════════════════");
LOG_INFO(" 🏭 FACTORY DEVICE IDENTITY LOADED FROM NVS (READ-ONLY)"); LOG_INFO(TAG, " 🏭 FACTORY DEVICE IDENTITY LOADED FROM NVS (READ-ONLY)");
LOG_INFO(" 🆔 Device UID: %s", deviceConfig.deviceUID.c_str()); LOG_INFO(TAG, " 🆔 Device UID: %s", deviceConfig.deviceUID.c_str());
LOG_INFO(" 🔧 Hardware Type: %s", deviceConfig.hwType.c_str()); LOG_INFO(TAG, " 🔧 Hardware Type: %s", deviceConfig.hwType.c_str());
LOG_INFO(" 📐 Hardware Version: %s", deviceConfig.hwVersion.c_str()); LOG_INFO(TAG, " 📐 Hardware Version: %s", deviceConfig.hwVersion.c_str());
LOG_INFO(" 🔒 These values are PERMANENT and cannot be changed by production firmware"); LOG_INFO(TAG, " 🔒 These values are PERMANENT and cannot be changed by production firmware");
LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(TAG, "═══════════════════════════════════════════════════════════════════════════");
return true; return true;
} }
@@ -185,7 +187,7 @@ bool ConfigManager::loadDeviceIdentityFromNVS() {
String ConfigManager::readNVSString(const char* key, const String& defaultValue) { String ConfigManager::readNVSString(const char* key, const String& defaultValue) {
if (nvsHandle == 0) { if (nvsHandle == 0) {
LOG_ERROR("ConfigManager - ❌ NVS not initialized, returning default for key: %s", key); LOG_ERROR(TAG, "ConfigManager - ❌ NVS not initialized, returning default for key: %s", key);
return defaultValue; return defaultValue;
} }
@@ -193,12 +195,12 @@ String ConfigManager::readNVSString(const char* key, const String& defaultValue)
esp_err_t err = nvs_get_str(nvsHandle, key, NULL, &required_size); esp_err_t err = nvs_get_str(nvsHandle, key, NULL, &required_size);
if (err == ESP_ERR_NVS_NOT_FOUND) { if (err == ESP_ERR_NVS_NOT_FOUND) {
LOG_DEBUG("ConfigManager - NVS key '%s' not found, using default: %s", key, defaultValue.c_str()); LOG_DEBUG(TAG, "ConfigManager - NVS key '%s' not found, using default: %s", key, defaultValue.c_str());
return defaultValue; return defaultValue;
} }
if (err != ESP_OK) { if (err != ESP_OK) {
LOG_ERROR("ConfigManager - ❌ Error reading NVS key '%s': %s", key, esp_err_to_name(err)); LOG_ERROR(TAG, "ConfigManager - ❌ Error reading NVS key '%s': %s", key, esp_err_to_name(err));
return defaultValue; return defaultValue;
} }
@@ -206,7 +208,7 @@ String ConfigManager::readNVSString(const char* key, const String& defaultValue)
err = nvs_get_str(nvsHandle, key, buffer, &required_size); err = nvs_get_str(nvsHandle, key, buffer, &required_size);
if (err != ESP_OK) { if (err != ESP_OK) {
LOG_ERROR("ConfigManager - ❌ Error reading NVS value for key '%s': %s", key, esp_err_to_name(err)); LOG_ERROR(TAG, "ConfigManager - ❌ Error reading NVS value for key '%s': %s", key, esp_err_to_name(err));
delete[] buffer; delete[] buffer;
return defaultValue; return defaultValue;
} }
@@ -214,7 +216,7 @@ String ConfigManager::readNVSString(const char* key, const String& defaultValue)
String result = String(buffer); String result = String(buffer);
delete[] buffer; delete[] buffer;
LOG_VERBOSE("ConfigManager - Read NVS key '%s': %s", key, result.c_str()); LOG_VERBOSE(TAG, "ConfigManager - Read NVS key '%s': %s", key, result.c_str());
return result; return result;
} }
@@ -226,7 +228,7 @@ bool ConfigManager::ensureSDCard() {
if (!sdInitialized) { if (!sdInitialized) {
sdInitialized = SD.begin(hardwareConfig.sdChipSelect); sdInitialized = SD.begin(hardwareConfig.sdChipSelect);
if (!sdInitialized) { if (!sdInitialized) {
LOG_ERROR("ConfigManager - ❌ SD Card not available"); LOG_ERROR(TAG, "ConfigManager - ❌ SD Card not available");
} }
} }
return sdInitialized; return sdInitialized;
@@ -258,12 +260,12 @@ bool ConfigManager::saveDeviceConfig() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize device config JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize device config JSON");
return false; return false;
} }
saveFileToSD("/settings", "deviceConfig.json", buffer); saveFileToSD("/settings", "deviceConfig.json", buffer);
LOG_DEBUG("ConfigManager - Device config saved - FwVer: %s", deviceConfig.fwVersion.c_str()); LOG_DEBUG(TAG, "ConfigManager - Device config saved - FwVer: %s", deviceConfig.fwVersion.c_str());
return true; return true;
} }
@@ -272,7 +274,7 @@ bool ConfigManager::loadDeviceConfig() {
File file = SD.open("/settings/deviceConfig.json", FILE_READ); File file = SD.open("/settings/deviceConfig.json", FILE_READ);
if (!file) { if (!file) {
LOG_WARNING("ConfigManager - ⚠️ Device config file not found - using firmware version default"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Device config file not found - using firmware version default");
return false; return false;
} }
@@ -281,13 +283,13 @@ bool ConfigManager::loadDeviceConfig() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse device config from SD: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse device config from SD: %s", error.c_str());
return false; return false;
} }
if (doc.containsKey("fwVersion")) { if (doc.containsKey("fwVersion")) {
deviceConfig.fwVersion = doc["fwVersion"].as<String>(); deviceConfig.fwVersion = doc["fwVersion"].as<String>();
LOG_VERBOSE("ConfigManager - Firmware version loaded from SD: %s", deviceConfig.fwVersion.c_str()); LOG_VERBOSE(TAG, "ConfigManager - Firmware version loaded from SD: %s", deviceConfig.fwVersion.c_str());
} }
return true; return true;
@@ -295,22 +297,22 @@ bool ConfigManager::loadDeviceConfig() {
bool ConfigManager::isHealthy() const { bool ConfigManager::isHealthy() const {
if (!sdInitialized) { if (!sdInitialized) {
LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - SD card not initialized"); LOG_VERBOSE(TAG, "ConfigManager - ⚠️ Unhealthy - SD card not initialized");
return false; return false;
} }
if (deviceConfig.deviceUID.isEmpty()) { if (deviceConfig.deviceUID.isEmpty()) {
LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - Device UID not set (factory configuration required)"); LOG_VERBOSE(TAG, "ConfigManager - ⚠️ Unhealthy - Device UID not set (factory configuration required)");
return false; return false;
} }
if (deviceConfig.hwType.isEmpty()) { if (deviceConfig.hwType.isEmpty()) {
LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - Hardware type not set (factory configuration required)"); LOG_VERBOSE(TAG, "ConfigManager - ⚠️ Unhealthy - Hardware type not set (factory configuration required)");
return false; return false;
} }
if (networkConfig.hostname.isEmpty()) { if (networkConfig.hostname.isEmpty()) {
LOG_VERBOSE("ConfigManager - ⚠️ Unhealthy - Hostname not generated (initialization issue)"); LOG_VERBOSE(TAG, "ConfigManager - ⚠️ Unhealthy - Hostname not generated (initialization issue)");
return false; return false;
} }
@@ -327,7 +329,7 @@ bool ConfigManager::loadBellDurations() {
File file = SD.open("/settings/relayTimings.json", FILE_READ); File file = SD.open("/settings/relayTimings.json", FILE_READ);
if (!file) { if (!file) {
LOG_WARNING("ConfigManager - ⚠️ Settings file not found on SD. Using default bell durations."); LOG_WARNING(TAG, "ConfigManager - ⚠️ Settings file not found on SD. Using default bell durations.");
return false; return false;
} }
@@ -336,7 +338,7 @@ bool ConfigManager::loadBellDurations() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse settings from SD. Using default bell durations."); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse settings from SD. Using default bell durations.");
return false; return false;
} }
@@ -347,7 +349,7 @@ bool ConfigManager::loadBellDurations() {
} }
} }
LOG_DEBUG("ConfigManager - Bell durations loaded from SD"); LOG_DEBUG(TAG, "ConfigManager - Bell durations loaded from SD");
return true; return true;
} }
@@ -364,12 +366,12 @@ bool ConfigManager::saveBellDurations() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize bell durations JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize bell durations JSON");
return false; return false;
} }
saveFileToSD("/settings", "relayTimings.json", buffer); saveFileToSD("/settings", "relayTimings.json", buffer);
LOG_DEBUG("ConfigManager - Bell durations saved to SD"); LOG_DEBUG(TAG, "ConfigManager - Bell durations saved to SD");
return true; return true;
} }
@@ -380,7 +382,7 @@ bool ConfigManager::loadBellOutputs() {
File file = SD.open("/settings/bellOutputs.json", FILE_READ); File file = SD.open("/settings/bellOutputs.json", FILE_READ);
if (!file) { if (!file) {
LOG_WARNING("ConfigManager - ⚠️ Bell outputs file not found on SD. Using default 1:1 mapping."); LOG_WARNING(TAG, "ConfigManager - ⚠️ Bell outputs file not found on SD. Using default 1:1 mapping.");
return false; return false;
} }
@@ -389,7 +391,7 @@ bool ConfigManager::loadBellOutputs() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse bell outputs from SD. Using defaults."); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse bell outputs from SD. Using defaults.");
return false; return false;
} }
@@ -400,7 +402,7 @@ bool ConfigManager::loadBellOutputs() {
} }
} }
LOG_DEBUG("ConfigManager - Bell outputs loaded from SD"); LOG_DEBUG(TAG, "ConfigManager - Bell outputs loaded from SD");
return true; return true;
} }
@@ -417,12 +419,12 @@ bool ConfigManager::saveBellOutputs() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize bell outputs JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize bell outputs JSON");
return false; return false;
} }
saveFileToSD("/settings", "bellOutputs.json", buffer); saveFileToSD("/settings", "bellOutputs.json", buffer);
LOG_DEBUG("ConfigManager - Bell outputs saved to SD"); LOG_DEBUG(TAG, "ConfigManager - Bell outputs saved to SD");
return true; return true;
} }
@@ -433,7 +435,7 @@ void ConfigManager::updateBellDurations(JsonVariant doc) {
bellConfig.durations[i] = doc[key].as<uint16_t>(); bellConfig.durations[i] = doc[key].as<uint16_t>();
} }
} }
LOG_DEBUG("ConfigManager - Updated bell durations"); LOG_DEBUG(TAG, "ConfigManager - Updated bell durations");
} }
void ConfigManager::updateBellOutputs(JsonVariant doc) { void ConfigManager::updateBellOutputs(JsonVariant doc) {
@@ -441,10 +443,10 @@ void ConfigManager::updateBellOutputs(JsonVariant doc) {
String key = String("b") + (i + 1); String key = String("b") + (i + 1);
if (doc.containsKey(key)) { if (doc.containsKey(key)) {
bellConfig.outputs[i] = doc[key].as<uint16_t>() - 1; bellConfig.outputs[i] = doc[key].as<uint16_t>() - 1;
LOG_VERBOSE("ConfigManager - Bell %d output set to %d", i + 1, bellConfig.outputs[i]); LOG_VERBOSE(TAG, "ConfigManager - Bell %d output set to %d", i + 1, bellConfig.outputs[i]);
} }
} }
LOG_DEBUG("ConfigManager - Updated bell outputs"); LOG_DEBUG(TAG, "ConfigManager - Updated bell outputs");
} }
@@ -488,13 +490,13 @@ void ConfigManager::saveFileToSD(const char* dirPath, const char* filename, cons
File file = SD.open(fullPath.c_str(), FILE_WRITE); File file = SD.open(fullPath.c_str(), FILE_WRITE);
if (!file) { if (!file) {
LOG_ERROR("ConfigManager - ❌ Failed to open file: %s", fullPath.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to open file: %s", fullPath.c_str());
return; return;
} }
file.print(data); file.print(data);
file.close(); file.close();
LOG_VERBOSE("ConfigManager - File %s saved successfully", fullPath.c_str()); LOG_VERBOSE(TAG, "ConfigManager - File %s saved successfully", fullPath.c_str());
} }
// Clock configuration methods and other remaining methods follow the same pattern... // Clock configuration methods and other remaining methods follow the same pattern...
@@ -517,7 +519,7 @@ void ConfigManager::updateClockOutputs(JsonVariant doc) {
if (doc.containsKey("pauseDuration")) { if (doc.containsKey("pauseDuration")) {
clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>(); clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>();
} }
LOG_DEBUG("ConfigManager - Updated Clock outputs to: C1: %d / C2: %d, Pulse: %dms, Pause: %dms", LOG_DEBUG(TAG, "ConfigManager - Updated Clock outputs to: C1: %d / C2: %d, Pulse: %dms, Pause: %dms",
clockConfig.c1output, clockConfig.c2output, clockConfig.pulseDuration, clockConfig.pauseDuration); clockConfig.c1output, clockConfig.c2output, clockConfig.pulseDuration, clockConfig.pauseDuration);
} }
@@ -529,15 +531,21 @@ void ConfigManager::updateClockAlerts(JsonVariant doc) {
clockConfig.alertRingInterval = doc["alertRingInterval"].as<uint16_t>(); clockConfig.alertRingInterval = doc["alertRingInterval"].as<uint16_t>();
} }
if (doc.containsKey("hourBell")) { if (doc.containsKey("hourBell")) {
clockConfig.hourBell = doc["hourBell"].as<uint8_t>(); uint8_t bellNum = doc["hourBell"].as<uint8_t>();
// Convert from 1-based (API) to 0-based (internal), or keep 255 (disabled)
clockConfig.hourBell = (bellNum == 255) ? 255 : bellNum - 1;
} }
if (doc.containsKey("halfBell")) { if (doc.containsKey("halfBell")) {
clockConfig.halfBell = doc["halfBell"].as<uint8_t>(); uint8_t bellNum = doc["halfBell"].as<uint8_t>();
// Convert from 1-based (API) to 0-based (internal), or keep 255 (disabled)
clockConfig.halfBell = (bellNum == 255) ? 255 : bellNum - 1;
} }
if (doc.containsKey("quarterBell")) { if (doc.containsKey("quarterBell")) {
clockConfig.quarterBell = doc["quarterBell"].as<uint8_t>(); uint8_t bellNum = doc["quarterBell"].as<uint8_t>();
// Convert from 1-based (API) to 0-based (internal), or keep 255 (disabled)
clockConfig.quarterBell = (bellNum == 255) ? 255 : bellNum - 1;
} }
LOG_DEBUG("ConfigManager - Updated Clock alerts"); LOG_DEBUG(TAG, "ConfigManager - Updated Clock alerts");
} }
void ConfigManager::updateClockBacklight(JsonVariant doc) { void ConfigManager::updateClockBacklight(JsonVariant doc) {
@@ -553,7 +561,7 @@ void ConfigManager::updateClockBacklight(JsonVariant doc) {
if (doc.containsKey("offTime")) { if (doc.containsKey("offTime")) {
clockConfig.backlightOffTime = doc["offTime"].as<String>(); clockConfig.backlightOffTime = doc["offTime"].as<String>();
} }
LOG_DEBUG("ConfigManager - Updated Clock backlight"); LOG_DEBUG(TAG, "ConfigManager - Updated Clock backlight");
} }
void ConfigManager::updateClockSilence(JsonVariant doc) { void ConfigManager::updateClockSilence(JsonVariant doc) {
@@ -581,7 +589,7 @@ void ConfigManager::updateClockSilence(JsonVariant doc) {
clockConfig.nighttimeSilenceOffTime = nighttime["offTime"].as<String>(); clockConfig.nighttimeSilenceOffTime = nighttime["offTime"].as<String>();
} }
} }
LOG_DEBUG("ConfigManager - Updated Clock silence"); LOG_DEBUG(TAG, "ConfigManager - Updated Clock silence");
} }
bool ConfigManager::loadClockConfig() { bool ConfigManager::loadClockConfig() {
@@ -589,49 +597,94 @@ bool ConfigManager::loadClockConfig() {
File file = SD.open("/settings/clockConfig.json", FILE_READ); File file = SD.open("/settings/clockConfig.json", FILE_READ);
if (!file) { if (!file) {
LOG_WARNING("ConfigManager - ⚠️ Clock config file not found - using defaults"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Clock config file not found - using defaults");
return false; return false;
} }
StaticJsonDocument<512> doc; StaticJsonDocument<1024> doc; // Increased size for all settings
DeserializationError error = deserializeJson(doc, file); DeserializationError error = deserializeJson(doc, file);
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse clock config from SD: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse clock config from SD: %s", error.c_str());
return false; return false;
} }
// Clock enable/outputs
if (doc.containsKey("enabled")) clockConfig.enabled = doc["enabled"].as<bool>(); if (doc.containsKey("enabled")) clockConfig.enabled = doc["enabled"].as<bool>();
if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as<uint8_t>(); if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as<uint8_t>();
if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as<uint8_t>(); if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as<uint8_t>();
if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>(); if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
if (doc.containsKey("pauseDuration")) clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>(); if (doc.containsKey("pauseDuration")) clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>();
LOG_DEBUG("ConfigManager - Clock config loaded"); // Alert settings
if (doc.containsKey("alertType")) clockConfig.alertType = doc["alertType"].as<String>();
if (doc.containsKey("alertRingInterval")) clockConfig.alertRingInterval = doc["alertRingInterval"].as<uint16_t>();
if (doc.containsKey("hourBell")) clockConfig.hourBell = doc["hourBell"].as<uint8_t>();
if (doc.containsKey("halfBell")) clockConfig.halfBell = doc["halfBell"].as<uint8_t>();
if (doc.containsKey("quarterBell")) clockConfig.quarterBell = doc["quarterBell"].as<uint8_t>();
// Backlight settings
if (doc.containsKey("backlight")) clockConfig.backlight = doc["backlight"].as<bool>();
if (doc.containsKey("backlightOutput")) clockConfig.backlightOutput = doc["backlightOutput"].as<uint8_t>();
if (doc.containsKey("backlightOnTime")) clockConfig.backlightOnTime = doc["backlightOnTime"].as<String>();
if (doc.containsKey("backlightOffTime")) clockConfig.backlightOffTime = doc["backlightOffTime"].as<String>();
// Silence period settings
if (doc.containsKey("daytimeSilenceEnabled")) clockConfig.daytimeSilenceEnabled = doc["daytimeSilenceEnabled"].as<bool>();
if (doc.containsKey("daytimeSilenceOnTime")) clockConfig.daytimeSilenceOnTime = doc["daytimeSilenceOnTime"].as<String>();
if (doc.containsKey("daytimeSilenceOffTime")) clockConfig.daytimeSilenceOffTime = doc["daytimeSilenceOffTime"].as<String>();
if (doc.containsKey("nighttimeSilenceEnabled")) clockConfig.nighttimeSilenceEnabled = doc["nighttimeSilenceEnabled"].as<bool>();
if (doc.containsKey("nighttimeSilenceOnTime")) clockConfig.nighttimeSilenceOnTime = doc["nighttimeSilenceOnTime"].as<String>();
if (doc.containsKey("nighttimeSilenceOffTime")) clockConfig.nighttimeSilenceOffTime = doc["nighttimeSilenceOffTime"].as<String>();
LOG_DEBUG(TAG, "ConfigManager - Clock config loaded");
return true; return true;
} }
bool ConfigManager::saveClockConfig() { bool ConfigManager::saveClockConfig() {
if (!ensureSDCard()) return false; if (!ensureSDCard()) return false;
StaticJsonDocument<512> doc; StaticJsonDocument<1024> doc; // Increased size for all settings
// Clock enable/outputs
doc["enabled"] = clockConfig.enabled; doc["enabled"] = clockConfig.enabled;
doc["c1output"] = clockConfig.c1output; doc["c1output"] = clockConfig.c1output;
doc["c2output"] = clockConfig.c2output; doc["c2output"] = clockConfig.c2output;
doc["pulseDuration"] = clockConfig.pulseDuration; doc["pulseDuration"] = clockConfig.pulseDuration;
doc["pauseDuration"] = clockConfig.pauseDuration; doc["pauseDuration"] = clockConfig.pauseDuration;
char buffer[512]; // Alert settings
doc["alertType"] = clockConfig.alertType;
doc["alertRingInterval"] = clockConfig.alertRingInterval;
doc["hourBell"] = clockConfig.hourBell;
doc["halfBell"] = clockConfig.halfBell;
doc["quarterBell"] = clockConfig.quarterBell;
// Backlight settings
doc["backlight"] = clockConfig.backlight;
doc["backlightOutput"] = clockConfig.backlightOutput;
doc["backlightOnTime"] = clockConfig.backlightOnTime;
doc["backlightOffTime"] = clockConfig.backlightOffTime;
// Silence period settings
doc["daytimeSilenceEnabled"] = clockConfig.daytimeSilenceEnabled;
doc["daytimeSilenceOnTime"] = clockConfig.daytimeSilenceOnTime;
doc["daytimeSilenceOffTime"] = clockConfig.daytimeSilenceOffTime;
doc["nighttimeSilenceEnabled"] = clockConfig.nighttimeSilenceEnabled;
doc["nighttimeSilenceOnTime"] = clockConfig.nighttimeSilenceOnTime;
doc["nighttimeSilenceOffTime"] = clockConfig.nighttimeSilenceOffTime;
char buffer[1024];
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize clock config JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize clock config JSON");
return false; return false;
} }
saveFileToSD("/settings", "clockConfig.json", buffer); saveFileToSD("/settings", "clockConfig.json", buffer);
LOG_DEBUG("ConfigManager - Clock config saved"); LOG_DEBUG(TAG, "ConfigManager - Clock config saved");
return true; return true;
} }
@@ -640,7 +693,7 @@ bool ConfigManager::loadClockState() {
File file = SD.open("/settings/clockState.json", FILE_READ); File file = SD.open("/settings/clockState.json", FILE_READ);
if (!file) { if (!file) {
LOG_WARNING("ConfigManager - ⚠️ Clock state file not found - using defaults"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Clock state file not found - using defaults");
clockConfig.physicalHour = 0; clockConfig.physicalHour = 0;
clockConfig.physicalMinute = 0; clockConfig.physicalMinute = 0;
clockConfig.nextOutputIsC1 = true; clockConfig.nextOutputIsC1 = true;
@@ -653,7 +706,7 @@ bool ConfigManager::loadClockState() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse clock state from SD: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse clock state from SD: %s", error.c_str());
return false; return false;
} }
@@ -662,7 +715,7 @@ bool ConfigManager::loadClockState() {
clockConfig.nextOutputIsC1 = doc["nextIsC1"].as<bool>(); clockConfig.nextOutputIsC1 = doc["nextIsC1"].as<bool>();
clockConfig.lastSyncTime = doc["lastSyncTime"].as<uint32_t>(); clockConfig.lastSyncTime = doc["lastSyncTime"].as<uint32_t>();
LOG_DEBUG("ConfigManager - Clock state loaded"); LOG_DEBUG(TAG, "ConfigManager - Clock state loaded");
return true; return true;
} }
@@ -679,12 +732,12 @@ bool ConfigManager::saveClockState() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize clock state JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize clock state JSON");
return false; return false;
} }
saveFileToSD("/settings", "clockState.json", buffer); saveFileToSD("/settings", "clockState.json", buffer);
LOG_VERBOSE("ConfigManager - Clock state saved"); LOG_VERBOSE(TAG, "ConfigManager - Clock state saved");
return true; return true;
} }
@@ -693,7 +746,7 @@ bool ConfigManager::loadUpdateServers() {
File file = SD.open("/settings/updateServers.json", FILE_READ); File file = SD.open("/settings/updateServers.json", FILE_READ);
if (!file) { if (!file) {
LOG_DEBUG("ConfigManager - Update servers file not found - using fallback only"); LOG_DEBUG(TAG, "ConfigManager - Update servers file not found - using fallback only");
return false; return false;
} }
@@ -702,7 +755,7 @@ bool ConfigManager::loadUpdateServers() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse update servers JSON: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse update servers JSON: %s", error.c_str());
return false; return false;
} }
@@ -719,7 +772,7 @@ bool ConfigManager::loadUpdateServers() {
} }
} }
LOG_DEBUG("ConfigManager - Loaded %d update servers from SD card", updateServers.size()); LOG_DEBUG(TAG, "ConfigManager - Loaded %d update servers from SD card", updateServers.size());
return true; return true;
} }
@@ -736,7 +789,7 @@ void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) {
timeConfig.gmtOffsetSec = gmtOffsetSec; timeConfig.gmtOffsetSec = gmtOffsetSec;
timeConfig.daylightOffsetSec = daylightOffsetSec; timeConfig.daylightOffsetSec = daylightOffsetSec;
saveTimeConfig(); // Save time config specifically saveTimeConfig(); // Save time config specifically
LOG_DEBUG("ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec", LOG_DEBUG(TAG, "ConfigManager - TimeConfig updated - GMT offset %ld sec, DST offset %d sec",
gmtOffsetSec, daylightOffsetSec); gmtOffsetSec, daylightOffsetSec);
} }
@@ -750,7 +803,7 @@ void ConfigManager::updateNetworkConfig(const String& hostname, bool useStaticIP
networkConfig.dns1 = dns1; networkConfig.dns1 = dns1;
networkConfig.dns2 = dns2; networkConfig.dns2 = dns2;
saveNetworkConfig(); // Save immediately to SD saveNetworkConfig(); // Save immediately to SD
LOG_DEBUG("ConfigManager - NetworkConfig updated - Hostname: %s, Static IP: %s, IP: %s", LOG_DEBUG(TAG, "ConfigManager - NetworkConfig updated - Hostname: %s, Static IP: %s, IP: %s",
hostname.c_str(), useStaticIP ? "enabled" : "disabled", ip.toString().c_str()); hostname.c_str(), useStaticIP ? "enabled" : "disabled", ip.toString().c_str());
} }
@@ -763,7 +816,7 @@ bool ConfigManager::loadNetworkConfig() {
File file = SD.open("/settings/networkConfig.json", FILE_READ); File file = SD.open("/settings/networkConfig.json", FILE_READ);
if (!file) { if (!file) {
LOG_DEBUG("ConfigManager - Network config file not found - using auto-generated hostname and DHCP"); LOG_DEBUG(TAG, "ConfigManager - Network config file not found - using auto-generated hostname and DHCP");
return false; return false;
} }
@@ -772,7 +825,7 @@ bool ConfigManager::loadNetworkConfig() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse network config from SD: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse network config from SD: %s", error.c_str());
return false; return false;
} }
@@ -781,7 +834,7 @@ bool ConfigManager::loadNetworkConfig() {
String customHostname = doc["hostname"].as<String>(); String customHostname = doc["hostname"].as<String>();
if (!customHostname.isEmpty()) { if (!customHostname.isEmpty()) {
networkConfig.hostname = customHostname; networkConfig.hostname = customHostname;
LOG_DEBUG("ConfigManager - Custom hostname loaded from SD: %s", customHostname.c_str()); LOG_DEBUG(TAG, "ConfigManager - Custom hostname loaded from SD: %s", customHostname.c_str());
} }
} }
@@ -793,7 +846,7 @@ bool ConfigManager::loadNetworkConfig() {
// Load permanent AP mode setting // Load permanent AP mode setting
if (doc.containsKey("permanentAPMode")) { if (doc.containsKey("permanentAPMode")) {
networkConfig.permanentAPMode = doc["permanentAPMode"].as<bool>(); networkConfig.permanentAPMode = doc["permanentAPMode"].as<bool>();
LOG_DEBUG("ConfigManager - Permanent AP mode: %s", networkConfig.permanentAPMode ? "enabled" : "disabled"); LOG_DEBUG(TAG, "ConfigManager - Permanent AP mode: %s", networkConfig.permanentAPMode ? "enabled" : "disabled");
} }
if (doc.containsKey("ip")) { if (doc.containsKey("ip")) {
@@ -831,7 +884,7 @@ bool ConfigManager::loadNetworkConfig() {
} }
} }
LOG_DEBUG("ConfigManager - Network config loaded - Hostname: %s, Static IP: %s", LOG_DEBUG(TAG, "ConfigManager - Network config loaded - Hostname: %s, Static IP: %s",
networkConfig.hostname.c_str(), networkConfig.hostname.c_str(),
networkConfig.useStaticIP ? "enabled" : "disabled"); networkConfig.useStaticIP ? "enabled" : "disabled");
@@ -861,12 +914,12 @@ bool ConfigManager::saveNetworkConfig() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize network config JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize network config JSON");
return false; return false;
} }
saveFileToSD("/settings", "networkConfig.json", buffer); saveFileToSD("/settings", "networkConfig.json", buffer);
LOG_DEBUG("ConfigManager - Network config saved to SD"); LOG_DEBUG(TAG, "ConfigManager - Network config saved to SD");
return true; return true;
} }
@@ -879,7 +932,7 @@ bool ConfigManager::loadTimeConfig() {
File file = SD.open("/settings/timeConfig.json", FILE_READ); File file = SD.open("/settings/timeConfig.json", FILE_READ);
if (!file) { if (!file) {
LOG_DEBUG("ConfigManager - Time config file not found - using defaults (GMT+2)"); LOG_DEBUG(TAG, "ConfigManager - Time config file not found - using defaults (GMT+2)");
return false; return false;
} }
@@ -888,7 +941,7 @@ bool ConfigManager::loadTimeConfig() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse time config from SD: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse time config from SD: %s", error.c_str());
return false; return false;
} }
@@ -910,7 +963,7 @@ bool ConfigManager::loadTimeConfig() {
timeConfig.daylightOffsetSec = doc["daylightOffsetSec"].as<int>(); timeConfig.daylightOffsetSec = doc["daylightOffsetSec"].as<int>();
} }
LOG_DEBUG("ConfigManager - Time config loaded - NTP: %s, GMT offset: %ld, DST offset: %d", LOG_DEBUG(TAG, "ConfigManager - Time config loaded - NTP: %s, GMT offset: %ld, DST offset: %d",
timeConfig.ntpServer.c_str(), timeConfig.ntpServer.c_str(),
timeConfig.gmtOffsetSec, timeConfig.gmtOffsetSec,
timeConfig.daylightOffsetSec); timeConfig.daylightOffsetSec);
@@ -934,12 +987,12 @@ bool ConfigManager::saveTimeConfig() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize time config JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize time config JSON");
return false; return false;
} }
saveFileToSD("/settings", "timeConfig.json", buffer); saveFileToSD("/settings", "timeConfig.json", buffer);
LOG_DEBUG("ConfigManager - Time config saved to SD"); LOG_DEBUG(TAG, "ConfigManager - Time config saved to SD");
return true; return true;
} }
@@ -949,9 +1002,9 @@ bool ConfigManager::saveTimeConfig() {
bool ConfigManager::resetAllToDefaults() { bool ConfigManager::resetAllToDefaults() {
LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(TAG, "═══════════════════════════════════════════════════════════════════════════");
LOG_INFO(" 🏭 RESET SETTINGS TO DEFAULTS INITIATED"); LOG_INFO(TAG, " 🏭 RESET SETTINGS TO DEFAULTS INITIATED");
LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(TAG, "═══════════════════════════════════════════════════════════════════════════");
if (!ensureSDCard()) { if (!ensureSDCard()) {
return false; return false;
@@ -978,22 +1031,22 @@ bool ConfigManager::resetAllToDefaults() {
int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]); int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]);
LOG_DEBUG("ConfigManager - Step 1: Deleting %d configuration files...", numFiles); LOG_DEBUG(TAG, "ConfigManager - Step 1: Deleting %d configuration files...", numFiles);
for (int i = 0; i < numFiles; i++) { for (int i = 0; i < numFiles; i++) {
const char* filepath = settingsFiles[i]; const char* filepath = settingsFiles[i];
if (SD.exists(filepath)) { if (SD.exists(filepath)) {
if (SD.remove(filepath)) { if (SD.remove(filepath)) {
LOG_VERBOSE("ConfigManager - ✅ Deleted: %s", filepath); LOG_VERBOSE(TAG, "ConfigManager - ✅ Deleted: %s", filepath);
filesDeleted++; filesDeleted++;
} else { } else {
LOG_ERROR("ConfigManager - ❌ Failed to delete: %s", filepath); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to delete: %s", filepath);
filesFailed++; filesFailed++;
allDeleted = false; allDeleted = false;
} }
} else { } else {
LOG_VERBOSE("ConfigManager - Skip (not found): %s", filepath); LOG_VERBOSE(TAG, "ConfigManager - Skip (not found): %s", filepath);
} }
} }
@@ -1002,7 +1055,7 @@ bool ConfigManager::resetAllToDefaults() {
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
if (SD.exists("/melodies")) { if (SD.exists("/melodies")) {
LOG_DEBUG("ConfigManager - Step 2: Deleting melody files..."); LOG_DEBUG(TAG, "ConfigManager - Step 2: Deleting melody files...");
File melodiesDir = SD.open("/melodies"); File melodiesDir = SD.open("/melodies");
if (melodiesDir && melodiesDir.isDirectory()) { if (melodiesDir && melodiesDir.isDirectory()) {
@@ -1015,10 +1068,10 @@ bool ConfigManager::resetAllToDefaults() {
if (!entry.isDirectory()) { if (!entry.isDirectory()) {
if (SD.remove(entryPath.c_str())) { if (SD.remove(entryPath.c_str())) {
LOG_VERBOSE("ConfigManager - ✅ Deleted melody: %s", entryPath.c_str()); LOG_VERBOSE(TAG, "ConfigManager - ✅ Deleted melody: %s", entryPath.c_str());
melodiesDeleted++; melodiesDeleted++;
} else { } else {
LOG_ERROR("ConfigManager - ❌ Failed to delete melody: %s", entryPath.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to delete melody: %s", entryPath.c_str());
melodiesFailed++; melodiesFailed++;
allDeleted = false; allDeleted = false;
} }
@@ -1032,33 +1085,33 @@ bool ConfigManager::resetAllToDefaults() {
// Try to remove the empty directory // Try to remove the empty directory
if (SD.rmdir("/melodies")) { if (SD.rmdir("/melodies")) {
LOG_VERBOSE("ConfigManager - ✅ Deleted /melodies directory"); LOG_VERBOSE(TAG, "ConfigManager - ✅ Deleted /melodies directory");
} else { } else {
LOG_WARNING("ConfigManager - ⚠️ Could not delete /melodies directory (may not be empty)"); LOG_WARNING(TAG, "ConfigManager - ⚠️ Could not delete /melodies directory (may not be empty)");
} }
LOG_DEBUG("ConfigManager - Melodies deleted: %d, failed: %d", melodiesDeleted, melodiesFailed); LOG_DEBUG(TAG, "ConfigManager - Melodies deleted: %d, failed: %d", melodiesDeleted, melodiesFailed);
filesDeleted += melodiesDeleted; filesDeleted += melodiesDeleted;
filesFailed += melodiesFailed; filesFailed += melodiesFailed;
} }
} else { } else {
LOG_VERBOSE("ConfigManager - /melodies directory not found"); LOG_VERBOSE(TAG, "ConfigManager - /melodies directory not found");
} }
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
// SUMMARY // SUMMARY
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(TAG, "═══════════════════════════════════════════════════════════════════════════");
LOG_INFO("ConfigManager - Full reset summary:"); LOG_INFO(TAG, "ConfigManager - Full reset summary:");
LOG_INFO("ConfigManager - ✅ Files deleted: %d", filesDeleted); LOG_INFO(TAG, "ConfigManager - ✅ Files deleted: %d", filesDeleted);
LOG_INFO("ConfigManager - ❌ Files failed: %d", filesFailed); LOG_INFO(TAG, "ConfigManager - ❌ Files failed: %d", filesFailed);
LOG_INFO("ConfigManager - 🔄 Total processed: %d", filesDeleted + filesFailed); LOG_INFO(TAG, "ConfigManager - 🔄 Total processed: %d", filesDeleted + filesFailed);
LOG_INFO("═══════════════════════════════════════════════════════════════════════════"); LOG_INFO(TAG, "═══════════════════════════════════════════════════════════════════════════");
LOG_INFO("ConfigManager - ✅ RESET TO DEFAULT COMPLETE"); LOG_INFO(TAG, "ConfigManager - ✅ RESET TO DEFAULT COMPLETE");
LOG_INFO("ConfigManager - 🔄 Device will boot with default settings on next restart"); LOG_INFO(TAG, "ConfigManager - 🔄 Device will boot with default settings on next restart");
LOG_INFO("ConfigManager - 🆔 Device identity (UID) preserved"); LOG_INFO(TAG, "ConfigManager - 🆔 Device identity (UID) preserved");
return allDeleted; return allDeleted;
} }
@@ -1095,12 +1148,19 @@ String ConfigManager::getAllSettingsAsJson() const {
time["daylightOffsetSec"] = timeConfig.daylightOffsetSec; time["daylightOffsetSec"] = timeConfig.daylightOffsetSec;
// Bell durations (relay timings) // Bell durations (relay timings)
JsonObject bells = doc.createNestedObject("bells"); JsonObject bellDurations = doc.createNestedObject("bellDurations");
for (uint8_t i = 0; i < 16; i++) { for (uint8_t i = 0; i < 16; i++) {
String key = String("b") + (i + 1); String key = String("b") + (i + 1);
bells[key] = bellConfig.durations[i]; bellDurations[key] = bellConfig.durations[i];
} }
// Bell outputs (physical output mapping)
JsonObject bellOutputs = doc.createNestedObject("bellOutputs");
for (uint8_t i = 0; i < 16; i++) {
String key = String("b") + (i + 1);
bellOutputs[key] = bellConfig.outputs[i];
}
// Clock configuration // Clock configuration
JsonObject clock = doc.createNestedObject("clock"); JsonObject clock = doc.createNestedObject("clock");
clock["enabled"] = clockConfig.enabled; clock["enabled"] = clockConfig.enabled;
@@ -1154,31 +1214,31 @@ String ConfigManager::getAllSettingsAsJson() const {
bool ConfigManager::setSerialLogLevel(uint8_t level) { bool ConfigManager::setSerialLogLevel(uint8_t level) {
if (level > 5) { // Max level is VERBOSE (5) if (level > 5) { // Max level is VERBOSE (5)
LOG_WARNING("ConfigManager - ⚠️ Invalid serial log level %d, valid range is 0-5", level); LOG_WARNING(TAG, "ConfigManager - ⚠️ Invalid serial log level %d, valid range is 0-5", level);
return false; return false;
} }
generalConfig.serialLogLevel = level; generalConfig.serialLogLevel = level;
LOG_DEBUG("ConfigManager - Serial log level set to %d", level); LOG_DEBUG(TAG, "ConfigManager - Serial log level set to %d", level);
return true; return true;
} }
bool ConfigManager::setSdLogLevel(uint8_t level) { bool ConfigManager::setSdLogLevel(uint8_t level) {
if (level > 5) { // Max level is VERBOSE (5) if (level > 5) { // Max level is VERBOSE (5)
LOG_WARNING("ConfigManager - ⚠️ Invalid SD log level %d, valid range is 0-5", level); LOG_WARNING(TAG, "ConfigManager - ⚠️ Invalid SD log level %d, valid range is 0-5", level);
return false; return false;
} }
generalConfig.sdLogLevel = level; generalConfig.sdLogLevel = level;
LOG_DEBUG("ConfigManager - SD log level set to %d", level); LOG_DEBUG(TAG, "ConfigManager - SD log level set to %d", level);
return true; return true;
} }
bool ConfigManager::setMqttLogLevel(uint8_t level) { bool ConfigManager::setMqttLogLevel(uint8_t level) {
if (level > 5) { // Max level is VERBOSE (5) if (level > 5) { // Max level is VERBOSE (5)
LOG_WARNING("ConfigManager - ⚠️ Invalid MQTT log level %d, valid range is 0-5", level); LOG_WARNING(TAG, "ConfigManager - ⚠️ Invalid MQTT log level %d, valid range is 0-5", level);
return false; return false;
} }
generalConfig.mqttLogLevel = level; generalConfig.mqttLogLevel = level;
LOG_DEBUG("ConfigManager - MQTT log level set to %d", level); LOG_DEBUG(TAG, "ConfigManager - MQTT log level set to %d", level);
return true; return true;
} }
@@ -1187,7 +1247,7 @@ bool ConfigManager::loadGeneralConfig() {
File file = SD.open("/settings/generalConfig.json", FILE_READ); File file = SD.open("/settings/generalConfig.json", FILE_READ);
if (!file) { if (!file) {
LOG_WARNING("ConfigManager - ⚠️ General config file not found - using defaults"); LOG_WARNING(TAG, "ConfigManager - ⚠️ General config file not found - using defaults");
return false; return false;
} }
@@ -1196,7 +1256,7 @@ bool ConfigManager::loadGeneralConfig() {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("ConfigManager - ❌ Failed to parse general config from SD: %s", error.c_str()); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to parse general config from SD: %s", error.c_str());
return false; return false;
} }
@@ -1214,7 +1274,7 @@ bool ConfigManager::loadGeneralConfig() {
mqttConfig.enabled = generalConfig.mqttEnabled; // Sync with mqttConfig mqttConfig.enabled = generalConfig.mqttEnabled; // Sync with mqttConfig
} }
LOG_DEBUG("ConfigManager - General config loaded - Serial log level: %d, SD log level: %d, MQTT log level: %d, MQTT enabled: %s", LOG_DEBUG(TAG, "ConfigManager - General config loaded - Serial log level: %d, SD log level: %d, MQTT log level: %d, MQTT enabled: %s",
generalConfig.serialLogLevel, generalConfig.sdLogLevel, generalConfig.mqttLogLevel, generalConfig.serialLogLevel, generalConfig.sdLogLevel, generalConfig.mqttLogLevel,
generalConfig.mqttEnabled ? "true" : "false"); generalConfig.mqttEnabled ? "true" : "false");
return true; return true;
@@ -1233,11 +1293,11 @@ bool ConfigManager::saveGeneralConfig() {
size_t len = serializeJson(doc, buffer, sizeof(buffer)); size_t len = serializeJson(doc, buffer, sizeof(buffer));
if (len == 0 || len >= sizeof(buffer)) { if (len == 0 || len >= sizeof(buffer)) {
LOG_ERROR("ConfigManager - ❌ Failed to serialize general config JSON"); LOG_ERROR(TAG, "ConfigManager - ❌ Failed to serialize general config JSON");
return false; return false;
} }
saveFileToSD("/settings", "generalConfig.json", buffer); saveFileToSD("/settings", "generalConfig.json", buffer);
LOG_DEBUG("ConfigManager - General config saved (MQTT enabled: %s)", generalConfig.mqttEnabled ? "true" : "false"); LOG_DEBUG(TAG, "ConfigManager - General config saved (MQTT enabled: %s)", generalConfig.mqttEnabled ? "true" : "false");
return true; return true;
} }

View File

@@ -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>
@@ -70,6 +69,8 @@ public:
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) 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
}; };
/** /**
@@ -104,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
@@ -407,6 +408,8 @@ public:
bool setSerialLogLevel(uint8_t level); bool setSerialLogLevel(uint8_t level);
bool setSdLogLevel(uint8_t level); bool setSdLogLevel(uint8_t level);
bool setMqttLogLevel(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; } uint8_t getMqttLogLevel() const { return generalConfig.mqttLogLevel; }
void setMqttEnabled(bool enabled) { generalConfig.mqttEnabled = enabled; mqttConfig.enabled = enabled; } void setMqttEnabled(bool enabled) { generalConfig.mqttEnabled = enabled; mqttConfig.enabled = enabled; }
bool getMqttEnabled() const { return generalConfig.mqttEnabled; } bool getMqttEnabled() const { return generalConfig.mqttEnabled; }

View File

@@ -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;
} }
@@ -56,45 +70,59 @@ bool FileManager::ensureDirectoryExists(const String& dirPath) {
} }
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 // Check if URL is HTTPS
bool isHttps = url.startsWith("https://"); bool isHttps = url.startsWith("https://");
HTTPClient http; HTTPClient http;
WiFiClientSecure* secureClient = nullptr;
// Configure HTTP client based on protocol // Configure HTTP client based on protocol
if (isHttps) { if (isHttps) {
WiFiClientSecure* secureClient = new WiFiClientSecure(); secureClient = new WiFiClientSecure();
secureClient->setInsecure(); // Skip certificate validation for Firebase secureClient->setInsecure(); // Skip certificate validation for Firebase
secureClient->setTimeout(15); // 15 second timeout for TLS operations
http.begin(*secureClient, url); http.begin(*secureClient, url);
LOG_DEBUG("Using HTTPS with secure client"); LOG_DEBUG(TAG, "Using HTTPS with secure client");
} else { } else {
http.begin(url); http.begin(url);
LOG_DEBUG("Using HTTP"); LOG_DEBUG(TAG, "Using HTTP");
} }
http.setTimeout(30000); // 30 second timeout for large files http.setTimeout(30000); // 30 second timeout for large files
http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); // Follow redirects automatically http.setFollowRedirects(HTTPC_FORCE_FOLLOW_REDIRECTS); // Follow redirects automatically
LOG_DEBUG("Sending HTTP GET request..."); // 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();
// Re-enable task watchdog after HTTP request completes
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) { if (httpCode != HTTP_CODE_OK && httpCode != HTTP_CODE_MOVED_PERMANENTLY && httpCode != HTTP_CODE_FOUND) {
LOG_ERROR("HTTP GET failed, code: %d, error: %s", httpCode, http.errorToString(httpCode).c_str()); 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;
} }
@@ -105,15 +133,16 @@ 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 // Get stream and file size
WiFiClient* stream = http.getStreamPtr(); WiFiClient* stream = http.getStreamPtr();
int contentLength = http.getSize(); int contentLength = http.getSize();
LOG_DEBUG("Content length: %d bytes", contentLength); LOG_DEBUG(TAG, "Content length: %d bytes", contentLength);
uint8_t buffer[512]; // Smaller buffer for better responsiveness uint8_t buffer[512]; // Smaller buffer for better responsiveness
size_t totalBytes = 0; size_t totalBytes = 0;
@@ -134,18 +163,18 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
file.write(buffer, bytesRead); file.write(buffer, bytesRead);
totalBytes += bytesRead; totalBytes += bytesRead;
// Log progress every 5KB // Log progress every 5 seconds
if (millis() - lastLog > 5000) { if (millis() - lastLog > 5000) {
LOG_DEBUG("Download progress: %u bytes", totalBytes); LOG_DEBUG(TAG, "Download progress: %u bytes", totalBytes);
lastLog = millis(); lastLog = millis();
} }
} }
} }
// Aggressive task yielding every 100ms to prevent watchdog timeout // Aggressive task yielding every 50ms to prevent watchdog timeout
if (millis() - lastYield > 100) { if (millis() - lastYield > 50) {
yield(); yield();
vTaskDelay(1 / portTICK_PERIOD_MS); // Let other tasks run vTaskDelay(5 / portTICK_PERIOD_MS); // Let other tasks run (5ms)
lastYield = millis(); lastYield = millis();
} }
@@ -154,27 +183,29 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
break; break;
} }
// Small delay if no data available yet // Yield and small delay if no data available yet
if (!availableSize) { if (!availableSize) {
delay(10); yield();
vTaskDelay(10 / portTICK_PERIOD_MS);
} }
} }
file.close(); file.close();
http.end(); http.end();
LOG_INFO("Download complete, file saved to: %s (%u bytes)", fullPath.c_str(), totalBytes); 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 "{}";
} }
@@ -213,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;
} }
} }
@@ -247,18 +278,18 @@ bool FileManager::writeJsonFile(const String& filePath, JsonDocument& doc) {
File file = SD.open(filePath.c_str(), FILE_WRITE); File file = SD.open(filePath.c_str(), FILE_WRITE);
if (!file) { if (!file) {
LOG_ERROR("Failed to open file for writing: %s", filePath.c_str()); LOG_ERROR(TAG, "Failed to open file for writing: %s", filePath.c_str());
return false; return false;
} }
if (serializeJson(doc, file) == 0) { if (serializeJson(doc, file) == 0) {
LOG_ERROR("Failed to write JSON to file: %s", filePath.c_str()); LOG_ERROR(TAG, "Failed to write JSON to file: %s", filePath.c_str());
file.close(); file.close();
return false; return false;
} }
file.close(); file.close();
LOG_DEBUG("JSON file written successfully: %s", filePath.c_str()); LOG_DEBUG(TAG, "JSON file written successfully: %s", filePath.c_str());
return true; return true;
} }
@@ -269,7 +300,7 @@ bool FileManager::readJsonFile(const String& filePath, JsonDocument& doc) {
File file = SD.open(filePath.c_str(), FILE_READ); File file = SD.open(filePath.c_str(), FILE_READ);
if (!file) { if (!file) {
LOG_ERROR("Failed to open file for reading: %s", filePath.c_str()); LOG_ERROR(TAG, "Failed to open file for reading: %s", filePath.c_str());
return false; return false;
} }
@@ -277,12 +308,12 @@ bool FileManager::readJsonFile(const String& filePath, JsonDocument& doc) {
file.close(); file.close();
if (error) { if (error) {
LOG_ERROR("Failed to parse JSON from file: %s, error: %s", LOG_ERROR(TAG, "Failed to parse JSON from file: %s, error: %s",
filePath.c_str(), error.c_str()); filePath.c_str(), error.c_str());
return false; return false;
} }
LOG_DEBUG("JSON file read successfully: %s", filePath.c_str()); LOG_DEBUG(TAG, "JSON file read successfully: %s", filePath.c_str());
return true; return true;
} }
@@ -293,26 +324,26 @@ bool FileManager::readJsonFile(const String& filePath, JsonDocument& doc) {
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;
} }
@@ -323,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;
} }
@@ -333,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;
} }
@@ -344,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;
}

View File

@@ -24,13 +24,14 @@
#include <WiFiClient.h> #include <WiFiClient.h>
#include <WiFiClientSecure.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"
class FileManager { class FileManager {
private: private:
ConfigManager* configManager; ConfigManager* configManager;
public: public:
// Constructor // Constructor
FileManager(ConfigManager* config); FileManager(ConfigManager* config);
@@ -49,6 +50,9 @@ public:
// Generic read/write for JSON data // Generic read/write for JSON data
bool writeJsonFile(const String& filePath, JsonDocument& doc); bool writeJsonFile(const String& filePath, JsonDocument& doc);
bool readJsonFile(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

View File

@@ -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() {

View File

@@ -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());

View File

@@ -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;

View File

@@ -1,138 +1,251 @@
#include "Logging.hpp" #include "Logging.hpp"
// Initialize static members // ═══════════════════════════════════════════════════════════════════════════════════
Logging::LogLevel Logging::currentLevel = Logging::VERBOSE; // Default to VERBOSE // STATIC MEMBER INITIALIZATION
Logging::LogLevel Logging::mqttLogLevel = Logging::NONE; // Default MQTT logs OFF // ═══════════════════════════════════════════════════════════════════════════════════
Logging::MqttPublishCallback Logging::mqttPublishCallback = nullptr;
String Logging::mqttLogTopic = "";
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::setMqttLogLevel(LogLevel level) { void Logging::setSdLevel(LogLevel level) {
mqttLogLevel = level; _sdLevel = level;
Serial.printf("[LOGGING] MQTT log level set to %d\n", level); Serial.printf("[Logger] SD level -> %d\n", level);
} }
Logging::LogLevel Logging::getMqttLogLevel() { Logging::LogLevel Logging::getSerialLevel() { return _serialLevel; }
return mqttLogLevel; 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) { void Logging::setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic) {
mqttPublishCallback = callback; _mqttCallback = callback;
mqttLogTopic = logTopic; _mqttLogTopic = logTopic;
Serial.printf("[LOGGING] MQTT publish callback registered for topic: %s\n", logTopic.c_str()); 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);
void Logging::debug(const char* format, ...) { bool serialEnabled = (serialEffective >= level);
if (!isLevelEnabled(DEBUG)) return; bool mqttEnabled = (mqttEffective >= level) && (_mqttCallback != nullptr);
bool sdEnabled = (sdEffective >= level) && (_sdCallback != nullptr);
va_list args; // Early exit if nothing will output this message
va_start(args, format); if (!serialEnabled && !mqttEnabled && !sdEnabled) {
log(DEBUG, "🐞 DEBG", format, args); return;
va_end(args); }
}
void Logging::verbose(const char* format, ...) { // Format the message once
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) {
// Print the formatted message
char buffer[512]; char buffer[512];
vsnprintf(buffer, sizeof(buffer), format, args); vsnprintf(buffer, sizeof(buffer), format, args);
// Serial output // Serial output
Serial.printf("[%s] ", levelStr); if (serialEnabled) {
Serial.print(buffer); Serial.printf("[%s][%s] %s\n", levelStr, tag, buffer);
Serial.println(); }
// MQTT output (if enabled and callback is set) // MQTT output
if (mqttLogLevel >= level && mqttPublishCallback) { if (mqttEnabled) {
publishToMqtt(level, levelStr, buffer); publishToMqtt(level, levelStr, tag, buffer);
}
// SD output
if (sdEnabled) {
writeToSd(level, levelStr, tag, buffer);
} }
} }
void Logging::publishToMqtt(LogLevel level, const char* levelStr, const char* message) { // ═══════════════════════════════════════════════════════════════════════════════════
if (!mqttPublishCallback || mqttLogTopic.isEmpty()) { // PRIVATE: MQTT PUBLISH
return; // 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;
// CRITICAL: Prevent infinite recursion if MQTT publish fails
// Temporarily disable MQTT logging during publish to avoid cascading errors
static bool isPublishing = false; static bool isPublishing = false;
if (isPublishing) { if (isPublishing) return;
return; // Already publishing, don't create recursive log loop
}
isPublishing = true; isPublishing = true;
// Build JSON manually to minimize stack usage (no StaticJsonDocument) // JSON: {"level":"WARNING","subsystem":"BellEngine","message":"...","timestamp":12345}
// Format: {"level":"🟢 INFO","message":"text","timestamp":12345}
String payload; String payload;
payload.reserve(600); // Pre-allocate to avoid fragmentation payload.reserve(600);
payload = "{\"level\":\""; payload = "{\"level\":\"";
payload += levelStr; payload += levelStr;
payload += "\",\"subsystem\":\"";
payload += tag;
payload += "\",\"message\":\""; payload += "\",\"message\":\"";
// Escape special JSON characters in message // Escape special JSON characters
String escapedMsg = message; const char* p = message;
escapedMsg.replace("\\", "\\\\"); while (*p) {
escapedMsg.replace("\"", "\\\""); char c = *p++;
escapedMsg.replace("\n", "\\n"); if (c == '\\') payload += "\\\\";
escapedMsg.replace("\r", "\\r"); else if (c == '"') payload += "\\\"";
else if (c == '\n') payload += "\\n";
else if (c == '\r') payload += "\\r";
else payload += c;
}
payload += escapedMsg;
payload += "\",\"timestamp\":"; payload += "\",\"timestamp\":";
payload += millis(); payload += millis();
payload += "}"; payload += "}";
// Publish with QoS 1 (guaranteed delivery) _mqttCallback(_mqttLogTopic, payload, 1);
// Note: If this fails, it won't trigger another MQTT log due to isPublishing flag
mqttPublishCallback(mqttLogTopic, payload, 1);
isPublishing = false; 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;
}

View File

@@ -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
* 📋 VERSION: 2.0 (Enhanced logging system) * • SD — Persistent log storage for post-mortem analysis
*
* 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,67 +32,114 @@
#define LOGGING_HPP #define LOGGING_HPP
#include <Arduino.h> #include <Arduino.h>
#include <map>
// Forward declaration #include <functional>
class MQTTAsyncClient;
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
}; };
// MQTT Log Publishing Callback // ═══════════════════════════════════════════════════════════════════════════════
// CALLBACK TYPES
// ═══════════════════════════════════════════════════════════════════════════════
using MqttPublishCallback = std::function<void(const String& topic, const String& payload, int qos)>; using MqttPublishCallback = std::function<void(const String& topic, const String& payload, int qos)>;
using SdWriteCallback = std::function<void(const String& line)>;
private: // ═══════════════════════════════════════════════════════════════════════════════
static LogLevel currentLevel; // GLOBAL CHANNEL LEVELS
static LogLevel mqttLogLevel; // Set the baseline level for each output channel.
static MqttPublishCallback mqttPublishCallback; // Per-subsystem overrides take precedence when set.
static String mqttLogTopic; // ═══════════════════════════════════════════════════════════════════════════════
static void setSerialLevel(LogLevel level);
static void setMqttLevel(LogLevel level);
static void setSdLevel(LogLevel level);
public: static LogLevel getSerialLevel();
// Set the active log level static LogLevel getMqttLevel();
static void setLevel(LogLevel level); static LogLevel getSdLevel();
// Get current log level // Legacy compatibility (maps to serial level)
static LogLevel getLevel(); static void setLevel(LogLevel level) { setSerialLevel(level); }
static LogLevel getLevel() { return getSerialLevel(); }
static void setMqttLogLevel(LogLevel level) { setMqttLevel(level); }
static LogLevel getMqttLogLevel() { return getMqttLevel(); }
// Set MQTT log level (independent from serial logging) // ═══════════════════════════════════════════════════════════════════════════════
static void setMqttLogLevel(LogLevel level); // 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);
// Get MQTT log level // ═══════════════════════════════════════════════════════════════════════════════
static LogLevel getMqttLogLevel(); // OUTPUT CHANNEL REGISTRATION
// ═══════════════════════════════════════════════════════════════════════════════
// Set MQTT callback for publishing logs
static void setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic); static void setMqttPublishCallback(MqttPublishCallback callback, const String& logTopic);
static void setSdWriteCallback(SdWriteCallback callback);
// Logging functions // ═══════════════════════════════════════════════════════════════════════════════
static void error(const char* format, ...); // LOGGING FUNCTIONS (tag = subsystem name, e.g. "BellEngine")
static void warning(const char* format, ...); // ═══════════════════════════════════════════════════════════════════════════════
static void info(const char* format, ...); static void error(const char* tag, const char* format, ...);
static void debug(const char* format, ...); static void warning(const char* tag, const char* format, ...);
static void verbose(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, ...);
// Check if level is enabled (for conditional logging) // ═══════════════════════════════════════════════════════════════════════════════
// 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 void publishToMqtt(LogLevel level, const char* levelStr, const char* message); 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

View File

@@ -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),
@@ -66,72 +68,106 @@ void Networking::begin() {
// Check if permanent AP mode is enabled // Check if permanent AP mode is enabled
if (_configManager.getPermanentAPMode()) { if (_configManager.getPermanentAPMode()) {
LOG_INFO("Permanent AP mode enabled - starting AP mode on 192.168.4.1"); LOG_INFO(TAG, "Permanent AP mode enabled - starting AP mode on 192.168.4.1");
startPermanentAPMode(); startPermanentAPMode();
return; return;
} }
// ETHERNET DISABLED - WiFi only mode
// Start Ethernet hardware // Start Ethernet hardware
auto& hwConfig = _configManager.getHardwareConfig(); // auto& hwConfig = _configManager.getHardwareConfig();
ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs, // ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs,
hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI); // hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI);
// Start connection sequence // Start connection sequence - Skip Ethernet, go directly to WiFi
LOG_INFO("Starting network connection sequence..."); LOG_INFO(TAG, "Starting WiFi connection (Ethernet disabled)...");
startEthernetConnection(); 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");
startWiFiConnection();
// Check if Ethernet hardware initialization failed
if (!ETH.linkUp()) { // Original Ethernet code (DISABLED):
LOG_WARNING("Ethernet hardware not detected or failed to initialize"); // LOG_INFO(TAG, "Attempting Ethernet connection...");
LOG_INFO("Falling back to WiFi immediately"); // setState(NetworkState::CONNECTING_ETHERNET);
startWiFiConnection(); //
return; // // Check if Ethernet hardware initialization failed
} // if (!ETH.linkUp()) {
// LOG_WARNING(TAG, "Ethernet hardware not detected or failed to initialize");
// Ethernet will auto-connect via events // LOG_INFO(TAG, "Falling back to WiFi immediately");
// Set timeout for Ethernet attempt (5 seconds) // startWiFiConnection();
_lastConnectionAttempt = millis(); // return;
// }
// Start reconnection timer to handle timeout //
xTimerStart(_reconnectionTimer, 0); // // 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 LOG_INFO(TAG, "Using DEFAULT WiFi credentials - SSID: %s", netConfig.defaultWifiSsid.c_str());
startWiFiPortal();
}
return;
}
// Get and log saved credentials (for debugging)
String savedSSID = _wifiManager->getWiFiSSID(true);
LOG_INFO("Using WiFiManager saved credentials - SSID: %s", savedSSID.c_str());
applyNetworkConfig(false); // false = WiFi config applyNetworkConfig(false); // false = WiFi config
WiFi.mode(WIFI_STA); WiFi.mode(WIFI_STA);
WiFi.begin(netConfig.defaultWifiSsid.c_str(), netConfig.defaultWifiPsk.c_str());
// Let WiFiManager handle credentials (uses saved SSID/password)
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);
@@ -139,7 +175,7 @@ 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 // Add custom HTML to WiFiManager portal for permanent AP mode toggle
String customHTML = "<br/><br/><h3>Network Mode</h3>"; String customHTML = "<br/><br/><h3>Network Mode</h3>";
@@ -154,10 +190,10 @@ void Networking::startWiFiPortal() {
_wifiManager->setCustomHeadElement(customHTML.c_str()); _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);
@@ -168,39 +204,40 @@ void Networking::handleReconnection() {
if (_state == NetworkState::CONNECTED_ETHERNET || _state == NetworkState::CONNECTED_WIFI) { if (_state == NetworkState::CONNECTED_ETHERNET || _state == NetworkState::CONNECTED_WIFI) {
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;
@@ -215,21 +252,16 @@ 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"); if (hasValidWiFiCredentials()) {
startEthernetConnection(); startWiFiConnection();
} else if (!_bootSequenceComplete) {
// No credentials during boot - start portal
startWiFiPortal();
} else { } else {
LOG_INFO("No Ethernet - trying WiFi"); LOG_WARNING(TAG, "No WiFi credentials and boot sequence complete - waiting");
if (hasValidWiFiCredentials()) {
startWiFiConnection();
} else if (!_bootSequenceComplete) {
// No credentials during boot - start portal
startWiFiPortal();
} else {
LOG_WARNING("No WiFi credentials and boot sequence complete - waiting");
}
} }
} }
@@ -240,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;
} }
} }
@@ -305,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);
// Original code (DISABLED):
// Stop WiFi if it was running // LOG_INFO(TAG, "Ethernet connected successfully");
if (WiFi.getMode() != WIFI_OFF) { // setState(NetworkState::CONNECTED_ETHERNET);
WiFi.disconnect(true); // setActiveConnection(ConnectionType::ETHERNET);
WiFi.mode(WIFI_OFF); //
} // // Stop WiFi if it was running
// if (WiFi.getMode() != WIFI_OFF) {
// Stop reconnection timer // WiFi.disconnect(true);
xTimerStop(_reconnectionTimer, 0); // 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) {
setState(NetworkState::DISCONNECTED); // Original code (DISABLED):
setActiveConnection(ConnectionType::NONE); // LOG_WARNING(TAG, "Ethernet disconnected");
notifyConnectionChange(false); //
// if (_activeConnection == ConnectionType::ETHERNET) {
// Start reconnection attempts // setState(NetworkState::DISCONNECTED);
xTimerStart(_reconnectionTimer, 0); // setActiveConnection(ConnectionType::NONE);
} // 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);
@@ -354,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);
@@ -367,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) {
ETH.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
} else {
WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
}
} else {
LOG_INFO("Using DHCP configuration");
}
if (ethernet) { if (ethernet) {
ETH.setHostname(netConfig.hostname.c_str()); LOG_WARNING(TAG, "applyNetworkConfig called with ethernet=true but Ethernet is disabled");
} else { return;
WiFi.setHostname(netConfig.hostname.c_str());
} }
if (netConfig.useStaticIP) {
LOG_INFO(TAG, "Applying static IP configuration");
WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
} else {
LOG_INFO(TAG, "Using DHCP configuration");
}
WiFi.setHostname(netConfig.hostname.c_str());
} }
bool Networking::hasValidWiFiCredentials() { bool Networking::hasValidWiFiCredentials() {
@@ -413,7 +456,9 @@ 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: case ConnectionType::AP:
@@ -426,7 +471,9 @@ String Networking::getLocalIP() const {
String Networking::getGateway() const { String Networking::getGateway() const {
switch (_activeConnection) { switch (_activeConnection) {
case ConnectionType::ETHERNET: case ConnectionType::ETHERNET:
return ETH.gatewayIP().toString(); // 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: case ConnectionType::WIFI:
return WiFi.gatewayIP().toString(); return WiFi.gatewayIP().toString();
default: default:
@@ -435,68 +482,52 @@ String Networking::getGateway() const {
} }
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);
// Disconnect everything // Disconnect everything
if (WiFi.getMode() != WIFI_OFF) { if (WiFi.getMode() != WIFI_OFF) {
WiFi.disconnect(true); WiFi.disconnect(true);
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:
break; break;
} }
@@ -509,7 +540,7 @@ void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
} }
void Networking::startPermanentAPMode() { void Networking::startPermanentAPMode() {
LOG_INFO("Starting Permanent AP Mode"); LOG_INFO(TAG, "Starting Permanent AP Mode");
setState(NetworkState::AP_MODE_PERMANENT); setState(NetworkState::AP_MODE_PERMANENT);
// Stop any existing connections // Stop any existing connections
@@ -530,24 +561,24 @@ void Networking::startPermanentAPMode() {
IPAddress subnet(255, 255, 255, 0); IPAddress subnet(255, 255, 255, 0);
if (!WiFi.softAPConfig(local_IP, gateway, subnet)) { if (!WiFi.softAPConfig(local_IP, gateway, subnet)) {
LOG_ERROR("Failed to configure AP IP address"); LOG_ERROR(TAG, "Failed to configure AP IP address");
} }
// Start AP // Start AP
bool apStarted; bool apStarted;
if (apPass.isEmpty()) { if (apPass.isEmpty()) {
apStarted = WiFi.softAP(apName.c_str()); apStarted = WiFi.softAP(apName.c_str());
LOG_INFO("Starting open AP (no password): %s", apName.c_str()); LOG_INFO(TAG, "Starting open AP (no password): %s", apName.c_str());
} else { } else {
apStarted = WiFi.softAP(apName.c_str(), apPass.c_str()); apStarted = WiFi.softAP(apName.c_str(), apPass.c_str());
LOG_INFO("Starting AP with password: %s", apName.c_str()); LOG_INFO(TAG, "Starting AP with password: %s", apName.c_str());
} }
if (apStarted) { if (apStarted) {
LOG_INFO("✅ Permanent AP Mode active"); LOG_INFO(TAG, "✅ Permanent AP Mode active");
LOG_INFO(" SSID: %s", apName.c_str()); LOG_INFO(TAG, " SSID: %s", apName.c_str());
LOG_INFO(" IP: 192.168.4.1"); LOG_INFO(TAG, " IP: 192.168.4.1");
LOG_INFO(" Subnet: 255.255.255.0"); LOG_INFO(TAG, " Subnet: 255.255.255.0");
setActiveConnection(ConnectionType::AP); setActiveConnection(ConnectionType::AP);
@@ -562,7 +593,7 @@ void Networking::startPermanentAPMode() {
// Notify connected // Notify connected
notifyConnectionChange(true); notifyConnectionChange(true);
} else { } else {
LOG_ERROR("❌ Failed to start AP Mode"); LOG_ERROR(TAG, "❌ Failed to start AP Mode");
setState(NetworkState::DISCONNECTED); setState(NetworkState::DISCONNECTED);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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,7 +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); // Custom firmware update 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;
@@ -104,7 +110,9 @@ 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;
uint16_t _availableVersion; uint16_t _availableVersion;
@@ -127,6 +135,19 @@ private:
TimerHandle_t _initialCheckTimer; TimerHandle_t _initialCheckTimer;
static void initialCheckCallback(TimerHandle_t xTimer); static void initialCheckCallback(TimerHandle_t xTimer);
void performInitialCheck(); // Async initial check after boot 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);
@@ -135,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);

View File

@@ -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,11 +69,11 @@ 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;
} }
@@ -80,14 +82,14 @@ uint8_t OutputManager::getPhysicalOutput(uint8_t virtualOutput) const {
// Handle unconfigured bells (255 = disabled) // Handle unconfigured bells (255 = disabled)
if (bellOutput == 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
} }
// Physical output is already 0-indexed from config // Physical output is already 0-indexed from config
uint8_t physicalOutput = (uint8_t)bellOutput; uint8_t physicalOutput = (uint8_t)bellOutput;
LOG_DEBUG("🔗 Bell %d → 0-indexed output %d", LOG_DEBUG(TAG, "🔗 Bell %d → 0-indexed output %d",
virtualOutput, physicalOutput); virtualOutput, physicalOutput);
return physicalOutput; return physicalOutput;
@@ -104,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;
} }
@@ -119,24 +121,24 @@ 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;
} }
// Physical output is already 0-indexed from config // Physical output is already 0-indexed from config
if (!isValidPhysicalOutput(physicalOutput)) { if (!isValidPhysicalOutput(physicalOutput)) {
LOG_ERROR("❌ Invalid physical output for clock: %d (max outputs: %d)", LOG_ERROR(TAG, "❌ Invalid physical output for clock: %d (max outputs: %d)",
physicalOutput, getMaxOutputs()); physicalOutput, getMaxOutputs());
return; return;
} }
@@ -144,7 +146,7 @@ void OutputManager::fireClockOutput(uint8_t virtualOutput, uint16_t durationMs)
// Fire the physical output directly // Fire the physical output directly
fireOutputForDuration(physicalOutput, durationMs); fireOutputForDuration(physicalOutput, durationMs);
LOG_DEBUG("🕐 FIRE Clock Virtual %d (C%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, durationMs); virtualOutput, virtualOutput + 1, physicalOutput, durationMs);
} }
@@ -175,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;
} }
@@ -196,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;
@@ -243,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) {
@@ -264,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) {
@@ -316,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);
@@ -344,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;
} }
@@ -352,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,
@@ -370,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;
} }
@@ -390,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;
@@ -398,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;
} }
@@ -406,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;
@@ -424,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) {
@@ -434,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;
} }
@@ -451,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;
} }
@@ -464,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;
} }
@@ -503,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;

View File

@@ -1,7 +1,10 @@
#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 "../Telemetry/Telemetry.hpp"
#include "../TimeKeeper/TimeKeeper.hpp" // 🔥 Include for Timekeeper class definition
#include "../BuiltInMelodies/BuiltInMelodies.hpp" #include "../BuiltInMelodies/BuiltInMelodies.hpp"
// Note: Removed global melody_steps dependency for cleaner architecture // Note: Removed global melody_steps dependency for cleaner architecture
@@ -31,11 +34,12 @@ Player::Player(CommunicationRouter* comm, FileManager* fm)
, _fileManager(fm) , _fileManager(fm)
, _bellEngine(nullptr) , _bellEngine(nullptr)
, _telemetry(nullptr) , _telemetry(nullptr)
, _timekeeper(nullptr)
, _durationTimerHandle(NULL) { , _durationTimerHandle(NULL) {
} }
// Default constructor (for backward compatibility) // Default constructor (for backward compatibility)
Player::Player() Player::Player()
: id(0) : id(0)
, name("melody1") , name("melody1")
, uid("x") , uid("x")
@@ -59,6 +63,7 @@ Player::Player()
, _fileManager(nullptr) , _fileManager(nullptr)
, _bellEngine(nullptr) , _bellEngine(nullptr)
, _telemetry(nullptr) , _telemetry(nullptr)
, _timekeeper(nullptr)
, _durationTimerHandle(NULL) { , _durationTimerHandle(NULL) {
} }
@@ -82,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(
@@ -95,28 +100,34 @@ 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();
} }
isPlaying = true; isPlaying = true;
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() {
@@ -133,7 +144,7 @@ void Player::forceStop() {
_telemetry->saveStrikeCounters(); _telemetry->saveStrikeCounters();
} }
LOG_DEBUG("Plbck: FORCE STOP"); LOG_DEBUG(TAG, "Plbck: FORCE STOP");
} }
void Player::stop() { void Player::stop() {
@@ -153,10 +164,10 @@ void Player::stop() {
_telemetry->saveStrikeCounters(); _telemetry->saveStrikeCounters();
} }
LOG_DEBUG("Plbck: STOP from PAUSED state"); LOG_DEBUG(TAG, "Plbck: STOP from PAUSED state");
} else { } else {
setStatus(PlayerStatus::STOPPING); setStatus(PlayerStatus::STOPPING);
LOG_DEBUG("Plbck: SOFT STOP (waiting for melody to complete)"); 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
@@ -165,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) {
@@ -180,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") {
@@ -190,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;
} }
} }
@@ -228,19 +239,18 @@ 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");
} }
@@ -250,20 +260,13 @@ void Player::loadMelodyInRAM() {
// 🎵 PRIORITY 1: Check if this is a built-in melody // 🎵 PRIORITY 1: Check if this is a built-in melody
if (BuiltInMelodies::isBuiltInMelody(uidStr)) { if (BuiltInMelodies::isBuiltInMelody(uidStr)) {
LOG_INFO("Loading built-in melody: %s", uidStr.c_str()); LOG_INFO(TAG, "Loading built-in melody: %s", uidStr.c_str());
if (BuiltInMelodies::loadBuiltInMelody(uidStr, _melodySteps)) { if (BuiltInMelodies::loadBuiltInMelody(uidStr, _melodySteps)) {
LOG_INFO("✅ Built-in melody loaded successfully: %d steps", _melodySteps.size()); LOG_INFO(TAG, "✅ Built-in melody loaded successfully: %d steps", _melodySteps.size());
// Set default speed from built-in melody info
const BuiltInMelodies::MelodyInfo* melodyInfo = BuiltInMelodies::findMelodyByUID(uidStr);
if (melodyInfo && speed == 0) {
speed = melodyInfo->defaultSpeed;
LOG_DEBUG("Using default speed: %d ms/beat", speed);
}
return; return;
} else { } else {
LOG_ERROR("Failed to load built-in melody: %s", uidStr.c_str()); LOG_ERROR(TAG, "Failed to load built-in melody: %s", uidStr.c_str());
return; return;
} }
} }
@@ -273,8 +276,8 @@ void Player::loadMelodyInRAM() {
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) {
@@ -283,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;
} }
@@ -310,7 +313,7 @@ void Player::loadMelodyInRAM() {
_melodySteps[i] = (high << 8) | low; _melodySteps[i] = (high << 8) | low;
} }
LOG_INFO("Melody loaded successfully from SD: %d steps", _melodySteps.size()); LOG_INFO(TAG, "Melody loaded successfully from SD: %d steps", _melodySteps.size());
bin_file.close(); bin_file.close();
} }
@@ -340,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;
} }
} }
@@ -391,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);
} }
} }
@@ -401,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
@@ -412,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;
} }
@@ -427,7 +430,7 @@ 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 segmentStartTime = now; // Reset segment start time for next cycle
return true; return true;
} }
@@ -465,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;
} }

View File

@@ -132,6 +132,12 @@ public:
* @param telemetry Pointer to Telemetry instance * @param telemetry Pointer to Telemetry instance
*/ */
void setTelemetry(Telemetry* telemetry) { _telemetry = telemetry; } 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
@@ -249,6 +255,7 @@ private:
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 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!)

View 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;
};

View File

@@ -5,6 +5,8 @@
*/ */
#include "SettingsWebServer.hpp" #include "SettingsWebServer.hpp"
#define TAG "SettingsWebServer"
#include "SettingsPage.h" #include "SettingsPage.h"
#include "../ConfigManager/ConfigManager.hpp" #include "../ConfigManager/ConfigManager.hpp"
#include "../Networking/Networking.hpp" #include "../Networking/Networking.hpp"
@@ -22,7 +24,7 @@ SettingsWebServer::~SettingsWebServer() {
} }
void SettingsWebServer::begin() { void SettingsWebServer::begin() {
LOG_INFO("SettingsWebServer - Initializing settings web interface"); LOG_INFO(TAG, "SettingsWebServer - Initializing settings web interface");
// GET /settings - Main settings page // GET /settings - Main settings page
_server.on("/settings", HTTP_GET, _server.on("/settings", HTTP_GET,
@@ -45,14 +47,14 @@ void SettingsWebServer::begin() {
} }
); );
LOG_INFO("SettingsWebServer - Endpoints registered"); LOG_INFO(TAG, "SettingsWebServer - Endpoints registered");
LOG_INFO(" GET /settings - Settings page"); LOG_INFO(TAG, " GET /settings - Settings page");
LOG_INFO(" POST /api/set-mode - Set network mode"); LOG_INFO(TAG, " POST /api/set-mode - Set network mode");
LOG_INFO(" POST /api/reboot - Reboot device"); LOG_INFO(TAG, " POST /api/reboot - Reboot device");
} }
void SettingsWebServer::handleSettingsPage(AsyncWebServerRequest* request) { void SettingsWebServer::handleSettingsPage(AsyncWebServerRequest* request) {
LOG_DEBUG("SettingsWebServer - Settings page requested"); LOG_DEBUG(TAG, "SettingsWebServer - Settings page requested");
String html = generateSettingsHTML(); String html = generateSettingsHTML();
request->send(200, "text/html", html); request->send(200, "text/html", html);
} }
@@ -64,13 +66,13 @@ void SettingsWebServer::handleSetMode(AsyncWebServerRequest* request) {
} }
String mode = request->getParam("mode", true)->value(); String mode = request->getParam("mode", true)->value();
LOG_INFO("SettingsWebServer - Mode change requested: %s", mode.c_str()); LOG_INFO(TAG, "SettingsWebServer - Mode change requested: %s", mode.c_str());
if (mode == "ap") { if (mode == "ap") {
// Switch to permanent AP mode // Switch to permanent AP mode
_configManager.setPermanentAPMode(true); _configManager.setPermanentAPMode(true);
_configManager.saveNetworkConfig(); _configManager.saveNetworkConfig();
LOG_INFO("✅ Permanent AP mode enabled - will activate on reboot"); 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."); request->send(200, "text/plain", "AP mode enabled. Device will reboot in 3 seconds.");
// Reboot after 3 seconds // Reboot after 3 seconds
@@ -81,7 +83,7 @@ void SettingsWebServer::handleSetMode(AsyncWebServerRequest* request) {
// Switch to station mode (router mode) // Switch to station mode (router mode)
_configManager.setPermanentAPMode(false); _configManager.setPermanentAPMode(false);
_configManager.saveNetworkConfig(); _configManager.saveNetworkConfig();
LOG_INFO("✅ Station mode enabled - will activate on reboot"); LOG_INFO(TAG, "✅ Station mode enabled - will activate on reboot");
request->send(200, "text/plain", "Station mode enabled. Device will reboot in 3 seconds."); request->send(200, "text/plain", "Station mode enabled. Device will reboot in 3 seconds.");
// Reboot after 3 seconds // Reboot after 3 seconds
@@ -94,7 +96,7 @@ void SettingsWebServer::handleSetMode(AsyncWebServerRequest* request) {
} }
void SettingsWebServer::handleReboot(AsyncWebServerRequest* request) { void SettingsWebServer::handleReboot(AsyncWebServerRequest* request) {
LOG_INFO("SettingsWebServer - Reboot requested"); LOG_INFO(TAG, "SettingsWebServer - Reboot requested");
request->send(200, "text/plain", "Rebooting device in 2 seconds..."); request->send(200, "text/plain", "Rebooting device in 2 seconds...");
delay(2000); delay(2000);

View File

@@ -1,4 +1,6 @@
#include "Telemetry.hpp" #include "Telemetry.hpp"
#define TAG "Telemetry"
#include <ArduinoJson.h> #include <ArduinoJson.h>
void Telemetry::begin() { void Telemetry::begin() {
@@ -17,27 +19,27 @@ void Telemetry::begin() {
// 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) { void Telemetry::setFileManager(FileManager* fm) {
fileManager = fm; fileManager = fm;
LOG_DEBUG("FileManager reference set"); 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;
} }
@@ -52,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];
@@ -65,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];
@@ -78,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];
@@ -100,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() {
@@ -132,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);
@@ -141,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);
@@ -149,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);
@@ -169,17 +171,20 @@ 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) {
// Only run if player is playing OR we're still cooling // Skip processing if paused (OTA freeze mode)
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ? if (!telemetry->isPaused) {
*(telemetry->playerIsPlayingPtr) : false; // Only run if player is playing OR we're still cooling
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
*(telemetry->playerIsPlayingPtr) : false;
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
} }
} }
@@ -190,7 +195,7 @@ void Telemetry::telemetryTask(void* parameter) {
void Telemetry::saveStrikeCounters() { void Telemetry::saveStrikeCounters() {
if (!fileManager) { if (!fileManager) {
LOG_WARNING("Cannot save strike counters: FileManager not set"); LOG_WARNING(TAG, "Cannot save strike counters: FileManager not set");
return; return;
} }
@@ -205,28 +210,28 @@ void Telemetry::saveStrikeCounters() {
portEXIT_CRITICAL(&telemetrySpinlock); portEXIT_CRITICAL(&telemetrySpinlock);
if (fileManager->writeJsonFile("/telemetry_data.json", doc)) { if (fileManager->writeJsonFile("/telemetry_data.json", doc)) {
LOG_INFO("Strike counters saved to SD card"); LOG_INFO(TAG, "Strike counters saved to SD card");
} else { } else {
LOG_ERROR("Failed to save strike counters to SD card"); LOG_ERROR(TAG, "Failed to save strike counters to SD card");
} }
} }
void Telemetry::loadStrikeCounters() { void Telemetry::loadStrikeCounters() {
if (!fileManager) { if (!fileManager) {
LOG_WARNING("Cannot load strike counters: FileManager not set"); LOG_WARNING(TAG, "Cannot load strike counters: FileManager not set");
return; return;
} }
StaticJsonDocument<512> doc; StaticJsonDocument<512> doc;
if (!fileManager->readJsonFile("/telemetry_data.json", doc)) { if (!fileManager->readJsonFile("/telemetry_data.json", doc)) {
LOG_INFO("No previous strike counter data found, starting fresh"); LOG_INFO(TAG, "No previous strike counter data found, starting fresh");
return; return;
} }
JsonArray counters = doc["strikeCounters"]; JsonArray counters = doc["strikeCounters"];
if (counters.isNull()) { if (counters.isNull()) {
LOG_WARNING("Invalid telemetry data format"); LOG_WARNING(TAG, "Invalid telemetry data format");
return; return;
} }
@@ -237,7 +242,7 @@ void Telemetry::loadStrikeCounters() {
} }
portEXIT_CRITICAL(&telemetrySpinlock); portEXIT_CRITICAL(&telemetrySpinlock);
LOG_INFO("Strike counters loaded from SD card"); LOG_INFO(TAG, "Strike counters loaded from SD card");
} }
// ════════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════════
@@ -247,20 +252,20 @@ void Telemetry::loadStrikeCounters() {
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;
} }
@@ -274,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;
} }

View File

@@ -77,6 +77,9 @@ private:
// 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();
@@ -108,6 +111,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

View File

@@ -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_WARNING("Cannot sync time: Networking or ConfigManager not set - using RTC time"); LOG_WARNING(TAG, "Cannot sync time: Networking or ConfigManager not set - using RTC time");
return; return;
} }
if (!_networking->isConnected()) { if (!_networking->isConnected()) {
LOG_INFO("No network connection - skipping NTP sync, using RTC time"); LOG_INFO(TAG, "No network connection - skipping NTP sync, using RTC time");
return; return;
} }
LOG_INFO("⏰ Starting non-blocking NTP sync..."); 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,14 +168,14 @@ 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());
// 🔥 NON-BLOCKING: Try to get time immediately without waiting // 🔥 NON-BLOCKING: Try to get time with reasonable timeout for network response
struct tm timeInfo; struct tm timeInfo;
if (getLocalTime(&timeInfo, 100)) { // 100ms timeout instead of blocking if (getLocalTime(&timeInfo, 5000)) { // 5 second timeout for NTP response
// Success! Update RTC with synchronized time // Success! 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("✅ NTP sync successful: %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);
@@ -167,7 +183,7 @@ void Timekeeper::syncTimeWithNTP() {
loadTodaysEvents(); loadTodaysEvents();
} else { } else {
// No internet or NTP server unreachable - this is NORMAL for local networks // No internet or NTP server unreachable - this is NORMAL for local networks
LOG_INFO("⚠️ NTP sync skipped (no internet) - using RTC time. This is normal for local networks."); LOG_INFO(TAG, "⚠️ NTP sync skipped (no internet) - using RTC time. This is normal for local networks.");
} }
} }
@@ -177,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;
@@ -209,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;
@@ -260,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);
} }
@@ -288,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);
@@ -316,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);
} }
@@ -331,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;
} }
@@ -340,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;
} }
@@ -358,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;
} }
@@ -412,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) {
@@ -446,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());
} }
@@ -496,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());
@@ -509,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
@@ -519,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
} }
@@ -532,26 +548,46 @@ void Timekeeper::checkClockAlerts() {
if (!_configManager || !_configManager->getClockEnabled()) { if (!_configManager || !_configManager->getClockEnabled()) {
return; // Clock is disabled - skip all alert functionality return; // Clock is disabled - skip all alert functionality
} }
// Check if we have required dependencies // Check if we have required dependencies
if (!_outputManager || !rtc.isrunning()) { if (!_outputManager || !rtc.isrunning()) {
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;
} }
// Get clock configuration // Get clock configuration
const auto& clockConfig = _configManager->getClockConfig(); const auto& clockConfig = _configManager->getClockConfig();
// Check if alerts are disabled // Check if alerts are disabled
if (clockConfig.alertType == "OFF") { if (clockConfig.alertType == "OFF") {
return; return;
@@ -571,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;
@@ -580,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;
@@ -604,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") {
@@ -613,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);
} }
} }
@@ -624,22 +660,34 @@ 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
_outputManager->fireOutputForDuration(bellNumber, bellDuration); _outputManager->fireOutputForDuration(bellNumber, bellDuration);
// Wait between rings (only if there's more than one ring) // Wait between rings (only if there's more than one ring)
if (i < count - 1) { if (i < count - 1) {
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() {
@@ -668,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;
@@ -688,7 +736,7 @@ bool Timekeeper::isInSilencePeriod() {
} }
const auto& clockConfig = _configManager->getClockConfig(); const auto& clockConfig = _configManager->getClockConfig();
// Get current time // Get current time
DateTime now = rtc.now(); DateTime now = rtc.now();
char currentTimeStr[6]; char currentTimeStr[6];
@@ -697,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;
} }
} }
@@ -730,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;
} }

View File

@@ -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,7 +62,8 @@ 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
View 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
}

View File

@@ -64,19 +64,23 @@
* 👨‍💻 AUTHOR: BellSystems bonamin * 👨‍💻 AUTHOR: BellSystems bonamin
*/ */
#define FW_VERSION "137" #define FW_VERSION "154"
/* /*
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
* 📅 VERSION HISTORY: * 📅 VERSION HISTORY:
* NOTE: Versions are now stored as integers (v1.3 = 130)
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
* v0.1 (100) - Vesper Launch Beta * v0.1 (100) - Vesper Launch Beta
* v1.2 (120) - Added Log Level Configuration via App/MQTT * v1.2 (120) - Added Log Level Configuration via App/MQTT
* v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes * v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes
* v137 - Made OTA and MQTT delays Async * v137 - Made OTA and MQTT delays Async
* ═══════════════════════════════════════════════════════════════════════════════ * v138 - Removed Ethernet, added default WiFi creds (Mikrotik AP) and fixed various Clock issues
* NOTE: Versions are now stored as integers (v1.3 = 130) * 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.
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
*/ */
@@ -122,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"
@@ -183,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
@@ -193,6 +199,14 @@ 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()
@@ -206,6 +220,15 @@ void setup()
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();
@@ -296,6 +319,7 @@ 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
@@ -337,6 +361,7 @@ void setup()
player.setDependencies(&communication, &fileManager); player.setDependencies(&communication, &fileManager);
player.setBellEngine(&bellEngine); // Connect the beast! player.setBellEngine(&bellEngine); // Connect the beast!
player.setTelemetry(&telemetry); 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);
@@ -347,15 +372,25 @@ void setup()
// Track if AsyncWebServer has been started to prevent duplicates // Track if AsyncWebServer has been started to prevent duplicates
static bool webServerStarted = false; 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]() { [&webServerStarted]() {
communication.onNetworkConnected(); communication.onNetworkConnected();
// Non-blocking NTP sync (graceful without internet) // Schedule non-blocking NTP sync after 3s network stabilization (like MQTT)
// Skip NTP sync in AP mode (no internet connection) // Skip NTP sync in AP mode (no internet connection)
if (!networking.isInAPMode()) { if (!networking.isInAPMode() && ntpSyncTimer) {
timekeeper.syncTimeWithNTP(); LOG_DEBUG("Network connected - scheduling NTP sync after 3s stabilization (non-blocking)");
xTimerStart(ntpSyncTimer, 0);
} }
// Start AsyncWebServer when network becomes available (only once!) // Start AsyncWebServer when network becomes available (only once!)
@@ -374,10 +409,11 @@ void setup()
LOG_INFO("Network already connected - initializing services"); LOG_INFO("Network already connected - initializing services");
communication.onNetworkConnected(); communication.onNetworkConnected();
// Non-blocking NTP sync (graceful without internet) // Schedule non-blocking NTP sync after 3s network stabilization (like MQTT)
// Skip NTP sync in AP mode (no internet connection) // Skip NTP sync in AP mode (no internet connection)
if (!networking.isInAPMode()) { if (!networking.isInAPMode() && ntpSyncTimer) {
timekeeper.syncTimeWithNTP(); 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
@@ -395,7 +431,9 @@ void setup()
// Initialize OTA Manager // Initialize OTA Manager
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
// 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay) // 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay)
// UDP discovery setup can happen immediately without conflicts // UDP discovery setup can happen immediately without conflicts
@@ -477,6 +515,9 @@ void loop()
lastWsCleanup = millis(); 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) {