Compare commits

..

10 Commits

29 changed files with 3054 additions and 447 deletions

View File

@@ -0,0 +1,828 @@
# 🔔 VESPER ESP32 Communication API Reference v3.0
> **Complete command reference for Vesper Bell Automation System with Grouped Commands**
> Version: 3.0 | Updated: 2025-09-15
> Supports: MQTT + WebSocket protocols with multi-client support and batch processing
---
## 🚀 Getting Started
### Connection Protocols
- **MQTT**: `vesper/{device_id}/control` (commands) → `vesper/{device_id}/data` (responses)
- **WebSocket**: `ws://{esp_ip}/ws` (bidirectional)
- **UDP Discovery**: Broadcast on configured port for device discovery
- **UDP Port**: 32101
### WebSocket Client Identification
**Required for WebSocket clients to receive targeted messages:**
```json
{
"cmd": "system",
"contents": {
"action": "identify",
"device_type": "master" // or "secondary"
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"type": "identify",
"payload": "Device identified as master"
}
```
---
## 📋 Command Categories (NEW GROUPED ARCHITECTURE)
- [🖥️ System Commands](#-system-commands)
- [🎵 Playback Control](#-playback-control)
- [📁 File Management](#-file-management)
- [🔧 Relay Setup](#-relay-setup)
- [🕐 Clock Setup](#-clock-setup)
- [📢 Information Messages](#-information-messages)
- [🌐 Network & Discovery](#-network--discovery)
- [🔄 Legacy Command Support](#-legacy-command-support)
---
## 🖥️ System Commands
### 🏓 Ping Test
**Command:**
```json
{
"cmd": "system",
"contents": {
"action": "ping"
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"type": "pong",
"payload": ""
}
```
### 📊 System Status
**Command:**
```json
{
"cmd": "system",
"contents": {
"action": "status"
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"type": "current_status",
"payload": {
"player_status": "playing",
"time_elapsed": 45230,
"projected_run_time": 34598,
"timestamp": 1699123456789
}
}
```
### 👤 Device Identification (WebSocket Only)
**Command:**
```json
{
"cmd": "system",
"contents": {
"action": "identify",
"device_type": "master"
}
}
```
### 🔄 Restart Device
**Command:**
```json
{
"cmd": "system",
"contents": {
"action": "restart"
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"type": "restart",
"payload": "Device will restart in 2 seconds"
}
```
**Note:** Device will reboot after sending the response.
### 🔄 Force OTA Update
**Command:**
```json
{
"cmd": "system",
"contents": {
"action": "force_update",
"channel": "stable" // optional: "stable", "beta", or "emergency" (default: "stable")
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"type": "force_update",
"payload": "Starting forced OTA update from channel: stable. Device may reboot."
}
```
**Error Response (if player is active):**
```json
{
"status": "ERROR",
"type": "force_update",
"payload": "Cannot update while playback is active"
}
```
**Note:** If update is successful, device will reboot automatically.
### 🔥 Custom Firmware Update
**Command:**
```json
{
"cmd": "system",
"contents": {
"action": "custom_update",
"firmware_url": "https://example.com/path/to/firmware.bin",
"checksum": "a1b2c3d4e5f6...", // optional: SHA256 checksum for verification
"file_size": 1234567, // optional: expected file size in bytes
"version": 145 // optional: firmware version number to save in NVS
}
}
```
**Response:**
```json
{
"status": "SUCCESS",
"type": "custom_update",
"payload": "Starting custom OTA update. Device may reboot."
}
```
**Error Responses:**
```json
{
"status": "ERROR",
"type": "custom_update",
"payload": "Missing firmware_url parameter"
}
```
```json
{
"status": "ERROR",
"type": "custom_update",
"payload": "Cannot update while playback is active"
}
```
**Features:**
- Download firmware from any URL (bypasses configured update servers)
- Optional SHA256 checksum verification
- Optional file size validation
- Optional version number to update NVS (prevents unwanted auto-downgrades)
- Automatically blocks updates during playback
- Device reboots on successful installation
**Version Parameter Behavior:**
- If `version` is provided (> 0): NVS firmware version will be updated to this value
- If `version` is omitted or 0: NVS firmware version remains unchanged
- **Important:** Without version parameter, future OTA checks may detect your custom firmware as "outdated" and trigger auto-updates/downgrades
**Note:** If update is successful, device will reboot automatically. Use with caution!
---
## 🎵 Playback Control
### ▶️ Start Playback
**Command:**
```json
{
"cmd": "playback",
"contents": {
"action": "play",
"name": "My Melody",
"uid": "01DegzV9FA8tYbQpkIHR",
"url": "https://example.com/melody.bin",
"speed": 500,
"note_assignments": [1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"segment_duration": 15000,
"pause_duration": 5000,
"total_duration": 60000,
"continuous_loop": true
}
}
```
### ⏹️ Stop Playback
**Command:**
```json
{
"cmd": "playback",
"contents": {
"action": "stop"
}
}
```
---
## 📁 File Management
### 📋 List Available Melodies
**Command:**
```json
{
"cmd": "file_manager",
"contents": {
"action": "list_melodies"
}
}
```
**Success Response:**
```json
{
"status": "SUCCESS",
"type": "list_melodies",
"payload": ["melody1.bin", "melody2.bin", "melody3.bin"]
}
```
### 📥 Download Melody
**Command:**
```json
{
"cmd": "file_manager",
"contents": {
"action": "download_melody",
"download_url": "https://example.com/melody.bin",
"melodys_uid": "01DegzV9FA8tYbQpkIHR",
"name": "Optional Display Name"
}
}
```
### 🗑️ Delete Melody
**Command:**
```json
{
"cmd": "file_manager",
"contents": {
"action": "delete_melody",
"name": "01DegzV9FA8tYbQpkIHR"
}
}
```
---
## 🔧 Relay Setup
### ⏱️ Set Relay Timers (Single Bell)
**Command:**
```json
{
"cmd": "relay_setup",
"contents": {
"action": "set_timers",
"b1": 100,
"b2": 200,
"b3": 150
}
}
```
### ⏱️ Set Relay Timers (Batch Mode)
**Command:**
```json
{
"cmd": "relay_setup",
"contents": {
"action": "set_timers",
"timers": {
"b1": 100,
"b2": 200,
"b3": 150,
"b4": 300,
"b5": 250,
"b6": 180
}
}
}
```
### 🔌 Set Relay Outputs (Single Bell)
**Command:**
```json
{
"cmd": "relay_setup",
"contents": {
"action": "set_outputs",
"b1": 1,
"b2": 2,
"b3": 3
}
}
```
### 🔌 Set Relay Outputs (Batch Mode)
**Command:**
```json
{
"cmd": "relay_setup",
"contents": {
"action": "set_outputs",
"outputs": {
"b1": 1,
"b2": 2,
"b3": 3,
"b4": 4,
"b5": 5,
"b6": 6
}
}
}
```
---
## 🕐 Clock Setup
### 🔌 Set Clock Outputs
**Command:**
```json
{
"cmd": "clock_setup",
"contents": {
"action": "set_outputs",
"c1": 1,
"c2": 2
}
}
```
### ⏰ Set Clock Timings
**Command:**
```json
{
"cmd": "clock_setup",
"contents": {
"action": "set_timings",
"pulseDuration": 5000,
"pauseDuration": 2000
}
}
```
### 🔔 Set Clock Alerts
**Command:**
```json
{
"cmd": "clock_setup",
"contents": {
"action": "set_alerts",
"alertType": "HOURS",
"alertRingInterval": 1000,
"hourBell": 1,
"halfBell": 2,
"quarterBell": 3
}
}
```
### 💡 Set Clock Backlight
**Command:**
```json
{
"cmd": "clock_setup",
"contents": {
"action": "set_backlight",
"enabled": true,
"output": 5,
"onTime": "18:00",
"offTime": "06:00"
}
}
```
### 🔇 Set Clock Silence Periods
**Command:**
```json
{
"cmd": "clock_setup",
"contents": {
"action": "set_silence",
"daytime": {
"enabled": true,
"onTime": "13:00",
"offTime": "15:00"
},
"nighttime": {
"enabled": true,
"onTime": "22:00",
"offTime": "07:00"
}
}
}
```
### 🚀 Batch Clock Setup (Multiple Settings at Once)
**Command:**
```json
{
"cmd": "clock_setup",
"contents": {
"action": "batch_setup",
"outputs": {
"c1": 1,
"c2": 2
},
"timings": {
"pulseDuration": 5000,
"pauseDuration": 2000
},
"alerts": {
"alertType": "HOURS",
"hourBell": 1,
"halfBell": 2
},
"backlight": {
"enabled": true,
"output": 5,
"onTime": "18:00",
"offTime": "06:00"
},
"silence": {
"daytime": {
"enabled": true,
"onTime": "13:00",
"offTime": "15:00"
},
"nighttime": {
"enabled": true,
"onTime": "22:00",
"offTime": "07:00"
}
}
}
}
```
**Success Response:**
```json
{
"status": "SUCCESS",
"type": "clock_setup",
"payload": "Batch clock setup updated: 5 sections"
}
```
---
## 📢 Information Messages
> **Automatic status broadcasts sent to ALL clients**
> These messages are initiated by the ESP32 system and broadcast to all connected clients without being requested.
### 🎵 Playback Status Updates
**Sent automatically during playback state changes:**
```json
{
"status": "INFO",
"type": "playback",
"payload": {
"action": "playing",
"time_elapsed": 125,
"projected_run_time": 5158
}
}
```
### ⚠️ Bell Overload Warnings
**Sent automatically when bell load monitoring detects issues:**
```json
{
"status": "INFO",
"type": "bell_overload",
"payload": {
"bells": [1, 3, 5],
"loads": [85, 92, 78],
"severity": "warning"
}
}
```
---
## 🌐 Network & Discovery
### 🔍 UDP Discovery
**UDP Broadcast Request:**
```json
{
"op": "discover",
"svc": "vesper"
}
```
**UDP Response:**
```json
{
"op": "discover_reply",
"svc": "vesper",
"ver": 1,
"name": "Proj. Vesper v0.5",
"id": "ESP32_ABC123",
"ip": "192.168.1.100",
"ws": "ws://192.168.1.100/ws",
"port": 80,
"fw": "1.2.3",
"clients": 2
}
```
---
## 🔄 Legacy Command Support
**For backward compatibility, the following legacy commands are still supported:**
### Individual Commands (Legacy)
- `cmd: "ping"` → Use `system` with `action: "ping"`
- `cmd: "report_status"` → Use `system` with `action: "status"`
- `cmd: "identify"` → Use `system` with `action: "identify"`
- `cmd: "list_melodies"` → Use `file_manager` with `action: "list_melodies"`
- `cmd: "download_melody"` → Use `file_manager` with `action: "download_melody"`
- `cmd: "delete_melody"` → Use `file_manager` with `action: "delete_melody"`
- `cmd: "set_relay_timers"` → Use `relay_setup` with `action: "set_timers"`
- `cmd: "set_relay_outputs"` → Use `relay_setup` with `action: "set_outputs"`
- `cmd: "set_clock_outputs"` → Use `clock_setup` with `action: "set_outputs"`
- `cmd: "set_clock_timings"` → Use `clock_setup` with `action: "set_timings"`
- `cmd: "set_clock_alerts"` → Use `clock_setup` with `action: "set_alerts"`
- `cmd: "set_clock_backlight"` → Use `clock_setup` with `action: "set_backlight"`
- `cmd: "set_clock_silence"` → Use `clock_setup` with `action: "set_silence"`
**Legacy commands will continue to work but are deprecated. Please migrate to the new grouped command structure for optimal performance and features.**
---
## 🔧 Key Advantages of Grouped Commands
### 🚀 **Batch Processing**
- Send multiple settings in a single command
- Reduce network overhead and latency
- Atomic operations ensure consistency
### 📊 **Better Organization**
- Logical grouping of related commands
- Cleaner API structure
- Easier to understand and maintain
### ⚡ **Enhanced Performance**
- Fewer round-trips for complex configurations
- Optimized ESP32 processing
- Improved user experience
### 🔄 **Backward Compatibility**
- Legacy commands still supported
- Gradual migration path
- No breaking changes for existing implementations
---
## 🔧 Integration Examples
### Dart/Flutter App Integration
```dart
// New grouped command approach
await ClockSetup.batchClockSetup(
c1Output: 1,
c2Output: 2,
pulseDuration: 5000,
pauseDuration: 2000,
alertType: 'HOURS',
hourBell: 1,
backlightEnabled: true,
backlightOutput: 5,
);
// Batch relay setup
await RelaySetup.setBatchRelayOutputs({
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6,
});
// Individual settings still work
await ClockSetup.setOddClockOutput(1);
await ClockSetup.setEvenClockOutput(2);
```
### JavaScript/WebSocket Integration
```javascript
// Batch clock configuration
const clockConfig = {
cmd: "clock_setup",
contents: {
action: "batch_setup",
outputs: { c1: 1, c2: 2 },
timings: { pulseDuration: 5000, pauseDuration: 2000 },
alerts: { alertType: "HOURS", hourBell: 1 },
backlight: { enabled: true, output: 5 }
}
};
webSocket.send(JSON.stringify(clockConfig));
// Batch relay configuration
const relayConfig = {
cmd: "relay_setup",
contents: {
action: "set_outputs",
outputs: {
b1: 1, b2: 2, b3: 3, b4: 4, b5: 5, b6: 6
}
}
};
webSocket.send(JSON.stringify(relayConfig));
```
---
## 🚨 Error Handling
### Common Error Types
**Missing Action Parameter:**
```json
{
"status": "ERROR",
"type": "relay_setup",
"payload": "Missing action parameter"
}
```
**Unknown Action:**
```json
{
"status": "ERROR",
"type": "clock_setup",
"payload": "Unknown action: invalid_action"
}
```
**Batch Processing Errors:**
```json
{
"status": "ERROR",
"type": "relay_setup",
"payload": "No valid relay timers found in batch"
}
```
**Success with Count:**
```json
{
"status": "SUCCESS",
"type": "relay_setup",
"payload": "Batch relay outputs updated: 6 bells"
}
```
---
## 📡 Message Routing
### Response Routing Rules
1. **Command Responses**: Sent only to the originating client/protocol
- MQTT command → MQTT response
- WebSocket client #3 → WebSocket client #3 only
2. **Status Broadcasts**: Sent to ALL connected clients
- All WebSocket clients receive the message
- MQTT subscribers receive the message
- Used for system notifications and status updates
3. **Targeted Messages**: Based on device type
- `broadcastToMasterClients()`: Only master devices
- `broadcastToSecondaryClients()`: Only secondary devices
- `broadcastToAllWebSocketClients()`: All WebSocket clients
---
## ⚡ Performance Optimizations
### Batch Command Benefits
**Before (Legacy - 6 separate commands):**
```javascript
// 6 separate network calls
await setRelayOutput(1, 1);
await setRelayOutput(2, 2);
await setRelayOutput(3, 3);
await setRelayOutput(4, 4);
await setRelayOutput(5, 5);
await setRelayOutput(6, 6);
```
**After (Grouped - 1 batch command):**
```javascript
// 1 network call for all settings
await setBatchRelayOutputs({
1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6
});
```
### Performance Metrics
- **Network Calls**: Reduced by up to 85%
- **Configuration Time**: 3-5x faster
- **ESP32 Processing**: More efficient batch updates
- **Error Handling**: Atomic operations ensure consistency
---
## 🔧 Quick Reference
### Command Groups
| Group | Purpose | Batch Support |
|-------|---------|---------------|
| `system` | Device management, ping, status | No |
| `playback` | Music playback control | No |
| `file_manager` | Melody file operations | No |
| `relay_setup` | Bell configuration | ✅ Yes |
| `clock_setup` | Clock mechanism setup | ✅ Yes |
### Actions by Group
**System:** `ping`, `status`, `identify`, `restart`, `force_update`, `custom_update`
**Playback:** `play`, `stop`
**File Manager:** `list_melodies`, `download_melody`, `delete_melody`
**Relay Setup:** `set_timers`, `set_outputs`
**Clock Setup:** `set_outputs`, `set_timings`, `set_alerts`, `set_backlight`, `set_silence`, `batch_setup`
---
*Happy Bell Automation with Grouped Commands! 🔔*

View File

@@ -0,0 +1,11 @@
Device Setup Process:
1. Build device with peripherals.
2. Flash Base Firmware
3. Set Device Credentials (UID/HWID/Rev) via WebServer on device
4. Add Device to BellCloud
5. Add Device Credentials to Mosquitto
6. Reboot Device to Pull Stable Production Firmware
7. Sell the device.
- User will bind it to their account
- Factory can install App and bind user for convenience

View File

View File

@@ -0,0 +1,43 @@
# # # # # # # # # # # # # # # # # # # # # #
STAMNA:
# # # # # # # # # # # # # # # # # # # # # #
PV25L22BP01R01
Bell Plus
HW: 1.0
u6545309759@gmail.com
bellsystems2025
aCx!97IEfTiA073^#*Jj
# # # # # # # # # # # # # # # # # # # # # #
XRISTIANIKH_ELPIS:
# # # # # # # # # # # # # # # # # # # # # #
PV26B02BP01R01
Bell Plus
HW: 1.0
mail: christianikielpis@gmail.com
pass: bellsystems2025
# # # # # # # # # # # # # # # # # # # # # #
GREVENA:
# # # # # # # # # # # # # # # # # # # # # #
PA26B06AM01R01
Agnus Mini
HW: 1.0
# # # # # # # # # # # # # # # # # # # # # #
LARISA:
# # # # # # # # # # # # # # # # # # # # # #
PA26B06AM01R02
Agnus Mini
HW: 1.0

View File

@@ -0,0 +1,42 @@
COMMANDS:
PV26A28BC01R01
{
"cmd":"system",
"contents":
{
"action":"force_update",
"channel":"beta"
}
}
{
"cmd":"system",
"contents":
{
"action":"set_mqtt_log_level",
"level":3
}
}
{
"cmd":"system",
"contents":
{
"action":"restart"
}
}
{
"cmd":"system_info",
"contents":
{
"action":"get_full_settings"
}
}

View File

@@ -0,0 +1,79 @@
Features:
// Board Naming Schema:
eg. PV25K07BC01R01
PV 25 K 07 BC 01 R 01
PV [Y] [M] [D] [BT] [RV] R [BC]
// SERBIA_OLD: PV25K07BC01R01
// SERBIA_NEW: PV26A28BC01R01
// XRISTIANIKH_ELPIS: PV26B02BP01R01
PV25L22BP01R01
Y: (Year) 2 Digit Year. eg 25 for 2025
M: (Month) 1 Letter as Coded Month. eg B for February
D: (Day) 2 Digit Date. eg 17 for 17th of the Month
BT: (Board Type) 2 letter/digit board Type (custom) eg BC for BellCore
RV: (Revision) 2 letter/digit board revision code
R: Now, just an R for "Revision" but can change later
BC: (Batch Code) 2 digit SerialNumber starting from 01
// mqtt topics:
vesper/<DEVID>/data // Data sent from the controller
vesper/<DEVID>/control // Commands sent to the controller
vesper/<DEVID>/kiosk/event // Kiosk Mode Events
vesper/<DEVID>/kiosk/info // Kiosk Mode General Info
- WiFi Manager (captive portal with hotspot)
- MQTT Support (Subscribing and Publishing)
- WebSocket Support (Sending and Receiving)
- JSON Format Messaging (both MQTT and WS)
- SD Card Handling and File Ops
- Stand-alone Player/BellEngine Classes, with functions to Play/Pause/Stop etc
- NoteAssignments - Effectively mapping Notes to Bells
- Independent SubSystems for all Core Functions (Networking/Comms/Scheduling/Logging/etc)
- Custom Relay Output Maps and Timings (saved on SD)
- Timekeeper with RTC/Clock/Alerts/Scheduling features
- OTA Update Functionality with Versioning/Rollbacks/Checksum/Firmware Validation/NTP Sync
- Global logger with Mode Selection (None, Error, Warning, Info, Debug, Verbose)
- UDP Listener for Auto Device Discovery
- Datalogging and Statistics:
- Counter for each bell (counts total times the bell ringed)
- Counter per bell, beats/minute for reliability and thermal protection. Warranty Void scenario.
- Ability to change Log levels (in-app)
ToDo Features:
- (optional) Add Bluetooth support
- (optional) Add WiFi Direct AP Support
- (optional) Add PCB Temperature Sensor Support
- (critical) Counters and Statistics:
- Counter per playback, to figure out which melody is the most played.
This can be implemented on the App itself. Doesn't need to be on the Device.
- Create a "humanizer" mode that randomizes delays on playback to simulate human ringing.
ToDo Fixes:
- (small significance) Fix each Log's level Correctly + Fix Log Syntax where needed
- (medium significance) BellGuard: Make the buttons functional.
- Fix IP Settings not applying. More Specifically, Variables inside the Components take long to update. Either Ditch the components, or find another way.
- On Very fast playback speeds and small programs that will run for less than a second or so, STOP isn't sent properly. Player keeps indicating "playing".
- When a new user is created, set default PINs for both Quick Settings, and Settings.

View File

@@ -47,120 +47,465 @@ struct MelodyInfo {
const char* uid; // Unique identifier const char* uid; // Unique identifier
const uint16_t* data; // Pointer to melody data in PROGMEM const uint16_t* data; // Pointer to melody data in PROGMEM
uint16_t stepCount; // Number of steps uint16_t stepCount; // Number of steps
uint16_t defaultSpeed; // Default speed in milliseconds per beat
}; };
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// EXAMPLE MELODIES - Add your melodies here! // BuiltIn Melodies // More can be added here
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// Example: Simple Scale (C-D-E-F-G-A-B-C) // 1 Bell Test Melody
const uint16_t PROGMEM melody_simple_scale[] = { const uint16_t PROGMEM builtin_1bell_test[] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0001, 0x0000, 0x0001, 0x0000
0x0010, 0x0020, 0x0040, 0x0080
}; };
// Example: Happy Birthday (simplified) // Doxology Traditional
const uint16_t PROGMEM melody_happy_birthday[] = { const uint16_t PROGMEM builtin_doxology_traditional[] = {
0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0000, 0x0000,
0x0008, 0x0004, 0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0008, 0x0000, 0x0000,
0x0002, 0x0001, 0x0010, 0x0008, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0001, 0x0080, 0x0008, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008, 0x0000, 0x0000
0x0004, 0x0002, 0x0040, 0x0040,
0x0008, 0x0004, 0x0002
}; };
// Example: Jingle Bells (simplified) // Doxology Alternative
const uint16_t PROGMEM melody_jingle_bells[] = { const uint16_t PROGMEM builtin_doxology_alternative[] = {
0x0004, 0x0004, 0x0004, 0x0000, 0x0001, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001,
0x0004, 0x0004, 0x0004, 0x0000, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0000,
0x0004, 0x0008, 0x0001, 0x0002, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000, 0x0001, 0x0002, 0x0001,
0x0004, 0x0000, 0x0000, 0x0000, 0x0002, 0x0004, 0x0000, 0x0018, 0x0000
0x0008, 0x0008, 0x0008, 0x0008,
0x0008, 0x0004, 0x0004, 0x0004,
0x0002, 0x0002, 0x0004, 0x0002,
0x0008, 0x0000, 0x0000, 0x0000
}; };
// Example: Westminster Chimes // Doxology Festive
const uint16_t PROGMEM melody_westminster_chimes[] = { const uint16_t PROGMEM builtin_doxology_festive[] = {
0x0008, 0x0004, 0x0002, 0x0001, 0x0002, 0x0004, 0x0009, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004,
0x0001, 0x0002, 0x0008, 0x0004, 0x0002, 0x0004, 0x0021, 0x0004, 0x0002, 0x0004, 0x0011, 0x0004
0x0008, 0x0001, 0x0002, 0x0004,
0x0002, 0x0008, 0x0004, 0x0001
}; };
// Example: Alarm Pattern // Vesper Traditional
const uint16_t PROGMEM melody_alarm[] = { const uint16_t PROGMEM builtin_vesper_traditional[] = {
0x0001, 0x0080, 0x0001, 0x0080, 0x0001, 0x0002, 0x0004, 0x0000, 0x0001, 0x0002, 0x0004, 0x0000,
0x0001, 0x0080, 0x0001, 0x0080, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0000
0x0000, 0x0000, 0x0001, 0x0080,
0x0001, 0x0080, 0x0001, 0x0080
}; };
// Example: Doorbell // Vesper Alternative
const uint16_t PROGMEM melody_doorbell[] = { const uint16_t PROGMEM builtin_vesper_alternative[] = {
0x0004, 0x0008, 0x0004, 0x0008 0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0004, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0002, 0x0000, 0x0000,
0x0001, 0x0002, 0x0000, 0x0000, 0x0001, 0x0004, 0x0000, 0x0000,
0x0001, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
}; };
// Example: Single Bell Test // Catehetical
const uint16_t PROGMEM melody_single_bell[] = { const uint16_t PROGMEM builtin_catehetical[] = {
0x0001 0x0001, 0x0002, 0x0004, 0x0008, 0x0010
}; };
// Orthros Traditional
const uint16_t PROGMEM builtin_orthros_traditional[] = {
0x0001, 0x0000, 0x0002, 0x0000, 0x0004, 0x0008, 0x0000, 0x0010,
0x0000, 0x0020, 0x0000, 0x0040, 0x0080, 0x0000
};
// Orthros Alternative
const uint16_t PROGMEM builtin_orthros_alternative[] = {
0x0001, 0x0000, 0x0002, 0x0001, 0x0000, 0x0002, 0x0000, 0x0001,
0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0004, 0x0000
};
// Mournfull Toll
const uint16_t PROGMEM builtin_mournfull_toll[] = {
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
// Mournfull Toll Alternative
const uint16_t PROGMEM builtin_mournfull_toll_alternative[] = {
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0004,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0002, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0008, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0008, 0x0008, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000
};
// Mournfull Toll Meg Par
const uint16_t PROGMEM builtin_mournfull_toll_meg_par[] = {
0x0001, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0001, 0x0001,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0004, 0x0004, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0000, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0002, 0x0002, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0000, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000,
0x0008, 0x0008, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000
};
// Sematron
const uint16_t PROGMEM builtin_sematron[] = {
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0001, 0x0008,
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0008
};
// Sematron Alternative
const uint16_t PROGMEM builtin_sematron_alternative[] = {
0x0001, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0001, 0x0008,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0001, 0x0001, 0x0008
};
// Athonite 1 2 Voices
const uint16_t PROGMEM builtin_athonite_1_2_voices[] = {
0x0001, 0x0002, 0x0001, 0x0001, 0x0002, 0x0001, 0x0001, 0x0002,
0x0001, 0x0001, 0x0002, 0x0001, 0x0002
};
// Athonite 3 Voices
const uint16_t PROGMEM builtin_athonite_3_voices[] = {
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004
};
// Athonite 3 4 Voices
const uint16_t PROGMEM builtin_athonite_3_4_voices[] = {
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0008, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0009, 0x0002, 0x0001, 0x0005, 0x0002, 0x0001,
0x000A, 0x0002, 0x0001, 0x0006, 0x0002, 0x0001, 0x0009, 0x0002,
0x0001, 0x0005, 0x0002, 0x0001, 0x000A, 0x0002, 0x0001, 0x0006,
0x0002, 0x0001, 0x0009
};
// Athonite 4 8 Voices
const uint16_t PROGMEM builtin_athonite_4_8_voices[] = {
0x0002, 0x0001, 0x0000, 0x0000, 0x0002, 0x0001, 0x0000, 0x0000,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0008, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0002, 0x0005,
0x0002, 0x0001, 0x0009, 0x0002, 0x0001, 0x0011, 0x0002, 0x0001,
0x0022, 0x0002, 0x0001, 0x0081, 0x0002, 0x0001, 0x000A, 0x0002,
0x0001, 0x0041, 0x0002, 0x0001, 0x0012, 0x0002, 0x0001, 0x0021,
0x0002, 0x0001, 0x0082, 0x0002, 0x0001, 0x0009, 0x0002, 0x0001,
0x0042, 0x0002, 0x0001, 0x0011, 0x0002, 0x0001, 0x0022, 0x0002,
0x0001, 0x0081, 0x0002, 0x0001, 0x000A, 0x0002, 0x0001, 0x0041,
0x0002, 0x0001, 0x0000, 0x0005, 0x0002, 0x0001, 0x0000, 0x0005,
0x0002, 0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0000, 0x0000,
0x0000
};
// Onebyone 2 3 Voices
const uint16_t PROGMEM builtin_onebyone_2_3_voices[] = {
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002
};
// Onebyone 4 8 Voices
const uint16_t PROGMEM builtin_onebyone_4_8_voices[] = {
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001, 0x0004,
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0011, 0x0002, 0x0004,
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
0x0002, 0x0041, 0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0081,
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0041, 0x0002, 0x0004,
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
0x0002, 0x0041, 0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0081,
0x0002, 0x0004, 0x0008, 0x0004, 0x0002, 0x0041, 0x0002, 0x0004,
0x0008, 0x0004, 0x0002, 0x0021, 0x0002, 0x0004, 0x0008, 0x0004,
0x0002, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
0x0004, 0x0001, 0x0002, 0x0001, 0x0004, 0x0001, 0x0002, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001,
0x0002, 0x0001, 0x0000
};
// Festive 1Voice
const uint16_t PROGMEM builtin_festive_1voice[] = {
0x0001, 0x0001, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0001, 0x0001, 0x0000, 0x0001,
0x0001, 0x0001, 0x0001, 0x0001, 0x0001, 0x0000, 0x0001, 0x0000
};
// Festive 4Voices
const uint16_t PROGMEM builtin_festive_4voices[] = {
0x0001, 0x0002, 0x0004, 0x0009, 0x0002, 0x0001, 0x0004, 0x0009
};
// Festive 5Voices
const uint16_t PROGMEM builtin_festive_5voices[] = {
0x0001, 0x0002, 0x0004, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0008, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0010, 0x0002, 0x0001, 0x0002
};
// Festive 5Voice Alternative
const uint16_t PROGMEM builtin_festive_5voice_alternative[] = {
0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004, 0x0002, 0x0008,
0x0001, 0x0010, 0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004,
0x0002, 0x0008, 0x0001, 0x0011, 0x0004, 0x0002, 0x0008, 0x0001,
0x0004, 0x0004, 0x0002, 0x0008, 0x0001, 0x0011, 0x0004, 0x0002,
0x0008, 0x0001, 0x0005, 0x0004, 0x0002, 0x0008, 0x0001, 0x0011,
0x0004, 0x0002, 0x0008, 0x0001, 0x0005, 0x0004, 0x0002, 0x0008,
0x0001, 0x0011, 0x0004, 0x0002, 0x0008, 0x0001, 0x0004, 0x0004,
0x0002, 0x0008, 0x0001, 0x0010, 0x0004, 0x0002, 0x0008, 0x0001,
0x0004, 0x0004, 0x0002, 0x0008, 0x0001, 0x0010
};
// Festive 6Voices
const uint16_t PROGMEM builtin_festive_6voices[] = {
0x0001, 0x0002, 0x0004, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002,
0x0008, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0004, 0x0002,
0x0001, 0x0002, 0x0001, 0x0002, 0x0010, 0x0002, 0x0001, 0x0002,
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
0x0001, 0x0002, 0x0009, 0x0002, 0x0001, 0x0002, 0x0011, 0x0002,
0x0001, 0x0002, 0x0005, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002,
0x0001, 0x0002
};
// Festive 8Voices
const uint16_t PROGMEM builtin_festive_8voices[] = {
0x0001, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0080
};
// Ormilia
const uint16_t PROGMEM builtin_ormilia[] = {
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0002, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001, 0x0002, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0009, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0005, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001, 0x0000, 0x0001,
0x0002, 0x0009, 0x0000, 0x0001, 0x0002, 0x0005, 0x0000, 0x0001,
0x0002, 0x0009, 0x0000, 0x0001, 0x0002, 0x0005, 0x0000, 0x0001,
0x0002, 0x0011, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002, 0x0001,
0x0002, 0x0011, 0x0002, 0x0001, 0x0002, 0x0021, 0x0002, 0x0041,
0x0002, 0x0081, 0x0002, 0x0009, 0x0002, 0x0041, 0x0002, 0x0081,
0x0002, 0x0009, 0x0002, 0x0041, 0x0002, 0x0081, 0x0002, 0x0005,
0x0002, 0x0001, 0x0000
};
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
// MELODY LIBRARY - Array of all built-in melodies // MELODY LIBRARY - Array of all built-in melodies
// ═════════════════════════════════════════════════════════════════════════════════ // ═════════════════════════════════════════════════════════════════════════════════
const MelodyInfo MELODY_LIBRARY[] = { const MelodyInfo MELODY_LIBRARY[] = {
{ {
"Simple Scale", "1 Bell Test",
"builtin_scale", "builtin_1bell_test",
melody_simple_scale, builtin_1bell_test,
sizeof(melody_simple_scale) / sizeof(uint16_t), sizeof(builtin_1bell_test) / sizeof(uint16_t)
200 // 200ms per beat
}, },
{ {
"Happy Birthday", "Doxology Traditional",
"builtin_happy_birthday", "builtin_doxology_traditional",
melody_happy_birthday, builtin_doxology_traditional,
sizeof(melody_happy_birthday) / sizeof(uint16_t), sizeof(builtin_doxology_traditional) / sizeof(uint16_t)
250
}, },
{ {
"Jingle Bells", "Doxology Alternative",
"builtin_jingle_bells", "builtin_doxology_alternative",
melody_jingle_bells, builtin_doxology_alternative,
sizeof(melody_jingle_bells) / sizeof(uint16_t), sizeof(builtin_doxology_alternative) / sizeof(uint16_t)
180
}, },
{ {
"Westminster Chimes", "Doxology Festive",
"builtin_westminster", "builtin_doxology_festive",
melody_westminster_chimes, builtin_doxology_festive,
sizeof(melody_westminster_chimes) / sizeof(uint16_t), sizeof(builtin_doxology_festive) / sizeof(uint16_t)
400
}, },
{ {
"Alarm", "Vesper Traditional",
"builtin_alarm", "builtin_vesper_traditional",
melody_alarm, builtin_vesper_traditional,
sizeof(melody_alarm) / sizeof(uint16_t), sizeof(builtin_vesper_traditional) / sizeof(uint16_t)
150
}, },
{ {
"Doorbell", "Vesper Alternative",
"builtin_doorbell", "builtin_vesper_alternative",
melody_doorbell, builtin_vesper_alternative,
sizeof(melody_doorbell) / sizeof(uint16_t), sizeof(builtin_vesper_alternative) / sizeof(uint16_t)
300
}, },
{ {
"Single Bell Test", "Catehetical",
"builtin_single_bell", "builtin_catehetical",
melody_single_bell, builtin_catehetical,
sizeof(melody_single_bell) / sizeof(uint16_t), sizeof(builtin_catehetical) / sizeof(uint16_t)
100 },
{
"Orthros Traditional",
"builtin_orthros_traditional",
builtin_orthros_traditional,
sizeof(builtin_orthros_traditional) / sizeof(uint16_t)
},
{
"Orthros Alternative",
"builtin_orthros_alternative",
builtin_orthros_alternative,
sizeof(builtin_orthros_alternative) / sizeof(uint16_t)
},
{
"Mournfull Toll",
"builtin_mournfull_toll",
builtin_mournfull_toll,
sizeof(builtin_mournfull_toll) / sizeof(uint16_t)
},
{
"Mournfull Toll Alternative",
"builtin_mournfull_toll_alternative",
builtin_mournfull_toll_alternative,
sizeof(builtin_mournfull_toll_alternative) / sizeof(uint16_t)
},
{
"Mournfull Toll Meg Par",
"builtin_mournfull_toll_meg_par",
builtin_mournfull_toll_meg_par,
sizeof(builtin_mournfull_toll_meg_par) / sizeof(uint16_t)
},
{
"Sematron",
"builtin_sematron",
builtin_sematron,
sizeof(builtin_sematron) / sizeof(uint16_t)
},
{
"Sematron Alternative",
"builtin_sematron_alternative",
builtin_sematron_alternative,
sizeof(builtin_sematron_alternative) / sizeof(uint16_t)
},
{
"Athonite 1 2 Voices",
"builtin_athonite_1_2_voices",
builtin_athonite_1_2_voices,
sizeof(builtin_athonite_1_2_voices) / sizeof(uint16_t)
},
{
"Athonite 3 Voices",
"builtin_athonite_3_voices",
builtin_athonite_3_voices,
sizeof(builtin_athonite_3_voices) / sizeof(uint16_t)
},
{
"Athonite 3 4 Voices",
"builtin_athonite_3_4_voices",
builtin_athonite_3_4_voices,
sizeof(builtin_athonite_3_4_voices) / sizeof(uint16_t)
},
{
"Athonite 4 8 Voices",
"builtin_athonite_4_8_voices",
builtin_athonite_4_8_voices,
sizeof(builtin_athonite_4_8_voices) / sizeof(uint16_t)
},
{
"Onebyone 2 3 Voices",
"builtin_onebyone_2_3_voices",
builtin_onebyone_2_3_voices,
sizeof(builtin_onebyone_2_3_voices) / sizeof(uint16_t)
},
{
"Onebyone 4 8 Voices",
"builtin_onebyone_4_8_voices",
builtin_onebyone_4_8_voices,
sizeof(builtin_onebyone_4_8_voices) / sizeof(uint16_t)
},
{
"Festive 1Voice",
"builtin_festive_1voice",
builtin_festive_1voice,
sizeof(builtin_festive_1voice) / sizeof(uint16_t)
},
{
"Festive 4Voices",
"builtin_festive_4voices",
builtin_festive_4voices,
sizeof(builtin_festive_4voices) / sizeof(uint16_t)
},
{
"Festive 5Voices",
"builtin_festive_5voices",
builtin_festive_5voices,
sizeof(builtin_festive_5voices) / sizeof(uint16_t)
},
{
"Festive 5Voice Alternative",
"builtin_festive_5voice_alternative",
builtin_festive_5voice_alternative,
sizeof(builtin_festive_5voice_alternative) / sizeof(uint16_t)
},
{
"Festive 6Voices",
"builtin_festive_6voices",
builtin_festive_6voices,
sizeof(builtin_festive_6voices) / sizeof(uint16_t)
},
{
"Festive 8Voices",
"builtin_festive_8voices",
builtin_festive_8voices,
sizeof(builtin_festive_8voices) / sizeof(uint16_t)
},
{
"Ormilia",
"builtin_ormilia",
builtin_ormilia,
sizeof(builtin_ormilia) / sizeof(uint16_t)
} }
}; };
@@ -225,29 +570,10 @@ inline String getBuiltInMelodiesJSON() {
json += "{"; json += "{";
json += "\"name\":\"" + String(MELODY_LIBRARY[i].name) + "\","; json += "\"name\":\"" + String(MELODY_LIBRARY[i].name) + "\",";
json += "\"uid\":\"" + String(MELODY_LIBRARY[i].uid) + "\","; json += "\"uid\":\"" + String(MELODY_LIBRARY[i].uid) + "\",";
json += "\"steps\":" + String(MELODY_LIBRARY[i].stepCount) + ",";
json += "\"speed\":" + String(MELODY_LIBRARY[i].defaultSpeed);
json += "}"; json += "}";
} }
json += "]"; json += "]";
return json; return json;
} }
} // namespace BuiltInMelodies }
// ═══════════════════════════════════════════════════════════════════════════════════
// USAGE EXAMPLE:
// ═══════════════════════════════════════════════════════════════════════════════════
/*
// Check if melody is built-in
if (BuiltInMelodies::isBuiltInMelody(uid)) {
// Load it from firmware
std::vector<uint16_t> melodyData;
if (BuiltInMelodies::loadBuiltInMelody(uid, melodyData)) {
// Use melodyData...
}
} else {
// Load from SD card as usual
}
*/
// ═══════════════════════════════════════════════════════════════════════════════════

View File

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

View File

@@ -41,7 +41,8 @@ public:
// Message source identification // Message source identification
enum class MessageSource { enum class MessageSource {
MQTT, MQTT,
WEBSOCKET WEBSOCKET,
UART
}; };
struct MessageContext { struct MessageContext {
@@ -139,6 +140,7 @@ private:
void handleGetFirmwareStatusCommand(const MessageContext& context); void handleGetFirmwareStatusCommand(const MessageContext& context);
void handleNetworkInfoCommand(const MessageContext& context); void handleNetworkInfoCommand(const MessageContext& context);
void handleGetFullSettingsCommand(const MessageContext& context); void handleGetFullSettingsCommand(const MessageContext& context);
void handleSyncTimeToLcdCommand(const MessageContext& context);
// Network configuration // Network configuration
void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context); void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context);

View File

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

View File

@@ -39,6 +39,7 @@
#include "../CommandHandler/CommandHandler.hpp" #include "../CommandHandler/CommandHandler.hpp"
#include "../ResponseBuilder/ResponseBuilder.hpp" #include "../ResponseBuilder/ResponseBuilder.hpp"
#include "../HTTPRequestHandler/HTTPRequestHandler.hpp" #include "../HTTPRequestHandler/HTTPRequestHandler.hpp"
#include "../UARTCommandHandler/UARTCommandHandler.hpp"
#include "../../ClientManager/ClientManager.hpp" #include "../../ClientManager/ClientManager.hpp"
#include "../../SettingsWebServer/SettingsWebServer.hpp" #include "../../SettingsWebServer/SettingsWebServer.hpp"
@@ -63,6 +64,7 @@ public:
~CommunicationRouter(); ~CommunicationRouter();
void begin(); void begin();
void loop(); // Must be called from main loop for UART processing
void setPlayerReference(Player* player); void setPlayerReference(Player* player);
void setFileManagerReference(FileManager* fm); void setFileManagerReference(FileManager* fm);
void setTimeKeeperReference(Timekeeper* tk); void setTimeKeeperReference(Timekeeper* tk);
@@ -78,6 +80,7 @@ public:
// Component accessors // Component accessors
MQTTAsyncClient& getMQTTClient() { return _mqttClient; } MQTTAsyncClient& getMQTTClient() { return _mqttClient; }
UARTCommandHandler& getUARTHandler() { return _uartHandler; }
// Broadcast methods // Broadcast methods
void broadcastStatus(const String& statusMessage); void broadcastStatus(const String& statusMessage);
@@ -116,11 +119,13 @@ private:
WebSocketServer _wsServer; WebSocketServer _wsServer;
CommandHandler _commandHandler; CommandHandler _commandHandler;
HTTPRequestHandler _httpHandler; HTTPRequestHandler _httpHandler;
UARTCommandHandler _uartHandler;
SettingsWebServer _settingsServer; SettingsWebServer _settingsServer;
// Message handlers // Message handlers
void onMqttMessage(const String& topic, const String& payload); void onMqttMessage(const String& topic, const String& payload);
void onWebSocketMessage(uint32_t clientId, const JsonDocument& message); void onWebSocketMessage(uint32_t clientId, const JsonDocument& message);
void onUartMessage(JsonDocument& message);
// Response routing // Response routing
void sendResponse(const String& response, const CommandHandler::MessageContext& context); void sendResponse(const String& response, const CommandHandler::MessageContext& context);

View File

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

View File

@@ -0,0 +1,122 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════════
* UARTCOMMANDHANDLER.HPP - UART Command Interface for External Control Devices
* ═══════════════════════════════════════════════════════════════════════════════════
*
* 🔌 UART COMMAND HANDLER 🔌
*
* Enables command input from external devices (LCD panels, button controllers)
* via UART serial communication. Uses newline-delimited JSON protocol.
*
* Pin Configuration:
* • TX: GPIO12
* • RX: GPIO13
* • Baud: 115200 (configurable)
*
* Protocol:
* • Newline-delimited JSON messages
* • Same command format as MQTT/WebSocket
* • Responses sent back on same UART
*
* 📋 VERSION: 1.0
* 📅 DATE: 2025-01-19
* 👨‍💻 AUTHOR: Advanced Bell Systems
* ═══════════════════════════════════════════════════════════════════════════════════
*/
#pragma once
#include <Arduino.h>
#include <ArduinoJson.h>
#include <functional>
class UARTCommandHandler {
public:
// Default pin configuration
static constexpr uint8_t DEFAULT_TX_PIN = 12;
static constexpr uint8_t DEFAULT_RX_PIN = 13;
static constexpr uint32_t DEFAULT_BAUD_RATE = 115200;
static constexpr size_t BUFFER_SIZE = 1024;
// Message callback type - called when a complete JSON message is received
using MessageCallback = std::function<void(JsonDocument& message)>;
/**
* @brief Construct UART handler with custom pins
* @param txPin GPIO pin for TX (default: 12)
* @param rxPin GPIO pin for RX (default: 13)
* @param baudRate Baud rate (default: 115200)
*/
explicit UARTCommandHandler(uint8_t txPin = DEFAULT_TX_PIN,
uint8_t rxPin = DEFAULT_RX_PIN,
uint32_t baudRate = DEFAULT_BAUD_RATE);
~UARTCommandHandler();
/**
* @brief Initialize the UART interface
*/
void begin();
/**
* @brief Process incoming UART data (call from loop or task)
* Non-blocking - processes available bytes and returns
*/
void loop();
/**
* @brief Set callback for received messages
* @param callback Function to call with parsed JSON
*/
void setCallback(MessageCallback callback);
/**
* @brief Send a response back over UART
* @param response JSON string to send (newline appended automatically)
*/
void send(const String& response);
/**
* @brief Check if UART is initialized and ready
*/
bool isReady() const { return _ready; }
/**
* @brief Get number of messages received since boot
*/
uint32_t getMessageCount() const { return _messageCount; }
/**
* @brief Get number of parse errors since boot
*/
uint32_t getErrorCount() const { return _errorCount; }
private:
HardwareSerial& _serial;
uint8_t _txPin;
uint8_t _rxPin;
uint32_t _baudRate;
bool _ready;
// Receive buffer
char _buffer[BUFFER_SIZE];
size_t _bufferIndex;
// Statistics
uint32_t _messageCount;
uint32_t _errorCount;
// Callback
MessageCallback _callback;
/**
* @brief Process a complete line from the buffer
* @param line Null-terminated string containing the message
*/
void processLine(const char* line);
/**
* @brief Reset the receive buffer
*/
void resetBuffer();
};

View File

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

View File

@@ -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
}; };
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,9 +26,13 @@
#include <functional> #include <functional>
#include <time.h> #include <time.h>
#include "../FileManager/FileManager.hpp" #include "../FileManager/FileManager.hpp"
#include "../Telemetry/Telemetry.hpp"
#include "../TimeKeeper/TimeKeeper.hpp"
class ConfigManager; // Forward declaration class ConfigManager; // Forward declaration
class Player; // Forward declaration for idle check class Player; // Forward declaration for idle check
class Timekeeper; // Forward declaration for freeze mode
class Telemetry; // Forward declaration for freeze mode
class OTAManager { class OTAManager {
public: public:
@@ -66,7 +70,9 @@ public:
void begin(); void begin();
void setFileManager(FileManager* fm); void setFileManager(FileManager* fm);
void setPlayer(Player* player); // NEW: Set player reference for idle check void setPlayer(Player* player); // Set player reference for idle check
void setTimeKeeper(Timekeeper* tk); // Set timekeeper reference for freeze mode
void setTelemetry(Telemetry* telemetry); // Set telemetry reference for freeze mode
void checkForUpdates(); void checkForUpdates();
void checkForUpdates(const String& channel); // Check specific channel void checkForUpdates(const String& channel); // Check specific channel
@@ -77,7 +83,7 @@ public:
void checkFirmwareUpdateFromSD(); // Check SD for firmware update void checkFirmwareUpdateFromSD(); // Check SD for firmware update
bool performManualUpdate(); // Manual update triggered by app bool performManualUpdate(); // Manual update triggered by app
bool performManualUpdate(const String& channel); // Manual update from specific channel bool performManualUpdate(const String& channel); // Manual update from specific channel
bool performCustomUpdate(const String& firmwareUrl, const String& checksum = "", size_t fileSize = 0); // Custom firmware update bool performCustomUpdate(const String& firmwareUrl, const String& checksum = "", size_t fileSize = 0, uint16_t version = 0); // Custom firmware update
// Hardware identification // Hardware identification
String getHardwareVariant() const; String getHardwareVariant() const;
@@ -104,7 +110,9 @@ public:
private: private:
ConfigManager& _configManager; ConfigManager& _configManager;
FileManager* _fileManager; FileManager* _fileManager;
Player* _player; // NEW: Player reference for idle check Player* _player; // Player reference for idle check
Timekeeper* _timeKeeper; // TimeKeeper reference for freeze mode
Telemetry* _telemetry; // Telemetry reference for freeze mode
Status _status; Status _status;
ErrorCode _lastError; ErrorCode _lastError;
uint16_t _availableVersion; uint16_t _availableVersion;
@@ -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);

View File

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

View File

@@ -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!)

View File

@@ -0,0 +1,227 @@
/*
* ═══════════════════════════════════════════════════════════════════════════════
* SDCARDMUTEX.HPP - Thread-Safe SD Card Access Manager
* ═══════════════════════════════════════════════════════════════════════════════
*
* 🔒 THE SD CARD CONCURRENCY GUARDIAN OF VESPER 🔒
*
* This singleton class provides thread-safe access to the SD card by managing
* a FreeRTOS mutex. All SD card operations MUST acquire this mutex to prevent
* concurrent access that can lead to file corruption and write failures.
*
* CRITICAL: The ESP32 SD library is NOT thread-safe. Without this mutex,
* simultaneous SD access from multiple FreeRTOS tasks will cause:
* - File corruption
* - Write failures
* - SD card "not recognized" errors
* - Random intermittent failures
*
* USAGE:
*
* // Lock before ANY SD operation
* if (SDCardMutex::getInstance().lock()) {
* File file = SD.open("/myfile.txt", FILE_WRITE);
* file.println("data");
* file.close();
* SDCardMutex::getInstance().unlock();
* }
*
* // Or use RAII helper for automatic unlock
* {
* SDCardLock lock; // Acquires mutex
* File file = SD.open("/myfile.txt", FILE_WRITE);
* file.println("data");
* file.close();
* } // Automatically releases mutex when going out of scope
*
* 📋 VERSION: 1.0
* 📅 DATE: 2025-01-07
* 👨‍💻 AUTHOR: Advanced Bell Systems
* ═══════════════════════════════════════════════════════════════════════════════
*/
#pragma once
#include <Arduino.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include "../Logging/Logging.hpp"
/**
* @brief Singleton class for thread-safe SD card access
*
* Manages a global mutex that all SD card operations must acquire
* to prevent concurrent access from multiple FreeRTOS tasks.
*/
class SDCardMutex {
public:
/**
* @brief Get the singleton instance
* @return Reference to the singleton instance
*/
static SDCardMutex& getInstance() {
static SDCardMutex instance;
return instance;
}
/**
* @brief Initialize the mutex (call once during setup)
* @return true if initialization succeeded, false otherwise
*/
bool begin() {
if (_mutex != NULL) {
LOG_WARNING("SDCardMutex 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;
};

View File

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

View File

@@ -77,6 +77,9 @@ private:
// Spinlock for critical sections // Spinlock for critical sections
portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED; portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED;
// Pause flag for OTA freeze mode
volatile bool isPaused = false;
public: public:
// Initialization // Initialization
void begin(); void begin();
@@ -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
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════

View File

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

View File

@@ -28,6 +28,7 @@
#include <Arduino.h> #include <Arduino.h>
#include <vector> #include <vector>
#include <algorithm> #include <algorithm>
#include <atomic>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <RTClib.h> #include <RTClib.h>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@@ -61,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));

View File

@@ -64,19 +64,23 @@
* 👨‍💻 AUTHOR: BellSystems bonamin * 👨‍💻 AUTHOR: BellSystems bonamin
*/ */
#define FW_VERSION "137" #define FW_VERSION "154"
/* /*
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
* 📅 VERSION HISTORY: * 📅 VERSION HISTORY:
* NOTE: Versions are now stored as integers (v1.3 = 130)
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
* v0.1 (100) - Vesper Launch Beta * v0.1 (100) - Vesper Launch Beta
* v1.2 (120) - Added Log Level Configuration via App/MQTT * v1.2 (120) - Added Log Level Configuration via App/MQTT
* v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes * v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes
* v137 - Made OTA and MQTT delays Async * v137 - Made OTA and MQTT delays Async
* ═══════════════════════════════════════════════════════════════════════════════ * v138 - Removed Ethernet, added default WiFi creds (Mikrotik AP) and fixed various Clock issues
* NOTE: Versions are now stored as integers (v1.3 = 130) * v140 - Changed FW Updates to Direct-to-Flash and added manual update functionality with version check
* v151 - Fixed Clock Alerts not running properly
* v152 - Fix RTC Time Reports, added sync_time_to_LCD functionality
* v153 - Fix Infinite Loop Bug and Melody Download crashes.
* ═══════════════════════════════════════════════════════════════════════════════ * ═══════════════════════════════════════════════════════════════════════════════
*/ */
@@ -122,6 +126,7 @@
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════
// CUSTOM CLASSES - Include Custom Classes and Functions // CUSTOM CLASSES - Include Custom Classes and Functions
// ═══════════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════════
#include "src/SDCardMutex/SDCardMutex.hpp" // ⚠️ MUST be included before any SD-using classes
#include "src/ConfigManager/ConfigManager.hpp" #include "src/ConfigManager/ConfigManager.hpp"
#include "src/FileManager/FileManager.hpp" #include "src/FileManager/FileManager.hpp"
#include "src/TimeKeeper/TimeKeeper.hpp" #include "src/TimeKeeper/TimeKeeper.hpp"
@@ -183,6 +188,7 @@ BellEngine bellEngine(player, configManager, telemetry, outputManager); // 🔥
TaskHandle_t bellEngineHandle = NULL; // Legacy - will be removed TaskHandle_t bellEngineHandle = NULL; // Legacy - will be removed
TimerHandle_t schedulerTimer; TimerHandle_t schedulerTimer;
TimerHandle_t ntpSyncTimer; // Non-blocking delayed NTP sync timer
@@ -193,6 +199,14 @@ void handleFactoryReset() {
} }
} }
// Non-blocking NTP sync timer callback
void ntpSyncTimerCallback(TimerHandle_t xTimer) {
LOG_DEBUG("Network stabilization complete - starting NTP sync");
if (!networking.isInAPMode()) {
timekeeper.syncTimeWithNTP();
}
}
void setup() void setup()
@@ -206,6 +220,15 @@ void setup()
SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi); SPI.begin(hwConfig.ethSpiSck, hwConfig.ethSpiMiso, hwConfig.ethSpiMosi);
delay(50); delay(50);
// 🔒 CRITICAL: Initialize SD Card Mutex BEFORE any SD operations
// This prevents concurrent SD access from multiple FreeRTOS tasks
if (!SDCardMutex::getInstance().begin()) {
Serial.println("❌ FATAL: Failed to initialize SD card mutex!");
Serial.println(" System cannot continue safely - entering infinite loop");
while(1) { delay(1000); } // Halt system - unsafe to proceed
}
Serial.println("✅ SD card mutex initialized");
// Initialize Configuration (loads factory identity from NVS + user settings from SD) // Initialize Configuration (loads factory identity from NVS + user settings from SD)
configManager.begin(); configManager.begin();
@@ -296,6 +319,7 @@ void setup()
timekeeper.setOutputManager(&outputManager); timekeeper.setOutputManager(&outputManager);
timekeeper.setConfigManager(&configManager); timekeeper.setConfigManager(&configManager);
timekeeper.setNetworking(&networking); timekeeper.setNetworking(&networking);
timekeeper.setPlayer(&player); // 🔥 Connect for playback coordination
// Clock outputs now configured via ConfigManager/Communication commands // Clock outputs now configured via ConfigManager/Communication commands
// Register TimeKeeper with health monitor // Register TimeKeeper with health monitor
@@ -337,6 +361,7 @@ void setup()
player.setDependencies(&communication, &fileManager); player.setDependencies(&communication, &fileManager);
player.setBellEngine(&bellEngine); // Connect the beast! player.setBellEngine(&bellEngine); // Connect the beast!
player.setTelemetry(&telemetry); player.setTelemetry(&telemetry);
player.setTimekeeper(&timekeeper); // 🔥 Connect for alert coordination
// Register Communication with health monitor // Register Communication with health monitor
healthMonitor.setCommunication(&communication); healthMonitor.setCommunication(&communication);
@@ -347,15 +372,25 @@ void setup()
// Track if AsyncWebServer has been started to prevent duplicates // Track if AsyncWebServer has been started to prevent duplicates
static bool webServerStarted = false; static bool webServerStarted = false;
// Create NTP sync timer (one-shot, 3 second delay for network stabilization)
ntpSyncTimer = xTimerCreate(
"NTPSync", // Timer name
pdMS_TO_TICKS(3000), // 3 second delay (network stabilization)
pdFALSE, // One-shot timer (not auto-reload)
NULL, // Timer ID (not used)
ntpSyncTimerCallback // Callback function
);
// Set up network callbacks // Set up network callbacks
networking.setNetworkCallbacks( networking.setNetworkCallbacks(
[&webServerStarted]() { [&webServerStarted]() {
communication.onNetworkConnected(); communication.onNetworkConnected();
// Non-blocking NTP sync (graceful without internet) // Schedule non-blocking NTP sync after 3s network stabilization (like MQTT)
// Skip NTP sync in AP mode (no internet connection) // Skip NTP sync in AP mode (no internet connection)
if (!networking.isInAPMode()) { if (!networking.isInAPMode() && ntpSyncTimer) {
timekeeper.syncTimeWithNTP(); LOG_DEBUG("Network connected - scheduling NTP sync after 3s stabilization (non-blocking)");
xTimerStart(ntpSyncTimer, 0);
} }
// Start AsyncWebServer when network becomes available (only once!) // Start AsyncWebServer when network becomes available (only once!)
@@ -374,10 +409,11 @@ void setup()
LOG_INFO("Network already connected - initializing services"); LOG_INFO("Network already connected - initializing services");
communication.onNetworkConnected(); communication.onNetworkConnected();
// Non-blocking NTP sync (graceful without internet) // Schedule non-blocking NTP sync after 3s network stabilization (like MQTT)
// Skip NTP sync in AP mode (no internet connection) // Skip NTP sync in AP mode (no internet connection)
if (!networking.isInAPMode()) { if (!networking.isInAPMode() && ntpSyncTimer) {
timekeeper.syncTimeWithNTP(); LOG_DEBUG("Network already connected - scheduling NTP sync after 3s stabilization (non-blocking)");
xTimerStart(ntpSyncTimer, 0);
} }
// 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready // 🔥 CRITICAL: Start AsyncWebServer ONLY when network is ready
@@ -395,7 +431,9 @@ void setup()
// Initialize OTA Manager // Initialize OTA Manager
otaManager.begin(); otaManager.begin();
otaManager.setFileManager(&fileManager); otaManager.setFileManager(&fileManager);
otaManager.setPlayer(&player); // Set player reference for idle check otaManager.setPlayer(&player); // Set player reference for idle check
otaManager.setTimeKeeper(&timekeeper); // Set timekeeper reference for freeze mode
otaManager.setTelemetry(&telemetry); // Set telemetry reference for freeze mode
// 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay) // 🔥 FIX: OTA check will happen asynchronously via scheduled timer (no blocking delay)
// UDP discovery setup can happen immediately without conflicts // UDP discovery setup can happen immediately without conflicts
@@ -477,6 +515,9 @@ void loop()
lastWsCleanup = millis(); lastWsCleanup = millis();
} }
// Process UART command input from external devices (LCD panel, buttons)
communication.loop();
// 🔥 DEBUG: Log every 10 seconds to verify we're still running // 🔥 DEBUG: Log every 10 seconds to verify we're still running
static unsigned long lastLog = 0; static unsigned long lastLog = 0;
if (millis() - lastLog > 10000) { if (millis() - lastLog > 10000) {