Refactored logging system to require a TAG as first argument on all LOG_* macros, enabling per-subsystem log filtering and cleaner output. Each subsystem now defines its own TAG (e.g. "BellEngine", "Player"). Also overhauled Logging.hpp/cpp with improved level control and output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
447 lines
19 KiB
C++
447 lines
19 KiB
C++
/*
|
|
* ═══════════════════════════════════════════════════════════════════════════════════
|
|
* 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<uint16_t>& 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<BellEngine*>(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<std::pair<uint8_t, uint16_t>> bellDurations; // For batch firing
|
|
std::vector<uint8_t> 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<uint8_t>& 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<JsonArray>();
|
|
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<uint8_t> criticalBells;
|
|
std::vector<uint16_t> criticalLoads;
|
|
std::vector<uint8_t> warningBells;
|
|
std::vector<uint16_t> 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");
|
|
}
|
|
}
|