Complete Rebuild, with Subsystems for each component. RTOS Tasks. (help by Claude)
This commit is contained in:
457
vesper/src/Player/Player.cpp
Normal file
457
vesper/src/Player/Player.cpp
Normal file
@@ -0,0 +1,457 @@
|
||||
#include "Player.hpp"
|
||||
#include "../Communication/Communication.hpp"
|
||||
#include "../BellEngine/BellEngine.hpp"
|
||||
|
||||
// Note: Removed global melody_steps dependency for cleaner architecture
|
||||
|
||||
// Constructor with dependencies
|
||||
Player::Player(Communication* comm, FileManager* fm)
|
||||
: id(0)
|
||||
, name("melody1")
|
||||
, uid("x")
|
||||
, url("-")
|
||||
, noteAssignments{1,2,3,4,5,6,0,0,0,0,0,0,0,0,0,0}
|
||||
, 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,0,0,0,0,0,0,0,0,0,0}
|
||||
, 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(Communication* 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<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");
|
||||
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;
|
||||
}
|
||||
267
vesper/src/Player/Player.hpp
Normal file
267
vesper/src/Player/Player.hpp
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* PLAYER.HPP - Melody Playback and Control System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🎵 THE MELODY MAESTRO OF VESPER 🎵
|
||||
*
|
||||
* This class manages melody playback, timing control, and coordination with
|
||||
* the BellEngine for precise bell activation. It handles melody loading,
|
||||
* duration management, and playback state control.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Clean separation between playback logic and timing engine
|
||||
* • FreeRTOS timer-based duration control (saves 4KB RAM vs tasks!)
|
||||
* • Dependency injection for loose coupling
|
||||
* • Thread-safe state management
|
||||
* • Comprehensive melody metadata handling
|
||||
*
|
||||
* 🎶 KEY FEATURES:
|
||||
* • Multi-format melody support with note assignments
|
||||
* • Flexible timing control (speed, segments, loops)
|
||||
* • Pause/resume functionality
|
||||
* • Duration-based automatic stopping
|
||||
* • Continuous and finite loop modes
|
||||
* • Real-time playback status tracking
|
||||
*
|
||||
* ⏱️ TIMING MANAGEMENT:
|
||||
* • Segment-based playback with configurable pauses
|
||||
* • Total duration limiting
|
||||
* • Precision timing coordination with BellEngine
|
||||
* • Memory-efficient timer implementation
|
||||
*
|
||||
* 🔗 INTEGRATION:
|
||||
* The Player coordinates with BellEngine for precise timing,
|
||||
* Communication for command handling, and FileManager for
|
||||
* melody file operations.
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Modular architecture with dependency injection)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// SYSTEM INCLUDES - Core libraries for melody playback
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
#include <Arduino.h> // Arduino core functionality
|
||||
#include <vector> // STL vector for melody data storage
|
||||
#include <string> // STL string for melody metadata
|
||||
#include <cstdint> // Fixed-width integer types
|
||||
#include <ArduinoJson.h> // JSON parsing for melody configuration
|
||||
#include <ESPAsyncWebServer.h> // WebSocket client handling
|
||||
#include <SD.h> // SD card operations for melody files
|
||||
#include "freertos/FreeRTOS.h" // FreeRTOS kernel
|
||||
#include "freertos/task.h" // FreeRTOS task management
|
||||
#include "../Logging/Logging.hpp" // Centralized logging system
|
||||
#include "../FileManager/FileManager.hpp" // File operations abstraction
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// FORWARD DECLARATIONS - Dependencies injected at runtime
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
class Communication; // Command handling and communication
|
||||
class BellEngine; // High-precision timing engine
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// PLAYER STATUS ENUMERATION
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
/**
|
||||
* @enum PlayerStatus
|
||||
* @brief Defines the current state of the player
|
||||
*/
|
||||
enum class PlayerStatus {
|
||||
STOPPED, // ⏹️ Not playing, engine stopped
|
||||
PLAYING, // ▶️ Actively playing melody
|
||||
PAUSED, // ⏸️ Temporarily paused between segments
|
||||
STOPPING // 🔄 Soft stop triggered, waiting for melody to complete
|
||||
};
|
||||
|
||||
/**
|
||||
* @class Player
|
||||
* @brief Melody playback and timing control system
|
||||
*
|
||||
* The Player class manages all aspects of melody playback including timing,
|
||||
* duration control, pause/resume functionality, and coordination with the
|
||||
* BellEngine for precise bell activation.
|
||||
*
|
||||
* Key responsibilities:
|
||||
* - Melody metadata management (name, speed, duration, etc.)
|
||||
* - Playback state control (play, pause, stop)
|
||||
* - Duration-based automatic stopping
|
||||
* - Note assignment mapping for bell activation
|
||||
* - Integration with BellEngine for precision timing
|
||||
*/
|
||||
class Player {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTORS & DEPENDENCY INJECTION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Constructor with dependency injection
|
||||
* @param comm Pointer to communication manager
|
||||
* @param fm Pointer to file manager
|
||||
*/
|
||||
Player(Communication* comm, FileManager* fm);
|
||||
|
||||
/**
|
||||
* @brief Default constructor for backward compatibility
|
||||
*
|
||||
* When using this constructor, must call setDependencies() before use.
|
||||
*/
|
||||
Player();
|
||||
|
||||
/**
|
||||
* @brief Set dependencies after construction
|
||||
* @param comm Pointer to communication manager
|
||||
* @param fm Pointer to file manager
|
||||
*/
|
||||
void setDependencies(Communication* comm, FileManager* fm);
|
||||
|
||||
/**
|
||||
* @brief Set BellEngine reference for precision timing
|
||||
* @param engine Pointer to BellEngine instance
|
||||
*/
|
||||
void setBellEngine(BellEngine* engine) { _bellEngine = engine; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// MELODY METADATA - Public access for compatibility
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
uint16_t id; // 🏷️ Internal ID of the selected melody
|
||||
std::string name; // 🏵️ Display name of the melody
|
||||
std::string uid; // 🆔 Unique identifier from Firestore
|
||||
std::string url; // 🌐 Download URL for melody binary
|
||||
uint16_t noteAssignments[16]; // 🎹 Note-to-bell mapping configuration
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// TIMING CONFIGURATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
uint16_t speed; // ⏱️ Time per beat in milliseconds
|
||||
uint32_t segment_duration; // ⏳ Duration per loop segment (milliseconds)
|
||||
uint32_t pause_duration; // ⏸️ Pause between segments (milliseconds)
|
||||
uint32_t total_duration; // ⏰ Total runtime limit (milliseconds)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// RUNTIME STATE TRACKING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
uint64_t segmentCmpltTime; // ✅ Timestamp of last segment completion
|
||||
uint64_t segmentStartTime; // 🚀 Timestamp when current segment started
|
||||
uint64_t startTime; // 🏁 Timestamp when melody playback began
|
||||
uint64_t pauseTime; // ⏸️ Timestamp when melody was paused
|
||||
bool isPlaying; // ▶️ Currently playing indicator
|
||||
bool isPaused; // ⏸️ Currently paused indicator
|
||||
bool hardStop; // 🚑 Emergency stop flag
|
||||
bool continuous_loop; // 🔄 Continuous loop mode flag
|
||||
bool infinite_play; // ∞ Infinite playback mode flag
|
||||
PlayerStatus _status; // 📊 Current player status
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// DESTRUCTOR
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Destructor - Clean up resources
|
||||
*
|
||||
* Ensures proper cleanup of timers and resources.
|
||||
*/
|
||||
~Player();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZATION & CONTROL METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Initialize playback control system */
|
||||
void begin();
|
||||
|
||||
/** @brief Start melody playback */
|
||||
void play();
|
||||
|
||||
/** @brief Stop melody playback gracefully */
|
||||
void stop();
|
||||
|
||||
/** @brief Force immediate stop */
|
||||
void forceStop();
|
||||
|
||||
/** @brief Pause current playback */
|
||||
void pause();
|
||||
|
||||
/** @brief Resume paused playback */
|
||||
void unpause();
|
||||
|
||||
/** @brief Handle JSON commands from communication layer */
|
||||
bool command(JsonVariant data);
|
||||
|
||||
/** @brief Set melody attributes from JSON configuration */
|
||||
void setMelodyAttributes(JsonVariant doc);
|
||||
|
||||
/** @brief Load melody data into RAM for playback */
|
||||
void loadMelodyInRAM();
|
||||
|
||||
/** @brief Static timer callback for FreeRTOS duration control */
|
||||
static void durationTimerCallback(TimerHandle_t xTimer);
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// STATUS QUERY METHODS
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Get current player status */
|
||||
PlayerStatus getStatus() const { return _status; }
|
||||
|
||||
/** @brief Check if player is currently playing */
|
||||
bool isCurrentlyPlaying() const { return _status == PlayerStatus::PLAYING; }
|
||||
|
||||
/** @brief Check if player is currently paused */
|
||||
bool isCurrentlyPaused() const { return _status == PlayerStatus::PAUSED; }
|
||||
|
||||
/** @brief Check if player is in stopping state (soft stop triggered) */
|
||||
bool isCurrentlyStopping() const { return _status == PlayerStatus::STOPPING; }
|
||||
|
||||
/** @brief Check if player is completely stopped */
|
||||
bool isCurrentlyStopped() const { return _status == PlayerStatus::STOPPED; }
|
||||
|
||||
/** @brief BellEngine callback when melody loop actually completes */
|
||||
void onMelodyLoopCompleted();
|
||||
|
||||
/** @brief Calculate the projected total run time of current playback */
|
||||
uint64_t calculateProjectedRunTime() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if Player is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE DEPENDENCIES AND DATA
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
Communication* _commManager; // 📡 Communication system reference
|
||||
FileManager* _fileManager; // 📁 File operations reference
|
||||
BellEngine* _bellEngine; // 🔥 High-precision timing engine reference
|
||||
|
||||
std::vector<uint16_t> _melodySteps; // 🎵 Melody data owned by Player
|
||||
TimerHandle_t _durationTimerHandle = NULL; // ⏱️ FreeRTOS timer (saves 4KB vs task!)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE HELPER METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if it's time to stop based on duration */
|
||||
bool timeToStop(unsigned long now);
|
||||
|
||||
/** @brief Check if it's time to pause based on segment timing */
|
||||
bool timeToPause(unsigned long now);
|
||||
|
||||
/** @brief Check if it's time to resume from pause */
|
||||
bool timeToResume(unsigned long now);
|
||||
|
||||
/** @brief Update player status and notify clients if changed */
|
||||
void setStatus(PlayerStatus newStatus);
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// END OF PLAYER.HPP
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
Reference in New Issue
Block a user