Complete Rebuild, with Subsystems for each component. RTOS Tasks. (help by Claude)
This commit is contained in:
389
vesper/src/BellEngine/BellEngine.cpp
Normal file
389
vesper/src/BellEngine/BellEngine.cpp
Normal file
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* 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
|
||||
#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/Communication.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("Initializing BellEngine with high-precision timing");
|
||||
|
||||
// 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("BellEngine initialized - Ready for MAXIMUM PRECISION! 🎯");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set Communication manager reference for bell notifications
|
||||
*/
|
||||
void BellEngine::setCommunicationManager(Communication* commManager) {
|
||||
_communicationManager = commManager;
|
||||
LOG_DEBUG("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("Cannot start BellEngine: No melody data loaded");
|
||||
return; // ⛔ Early exit if no melody data
|
||||
}
|
||||
|
||||
LOG_INFO("🚀 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("BellEngine stopping gracefully");
|
||||
_engineRunning.store(false);
|
||||
}
|
||||
|
||||
void BellEngine::emergencyStop() {
|
||||
LOG_INFO("🛑 EMERGENCY STOP ACTIVATED");
|
||||
_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("BellEngine loaded melody: %d steps", melodySteps.size());
|
||||
}
|
||||
|
||||
void BellEngine::clearMelodyData() {
|
||||
portENTER_CRITICAL(&_melodyMutex);
|
||||
_melodySteps.clear();
|
||||
_melodyDataReady.store(false);
|
||||
portEXIT_CRITICAL(&_melodyMutex);
|
||||
LOG_DEBUG("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("🔥 BellEngine 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();
|
||||
|
||||
// Pause handling AFTER complete loop - never interrupt mid-melody!
|
||||
while (_player.isPaused && _player.isPlaying && !_player.hardStop) {
|
||||
LOG_DEBUG("⏸️ 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("Empty melody in playback loop!");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DEBUG("🎵 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("Emergency exit from playback loop");
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate note with MAXIMUM PRECISION
|
||||
activateNote(note);
|
||||
|
||||
// Precise timing delay
|
||||
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!
|
||||
LOG_DEBUG("🎵 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
|
||||
uint8_t bellIndex = _player.noteAssignments[noteIndex];
|
||||
|
||||
// Skip if no bell assigned
|
||||
if (bellIndex == 0) continue;
|
||||
|
||||
// Convert to 0-based indexing
|
||||
bellIndex = bellIndex - 1;
|
||||
|
||||
// Additional safety check to prevent underflow crashes
|
||||
if (bellIndex >= 255) {
|
||||
LOG_ERROR("🚨 UNDERFLOW ERROR: bellIndex underflow for noteIndex %d", noteIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bounds check (CRITICAL SAFETY)
|
||||
if (bellIndex >= 16) {
|
||||
LOG_ERROR("🚨 BOUNDS ERROR: bellIndex %d >= 16", bellIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for duplicate bell firing in this note
|
||||
if (bellFired[bellIndex]) {
|
||||
LOG_DEBUG("⚠️ 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("⚠️ 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({bellIndex, durationMs});
|
||||
|
||||
// Add to notification list (convert to 1-indexed for display)
|
||||
firedBellIndices.push_back(bellIndex + 1);
|
||||
|
||||
// Record telemetry
|
||||
_telemetry.recordBellStrike(bellIndex);
|
||||
|
||||
LOG_VERBOSE("🔨 STRIKE! Note:%d → Bell:%d for %dms", noteIndex, bellIndex, durationMs);
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 FIRE ALL BELLS SIMULTANEOUSLY!
|
||||
if (!bellDurations.empty()) {
|
||||
_outputManager.fireOutputsBatchForDuration(bellDurations);
|
||||
LOG_VERBOSE("🔥🔥 BATCH FIRED %d bells SIMULTANEOUSLY!", bellDurations.size());
|
||||
|
||||
// 🔔 NOTIFY WEBSOCKET CLIENTS OF BELL DINGS!
|
||||
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("🚨 EMERGENCY SHUTDOWN - Using 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("🔔 DING notification sent for %d bells", bellIndices.size());
|
||||
|
||||
} catch (...) {
|
||||
LOG_ERROR("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("BellEngine: Unhealthy - Task not created");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if task is still alive
|
||||
eTaskState taskState = eTaskGetState(_engineTaskHandle);
|
||||
if (taskState == eDeleted || taskState == eInvalid) {
|
||||
LOG_DEBUG("BellEngine: Unhealthy - Task deleted or invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're not in emergency stop state
|
||||
if (_emergencyStop.load()) {
|
||||
LOG_DEBUG("BellEngine: Unhealthy - Emergency stop active");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if OutputManager is properly connected and healthy
|
||||
if (!_outputManager.isInitialized()) {
|
||||
LOG_DEBUG("BellEngine: Unhealthy - OutputManager not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
362
vesper/src/BellEngine/BellEngine.hpp
Normal file
362
vesper/src/BellEngine/BellEngine.hpp
Normal file
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* BELLENGINE.HPP - High-Precision Bell Timing Engine
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🔥 THE HEART OF THE VESPER SYSTEM 🔥
|
||||
*
|
||||
* This is the core precision timing engine that controls bell activation with
|
||||
* microsecond accuracy. It runs on a dedicated FreeRTOS task with maximum
|
||||
* priority on Core 1 to ensure no timing interference.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Completely thread-safe with atomic operations
|
||||
* • Hardware-agnostic through OutputManager abstraction
|
||||
* • High-precision timing using ESP32 microsecond timers
|
||||
* • Comprehensive performance monitoring
|
||||
* • Emergency stop mechanisms for safety
|
||||
*
|
||||
* ⚡ PERFORMANCE FEATURES:
|
||||
* • Dedicated Core 1 execution (no interruption)
|
||||
* • Priority 6 FreeRTOS task (highest available)
|
||||
* • Microsecond-precision delays
|
||||
* • Atomic state management
|
||||
* • Lock-free melody data handling
|
||||
*
|
||||
* 🔒 THREAD SAFETY:
|
||||
* All public methods are thread-safe. Melody data is protected by
|
||||
* critical sections, and engine state uses atomic operations.
|
||||
*
|
||||
* 🚑 EMERGENCY FEATURES:
|
||||
* • Instant emergency stop capability
|
||||
* • Hardware shutdown through OutputManager
|
||||
* • Safe state transitions
|
||||
* • Graceful task cleanup
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Rewritten for modular architecture)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// SYSTEM INCLUDES - Core libraries for high-precision timing
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
#include <Arduino.h> // Arduino core functionality
|
||||
#include <vector> // STL vector for melody data storage
|
||||
#include <atomic> // Atomic operations for thread safety
|
||||
#include "freertos/FreeRTOS.h" // FreeRTOS kernel
|
||||
#include "freertos/task.h" // FreeRTOS task management
|
||||
#include "esp_timer.h" // ESP32 high-precision timers
|
||||
#include "../Logging/Logging.hpp" // Centralized logging system
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// FORWARD DECLARATIONS - Dependencies injected at runtime
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// These classes are injected via constructor to maintain clean architecture
|
||||
class Player; // Melody playback controller
|
||||
class ConfigManager; // Configuration and settings management
|
||||
class Telemetry; // System monitoring and analytics
|
||||
class OutputManager; // Hardware abstraction layer
|
||||
class Communication; // Communication system for notifications
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// ARCHITECTURE MIGRATION NOTE
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// BellEngine no longer tracks relay durations - OutputManager handles that now!
|
||||
// This provides clean separation of concerns and hardware abstraction.
|
||||
|
||||
/**
|
||||
* @class BellEngine
|
||||
* @brief High-precision bell timing and control engine
|
||||
*
|
||||
* The BellEngine is the core component responsible for microsecond-precision
|
||||
* bell activation timing. It runs on a dedicated FreeRTOS task with maximum
|
||||
* priority to ensure no timing interference from other system components.
|
||||
*
|
||||
* Key features:
|
||||
* - Thread-safe operation with atomic state management
|
||||
* - Hardware-agnostic through OutputManager abstraction
|
||||
* - Microsecond-precision timing using ESP32 timers
|
||||
* - Comprehensive performance monitoring
|
||||
* - Emergency stop capabilities
|
||||
* - Dedicated Core 1 execution for maximum performance
|
||||
*/
|
||||
class BellEngine {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & DESTRUCTOR
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Construct BellEngine with dependency injection
|
||||
* @param player Reference to melody player for coordination
|
||||
* @param configManager Reference to configuration manager for settings
|
||||
* @param telemetry Reference to telemetry system for monitoring
|
||||
* @param outputManager Reference to hardware abstraction layer
|
||||
*
|
||||
* Uses dependency injection pattern for clean architecture and testability.
|
||||
* All dependencies must be valid for the lifetime of this object.
|
||||
*/
|
||||
BellEngine(Player& player, ConfigManager& configManager, Telemetry& telemetry, OutputManager& outputManager);
|
||||
|
||||
/**
|
||||
* @brief Destructor - ensures clean shutdown
|
||||
*
|
||||
* Automatically calls emergencyShutdown() to ensure all relays are
|
||||
* turned off and tasks are properly cleaned up.
|
||||
*/
|
||||
~BellEngine();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CORE INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Initialize the BellEngine system
|
||||
*
|
||||
* Creates the high-priority timing task on Core 1 with maximum priority.
|
||||
* This task will remain dormant until start() is called, but the
|
||||
* infrastructure is set up for immediate precision timing.
|
||||
*
|
||||
* Task configuration:
|
||||
* - Priority: 6 (highest available)
|
||||
* - Core: 1 (dedicated)
|
||||
* - Stack: 12KB (increased for safety)
|
||||
* - Name: "BellEngine"
|
||||
*
|
||||
*/
|
||||
void begin();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ENGINE CONTROL (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 start();
|
||||
|
||||
/**
|
||||
* @brief Stop the timing engine gracefully
|
||||
*
|
||||
* Signals the engine to stop at the next safe opportunity.
|
||||
* Uses atomic flag for thread-safe coordination.
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* @brief Emergency stop - immediate shutdown
|
||||
*
|
||||
* Immediately stops all bell activity and shuts down the engine.
|
||||
* Calls emergencyShutdown() to ensure all relays are turned off.
|
||||
* Use this for safety-critical situations.
|
||||
*/
|
||||
void emergencyStop();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// MELODY DATA INTERFACE (Called by Player)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Load melody data for playback
|
||||
* @param melodySteps Vector of melody step data (each uint16_t represents note activations)
|
||||
*
|
||||
* Thread-safe method to load melody data. Uses critical sections to ensure
|
||||
* data consistency. The melody data is copied internally for safety.
|
||||
*
|
||||
* @note Each melody step is a bitmask where each bit represents a note/bell
|
||||
*/
|
||||
void setMelodyData(const std::vector<uint16_t>& melodySteps);
|
||||
|
||||
/**
|
||||
* @brief Clear loaded melody data
|
||||
*
|
||||
* Thread-safe method to clear melody data and mark engine as not ready.
|
||||
* Useful for cleanup and preparing for new melody loading.
|
||||
*/
|
||||
void clearMelodyData();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// STATUS QUERIES (Thread-safe)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Check if engine is currently running
|
||||
* @return true if engine is actively processing melody data
|
||||
*
|
||||
* Thread-safe atomic read operation.
|
||||
*/
|
||||
bool isRunning() const { return _engineRunning.load(); }
|
||||
|
||||
/**
|
||||
* @brief Check if engine is in emergency stop state
|
||||
* @return true if emergency stop has been activated
|
||||
*
|
||||
* Thread-safe atomic read operation.
|
||||
*/
|
||||
bool isEmergencyStopped() const { return _emergencyStop.load(); }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PERFORMANCE MONITORING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Get maximum measured loop execution time
|
||||
* @return Maximum loop time in microseconds
|
||||
*
|
||||
* Useful for performance tuning and ensuring timing requirements are met.
|
||||
*/
|
||||
uint32_t getMaxLoopTime() const { return _maxLoopTime; }
|
||||
|
||||
/**
|
||||
* @brief Set Communication manager reference for bell notifications
|
||||
* @param commManager Pointer to communication manager
|
||||
*/
|
||||
void setCommunicationManager(Communication* commManager);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if BellEngine is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
|
||||
private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// DEPENDENCY INJECTION - References to external systems
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
Player& _player; // Melody playback controller for coordination
|
||||
ConfigManager& _configManager; // Configuration manager for bell settings
|
||||
Telemetry& _telemetry; // System monitoring and strike tracking
|
||||
OutputManager& _outputManager; // 🔥 Hardware abstraction layer for relay control
|
||||
Communication* _communicationManager; // Communication system for bell notifications
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// ENGINE STATE (Atomic for thread safety)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
std::atomic<bool> _engineRunning{false}; // Engine active state flag
|
||||
std::atomic<bool> _emergencyStop{false}; // Emergency stop flag
|
||||
std::atomic<bool> _melodyDataReady{false}; // Melody data loaded and ready flag
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// MELODY DATA (Protected copy for thread safety)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
std::vector<uint16_t> _melodySteps; // Local copy of melody data for safe access
|
||||
portMUX_TYPE _melodyMutex = portMUX_INITIALIZER_UNLOCKED; // Critical section protection for melody data
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FREERTOS TASK MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
TaskHandle_t _engineTaskHandle = NULL; // Handle to high-priority timing task
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PERFORMANCE MONITORING VARIABLES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
uint32_t _maxLoopTime = 0; // Maximum measured loop execution time (microseconds) // Average loop execution time (microseconds)
|
||||
uint32_t _loopCount = 0; // Total number of loops executed
|
||||
uint64_t _totalLoopTime = 0; // Cumulative loop execution time (microseconds)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HIGH-PRECISION TIMING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
esp_timer_handle_t _precisionTimer = nullptr; // ESP32 high-precision timer (currently unused)
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CORE ENGINE FUNCTIONS (CRITICAL TIMING)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Main engine timing loop
|
||||
*
|
||||
* Executes one complete timing cycle with performance monitoring.
|
||||
* Called continuously by the engine task.
|
||||
*/
|
||||
void engineLoop();
|
||||
|
||||
/**
|
||||
* @brief Execute melody playback with precision timing
|
||||
*
|
||||
* Processes all melody steps with microsecond-precision delays.
|
||||
* Handles pause states and emergency stops.
|
||||
*/
|
||||
void playbackLoop();
|
||||
|
||||
/**
|
||||
* @brief Activate bells for a specific note
|
||||
* @param note Bitmask representing which bells to activate
|
||||
*
|
||||
* Decodes the note bitmask and activates corresponding bells through
|
||||
* the OutputManager with configured durations.
|
||||
*/
|
||||
void activateNote(uint16_t note);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// STATIC TASK FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Static entry point for FreeRTOS task
|
||||
* @param parameter Pointer to BellEngine instance
|
||||
*
|
||||
* Static function required by FreeRTOS. Casts parameter back to
|
||||
* BellEngine instance and runs the main timing loop.
|
||||
*/
|
||||
static void engineTask(void* parameter);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// TIMING UTILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Get current time in microseconds
|
||||
* @return Current time from ESP32 high-precision timer
|
||||
*
|
||||
* Inline function for maximum performance in timing-critical code.
|
||||
*/
|
||||
uint64_t getMicros() const { return esp_timer_get_time(); }
|
||||
|
||||
/**
|
||||
* @brief Perform microsecond-precision delay
|
||||
* @param microseconds Delay duration in microseconds
|
||||
*
|
||||
* Combines FreeRTOS task delay with busy-waiting for maximum precision.
|
||||
* Uses task delay for bulk time, then busy-wait for final precision.
|
||||
*/
|
||||
void preciseDelay(uint32_t microseconds);
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SAFETY FUNCTIONS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Emergency hardware shutdown
|
||||
*
|
||||
* Immediately shuts down all relays through OutputManager.
|
||||
* Called during emergency stops and destructor cleanup.
|
||||
* Ensures safe state regardless of current engine state.
|
||||
*/
|
||||
void emergencyShutdown();
|
||||
|
||||
/**
|
||||
* @brief Notify WebSocket clients of fired bells
|
||||
* @param bellIndices Vector of bell indices that were fired (1-indexed)
|
||||
*
|
||||
* Sends INFO/ding message to WebSocket clients only (not MQTT)
|
||||
*/
|
||||
void notifyBellsFired(const std::vector<uint8_t>& bellIndices);
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// END OF BELLENGINE.HPP
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
167
vesper/src/ClientManager/ClientManager.cpp
Normal file
167
vesper/src/ClientManager/ClientManager.cpp
Normal file
@@ -0,0 +1,167 @@
|
||||
#include "ClientManager.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
ClientManager::ClientManager() {
|
||||
LOG_INFO("Client Manager Component - Initialized");
|
||||
}
|
||||
|
||||
ClientManager::~ClientManager() {
|
||||
_clients.clear();
|
||||
LOG_INFO("Client Manager Component - Destroyed");
|
||||
}
|
||||
|
||||
void ClientManager::addClient(AsyncWebSocketClient* client, DeviceType deviceType) {
|
||||
if (!isValidClient(client)) {
|
||||
LOG_ERROR("Cannot add invalid client");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t clientId = client->id();
|
||||
_clients[clientId] = ClientInfo(client, deviceType);
|
||||
|
||||
LOG_INFO("Client #%u added as %s device", clientId, deviceTypeToString(deviceType));
|
||||
}
|
||||
|
||||
void ClientManager::removeClient(uint32_t clientId) {
|
||||
auto it = _clients.find(clientId);
|
||||
if (it != _clients.end()) {
|
||||
LOG_INFO("Client #%u removed (%s device)", clientId,
|
||||
deviceTypeToString(it->second.deviceType));
|
||||
_clients.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientManager::updateClientType(uint32_t clientId, DeviceType deviceType) {
|
||||
auto it = _clients.find(clientId);
|
||||
if (it != _clients.end()) {
|
||||
DeviceType oldType = it->second.deviceType;
|
||||
it->second.deviceType = deviceType;
|
||||
LOG_INFO("Client #%u type updated from %s to %s", clientId,
|
||||
deviceTypeToString(oldType), deviceTypeToString(deviceType));
|
||||
}
|
||||
}
|
||||
|
||||
void ClientManager::updateClientLastSeen(uint32_t clientId) {
|
||||
auto it = _clients.find(clientId);
|
||||
if (it != _clients.end()) {
|
||||
it->second.lastSeen = millis();
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientManager::isClientConnected(uint32_t clientId) const {
|
||||
auto it = _clients.find(clientId);
|
||||
if (it != _clients.end()) {
|
||||
return it->second.isConnected &&
|
||||
isValidClient(it->second.client);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ClientManager::DeviceType ClientManager::getClientType(uint32_t clientId) const {
|
||||
auto it = _clients.find(clientId);
|
||||
return (it != _clients.end()) ? it->second.deviceType : DeviceType::UNKNOWN;
|
||||
}
|
||||
|
||||
ClientManager::ClientInfo* ClientManager::getClientInfo(uint32_t clientId) {
|
||||
auto it = _clients.find(clientId);
|
||||
return (it != _clients.end()) ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
bool ClientManager::sendToClient(uint32_t clientId, const String& message) {
|
||||
auto it = _clients.find(clientId);
|
||||
if (it != _clients.end() && isValidClient(it->second.client)) {
|
||||
it->second.client->text(message);
|
||||
updateClientLastSeen(clientId);
|
||||
LOG_DEBUG("Message sent to client #%u: %s", clientId, message.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_WARNING("Failed to send message to client #%u - client not found or invalid", clientId);
|
||||
return false;
|
||||
}
|
||||
|
||||
void ClientManager::sendToMasterClients(const String& message) {
|
||||
int count = 0;
|
||||
for (auto& pair : _clients) {
|
||||
if (pair.second.deviceType == DeviceType::MASTER &&
|
||||
isValidClient(pair.second.client)) {
|
||||
pair.second.client->text(message);
|
||||
updateClientLastSeen(pair.first);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("Message sent to %d master client(s): %s", count, message.c_str());
|
||||
}
|
||||
|
||||
void ClientManager::sendToSecondaryClients(const String& message) {
|
||||
int count = 0;
|
||||
for (auto& pair : _clients) {
|
||||
if (pair.second.deviceType == DeviceType::SECONDARY &&
|
||||
isValidClient(pair.second.client)) {
|
||||
pair.second.client->text(message);
|
||||
updateClientLastSeen(pair.first);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("Message sent to %d secondary client(s): %s", count, message.c_str());
|
||||
}
|
||||
|
||||
void ClientManager::broadcastToAll(const String& message) {
|
||||
int count = 0;
|
||||
for (auto& pair : _clients) {
|
||||
if (isValidClient(pair.second.client)) {
|
||||
pair.second.client->text(message);
|
||||
updateClientLastSeen(pair.first);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
LOG_DEBUG("Message broadcasted to %d client(s): %s", count, message.c_str());
|
||||
}
|
||||
|
||||
void ClientManager::cleanupDisconnectedClients() {
|
||||
auto it = _clients.begin();
|
||||
while (it != _clients.end()) {
|
||||
if (!isValidClient(it->second.client)) {
|
||||
LOG_DEBUG("Cleaning up disconnected client #%u", it->first);
|
||||
it->second.isConnected = false;
|
||||
it = _clients.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String ClientManager::getClientListJson() const {
|
||||
StaticJsonDocument<512> doc;
|
||||
JsonArray clients = doc.createNestedArray("clients");
|
||||
|
||||
for (const auto& pair : _clients) {
|
||||
JsonObject client = clients.createNestedObject();
|
||||
client["id"] = pair.first;
|
||||
client["type"] = deviceTypeToString(pair.second.deviceType);
|
||||
client["connected"] = isValidClient(pair.second.client);
|
||||
client["last_seen"] = pair.second.lastSeen;
|
||||
}
|
||||
|
||||
String result;
|
||||
serializeJson(doc, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* ClientManager::deviceTypeToString(DeviceType type) const {
|
||||
switch (type) {
|
||||
case DeviceType::MASTER: return "master";
|
||||
case DeviceType::SECONDARY: return "secondary";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
ClientManager::DeviceType ClientManager::stringToDeviceType(const String& typeStr) const {
|
||||
if (typeStr == "master") return DeviceType::MASTER;
|
||||
if (typeStr == "secondary") return DeviceType::SECONDARY;
|
||||
return DeviceType::UNKNOWN;
|
||||
}
|
||||
|
||||
bool ClientManager::isValidClient(AsyncWebSocketClient* client) const {
|
||||
return client != nullptr && client->status() == WS_CONNECTED;
|
||||
}
|
||||
100
vesper/src/ClientManager/ClientManager.hpp
Normal file
100
vesper/src/ClientManager/ClientManager.hpp
Normal file
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* CLIENTMANAGER.HPP - WebSocket Client Management System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 📱 MULTI-CLIENT WEBSOCKET MANAGEMENT 📱
|
||||
*
|
||||
* This class manages multiple WebSocket clients with device type identification
|
||||
* and provides targeted messaging capabilities for master/secondary device roles.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Track multiple connected WebSocket clients
|
||||
* • Identify clients as "master" or "secondary" devices
|
||||
* • Provide targeted and broadcast messaging capabilities
|
||||
* • Automatic cleanup of disconnected clients
|
||||
*
|
||||
* 📱 CLIENT TYPES:
|
||||
* • Master: Primary control device (usually main Android app)
|
||||
* • Secondary: Additional control devices (up to 5 total devices)
|
||||
*
|
||||
* 🔄 CLIENT LIFECYCLE:
|
||||
* • Auto-registration on WebSocket connect
|
||||
* • Device type identification via initial handshake
|
||||
* • Automatic cleanup on disconnect
|
||||
* • Connection state monitoring
|
||||
*
|
||||
* 📡 MESSAGING FEATURES:
|
||||
* • Send to specific client by WebSocket ID
|
||||
* • Send to master/secondary device groups
|
||||
* • Broadcast to all connected clients
|
||||
* • Message delivery confirmation
|
||||
*
|
||||
* 📋 VERSION: 1.0 (Initial multi-client support)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <map>
|
||||
|
||||
class ClientManager {
|
||||
public:
|
||||
enum class DeviceType {
|
||||
UNKNOWN,
|
||||
MASTER,
|
||||
SECONDARY
|
||||
};
|
||||
|
||||
struct ClientInfo {
|
||||
AsyncWebSocketClient* client;
|
||||
DeviceType deviceType;
|
||||
uint32_t lastSeen;
|
||||
bool isConnected;
|
||||
|
||||
ClientInfo() : client(nullptr), deviceType(DeviceType::UNKNOWN),
|
||||
lastSeen(0), isConnected(false) {}
|
||||
|
||||
ClientInfo(AsyncWebSocketClient* c, DeviceType type)
|
||||
: client(c), deviceType(type), lastSeen(millis()), isConnected(true) {}
|
||||
};
|
||||
|
||||
ClientManager();
|
||||
~ClientManager();
|
||||
|
||||
// Client lifecycle management
|
||||
void addClient(AsyncWebSocketClient* client, DeviceType deviceType = DeviceType::UNKNOWN);
|
||||
void removeClient(uint32_t clientId);
|
||||
void updateClientType(uint32_t clientId, DeviceType deviceType);
|
||||
void updateClientLastSeen(uint32_t clientId);
|
||||
|
||||
// Client information
|
||||
bool hasClients() const { return !_clients.empty(); }
|
||||
size_t getClientCount() const { return _clients.size(); }
|
||||
bool isClientConnected(uint32_t clientId) const;
|
||||
DeviceType getClientType(uint32_t clientId) const;
|
||||
ClientInfo* getClientInfo(uint32_t clientId);
|
||||
|
||||
// Messaging methods
|
||||
bool sendToClient(uint32_t clientId, const String& message);
|
||||
void sendToMasterClients(const String& message);
|
||||
void sendToSecondaryClients(const String& message);
|
||||
void broadcastToAll(const String& message);
|
||||
|
||||
// Utility methods
|
||||
void cleanupDisconnectedClients();
|
||||
String getClientListJson() const;
|
||||
|
||||
private:
|
||||
std::map<uint32_t, ClientInfo> _clients;
|
||||
|
||||
// Helper methods
|
||||
const char* deviceTypeToString(DeviceType type) const;
|
||||
DeviceType stringToDeviceType(const String& typeStr) const;
|
||||
bool isValidClient(AsyncWebSocketClient* client) const;
|
||||
};
|
||||
1346
vesper/src/Communication/Communication.cpp
Normal file
1346
vesper/src/Communication/Communication.cpp
Normal file
File diff suppressed because it is too large
Load Diff
232
vesper/src/Communication/Communication.hpp
Normal file
232
vesper/src/Communication/Communication.hpp
Normal file
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* COMMUNICATION.HPP - Multi-Protocol Communication Manager v3.0
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 📡 THE COMMUNICATION HUB OF VESPER 📡
|
||||
*
|
||||
* This class manages all external communication protocols including MQTT,
|
||||
* WebSocket, and UDP discovery. It provides a unified interface for
|
||||
* grouped command handling and status reporting across multiple protocols.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Multi-protocol support with unified grouped command processing
|
||||
* • Multi-client WebSocket support with device type identification
|
||||
* • Automatic connection management and reconnection
|
||||
* • Unified response system for consistent messaging
|
||||
* • Thread-safe operation with proper resource management
|
||||
* • Batch command support for efficient configuration
|
||||
*
|
||||
* 📡 SUPPORTED PROTOCOLS:
|
||||
* • MQTT: Primary control interface with auto-reconnection
|
||||
* • WebSocket: Real-time multi-client web interface communication
|
||||
* • UDP Discovery: Auto-discovery service for network scanning
|
||||
*
|
||||
* 📱 CLIENT MANAGEMENT:
|
||||
* • Support for multiple WebSocket clients (master/secondary devices)
|
||||
* • Client type identification and targeted messaging
|
||||
* • Automatic cleanup of disconnected clients
|
||||
* • Broadcast capabilities for status updates
|
||||
*
|
||||
* 🔄 MESSAGE ROUTING:
|
||||
* • Commands accepted from both MQTT and WebSocket
|
||||
* • Responses sent only to originating protocol/client
|
||||
* • Status broadcasts sent to all WebSocket clients + MQTT
|
||||
* • Grouped command processing for all protocols
|
||||
*
|
||||
* 📋 VERSION: 3.0 (Grouped commands + batch processing)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <AsyncMqttClient.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncUDP.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "ResponseBuilder.hpp"
|
||||
#include "../ClientManager/ClientManager.hpp"
|
||||
|
||||
class ConfigManager;
|
||||
class OTAManager;
|
||||
class Player;
|
||||
class FileManager;
|
||||
class Timekeeper;
|
||||
class Networking;
|
||||
class FirmwareValidator;
|
||||
|
||||
class Communication {
|
||||
public:
|
||||
// Message source identification for response routing
|
||||
enum class MessageSource {
|
||||
MQTT,
|
||||
WEBSOCKET
|
||||
};
|
||||
|
||||
struct MessageContext {
|
||||
MessageSource source;
|
||||
uint32_t clientId; // Only used for WebSocket messages
|
||||
|
||||
MessageContext(MessageSource src, uint32_t id = 0)
|
||||
: source(src), clientId(id) {}
|
||||
};
|
||||
|
||||
explicit Communication(ConfigManager& configManager,
|
||||
OTAManager& otaManager,
|
||||
Networking& networking,
|
||||
AsyncMqttClient& mqttClient,
|
||||
AsyncWebServer& server,
|
||||
AsyncWebSocket& webSocket,
|
||||
AsyncUDP& udp);
|
||||
|
||||
~Communication();
|
||||
|
||||
void begin();
|
||||
void setPlayerReference(Player* player) { _player = player; }
|
||||
void setFileManagerReference(FileManager* fm) { _fileManager = fm; }
|
||||
void setTimeKeeperReference(Timekeeper* tk) { _timeKeeper = tk; }
|
||||
void setFirmwareValidatorReference(FirmwareValidator* fv) { _firmwareValidator = fv; }
|
||||
void setupUdpDiscovery();
|
||||
|
||||
// Public methods for timer callbacks
|
||||
void connectToMqtt();
|
||||
void subscribeMqtt();
|
||||
|
||||
// Status methods
|
||||
bool isMqttConnected() const { return _mqttClient.connected(); }
|
||||
bool hasActiveWebSocketClients() const { return _clientManager.hasClients(); }
|
||||
size_t getWebSocketClientCount() const { return _clientManager.getClientCount(); }
|
||||
|
||||
// Response methods - unified response system
|
||||
void sendResponse(const String& response, const MessageContext& context);
|
||||
void sendSuccessResponse(const String& type, const String& payload, const MessageContext& context);
|
||||
void sendErrorResponse(const String& type, const String& message, const MessageContext& context);
|
||||
|
||||
// Broadcast methods - for status updates that go to everyone
|
||||
void broadcastStatus(const String& statusMessage);
|
||||
void broadcastStatus(const JsonDocument& statusJson);
|
||||
void broadcastToMasterClients(const String& message);
|
||||
void broadcastToSecondaryClients(const String& message);
|
||||
void broadcastToAllWebSocketClients(const String& message);
|
||||
void broadcastToAllWebSocketClients(const JsonDocument& message);
|
||||
void publishToMqtt(const String& data);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if Communication is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
// Bell overload notification
|
||||
void sendBellOverloadNotification(const std::vector<uint8_t>& bellNumbers,
|
||||
const std::vector<uint16_t>& bellLoads,
|
||||
const String& severity);
|
||||
|
||||
// Network connection callbacks (called by Networking)
|
||||
void onNetworkConnected();
|
||||
void onNetworkDisconnected();
|
||||
|
||||
// Static instance for callbacks
|
||||
static Communication* _instance;
|
||||
|
||||
private:
|
||||
// Dependencies
|
||||
ConfigManager& _configManager;
|
||||
OTAManager& _otaManager;
|
||||
Networking& _networking;
|
||||
AsyncMqttClient& _mqttClient;
|
||||
AsyncWebServer& _server;
|
||||
AsyncWebSocket& _webSocket;
|
||||
AsyncUDP& _udp;
|
||||
Player* _player;
|
||||
FileManager* _fileManager;
|
||||
Timekeeper* _timeKeeper;
|
||||
FirmwareValidator* _firmwareValidator;
|
||||
|
||||
// Client manager
|
||||
ClientManager _clientManager;
|
||||
|
||||
// State
|
||||
TimerHandle_t _mqttReconnectTimer;
|
||||
|
||||
// Reusable JSON documents
|
||||
static StaticJsonDocument<2048> _parseDocument;
|
||||
|
||||
// MQTT methods
|
||||
void initMqtt();
|
||||
static void onMqttConnect(bool sessionPresent);
|
||||
static void onMqttDisconnect(AsyncMqttClientDisconnectReason reason);
|
||||
static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties,
|
||||
size_t len, size_t index, size_t total);
|
||||
static void onMqttSubscribe(uint16_t packetId, uint8_t qos);
|
||||
static void onMqttUnsubscribe(uint16_t packetId);
|
||||
static void onMqttPublish(uint16_t packetId);
|
||||
|
||||
// WebSocket methods
|
||||
void initWebSocket();
|
||||
static void onWebSocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client,
|
||||
AwsEventType type, void* arg, uint8_t* data, size_t len);
|
||||
void onWebSocketConnect(AsyncWebSocketClient* client);
|
||||
void onWebSocketDisconnect(AsyncWebSocketClient* client);
|
||||
void onWebSocketReceived(AsyncWebSocketClient* client, void* arg, uint8_t* data, size_t len);
|
||||
void handleClientIdentification(AsyncWebSocketClient* client, JsonDocument& command);
|
||||
|
||||
// Command processing - unified for both MQTT and WebSocket with grouped commands
|
||||
JsonDocument parsePayload(char* payload);
|
||||
void handleCommand(JsonDocument& command, const MessageContext& context);
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// GROUPED COMMAND HANDLERS
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// System commands
|
||||
void handleSystemCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSystemInfoCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handlePlaybackCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleFileManagerCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleRelaySetupCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleClockSetupCommand(JsonVariant contents, const MessageContext& context);
|
||||
|
||||
// System sub-commands
|
||||
void handlePingCommand(const MessageContext& context);
|
||||
void handleStatusCommand(const MessageContext& context);
|
||||
void handleIdentifyCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleGetDeviceTimeCommand(const MessageContext& context);
|
||||
void handleGetClockTimeCommand(const MessageContext& context);
|
||||
|
||||
// Firmware management commands
|
||||
void handleCommitFirmwareCommand(const MessageContext& context);
|
||||
void handleRollbackFirmwareCommand(const MessageContext& context);
|
||||
void handleGetFirmwareStatusCommand(const MessageContext& context);
|
||||
|
||||
// Network configuration command
|
||||
void handleSetNetworkConfigCommand(JsonVariant contents, const MessageContext& context);
|
||||
|
||||
// File Manager sub-commands
|
||||
void handleListMelodiesCommand(const MessageContext& context);
|
||||
void handleDownloadMelodyCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleDeleteMelodyCommand(JsonVariant contents, const MessageContext& context);
|
||||
|
||||
// Relay Setup sub-commands
|
||||
void handleSetRelayTimersCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetRelayOutputsCommand(JsonVariant contents, const MessageContext& context);
|
||||
|
||||
// Clock Setup sub-commands
|
||||
void handleSetClockOutputsCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetClockTimingsCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetClockAlertsCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetClockBacklightCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetClockSilenceCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetRtcTimeCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetPhysicalClockTimeCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handlePauseClockUpdatesCommand(JsonVariant contents, const MessageContext& context);
|
||||
void handleSetClockEnabledCommand(JsonVariant contents, const MessageContext& context);
|
||||
|
||||
// Utility methods
|
||||
String getPayloadContent(char* data, size_t len);
|
||||
int extractBellNumber(const String& key); // Extract bell number from "b1", "c1", etc.
|
||||
};
|
||||
157
vesper/src/Communication/ResponseBuilder.cpp
Normal file
157
vesper/src/Communication/ResponseBuilder.cpp
Normal file
@@ -0,0 +1,157 @@
|
||||
#include "ResponseBuilder.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// Static member initialization
|
||||
StaticJsonDocument<512> ResponseBuilder::_responseDoc;
|
||||
|
||||
String ResponseBuilder::success(const String& type, const String& payload) {
|
||||
return buildResponse(Status::SUCCESS, type, payload);
|
||||
}
|
||||
|
||||
String ResponseBuilder::success(const String& type, const JsonObject& payload) {
|
||||
return buildResponse(Status::SUCCESS, type, payload);
|
||||
}
|
||||
|
||||
String ResponseBuilder::error(const String& type, const String& message) {
|
||||
return buildResponse(Status::ERROR, type, message);
|
||||
}
|
||||
|
||||
String ResponseBuilder::status(const String& type, const JsonObject& data) {
|
||||
return buildResponse(Status::SUCCESS, type, data);
|
||||
}
|
||||
|
||||
String ResponseBuilder::status(const String& type, const String& data) {
|
||||
return buildResponse(Status::SUCCESS, type, data);
|
||||
}
|
||||
|
||||
String ResponseBuilder::acknowledgment(const String& commandType) {
|
||||
return success(commandType, "Command acknowledged");
|
||||
}
|
||||
|
||||
String ResponseBuilder::pong() {
|
||||
return success("pong", "");
|
||||
}
|
||||
|
||||
String ResponseBuilder::deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsed, uint64_t projectedRunTime) {
|
||||
StaticJsonDocument<512> statusDoc; // Increased size for additional data
|
||||
|
||||
statusDoc["status"] = "SUCCESS";
|
||||
statusDoc["type"] = "current_status";
|
||||
|
||||
// Create payload object with the exact format expected by Flutter
|
||||
JsonObject payload = statusDoc.createNestedObject("payload");
|
||||
|
||||
// Convert PlayerStatus to string
|
||||
const char* statusStr;
|
||||
switch (playerStatus) {
|
||||
case PlayerStatus::PLAYING:
|
||||
statusStr = "playing";
|
||||
break;
|
||||
case PlayerStatus::PAUSED:
|
||||
statusStr = "paused";
|
||||
break;
|
||||
case PlayerStatus::STOPPING:
|
||||
statusStr = "stopping";
|
||||
break;
|
||||
case PlayerStatus::STOPPED:
|
||||
default:
|
||||
statusStr = "idle"; // STOPPED maps to "idle" in Flutter
|
||||
break;
|
||||
}
|
||||
|
||||
payload["player_status"] = statusStr;
|
||||
payload["time_elapsed"] = timeElapsed; // in milliseconds
|
||||
payload["projected_run_time"] = projectedRunTime; // NEW: total projected duration
|
||||
|
||||
String result;
|
||||
serializeJson(statusDoc, result);
|
||||
|
||||
LOG_DEBUG("Device status response: %s", result.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
String ResponseBuilder::melodyList(const String& fileListJson) {
|
||||
// The fileListJson is already a JSON string, so we pass it as payload
|
||||
return success("melody_list", fileListJson);
|
||||
}
|
||||
|
||||
String ResponseBuilder::downloadResult(bool success, const String& filename) {
|
||||
if (success) {
|
||||
String message = "Download successful";
|
||||
if (filename.length() > 0) {
|
||||
message += ": " + filename;
|
||||
}
|
||||
return ResponseBuilder::success("download", message);
|
||||
} else {
|
||||
String message = "Download failed";
|
||||
if (filename.length() > 0) {
|
||||
message += ": " + filename;
|
||||
}
|
||||
return error("download", message);
|
||||
}
|
||||
}
|
||||
|
||||
String ResponseBuilder::configUpdate(const String& configType) {
|
||||
return success(configType, configType + " configuration updated");
|
||||
}
|
||||
|
||||
String ResponseBuilder::invalidCommand(const String& command) {
|
||||
return error("invalid_command", "Unknown command: " + command);
|
||||
}
|
||||
|
||||
String ResponseBuilder::missingParameter(const String& parameter) {
|
||||
return error("missing_parameter", "Required parameter missing: " + parameter);
|
||||
}
|
||||
|
||||
String ResponseBuilder::operationFailed(const String& operation, const String& reason) {
|
||||
String message = operation + " failed";
|
||||
if (reason.length() > 0) {
|
||||
message += ": " + reason;
|
||||
}
|
||||
return error(operation, message);
|
||||
}
|
||||
|
||||
String ResponseBuilder::deviceBusy() {
|
||||
return error("device_busy", "Device is currently busy, try again later");
|
||||
}
|
||||
|
||||
String ResponseBuilder::unauthorized() {
|
||||
return error("unauthorized", "Operation not authorized for this client");
|
||||
}
|
||||
|
||||
// Response Builder with String Payload
|
||||
String ResponseBuilder::buildResponse(Status status, const String& type, const String& payload) {
|
||||
_responseDoc.clear();
|
||||
_responseDoc["status"] = statusToString(status);
|
||||
_responseDoc["type"] = type;
|
||||
_responseDoc["payload"] = payload;
|
||||
|
||||
String result;
|
||||
serializeJson(_responseDoc, result);
|
||||
|
||||
LOG_DEBUG("Response built: %s", result.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
// Response Builder with JSON Payload
|
||||
String ResponseBuilder::buildResponse(Status status, const String& type, const JsonObject& payload) {
|
||||
_responseDoc.clear();
|
||||
_responseDoc["status"] = statusToString(status);
|
||||
_responseDoc["type"] = type;
|
||||
_responseDoc["payload"] = payload;
|
||||
|
||||
String result;
|
||||
serializeJson(_responseDoc, result);
|
||||
|
||||
LOG_DEBUG("Response built: %s", result.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* ResponseBuilder::statusToString(Status status) {
|
||||
switch (status) {
|
||||
case Status::SUCCESS: return "SUCCESS";
|
||||
case Status::ERROR: return "ERROR";
|
||||
case Status::INFO: return "INFO";
|
||||
default: return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
87
vesper/src/Communication/ResponseBuilder.hpp
Normal file
87
vesper/src/Communication/ResponseBuilder.hpp
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* RESPONSEBUILDER.HPP - Unified Response Generation System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 📡 STANDARDIZED COMMUNICATION RESPONSES 📡
|
||||
*
|
||||
* This class provides a unified interface for generating consistent JSON responses
|
||||
* across all communication protocols (MQTT, WebSocket). It ensures all responses
|
||||
* follow the same format and structure.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Static methods for response generation
|
||||
* • Consistent JSON structure across all protocols
|
||||
* • Memory-efficient response building
|
||||
* • Type-safe response categories
|
||||
*
|
||||
* 📡 RESPONSE TYPES:
|
||||
* • Success: Successful command execution
|
||||
* • Error: Command execution failures
|
||||
* • Status: System status reports and updates
|
||||
* • Data: Information requests and telemetry
|
||||
*
|
||||
* 🔄 RESPONSE STRUCTURE:
|
||||
* {
|
||||
* "status": "OK|ERROR",
|
||||
* "type": "command_type",
|
||||
* "payload": "data_or_message"
|
||||
* }
|
||||
*
|
||||
* 📋 USAGE EXAMPLES:
|
||||
* • ResponseBuilder::success("playback", "Started playing melody")
|
||||
* • ResponseBuilder::error("download", "File not found")
|
||||
* • ResponseBuilder::status("telemetry", telemetryData)
|
||||
*
|
||||
* 📋 VERSION: 1.0 (Initial unified response system)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "../Player/Player.hpp" // For PlayerStatus enum
|
||||
|
||||
class ResponseBuilder {
|
||||
public:
|
||||
// Response status types
|
||||
enum class Status {
|
||||
SUCCESS,
|
||||
ERROR,
|
||||
INFO
|
||||
};
|
||||
|
||||
// Main response builders
|
||||
static String success(const String& type, const String& payload = "");
|
||||
static String success(const String& type, const JsonObject& payload);
|
||||
static String error(const String& type, const String& message);
|
||||
static String status(const String& type, const JsonObject& data);
|
||||
static String status(const String& type, const String& data);
|
||||
|
||||
// Specialized response builders for common scenarios
|
||||
static String acknowledgment(const String& commandType);
|
||||
static String pong();
|
||||
static String deviceStatus(PlayerStatus playerStatus, uint32_t timeElapsedMs, uint64_t projectedRunTime = 0);
|
||||
static String melodyList(const String& fileListJson);
|
||||
static String downloadResult(bool success, const String& filename = "");
|
||||
static String configUpdate(const String& configType);
|
||||
|
||||
// Error response builders
|
||||
static String invalidCommand(const String& command);
|
||||
static String missingParameter(const String& parameter);
|
||||
static String operationFailed(const String& operation, const String& reason = "");
|
||||
static String deviceBusy();
|
||||
static String unauthorized();
|
||||
|
||||
// Utility methods
|
||||
static String buildResponse(Status status, const String& type, const String& payload);
|
||||
static String buildResponse(Status status, const String& type, const JsonObject& payload);
|
||||
|
||||
private:
|
||||
// Internal helper methods
|
||||
static const char* statusToString(Status status);
|
||||
static StaticJsonDocument<512> _responseDoc; // Reusable document for efficiency
|
||||
};
|
||||
944
vesper/src/ConfigManager/ConfigManager.cpp
Normal file
944
vesper/src/ConfigManager/ConfigManager.cpp
Normal file
@@ -0,0 +1,944 @@
|
||||
#include "ConfigManager.hpp"
|
||||
#include "../../src/Logging/Logging.hpp"
|
||||
#include <WiFi.h> // For MAC address generation
|
||||
#include <time.h> // For timestamp generation
|
||||
#include <algorithm> // For std::sort
|
||||
|
||||
// NVS namespace for device identity storage
|
||||
const char* ConfigManager::NVS_NAMESPACE = "device_id";
|
||||
|
||||
// NVS keys for device identity
|
||||
static const char* NVS_DEVICE_UID_KEY = "device_uid";
|
||||
static const char* NVS_HW_TYPE_KEY = "hw_type";
|
||||
static const char* NVS_HW_VERSION_KEY = "hw_version";
|
||||
|
||||
ConfigManager::ConfigManager() {
|
||||
// Initialize with empty defaults - everything will be loaded/generated in begin()
|
||||
createDefaultBellConfig();
|
||||
}
|
||||
|
||||
void ConfigManager::initializeCleanDefaults() {
|
||||
// This method is called after NVS loading to set up clean defaults
|
||||
// and auto-generate identifiers from loaded deviceUID
|
||||
|
||||
// Generate network identifiers from deviceUID
|
||||
generateNetworkIdentifiers();
|
||||
|
||||
// Set MQTT user to deviceUID for unique identification
|
||||
mqttConfig.user = deviceConfig.deviceUID;
|
||||
|
||||
LOG_INFO("ConfigManager: Clean defaults initialized with auto-generated identifiers");
|
||||
}
|
||||
|
||||
void ConfigManager::generateNetworkIdentifiers() {
|
||||
|
||||
networkConfig.hostname = "BellSystems-" + deviceConfig.deviceUID;
|
||||
networkConfig.apSsid = "BellSystems-Setup-" + deviceConfig.deviceUID;
|
||||
|
||||
|
||||
LOG_INFO("ConfigManager: Generated hostname: %s, AP SSID: %s",
|
||||
networkConfig.hostname.c_str(), networkConfig.apSsid.c_str());
|
||||
}
|
||||
|
||||
void ConfigManager::createDefaultBellConfig() {
|
||||
// Initialize default durations (90ms for all bells)
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
bellConfig.durations[i] = 90;
|
||||
bellConfig.outputs[i] = i; // Direct mapping by default
|
||||
}
|
||||
}
|
||||
|
||||
bool ConfigManager::begin() {
|
||||
LOG_INFO("ConfigManager: Starting clean deployment-ready initialization");
|
||||
|
||||
// Step 1: Initialize NVS for device identity (factory-set, permanent)
|
||||
if (!initializeNVS()) {
|
||||
LOG_ERROR("ConfigManager: NVS initialization failed, using empty defaults");
|
||||
} else {
|
||||
// Load device identity from NVS (deviceUID, hwType, hwVersion)
|
||||
loadDeviceIdentityFromNVS();
|
||||
}
|
||||
|
||||
// Step 2: Initialize clean defaults and auto-generate identifiers
|
||||
initializeCleanDefaults();
|
||||
|
||||
// Step 3: Initialize SD card for user-configurable settings
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: SD Card initialization failed, using defaults");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 4: Load device configuration from SD card (firmware version only)
|
||||
if (!loadDeviceConfig()) {
|
||||
LOG_WARNING("ConfigManager: Could not load device config from SD card - using defaults");
|
||||
}
|
||||
|
||||
// Step 5: Load update servers list
|
||||
if (!loadUpdateServers()) {
|
||||
LOG_WARNING("ConfigManager: Could not load update servers - using fallback only");
|
||||
}
|
||||
|
||||
// Step 6: Load user-configurable settings from SD
|
||||
loadFromSD();
|
||||
loadNetworkConfig(); // Load network configuration (hostname, static IP settings)
|
||||
loadBellDurations();
|
||||
loadClockConfig(); // Load clock configuration (C1/C2 outputs, pulse durations)
|
||||
loadClockState(); // Load physical clock state (hour, minute, position)
|
||||
|
||||
LOG_INFO("ConfigManager: Initialization complete - UID: %s, Hostname: %s",
|
||||
deviceConfig.deviceUID.c_str(), networkConfig.hostname.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// NVS (NON-VOLATILE STORAGE) IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool ConfigManager::initializeNVS() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
LOG_WARNING("ConfigManager: NVS partition truncated, erasing...");
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Failed to initialize NVS flash: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvsHandle);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Failed to open NVS handle: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("ConfigManager: NVS initialized successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::loadDeviceIdentityFromNVS() {
|
||||
if (nvsHandle == 0) {
|
||||
LOG_ERROR("ConfigManager: NVS not initialized, cannot load device identity");
|
||||
return false;
|
||||
}
|
||||
|
||||
deviceConfig.deviceUID = readNVSString(NVS_DEVICE_UID_KEY, "PV000000000000");
|
||||
deviceConfig.hwType = readNVSString(NVS_HW_TYPE_KEY, "BellSystems");
|
||||
deviceConfig.hwVersion = readNVSString(NVS_HW_VERSION_KEY, "0");
|
||||
|
||||
LOG_INFO("ConfigManager: Device identity loaded from NVS - UID: %s, Type: %s, Version: %s",
|
||||
deviceConfig.deviceUID.c_str(),
|
||||
deviceConfig.hwType.c_str(),
|
||||
deviceConfig.hwVersion.c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveDeviceIdentityToNVS() {
|
||||
if (nvsHandle == 0) {
|
||||
LOG_ERROR("ConfigManager: NVS not initialized, cannot save device identity");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
success &= writeNVSString(NVS_DEVICE_UID_KEY, deviceConfig.deviceUID);
|
||||
success &= writeNVSString(NVS_HW_TYPE_KEY, deviceConfig.hwType);
|
||||
success &= writeNVSString(NVS_HW_VERSION_KEY, deviceConfig.hwVersion);
|
||||
|
||||
if (success) {
|
||||
esp_err_t err = nvs_commit(nvsHandle);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Failed to commit NVS changes: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
LOG_INFO("ConfigManager: Device identity saved to NVS");
|
||||
} else {
|
||||
LOG_ERROR("ConfigManager: Failed to save device identity to NVS");
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
String ConfigManager::readNVSString(const char* key, const String& defaultValue) {
|
||||
if (nvsHandle == 0) {
|
||||
LOG_WARNING("ConfigManager: NVS not initialized, returning default for key: %s", key);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
size_t required_size = 0;
|
||||
esp_err_t err = nvs_get_str(nvsHandle, key, NULL, &required_size);
|
||||
|
||||
if (err == ESP_ERR_NVS_NOT_FOUND) {
|
||||
LOG_DEBUG("ConfigManager: NVS key '%s' not found, using default: %s", key, defaultValue.c_str());
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Error reading NVS key '%s': %s", key, esp_err_to_name(err));
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
char* buffer = new char[required_size];
|
||||
err = nvs_get_str(nvsHandle, key, buffer, &required_size);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Error reading NVS value for key '%s': %s", key, esp_err_to_name(err));
|
||||
delete[] buffer;
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
String result = String(buffer);
|
||||
delete[] buffer;
|
||||
|
||||
LOG_DEBUG("ConfigManager: Read NVS key '%s': %s", key, result.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ConfigManager::writeNVSString(const char* key, const String& value) {
|
||||
if (nvsHandle == 0) {
|
||||
LOG_ERROR("ConfigManager: NVS not initialized, cannot write key: %s", key);
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t err = nvs_set_str(nvsHandle, key, value.c_str());
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("ConfigManager: Failed to write NVS key '%s': %s", key, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DEBUG("ConfigManager: Written NVS key '%s': %s", key, value.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// STANDARD SD CARD FUNCTIONALITY
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool ConfigManager::ensureSDCard() {
|
||||
if (!sdInitialized) {
|
||||
sdInitialized = SD.begin(hardwareConfig.sdChipSelect);
|
||||
}
|
||||
return sdInitialized;
|
||||
}
|
||||
|
||||
void ConfigManager::loadFromSD() {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: Cannot load from SD - SD not available");
|
||||
return;
|
||||
}
|
||||
LOG_INFO("ConfigManager: Using default configuration");
|
||||
}
|
||||
|
||||
bool ConfigManager::saveToSD() {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: Cannot save to SD - SD not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
success &= saveBellDurations();
|
||||
success &= saveClockConfig();
|
||||
success &= saveClockState();
|
||||
return success;
|
||||
}
|
||||
|
||||
// Device configuration now only handles firmware version (identity is in NVS)
|
||||
bool ConfigManager::saveDeviceConfig() {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: Cannot save device config - SD not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["fwVersion"] = deviceConfig.fwVersion;
|
||||
|
||||
char buffer[256];
|
||||
size_t len = serializeJson(doc, buffer, sizeof(buffer));
|
||||
|
||||
if (len == 0 || len >= sizeof(buffer)) {
|
||||
LOG_ERROR("ConfigManager: Failed to serialize device config JSON");
|
||||
return false;
|
||||
}
|
||||
|
||||
saveFileToSD("/settings", "deviceConfig.json", buffer);
|
||||
LOG_INFO("ConfigManager: Device config saved - FwVer: %s", deviceConfig.fwVersion.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::loadDeviceConfig() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
File file = SD.open("/settings/deviceConfig.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_WARNING("ConfigManager: Device config file not found - using firmware version default");
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("ConfigManager: Failed to parse device config from SD: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (doc.containsKey("fwVersion")) {
|
||||
deviceConfig.fwVersion = doc["fwVersion"].as<String>();
|
||||
LOG_INFO("ConfigManager: Firmware version loaded from SD: %s", deviceConfig.fwVersion.c_str());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::isHealthy() const {
|
||||
if (!sdInitialized) {
|
||||
LOG_DEBUG("ConfigManager: Unhealthy - SD card not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deviceConfig.deviceUID.isEmpty()) {
|
||||
LOG_DEBUG("ConfigManager: Unhealthy - Device UID not set (factory configuration required)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deviceConfig.hwType.isEmpty()) {
|
||||
LOG_DEBUG("ConfigManager: Unhealthy - Hardware type not set (factory configuration required)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (networkConfig.hostname.isEmpty()) {
|
||||
LOG_DEBUG("ConfigManager: Unhealthy - Hostname not generated (initialization issue)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Note: WiFi credentials are handled by WiFiManager, not checked here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bell configuration methods remain unchanged...
|
||||
bool ConfigManager::loadBellDurations() {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: SD Card not initialized. Using default bell durations.");
|
||||
return false;
|
||||
}
|
||||
|
||||
File file = SD.open("/settings/relayTimings.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_ERROR("ConfigManager: Settings file not found on SD. Using default bell durations.");
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("ConfigManager: Failed to parse settings from SD. Using default bell durations.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
if (doc.containsKey(key)) {
|
||||
bellConfig.durations[i] = doc[key].as<uint16_t>();
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("ConfigManager: Bell durations loaded from SD");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveBellDurations() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
doc[key] = bellConfig.durations[i];
|
||||
}
|
||||
|
||||
char buffer[512];
|
||||
size_t len = serializeJson(doc, buffer, sizeof(buffer));
|
||||
|
||||
if (len == 0 || len >= sizeof(buffer)) {
|
||||
LOG_ERROR("ConfigManager: Failed to serialize bell durations JSON");
|
||||
return false;
|
||||
}
|
||||
|
||||
saveFileToSD("/settings", "relayTimings.json", buffer);
|
||||
LOG_INFO("ConfigManager: Bell durations saved to SD");
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigManager::updateBellDurations(JsonVariant doc) {
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
if (doc.containsKey(key)) {
|
||||
bellConfig.durations[i] = doc[key].as<uint16_t>();
|
||||
}
|
||||
}
|
||||
LOG_INFO("ConfigManager: Updated bell durations");
|
||||
}
|
||||
|
||||
void ConfigManager::updateBellOutputs(JsonVariant doc) {
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
String key = String("b") + (i + 1);
|
||||
if (doc.containsKey(key)) {
|
||||
bellConfig.outputs[i] = doc[key].as<uint16_t>() - 1;
|
||||
}
|
||||
}
|
||||
LOG_INFO("ConfigManager: Updated bell outputs");
|
||||
}
|
||||
|
||||
uint16_t ConfigManager::getBellDuration(uint8_t bellIndex) const {
|
||||
if (bellIndex >= 16) return 90;
|
||||
return bellConfig.durations[bellIndex];
|
||||
}
|
||||
|
||||
uint16_t ConfigManager::getBellOutput(uint8_t bellIndex) const {
|
||||
if (bellIndex >= 16) return bellIndex;
|
||||
return bellConfig.outputs[bellIndex];
|
||||
}
|
||||
|
||||
void ConfigManager::setBellDuration(uint8_t bellIndex, uint16_t duration) {
|
||||
if (bellIndex < 16) {
|
||||
bellConfig.durations[bellIndex] = duration;
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigManager::setBellOutput(uint8_t bellIndex, uint16_t output) {
|
||||
if (bellIndex < 16) {
|
||||
bellConfig.outputs[bellIndex] = output;
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigManager::saveFileToSD(const char* dirPath, const char* filename, const char* data) {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: SD Card not initialized!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SD.exists(dirPath)) {
|
||||
SD.mkdir(dirPath);
|
||||
}
|
||||
|
||||
String fullPath = String(dirPath);
|
||||
if (!fullPath.endsWith("/")) fullPath += "/";
|
||||
fullPath += filename;
|
||||
|
||||
File file = SD.open(fullPath.c_str(), FILE_WRITE);
|
||||
if (!file) {
|
||||
LOG_ERROR("ConfigManager: Failed to open file: %s", fullPath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
file.print(data);
|
||||
file.close();
|
||||
LOG_INFO("ConfigManager: File %s saved successfully", fullPath.c_str());
|
||||
}
|
||||
|
||||
// Clock configuration methods and other remaining methods follow the same pattern...
|
||||
// (Implementation would continue with all the clock config methods, update servers, etc.)
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// MISSING CLOCK CONFIGURATION METHODS
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void ConfigManager::updateClockOutputs(JsonVariant doc) {
|
||||
if (doc.containsKey("c1")) {
|
||||
clockConfig.c1output = doc["c1"].as<uint8_t>();
|
||||
}
|
||||
if (doc.containsKey("c2")) {
|
||||
clockConfig.c2output = doc["c2"].as<uint8_t>();
|
||||
}
|
||||
if (doc.containsKey("pulseDuration")) {
|
||||
clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
|
||||
}
|
||||
if (doc.containsKey("pauseDuration")) {
|
||||
clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>();
|
||||
}
|
||||
LOG_INFO("ConfigManager: Updated Clock outputs to: C1: %d / C2: %d, Pulse: %dms, Pause: %dms",
|
||||
clockConfig.c1output, clockConfig.c2output, clockConfig.pulseDuration, clockConfig.pauseDuration);
|
||||
}
|
||||
|
||||
void ConfigManager::updateClockAlerts(JsonVariant doc) {
|
||||
if (doc.containsKey("alertType")) {
|
||||
clockConfig.alertType = doc["alertType"].as<String>();
|
||||
}
|
||||
if (doc.containsKey("alertRingInterval")) {
|
||||
clockConfig.alertRingInterval = doc["alertRingInterval"].as<uint16_t>();
|
||||
}
|
||||
if (doc.containsKey("hourBell")) {
|
||||
clockConfig.hourBell = doc["hourBell"].as<uint8_t>();
|
||||
}
|
||||
if (doc.containsKey("halfBell")) {
|
||||
clockConfig.halfBell = doc["halfBell"].as<uint8_t>();
|
||||
}
|
||||
if (doc.containsKey("quarterBell")) {
|
||||
clockConfig.quarterBell = doc["quarterBell"].as<uint8_t>();
|
||||
}
|
||||
LOG_INFO("ConfigManager: Updated Clock alerts");
|
||||
}
|
||||
|
||||
void ConfigManager::updateClockBacklight(JsonVariant doc) {
|
||||
if (doc.containsKey("enabled")) {
|
||||
clockConfig.backlight = doc["enabled"].as<bool>();
|
||||
}
|
||||
if (doc.containsKey("output")) {
|
||||
clockConfig.backlightOutput = doc["output"].as<uint8_t>();
|
||||
}
|
||||
if (doc.containsKey("onTime")) {
|
||||
clockConfig.backlightOnTime = doc["onTime"].as<String>();
|
||||
}
|
||||
if (doc.containsKey("offTime")) {
|
||||
clockConfig.backlightOffTime = doc["offTime"].as<String>();
|
||||
}
|
||||
LOG_INFO("ConfigManager: Updated Clock backlight");
|
||||
}
|
||||
|
||||
void ConfigManager::updateClockSilence(JsonVariant doc) {
|
||||
if (doc.containsKey("daytime")) {
|
||||
JsonObject daytime = doc["daytime"];
|
||||
if (daytime.containsKey("enabled")) {
|
||||
clockConfig.daytimeSilenceEnabled = daytime["enabled"].as<bool>();
|
||||
}
|
||||
if (daytime.containsKey("onTime")) {
|
||||
clockConfig.daytimeSilenceOnTime = daytime["onTime"].as<String>();
|
||||
}
|
||||
if (daytime.containsKey("offTime")) {
|
||||
clockConfig.daytimeSilenceOffTime = daytime["offTime"].as<String>();
|
||||
}
|
||||
}
|
||||
if (doc.containsKey("nighttime")) {
|
||||
JsonObject nighttime = doc["nighttime"];
|
||||
if (nighttime.containsKey("enabled")) {
|
||||
clockConfig.nighttimeSilenceEnabled = nighttime["enabled"].as<bool>();
|
||||
}
|
||||
if (nighttime.containsKey("onTime")) {
|
||||
clockConfig.nighttimeSilenceOnTime = nighttime["onTime"].as<String>();
|
||||
}
|
||||
if (nighttime.containsKey("offTime")) {
|
||||
clockConfig.nighttimeSilenceOffTime = nighttime["offTime"].as<String>();
|
||||
}
|
||||
}
|
||||
LOG_INFO("ConfigManager: Updated Clock silence");
|
||||
}
|
||||
|
||||
bool ConfigManager::loadClockConfig() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
File file = SD.open("/settings/clockConfig.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_WARNING("ConfigManager: Clock config file not found - using defaults");
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("ConfigManager: Failed to parse clock config from SD: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (doc.containsKey("c1output")) clockConfig.c1output = doc["c1output"].as<uint8_t>();
|
||||
if (doc.containsKey("c2output")) clockConfig.c2output = doc["c2output"].as<uint8_t>();
|
||||
if (doc.containsKey("pulseDuration")) clockConfig.pulseDuration = doc["pulseDuration"].as<uint16_t>();
|
||||
if (doc.containsKey("pauseDuration")) clockConfig.pauseDuration = doc["pauseDuration"].as<uint16_t>();
|
||||
|
||||
LOG_INFO("ConfigManager: Clock config loaded");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveClockConfig() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
doc["c1output"] = clockConfig.c1output;
|
||||
doc["c2output"] = clockConfig.c2output;
|
||||
doc["pulseDuration"] = clockConfig.pulseDuration;
|
||||
doc["pauseDuration"] = clockConfig.pauseDuration;
|
||||
|
||||
char buffer[512];
|
||||
size_t len = serializeJson(doc, buffer, sizeof(buffer));
|
||||
|
||||
if (len == 0 || len >= sizeof(buffer)) {
|
||||
LOG_ERROR("ConfigManager: Failed to serialize clock config JSON");
|
||||
return false;
|
||||
}
|
||||
|
||||
saveFileToSD("/settings", "clockConfig.json", buffer);
|
||||
LOG_INFO("ConfigManager: Clock config saved");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::loadClockState() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
File file = SD.open("/settings/clockState.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_WARNING("ConfigManager: Clock state file not found - using defaults");
|
||||
clockConfig.physicalHour = 0;
|
||||
clockConfig.physicalMinute = 0;
|
||||
clockConfig.nextOutputIsC1 = true;
|
||||
clockConfig.lastSyncTime = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("ConfigManager: Failed to parse clock state from SD: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
clockConfig.physicalHour = doc["hour"].as<uint8_t>() % 12;
|
||||
clockConfig.physicalMinute = doc["minute"].as<uint8_t>() % 60;
|
||||
clockConfig.nextOutputIsC1 = doc["nextIsC1"].as<bool>();
|
||||
clockConfig.lastSyncTime = doc["lastSyncTime"].as<uint32_t>();
|
||||
|
||||
LOG_INFO("ConfigManager: Clock state loaded");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveClockState() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
StaticJsonDocument<256> doc;
|
||||
doc["hour"] = clockConfig.physicalHour;
|
||||
doc["minute"] = clockConfig.physicalMinute;
|
||||
doc["nextIsC1"] = clockConfig.nextOutputIsC1;
|
||||
doc["lastSyncTime"] = clockConfig.lastSyncTime;
|
||||
|
||||
char buffer[256];
|
||||
size_t len = serializeJson(doc, buffer, sizeof(buffer));
|
||||
|
||||
if (len == 0 || len >= sizeof(buffer)) {
|
||||
LOG_ERROR("ConfigManager: Failed to serialize clock state JSON");
|
||||
return false;
|
||||
}
|
||||
|
||||
saveFileToSD("/settings", "clockState.json", buffer);
|
||||
LOG_DEBUG("ConfigManager: Clock state saved");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::loadUpdateServers() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
File file = SD.open("/settings/updateServers.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_INFO("ConfigManager: Update servers file not found - using fallback only");
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<1024> doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("ConfigManager: Failed to parse update servers JSON: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
updateServers.clear();
|
||||
if (doc.containsKey("servers") && doc["servers"].is<JsonArray>()) {
|
||||
JsonArray serversArray = doc["servers"];
|
||||
for (JsonObject server : serversArray) {
|
||||
if (server.containsKey("url")) {
|
||||
String url = server["url"].as<String>();
|
||||
if (!url.isEmpty()) {
|
||||
updateServers.push_back(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("ConfigManager: Loaded %d update servers from SD card", updateServers.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<String> ConfigManager::getUpdateServers() const {
|
||||
std::vector<String> servers;
|
||||
for (const String& server : updateServers) {
|
||||
servers.push_back(server);
|
||||
}
|
||||
servers.push_back(updateConfig.fallbackServerUrl);
|
||||
return servers;
|
||||
}
|
||||
|
||||
void ConfigManager::updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec) {
|
||||
timeConfig.gmtOffsetSec = gmtOffsetSec;
|
||||
timeConfig.daylightOffsetSec = daylightOffsetSec;
|
||||
saveToSD();
|
||||
LOG_INFO("ConfigManager: TimeConfig updated - GMT offset %ld sec, DST offset %d sec",
|
||||
gmtOffsetSec, daylightOffsetSec);
|
||||
}
|
||||
|
||||
void ConfigManager::updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||
IPAddress subnet, IPAddress dns1, IPAddress dns2) {
|
||||
networkConfig.useStaticIP = useStaticIP;
|
||||
networkConfig.ip = ip;
|
||||
networkConfig.gateway = gateway;
|
||||
networkConfig.subnet = subnet;
|
||||
networkConfig.dns1 = dns1;
|
||||
networkConfig.dns2 = dns2;
|
||||
saveNetworkConfig(); // Save immediately to SD
|
||||
LOG_INFO("ConfigManager: NetworkConfig updated - Static IP: %s, IP: %s",
|
||||
useStaticIP ? "enabled" : "disabled", ip.toString().c_str());
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// NETWORK CONFIGURATION PERSISTENCE
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool ConfigManager::loadNetworkConfig() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
File file = SD.open("/settings/networkConfig.json", FILE_READ);
|
||||
if (!file) {
|
||||
LOG_INFO("ConfigManager: Network config file not found - using auto-generated hostname and DHCP");
|
||||
return false;
|
||||
}
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("ConfigManager: Failed to parse network config from SD: %s", error.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load hostname if present (overrides auto-generated)
|
||||
if (doc.containsKey("hostname")) {
|
||||
String customHostname = doc["hostname"].as<String>();
|
||||
if (!customHostname.isEmpty()) {
|
||||
networkConfig.hostname = customHostname;
|
||||
LOG_INFO("ConfigManager: Custom hostname loaded from SD: %s", customHostname.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Load static IP configuration
|
||||
if (doc.containsKey("useStaticIP")) {
|
||||
networkConfig.useStaticIP = doc["useStaticIP"].as<bool>();
|
||||
}
|
||||
|
||||
if (doc.containsKey("ip")) {
|
||||
String ipStr = doc["ip"].as<String>();
|
||||
if (!ipStr.isEmpty() && ipStr != "0.0.0.0") {
|
||||
networkConfig.ip.fromString(ipStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.containsKey("gateway")) {
|
||||
String gwStr = doc["gateway"].as<String>();
|
||||
if (!gwStr.isEmpty() && gwStr != "0.0.0.0") {
|
||||
networkConfig.gateway.fromString(gwStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.containsKey("subnet")) {
|
||||
String subnetStr = doc["subnet"].as<String>();
|
||||
if (!subnetStr.isEmpty() && subnetStr != "0.0.0.0") {
|
||||
networkConfig.subnet.fromString(subnetStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.containsKey("dns1")) {
|
||||
String dns1Str = doc["dns1"].as<String>();
|
||||
if (!dns1Str.isEmpty() && dns1Str != "0.0.0.0") {
|
||||
networkConfig.dns1.fromString(dns1Str);
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.containsKey("dns2")) {
|
||||
String dns2Str = doc["dns2"].as<String>();
|
||||
if (!dns2Str.isEmpty() && dns2Str != "0.0.0.0") {
|
||||
networkConfig.dns2.fromString(dns2Str);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("ConfigManager: Network config loaded - Hostname: %s, Static IP: %s",
|
||||
networkConfig.hostname.c_str(),
|
||||
networkConfig.useStaticIP ? "enabled" : "disabled");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::saveNetworkConfig() {
|
||||
if (!ensureSDCard()) return false;
|
||||
|
||||
StaticJsonDocument<512> doc;
|
||||
|
||||
// Save hostname (user can customize)
|
||||
doc["hostname"] = networkConfig.hostname;
|
||||
|
||||
// Save static IP configuration
|
||||
doc["useStaticIP"] = networkConfig.useStaticIP;
|
||||
doc["ip"] = networkConfig.ip.toString();
|
||||
doc["gateway"] = networkConfig.gateway.toString();
|
||||
doc["subnet"] = networkConfig.subnet.toString();
|
||||
doc["dns1"] = networkConfig.dns1.toString();
|
||||
doc["dns2"] = networkConfig.dns2.toString();
|
||||
|
||||
char buffer[512];
|
||||
size_t len = serializeJson(doc, buffer, sizeof(buffer));
|
||||
|
||||
if (len == 0 || len >= sizeof(buffer)) {
|
||||
LOG_ERROR("ConfigManager: Failed to serialize network config JSON");
|
||||
return false;
|
||||
}
|
||||
|
||||
saveFileToSD("/settings", "networkConfig.json", buffer);
|
||||
LOG_INFO("ConfigManager: Network config saved to SD");
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FACTORY RESET IMPLEMENTATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool ConfigManager::factoryReset() {
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_WARNING("🏭 FACTORY RESET INITIATED");
|
||||
LOG_WARNING("═══════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("❌ ConfigManager: Cannot perform factory reset - SD card not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 1: Delete all configuration files
|
||||
LOG_INFO("🗑️ Step 1: Deleting all configuration files from SD card...");
|
||||
bool deleteSuccess = clearAllSettings();
|
||||
|
||||
if (!deleteSuccess) {
|
||||
LOG_ERROR("❌ ConfigManager: Factory reset failed - could not delete all settings");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Step 2: Reset in-memory configuration to defaults
|
||||
LOG_INFO("🔄 Step 2: Resetting in-memory configuration to defaults...");
|
||||
|
||||
// Reset network config (keep device-generated values)
|
||||
networkConfig.useStaticIP = false;
|
||||
networkConfig.ip = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.gateway = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.subnet = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.dns1 = IPAddress(0, 0, 0, 0);
|
||||
networkConfig.dns2 = IPAddress(0, 0, 0, 0);
|
||||
// hostname and apSsid are auto-generated from deviceUID, keep them
|
||||
|
||||
// Reset time config
|
||||
timeConfig.gmtOffsetSec = 0;
|
||||
timeConfig.daylightOffsetSec = 0;
|
||||
|
||||
// Reset bell config
|
||||
createDefaultBellConfig();
|
||||
|
||||
// Reset clock config to defaults
|
||||
clockConfig.c1output = 255;
|
||||
clockConfig.c2output = 255;
|
||||
clockConfig.pulseDuration = 5000;
|
||||
clockConfig.pauseDuration = 2000;
|
||||
clockConfig.physicalHour = 0;
|
||||
clockConfig.physicalMinute = 0;
|
||||
clockConfig.nextOutputIsC1 = true;
|
||||
clockConfig.lastSyncTime = 0;
|
||||
clockConfig.alertType = "OFF";
|
||||
clockConfig.alertRingInterval = 1200;
|
||||
clockConfig.hourBell = 255;
|
||||
clockConfig.halfBell = 255;
|
||||
clockConfig.quarterBell = 255;
|
||||
clockConfig.backlight = false;
|
||||
clockConfig.backlightOutput = 255;
|
||||
clockConfig.backlightOnTime = "18:00";
|
||||
clockConfig.backlightOffTime = "06:00";
|
||||
clockConfig.daytimeSilenceEnabled = false;
|
||||
clockConfig.daytimeSilenceOnTime = "14:00";
|
||||
clockConfig.daytimeSilenceOffTime = "17:00";
|
||||
clockConfig.nighttimeSilenceEnabled = false;
|
||||
clockConfig.nighttimeSilenceOnTime = "22:00";
|
||||
clockConfig.nighttimeSilenceOffTime = "07:00";
|
||||
|
||||
// Note: Device identity (deviceUID, hwType, hwVersion) in NVS is NOT reset
|
||||
// Note: WiFi credentials are handled by WiFiManager, not reset here
|
||||
|
||||
LOG_INFO("✅ Step 2: In-memory configuration reset to defaults");
|
||||
|
||||
LOG_WARNING("✅ FACTORY RESET COMPLETE");
|
||||
LOG_WARNING("🔄 Device will boot with default settings on next restart");
|
||||
LOG_WARNING("🆔 Device identity (UID) preserved in NVS");
|
||||
LOG_INFO("WiFi credentials should be cleared separately using WiFiManager");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::clearAllSettings() {
|
||||
if (!ensureSDCard()) {
|
||||
LOG_ERROR("ConfigManager: SD card not available for clearing settings");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool allDeleted = true;
|
||||
int filesDeleted = 0;
|
||||
int filesFailed = 0;
|
||||
|
||||
// List of all configuration files to delete
|
||||
const char* settingsFiles[] = {
|
||||
"/settings/deviceConfig.json",
|
||||
"/settings/networkConfig.json",
|
||||
"/settings/relayTimings.json",
|
||||
"/settings/clockConfig.json",
|
||||
"/settings/clockState.json",
|
||||
"/settings/updateServers.json"
|
||||
};
|
||||
|
||||
int numFiles = sizeof(settingsFiles) / sizeof(settingsFiles[0]);
|
||||
|
||||
LOG_INFO("ConfigManager: Attempting to delete %d configuration files...", numFiles);
|
||||
|
||||
// Delete each configuration file
|
||||
for (int i = 0; i < numFiles; i++) {
|
||||
const char* filepath = settingsFiles[i];
|
||||
|
||||
if (SD.exists(filepath)) {
|
||||
if (SD.remove(filepath)) {
|
||||
LOG_INFO("✅ Deleted: %s", filepath);
|
||||
filesDeleted++;
|
||||
} else {
|
||||
LOG_ERROR("❌ Failed to delete: %s", filepath);
|
||||
filesFailed++;
|
||||
allDeleted = false;
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG("⏩ Skip (not found): %s", filepath);
|
||||
}
|
||||
}
|
||||
|
||||
// Also delete the /melodies directory if you want a complete reset
|
||||
// Uncomment if you want to delete melodies too:
|
||||
/*
|
||||
if (SD.exists("/melodies")) {
|
||||
LOG_INFO("Deleting /melodies directory...");
|
||||
// Note: SD library doesn't have rmdir for non-empty dirs
|
||||
// You'd need to implement recursive delete or just leave melodies
|
||||
}
|
||||
*/
|
||||
|
||||
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
|
||||
LOG_INFO("📄 Settings cleanup summary:");
|
||||
LOG_INFO(" ✅ Files deleted: %d", filesDeleted);
|
||||
LOG_INFO(" ❌ Files failed: %d", filesFailed);
|
||||
LOG_INFO(" 🔄 Total processed: %d / %d", filesDeleted + filesFailed, numFiles);
|
||||
LOG_INFO("═══════════════════════════════════════════════════════════════════════════");
|
||||
|
||||
return allDeleted;
|
||||
}
|
||||
444
vesper/src/ConfigManager/ConfigManager.hpp
Normal file
444
vesper/src/ConfigManager/ConfigManager.hpp
Normal file
@@ -0,0 +1,444 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* CONFIGMANAGER.HPP - Deployment-Ready Configuration Management System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🗂️ CLEAN DEPLOYMENT-READY CONFIG SYSTEM 🗂️
|
||||
*
|
||||
* This restructured configuration system is designed for production deployment
|
||||
* with minimal hardcoded defaults, proper separation of factory vs user settings,
|
||||
* and clean configuration hierarchy.
|
||||
*
|
||||
* 🏗️ DEPLOYMENT PHILOSOPHY:
|
||||
* • Factory settings stored in NVS (permanent, set once)
|
||||
* • User settings stored on SD card (configurable via app)
|
||||
* • Hardware settings compiled per HWID variant
|
||||
* • Network credentials handled by WiFiManager only
|
||||
* • Auto-generated identifiers where appropriate
|
||||
*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <SD.h>
|
||||
#include <IPAddress.h>
|
||||
#include <ETH.h>
|
||||
#include <vector>
|
||||
#include <nvs_flash.h>
|
||||
#include <nvs.h>
|
||||
|
||||
class ConfigManager {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION STRUCTURES - Clean deployment-ready design
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @struct DeviceConfig
|
||||
* @brief Factory-set device identity (NVS storage)
|
||||
*
|
||||
* These values are set once during manufacturing and stored in NVS.
|
||||
* fwVersion is updated automatically after OTA updates (SD storage).
|
||||
*/
|
||||
struct DeviceConfig {
|
||||
String deviceUID = ""; // 🏷️ Factory-set UID (NVS) - NO DEFAULT
|
||||
String hwType = ""; // 🔧 Factory-set hardware type (NVS) - NO DEFAULT
|
||||
String hwVersion = ""; // 📐 Factory-set hardware revision (NVS) - NO DEFAULT
|
||||
String fwVersion = "0.0.0"; // 📋 Current firmware version (SD) - auto-updated
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct NetworkConfig
|
||||
* @brief Network connectivity settings
|
||||
*
|
||||
* WiFi credentials are handled entirely by WiFiManager.
|
||||
* Static IP settings are configured via app commands and stored on SD.
|
||||
* hostname is auto-generated from deviceUID.
|
||||
*/
|
||||
struct NetworkConfig {
|
||||
String hostname; // 🏭 Auto-generated: "BellSystems-<DEVID>"
|
||||
bool useStaticIP = false; // 🔧 Default DHCP, app-configurable via SD
|
||||
IPAddress ip; // 🏠 Empty default, read from SD
|
||||
IPAddress gateway; // 🌐 Empty default, read from SD
|
||||
IPAddress subnet; // 📊 Empty default, read from SD
|
||||
IPAddress dns1; // 📝 Empty default, read from SD
|
||||
IPAddress dns2; // 📝 Empty default, read from SD
|
||||
String apSsid; // 📡 Auto-generated AP name
|
||||
String apPass; // 🔐 AP is Open. No Password
|
||||
uint16_t discoveryPort = 32101; // 📡 Fixed discovery port
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct MqttConfig
|
||||
* @brief MQTT broker connection settings
|
||||
*
|
||||
* Cloud broker as default, can be overridden via SD card.
|
||||
* Username defaults to deviceUID for unique identification.
|
||||
*/
|
||||
struct MqttConfig {
|
||||
String host = "j2f24f16.ala.eu-central-1.emqxsl.com"; // 📡 Cloud MQTT broker (default)
|
||||
int port = 1883; // 🔌 Standard MQTT port (default)
|
||||
String user; // 👤 Auto-set to deviceUID
|
||||
String password = "vesper"; // 🔑 Default password - OK as is
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct HardwareConfig
|
||||
* @brief Hardware-specific settings (compiled per HWID)
|
||||
*
|
||||
* These values are hardware-variant specific and compiled into firmware.
|
||||
* Different values for different hardware revisions.
|
||||
*/
|
||||
struct HardwareConfig {
|
||||
uint8_t pcf8574Address = 0x24; // 🔌 Hardware-specific - OK as is
|
||||
uint8_t sdChipSelect = 5; // 💾 Hardware-specific - OK as is
|
||||
|
||||
// Ethernet SPI configuration - hardware-specific
|
||||
uint8_t ethSpiSck = 18; // ⏱️ Hardware-specific - OK as is
|
||||
uint8_t ethSpiMiso = 19; // 🔄 Hardware-specific - OK as is
|
||||
uint8_t ethSpiMosi = 23; // 🔄 Hardware-specific - OK as is
|
||||
|
||||
// ETH PHY Configuration - hardware-specific
|
||||
eth_phy_type_t ethPhyType = ETH_PHY_W5500; // 🔌 Hardware-specific - OK as is
|
||||
uint8_t ethPhyAddr = 1; // 📍 Hardware-specific - OK as is
|
||||
uint8_t ethPhyCs = 5; // 💾 Hardware-specific - OK as is
|
||||
int8_t ethPhyIrq = -1; // ⚡ Hardware-specific - OK as is
|
||||
int8_t ethPhyRst = -1; // 🔄 Hardware-specific - OK as is
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct TimeConfig
|
||||
* @brief Timezone and NTP settings
|
||||
*
|
||||
* NTP server is universal default.
|
||||
* Timezone offsets default to 0 (UTC) and are configured via app.
|
||||
*/
|
||||
struct TimeConfig {
|
||||
String ntpServer = "pool.ntp.org"; // ⏰ Universal NTP - OK as is
|
||||
long gmtOffsetSec = 0; // 🌍 Default UTC, app-configurable via SD
|
||||
int daylightOffsetSec = 0; // ☀️ Default no DST, app-configurable via SD
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct UpdateConfig
|
||||
* @brief OTA update server configuration
|
||||
*
|
||||
* Universal defaults for all devices.
|
||||
*/
|
||||
struct UpdateConfig {
|
||||
String fallbackServerUrl = "http://firmware.bonamin.space"; // 🛡️ Universal fallback - OK as is
|
||||
int timeout = 10000; // ⏱️ Universal timeout - OK as is
|
||||
int retries = 3; // 🔄 Universal retries - OK as is
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct BellConfig
|
||||
* @brief Bell system configuration (loaded from SD)
|
||||
*
|
||||
* All bell settings are loaded from SD card at startup.
|
||||
*/
|
||||
struct BellConfig {
|
||||
uint16_t durations[16]; // ⏱️ Loaded from SD at startup Factory Def: Min Size Hammer
|
||||
uint16_t outputs[16]; // 🔌 Loaded from SD at startup. Factory Def: Disabled
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct ClockConfig
|
||||
* @brief Clock mechanism configuration (loaded from SD)
|
||||
*
|
||||
* All clock settings are loaded from SD card at startup.
|
||||
* This struct is correctly designed and needs no changes.
|
||||
*/
|
||||
struct ClockConfig {
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// CLOCK ENABLE/DISABLE - Master control for all clock functionality
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
bool enabled = false; // 🔘 Enable/disable ALL clock functionality
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// CLOCK OUTPUTS - Physical output configuration
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
uint8_t c1output = 255; // 🕐 Clock output #1 pin (255 = disabled)
|
||||
uint8_t c2output = 255; // 🕑 Clock output #2 pin (255 = disabled)
|
||||
uint16_t pulseDuration = 5000; // ⏱️ Pulse duration in milliseconds
|
||||
uint16_t pauseDuration = 2000; // 🛑 Pause between consecutive pulses
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// PHYSICAL CLOCK STATE - Position tracking for sync
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
uint8_t physicalHour = 0; // 🕐 Physical clock hour (0-11)
|
||||
uint8_t physicalMinute = 0; // 🕐 Physical clock minute (0-59)
|
||||
bool nextOutputIsC1 = true; // 🔄 Which output fires next
|
||||
uint32_t lastSyncTime = 0; // ⏰ Last successful sync timestamp
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// ALERT CONFIGURATION - Bell alert behavior
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
String alertType = "OFF"; // 🔔 Alert mode: "SINGLE", "HOURS", or "OFF"
|
||||
uint16_t alertRingInterval = 1200; // ⏰ Interval between bell rings (ms) 1-2s 0.2s steps
|
||||
uint8_t hourBell = 255; // 🕐 Bell for hourly alerts (255 = disabled)
|
||||
uint8_t halfBell = 255; // 🕕 Bell for half-hour alerts (255 = disabled)
|
||||
uint8_t quarterBell = 255; // 🕒 Bell for quarter-hour alerts (255 = disabled)
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// BACKLIGHT AUTOMATION - Clock illumination control
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
bool backlight = false; // 💡 Enable/disable backlight automation
|
||||
uint8_t backlightOutput = 255; // 🔌 Backlight output pin (255 = disabled)
|
||||
String backlightOnTime = "18:00"; // 🌅 Time to turn backlight ON
|
||||
String backlightOffTime = "06:00"; // 🌄 Time to turn backlight OFF
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// SILENCE PERIODS - Quiet hours configuration
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
bool daytimeSilenceEnabled = false; // 🌞 Enable/disable daytime silence
|
||||
String daytimeSilenceOnTime = "14:00"; // 🌞 Start of daytime silence
|
||||
String daytimeSilenceOffTime = "17:00"; // 🌞 End of daytime silence
|
||||
bool nighttimeSilenceEnabled = false; // 🌙 Enable/disable nighttime silence
|
||||
String nighttimeSilenceOnTime = "22:00"; // 🌙 Start of nighttime silence
|
||||
String nighttimeSilenceOffTime = "07:00"; // 🌙 End of nighttime silence
|
||||
};
|
||||
|
||||
private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// MEMBER VARIABLES - Clean deployment-ready storage
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
DeviceConfig deviceConfig;
|
||||
NetworkConfig networkConfig;
|
||||
MqttConfig mqttConfig;
|
||||
HardwareConfig hardwareConfig;
|
||||
TimeConfig timeConfig;
|
||||
UpdateConfig updateConfig;
|
||||
BellConfig bellConfig;
|
||||
ClockConfig clockConfig;
|
||||
|
||||
bool sdInitialized = false;
|
||||
std::vector<String> updateServers;
|
||||
nvs_handle_t nvsHandle = 0;
|
||||
static const char* NVS_NAMESPACE;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE METHODS - Clean initialization and auto-generation
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Initialize configuration with clean defaults and auto-generation
|
||||
*
|
||||
* This method:
|
||||
* 1. Loads device identity from NVS (factory-set)
|
||||
* 2. Auto-generates hostname from deviceUID
|
||||
* 3. Auto-generates AP SSID from deviceUID
|
||||
* 4. Sets MQTT username to deviceUID
|
||||
* 5. Loads user-configurable settings from SD
|
||||
*/
|
||||
void initializeCleanDefaults();
|
||||
|
||||
/**
|
||||
* @brief Auto-generate network identifiers from deviceUID
|
||||
*
|
||||
* Generates:
|
||||
* - hostname: "BellSystems-<last6ofUID>"
|
||||
* - apSsid: "BellSystems-Setup-<last6ofUID>"
|
||||
* - mqttUser: deviceUID
|
||||
*/
|
||||
void generateNetworkIdentifiers();
|
||||
|
||||
bool ensureSDCard();
|
||||
void createDefaultBellConfig();
|
||||
|
||||
// NVS management (for factory-set device identity)
|
||||
bool initializeNVS();
|
||||
bool loadDeviceIdentityFromNVS();
|
||||
bool saveDeviceIdentityToNVS();
|
||||
String readNVSString(const char* key, const String& defaultValue);
|
||||
bool writeNVSString(const char* key, const String& value);
|
||||
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PUBLIC INTERFACE - Clean deployment-ready API
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ConfigManager();
|
||||
|
||||
/**
|
||||
* @brief Initialize clean deployment-ready configuration system
|
||||
*
|
||||
* Load order:
|
||||
* 1. Device identity from NVS (factory-set, permanent)
|
||||
* 2. Auto-generate network identifiers
|
||||
* 3. Load user settings from SD card
|
||||
* 4. Apply clean defaults for missing settings
|
||||
*/
|
||||
bool begin();
|
||||
|
||||
void loadFromSD();
|
||||
bool saveToSD();
|
||||
|
||||
// Configuration access (read-only getters)
|
||||
const DeviceConfig& getDeviceConfig() const { return deviceConfig; }
|
||||
const NetworkConfig& getNetworkConfig() const { return networkConfig; }
|
||||
const MqttConfig& getMqttConfig() const { return mqttConfig; }
|
||||
const HardwareConfig& getHardwareConfig() const { return hardwareConfig; }
|
||||
const TimeConfig& getTimeConfig() const { return timeConfig; }
|
||||
const UpdateConfig& getUpdateConfig() const { return updateConfig; }
|
||||
const BellConfig& getBellConfig() const { return bellConfig; }
|
||||
const ClockConfig& getClockConfig() const { return clockConfig; }
|
||||
|
||||
// Device identity methods (read-only - factory set via separate firmware)
|
||||
String getDeviceUID() const { return deviceConfig.deviceUID; }
|
||||
String getHwType() const { return deviceConfig.hwType; }
|
||||
String getHwVersion() const { return deviceConfig.hwVersion; }
|
||||
String getFwVersion() const { return deviceConfig.fwVersion; }
|
||||
|
||||
/** @brief Set device UID (factory programming only) */
|
||||
void setDeviceUID(const String& uid) {
|
||||
deviceConfig.deviceUID = uid;
|
||||
saveDeviceIdentityToNVS();
|
||||
generateNetworkIdentifiers();
|
||||
}
|
||||
|
||||
/** @brief Set hardware type (factory programming only) */
|
||||
void setHwType(const String& type) {
|
||||
deviceConfig.hwType = type;
|
||||
saveDeviceIdentityToNVS();
|
||||
}
|
||||
|
||||
/** @brief Set hardware version (factory programming only) */
|
||||
void setHwVersion(const String& version) {
|
||||
deviceConfig.hwVersion = version;
|
||||
saveDeviceIdentityToNVS();
|
||||
}
|
||||
|
||||
/** @brief Set firmware version (auto-updated after OTA) */
|
||||
void setFwVersion(const String& version) { deviceConfig.fwVersion = version; }
|
||||
|
||||
// Configuration update methods for app commands
|
||||
void updateTimeConfig(long gmtOffsetSec, int daylightOffsetSec);
|
||||
void updateNetworkConfig(bool useStaticIP, IPAddress ip, IPAddress gateway,
|
||||
IPAddress subnet, IPAddress dns1, IPAddress dns2);
|
||||
|
||||
// Network configuration persistence
|
||||
bool loadNetworkConfig();
|
||||
bool saveNetworkConfig();
|
||||
|
||||
// Bell and clock configuration methods (unchanged)
|
||||
bool loadBellDurations();
|
||||
bool saveBellDurations();
|
||||
void updateBellDurations(JsonVariant doc);
|
||||
void updateBellOutputs(JsonVariant doc);
|
||||
uint16_t getBellDuration(uint8_t bellIndex) const;
|
||||
uint16_t getBellOutput(uint8_t bellIndex) const;
|
||||
void setBellDuration(uint8_t bellIndex, uint16_t duration);
|
||||
void setBellOutput(uint8_t bellIndex, uint16_t output);
|
||||
|
||||
// Clock configuration methods (unchanged - already correct)
|
||||
bool getClockEnabled() const { return clockConfig.enabled; }
|
||||
void setClockEnabled(bool enabled) { clockConfig.enabled = enabled; }
|
||||
void updateClockOutputs(JsonVariant doc);
|
||||
uint8_t getClockOutput1() const { return clockConfig.c1output; }
|
||||
uint8_t getClockOutput2() const { return clockConfig.c2output; }
|
||||
uint16_t getClockPulseDuration() const { return clockConfig.pulseDuration; }
|
||||
uint16_t getClockPauseDuration() const { return clockConfig.pauseDuration; }
|
||||
void setClockOutput1(uint8_t output) { clockConfig.c1output = output; }
|
||||
void setClockOutput2(uint8_t output) { clockConfig.c2output = output; }
|
||||
void setClockPulseDuration(uint16_t duration) { clockConfig.pulseDuration = duration; }
|
||||
void setClockPauseDuration(uint16_t duration) { clockConfig.pauseDuration = duration; }
|
||||
|
||||
// Physical clock state methods (unchanged)
|
||||
uint8_t getPhysicalClockHour() const { return clockConfig.physicalHour; }
|
||||
uint8_t getPhysicalClockMinute() const { return clockConfig.physicalMinute; }
|
||||
bool getNextOutputIsC1() const { return clockConfig.nextOutputIsC1; }
|
||||
uint32_t getLastSyncTime() const { return clockConfig.lastSyncTime; }
|
||||
void setPhysicalClockHour(uint8_t hour) { clockConfig.physicalHour = hour % 12; }
|
||||
void setPhysicalClockMinute(uint8_t minute) { clockConfig.physicalMinute = minute % 60; }
|
||||
void setNextOutputIsC1(bool isC1) { clockConfig.nextOutputIsC1 = isC1; }
|
||||
void setLastSyncTime(uint32_t timestamp) { clockConfig.lastSyncTime = timestamp; }
|
||||
|
||||
// Alert configuration methods (unchanged)
|
||||
String getAlertType() const { return clockConfig.alertType; }
|
||||
uint16_t getAlertRingInterval() const { return clockConfig.alertRingInterval; }
|
||||
uint8_t getHourBell() const { return clockConfig.hourBell; }
|
||||
uint8_t getHalfBell() const { return clockConfig.halfBell; }
|
||||
uint8_t getQuarterBell() const { return clockConfig.quarterBell; }
|
||||
void setAlertType(const String& type) { clockConfig.alertType = type; }
|
||||
void setAlertRingInterval(uint16_t interval) { clockConfig.alertRingInterval = interval; }
|
||||
void setHourBell(uint8_t bell) { clockConfig.hourBell = bell; }
|
||||
void setHalfBell(uint8_t bell) { clockConfig.halfBell = bell; }
|
||||
void setQuarterBell(uint8_t bell) { clockConfig.quarterBell = bell; }
|
||||
|
||||
// Backlight configuration methods (unchanged)
|
||||
bool getBacklightEnabled() const { return clockConfig.backlight; }
|
||||
uint8_t getBacklightOutput() const { return clockConfig.backlightOutput; }
|
||||
String getBacklightOnTime() const { return clockConfig.backlightOnTime; }
|
||||
String getBacklightOffTime() const { return clockConfig.backlightOffTime; }
|
||||
void setBacklightEnabled(bool enabled) { clockConfig.backlight = enabled; }
|
||||
void setBacklightOutput(uint8_t output) { clockConfig.backlightOutput = output; }
|
||||
void setBacklightOnTime(const String& time) { clockConfig.backlightOnTime = time; }
|
||||
void setBacklightOffTime(const String& time) { clockConfig.backlightOffTime = time; }
|
||||
|
||||
// Silence periods methods (unchanged)
|
||||
bool getDaytimeSilenceEnabled() const { return clockConfig.daytimeSilenceEnabled; }
|
||||
String getDaytimeSilenceOnTime() const { return clockConfig.daytimeSilenceOnTime; }
|
||||
String getDaytimeSilenceOffTime() const { return clockConfig.daytimeSilenceOffTime; }
|
||||
bool getNighttimeSilenceEnabled() const { return clockConfig.nighttimeSilenceEnabled; }
|
||||
String getNighttimeSilenceOnTime() const { return clockConfig.nighttimeSilenceOnTime; }
|
||||
String getNighttimeSilenceOffTime() const { return clockConfig.nighttimeSilenceOffTime; }
|
||||
void setDaytimeSilenceEnabled(bool enabled) { clockConfig.daytimeSilenceEnabled = enabled; }
|
||||
void setDaytimeSilenceOnTime(const String& time) { clockConfig.daytimeSilenceOnTime = time; }
|
||||
void setDaytimeSilenceOffTime(const String& time) { clockConfig.daytimeSilenceOffTime = time; }
|
||||
void setNighttimeSilenceEnabled(bool enabled) { clockConfig.nighttimeSilenceEnabled = enabled; }
|
||||
void setNighttimeSilenceOnTime(const String& time) { clockConfig.nighttimeSilenceOnTime = time; }
|
||||
void setNighttimeSilenceOffTime(const String& time) { clockConfig.nighttimeSilenceOffTime = time; }
|
||||
|
||||
// Other methods (unchanged)
|
||||
void updateClockAlerts(JsonVariant doc);
|
||||
void updateClockBacklight(JsonVariant doc);
|
||||
void updateClockSilence(JsonVariant doc);
|
||||
bool loadClockConfig();
|
||||
bool saveClockConfig();
|
||||
bool loadClockState();
|
||||
bool saveClockState();
|
||||
void updateRealTimeFromCommand(JsonVariant doc);
|
||||
void updatePhysicalClockTimeFromCommand(JsonVariant doc);
|
||||
void saveFileToSD(const char* dirPath, const char* filename, const char* data);
|
||||
String getHardwareVariant() const { return deviceConfig.hwType; }
|
||||
bool loadDeviceConfig();
|
||||
bool saveDeviceConfig();
|
||||
bool loadUpdateServers();
|
||||
std::vector<String> getUpdateServers() const;
|
||||
String getAPSSID() const { return networkConfig.apSsid; }
|
||||
bool isHealthy() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FACTORY RESET
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Perform complete factory reset
|
||||
*
|
||||
* This method:
|
||||
* 1. Deletes all configuration files from SD card
|
||||
* 2. Does NOT touch NVS (device identity remains)
|
||||
* 3. On next boot, all settings will be recreated with defaults
|
||||
*
|
||||
* @return true if factory reset successful
|
||||
*/
|
||||
bool factoryReset();
|
||||
|
||||
/**
|
||||
* @brief Delete all settings files from SD card
|
||||
* @return true if all files deleted successfully
|
||||
*/
|
||||
bool clearAllSettings();
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// DEPLOYMENT NOTES:
|
||||
// 2. USER SETTINGS: All loaded from SD card, configured via app
|
||||
// 3. NETWORK: WiFiManager handles credentials, no hardcoded SSIDs/passwords
|
||||
// 4. IDENTIFIERS: Auto-generated from deviceUID for consistency
|
||||
// 5. DEFAULTS: Clean minimal defaults, everything configurable
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
241
vesper/src/FileManager/FileManager.cpp
Normal file
241
vesper/src/FileManager/FileManager.cpp
Normal file
@@ -0,0 +1,241 @@
|
||||
#include "FileManager.hpp"
|
||||
|
||||
FileManager::FileManager(ConfigManager* config) : configManager(config) {
|
||||
// Constructor - store reference to ConfigManager
|
||||
}
|
||||
|
||||
bool FileManager::initializeSD() {
|
||||
uint8_t sdPin = configManager->getHardwareConfig().sdChipSelect;
|
||||
if (!SD.begin(sdPin)) {
|
||||
LOG_ERROR("SD Card initialization failed!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FileManager::addMelody(JsonVariant doc) {
|
||||
LOG_INFO("Adding melody from JSON data...");
|
||||
|
||||
// Extract URL and filename from JSON
|
||||
if (!doc.containsKey("download_url") || !doc.containsKey("melodys_uid")) {
|
||||
LOG_ERROR("Missing required parameters: download_url or melodys_uid");
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* url = doc["download_url"];
|
||||
const char* filename = doc["melodys_uid"];
|
||||
|
||||
// Download the melody file to /melodies directory
|
||||
if (downloadFile(url, "/melodies", filename)) {
|
||||
LOG_INFO("Melody download successful: %s", filename);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERROR("Melody download failed: %s", filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileManager::ensureDirectoryExists(const String& dirPath) {
|
||||
if (!initializeSD()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure the directory ends with '/'
|
||||
String normalizedPath = dirPath;
|
||||
if (!normalizedPath.endsWith("/")) {
|
||||
normalizedPath += "/";
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
return SD.mkdir(normalizedPath.c_str());
|
||||
}
|
||||
|
||||
bool FileManager::downloadFile(const String& url, const String& directory, const String& filename) {
|
||||
LOG_INFO("Starting download from: %s", url.c_str());
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(url);
|
||||
int httpCode = http.GET();
|
||||
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
LOG_ERROR("HTTP GET failed, error: %s", http.errorToString(httpCode).c_str());
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!initializeSD()) {
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!ensureDirectoryExists(directory)) {
|
||||
LOG_ERROR("Failed to create directory: %s", directory.c_str());
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build full file path
|
||||
String dirPath = directory;
|
||||
if (!dirPath.endsWith("/")) dirPath += "/";
|
||||
String fullPath = dirPath + filename;
|
||||
|
||||
File file = SD.open(fullPath.c_str(), FILE_WRITE);
|
||||
if (!file) {
|
||||
LOG_ERROR("Failed to open file for writing: %s", fullPath.c_str());
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
uint8_t buffer[1024];
|
||||
int bytesRead;
|
||||
|
||||
while (http.connected() && (bytesRead = stream->readBytes(buffer, sizeof(buffer))) > 0) {
|
||||
file.write(buffer, bytesRead);
|
||||
}
|
||||
|
||||
file.close();
|
||||
http.end();
|
||||
LOG_INFO("Download complete, file saved to: %s", fullPath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
String FileManager::listFilesAsJson(const char* dirPath) {
|
||||
if (!initializeSD()) {
|
||||
LOG_ERROR("SD initialization failed");
|
||||
return "{}";
|
||||
}
|
||||
|
||||
File dir = SD.open(dirPath);
|
||||
if (!dir || !dir.isDirectory()) {
|
||||
LOG_ERROR("Directory not found: %s", dirPath);
|
||||
return "{}";
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(1024);
|
||||
JsonArray fileList = doc.createNestedArray("files");
|
||||
|
||||
File file = dir.openNextFile();
|
||||
while (file) {
|
||||
if (!file.isDirectory()) {
|
||||
fileList.add(file.name());
|
||||
}
|
||||
file = dir.openNextFile();
|
||||
}
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
return json;
|
||||
}
|
||||
|
||||
bool FileManager::fileExists(const String& filePath) {
|
||||
if (!initializeSD()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File file = SD.open(filePath.c_str());
|
||||
if (file) {
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool FileManager::deleteFile(const String& filePath) {
|
||||
if (!initializeSD()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SD.remove(filePath.c_str())) {
|
||||
LOG_INFO("File deleted: %s", filePath.c_str());
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("Failed to delete file: %s", filePath.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool FileManager::createDirectory(const String& dirPath) {
|
||||
return ensureDirectoryExists(dirPath);
|
||||
}
|
||||
|
||||
size_t FileManager::getFileSize(const String& filePath) {
|
||||
if (!initializeSD()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
File file = SD.open(filePath.c_str());
|
||||
if (!file) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t size = file.size();
|
||||
file.close();
|
||||
return size;
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool FileManager::isHealthy() const {
|
||||
// Check if ConfigManager is available
|
||||
if (!configManager) {
|
||||
LOG_DEBUG("FileManager: Unhealthy - ConfigManager not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if SD card can be initialized
|
||||
uint8_t sdPin = configManager->getHardwareConfig().sdChipSelect;
|
||||
if (!SD.begin(sdPin)) {
|
||||
LOG_DEBUG("FileManager: Unhealthy - SD Card initialization failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we can read from SD card (test with root directory)
|
||||
File root = SD.open("/");
|
||||
if (!root) {
|
||||
LOG_DEBUG("FileManager: Unhealthy - Cannot access SD root directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!root.isDirectory()) {
|
||||
LOG_DEBUG("FileManager: Unhealthy - SD root is not a directory");
|
||||
root.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
root.close();
|
||||
|
||||
// Check if we can write to SD card (create/delete a test file)
|
||||
String testFile = "/health_test.tmp";
|
||||
File file = SD.open(testFile.c_str(), FILE_WRITE);
|
||||
if (!file) {
|
||||
LOG_DEBUG("FileManager: Unhealthy - Cannot write to SD card");
|
||||
return false;
|
||||
}
|
||||
|
||||
file.print("health_check");
|
||||
file.close();
|
||||
|
||||
// Verify we can read the test file
|
||||
file = SD.open(testFile.c_str(), FILE_READ);
|
||||
if (!file) {
|
||||
LOG_DEBUG("FileManager: Unhealthy - Cannot read test file from SD card");
|
||||
return false;
|
||||
}
|
||||
|
||||
String content = file.readString();
|
||||
file.close();
|
||||
|
||||
// Clean up test file
|
||||
SD.remove(testFile.c_str());
|
||||
|
||||
if (content != "health_check") {
|
||||
LOG_DEBUG("FileManager: Unhealthy - SD card read/write test failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
61
vesper/src/FileManager/FileManager.hpp
Normal file
61
vesper/src/FileManager/FileManager.hpp
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* FILEMANAGER.HPP - SD Card and File Operations Manager
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 📁 THE FILE SYSTEM ORCHESTRATOR OF VESPER 📁
|
||||
*
|
||||
* This class provides a clean, robust interface for all file operations
|
||||
* including melody file management, configuration persistence, and
|
||||
* comprehensive error handling with automatic recovery.
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Enhanced file management)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#ifndef FILEMANAGER_HPP
|
||||
#define FILEMANAGER_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <SD.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <WiFiClient.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
|
||||
class FileManager {
|
||||
private:
|
||||
ConfigManager* configManager;
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
FileManager(ConfigManager* config);
|
||||
|
||||
// Core file operations
|
||||
bool downloadFile(const String& url, const String& directory, const String& filename);
|
||||
bool addMelody(JsonVariant doc); // Download melody file from JSON data
|
||||
String listFilesAsJson(const char* dirPath);
|
||||
|
||||
// File utilities
|
||||
bool fileExists(const String& filePath);
|
||||
bool deleteFile(const String& filePath);
|
||||
bool createDirectory(const String& dirPath);
|
||||
size_t getFileSize(const String& filePath);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if FileManager is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
private:
|
||||
// Helper functions
|
||||
bool initializeSD();
|
||||
bool ensureDirectoryExists(const String& dirPath);
|
||||
};
|
||||
|
||||
#endif
|
||||
697
vesper/src/FirmwareValidator/FirmwareValidator.cpp
Normal file
697
vesper/src/FirmwareValidator/FirmwareValidator.cpp
Normal file
@@ -0,0 +1,697 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* FIRMWAREVALIDATOR.CPP - Bulletproof Firmware Update Validation Implementation
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#include "FirmwareValidator.hpp"
|
||||
#include "../HealthMonitor/HealthMonitor.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include <esp_task_wdt.h>
|
||||
#include <esp_image_format.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// NVS keys for persistent validation state
|
||||
static const char* NVS_NAMESPACE = "fw_validator";
|
||||
static const char* NVS_STATE_KEY = "val_state";
|
||||
static const char* NVS_BOOT_COUNT_KEY = "boot_count";
|
||||
static const char* NVS_RETRY_COUNT_KEY = "retry_count";
|
||||
static const char* NVS_FAILURE_COUNT_KEY = "fail_count";
|
||||
static const char* NVS_LAST_BOOT_KEY = "last_boot";
|
||||
|
||||
FirmwareValidator::FirmwareValidator() {
|
||||
// Initialize default configuration
|
||||
_config = ValidationConfig();
|
||||
}
|
||||
|
||||
FirmwareValidator::~FirmwareValidator() {
|
||||
// Clean up resources
|
||||
if (_validationTimer) {
|
||||
xTimerDelete(_validationTimer, portMAX_DELAY);
|
||||
_validationTimer = nullptr;
|
||||
}
|
||||
|
||||
if (_monitoringTask) {
|
||||
vTaskDelete(_monitoringTask);
|
||||
_monitoringTask = nullptr;
|
||||
}
|
||||
|
||||
if (_nvsHandle) {
|
||||
nvs_close(_nvsHandle);
|
||||
_nvsHandle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool FirmwareValidator::begin(HealthMonitor* healthMonitor, ConfigManager* configManager) {
|
||||
LOG_INFO("🛡️ Initializing Firmware Validator System");
|
||||
|
||||
_healthMonitor = healthMonitor;
|
||||
_configManager = configManager;
|
||||
|
||||
// Initialize NVS for persistent state storage
|
||||
if (!initializeNVS()) {
|
||||
LOG_ERROR("❌ Failed to initialize NVS for firmware validation");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize ESP32 partition information
|
||||
if (!initializePartitions()) {
|
||||
LOG_ERROR("❌ Failed to initialize ESP32 partitions");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load previous validation state
|
||||
loadValidationState();
|
||||
|
||||
LOG_INFO("✅ Firmware Validator initialized");
|
||||
LOG_INFO("📍 Running partition: %s", getPartitionLabel(_runningPartition).c_str());
|
||||
LOG_INFO("📍 Backup partition: %s", getPartitionLabel(_backupPartition).c_str());
|
||||
LOG_INFO("🔄 Validation state: %s", validationStateToString(_validationState).c_str());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FirmwareValidator::performStartupValidation() {
|
||||
LOG_INFO("🚀 Starting firmware startup validation...");
|
||||
|
||||
// Check if this is a new firmware that needs validation
|
||||
const esp_partition_t* bootPartition = esp_ota_get_boot_partition();
|
||||
const esp_partition_t* runningPartition = esp_ota_get_running_partition();
|
||||
|
||||
if (bootPartition != runningPartition) {
|
||||
LOG_WARNING("⚠️ Boot partition differs from running partition!");
|
||||
LOG_WARNING(" Boot: %s", getPartitionLabel(bootPartition).c_str());
|
||||
LOG_WARNING(" Running: %s", getPartitionLabel(runningPartition).c_str());
|
||||
}
|
||||
|
||||
// Increment boot count for this session
|
||||
incrementBootCount();
|
||||
|
||||
// Check if we need to validate new firmware
|
||||
if (_validationState == FirmwareValidationState::UNKNOWN) {
|
||||
// First boot of potentially new firmware
|
||||
_validationState = FirmwareValidationState::STARTUP_PENDING;
|
||||
LOG_INFO("🆕 New firmware detected - entering validation mode");
|
||||
}
|
||||
|
||||
if (_validationState == FirmwareValidationState::STARTUP_PENDING) {
|
||||
LOG_INFO("🔍 Performing startup validation...");
|
||||
_validationState = FirmwareValidationState::STARTUP_RUNNING;
|
||||
_validationStartTime = millis();
|
||||
|
||||
// Perform basic health checks with timeout protection
|
||||
unsigned long startTime = millis();
|
||||
bool healthCheckPassed = false;
|
||||
|
||||
while ((millis() - startTime) < _config.startupTimeoutMs) {
|
||||
healthCheckPassed = performBasicHealthCheck();
|
||||
if (healthCheckPassed) {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_WARNING("⚠️ Startup health check failed, retrying...");
|
||||
delay(1000); // Wait 1 second before retry
|
||||
}
|
||||
|
||||
if (healthCheckPassed) {
|
||||
_validationState = FirmwareValidationState::RUNTIME_TESTING;
|
||||
_startupRetryCount = 0; // Reset retry count on success
|
||||
saveValidationState();
|
||||
LOG_INFO("✅ Firmware startup validation PASSED - proceeding with initialization");
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("❌ Startup validation FAILED after %lu ms", _config.startupTimeoutMs);
|
||||
_startupRetryCount++;
|
||||
|
||||
if (_startupRetryCount >= _config.maxStartupRetries) {
|
||||
LOG_ERROR("💥 Maximum startup retries exceeded - triggering rollback");
|
||||
handleValidationFailure("Startup validation failed repeatedly");
|
||||
return false; // This will trigger rollback and reboot
|
||||
} else {
|
||||
LOG_WARNING("🔄 Startup retry %d/%d - rebooting...",
|
||||
_startupRetryCount, _config.maxStartupRetries);
|
||||
saveValidationState();
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if (_validationState == FirmwareValidationState::VALIDATED) {
|
||||
LOG_INFO("✅ Firmware already validated - normal operation");
|
||||
return true;
|
||||
} else if (_validationState == FirmwareValidationState::STARTUP_RUNNING) {
|
||||
// Handle interrupted validation from previous boot
|
||||
LOG_INFO("🔄 Resuming interrupted validation - transitioning to runtime testing");
|
||||
_validationState = FirmwareValidationState::RUNTIME_TESTING;
|
||||
saveValidationState();
|
||||
return true;
|
||||
} else if (_validationState == FirmwareValidationState::RUNTIME_TESTING) {
|
||||
// Already in runtime testing from previous boot
|
||||
LOG_INFO("🔄 Continuing runtime validation from previous session");
|
||||
return true;
|
||||
} else {
|
||||
LOG_WARNING("⚠️ Unexpected validation state: %s",
|
||||
validationStateToString(_validationState).c_str());
|
||||
return true; // Continue anyway
|
||||
}
|
||||
}
|
||||
|
||||
void FirmwareValidator::startRuntimeValidation() {
|
||||
if (_validationState != FirmwareValidationState::RUNTIME_TESTING) {
|
||||
LOG_WARNING("⚠️ Runtime validation called in wrong state: %s",
|
||||
validationStateToString(_validationState).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("🏃 Starting extended runtime validation (%lu ms timeout)",
|
||||
_config.runtimeTimeoutMs);
|
||||
|
||||
_validationStartTime = millis();
|
||||
|
||||
// Create validation timer
|
||||
_validationTimer = xTimerCreate(
|
||||
"FW_Validation",
|
||||
pdMS_TO_TICKS(_config.runtimeTimeoutMs),
|
||||
pdFALSE, // One-shot timer
|
||||
this,
|
||||
validationTimerCallback
|
||||
);
|
||||
|
||||
if (_validationTimer) {
|
||||
xTimerStart(_validationTimer, 0);
|
||||
} else {
|
||||
LOG_ERROR("❌ Failed to create validation timer");
|
||||
handleValidationFailure("Timer creation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create monitoring task for continuous health checks
|
||||
xTaskCreatePinnedToCore(
|
||||
monitoringTaskFunction,
|
||||
"FW_Monitor",
|
||||
4096,
|
||||
this,
|
||||
4, // Higher priority than health monitor
|
||||
&_monitoringTask,
|
||||
0 // Core 0
|
||||
);
|
||||
|
||||
if (!_monitoringTask) {
|
||||
LOG_ERROR("❌ Failed to create monitoring task");
|
||||
handleValidationFailure("Monitoring task creation failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup watchdog if enabled
|
||||
if (_config.enableWatchdog) {
|
||||
setupWatchdog();
|
||||
}
|
||||
|
||||
LOG_INFO("✅ Runtime validation started - monitoring system health...");
|
||||
}
|
||||
|
||||
void FirmwareValidator::commitFirmware() {
|
||||
if (_validationState == FirmwareValidationState::VALIDATED) {
|
||||
LOG_INFO("✅ Firmware already committed");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("💾 Committing firmware as valid and stable...");
|
||||
|
||||
// Mark current partition as valid boot partition
|
||||
esp_err_t err = esp_ota_set_boot_partition(_runningPartition);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("❌ Failed to set boot partition: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
_validationState = FirmwareValidationState::VALIDATED;
|
||||
resetValidationCounters();
|
||||
saveValidationState();
|
||||
|
||||
// Clean up validation resources
|
||||
if (_validationTimer) {
|
||||
xTimerDelete(_validationTimer, portMAX_DELAY);
|
||||
_validationTimer = nullptr;
|
||||
}
|
||||
|
||||
if (_monitoringTask) {
|
||||
vTaskDelete(_monitoringTask);
|
||||
_monitoringTask = nullptr;
|
||||
}
|
||||
|
||||
LOG_INFO("🎉 Firmware successfully committed! System is now stable.");
|
||||
}
|
||||
|
||||
void FirmwareValidator::rollbackFirmware() {
|
||||
LOG_WARNING("🔄 Manual firmware rollback requested");
|
||||
handleValidationFailure("Manual rollback requested");
|
||||
}
|
||||
|
||||
// ... [rest of implementation continues with all the private methods] ...
|
||||
|
||||
bool FirmwareValidator::initializeNVS() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("❌ Failed to initialize NVS flash: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &_nvsHandle);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("❌ Failed to open NVS namespace: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FirmwareValidator::loadValidationState() {
|
||||
esp_err_t err;
|
||||
|
||||
// Load validation state
|
||||
uint8_t state = static_cast<uint8_t>(FirmwareValidationState::UNKNOWN);
|
||||
err = nvs_get_u8(_nvsHandle, NVS_STATE_KEY, &state);
|
||||
if (err == ESP_OK) {
|
||||
_validationState = static_cast<FirmwareValidationState>(state);
|
||||
LOG_DEBUG("📖 NVS validation state found: %s", validationStateToString(_validationState).c_str());
|
||||
} else {
|
||||
LOG_DEBUG("📖 No NVS validation state found, using UNKNOWN (error: %s)", esp_err_to_name(err));
|
||||
_validationState = FirmwareValidationState::UNKNOWN;
|
||||
}
|
||||
|
||||
// Load retry counts
|
||||
nvs_get_u8(_nvsHandle, NVS_RETRY_COUNT_KEY, &_startupRetryCount);
|
||||
nvs_get_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, &_runtimeFailureCount);
|
||||
|
||||
LOG_DEBUG("📖 Loaded validation state: %s (retries: %d, failures: %d)",
|
||||
validationStateToString(_validationState).c_str(),
|
||||
_startupRetryCount, _runtimeFailureCount);
|
||||
}
|
||||
|
||||
void FirmwareValidator::saveValidationState() {
|
||||
esp_err_t err;
|
||||
|
||||
// Save validation state
|
||||
err = nvs_set_u8(_nvsHandle, NVS_STATE_KEY, static_cast<uint8_t>(_validationState));
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("❌ Failed to save validation state: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
// Save retry counts
|
||||
nvs_set_u8(_nvsHandle, NVS_RETRY_COUNT_KEY, _startupRetryCount);
|
||||
nvs_set_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, _runtimeFailureCount);
|
||||
|
||||
// Save timestamp
|
||||
unsigned long currentTime = millis();
|
||||
nvs_set_u32(_nvsHandle, NVS_LAST_BOOT_KEY, currentTime);
|
||||
|
||||
// Commit changes
|
||||
err = nvs_commit(_nvsHandle);
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("❌ Failed to commit NVS changes: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
LOG_DEBUG("💾 Saved validation state: %s", validationStateToString(_validationState).c_str());
|
||||
}
|
||||
|
||||
bool FirmwareValidator::initializePartitions() {
|
||||
_runningPartition = esp_ota_get_running_partition();
|
||||
if (!_runningPartition) {
|
||||
LOG_ERROR("❌ Failed to get running partition");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find the other OTA partition (backup)
|
||||
esp_partition_iterator_t iterator = esp_partition_find(ESP_PARTITION_TYPE_APP,
|
||||
ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
|
||||
if (iterator) {
|
||||
const esp_partition_t* ota0 = esp_partition_get(iterator);
|
||||
esp_partition_iterator_release(iterator);
|
||||
|
||||
iterator = esp_partition_find(ESP_PARTITION_TYPE_APP,
|
||||
ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
|
||||
if (iterator) {
|
||||
const esp_partition_t* ota1 = esp_partition_get(iterator);
|
||||
esp_partition_iterator_release(iterator);
|
||||
|
||||
// Determine which is the backup partition
|
||||
if (_runningPartition == ota0) {
|
||||
_backupPartition = ota1;
|
||||
} else {
|
||||
_backupPartition = ota0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!_backupPartition) {
|
||||
LOG_ERROR("❌ Failed to find backup partition");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool FirmwareValidator::performBasicHealthCheck() {
|
||||
LOG_VERBOSE("🔍 Performing basic startup health check...");
|
||||
|
||||
// Check if health monitor is available
|
||||
if (!_healthMonitor) {
|
||||
LOG_ERROR("❌ Health monitor not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check critical subsystems only
|
||||
bool bellEngineOk = (_healthMonitor->checkSubsystemHealth("BellEngine") == HealthStatus::HEALTHY);
|
||||
bool outputManagerOk = (_healthMonitor->checkSubsystemHealth("OutputManager") == HealthStatus::HEALTHY);
|
||||
bool configManagerOk = (_healthMonitor->checkSubsystemHealth("ConfigManager") == HealthStatus::HEALTHY);
|
||||
bool fileManagerOk = (_healthMonitor->checkSubsystemHealth("FileManager") == HealthStatus::HEALTHY);
|
||||
|
||||
bool basicHealthOk = bellEngineOk && outputManagerOk && configManagerOk && fileManagerOk;
|
||||
|
||||
if (!basicHealthOk) {
|
||||
LOG_ERROR("❌ Basic health check failed:");
|
||||
if (!bellEngineOk) LOG_ERROR(" - BellEngine: FAILED");
|
||||
if (!outputManagerOk) LOG_ERROR(" - OutputManager: FAILED");
|
||||
if (!configManagerOk) LOG_ERROR(" - ConfigManager: FAILED");
|
||||
if (!fileManagerOk) LOG_ERROR(" - FileManager: FAILED");
|
||||
} else {
|
||||
LOG_VERBOSE("✅ Basic health check passed");
|
||||
}
|
||||
|
||||
return basicHealthOk;
|
||||
}
|
||||
|
||||
bool FirmwareValidator::performRuntimeHealthCheck() {
|
||||
LOG_VERBOSE("🔍 Performing comprehensive runtime health check...");
|
||||
|
||||
if (!_healthMonitor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Perform full health check
|
||||
HealthStatus overallHealth = _healthMonitor->performFullHealthCheck();
|
||||
uint8_t criticalFailures = _healthMonitor->getCriticalFailureCount();
|
||||
|
||||
bool runtimeHealthOk = (overallHealth == HealthStatus::HEALTHY || overallHealth == HealthStatus::WARNING)
|
||||
&& (criticalFailures == 0);
|
||||
|
||||
if (!runtimeHealthOk) {
|
||||
LOG_WARNING("⚠️ Runtime health check failed - Critical failures: %d, Overall: %s",
|
||||
criticalFailures,
|
||||
(overallHealth == HealthStatus::HEALTHY) ? "HEALTHY" :
|
||||
(overallHealth == HealthStatus::WARNING) ? "WARNING" :
|
||||
(overallHealth == HealthStatus::CRITICAL) ? "CRITICAL" : "FAILED");
|
||||
}
|
||||
|
||||
return runtimeHealthOk;
|
||||
}
|
||||
|
||||
void FirmwareValidator::validationTimerCallback(TimerHandle_t timer) {
|
||||
FirmwareValidator* validator = static_cast<FirmwareValidator*>(pvTimerGetTimerID(timer));
|
||||
|
||||
LOG_INFO("⏰ Runtime validation timeout reached - committing firmware");
|
||||
validator->handleValidationSuccess();
|
||||
}
|
||||
|
||||
void FirmwareValidator::monitoringTaskFunction(void* parameter) {
|
||||
FirmwareValidator* validator = static_cast<FirmwareValidator*>(parameter);
|
||||
LOG_INFO("🔍 Firmware validation monitoring task started on Core %d", xPortGetCoreID());
|
||||
|
||||
validator->monitoringLoop();
|
||||
|
||||
// Task should not reach here normally
|
||||
LOG_WARNING("⚠️ Firmware validation monitoring task ended unexpectedly");
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void FirmwareValidator::monitoringLoop() {
|
||||
while (_validationState == FirmwareValidationState::RUNTIME_TESTING) {
|
||||
// Feed watchdog if enabled
|
||||
if (_config.enableWatchdog) {
|
||||
feedWatchdog();
|
||||
}
|
||||
|
||||
// Perform runtime health check
|
||||
bool healthOk = performRuntimeHealthCheck();
|
||||
|
||||
if (!healthOk) {
|
||||
_runtimeFailureCount++;
|
||||
LOG_WARNING("⚠️ Runtime health check failed (%d/%d failures)",
|
||||
_runtimeFailureCount, _config.maxRuntimeFailures);
|
||||
|
||||
if (_runtimeFailureCount >= _config.maxRuntimeFailures) {
|
||||
LOG_ERROR("💥 Maximum runtime failures exceeded - triggering rollback");
|
||||
handleValidationFailure("Too many runtime health check failures");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Reset failure count on successful health check
|
||||
if (_runtimeFailureCount > 0) {
|
||||
_runtimeFailureCount = 0;
|
||||
LOG_INFO("✅ Runtime health recovered - reset failure count");
|
||||
}
|
||||
}
|
||||
|
||||
// Wait before next health check
|
||||
vTaskDelay(pdMS_TO_TICKS(_config.healthCheckIntervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
void FirmwareValidator::handleValidationSuccess() {
|
||||
LOG_INFO("🎉 Firmware validation completed successfully!");
|
||||
commitFirmware();
|
||||
}
|
||||
|
||||
void FirmwareValidator::handleValidationFailure(const String& reason) {
|
||||
LOG_ERROR("💥 Firmware validation FAILED: %s", reason.c_str());
|
||||
LOG_ERROR("🔄 Initiating firmware rollback...");
|
||||
|
||||
_validationState = FirmwareValidationState::FAILED_RUNTIME;
|
||||
saveValidationState();
|
||||
|
||||
executeRollback();
|
||||
}
|
||||
|
||||
void FirmwareValidator::executeRollback() {
|
||||
LOG_WARNING("🔄 Executing firmware rollback to previous version...");
|
||||
|
||||
// Clean up validation resources first
|
||||
if (_validationTimer) {
|
||||
xTimerDelete(_validationTimer, portMAX_DELAY);
|
||||
_validationTimer = nullptr;
|
||||
}
|
||||
|
||||
if (_monitoringTask) {
|
||||
vTaskDelete(_monitoringTask);
|
||||
_monitoringTask = nullptr;
|
||||
}
|
||||
|
||||
// Mark current firmware as invalid and rollback
|
||||
esp_err_t err = esp_ota_mark_app_invalid_rollback_and_reboot();
|
||||
|
||||
if (err != ESP_OK) {
|
||||
LOG_ERROR("❌ Failed to rollback firmware: %s", esp_err_to_name(err));
|
||||
LOG_ERROR("💀 System may be in unstable state - manual intervention required");
|
||||
|
||||
// If rollback fails, try manual reboot to backup partition
|
||||
LOG_WARNING("🆘 Attempting manual reboot to backup partition...");
|
||||
|
||||
if (_backupPartition) {
|
||||
esp_ota_set_boot_partition(_backupPartition);
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
} else {
|
||||
LOG_ERROR("💀 No backup partition available - system halt");
|
||||
while(1) {
|
||||
delay(1000); // Hang here to prevent further damage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This point should not be reached as the device should reboot
|
||||
LOG_ERROR("💀 Rollback function returned unexpectedly");
|
||||
}
|
||||
|
||||
FirmwareInfo FirmwareValidator::getCurrentFirmwareInfo() const {
|
||||
FirmwareInfo info;
|
||||
|
||||
if (_configManager) {
|
||||
info.version = _configManager->getFwVersion();
|
||||
info.buildDate = __DATE__ " " __TIME__;
|
||||
// Add more info as needed
|
||||
}
|
||||
|
||||
info.isValid = (_validationState == FirmwareValidationState::VALIDATED);
|
||||
info.isTesting = isInTestingMode();
|
||||
info.bootCount = getBootCount();
|
||||
info.lastBootTime = millis();
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
FirmwareInfo FirmwareValidator::getBackupFirmwareInfo() const {
|
||||
FirmwareInfo info;
|
||||
|
||||
// This would require reading partition metadata
|
||||
// For now, return basic info
|
||||
info.version = "Unknown";
|
||||
info.isValid = isPartitionValid(_backupPartition);
|
||||
info.isTesting = false;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
bool FirmwareValidator::isHealthy() const {
|
||||
// Check if validator itself is in a good state
|
||||
bool nvsOk = (_nvsHandle != 0);
|
||||
bool partitionsOk = (_runningPartition != nullptr && _backupPartition != nullptr);
|
||||
bool dependenciesOk = (_healthMonitor != nullptr && _configManager != nullptr);
|
||||
bool stateOk = (_validationState != FirmwareValidationState::FAILED_STARTUP &&
|
||||
_validationState != FirmwareValidationState::FAILED_RUNTIME);
|
||||
|
||||
return nvsOk && partitionsOk && dependenciesOk && stateOk;
|
||||
}
|
||||
|
||||
const esp_partition_t* FirmwareValidator::getRunningPartition() const {
|
||||
return _runningPartition;
|
||||
}
|
||||
|
||||
const esp_partition_t* FirmwareValidator::getBackupPartition() const {
|
||||
return _backupPartition;
|
||||
}
|
||||
|
||||
bool FirmwareValidator::isNewFirmwarePending() const {
|
||||
// Check if there's a new firmware in the backup partition that hasn't been tested
|
||||
return (_validationState == FirmwareValidationState::STARTUP_PENDING ||
|
||||
_validationState == FirmwareValidationState::STARTUP_RUNNING ||
|
||||
_validationState == FirmwareValidationState::RUNTIME_TESTING);
|
||||
}
|
||||
|
||||
String FirmwareValidator::validationStateToString(FirmwareValidationState state) const {
|
||||
switch (state) {
|
||||
case FirmwareValidationState::UNKNOWN:
|
||||
return "UNKNOWN";
|
||||
case FirmwareValidationState::STARTUP_PENDING:
|
||||
return "STARTUP_PENDING";
|
||||
case FirmwareValidationState::STARTUP_RUNNING:
|
||||
return "STARTUP_RUNNING";
|
||||
case FirmwareValidationState::RUNTIME_TESTING:
|
||||
return "RUNTIME_TESTING";
|
||||
case FirmwareValidationState::VALIDATED:
|
||||
return "VALIDATED";
|
||||
case FirmwareValidationState::FAILED_STARTUP:
|
||||
return "FAILED_STARTUP";
|
||||
case FirmwareValidationState::FAILED_RUNTIME:
|
||||
return "FAILED_RUNTIME";
|
||||
case FirmwareValidationState::ROLLED_BACK:
|
||||
return "ROLLED_BACK";
|
||||
default:
|
||||
return "INVALID";
|
||||
}
|
||||
}
|
||||
|
||||
String FirmwareValidator::getPartitionLabel(const esp_partition_t* partition) const {
|
||||
if (!partition) {
|
||||
return "NULL";
|
||||
}
|
||||
|
||||
String label = String(partition->label);
|
||||
if (label.isEmpty()) {
|
||||
// Generate label based on subtype
|
||||
switch (partition->subtype) {
|
||||
case ESP_PARTITION_SUBTYPE_APP_OTA_0:
|
||||
label = "ota_0";
|
||||
break;
|
||||
case ESP_PARTITION_SUBTYPE_APP_OTA_1:
|
||||
label = "ota_1";
|
||||
break;
|
||||
default:
|
||||
label = "app_" + String(partition->subtype);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
bool FirmwareValidator::isPartitionValid(const esp_partition_t* partition) const {
|
||||
if (!partition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if partition has valid app header
|
||||
esp_image_header_t header;
|
||||
esp_err_t err = esp_partition_read(partition, 0, &header, sizeof(header));
|
||||
|
||||
if (err != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check magic number
|
||||
return (header.magic == ESP_IMAGE_HEADER_MAGIC);
|
||||
}
|
||||
|
||||
unsigned long FirmwareValidator::getBootCount() const {
|
||||
uint32_t bootCount = 0;
|
||||
nvs_get_u32(_nvsHandle, NVS_BOOT_COUNT_KEY, &bootCount);
|
||||
return bootCount;
|
||||
}
|
||||
|
||||
void FirmwareValidator::incrementBootCount() {
|
||||
uint32_t bootCount = getBootCount();
|
||||
bootCount++;
|
||||
nvs_set_u32(_nvsHandle, NVS_BOOT_COUNT_KEY, bootCount);
|
||||
nvs_commit(_nvsHandle);
|
||||
|
||||
LOG_DEBUG("📊 Boot count: %lu", bootCount);
|
||||
}
|
||||
|
||||
void FirmwareValidator::resetValidationCounters() {
|
||||
_startupRetryCount = 0;
|
||||
_runtimeFailureCount = 0;
|
||||
|
||||
// Also reset in NVS
|
||||
nvs_set_u8(_nvsHandle, NVS_RETRY_COUNT_KEY, 0);
|
||||
nvs_set_u8(_nvsHandle, NVS_FAILURE_COUNT_KEY, 0);
|
||||
nvs_commit(_nvsHandle);
|
||||
|
||||
LOG_DEBUG("🔄 Reset validation counters");
|
||||
}
|
||||
|
||||
void FirmwareValidator::setupWatchdog() {
|
||||
// Check if watchdog is already initialized
|
||||
esp_task_wdt_config_t config = {
|
||||
.timeout_ms = _config.watchdogTimeoutMs,
|
||||
.idle_core_mask = (1 << portNUM_PROCESSORS) - 1,
|
||||
.trigger_panic = true
|
||||
};
|
||||
|
||||
esp_err_t err = esp_task_wdt_init(&config);
|
||||
if (err == ESP_ERR_INVALID_STATE) {
|
||||
LOG_DEBUG("🐕 Watchdog already initialized - skipping init");
|
||||
} else if (err != ESP_OK) {
|
||||
LOG_WARNING("⚠️ Failed to initialize task watchdog: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to add current task to watchdog
|
||||
err = esp_task_wdt_add(NULL);
|
||||
if (err == ESP_ERR_INVALID_ARG) {
|
||||
LOG_DEBUG("🐕 Task already added to watchdog");
|
||||
} else if (err != ESP_OK) {
|
||||
LOG_WARNING("⚠️ Failed to add task to watchdog: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("🐕 Watchdog enabled with %lu second timeout", _config.watchdogTimeoutMs / 1000);
|
||||
}
|
||||
|
||||
void FirmwareValidator::feedWatchdog() {
|
||||
esp_task_wdt_reset();
|
||||
}
|
||||
392
vesper/src/FirmwareValidator/FirmwareValidator.hpp
Normal file
392
vesper/src/FirmwareValidator/FirmwareValidator.hpp
Normal file
@@ -0,0 +1,392 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* FIRMWAREVALIDATOR.HPP - Bulletproof Firmware Update Validation System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🛡️ THE FIRMWARE SAFETY GUARDIAN OF VESPER 🛡️
|
||||
*
|
||||
* This class implements a bulletproof firmware update system using ESP32's dual
|
||||
* partition architecture. It ensures that firmware updates are safe, validated,
|
||||
* and can automatically rollback if critical systems fail during startup or runtime.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Dual partition management (OTA_0 and OTA_1)
|
||||
* • Immediate startup health checks within first few seconds
|
||||
* • Progressive validation from basic → comprehensive health checks
|
||||
* • Automatic rollback to previous working firmware on critical failures
|
||||
* • Testing mode for new firmware validation
|
||||
* • Permanent commit only after full health validation
|
||||
*
|
||||
* 🔄 FIRMWARE UPDATE FLOW:
|
||||
* 1. Download new firmware to inactive partition
|
||||
* 2. Boot new firmware in "TESTING" mode
|
||||
* 3. Immediate basic health check (within 10 seconds)
|
||||
* - If FAIL → Automatic rollback via esp_ota_mark_app_invalid_rollback_and_reboot()
|
||||
* 4. Extended validation period (configurable, default 5 minutes)
|
||||
* - Continuous health monitoring of all critical subsystems
|
||||
* - If PASS → Mark partition as valid via esp_ota_set_boot_partition()
|
||||
* - If FAIL → Rollback to previous firmware
|
||||
* 5. Normal operation mode
|
||||
*
|
||||
* 🚨 SAFETY MECHANISMS:
|
||||
* • Watchdog integration to handle complete system hangs
|
||||
* • Multi-level health checks (startup → runtime → comprehensive)
|
||||
* • Configurable validation timeouts and retry counts
|
||||
* • Persistent validation state tracking
|
||||
* • Emergency rollback triggers
|
||||
*
|
||||
* 🔍 VALIDATION LEVELS:
|
||||
* • STARTUP: Basic system initialization (I2C, SD, GPIO)
|
||||
* • RUNTIME: Core functionality (BellEngine, OutputManager, Player)
|
||||
* • COMPREHENSIVE: Full system health (Network, MQTT, Telemetry)
|
||||
*
|
||||
* 📋 VERSION: 1.0 (Initial firmware validation system)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_partition.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <nvs.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <freertos/timers.h>
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class HealthMonitor;
|
||||
class ConfigManager;
|
||||
|
||||
/**
|
||||
* @enum FirmwareValidationState
|
||||
* @brief Current state of firmware validation process
|
||||
*/
|
||||
enum class FirmwareValidationState {
|
||||
UNKNOWN, // Initial state, validation status unknown
|
||||
STARTUP_PENDING, // New firmware booted, startup validation pending
|
||||
STARTUP_RUNNING, // Currently performing startup validation
|
||||
RUNTIME_TESTING, // Startup passed, now in extended runtime testing
|
||||
VALIDATED, // Firmware fully validated and committed
|
||||
FAILED_STARTUP, // Failed startup validation, rollback triggered
|
||||
FAILED_RUNTIME, // Failed runtime validation, rollback triggered
|
||||
ROLLED_BACK // Successfully rolled back to previous firmware
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum ValidationLevel
|
||||
* @brief Different levels of health validation
|
||||
*/
|
||||
enum class ValidationLevel {
|
||||
STARTUP, // Basic system initialization checks
|
||||
RUNTIME, // Core functionality validation
|
||||
COMPREHENSIVE // Full system health validation
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct FirmwareInfo
|
||||
* @brief Information about a firmware partition
|
||||
*/
|
||||
struct FirmwareInfo {
|
||||
String version;
|
||||
String buildDate;
|
||||
String commitHash;
|
||||
bool isValid;
|
||||
bool isTesting;
|
||||
unsigned long bootCount;
|
||||
unsigned long lastBootTime;
|
||||
|
||||
FirmwareInfo() : version(""), buildDate(""), commitHash(""),
|
||||
isValid(false), isTesting(false), bootCount(0), lastBootTime(0) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct ValidationConfig
|
||||
* @brief Configuration parameters for firmware validation
|
||||
*/
|
||||
struct ValidationConfig {
|
||||
unsigned long startupTimeoutMs = 10000; // 10 seconds for startup validation
|
||||
unsigned long runtimeTimeoutMs = 300000; // 5 minutes for runtime validation
|
||||
unsigned long healthCheckIntervalMs = 30000; // 30 seconds between health checks
|
||||
uint8_t maxStartupRetries = 3; // Max startup failures before rollback
|
||||
uint8_t maxRuntimeFailures = 5; // Max runtime failures before rollback
|
||||
bool enableWatchdog = true; // Enable watchdog protection
|
||||
unsigned long watchdogTimeoutMs = 30000; // 30 seconds watchdog timeout
|
||||
};
|
||||
|
||||
/**
|
||||
* @class FirmwareValidator
|
||||
* @brief Bulletproof firmware update validation and rollback system
|
||||
*
|
||||
* This class manages the complete firmware validation lifecycle, from initial
|
||||
* boot to full system validation, with automatic rollback capabilities for
|
||||
* failed firmware updates.
|
||||
*/
|
||||
class FirmwareValidator {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Constructor - Initialize firmware validation system
|
||||
*/
|
||||
FirmwareValidator();
|
||||
|
||||
/**
|
||||
* @brief Destructor - Clean up resources
|
||||
*/
|
||||
~FirmwareValidator();
|
||||
|
||||
/**
|
||||
* @brief Initialize firmware validation system
|
||||
* @param healthMonitor Reference to system health monitor
|
||||
* @param configManager Reference to configuration manager
|
||||
* @return true if initialization successful
|
||||
*/
|
||||
bool begin(HealthMonitor* healthMonitor, ConfigManager* configManager);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// STARTUP VALIDATION (CRITICAL - MUST BE CALLED EARLY IN setup())
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Perform immediate startup validation
|
||||
*
|
||||
* This MUST be called early in setup() before initializing other subsystems.
|
||||
* If validation fails, the system will automatically rollback and reboot.
|
||||
*
|
||||
* @return true if startup validation passes, false if rollback was triggered
|
||||
*/
|
||||
bool performStartupValidation();
|
||||
|
||||
/**
|
||||
* @brief Check if current firmware is in testing mode
|
||||
* @return true if firmware is being tested and not yet committed
|
||||
*/
|
||||
bool isInTestingMode() const { return _validationState == FirmwareValidationState::STARTUP_PENDING ||
|
||||
_validationState == FirmwareValidationState::STARTUP_RUNNING ||
|
||||
_validationState == FirmwareValidationState::RUNTIME_TESTING; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// RUNTIME VALIDATION & MONITORING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Start extended runtime validation
|
||||
*
|
||||
* Call this after all subsystems are initialized to begin the extended
|
||||
* validation period before committing the firmware.
|
||||
*/
|
||||
void startRuntimeValidation();
|
||||
|
||||
/**
|
||||
* @brief Manually trigger firmware validation completion
|
||||
*
|
||||
* This commits the current firmware as valid and stable.
|
||||
* Normally called automatically after successful runtime validation.
|
||||
*/
|
||||
void commitFirmware();
|
||||
|
||||
/**
|
||||
* @brief Manually trigger firmware rollback
|
||||
*
|
||||
* Forces an immediate rollback to the previous firmware version.
|
||||
* Use this for emergency situations or failed validations.
|
||||
*/
|
||||
void rollbackFirmware();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// STATUS & INFORMATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Get current validation state
|
||||
* @return Current firmware validation state
|
||||
*/
|
||||
FirmwareValidationState getValidationState() const { return _validationState; }
|
||||
|
||||
/**
|
||||
* @brief Get information about the current running firmware
|
||||
* @return Firmware information structure
|
||||
*/
|
||||
FirmwareInfo getCurrentFirmwareInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Get information about the backup firmware partition
|
||||
* @return Firmware information structure for backup partition
|
||||
*/
|
||||
FirmwareInfo getBackupFirmwareInfo() const;
|
||||
|
||||
/**
|
||||
* @brief Get validation configuration
|
||||
* @return Current validation configuration
|
||||
*/
|
||||
const ValidationConfig& getValidationConfig() const { return _config; }
|
||||
|
||||
/**
|
||||
* @brief Set validation configuration
|
||||
* @param config New validation configuration
|
||||
*/
|
||||
void setValidationConfig(const ValidationConfig& config) { _config = config; }
|
||||
|
||||
/**
|
||||
* @brief Check if firmware validation system is healthy
|
||||
* @return true if validation system is functioning properly
|
||||
*/
|
||||
bool isHealthy() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PARTITION MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Get the currently running OTA partition
|
||||
* @return Pointer to running partition, nullptr on error
|
||||
*/
|
||||
const esp_partition_t* getRunningPartition() const;
|
||||
|
||||
/**
|
||||
* @brief Get the backup (inactive) OTA partition
|
||||
* @return Pointer to backup partition, nullptr on error
|
||||
*/
|
||||
const esp_partition_t* getBackupPartition() const;
|
||||
|
||||
/**
|
||||
* @brief Check if a new firmware update is available for testing
|
||||
* @return true if there's a new firmware waiting to be validated
|
||||
*/
|
||||
bool isNewFirmwarePending() const;
|
||||
|
||||
private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE MEMBERS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
HealthMonitor* _healthMonitor = nullptr;
|
||||
ConfigManager* _configManager = nullptr;
|
||||
|
||||
FirmwareValidationState _validationState = FirmwareValidationState::UNKNOWN;
|
||||
ValidationConfig _config;
|
||||
|
||||
// Partition information
|
||||
const esp_partition_t* _runningPartition = nullptr;
|
||||
const esp_partition_t* _backupPartition = nullptr;
|
||||
|
||||
// Validation tracking
|
||||
unsigned long _validationStartTime = 0;
|
||||
uint8_t _startupRetryCount = 0;
|
||||
uint8_t _runtimeFailureCount = 0;
|
||||
TimerHandle_t _validationTimer = nullptr;
|
||||
TaskHandle_t _monitoringTask = nullptr;
|
||||
|
||||
// NVS handles for persistent storage
|
||||
nvs_handle_t _nvsHandle = 0;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Initialize NVS storage for validation state persistence
|
||||
*/
|
||||
bool initializeNVS();
|
||||
|
||||
/**
|
||||
* @brief Load validation state from NVS
|
||||
*/
|
||||
void loadValidationState();
|
||||
|
||||
/**
|
||||
* @brief Save validation state to NVS
|
||||
*/
|
||||
void saveValidationState();
|
||||
|
||||
/**
|
||||
* @brief Initialize ESP32 partition information
|
||||
*/
|
||||
bool initializePartitions();
|
||||
|
||||
/**
|
||||
* @brief Perform basic startup health checks
|
||||
*/
|
||||
bool performBasicHealthCheck();
|
||||
|
||||
/**
|
||||
* @brief Perform comprehensive runtime health checks
|
||||
*/
|
||||
bool performRuntimeHealthCheck();
|
||||
|
||||
/**
|
||||
* @brief Timer callback for validation timeout
|
||||
*/
|
||||
static void validationTimerCallback(TimerHandle_t timer);
|
||||
|
||||
/**
|
||||
* @brief Monitoring task for continuous health checks during validation
|
||||
*/
|
||||
static void monitoringTaskFunction(void* parameter);
|
||||
|
||||
/**
|
||||
* @brief Main monitoring loop
|
||||
*/
|
||||
void monitoringLoop();
|
||||
|
||||
/**
|
||||
* @brief Handle validation success
|
||||
*/
|
||||
void handleValidationSuccess();
|
||||
|
||||
/**
|
||||
* @brief Handle validation failure
|
||||
*/
|
||||
void handleValidationFailure(const String& reason);
|
||||
|
||||
/**
|
||||
* @brief Execute firmware rollback
|
||||
*/
|
||||
void executeRollback();
|
||||
|
||||
/**
|
||||
* @brief Convert validation state to string
|
||||
*/
|
||||
String validationStateToString(FirmwareValidationState state) const;
|
||||
|
||||
/**
|
||||
* @brief Get partition label string
|
||||
*/
|
||||
String getPartitionLabel(const esp_partition_t* partition) const;
|
||||
|
||||
/**
|
||||
* @brief Check if partition has valid firmware
|
||||
*/
|
||||
bool isPartitionValid(const esp_partition_t* partition) const;
|
||||
|
||||
/**
|
||||
* @brief Get boot count for current session
|
||||
*/
|
||||
unsigned long getBootCount() const;
|
||||
|
||||
/**
|
||||
* @brief Increment boot count
|
||||
*/
|
||||
void incrementBootCount();
|
||||
|
||||
/**
|
||||
* @brief Reset validation counters
|
||||
*/
|
||||
void resetValidationCounters();
|
||||
|
||||
/**
|
||||
* @brief Setup watchdog protection
|
||||
*/
|
||||
void setupWatchdog();
|
||||
|
||||
/**
|
||||
* @brief Feed the watchdog timer
|
||||
*/
|
||||
void feedWatchdog();
|
||||
};
|
||||
428
vesper/src/HealthMonitor/HealthMonitor.cpp
Normal file
428
vesper/src/HealthMonitor/HealthMonitor.cpp
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* HEALTHMONITOR.CPP - System Health Monitoring Implementation
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#include "HealthMonitor.hpp"
|
||||
#include "../BellEngine/BellEngine.hpp"
|
||||
#include "../OutputManager/OutputManager.hpp"
|
||||
#include "../Communication/Communication.hpp"
|
||||
#include "../Player/Player.hpp"
|
||||
#include "../TimeKeeper/TimeKeeper.hpp"
|
||||
#include "../Telemetry/Telemetry.hpp"
|
||||
#include "../OTAManager/OTAManager.hpp"
|
||||
#include "../Networking/Networking.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../FileManager/FileManager.hpp"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
HealthMonitor::HealthMonitor() {
|
||||
initializeSubsystemHealth();
|
||||
}
|
||||
|
||||
HealthMonitor::~HealthMonitor() {
|
||||
if (_monitoringTaskHandle != nullptr) {
|
||||
vTaskDelete(_monitoringTaskHandle);
|
||||
_monitoringTaskHandle = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool HealthMonitor::begin() {
|
||||
LOG_INFO("🏥 Initializing Health Monitor System");
|
||||
|
||||
// Create monitoring task if auto-monitoring is enabled
|
||||
if (_autoMonitoring) {
|
||||
xTaskCreatePinnedToCore(
|
||||
monitoringTask,
|
||||
"HealthMonitor",
|
||||
4096,
|
||||
this,
|
||||
3, // Medium priority
|
||||
&_monitoringTaskHandle,
|
||||
0 // Core 0 (different from BellEngine which uses Core 1)
|
||||
);
|
||||
|
||||
if (_monitoringTaskHandle != nullptr) {
|
||||
LOG_INFO("✅ Health Monitor initialized with automatic monitoring");
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("❌ Failed to create Health Monitor task");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("✅ Health Monitor initialized (manual mode)");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void HealthMonitor::initializeSubsystemHealth() {
|
||||
// Initialize all subsystem health entries
|
||||
// Mark critical subsystems that must be healthy for operation
|
||||
|
||||
_subsystemHealth["BellEngine"] = SubsystemHealth("BellEngine", true);
|
||||
_subsystemHealth["OutputManager"] = SubsystemHealth("OutputManager", true);
|
||||
_subsystemHealth["ConfigManager"] = SubsystemHealth("ConfigManager", true);
|
||||
_subsystemHealth["FileManager"] = SubsystemHealth("FileManager", true);
|
||||
_subsystemHealth["Communication"] = SubsystemHealth("Communication", false); // Non-critical
|
||||
_subsystemHealth["Player"] = SubsystemHealth("Player", true);
|
||||
_subsystemHealth["TimeKeeper"] = SubsystemHealth("TimeKeeper", false); // Non-critical
|
||||
_subsystemHealth["Telemetry"] = SubsystemHealth("Telemetry", false); // Non-critical
|
||||
_subsystemHealth["OTAManager"] = SubsystemHealth("OTAManager", false); // Non-critical
|
||||
_subsystemHealth["Networking"] = SubsystemHealth("Networking", false); // Non-critical
|
||||
|
||||
LOG_DEBUG("🏗️ Initialized health monitoring for %d subsystems", _subsystemHealth.size());
|
||||
}
|
||||
|
||||
void HealthMonitor::monitoringTask(void* parameter) {
|
||||
HealthMonitor* monitor = static_cast<HealthMonitor*>(parameter);
|
||||
LOG_INFO("🏥 Health Monitor task started on Core %d", xPortGetCoreID());
|
||||
|
||||
while (true) {
|
||||
monitor->monitoringLoop();
|
||||
vTaskDelay(pdMS_TO_TICKS(monitor->_healthCheckInterval));
|
||||
}
|
||||
}
|
||||
|
||||
void HealthMonitor::monitoringLoop() {
|
||||
|
||||
if (_player) {
|
||||
if (_player->_status != PlayerStatus::STOPPED) {
|
||||
LOG_VERBOSE("⏸️ Skipping health check during active playback");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_VERBOSE("🔍 Performing periodic health check...");
|
||||
|
||||
HealthStatus overallHealth = performFullHealthCheck();
|
||||
|
||||
// Log warnings for any unhealthy subsystems
|
||||
uint8_t criticalCount = getCriticalFailureCount();
|
||||
uint8_t warningCount = getWarningCount();
|
||||
|
||||
if (criticalCount > 0) {
|
||||
LOG_WARNING("🚨 Health Monitor: %d critical failures detected!", criticalCount);
|
||||
|
||||
// List critical failures
|
||||
for (const auto& [name, health] : _subsystemHealth) {
|
||||
if (health.status == HealthStatus::CRITICAL || health.status == HealthStatus::FAILED) {
|
||||
LOG_ERROR("❌ CRITICAL: %s - %s", name.c_str(), health.lastError.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Check if firmware rollback is recommended
|
||||
if (shouldRollbackFirmware()) {
|
||||
LOG_ERROR("🔄 FIRMWARE ROLLBACK RECOMMENDED - Too many critical failures");
|
||||
// In a real system, this would trigger an OTA rollback
|
||||
// For now, we just log the recommendation
|
||||
}
|
||||
} else if (warningCount > 0) {
|
||||
LOG_WARNING("⚠️ Health Monitor: %d warnings detected", warningCount);
|
||||
} else {
|
||||
LOG_VERBOSE("✅ All subsystems healthy");
|
||||
}
|
||||
}
|
||||
|
||||
HealthStatus HealthMonitor::performFullHealthCheck() {
|
||||
unsigned long startTime = millis();
|
||||
uint8_t checkedSystems = 0;
|
||||
|
||||
// Check BellEngine
|
||||
if (_bellEngine) {
|
||||
bool healthy = _bellEngine->isHealthy();
|
||||
updateSubsystemHealth("BellEngine",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::CRITICAL,
|
||||
healthy ? "" : "BellEngine health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check OutputManager
|
||||
if (_outputManager) {
|
||||
bool healthy = _outputManager->isHealthy();
|
||||
updateSubsystemHealth("OutputManager",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::CRITICAL,
|
||||
healthy ? "" : "OutputManager health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check Communication
|
||||
if (_communication) {
|
||||
bool healthy = _communication->isHealthy();
|
||||
updateSubsystemHealth("Communication",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::WARNING,
|
||||
healthy ? "" : "Communication health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check Player
|
||||
if (_player) {
|
||||
bool healthy = _player->isHealthy();
|
||||
updateSubsystemHealth("Player",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::CRITICAL,
|
||||
healthy ? "" : "Player health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check TimeKeeper
|
||||
if (_timeKeeper) {
|
||||
bool healthy = _timeKeeper->isHealthy();
|
||||
updateSubsystemHealth("TimeKeeper",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::WARNING,
|
||||
healthy ? "" : "TimeKeeper health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check Telemetry
|
||||
if (_telemetry) {
|
||||
bool healthy = _telemetry->isHealthy();
|
||||
updateSubsystemHealth("Telemetry",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::WARNING,
|
||||
healthy ? "" : "Telemetry health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check OTAManager
|
||||
if (_otaManager) {
|
||||
bool healthy = _otaManager->isHealthy();
|
||||
updateSubsystemHealth("OTAManager",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::WARNING,
|
||||
healthy ? "" : "OTAManager health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check Networking
|
||||
if (_networking) {
|
||||
bool healthy = _networking->isHealthy();
|
||||
updateSubsystemHealth("Networking",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::WARNING,
|
||||
healthy ? "" : "Networking health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check ConfigManager
|
||||
if (_configManager) {
|
||||
bool healthy = _configManager->isHealthy();
|
||||
updateSubsystemHealth("ConfigManager",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::CRITICAL,
|
||||
healthy ? "" : "ConfigManager health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
// Check FileManager
|
||||
if (_fileManager) {
|
||||
bool healthy = _fileManager->isHealthy();
|
||||
updateSubsystemHealth("FileManager",
|
||||
healthy ? HealthStatus::HEALTHY : HealthStatus::CRITICAL,
|
||||
healthy ? "" : "FileManager health check failed");
|
||||
checkedSystems++;
|
||||
}
|
||||
|
||||
unsigned long elapsed = millis() - startTime;
|
||||
LOG_VERBOSE("🔍 Health check completed: %d systems in %lums", checkedSystems, elapsed);
|
||||
|
||||
return calculateOverallHealth();
|
||||
}
|
||||
|
||||
HealthStatus HealthMonitor::checkSubsystemHealth(const String& subsystemName) {
|
||||
// Perform health check on specific subsystem
|
||||
auto it = _subsystemHealth.find(subsystemName);
|
||||
if (it == _subsystemHealth.end()) {
|
||||
LOG_WARNING("❓ Unknown subsystem: %s", subsystemName.c_str());
|
||||
return HealthStatus::FAILED;
|
||||
}
|
||||
|
||||
bool healthy = false;
|
||||
|
||||
// Check specific subsystem
|
||||
if (subsystemName == "BellEngine" && _bellEngine) {
|
||||
healthy = _bellEngine->isHealthy();
|
||||
} else if (subsystemName == "OutputManager" && _outputManager) {
|
||||
healthy = _outputManager->isHealthy();
|
||||
} else if (subsystemName == "Communication" && _communication) {
|
||||
healthy = _communication->isHealthy();
|
||||
} else if (subsystemName == "Player" && _player) {
|
||||
healthy = _player->isHealthy();
|
||||
} else if (subsystemName == "TimeKeeper" && _timeKeeper) {
|
||||
healthy = _timeKeeper->isHealthy();
|
||||
} else if (subsystemName == "Telemetry" && _telemetry) {
|
||||
healthy = _telemetry->isHealthy();
|
||||
} else if (subsystemName == "OTAManager" && _otaManager) {
|
||||
healthy = _otaManager->isHealthy();
|
||||
} else if (subsystemName == "Networking" && _networking) {
|
||||
healthy = _networking->isHealthy();
|
||||
} else if (subsystemName == "ConfigManager" && _configManager) {
|
||||
healthy = _configManager->isHealthy();
|
||||
} else if (subsystemName == "FileManager" && _fileManager) {
|
||||
healthy = _fileManager->isHealthy();
|
||||
} else {
|
||||
LOG_WARNING("🔌 Subsystem %s not connected to health monitor", subsystemName.c_str());
|
||||
return HealthStatus::FAILED;
|
||||
}
|
||||
|
||||
HealthStatus status = healthy ? HealthStatus::HEALTHY :
|
||||
(it->second.isCritical ? HealthStatus::CRITICAL : HealthStatus::WARNING);
|
||||
|
||||
updateSubsystemHealth(subsystemName, status,
|
||||
healthy ? "" : subsystemName + " health check failed");
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
const std::map<String, SubsystemHealth>& HealthMonitor::getAllSubsystemHealth() const {
|
||||
return _subsystemHealth;
|
||||
}
|
||||
|
||||
SubsystemHealth HealthMonitor::getSubsystemHealth(const String& subsystemName) const {
|
||||
auto it = _subsystemHealth.find(subsystemName);
|
||||
if (it != _subsystemHealth.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Return default unhealthy status for unknown subsystems
|
||||
SubsystemHealth unknown(subsystemName);
|
||||
unknown.status = HealthStatus::FAILED;
|
||||
unknown.lastError = "Subsystem not found";
|
||||
return unknown;
|
||||
}
|
||||
|
||||
bool HealthMonitor::isFirmwareStable() const {
|
||||
return areCriticalSubsystemsHealthy() && (getCriticalFailureCount() == 0);
|
||||
}
|
||||
|
||||
uint8_t HealthMonitor::getCriticalFailureCount() const {
|
||||
uint8_t count = 0;
|
||||
|
||||
for (const auto& [name, health] : _subsystemHealth) {
|
||||
if (health.isCritical &&
|
||||
(health.status == HealthStatus::CRITICAL || health.status == HealthStatus::FAILED)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
uint8_t HealthMonitor::getWarningCount() const {
|
||||
uint8_t count = 0;
|
||||
|
||||
for (const auto& [name, health] : _subsystemHealth) {
|
||||
if (health.status == HealthStatus::WARNING) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool HealthMonitor::shouldRollbackFirmware() const {
|
||||
uint8_t criticalFailures = getCriticalFailureCount();
|
||||
|
||||
// Rollback if more than 2 critical subsystems have failed
|
||||
// This is configurable based on system requirements
|
||||
const uint8_t MAX_CRITICAL_FAILURES = 2;
|
||||
|
||||
return criticalFailures > MAX_CRITICAL_FAILURES;
|
||||
}
|
||||
|
||||
String HealthMonitor::generateHealthReport() const {
|
||||
StaticJsonDocument<2048> doc;
|
||||
|
||||
doc["timestamp"] = millis();
|
||||
doc["overall_health"] = healthStatusToString(calculateOverallHealth());
|
||||
doc["critical_failures"] = getCriticalFailureCount();
|
||||
doc["warnings"] = getWarningCount();
|
||||
doc["firmware_stable"] = isFirmwareStable();
|
||||
doc["rollback_recommended"] = shouldRollbackFirmware();
|
||||
|
||||
JsonObject subsystems = doc.createNestedObject("subsystems");
|
||||
|
||||
for (const auto& [name, health] : _subsystemHealth) {
|
||||
JsonObject subsystem = subsystems.createNestedObject(name);
|
||||
subsystem["status"] = healthStatusToString(health.status);
|
||||
subsystem["critical"] = health.isCritical;
|
||||
subsystem["last_check"] = health.lastCheck;
|
||||
|
||||
if (!health.lastError.isEmpty()) {
|
||||
subsystem["error"] = health.lastError;
|
||||
}
|
||||
}
|
||||
|
||||
String report;
|
||||
serializeJsonPretty(doc, report);
|
||||
return report;
|
||||
}
|
||||
|
||||
String HealthMonitor::getHealthSummary() const {
|
||||
HealthStatus overall = calculateOverallHealth();
|
||||
uint8_t critical = getCriticalFailureCount();
|
||||
uint8_t warnings = getWarningCount();
|
||||
|
||||
String summary = "System Health: " + healthStatusToString(overall);
|
||||
|
||||
if (critical > 0) {
|
||||
summary += " (" + String(critical) + " critical failures)";
|
||||
}
|
||||
|
||||
if (warnings > 0) {
|
||||
summary += " (" + String(warnings) + " warnings)";
|
||||
}
|
||||
|
||||
if (shouldRollbackFirmware()) {
|
||||
summary += " - ROLLBACK RECOMMENDED";
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
void HealthMonitor::updateSubsystemHealth(const String& name, HealthStatus status, const String& error) {
|
||||
auto it = _subsystemHealth.find(name);
|
||||
if (it != _subsystemHealth.end()) {
|
||||
it->second.status = status;
|
||||
it->second.lastError = error;
|
||||
it->second.lastCheck = millis();
|
||||
|
||||
LOG_VERBOSE("🔍 %s: %s %s",
|
||||
name.c_str(),
|
||||
healthStatusToString(status).c_str(),
|
||||
error.isEmpty() ? "" : ("(" + error + ")").c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool HealthMonitor::areCriticalSubsystemsHealthy() const {
|
||||
for (const auto& [name, health] : _subsystemHealth) {
|
||||
if (health.isCritical &&
|
||||
(health.status == HealthStatus::CRITICAL || health.status == HealthStatus::FAILED)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
HealthStatus HealthMonitor::calculateOverallHealth() const {
|
||||
bool hasCriticalFailures = (getCriticalFailureCount() > 0);
|
||||
bool hasWarnings = (getWarningCount() > 0);
|
||||
|
||||
if (hasCriticalFailures) {
|
||||
return HealthStatus::CRITICAL;
|
||||
} else if (hasWarnings) {
|
||||
return HealthStatus::WARNING;
|
||||
} else {
|
||||
return HealthStatus::HEALTHY;
|
||||
}
|
||||
}
|
||||
|
||||
String HealthMonitor::healthStatusToString(HealthStatus status) const {
|
||||
switch (status) {
|
||||
case HealthStatus::HEALTHY:
|
||||
return "HEALTHY";
|
||||
case HealthStatus::WARNING:
|
||||
return "WARNING";
|
||||
case HealthStatus::CRITICAL:
|
||||
return "CRITICAL";
|
||||
case HealthStatus::FAILED:
|
||||
return "FAILED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
314
vesper/src/HealthMonitor/HealthMonitor.hpp
Normal file
314
vesper/src/HealthMonitor/HealthMonitor.hpp
Normal file
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* HEALTHMONITOR.HPP - System Health Monitoring and Firmware Validation
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🏥 THE SYSTEM HEALTH GUARDIAN OF VESPER 🏥
|
||||
*
|
||||
* This class provides comprehensive system health monitoring across all subsystems.
|
||||
* It determines whether the current firmware is stable and functional, or if a
|
||||
* rollback to the previous firmware version should be performed.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Periodic health checks across all major subsystems
|
||||
* • Critical vs non-critical failure classification
|
||||
* • Firmware stability validation and rollback decision making
|
||||
* • Centralized health status reporting
|
||||
* • Thread-safe operation with configurable check intervals
|
||||
*
|
||||
* 🔍 MONITORED SUBSYSTEMS:
|
||||
* • BellEngine: Core timing and bell control system
|
||||
* • OutputManager: Hardware abstraction layer
|
||||
* • Communication: MQTT, WebSocket, and UDP protocols
|
||||
* • Player: Melody playback management
|
||||
* • TimeKeeper: RTC and time synchronization
|
||||
* • Telemetry: System monitoring and analytics
|
||||
* • OTAManager: Firmware update management
|
||||
* • Networking: Network connectivity management
|
||||
* • ConfigManager: Configuration and persistence
|
||||
* • FileManager: SD card and file operations
|
||||
*
|
||||
* 🚨 FAILURE CLASSIFICATION:
|
||||
* • CRITICAL: Failures that make the device unusable
|
||||
* • WARNING: Failures that affect functionality but allow operation
|
||||
* • INFO: Minor issues that don't affect core functionality
|
||||
*
|
||||
* 🔄 FIRMWARE VALIDATION:
|
||||
* • Boot-time stability check
|
||||
* • Runtime health monitoring
|
||||
* • Automatic rollback decision making
|
||||
* • Health status persistence
|
||||
*
|
||||
* 📋 VERSION: 1.0 (Initial health monitoring system)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// Forward declarations for all monitored subsystems
|
||||
class BellEngine;
|
||||
class OutputManager;
|
||||
class Communication;
|
||||
class Player;
|
||||
class Timekeeper;
|
||||
class Telemetry;
|
||||
class OTAManager;
|
||||
class Networking;
|
||||
class ConfigManager;
|
||||
class FileManager;
|
||||
|
||||
/**
|
||||
* @enum HealthStatus
|
||||
* @brief Health status levels for subsystems
|
||||
*/
|
||||
enum class HealthStatus {
|
||||
HEALTHY, // System is functioning normally
|
||||
WARNING, // System has minor issues but is operational
|
||||
CRITICAL, // System has major issues affecting functionality
|
||||
FAILED // System is non-functional
|
||||
};
|
||||
|
||||
/**
|
||||
* @struct SubsystemHealth
|
||||
* @brief Health information for a single subsystem
|
||||
*/
|
||||
struct SubsystemHealth {
|
||||
String name; // Subsystem name
|
||||
HealthStatus status; // Current health status
|
||||
String lastError; // Last error message (if any)
|
||||
unsigned long lastCheck; // Timestamp of last health check
|
||||
bool isCritical; // Whether this subsystem is critical for operation
|
||||
|
||||
// Default constructor for std::map compatibility
|
||||
SubsystemHealth()
|
||||
: name(""), status(HealthStatus::HEALTHY), lastCheck(0), isCritical(false) {}
|
||||
|
||||
SubsystemHealth(const String& n, bool critical = false)
|
||||
: name(n), status(HealthStatus::HEALTHY), lastCheck(0), isCritical(critical) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @class HealthMonitor
|
||||
* @brief Comprehensive system health monitoring and firmware validation
|
||||
*
|
||||
* The HealthMonitor continuously monitors all subsystems to ensure the firmware
|
||||
* is stable and functional. It can make decisions about firmware rollbacks
|
||||
* based on the overall system health.
|
||||
*/
|
||||
class HealthMonitor {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Constructor - Initialize health monitoring system
|
||||
*/
|
||||
HealthMonitor();
|
||||
|
||||
/**
|
||||
* @brief Destructor - Clean up resources
|
||||
*/
|
||||
~HealthMonitor();
|
||||
|
||||
/**
|
||||
* @brief Initialize health monitoring system
|
||||
* @return true if initialization successful
|
||||
*/
|
||||
bool begin();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SUBSYSTEM REGISTRATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Register BellEngine for monitoring */
|
||||
void setBellEngine(BellEngine* bellEngine) { _bellEngine = bellEngine; }
|
||||
|
||||
/** @brief Register OutputManager for monitoring */
|
||||
void setOutputManager(OutputManager* outputManager) { _outputManager = outputManager; }
|
||||
|
||||
/** @brief Register Communication for monitoring */
|
||||
void setCommunication(Communication* communication) { _communication = communication; }
|
||||
|
||||
/** @brief Register Player for monitoring */
|
||||
void setPlayer(Player* player) { _player = player; }
|
||||
|
||||
/** @brief Register TimeKeeper for monitoring */
|
||||
void setTimeKeeper(Timekeeper* timeKeeper) { _timeKeeper = timeKeeper; }
|
||||
|
||||
/** @brief Register Telemetry for monitoring */
|
||||
void setTelemetry(Telemetry* telemetry) { _telemetry = telemetry; }
|
||||
|
||||
/** @brief Register OTAManager for monitoring */
|
||||
void setOTAManager(OTAManager* otaManager) { _otaManager = otaManager; }
|
||||
|
||||
/** @brief Register Networking for monitoring */
|
||||
void setNetworking(Networking* networking) { _networking = networking; }
|
||||
|
||||
/** @brief Register ConfigManager for monitoring */
|
||||
void setConfigManager(ConfigManager* configManager) { _configManager = configManager; }
|
||||
|
||||
/** @brief Register FileManager for monitoring */
|
||||
void setFileManager(FileManager* fileManager) { _fileManager = fileManager; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Perform comprehensive health check on all subsystems
|
||||
* @return Overall system health status
|
||||
*/
|
||||
HealthStatus performFullHealthCheck();
|
||||
|
||||
/**
|
||||
* @brief Perform health check on a specific subsystem
|
||||
* @param subsystemName Name of the subsystem to check
|
||||
* @return Health status of the specified subsystem
|
||||
*/
|
||||
HealthStatus checkSubsystemHealth(const String& subsystemName);
|
||||
|
||||
/**
|
||||
* @brief Get current health status of all subsystems
|
||||
* @return Map of subsystem names to their health information
|
||||
*/
|
||||
const std::map<String, SubsystemHealth>& getAllSubsystemHealth() const;
|
||||
|
||||
/**
|
||||
* @brief Get health status of a specific subsystem
|
||||
* @param subsystemName Name of the subsystem
|
||||
* @return Health information for the subsystem
|
||||
*/
|
||||
SubsystemHealth getSubsystemHealth(const String& subsystemName) const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// FIRMWARE VALIDATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Check if current firmware is stable and should be kept
|
||||
* @return true if firmware is stable, false if rollback is recommended
|
||||
*/
|
||||
bool isFirmwareStable() const;
|
||||
|
||||
/**
|
||||
* @brief Get the number of critical failures detected
|
||||
* @return Count of subsystems with critical failures
|
||||
*/
|
||||
uint8_t getCriticalFailureCount() const;
|
||||
|
||||
/**
|
||||
* @brief Get the number of warning-level issues detected
|
||||
* @return Count of subsystems with warning-level issues
|
||||
*/
|
||||
uint8_t getWarningCount() const;
|
||||
|
||||
/**
|
||||
* @brief Check if a firmware rollback is recommended
|
||||
* @return true if rollback is recommended due to critical failures
|
||||
*/
|
||||
bool shouldRollbackFirmware() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH REPORTING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Generate a comprehensive health report
|
||||
* @return JSON string containing detailed health information
|
||||
*/
|
||||
String generateHealthReport() const;
|
||||
|
||||
/**
|
||||
* @brief Get a summary of system health
|
||||
* @return Brief health summary string
|
||||
*/
|
||||
String getHealthSummary() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Set health check interval
|
||||
* @param intervalMs Interval between health checks in milliseconds
|
||||
*/
|
||||
void setHealthCheckInterval(unsigned long intervalMs) { _healthCheckInterval = intervalMs; }
|
||||
|
||||
/**
|
||||
* @brief Enable or disable automatic health monitoring
|
||||
* @param enabled Whether to enable automatic monitoring
|
||||
*/
|
||||
void setAutoMonitoring(bool enabled) { _autoMonitoring = enabled; }
|
||||
|
||||
private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SUBSYSTEM REFERENCES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
BellEngine* _bellEngine = nullptr;
|
||||
OutputManager* _outputManager = nullptr;
|
||||
Communication* _communication = nullptr;
|
||||
Player* _player = nullptr;
|
||||
Timekeeper* _timeKeeper = nullptr;
|
||||
Telemetry* _telemetry = nullptr;
|
||||
OTAManager* _otaManager = nullptr;
|
||||
Networking* _networking = nullptr;
|
||||
ConfigManager* _configManager = nullptr;
|
||||
FileManager* _fileManager = nullptr;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH MONITORING STATE
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
std::map<String, SubsystemHealth> _subsystemHealth;
|
||||
TaskHandle_t _monitoringTaskHandle = nullptr;
|
||||
unsigned long _healthCheckInterval = 300000; // 5 minutes default
|
||||
bool _autoMonitoring = true;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE HELPER METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Initialize all subsystem health entries
|
||||
*/
|
||||
void initializeSubsystemHealth();
|
||||
|
||||
/**
|
||||
* @brief Monitoring task function
|
||||
*/
|
||||
static void monitoringTask(void* parameter);
|
||||
|
||||
/**
|
||||
* @brief Main monitoring loop
|
||||
*/
|
||||
void monitoringLoop();
|
||||
|
||||
/**
|
||||
* @brief Update health status for a specific subsystem
|
||||
*/
|
||||
void updateSubsystemHealth(const String& name, HealthStatus status, const String& error = "");
|
||||
|
||||
/**
|
||||
* @brief Check if enough critical subsystems are healthy
|
||||
*/
|
||||
bool areCriticalSubsystemsHealthy() const;
|
||||
|
||||
/**
|
||||
* @brief Calculate overall system health based on subsystem status
|
||||
*/
|
||||
HealthStatus calculateOverallHealth() const;
|
||||
|
||||
/**
|
||||
* @brief Convert health status to string
|
||||
*/
|
||||
String healthStatusToString(HealthStatus status) const;
|
||||
};
|
||||
266
vesper/src/InputManager/InputManager.cpp
Normal file
266
vesper/src/InputManager/InputManager.cpp
Normal file
@@ -0,0 +1,266 @@
|
||||
#include "InputManager.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// Static instance pointer
|
||||
InputManager* InputManager::_instance = nullptr;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & DESTRUCTOR
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
InputManager::InputManager()
|
||||
: _initialized(false)
|
||||
, _inputTaskHandle(nullptr) {
|
||||
// Initialize factory reset button configuration
|
||||
// GPIO 0, Active LOW (pull-up), 50ms debounce, 10s long press
|
||||
_factoryResetButton.config = ButtonConfig(0, false, 50, 10000);
|
||||
|
||||
_instance = this;
|
||||
}
|
||||
|
||||
InputManager::~InputManager() {
|
||||
end();
|
||||
_instance = nullptr;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool InputManager::begin() {
|
||||
LOG_INFO("InputManager: Initializing input handling system");
|
||||
|
||||
// Configure factory reset button
|
||||
configureButton(_factoryResetButton.config);
|
||||
|
||||
// Initialize button state
|
||||
_factoryResetButton.state = ButtonState::IDLE;
|
||||
_factoryResetButton.lastRawState = false;
|
||||
_factoryResetButton.stateChangeTime = millis();
|
||||
_factoryResetButton.pressStartTime = 0;
|
||||
_factoryResetButton.longPressTriggered = false;
|
||||
|
||||
// Create FreeRTOS task for input polling
|
||||
BaseType_t result = xTaskCreate(
|
||||
inputTaskFunction, // Task function
|
||||
"InputTask", // Task name
|
||||
INPUT_TASK_STACK_SIZE, // Stack size
|
||||
this, // Parameter (this instance)
|
||||
INPUT_TASK_PRIORITY, // Priority
|
||||
&_inputTaskHandle // Task handle
|
||||
);
|
||||
|
||||
if (result != pdPASS) {
|
||||
LOG_ERROR("InputManager: Failed to create input task!");
|
||||
return false;
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
||||
LOG_INFO("InputManager: Initialization complete - Factory Reset on GPIO 0 (Task running)");
|
||||
return true;
|
||||
}
|
||||
|
||||
void InputManager::end() {
|
||||
if (_inputTaskHandle != nullptr) {
|
||||
vTaskDelete(_inputTaskHandle);
|
||||
_inputTaskHandle = nullptr;
|
||||
LOG_INFO("InputManager: Input task stopped");
|
||||
}
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// CALLBACK REGISTRATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void InputManager::setFactoryResetPressCallback(ButtonCallback callback) {
|
||||
_factoryResetButton.config.onPress = callback;
|
||||
LOG_DEBUG("InputManager: Factory reset press callback registered");
|
||||
}
|
||||
|
||||
void InputManager::setFactoryResetLongPressCallback(ButtonCallback callback) {
|
||||
_factoryResetButton.config.onLongPress = callback;
|
||||
LOG_DEBUG("InputManager: Factory reset long press callback registered");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// STATUS METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool InputManager::isFactoryResetPressed() const {
|
||||
return _factoryResetButton.state != ButtonState::IDLE;
|
||||
}
|
||||
|
||||
uint32_t InputManager::getFactoryResetPressDuration() const {
|
||||
if (_factoryResetButton.state == ButtonState::IDLE) {
|
||||
return 0;
|
||||
}
|
||||
return millis() - _factoryResetButton.pressStartTime;
|
||||
}
|
||||
|
||||
bool InputManager::isHealthy() const {
|
||||
if (!_initialized) {
|
||||
LOG_DEBUG("InputManager: Unhealthy - not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Could add more health checks here if needed
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// FREERTOS TASK
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void InputManager::inputTaskFunction(void* parameter) {
|
||||
InputManager* manager = static_cast<InputManager*>(parameter);
|
||||
|
||||
LOG_INFO("InputManager: Input task started (polling every %dms)", INPUT_POLL_RATE_MS);
|
||||
|
||||
TickType_t lastWakeTime = xTaskGetTickCount();
|
||||
const TickType_t pollInterval = pdMS_TO_TICKS(INPUT_POLL_RATE_MS);
|
||||
|
||||
while (true) {
|
||||
// Update button states
|
||||
manager->update();
|
||||
|
||||
// Wait for next poll interval
|
||||
vTaskDelayUntil(&lastWakeTime, pollInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// UPDATE LOGIC (Called by task)
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void InputManager::update() {
|
||||
if (!_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update factory reset button
|
||||
updateButton(_factoryResetButton);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void InputManager::configureButton(const ButtonConfig& config) {
|
||||
// Configure pin as input with pull-up (for active-low buttons)
|
||||
if (config.activeHigh) {
|
||||
pinMode(config.pin, INPUT);
|
||||
} else {
|
||||
pinMode(config.pin, INPUT_PULLUP);
|
||||
}
|
||||
|
||||
LOG_DEBUG("InputManager: Configured GPIO %d as input (%s)",
|
||||
config.pin, config.activeHigh ? "active-high" : "active-low");
|
||||
}
|
||||
|
||||
bool InputManager::readButtonState(const ButtonData& button) const {
|
||||
bool rawState = digitalRead(button.config.pin);
|
||||
|
||||
// Invert reading if active-low
|
||||
if (!button.config.activeHigh) {
|
||||
rawState = !rawState;
|
||||
}
|
||||
|
||||
return rawState;
|
||||
}
|
||||
|
||||
void InputManager::updateButton(ButtonData& button) {
|
||||
uint32_t now = millis();
|
||||
bool currentState = readButtonState(button);
|
||||
|
||||
// State machine for button handling
|
||||
switch (button.state) {
|
||||
case ButtonState::IDLE:
|
||||
// Waiting for button press
|
||||
if (currentState && !button.lastRawState) {
|
||||
// Button just pressed - start debouncing
|
||||
button.state = ButtonState::DEBOUNCING_PRESS;
|
||||
button.stateChangeTime = now;
|
||||
LOG_DEBUG("InputManager: Button press detected on GPIO %d - debouncing",
|
||||
button.config.pin);
|
||||
}
|
||||
break;
|
||||
|
||||
case ButtonState::DEBOUNCING_PRESS:
|
||||
// Debouncing press
|
||||
if (!currentState) {
|
||||
// Button released during debounce - false trigger
|
||||
button.state = ButtonState::IDLE;
|
||||
LOG_DEBUG("InputManager: False trigger on GPIO %d (released during debounce)",
|
||||
button.config.pin);
|
||||
} else if (now - button.stateChangeTime >= button.config.debounceMs) {
|
||||
// Debounce time passed - confirm press
|
||||
button.state = ButtonState::LONG_PRESS_PENDING;
|
||||
button.pressStartTime = now;
|
||||
button.longPressTriggered = false;
|
||||
LOG_INFO("InputManager: Button press confirmed on GPIO %d",
|
||||
button.config.pin);
|
||||
}
|
||||
break;
|
||||
|
||||
case ButtonState::LONG_PRESS_PENDING:
|
||||
// Button is pressed, waiting to see if it's a long press
|
||||
if (!currentState) {
|
||||
// Button released before long press threshold - it's a short press
|
||||
button.state = ButtonState::DEBOUNCING_RELEASE;
|
||||
button.stateChangeTime = now;
|
||||
LOG_INFO("InputManager: Short press detected on GPIO %d (held for %lums)",
|
||||
button.config.pin, now - button.pressStartTime);
|
||||
} else if (now - button.pressStartTime >= button.config.longPressMs) {
|
||||
// Long press threshold reached
|
||||
button.state = ButtonState::LONG_PRESSED;
|
||||
button.longPressTriggered = true;
|
||||
|
||||
LOG_WARNING("InputManager: LONG PRESS DETECTED on GPIO %d (held for %lums)",
|
||||
button.config.pin, now - button.pressStartTime);
|
||||
|
||||
// Trigger long press callback
|
||||
if (button.config.onLongPress) {
|
||||
button.config.onLongPress();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ButtonState::LONG_PRESSED:
|
||||
// Long press has been triggered, waiting for release
|
||||
if (!currentState) {
|
||||
button.state = ButtonState::DEBOUNCING_RELEASE;
|
||||
button.stateChangeTime = now;
|
||||
LOG_INFO("InputManager: Long press released on GPIO %d (total duration: %lums)",
|
||||
button.config.pin, now - button.pressStartTime);
|
||||
}
|
||||
break;
|
||||
|
||||
case ButtonState::DEBOUNCING_RELEASE:
|
||||
// Debouncing release
|
||||
if (currentState) {
|
||||
// Button pressed again during release debounce - go back to pressed state
|
||||
button.state = ButtonState::LONG_PRESS_PENDING;
|
||||
LOG_DEBUG("InputManager: Button re-pressed during release debounce on GPIO %d",
|
||||
button.config.pin);
|
||||
} else if (now - button.stateChangeTime >= button.config.debounceMs) {
|
||||
// Debounce time passed - confirm release
|
||||
button.state = ButtonState::IDLE;
|
||||
|
||||
// If it was a short press (not long press), trigger the press callback
|
||||
if (!button.longPressTriggered && button.config.onPress) {
|
||||
LOG_INFO("InputManager: Triggering press callback for GPIO %d",
|
||||
button.config.pin);
|
||||
button.config.onPress();
|
||||
}
|
||||
|
||||
LOG_DEBUG("InputManager: Button release confirmed on GPIO %d",
|
||||
button.config.pin);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Store last raw state for edge detection
|
||||
button.lastRawState = currentState;
|
||||
}
|
||||
196
vesper/src/InputManager/InputManager.hpp
Normal file
196
vesper/src/InputManager/InputManager.hpp
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* INPUTMANAGER.HPP - Button and Input Handling System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🎛️ INPUT HANDLING FOR VESPER 🎛️
|
||||
*
|
||||
* This class manages all physical input handling including button presses,
|
||||
* long presses, and debouncing logic. It provides clean event-based callbacks
|
||||
* for different input actions.
|
||||
*
|
||||
* 🏗️ ARCHITECTURE:
|
||||
* • Non-blocking button state management
|
||||
* • Software debouncing with configurable timing
|
||||
* • Long press detection
|
||||
* • Event-driven callbacks for actions
|
||||
* • Easy expansion for multiple inputs
|
||||
*
|
||||
* 🔘 CURRENT INPUTS:
|
||||
* • GPIO 0: Factory Reset Button (Long Press = 10s)
|
||||
*
|
||||
* ⚙️ FEATURES:
|
||||
* • Debounce filtering (default 50ms)
|
||||
* • Long press detection (configurable, default 10s)
|
||||
* • Active-low button handling (pull-up enabled)
|
||||
* • Non-blocking state machine
|
||||
*
|
||||
* 📋 VERSION: 1.0
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <functional>
|
||||
|
||||
class InputManager {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CALLBACK TYPES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
using ButtonCallback = std::function<void()>;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// BUTTON CONFIGURATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
struct ButtonConfig {
|
||||
uint8_t pin; // GPIO pin number
|
||||
bool activeHigh; // true = active high, false = active low
|
||||
uint32_t debounceMs; // Debounce time in milliseconds
|
||||
uint32_t longPressMs; // Long press threshold in milliseconds
|
||||
ButtonCallback onPress; // Callback for normal press
|
||||
ButtonCallback onLongPress; // Callback for long press
|
||||
|
||||
ButtonConfig(uint8_t p = 0, bool ah = false, uint32_t db = 50, uint32_t lp = 10000)
|
||||
: pin(p), activeHigh(ah), debounceMs(db), longPressMs(lp),
|
||||
onPress(nullptr), onLongPress(nullptr) {}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
InputManager();
|
||||
~InputManager();
|
||||
|
||||
/**
|
||||
* @brief Initialize the InputManager and start the input task
|
||||
* @return true if initialization successful
|
||||
*/
|
||||
bool begin();
|
||||
|
||||
/**
|
||||
* @brief Stop the input task and cleanup
|
||||
*/
|
||||
void end();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CALLBACK REGISTRATION
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Set callback for factory reset button press
|
||||
* @param callback Function to call on short press
|
||||
*/
|
||||
void setFactoryResetPressCallback(ButtonCallback callback);
|
||||
|
||||
/**
|
||||
* @brief Set callback for factory reset button long press
|
||||
* @param callback Function to call on long press (10s)
|
||||
*/
|
||||
void setFactoryResetLongPressCallback(ButtonCallback callback);
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// STATUS METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Check if factory reset button is currently pressed
|
||||
* @return true if button is pressed
|
||||
*/
|
||||
bool isFactoryResetPressed() const;
|
||||
|
||||
/**
|
||||
* @brief Get how long the factory reset button has been pressed
|
||||
* @return Duration in milliseconds (0 if not pressed)
|
||||
*/
|
||||
uint32_t getFactoryResetPressDuration() const;
|
||||
|
||||
/**
|
||||
* @brief Check if InputManager is healthy and functioning
|
||||
* @return true if all inputs are properly configured
|
||||
*/
|
||||
bool isHealthy() const;
|
||||
|
||||
private:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// BUTTON STATE TRACKING
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
enum class ButtonState {
|
||||
IDLE, // Button not pressed
|
||||
DEBOUNCING_PRESS, // Waiting for debounce on press
|
||||
PRESSED, // Button confirmed pressed (short)
|
||||
LONG_PRESS_PENDING, // Button held, waiting for long press threshold
|
||||
LONG_PRESSED, // Long press confirmed and triggered
|
||||
DEBOUNCING_RELEASE // Waiting for debounce on release
|
||||
};
|
||||
|
||||
struct ButtonData {
|
||||
ButtonConfig config;
|
||||
ButtonState state;
|
||||
bool lastRawState; // Last raw reading from pin
|
||||
uint32_t stateChangeTime; // When state last changed
|
||||
uint32_t pressStartTime; // When button was first pressed
|
||||
bool longPressTriggered; // Has long press callback been fired?
|
||||
|
||||
ButtonData() : state(ButtonState::IDLE), lastRawState(false),
|
||||
stateChangeTime(0), pressStartTime(0),
|
||||
longPressTriggered(false) {}
|
||||
};
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// MEMBER VARIABLES
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
ButtonData _factoryResetButton;
|
||||
bool _initialized;
|
||||
|
||||
// FreeRTOS task management
|
||||
TaskHandle_t _inputTaskHandle;
|
||||
static constexpr uint32_t INPUT_TASK_STACK_SIZE = 4096;
|
||||
static constexpr UBaseType_t INPUT_TASK_PRIORITY = 2;
|
||||
static constexpr uint32_t INPUT_POLL_RATE_MS = 10; // Poll every 10ms
|
||||
|
||||
// Static instance for task callback
|
||||
static InputManager* _instance;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PRIVATE METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @brief Configure a single button pin
|
||||
* @param config Button configuration
|
||||
*/
|
||||
void configureButton(const ButtonConfig& config);
|
||||
|
||||
/**
|
||||
* @brief Update state of a single button
|
||||
* @param button Button data to update
|
||||
*/
|
||||
void updateButton(ButtonData& button);
|
||||
|
||||
/**
|
||||
* @brief Read current state of button (handles active high/low)
|
||||
* @param button Button to read
|
||||
* @return true if button is currently pressed
|
||||
*/
|
||||
bool readButtonState(const ButtonData& button) const;
|
||||
|
||||
/**
|
||||
* @brief Static task function for FreeRTOS
|
||||
* @param parameter Pointer to InputManager instance
|
||||
*/
|
||||
static void inputTaskFunction(void* parameter);
|
||||
|
||||
/**
|
||||
* @brief Internal update method called by task
|
||||
*/
|
||||
void update();
|
||||
};
|
||||
72
vesper/src/Logging/Logging.cpp
Normal file
72
vesper/src/Logging/Logging.cpp
Normal file
@@ -0,0 +1,72 @@
|
||||
#include "Logging.hpp"
|
||||
|
||||
// Initialize static member
|
||||
Logging::LogLevel Logging::currentLevel = Logging::VERBOSE; // Default to DEBUG
|
||||
|
||||
void Logging::setLevel(LogLevel level) {
|
||||
currentLevel = level;
|
||||
Serial.printf("[LOGGING] Log level set to %d\n", level);
|
||||
}
|
||||
|
||||
Logging::LogLevel Logging::getLevel() {
|
||||
return currentLevel;
|
||||
}
|
||||
|
||||
bool Logging::isLevelEnabled(LogLevel level) {
|
||||
return currentLevel >= level;
|
||||
}
|
||||
|
||||
void Logging::error(const char* format, ...) {
|
||||
if (!isLevelEnabled(ERROR)) return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(ERROR, "🔴 EROR", format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logging::warning(const char* format, ...) {
|
||||
if (!isLevelEnabled(WARNING)) return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(WARNING, "🟡 WARN", format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logging::info(const char* format, ...) {
|
||||
if (!isLevelEnabled(INFO)) return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(INFO, "🟢 INFO", format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logging::debug(const char* format, ...) {
|
||||
if (!isLevelEnabled(DEBUG)) return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(DEBUG, "🐞 DEBG", format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logging::verbose(const char* format, ...) {
|
||||
if (!isLevelEnabled(VERBOSE)) return;
|
||||
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log(VERBOSE, "🧾 VERB", format, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void Logging::log(LogLevel level, const char* levelStr, const char* format, va_list args) {
|
||||
Serial.printf("[%s] ", levelStr);
|
||||
|
||||
// Print the formatted message
|
||||
char buffer[512];
|
||||
vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
Serial.print(buffer);
|
||||
Serial.println();
|
||||
}
|
||||
65
vesper/src/Logging/Logging.hpp
Normal file
65
vesper/src/Logging/Logging.hpp
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* LOGGING.HPP - Centralized Logging System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 📝 THE INFORMATION CHRONICLER OF VESPER 📝
|
||||
*
|
||||
* This header provides a unified logging interface with multiple levels,
|
||||
* timestamps, and comprehensive debugging support throughout the system.
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Enhanced logging system)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#ifndef LOGGING_HPP
|
||||
#define LOGGING_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
|
||||
class Logging {
|
||||
public:
|
||||
// Log Levels
|
||||
enum LogLevel {
|
||||
NONE = 0, // No logs
|
||||
ERROR = 1, // Errors only
|
||||
WARNING = 2, // Warnings and errors
|
||||
INFO = 3, // Info, warnings, and errors
|
||||
DEBUG = 4, // Debug logs. Really high level (full debugging)
|
||||
VERBOSE = 5 // Nearly every command gets printed
|
||||
};
|
||||
|
||||
private:
|
||||
static LogLevel currentLevel;
|
||||
|
||||
public:
|
||||
// Set the active log level
|
||||
static void setLevel(LogLevel level);
|
||||
|
||||
// Get current log level
|
||||
static LogLevel getLevel();
|
||||
|
||||
// Logging functions
|
||||
static void error(const char* format, ...);
|
||||
static void warning(const char* format, ...);
|
||||
static void info(const char* format, ...);
|
||||
static void debug(const char* format, ...);
|
||||
static void verbose(const char* format, ...);
|
||||
|
||||
// Check if level is enabled (for conditional logging)
|
||||
static bool isLevelEnabled(LogLevel level);
|
||||
|
||||
private:
|
||||
static void log(LogLevel level, const char* levelStr, const char* format, va_list args);
|
||||
};
|
||||
|
||||
// Convenience macros for easier use
|
||||
#define LOG_ERROR(...) Logging::error(__VA_ARGS__)
|
||||
#define LOG_WARNING(...) Logging::warning(__VA_ARGS__)
|
||||
#define LOG_INFO(...) Logging::info(__VA_ARGS__)
|
||||
#define LOG_DEBUG(...) Logging::debug(__VA_ARGS__)
|
||||
#define LOG_VERBOSE(...) Logging::verbose(__VA_ARGS__)
|
||||
|
||||
#endif
|
||||
59
vesper/src/MqttSSL/MqttSSL.cpp
Normal file
59
vesper/src/MqttSSL/MqttSSL.cpp
Normal file
@@ -0,0 +1,59 @@
|
||||
#include "MqttSSL.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// EMQX Cloud CA Certificate (DigiCert Global Root CA)
|
||||
const char* MqttSSL::_emqxCloudCA = R"EOF(
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
|
||||
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
|
||||
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
|
||||
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
|
||||
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
|
||||
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
|
||||
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
|
||||
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
|
||||
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
|
||||
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
|
||||
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
|
||||
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
|
||||
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
|
||||
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
|
||||
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
|
||||
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
|
||||
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
|
||||
-----END CERTIFICATE-----
|
||||
)EOF";
|
||||
|
||||
MqttSSL::MqttSSL() {
|
||||
}
|
||||
|
||||
MqttSSL::~MqttSSL() {
|
||||
}
|
||||
|
||||
bool MqttSSL::isSSLAvailable() {
|
||||
#ifdef ASYNC_TCP_SSL_ENABLED
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* MqttSSL::getEMQXCA() {
|
||||
return _emqxCloudCA;
|
||||
}
|
||||
|
||||
void MqttSSL::logSSLStatus(const AsyncMqttClient& client, int port) {
|
||||
if (port == 8883) {
|
||||
if (isSSLAvailable()) {
|
||||
LOG_INFO("🔒 MQTT SSL/TLS enabled for port %d", port);
|
||||
LOG_INFO("🔐 Certificate validation: Using DigiCert Global Root CA");
|
||||
} else {
|
||||
LOG_ERROR("❌ SSL requested but not compiled in! Add ASYNC_TCP_SSL_ENABLED to build flags");
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING("⚠️ MQTT using unencrypted connection on port %d", port);
|
||||
}
|
||||
}
|
||||
48
vesper/src/MqttSSL/MqttSSL.hpp
Normal file
48
vesper/src/MqttSSL/MqttSSL.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* MQTTSSL.HPP - EMQX Cloud SSL/TLS Certificate Management
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🔒 SECURE MQTT CONNECTION FOR EMQX CLOUD 🔒
|
||||
*
|
||||
* This class manages SSL/TLS certificates for EMQX Cloud connections.
|
||||
* Note: AsyncMqttClient SSL is configured at compile time, not runtime.
|
||||
*
|
||||
* 📋 VERSION: 1.0
|
||||
* 📅 DATE: 2025-09-30
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <AsyncMqttClient.h>
|
||||
|
||||
class MqttSSL {
|
||||
public:
|
||||
MqttSSL();
|
||||
~MqttSSL();
|
||||
|
||||
/**
|
||||
* @brief Check if SSL is available (compile-time check)
|
||||
* @return true if SSL support is compiled in
|
||||
*/
|
||||
static bool isSSLAvailable();
|
||||
|
||||
/**
|
||||
* @brief Get EMQX Cloud CA certificate
|
||||
* @return CA certificate string
|
||||
*/
|
||||
static const char* getEMQXCA();
|
||||
|
||||
/**
|
||||
* @brief Log SSL status
|
||||
* @param client Reference to AsyncMqttClient
|
||||
* @param port MQTT port being used
|
||||
*/
|
||||
static void logSSLStatus(const AsyncMqttClient& client, int port);
|
||||
|
||||
private:
|
||||
static const char* _emqxCloudCA;
|
||||
};
|
||||
456
vesper/src/Networking/Networking.cpp
Normal file
456
vesper/src/Networking/Networking.cpp
Normal file
@@ -0,0 +1,456 @@
|
||||
#include "Networking.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include <WiFiManager.h>
|
||||
#include <SPI.h>
|
||||
|
||||
// Static instance for callbacks (with safety checks)
|
||||
Networking* Networking::_instance = nullptr;
|
||||
|
||||
Networking::Networking(ConfigManager& configManager)
|
||||
: _configManager(configManager)
|
||||
, _state(NetworkState::DISCONNECTED)
|
||||
, _activeConnection(ConnectionType::NONE)
|
||||
, _lastConnectionAttempt(0)
|
||||
, _bootStartTime(0)
|
||||
, _bootSequenceComplete(false)
|
||||
, _ethernetCableConnected(false)
|
||||
, _wifiManager(nullptr)
|
||||
, _reconnectionTimer(nullptr) {
|
||||
|
||||
// Safety check for multiple instances
|
||||
if (_instance != nullptr) {
|
||||
LOG_WARNING("Multiple Networking instances detected! Previous instance will be overridden.");
|
||||
}
|
||||
|
||||
_instance = this;
|
||||
_wifiManager = new WiFiManager();
|
||||
}
|
||||
|
||||
Networking::~Networking() {
|
||||
// Clear static instance safely
|
||||
if (_instance == this) {
|
||||
_instance = nullptr;
|
||||
}
|
||||
|
||||
// Cleanup timer
|
||||
if (_reconnectionTimer) {
|
||||
xTimerDelete(_reconnectionTimer, portMAX_DELAY);
|
||||
_reconnectionTimer = nullptr;
|
||||
}
|
||||
|
||||
// Cleanup WiFiManager
|
||||
if (_wifiManager) {
|
||||
delete _wifiManager;
|
||||
_wifiManager = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::begin() {
|
||||
LOG_INFO("Initializing Networking System");
|
||||
|
||||
_bootStartTime = millis();
|
||||
|
||||
// Create reconnection timer
|
||||
_reconnectionTimer = xTimerCreate("reconnectionTimer", pdMS_TO_TICKS(RECONNECTION_INTERVAL),
|
||||
pdTRUE, (void*)0, reconnectionTimerCallback);
|
||||
|
||||
// Setup network event handler
|
||||
WiFi.onEvent(networkEventHandler);
|
||||
|
||||
// Configure WiFiManager
|
||||
_wifiManager->setDebugOutput(false);
|
||||
_wifiManager->setConfigPortalTimeout(180); // 3 minutes
|
||||
|
||||
// Start Ethernet hardware
|
||||
auto& hwConfig = _configManager.getHardwareConfig();
|
||||
ETH.begin(hwConfig.ethPhyType, hwConfig.ethPhyAddr, hwConfig.ethPhyCs,
|
||||
hwConfig.ethPhyIrq, hwConfig.ethPhyRst, SPI);
|
||||
|
||||
// Start connection sequence
|
||||
LOG_INFO("Starting network connection sequence...");
|
||||
startEthernetConnection();
|
||||
}
|
||||
|
||||
void Networking::startEthernetConnection() {
|
||||
LOG_INFO("Attempting Ethernet connection...");
|
||||
setState(NetworkState::CONNECTING_ETHERNET);
|
||||
|
||||
// Check if Ethernet hardware initialization failed
|
||||
if (!ETH.linkUp()) {
|
||||
LOG_WARNING("Ethernet hardware not detected or failed to initialize");
|
||||
LOG_INFO("Falling back to WiFi immediately");
|
||||
startWiFiConnection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Ethernet will auto-connect via events
|
||||
// Set timeout for Ethernet attempt (5 seconds)
|
||||
_lastConnectionAttempt = millis();
|
||||
|
||||
// Start reconnection timer to handle timeout
|
||||
xTimerStart(_reconnectionTimer, 0);
|
||||
}
|
||||
|
||||
void Networking::startWiFiConnection() {
|
||||
LOG_INFO("Attempting WiFi connection...");
|
||||
setState(NetworkState::CONNECTING_WIFI);
|
||||
|
||||
if (!hasValidWiFiCredentials()) {
|
||||
LOG_WARNING("No valid WiFi credentials found");
|
||||
if (shouldStartPortal()) {
|
||||
startWiFiPortal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Using WiFiManager saved credentials");
|
||||
|
||||
WiFi.mode(WIFI_STA);
|
||||
applyNetworkConfig(false); // false = WiFi config
|
||||
|
||||
// Let WiFiManager handle credentials (uses saved SSID/password)
|
||||
WiFi.begin();
|
||||
|
||||
_lastConnectionAttempt = millis();
|
||||
|
||||
// Start reconnection timer to handle timeout
|
||||
xTimerStart(_reconnectionTimer, 0);
|
||||
}
|
||||
|
||||
void Networking::startWiFiPortal() {
|
||||
LOG_INFO("Starting WiFi configuration portal...");
|
||||
setState(NetworkState::WIFI_PORTAL_MODE);
|
||||
|
||||
WiFi.mode(WIFI_AP_STA);
|
||||
|
||||
auto& netConfig = _configManager.getNetworkConfig();
|
||||
String apName = "Vesper-" + _configManager.getDeviceUID();
|
||||
|
||||
LOG_INFO("WiFi Portal: SSID='%s', Password='%s'", apName.c_str(), netConfig.apPass.c_str());
|
||||
|
||||
if (_wifiManager->autoConnect(apName.c_str(), netConfig.apPass.c_str())) {
|
||||
LOG_INFO("WiFi configured successfully via portal");
|
||||
onWiFiConnected();
|
||||
} else {
|
||||
LOG_ERROR("WiFi portal configuration failed");
|
||||
setState(NetworkState::DISCONNECTED);
|
||||
// Start reconnection timer to try again
|
||||
xTimerStart(_reconnectionTimer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::handleReconnection() {
|
||||
if (_state == NetworkState::CONNECTED_ETHERNET || _state == NetworkState::CONNECTED_WIFI) {
|
||||
return; // Already connected
|
||||
}
|
||||
|
||||
LOG_DEBUG("Attempting reconnection...");
|
||||
|
||||
// Check for Ethernet timeout (fall back to WiFi)
|
||||
if (_state == NetworkState::CONNECTING_ETHERNET) {
|
||||
unsigned long now = millis();
|
||||
if (now - _lastConnectionAttempt > 5000) { // 5 second timeout
|
||||
LOG_INFO("Ethernet connection timeout - falling back to WiFi");
|
||||
startWiFiConnection();
|
||||
return;
|
||||
}
|
||||
return; // Still waiting for Ethernet
|
||||
}
|
||||
|
||||
// Check for WiFi timeout (try again)
|
||||
if (_state == NetworkState::CONNECTING_WIFI) {
|
||||
unsigned long now = millis();
|
||||
if (now - _lastConnectionAttempt > 10000) { // 10 second timeout
|
||||
LOG_INFO("WiFi connection timeout - retrying");
|
||||
startWiFiConnection(); // Retry WiFi
|
||||
}
|
||||
return; // Still waiting for WiFi
|
||||
}
|
||||
|
||||
// State is DISCONNECTED - decide what to try
|
||||
if (_ethernetCableConnected) {
|
||||
LOG_INFO("Ethernet cable detected - trying Ethernet");
|
||||
startEthernetConnection();
|
||||
} else {
|
||||
LOG_INFO("No Ethernet - trying WiFi");
|
||||
if (hasValidWiFiCredentials()) {
|
||||
startWiFiConnection();
|
||||
} else if (shouldStartPortal()) {
|
||||
startWiFiPortal();
|
||||
} else {
|
||||
LOG_WARNING("No WiFi credentials and boot sequence complete - waiting");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool Networking::isHealthy() const {
|
||||
// Check if we have any active connection
|
||||
if (_activeConnection == ConnectionType::NONE) {
|
||||
LOG_DEBUG("Networking: Unhealthy - No active connection");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check connection state
|
||||
if (_state != NetworkState::CONNECTED_ETHERNET && _state != NetworkState::CONNECTED_WIFI) {
|
||||
LOG_DEBUG("Networking: Unhealthy - Not in connected state");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check IP address validity
|
||||
String ip = getLocalIP();
|
||||
if (ip == "0.0.0.0" || ip.isEmpty()) {
|
||||
LOG_DEBUG("Networking: Unhealthy - Invalid IP address");
|
||||
return false;
|
||||
}
|
||||
|
||||
// For WiFi connections, check signal strength
|
||||
if (_activeConnection == ConnectionType::WIFI) {
|
||||
if (WiFi.status() != WL_CONNECTED) {
|
||||
LOG_DEBUG("Networking: Unhealthy - WiFi not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check signal strength (RSSI should be better than -80 dBm)
|
||||
int32_t rssi = WiFi.RSSI();
|
||||
if (rssi < -80) {
|
||||
LOG_DEBUG("Networking: Unhealthy - Poor WiFi signal: %d dBm", rssi);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// For Ethernet connections, check link status
|
||||
if (_activeConnection == ConnectionType::ETHERNET) {
|
||||
if (!ETH.linkUp()) {
|
||||
LOG_DEBUG("Networking: Unhealthy - Ethernet link down");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Networking::setState(NetworkState newState) {
|
||||
if (_state != newState) {
|
||||
LOG_DEBUG("Network state: %d -> %d", (int)_state, (int)newState);
|
||||
_state = newState;
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::setActiveConnection(ConnectionType type) {
|
||||
if (_activeConnection != type) {
|
||||
LOG_INFO("Active connection changed: %d -> %d", (int)_activeConnection, (int)type);
|
||||
_activeConnection = type;
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::notifyConnectionChange(bool connected) {
|
||||
if (connected && _onNetworkConnected) {
|
||||
_onNetworkConnected();
|
||||
} else if (!connected && _onNetworkDisconnected) {
|
||||
_onNetworkDisconnected();
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
void Networking::onEthernetConnected() {
|
||||
LOG_INFO("Ethernet connected successfully");
|
||||
setState(NetworkState::CONNECTED_ETHERNET);
|
||||
setActiveConnection(ConnectionType::ETHERNET);
|
||||
|
||||
// Stop WiFi if it was running
|
||||
if (WiFi.getMode() != WIFI_OFF) {
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
|
||||
// Stop reconnection timer
|
||||
xTimerStop(_reconnectionTimer, 0);
|
||||
|
||||
notifyConnectionChange(true);
|
||||
}
|
||||
|
||||
void Networking::onEthernetDisconnected() {
|
||||
LOG_WARNING("Ethernet disconnected");
|
||||
|
||||
if (_activeConnection == ConnectionType::ETHERNET) {
|
||||
setState(NetworkState::DISCONNECTED);
|
||||
setActiveConnection(ConnectionType::NONE);
|
||||
notifyConnectionChange(false);
|
||||
|
||||
// Start reconnection attempts
|
||||
xTimerStart(_reconnectionTimer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::onWiFiConnected() {
|
||||
LOG_INFO("WiFi connected successfully - IP: %s", WiFi.localIP().toString().c_str());
|
||||
setState(NetworkState::CONNECTED_WIFI);
|
||||
setActiveConnection(ConnectionType::WIFI);
|
||||
|
||||
// Stop reconnection timer
|
||||
xTimerStop(_reconnectionTimer, 0);
|
||||
|
||||
// Mark boot sequence as complete
|
||||
_bootSequenceComplete = true;
|
||||
|
||||
notifyConnectionChange(true);
|
||||
}
|
||||
|
||||
void Networking::onWiFiDisconnected() {
|
||||
LOG_WARNING("WiFi disconnected");
|
||||
|
||||
if (_activeConnection == ConnectionType::WIFI) {
|
||||
setState(NetworkState::DISCONNECTED);
|
||||
setActiveConnection(ConnectionType::NONE);
|
||||
notifyConnectionChange(false);
|
||||
|
||||
// Start reconnection attempts
|
||||
xTimerStart(_reconnectionTimer, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::onEthernetCableChange(bool connected) {
|
||||
_ethernetCableConnected = connected;
|
||||
LOG_INFO("Ethernet cable %s", connected ? "connected" : "disconnected");
|
||||
|
||||
if (connected && _activeConnection != ConnectionType::ETHERNET) {
|
||||
// Cable connected and we're not using Ethernet - try to connect
|
||||
startEthernetConnection();
|
||||
}
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
void Networking::applyNetworkConfig(bool ethernet) {
|
||||
auto& netConfig = _configManager.getNetworkConfig();
|
||||
|
||||
if (netConfig.useStaticIP) {
|
||||
LOG_INFO("Applying static IP configuration");
|
||||
if (ethernet) {
|
||||
ETH.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
|
||||
} else {
|
||||
WiFi.config(netConfig.ip, netConfig.gateway, netConfig.subnet, netConfig.dns1, netConfig.dns2);
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Using DHCP configuration");
|
||||
}
|
||||
|
||||
if (ethernet) {
|
||||
ETH.setHostname(netConfig.hostname.c_str());
|
||||
} else {
|
||||
WiFi.setHostname(netConfig.hostname.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
bool Networking::hasValidWiFiCredentials() {
|
||||
// Check if WiFiManager has saved credentials
|
||||
return WiFi.SSID().length() > 0;
|
||||
}
|
||||
|
||||
bool Networking::shouldStartPortal() {
|
||||
// Only start portal during boot sequence and if we're truly disconnected
|
||||
return !_bootSequenceComplete &&
|
||||
(millis() - _bootStartTime < BOOT_TIMEOUT) &&
|
||||
_activeConnection == ConnectionType::NONE;
|
||||
}
|
||||
|
||||
// Status methods
|
||||
bool Networking::isConnected() const {
|
||||
return _activeConnection != ConnectionType::NONE;
|
||||
}
|
||||
|
||||
String Networking::getLocalIP() const {
|
||||
switch (_activeConnection) {
|
||||
case ConnectionType::ETHERNET:
|
||||
return ETH.localIP().toString();
|
||||
case ConnectionType::WIFI:
|
||||
return WiFi.localIP().toString();
|
||||
default:
|
||||
return "0.0.0.0";
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::forceReconnect() {
|
||||
LOG_INFO("Forcing reconnection...");
|
||||
setState(NetworkState::RECONNECTING);
|
||||
setActiveConnection(ConnectionType::NONE);
|
||||
|
||||
// Disconnect everything
|
||||
if (WiFi.getMode() != WIFI_OFF) {
|
||||
WiFi.disconnect(true);
|
||||
WiFi.mode(WIFI_OFF);
|
||||
}
|
||||
|
||||
// Restart connection sequence
|
||||
delay(1000);
|
||||
startEthernetConnection();
|
||||
}
|
||||
|
||||
// Static callbacks
|
||||
void Networking::networkEventHandler(arduino_event_id_t event, arduino_event_info_t info) {
|
||||
if (!_instance) return;
|
||||
|
||||
LOG_DEBUG("Network event: %d", event);
|
||||
|
||||
switch (event) {
|
||||
case ARDUINO_EVENT_ETH_START:
|
||||
LOG_DEBUG("ETH Started");
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_ETH_CONNECTED:
|
||||
LOG_DEBUG("ETH Cable Connected");
|
||||
_instance->onEthernetCableChange(true);
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_ETH_GOT_IP:
|
||||
LOG_INFO("ETH Got IP: %s", ETH.localIP().toString().c_str());
|
||||
_instance->applyNetworkConfig(true);
|
||||
_instance->onEthernetConnected();
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_ETH_DISCONNECTED:
|
||||
LOG_WARNING("ETH Cable Disconnected");
|
||||
_instance->onEthernetCableChange(false);
|
||||
_instance->onEthernetDisconnected();
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_ETH_STOP:
|
||||
LOG_INFO("ETH Stopped");
|
||||
_instance->onEthernetDisconnected();
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
|
||||
LOG_INFO("WiFi Got IP: %s", WiFi.localIP().toString().c_str());
|
||||
_instance->onWiFiConnected();
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
|
||||
LOG_WARNING("WiFi Disconnected");
|
||||
_instance->onWiFiDisconnected();
|
||||
break;
|
||||
|
||||
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
|
||||
LOG_DEBUG("WiFi STA Connected");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Networking::reconnectionTimerCallback(TimerHandle_t xTimer) {
|
||||
if (_instance) {
|
||||
_instance->handleReconnection();
|
||||
|
||||
// Check if boot sequence should be marked complete
|
||||
if (!_instance->_bootSequenceComplete &&
|
||||
(millis() - _instance->_bootStartTime > BOOT_TIMEOUT)) {
|
||||
_instance->_bootSequenceComplete = true;
|
||||
LOG_INFO("Boot sequence timeout - no more portal attempts");
|
||||
}
|
||||
}
|
||||
}
|
||||
162
vesper/src/Networking/Networking.hpp
Normal file
162
vesper/src/Networking/Networking.hpp
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* NETWORKING.HPP - Intelligent Network Connection Manager
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🌐 THE NETWORK BRAIN OF VESPER 🌐
|
||||
*
|
||||
* This class provides intelligent network connectivity management with
|
||||
* automatic failover between Ethernet and WiFi. It handles all aspects
|
||||
* of network connectivity, configuration, and state management.
|
||||
*
|
||||
* 🏗️ INTELLIGENT CONNECTIVITY:
|
||||
* • Dual-stack support: Ethernet (primary) + WiFi (fallback)
|
||||
* • Automatic failover and recovery
|
||||
* • Smart connection prioritization
|
||||
* • State machine-based connection management
|
||||
* • Comprehensive event handling
|
||||
*
|
||||
* 🔄 AUTO-RECOVERY FEATURES:
|
||||
* • Automatic reconnection on connection loss
|
||||
* • Exponential backoff for failed attempts
|
||||
* • Cable detection for Ethernet
|
||||
* • WiFi portal fallback for configuration
|
||||
* • Boot timeout handling
|
||||
*
|
||||
* ⚙️ CONFIGURATION MANAGEMENT:
|
||||
* • Static IP configuration support
|
||||
* • Dynamic IP with DHCP fallback
|
||||
* • DNS configuration
|
||||
* • Hostname management
|
||||
* • WiFi credential management
|
||||
*
|
||||
* 📊 STATE MONITORING:
|
||||
* • Real-time connection status tracking
|
||||
* • Connection type identification
|
||||
* • Network quality monitoring
|
||||
* • Event-driven status updates
|
||||
* • Comprehensive logging
|
||||
*
|
||||
* 🔗 INTEGRATION:
|
||||
* • Clean callback interface for status changes
|
||||
* • ConfigManager integration for settings
|
||||
* • WiFiManager integration for portal mode
|
||||
* • Event-driven architecture
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Intelligent dual-stack networking)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <WiFi.h>
|
||||
#include <ETH.h>
|
||||
#include <functional>
|
||||
|
||||
class ConfigManager;
|
||||
class WiFiManager; // Forward declaration
|
||||
|
||||
enum class NetworkState {
|
||||
DISCONNECTED,
|
||||
CONNECTING_ETHERNET,
|
||||
CONNECTING_WIFI,
|
||||
WIFI_PORTAL_MODE,
|
||||
CONNECTED_ETHERNET,
|
||||
CONNECTED_WIFI,
|
||||
RECONNECTING
|
||||
};
|
||||
|
||||
enum class ConnectionType {
|
||||
NONE,
|
||||
ETHERNET,
|
||||
WIFI
|
||||
};
|
||||
|
||||
class Networking {
|
||||
public:
|
||||
explicit Networking(ConfigManager& configManager);
|
||||
~Networking(); // Destructor to clean up WiFiManager
|
||||
|
||||
void begin();
|
||||
|
||||
// Status methods
|
||||
bool isConnected() const;
|
||||
String getLocalIP() const;
|
||||
ConnectionType getActiveConnection() const { return _activeConnection; }
|
||||
NetworkState getState() const { return _state; }
|
||||
|
||||
// Network event callbacks
|
||||
void setNetworkCallbacks(std::function<void()> onConnected, std::function<void()> onDisconnected) {
|
||||
_onNetworkConnected = onConnected;
|
||||
_onNetworkDisconnected = onDisconnected;
|
||||
}
|
||||
|
||||
// Manual connection control (for testing/debugging)
|
||||
void forceReconnect();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if Networking is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
// Static instance for callbacks
|
||||
static Networking* _instance;
|
||||
|
||||
// Static event handler
|
||||
static void networkEventHandler(arduino_event_id_t event, arduino_event_info_t info);
|
||||
|
||||
private:
|
||||
// Dependencies
|
||||
ConfigManager& _configManager;
|
||||
WiFiManager* _wifiManager;
|
||||
|
||||
// State
|
||||
NetworkState _state;
|
||||
ConnectionType _activeConnection;
|
||||
unsigned long _lastConnectionAttempt;
|
||||
unsigned long _bootStartTime;
|
||||
bool _bootSequenceComplete;
|
||||
bool _ethernetCableConnected;
|
||||
|
||||
// Callbacks
|
||||
std::function<void()> _onNetworkConnected;
|
||||
std::function<void()> _onNetworkDisconnected;
|
||||
|
||||
// Timers
|
||||
TimerHandle_t _reconnectionTimer;
|
||||
|
||||
// Connection methods
|
||||
void startEthernetConnection();
|
||||
void startWiFiConnection();
|
||||
void startWiFiPortal();
|
||||
void handleReconnection();
|
||||
|
||||
// State management
|
||||
void setState(NetworkState newState);
|
||||
void setActiveConnection(ConnectionType type);
|
||||
void notifyConnectionChange(bool connected);
|
||||
|
||||
// Event handlers
|
||||
void onEthernetConnected();
|
||||
void onEthernetDisconnected();
|
||||
void onWiFiConnected();
|
||||
void onWiFiDisconnected();
|
||||
void onEthernetCableChange(bool connected);
|
||||
|
||||
// Utility methods
|
||||
void applyNetworkConfig(bool ethernet = false);
|
||||
bool hasValidWiFiCredentials();
|
||||
bool shouldStartPortal();
|
||||
|
||||
// Timer callback
|
||||
static void reconnectionTimerCallback(TimerHandle_t xTimer);
|
||||
|
||||
// Constants
|
||||
static const unsigned long RECONNECTION_INTERVAL = 5000; // 5 seconds
|
||||
static const unsigned long BOOT_TIMEOUT = 30000; // 30 seconds for boot sequence
|
||||
};
|
||||
603
vesper/src/OTAManager/OTAManager.cpp
Normal file
603
vesper/src/OTAManager/OTAManager.cpp
Normal file
@@ -0,0 +1,603 @@
|
||||
#include "OTAManager.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include <nvs_flash.h>
|
||||
#include <nvs.h>
|
||||
|
||||
OTAManager::OTAManager(ConfigManager& configManager)
|
||||
: _configManager(configManager)
|
||||
, _fileManager(nullptr)
|
||||
, _status(Status::IDLE)
|
||||
, _lastError(ErrorCode::NONE)
|
||||
, _availableVersion(0.0f)
|
||||
, _updateAvailable(false)
|
||||
, _availableChecksum("")
|
||||
, _updateChannel("stable")
|
||||
, _isMandatory(false)
|
||||
, _isEmergency(false)
|
||||
, _progressCallback(nullptr)
|
||||
, _statusCallback(nullptr) {
|
||||
}
|
||||
|
||||
void OTAManager::begin() {
|
||||
LOG_INFO("OTA Manager initialized");
|
||||
setStatus(Status::IDLE);
|
||||
}
|
||||
|
||||
void OTAManager::setFileManager(FileManager* fm) {
|
||||
_fileManager = fm;
|
||||
}
|
||||
|
||||
void OTAManager::checkForUpdates() {
|
||||
// Boot-time check: only check stable channel for emergency/mandatory updates
|
||||
checkForUpdates("stable");
|
||||
}
|
||||
|
||||
void OTAManager::checkForUpdates(const String& channel) {
|
||||
if (_status != Status::IDLE) {
|
||||
LOG_WARNING("OTA check already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus(Status::CHECKING_VERSION);
|
||||
LOG_INFO("Checking for firmware updates in %s channel for %s...",
|
||||
channel.c_str(), _configManager.getHardwareVariant().c_str());
|
||||
|
||||
if (checkVersion(channel)) {
|
||||
float currentVersion = getCurrentVersion();
|
||||
LOG_INFO("Current version: %.1f, Available version: %.1f (Channel: %s)",
|
||||
currentVersion, _availableVersion, channel.c_str());
|
||||
|
||||
if (_availableVersion > currentVersion) {
|
||||
_updateAvailable = true;
|
||||
LOG_INFO("New version available! Mandatory: %s, Emergency: %s",
|
||||
_isMandatory ? "YES" : "NO", _isEmergency ? "YES" : "NO");
|
||||
setStatus(Status::IDLE);
|
||||
|
||||
// Auto-update for emergency or mandatory updates during boot check
|
||||
if (channel == "stable" && (_isEmergency || _isMandatory)) {
|
||||
LOG_INFO("Emergency/Mandatory update detected - starting automatic update");
|
||||
update(channel);
|
||||
}
|
||||
} else {
|
||||
_updateAvailable = false;
|
||||
LOG_INFO("No new version available");
|
||||
setStatus(Status::IDLE);
|
||||
}
|
||||
} else {
|
||||
_updateAvailable = false;
|
||||
setStatus(Status::FAILED, _lastError);
|
||||
}
|
||||
}
|
||||
|
||||
void OTAManager::update() {
|
||||
update("stable"); // Default to stable channel
|
||||
}
|
||||
|
||||
void OTAManager::update(const String& channel) {
|
||||
if (_status != Status::IDLE) {
|
||||
LOG_WARNING("OTA update already in progress");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_updateAvailable) {
|
||||
LOG_WARNING("No update available for channel: %s", channel.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Starting OTA update from %s channel...", channel.c_str());
|
||||
setStatus(Status::DOWNLOADING);
|
||||
|
||||
if (downloadAndInstall(channel)) {
|
||||
setStatus(Status::SUCCESS);
|
||||
LOG_INFO("Update successfully finished. Rebooting...");
|
||||
delay(1000);
|
||||
ESP.restart();
|
||||
} else {
|
||||
setStatus(Status::FAILED, _lastError);
|
||||
}
|
||||
}
|
||||
|
||||
float OTAManager::getCurrentVersion() const {
|
||||
String fwVersionStr = _configManager.getFwVersion();
|
||||
return fwVersionStr.toFloat();
|
||||
}
|
||||
|
||||
void OTAManager::setStatus(Status status, ErrorCode error) {
|
||||
_status = status;
|
||||
_lastError = error;
|
||||
|
||||
if (_statusCallback) {
|
||||
_statusCallback(status, error);
|
||||
}
|
||||
}
|
||||
|
||||
void OTAManager::notifyProgress(size_t current, size_t total) {
|
||||
if (_progressCallback) {
|
||||
_progressCallback(current, total);
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced version checking with channel support and multiple servers
|
||||
bool OTAManager::checkVersion(const String& channel) {
|
||||
std::vector<String> servers = _configManager.getUpdateServers();
|
||||
auto& updateConfig = _configManager.getUpdateConfig();
|
||||
|
||||
for (size_t serverIndex = 0; serverIndex < servers.size(); serverIndex++) {
|
||||
String baseUrl = servers[serverIndex];
|
||||
String metadataUrl = baseUrl + "/ota/" + _configManager.getHardwareVariant() + "/" + channel + "/metadata.json";
|
||||
|
||||
LOG_INFO("OTA: Trying server %d/%d: %s", serverIndex + 1, servers.size(), baseUrl.c_str());
|
||||
|
||||
HTTPClient http;
|
||||
http.setTimeout(updateConfig.timeout);
|
||||
http.begin(metadataUrl.c_str());
|
||||
|
||||
int retryCount = 0;
|
||||
int httpCode = -1;
|
||||
|
||||
// Retry logic for current server
|
||||
while (retryCount < updateConfig.retries && httpCode != HTTP_CODE_OK) {
|
||||
if (retryCount > 0) {
|
||||
LOG_INFO("OTA: Retry %d/%d for %s", retryCount + 1, updateConfig.retries, baseUrl.c_str());
|
||||
delay(1000 * retryCount); // Exponential backoff
|
||||
}
|
||||
|
||||
httpCode = http.GET();
|
||||
retryCount++;
|
||||
}
|
||||
|
||||
if (httpCode == HTTP_CODE_OK) {
|
||||
String jsonStr = http.getString();
|
||||
http.end();
|
||||
|
||||
// Parse JSON metadata
|
||||
DynamicJsonDocument doc(1024);
|
||||
DeserializationError error = deserializeJson(doc, jsonStr);
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("OTA: Failed to parse metadata JSON from %s: %s",
|
||||
baseUrl.c_str(), error.c_str());
|
||||
continue; // Try next server
|
||||
}
|
||||
|
||||
// Extract metadata
|
||||
_availableVersion = doc["version"].as<float>();
|
||||
_availableChecksum = doc["checksum"].as<String>();
|
||||
_updateChannel = doc["channel"].as<String>();
|
||||
_isMandatory = doc["mandatory"].as<bool>();
|
||||
_isEmergency = doc["emergency"].as<bool>();
|
||||
|
||||
// Validate hardware variant matches
|
||||
String hwVariant = doc["hardwareVariant"].as<String>();
|
||||
String ourHardwareVariant = _configManager.getHardwareVariant();
|
||||
if (!hwVariant.isEmpty() && hwVariant != ourHardwareVariant) {
|
||||
LOG_ERROR("OTA: Hardware variant mismatch! Expected: %s, Got: %s",
|
||||
ourHardwareVariant.c_str(), hwVariant.c_str());
|
||||
continue; // Try next server
|
||||
}
|
||||
|
||||
if (_availableVersion == 0.0f) {
|
||||
LOG_ERROR("OTA: Invalid version in metadata from %s", baseUrl.c_str());
|
||||
continue; // Try next server
|
||||
}
|
||||
|
||||
if (_availableChecksum.length() != 64) { // SHA256 is 64 hex characters
|
||||
LOG_ERROR("OTA: Invalid checksum in metadata from %s", baseUrl.c_str());
|
||||
continue; // Try next server
|
||||
}
|
||||
|
||||
LOG_INFO("OTA: Successfully got metadata from %s", baseUrl.c_str());
|
||||
return true; // Success!
|
||||
} else {
|
||||
LOG_ERROR("OTA: Server %s failed after %d retries. HTTP error: %d",
|
||||
baseUrl.c_str(), updateConfig.retries, httpCode);
|
||||
http.end();
|
||||
}
|
||||
}
|
||||
|
||||
// All servers failed
|
||||
LOG_ERROR("OTA: All %d servers failed to provide metadata", servers.size());
|
||||
_lastError = ErrorCode::HTTP_ERROR;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Enhanced download and install with channel support and multiple servers
|
||||
bool OTAManager::downloadAndInstall(const String& channel) {
|
||||
std::vector<String> servers = _configManager.getUpdateServers();
|
||||
|
||||
for (size_t serverIndex = 0; serverIndex < servers.size(); serverIndex++) {
|
||||
String baseUrl = servers[serverIndex];
|
||||
String firmwareUrl = baseUrl + "/ota/" + _configManager.getHardwareVariant() + "/" + channel + "/firmware.bin";
|
||||
|
||||
LOG_INFO("OTA: Trying firmware download from server %d/%d: %s",
|
||||
serverIndex + 1, servers.size(), baseUrl.c_str());
|
||||
|
||||
if (downloadToSD(firmwareUrl, _availableChecksum)) {
|
||||
// Success! Now install from SD
|
||||
return installFromSD("/firmware/staged_update.bin");
|
||||
} else {
|
||||
LOG_WARNING("OTA: Firmware download failed from %s, trying next server", baseUrl.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// All servers failed
|
||||
LOG_ERROR("OTA: All %d servers failed to provide firmware", servers.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OTAManager::downloadToSD(const String& url, const String& expectedChecksum) {
|
||||
// This method now receives the exact firmware URL from downloadAndInstall
|
||||
// The server selection logic is handled there
|
||||
if (!_fileManager) {
|
||||
LOG_ERROR("FileManager not set!");
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure firmware directory exists
|
||||
_fileManager->createDirectory("/firmware");
|
||||
|
||||
// Download to temporary file
|
||||
String tempPath = "/firmware/staged_update.bin";
|
||||
|
||||
HTTPClient http;
|
||||
http.begin(url.c_str());
|
||||
int httpCode = http.GET();
|
||||
|
||||
if (httpCode != HTTP_CODE_OK) {
|
||||
LOG_ERROR("Download HTTP error code: %d", httpCode);
|
||||
setStatus(Status::FAILED, ErrorCode::HTTP_ERROR);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
int contentLength = http.getSize();
|
||||
if (contentLength <= 0) {
|
||||
LOG_ERROR("Invalid content length");
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Open file for writing
|
||||
File file = SD.open(tempPath.c_str(), FILE_WRITE);
|
||||
if (!file) {
|
||||
LOG_ERROR("Failed to create temporary update file");
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
http.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
WiFiClient* stream = http.getStreamPtr();
|
||||
uint8_t buffer[1024];
|
||||
size_t written = 0;
|
||||
|
||||
while (http.connected() && written < contentLength) {
|
||||
size_t available = stream->available();
|
||||
if (available) {
|
||||
size_t toRead = min(available, sizeof(buffer));
|
||||
size_t bytesRead = stream->readBytes(buffer, toRead);
|
||||
|
||||
if (bytesRead > 0) {
|
||||
size_t bytesWritten = file.write(buffer, bytesRead);
|
||||
if (bytesWritten != bytesRead) {
|
||||
LOG_ERROR("SD write failed");
|
||||
file.close();
|
||||
http.end();
|
||||
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
||||
return false;
|
||||
}
|
||||
written += bytesWritten;
|
||||
notifyProgress(written, contentLength);
|
||||
}
|
||||
}
|
||||
yield();
|
||||
}
|
||||
|
||||
file.close();
|
||||
http.end();
|
||||
|
||||
if (written != contentLength) {
|
||||
LOG_ERROR("Download incomplete: %d/%d bytes", written, contentLength);
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Download complete (%d bytes)", written);
|
||||
|
||||
// Verify checksum
|
||||
if (!verifyChecksum(tempPath, expectedChecksum)) {
|
||||
LOG_ERROR("Checksum verification failed after download");
|
||||
_fileManager->deleteFile(tempPath);
|
||||
setStatus(Status::FAILED, ErrorCode::CHECKSUM_MISMATCH);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Download and checksum verification successful");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OTAManager::verifyChecksum(const String& filePath, const String& expectedChecksum) {
|
||||
String calculatedChecksum = calculateSHA256(filePath);
|
||||
|
||||
if (calculatedChecksum.isEmpty()) {
|
||||
LOG_ERROR("Failed to calculate checksum");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool match = calculatedChecksum.equalsIgnoreCase(expectedChecksum);
|
||||
|
||||
if (match) {
|
||||
LOG_INFO("Checksum verification passed");
|
||||
} else {
|
||||
LOG_ERROR("Checksum mismatch!");
|
||||
LOG_ERROR("Expected: %s", expectedChecksum.c_str());
|
||||
LOG_ERROR("Calculated: %s", calculatedChecksum.c_str());
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
String OTAManager::calculateSHA256(const String& filePath) {
|
||||
File file = SD.open(filePath.c_str());
|
||||
if (!file) {
|
||||
LOG_ERROR("Failed to open file for checksum calculation: %s", filePath.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
mbedtls_md_context_t ctx;
|
||||
mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
|
||||
|
||||
mbedtls_md_init(&ctx);
|
||||
mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
|
||||
mbedtls_md_starts(&ctx);
|
||||
|
||||
uint8_t buffer[1024];
|
||||
size_t bytesRead;
|
||||
|
||||
while ((bytesRead = file.readBytes((char*)buffer, sizeof(buffer))) > 0) {
|
||||
mbedtls_md_update(&ctx, buffer, bytesRead);
|
||||
}
|
||||
|
||||
uint8_t hash[32];
|
||||
mbedtls_md_finish(&ctx, hash);
|
||||
mbedtls_md_free(&ctx);
|
||||
|
||||
file.close();
|
||||
|
||||
// Convert to hex string
|
||||
String hashString = "";
|
||||
for (int i = 0; i < 32; i++) {
|
||||
String hex = String(hash[i], HEX);
|
||||
if (hex.length() == 1) {
|
||||
hex = "0" + hex;
|
||||
}
|
||||
hashString += hex;
|
||||
}
|
||||
|
||||
return hashString;
|
||||
}
|
||||
|
||||
bool OTAManager::installFromSD(const String& filePath) {
|
||||
size_t updateSize = _fileManager->getFileSize(filePath);
|
||||
if (updateSize == 0) {
|
||||
LOG_ERROR("Empty update file");
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Installing firmware from SD (%u bytes)...", updateSize);
|
||||
setStatus(Status::INSTALLING);
|
||||
|
||||
if (!Update.begin(updateSize)) {
|
||||
LOG_ERROR("Not enough space to begin update");
|
||||
setStatus(Status::FAILED, ErrorCode::INSUFFICIENT_SPACE);
|
||||
return false;
|
||||
}
|
||||
|
||||
File updateBin = SD.open(filePath.c_str());
|
||||
if (!updateBin) {
|
||||
LOG_ERROR("Failed to open update file: %s", filePath.c_str());
|
||||
setStatus(Status::FAILED, ErrorCode::DOWNLOAD_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t written = Update.writeStream(updateBin);
|
||||
updateBin.close();
|
||||
|
||||
if (written == updateSize) {
|
||||
LOG_INFO("Update written successfully (%u bytes)", written);
|
||||
} else {
|
||||
LOG_ERROR("Written only %u/%u bytes", written, updateSize);
|
||||
setStatus(Status::FAILED, ErrorCode::WRITE_FAILED);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Update.end(true)) { // true = set new boot partition
|
||||
LOG_INFO("Update finished!");
|
||||
if (Update.isFinished()) {
|
||||
setStatus(Status::SUCCESS);
|
||||
LOG_INFO("Update complete. Cleaning up and rebooting...");
|
||||
|
||||
// Clean up the update files
|
||||
_fileManager->deleteFile(filePath);
|
||||
_fileManager->deleteFile("/firmware/staged_update.sha256");
|
||||
_fileManager->deleteFile("/firmware/update.sha256");
|
||||
|
||||
// Clear firmware validation state to force validation of new firmware
|
||||
nvs_handle_t nvsHandle;
|
||||
esp_err_t err = nvs_open("fw_validator", NVS_READWRITE, &nvsHandle);
|
||||
if (err == ESP_OK) {
|
||||
nvs_erase_key(nvsHandle, "val_state");
|
||||
nvs_erase_key(nvsHandle, "retry_count");
|
||||
nvs_erase_key(nvsHandle, "fail_count");
|
||||
nvs_commit(nvsHandle);
|
||||
nvs_close(nvsHandle);
|
||||
LOG_INFO("✅ OTA: Firmware validation state cleared - new firmware will be validated");
|
||||
} else {
|
||||
LOG_WARNING("⚠️ OTA: Failed to clear validation state: %s", esp_err_to_name(err));
|
||||
}
|
||||
|
||||
delay(1000);
|
||||
_configManager.setFwVersion(String(_availableVersion, 1)); // 1 decimal place
|
||||
_configManager.saveDeviceConfig();
|
||||
delay(500);
|
||||
ESP.restart();
|
||||
return true;
|
||||
} else {
|
||||
LOG_ERROR("Update not complete");
|
||||
setStatus(Status::FAILED, ErrorCode::VERIFICATION_FAILED);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("Update error: %s", Update.errorString());
|
||||
setStatus(Status::FAILED, ErrorCode::VERIFICATION_FAILED);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void OTAManager::checkFirmwareUpdateFromSD() {
|
||||
if (!_fileManager) {
|
||||
LOG_ERROR("FileManager not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_fileManager->fileExists("/firmware/update.bin")) {
|
||||
LOG_DEBUG("No update.bin found on SD card");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for checksum file
|
||||
String checksumFile = "/firmware/update.sha256";
|
||||
if (!_fileManager->fileExists(checksumFile)) {
|
||||
LOG_WARNING("No checksum file found, proceeding without verification");
|
||||
installFromSD("/firmware/update.bin");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read expected checksum
|
||||
File checksumFileHandle = SD.open(checksumFile.c_str());
|
||||
if (!checksumFileHandle) {
|
||||
LOG_ERROR("Failed to open checksum file");
|
||||
return;
|
||||
}
|
||||
|
||||
String expectedChecksum = checksumFileHandle.readString();
|
||||
checksumFileHandle.close();
|
||||
expectedChecksum.trim();
|
||||
|
||||
// Verify checksum
|
||||
if (!verifyChecksum("/firmware/update.bin", expectedChecksum)) {
|
||||
LOG_ERROR("Checksum verification failed, aborting update");
|
||||
setStatus(Status::FAILED, ErrorCode::CHECKSUM_MISMATCH);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Checksum verified, proceeding with update");
|
||||
installFromSD("/firmware/update.bin");
|
||||
}
|
||||
|
||||
bool OTAManager::performManualUpdate() {
|
||||
return performManualUpdate("stable");
|
||||
}
|
||||
|
||||
bool OTAManager::performManualUpdate(const String& channel) {
|
||||
if (_status != Status::IDLE) {
|
||||
LOG_WARNING("OTA update already in progress");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for updates in the specified channel first
|
||||
checkForUpdates(channel);
|
||||
|
||||
if (!_updateAvailable) {
|
||||
LOG_WARNING("No update available in %s channel", channel.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_INFO("Starting manual OTA update from %s channel via SD staging...", channel.c_str());
|
||||
setStatus(Status::DOWNLOADING);
|
||||
|
||||
String firmwareUrl = buildFirmwareUrl(channel);
|
||||
|
||||
// Download to SD first
|
||||
if (!downloadToSD(firmwareUrl, _availableChecksum)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Install from SD
|
||||
return installFromSD("/firmware/staged_update.bin");
|
||||
}
|
||||
|
||||
// Hardware variant management
|
||||
String OTAManager::getHardwareVariant() const {
|
||||
return _configManager.getHardwareVariant();
|
||||
}
|
||||
|
||||
void OTAManager::setHardwareVariant(const String& variant) {
|
||||
LOG_WARNING("OTAManager::setHardwareVariant is deprecated. Use ConfigManager::setHardwareVariant instead");
|
||||
// For backward compatibility, we could call configManager, but it's better to use ConfigManager directly
|
||||
}
|
||||
|
||||
// URL builders for multi-channel architecture
|
||||
String OTAManager::buildChannelUrl(const String& channel) const {
|
||||
auto& updateConfig = _configManager.getUpdateConfig();
|
||||
String baseUrl = updateConfig.fallbackServerUrl;
|
||||
|
||||
return baseUrl + "/" + _configManager.getHardwareVariant() + "/" + channel + "/";
|
||||
}
|
||||
|
||||
String OTAManager::buildMetadataUrl(const String& channel) const {
|
||||
return buildChannelUrl(channel) + "metadata.json";
|
||||
}
|
||||
|
||||
String OTAManager::buildFirmwareUrl(const String& channel) const {
|
||||
return buildChannelUrl(channel) + "firmware.bin";
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool OTAManager::isHealthy() const {
|
||||
// Check if FileManager dependency is set
|
||||
if (!_fileManager) {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - FileManager not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if we're not in a failed state
|
||||
if (_status == Status::FAILED) {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - In failed state");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if ConfigManager has valid configuration
|
||||
String hwVariant = _configManager.getHardwareVariant();
|
||||
if (hwVariant.isEmpty() || hwVariant == "BellSystems") {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - Invalid hardware variant: %s", hwVariant.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
String fwVersion = _configManager.getFwVersion();
|
||||
if (fwVersion.isEmpty() || fwVersion == "0") {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - Invalid firmware version: %s", fwVersion.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if update servers are available
|
||||
std::vector<String> servers = _configManager.getUpdateServers();
|
||||
if (servers.empty()) {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - No update servers configured");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if FileManager is healthy (can access SD card)
|
||||
if (!_fileManager->isHealthy()) {
|
||||
LOG_DEBUG("OTAManager: Unhealthy - FileManager is unhealthy");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
121
vesper/src/OTAManager/OTAManager.hpp
Normal file
121
vesper/src/OTAManager/OTAManager.hpp
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* OTAMANAGER.HPP - Over-The-Air Update Management System
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 🔄 THE UPDATE ORCHESTRATOR OF VESPER 🔄
|
||||
*
|
||||
* This class manages over-the-air firmware updates with safe, reliable
|
||||
* update mechanisms, version checking, and comprehensive error handling.
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Enhanced OTA management)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <Update.h>
|
||||
#include <WiFi.h>
|
||||
#include <SD.h>
|
||||
#include <mbedtls/md.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <functional>
|
||||
#include "../FileManager/FileManager.hpp"
|
||||
|
||||
class ConfigManager; // Forward declaration
|
||||
|
||||
class OTAManager {
|
||||
public:
|
||||
enum class Status {
|
||||
IDLE,
|
||||
CHECKING_VERSION,
|
||||
DOWNLOADING,
|
||||
INSTALLING,
|
||||
SUCCESS,
|
||||
FAILED
|
||||
};
|
||||
|
||||
enum class ErrorCode {
|
||||
NONE,
|
||||
HTTP_ERROR,
|
||||
VERSION_CHECK_FAILED,
|
||||
DOWNLOAD_FAILED,
|
||||
INSUFFICIENT_SPACE,
|
||||
WRITE_FAILED,
|
||||
VERIFICATION_FAILED,
|
||||
CHECKSUM_MISMATCH,
|
||||
METADATA_PARSE_FAILED
|
||||
};
|
||||
|
||||
// Callback types
|
||||
using ProgressCallback = std::function<void(size_t current, size_t total)>;
|
||||
using StatusCallback = std::function<void(Status status, ErrorCode error)>;
|
||||
|
||||
explicit OTAManager(ConfigManager& configManager);
|
||||
|
||||
void begin();
|
||||
void setFileManager(FileManager* fm);
|
||||
void checkForUpdates();
|
||||
void checkForUpdates(const String& channel); // Check specific channel
|
||||
void update();
|
||||
void update(const String& channel); // Update from specific channel
|
||||
void checkFirmwareUpdateFromSD(); // Check SD for firmware update
|
||||
bool performManualUpdate(); // Manual update triggered by app
|
||||
bool performManualUpdate(const String& channel); // Manual update from specific channel
|
||||
|
||||
// Hardware identification
|
||||
String getHardwareVariant() const;
|
||||
void setHardwareVariant(const String& variant); // Deprecated: Use ConfigManager instead
|
||||
|
||||
// Status and info
|
||||
Status getStatus() const { return _status; }
|
||||
ErrorCode getLastError() const { return _lastError; }
|
||||
float getCurrentVersion() const;
|
||||
float getAvailableVersion() const { return _availableVersion; }
|
||||
bool isUpdateAvailable() const { return _updateAvailable; }
|
||||
|
||||
// Callbacks
|
||||
void setProgressCallback(ProgressCallback callback) { _progressCallback = callback; }
|
||||
void setStatusCallback(StatusCallback callback) { _statusCallback = callback; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if OTAManager is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
private:
|
||||
ConfigManager& _configManager;
|
||||
FileManager* _fileManager;
|
||||
Status _status;
|
||||
ErrorCode _lastError;
|
||||
float _availableVersion;
|
||||
bool _updateAvailable;
|
||||
String _availableChecksum;
|
||||
String _updateChannel;
|
||||
bool _isMandatory;
|
||||
bool _isEmergency;
|
||||
|
||||
ProgressCallback _progressCallback;
|
||||
StatusCallback _statusCallback;
|
||||
|
||||
void setStatus(Status status, ErrorCode error = ErrorCode::NONE);
|
||||
void notifyProgress(size_t current, size_t total);
|
||||
bool checkVersion();
|
||||
bool checkVersion(const String& channel);
|
||||
bool checkChannelsMetadata();
|
||||
bool downloadAndInstall();
|
||||
bool downloadAndInstall(const String& channel);
|
||||
bool downloadToSD(const String& url, const String& expectedChecksum);
|
||||
bool verifyChecksum(const String& filePath, const String& expectedChecksum);
|
||||
String calculateSHA256(const String& filePath);
|
||||
bool installFromSD(const String& filePath);
|
||||
String buildChannelUrl(const String& channel) const;
|
||||
String buildMetadataUrl(const String& channel) const;
|
||||
String buildFirmwareUrl(const String& channel) const;
|
||||
};
|
||||
638
vesper/src/OutputManager/OutputManager.cpp
Normal file
638
vesper/src/OutputManager/OutputManager.cpp
Normal file
@@ -0,0 +1,638 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* OUTPUTMANAGER - FIXED VERSION - Complete Implementation
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#include "OutputManager.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../Logging/Logging.hpp"
|
||||
#include <Adafruit_PCF8574.h>
|
||||
#include <Adafruit_PCF8575.h>
|
||||
|
||||
// ==================== BASE CLASS IMPLEMENTATION ====================
|
||||
|
||||
OutputManager::~OutputManager() {
|
||||
stopDurationTask();
|
||||
if (_durationTaskHandle != nullptr) {
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
}
|
||||
|
||||
void OutputManager::startDurationTask() {
|
||||
if (_durationTaskHandle != nullptr) {
|
||||
return;
|
||||
}
|
||||
_activeOutputs.reserve(32); // Support up to 32 virtual outputs
|
||||
xTaskCreatePinnedToCore(durationTask, "OutputDuration", 4096, this, 5, &_durationTaskHandle, 1);
|
||||
LOG_INFO("⚡ Output Duration Management Task Initialized");
|
||||
}
|
||||
|
||||
void OutputManager::stopDurationTask() {
|
||||
if (_durationTaskHandle != nullptr) {
|
||||
vTaskDelete(_durationTaskHandle);
|
||||
_durationTaskHandle = nullptr;
|
||||
portENTER_CRITICAL(&_outputMutex);
|
||||
_activeOutputs.clear();
|
||||
portEXIT_CRITICAL(&_outputMutex);
|
||||
LOG_INFO("⚡ Output Duration Management Task Stopped");
|
||||
}
|
||||
}
|
||||
|
||||
void OutputManager::durationTask(void* parameter) {
|
||||
OutputManager* manager = static_cast<OutputManager*>(parameter);
|
||||
LOG_DEBUG("⚡ Output duration management task running on Core %d", xPortGetCoreID());
|
||||
while (true) {
|
||||
manager->processExpiredOutputs();
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
|
||||
void OutputManager::processExpiredOutputs() {
|
||||
uint64_t now = getMicros();
|
||||
portENTER_CRITICAL(&_outputMutex);
|
||||
for (auto it = _activeOutputs.begin(); it != _activeOutputs.end(); ++it) {
|
||||
uint64_t duration_micros = it->durationMs * 1000;
|
||||
if ((now - it->activationTime) >= duration_micros) {
|
||||
uint8_t outputIndex = it->outputIndex;
|
||||
_activeOutputs.erase(it);
|
||||
portEXIT_CRITICAL(&_outputMutex);
|
||||
extinguishOutput(outputIndex);
|
||||
LOG_VERBOSE("⚡ AUTO-EXTINGUISH Output:%d after %dms", outputIndex, duration_micros / 1000);
|
||||
return;
|
||||
}
|
||||
}
|
||||
portEXIT_CRITICAL(&_outputMutex);
|
||||
}
|
||||
|
||||
uint8_t OutputManager::getPhysicalOutput(uint8_t virtualOutput) const {
|
||||
if (!_configManager) {
|
||||
LOG_WARNING("⚠️ ConfigManager not available, using direct mapping for virtual output %d", virtualOutput);
|
||||
return virtualOutput;
|
||||
}
|
||||
if (!isValidVirtualOutput(virtualOutput)) {
|
||||
LOG_ERROR("❌ Invalid virtual output %d, using direct mapping", virtualOutput);
|
||||
return virtualOutput;
|
||||
}
|
||||
|
||||
// Get 1-indexed bell output from config
|
||||
uint16_t bellOutput1Indexed = _configManager->getBellOutput(virtualOutput);
|
||||
|
||||
// Handle unconfigured bells (255 = disabled)
|
||||
if (bellOutput1Indexed == 255) {
|
||||
LOG_WARNING("⚠️ Bell %d not configured (255)", virtualOutput);
|
||||
return 255; // Return invalid to prevent firing
|
||||
}
|
||||
|
||||
// Handle invalid 0 configuration
|
||||
if (bellOutput1Indexed == 0) {
|
||||
LOG_ERROR("❌ Bell %d configured as 0 (invalid - should be 1-indexed)", virtualOutput);
|
||||
return 255;
|
||||
}
|
||||
|
||||
// Convert 1-indexed config to 0-indexed physical output
|
||||
uint8_t physicalOutput = (uint8_t)(bellOutput1Indexed - 1);
|
||||
|
||||
LOG_DEBUG("🔗 Bell %d → 1-indexed config %d → 0-indexed output %d",
|
||||
virtualOutput, bellOutput1Indexed, physicalOutput);
|
||||
|
||||
return physicalOutput;
|
||||
}
|
||||
|
||||
bool OutputManager::isValidVirtualOutput(uint8_t virtualOutput) const {
|
||||
return virtualOutput < getMaxOutputs();
|
||||
}
|
||||
|
||||
bool OutputManager::isValidPhysicalOutput(uint8_t physicalOutput) const {
|
||||
if (physicalOutput == 255) { return false; }
|
||||
return physicalOutput < getMaxOutputs();
|
||||
}
|
||||
|
||||
void OutputManager::fireClockOutput(uint8_t virtualOutput, uint16_t durationMs) {
|
||||
if (!_initialized) {
|
||||
LOG_ERROR("❌ OutputManager not initialized for clock output!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_configManager) {
|
||||
LOG_ERROR("❌ ConfigManager not available for clock output mapping!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Map virtual clock output to physical output using clock configuration
|
||||
uint8_t physicalOutput;
|
||||
if (virtualOutput == 0) {
|
||||
// Virtual clock output 0 = C1
|
||||
physicalOutput = _configManager->getClockOutput1();
|
||||
if (physicalOutput == 255) {
|
||||
LOG_WARNING("⚠️ Clock C1 not configured (255)");
|
||||
return;
|
||||
}
|
||||
} else if (virtualOutput == 1) {
|
||||
// Virtual clock output 1 = C2
|
||||
physicalOutput = _configManager->getClockOutput2();
|
||||
if (physicalOutput == 255) {
|
||||
LOG_WARNING("⚠️ Clock C2 not configured (255)");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
LOG_ERROR("❌ Invalid virtual clock output: %d (only 0=C1, 1=C2 supported)", virtualOutput);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert 1-indexed config value to 0-indexed physical output
|
||||
if (physicalOutput == 0) {
|
||||
LOG_ERROR("❌ Clock output configured as 0 (invalid - should be 1-indexed)");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t zeroIndexedOutput = physicalOutput - 1; // Convert 1-indexed to 0-indexed
|
||||
|
||||
if (!isValidPhysicalOutput(zeroIndexedOutput)) {
|
||||
LOG_ERROR("❌ Invalid physical output for clock: %d (1-indexed config: %d, max outputs: %d)",
|
||||
zeroIndexedOutput, physicalOutput, getMaxOutputs());
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire the physical output directly
|
||||
fireOutputForDuration(zeroIndexedOutput, durationMs);
|
||||
|
||||
LOG_DEBUG("🕐 FIRE Clock Virtual %d (C%d) → 1-indexed config %d → 0-indexed output %d for %dms",
|
||||
virtualOutput, virtualOutput + 1, physicalOutput, zeroIndexedOutput, durationMs);
|
||||
}
|
||||
|
||||
// ==================== PCF8574/PCF8575 MULTI-CHIP IMPLEMENTATION ====================
|
||||
|
||||
// Single chip constructor
|
||||
PCF8574OutputManager::PCF8574OutputManager(uint8_t i2cAddress, ChipType chipType, uint8_t activeOutputs)
|
||||
: _chipCount(1), _totalOutputs(0), _allChipsInitialized(false) {
|
||||
_chips[0] = ChipConfig(i2cAddress, chipType, activeOutputs);
|
||||
updateTotalOutputs();
|
||||
}
|
||||
|
||||
// Dual chip constructor
|
||||
PCF8574OutputManager::PCF8574OutputManager(uint8_t addr1, ChipType chip1, uint8_t active1, uint8_t addr2, ChipType chip2, uint8_t active2)
|
||||
: _chipCount(2), _totalOutputs(0), _allChipsInitialized(false) {
|
||||
_chips[0] = ChipConfig(addr1, chip1, active1);
|
||||
_chips[1] = ChipConfig(addr2, chip2, active2);
|
||||
updateTotalOutputs();
|
||||
}
|
||||
|
||||
PCF8574OutputManager::~PCF8574OutputManager() {
|
||||
if (_allChipsInitialized) {
|
||||
emergencyShutdown();
|
||||
}
|
||||
for (uint8_t i = 0; i < _chipCount; i++) {
|
||||
shutdownChip(i);
|
||||
}
|
||||
}
|
||||
|
||||
bool PCF8574OutputManager::initialize() {
|
||||
LOG_INFO("🔌 Initializing Multi-Chip PCF857x Output Manager (%d chips)", _chipCount);
|
||||
delay(100);
|
||||
|
||||
bool allSuccess = true;
|
||||
for (uint8_t i = 0; i < _chipCount; i++) {
|
||||
if (!initializeChip(i)) {
|
||||
LOG_ERROR("❌ Failed to initialize chip %d!", i);
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!allSuccess) {
|
||||
LOG_ERROR("❌ Not all chips initialized successfully!");
|
||||
return false;
|
||||
}
|
||||
|
||||
emergencyShutdown();
|
||||
startDurationTask();
|
||||
_allChipsInitialized = true;
|
||||
_initialized = true; // Set base class flag too!
|
||||
|
||||
LOG_INFO("✅ Multi-Chip PCF857x Output Manager Initialized (%d total outputs)", _totalOutputs);
|
||||
generateHardwareTypeString();
|
||||
|
||||
if (_configManager) {
|
||||
LOG_INFO("📋 Virtual Output Configuration Mappings:");
|
||||
for (uint8_t i = 0; i < min(16, (int)_totalOutputs); i++) { // Check virtual outputs
|
||||
uint16_t configOutput = _configManager->getBellOutput(i);
|
||||
if (configOutput < _totalOutputs) {
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(configOutput);
|
||||
LOG_DEBUG(" Bell %d → Virtual Output %d → %s[%d] Pin %d", i, configOutput, info.chipType, info.chipIndex, info.localPin);
|
||||
} else if (configOutput == 255) {
|
||||
LOG_DEBUG(" Bell %d → Not configured (255)", i);
|
||||
} else {
|
||||
LOG_WARNING("⚠️ Bell %d mapped to invalid virtual output %d (max: %d)", i, configOutput, _totalOutputs - 1);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t c1 = _configManager->getClockOutput1();
|
||||
uint8_t c2 = _configManager->getClockOutput2();
|
||||
LOG_INFO("🕐 Clock Virtual Output Mappings:");
|
||||
|
||||
if (c1 != 255 && c1 < _totalOutputs) {
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(c1);
|
||||
LOG_DEBUG(" Clock C1 → Virtual Output %d → %s[%d] Pin %d", c1, info.chipType, info.chipIndex, info.localPin);
|
||||
} else {
|
||||
LOG_DEBUG(" Clock C1 → Not configured");
|
||||
}
|
||||
|
||||
if (c2 != 255 && c2 < _totalOutputs) {
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(c2);
|
||||
LOG_DEBUG(" Clock C2 → Virtual Output %d → %s[%d] Pin %d", c2, info.chipType, info.chipIndex, info.localPin);
|
||||
} else {
|
||||
LOG_DEBUG(" Clock C2 → Not configured");
|
||||
}
|
||||
|
||||
// Show virtual output mapping
|
||||
LOG_INFO("🔗 Virtual Output Mapping:");
|
||||
for (uint8_t i = 0; i < _totalOutputs; i++) {
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(i);
|
||||
LOG_DEBUG(" Virtual Output %d → %s[%d] Pin %d", i, info.chipType, info.chipIndex, info.localPin);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::fireOutput(uint8_t outputIndex) {
|
||||
if (!_allChipsInitialized) {
|
||||
LOG_ERROR("❌ PCF857x chips not initialized!");
|
||||
return;
|
||||
}
|
||||
if (!isValidVirtualOutput(outputIndex)) {
|
||||
LOG_ERROR("❌ Invalid virtual output: %d (max: %d)", outputIndex, _totalOutputs - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
||||
writeOutputToChip(info.chipIndex, info.localPin, false);
|
||||
|
||||
LOG_DEBUG("🔥 FIRE Virtual Output %d → %s[%d] Pin %d", outputIndex, info.chipType, info.chipIndex, info.localPin);
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::extinguishOutput(uint8_t outputIndex) {
|
||||
if (!_allChipsInitialized) return;
|
||||
if (!isValidVirtualOutput(outputIndex)) return;
|
||||
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
||||
writeOutputToChip(info.chipIndex, info.localPin, true);
|
||||
|
||||
LOG_DEBUG("💧 EXTINGUISH Virtual Output %d → %s[%d] Pin %d", outputIndex, info.chipType, info.chipIndex, info.localPin);
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::fireOutputForDuration(uint8_t outputIndex, uint16_t durationMs) {
|
||||
if (!_allChipsInitialized || !isValidVirtualOutput(outputIndex)) return;
|
||||
fireOutput(outputIndex);
|
||||
uint64_t now = getMicros();
|
||||
portENTER_CRITICAL(&_outputMutex);
|
||||
_activeOutputs.push_back({outputIndex, now, durationMs});
|
||||
portEXIT_CRITICAL(&_outputMutex);
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::firePhysicalOutput(uint8_t physicalOutput, bool state) {
|
||||
if (!_allChipsInitialized || !isValidPhysicalOutput(physicalOutput)) return;
|
||||
OutputMapping mapping = getOutputMapping(physicalOutput);
|
||||
writeOutputToChip(mapping.chipIndex, mapping.localPin, !state); // Invert because we're using active LOW
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::fireOutputsBatch(const std::vector<uint8_t>& outputIndices) {
|
||||
if (!_allChipsInitialized) return;
|
||||
for (uint8_t outputIndex : outputIndices) {
|
||||
if (!isValidVirtualOutput(outputIndex)) continue;
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
||||
writeOutputToChip(info.chipIndex, info.localPin, false);
|
||||
}
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::extinguishOutputsBatch(const std::vector<uint8_t>& outputIndices) {
|
||||
if (!_allChipsInitialized) return;
|
||||
for (uint8_t outputIndex : outputIndices) {
|
||||
if (!isValidVirtualOutput(outputIndex)) continue;
|
||||
VirtualOutputInfo info = getVirtualOutputInfo(outputIndex);
|
||||
writeOutputToChip(info.chipIndex, info.localPin, true);
|
||||
}
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::fireOutputsBatchForDuration(const std::vector<std::pair<uint8_t, uint16_t>>& outputDurations) {
|
||||
if (!_allChipsInitialized) return;
|
||||
uint64_t now = getMicros();
|
||||
std::vector<uint8_t> outputsToFire;
|
||||
portENTER_CRITICAL(&_outputMutex);
|
||||
for (const auto& [outputIndex, durationMs] : outputDurations) {
|
||||
if (isValidVirtualOutput(outputIndex)) {
|
||||
outputsToFire.push_back(outputIndex);
|
||||
_activeOutputs.push_back({outputIndex, now, durationMs});
|
||||
}
|
||||
}
|
||||
portEXIT_CRITICAL(&_outputMutex);
|
||||
fireOutputsBatch(outputsToFire);
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::emergencyShutdown() {
|
||||
LOG_WARNING("🚨 PCF857x EMERGENCY SHUTDOWN - All outputs HIGH");
|
||||
portENTER_CRITICAL(&_outputMutex);
|
||||
_activeOutputs.clear();
|
||||
portEXIT_CRITICAL(&_outputMutex);
|
||||
|
||||
for (uint8_t chipIndex = 0; chipIndex < _chipCount; chipIndex++) {
|
||||
if (_chips[chipIndex].initialized) {
|
||||
for (uint8_t pin = 0; pin < _chips[chipIndex].activeOutputs; pin++) {
|
||||
writeOutputToChip(chipIndex, pin, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const char* PCF8574OutputManager::getHardwareType() const {
|
||||
generateHardwareTypeString();
|
||||
return _hardwareTypeBuffer;
|
||||
}
|
||||
|
||||
ChipConfig PCF8574OutputManager::getChipConfig(uint8_t chipIndex) const {
|
||||
if (chipIndex < _chipCount) {
|
||||
return _chips[chipIndex];
|
||||
}
|
||||
return ChipConfig(); // Return default config if invalid index
|
||||
}
|
||||
|
||||
bool PCF8574OutputManager::addChip(uint8_t i2cAddress, ChipType chipType, uint8_t activeOutputs) {
|
||||
if (_chipCount >= MAX_CHIPS) {
|
||||
LOG_ERROR("❌ Cannot add more chips - maximum %d chips supported", MAX_CHIPS);
|
||||
return false;
|
||||
}
|
||||
|
||||
_chips[_chipCount] = ChipConfig(i2cAddress, chipType, activeOutputs);
|
||||
_chipCount++;
|
||||
updateTotalOutputs();
|
||||
|
||||
LOG_INFO("✅ Added chip %d: %s at 0x%02X (%d/%d active outputs)",
|
||||
_chipCount - 1,
|
||||
(chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575",
|
||||
i2cAddress,
|
||||
_chips[_chipCount - 1].activeOutputs,
|
||||
_chips[_chipCount - 1].maxOutputs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PCF8574OutputManager::VirtualOutputInfo PCF8574OutputManager::getVirtualOutputInfo(uint8_t virtualOutput) const {
|
||||
VirtualOutputInfo info;
|
||||
|
||||
if (virtualOutput >= _totalOutputs) {
|
||||
// Invalid - return chip 0, pin 0 as fallback
|
||||
info.chipIndex = 0;
|
||||
info.localPin = 0;
|
||||
info.chipType = (_chipCount > 0 && _chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
LOG_ERROR("❌ Invalid virtual output %d (max: %d)", virtualOutput, _totalOutputs - 1);
|
||||
return info;
|
||||
}
|
||||
|
||||
// Map virtual output to physical chip and pin
|
||||
if (virtualOutput < _chips[0].activeOutputs) {
|
||||
// Output is on first chip
|
||||
info.chipIndex = 0;
|
||||
info.localPin = virtualOutput;
|
||||
info.chipType = (_chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
} else if (_chipCount > 1) {
|
||||
// Output is on second chip
|
||||
info.chipIndex = 1;
|
||||
info.localPin = virtualOutput - _chips[0].activeOutputs;
|
||||
info.chipType = (_chips[1].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
} else {
|
||||
// Should not happen, but fallback to chip 0
|
||||
info.chipIndex = 0;
|
||||
info.localPin = 0;
|
||||
info.chipType = (_chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
LOG_ERROR("❌ Virtual output %d exceeds available outputs on single chip", virtualOutput);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::setChipActiveOutputs(uint8_t chipIndex, uint8_t activeOutputs) {
|
||||
if (chipIndex >= _chipCount) {
|
||||
LOG_ERROR("❌ Invalid chip index %d (max: %d)", chipIndex, _chipCount - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t maxOutputs = _chips[chipIndex].maxOutputs;
|
||||
_chips[chipIndex].activeOutputs = min(activeOutputs, maxOutputs);
|
||||
updateTotalOutputs();
|
||||
|
||||
LOG_INFO("✅ Updated chip %d active outputs: %d/%d", chipIndex, _chips[chipIndex].activeOutputs, maxOutputs);
|
||||
}
|
||||
|
||||
uint8_t PCF8574OutputManager::getChipActiveOutputs(uint8_t chipIndex) const {
|
||||
if (chipIndex >= _chipCount) {
|
||||
LOG_ERROR("❌ Invalid chip index %d (max: %d)", chipIndex, _chipCount - 1);
|
||||
return 0;
|
||||
}
|
||||
return _chips[chipIndex].activeOutputs;
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
bool PCF8574OutputManager::initializeChip(uint8_t chipIndex) {
|
||||
if (chipIndex >= _chipCount) return false;
|
||||
|
||||
ChipConfig& chip = _chips[chipIndex];
|
||||
const char* chipTypeStr = (chip.chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
|
||||
LOG_DEBUG("🔌 Initializing %s at address 0x%02X", chipTypeStr, chip.i2cAddress);
|
||||
|
||||
try {
|
||||
if (chip.chipType == ChipType::PCF8574) {
|
||||
// Use static instance to avoid memory allocation issues
|
||||
static Adafruit_PCF8574 pcf8574Instances[MAX_CHIPS];
|
||||
chip.chipInstance = &pcf8574Instances[chipIndex];
|
||||
Adafruit_PCF8574* pcf = static_cast<Adafruit_PCF8574*>(chip.chipInstance);
|
||||
|
||||
if (!pcf->begin(chip.i2cAddress, &Wire)) {
|
||||
LOG_ERROR("❌ Failed to initialize PCF8574 at address 0x%02X", chip.i2cAddress);
|
||||
chip.chipInstance = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure all pins as outputs and set them HIGH (inactive)
|
||||
for (uint8_t pin = 0; pin < 8; pin++) {
|
||||
pcf->pinMode(pin, OUTPUT);
|
||||
pcf->digitalWrite(pin, HIGH);
|
||||
}
|
||||
} else { // PCF8575
|
||||
// Use static instance to avoid memory allocation issues
|
||||
static Adafruit_PCF8575 pcf8575Instances[MAX_CHIPS];
|
||||
chip.chipInstance = &pcf8575Instances[chipIndex];
|
||||
Adafruit_PCF8575* pcf = static_cast<Adafruit_PCF8575*>(chip.chipInstance);
|
||||
|
||||
if (!pcf->begin(chip.i2cAddress, &Wire)) {
|
||||
LOG_ERROR("❌ Failed to initialize PCF8575 at address 0x%02X", chip.i2cAddress);
|
||||
chip.chipInstance = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Configure all pins as outputs and set them HIGH (inactive)
|
||||
for (uint8_t pin = 0; pin < 16; pin++) {
|
||||
pcf->pinMode(pin, OUTPUT);
|
||||
pcf->digitalWrite(pin, HIGH);
|
||||
}
|
||||
}
|
||||
|
||||
chip.initialized = true;
|
||||
LOG_DEBUG("✅ %s at 0x%02X initialized successfully", chipTypeStr, chip.i2cAddress);
|
||||
return true;
|
||||
|
||||
} catch (...) {
|
||||
LOG_ERROR("❌ Exception during %s initialization at 0x%02X", chipTypeStr, chip.i2cAddress);
|
||||
chip.chipInstance = nullptr;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::shutdownChip(uint8_t chipIndex) {
|
||||
if (chipIndex >= _chipCount) return;
|
||||
|
||||
ChipConfig& chip = _chips[chipIndex];
|
||||
if (chip.initialized && chip.chipInstance) {
|
||||
// Set all outputs to HIGH (inactive) before shutdown
|
||||
for (uint8_t pin = 0; pin < chip.activeOutputs; pin++) {
|
||||
writeOutputToChip(chipIndex, pin, true);
|
||||
}
|
||||
chip.initialized = false;
|
||||
chip.chipInstance = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
PCF8574OutputManager::OutputMapping PCF8574OutputManager::getOutputMapping(uint8_t physicalOutput) const {
|
||||
OutputMapping mapping;
|
||||
|
||||
if (physicalOutput < _chips[0].activeOutputs) {
|
||||
// Output is on first chip
|
||||
mapping.chipIndex = 0;
|
||||
mapping.localPin = physicalOutput;
|
||||
} else if (_chipCount > 1 && physicalOutput < _totalOutputs) {
|
||||
// Output is on second chip
|
||||
mapping.chipIndex = 1;
|
||||
mapping.localPin = physicalOutput - _chips[0].activeOutputs;
|
||||
} else {
|
||||
// Invalid output - return chip 0, pin 0 as safe fallback
|
||||
mapping.chipIndex = 0;
|
||||
mapping.localPin = 0;
|
||||
LOG_ERROR("❌ Invalid physical output %d mapped to fallback", physicalOutput);
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::writeOutputToChip(uint8_t chipIndex, uint8_t pin, bool state) {
|
||||
if (chipIndex >= _chipCount || !_chips[chipIndex].initialized) return;
|
||||
if (!isValidOutputForChip(chipIndex, pin)) return;
|
||||
|
||||
ChipConfig& chip = _chips[chipIndex];
|
||||
|
||||
if (chip.chipType == ChipType::PCF8574) {
|
||||
Adafruit_PCF8574* pcf = static_cast<Adafruit_PCF8574*>(chip.chipInstance);
|
||||
pcf->digitalWrite(pin, state ? HIGH : LOW);
|
||||
} else { // PCF8575
|
||||
Adafruit_PCF8575* pcf = static_cast<Adafruit_PCF8575*>(chip.chipInstance);
|
||||
pcf->digitalWrite(pin, state ? HIGH : LOW);
|
||||
}
|
||||
}
|
||||
|
||||
bool PCF8574OutputManager::isValidOutputForChip(uint8_t chipIndex, uint8_t pin) const {
|
||||
if (chipIndex >= _chipCount) return false;
|
||||
return pin < _chips[chipIndex].activeOutputs;
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::updateTotalOutputs() {
|
||||
_totalOutputs = 0;
|
||||
for (uint8_t i = 0; i < _chipCount; i++) {
|
||||
_totalOutputs += _chips[i].activeOutputs;
|
||||
}
|
||||
}
|
||||
|
||||
void PCF8574OutputManager::generateHardwareTypeString() const {
|
||||
if (_chipCount == 1) {
|
||||
const char* chipType = (_chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
snprintf(_hardwareTypeBuffer, sizeof(_hardwareTypeBuffer), "%s I2C Expander (%d/%d outputs)",
|
||||
chipType, _chips[0].activeOutputs, _chips[0].maxOutputs);
|
||||
} else {
|
||||
const char* chip1Type = (_chips[0].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
const char* chip2Type = (_chips[1].chipType == ChipType::PCF8574) ? "PCF8574" : "PCF8575";
|
||||
snprintf(_hardwareTypeBuffer, sizeof(_hardwareTypeBuffer), "%s+%s I2C Expanders (%d outputs total)",
|
||||
chip1Type, chip2Type, _totalOutputs);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== HEALTH CHECK IMPLEMENTATION ====================
|
||||
|
||||
bool OutputManager::isHealthy() const {
|
||||
// Basic health checks
|
||||
if (!_initialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if ConfigManager is available
|
||||
if (!_configManager) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if duration task is running when it should be
|
||||
if (_initialized && _durationTaskHandle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if max outputs is reasonable
|
||||
if (getMaxOutputs() == 0 || getMaxOutputs() > 64) { // Sanity check
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PCF8574OutputManager::isHealthy() const {
|
||||
// Call base class health check first
|
||||
if (!OutputManager::isHealthy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check chip-specific health
|
||||
if (_chipCount == 0 || _chipCount > MAX_CHIPS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_allChipsInitialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check each chip's health
|
||||
for (uint8_t i = 0; i < _chipCount; i++) {
|
||||
const ChipConfig& chip = _chips[i];
|
||||
|
||||
// Check if chip is properly initialized
|
||||
if (!chip.initialized || chip.chipInstance == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if active outputs are within valid range
|
||||
if (chip.activeOutputs == 0 || chip.activeOutputs > chip.maxOutputs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if I2C address is in valid range
|
||||
if (chip.i2cAddress < 0x20 || chip.i2cAddress > 0x27) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check total outputs consistency
|
||||
uint8_t calculatedTotal = 0;
|
||||
for (uint8_t i = 0; i < _chipCount; i++) {
|
||||
calculatedTotal += _chips[i].activeOutputs;
|
||||
}
|
||||
|
||||
if (calculatedTotal != _totalOutputs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
346
vesper/src/OutputManager/OutputManager.hpp
Normal file
346
vesper/src/OutputManager/OutputManager.hpp
Normal file
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* OUTPUTMANAGER.HPP - FIXED VERSION - Hardware Abstraction Layer for Relay Control
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* FIXES APPLIED:
|
||||
* - Better validation for physical outputs (handles 255 = unconfigured)
|
||||
* - Enhanced logging for debugging configuration issues
|
||||
* - Clear separation between bell outputs and clock outputs
|
||||
* - Improved error messages with context
|
||||
*
|
||||
* ⚡ THE HARDWARE ABSTRACTION POWERHOUSE ⚡
|
||||
*
|
||||
* This module provides a clean, unified interface for controlling different
|
||||
* types of relay/output hardware. It completely abstracts the hardware details
|
||||
* from the rest of the system, allowing easy swapping between different
|
||||
* relay control methods.
|
||||
*
|
||||
* 🏗️ CLEAN ARCHITECTURE:
|
||||
* • Abstract base class with polymorphic interface
|
||||
* • Multiple concrete implementations for different hardware
|
||||
* • Automatic duration management with microsecond precision
|
||||
* • Thread-safe operation with FreeRTOS integration
|
||||
* • Comprehensive error handling and safety features
|
||||
*
|
||||
* 🔧 SUPPORTED HARDWARE:
|
||||
* • PCF8574OutputManager: I2C GPIO expanders (PCF8574: 8 outputs, PCF8575: 16 outputs)
|
||||
* • Multi-chip support: Up to 2 chips for maximum flexibility
|
||||
*
|
||||
* ⏱️ PRECISION TIMING:
|
||||
* • Microsecond-accurate duration control
|
||||
* • Automatic relay timeout management
|
||||
* • Non-blocking operation with background task
|
||||
* • Configurable duration per bell/relay
|
||||
*
|
||||
* 🔒 SAFETY FEATURES:
|
||||
* • Emergency shutdown capability
|
||||
* • Bounds checking on all operations
|
||||
* • Hardware initialization validation
|
||||
* • Automatic cleanup on failure
|
||||
*
|
||||
* 📌 USAGE:
|
||||
* Choose the appropriate implementation in main.cpp:
|
||||
* - PCF8574OutputManager for I2C expander setups
|
||||
* - GPIOOutputManager for direct pin control
|
||||
* - MockOutputManager for testing and development
|
||||
*
|
||||
* 🔔 BELL vs CLOCK OUTPUTS:
|
||||
* - Bell outputs: Managed via ConfigManager->getBellOutput(bellIndex)
|
||||
* - Clock outputs: Managed via ConfigManager->getClockOutput1()/getClockOutput2()
|
||||
* - Both use the same OutputManager but for different purposes
|
||||
* - Bell indices: 0-15 (for 16 bells)
|
||||
* - Clock outputs: c1, c2 (for clock mechanism)
|
||||
*
|
||||
* 📋 VERSION: 2.1 (Fixed configuration validation)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// SYSTEM INCLUDES - Core libraries for hardware control
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
#include <Arduino.h> // Arduino core functionality
|
||||
#include <cstdint> // Fixed-width integer types
|
||||
#include <vector> // STL vector for active relay tracking
|
||||
#include "freertos/FreeRTOS.h" // FreeRTOS kernel
|
||||
#include "freertos/task.h" // FreeRTOS task management
|
||||
#include "esp_timer.h" // ESP32 high-precision timers
|
||||
#include "../Logging/Logging.hpp" // Centralized logging system
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// FORWARD DECLARATIONS
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
class ConfigManager; // Configuration management system
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
// ACTIVE OUTPUT TRACKING STRUCTURE
|
||||
// ═════════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* @struct ActiveOutput
|
||||
* @brief Tracks active outputs with microsecond precision timing
|
||||
*
|
||||
* This structure maintains the state of currently active outputs,
|
||||
* including their activation time and configured duration for
|
||||
* automatic timeout management.
|
||||
*/
|
||||
struct ActiveOutput {
|
||||
uint8_t outputIndex; // 🔌 Virtual output index (0-31)
|
||||
uint64_t activationTime; // ⏱️ Activation start time (microseconds)
|
||||
uint16_t durationMs; // ⏳ Duration in milliseconds
|
||||
};
|
||||
|
||||
/**
|
||||
* @class OutputManager
|
||||
* @brief Abstract base class for hardware output management
|
||||
*
|
||||
* Provides a clean abstraction layer for different relay/output systems.
|
||||
* This class defines the interface that all concrete implementations must
|
||||
* follow, ensuring consistent behavior across different hardware types.
|
||||
*
|
||||
* 🏗️ KEY DESIGN PRINCIPLES:
|
||||
* • Hardware-agnostic interface
|
||||
* • Automatic duration management
|
||||
* • Thread-safe operation
|
||||
* • Comprehensive error handling
|
||||
* • Emergency shutdown capability
|
||||
*
|
||||
* 🔌 SUPPORTED OPERATIONS:
|
||||
* • Immediate relay control (fire/extinguish)
|
||||
* • Timed relay activation with automatic shutoff
|
||||
* • Emergency shutdown of all outputs
|
||||
* • Hardware status monitoring
|
||||
*
|
||||
* ⏱️ TIMING FEATURES:
|
||||
* • Microsecond-precision activation timing
|
||||
* • Background task for duration management
|
||||
* • Non-blocking operation
|
||||
* • Configurable per-bell durations
|
||||
*/
|
||||
class OutputManager {
|
||||
public:
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR & DESTRUCTOR
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
OutputManager() = default;
|
||||
|
||||
/**
|
||||
* @brief Virtual destructor for proper cleanup
|
||||
*
|
||||
* Ensures derived classes can properly clean up their resources.
|
||||
*/
|
||||
virtual ~OutputManager();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// PURE VIRTUAL INTERFACE - Must be implemented by derived classes
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Initialize hardware - must be implemented */
|
||||
virtual bool initialize() = 0;
|
||||
|
||||
/** @brief Activate output immediately - hardware agnostic */
|
||||
virtual void fireOutput(uint8_t outputIndex) = 0;
|
||||
|
||||
/** @brief Deactivate output immediately - hardware agnostic */
|
||||
virtual void extinguishOutput(uint8_t outputIndex) = 0;
|
||||
|
||||
/** @brief Emergency shutdown all outputs - must be implemented */
|
||||
virtual void emergencyShutdown() = 0;
|
||||
|
||||
/** @brief Timed output activation - hardware agnostic */
|
||||
virtual void fireOutputForDuration(uint8_t outputIndex, uint16_t durationMs) = 0;
|
||||
|
||||
/** @brief Batch operations for simultaneous firing (CRITICAL for synchronization!) */
|
||||
virtual void fireOutputsBatch(const std::vector<uint8_t>& outputIndices) = 0;
|
||||
virtual void extinguishOutputsBatch(const std::vector<uint8_t>& outputIndices) = 0;
|
||||
virtual void fireOutputsBatchForDuration(const std::vector<std::pair<uint8_t, uint16_t>>& outputDurations) = 0;
|
||||
|
||||
|
||||
/** @brief Check if hardware is initialized - must be implemented */
|
||||
virtual bool isInitialized() const = 0;
|
||||
|
||||
/** @brief Get maximum number of outputs - must be implemented */
|
||||
virtual uint8_t getMaxOutputs() const = 0;
|
||||
|
||||
/** @brief Get hardware type description - must be implemented */
|
||||
virtual const char* getHardwareType() const = 0;
|
||||
|
||||
/** @brief Set configuration manager reference */
|
||||
virtual void setConfigManager(ConfigManager* config) { _configManager = config; }
|
||||
|
||||
/** @brief Get physical output mapping from virtual output (for validation) */
|
||||
uint8_t getPhysicalOutput(uint8_t virtualOutput) const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if OutputManager is in healthy state */
|
||||
virtual bool isHealthy() const;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// CLOCK-SPECIFIC OUTPUT METHODS - For TimeKeeper integration
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Fire clock output directly by virtual output number (for TimeKeeper) */
|
||||
virtual void fireClockOutput(uint8_t virtualOutput, uint16_t durationMs);
|
||||
|
||||
/** @brief Get total virtual outputs available */
|
||||
virtual uint8_t getTotalVirtualOutputs() const { return getMaxOutputs(); }
|
||||
|
||||
protected:
|
||||
ConfigManager* _configManager = nullptr;
|
||||
bool _initialized = false;
|
||||
|
||||
// Active outputs tracking
|
||||
std::vector<ActiveOutput> _activeOutputs;
|
||||
portMUX_TYPE _outputMutex = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
// Duration management task
|
||||
TaskHandle_t _durationTaskHandle = nullptr;
|
||||
|
||||
// Bounds checking
|
||||
bool isValidVirtualOutput(uint8_t virtualOutput) const;
|
||||
bool isValidPhysicalOutput(uint8_t physicalOutput) const;
|
||||
|
||||
|
||||
// Duration management
|
||||
void startDurationTask();
|
||||
void stopDurationTask();
|
||||
void processExpiredOutputs();
|
||||
static void durationTask(void* parameter);
|
||||
|
||||
// Direct physical output access (for clock outputs)
|
||||
virtual void firePhysicalOutput(uint8_t physicalOutput, bool state) = 0;
|
||||
|
||||
// Timing utilities
|
||||
uint64_t getMicros() const { return esp_timer_get_time(); }
|
||||
};
|
||||
|
||||
/**
|
||||
* Chip Type Enumeration
|
||||
* Defines the supported I2C GPIO expander types
|
||||
*/
|
||||
enum class ChipType {
|
||||
PCF8574, // 8 outputs
|
||||
PCF8575 // 16 outputs
|
||||
};
|
||||
|
||||
/**
|
||||
* Chip Configuration Structure
|
||||
* Holds configuration for each I2C GPIO expander chip
|
||||
*/
|
||||
struct ChipConfig {
|
||||
uint8_t i2cAddress; // I2C address (0x20-0x27 for PCF8574, 0x20-0x27 for PCF8575)
|
||||
ChipType chipType; // Type of chip (PCF8574 or PCF8575)
|
||||
uint8_t maxOutputs; // Maximum outputs chip supports (8 for PCF8574, 16 for PCF8575)
|
||||
uint8_t activeOutputs; // Number of active outputs (user-configurable, <= maxOutputs)
|
||||
void* chipInstance; // Pointer to chip instance
|
||||
bool initialized; // Initialization status
|
||||
|
||||
ChipConfig() : i2cAddress(0x20), chipType(ChipType::PCF8574), maxOutputs(8), activeOutputs(8), chipInstance(nullptr), initialized(false) {}
|
||||
|
||||
ChipConfig(uint8_t addr, ChipType type, uint8_t activeOuts = 0) : i2cAddress(addr), chipType(type), chipInstance(nullptr), initialized(false) {
|
||||
maxOutputs = (type == ChipType::PCF8574) ? 8 : 16;
|
||||
activeOutputs = (activeOuts == 0) ? maxOutputs : min(activeOuts, maxOutputs);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* PCF8574/PCF8575 I2C GPIO Expander Implementation
|
||||
* Supports single or dual chip configurations with configurable active outputs:
|
||||
* - Single PCF8574: 1-8 active outputs
|
||||
* - Single PCF8575: 1-16 active outputs
|
||||
* - PCF8574 + PCF8575: (1-8) + (1-16) active outputs
|
||||
* - Dual PCF8575: (1-16) + (1-16) active outputs
|
||||
*
|
||||
* Virtual Output Mapping:
|
||||
* - Virtual outputs 0 to (chip1_active-1) → Chip 1, pins 0 to (chip1_active-1)
|
||||
* - Virtual outputs chip1_active to (total_active-1) → Chip 2, pins 0 to (chip2_active-1)
|
||||
*/
|
||||
class PCF8574OutputManager : public OutputManager {
|
||||
public:
|
||||
// Single chip constructors
|
||||
PCF8574OutputManager(uint8_t i2cAddress = 0x20, ChipType chipType = ChipType::PCF8574, uint8_t activeOutputs = 0);
|
||||
|
||||
// Dual chip constructor
|
||||
PCF8574OutputManager(uint8_t addr1, ChipType chip1, uint8_t active1, uint8_t addr2, ChipType chip2, uint8_t active2);
|
||||
|
||||
virtual ~PCF8574OutputManager();
|
||||
|
||||
// OutputManager interface - NEW GENERIC METHODS
|
||||
bool initialize() override;
|
||||
void fireOutput(uint8_t outputIndex) override;
|
||||
void extinguishOutput(uint8_t outputIndex) override;
|
||||
void emergencyShutdown() override;
|
||||
void fireOutputForDuration(uint8_t outputIndex, uint16_t durationMs) override;
|
||||
|
||||
// Batch operations for simultaneous firing
|
||||
void fireOutputsBatch(const std::vector<uint8_t>& outputIndices) override;
|
||||
void extinguishOutputsBatch(const std::vector<uint8_t>& outputIndices) override;
|
||||
void fireOutputsBatchForDuration(const std::vector<std::pair<uint8_t, uint16_t>>& outputDurations) override;
|
||||
|
||||
bool isInitialized() const override { return _allChipsInitialized; }
|
||||
uint8_t getMaxOutputs() const override { return _totalOutputs; }
|
||||
const char* getHardwareType() const override;
|
||||
|
||||
// Multi-chip specific methods
|
||||
uint8_t getChipCount() const { return _chipCount; }
|
||||
ChipConfig getChipConfig(uint8_t chipIndex) const;
|
||||
bool addChip(uint8_t i2cAddress, ChipType chipType, uint8_t activeOutputs = 0);
|
||||
|
||||
// Virtual output mapping information
|
||||
struct VirtualOutputInfo {
|
||||
uint8_t chipIndex;
|
||||
uint8_t localPin;
|
||||
const char* chipType;
|
||||
};
|
||||
VirtualOutputInfo getVirtualOutputInfo(uint8_t virtualOutput) const;
|
||||
|
||||
// Configuration methods
|
||||
void setChipActiveOutputs(uint8_t chipIndex, uint8_t activeOutputs);
|
||||
uint8_t getChipActiveOutputs(uint8_t chipIndex) const;
|
||||
|
||||
// Legacy single-chip methods (for backward compatibility)
|
||||
void setI2CAddress(uint8_t address) { if (_chipCount > 0) _chips[0].i2cAddress = address; }
|
||||
uint8_t getI2CAddress() const { return (_chipCount > 0) ? _chips[0].i2cAddress : 0x20; }
|
||||
|
||||
bool isHealthy() const override;
|
||||
|
||||
protected:
|
||||
// Direct physical output access
|
||||
void firePhysicalOutput(uint8_t physicalOutput, bool state) override;
|
||||
|
||||
private:
|
||||
static const uint8_t MAX_CHIPS = 2; // Maximum supported chips
|
||||
ChipConfig _chips[MAX_CHIPS]; // Chip configurations
|
||||
uint8_t _chipCount; // Number of configured chips
|
||||
uint8_t _totalOutputs; // Total outputs across all chips
|
||||
bool _allChipsInitialized; // True if all chips are initialized
|
||||
mutable char _hardwareTypeBuffer[64]; // Buffer for hardware type string
|
||||
|
||||
// Chip management
|
||||
bool initializeChip(uint8_t chipIndex);
|
||||
void shutdownChip(uint8_t chipIndex);
|
||||
|
||||
// Output routing
|
||||
struct OutputMapping {
|
||||
uint8_t chipIndex; // Which chip (0 or 1)
|
||||
uint8_t localPin; // Pin on that chip (0-7 for PCF8574, 0-15 for PCF8575)
|
||||
};
|
||||
OutputMapping getOutputMapping(uint8_t physicalOutput) const;
|
||||
|
||||
// Low-level I/O operations
|
||||
void writeOutputToChip(uint8_t chipIndex, uint8_t pin, bool state);
|
||||
bool isValidOutputForChip(uint8_t chipIndex, uint8_t pin) const;
|
||||
|
||||
// Initialization helpers
|
||||
void updateTotalOutputs();
|
||||
void generateHardwareTypeString() const;
|
||||
};
|
||||
|
||||
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
|
||||
// ═══════════════════════════════════════════════════════════════════════════════════
|
||||
227
vesper/src/Telemetry/Telemetry.cpp
Normal file
227
vesper/src/Telemetry/Telemetry.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
#include "Telemetry.hpp"
|
||||
#include "../Communication/Communication.hpp"
|
||||
|
||||
void Telemetry::begin() {
|
||||
// Initialize arrays
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
strikeCounters[i] = 0;
|
||||
bellLoad[i] = 0;
|
||||
bellMaxLoad[i] = 60; // Default max load
|
||||
}
|
||||
|
||||
coolingActive = false;
|
||||
|
||||
// Create the telemetry task
|
||||
xTaskCreatePinnedToCore(telemetryTask, "TelemetryTask", 4096, this, 2, &telemetryTaskHandle, 1);
|
||||
|
||||
LOG_INFO("Telemetry initialized");
|
||||
}
|
||||
|
||||
void Telemetry::setPlayerReference(bool* isPlayingPtr) {
|
||||
playerIsPlayingPtr = isPlayingPtr;
|
||||
LOG_DEBUG("Player reference set");
|
||||
}
|
||||
|
||||
void Telemetry::setForceStopCallback(void (*callback)()) {
|
||||
forceStopCallback = callback;
|
||||
LOG_DEBUG("Force stop callback set");
|
||||
}
|
||||
|
||||
void Telemetry::recordBellStrike(uint8_t bellIndex) {
|
||||
if (bellIndex >= 16) {
|
||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// Critical section - matches your original code
|
||||
portENTER_CRITICAL(&telemetrySpinlock);
|
||||
strikeCounters[bellIndex]++; // Count strikes per bell (warranty)
|
||||
bellLoad[bellIndex]++; // Load per bell (heat simulation)
|
||||
coolingActive = true; // System needs cooling
|
||||
portEXIT_CRITICAL(&telemetrySpinlock);
|
||||
|
||||
}
|
||||
|
||||
uint32_t Telemetry::getStrikeCount(uint8_t bellIndex) {
|
||||
if (bellIndex >= 16) {
|
||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
||||
return 0;
|
||||
}
|
||||
return strikeCounters[bellIndex];
|
||||
}
|
||||
|
||||
void Telemetry::resetStrikeCounters() {
|
||||
portENTER_CRITICAL(&telemetrySpinlock);
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
strikeCounters[i] = 0;
|
||||
}
|
||||
portEXIT_CRITICAL(&telemetrySpinlock);
|
||||
|
||||
LOG_WARNING("Strike counters reset by user");
|
||||
}
|
||||
|
||||
uint16_t Telemetry::getBellLoad(uint8_t bellIndex) {
|
||||
if (bellIndex >= 16) {
|
||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
||||
return 0;
|
||||
}
|
||||
return bellLoad[bellIndex];
|
||||
}
|
||||
|
||||
void Telemetry::setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad) {
|
||||
if (bellIndex >= 16) {
|
||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
bellMaxLoad[bellIndex] = maxLoad;
|
||||
LOG_INFO("Bell %d max load set to %d", bellIndex, maxLoad);
|
||||
}
|
||||
|
||||
bool Telemetry::isOverloaded(uint8_t bellIndex) {
|
||||
if (bellIndex >= 16) {
|
||||
LOG_ERROR("Invalid bell index: %d", bellIndex);
|
||||
return false;
|
||||
}
|
||||
return bellLoad[bellIndex] > bellMaxLoad[bellIndex];
|
||||
}
|
||||
|
||||
bool Telemetry::isCoolingActive() {
|
||||
return coolingActive;
|
||||
}
|
||||
|
||||
void Telemetry::logTemperature(float temperature) {
|
||||
// Future implementation for temperature logging
|
||||
LOG_INFO("Temperature: %.2f°C", temperature);
|
||||
}
|
||||
|
||||
void Telemetry::logVibration(float vibration) {
|
||||
// Future implementation for vibration logging
|
||||
LOG_INFO("Vibration: %.2f", vibration);
|
||||
}
|
||||
|
||||
void Telemetry::checkBellLoads() {
|
||||
coolingActive = false; // Reset cooling flag
|
||||
|
||||
// Collect overloaded bells for batch notification
|
||||
std::vector<uint8_t> criticalBells;
|
||||
std::vector<uint16_t> criticalLoads;
|
||||
std::vector<uint8_t> warningBells;
|
||||
std::vector<uint16_t> warningLoads;
|
||||
|
||||
bool anyOverload = false;
|
||||
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
if (bellLoad[i] > 0) {
|
||||
bellLoad[i]--;
|
||||
coolingActive = true; // Still has heat left
|
||||
}
|
||||
|
||||
// Check for critical overload (90% of max load)
|
||||
uint16_t criticalThreshold = (bellMaxLoad[i] * 90) / 100;
|
||||
// Check for warning overload (60% of max load)
|
||||
uint16_t warningThreshold = (bellMaxLoad[i] * 60) / 100;
|
||||
|
||||
// Critical overload - protection kicks in
|
||||
if (bellLoad[i] > bellMaxLoad[i]) {
|
||||
LOG_ERROR("Bell %d OVERLOADED! load=%d max=%d",
|
||||
i, bellLoad[i], bellMaxLoad[i]);
|
||||
|
||||
criticalBells.push_back(i);
|
||||
criticalLoads.push_back(bellLoad[i]);
|
||||
anyOverload = true;
|
||||
|
||||
} else if (bellLoad[i] > criticalThreshold) {
|
||||
// Critical warning - approaching overload
|
||||
LOG_WARNING("Bell %d approaching overload! load=%d (critical threshold=%d)",
|
||||
i, bellLoad[i], criticalThreshold);
|
||||
|
||||
criticalBells.push_back(i);
|
||||
criticalLoads.push_back(bellLoad[i]);
|
||||
|
||||
} else if (bellLoad[i] > warningThreshold) {
|
||||
// Warning - moderate load
|
||||
LOG_INFO("Bell %d moderate load warning! load=%d (warning threshold=%d)",
|
||||
i, bellLoad[i], warningThreshold);
|
||||
|
||||
warningBells.push_back(i);
|
||||
warningLoads.push_back(bellLoad[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Send batch notifications if any bells are overloaded
|
||||
if (!criticalBells.empty()) {
|
||||
String severity = anyOverload ? "critical" : "warning";
|
||||
if (Communication::_instance) {
|
||||
Communication::_instance->sendBellOverloadNotification(criticalBells, criticalLoads, severity);
|
||||
}
|
||||
} else if (!warningBells.empty()) {
|
||||
if (Communication::_instance) {
|
||||
Communication::_instance->sendBellOverloadNotification(warningBells, warningLoads, "warning");
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger force stop if any bell is actually overloaded
|
||||
if (anyOverload && forceStopCallback != nullptr) {
|
||||
forceStopCallback();
|
||||
}
|
||||
}
|
||||
|
||||
void Telemetry::telemetryTask(void* parameter) {
|
||||
Telemetry* telemetry = static_cast<Telemetry*>(parameter);
|
||||
|
||||
LOG_INFO("Telemetry task started");
|
||||
|
||||
while(1) {
|
||||
// Only run if player is playing OR we're still cooling
|
||||
bool isPlaying = (telemetry->playerIsPlayingPtr != nullptr) ?
|
||||
*(telemetry->playerIsPlayingPtr) : false;
|
||||
|
||||
if (isPlaying || telemetry->coolingActive) {
|
||||
telemetry->checkBellLoads();
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1000)); // Run every 1s
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool Telemetry::isHealthy() const {
|
||||
// Check if telemetry task is created and running
|
||||
if (telemetryTaskHandle == NULL) {
|
||||
LOG_DEBUG("Telemetry: Unhealthy - Task not created");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if task is still alive
|
||||
eTaskState taskState = eTaskGetState(telemetryTaskHandle);
|
||||
if (taskState == eDeleted || taskState == eInvalid) {
|
||||
LOG_DEBUG("Telemetry: Unhealthy - Task deleted or invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if player reference is set
|
||||
if (playerIsPlayingPtr == nullptr) {
|
||||
LOG_DEBUG("Telemetry: Unhealthy - Player reference not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for any critical overloads that would indicate system stress
|
||||
bool hasCriticalOverload = false;
|
||||
for (uint8_t i = 0; i < 16; i++) {
|
||||
if (bellLoad[i] > bellMaxLoad[i]) {
|
||||
hasCriticalOverload = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCriticalOverload) {
|
||||
LOG_DEBUG("Telemetry: Unhealthy - Critical bell overload detected");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
120
vesper/src/Telemetry/Telemetry.hpp
Normal file
120
vesper/src/Telemetry/Telemetry.hpp
Normal file
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* TELEMETRY.HPP - System Monitoring and Analytics Engine
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* 📊 THE SYSTEM WATCHDOG OF VESPER 📊
|
||||
*
|
||||
* This class provides comprehensive system monitoring, performance tracking,
|
||||
* and safety management. It continuously monitors bell usage, load conditions,
|
||||
* and system health to ensure safe and optimal operation.
|
||||
*
|
||||
* 🏗️ MONITORING ARCHITECTURE:
|
||||
* • Real-time bell load tracking and thermal management
|
||||
* • Strike counting for warranty and maintenance tracking
|
||||
* • Performance metrics collection and analysis
|
||||
* • System health monitoring and alerting
|
||||
* • Thread-safe operation with spinlock protection
|
||||
*
|
||||
* 🔔 BELL MONITORING:
|
||||
* • Individual bell strike counting (warranty tracking)
|
||||
* • Load accumulation and thermal modeling
|
||||
* • Overload detection and protection
|
||||
* • Configurable thresholds per bell
|
||||
* • Automatic cooling period management
|
||||
*
|
||||
* 🔥 THERMAL PROTECTION:
|
||||
* • Real-time load calculation based on activation duration
|
||||
* • Thermal decay modeling for cooling
|
||||
* • Overload prevention with automatic cooling
|
||||
* • Emergency stop capability for safety
|
||||
* • System-wide thermal state tracking
|
||||
*
|
||||
* 📊 ANALYTICS & REPORTING:
|
||||
* • Performance metrics collection
|
||||
* • Usage pattern analysis
|
||||
* • Health status reporting
|
||||
* • Predictive maintenance indicators
|
||||
* • Historical data tracking
|
||||
*
|
||||
* ⚙️ SAFETY FEATURES:
|
||||
* • Automatic system protection from overload
|
||||
* • Force stop callback for emergency situations
|
||||
* • Comprehensive bounds checking
|
||||
* • Fail-safe operation modes
|
||||
* • Graceful degradation under stress
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Enhanced monitoring and safety)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#ifndef TELEMETRY_HPP
|
||||
#define TELEMETRY_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
class Telemetry {
|
||||
private:
|
||||
// Bell tracking
|
||||
uint32_t strikeCounters[16] = {0}; // Total strikes per bell (warranty tracking)
|
||||
uint16_t bellLoad[16] = {0}; // Current heat load per bell
|
||||
uint16_t bellMaxLoad[16] = {60}; // Max load threshold per bell
|
||||
bool coolingActive = false; // System-wide cooling flag
|
||||
|
||||
// Task handle
|
||||
TaskHandle_t telemetryTaskHandle = NULL;
|
||||
|
||||
// External references (to be set via setters)
|
||||
bool* playerIsPlayingPtr = nullptr;
|
||||
|
||||
// Spinlock for critical sections
|
||||
portMUX_TYPE telemetrySpinlock = portMUX_INITIALIZER_UNLOCKED;
|
||||
|
||||
public:
|
||||
// Initialization
|
||||
void begin();
|
||||
|
||||
// Set external references
|
||||
void setPlayerReference(bool* isPlayingPtr);
|
||||
|
||||
// Bell strike handling (call this on every hammer strike)
|
||||
void recordBellStrike(uint8_t bellIndex);
|
||||
|
||||
// Strike counter management (warranty tracking)
|
||||
uint32_t getStrikeCount(uint8_t bellIndex);
|
||||
void resetStrikeCounters(); // User-requested reset
|
||||
|
||||
// Bell load management
|
||||
uint16_t getBellLoad(uint8_t bellIndex);
|
||||
void setBellMaxLoad(uint8_t bellIndex, uint16_t maxLoad);
|
||||
bool isOverloaded(uint8_t bellIndex);
|
||||
bool isCoolingActive(); // Check if system needs cooling
|
||||
|
||||
// Data collection (future expansion)
|
||||
void logTemperature(float temperature);
|
||||
void logVibration(float vibration);
|
||||
|
||||
// Force stop callback (to be set by main application)
|
||||
void setForceStopCallback(void (*callback)());
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** @brief Check if Telemetry is in healthy state */
|
||||
bool isHealthy() const;
|
||||
|
||||
// Static task function
|
||||
static void telemetryTask(void* parameter);
|
||||
|
||||
private:
|
||||
void (*forceStopCallback)() = nullptr;
|
||||
void checkBellLoads();
|
||||
};
|
||||
|
||||
#endif
|
||||
772
vesper/src/TimeKeeper/TimeKeeper.cpp
Normal file
772
vesper/src/TimeKeeper/TimeKeeper.cpp
Normal file
@@ -0,0 +1,772 @@
|
||||
#include "TimeKeeper.hpp"
|
||||
#include "../OutputManager/OutputManager.hpp"
|
||||
#include "../ConfigManager/ConfigManager.hpp"
|
||||
#include "../Networking/Networking.hpp"
|
||||
#include "SD.h"
|
||||
#include <time.h>
|
||||
|
||||
void Timekeeper::begin() {
|
||||
LOG_INFO("Timekeeper initialized - clock outputs managed by ConfigManager");
|
||||
|
||||
// Initialize RTC
|
||||
if (!rtc.begin()) {
|
||||
LOG_ERROR("Couldn't find RTC");
|
||||
// Continue anyway, but log the error
|
||||
} else {
|
||||
LOG_INFO("RTC initialized successfully");
|
||||
|
||||
// Check if RTC lost power
|
||||
if (!rtc.isrunning()) {
|
||||
LOG_WARNING("RTC is NOT running! Setting time...");
|
||||
// Set to compile time as fallback
|
||||
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
|
||||
}
|
||||
}
|
||||
|
||||
// Load today's events
|
||||
loadTodaysEvents();
|
||||
|
||||
// Create SINGLE consolidated task (saves 8KB RAM!)
|
||||
xTaskCreatePinnedToCore(mainTimekeeperTask, "TimeKeeper", 4096, this, 2, &mainTaskHandle, 1);
|
||||
|
||||
LOG_INFO("TimeKeeper initialized with SIMPLE sync approach (like your Arduino code)");
|
||||
}
|
||||
|
||||
void Timekeeper::setOutputManager(OutputManager* outputManager) {
|
||||
_outputManager = outputManager;
|
||||
LOG_INFO("Timekeeper connected to OutputManager - CLEAN ARCHITECTURE!");
|
||||
}
|
||||
|
||||
void Timekeeper::setConfigManager(ConfigManager* configManager) {
|
||||
_configManager = configManager;
|
||||
LOG_INFO("Timekeeper connected to ConfigManager");
|
||||
}
|
||||
|
||||
void Timekeeper::setNetworking(Networking* networking) {
|
||||
_networking = networking;
|
||||
LOG_INFO("Timekeeper connected to Networking");
|
||||
}
|
||||
|
||||
void Timekeeper::setRelayWriteFunction(void (*func)(int, int)) {
|
||||
relayWriteFunc = func;
|
||||
LOG_WARNING("Using LEGACY relay function - consider upgrading to OutputManager");
|
||||
}
|
||||
|
||||
void Timekeeper::setClockOutputs(int relay1, int relay2) {
|
||||
LOG_WARNING("⚠️ setClockOutputs() is DEPRECATED! Use ConfigManager.setClockOutput1/2() instead");
|
||||
LOG_WARNING("⚠️ Clock outputs should be configured via MQTT/WebSocket commands");
|
||||
|
||||
// For backward compatibility, still set the config if ConfigManager is available
|
||||
if (_configManager) {
|
||||
_configManager->setClockOutput1(relay1);
|
||||
_configManager->setClockOutput2(relay2);
|
||||
LOG_INFO("Clock outputs updated via legacy method: C1=%d, C2=%d", relay1, relay2);
|
||||
} else {
|
||||
LOG_ERROR("ConfigManager not available - cannot set clock outputs");
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::setTime(unsigned long timestamp) {
|
||||
if (!rtc.begin()) {
|
||||
LOG_ERROR("RTC not available - cannot set time");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get timezone configuration
|
||||
auto& timeConfig = _configManager->getTimeConfig();
|
||||
|
||||
// Apply timezone offset to UTC timestamp
|
||||
long totalOffset = timeConfig.gmtOffsetSec + timeConfig.daylightOffsetSec;
|
||||
unsigned long localTimestamp = timestamp + totalOffset;
|
||||
|
||||
// Convert local timestamp to DateTime object
|
||||
DateTime newTime(localTimestamp);
|
||||
|
||||
// Set the RTC with local time
|
||||
rtc.adjust(newTime);
|
||||
|
||||
LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (UTC timestamp: %lu + %ld offset = %lu)",
|
||||
newTime.year(), newTime.month(), newTime.day(),
|
||||
newTime.hour(), newTime.minute(), newTime.second(),
|
||||
timestamp, totalOffset, localTimestamp);
|
||||
|
||||
// Reload today's events since the date might have changed
|
||||
loadTodaysEvents();
|
||||
}
|
||||
|
||||
void Timekeeper::setTimeWithLocalTimestamp(unsigned long localTimestamp) {
|
||||
if (!rtc.begin()) {
|
||||
LOG_ERROR("RTC not available - cannot set time");
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert local timestamp directly to DateTime object (no timezone conversion needed)
|
||||
DateTime newTime(localTimestamp);
|
||||
|
||||
// Set the RTC with local time
|
||||
rtc.adjust(newTime);
|
||||
|
||||
LOG_INFO("RTC time set to LOCAL: %04d-%02d-%02d %02d:%02d:%02d (local timestamp: %lu)",
|
||||
newTime.year(), newTime.month(), newTime.day(),
|
||||
newTime.hour(), newTime.minute(), newTime.second(),
|
||||
localTimestamp);
|
||||
|
||||
// Reload today's events since the date might have changed
|
||||
loadTodaysEvents();
|
||||
}
|
||||
|
||||
unsigned long Timekeeper::getTime() {
|
||||
if (!rtc.isrunning()) {
|
||||
LOG_ERROR("RTC not running - cannot get time");
|
||||
return 0;
|
||||
}
|
||||
|
||||
DateTime now = rtc.now();
|
||||
unsigned long timestamp = now.unixtime();
|
||||
|
||||
LOG_DEBUG("Current RTC time: %04d-%02d-%02d %02d:%02d:%02d (timestamp: %lu)",
|
||||
now.year(), now.month(), now.day(),
|
||||
now.hour(), now.minute(), now.second(),
|
||||
timestamp);
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
void Timekeeper::syncTimeWithNTP() {
|
||||
// Check if we have network connection and required dependencies
|
||||
if (!_networking || !_configManager) {
|
||||
LOG_ERROR("Cannot sync time: Networking or ConfigManager not set");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_networking->isConnected()) {
|
||||
LOG_WARNING("Cannot sync time: No network connection");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Syncing time with NTP server...");
|
||||
|
||||
// Get config from ConfigManager
|
||||
auto& timeConfig = _configManager->getTimeConfig();
|
||||
|
||||
// Configure NTP with settings from config
|
||||
configTime(timeConfig.gmtOffsetSec, timeConfig.daylightOffsetSec, timeConfig.ntpServer.c_str());
|
||||
|
||||
// Wait for time sync with timeout
|
||||
struct tm timeInfo;
|
||||
int attempts = 0;
|
||||
while (!getLocalTime(&timeInfo) && attempts < 10) {
|
||||
LOG_DEBUG("Waiting for NTP sync... attempt %d", attempts + 1);
|
||||
delay(1000);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (attempts >= 10) {
|
||||
LOG_ERROR("Failed to obtain time from NTP server after 10 attempts");
|
||||
return;
|
||||
}
|
||||
|
||||
// Update RTC with synchronized time
|
||||
rtc.adjust(DateTime(timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
|
||||
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec));
|
||||
|
||||
LOG_INFO("Time synced successfully: %04d-%02d-%02d %02d:%02d:%02d",
|
||||
timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday,
|
||||
timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);
|
||||
|
||||
// Reload today's events since the time might have changed significantly
|
||||
loadTodaysEvents();
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// CONSOLIDATED TimeKeeper Task - SIMPLE approach like your Arduino code
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Timekeeper::mainTimekeeperTask(void* parameter) {
|
||||
Timekeeper* keeper = static_cast<Timekeeper*>(parameter);
|
||||
LOG_INFO("🕒 SIMPLE TimeKeeper task started - based on your Arduino code approach");
|
||||
|
||||
unsigned long lastRtcCheck = 0;
|
||||
unsigned long lastScheduleCheck = 0;
|
||||
|
||||
while (true) {
|
||||
unsigned long now = millis();
|
||||
|
||||
// 🕐 SIMPLE PHYSICAL CLOCK SYNC (check every loop, fire if needed - your Arduino approach)
|
||||
keeper->checkAndSyncPhysicalClock();
|
||||
|
||||
// 🔔 ALERT MANAGEMENT (every second for precise timing)
|
||||
keeper->checkClockAlerts();
|
||||
|
||||
// 💡 BACKLIGHT AUTOMATION (every 10 seconds)
|
||||
if (now - lastRtcCheck >= 10000) {
|
||||
keeper->checkBacklightAutomation();
|
||||
}
|
||||
|
||||
// 📅 SCHEDULE CHECK (every second)
|
||||
if (now - lastScheduleCheck >= 1000) {
|
||||
keeper->checkScheduledEvents();
|
||||
lastScheduleCheck = now;
|
||||
}
|
||||
|
||||
// 🔧 RTC MAINTENANCE (every 10 seconds)
|
||||
if (now - lastRtcCheck >= 10000) {
|
||||
// RTC health check
|
||||
DateTime rtcNow = keeper->rtc.now();
|
||||
if (keeper->rtc.isrunning()) {
|
||||
// Check for midnight - reload events for new day
|
||||
if (rtcNow.hour() == 0 && rtcNow.minute() == 0 && rtcNow.second() < 10) {
|
||||
LOG_INFO("🌙 Midnight detected - reloading events");
|
||||
keeper->loadTodaysEvents();
|
||||
keeper->loadNextDayEvents();
|
||||
}
|
||||
|
||||
// Hourly maintenance
|
||||
if (rtcNow.minute() == 0 && rtcNow.second() < 10) {
|
||||
LOG_DEBUG("🕐 Hourly check at %02d:00", rtcNow.hour());
|
||||
}
|
||||
} else {
|
||||
static uint8_t rtcWarningCounter = 0;
|
||||
if (rtcWarningCounter++ % 6 == 0) { // Log every minute
|
||||
LOG_WARNING("⚠️ RTC not running!");
|
||||
}
|
||||
}
|
||||
lastRtcCheck = now;
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// SIMPLE CLOCK SYNC IMPLEMENTATION - Based on your Arduino code
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Timekeeper::checkAndSyncPhysicalClock() {
|
||||
// Check if clock is enabled in config
|
||||
if (!_configManager || !_configManager->getClockEnabled()) {
|
||||
return; // Clock is disabled - skip all clock functionality
|
||||
}
|
||||
|
||||
// Check if there is any Time Difference between the Physical Clock and the Actual Time and if yes, run the Motor
|
||||
|
||||
if (!_outputManager || !rtc.isrunning() || clockUpdatesPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current real time (your updateCurrentTime() equivalent)
|
||||
DateTime now = rtc.now();
|
||||
int8_t realHour = now.hour() % 12; // Convert to 12-hour format for clock face
|
||||
if (realHour == 0) realHour = 12; // 12 AM/PM shows as 12, not 0
|
||||
int8_t realMinute = now.minute();
|
||||
|
||||
// Get physical clock state (your clock.hour/clock.minute equivalent)
|
||||
int8_t physicalHour = _configManager->getPhysicalClockHour();
|
||||
int8_t physicalMinute = _configManager->getPhysicalClockMinute();
|
||||
|
||||
// Calculate time difference (your exact logic!)
|
||||
int16_t timeDifference = (realHour * 60 + realMinute) - (physicalHour * 60 + physicalMinute);
|
||||
|
||||
// Handle 12-hour rollover (if negative, add 12 hours)
|
||||
if (timeDifference < 0) {
|
||||
timeDifference += 12 * 60; // Add 12 hours to handle rollover
|
||||
}
|
||||
|
||||
// If there's a difference, advance the clock by one minute (your runMotor equivalent)
|
||||
if (timeDifference >= 1) {
|
||||
advancePhysicalClockOneMinute();
|
||||
LOG_DEBUG("⏰ SYNC: Advanced physical clock by 1 minute to %02d:%02d (real: %02d:%02d, diff: %lu mins)",
|
||||
_configManager->getPhysicalClockHour(), _configManager->getPhysicalClockMinute(),
|
||||
realHour, realMinute, timeDifference);
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::advancePhysicalClockOneMinute() {
|
||||
|
||||
const auto& clockConfig = _configManager->getClockConfig();
|
||||
if (clockConfig.c1output == 255 || clockConfig.c2output == 255) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool useC1 = _configManager->getNextOutputIsC1();
|
||||
uint8_t outputToFire = useC1 ? (clockConfig.c1output - 1) : (clockConfig.c2output - 1);
|
||||
|
||||
LOG_DEBUG("🔥 ADVANCE: Firing %s (output %d) for %dms",
|
||||
useC1 ? "C1" : "C2", outputToFire + 1, clockConfig.pulseDuration);
|
||||
|
||||
_outputManager->fireOutputForDuration(outputToFire, clockConfig.pulseDuration);
|
||||
vTaskDelay(pdMS_TO_TICKS(clockConfig.pulseDuration + clockConfig.pauseDuration)); // cool-off motor
|
||||
_configManager->setNextOutputIsC1(!useC1);
|
||||
updatePhysicalClockTime();
|
||||
}
|
||||
|
||||
void Timekeeper::updatePhysicalClockTime() {
|
||||
|
||||
uint8_t currentHour = _configManager->getPhysicalClockHour();
|
||||
uint8_t currentMinute = _configManager->getPhysicalClockMinute();
|
||||
|
||||
currentMinute++;
|
||||
if (currentMinute >= 60) {
|
||||
currentMinute = 0;
|
||||
currentHour++;
|
||||
if (currentHour > 12) { // 12-hour clock (your code used 24, but clock face is 12)
|
||||
currentHour = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_configManager->setPhysicalClockHour(currentHour);
|
||||
_configManager->setPhysicalClockMinute(currentMinute);
|
||||
_configManager->setLastSyncTime(millis() / 1000);
|
||||
_configManager->saveClockState();
|
||||
|
||||
LOG_DEBUG("📅 STATE: Physical clock advanced to %d:%02d", currentHour, currentMinute);
|
||||
}
|
||||
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// EVENT MANAGEMENT
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Timekeeper::loadTodaysEvents() {
|
||||
// Clear existing events
|
||||
todaysEvents.clear();
|
||||
|
||||
// Get current date/time from RTC
|
||||
DateTime now = rtc.now();
|
||||
if (!rtc.isrunning()) {
|
||||
LOG_ERROR("RTC not running - cannot load events");
|
||||
return;
|
||||
}
|
||||
|
||||
int currentYear = now.year();
|
||||
int currentMonth = now.month();
|
||||
int currentDay = now.day();
|
||||
int currentDayOfWeek = now.dayOfTheWeek(); // 0=Sunday, 1=Monday, etc.
|
||||
|
||||
LOG_INFO("Loading events for: %04d-%02d-%02d (day %d)",
|
||||
currentYear, currentMonth, currentDay, currentDayOfWeek);
|
||||
|
||||
// Open and parse events file
|
||||
File file = SD.open("/events/events.json");
|
||||
if (!file) {
|
||||
LOG_ERROR("Failed to open events.json");
|
||||
return;
|
||||
}
|
||||
|
||||
// Use static allocation instead of dynamic to avoid heap fragmentation
|
||||
static StaticJsonDocument<8192> doc;
|
||||
doc.clear();
|
||||
|
||||
DeserializationError error = deserializeJson(doc, file);
|
||||
file.close();
|
||||
|
||||
if (error) {
|
||||
LOG_ERROR("JSON parsing failed: %s", error.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
JsonArray events = doc["events"];
|
||||
int eventsLoaded = 0;
|
||||
|
||||
for (JsonObject event : events) {
|
||||
if (!event["enabled"].as<bool>()) {
|
||||
continue; // Skip disabled events
|
||||
}
|
||||
|
||||
String type = event["type"].as<String>();
|
||||
bool shouldAdd = false;
|
||||
|
||||
if (type == "single") {
|
||||
// Check if event date matches today
|
||||
String eventDateTime = event["datetime"].as<String>();
|
||||
if (isSameDate(eventDateTime, currentYear, currentMonth, currentDay)) {
|
||||
shouldAdd = true;
|
||||
}
|
||||
}
|
||||
else if (type == "weekly") {
|
||||
// Check if today's day of week is in the event's days
|
||||
JsonArray daysOfWeek = event["days_of_week"];
|
||||
for (JsonVariant dayVar : daysOfWeek) {
|
||||
int day = dayVar.as<int>();
|
||||
if (day == currentDayOfWeek) {
|
||||
shouldAdd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == "monthly") {
|
||||
// Check if today's date is in the event's days of month
|
||||
JsonArray daysOfMonth = event["days_of_month"];
|
||||
for (JsonVariant dayVar : daysOfMonth) {
|
||||
int day = dayVar.as<int>();
|
||||
if (day == currentDay) {
|
||||
shouldAdd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAdd) {
|
||||
addToTodaysSchedule(event);
|
||||
eventsLoaded++;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort events by time
|
||||
sortEventsByTime();
|
||||
|
||||
LOG_INFO("Loaded %d events for today", eventsLoaded);
|
||||
}
|
||||
|
||||
bool Timekeeper::isSameDate(String eventDateTime, int year, int month, int day) {
|
||||
// Parse "2025-12-25T09:00:00" format
|
||||
if (eventDateTime.length() < 10) return false;
|
||||
|
||||
int eventYear = eventDateTime.substring(0, 4).toInt();
|
||||
int eventMonth = eventDateTime.substring(5, 7).toInt();
|
||||
int eventDay = eventDateTime.substring(8, 10).toInt();
|
||||
|
||||
return (eventYear == year && eventMonth == month && eventDay == day);
|
||||
}
|
||||
|
||||
void Timekeeper::addToTodaysSchedule(JsonObject event) {
|
||||
ScheduledEvent schedEvent;
|
||||
|
||||
// Extract time based on event type
|
||||
if (event["type"] == "single") {
|
||||
// Extract time from datetime: "2025-12-25T09:00:00" -> "09:00:00"
|
||||
String datetime = event["datetime"].as<String>();
|
||||
if (datetime.length() >= 19) {
|
||||
schedEvent.timeStr = datetime.substring(11, 19);
|
||||
}
|
||||
} else {
|
||||
// Weekly/Monthly events have separate time field
|
||||
schedEvent.timeStr = event["time"].as<String>();
|
||||
}
|
||||
|
||||
// Store the entire event data (create a copy)
|
||||
schedEvent.eventData = event;
|
||||
|
||||
todaysEvents.push_back(schedEvent);
|
||||
|
||||
LOG_DEBUG("Added event '%s' at %s",
|
||||
event["name"].as<String>().c_str(),
|
||||
schedEvent.timeStr.c_str());
|
||||
}
|
||||
|
||||
void Timekeeper::sortEventsByTime() {
|
||||
std::sort(todaysEvents.begin(), todaysEvents.end(),
|
||||
[](const ScheduledEvent& a, const ScheduledEvent& b) {
|
||||
return a.timeStr < b.timeStr;
|
||||
});
|
||||
}
|
||||
|
||||
String Timekeeper::getCurrentTimeString() {
|
||||
DateTime now = rtc.now();
|
||||
if (!rtc.isrunning()) {
|
||||
return "00:00:00";
|
||||
}
|
||||
|
||||
char timeStr[9];
|
||||
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
|
||||
return String(timeStr);
|
||||
}
|
||||
|
||||
void Timekeeper::checkScheduledEvents() {
|
||||
String currentTime = getCurrentTimeString();
|
||||
|
||||
// Only check the seconds part for exact matching
|
||||
String currentTimeMinute = currentTime.substring(0, 5); // "HH:MM"
|
||||
|
||||
for (auto& event : todaysEvents) {
|
||||
String eventTimeMinute = event.timeStr.substring(0, 5); // "HH:MM"
|
||||
|
||||
if (eventTimeMinute == currentTimeMinute) {
|
||||
// Check if we haven't already triggered this event
|
||||
if (!event.triggered) {
|
||||
triggerEvent(event);
|
||||
event.triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Reset trigger flag when we're past the minute
|
||||
if (eventTimeMinute < currentTimeMinute) {
|
||||
event.triggered = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::triggerEvent(ScheduledEvent& event) {
|
||||
JsonObject eventData = event.eventData;
|
||||
|
||||
LOG_INFO("TRIGGERING EVENT: %s at %s",
|
||||
eventData["name"].as<String>().c_str(),
|
||||
event.timeStr.c_str());
|
||||
|
||||
// Here you would trigger your melody playback
|
||||
// You might want to call a function from your main program
|
||||
// or send a message to another task
|
||||
|
||||
// Example of what you might do:
|
||||
JsonObject melody = eventData["melody"];
|
||||
String melodyUID = melody["uid"].as<String>();
|
||||
String melodyName = melody["name"].as<String>();
|
||||
|
||||
LOG_INFO("Playing melody: %s (UID: %s)",
|
||||
melodyName.c_str(), melodyUID.c_str());
|
||||
|
||||
// TODO: Add your melody trigger code here
|
||||
// playMelody(melody);
|
||||
}
|
||||
|
||||
void Timekeeper::loadNextDayEvents() {
|
||||
// This function would load tomorrow's events for smooth midnight transition
|
||||
// Implementation similar to loadTodaysEvents() but for tomorrow's date
|
||||
LOG_DEBUG("Pre-loading tomorrow's events...");
|
||||
// TODO: Implement if needed for smoother transitions
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// CLOCK ALERTS IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
void Timekeeper::checkClockAlerts() {
|
||||
// Check if clock is enabled in config
|
||||
if (!_configManager || !_configManager->getClockEnabled()) {
|
||||
return; // Clock is disabled - skip all alert functionality
|
||||
}
|
||||
|
||||
// Check if we have required dependencies
|
||||
if (!_outputManager || !rtc.isrunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current time
|
||||
DateTime now = rtc.now();
|
||||
int currentHour = now.hour();
|
||||
int currentMinute = now.minute();
|
||||
int currentSecond = now.second();
|
||||
|
||||
// Only trigger alerts on exact seconds (0-2) to avoid multiple triggers
|
||||
if (currentSecond > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get clock configuration
|
||||
const auto& clockConfig = _configManager->getClockConfig();
|
||||
|
||||
// Check if alerts are disabled
|
||||
if (clockConfig.alertType == "OFF") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in a silence period
|
||||
if (isInSilencePeriod()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 🕐 HOURLY ALERTS (at xx:00)
|
||||
if (currentMinute == 0 && currentHour != lastHour) {
|
||||
triggerHourlyAlert(currentHour);
|
||||
lastHour = currentHour;
|
||||
}
|
||||
|
||||
// 🕕 HALF-HOUR ALERTS (at xx:30)
|
||||
if (currentMinute == 30 && lastMinute != 30) {
|
||||
if (clockConfig.halfBell != 255) { // 255 = disabled
|
||||
LOG_INFO("🕕 Half-hour alert at %02d:30", currentHour);
|
||||
fireAlertBell(clockConfig.halfBell, 1);
|
||||
}
|
||||
lastMinute = 30;
|
||||
}
|
||||
|
||||
// 🕒 QUARTER-HOUR ALERTS (at xx:15 and xx:45)
|
||||
if ((currentMinute == 15 || currentMinute == 45) && lastMinute != currentMinute) {
|
||||
if (clockConfig.quarterBell != 255) { // 255 = disabled
|
||||
LOG_INFO("🕒 Quarter-hour alert at %02d:%02d", currentHour, currentMinute);
|
||||
fireAlertBell(clockConfig.quarterBell, 1);
|
||||
}
|
||||
lastMinute = currentMinute;
|
||||
}
|
||||
|
||||
// Reset minute tracking for other minutes
|
||||
if (currentMinute != 0 && currentMinute != 15 && currentMinute != 30 && currentMinute != 45) {
|
||||
if (lastMinute != currentMinute) {
|
||||
lastMinute = currentMinute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::triggerHourlyAlert(int hour) {
|
||||
const auto& clockConfig = _configManager->getClockConfig();
|
||||
|
||||
// Check if hourly bell is configured
|
||||
if (clockConfig.hourBell == 255) { // 255 = disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if (clockConfig.alertType == "SINGLE") {
|
||||
// Single ding for any hour
|
||||
LOG_INFO("🕐 Hourly alert (SINGLE) at %02d:00", hour);
|
||||
fireAlertBell(clockConfig.hourBell, 1);
|
||||
}
|
||||
else if (clockConfig.alertType == "HOURS") {
|
||||
// Ring the number of times equal to the hour (1-12)
|
||||
int bellCount = hour;
|
||||
if (bellCount == 0) bellCount = 12; // Midnight = 12 bells
|
||||
if (bellCount > 12) bellCount = bellCount - 12; // 24h to 12h conversion
|
||||
|
||||
LOG_INFO("🕐 Hourly alert (HOURS) at %02d:00 - %d rings", hour, bellCount);
|
||||
fireAlertBell(clockConfig.hourBell, bellCount);
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::fireAlertBell(uint8_t bellNumber, int count) {
|
||||
if (!_outputManager || bellNumber == 255) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& clockConfig = _configManager->getClockConfig();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
// Get bell duration from bell configuration
|
||||
uint16_t bellDuration = _configManager->getBellDuration(bellNumber);
|
||||
|
||||
LOG_DEBUG("🔔 Alert bell #%d ring %d/%d (duration: %dms)",
|
||||
bellNumber + 1, i + 1, count, bellDuration);
|
||||
|
||||
// Fire the bell using OutputManager
|
||||
_outputManager->fireOutputForDuration(bellNumber, bellDuration);
|
||||
|
||||
// Wait between rings (only if there's more than one ring)
|
||||
if (i < count - 1) {
|
||||
vTaskDelay(pdMS_TO_TICKS(clockConfig.alertRingInterval));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Timekeeper::checkBacklightAutomation() {
|
||||
// Check if clock is enabled in config
|
||||
if (!_configManager || !_configManager->getClockEnabled()) {
|
||||
return; // Clock is disabled - skip all backlight functionality
|
||||
}
|
||||
|
||||
// Check if we have required dependencies
|
||||
if (!_outputManager || !rtc.isrunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& clockConfig = _configManager->getClockConfig();
|
||||
|
||||
// Check if backlight automation is enabled
|
||||
if (!clockConfig.backlight || clockConfig.backlightOutput == 255) {
|
||||
return; // Backlight automation disabled
|
||||
}
|
||||
|
||||
// Get current time
|
||||
DateTime now = rtc.now();
|
||||
char currentTimeStr[6];
|
||||
sprintf(currentTimeStr, "%02d:%02d", now.hour(), now.minute());
|
||||
String currentTime = String(currentTimeStr);
|
||||
|
||||
// Check if it's time to turn backlight ON
|
||||
if (currentTime == clockConfig.backlightOnTime && !backlightState) {
|
||||
LOG_INFO("💡 Turning backlight ON at %s (output #%d)",
|
||||
currentTime.c_str(), clockConfig.backlightOutput + 1);
|
||||
_outputManager->fireOutput(clockConfig.backlightOutput);
|
||||
backlightState = true;
|
||||
}
|
||||
// Check if it's time to turn backlight OFF
|
||||
else if (currentTime == clockConfig.backlightOffTime && backlightState) {
|
||||
LOG_INFO("💡 Turning backlight OFF at %s (output #%d)",
|
||||
currentTime.c_str(), clockConfig.backlightOutput + 1);
|
||||
_outputManager->extinguishOutput(clockConfig.backlightOutput);
|
||||
backlightState = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Timekeeper::isInSilencePeriod() {
|
||||
if (!_configManager || !rtc.isrunning()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& clockConfig = _configManager->getClockConfig();
|
||||
|
||||
// Get current time
|
||||
DateTime now = rtc.now();
|
||||
char currentTimeStr[6];
|
||||
sprintf(currentTimeStr, "%02d:%02d", now.hour(), now.minute());
|
||||
String currentTime = String(currentTimeStr);
|
||||
|
||||
// Check daytime silence period
|
||||
if (clockConfig.daytimeSilenceEnabled) {
|
||||
if (isTimeInRange(currentTime, clockConfig.daytimeSilenceOnTime, clockConfig.daytimeSilenceOffTime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check nighttime silence period
|
||||
if (clockConfig.nighttimeSilenceEnabled) {
|
||||
if (isTimeInRange(currentTime, clockConfig.nighttimeSilenceOnTime, clockConfig.nighttimeSilenceOffTime)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Timekeeper::isTimeInRange(const String& currentTime, const String& startTime, const String& endTime) const {
|
||||
// Handle the case where the time range crosses midnight (e.g., 22:00 to 07:00)
|
||||
if (startTime > endTime) {
|
||||
// Range crosses midnight - current time is in range if it's after start OR before end
|
||||
return (currentTime >= startTime || currentTime <= endTime);
|
||||
} else {
|
||||
// Normal range - current time is in range if it's between start and end
|
||||
return (currentTime >= startTime && currentTime <= endTime);
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK IMPLEMENTATION
|
||||
// ════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
bool Timekeeper::isHealthy() {
|
||||
// Check if RTC is running
|
||||
if (!rtc.isrunning()) {
|
||||
LOG_DEBUG("TimeKeeper: Unhealthy - RTC not running");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if main task is created and running
|
||||
if (mainTaskHandle == NULL) {
|
||||
LOG_DEBUG("TimeKeeper: Unhealthy - Main task not created");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if task is still alive
|
||||
eTaskState taskState = eTaskGetState(mainTaskHandle);
|
||||
if (taskState == eDeleted || taskState == eInvalid) {
|
||||
LOG_DEBUG("TimeKeeper: Unhealthy - Main task deleted or invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if required dependencies are set
|
||||
if (!_configManager) {
|
||||
LOG_DEBUG("TimeKeeper: Unhealthy - ConfigManager not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_outputManager) {
|
||||
LOG_DEBUG("TimeKeeper: Unhealthy - OutputManager not set");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if time is reasonable (not stuck at epoch or way in the future)
|
||||
DateTime now = rtc.now();
|
||||
if (now.year() < 2020 || now.year() > 2100) {
|
||||
LOG_DEBUG("TimeKeeper: Unhealthy - RTC time unreasonable: %d", now.year());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
152
vesper/src/TimeKeeper/TimeKeeper.hpp
Normal file
152
vesper/src/TimeKeeper/TimeKeeper.hpp
Normal file
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
* TIMEKEEPER.HPP - NTP Synchronization and Clock Management
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*
|
||||
* ⏰ THE TIME MASTER OF VESPER ⏰
|
||||
*
|
||||
* This class manages all time-related functionality including NTP synchronization,
|
||||
* timezone handling, and hardware clock signal generation. It ensures accurate
|
||||
* timekeeping across the entire system.
|
||||
*
|
||||
* 🏗️ TIME MANAGEMENT:
|
||||
* • NTP synchronization with automatic retry
|
||||
* • Timezone and daylight saving time support
|
||||
* • Hardware clock signal generation
|
||||
* • Network-dependent time sync
|
||||
* • Clean dependency injection architecture
|
||||
*
|
||||
* 📋 VERSION: 2.0 (Modular time management)
|
||||
* 📅 DATE: 2025
|
||||
* 👨💻 AUTHOR: Advanced Bell Systems
|
||||
* ═══════════════════════════════════════════════════════════════════════════════════
|
||||
*/
|
||||
|
||||
#ifndef TIMEKEEPER_HPP
|
||||
#define TIMEKEEPER_HPP
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <ArduinoJson.h>
|
||||
#include <RTClib.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "../Logging/Logging.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class OutputManager;
|
||||
class ConfigManager;
|
||||
class Networking;
|
||||
|
||||
// Structure to hold scheduled events
|
||||
struct ScheduledEvent {
|
||||
String timeStr; // Time in "HH:MM:SS" format
|
||||
JsonObject eventData; // Complete event data from JSON
|
||||
bool triggered = false; // Flag to prevent multiple triggers
|
||||
};
|
||||
|
||||
class Timekeeper {
|
||||
private:
|
||||
// RTC object
|
||||
RTC_DS1307 rtc;
|
||||
|
||||
// Event storage
|
||||
std::vector<ScheduledEvent> todaysEvents;
|
||||
std::vector<ScheduledEvent> tomorrowsEvents;
|
||||
|
||||
// Clocktower Updating Pause
|
||||
bool clockUpdatesPaused = false;
|
||||
|
||||
// Alert management - new functionality
|
||||
int lastHour = -1; // Track last processed hour to avoid duplicate alerts
|
||||
int lastMinute = -1; // Track last processed minute for quarter/half alerts
|
||||
|
||||
// Backlight management - new functionality
|
||||
bool backlightState = false; // Track current backlight state
|
||||
|
||||
// Clean dependencies
|
||||
OutputManager* _outputManager = nullptr;
|
||||
ConfigManager* _configManager = nullptr;
|
||||
Networking* _networking = nullptr;
|
||||
|
||||
// Legacy function pointer (DEPRECATED - will be removed)
|
||||
void (*relayWriteFunc)(int relay, int state) = nullptr;
|
||||
|
||||
// Task handles - CONSOLIDATED!
|
||||
TaskHandle_t mainTaskHandle = NULL; // Single task handles everything
|
||||
|
||||
public:
|
||||
// Main initialization (no clock outputs initially)
|
||||
void begin();
|
||||
|
||||
// Modern clean interface
|
||||
void setOutputManager(OutputManager* outputManager);
|
||||
void setConfigManager(ConfigManager* configManager);
|
||||
void setNetworking(Networking* networking);
|
||||
|
||||
// Clock Updates Pause Functions
|
||||
void pauseClockUpdates() { clockUpdatesPaused = true; }
|
||||
void resumeClockUpdates() { clockUpdatesPaused = false; }
|
||||
bool areClockUpdatesPaused() const { return clockUpdatesPaused; }
|
||||
|
||||
// Legacy interface (DEPRECATED - will be removed)
|
||||
void setRelayWriteFunction(void (*func)(int, int));
|
||||
|
||||
// DEPRECATED: Clock outputs now configured via ConfigManager
|
||||
void setClockOutputs(int relay1, int relay2) __attribute__((deprecated("Use ConfigManager to set clock outputs")));
|
||||
|
||||
// Time management functions
|
||||
void setTime(unsigned long timestamp);
|
||||
|
||||
/**
|
||||
* @brief Set RTC time with local timestamp (timezone already applied)
|
||||
* @param localTimestamp Unix timestamp with timezone offset already applied
|
||||
*/
|
||||
void setTimeWithLocalTimestamp(unsigned long localTimestamp); // Set RTC time from Unix timestamp
|
||||
unsigned long getTime(); // Get current time as Unix timestamp
|
||||
void syncTimeWithNTP(); // Sync RTC time with NTP server
|
||||
|
||||
// Event management
|
||||
void loadTodaysEvents();
|
||||
void loadNextDayEvents();
|
||||
|
||||
// Static task functions (CONSOLIDATED)
|
||||
static void mainTimekeeperTask(void* parameter);
|
||||
|
||||
bool isHealthy();
|
||||
|
||||
private:
|
||||
// Helper functions
|
||||
bool isSameDate(String eventDateTime, int year, int month, int day);
|
||||
void addToTodaysSchedule(JsonObject event);
|
||||
void sortEventsByTime();
|
||||
String getCurrentTimeString();
|
||||
|
||||
// Core functionality
|
||||
void checkScheduledEvents();
|
||||
void triggerEvent(ScheduledEvent& event);
|
||||
|
||||
// New clock features - comprehensive alert and automation system
|
||||
void checkClockAlerts();
|
||||
void triggerHourlyAlert(int hour);
|
||||
void checkBacklightAutomation();
|
||||
bool isInSilencePeriod();
|
||||
bool isTimeInRange(const String& currentTime, const String& startTime, const String& endTime) const;
|
||||
void fireAlertBell(uint8_t bellNumber, int count = 1);
|
||||
|
||||
// Physical clock synchronization - SIMPLE approach based on your Arduino code
|
||||
void checkAndSyncPhysicalClock();
|
||||
void advancePhysicalClockOneMinute();
|
||||
void updatePhysicalClockTime();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// HEALTH CHECK METHOD
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
public:
|
||||
/** @brief Check if TimeKeeper is in healthy state */
|
||||
bool isHealthy() const;
|
||||
};
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user