Compare commits

...

13 Commits

Author SHA1 Message Date
025ac7b810 fix: endpoints result 2025-09-23 21:29:18 +02:00
a9f56c1279 fix: tasks endpoint response 2025-09-23 21:03:13 +02:00
594b5e3af6 feat: implement complete JSON serialization system with response classes
- Add abstract JsonSerializable base class with toJson/fromJson methods
- Create comprehensive response classes for complete JSON document handling:
  * ClusterMembersResponse for cluster member data
  * TaskStatusResponse and TaskControlResponse for task operations
  * NodeStatusResponse, NodeEndpointsResponse, NodeOperationResponse for node data
- Implement concrete serializable classes for all data types:
  * NodeInfoSerializable, TaskInfoSerializable, SystemInfoSerializable
  * TaskSummarySerializable, EndpointInfoSerializable
- Refactor all service classes to use new serialization system
- Reduce service method complexity from 20-30 lines to 2-3 lines
- Eliminate manual JsonDocument creation and field mapping
- Ensure type safety and compile-time validation
- Maintain backward compatibility while improving maintainability

Breaking change: Service classes now use response objects instead of manual JSON creation
2025-09-22 21:55:25 +02:00
c11652c123 feat: optimize neopattern example 2025-09-20 22:05:52 +02:00
51d4d4bc94 feat: remove hostname from labels 2025-09-20 15:04:11 +02:00
e95eb09a11 feat: introduce new param type numberRange 2025-09-19 21:49:59 +02:00
4727405be1 feat: rewrite NeoPattern example 2025-09-19 21:02:26 +02:00
93f09c3bb4 feat: unified monitoring service 2025-09-16 20:09:48 +02:00
83d87159fc Merge pull request 'feature/relay-example-ui' (#6) from feature/relay-example-ui into main
Reviewed-on: #6
2025-09-16 18:14:58 +02:00
f7f5918509 fix: builds 2025-09-16 18:14:41 +02:00
95a7e3167e fix: revert low mem treshold 2025-09-16 17:06:14 +02:00
702eec5a13 feat: releay ui example, simplify logging 2025-09-16 15:32:49 +02:00
2d85f560bb feat: serve static files, relay example 2025-09-16 12:12:27 +02:00
57 changed files with 3618 additions and 2129 deletions

View File

@@ -1,238 +0,0 @@
# LoggingService - Centralized Event-Based Logging
The LoggingService provides a centralized, event-driven logging system for the SPORE framework. It allows components to log messages through the event system, enabling flexible log routing and filtering.
## Features
- **Event-Driven Architecture**: Uses the SPORE event system for decoupled logging
- **Multiple Log Levels**: DEBUG, INFO, WARN, ERROR with configurable filtering
- **Multiple Destinations**: Serial output (extensible)
- **Component-Based Logging**: Tag messages with component names
- **API Configuration**: Configure logging via HTTP API endpoints
- **Easy Integration**: Simple macros and functions for logging
## Usage
### Basic Logging
```cpp
#include "spore/services/LoggingService.h"
// Using convenience macros (requires NodeContext)
LOG_DEBUG(ctx, "ComponentName", "Debug message");
LOG_INFO(ctx, "ComponentName", "Info message");
LOG_WARN(ctx, "ComponentName", "Warning message");
LOG_ERROR(ctx, "ComponentName", "Error message");
// Using global functions
logDebug(ctx, "ComponentName", "Debug message");
logInfo(ctx, "ComponentName", "Info message");
logWarn(ctx, "ComponentName", "Warning message");
logError(ctx, "ComponentName", "Error message");
```
### Event System Integration
The logging system uses the following events:
- `log/debug` - Debug level messages
- `log/info` - Info level messages
- `log/warn` - Warning level messages
- `log/error` - Error level messages
- `log/serial` - Messages routed to serial output
You can subscribe to these events for custom log handling:
```cpp
// Subscribe to all debug messages
ctx.on("log/debug", [](void* data) {
LogData* logData = static_cast<LogData*>(data);
// Custom handling for debug messages
delete logData;
});
// Subscribe to serial log output
ctx.on("log/serial", [](void* data) {
LogData* logData = static_cast<LogData*>(data);
// Custom serial formatting
Serial.println("CUSTOM: " + logData->message);
delete logData;
});
```
### LoggingService Class
The LoggingService class provides programmatic control over logging:
```cpp
LoggingService logger(ctx, apiServer);
// Configure logging
logger.setLogLevel(LogLevel::DEBUG);
logger.enableSerialLogging(true);
// Direct logging
logger.debug("MyComponent", "Debug message");
logger.info("MyComponent", "Info message");
logger.warn("MyComponent", "Warning message");
logger.error("MyComponent", "Error message");
```
## API Endpoints
The LoggingService provides HTTP API endpoints for configuration with proper parameter validation:
### Get Log Level
```
GET /api/logging/level
```
Returns current log level as JSON.
**Response:**
```json
{
"level": "INFO"
}
```
### Set Log Level
```
POST /api/logging/level
Content-Type: application/x-www-form-urlencoded
level=DEBUG
```
Sets the log level with parameter validation.
**Parameters:**
- `level` (required): One of `DEBUG`, `INFO`, `WARN`, `ERROR`
**Success Response:**
```json
{
"success": true,
"level": "DEBUG"
}
```
**Error Response:**
```json
{
"error": "Invalid log level. Must be one of: DEBUG, INFO, WARN, ERROR"
}
```
### Get Configuration
```
GET /api/logging/config
```
Returns complete logging configuration.
**Response:**
```json
{
"level": "INFO",
"serialEnabled": true
}
```
## Log Levels
- **DEBUG**: Detailed information for debugging
- **INFO**: General information about system operation
- **WARN**: Warning messages for potentially harmful situations
- **ERROR**: Error messages for serious problems
Only messages at or above the current log level are processed.
## Log Format
Log messages are formatted as:
```
[timestamp] [level] [component] message
```
Example:
```
[12345] [INFO] [Spore] Framework setup complete
[12350] [DEBUG] [Network] WiFi connection established
[12355] [WARN] [Cluster] Node timeout detected
[12360] [ERROR] [API] Failed to start server
```
## Integration with SPORE
The LoggingService is automatically registered as a core service in the SPORE framework. It's initialized before other services to ensure logging is available throughout the system.
## Example
See `examples/logging_example/main.cpp` for a complete example demonstrating the logging system.
## Extending the Logging System
### Adding New Log Destinations
To add new log destinations (e.g., network logging, database logging):
1. Subscribe to the appropriate log events:
```cpp
ctx.on("log/info", [](void* data) {
LogData* logData = static_cast<LogData*>(data);
// Send to your custom destination
sendToNetwork(logData->message);
delete logData;
});
```
2. Or create a custom LoggingService subclass:
```cpp
class CustomLoggingService : public LoggingService {
public:
CustomLoggingService(NodeContext& ctx, ApiServer& apiServer)
: LoggingService(ctx, apiServer) {
// Register custom event handlers
ctx.on("log/custom", [this](void* data) {
this->handleCustomLog(data);
});
}
private:
void handleCustomLog(void* data) {
// Custom log handling
}
};
```
### Custom Log Formatting
Override the `formatLogMessage` method in a custom LoggingService to change log formatting:
```cpp
String CustomLoggingService::formatLogMessage(const LogData& logData) {
// Custom formatting
return "CUSTOM[" + logData.component + "] " + logData.message;
}
```
## Performance Considerations
- Log messages are processed asynchronously through the event system
- Memory is allocated for each log message (LogData struct)
- Consider log level filtering to reduce overhead in production
## Troubleshooting
### Logs Not Appearing
1. Check that LoggingService is registered
2. Verify log level is set appropriately
3. Ensure serial logging is enabled
4. Check Serial.begin() is called before logging
### Memory Issues
1. Reduce log frequency
2. Use higher log levels to filter messages
### Custom Event Handlers Not Working
1. Ensure event handlers are registered before logging starts
2. Check that LogData is properly deleted in custom handlers
3. Verify event names match exactly

View File

@@ -1,45 +0,0 @@
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include <Arduino.h>
Spore spore;
void setup() {
// Initialize Spore framework
spore.setup();
// Start the framework
spore.begin();
// Demonstrate different logging levels
LOG_INFO(spore.ctx, "Example", "Logging example started");
LOG_DEBUG(spore.ctx, "Example", "This is a debug message");
LOG_WARN(spore.ctx, "Example", "This is a warning message");
LOG_ERROR(spore.ctx, "Example", "This is an error message");
// Demonstrate logging with different components
LOG_INFO(spore.ctx, "Network", "WiFi connection established");
LOG_INFO(spore.ctx, "Cluster", "Node discovered: esp-123456");
LOG_INFO(spore.ctx, "API", "Server started on port 80");
// Demonstrate event-based logging
LogData* logData = new LogData(LogLevel::INFO, "Example", "Event-based logging demonstration");
spore.ctx.fire("log/serial", logData);
// Show that all logging now goes through the centralized system
LOG_INFO(spore.ctx, "Example", "All Serial.println calls have been replaced with event-based logging!");
}
void loop() {
// Run the Spore framework
spore.loop();
// Log some periodic information
static unsigned long lastLog = 0;
if (millis() - lastLog > 5000) {
LOG_DEBUG(spore.ctx, "Example", "System running - Free heap: " + String(ESP.getFreeHeap()));
lastLog = millis();
}
delay(100);
}

View File

@@ -1,470 +1,376 @@
/**
* Original NeoPattern code by Bill Earl
* https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview
*
* TODO
* - cleanup the mess
* - fnc table for patterns to replace switch case
*
* Custom modifications by 0x1d:
* - default OnComplete callback that sets pattern to reverse
* - separate animation update from timer; Update now updates directly, UpdateScheduled uses timer
*/
#ifndef __NeoPattern_INCLUDED__
#define __NeoPattern_INCLUDED__
#include "NeoPattern.h"
#include <Adafruit_NeoPixel.h>
using namespace std;
// Pattern types supported:
enum pattern
// Constructor - calls base-class constructor to initialize strip
NeoPattern::NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int))
: Adafruit_NeoPixel(pixels, pin, type)
{
NONE = 0,
RAINBOW_CYCLE = 1,
THEATER_CHASE = 2,
COLOR_WIPE = 3,
SCANNER = 4,
FADE = 5,
FIRE = 6
};
// Patern directions supported:
enum direction
frameBuffer = (uint8_t *)malloc(768);
OnComplete = callback;
TotalSteps = numPixels();
begin();
}
NeoPattern::NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
: Adafruit_NeoPixel(pixels, pin, type)
{
FORWARD,
REVERSE
};
frameBuffer = (uint8_t *)malloc(768);
TotalSteps = numPixels();
begin();
}
// NeoPattern Class - derived from the Adafruit_NeoPixel class
class NeoPattern : public Adafruit_NeoPixel
NeoPattern::~NeoPattern() {
if (frameBuffer) {
free(frameBuffer);
}
}
void NeoPattern::handleStream(uint8_t *data, size_t len)
{
public:
// Member Variables:
pattern ActivePattern = RAINBOW_CYCLE; // which pattern is running
direction Direction = FORWARD; // direction to run the pattern
//const uint16_t *data16 = (uint16_t *)data;
bufferSize = len;
memcpy(frameBuffer, data, len);
}
unsigned long Interval = 150; // milliseconds between updates
unsigned long lastUpdate = 0; // last update of position
uint32_t Color1 = 0;
uint32_t Color2 = 0; // What colors are in use
uint16_t TotalSteps = 32; // total number of steps in the pattern
uint16_t Index; // current step within the pattern
uint16_t completed = 0;
// FIXME return current NeoPatternState
void (*OnComplete)(int); // Callback on completion of pattern
uint8_t *frameBuffer;
int bufferSize = 0;
// Constructor - calls base-class constructor to initialize strip
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int))
: Adafruit_NeoPixel(pixels, pin, type)
void NeoPattern::drawFrameBuffer(int w, uint8_t *frame, int length)
{
for (int i = 0; i < length; i++)
{
frameBuffer = (uint8_t *)malloc(768);
OnComplete = callback;
TotalSteps = numPixels();
begin();
uint8_t r = frame[i];
uint8_t g = frame[i + 1];
uint8_t b = frame[i + 2];
setPixelColor(i, r, g, b);
}
}
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
: Adafruit_NeoPixel(pixels, pin, type)
void NeoPattern::onCompleteDefault(int pixels)
{
//Serial.println("onCompleteDefault");
// FIXME no specific code
if (ActivePattern == THEATER_CHASE)
{
frameBuffer = (uint8_t *)malloc(768);
TotalSteps = numPixels();
begin();
return;
}
Reverse();
//Serial.println("pattern completed");
}
void handleStream(uint8_t *data, size_t len)
// Increment the Index and reset at the end
void NeoPattern::Increment()
{
completed = 0;
if (Direction == FORWARD)
{
//const uint16_t *data16 = (uint16_t *)data;
bufferSize = len;
memcpy(frameBuffer, data, len);
}
void drawFrameBuffer(int w, uint8_t *frame, int length)
{
for (int i = 0; i < length; i++)
Index++;
if (Index >= TotalSteps)
{
uint8_t r = frame[i];
uint8_t g = frame[i + 1];
uint8_t b = frame[i + 2];
setPixelColor(i, r, g, b);
}
}
void onCompleteDefault(int pixels)
{
//Serial.println("onCompleteDefault");
// FIXME no specific code
if (ActivePattern == THEATER_CHASE)
{
return;
}
Reverse();
//Serial.println("pattern completed");
}
// Update the pattern
void Update()
{
switch (ActivePattern)
{
case RAINBOW_CYCLE:
RainbowCycleUpdate();
break;
case THEATER_CHASE:
TheaterChaseUpdate();
break;
case COLOR_WIPE:
ColorWipeUpdate();
break;
case SCANNER:
ScannerUpdate();
break;
case FADE:
FadeUpdate();
break;
case FIRE:
Fire(50, 120);
break;
default:
if (bufferSize > 0)
{
drawFrameBuffer(TotalSteps, frameBuffer, bufferSize);
}
break;
}
}
void UpdateScheduled()
{
if ((millis() - lastUpdate) > Interval) // time to update
{
lastUpdate = millis();
Update();
}
}
// Increment the Index and reset at the end
void Increment()
{
completed = 0;
if (Direction == FORWARD)
{
Index++;
if (Index >= TotalSteps)
{
Index = 0;
completed = 1;
if (OnComplete != NULL)
{
OnComplete(numPixels()); // call the comlpetion callback
}
else
{
onCompleteDefault(numPixels());
}
}
}
else // Direction == REVERSE
{
--Index;
if (Index <= 0)
{
Index = TotalSteps - 1;
completed = 1;
if (OnComplete != NULL)
{
OnComplete(numPixels()); // call the comlpetion callback
}
else
{
onCompleteDefault(numPixels());
}
}
}
}
// Reverse pattern direction
void Reverse()
{
if (Direction == FORWARD)
{
Direction = REVERSE;
Index = TotalSteps - 1;
}
else
{
Direction = FORWARD;
Index = 0;
}
}
// Initialize for a RainbowCycle
void RainbowCycle(uint8_t interval, direction dir = FORWARD)
{
ActivePattern = RAINBOW_CYCLE;
Interval = interval;
TotalSteps = 255;
Index = 0;
Direction = dir;
}
// Update the Rainbow Cycle Pattern
void RainbowCycleUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255));
}
show();
Increment();
}
// Initialize for a Theater Chase
void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD)
{
ActivePattern = THEATER_CHASE;
Interval = interval;
TotalSteps = numPixels();
Color1 = color1;
Color2 = color2;
Index = 0;
Direction = dir;
}
// Update the Theater Chase Pattern
void TheaterChaseUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
if ((i + Index) % 3 == 0)
completed = 1;
if (OnComplete != NULL)
{
setPixelColor(i, Color1);
OnComplete(numPixels()); // call the comlpetion callback
}
else
{
setPixelColor(i, Color2);
onCompleteDefault(numPixels());
}
}
show();
Increment();
}
// Initialize for a ColorWipe
void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD)
else // Direction == REVERSE
{
ActivePattern = COLOR_WIPE;
Interval = interval;
TotalSteps = numPixels();
Color1 = color;
Index = 0;
Direction = dir;
}
// Update the Color Wipe Pattern
void ColorWipeUpdate()
{
setPixelColor(Index, Color1);
show();
Increment();
}
// Initialize for a SCANNNER
void Scanner(uint32_t color1, uint8_t interval)
{
ActivePattern = SCANNER;
Interval = interval;
TotalSteps = (numPixels() - 1) * 2;
Color1 = color1;
Index = 0;
}
// Update the Scanner Pattern
void ScannerUpdate()
{
for (int i = 0; i < numPixels(); i++)
--Index;
if (Index <= 0)
{
if (i == Index) // Scan Pixel to the right
Index = TotalSteps - 1;
completed = 1;
if (OnComplete != NULL)
{
setPixelColor(i, Color1);
}
else if (i == TotalSteps - Index) // Scan Pixel to the left
{
setPixelColor(i, Color1);
}
else // Fading tail
{
setPixelColor(i, DimColor(getPixelColor(i)));
}
}
show();
Increment();
}
// Initialize for a Fade
void Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir = FORWARD)
{
ActivePattern = FADE;
Interval = interval;
TotalSteps = steps;
Color1 = color1;
Color2 = color2;
Index = 0;
Direction = dir;
}
// Update the Fade Pattern
void FadeUpdate()
{
// Calculate linear interpolation between Color1 and Color2
// Optimise order of operations to minimize truncation error
uint8_t red = ((Red(Color1) * (TotalSteps - Index)) + (Red(Color2) * Index)) / TotalSteps;
uint8_t green = ((Green(Color1) * (TotalSteps - Index)) + (Green(Color2) * Index)) / TotalSteps;
uint8_t blue = ((Blue(Color1) * (TotalSteps - Index)) + (Blue(Color2) * Index)) / TotalSteps;
ColorSet(Color(red, green, blue));
show();
Increment();
}
// Calculate 50% dimmed version of a color (used by ScannerUpdate)
uint32_t DimColor(uint32_t color)
{
// Shift R, G and B components one bit to the right
uint32_t dimColor = Color(Red(color) >> 1, Green(color) >> 1, Blue(color) >> 1);
return dimColor;
}
// Set all pixels to a color (synchronously)
void ColorSet(uint32_t color)
{
for (int i = 0; i < numPixels(); i++)
{
setPixelColor(i, color);
}
show();
}
// Returns the Red component of a 32-bit color
uint8_t Red(uint32_t color)
{
return (color >> 16) & 0xFF;
}
// Returns the Green component of a 32-bit color
uint8_t Green(uint32_t color)
{
return (color >> 8) & 0xFF;
}
// Returns the Blue component of a 32-bit color
uint8_t Blue(uint32_t color)
{
return color & 0xFF;
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(uint8_t WheelPos)
{
//if(WheelPos == 0) return Color(0,0,0);
WheelPos = 255 - WheelPos;
if (WheelPos < 85)
{
return Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
else if (WheelPos < 170)
{
WheelPos -= 85;
return Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
else
{
WheelPos -= 170;
return Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
}
/**
* Effects from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
*/
void Fire(int Cooling, int Sparking)
{
uint8_t heat[numPixels()];
int cooldown;
// Step 1. Cool down every cell a little
for (int i = 0; i < numPixels(); i++)
{
cooldown = random(0, ((Cooling * 10) / numPixels()) + 2);
if (cooldown > heat[i])
{
heat[i] = 0;
OnComplete(numPixels()); // call the comlpetion callback
}
else
{
heat[i] = heat[i] - cooldown;
onCompleteDefault(numPixels());
}
}
// Step 2. Heat from each cell drifts 'up' and diffuses a little
for (int k = numPixels() - 1; k >= 2; k--)
{
heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
}
// Step 3. Randomly ignite new 'sparks' near the bottom
if (random(255) < Sparking)
{
int y = random(7);
heat[y] = heat[y] + random(160, 255);
//heat[y] = random(160,255);
}
// Step 4. Convert heat to LED colors
for (int j = 0; j < numPixels(); j++)
{
setPixelHeatColor(j, heat[j]);
}
showStrip();
}
}
void setPixelHeatColor(int Pixel, uint8_t temperature)
// Reverse pattern direction
void NeoPattern::Reverse()
{
if (Direction == FORWARD)
{
// Scale 'heat' down from 0-255 to 0-191
uint8_t t192 = round((temperature / 255.0) * 191);
Direction = REVERSE;
Index = TotalSteps - 1;
}
else
{
Direction = FORWARD;
Index = 0;
}
}
// calculate ramp up from
uint8_t heatramp = t192 & 0x3F; // 0..63
heatramp <<= 2; // scale up to 0..252
// Initialize for a RainbowCycle
void NeoPattern::RainbowCycle(uint8_t interval, direction dir)
{
ActivePattern = RAINBOW_CYCLE;
Interval = interval;
TotalSteps = 255;
Index = 0;
Direction = dir;
}
// figure out which third of the spectrum we're in:
if (t192 > 0x80)
{ // hottest
setPixel(Pixel, 255, 255, heatramp);
}
else if (t192 > 0x40)
{ // middle
setPixel(Pixel, 255, heatramp, 0);
// Update the Rainbow Cycle Pattern
void NeoPattern::RainbowCycleUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255));
}
show();
Increment();
}
// Initialize for a Theater Chase
void NeoPattern::TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir)
{
ActivePattern = THEATER_CHASE;
Interval = interval;
TotalSteps = numPixels();
Color1 = color1;
Color2 = color2;
Index = 0;
Direction = dir;
}
// Update the Theater Chase Pattern
void NeoPattern::TheaterChaseUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
if ((i + Index) % 3 == 0)
{
setPixelColor(i, Color1);
}
else
{ // coolest
setPixel(Pixel, heatramp, 0, 0);
{
setPixelColor(i, Color2);
}
}
show();
Increment();
}
// Initialize for a ColorWipe
void NeoPattern::ColorWipe(uint32_t color, uint8_t interval, direction dir)
{
ActivePattern = COLOR_WIPE;
Interval = interval;
TotalSteps = numPixels();
Color1 = color;
Index = 0;
Direction = dir;
}
// Update the Color Wipe Pattern
void NeoPattern::ColorWipeUpdate()
{
setPixelColor(Index, Color1);
show();
Increment();
}
// Initialize for a SCANNNER
void NeoPattern::Scanner(uint32_t color1, uint8_t interval)
{
ActivePattern = SCANNER;
Interval = interval;
TotalSteps = (numPixels() - 1) * 2;
Color1 = color1;
Index = 0;
}
// Update the Scanner Pattern
void NeoPattern::ScannerUpdate()
{
for (int i = 0; i < numPixels(); i++)
{
if (i == Index) // Scan Pixel to the right
{
setPixelColor(i, Color1);
}
else if (i == TotalSteps - Index) // Scan Pixel to the left
{
setPixelColor(i, Color1);
}
else // Fading tail
{
setPixelColor(i, DimColor(getPixelColor(i)));
}
}
show();
Increment();
}
// Initialize for a Fade
void NeoPattern::Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir)
{
ActivePattern = FADE;
Interval = interval;
TotalSteps = steps;
Color1 = color1;
Color2 = color2;
Index = 0;
Direction = dir;
}
// Update the Fade Pattern
void NeoPattern::FadeUpdate()
{
// Calculate linear interpolation between Color1 and Color2
// Optimise order of operations to minimize truncation error
uint8_t red = ((Red(Color1) * (TotalSteps - Index)) + (Red(Color2) * Index)) / TotalSteps;
uint8_t green = ((Green(Color1) * (TotalSteps - Index)) + (Green(Color2) * Index)) / TotalSteps;
uint8_t blue = ((Blue(Color1) * (TotalSteps - Index)) + (Blue(Color2) * Index)) / TotalSteps;
ColorSet(Color(red, green, blue));
show();
Increment();
}
// Calculate 50% dimmed version of a color (used by ScannerUpdate)
uint32_t NeoPattern::DimColor(uint32_t color)
{
// Shift R, G and B components one bit to the right
uint32_t dimColor = Color(Red(color) >> 1, Green(color) >> 1, Blue(color) >> 1);
return dimColor;
}
// Set all pixels to a color (synchronously)
void NeoPattern::ColorSet(uint32_t color)
{
for (int i = 0; i < numPixels(); i++)
{
setPixelColor(i, color);
}
show();
}
// Returns the Red component of a 32-bit color
uint8_t NeoPattern::Red(uint32_t color)
{
return (color >> 16) & 0xFF;
}
// Returns the Green component of a 32-bit color
uint8_t NeoPattern::Green(uint32_t color)
{
return (color >> 8) & 0xFF;
}
// Returns the Blue component of a 32-bit color
uint8_t NeoPattern::Blue(uint32_t color)
{
return color & 0xFF;
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t NeoPattern::Wheel(uint8_t WheelPos)
{
//if(WheelPos == 0) return Color(0,0,0);
WheelPos = 255 - WheelPos;
if (WheelPos < 85)
{
return Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
else if (WheelPos < 170)
{
WheelPos -= 85;
return Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
else
{
WheelPos -= 170;
return Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
}
/**
* Effects from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/
*/
void NeoPattern::Fire(int Cooling, int Sparking)
{
uint8_t heat[numPixels()];
int cooldown;
// Step 1. Cool down every cell a little
for (int i = 0; i < numPixels(); i++)
{
cooldown = random(0, ((Cooling * 10) / numPixels()) + 2);
if (cooldown > heat[i])
{
heat[i] = 0;
}
else
{
heat[i] = heat[i] - cooldown;
}
}
void setPixel(int Pixel, uint8_t red, uint8_t green, uint8_t blue)
// Step 2. Heat from each cell drifts 'up' and diffuses a little
for (int k = numPixels() - 1; k >= 2; k--)
{
setPixelColor(Pixel, Color(red, green, blue));
heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3;
}
void showStrip()
{
show();
}
};
#endif
// Step 3. Randomly ignite new 'sparks' near the bottom
if (random(255) < Sparking)
{
int y = random(7);
heat[y] = heat[y] + random(160, 255);
//heat[y] = random(160,255);
}
// Step 4. Convert heat to LED colors
for (int j = 0; j < numPixels(); j++)
{
setPixelHeatColor(j, heat[j]);
}
showStrip();
}
void NeoPattern::setPixelHeatColor(int Pixel, uint8_t temperature)
{
// Scale 'heat' down from 0-255 to 0-191
uint8_t t192 = round((temperature / 255.0) * 191);
// calculate ramp up from
uint8_t heatramp = t192 & 0x3F; // 0..63
heatramp <<= 2; // scale up to 0..252
// figure out which third of the spectrum we're in:
if (t192 > 0x80)
{ // hottest
setPixel(Pixel, 255, 255, heatramp);
}
else if (t192 > 0x40)
{ // middle
setPixel(Pixel, 255, heatramp, 0);
}
else
{ // coolest
setPixel(Pixel, heatramp, 0, 0);
}
}
void NeoPattern::setPixel(int Pixel, uint8_t red, uint8_t green, uint8_t blue)
{
setPixelColor(Pixel, Color(red, green, blue));
}
void NeoPattern::showStrip()
{
show();
}

View File

@@ -0,0 +1,109 @@
/**
* Original NeoPattern code by Bill Earl
* https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview
*
* Custom modifications by 0x1d:
* - default OnComplete callback that sets pattern to reverse
* - separate animation update from timer; Update now updates directly, UpdateScheduled uses timer
*/
#ifndef __NeoPattern_INCLUDED__
#define __NeoPattern_INCLUDED__
#include <Adafruit_NeoPixel.h>
using namespace std;
// Pattern types supported:
enum pattern
{
NONE = 0,
RAINBOW_CYCLE = 1,
THEATER_CHASE = 2,
COLOR_WIPE = 3,
SCANNER = 4,
FADE = 5,
FIRE = 6
};
// Pattern directions supported:
enum direction
{
FORWARD,
REVERSE
};
// NeoPattern Class - derived from the Adafruit_NeoPixel class
class NeoPattern : public Adafruit_NeoPixel
{
public:
// Member Variables:
pattern ActivePattern = RAINBOW_CYCLE; // which pattern is running
direction Direction = FORWARD; // direction to run the pattern
unsigned long Interval = 150; // milliseconds between updates
unsigned long lastUpdate = 0; // last update of position
uint32_t Color1 = 0;
uint32_t Color2 = 0; // What colors are in use
uint16_t TotalSteps = 32; // total number of steps in the pattern
uint16_t Index; // current step within the pattern
uint16_t completed = 0;
// Callback on completion of pattern
void (*OnComplete)(int);
uint8_t *frameBuffer;
int bufferSize = 0;
// Constructor - calls base-class constructor to initialize strip
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int));
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type);
~NeoPattern();
// Stream handling
void handleStream(uint8_t *data, size_t len);
void drawFrameBuffer(int w, uint8_t *frame, int length);
// Pattern completion
void onCompleteDefault(int pixels);
// Pattern control
void Increment();
void Reverse();
// Rainbow Cycle
void RainbowCycle(uint8_t interval, direction dir = FORWARD);
void RainbowCycleUpdate();
// Theater Chase
void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD);
void TheaterChaseUpdate();
// Color Wipe
void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD);
void ColorWipeUpdate();
// Scanner
void Scanner(uint32_t color1, uint8_t interval);
void ScannerUpdate();
// Fade
void Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir = FORWARD);
void FadeUpdate();
// Fire effect
void Fire(int Cooling, int Sparking);
void setPixelHeatColor(int Pixel, uint8_t temperature);
void setPixel(int Pixel, uint8_t red, uint8_t green, uint8_t blue);
void showStrip();
// Utility methods
uint32_t DimColor(uint32_t color);
void ColorSet(uint32_t color);
uint8_t Red(uint32_t color);
uint8_t Green(uint32_t color);
uint8_t Blue(uint32_t color);
uint32_t Wheel(uint8_t WheelPos);
};
#endif

