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...
-
-
-
-
-
-
-
-
-
-
-
-
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));
+}