feat: releay ui example, simplify logging

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

View File

@@ -1,165 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spore</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
h1 {
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.status-card {
background: rgba(255, 255, 255, 0.2);
border-radius: 15px;
padding: 20px;
text-align: center;
transition: transform 0.3s ease;
}
.status-card:hover {
transform: translateY(-5px);
}
.status-value {
font-size: 2em;
font-weight: bold;
margin: 10px 0;
}
.api-section {
margin-top: 30px;
}
.api-links {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
}
.api-link {
background: rgba(255, 255, 255, 0.2);
color: white;
text-decoration: none;
padding: 10px 20px;
border-radius: 25px;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.api-link:hover {
background: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
}
.loading {
text-align: center;
font-style: italic;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<h1>🍄 Spore Node</h1>
<div class="status-grid">
<div class="status-card">
<h3>Node Status</h3>
<div class="status-value" id="nodeStatus">Loading...</div>
</div>
<div class="status-card">
<h3>Network</h3>
<div class="status-value" id="networkStatus">Loading...</div>
</div>
<div class="status-card">
<h3>Tasks</h3>
<div class="status-value" id="taskStatus">Loading...</div>
</div>
<div class="status-card">
<h3>Cluster</h3>
<div class="status-value" id="clusterStatus">Loading...</div>
</div>
</div>
<div class="api-section">
<h2>API Endpoints</h2>
<div class="api-links">
<a href="/api/node/status" class="api-link">Node Status</a>
<a href="/api/network/status" class="api-link">Network Status</a>
<a href="/api/tasks/status" class="api-link">Tasks Status</a>
<a href="/api/cluster/members" class="api-link">Cluster Members</a>
<a href="/api/node/endpoints" class="api-link">All Endpoints</a>
</div>
</div>
</div>
<script>
// Load initial data
async function loadStatus() {
try {
const [nodeResponse, networkResponse, taskResponse, clusterResponse] = await Promise.all([
fetch('/api/node/status').then(r => r.json()).catch(() => null),
fetch('/api/network/status').then(r => r.json()).catch(() => null),
fetch('/api/tasks/status').then(r => r.json()).catch(() => null),
fetch('/api/cluster/members').then(r => r.json()).catch(() => null)
]);
// Update node status
if (nodeResponse) {
document.getElementById('nodeStatus').textContent =
nodeResponse.uptime ? `${Math.floor(nodeResponse.uptime / 1000)}s` : 'Online';
}
// Update network status
if (networkResponse) {
document.getElementById('networkStatus').textContent =
networkResponse.connected ? 'Connected' : 'Disconnected';
}
// Update task status
if (taskResponse) {
const activeTasks = taskResponse.tasks ?
taskResponse.tasks.filter(t => t.status === 'running').length : 0;
document.getElementById('taskStatus').textContent = `${activeTasks} active`;
}
// Update cluster status
if (clusterResponse) {
const memberCount = clusterResponse.members ?
Object.keys(clusterResponse.members).length : 0;
document.getElementById('clusterStatus').textContent = `${memberCount} nodes`;
}
} catch (error) {
console.error('Error loading status:', error);
}
}
// Load status on page load
loadStatus();
// Refresh status every 5 seconds
setInterval(loadStatus, 5000);
</script>
</body>
</html>

View File

@@ -1,305 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Relay Control - Spore</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
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: 90%;
}
h1 {
margin-bottom: 30px;
font-size: 2.2em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.relay-status {
margin-bottom: 30px;
}
.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);
}
.status-indicator.off {
background: rgba(255, 0, 0, 0.3);
color: #ff6b6b;
}
.status-indicator.on {
background: rgba(0, 255, 0, 0.3);
color: #51cf66;
box-shadow: 0 0 20px rgba(81, 207, 102, 0.5);
}
.status-text {
font-size: 1.5em;
margin-bottom: 10px;
}
.pin-info {
font-size: 0.9em;
opacity: 0.8;
}
.controls {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.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;
}
.btn-primary {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-success {
background: rgba(81, 207, 102, 0.8);
color: white;
border: 2px solid rgba(81, 207, 102, 1);
}
.btn-success:hover {
background: rgba(81, 207, 102, 1);
transform: translateY(-2px);
}
.btn-danger {
background: rgba(255, 107, 107, 0.8);
color: white;
border: 2px solid rgba(255, 107, 107, 1);
}
.btn-danger:hover {
background: rgba(255, 107, 107, 1);
transform: translateY(-2px);
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.error {
color: #ff6b6b;
margin-top: 15px;
font-size: 0.9em;
}
.success {
color: #51cf66;
margin-top: 15px;
font-size: 0.9em;
}
.uptime {
margin-top: 20px;
font-size: 0.8em;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<h1>🔌 Relay Control</h1>
<div class="relay-status">
<div class="status-indicator off" id="statusIndicator">
OFF
</div>
<div class="status-text" id="statusText">Relay is OFF</div>
<div class="pin-info" id="pinInfo">Pin: Loading...</div>
</div>
<div class="controls">
<button class="btn btn-success" id="turnOnBtn" onclick="controlRelay('on')">
Turn ON
</button>
<button class="btn btn-danger" id="turnOffBtn" onclick="controlRelay('off')">
Turn OFF
</button>
<button class="btn btn-primary" id="toggleBtn" onclick="controlRelay('toggle')">
Toggle
</button>
</div>
<div id="message"></div>
<div class="uptime" id="uptime"></div>
</div>
<script>
let currentState = 'off';
let relayPin = '';
// Load initial relay status
async function 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();
currentState = data.state;
relayPin = data.pin;
updateUI();
updateUptime(data.uptime);
} catch (error) {
console.error('Error loading relay status:', error);
showMessage('Error loading relay status: ' + error.message, 'error');
}
}
// Control relay
async function 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) {
currentState = data.state;
updateUI();
showMessage(`Relay turned ${data.state.toUpperCase()}`, 'success');
} else {
showMessage(data.message || 'Failed to control relay', 'error');
}
} catch (error) {
console.error('Error controlling relay:', error);
showMessage('Error controlling relay: ' + error.message, 'error');
} finally {
buttons.forEach(btn => btn.classList.remove('loading'));
}
}
// Update UI based on current state
function updateUI() {
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const pinInfo = document.getElementById('pinInfo');
if (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 (relayPin) {
pinInfo.textContent = `Pin: ${relayPin}`;
}
}
// Update uptime display
function 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
function 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);
}
// Load status on page load
loadRelayStatus();
// Refresh status every 2 seconds
setInterval(loadRelayStatus, 2000);
</script>
</body>
</html>

