feat: logging service

This commit is contained in:
2025-09-16 10:10:23 +02:00
parent 6a30dc0dc1
commit 8a2988cb50
20 changed files with 676 additions and 101 deletions

238
docs/LoggingService.md Normal file
View File

@@ -0,0 +1,238 @@
# 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<LogData*>(data);
// Custom handling for debug messages
delete logData;
});
// Subscribe to serial log output
ctx.on("log/serial", [](void* data) {
LogData* logData = static_cast<LogData*>(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<LogData*>(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

View File

@@ -0,0 +1,45 @@
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include <Arduino.h>
Spore spore;
void setup() {
// Initialize Spore framework
spore.setup();
// Start the framework
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");
// 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");
// 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!");
}
void loop() {
// Run the Spore framework
spore.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()));
lastLog = millis();
}
delay(100);
}

View File

@@ -82,7 +82,8 @@ void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0; if (b > 255) b = 255;
if (b < 0) b = 0;
if (b > 255) b = 255;
setBrightness((uint8_t)b);
}

View File

@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "NeoPatternService.h"
#ifndef LED_STRIP_PIN
@@ -36,7 +37,7 @@ void setup() {
// Start the API server and complete initialization
spore.begin();
Serial.println("[Main] NeoPattern service registered and ready!");
LOG_INFO(spore.getContext(), "Main", "NeoPattern service registered and ready!");
}
void loop() {

View File

@@ -1,5 +1,6 @@
#include "NeoPixelService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
// Wheel helper: map 0-255 to RGB rainbow
static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) {
@@ -104,7 +105,8 @@ void NeoPixelService::handleControlRequest(AsyncWebServerRequest* request) {
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0; if (b > 255) b = 255;
if (b < 0) b = 0;
if (b > 255) b = 255;
setBrightness((uint8_t)b);
}

View File

@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "NeoPixelService.h"
#ifndef NEOPIXEL_PIN
@@ -36,7 +37,7 @@ void setup() {
// Start the API server and complete initialization
spore.begin();
Serial.println("[Main] NeoPixel service registered and ready!");
LOG_INFO(spore.getContext(), "Main", "NeoPixel service registered and ready!");
}
void loop() {

View File

@@ -1,8 +1,9 @@
#include "RelayService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
RelayService::RelayService(TaskManager& taskMgr, int pin)
: taskManager(taskMgr), relayPin(pin), relayOn(false) {
RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin)
: ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) {
pinMode(relayPin, OUTPUT);
// Many relay modules are active LOW. Start in OFF state (relay de-energized).
digitalWrite(relayPin, HIGH);
@@ -64,13 +65,13 @@ void RelayService::turnOn() {
relayOn = true;
// Active LOW relay
digitalWrite(relayPin, LOW);
Serial.println("[RelayService] Relay ON");
LOG_INFO(ctx, "RelayService", "Relay ON");
}
void RelayService::turnOff() {
relayOn = false;
digitalWrite(relayPin, HIGH);
Serial.println("[RelayService] Relay OFF");
LOG_INFO(ctx, "RelayService", "Relay OFF");
}
void RelayService::toggle() {
@@ -83,7 +84,6 @@ void RelayService::toggle() {
void RelayService::registerTasks() {
taskManager.registerTask("relay_status_print", 5000, [this]() {
Serial.printf("[RelayService] Status - pin: %d, state: %s\n",
relayPin, relayOn ? "ON" : "OFF");
LOG_INFO(ctx, "RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF"));
});
}

View File

@@ -1,11 +1,12 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/TaskManager.h"
#include "spore/core/NodeContext.h"
#include <ArduinoJson.h>
class RelayService : public Service {
public:
RelayService(TaskManager& taskMgr, int pin);
RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Relay"; }
@@ -16,6 +17,7 @@ public:
private:
void registerTasks();
NodeContext& ctx;
TaskManager& taskManager;
int relayPin;
bool relayOn;

View File

@@ -1,5 +1,6 @@
#include <Arduino.h>
#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.
@@ -22,13 +23,13 @@ void setup() {
spore.setup();
// Create and add custom service
relayService = new RelayService(spore.getTaskManager(), RELAY_PIN);
relayService = new RelayService(spore.getContext(), spore.getTaskManager(), RELAY_PIN);
spore.addService(relayService);
// Start the API server and complete initialization
spore.begin();
Serial.println("[Main] Relay service registered and ready!");
LOG_INFO(spore.getContext(), "Main", "Relay service registered and ready!");
}
void loop() {

View File

@@ -1,5 +1,6 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
// Create Spore instance
Spore spore;
@@ -11,8 +12,8 @@ void setup() {
// Start the framework (this will start the API server with static file serving)
spore.begin();
Serial.println("Static web server started!");
Serial.println("Visit http://<node-ip>/ to see the web interface");
LOG_INFO(spore.getContext(), "Example", "Static web server started!");
LOG_INFO(spore.getContext(), "Example", "Visit http://<node-ip>/ to see the web interface");
}
void loop() {

View File

@@ -0,0 +1,72 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/NodeContext.h"
#include "spore/core/ApiServer.h"
#include <Arduino.h>
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)

View File

@@ -9,7 +9,7 @@
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = esp01_1m
;default_envs = esp01_1m
src_dir = .
[common]

View File

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

View File

@@ -1,5 +1,6 @@
#include "spore/core/ApiServer.h"
#include "spore/Service.h"
#include "spore/services/LoggingService.h"
#include <algorithm>
const char* ApiServer::methodToStr(int method) {
@@ -77,14 +78,14 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
void ApiServer::addService(Service& service) {
services.push_back(service);
Serial.printf("[API] Added service: %s\n", service.getName());
LOG_INFO(ctx, "API", "Added service: " + String(service.getName()));
}
void ApiServer::begin() {
// Register all service endpoints
for (auto& service : services) {
service.get().registerEndpoints(*this);
Serial.printf("[API] Registered endpoints for service: %s\n", service.get().getName());
LOG_INFO(ctx, "API", "Registered endpoints for service: " + String(service.get().getName()));
}
server.begin();

View File

@@ -1,5 +1,6 @@
#include "spore/core/ClusterManager.h"
#include "spore/internal/Globals.h"
#include "spore/services/LoggingService.h"
ClusterManager::ClusterManager(NodeContext& ctx, TaskManager& taskMgr) : ctx(ctx), taskManager(taskMgr) {
// Register callback for node_discovered event
@@ -18,11 +19,11 @@ 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(); });
Serial.println("[ClusterManager] Registered all cluster tasks");
LOG_INFO(ctx, "ClusterManager", "Registered all cluster tasks");
}
void ClusterManager::sendDiscovery() {
//Serial.println("[Cluster] Sending discovery packet...");
//LOG_DEBUG(ctx, "Cluster", "Sending discovery packet...");
ctx.udp->beginPacket("255.255.255.255", ctx.config.udp_port);
ctx.udp->write(ClusterProtocol::DISCOVERY_MSG);
ctx.udp->endPacket();
@@ -36,14 +37,14 @@ void ClusterManager::listenForDiscovery() {
if (len > 0) {
incoming[len] = 0;
}
//Serial.printf("[UDP] Packet received: %s\n", incoming);
//LOG_DEBUG(ctx, "UDP", "Packet received: " + String(incoming));
if (strcmp(incoming, ClusterProtocol::DISCOVERY_MSG) == 0) {
//Serial.printf("[UDP] Discovery request from: %s\n", ctx.udp->remoteIP().toString().c_str());
//LOG_DEBUG(ctx, "UDP", "Discovery request from: " + ctx.udp->remoteIP().toString());
ctx.udp->beginPacket(ctx.udp->remoteIP(), ctx.config.udp_port);
String response = String(ClusterProtocol::RESPONSE_MSG) + ":" + ctx.hostname;
ctx.udp->write(response.c_str());
ctx.udp->endPacket();
//Serial.printf("[UDP] Sent response with hostname: %s\n", ctx.hostname.c_str());
//LOG_DEBUG(ctx, "UDP", "Sent response with hostname: " + ctx.hostname);
} else if (strncmp(incoming, ClusterProtocol::RESPONSE_MSG, strlen(ClusterProtocol::RESPONSE_MSG)) == 0) {
char* hostPtr = incoming + strlen(ClusterProtocol::RESPONSE_MSG) + 1;
String nodeHost = String(hostPtr);
@@ -72,16 +73,13 @@ 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;
Serial.printf("[Cluster] Added node: %s @ %s | Status: %s | last update: 0\n",
nodeHost.c_str(),
newNode.ip.toString().c_str(),
statusToStr(newNode.status));
LOG_INFO(ctx, "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) {
Serial.println("[Cluster] Skipping fetch for local node");
LOG_DEBUG(ctx, "Cluster", "Skipping fetch for local node");
return;
}
unsigned long requestStart = millis();
@@ -135,13 +133,13 @@ void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
node.labels[k] = v;
}
}
Serial.printf("[Cluster] Fetched info for node: %s @ %s\n", node.hostname.c_str(), ip.toString().c_str());
LOG_DEBUG(ctx, "Cluster", "Fetched info for node: " + node.hostname + " @ " + ip.toString());
break;
}
}
}
} else {
Serial.printf("[Cluster] Failed to fetch info for node @ %s, HTTP code: %d\n", ip.toString().c_str(), httpCode);
LOG_ERROR(ctx, "Cluster", "Failed to fetch info for node @ " + ip.toString() + ", HTTP code: " + String(httpCode));
}
http.end();
}
@@ -185,7 +183,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) {
Serial.printf("[Cluster] Removing node: %s\n", it->second.hostname.c_str());
LOG_INFO(ctx, "Cluster", "Removing node: " + it->second.hostname);
it = memberList.erase(it);
} else {
++it;
@@ -196,13 +194,13 @@ void ClusterManager::removeDeadNodes() {
void ClusterManager::printMemberList() {
auto& memberList = *ctx.memberList;
if (memberList.empty()) {
Serial.println("[Cluster] Member List: empty");
LOG_INFO(ctx, "Cluster", "Member List: empty");
return;
}
Serial.println("[Cluster] Member List:");
LOG_INFO(ctx, "Cluster", "Member List:");
for (const auto& pair : memberList) {
const NodeInfo& node = pair.second;
Serial.printf(" %s @ %s | Status: %s | last seen: %lu\n", node.hostname.c_str(), node.ip.toString().c_str(), statusToStr(node.status), millis() - node.lastSeen);
LOG_INFO(ctx, "Cluster", " " + node.hostname + " @ " + node.ip.toString() + " | Status: " + statusToStr(node.status) + " | last seen: " + String(millis() - node.lastSeen));
}
}

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
#include "spore/services/NodeService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
@@ -65,8 +66,8 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([]() {
Serial.println("[API] Restart device");
request->onDisconnect([this]() {
LOG_INFO(ctx, "API", "Restart device");
delay(10);
ESP.restart();
});
@@ -75,11 +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) {
Serial.print("[OTA] Update Start ");
Serial.println(filename);
LOG_INFO(ctx, "OTA", "Update Start " + String(filename));
Update.runAsync(true);
if(!Update.begin(request->contentLength(), U_FLASH)) {
Serial.println("[OTA] Update failed: not enough space");
LOG_ERROR(ctx, "OTA", "Update failed: not enough space");
Update.printError(Serial);
AsyncWebServerResponse* response = request->beginResponse(500, "application/json",
"{\"status\": \"FAIL\"}");
@@ -96,14 +96,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
if (final) {
if (Update.end(true)) {
if(Update.isFinished()) {
Serial.print("[OTA] Update Success with ");
Serial.print(index + len);
Serial.println("B");
LOG_INFO(ctx, "OTA", "Update Success with " + String(index + len) + "B");
} else {
Serial.println("[OTA] Update not finished");
LOG_WARN(ctx, "OTA", "Update not finished");
}
} else {
Serial.print("[OTA] Update failed: ");
LOG_ERROR(ctx, "OTA", "Update failed: " + String(Update.getError()));
Update.printError(Serial);
}
}
@@ -114,8 +112,8 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
"{\"status\": \"restarting\"}");
response->addHeader("Connection", "close");
request->send(response);
request->onDisconnect([]() {
Serial.println("[API] Restart device");
request->onDisconnect([this]() {
LOG_INFO(ctx, "API", "Restart device");
delay(10);
ESP.restart();
});

View File

@@ -1,4 +1,5 @@
#include "spore/services/StaticFileService.h"
#include "spore/services/LoggingService.h"
#include <Arduino.h>
const String StaticFileService::name = "StaticFileService";
@@ -10,10 +11,10 @@ StaticFileService::StaticFileService(NodeContext& ctx, ApiServer& apiServer)
void StaticFileService::registerEndpoints(ApiServer& api) {
// Initialize LittleFS
if (!LittleFS.begin()) {
Serial.println("[StaticFileService] LittleFS Mount Failed");
LOG_ERROR(ctx, "StaticFileService", "LittleFS Mount Failed");
return;
}
Serial.println("[StaticFileService] LittleFS mounted successfully");
LOG_INFO(ctx, "StaticFileService", "LittleFS mounted successfully");
// Root endpoint - serve index.html
api.addEndpoint("/", HTTP_GET,