diff --git a/examples/pixelstream/PixelStreamService.cpp b/examples/pixelstream/PixelStreamService.cpp new file mode 100644 index 0000000..4c8f306 --- /dev/null +++ b/examples/pixelstream/PixelStreamService.cpp @@ -0,0 +1,202 @@ +#include "PixelStreamService.h" +#include "spore/util/Logging.h" +#include + +PixelStreamService::PixelStreamService(NodeContext& ctx, ApiServer& apiServer, PixelStreamController* controller) + : ctx(ctx), apiServer(apiServer), controller(controller) {} + +void PixelStreamService::registerEndpoints(ApiServer& api) { + // Config endpoint for setting pixelstream configuration + api.registerEndpoint("/api/pixelstream/config", HTTP_PUT, + [this](AsyncWebServerRequest* request) { handleConfigRequest(request); }, + std::vector{ + ParamSpec{String("pin"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("pixel_count"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("matrix_width"), false, String("body"), String("number"), {}, String("")}, + ParamSpec{String("matrix_serpentine"), false, String("body"), String("boolean"), {}, String("")}, + ParamSpec{String("pixel_type"), false, String("body"), String("number"), {}, String("")} + }); + + // Config endpoint for getting pixelstream configuration + api.registerEndpoint("/api/pixelstream/config", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetConfigRequest(request); }, + std::vector{}); +} + +void PixelStreamService::registerTasks(TaskManager& taskManager) { + // PixelStreamService doesn't register any tasks itself +} + +PixelStreamConfig PixelStreamService::loadConfig() { + // Initialize with proper defaults + PixelStreamConfig config; + config.pin = 2; + config.pixelCount = 16; + config.brightness = 80; + config.matrixWidth = 16; + config.matrixSerpentine = false; + config.pixelType = NEO_GRB + NEO_KHZ800; + + if (!LittleFS.begin()) { + LOG_WARN("PixelStream", "Failed to initialize LittleFS, using defaults"); + return config; + } + + if (!LittleFS.exists(CONFIG_FILE())) { + LOG_INFO("PixelStream", "No pixelstream config file found, using defaults"); + // Save defaults + saveConfig(config); + return config; + } + + File file = LittleFS.open(CONFIG_FILE(), "r"); + if (!file) { + LOG_ERROR("PixelStream", "Failed to open config file for reading"); + return config; + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, file); + file.close(); + + if (error) { + LOG_ERROR("PixelStream", "Failed to parse config file: " + String(error.c_str())); + return config; + } + + if (doc["pin"].is()) config.pin = doc["pin"].as(); + if (doc["pixel_count"].is()) config.pixelCount = doc["pixel_count"].as(); + if (doc["brightness"].is()) config.brightness = doc["brightness"].as(); + if (doc["matrix_width"].is()) config.matrixWidth = doc["matrix_width"].as(); + if (doc["matrix_serpentine"].is()) config.matrixSerpentine = doc["matrix_serpentine"].as(); + if (doc["pixel_type"].is()) config.pixelType = static_cast(doc["pixel_type"].as()); + + LOG_INFO("PixelStream", "Configuration loaded from " + String(CONFIG_FILE())); + return config; +} + +bool PixelStreamService::saveConfig(const PixelStreamConfig& config) { + if (!LittleFS.begin()) { + LOG_ERROR("PixelStream", "LittleFS not initialized, cannot save config"); + return false; + } + + File file = LittleFS.open(CONFIG_FILE(), "w"); + if (!file) { + LOG_ERROR("PixelStream", "Failed to open config file for writing"); + return false; + } + + JsonDocument doc; + doc["pin"] = config.pin; + doc["pixel_count"] = config.pixelCount; + doc["brightness"] = config.brightness; + doc["matrix_width"] = config.matrixWidth; + doc["matrix_serpentine"] = config.matrixSerpentine; + doc["pixel_type"] = static_cast(config.pixelType); + + size_t bytesWritten = serializeJson(doc, file); + file.close(); + + if (bytesWritten > 0) { + LOG_INFO("PixelStream", "Configuration saved to " + String(CONFIG_FILE()) + " (" + String(bytesWritten) + " bytes)"); + return true; + } else { + LOG_ERROR("PixelStream", "Failed to write configuration to file"); + return false; + } +} + +void PixelStreamService::handleConfigRequest(AsyncWebServerRequest* request) { + // Load current config from file + PixelStreamConfig config = loadConfig(); + + bool updated = false; + + // Handle individual form parameters + if (request->hasParam("pin", true)) { + String pinStr = request->getParam("pin", true)->value(); + if (pinStr.length() > 0) { + int pinValue = pinStr.toInt(); + if (pinValue >= 0 && pinValue <= 255) { + config.pin = static_cast(pinValue); + updated = true; + } + } + } + if (request->hasParam("pixel_count", true)) { + String countStr = request->getParam("pixel_count", true)->value(); + if (countStr.length() > 0) { + int countValue = countStr.toInt(); + if (countValue > 0 && countValue <= 65535) { + config.pixelCount = static_cast(countValue); + updated = true; + } + } + } + if (request->hasParam("brightness", true)) { + String brightnessStr = request->getParam("brightness", true)->value(); + if (brightnessStr.length() > 0) { + int brightnessValue = brightnessStr.toInt(); + if (brightnessValue >= 0 && brightnessValue <= 255) { + config.brightness = static_cast(brightnessValue); + updated = true; + } + } + } + if (request->hasParam("matrix_width", true)) { + String widthStr = request->getParam("matrix_width", true)->value(); + if (widthStr.length() > 0) { + int widthValue = widthStr.toInt(); + if (widthValue > 0 && widthValue <= 65535) { + config.matrixWidth = static_cast(widthValue); + updated = true; + } + } + } + if (request->hasParam("matrix_serpentine", true)) { + String serpentineStr = request->getParam("matrix_serpentine", true)->value(); + config.matrixSerpentine = (serpentineStr.equalsIgnoreCase("true") || serpentineStr == "1"); + updated = true; + } + if (request->hasParam("pixel_type", true)) { + String typeStr = request->getParam("pixel_type", true)->value(); + if (typeStr.length() > 0) { + int typeValue = typeStr.toInt(); + config.pixelType = static_cast(typeValue); + updated = true; + } + } + + if (!updated) { + request->send(400, "application/json", "{\"error\":\"No valid configuration fields provided\"}"); + return; + } + + // Save config to file + if (saveConfig(config)) { + LOG_INFO("PixelStreamService", "Configuration updated and saved to pixelstream.json"); + request->send(200, "application/json", "{\"status\":\"success\",\"message\":\"Configuration updated and saved\"}"); + } else { + LOG_ERROR("PixelStreamService", "Failed to save configuration to file"); + request->send(500, "application/json", "{\"error\":\"Failed to save configuration\"}"); + } +} + +void PixelStreamService::handleGetConfigRequest(AsyncWebServerRequest* request) { + PixelStreamConfig config = loadConfig(); + + JsonDocument doc; + doc["pin"] = config.pin; + doc["pixel_count"] = config.pixelCount; + doc["brightness"] = config.brightness; + doc["matrix_width"] = config.matrixWidth; + doc["matrix_serpentine"] = config.matrixSerpentine; + doc["pixel_type"] = static_cast(config.pixelType); + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + diff --git a/examples/pixelstream/PixelStreamService.h b/examples/pixelstream/PixelStreamService.h new file mode 100644 index 0000000..e0925ae --- /dev/null +++ b/examples/pixelstream/PixelStreamService.h @@ -0,0 +1,33 @@ +#pragma once +#include "spore/Service.h" +#include "spore/core/NodeContext.h" +#include "PixelStreamController.h" +#include +#include +#include "spore/util/Logging.h" + +// PixelStreamConfig is defined in PixelStreamController.h + +class PixelStreamService : public Service { +public: + PixelStreamService(NodeContext& ctx, ApiServer& apiServer, PixelStreamController* controller); + void registerEndpoints(ApiServer& api) override; + void registerTasks(TaskManager& taskManager) override; + const char* getName() const override { return "PixelStream"; } + + // Config management + PixelStreamConfig loadConfig(); + bool saveConfig(const PixelStreamConfig& config); + void setController(PixelStreamController* ctrl) { controller = ctrl; } + +private: + NodeContext& ctx; + ApiServer& apiServer; + PixelStreamController* controller; + + void handleConfigRequest(AsyncWebServerRequest* request); + void handleGetConfigRequest(AsyncWebServerRequest* request); + + static const char* CONFIG_FILE() { return "/pixelstream.json"; } +}; + diff --git a/examples/pixelstream/main.cpp b/examples/pixelstream/main.cpp index f053938..2b705af 100644 --- a/examples/pixelstream/main.cpp +++ b/examples/pixelstream/main.cpp @@ -2,7 +2,11 @@ #include "spore/Spore.h" #include "spore/util/Logging.h" #include "PixelStreamController.h" +#include "PixelStreamService.h" +#include +// Defaults are now loaded from config.json on LittleFS +// Can still be overridden with preprocessor defines if needed #ifndef PIXEL_PIN #define PIXEL_PIN 2 #endif @@ -34,22 +38,28 @@ Spore spore({ }); PixelStreamController* controller = nullptr; +PixelStreamService* service = nullptr; void setup() { spore.setup(); - PixelStreamConfig config{ - static_cast(PIXEL_PIN), - static_cast(PIXEL_COUNT), - static_cast(PIXEL_BRIGHTNESS), - static_cast(PIXEL_MATRIX_WIDTH), - static_cast(PIXEL_MATRIX_SERPENTINE), - static_cast(PIXEL_TYPE) - }; - + // Create service first (need it to load config) + service = new PixelStreamService(spore.getContext(), spore.getApiServer(), nullptr); + + // Load pixelstream config from LittleFS (pixelstream.json) or use defaults + PixelStreamConfig config = service->loadConfig(); + + // Create controller with loaded config controller = new PixelStreamController(spore.getContext(), config); controller->begin(); - + + // Update service with the actual controller + service->setController(controller); + + // Register service + spore.registerService(service); + + // Start the API server spore.begin(); } diff --git a/include/spore/Spore.h b/include/spore/Spore.h index 55a732f..9a44dd6 100644 --- a/include/spore/Spore.h +++ b/include/spore/Spore.h @@ -11,7 +11,6 @@ #include "core/TaskManager.h" #include "Service.h" #include "util/Logging.h" -#include "util/CpuUsage.h" class Spore { public: @@ -34,12 +33,6 @@ 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: void initializeCore(); @@ -51,7 +44,6 @@ 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 index 91f62f8..18ea01a 100644 --- a/include/spore/services/MonitoringService.h +++ b/include/spore/services/MonitoringService.h @@ -1,11 +1,10 @@ #pragma once #include "spore/Service.h" -#include "spore/util/CpuUsage.h" #include class MonitoringService : public Service { public: - MonitoringService(CpuUsage& cpuUsage); + MonitoringService(); void registerEndpoints(ApiServer& api) override; void registerTasks(TaskManager& taskManager) override; const char* getName() const override { return "Monitoring"; } @@ -41,6 +40,4 @@ private: // Helper methods 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 deleted file mode 100644 index 7218294..0000000 --- a/include/spore/util/CpuUsage.h +++ /dev/null @@ -1,118 +0,0 @@ -#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/src/spore/Spore.cpp b/src/spore/Spore.cpp index 706671c..8390b1a 100644 --- a/src/spore/Spore.cpp +++ b/src/spore/Spore.cpp @@ -9,7 +9,7 @@ Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager), apiServer(ctx, taskManager, ctx.config.api_server_port), - cpuUsage(), initialized(false), apiServerStarted(false) { + initialized(false), apiServerStarted(false) { // Rebuild labels from constructor + config labels ctx.rebuildLabels(); @@ -18,7 +18,7 @@ Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager Spore::Spore(std::initializer_list> initialLabels) : ctx(initialLabels), network(ctx), taskManager(ctx), cluster(ctx, taskManager), apiServer(ctx, taskManager, ctx.config.api_server_port), - cpuUsage(), initialized(false), apiServerStarted(false) { + initialized(false), apiServerStarted(false) { // Rebuild labels from constructor + config labels (config takes precedence) ctx.rebuildLabels(); @@ -40,9 +40,6 @@ void Spore::setup() { // Initialize core components initializeCore(); - // Initialize CPU usage monitoring - cpuUsage.begin(); - // Register core services registerCoreServices(); @@ -78,15 +75,9 @@ 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(); } @@ -141,7 +132,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); + auto monitoringService = std::make_shared(); // Add to services list services.push_back(nodeService); diff --git a/src/spore/services/MonitoringService.cpp b/src/spore/services/MonitoringService.cpp index c6a466b..33ec425 100644 --- a/src/spore/services/MonitoringService.cpp +++ b/src/spore/services/MonitoringService.cpp @@ -5,8 +5,7 @@ #include #include -MonitoringService::MonitoringService(CpuUsage& cpuUsage) - : cpuUsage(cpuUsage) { +MonitoringService::MonitoringService() { } void MonitoringService::registerEndpoints(ApiServer& api) { @@ -22,11 +21,11 @@ void MonitoringService::registerTasks(TaskManager& taskManager) { MonitoringService::SystemResources MonitoringService::getSystemResources() const { SystemResources resources; - // CPU information - resources.currentCpuUsage = cpuUsage.getCpuUsage(); - resources.averageCpuUsage = cpuUsage.getAverageCpuUsage(); - resources.measurementCount = cpuUsage.getMeasurementCount(); - resources.isMeasuring = cpuUsage.isMeasuring(); + // CPU information - sending fixed value of 100 + resources.currentCpuUsage = 100.0f; + resources.averageCpuUsage = 100.0f; + resources.measurementCount = 0; + resources.isMeasuring = false; // Memory information - ESP8266 compatible resources.freeHeap = ESP.getFreeHeap(); diff --git a/src/spore/util/CpuUsage.cpp b/src/spore/util/CpuUsage.cpp deleted file mode 100644 index 3fef16e..0000000 --- a/src/spore/util/CpuUsage.cpp +++ /dev/null @@ -1,185 +0,0 @@ -#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; - } -}