feat: releay ui example, simplify logging

This commit is contained in:
2025-09-16 15:32:49 +02:00
parent 2d85f560bb
commit 702eec5a13
27 changed files with 577 additions and 1106 deletions

View File

@@ -4,7 +4,6 @@
#include "spore/services/ClusterService.h"
#include "spore/services/TaskService.h"
#include "spore/services/StaticFileService.h"
#include "spore/services/LoggingService.h"
#include <Arduino.h>
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
@@ -24,12 +23,12 @@ Spore::~Spore() {
void Spore::setup() {
if (initialized) {
LOG_INFO(ctx, "Spore", "Already initialized, skipping setup");
LOG_INFO("Spore", "Already initialized, skipping setup");
return;
}
Serial.begin(115200);
LOG_INFO(ctx, "Spore", "Starting Spore framework...");
LOG_INFO("Spore", "Starting Spore framework...");
// Initialize core components
initializeCore();
@@ -38,21 +37,21 @@ void Spore::setup() {
registerCoreServices();
initialized = true;
LOG_INFO(ctx, "Spore", "Framework setup complete - call begin() to start API server");
LOG_INFO("Spore", "Framework setup complete - call begin() to start API server");
}
void Spore::begin() {
if (!initialized) {
LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first");
LOG_ERROR("Spore", "Framework not initialized, call setup() first");
return;
}
if (apiServerStarted) {
LOG_WARN(ctx, "Spore", "API server already started");
LOG_WARN("Spore", "API server already started");
return;
}
LOG_INFO(ctx, "Spore", "Starting API server...");
LOG_INFO("Spore", "Starting API server...");
// Start API server
startApiServer();
@@ -60,12 +59,12 @@ void Spore::begin() {
// Print initial task status
taskManager.printTaskStatus();
LOG_INFO(ctx, "Spore", "Framework ready!");
LOG_INFO("Spore", "Framework ready!");
}
void Spore::loop() {
if (!initialized) {
LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first");
LOG_ERROR("Spore", "Framework not initialized, call setup() first");
return;
}
@@ -75,7 +74,7 @@ void Spore::loop() {
void Spore::addService(std::shared_ptr<Service> service) {
if (!service) {
LOG_WARN(ctx, "Spore", "Attempted to add null service");
LOG_WARN("Spore", "Attempted to add null service");
return;
}
@@ -84,15 +83,15 @@ void Spore::addService(std::shared_ptr<Service> service) {
if (apiServerStarted) {
// If API server is already started, register the service immediately
apiServer.addService(*service);
LOG_INFO(ctx, "Spore", "Added service '" + String(service->getName()) + "' to running API server");
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to running API server");
} else {
LOG_INFO(ctx, "Spore", "Registered service '" + String(service->getName()) + "' (will be added to API server when begin() is called)");
LOG_INFO("Spore", "Registered service '" + String(service->getName()) + "' (will be added to API server when begin() is called)");
}
}
void Spore::addService(Service* service) {
if (!service) {
LOG_WARN(ctx, "Spore", "Attempted to add null service");
LOG_WARN("Spore", "Attempted to add null service");
return;
}
@@ -102,7 +101,7 @@ void Spore::addService(Service* service) {
void Spore::initializeCore() {
LOG_INFO(ctx, "Spore", "Initializing core components...");
LOG_INFO("Spore", "Initializing core components...");
// Setup WiFi first
network.setupWiFi();
@@ -110,14 +109,11 @@ void Spore::initializeCore() {
// Initialize task manager
taskManager.initialize();
LOG_INFO(ctx, "Spore", "Core components initialized");
LOG_INFO("Spore", "Core components initialized");
}
void Spore::registerCoreServices() {
LOG_INFO(ctx, "Spore", "Registering core services...");
// Create logging service first (other services may use it)
auto loggingService = std::make_shared<LoggingService>(ctx, apiServer);
LOG_INFO("Spore", "Registering core services...");
// Create core services
auto nodeService = std::make_shared<NodeService>(ctx, apiServer);
@@ -127,29 +123,28 @@ void Spore::registerCoreServices() {
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
// Add to services list
services.push_back(loggingService);
services.push_back(nodeService);
services.push_back(networkService);
services.push_back(clusterService);
services.push_back(taskService);
services.push_back(staticFileService);
LOG_INFO(ctx, "Spore", "Core services registered");
LOG_INFO("Spore", "Core services registered");
}
void Spore::startApiServer() {
if (apiServerStarted) {
LOG_WARN(ctx, "Spore", "API server already started");
LOG_WARN("Spore", "API server already started");
return;
}
LOG_INFO(ctx, "Spore", "Starting API server...");
LOG_INFO("Spore", "Starting API server...");
// Register all services with API server
for (auto& service : services) {
if (service) {
apiServer.addService(*service);
LOG_INFO(ctx, "Spore", "Added service '" + String(service->getName()) + "' to API server");
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to API server");
}
}
@@ -157,5 +152,5 @@ void Spore::startApiServer() {
apiServer.begin();
apiServerStarted = true;
LOG_INFO(ctx, "Spore", "API server started");
LOG_INFO("Spore", "API server started");
}

View File

@@ -1,6 +1,6 @@
#include "spore/core/ApiServer.h"
#include "spore/Service.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include <algorithm>
const char* ApiServer::methodToStr(int method) {
@@ -78,19 +78,19 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
void ApiServer::addService(Service& service) {
services.push_back(service);
LOG_INFO(ctx, "API", "Added service: " + String(service.getName()));
LOG_INFO("API", "Added service: " + String(service.getName()));
}
void ApiServer::serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header) {
server.serveStatic(uri.c_str(), fs, path.c_str(), cache_header.c_str()).setDefaultFile("index.html");
LOG_INFO(ctx, "API", "Registered static file serving: " + uri + " -> " + path);
LOG_INFO("API", "Registered static file serving: " + uri + " -> " + path);
}
void ApiServer::begin() {
// Register all service endpoints
for (auto& service : services) {
service.get().registerEndpoints(*this);
LOG_INFO(ctx, "API", "Registered endpoints for service: " + String(service.get().getName()));
LOG_INFO("API", "Registered endpoints for service: " + String(service.get().getName()));
}
server.begin();

View File

@@ -1,6 +1,6 @@
#include "spore/core/ClusterManager.h"
#include "spore/internal/Globals.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
ClusterManager::ClusterManager(NodeContext& ctx, TaskManager& taskMgr) : ctx(ctx), taskManager(taskMgr) {
// Register callback for node_discovered event
@@ -19,7 +19,7 @@ void ClusterManager::registerTasks() {
taskManager.registerTask("print_members", ctx.config.print_interval_ms, [this]() { printMemberList(); });
taskManager.registerTask("heartbeat", ctx.config.heartbeat_interval_ms, [this]() { heartbeatTaskCallback(); });
taskManager.registerTask("update_members_info", ctx.config.member_info_update_interval_ms, [this]() { updateAllMembersInfoTaskCallback(); });
LOG_INFO(ctx, "ClusterManager", "Registered all cluster tasks");
LOG_INFO("ClusterManager", "Registered all cluster tasks");
}
void ClusterManager::sendDiscovery() {
@@ -73,33 +73,52 @@ void ClusterManager::addOrUpdateNode(const String& nodeHost, IPAddress nodeIP) {
newNode.lastSeen = millis();
updateNodeStatus(newNode, newNode.lastSeen, ctx.config.node_inactive_threshold_ms, ctx.config.node_dead_threshold_ms);
memberList[nodeHost] = newNode;
LOG_INFO(ctx, "Cluster", "Added node: " + nodeHost + " @ " + newNode.ip.toString() + " | Status: " + statusToStr(newNode.status) + " | last update: 0");
LOG_INFO("Cluster", "Added node: " + nodeHost + " @ " + newNode.ip.toString() + " | Status: " + statusToStr(newNode.status) + " | last update: 0");
//fetchNodeInfo(nodeIP); // Do not fetch here, handled by periodic task
}
void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
if(ip == ctx.localIP) {
LOG_DEBUG(ctx, "Cluster", "Skipping fetch for local node");
LOG_DEBUG("Cluster", "Skipping fetch for local node");
return;
}
unsigned long requestStart = millis();
HTTPClient http;
WiFiClient client;
String url = "http://" + ip.toString() + ClusterProtocol::API_NODE_STATUS;
http.begin(client, url);
// Use RAII pattern to ensure http.end() is always called
bool httpInitialized = false;
bool success = false;
httpInitialized = http.begin(client, url);
if (!httpInitialized) {
LOG_ERROR("Cluster", "Failed to initialize HTTP client for " + ip.toString());
return;
}
// Set timeout to prevent hanging
http.setTimeout(5000); // 5 second timeout
int httpCode = http.GET();
unsigned long requestEnd = millis();
unsigned long requestDuration = requestEnd - requestStart;
if (httpCode == 200) {
String payload = http.getString();
// Use stack-allocated JsonDocument with proper cleanup
JsonDocument doc;
DeserializationError err = deserializeJson(doc, payload);
if (!err) {
auto& memberList = *ctx.memberList;
// Still need to iterate since we're searching by IP, not hostname
for (auto& pair : memberList) {
NodeInfo& node = pair.second;
if (node.ip == ip) {
// Update resources efficiently
node.resources.freeHeap = doc["freeHeap"];
node.resources.chipId = doc["chipId"];
node.resources.sdkVersion = (const char*)doc["sdkVersion"];
@@ -108,40 +127,61 @@ void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
node.status = NodeInfo::ACTIVE;
node.latency = requestDuration;
node.lastSeen = millis();
// Clear and rebuild endpoints efficiently
node.endpoints.clear();
node.endpoints.reserve(10); // Pre-allocate to avoid reallocations
if (doc["api"].is<JsonArray>()) {
JsonArray apiArr = doc["api"].as<JsonArray>();
for (JsonObject apiObj : apiArr) {
String uri = (const char*)apiObj["uri"];
// Use const char* to avoid String copies
const char* uri = apiObj["uri"];
int method = apiObj["method"];
// Create basic EndpointInfo without params for cluster nodes
EndpointInfo endpoint;
endpoint.uri = uri;
endpoint.uri = uri; // String assignment is more efficient than construction
endpoint.method = method;
endpoint.isLocal = false;
endpoint.serviceName = "remote";
node.endpoints.push_back(endpoint);
node.endpoints.push_back(std::move(endpoint));
}
}
// Parse labels if present
// Parse labels efficiently
node.labels.clear();
if (doc["labels"].is<JsonObject>()) {
JsonObject labelsObj = doc["labels"].as<JsonObject>();
for (JsonPair kvp : labelsObj) {
String k = String(kvp.key().c_str());
String v = String(labelsObj[kvp.key()]);
node.labels[k] = v;
// Use const char* to avoid String copies
const char* key = kvp.key().c_str();
const char* value = labelsObj[kvp.key()];
node.labels[key] = value;
}
}
LOG_DEBUG(ctx, "Cluster", "Fetched info for node: " + node.hostname + " @ " + ip.toString());
LOG_DEBUG("Cluster", "Fetched info for node: " + node.hostname + " @ " + ip.toString());
success = true;
break;
}
}
} else {
LOG_ERROR("Cluster", "JSON parse error for node @ " + ip.toString() + ": " + String(err.c_str()));
}
} else {
LOG_ERROR(ctx, "Cluster", "Failed to fetch info for node @ " + ip.toString() + ", HTTP code: " + String(httpCode));
LOG_ERROR("Cluster", "Failed to fetch info for node @ " + ip.toString() + ", HTTP code: " + String(httpCode));
}
// Always ensure HTTP client is properly closed
if (httpInitialized) {
http.end();
}
// Log success/failure for debugging
if (!success) {
LOG_DEBUG("Cluster", "Failed to update node info for " + ip.toString());
}
http.end();
}
void ClusterManager::heartbeatTaskCallback() {
@@ -158,10 +198,25 @@ void ClusterManager::heartbeatTaskCallback() {
void ClusterManager::updateAllMembersInfoTaskCallback() {
auto& memberList = *ctx.memberList;
// Limit concurrent HTTP requests to prevent memory pressure
const size_t maxConcurrentRequests = ctx.config.max_concurrent_http_requests;
size_t requestCount = 0;
for (auto& pair : memberList) {
const NodeInfo& node = pair.second;
if (node.ip != ctx.localIP) {
// Only process a limited number of requests per cycle
if (requestCount >= maxConcurrentRequests) {
LOG_DEBUG("Cluster", "Limiting concurrent HTTP requests to prevent memory pressure");
break;
}
fetchNodeInfo(node.ip);
requestCount++;
// Add small delay between requests to prevent overwhelming the system
delay(100);
}
}
}
@@ -183,7 +238,7 @@ void ClusterManager::removeDeadNodes() {
for (auto it = memberList.begin(); it != memberList.end(); ) {
unsigned long diff = now - it->second.lastSeen;
if (it->second.status == NodeInfo::DEAD && diff > ctx.config.node_dead_threshold_ms) {
LOG_INFO(ctx, "Cluster", "Removing node: " + it->second.hostname);
LOG_INFO("Cluster", "Removing node: " + it->second.hostname);
it = memberList.erase(it);
} else {
++it;
@@ -194,13 +249,13 @@ void ClusterManager::removeDeadNodes() {
void ClusterManager::printMemberList() {
auto& memberList = *ctx.memberList;
if (memberList.empty()) {
LOG_INFO(ctx, "Cluster", "Member List: empty");
LOG_INFO("Cluster", "Member List: empty");
return;
}
LOG_INFO(ctx, "Cluster", "Member List:");
LOG_INFO("Cluster", "Member List:");
for (const auto& pair : memberList) {
const NodeInfo& node = pair.second;
LOG_INFO(ctx, "Cluster", " " + node.hostname + " @ " + node.ip.toString() + " | Status: " + statusToStr(node.status) + " | last seen: " + String(millis() - node.lastSeen));
LOG_INFO("Cluster", " " + node.hostname + " @ " + node.ip.toString() + " | Status: " + statusToStr(node.status) + " | last seen: " + String(millis() - node.lastSeen));
}
}
@@ -209,10 +264,18 @@ void ClusterManager::updateLocalNodeResources() {
auto it = memberList.find(ctx.hostname);
if (it != memberList.end()) {
NodeInfo& node = it->second;
node.resources.freeHeap = ESP.getFreeHeap();
uint32_t freeHeap = ESP.getFreeHeap();
node.resources.freeHeap = freeHeap;
node.resources.chipId = ESP.getChipId();
node.resources.sdkVersion = String(ESP.getSdkVersion());
node.resources.cpuFreqMHz = ESP.getCpuFreqMHz();
node.resources.flashChipSize = ESP.getFlashChipSize();
// Log memory warnings if heap is getting low
if (freeHeap < ctx.config.low_memory_threshold_bytes) {
LOG_WARN("Cluster", "Low memory warning: " + String(freeHeap) + " bytes free");
} else if (freeHeap < ctx.config.critical_memory_threshold_bytes) {
LOG_ERROR("Cluster", "Critical memory warning: " + String(freeHeap) + " bytes free");
}
}
}

View File

@@ -1,27 +1,27 @@
#include "spore/core/NetworkManager.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
// SSID and password are now configured via Config class
void NetworkManager::scanWifi() {
if (!isScanning) {
isScanning = true;
LOG_INFO(ctx, "WiFi", "Starting WiFi scan...");
LOG_INFO("WiFi", "Starting WiFi scan...");
// Start async WiFi scan
WiFi.scanNetworksAsync([this](int networksFound) {
LOG_INFO(ctx, "WiFi", "Scan completed, found " + String(networksFound) + " networks");
LOG_INFO("WiFi", "Scan completed, found " + String(networksFound) + " networks");
this->processAccessPoints();
this->isScanning = false;
}, true);
} else {
LOG_WARN(ctx, "WiFi", "Scan already in progress...");
LOG_WARN("WiFi", "Scan already in progress...");
}
}
void NetworkManager::processAccessPoints() {
int numNetworks = WiFi.scanComplete();
if (numNetworks <= 0) {
LOG_WARN(ctx, "WiFi", "No networks found or scan not complete");
LOG_WARN("WiFi", "No networks found or scan not complete");
return;
}
@@ -44,7 +44,7 @@ void NetworkManager::processAccessPoints() {
accessPoints.push_back(ap);
LOG_DEBUG(ctx, "WiFi", "Found network " + String(i + 1) + ": " + ap.ssid + ", Ch: " + String(ap.channel) + ", RSSI: " + String(ap.rssi));
LOG_DEBUG("WiFi", "Found network " + String(i + 1) + ": " + ap.ssid + ", Ch: " + String(ap.channel) + ", RSSI: " + String(ap.rssi));
}
// Free the memory used by the scan
@@ -77,26 +77,26 @@ void NetworkManager::setHostnameFromMac() {
void NetworkManager::setupWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str());
LOG_INFO(ctx, "WiFi", "Connecting to AP...");
LOG_INFO("WiFi", "Connecting to AP...");
unsigned long startAttemptTime = millis();
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < ctx.config.wifi_connect_timeout_ms) {
delay(ctx.config.wifi_retry_delay_ms);
// Progress dots handled by delay, no logging needed
}
if (WiFi.status() == WL_CONNECTED) {
LOG_INFO(ctx, "WiFi", "Connected to AP, IP: " + WiFi.localIP().toString());
LOG_INFO("WiFi", "Connected to AP, IP: " + WiFi.localIP().toString());
} else {
LOG_WARN(ctx, "WiFi", "Failed to connect to AP. Creating AP...");
LOG_WARN("WiFi", "Failed to connect to AP. Creating AP...");
WiFi.mode(WIFI_AP);
WiFi.softAP(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str());
LOG_INFO(ctx, "WiFi", "AP created, IP: " + WiFi.softAPIP().toString());
LOG_INFO("WiFi", "AP created, IP: " + WiFi.softAPIP().toString());
}
setHostnameFromMac();
ctx.udp->begin(ctx.config.udp_port);
ctx.localIP = WiFi.localIP();
ctx.hostname = WiFi.hostname();
LOG_INFO(ctx, "WiFi", "Hostname set to: " + ctx.hostname);
LOG_INFO(ctx, "WiFi", "UDP listening on port " + String(ctx.config.udp_port));
LOG_INFO("WiFi", "Hostname set to: " + ctx.hostname);
LOG_INFO("WiFi", "UDP listening on port " + String(ctx.config.udp_port));
// Populate self NodeInfo
ctx.self.hostname = ctx.hostname;

View File

@@ -1,5 +1,5 @@
#include "spore/core/TaskManager.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include <Arduino.h>
TaskManager::TaskManager(NodeContext& ctx) : ctx(ctx) {}
@@ -32,9 +32,9 @@ void TaskManager::enableTask(const std::string& name) {
int idx = findTaskIndex(name);
if (idx >= 0) {
taskDefinitions[idx].enabled = true;
LOG_INFO(ctx, "TaskManager", "Enabled task: " + String(name.c_str()));
LOG_INFO("TaskManager", "Enabled task: " + String(name.c_str()));
} else {
LOG_WARN(ctx, "TaskManager", "Task not found: " + String(name.c_str()));
LOG_WARN("TaskManager", "Task not found: " + String(name.c_str()));
}
}
@@ -42,9 +42,9 @@ void TaskManager::disableTask(const std::string& name) {
int idx = findTaskIndex(name);
if (idx >= 0) {
taskDefinitions[idx].enabled = false;
LOG_INFO(ctx, "TaskManager", "Disabled task: " + String(name.c_str()));
LOG_INFO("TaskManager", "Disabled task: " + String(name.c_str()));
} else {
LOG_WARN(ctx, "TaskManager", "Task not found: " + String(name.c_str()));
LOG_WARN("TaskManager", "Task not found: " + String(name.c_str()));
}
}
@@ -52,9 +52,9 @@ void TaskManager::setTaskInterval(const std::string& name, unsigned long interva
int idx = findTaskIndex(name);
if (idx >= 0) {
taskDefinitions[idx].interval = interval;
LOG_INFO(ctx, "TaskManager", "Set interval for task " + String(name.c_str()) + ": " + String(interval) + " ms");
LOG_INFO("TaskManager", "Set interval for task " + String(name.c_str()) + ": " + String(interval) + " ms");
} else {
LOG_WARN(ctx, "TaskManager", "Task not found: " + String(name.c_str()));
LOG_WARN("TaskManager", "Task not found: " + String(name.c_str()));
}
}
@@ -85,24 +85,24 @@ void TaskManager::enableAllTasks() {
for (auto& taskDef : taskDefinitions) {
taskDef.enabled = true;
}
LOG_INFO(ctx, "TaskManager", "Enabled all tasks");
LOG_INFO("TaskManager", "Enabled all tasks");
}
void TaskManager::disableAllTasks() {
for (auto& taskDef : taskDefinitions) {
taskDef.enabled = false;
}
LOG_INFO(ctx, "TaskManager", "Disabled all tasks");
LOG_INFO("TaskManager", "Disabled all tasks");
}
void TaskManager::printTaskStatus() const {
LOG_INFO(ctx, "TaskManager", "\nTask Status:");
LOG_INFO(ctx, "TaskManager", "==========================");
LOG_INFO("TaskManager", "\nTask Status:");
LOG_INFO("TaskManager", "==========================");
for (const auto& taskDef : taskDefinitions) {
LOG_INFO(ctx, "TaskManager", " " + String(taskDef.name.c_str()) + ": " + (taskDef.enabled ? "ENABLED" : "DISABLED") + " (interval: " + String(taskDef.interval) + " ms)");
LOG_INFO("TaskManager", " " + String(taskDef.name.c_str()) + ": " + (taskDef.enabled ? "ENABLED" : "DISABLED") + " (interval: " + String(taskDef.interval) + " ms)");
}
LOG_INFO(ctx, "TaskManager", "==========================\n");
LOG_INFO("TaskManager", "==========================\n");
}
void TaskManager::execute() {

View File

@@ -1,215 +0,0 @@
#include "spore/services/LoggingService.h"
#include <Arduino.h>
LoggingService::LoggingService(NodeContext& ctx, ApiServer& apiServer)
: ctx(ctx), apiServer(apiServer), currentLogLevel(LogLevel::INFO),
serialLoggingEnabled(true) {
setupEventHandlers();
}
void LoggingService::setupEventHandlers() {
// Register event handlers for different log destinations
ctx.on("log/serial", [this](void* data) {
this->handleSerialLog(data);
});
// Register for all log events
ctx.on("log/debug", [this](void* data) {
LogData* logData = static_cast<LogData*>(data);
if (logData->level >= currentLogLevel) {
this->log(logData->level, logData->component, logData->message);
}
delete logData;
});
ctx.on("log/info", [this](void* data) {
LogData* logData = static_cast<LogData*>(data);
if (logData->level >= currentLogLevel) {
this->log(logData->level, logData->component, logData->message);
}
delete logData;
});
ctx.on("log/warn", [this](void* data) {
LogData* logData = static_cast<LogData*>(data);
if (logData->level >= currentLogLevel) {
this->log(logData->level, logData->component, logData->message);
}
delete logData;
});
ctx.on("log/error", [this](void* data) {
LogData* logData = static_cast<LogData*>(data);
if (logData->level >= currentLogLevel) {
this->log(logData->level, logData->component, logData->message);
}
delete logData;
});
}
void LoggingService::registerEndpoints(ApiServer& api) {
LOG_INFO(ctx, "LoggingService", "Registering logging API endpoints");
// Register API endpoints for logging configuration
api.addEndpoint("/api/logging/level", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetLogLevel(request); },
std::vector<ParamSpec>{});
LOG_DEBUG(ctx, "LoggingService", "Registered GET /api/logging/level");
api.addEndpoint("/api/logging/level", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleSetLogLevel(request); },
std::vector<ParamSpec>{
ParamSpec{String("level"), true, String("body"), String("string"),
std::vector<String>{"DEBUG", "INFO", "WARN", "ERROR"}, String("INFO")}
});
LOG_DEBUG(ctx, "LoggingService", "Registered POST /api/logging/level with level parameter");
api.addEndpoint("/api/logging/config", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetConfig(request); },
std::vector<ParamSpec>{});
LOG_DEBUG(ctx, "LoggingService", "Registered GET /api/logging/config");
LOG_INFO(ctx, "LoggingService", "All logging API endpoints registered successfully");
}
void LoggingService::log(LogLevel level, const String& component, const String& message) {
if (level < currentLogLevel) {
return; // Skip logging if below current level
}
LogData* logData = new LogData(level, component, message);
// Fire events for different log destinations
if (serialLoggingEnabled) {
ctx.fire("log/serial", logData);
}
}
void LoggingService::debug(const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::DEBUG, component, message);
ctx.fire("log/debug", logData);
}
void LoggingService::info(const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::INFO, component, message);
ctx.fire("log/info", logData);
}
void LoggingService::warn(const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::WARN, component, message);
ctx.fire("log/warn", logData);
}
void LoggingService::error(const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::ERROR, component, message);
ctx.fire("log/error", logData);
}
void LoggingService::setLogLevel(LogLevel level) {
currentLogLevel = level;
info("LoggingService", "Log level set to " + logLevelToString(level));
}
void LoggingService::enableSerialLogging(bool enable) {
serialLoggingEnabled = enable;
info("LoggingService", "Serial logging " + String(enable ? "enabled" : "disabled"));
}
void LoggingService::handleSerialLog(void* data) {
LogData* logData = static_cast<LogData*>(data);
String formattedMessage = formatLogMessage(*logData);
Serial.println(formattedMessage);
}
String LoggingService::formatLogMessage(const LogData& logData) {
String timestamp = String(logData.timestamp);
String level = logLevelToString(logData.level);
return "[" + timestamp + "] [" + level + "] [" + logData.component + "] " + logData.message;
}
String LoggingService::logLevelToString(LogLevel level) {
switch (level) {
case LogLevel::DEBUG: return "DEBUG";
case LogLevel::INFO: return "INFO";
case LogLevel::WARN: return "WARN";
case LogLevel::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
// Global logging functions that use the event system directly
void logDebug(NodeContext& ctx, const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::DEBUG, component, message);
ctx.fire("log/debug", logData);
}
void logInfo(NodeContext& ctx, const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::INFO, component, message);
ctx.fire("log/info", logData);
}
void logWarn(NodeContext& ctx, const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::WARN, component, message);
ctx.fire("log/warn", logData);
}
void logError(NodeContext& ctx, const String& component, const String& message) {
LogData* logData = new LogData(LogLevel::ERROR, component, message);
ctx.fire("log/error", logData);
}
// API endpoint handlers
void LoggingService::handleGetLogLevel(AsyncWebServerRequest* request) {
LOG_DEBUG(ctx, "LoggingService", "handleGetLogLevel called");
String response = "{\"level\":\"" + logLevelToString(currentLogLevel) + "\"}";
LOG_DEBUG(ctx, "LoggingService", "Sending response: " + response);
request->send(200, "application/json", response);
}
void LoggingService::handleSetLogLevel(AsyncWebServerRequest* request) {
LOG_DEBUG(ctx, "LoggingService", "handleSetLogLevel called");
LOG_DEBUG(ctx, "LoggingService", "Request method: " + String(request->method()));
LOG_DEBUG(ctx, "LoggingService", "Request URL: " + request->url());
LOG_DEBUG(ctx, "LoggingService", "Content type: " + request->contentType());
if (request->hasParam("level", true)) {
String levelStr = request->getParam("level", true)->value();
LOG_DEBUG(ctx, "LoggingService", "Level parameter found: '" + levelStr + "'");
if (levelStr == "DEBUG") {
LOG_DEBUG(ctx, "LoggingService", "Setting log level to DEBUG");
setLogLevel(LogLevel::DEBUG);
} else if (levelStr == "INFO") {
LOG_DEBUG(ctx, "LoggingService", "Setting log level to INFO");
setLogLevel(LogLevel::INFO);
} else if (levelStr == "WARN") {
LOG_DEBUG(ctx, "LoggingService", "Setting log level to WARN");
setLogLevel(LogLevel::WARN);
} else if (levelStr == "ERROR") {
LOG_DEBUG(ctx, "LoggingService", "Setting log level to ERROR");
setLogLevel(LogLevel::ERROR);
} else {
LOG_ERROR(ctx, "LoggingService", "Invalid log level received: '" + levelStr + "'");
request->send(400, "application/json", "{\"error\":\"Invalid log level. Must be one of: DEBUG, INFO, WARN, ERROR\"}");
return;
}
LOG_INFO(ctx, "LoggingService", "Log level successfully set to " + logLevelToString(currentLogLevel));
request->send(200, "application/json", "{\"success\":true, \"level\":\"" + logLevelToString(currentLogLevel) + "\"}");
} else {
LOG_ERROR(ctx, "LoggingService", "Missing required parameter: level");
request->send(400, "application/json", "{\"error\":\"Missing required parameter: level\"}");
}
}
void LoggingService::handleGetConfig(AsyncWebServerRequest* request) {
LOG_DEBUG(ctx, "LoggingService", "handleGetConfig called");
JsonDocument doc;
doc["level"] = logLevelToString(currentLogLevel);
doc["serialEnabled"] = serialLoggingEnabled;
String response;
serializeJson(doc, response);
LOG_DEBUG(ctx, "LoggingService", "Sending config response: " + response);
request->send(200, "application/json", response);
}

View File

@@ -1,6 +1,6 @@
#include "spore/services/NodeService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
@@ -67,7 +67,7 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([this]() {
LOG_INFO(ctx, "API", "Restart device");
LOG_INFO("API", "Restart device");
delay(10);
ESP.restart();
});
@@ -76,10 +76,10 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename,
size_t index, uint8_t* data, size_t len, bool final) {
if (!index) {
LOG_INFO(ctx, "OTA", "Update Start " + String(filename));
LOG_INFO("OTA", "Update Start " + String(filename));
Update.runAsync(true);
if(!Update.begin(request->contentLength(), U_FLASH)) {
LOG_ERROR(ctx, "OTA", "Update failed: not enough space");
LOG_ERROR("OTA", "Update failed: not enough space");
Update.printError(Serial);
AsyncWebServerResponse* response = request->beginResponse(500, "application/json",
"{\"status\": \"FAIL\"}");
@@ -96,12 +96,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
if (final) {
if (Update.end(true)) {
if(Update.isFinished()) {
LOG_INFO(ctx, "OTA", "Update Success with " + String(index + len) + "B");
LOG_INFO("OTA", "Update Success with " + String(index + len) + "B");
} else {
LOG_WARN(ctx, "OTA", "Update not finished");
LOG_WARN("OTA", "Update not finished");
}
} else {
LOG_ERROR(ctx, "OTA", "Update failed: " + String(Update.getError()));
LOG_ERROR("OTA", "Update failed: " + String(Update.getError()));
Update.printError(Serial);
}
}
@@ -113,7 +113,7 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([this]() {
LOG_INFO(ctx, "API", "Restart device");
LOG_INFO("API", "Restart device");
delay(10);
ESP.restart();
});

View File

@@ -1,5 +1,5 @@
#include "spore/services/StaticFileService.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include <Arduino.h>
const String StaticFileService::name = "StaticFileService";
@@ -11,10 +11,10 @@ StaticFileService::StaticFileService(NodeContext& ctx, ApiServer& apiServer)
void StaticFileService::registerEndpoints(ApiServer& api) {
// Initialize LittleFS
if (!LittleFS.begin()) {
LOG_ERROR(ctx, "StaticFileService", "LittleFS Mount Failed");
LOG_ERROR("StaticFileService", "LittleFS Mount Failed");
return;
}
LOG_INFO(ctx, "StaticFileService", "LittleFS mounted successfully");
LOG_INFO("StaticFileService", "LittleFS mounted successfully");
// Use the built-in static file serving from ESPAsyncWebServer
api.serveStatic("/", LittleFS, "/public", "max-age=3600");

View File

@@ -28,4 +28,9 @@ Config::Config() {
// System Configuration
restart_delay_ms = 10;
json_doc_size = 1024;
// Memory Management
low_memory_threshold_bytes = 100000; // 10KB
critical_memory_threshold_bytes = 5000; // 5KB
max_concurrent_http_requests = 3;
}

View File

@@ -0,0 +1,47 @@
#include "spore/util/Logging.h"
// Global log level - defaults to INFO
LogLevel g_logLevel = LogLevel::INFO;
void logMessage(LogLevel level, const String& component, const String& message) {
// Skip if below current log level
if (level < g_logLevel) {
return;
}
// Format: [timestamp] [level] [component] message
String timestamp = String(millis());
String levelStr;
switch (level) {
case LogLevel::DEBUG: levelStr = "DEBUG"; break;
case LogLevel::INFO: levelStr = "INFO"; break;
case LogLevel::WARN: levelStr = "WARN"; break;
case LogLevel::ERROR: levelStr = "ERROR"; break;
default: levelStr = "UNKNOWN"; break;
}
String formatted = "[" + timestamp + "] [" + levelStr + "] [" + component + "] " + message;
Serial.println(formatted);
}
void logDebug(const String& component, const String& message) {
logMessage(LogLevel::DEBUG, component, message);
}
void logInfo(const String& component, const String& message) {
logMessage(LogLevel::INFO, component, message);
}
void logWarn(const String& component, const String& message) {
logMessage(LogLevel::WARN, component, message);
}
void logError(const String& component, const String& message) {
logMessage(LogLevel::ERROR, component, message);
}
void setLogLevel(LogLevel level) {
g_logLevel = level;
logInfo("Logging", "Log level set to " + String((int)level));
}