diff --git a/vesper/documentation/API_Reference.md b/vesper/documentation/API_Reference.md new file mode 100644 index 0000000..0c62275 --- /dev/null +++ b/vesper/documentation/API_Reference.md @@ -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! 🔔* \ No newline at end of file diff --git a/vesper/documentation/BuildProcedure.md b/vesper/documentation/BuildProcedure.md new file mode 100644 index 0000000..0a351fc --- /dev/null +++ b/vesper/documentation/BuildProcedure.md @@ -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 diff --git a/vesper/documentation/Roadmap.md b/vesper/documentation/Roadmap.md new file mode 100644 index 0000000..e69de29 diff --git a/vesper/documentation/features.info b/vesper/documentation/features.info new file mode 100644 index 0000000..c2ebf04 --- /dev/null +++ b/vesper/documentation/features.info @@ -0,0 +1,85 @@ +Features: + +// Board Naming Schema: + +eg. PV25K07BC01R01 + +PV 25 K 07 BC 01 R 01 +PV [Y] [M] [D] [BT] [RV] R [BC] + + + +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//data // Data sent from the controller +vesper//control // Commands sent to the controller +vesper//kiosk/event // Kiosk Mode Events +vesper//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. + + + +// Stamna: + +PV25L22BP01R01 +Bell Plus +HW: 1.0 + +u6545309759@gmail.com +bellsystems2025 +aCx!97IEfTiA073^#*Jj \ No newline at end of file diff --git a/vesper/src/Communication/CommandHandler/CommandHandler.hpp b/vesper/src/Communication/CommandHandler/CommandHandler.hpp index 574ddce..7507971 100644 --- a/vesper/src/Communication/CommandHandler/CommandHandler.hpp +++ b/vesper/src/Communication/CommandHandler/CommandHandler.hpp @@ -41,7 +41,8 @@ public: // Message source identification enum class MessageSource { MQTT, - WEBSOCKET + WEBSOCKET, + UART }; struct MessageContext { diff --git a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp index 4d73427..216eb81 100644 --- a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp +++ b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp @@ -33,6 +33,7 @@ CommunicationRouter::CommunicationRouter(ConfigManager& configManager, , _wsServer(webSocket, _clientManager) , _commandHandler(configManager, otaManager) , _httpHandler(server, configManager) + , _uartHandler() , _settingsServer(server, configManager, networking) {} CommunicationRouter::~CommunicationRouter() {} @@ -106,13 +107,27 @@ void CommunicationRouter::begin() { _settingsServer.begin(); 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(" â€ĸ MQTT: AsyncMqttClient"); LOG_INFO(" â€ĸ WebSocket: Multi-client support"); LOG_INFO(" â€ĸ HTTP REST API: /api endpoints"); + LOG_INFO(" â€ĸ UART: External device control"); LOG_INFO(" â€ĸ Settings Page: /settings"); } +void CommunicationRouter::loop() { + // Process UART incoming data + _uartHandler.loop(); +} + void CommunicationRouter::setPlayerReference(Player* player) { _player = player; _commandHandler.setPlayerReference(player); @@ -327,6 +342,20 @@ void CommunicationRouter::onWebSocketMessage(uint32_t clientId, const JsonDocume LOG_DEBUG("WebSocket message from client #%u processed", clientId); } +void CommunicationRouter::onUartMessage(JsonDocument& message) { + // Extract command for logging + String cmd = message["cmd"] | "unknown"; + LOG_INFO("🔌 UART message received: cmd=%s", cmd.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) { if (context.source == CommandHandler::MessageSource::MQTT) { LOG_DEBUG("â†—ī¸ Sending response via MQTT: %s", response.c_str()); @@ -334,6 +363,9 @@ void CommunicationRouter::sendResponse(const String& response, const CommandHand } else if (context.source == CommandHandler::MessageSource::WEBSOCKET) { LOG_DEBUG("â†—ī¸ Sending response to WebSocket client #%u: %s", context.clientId, response.c_str()); _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 { LOG_ERROR("❌ Unknown message source for response routing!"); } diff --git a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp index 5fadc93..33cc8d1 100644 --- a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp +++ b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp @@ -39,6 +39,7 @@ #include "../CommandHandler/CommandHandler.hpp" #include "../ResponseBuilder/ResponseBuilder.hpp" #include "../HTTPRequestHandler/HTTPRequestHandler.hpp" +#include "../UARTCommandHandler/UARTCommandHandler.hpp" #include "../../ClientManager/ClientManager.hpp" #include "../../SettingsWebServer/SettingsWebServer.hpp" @@ -63,6 +64,7 @@ public: ~CommunicationRouter(); void begin(); + void loop(); // Must be called from main loop for UART processing void setPlayerReference(Player* player); void setFileManagerReference(FileManager* fm); void setTimeKeeperReference(Timekeeper* tk); @@ -78,6 +80,7 @@ public: // Component accessors MQTTAsyncClient& getMQTTClient() { return _mqttClient; } + UARTCommandHandler& getUARTHandler() { return _uartHandler; } // Broadcast methods void broadcastStatus(const String& statusMessage); @@ -116,11 +119,13 @@ private: WebSocketServer _wsServer; CommandHandler _commandHandler; HTTPRequestHandler _httpHandler; + UARTCommandHandler _uartHandler; SettingsWebServer _settingsServer; // Message handlers void onMqttMessage(const String& topic, const String& payload); void onWebSocketMessage(uint32_t clientId, const JsonDocument& message); + void onUartMessage(JsonDocument& message); // Response routing void sendResponse(const String& response, const CommandHandler::MessageContext& context); diff --git a/vesper/src/Communication/UARTCommandHandler/UARTCommandHandler.cpp b/vesper/src/Communication/UARTCommandHandler/UARTCommandHandler.cpp new file mode 100644 index 0000000..e9bee20 --- /dev/null +++ b/vesper/src/Communication/UARTCommandHandler/UARTCommandHandler.cpp @@ -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); +} diff --git a/vesper/src/Communication/UARTCommandHandler/UARTCommandHandler.hpp b/vesper/src/Communication/UARTCommandHandler/UARTCommandHandler.hpp new file mode 100644 index 0000000..dd06080 --- /dev/null +++ b/vesper/src/Communication/UARTCommandHandler/UARTCommandHandler.hpp @@ -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 +#include +#include + +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; + + /** + * @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(); +}; diff --git a/vesper/vesper.ino b/vesper/vesper.ino index 4519cc8..bf2d163 100644 --- a/vesper/vesper.ino +++ b/vesper/vesper.ino @@ -513,6 +513,9 @@ void loop() lastWsCleanup = millis(); } + // Process UART command input from external devices (LCD panel, buttons) + communication.loop(); + // đŸ”Ĩ DEBUG: Log every 10 seconds to verify we're still running static unsigned long lastLog = 0; if (millis() - lastLog > 10000) {