463 lines
14 KiB
C++
463 lines
14 KiB
C++
#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
|
|
if (isPaused) {
|
|
setStatus(PlayerStatus::STOPPED);
|
|
LOG_DEBUG("Plbck: STOP from PAUSED state");
|
|
} else {
|
|
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<const char*>();
|
|
}
|
|
if (doc.containsKey("uid")) {
|
|
uid = doc["uid"].as<const char*>();
|
|
}
|
|
if (doc.containsKey("url")) {
|
|
url = doc["url"].as<const char*>();
|
|
}
|
|
if (doc.containsKey("speed")) {
|
|
speed = doc["speed"].as<uint16_t>();
|
|
}
|
|
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<uint32_t>();
|
|
}
|
|
if (doc.containsKey("pause_duration")) {
|
|
pause_duration = doc["pause_duration"].as<uint32_t>();
|
|
}
|
|
if (doc.containsKey("total_duration")) {
|
|
total_duration = doc["total_duration"].as<uint32_t>();
|
|
}
|
|
if (doc.containsKey("continuous_loop")) {
|
|
continuous_loop = doc["continuous_loop"].as<bool>();
|
|
}
|
|
|
|
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<Player*>(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) {
|
|
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) {
|
|
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;
|
|
}
|