View File

@@ -1,56 +1,96 @@
#include "NeoPatternService.h"
#include "spore/core/ApiServer.h"
#include "spore/util/Logging.h"
#include <ArduinoJson.h>
NeoPatternService::NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type)
NeoPatternService::NeoPatternService(TaskManager& taskMgr, const NeoPixelConfig& config)
: taskManager(taskMgr),
pixels(numPixels, pin, type),
updateIntervalMs(100),
brightness(48) {
pixels.setBrightness(brightness);
pixels.show();
config(config),
activePattern(NeoPatternType::RAINBOW_CYCLE),
direction(NeoDirection::FORWARD),
updateIntervalMs(config.updateInterval),
lastUpdateMs(0),
initialized(false) {
// Initialize NeoPattern
neoPattern = new NeoPattern(config.length, config.pin, NEO_GRB + NEO_KHZ800);
neoPattern->setBrightness(config.brightness);
// Initialize state
currentState.pattern = static_cast<uint>(activePattern);
currentState.color = 0xFF0000; // Red
currentState.color2 = 0x0000FF; // Blue
currentState.totalSteps = 16;
currentState.brightness = config.brightness;
// Set initial pattern
neoPattern->Color1 = currentState.color;
neoPattern->Color2 = currentState.color2;
neoPattern->TotalSteps = currentState.totalSteps;
neoPattern->ActivePattern = static_cast<::pattern>(activePattern);
neoPattern->Direction = static_cast<::direction>(direction);
registerPatterns();
registerTasks();
setPatternByName("rainbow_cycle");
initialized = true;
LOG_INFO("NeoPattern", "Service initialized");
}
NeoPatternService::~NeoPatternService() {
if (neoPattern) {
delete neoPattern;
}
}
void NeoPatternService::registerEndpoints(ApiServer& api) {
// Status endpoint
api.addEndpoint("/api/neopattern/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
// Patterns list endpoint
api.addEndpoint("/api/neopattern/patterns", HTTP_GET,
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
std::vector<ParamSpec>{});
// Control endpoint
api.addEndpoint("/api/neopattern", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
ParamSpec{String("interval_ms"), false, String("body"), String("number"), {}, String("100")},
ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("50")},
ParamSpec{String("color"), false, String("body"), String("color"), {}},
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
ParamSpec{String("r"), false, String("body"), String("number"), {}},
ParamSpec{String("g"), false, String("body"), String("number"), {}},
ParamSpec{String("b"), false, String("body"), String("number"), {}},
ParamSpec{String("r2"), false, String("body"), String("number"), {}},
ParamSpec{String("g2"), false, String("body"), String("number"), {}},
ParamSpec{String("b2"), false, String("body"), String("number"), {}},
ParamSpec{String("total_steps"), false, String("body"), String("number"), {}},
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}}
ParamSpec{String("brightness"), false, String("body"), String("numberRange"), {}, String("80")},
ParamSpec{String("total_steps"), false, String("body"), String("numberRange"), {}, String("16")},
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}},
ParamSpec{String("interval"), false, String("body"), String("number"), {}, String("100")}
});
// State endpoint for complex state updates
api.addEndpoint("/api/neopattern/state", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleStateRequest(request); },
std::vector<ParamSpec>{});
}
void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["pin"] = pixels.getPin();
doc["count"] = pixels.numPixels();
doc["interval_ms"] = updateIntervalMs;
doc["brightness"] = brightness;
doc["pin"] = config.pin;
doc["length"] = config.length;
doc["brightness"] = currentState.brightness;
doc["pattern"] = currentPatternName();
doc["total_steps"] = pixels.TotalSteps;
doc["color1"] = pixels.Color1;
doc["color2"] = pixels.Color2;
doc["color"] = String(currentState.color, HEX);
doc["color2"] = String(currentState.color2, HEX);
doc["total_steps"] = currentState.totalSteps;
doc["direction"] = (direction == NeoDirection::FORWARD) ? "forward" : "reverse";
doc["interval"] = updateIntervalMs;
doc["active"] = initialized;
// Add pattern metadata
String currentPattern = currentPatternName();
doc["pattern_description"] = getPatternDescription(currentPattern);
doc["pattern_requires_color2"] = patternRequiresColor2(currentPattern);
doc["pattern_supports_direction"] = patternSupportsDirection(currentPattern);
String json;
serializeJson(doc, json);
@@ -60,7 +100,17 @@ void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (auto& kv : patternSetters) arr.add(kv.first);
// Get all patterns from registry and include metadata
auto patterns = patternRegistry.getAllPatterns();
for (const auto& pattern : patterns) {
JsonObject patternObj = arr.add<JsonObject>();
patternObj["name"] = pattern.name;
patternObj["type"] = pattern.type;
patternObj["description"] = pattern.description;
patternObj["requires_color2"] = pattern.requiresColor2;
patternObj["supports_direction"] = pattern.supportsDirection;
}
String json;
serializeJson(doc, json);
@@ -68,121 +118,334 @@ void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
}
void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
bool updated = false;
if (request->hasParam("pattern", true)) {
String name = request->getParam("pattern", true)->value();
setPatternByName(name);
if (isValidPattern(name)) {
setPatternByName(name);
updated = true;
} else {
// Invalid pattern name - could add error handling here
LOG_WARN("NeoPattern", "Invalid pattern name: " + name);
}
}
if (request->hasParam("interval_ms", true)) {
unsigned long v = request->getParam("interval_ms", true)->value().toInt();
if (v < 1) v = 1;
updateIntervalMs = v;
taskManager.setTaskInterval("neopattern_update", updateIntervalMs);
if (request->hasParam("color", true)) {
String colorStr = request->getParam("color", true)->value();
uint32_t color = parseColor(colorStr);
setColor(color);
updated = true;
}
if (request->hasParam("color2", true)) {
String colorStr = request->getParam("color2", true)->value();
uint32_t color = parseColor(colorStr);
setColor2(color);
updated = true;
}
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0;
if (b > 255) b = 255;
setBrightness((uint8_t)b);
}
// Accept packed color ints or r,g,b triplets
if (request->hasParam("color", true)) {
pixels.Color1 = (uint32_t)strtoul(request->getParam("color", true)->value().c_str(), nullptr, 0);
}
if (request->hasParam("color2", true)) {
pixels.Color2 = (uint32_t)strtoul(request->getParam("color2", true)->value().c_str(), nullptr, 0);
}
if (request->hasParam("r", true) || request->hasParam("g", true) || request->hasParam("b", true)) {
int r = request->hasParam("r", true) ? request->getParam("r", true)->value().toInt() : 0;
int g = request->hasParam("g", true) ? request->getParam("g", true)->value().toInt() : 0;
int b = request->hasParam("b", true) ? request->getParam("b", true)->value().toInt() : 0;
pixels.Color1 = pixels.Color(r, g, b);
}
if (request->hasParam("r2", true) || request->hasParam("g2", true) || request->hasParam("b2", true)) {
int r = request->hasParam("r2", true) ? request->getParam("r2", true)->value().toInt() : 0;
int g = request->hasParam("g2", true) ? request->getParam("g2", true)->value().toInt() : 0;
int b = request->hasParam("b2", true) ? request->getParam("b2", true)->value().toInt() : 0;
pixels.Color2 = pixels.Color(r, g, b);
setBrightness(static_cast<uint8_t>(b));
updated = true;
}
if (request->hasParam("total_steps", true)) {
pixels.TotalSteps = request->getParam("total_steps", true)->value().toInt();
int steps = request->getParam("total_steps", true)->value().toInt();
if (steps > 0) {
setTotalSteps(static_cast<uint16_t>(steps));
updated = true;
}
}
if (request->hasParam("direction", true)) {
String dir = request->getParam("direction", true)->value();
if (dir.equalsIgnoreCase("forward")) pixels.Direction = FORWARD;
else if (dir.equalsIgnoreCase("reverse")) pixels.Direction = REVERSE;
String dirStr = request->getParam("direction", true)->value();
NeoDirection dir = (dirStr.equalsIgnoreCase("reverse")) ? NeoDirection::REVERSE : NeoDirection::FORWARD;
setDirection(dir);
updated = true;
}
if (request->hasParam("interval", true)) {
unsigned long interval = request->getParam("interval", true)->value().toInt();
if (interval > 0) {
setUpdateInterval(interval);
updated = true;
}
}
// Return current state
JsonDocument resp;
resp["ok"] = true;
resp["pattern"] = currentPatternName();
resp["interval_ms"] = updateIntervalMs;
resp["brightness"] = brightness;
resp["color"] = String(currentState.color, HEX);
resp["color2"] = String(currentState.color2, HEX);
resp["brightness"] = currentState.brightness;
resp["total_steps"] = currentState.totalSteps;
resp["direction"] = (direction == NeoDirection::FORWARD) ? "forward" : "reverse";
resp["interval"] = updateIntervalMs;
resp["updated"] = updated;
String json;
serializeJson(resp, json);
request->send(200, "application/json", json);
}
void NeoPatternService::setBrightness(uint8_t b) {
brightness = b;
pixels.setBrightness(brightness);
pixels.show();
void NeoPatternService::handleStateRequest(AsyncWebServerRequest* request) {
if (request->contentType() != "application/json") {
request->send(400, "application/json", "{\"error\":\"Content-Type must be application/json\"}");
return;
}
// Note: AsyncWebServerRequest doesn't have getBody() method
// For now, we'll skip JSON body parsing and use form parameters
request->send(400, "application/json", "{\"error\":\"JSON body parsing not implemented\"}");
return;
// JSON body parsing not implemented for AsyncWebServerRequest
// This would need to be implemented differently
request->send(501, "application/json", "{\"error\":\"Not implemented\"}");
}
void NeoPatternService::setPattern(NeoPatternType pattern) {
activePattern = pattern;
currentState.pattern = static_cast<uint>(pattern);
neoPattern->ActivePattern = static_cast<::pattern>(pattern);
resetStateForPattern(pattern);
// Initialize the pattern using the registry
patternRegistry.initializePattern(static_cast<uint8_t>(pattern));
}
void NeoPatternService::setPatternByName(const String& name) {
NeoPatternType pattern = nameToPattern(name);
setPattern(pattern);
}
void NeoPatternService::setColor(uint32_t color) {
currentState.color = color;
neoPattern->Color1 = color;
if (activePattern == NeoPatternType::NONE) {
neoPattern->ColorSet(color);
}
}
void NeoPatternService::setColor2(uint32_t color) {
currentState.color2 = color;
neoPattern->Color2 = color;
}
void NeoPatternService::setBrightness(uint8_t brightness) {
currentState.brightness = brightness;
neoPattern->setBrightness(brightness);
neoPattern->show();
}
void NeoPatternService::setTotalSteps(uint16_t steps) {
currentState.totalSteps = steps;
neoPattern->TotalSteps = steps;
}
void NeoPatternService::setDirection(NeoDirection dir) {
direction = dir;
neoPattern->Direction = static_cast<::direction>(dir);
}
void NeoPatternService::setUpdateInterval(unsigned long interval) {
updateIntervalMs = interval;
taskManager.setTaskInterval("neopattern_update", interval);
}
void NeoPatternService::setState(const NeoPatternState& state) {
currentState = state;
// Apply state to NeoPattern
neoPattern->Index = 0;
neoPattern->Color1 = state.color;
neoPattern->Color2 = state.color2;
neoPattern->TotalSteps = state.totalSteps;
neoPattern->ActivePattern = static_cast<::pattern>(state.pattern);
neoPattern->Direction = FORWARD;
setBrightness(state.brightness);
setPattern(static_cast<NeoPatternType>(state.pattern));
}
NeoPatternState NeoPatternService::getState() const {
return currentState;
}
void NeoPatternService::registerTasks() {
taskManager.registerTask("neopattern_update", updateIntervalMs, [this]() { update(); });
taskManager.registerTask("neopattern_status_print", 10000, [this]() {
Serial.printf("[NeoPattern] pattern=%s interval=%lu ms brightness=%u\n",
currentPatternName().c_str(), updateIntervalMs, brightness);
LOG_INFO("NeoPattern", "Status update");
});
}
void NeoPatternService::registerPatterns() {
patternSetters["off"] = [this]() { pixels.ActivePattern = NONE; };
patternSetters["rainbow_cycle"] = [this]() { pixels.RainbowCycle(updateIntervalMs); };
patternSetters["theater_chase"] = [this]() { pixels.TheaterChase(pixels.Color1 ? pixels.Color1 : pixels.Color(127,127,127), pixels.Color2, updateIntervalMs); };
patternSetters["color_wipe"] = [this]() { pixels.ColorWipe(pixels.Color1 ? pixels.Color1 : pixels.Color(255,0,0), updateIntervalMs); };
patternSetters["scanner"] = [this]() { pixels.Scanner(pixels.Color1 ? pixels.Color1 : pixels.Color(0,0,255), updateIntervalMs); };
patternSetters["fade"] = [this]() { pixels.Fade(pixels.Color1, pixels.Color2, pixels.TotalSteps ? pixels.TotalSteps : 32, updateIntervalMs); };
patternSetters["fire"] = [this]() { pixels.ActivePattern = FIRE; pixels.Interval = updateIntervalMs; };
// Register all patterns with their metadata and callbacks
patternRegistry.registerPattern(
"none",
static_cast<uint8_t>(NeoPatternType::NONE),
"No pattern - solid color",
[this]() { /* No initialization needed */ },
[this]() { updateNone(); },
false, // doesn't require color2
false // doesn't support direction
);
patternRegistry.registerPattern(
"rainbow_cycle",
static_cast<uint8_t>(NeoPatternType::RAINBOW_CYCLE),
"Rainbow cycle pattern",
[this]() { neoPattern->RainbowCycle(updateIntervalMs, static_cast<::direction>(direction)); },
[this]() { updateRainbowCycle(); },
false, // doesn't require color2
true // supports direction
);
patternRegistry.registerPattern(
"theater_chase",
static_cast<uint8_t>(NeoPatternType::THEATER_CHASE),
"Theater chase pattern",
[this]() { neoPattern->TheaterChase(currentState.color, currentState.color2, updateIntervalMs, static_cast<::direction>(direction)); },
[this]() { updateTheaterChase(); },
true, // requires color2
true // supports direction
);
patternRegistry.registerPattern(
"color_wipe",
static_cast<uint8_t>(NeoPatternType::COLOR_WIPE),
"Color wipe pattern",
[this]() { neoPattern->ColorWipe(currentState.color, updateIntervalMs, static_cast<::direction>(direction)); },
[this]() { updateColorWipe(); },
false, // doesn't require color2
true // supports direction
);
patternRegistry.registerPattern(
"scanner",
static_cast<uint8_t>(NeoPatternType::SCANNER),
"Scanner pattern",
[this]() { neoPattern->Scanner(currentState.color, updateIntervalMs); },
[this]() { updateScanner(); },
false, // doesn't require color2
false // doesn't support direction
);
patternRegistry.registerPattern(
"fade",
static_cast<uint8_t>(NeoPatternType::FADE),
"Fade pattern",
[this]() { neoPattern->Fade(currentState.color, currentState.color2, currentState.totalSteps, updateIntervalMs, static_cast<::direction>(direction)); },
[this]() { updateFade(); },
true, // requires color2
true // supports direction
);
patternRegistry.registerPattern(
"fire",
static_cast<uint8_t>(NeoPatternType::FIRE),
"Fire effect pattern",
[this]() { neoPattern->Fire(50, 120); },
[this]() { updateFire(); },
false, // doesn't require color2
false // doesn't support direction
);
}
std::vector<String> NeoPatternService::patternNamesVector() {
if (patternSetters.empty()) registerPatterns();
std::vector<String> v;
v.reserve(patternSetters.size());
for (const auto& kv : patternSetters) v.push_back(kv.first);
return v;
std::vector<String> NeoPatternService::patternNamesVector() const {
return patternRegistry.getAllPatternNames();
}
String NeoPatternService::currentPatternName() {
switch (pixels.ActivePattern) {
case NONE: return String("off");
case RAINBOW_CYCLE: return String("rainbow_cycle");
case THEATER_CHASE: return String("theater_chase");
case COLOR_WIPE: return String("color_wipe");
case SCANNER: return String("scanner");
case FADE: return String("fade");
case FIRE: return String("fire");
String NeoPatternService::currentPatternName() const {
return patternRegistry.getPatternName(static_cast<uint8_t>(activePattern));
}
NeoPatternService::NeoPatternType NeoPatternService::nameToPattern(const String& name) const {
uint8_t type = patternRegistry.getPatternType(name);
return static_cast<NeoPatternType>(type);
}
void NeoPatternService::resetStateForPattern(NeoPatternType pattern) {
neoPattern->Index = 0;
neoPattern->Direction = static_cast<::direction>(direction);
neoPattern->completed = 0;
lastUpdateMs = 0;
}
uint32_t NeoPatternService::parseColor(const String& colorStr) const {
if (colorStr.startsWith("#")) {
return strtoul(colorStr.substring(1).c_str(), nullptr, 16);
} else if (colorStr.startsWith("0x") || colorStr.startsWith("0X")) {
return strtoul(colorStr.c_str(), nullptr, 16);
} else {
return strtoul(colorStr.c_str(), nullptr, 10);
}
return String("off");
}
void NeoPatternService::setPatternByName(const String& name) {
if (patternSetters.empty()) registerPatterns();
auto it = patternSetters.find(name);
if (it != patternSetters.end()) {
pixels.Index = 0;
pixels.Direction = FORWARD;
it->second();
}
bool NeoPatternService::isValidPattern(const String& name) const {
return patternRegistry.isValidPattern(name);
}
bool NeoPatternService::isValidPattern(NeoPatternType type) const {
return patternRegistry.isValidPattern(static_cast<uint8_t>(type));
}
bool NeoPatternService::patternRequiresColor2(const String& name) const {
const PatternInfo* info = patternRegistry.getPattern(name);
return info ? info->requiresColor2 : false;
}
bool NeoPatternService::patternSupportsDirection(const String& name) const {
const PatternInfo* info = patternRegistry.getPattern(name);
return info ? info->supportsDirection : false;
}
String NeoPatternService::getPatternDescription(const String& name) const {
const PatternInfo* info = patternRegistry.getPattern(name);
return info ? info->description : "";
}
void NeoPatternService::update() {
pixels.Update();
if (!initialized) return;
//unsigned long now = millis();
//if (now - lastUpdateMs < updateIntervalMs) return;
//lastUpdateMs = now;
// Use pattern registry to execute the current pattern
patternRegistry.executePattern(static_cast<uint8_t>(activePattern));
}
void NeoPatternService::updateRainbowCycle() {
neoPattern->RainbowCycleUpdate();
}
void NeoPatternService::updateTheaterChase() {
neoPattern->TheaterChaseUpdate();
}
void NeoPatternService::updateColorWipe() {
neoPattern->ColorWipeUpdate();
}
void NeoPatternService::updateScanner() {
neoPattern->ScannerUpdate();
}
void NeoPatternService::updateFade() {
neoPattern->FadeUpdate();
}
void NeoPatternService::updateFire() {
neoPattern->Fire(50, 120);
}
void NeoPatternService::updateNone() {
// For NONE pattern, just show the current color
neoPattern->ColorSet(neoPattern->Color1);
}

View File

@@ -1,34 +1,95 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/TaskManager.h"
#include "NeoPattern.cpp"
#include "NeoPattern.h"
#include "NeoPatternState.h"
#include "NeoPixelConfig.h"
#include "PatternRegistry.h"
#include <map>
#include <vector>
#include <functional>
class NeoPatternService : public Service {
public:
NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type);
enum class NeoPatternType {
NONE = 0,
RAINBOW_CYCLE = 1,
THEATER_CHASE = 2,
COLOR_WIPE = 3,
SCANNER = 4,
FADE = 5,
FIRE = 6
};
enum class NeoDirection {
FORWARD,
REVERSE
};
NeoPatternService(TaskManager& taskMgr, const NeoPixelConfig& config);
~NeoPatternService();
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "NeoPattern"; }
void setBrightness(uint8_t b);
// Pattern control methods
void setPattern(NeoPatternType pattern);
void setPatternByName(const String& name);
void setColor(uint32_t color);
void setColor2(uint32_t color2);
void setBrightness(uint8_t brightness);
void setTotalSteps(uint16_t steps);
void setDirection(NeoDirection direction);
void setUpdateInterval(unsigned long interval);
// State management
void setState(const NeoPatternState& state);
NeoPatternState getState() const;
private:
void registerTasks();
void registerPatterns();
std::vector<String> patternNamesVector();
String currentPatternName();
void update();
// Pattern updaters
void updateRainbowCycle();
void updateTheaterChase();
void updateColorWipe();
void updateScanner();
void updateFade();
void updateFire();
void updateNone();
// Handlers
// API handlers
void handleStatusRequest(AsyncWebServerRequest* request);
void handlePatternsRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
void handleStateRequest(AsyncWebServerRequest* request);
// Utility methods
std::vector<String> patternNamesVector() const;
String currentPatternName() const;
NeoPatternType nameToPattern(const String& name) const;
void resetStateForPattern(NeoPatternType pattern);
uint32_t parseColor(const String& colorStr) const;
// Pattern validation methods
bool isValidPattern(const String& name) const;
bool isValidPattern(NeoPatternType type) const;
bool patternRequiresColor2(const String& name) const;
bool patternSupportsDirection(const String& name) const;
String getPatternDescription(const String& name) const;
TaskManager& taskManager;
NeoPattern pixels;
NeoPattern* neoPattern;
NeoPixelConfig config;
NeoPatternState currentState;
// Pattern registry for centralized pattern management
PatternRegistry patternRegistry;
NeoPatternType activePattern;
NeoDirection direction;
unsigned long updateIntervalMs;
uint8_t brightness;
std::map<String, std::function<void()>> patternSetters;
unsigned long lastUpdateMs;
bool initialized;
};

View File

@@ -0,0 +1,59 @@
#ifndef __NEOPATTERN_STATE__
#define __NEOPATTERN_STATE__
#include <ArduinoJson.h>
struct NeoPatternState {
// Default values
static constexpr uint DEFAULT_PATTERN = 0;
static constexpr uint DEFAULT_COLOR = 0xFF0000; // Red
static constexpr uint DEFAULT_COLOR2 = 0x0000FF; // Blue
static constexpr uint DEFAULT_TOTAL_STEPS = 16;
static constexpr uint DEFAULT_BRIGHTNESS = 64;
uint pattern = DEFAULT_PATTERN;
uint color = DEFAULT_COLOR;
uint color2 = DEFAULT_COLOR2;
uint totalSteps = DEFAULT_TOTAL_STEPS;
uint brightness = DEFAULT_BRIGHTNESS;
NeoPatternState() = default;
NeoPatternState(uint p, uint c, uint c2, uint steps, uint b)
: pattern(p), color(c), color2(c2), totalSteps(steps), brightness(b) {}
void mapJsonObject(JsonObject& root) const {
root["pattern"] = pattern;
root["color"] = color;
root["color2"] = color2;
root["totalSteps"] = totalSteps;
root["brightness"] = brightness;
}
void fromJsonObject(JsonObject& json) {
pattern = json["pattern"] | pattern;
color = json["color"] | color;
color2 = json["color2"] | color2;
totalSteps = json["totalSteps"] | totalSteps;
brightness = json["brightness"] | brightness;
}
// Helper methods for JSON string conversion
String toJsonString() const {
JsonDocument doc;
JsonObject root = doc.to<JsonObject>();
mapJsonObject(root);
String result;
serializeJson(doc, result);
return result;
}
void fromJsonString(const String& jsonStr) {
JsonDocument doc;
deserializeJson(doc, jsonStr);
JsonObject root = doc.as<JsonObject>();
fromJsonObject(root);
}
};
#endif

View File

@@ -0,0 +1,45 @@
#ifndef __NEOPIXEL_CONFIG__
#define __NEOPIXEL_CONFIG__
#include <ArduinoJson.h>
struct NeoPixelConfig
{
// Configuration constants
static constexpr int DEFAULT_PIN = 2;
static constexpr int DEFAULT_LENGTH = 8;
static constexpr int DEFAULT_BRIGHTNESS = 100;
static constexpr int DEFAULT_UPDATE_INTERVAL = 100;
static constexpr int DEFAULT_COLOR = 0xFF0000; // Red
int pin = DEFAULT_PIN;
int length = DEFAULT_LENGTH;
int brightness = DEFAULT_BRIGHTNESS;
int updateInterval = DEFAULT_UPDATE_INTERVAL;
int defaultColor = DEFAULT_COLOR;
NeoPixelConfig() = default;
NeoPixelConfig(int p, int l, int b, int interval, int color = DEFAULT_COLOR)
: pin(p), length(l), brightness(b), updateInterval(interval), defaultColor(color) {}
void mapJsonObject(JsonObject &root) const
{
root["pin"] = pin;
root["length"] = length;
root["brightness"] = brightness;
root["updateInterval"] = updateInterval;
root["defaultColor"] = defaultColor;
}
void fromJsonObject(JsonObject &json)
{
pin = json["pin"] | pin;
length = json["length"] | length;
brightness = json["brightness"] | brightness;
updateInterval = json["updateInterval"] | updateInterval;
defaultColor = json["defaultColor"] | defaultColor;
}
};
#endif

View File

@@ -0,0 +1,101 @@
#include "PatternRegistry.h"
PatternRegistry::PatternRegistry() {
// Constructor - patterns will be registered by the service
}
void PatternRegistry::registerPattern(const PatternInfo& pattern) {
patternMap_[pattern.name] = pattern;
typeToNameMap_[pattern.type] = pattern.name;
}
void PatternRegistry::registerPattern(const String& name, uint8_t type, const String& description,
std::function<void()> initializer, std::function<void()> updater,
bool requiresColor2, bool supportsDirection) {
PatternInfo info(name, type, description, initializer, updater, requiresColor2, supportsDirection);
registerPattern(info);
}
const PatternInfo* PatternRegistry::getPattern(const String& name) const {
auto it = patternMap_.find(name);
return (it != patternMap_.end()) ? &it->second : nullptr;
}
const PatternInfo* PatternRegistry::getPattern(uint8_t type) const {
auto typeIt = typeToNameMap_.find(type);
if (typeIt == typeToNameMap_.end()) {
return nullptr;
}
return getPattern(typeIt->second);
}
String PatternRegistry::getPatternName(uint8_t type) const {
auto it = typeToNameMap_.find(type);
return (it != typeToNameMap_.end()) ? it->second : "";
}
uint8_t PatternRegistry::getPatternType(const String& name) const {
const PatternInfo* info = getPattern(name);
return info ? info->type : 0;
}
std::vector<String> PatternRegistry::getAllPatternNames() const {
std::vector<String> names;
names.reserve(patternMap_.size());
for (const auto& pair : patternMap_) {
names.push_back(pair.first);
}
return names;
}
std::vector<PatternInfo> PatternRegistry::getAllPatterns() const {
std::vector<PatternInfo> patterns;
patterns.reserve(patternMap_.size());
for (const auto& pair : patternMap_) {
patterns.push_back(pair.second);
}
return patterns;
}
bool PatternRegistry::isValidPattern(const String& name) const {
return patternMap_.find(name) != patternMap_.end();
}
bool PatternRegistry::isValidPattern(uint8_t type) const {
return typeToNameMap_.find(type) != typeToNameMap_.end();
}
void PatternRegistry::executePattern(const String& name) const {
const PatternInfo* info = getPattern(name);
if (info && info->updater) {
info->updater();
}
}
void PatternRegistry::executePattern(uint8_t type) const {
const PatternInfo* info = getPattern(type);
if (info && info->updater) {
info->updater();
}
}
void PatternRegistry::initializePattern(const String& name) const {
const PatternInfo* info = getPattern(name);
if (info && info->initializer) {
info->initializer();
}
}
void PatternRegistry::initializePattern(uint8_t type) const {
const PatternInfo* info = getPattern(type);
if (info && info->initializer) {
info->initializer();
}
}
void PatternRegistry::updateTypeMap() {
typeToNameMap_.clear();
for (const auto& pair : patternMap_) {
typeToNameMap_[pair.second.type] = pair.first;
}
}

View File

@@ -0,0 +1,73 @@
#pragma once
#include <map>
#include <functional>
#include <vector>
#include <Arduino.h>
/**
* PatternInfo structure containing all information needed for a pattern
*/
struct PatternInfo {
String name;
uint8_t type;
String description;
std::function<void()> updater;
std::function<void()> initializer;
bool requiresColor2;
bool supportsDirection;
// Default constructor for std::map compatibility
PatternInfo() : type(0), requiresColor2(false), supportsDirection(false) {}
// Parameterized constructor
PatternInfo(const String& n, uint8_t t, const String& desc,
std::function<void()> initFunc, std::function<void()> updateFunc = nullptr,
bool needsColor2 = false, bool supportsDir = true)
: name(n), type(t), description(desc), updater(updateFunc),
initializer(initFunc), requiresColor2(needsColor2), supportsDirection(supportsDir) {}
};
/**
* PatternRegistry class for centralized pattern management
*/
class PatternRegistry {
public:
using PatternMap = std::map<String, PatternInfo>;
using PatternTypeMap = std::map<uint8_t, String>;
PatternRegistry();
~PatternRegistry() = default;
// Pattern registration
void registerPattern(const PatternInfo& pattern);
void registerPattern(const String& name, uint8_t type, const String& description,
std::function<void()> initializer, std::function<void()> updater = nullptr,
bool requiresColor2 = false, bool supportsDirection = true);
// Pattern lookup
const PatternInfo* getPattern(const String& name) const;
const PatternInfo* getPattern(uint8_t type) const;
String getPatternName(uint8_t type) const;
uint8_t getPatternType(const String& name) const;
// Pattern enumeration
std::vector<String> getAllPatternNames() const;
std::vector<PatternInfo> getAllPatterns() const;
const PatternMap& getPatternMap() const { return patternMap_; }
// Pattern validation
bool isValidPattern(const String& name) const;
bool isValidPattern(uint8_t type) const;
// Pattern execution
void executePattern(const String& name) const;
void executePattern(uint8_t type) const;
void initializePattern(const String& name) const;
void initializePattern(uint8_t type) const;
private:
PatternMap patternMap_;
PatternTypeMap typeToNameMap_;
void updateTypeMap();
};

View File

@@ -0,0 +1,131 @@
# NeoPattern Service for Spore Framework
This example demonstrates how to integrate a NeoPixel pattern service with the Spore framework. It provides a comprehensive LED strip control system with multiple animation patterns and REST API endpoints.
## Features
- **Multiple Animation Patterns**: Rainbow cycle, theater chase, color wipe, scanner, fade, and fire effects
- **REST API Control**: Full HTTP API for pattern control and configuration
- **Real-time Updates**: Smooth pattern transitions and real-time parameter changes
- **State Management**: Persistent state with JSON serialization
- **Spore Integration**: Built on the Spore framework for networking and task management
## Hardware Requirements
- ESP32 or compatible microcontroller
- NeoPixel LED strip (WS2812B or compatible)
- Appropriate power supply for your LED strip
## Configuration
Edit `NeoPixelConfig.h` to configure your setup:
```cpp
static constexpr int DEFAULT_PIN = 2;
static constexpr int DEFAULT_LENGTH = 8;
static constexpr int DEFAULT_BRIGHTNESS = 100;
static constexpr int DEFAULT_UPDATE_INTERVAL = 100;
static constexpr int DEFAULT_COLOR = 0xFF0000; // Red
```
## API Endpoints
### Status Information
- `GET /api/neopattern/status` - Get current status and configuration
- `GET /api/neopattern/patterns` - List available patterns
### Pattern Control
- `POST /api/neopattern` - Control pattern parameters
- `pattern`: Pattern name (none, rainbow_cycle, theater_chase, color_wipe, scanner, fade, fire)
- `color`: Primary color (hex string like "#FF0000" or "0xFF0000")
- `color2`: Secondary color for two-color patterns
- `brightness`: Brightness level (0-255)
- `total_steps`: Number of steps for patterns
- `direction`: Pattern direction (forward, reverse)
- `interval`: Update interval in milliseconds
### State Management
- `POST /api/neopattern/state` - Set complete state via JSON
## Example API Usage
### Set a rainbow cycle pattern
```bash
curl -X POST http://esp32-ip/api/neopattern \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "pattern=rainbow_cycle&interval=50"
```
### Set custom colors for theater chase
```bash
curl -X POST http://esp32-ip/api/neopattern \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "pattern=theater_chase&color=0xFF0000&color2=0x0000FF&brightness=150"
```
### Set complete state via JSON
```bash
curl -X POST http://esp32-ip/api/neopattern/state \
-H "Content-Type: application/json" \
-d '{
"pattern": 3,
"color": 16711680,
"color2": 255,
"totalSteps": 32,
"brightness": 128
}'
```
## Available Patterns
1. **none** - Static color display
2. **rainbow_cycle** - Smooth rainbow cycling
3. **theater_chase** - Moving dots with two colors
4. **color_wipe** - Progressive color filling
5. **scanner** - Scanning light effect
6. **fade** - Smooth color transition
7. **fire** - Fire simulation effect
## Building and Flashing
1. Install PlatformIO
2. Configure your `platformio.ini` to include the Spore framework
3. Build and upload:
```bash
pio run -t upload
```
## Integration with Spore Framework
This service integrates with the Spore framework by:
- Extending the `Service` base class
- Using `TaskManager` for pattern updates
- Registering REST API endpoints with `ApiServer`
- Following Spore's logging and configuration patterns
The service automatically registers itself with the Spore framework and provides all functionality through the standard Spore API infrastructure.
## Customization
To add new patterns:
1. Add the pattern enum to `NeoPatternService.h`
2. Implement the pattern logic in `NeoPattern.cpp`
3. Add the pattern updater to `registerPatterns()` in `NeoPatternService.cpp`
4. Update the pattern mapping in `nameToPatternMap`
## Troubleshooting
- **LEDs not lighting**: Check wiring and power supply
- **Patterns not updating**: Verify the update interval and task registration
- **API not responding**: Check network configuration and Spore framework setup
- **Memory issues**: Reduce LED count or pattern complexity
## Dependencies
- Spore Framework
- Adafruit NeoPixel library
- ArduinoJson
- ESP32 Arduino Core

View File

@@ -1,31 +0,0 @@
#ifndef __DEVICE_CONFIG__
#define __DEVICE_CONFIG__
// Scheduler config
#define _TASK_SLEEP_ON_IDLE_RUN
#define _TASK_STD_FUNCTION
#define _TASK_PRIORITY
// Chip config
#define SPROCKET_TYPE "SPROCKET"
#define SERIAL_BAUD_RATE 115200
#define STARTUP_DELAY 1000
// network config
#define SPROCKET_MODE 1
#define WIFI_CHANNEL 11
#define AP_SSID "sprocket"
#define AP_PASSWORD "th3r31sn0sp00n"
#define STATION_SSID "MyAP"
#define STATION_PASSWORD "th3r31sn0sp00n"
#define HOSTNAME "sprocket"
#define CONNECT_TIMEOUT 10000
// NeoPixel conig
#define LED_STRIP_PIN D2
#define LED_STRIP_LENGTH 8
#define LED_STRIP_BRIGHTNESS 48
#define LED_STRIP_UPDATE_INTERVAL 200
#define LED_STRIP_DEFAULT_COLOR 100
#endif

View File

@@ -1,26 +1,32 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include "NeoPatternService.h"
#include "NeoPixelConfig.h"
#ifndef LED_STRIP_PIN
#define LED_STRIP_PIN 2
// Configuration constants
#ifndef NEOPIXEL_PIN
#define NEOPIXEL_PIN 2
#endif
#ifndef LED_STRIP_LENGTH
#define LED_STRIP_LENGTH 8
#ifndef NEOPIXEL_LENGTH
#define NEOPIXEL_LENGTH 8
#endif
#ifndef LED_STRIP_TYPE
#define LED_STRIP_TYPE (NEO_GRB + NEO_KHZ800)
#ifndef NEOPIXEL_BRIGHTNESS
#define NEOPIXEL_BRIGHTNESS 100
#endif
#ifndef NEOPIXEL_UPDATE_INTERVAL
#define NEOPIXEL_UPDATE_INTERVAL 100
#endif
// Create Spore instance with custom labels
Spore spore({
{"app", "neopattern"},
{"device", "light"},
{"pixels", String(LED_STRIP_LENGTH)},
{"pin", String(LED_STRIP_PIN)}
{"role", "led"},
{"pixels", String(NEOPIXEL_LENGTH)},
{"pin", String(NEOPIXEL_PIN)}
});
// Create custom service
@@ -30,17 +36,25 @@ void setup() {
// Initialize the Spore framework
spore.setup();
// Create configuration
NeoPixelConfig config(
NEOPIXEL_PIN,
NEOPIXEL_LENGTH,
NEOPIXEL_BRIGHTNESS,
NEOPIXEL_UPDATE_INTERVAL
);
// Create and add custom service
neoPatternService = new NeoPatternService(spore.getTaskManager(), LED_STRIP_LENGTH, LED_STRIP_PIN, LED_STRIP_TYPE);
neoPatternService = new NeoPatternService(spore.getTaskManager(), config);
spore.addService(neoPatternService);
// Start the API server and complete initialization
spore.begin();
LOG_INFO(spore.getContext(), "Main", "NeoPattern service registered and ready!");
LOG_INFO("Main", "NeoPattern service registered and ready!");
}
void loop() {
// Run the Spore framework loop
spore.loop();
}
}

View File

@@ -1,293 +0,0 @@
#include "NeoPixelService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
// Wheel helper: map 0-255 to RGB rainbow
static uint32_t colorWheel(Adafruit_NeoPixel& strip, uint8_t pos) {
pos = 255 - pos;
if (pos < 85) {
return strip.Color(255 - pos * 3, 0, pos * 3);
}
if (pos < 170) {
pos -= 85;
return strip.Color(0, pos * 3, 255 - pos * 3);
}
pos -= 170;
return strip.Color(pos * 3, 255 - pos * 3, 0);
}
NeoPixelService::NeoPixelService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, neoPixelType type)
: taskManager(taskMgr),
strip(numPixels, pin, type),
currentPattern(Pattern::Off),
updateIntervalMs(20),
lastUpdateMs(0),
wipeIndex(0),
wipeColor(strip.Color(255, 0, 0)),
rainbowJ(0),
cycleJ(0),
chaseJ(0),
chaseQ(0),
chasePhaseOn(true),
chaseColor(strip.Color(127, 127, 127)),
brightness(50) {
strip.begin();
strip.setBrightness(brightness);
strip.show();
registerPatterns();
registerTasks();
}
void NeoPixelService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/neopixel/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/neopixel/patterns", HTTP_GET,
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/neopixel", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
ParamSpec{String("interval_ms"), false, String("body"), String("number"), {}, String("100")},
ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("50")},
ParamSpec{String("color"), false, String("body"), String("color"), {}},
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
ParamSpec{String("r"), false, String("body"), String("number"), {}},
ParamSpec{String("g"), false, String("body"), String("number"), {}},
ParamSpec{String("b"), false, String("body"), String("number"), {}},
ParamSpec{String("r2"), false, String("body"), String("number"), {}},
ParamSpec{String("g2"), false, String("body"), String("number"), {}},
ParamSpec{String("b2"), false, String("body"), String("number"), {}},
ParamSpec{String("total_steps"), false, String("body"), String("number"), {}},
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}}
});
}
void NeoPixelService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["pin"] = strip.getPin();
doc["count"] = strip.numPixels();
doc["interval_ms"] = updateIntervalMs;
doc["brightness"] = brightness;
doc["pattern"] = currentPatternName();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NeoPixelService::handlePatternsRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc.to<JsonArray>();
for (auto& kv : patternUpdaters) arr.add(kv.first);
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
void NeoPixelService::handleControlRequest(AsyncWebServerRequest* request) {
if (request->hasParam("pattern", true)) {
String name = request->getParam("pattern", true)->value();
setPatternByName(name);
}
if (request->hasParam("interval_ms", true)) {
unsigned long v = request->getParam("interval_ms", true)->value().toInt();
if (v < 1) v = 1;
updateIntervalMs = v;
taskManager.setTaskInterval("neopixel_update", updateIntervalMs);
}
if (request->hasParam("brightness", true)) {
int b = request->getParam("brightness", true)->value().toInt();
if (b < 0) b = 0;
if (b > 255) b = 255;
setBrightness((uint8_t)b);
}
// Accept packed color ints or r,g,b triplets
if (request->hasParam("color", true)) {
wipeColor = (uint32_t)strtoul(request->getParam("color", true)->value().c_str(), nullptr, 0);
chaseColor = wipeColor;
}
if (request->hasParam("r", true) || request->hasParam("g", true) || request->hasParam("b", true)) {
int r = request->hasParam("r", true) ? request->getParam("r", true)->value().toInt() : 0;
int g = request->hasParam("g", true) ? request->getParam("g", true)->value().toInt() : 0;
int b = request->hasParam("b", true) ? request->getParam("b", true)->value().toInt() : 0;
wipeColor = strip.Color(r, g, b);
chaseColor = strip.Color(r / 2, g / 2, b / 2); // dimmer for chase
}
JsonDocument resp;
resp["ok"] = true;
resp["pattern"] = currentPatternName();
resp["interval_ms"] = updateIntervalMs;
resp["brightness"] = brightness;
String json;
serializeJson(resp, json);
request->send(200, "application/json", json);
}
void NeoPixelService::setBrightness(uint8_t b) {
brightness = b;
strip.setBrightness(brightness);
strip.show();
}
void NeoPixelService::registerTasks() {
taskManager.registerTask("neopixel_update", updateIntervalMs, [this]() { update(); });
taskManager.registerTask("neopixel_status_print", 10000, [this]() {
Serial.printf("[NeoPixel] pattern=%s interval=%lu ms brightness=%u\n",
currentPatternName().c_str(), updateIntervalMs, brightness);
});
}
void NeoPixelService::registerPatterns() {
patternUpdaters["off"] = [this]() { updateOff(); };
patternUpdaters["color_wipe"] = [this]() { updateColorWipe(); };
patternUpdaters["rainbow"] = [this]() { updateRainbow(); };
patternUpdaters["rainbow_cycle"] = [this]() { updateRainbowCycle(); };
patternUpdaters["theater_chase"] = [this]() { updateTheaterChase(); };
patternUpdaters["theater_chase_rainbow"] = [this]() { updateTheaterChaseRainbow(); };
}
std::vector<String> NeoPixelService::patternNamesVector() const {
std::vector<String> v;
v.reserve(patternUpdaters.size());
for (const auto& kv : patternUpdaters) v.push_back(kv.first);
return v;
}
String NeoPixelService::currentPatternName() const {
switch (currentPattern) {
case Pattern::Off: return String("off");
case Pattern::ColorWipe: return String("color_wipe");
case Pattern::Rainbow: return String("rainbow");
case Pattern::RainbowCycle: return String("rainbow_cycle");
case Pattern::TheaterChase: return String("theater_chase");
case Pattern::TheaterChaseRainbow: return String("theater_chase_rainbow");
}
return String("off");
}
NeoPixelService::Pattern NeoPixelService::nameToPattern(const String& name) const {
if (name.equalsIgnoreCase("color_wipe")) return Pattern::ColorWipe;
if (name.equalsIgnoreCase("rainbow")) return Pattern::Rainbow;
if (name.equalsIgnoreCase("rainbow_cycle")) return Pattern::RainbowCycle;
if (name.equalsIgnoreCase("theater_chase")) return Pattern::TheaterChase;
if (name.equalsIgnoreCase("theater_chase_rainbow")) return Pattern::TheaterChaseRainbow;
return Pattern::Off;
}
void NeoPixelService::setPatternByName(const String& name) {
Pattern p = nameToPattern(name);
resetStateForPattern(p);
}
void NeoPixelService::resetStateForPattern(Pattern p) {
strip.clear();
strip.show();
wipeIndex = 0;
rainbowJ = 0;
cycleJ = 0;
chaseJ = 0;
chaseQ = 0;
chasePhaseOn = true;
lastUpdateMs = 0;
currentPattern = p;
}
void NeoPixelService::update() {
unsigned long now = millis();
if (now - lastUpdateMs < updateIntervalMs) return;
lastUpdateMs = now;
const String name = currentPatternName();
auto it = patternUpdaters.find(name);
if (it != patternUpdaters.end()) {
it->second();
} else {
updateOff();
}
}
void NeoPixelService::updateOff() {
strip.clear();
strip.show();
}
void NeoPixelService::updateColorWipe() {
if (wipeIndex < strip.numPixels()) {
strip.setPixelColor(wipeIndex, wipeColor);
++wipeIndex;
strip.show();
} else {
strip.clear();
wipeIndex = 0;
}
}
void NeoPixelService::updateRainbow() {
for (uint16_t i = 0; i < strip.numPixels(); i++) {
strip.setPixelColor(i, colorWheel(strip, (i + rainbowJ) & 255));
}
strip.show();
rainbowJ = (rainbowJ + 1) & 0xFF;
}
void NeoPixelService::updateRainbowCycle() {
for (uint16_t i = 0; i < strip.numPixels(); i++) {
uint8_t pos = ((i * 256 / strip.numPixels()) + cycleJ) & 0xFF;
strip.setPixelColor(i, colorWheel(strip, pos));
}
strip.show();
cycleJ = (cycleJ + 1) & 0xFF;
}
void NeoPixelService::updateTheaterChase() {
if (chasePhaseOn) {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, chaseColor);
}
strip.show();
chasePhaseOn = false;
} else {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, 0);
}
strip.show();
chasePhaseOn = true;
chaseQ = (chaseQ + 1) % 3;
chaseJ = (chaseJ + 1) % 10;
}
}
void NeoPixelService::updateTheaterChaseRainbow() {
if (chasePhaseOn) {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, colorWheel(strip, (idx + chaseJ) % 255));
}
strip.show();
chasePhaseOn = false;
} else {
for (uint16_t i = 0; i < strip.numPixels(); i += 3) {
uint16_t idx = i + chaseQ;
if (idx < strip.numPixels()) strip.setPixelColor(idx, 0);
}
strip.show();
chasePhaseOn = true;
chaseQ = (chaseQ + 1) % 3;
chaseJ = (chaseJ + 1) & 0xFF;
}
}

