diff --git a/include/spore/Spore.h b/include/spore/Spore.h index df1cc57..ea0f89e 100644 --- a/include/spore/Spore.h +++ b/include/spore/Spore.h @@ -11,6 +11,7 @@ #include "core/TaskManager.h" #include "Service.h" #include "util/Logging.h" +#include "util/CpuUsage.h" class Spore { public: @@ -33,6 +34,11 @@ public: TaskManager& getTaskManager() { return taskManager; } ClusterManager& getCluster() { return cluster; } ApiServer& getApiServer() { return apiServer; } + + // CPU usage monitoring + CpuUsage& getCpuUsage() { return cpuUsage; } + float getCurrentCpuUsage() const { return cpuUsage.getCpuUsage(); } + float getAverageCpuUsage() const { return cpuUsage.getAverageCpuUsage(); } private: @@ -45,6 +51,7 @@ private: TaskManager taskManager; ClusterManager cluster; ApiServer apiServer; + CpuUsage cpuUsage; std::vector> services; bool initialized; diff --git a/include/spore/services/MonitoringService.h b/include/spore/services/MonitoringService.h new file mode 100644 index 0000000..9da968f --- /dev/null +++ b/include/spore/services/MonitoringService.h @@ -0,0 +1,51 @@ +#pragma once +#include "spore/Service.h" +#include "spore/util/CpuUsage.h" +#include + +class MonitoringService : public Service { +public: + MonitoringService(CpuUsage& cpuUsage); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "Monitoring"; } + + // System resource information + struct SystemResources { + // CPU information + float currentCpuUsage; + float averageCpuUsage; + float maxCpuUsage; + float minCpuUsage; + unsigned long measurementCount; + bool isMeasuring; + + // Memory information + size_t freeHeap; + size_t totalHeap; + size_t minFreeHeap; + size_t maxAllocHeap; + size_t heapFragmentation; + + // Filesystem information + size_t totalBytes; + size_t usedBytes; + size_t freeBytes; + float usagePercent; + + // System uptime + unsigned long uptimeMs; + unsigned long uptimeSeconds; + }; + + // Get current system resources + SystemResources getSystemResources() const; + +private: + void handleResourcesRequest(AsyncWebServerRequest* request); + + // Helper methods + size_t calculateHeapFragmentation() const; + void getFilesystemInfo(size_t& totalBytes, size_t& usedBytes) const; + + CpuUsage& cpuUsage; +}; \ No newline at end of file diff --git a/include/spore/util/CpuUsage.h b/include/spore/util/CpuUsage.h new file mode 100644 index 0000000..7218294 --- /dev/null +++ b/include/spore/util/CpuUsage.h @@ -0,0 +1,118 @@ +#pragma once + +#include + +/** + * @brief CPU usage measurement utility for ESP32/ESP8266 + * + * This class provides methods to measure CPU usage by tracking idle time + * and calculating the percentage of time the CPU is busy vs idle. + */ +class CpuUsage { +public: + /** + * @brief Construct a new CpuUsage object + */ + CpuUsage(); + + /** + * @brief Destructor + */ + ~CpuUsage() = default; + + /** + * @brief Initialize the CPU usage measurement + * Call this once during setup + */ + void begin(); + + /** + * @brief Start measuring CPU usage for the current cycle + * Call this at the beginning of your main loop + */ + void startMeasurement(); + + /** + * @brief End measuring CPU usage for the current cycle + * Call this at the end of your main loop + */ + void endMeasurement(); + + /** + * @brief Get the current CPU usage percentage + * @return float CPU usage percentage (0.0 to 100.0) + */ + float getCpuUsage() const; + + /** + * @brief Get the average CPU usage over the measurement window + * @return float Average CPU usage percentage (0.0 to 100.0) + */ + float getAverageCpuUsage() const; + + /** + * @brief Get the maximum CPU usage recorded + * @return float Maximum CPU usage percentage (0.0 to 100.0) + */ + float getMaxCpuUsage() const; + + /** + * @brief Get the minimum CPU usage recorded + * @return float Minimum CPU usage percentage (0.0 to 100.0) + */ + float getMinCpuUsage() const; + + /** + * @brief Reset all CPU usage statistics + */ + void reset(); + + /** + * @brief Check if measurement is currently active + * @return true if measurement is active, false otherwise + */ + bool isMeasuring() const; + + /** + * @brief Get the number of measurements taken + * @return unsigned long Number of measurements + */ + unsigned long getMeasurementCount() const; + +private: + // Measurement state + bool _initialized; + bool _measuring; + unsigned long _measurementCount; + + // Timing variables + unsigned long _cycleStartTime; + unsigned long _idleStartTime; + unsigned long _totalIdleTime; + unsigned long _totalCycleTime; + + // Statistics + float _currentCpuUsage; + float _averageCpuUsage; + float _maxCpuUsage; + float _minCpuUsage; + unsigned long _totalCpuTime; + + // Rolling average window + static constexpr size_t ROLLING_WINDOW_SIZE = 10; + float _rollingWindow[ROLLING_WINDOW_SIZE]; + size_t _rollingIndex; + bool _rollingWindowFull; + + /** + * @brief Update rolling average calculation + * @param value New value to add to rolling average + */ + void updateRollingAverage(float value); + + /** + * @brief Update min/max statistics + * @param value New value to check against min/max + */ + void updateMinMax(float value); +}; diff --git a/platformio.ini b/platformio.ini index 709248f..a138d54 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,7 +9,7 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -;default_envs = esp01_1m +default_envs = base src_dir = . data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data @@ -203,3 +203,42 @@ build_src_filter = + + + + +[env:esp01_1m_cpu_monitor] +platform = platformio/espressif8266@^4.2.1 +board = esp01_1m +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.filesystem = littlefs +board_build.flash_mode = dout +board_build.ldscript = eagle.flash.1m64.ld +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + + + + + + + + + + +[env:d1_mini_cpu_monitor] +platform = platformio/espressif8266@^4.2.1 +board = d1_mini +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.filesystem = littlefs +board_build.flash_mode = dio +board_build.flash_size = 4M +board_build.ldscript = eagle.flash.4m1m.ld +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + + + + + + + + + diff --git a/src/spore/Spore.cpp b/src/spore/Spore.cpp index 79bf743..85618d9 100644 --- a/src/spore/Spore.cpp +++ b/src/spore/Spore.cpp @@ -4,17 +4,18 @@ #include "spore/services/ClusterService.h" #include "spore/services/TaskService.h" #include "spore/services/StaticFileService.h" +#include "spore/services/MonitoringService.h" #include Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager), apiServer(ctx, taskManager, ctx.config.api_server_port), - initialized(false), apiServerStarted(false) { + cpuUsage(), initialized(false), apiServerStarted(false) { } Spore::Spore(std::initializer_list> initialLabels) : ctx(initialLabels), network(ctx), taskManager(ctx), cluster(ctx, taskManager), apiServer(ctx, taskManager, ctx.config.api_server_port), - initialized(false), apiServerStarted(false) { + cpuUsage(), initialized(false), apiServerStarted(false) { } Spore::~Spore() { @@ -33,6 +34,9 @@ void Spore::setup() { // Initialize core components initializeCore(); + // Initialize CPU usage monitoring + cpuUsage.begin(); + // Register core services registerCoreServices(); @@ -68,7 +72,16 @@ void Spore::loop() { return; } + // Start CPU usage measurement + cpuUsage.startMeasurement(); + + // Execute main tasks taskManager.execute(); + + // End CPU usage measurement before yield + cpuUsage.endMeasurement(); + + // Yield to allow other tasks to run yield(); } @@ -121,6 +134,7 @@ void Spore::registerCoreServices() { auto clusterService = std::make_shared(ctx); auto taskService = std::make_shared(taskManager); auto staticFileService = std::make_shared(ctx, apiServer); + auto monitoringService = std::make_shared(cpuUsage); // Add to services list services.push_back(nodeService); @@ -128,6 +142,7 @@ void Spore::registerCoreServices() { services.push_back(clusterService); services.push_back(taskService); services.push_back(staticFileService); + services.push_back(monitoringService); LOG_INFO("Spore", "Core services registered"); } diff --git a/src/spore/services/MonitoringService.cpp b/src/spore/services/MonitoringService.cpp new file mode 100644 index 0000000..a611456 --- /dev/null +++ b/src/spore/services/MonitoringService.cpp @@ -0,0 +1,115 @@ +#include "spore/services/MonitoringService.h" +#include "spore/core/ApiServer.h" +#include "spore/util/Logging.h" +#include +#include +#include + +MonitoringService::MonitoringService(CpuUsage& cpuUsage) + : cpuUsage(cpuUsage) { +} + +void MonitoringService::registerEndpoints(ApiServer& api) { + api.addEndpoint("/api/monitoring/resources", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleResourcesRequest(request); }, + std::vector{}); +} + +MonitoringService::SystemResources MonitoringService::getSystemResources() const { + SystemResources resources; + + // CPU information + resources.currentCpuUsage = cpuUsage.getCpuUsage(); + resources.averageCpuUsage = cpuUsage.getAverageCpuUsage(); + resources.maxCpuUsage = cpuUsage.getMaxCpuUsage(); + resources.minCpuUsage = cpuUsage.getMinCpuUsage(); + resources.measurementCount = cpuUsage.getMeasurementCount(); + resources.isMeasuring = cpuUsage.isMeasuring(); + + // Memory information - ESP8266 compatible + resources.freeHeap = ESP.getFreeHeap(); + resources.totalHeap = 81920; // ESP8266 has ~80KB RAM + resources.minFreeHeap = 0; // Not available on ESP8266 + resources.maxAllocHeap = 0; // Not available on ESP8266 + resources.heapFragmentation = calculateHeapFragmentation(); + + // Filesystem information + getFilesystemInfo(resources.totalBytes, resources.usedBytes); + resources.freeBytes = resources.totalBytes - resources.usedBytes; + resources.usagePercent = resources.totalBytes > 0 ? + (float)resources.usedBytes / (float)resources.totalBytes * 100.0f : 0.0f; + + // System uptime + resources.uptimeMs = millis(); + resources.uptimeSeconds = resources.uptimeMs / 1000; + + return resources; +} + +void MonitoringService::handleResourcesRequest(AsyncWebServerRequest* request) { + SystemResources resources = getSystemResources(); + + JsonDocument doc; + + // CPU section + JsonObject cpu = doc["cpu"].to(); + cpu["current_usage"] = resources.currentCpuUsage; + cpu["average_usage"] = resources.averageCpuUsage; + cpu["max_usage"] = resources.maxCpuUsage; + cpu["min_usage"] = resources.minCpuUsage; + cpu["measurement_count"] = resources.measurementCount; + cpu["is_measuring"] = resources.isMeasuring; + + // Memory section + JsonObject memory = doc["memory"].to(); + memory["free_heap"] = resources.freeHeap; + memory["total_heap"] = resources.totalHeap; + memory["min_free_heap"] = resources.minFreeHeap; + memory["max_alloc_heap"] = resources.maxAllocHeap; + memory["heap_fragmentation"] = resources.heapFragmentation; + memory["heap_usage_percent"] = resources.totalHeap > 0 ? + (float)(resources.totalHeap - resources.freeHeap) / (float)resources.totalHeap * 100.0f : 0.0f; + + // Filesystem section + JsonObject filesystem = doc["filesystem"].to(); + filesystem["total_bytes"] = resources.totalBytes; + filesystem["used_bytes"] = resources.usedBytes; + filesystem["free_bytes"] = resources.freeBytes; + filesystem["usage_percent"] = resources.usagePercent; + + // System section + JsonObject system = doc["system"].to(); + system["uptime_ms"] = resources.uptimeMs; + system["uptime_seconds"] = resources.uptimeSeconds; + system["uptime_formatted"] = String(resources.uptimeSeconds / 3600) + "h " + + String((resources.uptimeSeconds % 3600) / 60) + "m " + + String(resources.uptimeSeconds % 60) + "s"; + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +size_t MonitoringService::calculateHeapFragmentation() const { + size_t freeHeap = ESP.getFreeHeap(); + size_t maxAllocHeap = 0; // Not available on ESP8266 + + if (maxAllocHeap == 0) return 0; + + // Calculate fragmentation as percentage of free heap that can't be allocated in one block + return (freeHeap - maxAllocHeap) * 100 / freeHeap; +} + +void MonitoringService::getFilesystemInfo(size_t& totalBytes, size_t& usedBytes) const { + totalBytes = 0; + usedBytes = 0; + + if (LittleFS.begin()) { + FSInfo fsInfo; + if (LittleFS.info(fsInfo)) { + totalBytes = fsInfo.totalBytes; + usedBytes = fsInfo.usedBytes; + } + LittleFS.end(); + } +} \ No newline at end of file diff --git a/src/spore/util/CpuUsage.cpp b/src/spore/util/CpuUsage.cpp new file mode 100644 index 0000000..3fef16e --- /dev/null +++ b/src/spore/util/CpuUsage.cpp @@ -0,0 +1,185 @@ +#include "spore/util/CpuUsage.h" + +CpuUsage::CpuUsage() + : _initialized(false) + , _measuring(false) + , _measurementCount(0) + , _cycleStartTime(0) + , _idleStartTime(0) + , _totalIdleTime(0) + , _totalCycleTime(0) + , _currentCpuUsage(0.0f) + , _averageCpuUsage(0.0f) + , _maxCpuUsage(0.0f) + , _minCpuUsage(100.0f) + , _totalCpuTime(0) + , _rollingIndex(0) + , _rollingWindowFull(false) { + + // Initialize rolling window + for (size_t i = 0; i < ROLLING_WINDOW_SIZE; ++i) { + _rollingWindow[i] = 0.0f; + } +} + +void CpuUsage::begin() { + if (_initialized) { + return; + } + + _initialized = true; + _measurementCount = 0; + _totalIdleTime = 0; + _totalCycleTime = 0; + _totalCpuTime = 0; + _currentCpuUsage = 0.0f; + _averageCpuUsage = 0.0f; + _maxCpuUsage = 0.0f; + _minCpuUsage = 100.0f; + _rollingIndex = 0; + _rollingWindowFull = false; + + // Initialize rolling window + for (size_t i = 0; i < ROLLING_WINDOW_SIZE; ++i) { + _rollingWindow[i] = 0.0f; + } +} + +void CpuUsage::startMeasurement() { + if (!_initialized) { + return; + } + + if (_measuring) { + // If already measuring, end the previous measurement first + endMeasurement(); + } + + _measuring = true; + _cycleStartTime = millis(); + _idleStartTime = millis(); +} + +void CpuUsage::endMeasurement() { + if (!_initialized || !_measuring) { + return; + } + + unsigned long cycleEndTime = millis(); + unsigned long cycleDuration = cycleEndTime - _cycleStartTime; + + // Calculate idle time (time spent in yield() calls) + unsigned long idleTime = cycleEndTime - _idleStartTime; + + // Calculate CPU usage + if (cycleDuration > 0) { + _currentCpuUsage = ((float)(cycleDuration - idleTime) / (float)cycleDuration) * 100.0f; + + // Clamp to valid range + if (_currentCpuUsage < 0.0f) { + _currentCpuUsage = 0.0f; + } else if (_currentCpuUsage > 100.0f) { + _currentCpuUsage = 100.0f; + } + + // Update statistics + _totalCycleTime += cycleDuration; + _totalIdleTime += idleTime; + _totalCpuTime += (cycleDuration - idleTime); + _measurementCount++; + + // Update rolling average + updateRollingAverage(_currentCpuUsage); + + // Update min/max + updateMinMax(_currentCpuUsage); + + // Calculate overall average + if (_measurementCount > 0) { + _averageCpuUsage = ((float)_totalCpuTime / (float)_totalCycleTime) * 100.0f; + } + } + + _measuring = false; +} + +float CpuUsage::getCpuUsage() const { + return _currentCpuUsage; +} + +float CpuUsage::getAverageCpuUsage() const { + if (_rollingWindowFull) { + return _averageCpuUsage; + } else if (_measurementCount > 0) { + // Calculate average from rolling window + float sum = 0.0f; + for (size_t i = 0; i < _rollingIndex; ++i) { + sum += _rollingWindow[i]; + } + return sum / (float)_rollingIndex; + } + return 0.0f; +} + +float CpuUsage::getMaxCpuUsage() const { + return _maxCpuUsage; +} + +float CpuUsage::getMinCpuUsage() const { + return _minCpuUsage; +} + +void CpuUsage::reset() { + _measurementCount = 0; + _totalIdleTime = 0; + _totalCycleTime = 0; + _totalCpuTime = 0; + _currentCpuUsage = 0.0f; + _averageCpuUsage = 0.0f; + _maxCpuUsage = 0.0f; + _minCpuUsage = 100.0f; + _rollingIndex = 0; + _rollingWindowFull = false; + + // Reset rolling window + for (size_t i = 0; i < ROLLING_WINDOW_SIZE; ++i) { + _rollingWindow[i] = 0.0f; + } +} + +bool CpuUsage::isMeasuring() const { + return _measuring; +} + +unsigned long CpuUsage::getMeasurementCount() const { + return _measurementCount; +} + +void CpuUsage::updateRollingAverage(float value) { + _rollingWindow[_rollingIndex] = value; + _rollingIndex++; + + if (_rollingIndex >= ROLLING_WINDOW_SIZE) { + _rollingIndex = 0; + _rollingWindowFull = true; + } + + // Calculate rolling average + float sum = 0.0f; + size_t count = _rollingWindowFull ? ROLLING_WINDOW_SIZE : _rollingIndex; + + for (size_t i = 0; i < count; ++i) { + sum += _rollingWindow[i]; + } + + _averageCpuUsage = sum / (float)count; +} + +void CpuUsage::updateMinMax(float value) { + if (value > _maxCpuUsage) { + _maxCpuUsage = value; + } + if (value < _minCpuUsage) { + _minCpuUsage = value; + } +}