From 702eec5a131a362b249e78c7833beb1fb2f456b9 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Tue, 16 Sep 2025 15:32:49 +0200 Subject: [PATCH] feat: releay ui example, simplify logging --- data/public/index.html | 165 ------------ data/public/relay.html | 305 ----------------------- docs/LoggingService.md | 238 ------------------ examples/logging_example/main.cpp | 25 +- examples/neopattern/main.cpp | 2 +- examples/neopixel/NeoPixelService.cpp | 2 +- examples/neopixel/main.cpp | 2 +- examples/relay/RelayService.cpp | 8 +- examples/relay/data/public/script.js | 177 +++++++++++++ examples/relay/data/public/styles.css | 141 +++++++++++ examples/relay/main.cpp | 5 +- examples/static_web/main.cpp | 2 +- include/spore/Spore.h | 1 + include/spore/services/LoggingService.h | 72 ------ include/spore/types/Config.h | 5 + include/spore/util/Logging.h | 29 +++ platformio.ini | 9 + src/spore/Spore.cpp | 45 ++-- src/spore/core/ApiServer.cpp | 8 +- src/spore/core/ClusterManager.cpp | 103 ++++++-- src/spore/core/NetworkManager.cpp | 24 +- src/spore/core/TaskManager.cpp | 26 +- src/spore/services/LoggingService.cpp | 215 ---------------- src/spore/services/NodeService.cpp | 16 +- src/spore/services/StaticFileService.cpp | 6 +- src/spore/types/Config.cpp | 5 + src/spore/util/Logging.cpp | 47 ++++ 27 files changed, 577 insertions(+), 1106 deletions(-) delete mode 100644 data/public/index.html delete mode 100644 data/public/relay.html delete mode 100644 docs/LoggingService.md create mode 100644 examples/relay/data/public/script.js create mode 100644 examples/relay/data/public/styles.css delete mode 100644 include/spore/services/LoggingService.h create mode 100644 include/spore/util/Logging.h delete mode 100644 src/spore/services/LoggingService.cpp create mode 100644 src/spore/util/Logging.cpp diff --git a/data/public/index.html b/data/public/index.html deleted file mode 100644 index d52d7bc..0000000 --- a/data/public/index.html +++ /dev/null @@ -1,165 +0,0 @@ - - - - - - Spore - - - -
-

🍄 Spore Node

- -
-
-

Node Status

-
Loading...
-
-
-

Network

-
Loading...
-
-
-

Tasks

-
Loading...
-
-
-

Cluster

-
Loading...
-
-
- - -
- - - - diff --git a/data/public/relay.html b/data/public/relay.html deleted file mode 100644 index b418789..0000000 --- a/data/public/relay.html +++ /dev/null @@ -1,305 +0,0 @@ - - - - - - Relay Control - Spore - - - -
-

🔌 Relay Control