View File

@@ -1,67 +0,0 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/TaskManager.h"
#include <Adafruit_NeoPixel.h>
#include <map>
#include <vector>
class NeoPixelService : public Service {
public:
enum class Pattern {
Off,
ColorWipe,
Rainbow,
RainbowCycle,
TheaterChase,
TheaterChaseRainbow
};
NeoPixelService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, neoPixelType type);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "NeoPixel"; }
void setPatternByName(const String& name);
void setBrightness(uint8_t b);
private:
void registerTasks();
void registerPatterns();
std::vector<String> patternNamesVector() const;
String currentPatternName() const;
Pattern nameToPattern(const String& name) const;
void resetStateForPattern(Pattern p);
void update();
// Pattern updaters
void updateOff();
void updateColorWipe();
void updateRainbow();
void updateRainbowCycle();
void updateTheaterChase();
void updateTheaterChaseRainbow();
// Handlers
void handleStatusRequest(AsyncWebServerRequest* request);
void handlePatternsRequest(AsyncWebServerRequest* request);
void handleControlRequest(AsyncWebServerRequest* request);
TaskManager& taskManager;
Adafruit_NeoPixel strip;
std::map<String, std::function<void()>> patternUpdaters;
Pattern currentPattern;
unsigned long updateIntervalMs;
unsigned long lastUpdateMs;
// State for patterns
uint16_t wipeIndex;
uint32_t wipeColor;
uint8_t rainbowJ;
uint8_t cycleJ;
int chaseJ;
int chaseQ;
bool chasePhaseOn;
uint32_t chaseColor;
uint8_t brightness;
};