View File

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

@@ -1,5 +1,5 @@
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include <Arduino.h>
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();
}

View File

@@ -1,6 +1,6 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include "NeoPatternService.h"
#ifndef LED_STRIP_PIN

View File

@@ -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) {

View File

@@ -1,6 +1,6 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include "NeoPixelService.h"
#ifndef NEOPIXEL_PIN

View File

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

View File

@@ -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 = `
<h1>🔌 Relay Control</h1>
<div class="relay-status">
<div class="status-indicator off" id="statusIndicator">
OFF
</div>
<div class="status-text" id="statusText">Relay is OFF</div>
<div class="pin-info" id="pinInfo">Pin: Loading...</div>
</div>
<div class="controls">
<button class="btn btn-success" id="turnOnBtn">
Turn ON
</button>
<button class="btn btn-danger" id="turnOffBtn">
Turn OFF
</button>
<button class="btn btn-primary" id="toggleBtn">
Toggle
</button>
</div>
<div id="message"></div>
<div class="uptime" id="uptime"></div>
`;
// 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;
}

View File

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

View File

@@ -1,6 +1,5 @@
#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.
@@ -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://<node-ip>/relay.html");
LOG_INFO("Main", "Relay service registered and ready!");
LOG_INFO("Main", "Web interface available at http://<node-ip>/relay.html");
}
void loop() {

View File

@@ -1,6 +1,6 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
// Create Spore instance
Spore spore;

View File

@@ -10,6 +10,7 @@
#include "core/ApiServer.h"
#include "core/TaskManager.h"
#include "Service.h"
#include "util/Logging.h"
class Spore {
public:

View File

@@ -1,72 +0,0 @@
#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

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

View File

@@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
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)

View File

@@ -37,6 +37,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini]
@@ -56,6 +57,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:relay]
@@ -75,6 +77,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_relay]
@@ -95,6 +98,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:esp01_1m_neopixel]
@@ -114,6 +118,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_neopixel]
@@ -134,6 +139,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:esp01_1m_neopattern]
@@ -154,6 +160,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_neopattern]
@@ -174,6 +181,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_static_web]
@@ -193,4 +201,5 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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