- -
-
- OFF -
-
Relay is OFF
-
Pin: Loading...
-
- -
- - - -
- -
-
-
- - - - diff --git a/docs/LoggingService.md b/docs/LoggingService.md deleted file mode 100644 index e9521e5..0000000 --- a/docs/LoggingService.md +++ /dev/null @@ -1,238 +0,0 @@ -# LoggingService - Centralized Event-Based Logging - -The LoggingService provides a centralized, event-driven logging system for the SPORE framework. It allows components to log messages through the event system, enabling flexible log routing and filtering. - -## Features - -- **Event-Driven Architecture**: Uses the SPORE event system for decoupled logging -- **Multiple Log Levels**: DEBUG, INFO, WARN, ERROR with configurable filtering -- **Multiple Destinations**: Serial output (extensible) -- **Component-Based Logging**: Tag messages with component names -- **API Configuration**: Configure logging via HTTP API endpoints -- **Easy Integration**: Simple macros and functions for logging - -## Usage - -### Basic Logging - -```cpp -#include "spore/services/LoggingService.h" - -// Using convenience macros (requires NodeContext) -LOG_DEBUG(ctx, "ComponentName", "Debug message"); -LOG_INFO(ctx, "ComponentName", "Info message"); -LOG_WARN(ctx, "ComponentName", "Warning message"); -LOG_ERROR(ctx, "ComponentName", "Error message"); - -// Using global functions -logDebug(ctx, "ComponentName", "Debug message"); -logInfo(ctx, "ComponentName", "Info message"); -logWarn(ctx, "ComponentName", "Warning message"); -logError(ctx, "ComponentName", "Error message"); -``` - -### Event System Integration - -The logging system uses the following events: - -- `log/debug` - Debug level messages -- `log/info` - Info level messages -- `log/warn` - Warning level messages -- `log/error` - Error level messages -- `log/serial` - Messages routed to serial output - -You can subscribe to these events for custom log handling: - -```cpp -// Subscribe to all debug messages -ctx.on("log/debug", [](void* data) { - LogData* logData = static_cast(data); - // Custom handling for debug messages - delete logData; -}); - -// Subscribe to serial log output -ctx.on("log/serial", [](void* data) { - LogData* logData = static_cast(data); - // Custom serial formatting - Serial.println("CUSTOM: " + logData->message); - delete logData; -}); -``` - -### LoggingService Class - -The LoggingService class provides programmatic control over logging: - -```cpp -LoggingService logger(ctx, apiServer); - -// Configure logging -logger.setLogLevel(LogLevel::DEBUG); -logger.enableSerialLogging(true); - -// Direct logging -logger.debug("MyComponent", "Debug message"); -logger.info("MyComponent", "Info message"); -logger.warn("MyComponent", "Warning message"); -logger.error("MyComponent", "Error message"); -``` - -## API Endpoints - -The LoggingService provides HTTP API endpoints for configuration with proper parameter validation: - -### Get Log Level -``` -GET /api/logging/level -``` -Returns current log level as JSON. - -**Response:** -```json -{ - "level": "INFO" -} -``` - -### Set Log Level -``` -POST /api/logging/level -Content-Type: application/x-www-form-urlencoded - -level=DEBUG -``` -Sets the log level with parameter validation. - -**Parameters:** -- `level` (required): One of `DEBUG`, `INFO`, `WARN`, `ERROR` - -**Success Response:** -```json -{ - "success": true, - "level": "DEBUG" -} -``` - -**Error Response:** -```json -{ - "error": "Invalid log level. Must be one of: DEBUG, INFO, WARN, ERROR" -} -``` - -### Get Configuration -``` -GET /api/logging/config -``` -Returns complete logging configuration. - -**Response:** -```json -{ - "level": "INFO", - "serialEnabled": true -} -``` - -## Log Levels - -- **DEBUG**: Detailed information for debugging -- **INFO**: General information about system operation -- **WARN**: Warning messages for potentially harmful situations -- **ERROR**: Error messages for serious problems - -Only messages at or above the current log level are processed. - -## Log Format - -Log messages are formatted as: -``` -[timestamp] [level] [component] message -``` - -Example: -``` -[12345] [INFO] [Spore] Framework setup complete -[12350] [DEBUG] [Network] WiFi connection established -[12355] [WARN] [Cluster] Node timeout detected -[12360] [ERROR] [API] Failed to start server -``` - -## Integration with SPORE - -The LoggingService is automatically registered as a core service in the SPORE framework. It's initialized before other services to ensure logging is available throughout the system. - -## Example - -See `examples/logging_example/main.cpp` for a complete example demonstrating the logging system. - -## Extending the Logging System - -### Adding New Log Destinations - -To add new log destinations (e.g., network logging, database logging): - -1. Subscribe to the appropriate log events: -```cpp -ctx.on("log/info", [](void* data) { - LogData* logData = static_cast(data); - // Send to your custom destination - sendToNetwork(logData->message); - delete logData; -}); -``` - -2. Or create a custom LoggingService subclass: -```cpp -class CustomLoggingService : public LoggingService { -public: - CustomLoggingService(NodeContext& ctx, ApiServer& apiServer) - : LoggingService(ctx, apiServer) { - // Register custom event handlers - ctx.on("log/custom", [this](void* data) { - this->handleCustomLog(data); - }); - } - -private: - void handleCustomLog(void* data) { - // Custom log handling - } -}; -``` - -### Custom Log Formatting - -Override the `formatLogMessage` method in a custom LoggingService to change log formatting: - -```cpp -String CustomLoggingService::formatLogMessage(const LogData& logData) { - // Custom formatting - return "CUSTOM[" + logData.component + "] " + logData.message; -} -``` - -## Performance Considerations - -- Log messages are processed asynchronously through the event system -- Memory is allocated for each log message (LogData struct) -- Consider log level filtering to reduce overhead in production - -## Troubleshooting - -### Logs Not Appearing -1. Check that LoggingService is registered -2. Verify log level is set appropriately -3. Ensure serial logging is enabled -4. Check Serial.begin() is called before logging - -### Memory Issues -1. Reduce log frequency -2. Use higher log levels to filter messages - -### Custom Event Handlers Not Working -1. Ensure event handlers are registered before logging starts -2. Check that LogData is properly deleted in custom handlers -3. Verify event names match exactly diff --git a/examples/logging_example/main.cpp b/examples/logging_example/main.cpp index ffbd03b..9708142 100644 --- a/examples/logging_example/main.cpp +++ b/examples/logging_example/main.cpp @@ -1,5 +1,5 @@ #include "spore/Spore.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" #include Spore spore; @@ -12,22 +12,17 @@ void setup() { spore.begin(); // Demonstrate different logging levels - LOG_INFO(spore.ctx, "Example", "Logging example started"); - LOG_DEBUG(spore.ctx, "Example", "This is a debug message"); - LOG_WARN(spore.ctx, "Example", "This is a warning message"); - LOG_ERROR(spore.ctx, "Example", "This is an error message"); + LOG_INFO("Example", "Logging example started"); + LOG_DEBUG("Example", "This is a debug message"); + LOG_WARN("Example", "This is a warning message"); + LOG_ERROR("Example", "This is an error message"); // Demonstrate logging with different components - LOG_INFO(spore.ctx, "Network", "WiFi connection established"); - LOG_INFO(spore.ctx, "Cluster", "Node discovered: esp-123456"); - LOG_INFO(spore.ctx, "API", "Server started on port 80"); + LOG_INFO("Network", "WiFi connection established"); + LOG_INFO("Cluster", "Node discovered: esp-123456"); + LOG_INFO("API", "Server started on port 80"); - // Demonstrate event-based logging - LogData* logData = new LogData(LogLevel::INFO, "Example", "Event-based logging demonstration"); - spore.ctx.fire("log/serial", logData); - - // Show that all logging now goes through the centralized system - LOG_INFO(spore.ctx, "Example", "All Serial.println calls have been replaced with event-based logging!"); + LOG_INFO("Example", "All logging now uses the simplified system!"); } void loop() { @@ -37,7 +32,7 @@ void loop() { // Log some periodic information static unsigned long lastLog = 0; if (millis() - lastLog > 5000) { - LOG_DEBUG(spore.ctx, "Example", "System running - Free heap: " + String(ESP.getFreeHeap())); + LOG_DEBUG("Example", "System running - Free heap: " + String(ESP.getFreeHeap())); lastLog = millis(); } diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp index c48206d..c4bd2c9 100644 --- a/examples/neopattern/main.cpp +++ b/examples/neopattern/main.cpp @@ -1,6 +1,6 @@ #include #include "spore/Spore.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" #include "NeoPatternService.h" #ifndef LED_STRIP_PIN diff --git a/examples/neopixel/NeoPixelService.cpp b/examples/neopixel/NeoPixelService.cpp index 162f296..899eda9 100644 --- a/examples/neopixel/NeoPixelService.cpp +++ b/examples/neopixel/NeoPixelService.cpp @@ -1,6 +1,6 @@ #include "NeoPixelService.h" #include "spore/core/ApiServer.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" // Wheel helper: map 0-255 to RGB rainbow static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) { diff --git a/examples/neopixel/main.cpp b/examples/neopixel/main.cpp index 43531fa..471ae92 100644 --- a/examples/neopixel/main.cpp +++ b/examples/neopixel/main.cpp @@ -1,6 +1,6 @@ #include #include "spore/Spore.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" #include "NeoPixelService.h" #ifndef NEOPIXEL_PIN diff --git a/examples/relay/RelayService.cpp b/examples/relay/RelayService.cpp index f9fbf78..7d5890c 100644 --- a/examples/relay/RelayService.cpp +++ b/examples/relay/RelayService.cpp @@ -1,6 +1,6 @@ #include "RelayService.h" #include "spore/core/ApiServer.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin) : ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) { @@ -65,13 +65,13 @@ void RelayService::turnOn() { relayOn = true; // Active LOW relay digitalWrite(relayPin, LOW); - LOG_INFO(ctx, "RelayService", "Relay ON"); + LOG_INFO("RelayService", "Relay ON"); } void RelayService::turnOff() { relayOn = false; digitalWrite(relayPin, HIGH); - LOG_INFO(ctx, "RelayService", "Relay OFF"); + LOG_INFO("RelayService", "Relay OFF"); } void RelayService::toggle() { @@ -84,6 +84,6 @@ void RelayService::toggle() { void RelayService::registerTasks() { taskManager.registerTask("relay_status_print", 5000, [this]() { - LOG_INFO(ctx, "RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF")); + LOG_INFO("RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF")); }); } diff --git a/examples/relay/data/public/script.js b/examples/relay/data/public/script.js new file mode 100644 index 0000000..e673096 --- /dev/null +++ b/examples/relay/data/public/script.js @@ -0,0 +1,177 @@ +// Only initialize if not already done +if (!window.relayControlInitialized) { + class RelayControl { + constructor() { + this.currentState = 'off'; + this.relayPin = ''; + this.init(); + } + + init() { + this.createComponent(); + this.loadRelayStatus(); + this.setupEventListeners(); + + // Refresh status every 2 seconds + setInterval(() => this.loadRelayStatus(), 2000); + } + + createComponent() { + // Create the main container + const container = document.createElement('div'); + container.className = 'relay-component'; + container.innerHTML = ` +

