From 11b98166d19fb73c8907c5fc36e636c1c010aaa2 Mon Sep 17 00:00:00 2001 From: bonamin Date: Mon, 19 Jan 2026 19:02:25 +0200 Subject: [PATCH] Fixed OTA problems, Clock Alerts and MQTT Logs. V151 --- .../CommandHandler/CommandHandler.cpp | 9 ++- vesper/src/ConfigManager/ConfigManager.cpp | 13 +++- vesper/src/Logging/Logging.cpp | 38 +++++---- vesper/src/OTAManager/OTAManager.cpp | 67 +++++++++++++++- vesper/src/OTAManager/OTAManager.hpp | 2 +- vesper/src/Player/Player.cpp | 15 +++- vesper/src/Player/Player.hpp | 7 ++ vesper/src/TimeKeeper/TimeKeeper.cpp | 78 ++++++++++++++++--- vesper/src/TimeKeeper/TimeKeeper.hpp | 9 ++- vesper/vesper.ino | 6 +- 10 files changed, 200 insertions(+), 44 deletions(-) diff --git a/vesper/src/Communication/CommandHandler/CommandHandler.cpp b/vesper/src/Communication/CommandHandler/CommandHandler.cpp index fc8b45d..ef60a1a 100644 --- a/vesper/src/Communication/CommandHandler/CommandHandler.cpp +++ b/vesper/src/Communication/CommandHandler/CommandHandler.cpp @@ -1249,6 +1249,8 @@ void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const Messa contents["checksum"].as() : ""; size_t fileSize = contents.containsKey("file_size") ? contents["file_size"].as() : 0; + uint16_t version = contents.containsKey("version") ? + contents["version"].as() : 0; // Check if player is active if (_player && _player->isCurrentlyPlaying()) { @@ -1257,10 +1259,11 @@ void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const Messa 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(), checksum.isEmpty() ? "none" : checksum.c_str(), - fileSize); + fileSize, + version); sendSuccessResponse("custom_update", "Starting custom OTA update. Device may reboot.", context); @@ -1269,7 +1272,7 @@ void CommandHandler::handleCustomUpdateCommand(JsonVariant contents, const Messa delay(1000); // 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 if (!result) { diff --git a/vesper/src/ConfigManager/ConfigManager.cpp b/vesper/src/ConfigManager/ConfigManager.cpp index a5ed716..7d192a4 100644 --- a/vesper/src/ConfigManager/ConfigManager.cpp +++ b/vesper/src/ConfigManager/ConfigManager.cpp @@ -1146,12 +1146,19 @@ String ConfigManager::getAllSettingsAsJson() const { time["daylightOffsetSec"] = timeConfig.daylightOffsetSec; // Bell durations (relay timings) - JsonObject bells = doc.createNestedObject("bells"); + JsonObject bellDurations = doc.createNestedObject("bellDurations"); for (uint8_t i = 0; i < 16; i++) { 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 JsonObject clock = doc.createNestedObject("clock"); clock["enabled"] = clockConfig.enabled; diff --git a/vesper/src/Logging/Logging.cpp b/vesper/src/Logging/Logging.cpp index fccdc4a..5e97461 100644 --- a/vesper/src/Logging/Logging.cpp +++ b/vesper/src/Logging/Logging.cpp @@ -35,8 +35,6 @@ bool Logging::isLevelEnabled(LogLevel level) { } void Logging::error(const char* format, ...) { - if (!isLevelEnabled(ERROR)) return; - va_list args; va_start(args, format); log(ERROR, "๐Ÿ”ด EROR", format, args); @@ -44,8 +42,6 @@ void Logging::error(const char* format, ...) { } void Logging::warning(const char* format, ...) { - if (!isLevelEnabled(WARNING)) return; - va_list args; va_start(args, format); log(WARNING, "๐ŸŸก WARN", format, args); @@ -53,8 +49,6 @@ void Logging::warning(const char* format, ...) { } void Logging::info(const char* format, ...) { - if (!isLevelEnabled(INFO)) return; - va_list args; va_start(args, format); log(INFO, "๐ŸŸข INFO", format, args); @@ -62,8 +56,6 @@ void Logging::info(const char* format, ...) { } void Logging::debug(const char* format, ...) { - if (!isLevelEnabled(DEBUG)) return; - va_list args; va_start(args, format); log(DEBUG, "๐Ÿž DEBG", format, args); @@ -71,8 +63,6 @@ void Logging::debug(const char* format, ...) { } void Logging::verbose(const char* format, ...) { - if (!isLevelEnabled(VERBOSE)) return; - va_list args; va_start(args, format); 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) { - // 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]; vsnprintf(buffer, sizeof(buffer), format, args); - // Serial output - Serial.printf("[%s] ", levelStr); - Serial.print(buffer); - Serial.println(); + // Serial output (independent check) + if (serialEnabled) { + Serial.printf("[%s] ", levelStr); + Serial.print(buffer); + Serial.println(); + } - // MQTT output (if enabled and callback is set) - if (mqttLogLevel >= level && mqttPublishCallback) { + // MQTT output (independent check) + if (mqttEnabled) { 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) { diff --git a/vesper/src/OTAManager/OTAManager.cpp b/vesper/src/OTAManager/OTAManager.cpp index 916078f..b7bb296 100644 --- a/vesper/src/OTAManager/OTAManager.cpp +++ b/vesper/src/OTAManager/OTAManager.cpp @@ -493,9 +493,57 @@ bool OTAManager::downloadDirectToFlash(const String& url, size_t expectedSize) { LOG_INFO("OTA: Checksum validation will be performed by ESP32 bootloader"); setStatus(Status::INSTALLING); - // Stream directly to flash + // Stream directly to flash with periodic watchdog feeding WiFiClient* stream = http.getStreamPtr(); - size_t written = Update.writeStream(*stream); + 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(); @@ -1038,7 +1086,7 @@ bool OTAManager::performManualUpdate(const String& channel) { // 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) { LOG_WARNING("OTA update already in progress"); return false; @@ -1059,12 +1107,25 @@ bool OTAManager::performCustomUpdate(const String& firmwareUrl, const String& ch 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); // Download directly to flash bool result = downloadDirectToFlash(firmwareUrl, fileSize); 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"); } else { LOG_ERROR("โŒ Custom firmware installation failed"); diff --git a/vesper/src/OTAManager/OTAManager.hpp b/vesper/src/OTAManager/OTAManager.hpp index fd11e59..c6e1d18 100644 --- a/vesper/src/OTAManager/OTAManager.hpp +++ b/vesper/src/OTAManager/OTAManager.hpp @@ -83,7 +83,7 @@ public: void checkFirmwareUpdateFromSD(); // Check SD for firmware update bool performManualUpdate(); // Manual update triggered by app 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 String getHardwareVariant() const; diff --git a/vesper/src/Player/Player.cpp b/vesper/src/Player/Player.cpp index e52c348..99b322c 100644 --- a/vesper/src/Player/Player.cpp +++ b/vesper/src/Player/Player.cpp @@ -2,6 +2,7 @@ #include "../Communication/CommunicationRouter/CommunicationRouter.hpp" #include "../BellEngine/BellEngine.hpp" #include "../Telemetry/Telemetry.hpp" +#include "../TimeKeeper/TimeKeeper.hpp" // ๐Ÿ”ฅ Include for Timekeeper class definition #include "../BuiltInMelodies/BuiltInMelodies.hpp" // Note: Removed global melody_steps dependency for cleaner architecture @@ -31,11 +32,12 @@ Player::Player(CommunicationRouter* comm, FileManager* fm) , _fileManager(fm) , _bellEngine(nullptr) , _telemetry(nullptr) + , _timekeeper(nullptr) , _durationTimerHandle(NULL) { } // Default constructor (for backward compatibility) -Player::Player() +Player::Player() : id(0) , name("melody1") , uid("x") @@ -59,6 +61,7 @@ Player::Player() , _fileManager(nullptr) , _bellEngine(nullptr) , _telemetry(nullptr) + , _timekeeper(nullptr) , _durationTimerHandle(NULL) { } @@ -106,12 +109,18 @@ void Player::play() { LOG_ERROR("Cannot play: No melody loaded"); 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) { _bellEngine->setMelodyData(_melodySteps); _bellEngine->start(); } - + isPlaying = true; hardStop = false; startTime = segmentStartTime = millis(); diff --git a/vesper/src/Player/Player.hpp b/vesper/src/Player/Player.hpp index 574c0f2..81a6cb5 100644 --- a/vesper/src/Player/Player.hpp +++ b/vesper/src/Player/Player.hpp @@ -132,6 +132,12 @@ public: * @param telemetry Pointer to Telemetry instance */ 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 @@ -249,6 +255,7 @@ private: FileManager* _fileManager; // ๐Ÿ“ File operations reference BellEngine* _bellEngine; // ๐Ÿ”ฅ High-precision timing engine reference Telemetry* _telemetry; // ๐Ÿ“„ Telemetry system reference + class Timekeeper* _timekeeper; // โฐ Timekeeper reference for alert coordination std::vector _melodySteps; // ๐ŸŽต Melody data owned by Player TimerHandle_t _durationTimerHandle = NULL; // โฑ๏ธ FreeRTOS timer (saves 4KB vs task!) diff --git a/vesper/src/TimeKeeper/TimeKeeper.cpp b/vesper/src/TimeKeeper/TimeKeeper.cpp index 6816381..9f0b4ab 100644 --- a/vesper/src/TimeKeeper/TimeKeeper.cpp +++ b/vesper/src/TimeKeeper/TimeKeeper.cpp @@ -2,6 +2,7 @@ #include "../OutputManager/OutputManager.hpp" #include "../ConfigManager/ConfigManager.hpp" #include "../Networking/Networking.hpp" +#include "../Player/Player.hpp" // ๐Ÿ”ฅ Include for Player class definition #include "SD.h" #include @@ -47,6 +48,19 @@ void Timekeeper::setNetworking(Networking* 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)) { relayWriteFunc = func; LOG_WARNING("Using LEGACY relay function - consider upgrading to OutputManager"); @@ -532,26 +546,46 @@ void Timekeeper::checkClockAlerts() { if (!_configManager || !_configManager->getClockEnabled()) { return; // Clock is disabled - skip all alert functionality } - + // Check if we have required dependencies if (!_outputManager || !rtc.isrunning()) { 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 DateTime now = rtc.now(); int currentHour = now.hour(); int currentMinute = now.minute(); int currentSecond = now.second(); - // Only trigger alerts on exact seconds (0-2) to avoid multiple triggers - if (currentSecond > 2) { + // Only trigger alerts in first 30 seconds of the minute + // The lastHour/lastMinute tracking prevents duplicate triggers + if (currentSecond > 30) { return; } // Get clock configuration const auto& clockConfig = _configManager->getClockConfig(); - + // Check if alerts are disabled if (clockConfig.alertType == "OFF") { return; @@ -624,22 +658,34 @@ void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) { } const auto& clockConfig = _configManager->getClockConfig(); - + + // Mark alert as in progress + alertInProgress.store(true); + 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 uint16_t bellDuration = _configManager->getBellDuration(bellNumber); - - LOG_DEBUG("๐Ÿ”” Alert bell #%d ring %d/%d (duration: %dms)", + + LOG_DEBUG("๐Ÿ”” Alert bell #%d ring %d/%d (duration: %dms)", bellNumber + 1, i + 1, count, bellDuration); - + // Fire the bell using OutputManager _outputManager->fireOutputForDuration(bellNumber, bellDuration); - + // Wait between rings (only if there's more than one ring) if (i < count - 1) { vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval)); } } + + // Mark alert as complete + alertInProgress.store(false); } void Timekeeper::checkBacklightAutomation() { @@ -688,7 +734,7 @@ bool Timekeeper::isInSilencePeriod() { } const auto& clockConfig = _configManager->getClockConfig(); - + // Get current time DateTime now = rtc.now(); char currentTimeStr[6]; @@ -697,14 +743,22 @@ bool Timekeeper::isInSilencePeriod() { // Check daytime silence period 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; } } // Check nighttime silence period 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; } } diff --git a/vesper/src/TimeKeeper/TimeKeeper.hpp b/vesper/src/TimeKeeper/TimeKeeper.hpp index d1fb376..f705039 100644 --- a/vesper/src/TimeKeeper/TimeKeeper.hpp +++ b/vesper/src/TimeKeeper/TimeKeeper.hpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include "freertos/FreeRTOS.h" @@ -61,7 +62,8 @@ private: // Alert management - new functionality int lastHour = -1; // Track last processed hour to avoid duplicate alerts int lastMinute = -1; // Track last processed minute for quarter/half alerts - + std::atomic alertInProgress{false}; // Flag to track if alert is currently playing + // Backlight management - new functionality bool backlightState = false; // Track current backlight state @@ -69,6 +71,7 @@ private: OutputManager* _outputManager = nullptr; ConfigManager* _configManager = nullptr; Networking* _networking = nullptr; + class Player* _player = nullptr; // Reference to Player for playback status checks // Legacy function pointer (DEPRECATED - will be removed) void (*relayWriteFunc)(int relay, int state) = nullptr; @@ -84,12 +87,16 @@ public: void setOutputManager(OutputManager* outputManager); void setConfigManager(ConfigManager* configManager); void setNetworking(Networking* networking); + void setPlayer(class Player* player); // Set Player reference for playback coordination // Clock Updates Pause Functions void pauseClockUpdates() { clockUpdatesPaused = true; } void resumeClockUpdates() { clockUpdatesPaused = false; } bool areClockUpdatesPaused() const { return clockUpdatesPaused; } + // Alert interruption - called by Player when starting playback + void interruptActiveAlert(); + // Legacy interface (DEPRECATED - will be removed) void setRelayWriteFunction(void (*func)(int, int)); diff --git a/vesper/vesper.ino b/vesper/vesper.ino index e1cbb01..4519cc8 100644 --- a/vesper/vesper.ino +++ b/vesper/vesper.ino @@ -64,7 +64,7 @@ * ๐Ÿ‘จโ€๐Ÿ’ป AUTHOR: BellSystems bonamin */ -#define FW_VERSION "138" +#define FW_VERSION "151" /* @@ -77,6 +77,8 @@ * v1.3 (130) - Added Telemetry Reports to App, Various Playback Fixes * v137 - Made OTA and MQTT delays Async * v138 - Removed Ethernet, added default WiFi creds (Mikrotik AP) and fixed various Clock issues + * v140 - Changed FW Updates to Direct-to-Flash and added manual update functionality with version check + * v151 - Fixed Clock Alerts not running properly * โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ @@ -315,6 +317,7 @@ void setup() timekeeper.setOutputManager(&outputManager); timekeeper.setConfigManager(&configManager); timekeeper.setNetworking(&networking); + timekeeper.setPlayer(&player); // ๐Ÿ”ฅ Connect for playback coordination // Clock outputs now configured via ConfigManager/Communication commands // Register TimeKeeper with health monitor @@ -356,6 +359,7 @@ void setup() player.setDependencies(&communication, &fileManager); player.setBellEngine(&bellEngine); // Connect the beast! player.setTelemetry(&telemetry); + player.setTimekeeper(&timekeeper); // ๐Ÿ”ฅ Connect for alert coordination // Register Communication with health monitor healthMonitor.setCommunication(&communication);