#include "Player.hpp" #include "../Communication/CommunicationRouter/CommunicationRouter.hpp" #include "../BellEngine/BellEngine.hpp" // Note: Removed global melody_steps dependency for cleaner architecture // Constructor with dependencies Player::Player(CommunicationRouter* comm, FileManager* fm) : id(0) , name("melody1") , uid("x") , url("-") , noteAssignments{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16} , speed(500) , segment_duration(15000) , pause_duration(0) , total_duration(0) , segmentCmpltTime(0) , segmentStartTime(0) , startTime(0) , pauseTime(0) , continuous_loop(false) , infinite_play(false) , isPlaying(false) , isPaused(false) , hardStop(false) , _status(PlayerStatus::STOPPED) , _commManager(comm) , _fileManager(fm) , _bellEngine(nullptr) , _durationTimerHandle(NULL) { } // Default constructor (for backward compatibility) Player::Player() : id(0) , name("melody1") , uid("x") , url("-") , noteAssignments{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16} , speed(500) , segment_duration(15000) , pause_duration(0) , total_duration(0) , segmentCmpltTime(0) , segmentStartTime(0) , startTime(0) , pauseTime(0) , continuous_loop(false) , infinite_play(false) , isPlaying(false) , isPaused(false) , hardStop(false) , _status(PlayerStatus::STOPPED) , _commManager(nullptr) , _fileManager(nullptr) , _bellEngine(nullptr) , _durationTimerHandle(NULL) { } void Player::setDependencies(CommunicationRouter* comm, FileManager* fm) { _commManager = comm; _fileManager = fm; } // Destructor Player::~Player() { // Stop any ongoing playback if (isPlaying) { forceStop(); } // Properly cleanup timer if (_durationTimerHandle != NULL) { xTimerDelete(_durationTimerHandle, portMAX_DELAY); _durationTimerHandle = NULL; } } void Player::begin() { LOG_INFO("Initializing Player with FreeRTOS Timer (saves 4KB RAM!)"); // Create a periodic timer that fires every 500ms _durationTimerHandle = xTimerCreate( "PlayerTimer", // Timer name pdMS_TO_TICKS(500), // Period (500ms) pdTRUE, // Auto-reload (periodic) this, // Timer ID (pass Player instance) durationTimerCallback // Callback function ); if (_durationTimerHandle != NULL) { xTimerStart(_durationTimerHandle, 0); LOG_INFO("Player initialized successfully with timer"); } else { LOG_ERROR("Failed to create Player timer!"); } } void Player::play() { if (_melodySteps.empty()) { LOG_ERROR("Cannot play: No melody loaded"); return; } if (_bellEngine) { _bellEngine->setMelodyData(_melodySteps); _bellEngine->start(); } isPlaying = true; hardStop = false; startTime = segmentStartTime = millis(); setStatus(PlayerStatus::PLAYING); // Update status and notify clients LOG_DEBUG("Plbck: PLAY"); } void Player::forceStop() { if (_bellEngine) { _bellEngine->emergencyStop(); } hardStop = true; isPlaying = false; setStatus(PlayerStatus::STOPPED); // Immediate stop, notify clients LOG_DEBUG("Plbck: FORCE STOP"); } void Player::stop() { if (_bellEngine) { _bellEngine->stop(); } hardStop = false; isPlaying = false; // Set STOPPING status - actual stop message will be sent when BellEngine finishes setStatus(PlayerStatus::STOPPING); LOG_DEBUG("Plbck: SOFT STOP (waiting for melody to complete)"); // NOTE: The actual "stop" message is now sent in onMelodyLoopCompleted() // when the BellEngine actually finishes the current loop } void Player::pause() { isPaused = true; setStatus(PlayerStatus::PAUSED); LOG_DEBUG("Plbck: PAUSE"); } void Player::unpause() { isPaused = false; segmentStartTime = millis(); setStatus(PlayerStatus::PLAYING); LOG_DEBUG("Plbck: RESUME"); } bool Player::command(JsonVariant data) { setMelodyAttributes(data); loadMelodyInRAM(); // Removed parameter - use internal storage String action = data["action"]; LOG_DEBUG("Incoming Command: %s", action.c_str()); // Play or Stop Logic if (action == "play") { play(); return true; } else if (action == "stop") { forceStop(); return true; } else { LOG_WARNING("Unknown playback action: %s", action.c_str()); return false; } } void Player::setMelodyAttributes(JsonVariant doc) { if (doc.containsKey("name")) { name = doc["name"].as(); } if (doc.containsKey("uid")) { uid = doc["uid"].as(); } if (doc.containsKey("url")) { url = doc["url"].as(); } if (doc.containsKey("speed")) { speed = doc["speed"].as(); } if (doc.containsKey("note_assignments")) { JsonArray noteArray = doc["note_assignments"]; size_t arraySize = min(noteArray.size(), (size_t)16); for (size_t i = 0; i < arraySize; i++) { noteAssignments[i] = noteArray[i]; } } if (doc.containsKey("segment_duration")) { segment_duration = doc["segment_duration"].as(); } if (doc.containsKey("pause_duration")) { pause_duration = doc["pause_duration"].as(); } if (doc.containsKey("total_duration")) { total_duration = doc["total_duration"].as(); } if (doc.containsKey("continuous_loop")) { continuous_loop = doc["continuous_loop"].as(); } if (continuous_loop && total_duration == 0) { infinite_play = true; } if (!continuous_loop) { total_duration = segment_duration; } // Print Just for Debugging Purposes LOG_DEBUG("Set Melody Vars / Name: %s, UID: %s", name.c_str(), uid.c_str()); LOG_DEBUG("URL: %s", url.c_str()); LOG_DEBUG("Speed: %d, Per Segment Duration: %lu, Pause Duration: %lu, Total Duration: %d, Continuous: %s, Infinite: %s", speed, segment_duration, pause_duration, total_duration, continuous_loop ? "true" : "false", infinite_play ? "true" : "false"); } void Player::loadMelodyInRAM() { String filePath = "/melodies/" + String(uid.c_str()); File bin_file = SD.open(filePath.c_str(), FILE_READ); if (!bin_file) { LOG_ERROR("Failed to open file: %s", filePath.c_str()); LOG_ERROR("Check Servers for the File..."); // Try to download the file using FileManager if (_fileManager) { StaticJsonDocument<128> doc; doc["download_url"] = url; doc["melodys_uid"] = uid; if (!_fileManager->addMelody(doc)) { LOG_ERROR("Failed to Download File. Check Internet Connection"); return; } else { bin_file = SD.open(filePath.c_str(), FILE_READ); } } else { LOG_ERROR("FileManager not available for download"); return; } } size_t fileSize = bin_file.size(); if (fileSize % 2 != 0) { LOG_ERROR("Invalid file size: %u (not a multiple of 2)", fileSize); bin_file.close(); return; } // Load into Player's internal melody storage only _melodySteps.resize(fileSize / 2); for (size_t i = 0; i < _melodySteps.size(); i++) { uint8_t high = bin_file.read(); uint8_t low = bin_file.read(); _melodySteps[i] = (high << 8) | low; } LOG_INFO("Melody loaded successfully: %d steps", _melodySteps.size()); bin_file.close(); } // Static timer callback function for FreeRTOS void Player::durationTimerCallback(TimerHandle_t xTimer) { // Get Player instance from timer ID Player* player = static_cast(pvTimerGetTimerID(xTimer)); // Only run checks when actually playing if (!player->isPlaying) { return; } unsigned long now = millis(); if (player->timeToStop(now)) { player->stop(); } else if (player->timeToPause(now)) { player->pause(); } else if (player->timeToResume(now)) { player->unpause(); } } // Check if it's time to stop playback bool Player::timeToStop(unsigned long now) { if (isPlaying && !infinite_play && total_duration == 0) { if (now > startTime){} LOG_DEBUG("(Single Loop Run Seelected) Soft Stopping."); return true; } } else if (isPlaying && !infinite_play) { uint64_t stopTime = startTime + total_duration; if (now >= stopTime) { LOG_DEBUG("(TimerFunction) Total Run Duration Reached. Soft Stopping."); return true; } } return false; } // Status management and BellEngine callback void Player::setStatus(PlayerStatus newStatus) { if (_status == newStatus) { return; // No change, don't send duplicate messages } PlayerStatus oldStatus = _status; _status = newStatus; // Send appropriate message to ALL clients (WebSocket + MQTT) based on status change if (_commManager) { StaticJsonDocument<256> doc; doc["status"] = "INFO"; doc["type"] = "playback"; // Create payload object for complex data JsonObject payload = doc.createNestedObject("payload"); switch (newStatus) { case PlayerStatus::PLAYING: payload["action"] = "playing"; payload["time_elapsed"] = (millis() - startTime) / 1000; // Convert to seconds break; case PlayerStatus::PAUSED: payload["action"] = "paused"; payload["time_elapsed"] = (millis() - startTime) / 1000; break; case PlayerStatus::STOPPED: payload["action"] = "idle"; payload["time_elapsed"] = 0; break; case PlayerStatus::STOPPING: payload["action"] = "stopping"; payload["time_elapsed"] = (millis() - startTime) / 1000; break; } // Add projected run time for all states (0 if not applicable) uint64_t projectedRunTime = calculateProjectedRunTime(); payload["projected_run_time"] = projectedRunTime; // 🔥 Use broadcastStatus() to send to BOTH WebSocket AND MQTT clients! _commManager->broadcastStatus(doc); LOG_DEBUG("Status changed: %d → %d, broadcast sent with runTime: %llu", (int)oldStatus, (int)newStatus, projectedRunTime); } } 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); LOG_DEBUG("Plbck: ACTUAL STOP (melody loop completed)"); } // Mark segment completion time segmentCmpltTime = millis(); } // Check if it's time to pause playback bool Player::timeToPause(unsigned long now) { if (isPlaying && continuous_loop) { // Special case: segment_duration = 0 means "one loop only" if (segment_duration == 0) { // Only pause after first loop completes (segmentCmpltTime updated) if (segmentCmpltTime > segmentStartTime && !isPaused) { LOG_DEBUG("(TimerFunction) One-loop segment completed. Pausing."); pauseTime = now; return true; } } else { // Normal duration-based pausing uint64_t timeToPause = segmentStartTime + segment_duration; LOG_DEBUG("PTL: %llu // NOW: %lu", timeToPause, now); if (now >= timeToPause && !isPaused) { LOG_DEBUG("(TimerFunction) Segment Duration Reached. Pausing."); pauseTime = now; return true; } } } return false; } // Check if it's time to resume playback bool Player::timeToResume(unsigned long now) { if (isPaused) { uint64_t timeToResume = segmentCmpltTime + pause_duration; if (now >= timeToResume) { LOG_DEBUG("(TimerFunction) Pause Duration Reached. Resuming"); segmentStartTime = now; // Reset segment start time for next cycle return true; } } return false; } // Calculate the projected total run time of current playback uint64_t Player::calculateProjectedRunTime() const { if (_melodySteps.empty() || (_status == PlayerStatus::STOPPED)) { return 0; // No melody loaded or actually stopped } // Calculate single loop duration: steps * speed (in milliseconds) uint32_t singleLoopDuration = _melodySteps.size() * speed; if (infinite_play || total_duration == 0) { return 0; // Infinite playback has no end time } // Calculate how many loops are needed to meet or exceed total_duration uint32_t loopsNeeded = (total_duration + singleLoopDuration - 1) / singleLoopDuration; // Ceiling division // Calculate actual total duration (this is the projected run time) uint32_t actualTotalDuration = singleLoopDuration * loopsNeeded; // Return the total duration (offset from start), not a timestamp return actualTotalDuration; } // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ bool Player::isHealthy() const { // Check if dependencies are properly set if (!_commManager) { LOG_DEBUG("Player: Unhealthy - Communication manager not set"); return false; } if (!_fileManager) { LOG_DEBUG("Player: Unhealthy - File manager not set"); return false; } if (!_bellEngine) { LOG_DEBUG("Player: Unhealthy - BellEngine not set"); return false; } // Check if timer is properly created if (_durationTimerHandle == NULL) { LOG_DEBUG("Player: Unhealthy - Duration timer not created"); return false; } // Check if timer is actually running if (xTimerIsTimerActive(_durationTimerHandle) == pdFALSE) { LOG_DEBUG("Player: Unhealthy - Duration timer not active"); return false; } // Check for consistent playback state if (isPlaying && (_status == PlayerStatus::STOPPED)) { LOG_DEBUG("Player: Unhealthy - Inconsistent playback state"); return false; } return true; }