diff --git a/vesper/src/Communication/CommandHandler/CommandHandler.cpp b/vesper/src/Communication/CommandHandler/CommandHandler.cpp index 1e8e835..7c1638e 100644 --- a/vesper/src/Communication/CommandHandler/CommandHandler.cpp +++ b/vesper/src/Communication/CommandHandler/CommandHandler.cpp @@ -10,6 +10,7 @@ #include "../../TimeKeeper/TimeKeeper.hpp" #include "../../FirmwareValidator/FirmwareValidator.hpp" #include "../../ClientManager/ClientManager.hpp" +#include "../../Telemetry/Telemetry.hpp" #include "../../Logging/Logging.hpp" #include "../ResponseBuilder/ResponseBuilder.hpp" @@ -21,6 +22,7 @@ CommandHandler::CommandHandler(ConfigManager& configManager, OTAManager& otaMana , _timeKeeper(nullptr) , _firmwareValidator(nullptr) , _clientManager(nullptr) + , _telemetry(nullptr) , _responseCallback(nullptr) {} CommandHandler::~CommandHandler() {} @@ -45,6 +47,10 @@ void CommandHandler::setClientManagerReference(ClientManager* cm) { _clientManager = cm; } +void CommandHandler::setTelemetryReference(Telemetry* telemetry) { + _telemetry = telemetry; +} + void CommandHandler::setResponseCallback(ResponseCallback callback) { _responseCallback = callback; } @@ -105,16 +111,24 @@ void CommandHandler::handleStatusCommand(const MessageContext& context) { uint64_t projectedRunTime = 0; if (_player) { - if (_player->getStatus() == PlayerStatus::PLAYING || + if (_player->getStatus() == PlayerStatus::PLAYING || _player->getStatus() == PlayerStatus::PAUSED || _player->getStatus() == PlayerStatus::STOPPING) { timeElapsedMs = millis() - _player->startTime; } - + projectedRunTime = _player->calculateProjectedRunTime(); } - - String response = ResponseBuilder::deviceStatus(playerStatus, timeElapsedMs, projectedRunTime); + + // Collect strike counters from Telemetry + uint32_t strikeCounters[16] = {0}; + if (_telemetry) { + for (uint8_t i = 0; i < 16; i++) { + strikeCounters[i] = _telemetry->getStrikeCount(i); + } + } + + String response = ResponseBuilder::deviceStatus(playerStatus, timeElapsedMs, projectedRunTime, strikeCounters); sendResponse(response, context); } @@ -257,10 +271,6 @@ void CommandHandler::handleSystemInfoCommand(JsonVariant contents, const Message handleGetDeviceTimeCommand(context); } else if (action == "get_clock_time") { handleGetClockTimeCommand(context); - } else if (action == "commit_firmware") { - handleCommitFirmwareCommand(context); - } else if (action == "rollback_firmware") { - handleRollbackFirmwareCommand(context); } else if (action == "get_firmware_status") { handleGetFirmwareStatusCommand(context); } else if (action == "network_info") { diff --git a/vesper/src/Communication/CommandHandler/CommandHandler.hpp b/vesper/src/Communication/CommandHandler/CommandHandler.hpp index b68e5f9..6a6b57f 100644 --- a/vesper/src/Communication/CommandHandler/CommandHandler.hpp +++ b/vesper/src/Communication/CommandHandler/CommandHandler.hpp @@ -33,6 +33,7 @@ class FileManager; class Timekeeper; class FirmwareValidator; class ClientManager; +class Telemetry; class CommandHandler { public: @@ -65,6 +66,7 @@ public: void setTimeKeeperReference(Timekeeper* tk); void setFirmwareValidatorReference(FirmwareValidator* fv); void setClientManagerReference(ClientManager* cm); + void setTelemetryReference(Telemetry* telemetry); /** * @brief Set response callback for sending responses back @@ -87,6 +89,7 @@ private: Timekeeper* _timeKeeper; FirmwareValidator* _firmwareValidator; ClientManager* _clientManager; + Telemetry* _telemetry; ResponseCallback _responseCallback; // Response helpers diff --git a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp index 26f26a9..d2abddb 100644 --- a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp +++ b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.cpp @@ -97,6 +97,10 @@ void CommunicationRouter::setFirmwareValidatorReference(FirmwareValidator* fv) { _commandHandler.setFirmwareValidatorReference(fv); } +void CommunicationRouter::setTelemetryReference(Telemetry* telemetry) { + _commandHandler.setTelemetryReference(telemetry); +} + void CommunicationRouter::setupUdpDiscovery() { uint16_t discoveryPort = _configManager.getNetworkConfig().discoveryPort; if (_udp.listen(discoveryPort)) { diff --git a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp index 4e52d87..f8ab0cd 100644 --- a/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp +++ b/vesper/src/Communication/CommunicationRouter/CommunicationRouter.hpp @@ -47,6 +47,7 @@ class FileManager; class Timekeeper; class Networking; class FirmwareValidator; +class Telemetry; class CommunicationRouter { public: @@ -64,6 +65,7 @@ public: void setFileManagerReference(FileManager* fm); void setTimeKeeperReference(Timekeeper* tk); void setFirmwareValidatorReference(FirmwareValidator* fv); + void setTelemetryReference(Telemetry* telemetry); void setupUdpDiscovery(); // Status methods diff --git a/vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp index 3923bf1..c88a6d7 100644 --- a/vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp +++ b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.cpp @@ -32,15 +32,15 @@ String ResponseBuilder::pong() { return success("pong", ""); } -String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsed, uint64_t projectedRunTime) { - StaticJsonDocument<512> statusDoc; // Increased size for additional data - +String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsed, uint64_t projectedRunTime, const uint32_t strikeCounters[16]) { + DynamicJsonDocument statusDoc(1024); // Increased size for strikeCounters array + statusDoc["status"] = "SUCCESS"; statusDoc["type"] = "current_status"; - + // Create payload object with the exact format expected by Flutter JsonObject payload = statusDoc.createNestedObject("payload"); - + // Convert PlayerStatus to string const char* statusStr; switch (playerStatus) { @@ -58,14 +58,20 @@ String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeEla statusStr = "idle"; // STOPPED maps to "idle" in Flutter break; } - + payload["player_status"] = statusStr; payload["time_elapsed"] = timeElapsed; // in milliseconds payload["projected_run_time"] = projectedRunTime; // NEW: total projected duration - + + // Add strike counters array + JsonArray strikeCountersArray = payload.createNestedArray("strike_counters"); + for (uint8_t i = 0; i < 16; i++) { + strikeCountersArray.add(strikeCounters[i]); + } + String result; serializeJson(statusDoc, result); - + LOG_DEBUG("Device status response: %s", result.c_str()); return result; } diff --git a/vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp index aa4cfc0..8910297 100644 --- a/vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp +++ b/vesper/src/Communication/ResponseBuilder/ResponseBuilder.hpp @@ -64,7 +64,7 @@ public: // Specialized response builders for common scenarios static String acknowledgment(const String& commandType); static String pong(); - static String deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsedMs, uint64_t projectedRunTime = 0); + static String deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsedMs, uint64_t projectedRunTime, const uint32_t strikeCounters[16]); static String melodyList(const String& fileListJson); static String downloadResult(bool success, const String& filename = ""); static String configUpdate(const String& configType); diff --git a/vesper/src/FileManager/FileManager.cpp b/vesper/src/FileManager/FileManager.cpp index f4da9ef..66b00c2 100644 --- a/vesper/src/FileManager/FileManager.cpp +++ b/vesper/src/FileManager/FileManager.cpp @@ -175,6 +175,52 @@ size_t FileManager::getFileSize(const String& filePath) { return size; } +bool FileManager::writeJsonFile(const String& filePath, JsonDocument& doc) { + if (!initializeSD()) { + return false; + } + + File file = SD.open(filePath.c_str(), FILE_WRITE); + if (!file) { + LOG_ERROR("Failed to open file for writing: %s", filePath.c_str()); + return false; + } + + if (serializeJson(doc, file) == 0) { + LOG_ERROR("Failed to write JSON to file: %s", filePath.c_str()); + file.close(); + return false; + } + + file.close(); + LOG_DEBUG("JSON file written successfully: %s", filePath.c_str()); + return true; +} + +bool FileManager::readJsonFile(const String& filePath, JsonDocument& doc) { + if (!initializeSD()) { + return false; + } + + File file = SD.open(filePath.c_str(), FILE_READ); + if (!file) { + LOG_ERROR("Failed to open file for reading: %s", filePath.c_str()); + return false; + } + + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { + LOG_ERROR("Failed to parse JSON from file: %s, error: %s", + filePath.c_str(), error.c_str()); + return false; + } + + LOG_DEBUG("JSON file read successfully: %s", filePath.c_str()); + return true; +} + // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ diff --git a/vesper/src/FileManager/FileManager.hpp b/vesper/src/FileManager/FileManager.hpp index 3c91a61..db85826 100644 --- a/vesper/src/FileManager/FileManager.hpp +++ b/vesper/src/FileManager/FileManager.hpp @@ -45,6 +45,10 @@ public: bool createDirectory(const String& dirPath); size_t getFileSize(const String& filePath); + // Generic read/write for JSON data + bool writeJsonFile(const String& filePath, JsonDocument& doc); + bool readJsonFile(const String& filePath, JsonDocument& doc); + // ═══════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK METHOD // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/vesper/src/Player/Player.cpp b/vesper/src/Player/Player.cpp index 5c0beaf..783bec5 100644 --- a/vesper/src/Player/Player.cpp +++ b/vesper/src/Player/Player.cpp @@ -1,6 +1,7 @@ #include "Player.hpp" #include "../Communication/CommunicationRouter/CommunicationRouter.hpp" #include "../BellEngine/BellEngine.hpp" +#include "../Telemetry/Telemetry.hpp" // Note: Removed global melody_steps dependency for cleaner architecture @@ -28,6 +29,7 @@ Player::Player(CommunicationRouter* comm, FileManager* fm) , _commManager(comm) , _fileManager(fm) , _bellEngine(nullptr) + , _telemetry(nullptr) , _durationTimerHandle(NULL) { } @@ -55,6 +57,7 @@ Player::Player() , _commManager(nullptr) , _fileManager(nullptr) , _bellEngine(nullptr) + , _telemetry(nullptr) , _durationTimerHandle(NULL) { } @@ -123,6 +126,12 @@ void Player::forceStop() { hardStop = true; isPlaying = false; setStatus(PlayerStatus::STOPPED); // Immediate stop, notify clients + + // Save strike counters after melody stops + if (_telemetry) { + _telemetry->saveStrikeCounters(); + } + LOG_DEBUG("Plbck: FORCE STOP"); } @@ -137,6 +146,12 @@ void Player::stop() { // Set STOPPING status - actual stop message will be sent when BellEngine finishes if (isPaused) { setStatus(PlayerStatus::STOPPED); + + // Save strike counters after melody stops + if (_telemetry) { + _telemetry->saveStrikeCounters(); + } + LOG_DEBUG("Plbck: STOP from PAUSED state"); } else { setStatus(PlayerStatus::STOPPING); @@ -361,7 +376,7 @@ void Player::onMelodyLoopCompleted() { // This is called by BellEngine when a melody loop actually finishes if (_status == PlayerStatus::STOPPING) { // We were in soft stop mode, now actually stop - setStatus(PlayerStatus::STOPPED); + setStatus(PlayerStatus::STOPPED); LOG_DEBUG("Plbck: ACTUAL STOP (melody loop completed)"); } diff --git a/vesper/src/Player/Player.hpp b/vesper/src/Player/Player.hpp index 0567a47..574c0f2 100644 --- a/vesper/src/Player/Player.hpp +++ b/vesper/src/Player/Player.hpp @@ -63,6 +63,7 @@ // ═════════════════════════════════════════════════════════════════════════════════ class CommunicationRouter; // Command handling and communication class BellEngine; // High-precision timing engine +class Telemetry; // System telemetry and monitoring // ═════════════════════════════════════════════════════════════════════════════════ // PLAYER STATUS ENUMERATION @@ -126,6 +127,12 @@ public: */ void setBellEngine(BellEngine* engine) { _bellEngine = engine; } + /** + * @brief Set Telemetry reference for strike counter persistence + * @param telemetry Pointer to Telemetry instance + */ + void setTelemetry(Telemetry* telemetry) { _telemetry = telemetry; } + // ═══════════════════════════════════════════════════════════════════════════════ // MELODY METADATA - Public access for compatibility // ═══════════════════════════════════════════════════════════════════════════════ @@ -241,6 +248,7 @@ private: CommunicationRouter* _commManager; // 📡 Communication system reference FileManager* _fileManager; // 📁 File operations reference BellEngine* _bellEngine; // 🔥 High-precision timing engine reference + Telemetry* _telemetry; // 📄 Telemetry system reference std::vector _melodySteps; // 🎵 Melody data owned by Player TimerHandle_t _durationTimerHandle = NULL; // ⏱️ FreeRTOS timer (saves 4KB vs task!) diff --git a/vesper/src/Telemetry/Telemetry.cpp b/vesper/src/Telemetry/Telemetry.cpp index ed301d5..4b3334a 100644 --- a/vesper/src/Telemetry/Telemetry.cpp +++ b/vesper/src/Telemetry/Telemetry.cpp @@ -1,4 +1,5 @@ #include "Telemetry.hpp" +#include void Telemetry::begin() { // Initialize arrays @@ -10,6 +11,9 @@ void Telemetry::begin() { coolingActive = false; + // Load strike counters from SD if available + loadStrikeCounters(); + // Create the telemetry task xTaskCreatePinnedToCore(telemetryTask, "TelemetryTask", 4096, this, 2, &telemetryTaskHandle, 1); @@ -21,6 +25,11 @@ void Telemetry::setPlayerReference(bool* isPlayingPtr) { LOG_DEBUG("Player reference set"); } +void Telemetry::setFileManager(FileManager* fm) { + fileManager = fm; + LOG_DEBUG("FileManager reference set"); +} + void Telemetry::setForceStopCallback(void (*callback)()) { forceStopCallback = callback; LOG_DEBUG("Force stop callback set"); @@ -175,6 +184,62 @@ void Telemetry::telemetryTask(void* parameter) { } } +// ════════════════════════════════════════════════════════════════════════════ +// STRIKE COUNTER PERSISTENCE +// ════════════════════════════════════════════════════════════════════════════ + +void Telemetry::saveStrikeCounters() { + if (!fileManager) { + LOG_WARNING("Cannot save strike counters: FileManager not set"); + return; + } + + StaticJsonDocument<512> doc; + JsonArray counters = doc.createNestedArray("strikeCounters"); + + // Thread-safe read of strike counters + portENTER_CRITICAL(&telemetrySpinlock); + for (uint8_t i = 0; i < 16; i++) { + counters.add(strikeCounters[i]); + } + portEXIT_CRITICAL(&telemetrySpinlock); + + if (fileManager->writeJsonFile("/telemetry_data.json", doc)) { + LOG_INFO("Strike counters saved to SD card"); + } else { + LOG_ERROR("Failed to save strike counters to SD card"); + } +} + +void Telemetry::loadStrikeCounters() { + if (!fileManager) { + LOG_WARNING("Cannot load strike counters: FileManager not set"); + return; + } + + StaticJsonDocument<512> doc; + + if (!fileManager->readJsonFile("/telemetry_data.json", doc)) { + LOG_INFO("No previous strike counter data found, starting fresh"); + return; + } + + JsonArray counters = doc["strikeCounters"]; + if (counters.isNull()) { + LOG_WARNING("Invalid telemetry data format"); + return; + } + + // Thread-safe write of strike counters + portENTER_CRITICAL(&telemetrySpinlock); + for (uint8_t i = 0; i < 16 && i < counters.size(); i++) { + strikeCounters[i] = counters[i].as(); + } + portEXIT_CRITICAL(&telemetrySpinlock); + + LOG_INFO("Strike counters loaded from SD card"); +} + // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ diff --git a/vesper/src/Telemetry/Telemetry.hpp b/vesper/src/Telemetry/Telemetry.hpp index 3c6cec7..ca98bab 100644 --- a/vesper/src/Telemetry/Telemetry.hpp +++ b/vesper/src/Telemetry/Telemetry.hpp @@ -57,6 +57,7 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "../Logging/Logging.hpp" +#include "../FileManager/FileManager.hpp" class Telemetry { private: @@ -71,6 +72,7 @@ private: // External references (to be set via setters) bool* playerIsPlayingPtr = nullptr; + FileManager* fileManager = nullptr; // Spinlock for critical sections portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED; @@ -81,6 +83,7 @@ public: // Set external references void setPlayerReference(bool* isPlayingPtr); + void setFileManager(FileManager* fm); // Bell strike handling (call this on every hammer strike) void recordBellStrike(uint8_t bellIndex); @@ -89,6 +92,10 @@ public: uint32_t getStrikeCount(uint8_t bellIndex); void resetStrikeCounters(); // User-requested reset + // Persistence methods + void saveStrikeCounters(); + void loadStrikeCounters(); + // Bell load management uint16_t getBellLoad(uint8_t bellIndex); void setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad); diff --git a/vesper/vesper.ino b/vesper/vesper.ino index 699a754..8089633 100644 --- a/vesper/vesper.ino +++ b/vesper/vesper.ino @@ -62,7 +62,7 @@ * 👨‍💻 AUTHOR: BellSystems bonamin */ -#define FW_VERSION "1.2" +#define FW_VERSION "1.3" /* @@ -71,6 +71,7 @@ * ═══════════════════════════════════════════════════════════════════════════════ * v0.1 - Vesper Launch Beta * v1.2 - Added Log Level Configuration via App/MQTT + * v1.3 - Added Telemtry Reports to App, Various Playback Fixes * ═══════════════════════════════════════════════════════════════════════════════ */ @@ -283,10 +284,11 @@ void setup() healthMonitor.setTimeKeeper(&timekeeper); // Initialize Telemetry - telemetry.begin(); telemetry.setPlayerReference(&player.isPlaying); // 🚑 CRITICAL: Connect force stop callback for overload protection! telemetry.setForceStopCallback([]() { player.forceStop(); }); + telemetry.setFileManager(&fileManager); + telemetry.begin(); // Register Telemetry with health monitor healthMonitor.setTelemetry(&telemetry); @@ -312,9 +314,11 @@ void setup() communication.setFileManagerReference(&fileManager); communication.setTimeKeeperReference(&timekeeper); communication.setFirmwareValidatorReference(&firmwareValidator); + communication.setTelemetryReference(&telemetry); player.setDependencies(&communication, &fileManager); player.setBellEngine(&bellEngine); // Connect the beast! + player.setTelemetry(&telemetry); // Register Communication with health monitor healthMonitor.setCommunication(&communication);