/* * ═══════════════════════════════════════════════════════════════════════════════════ * BELLENGINE.CPP - High-Precision Bell Timing Engine Implementation * ═══════════════════════════════════════════════════════════════════════════════════ * * This file contains the implementation of the BellEngine class - the core * precision timing system that controls bell activation with microsecond accuracy. * * 🔥 CRITICAL PERFORMANCE SECTION 🔥 * * The code in this file is performance-critical and runs on a dedicated * FreeRTOS task with maximum priority on Core 1. Any modifications should * be thoroughly tested for timing impact. * * 📋 VERSION: 2.0 (Rewritten for modular architecture) * 📅 DATE: 2025 * 👨‍💻 AUTHOR: Advanced Bell Systems * ═══════════════════════════════════════════════════════════════════════════════════ */ // ═════════════════════════════════════════════════════════════════════════════════ // DEPENDENCY INCLUDES - Required system components // ═════════════════════════════════════════════════════════════════════════════════ #include "BellEngine.hpp" // Header file with class definition #define TAG "BellEngine" #include "../Player/Player.hpp" // Melody playback controller #include "../ConfigManager/ConfigManager.hpp" // Configuration and settings #include "../Telemetry/Telemetry.hpp" // System monitoring and analytics #include "../OutputManager/OutputManager.hpp" // Hardware abstraction layer #include "../Communication/CommunicationRouter/CommunicationRouter.hpp" // Communication system for notifications // ═════════════════════════════════════════════════════════════════════════════════ // CONSTRUCTOR & DESTRUCTOR IMPLEMENTATION // ═════════════════════════════════════════════════════════════════════════════════ /** * @brief Constructor - Initialize BellEngine with dependency injection * * Sets up all dependency references and initializes the OutputManager integration. * Note: The OutputManager now handles all relay duration tracking, providing * clean separation of concerns and hardware abstraction. */ BellEngine::BellEngine(Player& player, ConfigManager& configManager, Telemetry& telemetry, OutputManager& outputManager) : _player(player) // Reference to melody playback controller , _configManager(configManager) // Reference to configuration manager , _telemetry(telemetry) // Reference to system monitoring , _outputManager(outputManager) // 🔥 Reference to hardware abstraction layer , _communicationManager(nullptr) { // Initialize communication manager to nullptr // 🏗️ ARCHITECTURAL NOTE: // OutputManager now handles all relay duration tracking automatically! // This provides clean separation of concerns and hardware abstraction. } /** * @brief Destructor - Ensures safe cleanup * * Automatically calls emergencyShutdown() to ensure all relays are turned off * and the system is in a safe state before object destruction. */ BellEngine::~BellEngine() { emergencyShutdown(); // 🚑 Ensure safe shutdown on destruction } // ═════════════════════════════════════════════════════════════════════════════════ // CORE INITIALIZATION IMPLEMENTATION // ═════════════════════════════════════════════════════════════════════════════════ /** * @brief Initialize the BellEngine system * * Creates the high-priority timing task on Core 1 with maximum priority. * This task provides the microsecond-precision timing that makes the * bell system so accurate and reliable. * */ void BellEngine::begin() { LOG_DEBUG(TAG, "Initializing BellEngine..."); // Create engine task with HIGHEST priority on dedicated Core 1 // This ensures maximum performance and timing precision xTaskCreatePinnedToCore( engineTask, // 📋 Task function pointer "BellEngine", // 🏷️ Task name for debugging 12288, // 💾 Stack size (12KB - increased for safety) this, // 🔗 Parameter (this instance) 6, // ⚡ HIGHEST Priority (0-7, 7 is highest) &_engineTaskHandle, // 💼 Task handle storage 1 // 💻 Pin to Core 1 (dedicated) ); LOG_INFO(TAG, "BellEngine initialized !"); } /** * @brief Set Communication manager reference for bell notifications */ void BellEngine::setCommunicationManager(CommunicationRouter* commManager) { _communicationManager = commManager; LOG_DEBUG(TAG, "BellEngine: Communication manager %s", commManager ? "connected" : "disconnected"); } // ═════════════════════════════════════════════════════════════════════════════════ // ENGINE CONTROL IMPLEMENTATION (Thread-safe) // ═════════════════════════════════════════════════════════════════════════════════ /** * @brief Start the precision timing engine * * Activates the high-precision timing loop. Requires melody data to be * loaded via setMelodyData() before calling. Uses atomic operations * for thread-safe state management. * * @note Will log error and return if no melody data is available */ void BellEngine::start() { // Validate that melody data is ready before starting if (!_melodyDataReady.load()) { LOG_ERROR(TAG, "Cannot start BellEngine: No melody data loaded"); return; // ⛔ Early exit if no melody data } LOG_INFO(TAG, "🚀 BellEngine Ignition - Starting precision playback"); _emergencyStop.store(false); // ✅ Clear any emergency stop state _engineRunning.store(true); // ✅ Activate the engine atomically } void BellEngine::stop() { LOG_INFO(TAG, "BellEngine - Stopping Gracefully"); _engineRunning.store(false); } void BellEngine::emergencyStop() { LOG_INFO(TAG, "BellEngine - 🛑 Forcing Stop Immediately"); _emergencyStop.store(true); _engineRunning.store(false); emergencyShutdown(); } void BellEngine::setMelodyData(const std::vector& melodySteps) { portENTER_CRITICAL(&_melodyMutex); _melodySteps = melodySteps; _melodyDataReady.store(true); portEXIT_CRITICAL(&_melodyMutex); LOG_DEBUG(TAG, "BellEngine - Loaded melody: %d steps", melodySteps.size()); } void BellEngine::clearMelodyData() { portENTER_CRITICAL(&_melodyMutex); _melodySteps.clear(); _melodyDataReady.store(false); portEXIT_CRITICAL(&_melodyMutex); LOG_DEBUG(TAG, "BellEngine - Melody data cleared"); } // ================== CRITICAL TIMING SECTION ================== // This is where the magic happens! Maximum precision required ! void BellEngine::engineTask(void* parameter) { BellEngine* engine = static_cast(parameter); LOG_DEBUG(TAG, "BellEngine - 🔥 Engine task started on Core %d with MAXIMUM priority", xPortGetCoreID()); while (true) { if (engine->_engineRunning.load() && !engine->_emergencyStop.load()) { engine->engineLoop(); } else { // Low-power wait when not running vTaskDelay(pdMS_TO_TICKS(10)); } } } void BellEngine::engineLoop() { uint64_t loopStartTime = getMicros(); // Safety check. Stop if Emergency Stop is Active if (_emergencyStop.load()) { emergencyShutdown(); return; } playbackLoop(); // Check telemetry for overloads and send notifications if needed checkAndNotifyOverloads(); // Pause handling AFTER complete loop - never interrupt mid-melody! while (_player.isPaused && _player.isPlaying && !_player.hardStop) { LOG_VERBOSE(TAG, "BellEngine - ⏸️ Pausing between melody loops"); vTaskDelay(pdMS_TO_TICKS(10)); // Wait during pause } uint64_t loopEndTime = getMicros(); uint32_t loopTime = (uint32_t)(loopEndTime - loopStartTime); } void BellEngine::playbackLoop() { // Check if player wants us to run if (!_player.isPlaying || _player.hardStop) { _engineRunning.store(false); return; } // Get melody data safely portENTER_CRITICAL(&_melodyMutex); auto melodySteps = _melodySteps; // Fast copy portEXIT_CRITICAL(&_melodyMutex); if (melodySteps.empty()) { LOG_ERROR(TAG, "BellEngine - ❌ Empty melody in playback loop!"); return; } LOG_DEBUG(TAG, "BellEngine - 🎵 Starting melody loop (%d steps)", melodySteps.size()); // CRITICAL TIMING LOOP - Complete the entire melody without interruption for (uint16_t note : melodySteps) { // Emergency exit check (only emergency stops can interrupt mid-loop) if (_emergencyStop.load() || _player.hardStop) { LOG_DEBUG(TAG, "BellEngine - Emergency exit from playback loop"); return; } // Activate note with MAXIMUM PRECISION activateNote(note); // Precise timing delay - validate speed to prevent division by zero // I THINK this should be moved outside the Bell Engine if (_player.speed == 0) { LOG_ERROR(TAG, "BellEngine - ❌ Invalid Speed (0) detected, stopping playback"); _player.hardStop = true; _engineRunning.store(false); return; } uint32_t tempoMicros = _player.speed * 1000; // Convert ms to microseconds preciseDelay(tempoMicros); } // Mark segment completion and notify Player _player.segmentCmpltTime = millis(); _player.onMelodyLoopCompleted(); // 🔥 Notify Player that melody actually finished! if ((_player.continuous_loop && _player.segment_duration == 0) || _player.total_duration == 0) { vTaskDelay(pdMS_TO_TICKS(500)); //Give Player time to pause/stop LOG_VERBOSE(TAG, "BellEngine - Loop completed in SINGLE Mode - waiting for Player to handle pause/stop"); } LOG_DEBUG(TAG, "BellEngine - 🎵 Melody loop completed with PRECISION"); } void BellEngine::activateNote(uint16_t note) { // Track which bells we've already added to prevent duplicates bool bellFired[16] = {false}; std::vector> bellDurations; // For batch firing std::vector firedBellIndices; // Track which bells were fired for notification // Iterate through each bit position (note index) for (uint8_t noteIndex = 0; noteIndex < 16; noteIndex++) { if (note & (1 << noteIndex)) { // Get bell mapping (noteAssignments stored as 1-indexed) uint8_t bellConfig = _player.noteAssignments[noteIndex]; // Skip if no bell assigned if (bellConfig == 0) continue; // Convert 1-indexed config to 0-indexed bellIndex uint8_t bellIndex = bellConfig - 1; // Additional safety check to prevent underflow crashes if (bellIndex >= 255) { LOG_ERROR(TAG, "BellEngine - 🚨 UNDERFLOW ERROR: bellIndex underflow for noteIndex %d", noteIndex); continue; } // Bounds check (CRITICAL SAFETY) if (bellIndex >= 16) { LOG_ERROR(TAG, "BellEngine - 🚨 BOUNDS ERROR: bellIndex %d >= 16", bellIndex); continue; } // Check for duplicate bell firing in this note if (bellFired[bellIndex]) { LOG_DEBUG(TAG, "BellEngine - ⚠️ DUPLICATE BELL: Skipping duplicate firing of bell %d for note %d", bellIndex, noteIndex); continue; } // Check if bell is configured (OutputManager will validate this) uint8_t physicalOutput = _outputManager.getPhysicalOutput(bellIndex); if (physicalOutput == 255) { LOG_DEBUG(TAG, "BellEngine - ⚠️ UNCONFIGURED: Bell %d not configured, skipping", bellIndex); continue; } // Mark this bell as fired bellFired[bellIndex] = true; // Get duration from config uint16_t durationMs = _configManager.getBellDuration(bellIndex); // Add to batch firing list bellDurations.push_back({physicalOutput, durationMs}); // Add to notification list (convert to 1-indexed for display) firedBellIndices.push_back(bellIndex + 1); // Record telemetry _telemetry.recordBellStrike(bellIndex); LOG_VERBOSE(TAG, "BellEngine - 🔨 STRIKE! Note:%d → Bell:%d for %dms", noteIndex, bellIndex, durationMs); } } // 🚀 FIRE ALL BELLS SIMULTANEOUSLY! if (!bellDurations.empty()) { _outputManager.fireOutputsBatchForDuration(bellDurations); LOG_VERBOSE(TAG, "BellEngine - 🔥 Batch Fired %d bells Simultaneously !", bellDurations.size()); // 🔔 NOTIFY WEBSOCKET CLIENTS OF BELL DINGS! // * deactivated currently, since unstable and causes performance issues * // notifyBellsFired(firedBellIndices); } } void BellEngine::preciseDelay(uint32_t microseconds) { uint64_t start = getMicros(); uint64_t target = start + microseconds; // For delays > 1ms, use task delay for most of it if (microseconds > 1000) { uint32_t taskDelayMs = (microseconds - 500) / 1000; // Leave 500µs for busy wait vTaskDelay(pdMS_TO_TICKS(taskDelayMs)); } // Busy wait for final precision while (getMicros() < target) { // Tight loop for maximum precision asm volatile("nop"); } } void BellEngine::emergencyShutdown() { LOG_INFO(TAG, "BellEngine - 🚨 Emergency Shutdown - Notifying OutputManager"); _outputManager.emergencyShutdown(); } void BellEngine::notifyBellsFired(const std::vector& bellIndices) { if (!_communicationManager || bellIndices.empty()) { return; // No communication manager or no bells fired } try { // Create notification message StaticJsonDocument<256> dingMsg; dingMsg["status"] = "INFO"; dingMsg["type"] = "ding"; // Create payload array with fired bell numbers (1-indexed for display) JsonArray bellsArray = dingMsg["payload"].to(); for (uint8_t bellIndex : bellIndices) { bellsArray.add(bellIndex); // Already converted to 1-indexed in activateNote } // Send notification to WebSocket clients only (not MQTT) _communicationManager->broadcastToAllWebSocketClients(dingMsg); LOG_DEBUG(TAG, "BellEngine - 🔔 DING notification sent for %d bells", bellIndices.size()); } catch (...) { LOG_WARNING(TAG, "BellEngine - ❌ Failed to send ding notification"); } } // ════════════════════════════════════════════════════════════════════════════ // HEALTH CHECK IMPLEMENTATION // ════════════════════════════════════════════════════════════════════════════ bool BellEngine::isHealthy() const { // Check if engine task is created and running if (_engineTaskHandle == NULL) { LOG_DEBUG(TAG, "BellEngine: Unhealthy - Task not created"); return false; } // Check if task is still alive eTaskState taskState = eTaskGetState(_engineTaskHandle); if (taskState == eDeleted || taskState == eInvalid) { LOG_DEBUG(TAG, "BellEngine: Unhealthy - Task deleted or invalid"); return false; } // Check if OutputManager is properly connected and healthy if (!_outputManager.isInitialized()) { LOG_DEBUG(TAG, "BellEngine: Unhealthy - OutputManager not initialized"); return false; } return true; } void BellEngine::checkAndNotifyOverloads() { if (!_communicationManager) { return; // No communication manager available } // Collect overloaded bells from telemetry std::vector criticalBells; std::vector criticalLoads; std::vector warningBells; std::vector warningLoads; bool hasOverload = false; for (uint8_t i = 0; i < 16; i++) { uint16_t load = _telemetry.getBellLoad(i); if (load == 0) continue; // Skip inactive bells if (_telemetry.isOverloaded(i)) { criticalBells.push_back(i); criticalLoads.push_back(load); hasOverload = true; } else { // Check thresholds - get max load for this bell (assume 60 default) uint16_t criticalThreshold = 54; // 90% of 60 uint16_t warningThreshold = 36; // 60% of 60 if (load > criticalThreshold) { criticalBells.push_back(i); criticalLoads.push_back(load); } else if (load > warningThreshold) { warningBells.push_back(i); warningLoads.push_back(load); } } } // Send notifications if needed if (!criticalBells.empty()) { String severity = hasOverload ? "critical" : "warning"; _communicationManager->sendBellOverloadNotification(criticalBells, criticalLoads, severity); } else if (!warningBells.empty()) { _communicationManager->sendBellOverloadNotification(warningBells, warningLoads, "warning"); } }