View File

@@ -1,46 +0,0 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "NeoPixelService.h"
#ifndef NEOPIXEL_PIN
#define NEOPIXEL_PIN 2
#endif
#ifndef NEOPIXEL_COUNT
#define NEOPIXEL_COUNT 16
#endif
#ifndef NEOPIXEL_TYPE
#define NEOPIXEL_TYPE (NEO_GRB + NEO_KHZ800)
#endif
// Create Spore instance with custom labels
Spore spore({
{"app", "neopixel"},
{"device", "light"},
{"pixels", String(NEOPIXEL_COUNT)},
{"pin", String(NEOPIXEL_PIN)}
});
// Create custom service
NeoPixelService* neoPixelService = nullptr;
void setup() {
// Initialize the Spore framework
spore.setup();
// Create and add custom service
neoPixelService = new NeoPixelService(spore.getTaskManager(), NEOPIXEL_COUNT, NEOPIXEL_PIN, NEOPIXEL_TYPE);
spore.addService(neoPixelService);
// Start the API server and complete initialization
spore.begin();
LOG_INFO(spore.getContext(), "Main", "NeoPixel service registered and ready!");
}
void loop() {
// Run the Spore framework loop
spore.loop();
}

View File

@@ -1,6 +1,13 @@
# Relay Service Example
A minimal example that demonstrates the Spore framework with a custom RelayService. The Spore framework automatically handles all core functionality (WiFi, clustering, API server, task management) while allowing easy registration of custom services.
A minimal example that demonstrates the Spore framework with a custom RelayService and web interface. The Spore framework automatically handles all core functionality (WiFi, clustering, API server, task management) while allowing easy registration of custom services.
## Features
- **API Control**: RESTful API endpoints for programmatic control
- **Web Interface**: Beautiful web UI for manual control at `http://<device-ip>/relay.html`
- **Real-time Status**: Live status updates and visual feedback
- **Toggle Functionality**: One-click toggle between ON/OFF states
- Default relay pin: `GPIO0` (ESP-01). Override with `-DRELAY_PIN=<pin>`.
- WiFi and API port are configured in `src/Config.cpp`.
@@ -25,7 +32,7 @@ RelayService* relayService = nullptr;
void setup() {
spore.setup();
relayService = new RelayService(spore.getTaskManager(), RELAY_PIN);
relayService = new RelayService(spore.getContext(), spore.getTaskManager(), RELAY_PIN);
spore.addService(relayService);
spore.begin();
@@ -42,6 +49,7 @@ The Spore framework automatically provides:
- REST API server with core endpoints
- Task scheduling and execution
- Node status monitoring
- Static file serving for web interfaces (core service)
## Build & Upload
@@ -61,6 +69,20 @@ pio device monitor -b 115200
Assume the device IP is 192.168.1.50 below (replace with your device's IP shown in serial output).
## Web Interface
The web interface is located in the `data/` folder and will be served by the core StaticFileService.
Access the web interface at: `http://192.168.1.50/relay.html`
The web interface provides:
- Visual status indicator (red for OFF, green for ON)
- Turn ON/OFF buttons
- Toggle button for quick switching
- Real-time status updates every 2 seconds
- Uptime display
- Error handling and user feedback
## Relay API
- Get relay status

View File

@@ -1,6 +1,6 @@
#include "RelayService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
RelayService::RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin)
: ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) {
@@ -65,13 +65,13 @@ void RelayService::turnOn() {
relayOn = true;
// Active LOW relay
digitalWrite(relayPin, LOW);
LOG_INFO(ctx, "RelayService", "Relay ON");
LOG_INFO("RelayService", "Relay ON");
}
void RelayService::turnOff() {
relayOn = false;
digitalWrite(relayPin, HIGH);
LOG_INFO(ctx, "RelayService", "Relay OFF");
LOG_INFO("RelayService", "Relay OFF");
}
void RelayService::toggle() {
@@ -84,6 +84,6 @@ void RelayService::toggle() {
void RelayService::registerTasks() {
taskManager.registerTask("relay_status_print", 5000, [this]() {
LOG_INFO(ctx, "RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF"));
LOG_INFO("RelayService", "Status - pin: " + String(relayPin) + ", state: " + (relayOn ? "ON" : "OFF"));
});
}

View File

@@ -0,0 +1,305 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Relay Control - Spore</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
max-width: 400px;
width: 90%;
}
h1 {
margin-bottom: 30px;
font-size: 2.2em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.relay-status {
margin-bottom: 30px;
}
.status-indicator {
width: 120px;
height: 120px;
border-radius: 50%;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
font-weight: bold;
transition: all 0.3s ease;
border: 4px solid rgba(255, 255, 255, 0.3);
}
.status-indicator.off {
background: rgba(255, 0, 0, 0.3);
color: #ff6b6b;
}
.status-indicator.on {
background: rgba(0, 255, 0, 0.3);
color: #51cf66;
box-shadow: 0 0 20px rgba(81, 207, 102, 0.5);
}
.status-text {
font-size: 1.5em;
margin-bottom: 10px;
}
.pin-info {
font-size: 0.9em;
opacity: 0.8;
}
.controls {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 25px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
min-width: 100px;
text-transform: uppercase;
letter-spacing: 1px;
}
.btn-primary {
background: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.btn-primary:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-success {
background: rgba(81, 207, 102, 0.8);
color: white;
border: 2px solid rgba(81, 207, 102, 1);
}
.btn-success:hover {
background: rgba(81, 207, 102, 1);
transform: translateY(-2px);
}
.btn-danger {
background: rgba(255, 107, 107, 0.8);
color: white;
border: 2px solid rgba(255, 107, 107, 1);
}
.btn-danger:hover {
background: rgba(255, 107, 107, 1);
transform: translateY(-2px);
}
.loading {
opacity: 0.6;
pointer-events: none;
}
.error {
color: #ff6b6b;
margin-top: 15px;
font-size: 0.9em;
}
.success {
color: #51cf66;
margin-top: 15px;
font-size: 0.9em;
}
.uptime {
margin-top: 20px;
font-size: 0.8em;
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<h1>🔌 Relay Control</h1>
<div class="relay-status">
<div class="status-indicator off" id="statusIndicator">
OFF
</div>
<div class="status-text" id="statusText">Relay is OFF</div>
<div class="pin-info" id="pinInfo">Pin: Loading...</div>
</div>
<div class="controls">
<button class="btn btn-success" id="turnOnBtn" onclick="controlRelay('on')">
Turn ON
</button>
<button class="btn btn-danger" id="turnOffBtn" onclick="controlRelay('off')">
Turn OFF
</button>
<button class="btn btn-primary" id="toggleBtn" onclick="controlRelay('toggle')">
Toggle
</button>
</div>
<div id="message"></div>
<div class="uptime" id="uptime"></div>
</div>
<script>
let currentState = 'off';
let relayPin = '';
// Load initial relay status
async function loadRelayStatus() {
try {
const response = await fetch('/api/relay/status');
if (!response.ok) {
throw new Error('Failed to fetch relay status');
}
const data = await response.json();
currentState = data.state;
relayPin = data.pin;
updateUI();
updateUptime(data.uptime);
} catch (error) {
console.error('Error loading relay status:', error);
showMessage('Error loading relay status: ' + error.message, 'error');
}
}
// Control relay
async function controlRelay(action) {
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => btn.classList.add('loading'));
try {
const response = await fetch('/api/relay', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `state=${action}`
});
const data = await response.json();
if (data.success) {
currentState = data.state;
updateUI();
showMessage(`Relay turned ${data.state.toUpperCase()}`, 'success');
} else {
showMessage(data.message || 'Failed to control relay', 'error');
}
} catch (error) {
console.error('Error controlling relay:', error);
showMessage('Error controlling relay: ' + error.message, 'error');
} finally {
buttons.forEach(btn => btn.classList.remove('loading'));
}
}
// Update UI based on current state
function updateUI() {
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const pinInfo = document.getElementById('pinInfo');
if (currentState === 'on') {
statusIndicator.className = 'status-indicator on';
statusIndicator.textContent = 'ON';
statusText.textContent = 'Relay is ON';
} else {
statusIndicator.className = 'status-indicator off';
statusIndicator.textContent = 'OFF';
statusText.textContent = 'Relay is OFF';
}
if (relayPin) {
pinInfo.textContent = `Pin: ${relayPin}`;
}
}
// Update uptime display
function updateUptime(uptime) {
const uptimeElement = document.getElementById('uptime');
if (uptime) {
const seconds = Math.floor(uptime / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
let uptimeText = `Uptime: ${seconds}s`;
if (hours > 0) {
uptimeText = `Uptime: ${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
uptimeText = `Uptime: ${minutes}m ${seconds % 60}s`;
}
uptimeElement.textContent = uptimeText;
}
}
// Show message to user
function showMessage(message, type) {
const messageElement = document.getElementById('message');
messageElement.textContent = message;
messageElement.className = type;
// Clear message after 3 seconds
setTimeout(() => {
messageElement.textContent = '';
messageElement.className = '';
}, 3000);
}
// Load status on page load
loadRelayStatus();
// Refresh status every 2 seconds
setInterval(loadRelayStatus, 2000);
</script>
</body>
</html>

View File

@@ -0,0 +1,177 @@
// Only initialize if not already done
if (!window.relayControlInitialized) {
class RelayControl {
constructor() {
this.currentState = 'off';
this.relayPin = '';
this.init();
}
init() {
this.createComponent();
this.loadRelayStatus();
this.setupEventListeners();
// Refresh status every 2 seconds
setInterval(() => this.loadRelayStatus(), 2000);
}
createComponent() {
// Create the main container
const container = document.createElement('div');
container.className = 'relay-component';
container.innerHTML = `
<h1>🔌 Relay Control</h1>
<div class="relay-status">
<div class="status-indicator off" id="statusIndicator">
OFF
</div>
<div class="status-text" id="statusText">Relay is OFF</div>
<div class="pin-info" id="pinInfo">Pin: Loading...</div>
</div>
<div class="controls">
<button class="btn btn-success" id="turnOnBtn">
Turn ON
</button>
<button class="btn btn-danger" id="turnOffBtn">
Turn OFF
</button>
<button class="btn btn-primary" id="toggleBtn">
Toggle
</button>
</div>
<div id="message"></div>
<div class="uptime" id="uptime"></div>
`;
// Append to component div
const componentDiv = document.getElementById('component');
if (componentDiv) {
componentDiv.appendChild(container);
} else {
// Fallback to body if component div not found
document.body.appendChild(container);
}
}
setupEventListeners() {
document.getElementById('turnOnBtn').addEventListener('click', () => this.controlRelay('on'));
document.getElementById('turnOffBtn').addEventListener('click', () => this.controlRelay('off'));
document.getElementById('toggleBtn').addEventListener('click', () => this.controlRelay('toggle'));
}
// Load initial relay status
async loadRelayStatus() {
try {
const response = await fetch('/api/relay/status');
if (!response.ok) {
throw new Error('Failed to fetch relay status');
}
const data = await response.json();
this.currentState = data.state;
this.relayPin = data.pin;
this.updateUI();
this.updateUptime(data.uptime);
} catch (error) {
console.error('Error loading relay status:', error);
this.showMessage('Error loading relay status: ' + error.message, 'error');
}
}
// Control relay
async controlRelay(action) {
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => btn.classList.add('loading'));
try {
const response = await fetch('/api/relay', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `state=${action}`
});
const data = await response.json();
if (data.success) {
this.currentState = data.state;
this.updateUI();
this.showMessage(`Relay turned ${data.state.toUpperCase()}`, 'success');
} else {
this.showMessage(data.message || 'Failed to control relay', 'error');
}
} catch (error) {
console.error('Error controlling relay:', error);
this.showMessage('Error controlling relay: ' + error.message, 'error');
} finally {
buttons.forEach(btn => btn.classList.remove('loading'));
}
}
// Update UI based on current state
updateUI() {
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
const pinInfo = document.getElementById('pinInfo');
if (this.currentState === 'on') {
statusIndicator.className = 'status-indicator on';
statusIndicator.textContent = 'ON';
statusText.textContent = 'Relay is ON';
} else {
statusIndicator.className = 'status-indicator off';
statusIndicator.textContent = 'OFF';
statusText.textContent = 'Relay is OFF';
}
if (this.relayPin) {
pinInfo.textContent = `Pin: ${this.relayPin}`;
}
}
// Update uptime display
updateUptime(uptime) {
const uptimeElement = document.getElementById('uptime');
if (uptime) {
const seconds = Math.floor(uptime / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
let uptimeText = `Uptime: ${seconds}s`;
if (hours > 0) {
uptimeText = `Uptime: ${hours}h ${minutes % 60}m ${seconds % 60}s`;
} else if (minutes > 0) {
uptimeText = `Uptime: ${minutes}m ${seconds % 60}s`;
}
uptimeElement.textContent = uptimeText;
}
}
// Show message to user
showMessage(message, type) {
const messageElement = document.getElementById('message');
messageElement.textContent = message;
messageElement.className = type;
// Clear message after 3 seconds
setTimeout(() => {
messageElement.textContent = '';
messageElement.className = '';
}, 3000);
}
}
// Initialize the relay control when the page loads
document.addEventListener('DOMContentLoaded', () => {
new RelayControl();
});
window.relayControlInitialized = true;
}

View File

@@ -0,0 +1,141 @@
.relay-component {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 40px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
text-align: center;
max-width: 400px;
width: 100%;
color: white;
box-sizing: border-box;
}
.relay-component h1 {
margin: 0 0 30px 0;
font-size: 2.2em;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.relay-component .relay-status {
margin-bottom: 30px;
}
.relay-component .status-indicator {
width: 120px;
height: 120px;
border-radius: 50%;
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
font-weight: bold;
transition: all 0.3s ease;
border: 4px solid rgba(255, 255, 255, 0.3);
}
.relay-component .status-indicator.off {
background: rgba(255, 0, 0, 0.3);
color: #ff6b6b;
}
.relay-component .status-indicator.on {
background: rgba(0, 255, 0, 0.3);
color: #51cf66;
box-shadow: 0 0 20px rgba(81, 207, 102, 0.5);
}
.relay-component .status-text {
font-size: 1.5em;
margin-bottom: 10px;
}
.relay-component .pin-info {
font-size: 0.9em;
opacity: 0.8;
}
.relay-component .controls {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.relay-component .btn {
padding: 12px 24px;
border: none;
border-radius: 25px;
font-size: 1em;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
min-width: 100px;
text-transform: uppercase;
letter-spacing: 1px;
}
.relay-component .btn-primary {
background: rgba(255, 255, 255, 0.2);
color: #333;
border: 2px solid rgba(0, 0, 0, 0.8);
font-weight: bold;
}
.relay-component .btn-primary:hover {
background: rgba(255, 255, 255, 0.4);
color: #222;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}
.relay-component .btn-primary:active {
transform: translateY(0);
}
.relay-component .btn-success {
background: rgba(81, 207, 102, 0.8);
color: white;
border: 2px solid rgba(81, 207, 102, 1);
}
.relay-component .btn-success:hover {
background: rgba(81, 207, 102, 1);
transform: translateY(-2px);
}
.relay-component .btn-danger {
background: rgba(255, 107, 107, 0.8);
color: white;
border: 2px solid rgba(255, 107, 107, 1);
}
.relay-component .btn-danger:hover {
background: rgba(255, 107, 107, 1);
transform: translateY(-2px);
}
.relay-component .loading {
opacity: 0.6;
pointer-events: none;
}
.relay-component .error {
color: #ff6b6b;
margin-top: 15px;
font-size: 0.9em;
}
.relay-component .success {
color: #51cf66;
margin-top: 15px;
font-size: 0.9em;
}
.relay-component .uptime {
margin-top: 20px;
font-size: 0.8em;
opacity: 0.7;
}

View File

@@ -1,6 +1,5 @@
#include <Arduino.h>
#include "spore/Spore.h"
#include "spore/services/LoggingService.h"
#include "RelayService.h"
// Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board.
@@ -29,7 +28,8 @@ void setup() {
// Start the API server and complete initialization
spore.begin();
LOG_INFO(spore.getContext(), "Main", "Relay service registered and ready!");
LOG_INFO("Main", "Relay service registered and ready!");
LOG_INFO("Main", "Web interface available at http://<node-ip>/relay.html");
}
void loop() {

View File

@@ -1,82 +0,0 @@
# Static Web Server Example
This example demonstrates how to serve static HTML files from LittleFS using the Spore framework.
## Features
- Serves static files from LittleFS filesystem
- Beautiful web interface with real-time status updates
- Responsive design that works on mobile and desktop
- Automatic API endpoint discovery and status monitoring
## Files
- `main.cpp` - Main application entry point
- `data/index.html` - Web interface (served from LittleFS)
## Web Interface
The web interface provides:
- **Node Status** - Shows uptime and system information
- **Network Status** - Displays WiFi connection status
- **Task Status** - Shows active background tasks
- **Cluster Status** - Displays cluster membership
- **API Links** - Quick access to all available API endpoints
## Building and Flashing
### Build for D1 Mini (recommended for web interface)
```bash
# Build the firmware
pio run -e d1_mini_static_web
# Flash to device
pio run -e d1_mini_static_web -t upload
```
### Upload Files to LittleFS
After flashing the firmware, you need to upload the HTML files to the LittleFS filesystem:
```bash
# Upload data directory to LittleFS
pio run -e d1_mini_static_web -t uploadfs
```
## Usage
1. Flash the firmware to your ESP8266 device
2. Upload the data files to LittleFS
3. Connect to the device's WiFi network
4. Open a web browser and navigate to `http://<device-ip>/`
5. The web interface will load and display real-time status information
## API Endpoints
The web interface automatically discovers and displays links to all available API endpoints:
- `/api/node/status` - Node status and system information
- `/api/network/status` - Network and WiFi status
- `/api/tasks/status` - Task management status
- `/api/cluster/members` - Cluster membership
- `/api/node/endpoints` - Complete API endpoint list
## Customization
To customize the web interface:
1. Modify `data/index.html` with your desired content
2. Add additional static files (CSS, JS, images) to the `data/` directory
3. Re-upload the files using `pio run -e d1_mini_static_web -t uploadfs`
## File Structure
```
data/
├── index.html # Main web interface
└── (other static files) # Additional web assets
```
The StaticFileService automatically serves files from the LittleFS root directory, so any files placed in the `data/` directory will be accessible via HTTP.

View File

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

View File

@@ -10,6 +10,8 @@
#include "core/ApiServer.h"
#include "core/TaskManager.h"
#include "Service.h"
#include "util/Logging.h"
#include "util/CpuUsage.h"
class Spore {
public:
@@ -32,6 +34,11 @@ public:
TaskManager& getTaskManager() { return taskManager; }
ClusterManager& getCluster() { return cluster; }
ApiServer& getApiServer() { return apiServer; }
// CPU usage monitoring
CpuUsage& getCpuUsage() { return cpuUsage; }
float getCurrentCpuUsage() const { return cpuUsage.getCpuUsage(); }
float getAverageCpuUsage() const { return cpuUsage.getAverageCpuUsage(); }
private:
@@ -44,6 +51,7 @@ private:
TaskManager taskManager;
ClusterManager cluster;
ApiServer apiServer;
CpuUsage cpuUsage;
std::vector<std::shared_ptr<Service>> services;
bool initialized;

View File

@@ -11,6 +11,7 @@
#include "spore/types/NodeInfo.h"
#include "spore/core/TaskManager.h"
#include "spore/types/ApiTypes.h"
#include "spore/util/Logging.h"
class Service; // Forward declaration
@@ -19,15 +20,20 @@ public:
ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80);
void begin();
void addService(Service& service);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params);
const String& serviceName = "unknown");
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params);
const String& serviceName = "unknown");
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params, const String& serviceName = "unknown");
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params, const String& serviceName = "unknown");
// Static file serving
void serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header = "");
static const char* methodToStr(int method);

