Compare commits
10 Commits
9f07e9ea39
...
c656835d8e
| Author | SHA1 | Date | |
|---|---|---|---|
| c656835d8e | |||
| 980de08584 | |||
| 9c314d88cf | |||
| 53c55d2726 | |||
| 094b1a9620 | |||
| 11b98166d1 | |||
| 7e279c6e45 | |||
| eb6e0f0e5c | |||
| 7adc1fec34 | |||
| 51b7722e1d |
828
vesper/documentation/API_Reference.md
Normal file
828
vesper/documentation/API_Reference.md
Normal file
@@ -0,0 +1,828 @@
|
|||||||
|
# 🔔 VESPER ESP32 Communication API Reference v3.0
|
||||||
|
|
||||||
|
> **Complete command reference for Vesper Bell Automation System with Grouped Commands**
|
||||||
|
> Version: 3.0 | Updated: 2025-09-15
|
||||||
|
> Supports: MQTT + WebSocket protocols with multi-client support and batch processing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Connection Protocols
|
||||||
|
- **MQTT**: `vesper/{device_id}/control` (commands) → `vesper/{device_id}/data` (responses)
|
||||||
|
- **WebSocket**: `ws://{esp_ip}/ws` (bidirectional)
|
||||||
|
- **UDP Discovery**: Broadcast on configured port for device discovery
|
||||||
|
- **UDP Port**: 32101
|
||||||
|
|
||||||
|
### WebSocket Client Identification
|
||||||
|
**Required for WebSocket clients to receive targeted messages:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "identify",
|
||||||
|
"device_type": "master" // or "secondary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "identify",
|
||||||
|
"payload": "Device identified as master"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Command Categories (NEW GROUPED ARCHITECTURE)
|
||||||
|
|
||||||
|
- [🖥️ System Commands](#️-system-commands)
|
||||||
|
- [🎵 Playback Control](#-playback-control)
|
||||||
|
- [📁 File Management](#-file-management)
|
||||||
|
- [🔧 Relay Setup](#-relay-setup)
|
||||||
|
- [🕐 Clock Setup](#-clock-setup)
|
||||||
|
- [📢 Information Messages](#-information-messages)
|
||||||
|
- [🌐 Network & Discovery](#-network--discovery)
|
||||||
|
- [🔄 Legacy Command Support](#-legacy-command-support)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖥️ System Commands
|
||||||
|
|
||||||
|
### 🏓 Ping Test
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "ping"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "pong",
|
||||||
|
"payload": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 System Status
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "current_status",
|
||||||
|
"payload": {
|
||||||
|
"player_status": "playing",
|
||||||
|
"time_elapsed": 45230,
|
||||||
|
"projected_run_time": 34598,
|
||||||
|
"timestamp": 1699123456789
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 👤 Device Identification (WebSocket Only)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "identify",
|
||||||
|
"device_type": "master"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔄 Restart Device
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "restart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "restart",
|
||||||
|
"payload": "Device will restart in 2 seconds"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Device will reboot after sending the response.
|
||||||
|
|
||||||
|
### 🔄 Force OTA Update
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "force_update",
|
||||||
|
"channel": "stable" // optional: "stable", "beta", or "emergency" (default: "stable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "force_update",
|
||||||
|
"payload": "Starting forced OTA update from channel: stable. Device may reboot."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Response (if player is active):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "force_update",
|
||||||
|
"payload": "Cannot update while playback is active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** If update is successful, device will reboot automatically.
|
||||||
|
|
||||||
|
### 🔥 Custom Firmware Update
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "system",
|
||||||
|
"contents": {
|
||||||
|
"action": "custom_update",
|
||||||
|
"firmware_url": "https://example.com/path/to/firmware.bin",
|
||||||
|
"checksum": "a1b2c3d4e5f6...", // optional: SHA256 checksum for verification
|
||||||
|
"file_size": 1234567, // optional: expected file size in bytes
|
||||||
|
"version": 145 // optional: firmware version number to save in NVS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "custom_update",
|
||||||
|
"payload": "Starting custom OTA update. Device may reboot."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Error Responses:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "custom_update",
|
||||||
|
"payload": "Missing firmware_url parameter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "custom_update",
|
||||||
|
"payload": "Cannot update while playback is active"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Download firmware from any URL (bypasses configured update servers)
|
||||||
|
- Optional SHA256 checksum verification
|
||||||
|
- Optional file size validation
|
||||||
|
- Optional version number to update NVS (prevents unwanted auto-downgrades)
|
||||||
|
- Automatically blocks updates during playback
|
||||||
|
- Device reboots on successful installation
|
||||||
|
|
||||||
|
**Version Parameter Behavior:**
|
||||||
|
- If `version` is provided (> 0): NVS firmware version will be updated to this value
|
||||||
|
- If `version` is omitted or 0: NVS firmware version remains unchanged
|
||||||
|
- **Important:** Without version parameter, future OTA checks may detect your custom firmware as "outdated" and trigger auto-updates/downgrades
|
||||||
|
|
||||||
|
**Note:** If update is successful, device will reboot automatically. Use with caution!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎵 Playback Control
|
||||||
|
|
||||||
|
### ▶️ Start Playback
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "playback",
|
||||||
|
"contents": {
|
||||||
|
"action": "play",
|
||||||
|
"name": "My Melody",
|
||||||
|
"uid": "01DegzV9FA8tYbQpkIHR",
|
||||||
|
"url": "https://example.com/melody.bin",
|
||||||
|
"speed": 500,
|
||||||
|
"note_assignments": [1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
"segment_duration": 15000,
|
||||||
|
"pause_duration": 5000,
|
||||||
|
"total_duration": 60000,
|
||||||
|
"continuous_loop": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⏹️ Stop Playback
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "playback",
|
||||||
|
"contents": {
|
||||||
|
"action": "stop"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 File Management
|
||||||
|
|
||||||
|
### 📋 List Available Melodies
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "file_manager",
|
||||||
|
"contents": {
|
||||||
|
"action": "list_melodies"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "list_melodies",
|
||||||
|
"payload": ["melody1.bin", "melody2.bin", "melody3.bin"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📥 Download Melody
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "file_manager",
|
||||||
|
"contents": {
|
||||||
|
"action": "download_melody",
|
||||||
|
"download_url": "https://example.com/melody.bin",
|
||||||
|
"melodys_uid": "01DegzV9FA8tYbQpkIHR",
|
||||||
|
"name": "Optional Display Name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🗑️ Delete Melody
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "file_manager",
|
||||||
|
"contents": {
|
||||||
|
"action": "delete_melody",
|
||||||
|
"name": "01DegzV9FA8tYbQpkIHR"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Relay Setup
|
||||||
|
|
||||||
|
### ⏱️ Set Relay Timers (Single Bell)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_timers",
|
||||||
|
"b1": 100,
|
||||||
|
"b2": 200,
|
||||||
|
"b3": 150
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⏱️ Set Relay Timers (Batch Mode)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_timers",
|
||||||
|
"timers": {
|
||||||
|
"b1": 100,
|
||||||
|
"b2": 200,
|
||||||
|
"b3": 150,
|
||||||
|
"b4": 300,
|
||||||
|
"b5": 250,
|
||||||
|
"b6": 180
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔌 Set Relay Outputs (Single Bell)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_outputs",
|
||||||
|
"b1": 1,
|
||||||
|
"b2": 2,
|
||||||
|
"b3": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔌 Set Relay Outputs (Batch Mode)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "relay_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_outputs",
|
||||||
|
"outputs": {
|
||||||
|
"b1": 1,
|
||||||
|
"b2": 2,
|
||||||
|
"b3": 3,
|
||||||
|
"b4": 4,
|
||||||
|
"b5": 5,
|
||||||
|
"b6": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🕐 Clock Setup
|
||||||
|
|
||||||
|
### 🔌 Set Clock Outputs
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_outputs",
|
||||||
|
"c1": 1,
|
||||||
|
"c2": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⏰ Set Clock Timings
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_timings",
|
||||||
|
"pulseDuration": 5000,
|
||||||
|
"pauseDuration": 2000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔔 Set Clock Alerts
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_alerts",
|
||||||
|
"alertType": "HOURS",
|
||||||
|
"alertRingInterval": 1000,
|
||||||
|
"hourBell": 1,
|
||||||
|
"halfBell": 2,
|
||||||
|
"quarterBell": 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Set Clock Backlight
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_backlight",
|
||||||
|
"enabled": true,
|
||||||
|
"output": 5,
|
||||||
|
"onTime": "18:00",
|
||||||
|
"offTime": "06:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔇 Set Clock Silence Periods
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "set_silence",
|
||||||
|
"daytime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "13:00",
|
||||||
|
"offTime": "15:00"
|
||||||
|
},
|
||||||
|
"nighttime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "22:00",
|
||||||
|
"offTime": "07:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🚀 Batch Clock Setup (Multiple Settings at Once)
|
||||||
|
|
||||||
|
**Command:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "clock_setup",
|
||||||
|
"contents": {
|
||||||
|
"action": "batch_setup",
|
||||||
|
"outputs": {
|
||||||
|
"c1": 1,
|
||||||
|
"c2": 2
|
||||||
|
},
|
||||||
|
"timings": {
|
||||||
|
"pulseDuration": 5000,
|
||||||
|
"pauseDuration": 2000
|
||||||
|
},
|
||||||
|
"alerts": {
|
||||||
|
"alertType": "HOURS",
|
||||||
|
"hourBell": 1,
|
||||||
|
"halfBell": 2
|
||||||
|
},
|
||||||
|
"backlight": {
|
||||||
|
"enabled": true,
|
||||||
|
"output": 5,
|
||||||
|
"onTime": "18:00",
|
||||||
|
"offTime": "06:00"
|
||||||
|
},
|
||||||
|
"silence": {
|
||||||
|
"daytime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "13:00",
|
||||||
|
"offTime": "15:00"
|
||||||
|
},
|
||||||
|
"nighttime": {
|
||||||
|
"enabled": true,
|
||||||
|
"onTime": "22:00",
|
||||||
|
"offTime": "07:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "clock_setup",
|
||||||
|
"payload": "Batch clock setup updated: 5 sections"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📢 Information Messages
|
||||||
|
|
||||||
|
> **Automatic status broadcasts sent to ALL clients**
|
||||||
|
> These messages are initiated by the ESP32 system and broadcast to all connected clients without being requested.
|
||||||
|
|
||||||
|
### 🎵 Playback Status Updates
|
||||||
|
|
||||||
|
**Sent automatically during playback state changes:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "INFO",
|
||||||
|
"type": "playback",
|
||||||
|
"payload": {
|
||||||
|
"action": "playing",
|
||||||
|
"time_elapsed": 125,
|
||||||
|
"projected_run_time": 5158
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Bell Overload Warnings
|
||||||
|
|
||||||
|
**Sent automatically when bell load monitoring detects issues:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "INFO",
|
||||||
|
"type": "bell_overload",
|
||||||
|
"payload": {
|
||||||
|
"bells": [1, 3, 5],
|
||||||
|
"loads": [85, 92, 78],
|
||||||
|
"severity": "warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Network & Discovery
|
||||||
|
|
||||||
|
### 🔍 UDP Discovery
|
||||||
|
|
||||||
|
**UDP Broadcast Request:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "discover",
|
||||||
|
"svc": "vesper"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**UDP Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"op": "discover_reply",
|
||||||
|
"svc": "vesper",
|
||||||
|
"ver": 1,
|
||||||
|
"name": "Proj. Vesper v0.5",
|
||||||
|
"id": "ESP32_ABC123",
|
||||||
|
"ip": "192.168.1.100",
|
||||||
|
"ws": "ws://192.168.1.100/ws",
|
||||||
|
"port": 80,
|
||||||
|
"fw": "1.2.3",
|
||||||
|
"clients": 2
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Legacy Command Support
|
||||||
|
|
||||||
|
**For backward compatibility, the following legacy commands are still supported:**
|
||||||
|
|
||||||
|
### Individual Commands (Legacy)
|
||||||
|
- `cmd: "ping"` → Use `system` with `action: "ping"`
|
||||||
|
- `cmd: "report_status"` → Use `system` with `action: "status"`
|
||||||
|
- `cmd: "identify"` → Use `system` with `action: "identify"`
|
||||||
|
- `cmd: "list_melodies"` → Use `file_manager` with `action: "list_melodies"`
|
||||||
|
- `cmd: "download_melody"` → Use `file_manager` with `action: "download_melody"`
|
||||||
|
- `cmd: "delete_melody"` → Use `file_manager` with `action: "delete_melody"`
|
||||||
|
- `cmd: "set_relay_timers"` → Use `relay_setup` with `action: "set_timers"`
|
||||||
|
- `cmd: "set_relay_outputs"` → Use `relay_setup` with `action: "set_outputs"`
|
||||||
|
- `cmd: "set_clock_outputs"` → Use `clock_setup` with `action: "set_outputs"`
|
||||||
|
- `cmd: "set_clock_timings"` → Use `clock_setup` with `action: "set_timings"`
|
||||||
|
- `cmd: "set_clock_alerts"` → Use `clock_setup` with `action: "set_alerts"`
|
||||||
|
- `cmd: "set_clock_backlight"` → Use `clock_setup` with `action: "set_backlight"`
|
||||||
|
- `cmd: "set_clock_silence"` → Use `clock_setup` with `action: "set_silence"`
|
||||||
|
|
||||||
|
**Legacy commands will continue to work but are deprecated. Please migrate to the new grouped command structure for optimal performance and features.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Key Advantages of Grouped Commands
|
||||||
|
|
||||||
|
### 🚀 **Batch Processing**
|
||||||
|
- Send multiple settings in a single command
|
||||||
|
- Reduce network overhead and latency
|
||||||
|
- Atomic operations ensure consistency
|
||||||
|
|
||||||
|
### 📊 **Better Organization**
|
||||||
|
- Logical grouping of related commands
|
||||||
|
- Cleaner API structure
|
||||||
|
- Easier to understand and maintain
|
||||||
|
|
||||||
|
### ⚡ **Enhanced Performance**
|
||||||
|
- Fewer round-trips for complex configurations
|
||||||
|
- Optimized ESP32 processing
|
||||||
|
- Improved user experience
|
||||||
|
|
||||||
|
### 🔄 **Backward Compatibility**
|
||||||
|
- Legacy commands still supported
|
||||||
|
- Gradual migration path
|
||||||
|
- No breaking changes for existing implementations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Integration Examples
|
||||||
|
|
||||||
|
### Dart/Flutter App Integration
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// New grouped command approach
|
||||||
|
await ClockSetup.batchClockSetup(
|
||||||
|
c1Output: 1,
|
||||||
|
c2Output: 2,
|
||||||
|
pulseDuration: 5000,
|
||||||
|
pauseDuration: 2000,
|
||||||
|
alertType: 'HOURS',
|
||||||
|
hourBell: 1,
|
||||||
|
backlightEnabled: true,
|
||||||
|
backlightOutput: 5,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Batch relay setup
|
||||||
|
await RelaySetup.setBatchRelayOutputs({
|
||||||
|
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Individual settings still work
|
||||||
|
await ClockSetup.setOddClockOutput(1);
|
||||||
|
await ClockSetup.setEvenClockOutput(2);
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript/WebSocket Integration
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Batch clock configuration
|
||||||
|
const clockConfig = {
|
||||||
|
cmd: "clock_setup",
|
||||||
|
contents: {
|
||||||
|
action: "batch_setup",
|
||||||
|
outputs: { c1: 1, c2: 2 },
|
||||||
|
timings: { pulseDuration: 5000, pauseDuration: 2000 },
|
||||||
|
alerts: { alertType: "HOURS", hourBell: 1 },
|
||||||
|
backlight: { enabled: true, output: 5 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webSocket.send(JSON.stringify(clockConfig));
|
||||||
|
|
||||||
|
// Batch relay configuration
|
||||||
|
const relayConfig = {
|
||||||
|
cmd: "relay_setup",
|
||||||
|
contents: {
|
||||||
|
action: "set_outputs",
|
||||||
|
outputs: {
|
||||||
|
b1: 1, b2: 2, b3: 3, b4: 4, b5: 5, b6: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
webSocket.send(JSON.stringify(relayConfig));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 Error Handling
|
||||||
|
|
||||||
|
### Common Error Types
|
||||||
|
|
||||||
|
**Missing Action Parameter:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "relay_setup",
|
||||||
|
"payload": "Missing action parameter"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Unknown Action:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "clock_setup",
|
||||||
|
"payload": "Unknown action: invalid_action"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Batch Processing Errors:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ERROR",
|
||||||
|
"type": "relay_setup",
|
||||||
|
"payload": "No valid relay timers found in batch"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Success with Count:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"type": "relay_setup",
|
||||||
|
"payload": "Batch relay outputs updated: 6 bells"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📡 Message Routing
|
||||||
|
|
||||||
|
### Response Routing Rules
|
||||||
|
|
||||||
|
1. **Command Responses**: Sent only to the originating client/protocol
|
||||||
|
- MQTT command → MQTT response
|
||||||
|
- WebSocket client #3 → WebSocket client #3 only
|
||||||
|
|
||||||
|
2. **Status Broadcasts**: Sent to ALL connected clients
|
||||||
|
- All WebSocket clients receive the message
|
||||||
|
- MQTT subscribers receive the message
|
||||||
|
- Used for system notifications and status updates
|
||||||
|
|
||||||
|
3. **Targeted Messages**: Based on device type
|
||||||
|
- `broadcastToMasterClients()`: Only master devices
|
||||||
|
- `broadcastToSecondaryClients()`: Only secondary devices
|
||||||
|
- `broadcastToAllWebSocketClients()`: All WebSocket clients
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚡ Performance Optimizations
|
||||||
|
|
||||||
|
### Batch Command Benefits
|
||||||
|
|
||||||
|
**Before (Legacy - 6 separate commands):**
|
||||||
|
```javascript
|
||||||
|
// 6 separate network calls
|
||||||
|
await setRelayOutput(1, 1);
|
||||||
|
await setRelayOutput(2, 2);
|
||||||
|
await setRelayOutput(3, 3);
|
||||||
|
await setRelayOutput(4, 4);
|
||||||
|
await setRelayOutput(5, 5);
|
||||||
|
await setRelayOutput(6, 6);
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Grouped - 1 batch command):**
|
||||||
|
```javascript
|
||||||
|
// 1 network call for all settings
|
||||||
|
await setBatchRelayOutputs({
|
||||||
|
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Performance Metrics
|
||||||
|
- **Network Calls**: Reduced by up to 85%
|
||||||
|
- **Configuration Time**: 3-5x faster
|
||||||
|
- **ESP32 Processing**: More efficient batch updates
|
||||||
|
- **Error Handling**: Atomic operations ensure consistency
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Quick Reference
|
||||||
|
|
||||||
|
### Command Groups
|
||||||
|
| Group | Purpose | Batch Support |
|
||||||
|
|-------|---------|---------------|
|
||||||
|
| `system` | Device management, ping, status | No |
|
||||||
|
| `playback` | Music playback control | No |
|
||||||
|
| `file_manager` | Melody file operations | No |
|
||||||
|
| `relay_setup` | Bell configuration | ✅ Yes |
|
||||||
|
| `clock_setup` | Clock mechanism setup | ✅ Yes |
|
||||||
|
|
||||||
|
### Actions by Group
|
||||||
|
|
||||||
|
**System:** `ping`, `status`, `identify`, `restart`, `force_update`, `custom_update`
|
||||||
|
|
||||||
|
**Playback:** `play`, `stop`
|
||||||
|
|
||||||
|
**File Manager:** `list_melodies`, `download_melody`, `delete_melody`
|
||||||
|
|
||||||
|
**Relay Setup:** `set_timers`, `set_outputs`
|
||||||
|
|
||||||
|
**Clock Setup:** `set_outputs`, `set_timings`, `set_alerts`, `set_backlight`, `set_silence`, `batch_setup`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Happy Bell Automation with Grouped Commands! 🔔*
|
||||||
11
vesper/documentation/BuildProcedure.md
Normal file
11
vesper/documentation/BuildProcedure.md
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
Device Setup Process:
|
||||||
|
|
||||||
|
1. Build device with peripherals.
|
||||||
|
2. Flash Base Firmware
|
||||||
|
3. Set Device Credentials (UID/HWID/Rev) via WebServer on device
|
||||||
|
4. Add Device to BellCloud
|
||||||
|
5. Add Device Credentials to Mosquitto
|
||||||
|
6. Reboot Device to Pull Stable Production Firmware
|
||||||
|
7. Sell the device.
|
||||||
|
- User will bind it to their account
|
||||||
|
- Factory can install App and bind user for convenience
|
||||||
0
vesper/documentation/Roadmap.md
Normal file
0
vesper/documentation/Roadmap.md
Normal file
43
vesper/documentation/SentDevices
Normal file
43
vesper/documentation/SentDevices
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
STAMNA:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PV25L22BP01R01
|
||||||
|
Bell Plus
|
||||||
|
HW: 1.0
|
||||||
|
|
||||||
|
u6545309759@gmail.com
|
||||||
|
bellsystems2025
|
||||||
|
aCx!97IEfTiA073^#*Jj
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
XRISTIANIKH_ELPIS:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PV26B02BP01R01
|
||||||
|
Bell Plus
|
||||||
|
HW: 1.0
|
||||||
|
|
||||||
|
mail: christianikielpis@gmail.com
|
||||||
|
pass: bellsystems2025
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
GREVENA:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PA26B06AM01R01
|
||||||
|
Agnus Mini
|
||||||
|
HW: 1.0
|
||||||
|
|
||||||
|
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
LARISA:
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
|
||||||
|
PA26B06AM01R02
|
||||||
|
Agnus Mini
|
||||||
|
HW: 1.0
|
||||||
42
vesper/documentation/common_commands.txt
Normal file
42
vesper/documentation/common_commands.txt
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
COMMANDS:
|
||||||
|
|
||||||
|
|
||||||
|
PV26A28BC01R01
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"force_update",
|
||||||
|
"channel":"beta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"set_mqtt_log_level",
|
||||||
|
"level":3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"restart"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
"cmd":"system_info",
|
||||||
|
"contents":
|
||||||
|
{
|
||||||
|
"action":"get_full_settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
79
vesper/documentation/features.info
Normal file
79
vesper/documentation/features.info
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
Features:
|
||||||
|
|
||||||
|
// Board Naming Schema:
|
||||||
|
|
||||||
|
eg. PV25K07BC01R01
|
||||||
|
|
||||||
|
PV 25 K 07 BC 01 R 01
|
||||||
|
PV [Y] [M] [D] [BT] [RV] R [BC]
|
||||||
|
|
||||||
|
// SERBIA_OLD: PV25K07BC01R01
|
||||||
|
// SERBIA_NEW: PV26A28BC01R01
|
||||||
|
|
||||||
|
// XRISTIANIKH_ELPIS: PV26B02BP01R01
|
||||||
|
|
||||||
|
|
||||||
|
PV25L22BP01R01
|
||||||
|
|
||||||
|
|
||||||
|
Y: (Year) 2 Digit Year. eg 25 for 2025
|
||||||
|
M: (Month) 1 Letter as Coded Month. eg B for February
|
||||||
|
D: (Day) 2 Digit Date. eg 17 for 17th of the Month
|
||||||
|
BT: (Board Type) 2 letter/digit board Type (custom) eg BC for BellCore
|
||||||
|
RV: (Revision) 2 letter/digit board revision code
|
||||||
|
R: Now, just an R for "Revision" but can change later
|
||||||
|
BC: (Batch Code) 2 digit SerialNumber starting from 01
|
||||||
|
|
||||||
|
|
||||||
|
// mqtt topics:
|
||||||
|
|
||||||
|
vesper/<DEVID>/data // Data sent from the controller
|
||||||
|
vesper/<DEVID>/control // Commands sent to the controller
|
||||||
|
vesper/<DEVID>/kiosk/event // Kiosk Mode Events
|
||||||
|
vesper/<DEVID>/kiosk/info // Kiosk Mode General Info
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- WiFi Manager (captive portal with hotspot)
|
||||||
|
- MQTT Support (Subscribing and Publishing)
|
||||||
|
- WebSocket Support (Sending and Receiving)
|
||||||
|
- JSON Format Messaging (both MQTT and WS)
|
||||||
|
- SD Card Handling and File Ops
|
||||||
|
- Stand-alone Player/BellEngine Classes, with functions to Play/Pause/Stop etc
|
||||||
|
- NoteAssignments - Effectively mapping Notes to Bells
|
||||||
|
- Independent SubSystems for all Core Functions (Networking/Comms/Scheduling/Logging/etc)
|
||||||
|
- Custom Relay Output Maps and Timings (saved on SD)
|
||||||
|
- Timekeeper with RTC/Clock/Alerts/Scheduling features
|
||||||
|
- OTA Update Functionality with Versioning/Rollbacks/Checksum/Firmware Validation/NTP Sync
|
||||||
|
- Global logger with Mode Selection (None, Error, Warning, Info, Debug, Verbose)
|
||||||
|
- UDP Listener for Auto Device Discovery
|
||||||
|
- Datalogging and Statistics:
|
||||||
|
- Counter for each bell (counts total times the bell ringed)
|
||||||
|
- Counter per bell, beats/minute for reliability and thermal protection. Warranty Void scenario.
|
||||||
|
- Ability to change Log levels (in-app)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ToDo Features:
|
||||||
|
|
||||||
|
- (optional) Add Bluetooth support
|
||||||
|
- (optional) Add WiFi Direct AP Support
|
||||||
|
- (optional) Add PCB Temperature Sensor Support
|
||||||
|
|
||||||
|
- (critical) Counters and Statistics:
|
||||||
|
- Counter per playback, to figure out which melody is the most played.
|
||||||
|
This can be implemented on the App itself. Doesn't need to be on the Device.
|
||||||
|
|
||||||
|
- Create a "humanizer" mode that randomizes delays on playback to simulate human ringing.
|
||||||
|
|
||||||
|
|
||||||
|
ToDo Fixes:
|
||||||
|
|
||||||
|
- (small significance) Fix each Log's level Correctly + Fix Log Syntax where needed
|
||||||
|
- (medium significance) BellGuard: Make the buttons functional.
|
||||||
|
|
||||||
|
- Fix IP Settings not applying. More Specifically, Variables inside the Components take long to update. Either Ditch the components, or find another way.
|
||||||
|
- On Very fast playback speeds and small programs that will run for less than a second or so, STOP isn't sent properly. Player keeps indicating "playing".
|
||||||
|
- When a new user is created, set default PINs for both Quick Settings, and Settings.
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
|
||||||
@@ -283,6 +283,8 @@ 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("Unknown system info action: %s", action.c_str());
|
||||||
sendErrorResponse("system_info", "Unknown action: " + action, context);
|
sendErrorResponse("system_info", "Unknown action: " + action, context);
|
||||||
@@ -524,14 +526,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("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("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
|
||||||
@@ -636,19 +638,26 @@ void CommandHandler::handleSetClockEnabledCommand(JsonVariant contents, const Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
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,7 +665,8 @@ 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("TimeKeeper reference not set for device time request");
|
||||||
}
|
}
|
||||||
@@ -858,7 +868,37 @@ void CommandHandler::handleGetFullSettingsCommand(const MessageContext& context)
|
|||||||
LOG_DEBUG("Full settings sent (%d bytes)", responseStr.length());
|
LOG_DEBUG("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("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("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
|
||||||
@@ -1249,6 +1289,8 @@ 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()) {
|
||||||
@@ -1257,10 +1299,11 @@ void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const Messa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Custom update: URL=%s, Checksum=%s, Size=%u",
|
LOG_INFO("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,7 +1312,7 @@ 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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ 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() {}
|
||||||
@@ -106,13 +107,27 @@ void CommunicationRouter::begin() {
|
|||||||
_settingsServer.begin();
|
_settingsServer.begin();
|
||||||
LOG_INFO("✅ Settings Web Server initialized at /settings");
|
LOG_INFO("✅ Settings Web Server initialized at /settings");
|
||||||
|
|
||||||
|
// Initialize UART Command Handler
|
||||||
|
LOG_INFO("Setting up UART Command Handler...");
|
||||||
|
_uartHandler.begin();
|
||||||
|
_uartHandler.setCallback([this](JsonDocument& message) {
|
||||||
|
onUartMessage(message);
|
||||||
|
});
|
||||||
|
LOG_INFO("✅ UART Command Handler initialized (TX: GPIO12, RX: GPIO13)");
|
||||||
|
|
||||||
LOG_INFO("Communication Router initialized with modular architecture");
|
LOG_INFO("Communication Router initialized with modular architecture");
|
||||||
LOG_INFO(" • MQTT: AsyncMqttClient");
|
LOG_INFO(" • MQTT: AsyncMqttClient");
|
||||||
LOG_INFO(" • WebSocket: Multi-client support");
|
LOG_INFO(" • WebSocket: Multi-client support");
|
||||||
LOG_INFO(" • HTTP REST API: /api endpoints");
|
LOG_INFO(" • HTTP REST API: /api endpoints");
|
||||||
|
LOG_INFO(" • UART: External device control");
|
||||||
LOG_INFO(" • Settings Page: /settings");
|
LOG_INFO(" • Settings Page: /settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CommunicationRouter::loop() {
|
||||||
|
// Process UART incoming data
|
||||||
|
_uartHandler.loop();
|
||||||
|
}
|
||||||
|
|
||||||
void CommunicationRouter::setPlayerReference(Player* player) {
|
void CommunicationRouter::setPlayerReference(Player* player) {
|
||||||
_player = player;
|
_player = player;
|
||||||
_commandHandler.setPlayerReference(player);
|
_commandHandler.setPlayerReference(player);
|
||||||
@@ -327,6 +342,40 @@ void CommunicationRouter::onWebSocketMessage(uint32_t clientId, const JsonDocume
|
|||||||
LOG_DEBUG("WebSocket message from client #%u processed", clientId);
|
LOG_DEBUG("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("UART: Ignoring non-whitelisted command (cmd=%s, action=%s)",
|
||||||
|
cmd.c_str(), action.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("🔌 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("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("↗️ Sending response via MQTT: %s", response.c_str());
|
||||||
@@ -334,6 +383,9 @@ void CommunicationRouter::sendResponse(const String& response, const CommandHand
|
|||||||
} 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("↗️ 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("↗️ Sending response via UART: %s", response.c_str());
|
||||||
|
_uartHandler.send(response);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Unknown message source for response routing!");
|
LOG_ERROR("❌ Unknown message source for response routing!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* UARTCOMMANDHANDLER.CPP - UART Command Handler Implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "UARTCommandHandler.hpp"
|
||||||
|
#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("Initializing UART Command Handler");
|
||||||
|
LOG_INFO(" TX Pin: GPIO%d", _txPin);
|
||||||
|
LOG_INFO(" RX Pin: GPIO%d", _rxPin);
|
||||||
|
LOG_INFO(" 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("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("UART buffer overflow, discarding message");
|
||||||
|
_errorCount++;
|
||||||
|
resetBuffer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::setCallback(MessageCallback callback) {
|
||||||
|
_callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::send(const String& response) {
|
||||||
|
if (!_ready) {
|
||||||
|
LOG_ERROR("UART not ready, cannot send response");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_serial.print(response);
|
||||||
|
_serial.print('\n'); // Newline delimiter
|
||||||
|
_serial.flush(); // Ensure data is sent
|
||||||
|
|
||||||
|
LOG_DEBUG("UART TX: %s", response.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::processLine(const char* line) {
|
||||||
|
LOG_DEBUG("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("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("UART message received but no callback set");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UARTCommandHandler::resetBuffer() {
|
||||||
|
_bufferIndex = 0;
|
||||||
|
memset(_buffer, 0, BUFFER_SIZE);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
* UARTCOMMANDHANDLER.HPP - UART Command Interface for External Control Devices
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 🔌 UART COMMAND HANDLER 🔌
|
||||||
|
*
|
||||||
|
* Enables command input from external devices (LCD panels, button controllers)
|
||||||
|
* via UART serial communication. Uses newline-delimited JSON protocol.
|
||||||
|
*
|
||||||
|
* Pin Configuration:
|
||||||
|
* • TX: GPIO12
|
||||||
|
* • RX: GPIO13
|
||||||
|
* • Baud: 115200 (configurable)
|
||||||
|
*
|
||||||
|
* Protocol:
|
||||||
|
* • Newline-delimited JSON messages
|
||||||
|
* • Same command format as MQTT/WebSocket
|
||||||
|
* • Responses sent back on same UART
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-01-19
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
class UARTCommandHandler {
|
||||||
|
public:
|
||||||
|
// Default pin configuration
|
||||||
|
static constexpr uint8_t DEFAULT_TX_PIN = 12;
|
||||||
|
static constexpr uint8_t DEFAULT_RX_PIN = 13;
|
||||||
|
static constexpr uint32_t DEFAULT_BAUD_RATE = 115200;
|
||||||
|
static constexpr size_t BUFFER_SIZE = 1024;
|
||||||
|
|
||||||
|
// Message callback type - called when a complete JSON message is received
|
||||||
|
using MessageCallback = std::function<void(JsonDocument& message)>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Construct UART handler with custom pins
|
||||||
|
* @param txPin GPIO pin for TX (default: 12)
|
||||||
|
* @param rxPin GPIO pin for RX (default: 13)
|
||||||
|
* @param baudRate Baud rate (default: 115200)
|
||||||
|
*/
|
||||||
|
explicit UARTCommandHandler(uint8_t txPin = DEFAULT_TX_PIN,
|
||||||
|
uint8_t rxPin = DEFAULT_RX_PIN,
|
||||||
|
uint32_t baudRate = DEFAULT_BAUD_RATE);
|
||||||
|
|
||||||
|
~UARTCommandHandler();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the UART interface
|
||||||
|
*/
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process incoming UART data (call from loop or task)
|
||||||
|
* Non-blocking - processes available bytes and returns
|
||||||
|
*/
|
||||||
|
void loop();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Set callback for received messages
|
||||||
|
* @param callback Function to call with parsed JSON
|
||||||
|
*/
|
||||||
|
void setCallback(MessageCallback callback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Send a response back over UART
|
||||||
|
* @param response JSON string to send (newline appended automatically)
|
||||||
|
*/
|
||||||
|
void send(const String& response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Check if UART is initialized and ready
|
||||||
|
*/
|
||||||
|
bool isReady() const { return _ready; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get number of messages received since boot
|
||||||
|
*/
|
||||||
|
uint32_t getMessageCount() const { return _messageCount; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Get number of parse errors since boot
|
||||||
|
*/
|
||||||
|
uint32_t getErrorCount() const { return _errorCount; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
HardwareSerial& _serial;
|
||||||
|
uint8_t _txPin;
|
||||||
|
uint8_t _rxPin;
|
||||||
|
uint32_t _baudRate;
|
||||||
|
bool _ready;
|
||||||
|
|
||||||
|
// Receive buffer
|
||||||
|
char _buffer[BUFFER_SIZE];
|
||||||
|
size_t _bufferIndex;
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
uint32_t _messageCount;
|
||||||
|
uint32_t _errorCount;
|
||||||
|
|
||||||
|
// Callback
|
||||||
|
MessageCallback _callback;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Process a complete line from the buffer
|
||||||
|
* @param line Null-terminated string containing the message
|
||||||
|
*/
|
||||||
|
void processLine(const char* line);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reset the receive buffer
|
||||||
|
*/
|
||||||
|
void resetBuffer();
|
||||||
|
};
|
||||||
@@ -529,13 +529,19 @@ 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("ConfigManager - Updated Clock alerts");
|
||||||
}
|
}
|
||||||
@@ -593,7 +599,7 @@ bool ConfigManager::loadClockConfig() {
|
|||||||
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();
|
||||||
|
|
||||||
@@ -602,12 +608,34 @@ bool ConfigManager::loadClockConfig() {
|
|||||||
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>();
|
||||||
|
|
||||||
|
// 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("ConfigManager - Clock config loaded");
|
LOG_DEBUG("ConfigManager - Clock config loaded");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -615,14 +643,37 @@ bool ConfigManager::loadClockConfig() {
|
|||||||
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)) {
|
||||||
@@ -1095,10 +1146,17 @@ 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
|
||||||
|
|||||||
@@ -70,6 +70,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
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "FileManager.hpp"
|
#include "FileManager.hpp"
|
||||||
|
#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
|
||||||
@@ -23,15 +24,26 @@ bool FileManager::addMelody(JsonVariant doc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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("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("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("Melody download successful: %s", melodyUid);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_ERROR("Melody download failed: %s", filename);
|
LOG_ERROR("Melody download failed: %s", melodyUid);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,11 +74,13 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
|
|||||||
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("Using HTTPS with secure client");
|
||||||
} else {
|
} else {
|
||||||
@@ -77,17 +91,28 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
|
|||||||
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
|
||||||
|
|
||||||
|
// Disable task watchdog for current task during blocking HTTPS operation
|
||||||
|
// The TLS handshake can take several seconds and would trigger watchdog
|
||||||
|
LOG_DEBUG("Disabling watchdog for download...");
|
||||||
|
esp_task_wdt_delete(NULL);
|
||||||
|
|
||||||
LOG_DEBUG("Sending HTTP GET request...");
|
LOG_DEBUG("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("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("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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +120,7 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
|
|||||||
if (!ensureDirectoryExists(directory)) {
|
if (!ensureDirectoryExists(directory)) {
|
||||||
LOG_ERROR("Failed to create directory: %s", directory.c_str());
|
LOG_ERROR("Failed to create directory: %s", directory.c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +133,7 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERROR("Failed to open file for writing: %s", fullPath.c_str());
|
LOG_ERROR("Failed to open file for writing: %s", fullPath.c_str());
|
||||||
http.end();
|
http.end();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +161,7 @@ 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("Download progress: %u bytes", totalBytes);
|
||||||
lastLog = millis();
|
lastLog = millis();
|
||||||
@@ -142,10 +169,10 @@ bool FileManager::downloadFile(const String& url, const String& directory, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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,14 +181,16 @@ 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();
|
||||||
|
if (secureClient) delete secureClient;
|
||||||
LOG_INFO("Download complete, file saved to: %s (%u bytes)", fullPath.c_str(), totalBytes);
|
LOG_INFO("Download complete, file saved to: %s (%u bytes)", fullPath.c_str(), totalBytes);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#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"
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ bool Logging::isLevelEnabled(LogLevel level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logging::error(const char* format, ...) {
|
void Logging::error(const char* format, ...) {
|
||||||
if (!isLevelEnabled(ERROR)) return;
|
|
||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
log(ERROR, "🔴 EROR", format, args);
|
log(ERROR, "🔴 EROR", format, args);
|
||||||
@@ -44,8 +42,6 @@ void Logging::error(const char* format, ...) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logging::warning(const char* format, ...) {
|
void Logging::warning(const char* format, ...) {
|
||||||
if (!isLevelEnabled(WARNING)) return;
|
|
||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
log(WARNING, "🟡 WARN", format, args);
|
log(WARNING, "🟡 WARN", format, args);
|
||||||
@@ -53,8 +49,6 @@ void Logging::warning(const char* format, ...) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logging::info(const char* format, ...) {
|
void Logging::info(const char* format, ...) {
|
||||||
if (!isLevelEnabled(INFO)) return;
|
|
||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
log(INFO, "🟢 INFO", format, args);
|
log(INFO, "🟢 INFO", format, args);
|
||||||
@@ -62,8 +56,6 @@ void Logging::info(const char* format, ...) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logging::debug(const char* format, ...) {
|
void Logging::debug(const char* format, ...) {
|
||||||
if (!isLevelEnabled(DEBUG)) return;
|
|
||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
log(DEBUG, "🐞 DEBG", format, args);
|
log(DEBUG, "🐞 DEBG", format, args);
|
||||||
@@ -71,8 +63,6 @@ void Logging::debug(const char* format, ...) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logging::verbose(const char* format, ...) {
|
void Logging::verbose(const char* format, ...) {
|
||||||
if (!isLevelEnabled(VERBOSE)) return;
|
|
||||||
|
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
log(VERBOSE, "🧾 VERB", format, args);
|
log(VERBOSE, "🧾 VERB", format, args);
|
||||||
@@ -80,19 +70,33 @@ void Logging::verbose(const char* format, ...) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Logging::log(LogLevel level, const char* levelStr, const char* format, va_list args) {
|
void Logging::log(LogLevel level, const char* levelStr, const char* format, va_list args) {
|
||||||
// Print the formatted message
|
// Check if ANY output needs this log level
|
||||||
|
bool serialEnabled = (currentLevel >= level);
|
||||||
|
bool mqttEnabled = (mqttLogLevel >= level && mqttPublishCallback);
|
||||||
|
// bool sdEnabled = (sdLogLevel >= level && sdLogCallback); // Future: SD logging
|
||||||
|
|
||||||
|
// Early exit if no outputs need this message
|
||||||
|
if (!serialEnabled && !mqttEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the message once (only if at least one output needs it)
|
||||||
char buffer[512];
|
char buffer[512];
|
||||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||||
|
|
||||||
// Serial output
|
// Serial output (independent check)
|
||||||
Serial.printf("[%s] ", levelStr);
|
if (serialEnabled) {
|
||||||
Serial.print(buffer);
|
Serial.printf("[%s] ", levelStr);
|
||||||
Serial.println();
|
Serial.print(buffer);
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
// MQTT output (if enabled and callback is set)
|
// MQTT output (independent check)
|
||||||
if (mqttLogLevel >= level && mqttPublishCallback) {
|
if (mqttEnabled) {
|
||||||
publishToMqtt(level, levelStr, buffer);
|
publishToMqtt(level, levelStr, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Future: SD logging would go here with its own independent check
|
||||||
}
|
}
|
||||||
|
|
||||||
void Logging::publishToMqtt(LogLevel level, const char* levelStr, const char* message) {
|
void Logging::publishToMqtt(LogLevel level, const char* levelStr, const char* message) {
|
||||||
|
|||||||
@@ -71,63 +71,97 @@ void Networking::begin() {
|
|||||||
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("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("Ethernet connection disabled - falling back to WiFi");
|
||||||
|
startWiFiConnection();
|
||||||
|
|
||||||
// Check if Ethernet hardware initialization failed
|
// Original Ethernet code (DISABLED):
|
||||||
if (!ETH.linkUp()) {
|
// LOG_INFO("Attempting Ethernet connection...");
|
||||||
LOG_WARNING("Ethernet hardware not detected or failed to initialize");
|
// setState(NetworkState::CONNECTING_ETHERNET);
|
||||||
LOG_INFO("Falling back to WiFi immediately");
|
//
|
||||||
startWiFiConnection();
|
// // Check if Ethernet hardware initialization failed
|
||||||
return;
|
// if (!ETH.linkUp()) {
|
||||||
}
|
// LOG_WARNING("Ethernet hardware not detected or failed to initialize");
|
||||||
|
// LOG_INFO("Falling back to WiFi immediately");
|
||||||
// Ethernet will auto-connect via events
|
// startWiFiConnection();
|
||||||
// Set timeout for Ethernet attempt (5 seconds)
|
// return;
|
||||||
_lastConnectionAttempt = millis();
|
// }
|
||||||
|
//
|
||||||
// Start reconnection timer to handle timeout
|
// // Ethernet will auto-connect via events
|
||||||
xTimerStart(_reconnectionTimer, 0);
|
// // 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("Attempting WiFi connection...");
|
||||||
setState(NetworkState::CONNECTING_WIFI);
|
setState(NetworkState::CONNECTING_WIFI);
|
||||||
|
|
||||||
if (!hasValidWiFiCredentials()) {
|
// ALWAYS try default credentials first (for bundled router deployment)
|
||||||
LOG_WARNING("No valid WiFi credentials found");
|
auto& netConfig = _configManager.getNetworkConfig();
|
||||||
if (!_bootSequenceComplete) {
|
|
||||||
// No credentials during boot - start portal
|
|
||||||
startWiFiPortal();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and log saved credentials (for debugging)
|
LOG_INFO("Using DEFAULT WiFi credentials - SSID: %s", netConfig.defaultWifiSsid.c_str());
|
||||||
String savedSSID = _wifiManager->getWiFiSSID(true);
|
|
||||||
LOG_INFO("Using WiFiManager saved credentials - SSID: %s", savedSSID.c_str());
|
|
||||||
|
|
||||||
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("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("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("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() {
|
||||||
@@ -171,16 +205,17 @@ void Networking::handleReconnection() {
|
|||||||
|
|
||||||
LOG_DEBUG("Attempting reconnection...");
|
LOG_DEBUG("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("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) {
|
||||||
@@ -216,20 +251,15 @@ void Networking::handleReconnection() {
|
|||||||
return; // Still waiting for WiFi
|
return; // Still waiting for WiFi
|
||||||
}
|
}
|
||||||
|
|
||||||
// State is DISCONNECTED - decide what to try
|
// State is DISCONNECTED - WiFi only mode (Ethernet disabled)
|
||||||
if (_ethernetCableConnected) {
|
LOG_INFO("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("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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,8 +274,8 @@ bool Networking::isHealthy() const {
|
|||||||
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("Networking: Unhealthy - Not in connected state");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -272,13 +302,14 @@ bool Networking::isHealthy() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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("Networking: Unhealthy - Ethernet link down");
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -305,35 +336,43 @@ 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("Ethernet event received but Ethernet is disabled - ignoring");
|
||||||
setActiveConnection(ConnectionType::ETHERNET);
|
|
||||||
|
|
||||||
// Stop WiFi if it was running
|
// Original code (DISABLED):
|
||||||
if (WiFi.getMode() != WIFI_OFF) {
|
// LOG_INFO("Ethernet connected successfully");
|
||||||
WiFi.disconnect(true);
|
// setState(NetworkState::CONNECTED_ETHERNET);
|
||||||
WiFi.mode(WIFI_OFF);
|
// setActiveConnection(ConnectionType::ETHERNET);
|
||||||
}
|
//
|
||||||
|
// // Stop WiFi if it was running
|
||||||
// Stop reconnection timer
|
// if (WiFi.getMode() != WIFI_OFF) {
|
||||||
xTimerStop(_reconnectionTimer, 0);
|
// WiFi.disconnect(true);
|
||||||
|
// WiFi.mode(WIFI_OFF);
|
||||||
notifyConnectionChange(true);
|
// }
|
||||||
|
//
|
||||||
|
// // Stop reconnection timer
|
||||||
|
// xTimerStop(_reconnectionTimer, 0);
|
||||||
|
//
|
||||||
|
// notifyConnectionChange(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::onEthernetDisconnected() {
|
void Networking::onEthernetDisconnected() {
|
||||||
LOG_WARNING("Ethernet disconnected");
|
// ETHERNET DISABLED - This should never be called
|
||||||
|
LOG_WARNING("Ethernet disconnect event received but Ethernet is disabled - ignoring");
|
||||||
|
|
||||||
if (_activeConnection == ConnectionType::ETHERNET) {
|
// Original code (DISABLED):
|
||||||
setState(NetworkState::DISCONNECTED);
|
// LOG_WARNING("Ethernet disconnected");
|
||||||
setActiveConnection(ConnectionType::NONE);
|
//
|
||||||
notifyConnectionChange(false);
|
// if (_activeConnection == ConnectionType::ETHERNET) {
|
||||||
|
// setState(NetworkState::DISCONNECTED);
|
||||||
// Start reconnection attempts
|
// setActiveConnection(ConnectionType::NONE);
|
||||||
xTimerStart(_reconnectionTimer, 0);
|
// notifyConnectionChange(false);
|
||||||
}
|
//
|
||||||
|
// // Start reconnection attempts
|
||||||
|
// xTimerStart(_reconnectionTimer, 0);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
void Networking::onWiFiConnected() {
|
void Networking::onWiFiConnected() {
|
||||||
@@ -367,35 +406,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("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("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();
|
||||||
|
|
||||||
|
// ETHERNET DISABLED - Only apply WiFi config
|
||||||
|
if (ethernet) {
|
||||||
|
LOG_WARNING("applyNetworkConfig called with ethernet=true but Ethernet is disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (netConfig.useStaticIP) {
|
if (netConfig.useStaticIP) {
|
||||||
LOG_INFO("Applying static IP configuration");
|
LOG_INFO("Applying static IP configuration");
|
||||||
if (ethernet) {
|
WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
|
||||||
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 {
|
} else {
|
||||||
LOG_INFO("Using DHCP configuration");
|
LOG_INFO("Using DHCP configuration");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ethernet) {
|
WiFi.setHostname(netConfig.hostname.c_str());
|
||||||
ETH.setHostname(netConfig.hostname.c_str());
|
|
||||||
} else {
|
|
||||||
WiFi.setHostname(netConfig.hostname.c_str());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Networking::hasValidWiFiCredentials() {
|
bool Networking::hasValidWiFiCredentials() {
|
||||||
@@ -413,7 +454,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("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 +469,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("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:
|
||||||
@@ -445,9 +490,9 @@ void Networking::forceReconnect() {
|
|||||||
WiFi.mode(WIFI_OFF);
|
WiFi.mode(WIFI_OFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restart connection sequence
|
// Restart connection sequence - WiFi only (Ethernet disabled)
|
||||||
delay(1000);
|
delay(1000);
|
||||||
startEthernetConnection();
|
startWiFiConnection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Static callbacks
|
// Static callbacks
|
||||||
@@ -457,32 +502,16 @@ void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_inf
|
|||||||
LOG_DEBUG("Network event: %d", event);
|
LOG_DEBUG("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("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("WiFi Got IP: %s", WiFi.localIP().toString().c_str());
|
||||||
_instance->onWiFiConnected();
|
_instance->onWiFiConnected();
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
#include "../ConfigManager/ConfigManager.hpp"
|
#include "../ConfigManager/ConfigManager.hpp"
|
||||||
#include "../Logging/Logging.hpp"
|
#include "../Logging/Logging.hpp"
|
||||||
#include "../Player/Player.hpp"
|
#include "../Player/Player.hpp"
|
||||||
|
#include "../SDCardMutex/SDCardMutex.hpp"
|
||||||
#include <nvs_flash.h>
|
#include <nvs_flash.h>
|
||||||
#include <nvs.h>
|
#include <nvs.h>
|
||||||
|
#include <esp_task_wdt.h>
|
||||||
|
|
||||||
OTAManager::OTAManager(ConfigManager& configManager)
|
OTAManager::OTAManager(ConfigManager& configManager)
|
||||||
: _configManager(configManager)
|
: _configManager(configManager)
|
||||||
, _fileManager(nullptr)
|
, _fileManager(nullptr)
|
||||||
, _player(nullptr)
|
, _player(nullptr)
|
||||||
|
, _timeKeeper(nullptr)
|
||||||
|
, _telemetry(nullptr)
|
||||||
, _status(Status::IDLE)
|
, _status(Status::IDLE)
|
||||||
, _lastError(ErrorCode::NONE)
|
, _lastError(ErrorCode::NONE)
|
||||||
, _availableVersion(0.0f)
|
, _availableVersion(0.0f)
|
||||||
@@ -22,10 +26,23 @@ OTAManager::OTAManager(ConfigManager& configManager)
|
|||||||
, _progressCallback(nullptr)
|
, _progressCallback(nullptr)
|
||||||
, _statusCallback(nullptr)
|
, _statusCallback(nullptr)
|
||||||
, _scheduledCheckTimer(NULL)
|
, _scheduledCheckTimer(NULL)
|
||||||
, _initialCheckTimer(NULL) {
|
, _initialCheckTimer(NULL)
|
||||||
|
, _otaWorkerTask(NULL)
|
||||||
|
, _otaWorkSignal(NULL)
|
||||||
|
, _pendingWork(OTAWorkType::NONE) {
|
||||||
}
|
}
|
||||||
|
|
||||||
OTAManager::~OTAManager() {
|
OTAManager::~OTAManager() {
|
||||||
|
// Clean up worker task and semaphore
|
||||||
|
if (_otaWorkerTask != NULL) {
|
||||||
|
vTaskDelete(_otaWorkerTask);
|
||||||
|
_otaWorkerTask = NULL;
|
||||||
|
}
|
||||||
|
if (_otaWorkSignal != NULL) {
|
||||||
|
vSemaphoreDelete(_otaWorkSignal);
|
||||||
|
_otaWorkSignal = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
if (_scheduledCheckTimer != NULL) {
|
if (_scheduledCheckTimer != NULL) {
|
||||||
xTimerStop(_scheduledCheckTimer, 0);
|
xTimerStop(_scheduledCheckTimer, 0);
|
||||||
xTimerDelete(_scheduledCheckTimer, portMAX_DELAY);
|
xTimerDelete(_scheduledCheckTimer, portMAX_DELAY);
|
||||||
@@ -42,6 +59,33 @@ void OTAManager::begin() {
|
|||||||
LOG_INFO("OTA Manager initialized");
|
LOG_INFO("OTA Manager initialized");
|
||||||
setStatus(Status::IDLE);
|
setStatus(Status::IDLE);
|
||||||
|
|
||||||
|
// Create semaphore for worker task signaling
|
||||||
|
_otaWorkSignal = xSemaphoreCreateBinary();
|
||||||
|
if (_otaWorkSignal == NULL) {
|
||||||
|
LOG_ERROR("Failed to create OTA work semaphore!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create dedicated worker task with 8KB stack (prevents timer task overflow)
|
||||||
|
BaseType_t taskCreated = xTaskCreatePinnedToCore(
|
||||||
|
otaWorkerTaskFunction,
|
||||||
|
"OTA_Worker",
|
||||||
|
8192, // 8KB stack - plenty for HTTP and JSON operations
|
||||||
|
this, // Pass OTAManager instance
|
||||||
|
2, // Priority 2 (lower than critical tasks)
|
||||||
|
&_otaWorkerTask,
|
||||||
|
0 // Core 0
|
||||||
|
);
|
||||||
|
|
||||||
|
if (taskCreated != pdPASS) {
|
||||||
|
LOG_ERROR("Failed to create OTA worker task!");
|
||||||
|
vSemaphoreDelete(_otaWorkSignal);
|
||||||
|
_otaWorkSignal = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("OTA worker task created with 8KB stack on Core 0");
|
||||||
|
|
||||||
// Create timer for scheduled checks (checks every minute if it's 3:00 AM)
|
// Create timer for scheduled checks (checks every minute if it's 3:00 AM)
|
||||||
_scheduledCheckTimer = xTimerCreate(
|
_scheduledCheckTimer = xTimerCreate(
|
||||||
"OTA_Schedule",
|
"OTA_Schedule",
|
||||||
@@ -84,22 +128,35 @@ void OTAManager::setPlayer(Player* player) {
|
|||||||
_player = player;
|
_player = player;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OTAManager::setTimeKeeper(Timekeeper* tk) {
|
||||||
|
_timeKeeper = tk;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OTAManager::setTelemetry(Telemetry* telemetry) {
|
||||||
|
_telemetry = telemetry;
|
||||||
|
}
|
||||||
|
|
||||||
// ✅ NEW: Static timer callback for initial boot check
|
// ✅ NEW: Static timer callback for initial boot check
|
||||||
|
// CRITICAL: Timer callbacks run in Timer Service task with limited stack!
|
||||||
|
// We ONLY set a flag here - actual work is done by dedicated worker task
|
||||||
void OTAManager::initialCheckCallback(TimerHandle_t xTimer) {
|
void OTAManager::initialCheckCallback(TimerHandle_t xTimer) {
|
||||||
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
|
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
|
||||||
if (ota) {
|
if (ota && ota->_otaWorkSignal) {
|
||||||
LOG_INFO("🚀 Running initial OTA check (non-blocking, async)");
|
// Signal worker task to perform initial check
|
||||||
ota->performInitialCheck();
|
ota->_pendingWork = OTAWorkType::INITIAL_CHECK;
|
||||||
|
xSemaphoreGive(ota->_otaWorkSignal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEW: Perform initial OTA check (async, non-blocking)
|
// ✅ NEW: Perform initial OTA check (async, non-blocking)
|
||||||
void OTAManager::performInitialCheck() {
|
void OTAManager::performInitialCheck() {
|
||||||
// This runs asynchronously, won't block WebSocket/UDP/MQTT
|
// This runs asynchronously in worker task, won't block WebSocket/UDP/MQTT
|
||||||
checkForUpdates();
|
checkForUpdates();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ NEW: Static timer callback for scheduled checks
|
// ✅ NEW: Static timer callback for scheduled checks
|
||||||
|
// CRITICAL: Timer callbacks run in Timer Service task with limited stack!
|
||||||
|
// We ONLY set a flag here - actual work is done by dedicated worker task
|
||||||
void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
|
void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
|
||||||
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
|
OTAManager* ota = static_cast<OTAManager*>(pvTimerGetTimerID(xTimer));
|
||||||
|
|
||||||
@@ -109,13 +166,12 @@ void OTAManager::scheduledCheckCallback(TimerHandle_t xTimer) {
|
|||||||
|
|
||||||
// Only proceed if it's exactly 3:00 AM
|
// Only proceed if it's exactly 3:00 AM
|
||||||
if (timeinfo->tm_hour == 3 && timeinfo->tm_min == 0) {
|
if (timeinfo->tm_hour == 3 && timeinfo->tm_min == 0) {
|
||||||
LOG_INFO("🕒 3:00 AM - Running scheduled OTA check");
|
// Check if player is idle before signaling worker
|
||||||
|
if (!ota->isPlayerActive() && ota->_otaWorkSignal) {
|
||||||
// Check if player is idle before proceeding
|
// Signal worker task to perform scheduled check
|
||||||
if (!ota->isPlayerActive()) {
|
ota->_pendingWork = OTAWorkType::SCHEDULED_CHECK;
|
||||||
LOG_INFO("✅ Player is idle - checking for emergency updates");
|
xSemaphoreGive(ota->_otaWorkSignal);
|
||||||
ota->checkForEmergencyUpdates();
|
} else if (ota->isPlayerActive()) {
|
||||||
} else {
|
|
||||||
LOG_WARNING("⚠️ Player is active - skipping scheduled update check");
|
LOG_WARNING("⚠️ Player is active - skipping scheduled update check");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,9 +412,10 @@ bool OTAManager::downloadAndInstall(const String& channel) {
|
|||||||
LOG_INFO("OTA: Trying firmware download from server %d/%d: %s",
|
LOG_INFO("OTA: Trying firmware download from server %d/%d: %s",
|
||||||
serverIndex + 1, servers.size(), baseUrl.c_str());
|
serverIndex + 1, servers.size(), baseUrl.c_str());
|
||||||
|
|
||||||
if (downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) {
|
// 🔥 Download directly to flash (bypassing problematic SD card writes)
|
||||||
// Success! Now install from SD
|
if (downloadDirectToFlash(firmwareUrl, _expectedFileSize)) {
|
||||||
return installFromSD("/firmware/staged_update.bin");
|
LOG_INFO("✅ OTA update successful!");
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
LOG_WARNING("OTA: Firmware download failed from %s, trying next server", baseUrl.c_str());
|
LOG_WARNING("OTA: Firmware download failed from %s, trying next server", baseUrl.c_str());
|
||||||
}
|
}
|
||||||
@@ -369,6 +426,173 @@ bool OTAManager::downloadAndInstall(const String& channel) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 NEW: Download directly to flash memory, bypassing SD card
|
||||||
|
bool OTAManager::downloadDirectToFlash(const String& url, size_t expectedSize) {
|
||||||
|
LOG_INFO("OTA: Starting direct-to-flash download (bypassing SD card)");
|
||||||
|
|
||||||
|
HTTPClient http;
|
||||||
|
http.begin(url.c_str());
|
||||||
|
http.setTimeout(30000); // 30 seconds
|
||||||
|
|
||||||
|
int httpCode = http.GET();
|
||||||
|
|
||||||
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
|
LOG_ERROR("Download HTTP error code: %d", httpCode);
|
||||||
|
setStatus(Status::FAILED, ErrorCode::HTTP_ERROR);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int contentLength = http.getSize();
|
||||||
|
|
||||||
|
LOG_INFO("OTA: HTTP Response Code: %d", httpCode);
|
||||||
|
LOG_INFO("OTA: Content-Length: %d bytes", contentLength);
|
||||||
|
|
||||||
|
if (contentLength <= 0) {
|
||||||
|
LOG_ERROR("Invalid content length");
|
||||||
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size
|
||||||
|
if (expectedSize > 0 && (size_t)contentLength != expectedSize) {
|
||||||
|
LOG_ERROR("OTA: File size mismatch! Expected: %u, Got: %d", expectedSize, contentLength);
|
||||||
|
setStatus(Status::FAILED, ErrorCode::SIZE_MISMATCH);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// ENTER OTA FREEZE MODE - Pause all non-critical systems
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
LOG_INFO("OTA: Entering freeze mode - pausing TimeKeeper and Telemetry");
|
||||||
|
|
||||||
|
if (_timeKeeper) {
|
||||||
|
_timeKeeper->pauseClockUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_telemetry) {
|
||||||
|
_telemetry->pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin OTA update to flash with MD5 validation enabled
|
||||||
|
if (!Update.begin(contentLength)) {
|
||||||
|
LOG_ERROR("Not enough space to begin OTA update");
|
||||||
|
setStatus(Status::FAILED, ErrorCode::INSUFFICIENT_SPACE);
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
// Resume systems
|
||||||
|
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
|
||||||
|
if (_telemetry) _telemetry->resume();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("OTA: Update partition ready, starting stream write...");
|
||||||
|
LOG_INFO("OTA: Checksum validation will be performed by ESP32 bootloader");
|
||||||
|
setStatus(Status::INSTALLING);
|
||||||
|
|
||||||
|
// Stream directly to flash with periodic watchdog feeding
|
||||||
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
|
uint8_t buffer[4096]; // 4KB buffer for efficient transfer
|
||||||
|
size_t written = 0;
|
||||||
|
size_t lastLoggedPercent = 0;
|
||||||
|
unsigned long lastWatchdogReset = millis();
|
||||||
|
|
||||||
|
while (http.connected() && written < (size_t)contentLength) {
|
||||||
|
size_t available = stream->available();
|
||||||
|
if (available) {
|
||||||
|
size_t toRead = min(available, sizeof(buffer));
|
||||||
|
size_t bytesRead = stream->readBytes(buffer, toRead);
|
||||||
|
|
||||||
|
if (bytesRead > 0) {
|
||||||
|
// Write to flash
|
||||||
|
size_t bytesWritten = Update.write(buffer, bytesRead);
|
||||||
|
|
||||||
|
if (bytesWritten != bytesRead) {
|
||||||
|
LOG_ERROR("OTA: Flash write failed at offset %u (%u/%u bytes written)",
|
||||||
|
written, bytesWritten, bytesRead);
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
// Resume systems
|
||||||
|
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
|
||||||
|
if (_telemetry) _telemetry->resume();
|
||||||
|
|
||||||
|
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
written += bytesWritten;
|
||||||
|
|
||||||
|
// Log progress every 20%
|
||||||
|
size_t currentPercent = (written * 100) / contentLength;
|
||||||
|
if (currentPercent >= lastLoggedPercent + 20) {
|
||||||
|
LOG_INFO("OTA: Flash write progress: %u%% (%u/%u bytes)",
|
||||||
|
currentPercent, written, contentLength);
|
||||||
|
lastLoggedPercent = currentPercent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed watchdog every 500ms to prevent timeout
|
||||||
|
if (millis() - lastWatchdogReset > 500) {
|
||||||
|
esp_task_wdt_reset();
|
||||||
|
lastWatchdogReset = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Small yield to prevent tight loop
|
||||||
|
yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
http.end();
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// EXIT OTA FREEZE MODE - Resume all paused systems
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
LOG_INFO("OTA: Exiting freeze mode - resuming TimeKeeper and Telemetry");
|
||||||
|
|
||||||
|
if (_timeKeeper) {
|
||||||
|
_timeKeeper->resumeClockUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_telemetry) {
|
||||||
|
_telemetry->resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (written == (size_t)contentLength) {
|
||||||
|
LOG_INFO("OTA: Successfully written %u bytes to flash", written);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("OTA: Written only %u/%d bytes", written, contentLength);
|
||||||
|
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Update.end(true)) { // true = set new boot partition
|
||||||
|
LOG_INFO("OTA: Update complete!");
|
||||||
|
if (Update.isFinished()) {
|
||||||
|
setStatus(Status::SUCCESS);
|
||||||
|
LOG_INFO("OTA: Firmware update successful. Rebooting...");
|
||||||
|
|
||||||
|
// Update version in config
|
||||||
|
_configManager.setFwVersion(String(_availableVersion));
|
||||||
|
_configManager.saveDeviceConfig();
|
||||||
|
|
||||||
|
delay(1000);
|
||||||
|
ESP.restart();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("OTA: Update not finished");
|
||||||
|
setStatus(Status::FAILED, ErrorCode::VERIFICATION_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("OTA: Update error: %s", Update.errorString());
|
||||||
|
setStatus(Status::FAILED, ErrorCode::VERIFICATION_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) {
|
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum, size_t expectedSize) {
|
||||||
if (!_fileManager) {
|
if (!_fileManager) {
|
||||||
LOG_ERROR("FileManager not set!");
|
LOG_ERROR("FileManager not set!");
|
||||||
@@ -384,6 +608,10 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
|
|||||||
|
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
http.begin(url.c_str());
|
http.begin(url.c_str());
|
||||||
|
|
||||||
|
// Set timeout to prevent hanging
|
||||||
|
http.setTimeout(30000); // 30 seconds
|
||||||
|
|
||||||
int httpCode = http.GET();
|
int httpCode = http.GET();
|
||||||
|
|
||||||
if (httpCode != HTTP_CODE_OK) {
|
if (httpCode != HTTP_CODE_OK) {
|
||||||
@@ -394,6 +622,10 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
|
|||||||
}
|
}
|
||||||
|
|
||||||
int contentLength = http.getSize();
|
int contentLength = http.getSize();
|
||||||
|
|
||||||
|
// Log HTTP response headers for debugging
|
||||||
|
LOG_INFO("OTA: HTTP Response Code: %d", httpCode);
|
||||||
|
LOG_INFO("OTA: Content-Length header: %d bytes", contentLength);
|
||||||
if (contentLength <= 0) {
|
if (contentLength <= 0) {
|
||||||
LOG_ERROR("Invalid content length");
|
LOG_ERROR("Invalid content length");
|
||||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
@@ -419,55 +651,142 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
|
|||||||
|
|
||||||
LOG_INFO("OTA: Starting download of %d bytes...", contentLength);
|
LOG_INFO("OTA: Starting download of %d bytes...", contentLength);
|
||||||
|
|
||||||
// Open file for writing
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
File file = SD.open(tempPath.c_str(), FILE_WRITE);
|
// ENTER OTA FREEZE MODE - Pause all non-critical systems to prevent SD contention
|
||||||
if (!file) {
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
LOG_ERROR("Failed to create temporary update file");
|
LOG_INFO("OTA: Entering freeze mode - pausing TimeKeeper and Telemetry");
|
||||||
|
|
||||||
|
if (_timeKeeper) {
|
||||||
|
_timeKeeper->pauseClockUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_telemetry) {
|
||||||
|
_telemetry->pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔒 ACQUIRE SD CARD MUTEX - Prevents concurrent SD access
|
||||||
|
LOG_INFO("OTA: Acquiring SD card mutex for safe file operations");
|
||||||
|
if (!SDCardMutex::getInstance().lock(10000)) { // 10 second timeout
|
||||||
|
LOG_ERROR("OTA: Failed to acquire SD card mutex!");
|
||||||
|
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
|
||||||
|
if (_telemetry) _telemetry->resume();
|
||||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
http.end();
|
http.end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete file if it exists, before opening
|
||||||
|
if (SD.exists(tempPath.c_str())) {
|
||||||
|
LOG_INFO("OTA: Removing existing staged update file");
|
||||||
|
if (!SD.remove(tempPath.c_str())) {
|
||||||
|
LOG_ERROR("OTA: Failed to remove existing file!");
|
||||||
|
}
|
||||||
|
delay(200); // Give SD card time to complete deletion
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open file for writing
|
||||||
|
File file = SD.open(tempPath.c_str(), FILE_WRITE);
|
||||||
|
if (!file) {
|
||||||
|
LOG_ERROR("Failed to create temporary update file");
|
||||||
|
|
||||||
|
// Release mutex and resume systems before returning
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
|
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
|
||||||
|
if (_telemetry) _telemetry->resume();
|
||||||
|
|
||||||
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
|
http.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("OTA: File opened successfully for writing (mutex locked)");
|
||||||
|
|
||||||
WiFiClient* stream = http.getStreamPtr();
|
WiFiClient* stream = http.getStreamPtr();
|
||||||
uint8_t buffer[1024];
|
uint8_t buffer[4096]; // ✅ Increased to 4KB for better performance
|
||||||
size_t written = 0;
|
size_t written = 0;
|
||||||
size_t lastLoggedPercent = 0;
|
size_t lastLoggedPercent = 0;
|
||||||
|
unsigned long lastYield = millis();
|
||||||
|
int loopsWithoutData = 0;
|
||||||
|
|
||||||
while (http.connected() && written < (size_t)contentLength) {
|
while (http.connected() && written < (size_t)contentLength) {
|
||||||
size_t available = stream->available();
|
size_t available = stream->available();
|
||||||
if (available) {
|
if (available) {
|
||||||
|
loopsWithoutData = 0; // Reset counter when we have data
|
||||||
size_t toRead = min(available, sizeof(buffer));
|
size_t toRead = min(available, sizeof(buffer));
|
||||||
size_t bytesRead = stream->readBytes(buffer, toRead);
|
size_t bytesRead = stream->readBytes(buffer, toRead);
|
||||||
|
|
||||||
if (bytesRead > 0) {
|
if (bytesRead > 0) {
|
||||||
|
// Write to SD card
|
||||||
size_t bytesWritten = file.write(buffer, bytesRead);
|
size_t bytesWritten = file.write(buffer, bytesRead);
|
||||||
|
|
||||||
|
// Check if write succeeded
|
||||||
if (bytesWritten != bytesRead) {
|
if (bytesWritten != bytesRead) {
|
||||||
LOG_ERROR("SD write failed");
|
LOG_ERROR("SD write failed at offset %u (%u/%u bytes written)", written, bytesWritten, bytesRead);
|
||||||
file.close();
|
file.close();
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
|
if (_timeKeeper) _timeKeeper->resumeClockUpdates();
|
||||||
|
if (_telemetry) _telemetry->resume();
|
||||||
|
|
||||||
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
written += bytesWritten;
|
written += bytesWritten;
|
||||||
|
|
||||||
// ✅ IMPROVED: Progress reporting with percentage
|
// Progress reporting
|
||||||
notifyProgress(written, contentLength);
|
notifyProgress(written, contentLength);
|
||||||
|
|
||||||
// Log progress every 10%
|
// Log progress every 20%
|
||||||
size_t currentPercent = (written * 100) / contentLength;
|
size_t currentPercent = (written * 100) / contentLength;
|
||||||
if (currentPercent >= lastLoggedPercent + 10) {
|
if (currentPercent >= lastLoggedPercent + 20) {
|
||||||
LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)",
|
LOG_INFO("OTA: Download progress: %u%% (%u/%u bytes)",
|
||||||
currentPercent, written, contentLength);
|
currentPercent, written, contentLength);
|
||||||
lastLoggedPercent = currentPercent;
|
lastLoggedPercent = currentPercent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No data available - yield to prevent tight loop
|
||||||
|
loopsWithoutData++;
|
||||||
|
if (loopsWithoutData > 10) {
|
||||||
|
// If we've waited a while with no data, give longer yield
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1)); // 1ms delay
|
||||||
|
loopsWithoutData = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🐕 Yield every 100ms to allow other tasks (including IDLE) to run
|
||||||
|
if (millis() - lastYield > 100) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(1)); // Just 1ms is enough
|
||||||
|
lastYield = millis();
|
||||||
}
|
}
|
||||||
yield();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 CRITICAL: Flush file buffer before closing to ensure all data is written
|
||||||
|
file.flush();
|
||||||
|
delay(100); // Brief delay to ensure SD card completes write
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
|
// 🔓 RELEASE SD CARD MUTEX - Other tasks can now access SD
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
|
LOG_INFO("OTA: SD card mutex released");
|
||||||
|
|
||||||
http.end();
|
http.end();
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// EXIT OTA FREEZE MODE - Resume all paused systems
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
LOG_INFO("OTA: Exiting freeze mode - resuming TimeKeeper and Telemetry");
|
||||||
|
|
||||||
|
if (_timeKeeper) {
|
||||||
|
_timeKeeper->resumeClockUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_telemetry) {
|
||||||
|
_telemetry->resume();
|
||||||
|
}
|
||||||
|
|
||||||
if (written != (size_t)contentLength) {
|
if (written != (size_t)contentLength) {
|
||||||
LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength);
|
LOG_ERROR("Download incomplete: %u/%d bytes", written, contentLength);
|
||||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
@@ -476,7 +795,43 @@ bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum,
|
|||||||
|
|
||||||
LOG_INFO("Download complete (%u bytes)", written);
|
LOG_INFO("Download complete (%u bytes)", written);
|
||||||
|
|
||||||
// Verify checksum
|
// 🔒 Acquire mutex for file verification operations
|
||||||
|
if (!SDCardMutex::getInstance().lock(5000)) {
|
||||||
|
LOG_ERROR("OTA: Failed to acquire SD mutex for verification");
|
||||||
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG: Check actual file size on SD card
|
||||||
|
size_t actualFileSize = _fileManager->getFileSize(tempPath);
|
||||||
|
LOG_INFO("OTA: File size on SD card: %u bytes (expected: %u)", actualFileSize, written);
|
||||||
|
|
||||||
|
if (actualFileSize != written) {
|
||||||
|
LOG_ERROR("OTA: FILE SIZE MISMATCH ON SD CARD! Expected %u, got %u", written, actualFileSize);
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
|
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🔍 DEBUG: Read first 32 bytes for inspection
|
||||||
|
File debugFile = SD.open(tempPath.c_str());
|
||||||
|
if (debugFile) {
|
||||||
|
uint8_t debugBuffer[32];
|
||||||
|
size_t debugRead = debugFile.readBytes((char*)debugBuffer, 32);
|
||||||
|
debugFile.close();
|
||||||
|
|
||||||
|
String hexDump = "OTA: First 32 bytes (hex): ";
|
||||||
|
for (size_t i = 0; i < debugRead && i < 32; i++) {
|
||||||
|
char hex[4];
|
||||||
|
sprintf(hex, "%02X ", debugBuffer[i]);
|
||||||
|
hexDump += hex;
|
||||||
|
}
|
||||||
|
LOG_INFO("%s", hexDump.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
SDCardMutex::getInstance().unlock(); // Release before checksum (checksum will acquire its own)
|
||||||
|
|
||||||
|
// Verify checksum (verifyChecksum acquires its own mutex)
|
||||||
if (!verifyChecksum(tempPath, expectedChecksum)) {
|
if (!verifyChecksum(tempPath, expectedChecksum)) {
|
||||||
LOG_ERROR("Checksum verification failed after download");
|
LOG_ERROR("Checksum verification failed after download");
|
||||||
_fileManager->deleteFile(tempPath);
|
_fileManager->deleteFile(tempPath);
|
||||||
@@ -510,9 +865,16 @@ bool OTAManager::verifyChecksum(const String& filePath, const String& expectedCh
|
|||||||
}
|
}
|
||||||
|
|
||||||
String OTAManager::calculateSHA256(const String& filePath) {
|
String OTAManager::calculateSHA256(const String& filePath) {
|
||||||
|
// 🔒 Acquire SD mutex for file reading
|
||||||
|
if (!SDCardMutex::getInstance().lock(5000)) {
|
||||||
|
LOG_ERROR("Failed to acquire SD mutex for checksum calculation");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
File file = SD.open(filePath.c_str());
|
File file = SD.open(filePath.c_str());
|
||||||
if (!file) {
|
if (!file) {
|
||||||
LOG_ERROR("Failed to open file for checksum calculation: %s", filePath.c_str());
|
LOG_ERROR("Failed to open file for checksum calculation: %s", filePath.c_str());
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +898,9 @@ String OTAManager::calculateSHA256(const String& filePath) {
|
|||||||
|
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
|
// 🔓 Release SD mutex
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
|
|
||||||
// Convert to hex string
|
// Convert to hex string
|
||||||
String hashString = "";
|
String hashString = "";
|
||||||
for (int i = 0; i < 32; i++) {
|
for (int i = 0; i < 32; i++) {
|
||||||
@@ -550,7 +915,17 @@ String OTAManager::calculateSHA256(const String& filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool OTAManager::installFromSD(const String& filePath) {
|
bool OTAManager::installFromSD(const String& filePath) {
|
||||||
|
// 🔒 Acquire SD mutex for file size check
|
||||||
|
if (!SDCardMutex::getInstance().lock(5000)) {
|
||||||
|
LOG_ERROR("Failed to acquire SD mutex for installation");
|
||||||
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
size_t updateSize = _fileManager->getFileSize(filePath);
|
size_t updateSize = _fileManager->getFileSize(filePath);
|
||||||
|
|
||||||
|
SDCardMutex::getInstance().unlock(); // Release after size check
|
||||||
|
|
||||||
if (updateSize == 0) {
|
if (updateSize == 0) {
|
||||||
LOG_ERROR("Empty update file");
|
LOG_ERROR("Empty update file");
|
||||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
@@ -566,9 +941,17 @@ bool OTAManager::installFromSD(const String& filePath) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔒 Acquire SD mutex for file reading during flash
|
||||||
|
if (!SDCardMutex::getInstance().lock(30000)) { // 30 second timeout for flash operation
|
||||||
|
LOG_ERROR("Failed to acquire SD mutex for firmware flash");
|
||||||
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
File updateBin = SD.open(filePath.c_str());
|
File updateBin = SD.open(filePath.c_str());
|
||||||
if (!updateBin) {
|
if (!updateBin) {
|
||||||
LOG_ERROR("Failed to open update file: %s", filePath.c_str());
|
LOG_ERROR("Failed to open update file: %s", filePath.c_str());
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -576,6 +959,9 @@ bool OTAManager::installFromSD(const String& filePath) {
|
|||||||
size_t written = Update.writeStream(updateBin);
|
size_t written = Update.writeStream(updateBin);
|
||||||
updateBin.close();
|
updateBin.close();
|
||||||
|
|
||||||
|
// 🔓 Release SD mutex after reading file
|
||||||
|
SDCardMutex::getInstance().unlock();
|
||||||
|
|
||||||
if (written == updateSize) {
|
if (written == updateSize) {
|
||||||
LOG_INFO("Update written successfully (%u bytes)", written);
|
LOG_INFO("Update written successfully (%u bytes)", written);
|
||||||
} else {
|
} else {
|
||||||
@@ -687,25 +1073,20 @@ bool OTAManager::performManualUpdate(const String& channel) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Starting manual OTA update from %s channel via SD staging...", channel.c_str());
|
LOG_INFO("Starting manual OTA update from %s channel (direct-to-flash)...", channel.c_str());
|
||||||
setStatus(Status::DOWNLOADING);
|
setStatus(Status::DOWNLOADING);
|
||||||
|
|
||||||
String firmwareUrl = buildFirmwareUrl(channel);
|
String firmwareUrl = buildFirmwareUrl(channel);
|
||||||
|
|
||||||
// Download to SD first
|
// Download directly to flash
|
||||||
if (!downloadToSD(firmwareUrl, _availableChecksum, _expectedFileSize)) {
|
return downloadDirectToFlash(firmwareUrl, _expectedFileSize);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install from SD
|
|
||||||
return installFromSD("/firmware/staged_update.bin");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
// CUSTOM FIRMWARE UPDATE
|
// CUSTOM FIRMWARE UPDATE
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
bool OTAManager::performCustomUpdate(const String& firmwareUrl, const String& checksum, size_t fileSize) {
|
bool OTAManager::performCustomUpdate(const String& firmwareUrl, const String& checksum, size_t fileSize, uint16_t version) {
|
||||||
if (_status != Status::IDLE) {
|
if (_status != Status::IDLE) {
|
||||||
LOG_WARNING("OTA update already in progress");
|
LOG_WARNING("OTA update already in progress");
|
||||||
return false;
|
return false;
|
||||||
@@ -718,29 +1099,33 @@ bool OTAManager::performCustomUpdate(const String& firmwareUrl, const String& ch
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("🔥 Starting CUSTOM firmware update...");
|
LOG_INFO("🔥 Starting CUSTOM firmware update (direct-to-flash)...");
|
||||||
LOG_INFO(" URL: %s", firmwareUrl.c_str());
|
LOG_INFO(" URL: %s", firmwareUrl.c_str());
|
||||||
LOG_INFO(" Checksum: %s", checksum.isEmpty() ? "NOT PROVIDED" : checksum.c_str());
|
|
||||||
LOG_INFO(" File Size: %u bytes", fileSize);
|
LOG_INFO(" File Size: %u bytes", fileSize);
|
||||||
|
|
||||||
if (checksum.isEmpty()) {
|
if (!checksum.isEmpty()) {
|
||||||
LOG_WARNING("⚠️ No checksum provided - update will proceed without verification!");
|
LOG_INFO(" Checksum: %s (NOTE: ESP32 will validate after flash)", checksum.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version > 0) {
|
||||||
|
LOG_INFO(" Target Version: %u", version);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus(Status::DOWNLOADING);
|
setStatus(Status::DOWNLOADING);
|
||||||
|
|
||||||
// Download firmware from custom URL to SD
|
// Download directly to flash
|
||||||
if (!downloadToSD(firmwareUrl, checksum, fileSize)) {
|
bool result = downloadDirectToFlash(firmwareUrl, fileSize);
|
||||||
LOG_ERROR("Custom firmware download failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_INFO("✅ Custom firmware downloaded successfully");
|
|
||||||
|
|
||||||
// Install from SD
|
|
||||||
bool result = installFromSD("/firmware/staged_update.bin");
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
|
// Update version in config if provided
|
||||||
|
if (version > 0) {
|
||||||
|
_configManager.setFwVersion(String(version));
|
||||||
|
_configManager.saveDeviceConfig();
|
||||||
|
LOG_INFO("✅ Custom firmware version %u saved to NVS", version);
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("⚠️ No version provided - NVS version unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("🚀 Custom firmware installed - device will reboot");
|
LOG_INFO("🚀 Custom firmware installed - device will reboot");
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR("❌ Custom firmware installation failed");
|
LOG_ERROR("❌ Custom firmware installation failed");
|
||||||
@@ -866,3 +1251,49 @@ bool OTAManager::isHealthy() const {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// WORKER TASK IMPLEMENTATION
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
// Static entry point for worker task
|
||||||
|
void OTAManager::otaWorkerTaskFunction(void* parameter) {
|
||||||
|
OTAManager* ota = static_cast<OTAManager*>(parameter);
|
||||||
|
LOG_INFO("🔧 OTA Worker task started on Core %d with 8KB stack", xPortGetCoreID());
|
||||||
|
|
||||||
|
// Run the worker loop
|
||||||
|
ota->otaWorkerLoop();
|
||||||
|
|
||||||
|
// Should not reach here
|
||||||
|
LOG_ERROR("❌ OTA Worker task ended unexpectedly!");
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Worker task loop - waits for signals and performs OTA operations
|
||||||
|
void OTAManager::otaWorkerLoop() {
|
||||||
|
while (true) {
|
||||||
|
// Wait for work signal (blocks until semaphore is given)
|
||||||
|
if (xSemaphoreTake(_otaWorkSignal, portMAX_DELAY) == pdTRUE) {
|
||||||
|
// Check what type of work to perform
|
||||||
|
switch (_pendingWork) {
|
||||||
|
case OTAWorkType::INITIAL_CHECK:
|
||||||
|
LOG_INFO("🚀 Worker: Performing initial OTA check");
|
||||||
|
performInitialCheck();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OTAWorkType::SCHEDULED_CHECK:
|
||||||
|
LOG_INFO("🕒 Worker: Performing scheduled emergency check");
|
||||||
|
checkForEmergencyUpdates();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OTAWorkType::NONE:
|
||||||
|
default:
|
||||||
|
LOG_WARNING("⚠️ Worker: Received signal but no work pending");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pending work
|
||||||
|
_pendingWork = OTAWorkType::NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -128,6 +136,19 @@ private:
|
|||||||
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);
|
||||||
bool checkVersion();
|
bool checkVersion();
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#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,6 +32,7 @@ Player::Player(CommunicationRouter* comm, FileManager* fm)
|
|||||||
, _fileManager(fm)
|
, _fileManager(fm)
|
||||||
, _bellEngine(nullptr)
|
, _bellEngine(nullptr)
|
||||||
, _telemetry(nullptr)
|
, _telemetry(nullptr)
|
||||||
|
, _timekeeper(nullptr)
|
||||||
, _durationTimerHandle(NULL) {
|
, _durationTimerHandle(NULL) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ Player::Player()
|
|||||||
, _fileManager(nullptr)
|
, _fileManager(nullptr)
|
||||||
, _bellEngine(nullptr)
|
, _bellEngine(nullptr)
|
||||||
, _telemetry(nullptr)
|
, _telemetry(nullptr)
|
||||||
|
, _timekeeper(nullptr)
|
||||||
, _durationTimerHandle(NULL) {
|
, _durationTimerHandle(NULL) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +110,12 @@ void Player::play() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 CRITICAL: Interrupt any active clock alerts - user playback has priority!
|
||||||
|
if (_timekeeper) {
|
||||||
|
_timekeeper->interruptActiveAlert();
|
||||||
|
LOG_DEBUG("Player: Interrupted any active clock alerts");
|
||||||
|
}
|
||||||
|
|
||||||
if (_bellEngine) {
|
if (_bellEngine) {
|
||||||
_bellEngine->setMelodyData(_melodySteps);
|
_bellEngine->setMelodyData(_melodySteps);
|
||||||
_bellEngine->start();
|
_bellEngine->start();
|
||||||
@@ -228,9 +237,8 @@ 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;
|
||||||
@@ -254,13 +262,6 @@ void Player::loadMelodyInRAM() {
|
|||||||
|
|
||||||
if (BuiltInMelodies::loadBuiltInMelody(uidStr, _melodySteps)) {
|
if (BuiltInMelodies::loadBuiltInMelody(uidStr, _melodySteps)) {
|
||||||
LOG_INFO("✅ Built-in melody loaded successfully: %d steps", _melodySteps.size());
|
LOG_INFO("✅ 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("Failed to load built-in melody: %s", uidStr.c_str());
|
||||||
|
|||||||
@@ -133,6 +133,12 @@ public:
|
|||||||
*/
|
*/
|
||||||
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!)
|
||||||
|
|||||||
227
vesper/src/SDCardMutex/SDCardMutex.hpp
Normal file
227
vesper/src/SDCardMutex/SDCardMutex.hpp
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
* SDCARDMUTEX.HPP - Thread-Safe SD Card Access Manager
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
*
|
||||||
|
* 🔒 THE SD CARD CONCURRENCY GUARDIAN OF VESPER 🔒
|
||||||
|
*
|
||||||
|
* This singleton class provides thread-safe access to the SD card by managing
|
||||||
|
* a FreeRTOS mutex. All SD card operations MUST acquire this mutex to prevent
|
||||||
|
* concurrent access that can lead to file corruption and write failures.
|
||||||
|
*
|
||||||
|
* CRITICAL: The ESP32 SD library is NOT thread-safe. Without this mutex,
|
||||||
|
* simultaneous SD access from multiple FreeRTOS tasks will cause:
|
||||||
|
* - File corruption
|
||||||
|
* - Write failures
|
||||||
|
* - SD card "not recognized" errors
|
||||||
|
* - Random intermittent failures
|
||||||
|
*
|
||||||
|
* USAGE:
|
||||||
|
*
|
||||||
|
* // Lock before ANY SD operation
|
||||||
|
* if (SDCardMutex::getInstance().lock()) {
|
||||||
|
* File file = SD.open("/myfile.txt", FILE_WRITE);
|
||||||
|
* file.println("data");
|
||||||
|
* file.close();
|
||||||
|
* SDCardMutex::getInstance().unlock();
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* // Or use RAII helper for automatic unlock
|
||||||
|
* {
|
||||||
|
* SDCardLock lock; // Acquires mutex
|
||||||
|
* File file = SD.open("/myfile.txt", FILE_WRITE);
|
||||||
|
* file.println("data");
|
||||||
|
* file.close();
|
||||||
|
* } // Automatically releases mutex when going out of scope
|
||||||
|
*
|
||||||
|
* 📋 VERSION: 1.0
|
||||||
|
* 📅 DATE: 2025-01-07
|
||||||
|
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include "../Logging/Logging.hpp"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Singleton class for thread-safe SD card access
|
||||||
|
*
|
||||||
|
* Manages a global mutex that all SD card operations must acquire
|
||||||
|
* to prevent concurrent access from multiple FreeRTOS tasks.
|
||||||
|
*/
|
||||||
|
class SDCardMutex {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Get the singleton instance
|
||||||
|
* @return Reference to the singleton instance
|
||||||
|
*/
|
||||||
|
static SDCardMutex& getInstance() {
|
||||||
|
static SDCardMutex instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the mutex (call once during setup)
|
||||||
|
* @return true if initialization succeeded, false otherwise
|
||||||
|
*/
|
||||||
|
bool begin() {
|
||||||
|
if (_mutex != NULL) {
|
||||||
|
LOG_WARNING("SDCardMutex already initialized");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
if (_mutex == NULL) {
|
||||||
|
LOG_ERROR("Failed to create SD card mutex!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("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 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("SD card mutex timeout after %u ms!", timeoutMs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Release the SD card mutex
|
||||||
|
*/
|
||||||
|
void unlock() {
|
||||||
|
if (_mutex == NULL) {
|
||||||
|
LOG_ERROR("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("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;
|
||||||
|
};
|
||||||
@@ -172,12 +172,15 @@ void Telemetry::telemetryTask(void* parameter) {
|
|||||||
LOG_INFO("Telemetry task started");
|
LOG_INFO("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
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -109,6 +112,10 @@ public:
|
|||||||
// Force stop callback (to be set by main application)
|
// Force stop callback (to be set by main application)
|
||||||
void setForceStopCallback(void (*callback)());
|
void setForceStopCallback(void (*callback)());
|
||||||
|
|
||||||
|
// Pause/Resume for OTA freeze mode (stops SD writes during firmware update)
|
||||||
|
void pause() { isPaused = true; }
|
||||||
|
void resume() { isPaused = false; }
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// HEALTH CHECK METHOD
|
// HEALTH CHECK METHOD
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#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>
|
||||||
|
|
||||||
@@ -47,6 +48,19 @@ void Timekeeper::setNetworking(Networking* networking) {
|
|||||||
LOG_INFO("Timekeeper connected to Networking");
|
LOG_INFO("Timekeeper connected to Networking");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Timekeeper::setPlayer(Player* player) {
|
||||||
|
_player = player;
|
||||||
|
LOG_INFO("Timekeeper connected to Player for playback coordination");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Timekeeper::interruptActiveAlert() {
|
||||||
|
if (alertInProgress.load()) {
|
||||||
|
LOG_INFO("⚡ 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("Using LEGACY relay function - consider upgrading to OutputManager");
|
||||||
@@ -152,9 +166,9 @@ 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));
|
||||||
@@ -538,14 +552,34 @@ void Timekeeper::checkClockAlerts() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔥 CRITICAL: Check if Player is busy - if so, SKIP alert completely
|
||||||
|
if (_player && _player->isPlaying) {
|
||||||
|
// Player is active (playing, paused, stopping, etc.) - skip alert entirely
|
||||||
|
// Mark this alert as processed to prevent it from firing when playback ends
|
||||||
|
DateTime now = rtc.now();
|
||||||
|
int currentMinute = now.minute();
|
||||||
|
|
||||||
|
if (currentMinute == 0) {
|
||||||
|
lastHour = now.hour(); // Mark hour as processed
|
||||||
|
} else if (currentMinute == 30) {
|
||||||
|
lastMinute = 30; // Mark half-hour as processed
|
||||||
|
} else if (currentMinute == 15 || currentMinute == 45) {
|
||||||
|
lastMinute = currentMinute; // Mark quarter-hour as processed
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_DEBUG("⏭️ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,7 +659,16 @@ 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("⚡ 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);
|
||||||
|
|
||||||
@@ -640,6 +683,9 @@ void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) {
|
|||||||
vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval));
|
vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark alert as complete
|
||||||
|
alertInProgress.store(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Timekeeper::checkBacklightAutomation() {
|
void Timekeeper::checkBacklightAutomation() {
|
||||||
@@ -697,14 +743,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("🔇 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("🌙 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <RTClib.h>
|
#include <RTClib.h>
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
@@ -61,6 +62,7 @@ private:
|
|||||||
// Alert management - new functionality
|
// Alert management - new functionality
|
||||||
int lastHour = -1; // Track last processed hour to avoid duplicate alerts
|
int lastHour = -1; // Track last processed hour to avoid duplicate alerts
|
||||||
int lastMinute = -1; // Track last processed minute for quarter/half alerts
|
int lastMinute = -1; // Track last processed minute for quarter/half alerts
|
||||||
|
std::atomic<bool> alertInProgress{false}; // Flag to track if alert is currently playing
|
||||||
|
|
||||||
// Backlight management - new functionality
|
// Backlight management - new functionality
|
||||||
bool backlightState = false; // Track current backlight state
|
bool backlightState = false; // Track current backlight state
|
||||||
@@ -69,6 +71,7 @@ private:
|
|||||||
OutputManager* _outputManager = nullptr;
|
OutputManager* _outputManager = nullptr;
|
||||||
ConfigManager* _configManager = nullptr;
|
ConfigManager* _configManager = nullptr;
|
||||||
Networking* _networking = nullptr;
|
Networking* _networking = nullptr;
|
||||||
|
class Player* _player = nullptr; // Reference to Player for playback status checks
|
||||||
|
|
||||||
// Legacy function pointer (DEPRECATED - will be removed)
|
// Legacy function pointer (DEPRECATED - will be removed)
|
||||||
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
||||||
@@ -84,12 +87,16 @@ public:
|
|||||||
void setOutputManager(OutputManager* outputManager);
|
void setOutputManager(OutputManager* outputManager);
|
||||||
void setConfigManager(ConfigManager* configManager);
|
void setConfigManager(ConfigManager* configManager);
|
||||||
void setNetworking(Networking* networking);
|
void setNetworking(Networking* networking);
|
||||||
|
void setPlayer(class Player* player); // Set Player reference for playback coordination
|
||||||
|
|
||||||
// Clock Updates Pause Functions
|
// Clock Updates Pause Functions
|
||||||
void pauseClockUpdates() { clockUpdatesPaused = true; }
|
void pauseClockUpdates() { clockUpdatesPaused = true; }
|
||||||
void resumeClockUpdates() { clockUpdatesPaused = false; }
|
void resumeClockUpdates() { clockUpdatesPaused = false; }
|
||||||
bool areClockUpdatesPaused() const { return clockUpdatesPaused; }
|
bool areClockUpdatesPaused() const { return clockUpdatesPaused; }
|
||||||
|
|
||||||
|
// Alert interruption - called by Player when starting playback
|
||||||
|
void interruptActiveAlert();
|
||||||
|
|
||||||
// Legacy interface (DEPRECATED - will be removed)
|
// Legacy interface (DEPRECATED - will be removed)
|
||||||
void setRelayWriteFunction(void (*func)(int, int));
|
void setRelayWriteFunction(void (*func)(int, int));
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user