Complete Rebuild, with Subsystems for each component. RTOS Tasks. (help by Claude)

This commit is contained in:
2025-10-01 12:42:00 +03:00
parent 104c1d04d4
commit f696984cd1
57 changed files with 11757 additions and 2290 deletions

View 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;
}

View 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
// ═══════════════════════════════════════════════════════════════════════════════════