View File

@@ -1,72 +0,0 @@
#pragma once
#include "spore/Service.h"
#include "spore/core/NodeContext.h"
#include "spore/core/ApiServer.h"
#include <Arduino.h>
enum class LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
};
struct LogData {
LogLevel level;
String component;
String message;
unsigned long timestamp;
LogData(LogLevel lvl, const String& comp, const String& msg)
: level(lvl), component(comp), message(msg), timestamp(millis()) {}
};
class LoggingService : public Service {
public:
LoggingService(NodeContext& ctx, ApiServer& apiServer);
virtual ~LoggingService() = default;
// Service interface
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "logging"; }
// Logging methods
void log(LogLevel level, const String& component, const String& message);
void debug(const String& component, const String& message);
void info(const String& component, const String& message);
void warn(const String& component, const String& message);
void error(const String& component, const String& message);
// Configuration
void setLogLevel(LogLevel level);
void enableSerialLogging(bool enable);
private:
NodeContext& ctx;
ApiServer& apiServer;
LogLevel currentLogLevel;
bool serialLoggingEnabled;
void setupEventHandlers();
void handleSerialLog(void* data);
String formatLogMessage(const LogData& logData);
String logLevelToString(LogLevel level);
// API endpoint handlers
void handleGetLogLevel(AsyncWebServerRequest* request);
void handleSetLogLevel(AsyncWebServerRequest* request);
void handleGetConfig(AsyncWebServerRequest* request);
};
// Global logging functions that use the event system directly
void logDebug(NodeContext& ctx, const String& component, const String& message);
void logInfo(NodeContext& ctx, const String& component, const String& message);
void logWarn(NodeContext& ctx, const String& component, const String& message);
void logError(NodeContext& ctx, const String& component, const String& message);
// Convenience macros for easy logging (requires ctx to be available)
#define LOG_DEBUG(ctx, component, message) logDebug(ctx, component, message)
#define LOG_INFO(ctx, component, message) logInfo(ctx, component, message)
#define LOG_WARN(ctx, component, message) logWarn(ctx, component, message)
#define LOG_ERROR(ctx, component, message) logError(ctx, component, message)

View File