🔌 Relay Control

+ +
+
+ OFF +
+
Relay is OFF
+
Pin: Loading...
+
+ +
+ + + +
+ +
+
+ `; + + // Append to component div + const componentDiv = document.getElementById('component'); + if (componentDiv) { + componentDiv.appendChild(container); + } else { + // Fallback to body if component div not found + document.body.appendChild(container); + } + } + + setupEventListeners() { + document.getElementById('turnOnBtn').addEventListener('click', () => this.controlRelay('on')); + document.getElementById('turnOffBtn').addEventListener('click', () => this.controlRelay('off')); + document.getElementById('toggleBtn').addEventListener('click', () => this.controlRelay('toggle')); + } + + // Load initial relay status + async loadRelayStatus() { + try { + const response = await fetch('/api/relay/status'); + if (!response.ok) { + throw new Error('Failed to fetch relay status'); + } + + const data = await response.json(); + this.currentState = data.state; + this.relayPin = data.pin; + + this.updateUI(); + this.updateUptime(data.uptime); + } catch (error) { + console.error('Error loading relay status:', error); + this.showMessage('Error loading relay status: ' + error.message, 'error'); + } + } + + // Control relay + async controlRelay(action) { + const buttons = document.querySelectorAll('.btn'); + buttons.forEach(btn => btn.classList.add('loading')); + + try { + const response = await fetch('/api/relay', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `state=${action}` + }); + + const data = await response.json(); + + if (data.success) { + this.currentState = data.state; + this.updateUI(); + this.showMessage(`Relay turned ${data.state.toUpperCase()}`, 'success'); + } else { + this.showMessage(data.message || 'Failed to control relay', 'error'); + } + } catch (error) { + console.error('Error controlling relay:', error); + this.showMessage('Error controlling relay: ' + error.message, 'error'); + } finally { + buttons.forEach(btn => btn.classList.remove('loading')); + } + } + + // Update UI based on current state + updateUI() { + const statusIndicator = document.getElementById('statusIndicator'); + const statusText = document.getElementById('statusText'); + const pinInfo = document.getElementById('pinInfo'); + + if (this.currentState === 'on') { + statusIndicator.className = 'status-indicator on'; + statusIndicator.textContent = 'ON'; + statusText.textContent = 'Relay is ON'; + } else { + statusIndicator.className = 'status-indicator off'; + statusIndicator.textContent = 'OFF'; + statusText.textContent = 'Relay is OFF'; + } + + if (this.relayPin) { + pinInfo.textContent = `Pin: ${this.relayPin}`; + } + } + + // Update uptime display + updateUptime(uptime) { + const uptimeElement = document.getElementById('uptime'); + if (uptime) { + const seconds = Math.floor(uptime / 1000); + const minutes = Math.floor(seconds / 60); + const hours = Math.floor(minutes / 60); + + let uptimeText = `Uptime: ${seconds}s`; + if (hours > 0) { + uptimeText = `Uptime: ${hours}h ${minutes % 60}m ${seconds % 60}s`; + } else if (minutes > 0) { + uptimeText = `Uptime: ${minutes}m ${seconds % 60}s`; + } + + uptimeElement.textContent = uptimeText; + } + } + + // Show message to user + showMessage(message, type) { + const messageElement = document.getElementById('message'); + messageElement.textContent = message; + messageElement.className = type; + + // Clear message after 3 seconds + setTimeout(() => { + messageElement.textContent = ''; + messageElement.className = ''; + }, 3000); + } + } + + // Initialize the relay control when the page loads + document.addEventListener('DOMContentLoaded', () => { + new RelayControl(); + }); + + window.relayControlInitialized = true; +} diff --git a/examples/relay/data/public/styles.css b/examples/relay/data/public/styles.css new file mode 100644 index 0000000..096482f --- /dev/null +++ b/examples/relay/data/public/styles.css @@ -0,0 +1,141 @@ +.relay-component { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + border-radius: 20px; + padding: 40px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + text-align: center; + max-width: 400px; + width: 100%; + color: white; + box-sizing: border-box; +} + +.relay-component h1 { + margin: 0 0 30px 0; + font-size: 2.2em; + text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); +} + +.relay-component .relay-status { + margin-bottom: 30px; +} + +.relay-component .status-indicator { + width: 120px; + height: 120px; + border-radius: 50%; + margin: 0 auto 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2em; + font-weight: bold; + transition: all 0.3s ease; + border: 4px solid rgba(255, 255, 255, 0.3); +} + +.relay-component .status-indicator.off { + background: rgba(255, 0, 0, 0.3); + color: #ff6b6b; +} + +.relay-component .status-indicator.on { + background: rgba(0, 255, 0, 0.3); + color: #51cf66; + box-shadow: 0 0 20px rgba(81, 207, 102, 0.5); +} + +.relay-component .status-text { + font-size: 1.5em; + margin-bottom: 10px; +} + +.relay-component .pin-info { + font-size: 0.9em; + opacity: 0.8; +} + +.relay-component .controls { + display: flex; + gap: 15px; + justify-content: center; + flex-wrap: wrap; +} + +.relay-component .btn { + padding: 12px 24px; + border: none; + border-radius: 25px; + font-size: 1em; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + min-width: 100px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.relay-component .btn-primary { + background: rgba(255, 255, 255, 0.2); + color: #333; + border: 2px solid rgba(0, 0, 0, 0.8); + font-weight: bold; +} + +.relay-component .btn-primary:hover { + background: rgba(255, 255, 255, 0.4); + color: #222; + transform: translateY(-2px); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); +} + +.relay-component .btn-primary:active { + transform: translateY(0); +} + +.relay-component .btn-success { + background: rgba(81, 207, 102, 0.8); + color: white; + border: 2px solid rgba(81, 207, 102, 1); +} + +.relay-component .btn-success:hover { + background: rgba(81, 207, 102, 1); + transform: translateY(-2px); +} + +.relay-component .btn-danger { + background: rgba(255, 107, 107, 0.8); + color: white; + border: 2px solid rgba(255, 107, 107, 1); +} + +.relay-component .btn-danger:hover { + background: rgba(255, 107, 107, 1); + transform: translateY(-2px); +} + +.relay-component .loading { + opacity: 0.6; + pointer-events: none; +} + +.relay-component .error { + color: #ff6b6b; + margin-top: 15px; + font-size: 0.9em; +} + +.relay-component .success { + color: #51cf66; + margin-top: 15px; + font-size: 0.9em; +} + +.relay-component .uptime { + margin-top: 20px; + font-size: 0.8em; + opacity: 0.7; +} diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 50735fd..27f3a75 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -1,6 +1,5 @@ #include #include "spore/Spore.h" -#include "spore/services/LoggingService.h" #include "RelayService.h" // Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board. @@ -29,8 +28,8 @@ void setup() { // Start the API server and complete initialization spore.begin(); - LOG_INFO(spore.getContext(), "Main", "Relay service registered and ready!"); - LOG_INFO(spore.getContext(), "Main", "Web interface available at http:///relay.html"); + LOG_INFO("Main", "Relay service registered and ready!"); + LOG_INFO("Main", "Web interface available at http:///relay.html"); } void loop() { diff --git a/examples/static_web/main.cpp b/examples/static_web/main.cpp index 20518da..426331e 100644 --- a/examples/static_web/main.cpp +++ b/examples/static_web/main.cpp @@ -1,6 +1,6 @@ #include #include "spore/Spore.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" // Create Spore instance Spore spore; diff --git a/include/spore/Spore.h b/include/spore/Spore.h index dcaf2f1..df1cc57 100644 --- a/include/spore/Spore.h +++ b/include/spore/Spore.h @@ -10,6 +10,7 @@ #include "core/ApiServer.h" #include "core/TaskManager.h" #include "Service.h" +#include "util/Logging.h" class Spore { public: diff --git a/include/spore/services/LoggingService.h b/include/spore/services/LoggingService.h deleted file mode 100644 index 963b382..0000000 --- a/include/spore/services/LoggingService.h +++ /dev/null @@ -1,72 +0,0 @@ -#pragma once - -#include "spore/Service.h" -#include "spore/core/NodeContext.h" -#include "spore/core/ApiServer.h" -#include - -enum class LogLevel { - DEBUG = 0, - INFO = 1, - WARN = 2, - ERROR = 3 -}; - -struct LogData { - LogLevel level; - String component; - String message; - unsigned long timestamp; - - LogData(LogLevel lvl, const String& comp, const String& msg) - : level(lvl), component(comp), message(msg), timestamp(millis()) {} -}; - -class LoggingService : public Service { -public: - LoggingService(NodeContext& ctx, ApiServer& apiServer); - virtual ~LoggingService() = default; - - // Service interface - void registerEndpoints(ApiServer& api) override; - const char* getName() const override { return "logging"; } - - // Logging methods - void log(LogLevel level, const String& component, const String& message); - void debug(const String& component, const String& message); - void info(const String& component, const String& message); - void warn(const String& component, const String& message); - void error(const String& component, const String& message); - - // Configuration - void setLogLevel(LogLevel level); - void enableSerialLogging(bool enable); - -private: - NodeContext& ctx; - ApiServer& apiServer; - LogLevel currentLogLevel; - bool serialLoggingEnabled; - - void setupEventHandlers(); - void handleSerialLog(void* data); - String formatLogMessage(const LogData& logData); - String logLevelToString(LogLevel level); - - // API endpoint handlers - void handleGetLogLevel(AsyncWebServerRequest* request); - void handleSetLogLevel(AsyncWebServerRequest* request); - void handleGetConfig(AsyncWebServerRequest* request); -}; - -// Global logging functions that use the event system directly -void logDebug(NodeContext& ctx, const String& component, const String& message); -void logInfo(NodeContext& ctx, const String& component, const String& message); -void logWarn(NodeContext& ctx, const String& component, const String& message); -void logError(NodeContext& ctx, const String& component, const String& message); - -// Convenience macros for easy logging (requires ctx to be available) -#define LOG_DEBUG(ctx, component, message) logDebug(ctx, component, message) -#define LOG_INFO(ctx, component, message) logInfo(ctx, component, message) -#define LOG_WARN(ctx, component, message) logWarn(ctx, component, message) -#define LOG_ERROR(ctx, component, message) logError(ctx, component, message) diff --git a/include/spore/types/Config.h b/include/spore/types/Config.h index 95740eb..c9ca59e 100644 --- a/include/spore/types/Config.h +++ b/include/spore/types/Config.h @@ -32,6 +32,11 @@ public: unsigned long restart_delay_ms; uint16_t json_doc_size; + // Memory Management + uint32_t low_memory_threshold_bytes; + uint32_t critical_memory_threshold_bytes; + size_t max_concurrent_http_requests; + // Constructor Config(); }; \ No newline at end of file diff --git a/include/spore/util/Logging.h b/include/spore/util/Logging.h new file mode 100644 index 0000000..e49b96f --- /dev/null +++ b/include/spore/util/Logging.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +enum class LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3 +}; + +// Global log level - can be changed at runtime +extern LogLevel g_logLevel; + +// Simple logging functions +void logMessage(LogLevel level, const String& component, const String& message); +void logDebug(const String& component, const String& message); +void logInfo(const String& component, const String& message); +void logWarn(const String& component, const String& message); +void logError(const String& component, const String& message); + +// Set global log level +void setLogLevel(LogLevel level); + +// Convenience macros - no context needed +#define LOG_DEBUG(component, message) logDebug(component, message) +#define LOG_INFO(component, message) logInfo(component, message) +#define LOG_WARN(component, message) logWarn(component, message) +#define LOG_ERROR(component, message) logError(component, message) diff --git a/platformio.ini b/platformio.ini index a0e0f8c..709248f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -37,6 +37,7 @@ build_src_filter = + + + + + + [env:d1_mini] @@ -56,6 +57,7 @@ build_src_filter = + + + + + + [env:relay] @@ -75,6 +77,7 @@ build_src_filter = + + + + + + [env:d1_mini_relay] @@ -95,6 +98,7 @@ build_src_filter = + + + + + + [env:esp01_1m_neopixel] @@ -114,6 +118,7 @@ build_src_filter = + + + + + + [env:d1_mini_neopixel] @@ -134,6 +139,7 @@ build_src_filter = + + + + + + [env:esp01_1m_neopattern] @@ -154,6 +160,7 @@ build_src_filter = + + + + + + [env:d1_mini_neopattern] @@ -174,6 +181,7 @@ build_src_filter = + + + + + + [env:d1_mini_static_web] @@ -193,4 +201,5 @@ build_src_filter = + + + + + + diff --git a/src/spore/Spore.cpp b/src/spore/Spore.cpp index c27c719..79bf743 100644 --- a/src/spore/Spore.cpp +++ b/src/spore/Spore.cpp @@ -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 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) { 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) { 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(ctx, apiServer); + LOG_INFO("Spore", "Registering core services..."); // Create core services auto nodeService = std::make_shared(ctx, apiServer); @@ -127,29 +123,28 @@ void Spore::registerCoreServices() { auto staticFileService = std::make_shared(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"); } diff --git a/src/spore/core/ApiServer.cpp b/src/spore/core/ApiServer.cpp index 119fcdd..9b44f7c 100644 --- a/src/spore/core/ApiServer.cpp +++ b/src/spore/core/ApiServer.cpp @@ -1,6 +1,6 @@ #include "spore/core/ApiServer.h" #include "spore/Service.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" #include const char* ApiServer::methodToStr(int method) { @@ -78,19 +78,19 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function " + 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(); diff --git a/src/spore/core/ClusterManager.cpp b/src/spore/core/ClusterManager.cpp index 1c64db8..81c1ecc 100644 --- a/src/spore/core/ClusterManager.cpp +++ b/src/spore/core/ClusterManager.cpp @@ -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 apiArr = doc["api"].as(); 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 labelsObj = doc["labels"].as(); 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"); + } } } diff --git a/src/spore/core/NetworkManager.cpp b/src/spore/core/NetworkManager.cpp index 356cf5a..0021e84 100644 --- a/src/spore/core/NetworkManager.cpp +++ b/src/spore/core/NetworkManager.cpp @@ -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; diff --git a/src/spore/core/TaskManager.cpp b/src/spore/core/TaskManager.cpp index 9726e06..76cbcdc 100644 --- a/src/spore/core/TaskManager.cpp +++ b/src/spore/core/TaskManager.cpp @@ -1,5 +1,5 @@ #include "spore/core/TaskManager.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" #include 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() { diff --git a/src/spore/services/LoggingService.cpp b/src/spore/services/LoggingService.cpp deleted file mode 100644 index a7aaabc..0000000 --- a/src/spore/services/LoggingService.cpp +++ /dev/null @@ -1,215 +0,0 @@ -#include "spore/services/LoggingService.h" -#include - -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(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(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(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(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{}); - LOG_DEBUG(ctx, "LoggingService", "Registered GET /api/logging/level"); - - api.addEndpoint("/api/logging/level", HTTP_POST, - [this](AsyncWebServerRequest* request) { handleSetLogLevel(request); }, - std::vector{ - ParamSpec{String("level"), true, String("body"), String("string"), - std::vector{"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{}); - 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(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); -} diff --git a/src/spore/services/NodeService.cpp b/src/spore/services/NodeService.cpp index 42798bf..9da9015 100644 --- a/src/spore/services/NodeService.cpp +++ b/src/spore/services/NodeService.cpp @@ -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(); }); diff --git a/src/spore/services/StaticFileService.cpp b/src/spore/services/StaticFileService.cpp index bd681de..dbb5298 100644 --- a/src/spore/services/StaticFileService.cpp +++ b/src/spore/services/StaticFileService.cpp @@ -1,5 +1,5 @@ #include "spore/services/StaticFileService.h" -#include "spore/services/LoggingService.h" +#include "spore/util/Logging.h" #include 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"); diff --git a/src/spore/types/Config.cpp b/src/spore/types/Config.cpp index 4467884..0e8d8f7 100644 --- a/src/spore/types/Config.cpp +++ b/src/spore/types/Config.cpp @@ -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; } \ No newline at end of file diff --git a/src/spore/util/Logging.cpp b/src/spore/util/Logging.cpp new file mode 100644 index 0000000..34faeb6 --- /dev/null +++ b/src/spore/util/Logging.cpp @@ -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)); +}