Compare commits

6 Commits

13 changed files with 295 additions and 362 deletions

View File

@@ -195,13 +195,13 @@ void NeoPatternService::registerEventHandlers() {
JsonDocument doc;
DeserializationError err = deserializeJson(doc, *jsonStr);
if (err) {
LOG_WARN("NeoPattern", String("Failed to parse CLUSTER_EVENT data: ") + err.c_str());
LOG_WARN("NeoPattern", String("Failed to parse cluster/event data: ") + err.c_str());
return;
}
JsonObject obj = doc.as<JsonObject>();
bool applied = applyControlParams(obj);
if (applied) {
LOG_INFO("NeoPattern", "Applied control from CLUSTER_EVENT");
LOG_INFO("NeoPattern", "Applied control from cluster/event");
}
});

View File

@@ -0,0 +1,202 @@
#include "PixelStreamService.h"
#include "spore/util/Logging.h"
#include <Adafruit_NeoPixel.h>
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>{
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<ParamSpec>{});
}
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<uint8_t>()) config.pin = doc["pin"].as<uint8_t>();
if (doc["pixel_count"].is<uint16_t>()) config.pixelCount = doc["pixel_count"].as<uint16_t>();
if (doc["brightness"].is<uint8_t>()) config.brightness = doc["brightness"].as<uint8_t>();
if (doc["matrix_width"].is<uint16_t>()) config.matrixWidth = doc["matrix_width"].as<uint16_t>();
if (doc["matrix_serpentine"].is<bool>()) config.matrixSerpentine = doc["matrix_serpentine"].as<bool>();
if (doc["pixel_type"].is<uint8_t>()) config.pixelType = static_cast<neoPixelType>(doc["pixel_type"].as<uint8_t>());
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<uint8_t>(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<uint8_t>(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<uint16_t>(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<uint8_t>(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<uint16_t>(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<neoPixelType>(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<uint8_t>(config.pixelType);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/NodeContext.h"
#include "PixelStreamController.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#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"; }
};

View File

@@ -2,7 +2,11 @@
#include "spore/Spore.h"
#include "spore/util/Logging.h"
#include "PixelStreamController.h"
#include "PixelStreamService.h"
#include <Adafruit_NeoPixel.h>
// 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<uint8_t>(PIXEL_PIN),
static_cast<uint16_t>(PIXEL_COUNT),
static_cast<uint8_t>(PIXEL_BRIGHTNESS),
static_cast<uint16_t>(PIXEL_MATRIX_WIDTH),
static_cast<bool>(PIXEL_MATRIX_SERPENTINE),
static_cast<neoPixelType>(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();
}

View File

@@ -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<std::shared_ptr<Service>> services;
bool initialized;

View File

@@ -5,10 +5,9 @@
// Cluster protocol and API constants
namespace ClusterProtocol {
// Simplified heartbeat-only protocol
constexpr const char* HEARTBEAT_MSG = "CLUSTER_HEARTBEAT";
constexpr const char* NODE_UPDATE_MSG = "NODE_UPDATE";
constexpr const char* CLUSTER_EVENT_MSG = "CLUSTER_EVENT";
constexpr const char* HEARTBEAT_MSG = "cluster/heartbeat";
constexpr const char* NODE_UPDATE_MSG = "node/update";
constexpr const char* CLUSTER_EVENT_MSG = "cluster/event";
constexpr const char* RAW_MSG = "RAW";
constexpr uint16_t UDP_PORT = 4210;
// Increased buffer to accommodate larger RAW pixel streams and node info JSON over UDP

View File

@@ -1,11 +1,10 @@
#pragma once
#include "spore/Service.h"
#include "spore/util/CpuUsage.h"
#include <functional>
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;
};

View File

@@ -1,118 +0,0 @@
#pragma once
#include <Arduino.h>
/**
* @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);
};

View File

@@ -18,6 +18,15 @@ monitor_speed = 115200
lib_deps =
esp32async/ESPAsyncWebServer@^3.8.0
bblanchon/ArduinoJson@^7.4.2
build_flags =
-Os ; Optimize for size
-ffunction-sections ; Place each function in its own section
-fdata-sections ; Place data in separate sections
-Wl,--gc-sections ; Remove unused sections at link time
-DNDEBUG ; Disable debug assertions
-DVTABLES_IN_FLASH ; Move virtual tables to flash
-fno-exceptions ; Disable C++ exceptions
-fno-rtti ; Disable runtime type information
[env:base]
platform = platformio/espressif8266@^4.2.1
@@ -31,6 +40,7 @@ board_build.filesystem = littlefs
; note: somehow partition table is not working, so we need to use the ldscript
board_build.ldscript = eagle.flash.1m64.ld ; 64KB -> FS Size
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
build_src_filter =
+<examples/base/*.cpp>
+<src/spore/*.cpp>
@@ -51,6 +61,7 @@ board_build.flash_mode = dio ; D1 Mini uses DIO on 4 Mbit flash
board_build.flash_size = 4M
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
build_flags = ${common.build_flags}
build_src_filter =
+<examples/base/*.cpp>
+<src/spore/*.cpp>
@@ -71,6 +82,7 @@ board_build.flash_mode = dout
board_build.ldscript = eagle.flash.1m64.ld
lib_deps = ${common.lib_deps}
;data_dir = examples/relay/data
build_flags = ${common.build_flags}
build_src_filter =
+<examples/relay/*.cpp>
+<src/spore/*.cpp>
@@ -91,7 +103,7 @@ board_build.flash_mode = dout
board_build.ldscript = eagle.flash.1m64.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_flags = -DLED_STRIP_PIN=2
build_flags = -DLED_STRIP_PIN=2 ;${common.build_flags}
build_src_filter =
+<examples/neopattern/*.cpp>
+<src/spore/*.cpp>
@@ -112,7 +124,7 @@ board_build.flash_mode = dout
board_build.ldscript = eagle.flash.1m64.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_flags =
build_flags = ${common.build_flags}
build_src_filter =
+<examples/pixelstream/*.cpp>
+<src/spore/*.cpp>
@@ -133,7 +145,7 @@ board_build.flash_mode = dout
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_flags = -DPIXEL_PIN=TX -DPIXEL_COUNT=256 -DMATRIX_WIDTH=16
build_flags = -DPIXEL_PIN=TX -DPIXEL_COUNT=256 -DMATRIX_WIDTH=16 ${common.build_flags}
build_src_filter =
+<examples/pixelstream/*.cpp>
+<src/spore/*.cpp>
@@ -156,6 +168,7 @@ board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
dfrobot/DFRobotDFPlayerMini@^1.0.6
build_flags = ${common.build_flags}
build_src_filter =
+<examples/multimatrix/*.cpp>
+<examples/pixelstream/PixelStreamController.cpp>

View File

@@ -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<std::pair<String, String>> 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<ClusterService>(ctx);
auto taskService = std::make_shared<TaskService>(taskManager);
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
auto monitoringService = std::make_shared<MonitoringService>(cpuUsage);
auto monitoringService = std::make_shared<MonitoringService>();
// Add to services list
services.push_back(nodeService);

View File

@@ -9,7 +9,7 @@ ClusterManager::ClusterManager(NodeContext& ctx, TaskManager& taskMgr) : ctx(ctx
NodeInfo* node = static_cast<NodeInfo*>(data);
this->addOrUpdateNode(node->hostname, node->ip);
});
// Centralized broadcast handler: services fire 'cluster/broadcast' with CLUSTER_EVENT JSON payload
// Centralized broadcast handler: services fire 'cluster/broadcast' with cluster/event JSON payload
ctx.on("cluster/broadcast", [this](void* data) {
String* jsonStr = static_cast<String*>(data);
if (!jsonStr) {
@@ -20,7 +20,7 @@ ClusterManager::ClusterManager(NodeContext& ctx, TaskManager& taskMgr) : ctx(ctx
IPAddress ip = WiFi.localIP();
IPAddress mask = WiFi.subnetMask();
IPAddress bcast(ip[0] | ~mask[0], ip[1] | ~mask[1], ip[2] | ~mask[2], ip[3] | ~mask[3]);
LOG_DEBUG("Cluster", String("Broadcasting CLUSTER_EVENT to ") + bcast.toString() + " len=" + String(jsonStr->length()));
LOG_DEBUG("Cluster", String("Broadcasting cluster/event to ") + bcast.toString() + " len=" + String(jsonStr->length()));
this->ctx.udp->beginPacket(bcast, this->ctx.config.udp_port);
String msg = String(ClusterProtocol::CLUSTER_EVENT_MSG) + ":" + *jsonStr;
this->ctx.udp->write(msg.c_str());
@@ -120,7 +120,7 @@ bool ClusterManager::isRawMsg(const char* msg) {
// Discovery functionality removed - using heartbeat-only approach
void ClusterManager::onHeartbeat(const char* msg) {
// Extract hostname from heartbeat message: "CLUSTER_HEARTBEAT:hostname"
// Extract hostname from heartbeat message: "cluster/heartbeat:hostname"
const char* colon = strchr(msg, ':');
if (!colon) {
LOG_WARN("Cluster", "Invalid heartbeat message format");
@@ -138,7 +138,7 @@ void ClusterManager::onHeartbeat(const char* msg) {
}
void ClusterManager::onNodeUpdate(const char* msg) {
// Message format: "NODE_UPDATE:hostname:{json}"
// Message format: "node/update:hostname:{json}"
const char* firstColon = strchr(msg, ':');
if (!firstColon) {
LOG_WARN("Cluster", "Invalid NODE_UPDATE message format");
@@ -286,17 +286,17 @@ void ClusterManager::sendNodeInfo(const String& targetHostname, const IPAddress&
}
void ClusterManager::onClusterEvent(const char* msg) {
// Message format: CLUSTER_EVENT:{"event":"...","data":"<json string>"}
// Message format: cluster/event:{"event":"...","data":"<json string>"}
const char* jsonStart = msg + strlen(ClusterProtocol::CLUSTER_EVENT_MSG) + 1; // skip prefix and ':'
if (*jsonStart == '\0') {
LOG_DEBUG("Cluster", "CLUSTER_EVENT received with empty payload");
LOG_DEBUG("Cluster", "cluster/event received with empty payload");
return;
}
LOG_DEBUG("Cluster", String("CLUSTER_EVENT raw from ") + ctx.udp->remoteIP().toString() + " len=" + String(strlen(jsonStart)));
LOG_DEBUG("Cluster", String("cluster/event raw from ") + ctx.udp->remoteIP().toString() + " len=" + String(strlen(jsonStart)));
JsonDocument doc;
DeserializationError err = deserializeJson(doc, jsonStart);
if (err) {
LOG_ERROR("Cluster", String("Failed to parse CLUSTER_EVENT JSON from ") + ctx.udp->remoteIP().toString());
LOG_ERROR("Cluster", String("Failed to parse cluster/event JSON from ") + ctx.udp->remoteIP().toString());
return;
}
// Robust extraction of event and data
@@ -320,7 +320,7 @@ void ClusterManager::onClusterEvent(const char* msg) {
if (eventStr.length() == 0 || data.length() == 0) {
String dbg;
serializeJson(doc, dbg);
LOG_WARN("Cluster", String("CLUSTER_EVENT missing 'event' or 'data' | payload=") + dbg);
LOG_WARN("Cluster", String("cluster/event missing 'event' or 'data' | payload=") + dbg);
return;
}

View File

@@ -5,8 +5,7 @@
#include <FS.h>
#include <LittleFS.h>
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();

View File

@@ -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;
}
}