@@ -0,0 +1,51 @@
#pragma once
#include "spore/Service.h"
#include "spore/util/CpuUsage.h"
#include <functional>
class MonitoringService : public Service {
public:
MonitoringService(CpuUsage& cpuUsage);
void registerEndpoints(ApiServer& api) override;
const char* getName() const override { return "Monitoring"; }
// System resource information
struct SystemResources {
// CPU information
float currentCpuUsage;
float averageCpuUsage;
float maxCpuUsage;
float minCpuUsage;
unsigned long measurementCount;
bool isMeasuring;
// Memory information
size_t freeHeap;
size_t totalHeap;
size_t minFreeHeap;
size_t maxAllocHeap;
size_t heapFragmentation;
// Filesystem information
size_t totalBytes;
size_t usedBytes;
size_t freeBytes;
float usagePercent;
// System uptime
unsigned long uptimeMs;
unsigned long uptimeSeconds;
};
// Get current system resources
SystemResources getSystemResources() const;
private:
void handleResourcesRequest(AsyncWebServerRequest* request);
// Helper methods
size_t calculateHeapFragmentation() const;
void getFilesystemInfo(size_t& totalBytes, size_t& usedBytes) const;
CpuUsage& cpuUsage;
};

View File

@@ -15,10 +15,4 @@ private:
static const String name;
NodeContext& ctx;
ApiServer& apiServer;
void handleRootRequest(AsyncWebServerRequest* request);
void handleStaticFileRequest(AsyncWebServerRequest* request);
void handleNotFound(AsyncWebServerRequest* request);
String getContentType(const String& filename);
bool fileExists(const String& path);
};

View File

