feat: releay ui example, simplify logging
This commit is contained in:
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "spore/Spore.h"
|
#include "spore/Spore.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
Spore spore;
|
Spore spore;
|
||||||
@@ -12,22 +12,17 @@ void setup() {
|
|||||||
spore.begin();
|
spore.begin();
|
||||||
|
|
||||||
// Demonstrate different logging levels
|
// Demonstrate different logging levels
|
||||||
LOG_INFO(spore.ctx, "Example", "Logging example started");
|
LOG_INFO("Example", "Logging example started");
|
||||||
LOG_DEBUG(spore.ctx, "Example", "This is a debug message");
|
LOG_DEBUG("Example", "This is a debug message");
|
||||||
LOG_WARN(spore.ctx, "Example", "This is a warning message");
|
LOG_WARN("Example", "This is a warning message");
|
||||||
LOG_ERROR(spore.ctx, "Example", "This is an error message");
|
LOG_ERROR("Example", "This is an error message");
|
||||||
|
|
||||||
// Demonstrate logging with different components
|
// Demonstrate logging with different components
|
||||||
LOG_INFO(spore.ctx, "Network", "WiFi connection established");
|
LOG_INFO("Network", "WiFi connection established");
|
||||||
LOG_INFO(spore.ctx, "Cluster", "Node discovered: esp-123456");
|
LOG_INFO("Cluster", "Node discovered: esp-123456");
|
||||||
LOG_INFO(spore.ctx, "API", "Server started on port 80");
|
LOG_INFO("API", "Server started on port 80");
|
||||||
|
|
||||||
// Demonstrate event-based logging
|
LOG_INFO("Example", "All logging now uses the simplified system!");
|
||||||
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() {
|
void loop() {
|
||||||
@@ -37,7 +32,7 @@ void loop() {
|
|||||||
// Log some periodic information
|
// Log some periodic information
|
||||||
static unsigned long lastLog = 0;
|
static unsigned long lastLog = 0;
|
||||||
if (millis() - lastLog > 5000) {
|
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();
|
lastLog = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "spore/Spore.h"
|
#include "spore/Spore.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
#include "NeoPatternService.h"
|
#include "NeoPatternService.h"
|
||||||
|
|
||||||
#ifndef LED_STRIP_PIN
|
#ifndef LED_STRIP_PIN
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "NeoPixelService.h"
|
#include "NeoPixelService.h"
|
||||||
#include "spore/core/ApiServer.h"
|
#include "spore/core/ApiServer.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
|
|
||||||
// Wheel helper: map 0-255 to RGB rainbow
|
// Wheel helper: map 0-255 to RGB rainbow
|
||||||
static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) {
|
static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "spore/Spore.h"
|
#include "spore/Spore.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
#include "NeoPixelService.h"
|
#include "NeoPixelService.h"
|
||||||
|
|
||||||
#ifndef NEOPIXEL_PIN
|
#ifndef NEOPIXEL_PIN
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "RelayService.h"
|
#include "RelayService.h"
|
||||||
#include "spore/core/ApiServer.h"
|
#include "spore/core/ApiServer.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
|
|
||||||
RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin)
|
RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin)
|
||||||
: ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) {
|
: ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) {
|
||||||
@@ -65,13 +65,13 @@ void RelayService::turnOn() {
|
|||||||
relayOn = true;
|
relayOn = true;
|
||||||
// Active LOW relay
|
// Active LOW relay
|
||||||
digitalWrite(relayPin, LOW);
|
digitalWrite(relayPin, LOW);
|
||||||
LOG_INFO(ctx, "RelayService", "Relay ON");
|
LOG_INFO("RelayService", "Relay ON");
|
||||||
}
|
}
|
||||||
|
|
||||||
void RelayService::turnOff() {
|
void RelayService::turnOff() {
|
||||||
relayOn = false;
|
relayOn = false;
|
||||||
digitalWrite(relayPin, HIGH);
|
digitalWrite(relayPin, HIGH);
|
||||||
LOG_INFO(ctx, "RelayService", "Relay OFF");
|
LOG_INFO("RelayService", "Relay OFF");
|
||||||
}
|
}
|
||||||
|
|
||||||
void RelayService::toggle() {
|
void RelayService::toggle() {
|
||||||
@@ -84,6 +84,6 @@ void RelayService::toggle() {
|
|||||||
|
|
||||||
void RelayService::registerTasks() {
|
void RelayService::registerTasks() {
|
||||||
taskManager.registerTask("relay_status_print", 5000, [this]() {
|
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"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
177
examples/relay/data/public/script.js
Normal file
177
examples/relay/data/public/script.js
Normal 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;
|
||||||
|
}
|
||||||
141
examples/relay/data/public/styles.css
Normal file
141
examples/relay/data/public/styles.css
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "spore/Spore.h"
|
#include "spore/Spore.h"
|
||||||
#include "spore/services/LoggingService.h"
|
|
||||||
#include "RelayService.h"
|
#include "RelayService.h"
|
||||||
|
|
||||||
// Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board.
|
// 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
|
// Start the API server and complete initialization
|
||||||
spore.begin();
|
spore.begin();
|
||||||
|
|
||||||
LOG_INFO(spore.getContext(), "Main", "Relay service registered and ready!");
|
LOG_INFO("Main", "Relay service registered and ready!");
|
||||||
LOG_INFO(spore.getContext(), "Main", "Web interface available at http://<node-ip>/relay.html");
|
LOG_INFO("Main", "Web interface available at http://<node-ip>/relay.html");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "spore/Spore.h"
|
#include "spore/Spore.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
|
|
||||||
// Create Spore instance
|
// Create Spore instance
|
||||||
Spore spore;
|
Spore spore;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "core/ApiServer.h"
|
#include "core/ApiServer.h"
|
||||||
#include "core/TaskManager.h"
|
#include "core/TaskManager.h"
|
||||||
#include "Service.h"
|
#include "Service.h"
|
||||||
|
#include "util/Logging.h"
|
||||||
|
|
||||||
class Spore {
|
class Spore {
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -32,6 +32,11 @@ public:
|
|||||||
unsigned long restart_delay_ms;
|
unsigned long restart_delay_ms;
|
||||||
uint16_t json_doc_size;
|
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
|
// Constructor
|
||||||
Config();
|
Config();
|
||||||
};
|
};
|
||||||
29
include/spore/util/Logging.h
Normal file
29
include/spore/util/Logging.h
Normal 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)
|
||||||
@@ -37,6 +37,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:d1_mini]
|
[env:d1_mini]
|
||||||
@@ -56,6 +57,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:relay]
|
[env:relay]
|
||||||
@@ -75,6 +77,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:d1_mini_relay]
|
[env:d1_mini_relay]
|
||||||
@@ -95,6 +98,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:esp01_1m_neopixel]
|
[env:esp01_1m_neopixel]
|
||||||
@@ -114,6 +118,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:d1_mini_neopixel]
|
[env:d1_mini_neopixel]
|
||||||
@@ -134,6 +139,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:esp01_1m_neopattern]
|
[env:esp01_1m_neopattern]
|
||||||
@@ -154,6 +160,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:d1_mini_neopattern]
|
[env:d1_mini_neopattern]
|
||||||
@@ -174,6 +181,7 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:d1_mini_static_web]
|
[env:d1_mini_static_web]
|
||||||
@@ -193,4 +201,5 @@ build_src_filter =
|
|||||||
+<src/spore/core/*.cpp>
|
+<src/spore/core/*.cpp>
|
||||||
+<src/spore/services/*.cpp>
|
+<src/spore/services/*.cpp>
|
||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
#include "spore/services/ClusterService.h"
|
#include "spore/services/ClusterService.h"
|
||||||
#include "spore/services/TaskService.h"
|
#include "spore/services/TaskService.h"
|
||||||
#include "spore/services/StaticFileService.h"
|
#include "spore/services/StaticFileService.h"
|
||||||
#include "spore/services/LoggingService.h"
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
||||||
@@ -24,12 +23,12 @@ Spore::~Spore() {
|
|||||||
|
|
||||||
void Spore::setup() {
|
void Spore::setup() {
|
||||||
if (initialized) {
|
if (initialized) {
|
||||||
LOG_INFO(ctx, "Spore", "Already initialized, skipping setup");
|
LOG_INFO("Spore", "Already initialized, skipping setup");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
LOG_INFO(ctx, "Spore", "Starting Spore framework...");
|
LOG_INFO("Spore", "Starting Spore framework...");
|
||||||
|
|
||||||
// Initialize core components
|
// Initialize core components
|
||||||
initializeCore();
|
initializeCore();
|
||||||
@@ -38,21 +37,21 @@ void Spore::setup() {
|
|||||||
registerCoreServices();
|
registerCoreServices();
|
||||||
|
|
||||||
initialized = true;
|
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() {
|
void Spore::begin() {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first");
|
LOG_ERROR("Spore", "Framework not initialized, call setup() first");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (apiServerStarted) {
|
if (apiServerStarted) {
|
||||||
LOG_WARN(ctx, "Spore", "API server already started");
|
LOG_WARN("Spore", "API server already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO(ctx, "Spore", "Starting API server...");
|
LOG_INFO("Spore", "Starting API server...");
|
||||||
|
|
||||||
// Start API server
|
// Start API server
|
||||||
startApiServer();
|
startApiServer();
|
||||||
@@ -60,12 +59,12 @@ void Spore::begin() {
|
|||||||
// Print initial task status
|
// Print initial task status
|
||||||
taskManager.printTaskStatus();
|
taskManager.printTaskStatus();
|
||||||
|
|
||||||
LOG_INFO(ctx, "Spore", "Framework ready!");
|
LOG_INFO("Spore", "Framework ready!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Spore::loop() {
|
void Spore::loop() {
|
||||||
if (!initialized) {
|
if (!initialized) {
|
||||||
LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first");
|
LOG_ERROR("Spore", "Framework not initialized, call setup() first");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ void Spore::loop() {
|
|||||||
|
|
||||||
void Spore::addService(std::shared_ptr<Service> service) {
|
void Spore::addService(std::shared_ptr<Service> service) {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
LOG_WARN(ctx, "Spore", "Attempted to add null service");
|
LOG_WARN("Spore", "Attempted to add null service");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,15 +83,15 @@ void Spore::addService(std::shared_ptr<Service> service) {
|
|||||||
if (apiServerStarted) {
|
if (apiServerStarted) {
|
||||||
// If API server is already started, register the service immediately
|
// If API server is already started, register the service immediately
|
||||||
apiServer.addService(*service);
|
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 {
|
} 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) {
|
void Spore::addService(Service* service) {
|
||||||
if (!service) {
|
if (!service) {
|
||||||
LOG_WARN(ctx, "Spore", "Attempted to add null service");
|
LOG_WARN("Spore", "Attempted to add null service");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +101,7 @@ void Spore::addService(Service* service) {
|
|||||||
|
|
||||||
|
|
||||||
void Spore::initializeCore() {
|
void Spore::initializeCore() {
|
||||||
LOG_INFO(ctx, "Spore", "Initializing core components...");
|
LOG_INFO("Spore", "Initializing core components...");
|
||||||
|
|
||||||
// Setup WiFi first
|
// Setup WiFi first
|
||||||
network.setupWiFi();
|
network.setupWiFi();
|
||||||
@@ -110,14 +109,11 @@ void Spore::initializeCore() {
|
|||||||
// Initialize task manager
|
// Initialize task manager
|
||||||
taskManager.initialize();
|
taskManager.initialize();
|
||||||
|
|
||||||
LOG_INFO(ctx, "Spore", "Core components initialized");
|
LOG_INFO("Spore", "Core components initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Spore::registerCoreServices() {
|
void Spore::registerCoreServices() {
|
||||||
LOG_INFO(ctx, "Spore", "Registering core services...");
|
LOG_INFO("Spore", "Registering core services...");
|
||||||
|
|
||||||
// Create logging service first (other services may use it)
|
|
||||||
auto loggingService = std::make_shared<LoggingService>(ctx, apiServer);
|
|
||||||
|
|
||||||
// Create core services
|
// Create core services
|
||||||
auto nodeService = std::make_shared<NodeService>(ctx, apiServer);
|
auto nodeService = std::make_shared<NodeService>(ctx, apiServer);
|
||||||
@@ -127,29 +123,28 @@ void Spore::registerCoreServices() {
|
|||||||
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
|
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
|
||||||
|
|
||||||
// Add to services list
|
// Add to services list
|
||||||
services.push_back(loggingService);
|
|
||||||
services.push_back(nodeService);
|
services.push_back(nodeService);
|
||||||
services.push_back(networkService);
|
services.push_back(networkService);
|
||||||
services.push_back(clusterService);
|
services.push_back(clusterService);
|
||||||
services.push_back(taskService);
|
services.push_back(taskService);
|
||||||
services.push_back(staticFileService);
|
services.push_back(staticFileService);
|
||||||
|
|
||||||
LOG_INFO(ctx, "Spore", "Core services registered");
|
LOG_INFO("Spore", "Core services registered");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Spore::startApiServer() {
|
void Spore::startApiServer() {
|
||||||
if (apiServerStarted) {
|
if (apiServerStarted) {
|
||||||
LOG_WARN(ctx, "Spore", "API server already started");
|
LOG_WARN("Spore", "API server already started");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO(ctx, "Spore", "Starting API server...");
|
LOG_INFO("Spore", "Starting API server...");
|
||||||
|
|
||||||
// Register all services with API server
|
// Register all services with API server
|
||||||
for (auto& service : services) {
|
for (auto& service : services) {
|
||||||
if (service) {
|
if (service) {
|
||||||
apiServer.addService(*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();
|
apiServer.begin();
|
||||||
apiServerStarted = true;
|
apiServerStarted = true;
|
||||||
|
|
||||||
LOG_INFO(ctx, "Spore", "API server started");
|
LOG_INFO("Spore", "API server started");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "spore/core/ApiServer.h"
|
#include "spore/core/ApiServer.h"
|
||||||
#include "spore/Service.h"
|
#include "spore/Service.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
const char* ApiServer::methodToStr(int method) {
|
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) {
|
void ApiServer::addService(Service& service) {
|
||||||
services.push_back(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) {
|
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");
|
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() {
|
void ApiServer::begin() {
|
||||||
// Register all service endpoints
|
// Register all service endpoints
|
||||||
for (auto& service : services) {
|
for (auto& service : services) {
|
||||||
service.get().registerEndpoints(*this);
|
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();
|
server.begin();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "spore/core/ClusterManager.h"
|
#include "spore/core/ClusterManager.h"
|
||||||
#include "spore/internal/Globals.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) {
|
ClusterManager::ClusterManager(NodeContext& ctx, TaskManager& taskMgr) : ctx(ctx), taskManager(taskMgr) {
|
||||||
// Register callback for node_discovered event
|
// 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("print_members", ctx.config.print_interval_ms, [this]() { printMemberList(); });
|
||||||
taskManager.registerTask("heartbeat", ctx.config.heartbeat_interval_ms, [this]() { heartbeatTaskCallback(); });
|
taskManager.registerTask("heartbeat", ctx.config.heartbeat_interval_ms, [this]() { heartbeatTaskCallback(); });
|
||||||
taskManager.registerTask("update_members_info", ctx.config.member_info_update_interval_ms, [this]() { updateAllMembersInfoTaskCallback(); });
|
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() {
|
void ClusterManager::sendDiscovery() {
|
||||||
@@ -73,33 +73,52 @@ void ClusterManager::addOrUpdateNode(const String& nodeHost, IPAddress nodeIP) {
|
|||||||
newNode.lastSeen = millis();
|
newNode.lastSeen = millis();
|
||||||
updateNodeStatus(newNode, newNode.lastSeen, ctx.config.node_inactive_threshold_ms, ctx.config.node_dead_threshold_ms);
|
updateNodeStatus(newNode, newNode.lastSeen, ctx.config.node_inactive_threshold_ms, ctx.config.node_dead_threshold_ms);
|
||||||
memberList[nodeHost] = newNode;
|
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
|
//fetchNodeInfo(nodeIP); // Do not fetch here, handled by periodic task
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
|
void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
|
||||||
if(ip == ctx.localIP) {
|
if(ip == ctx.localIP) {
|
||||||
LOG_DEBUG(ctx, "Cluster", "Skipping fetch for local node");
|
LOG_DEBUG("Cluster", "Skipping fetch for local node");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unsigned long requestStart = millis();
|
unsigned long requestStart = millis();
|
||||||
HTTPClient http;
|
HTTPClient http;
|
||||||
WiFiClient client;
|
WiFiClient client;
|
||||||
String url = "http://" + ip.toString() + ClusterProtocol::API_NODE_STATUS;
|
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();
|
int httpCode = http.GET();
|
||||||
unsigned long requestEnd = millis();
|
unsigned long requestEnd = millis();
|
||||||
unsigned long requestDuration = requestEnd - requestStart;
|
unsigned long requestDuration = requestEnd - requestStart;
|
||||||
|
|
||||||
if (httpCode == 200) {
|
if (httpCode == 200) {
|
||||||
String payload = http.getString();
|
String payload = http.getString();
|
||||||
|
|
||||||
|
// Use stack-allocated JsonDocument with proper cleanup
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
DeserializationError err = deserializeJson(doc, payload);
|
DeserializationError err = deserializeJson(doc, payload);
|
||||||
|
|
||||||
if (!err) {
|
if (!err) {
|
||||||
auto& memberList = *ctx.memberList;
|
auto& memberList = *ctx.memberList;
|
||||||
// Still need to iterate since we're searching by IP, not hostname
|
// Still need to iterate since we're searching by IP, not hostname
|
||||||
for (auto& pair : memberList) {
|
for (auto& pair : memberList) {
|
||||||
NodeInfo& node = pair.second;
|
NodeInfo& node = pair.second;
|
||||||
if (node.ip == ip) {
|
if (node.ip == ip) {
|
||||||
|
// Update resources efficiently
|
||||||
node.resources.freeHeap = doc["freeHeap"];
|
node.resources.freeHeap = doc["freeHeap"];
|
||||||
node.resources.chipId = doc["chipId"];
|
node.resources.chipId = doc["chipId"];
|
||||||
node.resources.sdkVersion = (const char*)doc["sdkVersion"];
|
node.resources.sdkVersion = (const char*)doc["sdkVersion"];
|
||||||
@@ -108,42 +127,63 @@ void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
|
|||||||
node.status = NodeInfo::ACTIVE;
|
node.status = NodeInfo::ACTIVE;
|
||||||
node.latency = requestDuration;
|
node.latency = requestDuration;
|
||||||
node.lastSeen = millis();
|
node.lastSeen = millis();
|
||||||
|
|
||||||
|
// Clear and rebuild endpoints efficiently
|
||||||
node.endpoints.clear();
|
node.endpoints.clear();
|
||||||
|
node.endpoints.reserve(10); // Pre-allocate to avoid reallocations
|
||||||
|
|
||||||
if (doc["api"].is<JsonArray>()) {
|
if (doc["api"].is<JsonArray>()) {
|
||||||
JsonArray apiArr = doc["api"].as<JsonArray>();
|
JsonArray apiArr = doc["api"].as<JsonArray>();
|
||||||
for (JsonObject apiObj : apiArr) {
|
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"];
|
int method = apiObj["method"];
|
||||||
|
|
||||||
// Create basic EndpointInfo without params for cluster nodes
|
// Create basic EndpointInfo without params for cluster nodes
|
||||||
EndpointInfo endpoint;
|
EndpointInfo endpoint;
|
||||||
endpoint.uri = uri;
|
endpoint.uri = uri; // String assignment is more efficient than construction
|
||||||
endpoint.method = method;
|
endpoint.method = method;
|
||||||
endpoint.isLocal = false;
|
endpoint.isLocal = false;
|
||||||
endpoint.serviceName = "remote";
|
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();
|
node.labels.clear();
|
||||||
if (doc["labels"].is<JsonObject>()) {
|
if (doc["labels"].is<JsonObject>()) {
|
||||||
JsonObject labelsObj = doc["labels"].as<JsonObject>();
|
JsonObject labelsObj = doc["labels"].as<JsonObject>();
|
||||||
for (JsonPair kvp : labelsObj) {
|
for (JsonPair kvp : labelsObj) {
|
||||||
String k = String(kvp.key().c_str());
|
// Use const char* to avoid String copies
|
||||||
String v = String(labelsObj[kvp.key()]);
|
const char* key = kvp.key().c_str();
|
||||||
node.labels[k] = v;
|
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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Cluster", "JSON parse error for node @ " + ip.toString() + ": " + String(err.c_str()));
|
||||||
}
|
}
|
||||||
} else {
|
} 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();
|
http.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Log success/failure for debugging
|
||||||
|
if (!success) {
|
||||||
|
LOG_DEBUG("Cluster", "Failed to update node info for " + ip.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ClusterManager::heartbeatTaskCallback() {
|
void ClusterManager::heartbeatTaskCallback() {
|
||||||
auto& memberList = *ctx.memberList;
|
auto& memberList = *ctx.memberList;
|
||||||
auto it = memberList.find(ctx.hostname);
|
auto it = memberList.find(ctx.hostname);
|
||||||
@@ -158,10 +198,25 @@ void ClusterManager::heartbeatTaskCallback() {
|
|||||||
|
|
||||||
void ClusterManager::updateAllMembersInfoTaskCallback() {
|
void ClusterManager::updateAllMembersInfoTaskCallback() {
|
||||||
auto& memberList = *ctx.memberList;
|
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) {
|
for (auto& pair : memberList) {
|
||||||
const NodeInfo& node = pair.second;
|
const NodeInfo& node = pair.second;
|
||||||
if (node.ip != ctx.localIP) {
|
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);
|
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(); ) {
|
for (auto it = memberList.begin(); it != memberList.end(); ) {
|
||||||
unsigned long diff = now - it->second.lastSeen;
|
unsigned long diff = now - it->second.lastSeen;
|
||||||
if (it->second.status == NodeInfo::DEAD && diff > ctx.config.node_dead_threshold_ms) {
|
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);
|
it = memberList.erase(it);
|
||||||
} else {
|
} else {
|
||||||
++it;
|
++it;
|
||||||
@@ -194,13 +249,13 @@ void ClusterManager::removeDeadNodes() {
|
|||||||
void ClusterManager::printMemberList() {
|
void ClusterManager::printMemberList() {
|
||||||
auto& memberList = *ctx.memberList;
|
auto& memberList = *ctx.memberList;
|
||||||
if (memberList.empty()) {
|
if (memberList.empty()) {
|
||||||
LOG_INFO(ctx, "Cluster", "Member List: empty");
|
LOG_INFO("Cluster", "Member List: empty");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG_INFO(ctx, "Cluster", "Member List:");
|
LOG_INFO("Cluster", "Member List:");
|
||||||
for (const auto& pair : memberList) {
|
for (const auto& pair : memberList) {
|
||||||
const NodeInfo& node = pair.second;
|
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);
|
auto it = memberList.find(ctx.hostname);
|
||||||
if (it != memberList.end()) {
|
if (it != memberList.end()) {
|
||||||
NodeInfo& node = it->second;
|
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.chipId = ESP.getChipId();
|
||||||
node.resources.sdkVersion = String(ESP.getSdkVersion());
|
node.resources.sdkVersion = String(ESP.getSdkVersion());
|
||||||
node.resources.cpuFreqMHz = ESP.getCpuFreqMHz();
|
node.resources.cpuFreqMHz = ESP.getCpuFreqMHz();
|
||||||
node.resources.flashChipSize = ESP.getFlashChipSize();
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
#include "spore/core/NetworkManager.h"
|
#include "spore/core/NetworkManager.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
|
|
||||||
// SSID and password are now configured via Config class
|
// SSID and password are now configured via Config class
|
||||||
|
|
||||||
void NetworkManager::scanWifi() {
|
void NetworkManager::scanWifi() {
|
||||||
if (!isScanning) {
|
if (!isScanning) {
|
||||||
isScanning = true;
|
isScanning = true;
|
||||||
LOG_INFO(ctx, "WiFi", "Starting WiFi scan...");
|
LOG_INFO("WiFi", "Starting WiFi scan...");
|
||||||
// Start async WiFi scan
|
// Start async WiFi scan
|
||||||
WiFi.scanNetworksAsync([this](int networksFound) {
|
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->processAccessPoints();
|
||||||
this->isScanning = false;
|
this->isScanning = false;
|
||||||
}, true);
|
}, true);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN(ctx, "WiFi", "Scan already in progress...");
|
LOG_WARN("WiFi", "Scan already in progress...");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkManager::processAccessPoints() {
|
void NetworkManager::processAccessPoints() {
|
||||||
int numNetworks = WiFi.scanComplete();
|
int numNetworks = WiFi.scanComplete();
|
||||||
if (numNetworks <= 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ void NetworkManager::processAccessPoints() {
|
|||||||
|
|
||||||
accessPoints.push_back(ap);
|
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
|
// Free the memory used by the scan
|
||||||
@@ -77,26 +77,26 @@ void NetworkManager::setHostnameFromMac() {
|
|||||||
void NetworkManager::setupWiFi() {
|
void NetworkManager::setupWiFi() {
|
||||||
WiFi.mode(WIFI_STA);
|
WiFi.mode(WIFI_STA);
|
||||||
WiFi.begin(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str());
|
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();
|
unsigned long startAttemptTime = millis();
|
||||||
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < ctx.config.wifi_connect_timeout_ms) {
|
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < ctx.config.wifi_connect_timeout_ms) {
|
||||||
delay(ctx.config.wifi_retry_delay_ms);
|
delay(ctx.config.wifi_retry_delay_ms);
|
||||||
// Progress dots handled by delay, no logging needed
|
// Progress dots handled by delay, no logging needed
|
||||||
}
|
}
|
||||||
if (WiFi.status() == WL_CONNECTED) {
|
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 {
|
} 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.mode(WIFI_AP);
|
||||||
WiFi.softAP(ctx.config.wifi_ssid.c_str(), ctx.config.wifi_password.c_str());
|
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();
|
setHostnameFromMac();
|
||||||
ctx.udp->begin(ctx.config.udp_port);
|
ctx.udp->begin(ctx.config.udp_port);
|
||||||
ctx.localIP = WiFi.localIP();
|
ctx.localIP = WiFi.localIP();
|
||||||
ctx.hostname = WiFi.hostname();
|
ctx.hostname = WiFi.hostname();
|
||||||
LOG_INFO(ctx, "WiFi", "Hostname set to: " + ctx.hostname);
|
LOG_INFO("WiFi", "Hostname set to: " + ctx.hostname);
|
||||||
LOG_INFO(ctx, "WiFi", "UDP listening on port " + String(ctx.config.udp_port));
|
LOG_INFO("WiFi", "UDP listening on port " + String(ctx.config.udp_port));
|
||||||
|
|
||||||
// Populate self NodeInfo
|
// Populate self NodeInfo
|
||||||
ctx.self.hostname = ctx.hostname;
|
ctx.self.hostname = ctx.hostname;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "spore/core/TaskManager.h"
|
#include "spore/core/TaskManager.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
TaskManager::TaskManager(NodeContext& ctx) : ctx(ctx) {}
|
TaskManager::TaskManager(NodeContext& ctx) : ctx(ctx) {}
|
||||||
@@ -32,9 +32,9 @@ void TaskManager::enableTask(const std::string& name) {
|
|||||||
int idx = findTaskIndex(name);
|
int idx = findTaskIndex(name);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
taskDefinitions[idx].enabled = true;
|
taskDefinitions[idx].enabled = true;
|
||||||
LOG_INFO(ctx, "TaskManager", "Enabled task: " + String(name.c_str()));
|
LOG_INFO("TaskManager", "Enabled task: " + String(name.c_str()));
|
||||||
} else {
|
} 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);
|
int idx = findTaskIndex(name);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
taskDefinitions[idx].enabled = false;
|
taskDefinitions[idx].enabled = false;
|
||||||
LOG_INFO(ctx, "TaskManager", "Disabled task: " + String(name.c_str()));
|
LOG_INFO("TaskManager", "Disabled task: " + String(name.c_str()));
|
||||||
} else {
|
} 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);
|
int idx = findTaskIndex(name);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
taskDefinitions[idx].interval = interval;
|
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 {
|
} 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) {
|
for (auto& taskDef : taskDefinitions) {
|
||||||
taskDef.enabled = true;
|
taskDef.enabled = true;
|
||||||
}
|
}
|
||||||
LOG_INFO(ctx, "TaskManager", "Enabled all tasks");
|
LOG_INFO("TaskManager", "Enabled all tasks");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskManager::disableAllTasks() {
|
void TaskManager::disableAllTasks() {
|
||||||
for (auto& taskDef : taskDefinitions) {
|
for (auto& taskDef : taskDefinitions) {
|
||||||
taskDef.enabled = false;
|
taskDef.enabled = false;
|
||||||
}
|
}
|
||||||
LOG_INFO(ctx, "TaskManager", "Disabled all tasks");
|
LOG_INFO("TaskManager", "Disabled all tasks");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskManager::printTaskStatus() const {
|
void TaskManager::printTaskStatus() const {
|
||||||
LOG_INFO(ctx, "TaskManager", "\nTask Status:");
|
LOG_INFO("TaskManager", "\nTask Status:");
|
||||||
LOG_INFO(ctx, "TaskManager", "==========================");
|
LOG_INFO("TaskManager", "==========================");
|
||||||
|
|
||||||
for (const auto& taskDef : taskDefinitions) {
|
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() {
|
void TaskManager::execute() {
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#include "spore/services/NodeService.h"
|
#include "spore/services/NodeService.h"
|
||||||
#include "spore/core/ApiServer.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) {}
|
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
|
|||||||
response->addHeader("Connection", "close");
|
response->addHeader("Connection", "close");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
request->onDisconnect([this]() {
|
request->onDisconnect([this]() {
|
||||||
LOG_INFO(ctx, "API", "Restart device");
|
LOG_INFO("API", "Restart device");
|
||||||
delay(10);
|
delay(10);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
});
|
});
|
||||||
@@ -76,10 +76,10 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
|
|||||||
void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename,
|
void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename,
|
||||||
size_t index, uint8_t* data, size_t len, bool final) {
|
size_t index, uint8_t* data, size_t len, bool final) {
|
||||||
if (!index) {
|
if (!index) {
|
||||||
LOG_INFO(ctx, "OTA", "Update Start " + String(filename));
|
LOG_INFO("OTA", "Update Start " + String(filename));
|
||||||
Update.runAsync(true);
|
Update.runAsync(true);
|
||||||
if(!Update.begin(request->contentLength(), U_FLASH)) {
|
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);
|
Update.printError(Serial);
|
||||||
AsyncWebServerResponse* response = request->beginResponse(500, "application/json",
|
AsyncWebServerResponse* response = request->beginResponse(500, "application/json",
|
||||||
"{\"status\": \"FAIL\"}");
|
"{\"status\": \"FAIL\"}");
|
||||||
@@ -96,12 +96,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
|
|||||||
if (final) {
|
if (final) {
|
||||||
if (Update.end(true)) {
|
if (Update.end(true)) {
|
||||||
if(Update.isFinished()) {
|
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 {
|
} else {
|
||||||
LOG_WARN(ctx, "OTA", "Update not finished");
|
LOG_WARN("OTA", "Update not finished");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR(ctx, "OTA", "Update failed: " + String(Update.getError()));
|
LOG_ERROR("OTA", "Update failed: " + String(Update.getError()));
|
||||||
Update.printError(Serial);
|
Update.printError(Serial);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +113,7 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
|
|||||||
response->addHeader("Connection", "close");
|
response->addHeader("Connection", "close");
|
||||||
request->send(response);
|
request->send(response);
|
||||||
request->onDisconnect([this]() {
|
request->onDisconnect([this]() {
|
||||||
LOG_INFO(ctx, "API", "Restart device");
|
LOG_INFO("API", "Restart device");
|
||||||
delay(10);
|
delay(10);
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#include "spore/services/StaticFileService.h"
|
#include "spore/services/StaticFileService.h"
|
||||||
#include "spore/services/LoggingService.h"
|
#include "spore/util/Logging.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
const String StaticFileService::name = "StaticFileService";
|
const String StaticFileService::name = "StaticFileService";
|
||||||
@@ -11,10 +11,10 @@ StaticFileService::StaticFileService(NodeContext& ctx, ApiServer& apiServer)
|
|||||||
void StaticFileService::registerEndpoints(ApiServer& api) {
|
void StaticFileService::registerEndpoints(ApiServer& api) {
|
||||||
// Initialize LittleFS
|
// Initialize LittleFS
|
||||||
if (!LittleFS.begin()) {
|
if (!LittleFS.begin()) {
|
||||||
LOG_ERROR(ctx, "StaticFileService", "LittleFS Mount Failed");
|
LOG_ERROR("StaticFileService", "LittleFS Mount Failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
LOG_INFO(ctx, "StaticFileService", "LittleFS mounted successfully");
|
LOG_INFO("StaticFileService", "LittleFS mounted successfully");
|
||||||
|
|
||||||
// Use the built-in static file serving from ESPAsyncWebServer
|
// Use the built-in static file serving from ESPAsyncWebServer
|
||||||
api.serveStatic("/", LittleFS, "/public", "max-age=3600");
|
api.serveStatic("/", LittleFS, "/public", "max-age=3600");
|
||||||
|
|||||||
@@ -28,4 +28,9 @@ Config::Config() {
|
|||||||
// System Configuration
|
// System Configuration
|
||||||
restart_delay_ms = 10;
|
restart_delay_ms = 10;
|
||||||
json_doc_size = 1024;
|
json_doc_size = 1024;
|
||||||
|
|
||||||
|
// Memory Management
|
||||||
|
low_memory_threshold_bytes = 100000; // 10KB
|
||||||
|
critical_memory_threshold_bytes = 5000; // 5KB
|
||||||
|
max_concurrent_http_requests = 3;
|
||||||
}
|
}
|
||||||
47
src/spore/util/Logging.cpp
Normal file
47
src/spore/util/Logging.cpp
Normal 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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user