@@ -0,0 +1,114 @@
#pragma once
#include "spore/util/JsonSerializable.h"
#include "spore/util/Logging.h"
#include <ArduinoJson.h>
namespace spore {
namespace types {
/**
* Base class for API responses that can be serialized to JSON
* Handles complete JsonDocument creation and serialization
*/
class ApiResponse {
protected:
mutable JsonDocument doc;
public:
virtual ~ApiResponse() = default;
/**
* Get the complete JSON string representation of this response
* @return JSON string ready to send as HTTP response
*/
virtual String toJsonString() const {
String json;
serializeJson(doc, json);
return json;
}
/**
* Get the JsonDocument for direct manipulation if needed
* @return Reference to the internal JsonDocument
*/
JsonDocument& getDocument() { return doc; }
const JsonDocument& getDocument() const { return doc; }
/**
* Clear the document and reset for reuse
*/
virtual void clear() {
doc.clear();
}
};
/**
* Base class for API responses that contain a collection of serializable items
*/
template<typename ItemType>
class CollectionResponse : public ApiResponse {
protected:
String collectionKey;
public:
explicit CollectionResponse(const String& key) : collectionKey(key) {}
/**
* Add a serializable item to the collection
* @param item The serializable item to add
*/
void addItem(const util::JsonSerializable& item) {
// Ensure the array exists and get a reference to it
if (!doc[collectionKey].is<JsonArray>()) {
doc[collectionKey] = JsonArray();
}
JsonArray arr = doc[collectionKey].as<JsonArray>();
JsonObject obj = arr.add<JsonObject>();
item.toJson(obj);
}
/**
* Add a serializable item to the collection (move version)
* @param item The serializable item to add
*/
void addItem(util::JsonSerializable&& item) {
// Ensure the array exists and get a reference to it
if (!doc[collectionKey].is<JsonArray>()) {
doc[collectionKey] = JsonArray();
}
JsonArray arr = doc[collectionKey].as<JsonArray>();
JsonObject obj = arr.add<JsonObject>();
item.toJson(obj);
}
/**
* Add multiple items from a container
* @param items Container of serializable items
*/
template<typename Container>
void addItems(const Container& items) {
// Ensure the array exists and get a reference to it
if (!doc[collectionKey].is<JsonArray>()) {
doc[collectionKey] = JsonArray();
}
JsonArray arr = doc[collectionKey].as<JsonArray>();
for (const auto& item : items) {
JsonObject obj = arr.add<JsonObject>();
item.toJson(obj);
}
}
/**
* Get the current number of items in the collection
* @return Number of items
*/
size_t getItemCount() const {
if (doc[collectionKey].is<JsonArray>()) {
return doc[collectionKey].as<JsonArray>().size();
}
return 0;
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,47 @@
#pragma once
#include "ApiResponse.h"
#include "NodeInfoSerializable.h"
#include <map>
namespace spore {
namespace types {
/**
* Response class for cluster members endpoint
* Handles complete JSON document creation for cluster member data
*/
class ClusterMembersResponse : public CollectionResponse<NodeInfoSerializable> {
public:
ClusterMembersResponse() : CollectionResponse("members") {}
/**
* Add a single node to the response
* @param node The NodeInfo to add
*/
void addNode(const NodeInfo& node) {
addItem(NodeInfoSerializable(const_cast<NodeInfo&>(node)));
}
/**
* Add multiple nodes from a member list
* @param memberList Map of hostname to NodeInfo
*/
void addNodes(const std::map<String, NodeInfo>& memberList) {
for (const auto& pair : memberList) {
addNode(pair.second);
}
}
/**
* Add nodes from a pointer to member list
* @param memberList Pointer to map of hostname to NodeInfo
*/
void addNodes(const std::map<String, NodeInfo>* memberList) {
if (memberList) {
addNodes(*memberList);
}
}
};
} // namespace types
} // namespace spore

View File

@@ -32,6 +32,11 @@ public:
unsigned long restart_delay_ms;
uint16_t json_doc_size;
// Memory Management
uint32_t low_memory_threshold_bytes;
uint32_t critical_memory_threshold_bytes;
size_t max_concurrent_http_requests;
// Constructor
Config();
};

View File

@@ -0,0 +1,76 @@
#pragma once
#include "ApiTypes.h"
#include "spore/util/JsonSerializable.h"
#include <ArduinoJson.h>
namespace spore {
namespace types {
/**
* Serializable wrapper for EndpointInfo that implements JsonSerializable interface
* Handles conversion between EndpointInfo struct and JSON representation
*/
class EndpointInfoSerializable : public util::JsonSerializable {
private:
const EndpointInfo& endpoint;
public:
explicit EndpointInfoSerializable(const EndpointInfo& ep) : endpoint(ep) {}
/**
* Serialize EndpointInfo to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["uri"] = endpoint.uri;
obj["method"] = endpoint.method;
// Add parameters if present
if (!endpoint.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& param : endpoint.params) {
JsonObject paramObj = paramsArr.add<JsonObject>();
paramObj["name"] = param.name;
paramObj["location"] = param.location;
paramObj["required"] = param.required;
paramObj["type"] = param.type;
if (!param.values.empty()) {
JsonArray valuesArr = paramObj["values"].to<JsonArray>();
for (const auto& value : param.values) {
valuesArr.add(value);
}
}
if (param.defaultValue.length() > 0) {
paramObj["default"] = param.defaultValue;
}
}
}
}
/**
* Deserialize EndpointInfo from JsonObject
*/
void fromJson(const JsonObject& obj) override {
// Note: This would require modifying the EndpointInfo struct to be mutable
// For now, this is a placeholder as EndpointInfo is typically read-only
// in the context where this serialization is used
}
};
/**
* Convenience function to create a JsonArray from a collection of EndpointInfo objects
*/
template<typename Container>
JsonArray endpointInfoToJsonArray(JsonDocument& doc, const Container& endpoints) {
JsonArray arr = doc.to<JsonArray>();
for (const auto& endpoint : endpoints) {
EndpointInfoSerializable serializable(endpoint);
JsonObject obj = arr.add<JsonObject>();
serializable.toJson(obj);
}
return arr;
}
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,141 @@
#pragma once
#include "NodeInfo.h"
#include "ApiTypes.h"
#include "spore/util/JsonSerializable.h"
#include <ArduinoJson.h>
namespace spore {
namespace types {
/**
* Serializable wrapper for NodeInfo that implements JsonSerializable interface
* Handles conversion between NodeInfo struct and JSON representation
*/
class NodeInfoSerializable : public util::JsonSerializable {
private:
NodeInfo& nodeInfo;
public:
explicit NodeInfoSerializable(NodeInfo& node) : nodeInfo(node) {}
/**
* Serialize NodeInfo to JsonObject
* Maps all NodeInfo fields to appropriate JSON structure
*/
void toJson(JsonObject& obj) const override {
obj["hostname"] = nodeInfo.hostname;
obj["ip"] = nodeInfo.ip.toString();
obj["lastSeen"] = nodeInfo.lastSeen;
obj["latency"] = nodeInfo.latency;
obj["status"] = statusToStr(nodeInfo.status);
// Serialize resources
JsonObject resources = obj["resources"].to<JsonObject>();
resources["freeHeap"] = nodeInfo.resources.freeHeap;
resources["chipId"] = nodeInfo.resources.chipId;
resources["sdkVersion"] = nodeInfo.resources.sdkVersion;
resources["cpuFreqMHz"] = nodeInfo.resources.cpuFreqMHz;
resources["flashChipSize"] = nodeInfo.resources.flashChipSize;
// Serialize labels if present
if (!nodeInfo.labels.empty()) {
JsonObject labels = obj["labels"].to<JsonObject>();
for (const auto& kv : nodeInfo.labels) {
labels[kv.first.c_str()] = kv.second;
}
}
// Serialize endpoints if present
if (!nodeInfo.endpoints.empty()) {
JsonArray endpoints = obj["api"].to<JsonArray>();
for (const auto& endpoint : nodeInfo.endpoints) {
JsonObject endpointObj = endpoints.add<JsonObject>();
endpointObj["uri"] = endpoint.uri;
endpointObj["method"] = endpoint.method;
}
}
}
/**
* Deserialize NodeInfo from JsonObject
* Populates NodeInfo fields from JSON structure
*/
void fromJson(const JsonObject& obj) override {
nodeInfo.hostname = obj["hostname"].as<String>();
// Parse IP address
const char* ipStr = obj["ip"];
if (ipStr) {
nodeInfo.ip.fromString(ipStr);
}
nodeInfo.lastSeen = obj["lastSeen"].as<unsigned long>();
nodeInfo.latency = obj["latency"].as<unsigned long>();
// Parse status
const char* statusStr = obj["status"];
if (statusStr) {
if (strcmp(statusStr, "ACTIVE") == 0) {
nodeInfo.status = NodeInfo::ACTIVE;
} else if (strcmp(statusStr, "INACTIVE") == 0) {
nodeInfo.status = NodeInfo::INACTIVE;
} else if (strcmp(statusStr, "DEAD") == 0) {
nodeInfo.status = NodeInfo::DEAD;
}
}
// Parse resources
if (obj["resources"].is<JsonObject>()) {
JsonObject resources = obj["resources"].as<JsonObject>();
nodeInfo.resources.freeHeap = resources["freeHeap"].as<uint32_t>();
nodeInfo.resources.chipId = resources["chipId"].as<uint32_t>();
nodeInfo.resources.sdkVersion = resources["sdkVersion"].as<String>();
nodeInfo.resources.cpuFreqMHz = resources["cpuFreqMHz"].as<uint32_t>();
nodeInfo.resources.flashChipSize = resources["flashChipSize"].as<uint32_t>();
}
// Parse labels
nodeInfo.labels.clear();
if (obj["labels"].is<JsonObject>()) {
JsonObject labels = obj["labels"].as<JsonObject>();
for (JsonPair kvp : labels) {
nodeInfo.labels[kvp.key().c_str()] = kvp.value().as<String>();
}
}
// Parse endpoints
nodeInfo.endpoints.clear();
if (obj["api"].is<JsonArray>()) {
JsonArray endpoints = obj["api"].as<JsonArray>();
for (JsonObject endpointObj : endpoints) {
EndpointInfo endpoint;
endpoint.uri = endpointObj["uri"].as<String>();
endpoint.method = endpointObj["method"].as<int>();
endpoint.isLocal = false;
endpoint.serviceName = "remote";
nodeInfo.endpoints.push_back(std::move(endpoint));
}
}
}
};
/**
* Convenience function to create a JsonArray from a collection of NodeInfo objects
* @param doc The JsonDocument to create the array in
* @param nodes Collection of NodeInfo objects
* @return A JsonArray containing all serialized NodeInfo objects
*/
template<typename Container>
JsonArray nodeInfoToJsonArray(JsonDocument& doc, const Container& nodes) {
JsonArray arr = doc.to<JsonArray>();
for (const auto& pair : nodes) {
const NodeInfo& node = pair.second;
NodeInfoSerializable serializable(const_cast<NodeInfo&>(node));
JsonObject obj = arr.add<JsonObject>();
serializable.toJson(obj);
}
return arr;
}
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,102 @@
#pragma once
#include "ApiResponse.h"
#include "EndpointInfoSerializable.h"
#include "NodeInfo.h"
#include "spore/util/Logging.h"
#include <vector>
namespace spore {
namespace types {
/**
* Response class for node status endpoint
* Handles complete JSON document creation for node status data
*/
class NodeStatusResponse : public ApiResponse {
public:
/**
* Set basic system information
*/
void setSystemInfo() {
doc["freeHeap"] = ESP.getFreeHeap();
doc["chipId"] = ESP.getChipId();
doc["sdkVersion"] = ESP.getSdkVersion();
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
doc["flashChipSize"] = ESP.getFlashChipSize();
}
/**
* Add labels to the response
* @param labels Map of label key-value pairs
*/
void addLabels(const std::map<String, String>& labels) {
if (!labels.empty()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
/**
* Build complete response with system info and labels
* @param labels Optional labels to include
*/
void buildCompleteResponse(const std::map<String, String>& labels = {}) {
setSystemInfo();
addLabels(labels);
}
};
/**
* Response class for node endpoints endpoint
* Handles complete JSON document creation for endpoint data
*/
class NodeEndpointsResponse : public CollectionResponse<EndpointInfoSerializable> {
public:
NodeEndpointsResponse() : CollectionResponse("endpoints") {}
/**
* Add a single endpoint to the response
* @param endpoint The EndpointInfo to add
*/
void addEndpoint(const EndpointInfo& endpoint) {
addItem(EndpointInfoSerializable(endpoint));
}
/**
* Add multiple endpoints from a container
* @param endpoints Container of EndpointInfo objects
*/
void addEndpoints(const std::vector<EndpointInfo>& endpoints) {
for (const auto& endpoint : endpoints) {
addEndpoint(endpoint);
}
}
};
/**
* Response class for simple status operations (update, restart)
* Handles simple JSON responses for node operations
*/
class NodeOperationResponse : public ApiResponse {
public:
/**
* Set success response
* @param status Status message
*/
void setSuccess(const String& status) {
doc["status"] = status;
}
/**
* Set error response
* @param status Error status message
*/
void setError(const String& status) {
doc["status"] = status;
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,99 @@
#pragma once
#include "spore/util/JsonSerializable.h"
#include <ArduinoJson.h>
#include <Arduino.h>
namespace spore {
namespace types {
/**
* Serializable wrapper for task information that implements JsonSerializable interface
* Handles conversion between task data and JSON representation
*/
class TaskInfoSerializable : public util::JsonSerializable {
private:
String taskName;
const JsonObject& taskData;
public:
TaskInfoSerializable(const String& name, const JsonObject& data)
: taskName(name), taskData(data) {}
TaskInfoSerializable(const std::string& name, const JsonObject& data)
: taskName(name.c_str()), taskData(data) {}
/**
* Serialize task info to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["name"] = taskName;
obj["interval"] = taskData["interval"];
obj["enabled"] = taskData["enabled"];
obj["running"] = taskData["running"];
obj["autoStart"] = taskData["autoStart"];
}
/**
* Deserialize task info from JsonObject
* Note: This is read-only for task status, so fromJson is not implemented
*/
void fromJson(const JsonObject& obj) override {
// Task info is typically read-only in this context
// Implementation would go here if needed
}
};
/**
* Serializable wrapper for system information
*/
class SystemInfoSerializable : public util::JsonSerializable {
public:
/**
* Serialize system info to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["freeHeap"] = ESP.getFreeHeap();
obj["uptime"] = millis();
}
/**
* Deserialize system info from JsonObject
* Note: System info is typically read-only, so fromJson is not implemented
*/
void fromJson(const JsonObject& obj) override {
// System info is typically read-only
// Implementation would go here if needed
}
};
/**
* Serializable wrapper for task summary information
*/
class TaskSummarySerializable : public util::JsonSerializable {
private:
size_t totalTasks;
size_t activeTasks;
public:
TaskSummarySerializable(size_t total, size_t active)
: totalTasks(total), activeTasks(active) {}
/**
* Serialize task summary to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["totalTasks"] = totalTasks;
obj["activeTasks"] = activeTasks;
}
/**
* Deserialize task summary from JsonObject
*/
void fromJson(const JsonObject& obj) override {
totalTasks = obj["totalTasks"].as<size_t>();
activeTasks = obj["activeTasks"].as<size_t>();
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,194 @@
#pragma once
#include "ApiResponse.h"
#include "TaskInfoSerializable.h"
#include <map>
namespace spore {
namespace types {
/**
* Response class for task status endpoint
* Handles complete JSON document creation for task status data
*/
class TaskStatusResponse : public ApiResponse {
public:
/**
* Set the task summary information
* @param totalTasks Total number of tasks
* @param activeTasks Number of active tasks
*/
void setSummary(size_t totalTasks, size_t activeTasks) {
TaskSummarySerializable summary(totalTasks, activeTasks);
JsonObject summaryObj = doc["summary"].to<JsonObject>();
summary.toJson(summaryObj);
}
/**
* Add a single task to the response
* @param taskName Name of the task
* @param taskData Task data as JsonObject
*/
void addTask(const String& taskName, const JsonObject& taskData) {
TaskInfoSerializable serializable(taskName, taskData);
if (!doc["tasks"].is<JsonArray>()) {
doc["tasks"] = JsonArray();
}
JsonArray tasksArr = doc["tasks"].as<JsonArray>();
JsonObject taskObj = tasksArr.add<JsonObject>();
serializable.toJson(taskObj);
}
/**
* Add a single task to the response (std::string version)
* @param taskName Name of the task
* @param taskData Task data as JsonObject
*/
void addTask(const std::string& taskName, const JsonObject& taskData) {
TaskInfoSerializable serializable(taskName, taskData);
if (!doc["tasks"].is<JsonArray>()) {
doc["tasks"] = JsonArray();
}
JsonArray tasksArr = doc["tasks"].as<JsonArray>();
JsonObject taskObj = tasksArr.add<JsonObject>();
serializable.toJson(taskObj);
}
/**
* Add multiple tasks from a task statuses map
* @param taskStatuses Map of task name to task data
*/
void addTasks(const std::map<String, JsonObject>& taskStatuses) {
for (const auto& pair : taskStatuses) {
addTask(pair.first, pair.second);
}
}
/**
* Set the system information
*/
void setSystemInfo() {
SystemInfoSerializable systemInfo;
JsonObject systemObj = doc["system"].to<JsonObject>();
systemInfo.toJson(systemObj);
}
/**
* Build complete response with all components
* @param taskStatuses Map of task name to task data
*/
void buildCompleteResponse(const std::map<String, JsonObject>& taskStatuses) {
// Set summary
size_t totalTasks = taskStatuses.size();
size_t activeTasks = 0;
for (const auto& pair : taskStatuses) {
if (pair.second["enabled"]) {
activeTasks++;
}
}
setSummary(totalTasks, activeTasks);
// Add all tasks
addTasks(taskStatuses);
// Set system info
setSystemInfo();
}
/**
* Build complete response with all components from vector
* @param taskStatuses Vector of pairs of task name to task data
*/
void buildCompleteResponse(const std::vector<std::pair<std::string, JsonObject>>& taskStatuses) {
// Clear the document first since getAllTaskStatuses creates a root array
doc.clear();
// Set summary
size_t totalTasks = taskStatuses.size();
size_t activeTasks = 0;
for (const auto& pair : taskStatuses) {
if (pair.second["enabled"]) {
activeTasks++;
}
}
setSummary(totalTasks, activeTasks);
// Add all tasks - extract data before clearing to avoid invalid references
JsonArray tasksArr = doc["tasks"].to<JsonArray>();
for (const auto& pair : taskStatuses) {
// Extract data from JsonObject before it becomes invalid
String taskName = pair.first.c_str();
unsigned long interval = pair.second["interval"];
bool enabled = pair.second["enabled"];
bool running = pair.second["running"];
bool autoStart = pair.second["autoStart"];
// Create new JsonObject in our document
JsonObject taskObj = tasksArr.add<JsonObject>();
taskObj["name"] = taskName;
taskObj["interval"] = interval;
taskObj["enabled"] = enabled;
taskObj["running"] = running;
taskObj["autoStart"] = autoStart;
}
// Set system info
setSystemInfo();
}
};
/**
* Response class for task control operations
* Handles JSON responses for task enable/disable/start/stop operations
*/
class TaskControlResponse : public ApiResponse {
public:
/**
* Set the response data for a task control operation
* @param success Whether the operation was successful
* @param message Response message
* @param taskName Name of the task
* @param action Action performed
*/
void setResponse(bool success, const String& message, const String& taskName, const String& action) {
doc["success"] = success;
doc["message"] = message;
doc["task"] = taskName;
doc["action"] = action;
}
/**
* Add detailed task information to the response
* @param taskName Name of the task
* @param enabled Whether task is enabled
* @param running Whether task is running
* @param interval Task interval
*/
void addTaskDetails(const String& taskName, bool enabled, bool running, unsigned long interval) {
JsonObject taskDetails = doc["taskDetails"].to<JsonObject>();
taskDetails["name"] = taskName;
taskDetails["enabled"] = enabled;
taskDetails["running"] = running;
taskDetails["interval"] = interval;
// Add system info
SystemInfoSerializable systemInfo;
JsonObject systemObj = taskDetails["system"].to<JsonObject>();
systemInfo.toJson(systemObj);
}
/**
* Set error response
* @param message Error message
* @param example Optional example for correct usage
*/
void setError(const String& message, const String& example = "") {
doc["success"] = false;
doc["message"] = message;
if (example.length() > 0) {
doc["example"] = example;
}
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,118 @@
#pragma once
#include <Arduino.h>
/**
* @brief CPU usage measurement utility for ESP32/ESP8266
*
* This class provides methods to measure CPU usage by tracking idle time
* and calculating the percentage of time the CPU is busy vs idle.
*/
class CpuUsage {
public:
/**
* @brief Construct a new CpuUsage object
*/
CpuUsage();
/**
* @brief Destructor
*/
~CpuUsage() = default;
/**
* @brief Initialize the CPU usage measurement
* Call this once during setup
*/
void begin();
/**
* @brief Start measuring CPU usage for the current cycle
* Call this at the beginning of your main loop
*/
void startMeasurement();
/**
* @brief End measuring CPU usage for the current cycle
* Call this at the end of your main loop
*/
void endMeasurement();
/**
* @brief Get the current CPU usage percentage
* @return float CPU usage percentage (0.0 to 100.0)
*/
float getCpuUsage() const;
/**
* @brief Get the average CPU usage over the measurement window
* @return float Average CPU usage percentage (0.0 to 100.0)
*/
float getAverageCpuUsage() const;
/**
* @brief Get the maximum CPU usage recorded
* @return float Maximum CPU usage percentage (0.0 to 100.0)
*/
float getMaxCpuUsage() const;
/**
* @brief Get the minimum CPU usage recorded
* @return float Minimum CPU usage percentage (0.0 to 100.0)
*/
float getMinCpuUsage() const;
/**
* @brief Reset all CPU usage statistics
*/
void reset();
/**
* @brief Check if measurement is currently active
* @return true if measurement is active, false otherwise
*/
bool isMeasuring() const;
/**
* @brief Get the number of measurements taken
* @return unsigned long Number of measurements
*/
unsigned long getMeasurementCount() const;
private:
// Measurement state
bool _initialized;
bool _measuring;
unsigned long _measurementCount;
// Timing variables
unsigned long _cycleStartTime;
unsigned long _idleStartTime;
unsigned long _totalIdleTime;
unsigned long _totalCycleTime;
// Statistics
float _currentCpuUsage;
float _averageCpuUsage;
float _maxCpuUsage;
float _minCpuUsage;
unsigned long _totalCpuTime;
// Rolling average window
static constexpr size_t ROLLING_WINDOW_SIZE = 10;
float _rollingWindow[ROLLING_WINDOW_SIZE];
size_t _rollingIndex;
bool _rollingWindowFull;
/**
* @brief Update rolling average calculation
* @param value New value to add to rolling average
*/
void updateRollingAverage(float value);
/**
* @brief Update min/max statistics
* @param value New value to check against min/max
*/
void updateMinMax(float value);
};

View File

@@ -0,0 +1,56 @@
#pragma once
#include <ArduinoJson.h>
namespace spore {
namespace util {
/**
* Abstract base class for objects that can be serialized to/from JSON
* Provides a clean interface for converting objects to JsonObject and back
*/
class JsonSerializable {
public:
virtual ~JsonSerializable() = default;
/**
* Serialize this object to a JsonObject
* @param obj The JsonObject to populate with this object's data
*/
virtual void toJson(JsonObject& obj) const = 0;
/**
* Deserialize this object from a JsonObject
* @param obj The JsonObject containing the data to populate this object
*/
virtual void fromJson(const JsonObject& obj) = 0;
/**
* Convenience method to create a JsonObject from this object
* @param doc The JsonDocument to create the object in
* @return A JsonObject containing this object's serialized data
*/
JsonObject toJsonObject(JsonDocument& doc) const {
JsonObject obj = doc.to<JsonObject>();
toJson(obj);
return obj;
}
/**
* Convenience method to create a JsonArray from a collection of serializable objects
* @param doc The JsonDocument to create the array in
* @param objects Collection of objects implementing JsonSerializable
* @return A JsonArray containing all serialized objects
*/
template<typename Container>
static JsonArray toJsonArray(JsonDocument& doc, const Container& objects) {
JsonArray arr = doc.to<JsonArray>();
for (const auto& obj : objects) {
JsonObject item = arr.add<JsonObject>();
obj.toJson(item);
}
return arr;
}
};
} // namespace util
} // namespace spore

View File

@@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
enum class LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3
};
// Global log level - can be changed at runtime
extern LogLevel g_logLevel;
// Simple logging functions
void logMessage(LogLevel level, const String& component, const String& message);
void logDebug(const String& component, const String& message);
void logInfo(const String& component, const String& message);
void logWarn(const String& component, const String& message);
void logError(const String& component, const String& message);
// Set global log level
void setLogLevel(LogLevel level);
// Convenience macros - no context needed
#define LOG_DEBUG(component, message) logDebug(component, message)
#define LOG_INFO(component, message) logInfo(component, message)
#define LOG_WARN(component, message) logWarn(component, message)
#define LOG_ERROR(component, message) logError(component, message)

View File

@@ -9,8 +9,9 @@
; https://docs.platformio.org/page/projectconf.html
[platformio]
;default_envs = esp01_1m
default_envs = base
src_dir = .
data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data
[common]
monitor_speed = 115200
@@ -18,7 +19,7 @@ lib_deps =
esp32async/ESPAsyncWebServer@^3.8.0
bblanchon/ArduinoJson@^7.4.2
[env:esp01_1m]
[env:base]
platform = platformio/espressif8266@^4.2.1
board = esp01_1m
framework = arduino
@@ -36,6 +37,7 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini]
@@ -55,9 +57,10 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:esp01_1m_relay]
[env:relay]
platform = platformio/espressif8266@^4.2.1
board = esp01_1m
framework = arduino
@@ -67,54 +70,17 @@ board_build.filesystem = littlefs
board_build.flash_mode = dout
board_build.ldscript = eagle.flash.1m64.ld
lib_deps = ${common.lib_deps}
;data_dir = examples/relay/data
build_src_filter =
+<examples/relay/*.cpp>
+<src/spore/*.cpp>
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>
[env:esp01_1m_neopixel]
platform = platformio/espressif8266@^4.2.1
board = esp01_1m
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.filesystem = littlefs
board_build.flash_mode = dout
board_build.ldscript = eagle.flash.1m64.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_src_filter =
+<examples/neopixel/*.cpp>
+<src/spore/*.cpp>
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_neopixel]
platform = platformio/espressif8266@^4.2.1
board = d1_mini
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.filesystem = littlefs
board_build.flash_mode = dio
board_build.flash_size = 4M
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_src_filter =
+<examples/neopixel/*.cpp>
+<src/spore/*.cpp>
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/internal/*.cpp>
[env:esp01_1m_neopattern]
[env:neopattern]
platform = platformio/espressif8266@^4.2.1
board = esp01_1m
framework = arduino
@@ -132,43 +98,5 @@ build_src_filter =
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_neopattern]
platform = platformio/espressif8266@^4.2.1
board = d1_mini
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.filesystem = littlefs
board_build.flash_mode = dio
board_build.flash_size = 4M
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
adafruit/Adafruit NeoPixel@^1.15.1
build_src_filter =
+<examples/neopattern/*.cpp>
+<src/spore/*.cpp>
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/internal/*.cpp>
[env:d1_mini_static_web]
platform = platformio/espressif8266@^4.2.1
board = d1_mini
framework = arduino
upload_speed = 115200
monitor_speed = 115200
board_build.flash_mode = dio
board_build.flash_size = 4M
board_build.filesystem = littlefs
board_build.ldscript = eagle.flash.4m1m.ld
lib_deps = ${common.lib_deps}
build_src_filter =
+<examples/static_web/*.cpp>
+<src/spore/*.cpp>
+<src/spore/core/*.cpp>
+<src/spore/services/*.cpp>
+<src/spore/types/*.cpp>
+<src/spore/util/*.cpp>
+<src/internal/*.cpp>

View File

@@ -4,18 +4,18 @@
#include "spore/services/ClusterService.h"
#include "spore/services/TaskService.h"
#include "spore/services/StaticFileService.h"
#include "spore/services/LoggingService.h"
#include "spore/services/MonitoringService.h"
#include <Arduino.h>
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
apiServer(ctx, taskManager, ctx.config.api_server_port),
initialized(false), apiServerStarted(false) {
cpuUsage(), initialized(false), apiServerStarted(false) {
}
Spore::Spore(std::initializer_list<std::pair<String, String>> initialLabels)
: ctx(initialLabels), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
apiServer(ctx, taskManager, ctx.config.api_server_port),
initialized(false), apiServerStarted(false) {
cpuUsage(), initialized(false), apiServerStarted(false) {
}
Spore::~Spore() {
@@ -24,35 +24,38 @@ Spore::~Spore() {
void Spore::setup() {
if (initialized) {
LOG_INFO(ctx, "Spore", "Already initialized, skipping setup");
LOG_INFO("Spore", "Already initialized, skipping setup");
return;
}
Serial.begin(115200);
LOG_INFO(ctx, "Spore", "Starting Spore framework...");
LOG_INFO("Spore", "Starting Spore framework...");
// Initialize core components
initializeCore();
// Initialize CPU usage monitoring
cpuUsage.begin();
// Register core services
registerCoreServices();
initialized = true;
LOG_INFO(ctx, "Spore", "Framework setup complete - call begin() to start API server");
LOG_INFO("Spore", "Framework setup complete - call begin() to start API server");
}
void Spore::begin() {
if (!initialized) {
LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first");
LOG_ERROR("Spore", "Framework not initialized, call setup() first");
return;
}
if (apiServerStarted) {
LOG_WARN(ctx, "Spore", "API server already started");
LOG_WARN("Spore", "API server already started");
return;
}
LOG_INFO(ctx, "Spore", "Starting API server...");
LOG_INFO("Spore", "Starting API server...");
// Start API server
startApiServer();
@@ -60,22 +63,31 @@ void Spore::begin() {
// Print initial task status
taskManager.printTaskStatus();
LOG_INFO(ctx, "Spore", "Framework ready!");
LOG_INFO("Spore", "Framework ready!");
}
void Spore::loop() {
if (!initialized) {
LOG_ERROR(ctx, "Spore", "Framework not initialized, call setup() first");
LOG_ERROR("Spore", "Framework not initialized, call setup() first");
return;
}
// Start CPU usage measurement
cpuUsage.startMeasurement();
// Execute main tasks
taskManager.execute();
// End CPU usage measurement before yield
cpuUsage.endMeasurement();
// Yield to allow other tasks to run
yield();
}
void Spore::addService(std::shared_ptr<Service> service) {
if (!service) {
LOG_WARN(ctx, "Spore", "Attempted to add null service");
LOG_WARN("Spore", "Attempted to add null service");
return;
}
@@ -84,15 +96,15 @@ void Spore::addService(std::shared_ptr<Service> service) {
if (apiServerStarted) {
// If API server is already started, register the service immediately
apiServer.addService(*service);
LOG_INFO(ctx, "Spore", "Added service '" + String(service->getName()) + "' to running API server");
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to running API server");
} else {
LOG_INFO(ctx, "Spore", "Registered service '" + String(service->getName()) + "' (will be added to API server when begin() is called)");
LOG_INFO("Spore", "Registered service '" + String(service->getName()) + "' (will be added to API server when begin() is called)");
}
}
void Spore::addService(Service* service) {
if (!service) {
LOG_WARN(ctx, "Spore", "Attempted to add null service");
LOG_WARN("Spore", "Attempted to add null service");
return;
}
@@ -102,7 +114,7 @@ void Spore::addService(Service* service) {
void Spore::initializeCore() {
LOG_INFO(ctx, "Spore", "Initializing core components...");
LOG_INFO("Spore", "Initializing core components...");
// Setup WiFi first
network.setupWiFi();
@@ -110,14 +122,11 @@ void Spore::initializeCore() {
// Initialize task manager
taskManager.initialize();
LOG_INFO(ctx, "Spore", "Core components initialized");
LOG_INFO("Spore", "Core components initialized");
}
void Spore::registerCoreServices() {
LOG_INFO(ctx, "Spore", "Registering core services...");
// Create logging service first (other services may use it)
auto loggingService = std::make_shared<LoggingService>(ctx, apiServer);
LOG_INFO("Spore", "Registering core services...");
// Create core services
auto nodeService = std::make_shared<NodeService>(ctx, apiServer);
@@ -125,31 +134,32 @@ void Spore::registerCoreServices() {
auto clusterService = std::make_shared<ClusterService>(ctx);
auto taskService = std::make_shared<TaskService>(taskManager);
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
auto monitoringService = std::make_shared<MonitoringService>(cpuUsage);
// Add to services list
services.push_back(loggingService);
services.push_back(nodeService);
services.push_back(networkService);
services.push_back(clusterService);
services.push_back(taskService);
services.push_back(staticFileService);
services.push_back(monitoringService);
LOG_INFO(ctx, "Spore", "Core services registered");
LOG_INFO("Spore", "Core services registered");
}
void Spore::startApiServer() {
if (apiServerStarted) {
LOG_WARN(ctx, "Spore", "API server already started");
LOG_WARN("Spore", "API server already started");
return;
}
LOG_INFO(ctx, "Spore", "Starting API server...");
LOG_INFO("Spore", "Starting API server...");
// Register all services with API server
for (auto& service : services) {
if (service) {
apiServer.addService(*service);
LOG_INFO(ctx, "Spore", "Added service '" + String(service->getName()) + "' to API server");
LOG_INFO("Spore", "Added service '" + String(service->getName()) + "' to API server");
}
}
@@ -157,5 +167,5 @@ void Spore::startApiServer() {
apiServer.begin();
apiServerStarted = true;
LOG_INFO(ctx, "Spore", "API server started");
LOG_INFO("Spore", "API server started");
}

View File

@@ -1,6 +1,6 @@
#include "spore/core/ApiServer.h"
#include "spore/Service.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include <algorithm>
const char* ApiServer::methodToStr(int method) {
@@ -21,71 +21,50 @@ void ApiServer::registerEndpoint(const String& uri, int method,
const String& serviceName) {
// Add to local endpoints
endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
// Update cluster if needed
if (ctx.memberList && !ctx.memberList->empty()) {
auto it = ctx.memberList->find(ctx.hostname);
if (it != ctx.memberList->end()) {
it->second.endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
}
}
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const String& serviceName) {
registerEndpoint(uri, method, {}, serviceName);
server.on(uri.c_str(), method, requestHandler);
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const String& serviceName) {
registerEndpoint(uri, method, {}, serviceName);
server.on(uri.c_str(), method, requestHandler, uploadHandler);
}
// Overloads that also record minimal capability specs
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
const std::vector<ParamSpec>& params, const String& serviceName) {
registerEndpoint(uri, method, params, serviceName);
server.on(uri.c_str(), method, requestHandler);
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& params) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
const std::vector<ParamSpec>& params, const String& serviceName) {
registerEndpoint(uri, method, params, serviceName);
server.on(uri.c_str(), method, requestHandler, uploadHandler);
}
void ApiServer::addService(Service& service) {
services.push_back(service);
LOG_INFO(ctx, "API", "Added service: " + String(service.getName()));
LOG_INFO("API", "Added service: " + String(service.getName()));
}
void ApiServer::serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header) {
server.serveStatic(uri.c_str(), fs, path.c_str(), cache_header.c_str()).setDefaultFile("index.html");
LOG_INFO("API", "Registered static file serving: " + uri + " -> " + path);
}
void ApiServer::begin() {
// Register all service endpoints
for (auto& service : services) {
service.get().registerEndpoints(*this);
LOG_INFO(ctx, "API", "Registered endpoints for service: " + String(service.get().getName()));
LOG_INFO("API", "Registered endpoints for service: " + String(service.get().getName()));
}
server.begin();

View File

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

View File

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

View File

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

View File

@@ -1,42 +1,19 @@
#include "spore/services/ClusterService.h"
#include "spore/core/ApiServer.h"
#include "spore/types/ClusterResponse.h"
using spore::types::ClusterMembersResponse;
ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {}
void ClusterService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/cluster/members", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleMembersRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "ClusterService");
}
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc["members"].to<JsonArray>();
for (const auto& pair : *ctx.memberList) {
const NodeInfo& node = pair.second;
JsonObject obj = arr.add<JsonObject>();
obj["hostname"] = node.hostname;
obj["ip"] = node.ip.toString();
obj["lastSeen"] = node.lastSeen;
obj["latency"] = node.latency;
obj["status"] = statusToStr(node.status);
obj["resources"]["freeHeap"] = node.resources.freeHeap;
obj["resources"]["chipId"] = node.resources.chipId;
obj["resources"]["sdkVersion"] = node.resources.sdkVersion;
obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz;
obj["resources"]["flashChipSize"] = node.resources.flashChipSize;
// Add labels if present
if (!node.labels.empty()) {
JsonObject labelsObj = obj["labels"].to<JsonObject>();
for (const auto& kv : node.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
ClusterMembersResponse response;
response.addNodes(ctx.memberList);
request->send(200, "application/json", response.toJsonString());
}

View File

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

View File

@@ -0,0 +1,115 @@
#include "spore/services/MonitoringService.h"
#include "spore/core/ApiServer.h"
#include "spore/util/Logging.h"
#include <Arduino.h>
#include <FS.h>
#include <LittleFS.h>
MonitoringService::MonitoringService(CpuUsage& cpuUsage)
: cpuUsage(cpuUsage) {
}
void MonitoringService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/monitoring/resources", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleResourcesRequest(request); },
std::vector<ParamSpec>{}, "MonitoringService");
}
MonitoringService::SystemResources MonitoringService::getSystemResources() const {
SystemResources resources;
// CPU information
resources.currentCpuUsage = cpuUsage.getCpuUsage();
resources.averageCpuUsage = cpuUsage.getAverageCpuUsage();
resources.maxCpuUsage = cpuUsage.getMaxCpuUsage();
resources.minCpuUsage = cpuUsage.getMinCpuUsage();
resources.measurementCount = cpuUsage.getMeasurementCount();
resources.isMeasuring = cpuUsage.isMeasuring();
// Memory information - ESP8266 compatible
resources.freeHeap = ESP.getFreeHeap();
resources.totalHeap = 81920; // ESP8266 has ~80KB RAM
resources.minFreeHeap = 0; // Not available on ESP8266
resources.maxAllocHeap = 0; // Not available on ESP8266
resources.heapFragmentation = calculateHeapFragmentation();
// Filesystem information
getFilesystemInfo(resources.totalBytes, resources.usedBytes);
resources.freeBytes = resources.totalBytes - resources.usedBytes;
resources.usagePercent = resources.totalBytes > 0 ?
(float)resources.usedBytes / (float)resources.totalBytes * 100.0f : 0.0f;
// System uptime
resources.uptimeMs = millis();
resources.uptimeSeconds = resources.uptimeMs / 1000;
return resources;
}
void MonitoringService::handleResourcesRequest(AsyncWebServerRequest* request) {
SystemResources resources = getSystemResources();
JsonDocument doc;
// CPU section
JsonObject cpu = doc["cpu"].to<JsonObject>();
cpu["current_usage"] = resources.currentCpuUsage;
cpu["average_usage"] = resources.averageCpuUsage;
cpu["max_usage"] = resources.maxCpuUsage;
cpu["min_usage"] = resources.minCpuUsage;
cpu["measurement_count"] = resources.measurementCount;
cpu["is_measuring"] = resources.isMeasuring;
// Memory section
JsonObject memory = doc["memory"].to<JsonObject>();
memory["free_heap"] = resources.freeHeap;
memory["total_heap"] = resources.totalHeap;
memory["min_free_heap"] = resources.minFreeHeap;
memory["max_alloc_heap"] = resources.maxAllocHeap;
memory["heap_fragmentation"] = resources.heapFragmentation;
memory["heap_usage_percent"] = resources.totalHeap > 0 ?
(float)(resources.totalHeap - resources.freeHeap) / (float)resources.totalHeap * 100.0f : 0.0f;
// Filesystem section
JsonObject filesystem = doc["filesystem"].to<JsonObject>();
filesystem["total_bytes"] = resources.totalBytes;
filesystem["used_bytes"] = resources.usedBytes;
filesystem["free_bytes"] = resources.freeBytes;
filesystem["usage_percent"] = resources.usagePercent;
// System section
JsonObject system = doc["system"].to<JsonObject>();
system["uptime_ms"] = resources.uptimeMs;
system["uptime_seconds"] = resources.uptimeSeconds;
system["uptime_formatted"] = String(resources.uptimeSeconds / 3600) + "h " +
String((resources.uptimeSeconds % 3600) / 60) + "m " +
String(resources.uptimeSeconds % 60) + "s";
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}
size_t MonitoringService::calculateHeapFragmentation() const {
size_t freeHeap = ESP.getFreeHeap();
size_t maxAllocHeap = 0; // Not available on ESP8266
if (maxAllocHeap == 0) return 0;
// Calculate fragmentation as percentage of free heap that can't be allocated in one block
return (freeHeap - maxAllocHeap) * 100 / freeHeap;
}
void MonitoringService::getFilesystemInfo(size_t& totalBytes, size_t& usedBytes) const {
totalBytes = 0;
usedBytes = 0;
if (LittleFS.begin()) {
FSInfo fsInfo;
if (LittleFS.info(fsInfo)) {
totalBytes = fsInfo.totalBytes;
usedBytes = fsInfo.usedBytes;
}
LittleFS.end();
}
}

View File

@@ -8,16 +8,16 @@ void NetworkService::registerEndpoints(ApiServer& api) {
// WiFi scanning endpoints
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NetworkService");
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NetworkService");
// Network status and configuration endpoints
api.addEndpoint("/api/network/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NetworkService");
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
@@ -26,7 +26,7 @@ void NetworkService::registerEndpoints(ApiServer& api) {
ParamSpec{String("password"), true, String("body"), String("string"), {}, String("")},
ParamSpec{String("connect_timeout_ms"), false, String("body"), String("number"), {}, String("10000")},
ParamSpec{String("retry_delay_ms"), false, String("body"), String("number"), {}, String("500")}
});
}, "NetworkService");
}
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {

View File

@@ -1,6 +1,11 @@
#include "spore/services/NodeService.h"
#include "spore/core/ApiServer.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include "spore/types/NodeResponse.h"
using spore::types::NodeStatusResponse;
using spore::types::NodeOperationResponse;
using spore::types::NodeEndpointsResponse;
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
@@ -8,7 +13,7 @@ void NodeService::registerEndpoints(ApiServer& api) {
// Status endpoint
api.addEndpoint("/api/node/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NodeService");
// Update endpoint with file upload
api.addEndpoint("/api/node/update", HTTP_POST,
@@ -18,56 +23,47 @@ void NodeService::registerEndpoints(ApiServer& api) {
},
std::vector<ParamSpec>{
ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")}
});
}, "NodeService");
// Restart endpoint
api.addEndpoint("/api/node/restart", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NodeService");
// Endpoints endpoint
api.addEndpoint("/api/node/endpoints", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NodeService");
}
void NodeService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
doc["freeHeap"] = ESP.getFreeHeap();
doc["chipId"] = ESP.getChipId();
doc["sdkVersion"] = ESP.getSdkVersion();
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
doc["flashChipSize"] = ESP.getFlashChipSize();
// Include local node labels if present
NodeStatusResponse response;
// Get labels from member list or self
std::map<String, String> labels;
if (ctx.memberList) {
auto it = ctx.memberList->find(ctx.hostname);
if (it != ctx.memberList->end()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : it->second.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
labels = it->second.labels;
} else if (!ctx.self.labels.empty()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : ctx.self.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
labels = ctx.self.labels;
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
response.buildCompleteResponse(labels);
request->send(200, "application/json", response.toJsonString());
}
void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
bool success = !Update.hasError();
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
request->send(response);
NodeOperationResponse response;
response.setSuccess(success ? "OK" : "FAIL");
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
httpResponse->addHeader("Connection", "close");
request->send(httpResponse);
request->onDisconnect([this]() {
LOG_INFO(ctx, "API", "Restart device");
LOG_INFO("API", "Restart device");
delay(10);
ESP.restart();
});
@@ -76,10 +72,10 @@ void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename,
size_t index, uint8_t* data, size_t len, bool final) {
if (!index) {
LOG_INFO(ctx, "OTA", "Update Start " + String(filename));
LOG_INFO("OTA", "Update Start " + String(filename));
Update.runAsync(true);
if(!Update.begin(request->contentLength(), U_FLASH)) {
LOG_ERROR(ctx, "OTA", "Update failed: not enough space");
LOG_ERROR("OTA", "Update failed: not enough space");
Update.printError(Serial);
AsyncWebServerResponse* response = request->beginResponse(500, "application/json",
"{\"status\": \"FAIL\"}");
@@ -96,60 +92,33 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
if (final) {
if (Update.end(true)) {
if(Update.isFinished()) {
LOG_INFO(ctx, "OTA", "Update Success with " + String(index + len) + "B");
LOG_INFO("OTA", "Update Success with " + String(index + len) + "B");
} else {
LOG_WARN(ctx, "OTA", "Update not finished");
LOG_WARN("OTA", "Update not finished");
}
} else {
LOG_ERROR(ctx, "OTA", "Update failed: " + String(Update.getError()));
LOG_ERROR("OTA", "Update failed: " + String(Update.getError()));
Update.printError(Serial);
}
}
}
void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
"{\"status\": \"restarting\"}");
response->addHeader("Connection", "close");
request->send(response);
NodeOperationResponse response;
response.setSuccess("restarting");
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
httpResponse->addHeader("Connection", "close");
request->send(httpResponse);
request->onDisconnect([this]() {
LOG_INFO(ctx, "API", "Restart device");
LOG_INFO("API", "Restart device");
delay(10);
ESP.restart();
});
}
void NodeService::handleEndpointsRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray endpointsArr = doc["endpoints"].to<JsonArray>();
// Add all registered endpoints from ApiServer
for (const auto& endpoint : apiServer.getEndpoints()) {
JsonObject obj = endpointsArr.add<JsonObject>();
obj["uri"] = endpoint.uri;
obj["method"] = ApiServer::methodToStr(endpoint.method);
if (!endpoint.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& ps : endpoint.params) {
JsonObject p = paramsArr.add<JsonObject>();
p["name"] = ps.name;
p["location"] = ps.location;
p["required"] = ps.required;
p["type"] = ps.type;
if (!ps.values.empty()) {
JsonArray allowed = p["values"].to<JsonArray>();
for (const auto& v : ps.values) {
allowed.add(v);
}
}
if (ps.defaultValue.length() > 0) {
p["default"] = ps.defaultValue;
}
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
NodeEndpointsResponse response;
response.addEndpoints(apiServer.getEndpoints());
request->send(200, "application/json", response.toJsonString());
}

View File

@@ -1,5 +1,5 @@
#include "spore/services/StaticFileService.h"
#include "spore/services/LoggingService.h"
#include "spore/util/Logging.h"
#include <Arduino.h>
const String StaticFileService::name = "StaticFileService";
@@ -11,100 +11,12 @@ StaticFileService::StaticFileService(NodeContext& ctx, ApiServer& apiServer)
void StaticFileService::registerEndpoints(ApiServer& api) {
// Initialize LittleFS
if (!LittleFS.begin()) {
LOG_ERROR(ctx, "StaticFileService", "LittleFS Mount Failed");
LOG_ERROR("StaticFileService", "LittleFS Mount Failed");
return;
}
LOG_INFO(ctx, "StaticFileService", "LittleFS mounted successfully");
LOG_INFO("StaticFileService", "LittleFS mounted successfully");
// Root endpoint - serve index.html
api.addEndpoint("/", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleRootRequest(request); },
std::vector<ParamSpec>{});
// Static file serving for any path
api.addEndpoint("/*", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStaticFileRequest(request); },
std::vector<ParamSpec>{});
// Use the built-in static file serving from ESPAsyncWebServer
api.serveStatic("/", LittleFS, "/public", "max-age=3600");
}
void StaticFileService::handleRootRequest(AsyncWebServerRequest* request) {
// Serve index.html from root
String path = "/index.html";
if (!fileExists(path)) {
request->send(404, "text/plain", "File not found");
return;
}
File file = LittleFS.open(path, "r");
if (!file) {
request->send(500, "text/plain", "Failed to open file");
return;
}
String contentType = getContentType(path);
request->send(LittleFS, path, contentType);
file.close();
}
void StaticFileService::handleStaticFileRequest(AsyncWebServerRequest* request) {
String path = request->url();
// Remove leading slash for LittleFS path
if (path.startsWith("/")) {
path = path.substring(1);
}
// If path is empty or just "/", serve index.html
if (path.isEmpty() || path == "/") {
path = "index.html";
}
// Check if file exists
if (!fileExists("/" + path)) {
request->send(404, "text/plain", "File not found: " + path);
return;
}
String contentType = getContentType(path);
request->send(LittleFS, "/" + path, contentType);
}
void StaticFileService::handleNotFound(AsyncWebServerRequest* request) {
// Try to serve index.html as fallback
if (fileExists("/index.html")) {
request->send(LittleFS, "/index.html", "text/html");
} else {
request->send(404, "text/plain", "Not found");
}
}
String StaticFileService::getContentType(const String& filename) {
if (filename.endsWith(".html") || filename.endsWith(".htm")) {
return "text/html";
} else if (filename.endsWith(".css")) {
return "text/css";
} else if (filename.endsWith(".js")) {
return "application/javascript";
} else if (filename.endsWith(".json")) {
return "application/json";
} else if (filename.endsWith(".png")) {
return "image/png";
} else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg")) {
return "image/jpeg";
} else if (filename.endsWith(".gif")) {
return "image/gif";
} else if (filename.endsWith(".svg")) {
return "image/svg+xml";
} else if (filename.endsWith(".ico")) {
return "image/x-icon";
} else if (filename.endsWith(".txt")) {
return "text/plain";
} else {
return "application/octet-stream";
}
}
bool StaticFileService::fileExists(const String& path) {
return LittleFS.exists(path);
}

View File

@@ -1,13 +1,17 @@
#include "spore/services/TaskService.h"
#include "spore/core/ApiServer.h"
#include "spore/types/TaskResponse.h"
#include <algorithm>
using spore::types::TaskStatusResponse;
using spore::types::TaskControlResponse;
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
void TaskService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/tasks/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "TaskService");
api.addEndpoint("/api/tasks/control", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
@@ -28,36 +32,19 @@ void TaskService::registerEndpoints(ApiServer& api) {
{String("enable"), String("disable"), String("start"), String("stop"), String("status")},
String("")
}
});
}, "TaskService");
}
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
TaskStatusResponse response;
// Get task statuses using a separate document to avoid reference issues
JsonDocument scratch;
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
JsonDocument doc;
JsonObject summaryObj = doc["summary"].to<JsonObject>();
summaryObj["totalTasks"] = taskStatuses.size();
summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(),
[](const auto& pair) { return pair.second["enabled"]; });
JsonArray tasksArr = doc["tasks"].to<JsonArray>();
for (const auto& taskPair : taskStatuses) {
JsonObject taskObj = tasksArr.add<JsonObject>();
taskObj["name"] = taskPair.first;
taskObj["interval"] = taskPair.second["interval"];
taskObj["enabled"] = taskPair.second["enabled"];
taskObj["running"] = taskPair.second["running"];
taskObj["autoStart"] = taskPair.second["autoStart"];
}
JsonObject systemObj = doc["system"].to<JsonObject>();
systemObj["freeHeap"] = ESP.getFreeHeap();
systemObj["uptime"] = millis();
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
// Build the complete response with the task data
response.buildCompleteResponse(taskStatuses);
request->send(200, "application/json", response.toJsonString());
}
void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
@@ -88,50 +75,27 @@ void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
success = true;
message = "Task status retrieved";
JsonDocument statusDoc;
statusDoc["success"] = success;
statusDoc["message"] = message;
statusDoc["task"] = taskName;
statusDoc["action"] = action;
TaskControlResponse response;
response.setResponse(success, message, taskName, action);
response.addTaskDetails(taskName,
taskManager.isTaskEnabled(taskName.c_str()),
taskManager.isTaskRunning(taskName.c_str()),
taskManager.getTaskInterval(taskName.c_str()));
statusDoc["taskDetails"] = JsonObject();
JsonObject taskDetails = statusDoc["taskDetails"];
taskDetails["name"] = taskName;
taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str());
taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str());
taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str());
taskDetails["system"] = JsonObject();
JsonObject systemInfo = taskDetails["system"];
systemInfo["freeHeap"] = ESP.getFreeHeap();
systemInfo["uptime"] = millis();
String statusJson;
serializeJson(statusDoc, statusJson);
request->send(200, "application/json", statusJson);
request->send(200, "application/json", response.toJsonString());
return;
} else {
success = false;
message = "Invalid action. Use: enable, disable, start, stop, or status";
}
JsonDocument doc;
doc["success"] = success;
doc["message"] = message;
doc["task"] = taskName;
doc["action"] = action;
String json;
serializeJson(doc, json);
request->send(success ? 200 : 400, "application/json", json);
TaskControlResponse response;
response.setResponse(success, message, taskName, action);
request->send(success ? 200 : 400, "application/json", response.toJsonString());
} else {
JsonDocument doc;
doc["success"] = false;
doc["message"] = "Missing parameters. Required: task, action";
doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}";
String json;
serializeJson(doc, json);
request->send(400, "application/json", json);
TaskControlResponse response;
response.setError("Missing parameters. Required: task, action",
"{\"task\": \"discovery_send\", \"action\": \"status\"}");
request->send(400, "application/json", response.toJsonString());
}
}

View File

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

185
src/spore/util/CpuUsage.cpp Normal file
View File

@@ -0,0 +1,185 @@
#include "spore/util/CpuUsage.h"
CpuUsage::CpuUsage()
: _initialized(false)
, _measuring(false)
, _measurementCount(0)
, _cycleStartTime(0)
, _idleStartTime(0)
, _totalIdleTime(0)
, _totalCycleTime(0)
, _currentCpuUsage(0.0f)
, _averageCpuUsage(0.0f)
, _maxCpuUsage(0.0f)
, _minCpuUsage(100.0f)
, _totalCpuTime(0)
, _rollingIndex(0)
, _rollingWindowFull(false) {
// Initialize rolling window
for (size_t i = 0; i < ROLLING_WINDOW_SIZE; ++i) {
_rollingWindow[i] = 0.0f;
}
}
void CpuUsage::begin() {
if (_initialized) {
return;
}
_initialized = true;
_measurementCount = 0;
_totalIdleTime = 0;
_totalCycleTime = 0;
_totalCpuTime = 0;
_currentCpuUsage = 0.0f;
_averageCpuUsage = 0.0f;
_maxCpuUsage = 0.0f;
_minCpuUsage = 100.0f;
_rollingIndex = 0;
_rollingWindowFull = false;
// Initialize rolling window
for (size_t i = 0; i < ROLLING_WINDOW_SIZE; ++i) {
_rollingWindow[i] = 0.0f;
}
}
void CpuUsage::startMeasurement() {
if (!_initialized) {
return;
}
if (_measuring) {
// If already measuring, end the previous measurement first
endMeasurement();
}
_measuring = true;
_cycleStartTime = millis();
_idleStartTime = millis();
}
void CpuUsage::endMeasurement() {
if (!_initialized || !_measuring) {
return;
}
unsigned long cycleEndTime = millis();
unsigned long cycleDuration = cycleEndTime - _cycleStartTime;
// Calculate idle time (time spent in yield() calls)
unsigned long idleTime = cycleEndTime - _idleStartTime;
// Calculate CPU usage
if (cycleDuration > 0) {
_currentCpuUsage = ((float)(cycleDuration - idleTime) / (float)cycleDuration) * 100.0f;
// Clamp to valid range
if (_currentCpuUsage < 0.0f) {
_currentCpuUsage = 0.0f;
} else if (_currentCpuUsage > 100.0f) {
_currentCpuUsage = 100.0f;
}
// Update statistics
_totalCycleTime += cycleDuration;
_totalIdleTime += idleTime;
_totalCpuTime += (cycleDuration - idleTime);
_measurementCount++;
// Update rolling average
updateRollingAverage(_currentCpuUsage);
// Update min/max
updateMinMax(_currentCpuUsage);
// Calculate overall average
if (_measurementCount > 0) {
_averageCpuUsage = ((float)_totalCpuTime / (float)_totalCycleTime) * 100.0f;
}
}
_measuring = false;
}
float CpuUsage::getCpuUsage() const {
return _currentCpuUsage;
}
float CpuUsage::getAverageCpuUsage() const {
if (_rollingWindowFull) {
return _averageCpuUsage;
} else if (_measurementCount > 0) {
// Calculate average from rolling window
float sum = 0.0f;
for (size_t i = 0; i < _rollingIndex; ++i) {
sum += _rollingWindow[i];
}
return sum / (float)_rollingIndex;
}
return 0.0f;
}
float CpuUsage::getMaxCpuUsage() const {
return _maxCpuUsage;
}
float CpuUsage::getMinCpuUsage() const {
return _minCpuUsage;
}
void CpuUsage::reset() {
_measurementCount = 0;
_totalIdleTime = 0;
_totalCycleTime = 0;
_totalCpuTime = 0;
_currentCpuUsage = 0.0f;
_averageCpuUsage = 0.0f;
_maxCpuUsage = 0.0f;
_minCpuUsage = 100.0f;
_rollingIndex = 0;
_rollingWindowFull = false;
// Reset rolling window
for (size_t i = 0; i < ROLLING_WINDOW_SIZE; ++i) {
_rollingWindow[i] = 0.0f;
}
}
bool CpuUsage::isMeasuring() const {
return _measuring;
}
unsigned long CpuUsage::getMeasurementCount() const {
return _measurementCount;
}
void CpuUsage::updateRollingAverage(float value) {
_rollingWindow[_rollingIndex] = value;
_rollingIndex++;
if (_rollingIndex >= ROLLING_WINDOW_SIZE) {
_rollingIndex = 0;
_rollingWindowFull = true;
}
// Calculate rolling average
float sum = 0.0f;
size_t count = _rollingWindowFull ? ROLLING_WINDOW_SIZE : _rollingIndex;
for (size_t i = 0; i < count; ++i) {
sum += _rollingWindow[i];
}
_averageCpuUsage = sum / (float)count;
}
void CpuUsage::updateMinMax(float value) {
if (value > _maxCpuUsage) {
_maxCpuUsage = value;
}
if (value < _minCpuUsage) {
_minCpuUsage = value;
}
}

View File

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