Compare commits
9 Commits
f7f5918509
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
| 025ac7b810 | |||
| a9f56c1279 | |||
| 594b5e3af6 | |||
| c11652c123 | |||
| 51d4d4bc94 | |||
| e95eb09a11 | |||
| 4727405be1 | |||
| 93f09c3bb4 | |||
| 83d87159fc |
@@ -1,470 +1,376 @@
|
|||||||
/**
|
#include "NeoPattern.h"
|
||||||
* 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 <Adafruit_NeoPixel.h>
|
// Constructor - calls base-class constructor to initialize strip
|
||||||
|
NeoPattern::NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int))
|
||||||
using namespace std;
|
: Adafruit_NeoPixel(pixels, pin, type)
|
||||||
|
|
||||||
// Pattern types supported:
|
|
||||||
enum pattern
|
|
||||||
{
|
{
|
||||||
NONE = 0,
|
frameBuffer = (uint8_t *)malloc(768);
|
||||||
RAINBOW_CYCLE = 1,
|
OnComplete = callback;
|
||||||
THEATER_CHASE = 2,
|
TotalSteps = numPixels();
|
||||||
COLOR_WIPE = 3,
|
begin();
|
||||||
SCANNER = 4,
|
}
|
||||||
FADE = 5,
|
|
||||||
FIRE = 6
|
NeoPattern::NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
|
||||||
};
|
: Adafruit_NeoPixel(pixels, pin, type)
|
||||||
// Patern directions supported:
|
|
||||||
enum direction
|
|
||||||
{
|
{
|
||||||
FORWARD,
|
frameBuffer = (uint8_t *)malloc(768);
|
||||||
REVERSE
|
TotalSteps = numPixels();
|
||||||
};
|
begin();
|
||||||
|
}
|
||||||
|
|
||||||
// NeoPattern Class - derived from the Adafruit_NeoPixel class
|
NeoPattern::~NeoPattern() {
|
||||||
class NeoPattern : public Adafruit_NeoPixel
|
if (frameBuffer) {
|
||||||
|
free(frameBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NeoPattern::handleStream(uint8_t *data, size_t len)
|
||||||
{
|
{
|
||||||
public:
|
//const uint16_t *data16 = (uint16_t *)data;
|
||||||
// Member Variables:
|
bufferSize = len;
|
||||||
pattern ActivePattern = RAINBOW_CYCLE; // which pattern is running
|
memcpy(frameBuffer, data, len);
|
||||||
direction Direction = FORWARD; // direction to run the pattern
|
}
|
||||||
|
|
||||||
unsigned long Interval = 150; // milliseconds between updates
|
void NeoPattern::drawFrameBuffer(int w, uint8_t *frame, int length)
|
||||||
unsigned long lastUpdate = 0; // last update of position
|
{
|
||||||
|
for (int i = 0; i < length; i++)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
frameBuffer = (uint8_t *)malloc(768);
|
uint8_t r = frame[i];
|
||||||
OnComplete = callback;
|
uint8_t g = frame[i + 1];
|
||||||
TotalSteps = numPixels();
|
uint8_t b = frame[i + 2];
|
||||||
begin();
|
setPixelColor(i, r, g, b);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
|
void NeoPattern::onCompleteDefault(int pixels)
|
||||||
: Adafruit_NeoPixel(pixels, pin, type)
|
{
|
||||||
|
//Serial.println("onCompleteDefault");
|
||||||
|
// FIXME no specific code
|
||||||
|
if (ActivePattern == THEATER_CHASE)
|
||||||
{
|
{
|
||||||
frameBuffer = (uint8_t *)malloc(768);
|
return;
|
||||||
TotalSteps = numPixels();
|
|
||||||
begin();
|
|
||||||
}
|
}
|
||||||
|
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;
|
Index++;
|
||||||
bufferSize = len;
|
if (Index >= TotalSteps)
|
||||||
memcpy(frameBuffer, data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
void drawFrameBuffer(int w, uint8_t *frame, int length)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < length; i++)
|
|
||||||
{
|
{
|
||||||
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;
|
Index = 0;
|
||||||
}
|
completed = 1;
|
||||||
}
|
if (OnComplete != NULL)
|
||||||
|
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
setPixelColor(i, Color1);
|
OnComplete(numPixels()); // call the comlpetion callback
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
setPixelColor(i, Color2);
|
onCompleteDefault(numPixels());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
show();
|
|
||||||
Increment();
|
|
||||||
}
|
}
|
||||||
|
else // Direction == REVERSE
|
||||||
// Initialize for a ColorWipe
|
|
||||||
void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD)
|
|
||||||
{
|
{
|
||||||
ActivePattern = COLOR_WIPE;
|
--Index;
|
||||||
Interval = interval;
|
if (Index <= 0)
|
||||||
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++)
|
|
||||||
{
|
{
|
||||||
if (i == Index) // Scan Pixel to the right
|
Index = TotalSteps - 1;
|
||||||
|
completed = 1;
|
||||||
|
if (OnComplete != NULL)
|
||||||
{
|
{
|
||||||
setPixelColor(i, Color1);
|
OnComplete(numPixels()); // call the comlpetion callback
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
else
|
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
|
Direction = REVERSE;
|
||||||
uint8_t t192 = round((temperature / 255.0) * 191);
|
Index = TotalSteps - 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Direction = FORWARD;
|
||||||
|
Index = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// calculate ramp up from
|
// Initialize for a RainbowCycle
|
||||||
uint8_t heatramp = t192 & 0x3F; // 0..63
|
void NeoPattern::RainbowCycle(uint8_t interval, direction dir)
|
||||||
heatramp <<= 2; // scale up to 0..252
|
{
|
||||||
|
ActivePattern = RAINBOW_CYCLE;
|
||||||
|
Interval = interval;
|
||||||
|
TotalSteps = 255;
|
||||||
|
Index = 0;
|
||||||
|
Direction = dir;
|
||||||
|
}
|
||||||
|
|
||||||
// figure out which third of the spectrum we're in:
|
// Update the Rainbow Cycle Pattern
|
||||||
if (t192 > 0x80)
|
void NeoPattern::RainbowCycleUpdate()
|
||||||
{ // hottest
|
{
|
||||||
setPixel(Pixel, 255, 255, heatramp);
|
for (int i = 0; i < numPixels(); i++)
|
||||||
}
|
{
|
||||||
else if (t192 > 0x40)
|
setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255));
|
||||||
{ // middle
|
}
|
||||||
setPixel(Pixel, 255, heatramp, 0);
|
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
|
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();
|
||||||
|
}
|
||||||
|
|||||||
109
examples/neopattern/NeoPattern.h
Normal file
109
examples/neopattern/NeoPattern.h
Normal 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
|
||||||
@@ -1,56 +1,96 @@
|
|||||||
#include "NeoPatternService.h"
|
#include "NeoPatternService.h"
|
||||||
#include "spore/core/ApiServer.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),
|
: taskManager(taskMgr),
|
||||||
pixels(numPixels, pin, type),
|
config(config),
|
||||||
updateIntervalMs(100),
|
activePattern(NeoPatternType::RAINBOW_CYCLE),
|
||||||
brightness(48) {
|
direction(NeoDirection::FORWARD),
|
||||||
pixels.setBrightness(brightness);
|
updateIntervalMs(config.updateInterval),
|
||||||
pixels.show();
|
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();
|
registerTasks();
|
||||||
setPatternByName("rainbow_cycle");
|
initialized = true;
|
||||||
|
|
||||||
|
LOG_INFO("NeoPattern", "Service initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
NeoPatternService::~NeoPatternService() {
|
||||||
|
if (neoPattern) {
|
||||||
|
delete neoPattern;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoPatternService::registerEndpoints(ApiServer& api) {
|
void NeoPatternService::registerEndpoints(ApiServer& api) {
|
||||||
|
// Status endpoint
|
||||||
api.addEndpoint("/api/neopattern/status", HTTP_GET,
|
api.addEndpoint("/api/neopattern/status", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{});
|
||||||
|
|
||||||
|
// Patterns list endpoint
|
||||||
api.addEndpoint("/api/neopattern/patterns", HTTP_GET,
|
api.addEndpoint("/api/neopattern/patterns", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
|
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{});
|
||||||
|
|
||||||
|
// Control endpoint
|
||||||
api.addEndpoint("/api/neopattern", HTTP_POST,
|
api.addEndpoint("/api/neopattern", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
||||||
std::vector<ParamSpec>{
|
std::vector<ParamSpec>{
|
||||||
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
|
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("color"), false, String("body"), String("color"), {}},
|
||||||
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
|
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
|
||||||
ParamSpec{String("r"), false, String("body"), String("number"), {}},
|
ParamSpec{String("brightness"), false, String("body"), String("numberRange"), {}, String("80")},
|
||||||
ParamSpec{String("g"), false, String("body"), String("number"), {}},
|
ParamSpec{String("total_steps"), false, String("body"), String("numberRange"), {}, String("16")},
|
||||||
ParamSpec{String("b"), false, String("body"), String("number"), {}},
|
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}},
|
||||||
ParamSpec{String("r2"), false, String("body"), String("number"), {}},
|
ParamSpec{String("interval"), false, String("body"), String("number"), {}, String("100")}
|
||||||
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")}}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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) {
|
void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
doc["pin"] = pixels.getPin();
|
doc["pin"] = config.pin;
|
||||||
doc["count"] = pixels.numPixels();
|
doc["length"] = config.length;
|
||||||
doc["interval_ms"] = updateIntervalMs;
|
doc["brightness"] = currentState.brightness;
|
||||||
doc["brightness"] = brightness;
|
|
||||||
doc["pattern"] = currentPatternName();
|
doc["pattern"] = currentPatternName();
|
||||||
doc["total_steps"] = pixels.TotalSteps;
|
doc["color"] = String(currentState.color, HEX);
|
||||||
doc["color1"] = pixels.Color1;
|
doc["color2"] = String(currentState.color2, HEX);
|
||||||
doc["color2"] = pixels.Color2;
|
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;
|
String json;
|
||||||
serializeJson(doc, json);
|
serializeJson(doc, json);
|
||||||
@@ -60,7 +100,17 @@ void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
|
|||||||
void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
|
void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
JsonArray arr = doc.to<JsonArray>();
|
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;
|
String json;
|
||||||
serializeJson(doc, json);
|
serializeJson(doc, json);
|
||||||
@@ -68,121 +118,334 @@ void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
|
void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
|
||||||
|
bool updated = false;
|
||||||
|
|
||||||
if (request->hasParam("pattern", true)) {
|
if (request->hasParam("pattern", true)) {
|
||||||
String name = request->getParam("pattern", true)->value();
|
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)) {
|
if (request->hasParam("color", true)) {
|
||||||
unsigned long v = request->getParam("interval_ms", true)->value().toInt();
|
String colorStr = request->getParam("color", true)->value();
|
||||||
if (v < 1) v = 1;
|
uint32_t color = parseColor(colorStr);
|
||||||
updateIntervalMs = v;
|
setColor(color);
|
||||||
taskManager.setTaskInterval("neopattern_update", updateIntervalMs);
|
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)) {
|
if (request->hasParam("brightness", true)) {
|
||||||
int b = request->getParam("brightness", true)->value().toInt();
|
int b = request->getParam("brightness", true)->value().toInt();
|
||||||
if (b < 0) b = 0;
|
if (b < 0) b = 0;
|
||||||
if (b > 255) b = 255;
|
if (b > 255) b = 255;
|
||||||
setBrightness((uint8_t)b);
|
setBrightness(static_cast<uint8_t>(b));
|
||||||
}
|
updated = true;
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request->hasParam("total_steps", 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)) {
|
if (request->hasParam("direction", true)) {
|
||||||
String dir = request->getParam("direction", true)->value();
|
String dirStr = request->getParam("direction", true)->value();
|
||||||
if (dir.equalsIgnoreCase("forward")) pixels.Direction = FORWARD;
|
NeoDirection dir = (dirStr.equalsIgnoreCase("reverse")) ? NeoDirection::REVERSE : NeoDirection::FORWARD;
|
||||||
else if (dir.equalsIgnoreCase("reverse")) pixels.Direction = REVERSE;
|
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;
|
JsonDocument resp;
|
||||||
resp["ok"] = true;
|
resp["ok"] = true;
|
||||||
resp["pattern"] = currentPatternName();
|
resp["pattern"] = currentPatternName();
|
||||||
resp["interval_ms"] = updateIntervalMs;
|
resp["color"] = String(currentState.color, HEX);
|
||||||
resp["brightness"] = brightness;
|
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;
|
String json;
|
||||||
serializeJson(resp, json);
|
serializeJson(resp, json);
|
||||||
request->send(200, "application/json", json);
|
request->send(200, "application/json", json);
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoPatternService::setBrightness(uint8_t b) {
|
void NeoPatternService::handleStateRequest(AsyncWebServerRequest* request) {
|
||||||
brightness = b;
|
if (request->contentType() != "application/json") {
|
||||||
pixels.setBrightness(brightness);
|
request->send(400, "application/json", "{\"error\":\"Content-Type must be application/json\"}");
|
||||||
pixels.show();
|
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() {
|
void NeoPatternService::registerTasks() {
|
||||||
taskManager.registerTask("neopattern_update", updateIntervalMs, [this]() { update(); });
|
taskManager.registerTask("neopattern_update", updateIntervalMs, [this]() { update(); });
|
||||||
taskManager.registerTask("neopattern_status_print", 10000, [this]() {
|
taskManager.registerTask("neopattern_status_print", 10000, [this]() {
|
||||||
Serial.printf("[NeoPattern] pattern=%s interval=%lu ms brightness=%u\n",
|
LOG_INFO("NeoPattern", "Status update");
|
||||||
currentPatternName().c_str(), updateIntervalMs, brightness);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void NeoPatternService::registerPatterns() {
|
void NeoPatternService::registerPatterns() {
|
||||||
patternSetters["off"] = [this]() { pixels.ActivePattern = NONE; };
|
// Register all patterns with their metadata and callbacks
|
||||||
patternSetters["rainbow_cycle"] = [this]() { pixels.RainbowCycle(updateIntervalMs); };
|
patternRegistry.registerPattern(
|
||||||
patternSetters["theater_chase"] = [this]() { pixels.TheaterChase(pixels.Color1 ? pixels.Color1 : pixels.Color(127,127,127), pixels.Color2, updateIntervalMs); };
|
"none",
|
||||||
patternSetters["color_wipe"] = [this]() { pixels.ColorWipe(pixels.Color1 ? pixels.Color1 : pixels.Color(255,0,0), updateIntervalMs); };
|
static_cast<uint8_t>(NeoPatternType::NONE),
|
||||||
patternSetters["scanner"] = [this]() { pixels.Scanner(pixels.Color1 ? pixels.Color1 : pixels.Color(0,0,255), updateIntervalMs); };
|
"No pattern - solid color",
|
||||||
patternSetters["fade"] = [this]() { pixels.Fade(pixels.Color1, pixels.Color2, pixels.TotalSteps ? pixels.TotalSteps : 32, updateIntervalMs); };
|
[this]() { /* No initialization needed */ },
|
||||||
patternSetters["fire"] = [this]() { pixels.ActivePattern = FIRE; pixels.Interval = updateIntervalMs; };
|
[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() {
|
std::vector<String> NeoPatternService::patternNamesVector() const {
|
||||||
if (patternSetters.empty()) registerPatterns();
|
return patternRegistry.getAllPatternNames();
|
||||||
std::vector<String> v;
|
|
||||||
v.reserve(patternSetters.size());
|
|
||||||
for (const auto& kv : patternSetters) v.push_back(kv.first);
|
|
||||||
return v;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String NeoPatternService::currentPatternName() {
|
String NeoPatternService::currentPatternName() const {
|
||||||
switch (pixels.ActivePattern) {
|
return patternRegistry.getPatternName(static_cast<uint8_t>(activePattern));
|
||||||
case NONE: return String("off");
|
}
|
||||||
case RAINBOW_CYCLE: return String("rainbow_cycle");
|
|
||||||
case THEATER_CHASE: return String("theater_chase");
|
NeoPatternService::NeoPatternType NeoPatternService::nameToPattern(const String& name) const {
|
||||||
case COLOR_WIPE: return String("color_wipe");
|
uint8_t type = patternRegistry.getPatternType(name);
|
||||||
case SCANNER: return String("scanner");
|
return static_cast<NeoPatternType>(type);
|
||||||
case FADE: return String("fade");
|
}
|
||||||
case FIRE: return String("fire");
|
|
||||||
|
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) {
|
bool NeoPatternService::isValidPattern(const String& name) const {
|
||||||
if (patternSetters.empty()) registerPatterns();
|
return patternRegistry.isValidPattern(name);
|
||||||
auto it = patternSetters.find(name);
|
}
|
||||||
if (it != patternSetters.end()) {
|
|
||||||
pixels.Index = 0;
|
bool NeoPatternService::isValidPattern(NeoPatternType type) const {
|
||||||
pixels.Direction = FORWARD;
|
return patternRegistry.isValidPattern(static_cast<uint8_t>(type));
|
||||||
it->second();
|
}
|
||||||
}
|
|
||||||
|
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() {
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,95 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "spore/Service.h"
|
#include "spore/Service.h"
|
||||||
#include "spore/core/TaskManager.h"
|
#include "spore/core/TaskManager.h"
|
||||||
#include "NeoPattern.cpp"
|
#include "NeoPattern.h"
|
||||||
|
#include "NeoPatternState.h"
|
||||||
|
#include "NeoPixelConfig.h"
|
||||||
|
#include "PatternRegistry.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <vector>
|
#include <functional>
|
||||||
|
|
||||||
class NeoPatternService : public Service {
|
class NeoPatternService : public Service {
|
||||||
public:
|
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;
|
void registerEndpoints(ApiServer& api) override;
|
||||||
const char* getName() const override { return "NeoPattern"; }
|
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 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:
|
private:
|
||||||
void registerTasks();
|
void registerTasks();
|
||||||
void registerPatterns();
|
void registerPatterns();
|
||||||
std::vector<String> patternNamesVector();
|
|
||||||
String currentPatternName();
|
|
||||||
void update();
|
void update();
|
||||||
|
|
||||||
// Handlers
|
// Pattern updaters
|
||||||
|
void updateRainbowCycle();
|
||||||
|
void updateTheaterChase();
|
||||||
|
void updateColorWipe();
|
||||||
|
void updateScanner();
|
||||||
|
void updateFade();
|
||||||
|
void updateFire();
|
||||||
|
void updateNone();
|
||||||
|
|
||||||
|
// API handlers
|
||||||
void handleStatusRequest(AsyncWebServerRequest* request);
|
void handleStatusRequest(AsyncWebServerRequest* request);
|
||||||
void handlePatternsRequest(AsyncWebServerRequest* request);
|
void handlePatternsRequest(AsyncWebServerRequest* request);
|
||||||
void handleControlRequest(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;
|
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;
|
unsigned long updateIntervalMs;
|
||||||
uint8_t brightness;
|
unsigned long lastUpdateMs;
|
||||||
std::map<String, std::function<void()>> patternSetters;
|
bool initialized;
|
||||||
};
|
};
|
||||||
|
|||||||
59
examples/neopattern/NeoPatternState.h
Normal file
59
examples/neopattern/NeoPatternState.h
Normal 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
|
||||||
45
examples/neopattern/NeoPixelConfig.h
Normal file
45
examples/neopattern/NeoPixelConfig.h
Normal 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
|
||||||
101
examples/neopattern/PatternRegistry.cpp
Normal file
101
examples/neopattern/PatternRegistry.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
examples/neopattern/PatternRegistry.h
Normal file
73
examples/neopattern/PatternRegistry.h
Normal 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();
|
||||||
|
};
|
||||||
131
examples/neopattern/README.md
Normal file
131
examples/neopattern/README.md
Normal 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
|
||||||
@@ -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
|
|
||||||
@@ -2,25 +2,31 @@
|
|||||||
#include "spore/Spore.h"
|
#include "spore/Spore.h"
|
||||||
#include "spore/util/Logging.h"
|
#include "spore/util/Logging.h"
|
||||||
#include "NeoPatternService.h"
|
#include "NeoPatternService.h"
|
||||||
|
#include "NeoPixelConfig.h"
|
||||||
|
|
||||||
#ifndef LED_STRIP_PIN
|
// Configuration constants
|
||||||
#define LED_STRIP_PIN 2
|
#ifndef NEOPIXEL_PIN
|
||||||
|
#define NEOPIXEL_PIN 2
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef LED_STRIP_LENGTH
|
#ifndef NEOPIXEL_LENGTH
|
||||||
#define LED_STRIP_LENGTH 8
|
#define NEOPIXEL_LENGTH 8
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef LED_STRIP_TYPE
|
#ifndef NEOPIXEL_BRIGHTNESS
|
||||||
#define LED_STRIP_TYPE (NEO_GRB + NEO_KHZ800)
|
#define NEOPIXEL_BRIGHTNESS 100
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef NEOPIXEL_UPDATE_INTERVAL
|
||||||
|
#define NEOPIXEL_UPDATE_INTERVAL 100
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Create Spore instance with custom labels
|
// Create Spore instance with custom labels
|
||||||
Spore spore({
|
Spore spore({
|
||||||
{"app", "neopattern"},
|
{"app", "neopattern"},
|
||||||
{"device", "light"},
|
{"role", "led"},
|
||||||
{"pixels", String(LED_STRIP_LENGTH)},
|
{"pixels", String(NEOPIXEL_LENGTH)},
|
||||||
{"pin", String(LED_STRIP_PIN)}
|
{"pin", String(NEOPIXEL_PIN)}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create custom service
|
// Create custom service
|
||||||
@@ -30,14 +36,22 @@ void setup() {
|
|||||||
// Initialize the Spore framework
|
// Initialize the Spore framework
|
||||||
spore.setup();
|
spore.setup();
|
||||||
|
|
||||||
|
// Create configuration
|
||||||
|
NeoPixelConfig config(
|
||||||
|
NEOPIXEL_PIN,
|
||||||
|
NEOPIXEL_LENGTH,
|
||||||
|
NEOPIXEL_BRIGHTNESS,
|
||||||
|
NEOPIXEL_UPDATE_INTERVAL
|
||||||
|
);
|
||||||
|
|
||||||
// Create and add custom service
|
// 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);
|
spore.addService(neoPatternService);
|
||||||
|
|
||||||
// Start the API server and complete initialization
|
// Start the API server and complete initialization
|
||||||
spore.begin();
|
spore.begin();
|
||||||
|
|
||||||
LOG_INFO( "Main", "NeoPattern service registered and ready!");
|
LOG_INFO("Main", "NeoPattern service registered and ready!");
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
|||||||
@@ -1,293 +0,0 @@
|
|||||||
#include "NeoPixelService.h"
|
|
||||||
#include "spore/core/ApiServer.h"
|
|
||||||
#include "spore/util/Logging.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
#include <Arduino.h>
|
|
||||||
#include "spore/Spore.h"
|
|
||||||
#include "spore/util/Logging.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( "Main", "NeoPixel service registered and ready!");
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
// Run the Spore framework loop
|
|
||||||
spore.loop();
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
#include <Arduino.h>
|
|
||||||
#include "spore/Spore.h"
|
|
||||||
#include "spore/util/Logging.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( "Example", "Static web server started!");
|
|
||||||
LOG_INFO( "Example", "Visit http://<node-ip>/ to see the web interface");
|
|
||||||
}
|
|
||||||
|
|
||||||
void loop() {
|
|
||||||
// Let Spore handle the main loop
|
|
||||||
spore.loop();
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "core/TaskManager.h"
|
#include "core/TaskManager.h"
|
||||||
#include "Service.h"
|
#include "Service.h"
|
||||||
#include "util/Logging.h"
|
#include "util/Logging.h"
|
||||||
|
#include "util/CpuUsage.h"
|
||||||
|
|
||||||
class Spore {
|
class Spore {
|
||||||
public:
|
public:
|
||||||
@@ -34,6 +35,11 @@ public:
|
|||||||
ClusterManager& getCluster() { return cluster; }
|
ClusterManager& getCluster() { return cluster; }
|
||||||
ApiServer& getApiServer() { return apiServer; }
|
ApiServer& getApiServer() { return apiServer; }
|
||||||
|
|
||||||
|
// CPU usage monitoring
|
||||||
|
CpuUsage& getCpuUsage() { return cpuUsage; }
|
||||||
|
float getCurrentCpuUsage() const { return cpuUsage.getCpuUsage(); }
|
||||||
|
float getAverageCpuUsage() const { return cpuUsage.getAverageCpuUsage(); }
|
||||||
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initializeCore();
|
void initializeCore();
|
||||||
@@ -45,6 +51,7 @@ private:
|
|||||||
TaskManager taskManager;
|
TaskManager taskManager;
|
||||||
ClusterManager cluster;
|
ClusterManager cluster;
|
||||||
ApiServer apiServer;
|
ApiServer apiServer;
|
||||||
|
CpuUsage cpuUsage;
|
||||||
|
|
||||||
std::vector<std::shared_ptr<Service>> services;
|
std::vector<std::shared_ptr<Service>> services;
|
||||||
bool initialized;
|
bool initialized;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "spore/types/NodeInfo.h"
|
#include "spore/types/NodeInfo.h"
|
||||||
#include "spore/core/TaskManager.h"
|
#include "spore/core/TaskManager.h"
|
||||||
#include "spore/types/ApiTypes.h"
|
#include "spore/types/ApiTypes.h"
|
||||||
|
#include "spore/util/Logging.h"
|
||||||
|
|
||||||
class Service; // Forward declaration
|
class Service; // Forward declaration
|
||||||
|
|
||||||
@@ -19,15 +20,17 @@ public:
|
|||||||
ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80);
|
ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80);
|
||||||
void begin();
|
void begin();
|
||||||
void addService(Service& service);
|
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,
|
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 String& serviceName = "unknown");
|
||||||
|
|
||||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
|
||||||
const std::vector<ParamSpec>& params);
|
|
||||||
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,
|
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
|
// Static file serving
|
||||||
void serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header = "");
|
void serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header = "");
|
||||||
|
|||||||
51
include/spore/services/MonitoringService.h
Normal file
51
include/spore/services/MonitoringService.h
Normal 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;
|
||||||
|
};
|
||||||
114
include/spore/types/ApiResponse.h
Normal file
114
include/spore/types/ApiResponse.h
Normal 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
|
||||||
47
include/spore/types/ClusterResponse.h
Normal file
47
include/spore/types/ClusterResponse.h
Normal 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
|
||||||
76
include/spore/types/EndpointInfoSerializable.h
Normal file
76
include/spore/types/EndpointInfoSerializable.h
Normal 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
|
||||||
141
include/spore/types/NodeInfoSerializable.h
Normal file
141
include/spore/types/NodeInfoSerializable.h
Normal 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
|
||||||
102
include/spore/types/NodeResponse.h
Normal file
102
include/spore/types/NodeResponse.h
Normal 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
|
||||||
99
include/spore/types/TaskInfoSerializable.h
Normal file
99
include/spore/types/TaskInfoSerializable.h
Normal 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
|
||||||
194
include/spore/types/TaskResponse.h
Normal file
194
include/spore/types/TaskResponse.h
Normal 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
|
||||||
118
include/spore/util/CpuUsage.h
Normal file
118
include/spore/util/CpuUsage.h
Normal 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);
|
||||||
|
};
|
||||||
56
include/spore/util/JsonSerializable.h
Normal file
56
include/spore/util/JsonSerializable.h
Normal 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
|
||||||
107
platformio.ini
107
platformio.ini
@@ -9,7 +9,7 @@
|
|||||||
; https://docs.platformio.org/page/projectconf.html
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
[platformio]
|
[platformio]
|
||||||
;default_envs = esp01_1m
|
default_envs = base
|
||||||
src_dir = .
|
src_dir = .
|
||||||
data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data
|
data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data
|
||||||
|
|
||||||
@@ -80,69 +80,7 @@ build_src_filter =
|
|||||||
+<src/spore/util/*.cpp>
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.cpp>
|
+<src/internal/*.cpp>
|
||||||
|
|
||||||
[env:d1_mini_relay]
|
[env: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}
|
|
||||||
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/spore/util/*.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/spore/util/*.cpp>
|
|
||||||
+<src/internal/*.cpp>
|
|
||||||
|
|
||||||
[env:esp01_1m_neopattern]
|
|
||||||
platform = platformio/espressif8266@^4.2.1
|
platform = platformio/espressif8266@^4.2.1
|
||||||
board = esp01_1m
|
board = esp01_1m
|
||||||
framework = arduino
|
framework = arduino
|
||||||
@@ -162,44 +100,3 @@ build_src_filter =
|
|||||||
+<src/spore/types/*.cpp>
|
+<src/spore/types/*.cpp>
|
||||||
+<src/spore/util/*.cpp>
|
+<src/spore/util/*.cpp>
|
||||||
+<src/internal/*.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/spore/util/*.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>
|
|
||||||
|
|||||||
@@ -4,17 +4,18 @@
|
|||||||
#include "spore/services/ClusterService.h"
|
#include "spore/services/ClusterService.h"
|
||||||
#include "spore/services/TaskService.h"
|
#include "spore/services/TaskService.h"
|
||||||
#include "spore/services/StaticFileService.h"
|
#include "spore/services/StaticFileService.h"
|
||||||
|
#include "spore/services/MonitoringService.h"
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
|
||||||
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
||||||
apiServer(ctx, taskManager, ctx.config.api_server_port),
|
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)
|
Spore::Spore(std::initializer_list<std::pair<String, String>> initialLabels)
|
||||||
: ctx(initialLabels), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
: ctx(initialLabels), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
||||||
apiServer(ctx, taskManager, ctx.config.api_server_port),
|
apiServer(ctx, taskManager, ctx.config.api_server_port),
|
||||||
initialized(false), apiServerStarted(false) {
|
cpuUsage(), initialized(false), apiServerStarted(false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Spore::~Spore() {
|
Spore::~Spore() {
|
||||||
@@ -33,6 +34,9 @@ void Spore::setup() {
|
|||||||
// Initialize core components
|
// Initialize core components
|
||||||
initializeCore();
|
initializeCore();
|
||||||
|
|
||||||
|
// Initialize CPU usage monitoring
|
||||||
|
cpuUsage.begin();
|
||||||
|
|
||||||
// Register core services
|
// Register core services
|
||||||
registerCoreServices();
|
registerCoreServices();
|
||||||
|
|
||||||
@@ -68,7 +72,16 @@ void Spore::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start CPU usage measurement
|
||||||
|
cpuUsage.startMeasurement();
|
||||||
|
|
||||||
|
// Execute main tasks
|
||||||
taskManager.execute();
|
taskManager.execute();
|
||||||
|
|
||||||
|
// End CPU usage measurement before yield
|
||||||
|
cpuUsage.endMeasurement();
|
||||||
|
|
||||||
|
// Yield to allow other tasks to run
|
||||||
yield();
|
yield();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +134,7 @@ void Spore::registerCoreServices() {
|
|||||||
auto clusterService = std::make_shared<ClusterService>(ctx);
|
auto clusterService = std::make_shared<ClusterService>(ctx);
|
||||||
auto taskService = std::make_shared<TaskService>(taskManager);
|
auto taskService = std::make_shared<TaskService>(taskManager);
|
||||||
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
|
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
|
||||||
|
auto monitoringService = std::make_shared<MonitoringService>(cpuUsage);
|
||||||
|
|
||||||
// Add to services list
|
// Add to services list
|
||||||
services.push_back(nodeService);
|
services.push_back(nodeService);
|
||||||
@@ -128,6 +142,7 @@ void Spore::registerCoreServices() {
|
|||||||
services.push_back(clusterService);
|
services.push_back(clusterService);
|
||||||
services.push_back(taskService);
|
services.push_back(taskService);
|
||||||
services.push_back(staticFileService);
|
services.push_back(staticFileService);
|
||||||
|
services.push_back(monitoringService);
|
||||||
|
|
||||||
LOG_INFO("Spore", "Core services registered");
|
LOG_INFO("Spore", "Core services registered");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,57 +21,31 @@ void ApiServer::registerEndpoint(const String& uri, int method,
|
|||||||
const String& serviceName) {
|
const String& serviceName) {
|
||||||
// Add to local endpoints
|
// Add to local endpoints
|
||||||
endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
|
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) {
|
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||||
// Get current service name if available
|
const String& serviceName) {
|
||||||
String serviceName = "unknown";
|
|
||||||
if (!services.empty()) {
|
|
||||||
serviceName = services.back().get().getName();
|
|
||||||
}
|
|
||||||
registerEndpoint(uri, method, {}, serviceName);
|
registerEndpoint(uri, method, {}, serviceName);
|
||||||
server.on(uri.c_str(), method, requestHandler);
|
server.on(uri.c_str(), method, requestHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> 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) {
|
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
||||||
// Get current service name if available
|
const String& serviceName) {
|
||||||
String serviceName = "unknown";
|
|
||||||
if (!services.empty()) {
|
|
||||||
serviceName = services.back().get().getName();
|
|
||||||
}
|
|
||||||
registerEndpoint(uri, method, {}, serviceName);
|
registerEndpoint(uri, method, {}, serviceName);
|
||||||
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Overloads that also record minimal capability specs
|
// Overloads that also record minimal capability specs
|
||||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||||
const std::vector<ParamSpec>& params) {
|
const std::vector<ParamSpec>& params, const String& serviceName) {
|
||||||
// Get current service name if available
|
|
||||||
String serviceName = "unknown";
|
|
||||||
if (!services.empty()) {
|
|
||||||
serviceName = services.back().get().getName();
|
|
||||||
}
|
|
||||||
registerEndpoint(uri, method, params, serviceName);
|
registerEndpoint(uri, method, params, serviceName);
|
||||||
server.on(uri.c_str(), method, requestHandler);
|
server.on(uri.c_str(), method, requestHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> 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,
|
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
||||||
const std::vector<ParamSpec>& params) {
|
const std::vector<ParamSpec>& params, const String& serviceName) {
|
||||||
// Get current service name if available
|
|
||||||
String serviceName = "unknown";
|
|
||||||
if (!services.empty()) {
|
|
||||||
serviceName = services.back().get().getName();
|
|
||||||
}
|
|
||||||
registerEndpoint(uri, method, params, serviceName);
|
registerEndpoint(uri, method, params, serviceName);
|
||||||
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,6 @@ void NetworkManager::setupWiFi() {
|
|||||||
}
|
}
|
||||||
ctx.self.lastSeen = millis();
|
ctx.self.lastSeen = millis();
|
||||||
ctx.self.status = NodeInfo::ACTIVE;
|
ctx.self.status = NodeInfo::ACTIVE;
|
||||||
ctx.self.labels["hostname"] = ctx.hostname;
|
|
||||||
|
|
||||||
// Ensure member list has an entry for this node
|
// Ensure member list has an entry for this node
|
||||||
auto &memberList = *ctx.memberList;
|
auto &memberList = *ctx.memberList;
|
||||||
|
|||||||
@@ -1,42 +1,19 @@
|
|||||||
#include "spore/services/ClusterService.h"
|
#include "spore/services/ClusterService.h"
|
||||||
#include "spore/core/ApiServer.h"
|
#include "spore/core/ApiServer.h"
|
||||||
|
#include "spore/types/ClusterResponse.h"
|
||||||
|
|
||||||
|
using spore::types::ClusterMembersResponse;
|
||||||
|
|
||||||
ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {}
|
ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {}
|
||||||
|
|
||||||
void ClusterService::registerEndpoints(ApiServer& api) {
|
void ClusterService::registerEndpoints(ApiServer& api) {
|
||||||
api.addEndpoint("/api/cluster/members", HTTP_GET,
|
api.addEndpoint("/api/cluster/members", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleMembersRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleMembersRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "ClusterService");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
|
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
|
||||||
JsonDocument doc;
|
ClusterMembersResponse response;
|
||||||
JsonArray arr = doc["members"].to<JsonArray>();
|
response.addNodes(ctx.memberList);
|
||||||
|
request->send(200, "application/json", response.toJsonString());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/spore/services/MonitoringService.cpp
Normal file
115
src/spore/services/MonitoringService.cpp
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,16 +8,16 @@ void NetworkService::registerEndpoints(ApiServer& api) {
|
|||||||
// WiFi scanning endpoints
|
// WiFi scanning endpoints
|
||||||
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
|
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "NetworkService");
|
||||||
|
|
||||||
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
|
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
|
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "NetworkService");
|
||||||
|
|
||||||
// Network status and configuration endpoints
|
// Network status and configuration endpoints
|
||||||
api.addEndpoint("/api/network/status", HTTP_GET,
|
api.addEndpoint("/api/network/status", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
|
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "NetworkService");
|
||||||
|
|
||||||
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
|
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
|
[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("password"), true, String("body"), String("string"), {}, String("")},
|
||||||
ParamSpec{String("connect_timeout_ms"), false, String("body"), String("number"), {}, String("10000")},
|
ParamSpec{String("connect_timeout_ms"), false, String("body"), String("number"), {}, String("10000")},
|
||||||
ParamSpec{String("retry_delay_ms"), false, String("body"), String("number"), {}, String("500")}
|
ParamSpec{String("retry_delay_ms"), false, String("body"), String("number"), {}, String("500")}
|
||||||
});
|
}, "NetworkService");
|
||||||
}
|
}
|
||||||
|
|
||||||
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {
|
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include "spore/services/NodeService.h"
|
#include "spore/services/NodeService.h"
|
||||||
#include "spore/core/ApiServer.h"
|
#include "spore/core/ApiServer.h"
|
||||||
#include "spore/util/Logging.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) {}
|
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
|
||||||
|
|
||||||
@@ -8,7 +13,7 @@ void NodeService::registerEndpoints(ApiServer& api) {
|
|||||||
// Status endpoint
|
// Status endpoint
|
||||||
api.addEndpoint("/api/node/status", HTTP_GET,
|
api.addEndpoint("/api/node/status", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "NodeService");
|
||||||
|
|
||||||
// Update endpoint with file upload
|
// Update endpoint with file upload
|
||||||
api.addEndpoint("/api/node/update", HTTP_POST,
|
api.addEndpoint("/api/node/update", HTTP_POST,
|
||||||
@@ -18,54 +23,45 @@ void NodeService::registerEndpoints(ApiServer& api) {
|
|||||||
},
|
},
|
||||||
std::vector<ParamSpec>{
|
std::vector<ParamSpec>{
|
||||||
ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")}
|
ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")}
|
||||||
});
|
}, "NodeService");
|
||||||
|
|
||||||
// Restart endpoint
|
// Restart endpoint
|
||||||
api.addEndpoint("/api/node/restart", HTTP_POST,
|
api.addEndpoint("/api/node/restart", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "NodeService");
|
||||||
|
|
||||||
// Endpoints endpoint
|
// Endpoints endpoint
|
||||||
api.addEndpoint("/api/node/endpoints", HTTP_GET,
|
api.addEndpoint("/api/node/endpoints", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "NodeService");
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodeService::handleStatusRequest(AsyncWebServerRequest* request) {
|
void NodeService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||||
JsonDocument doc;
|
NodeStatusResponse response;
|
||||||
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
|
// Get labels from member list or self
|
||||||
|
std::map<String, String> labels;
|
||||||
if (ctx.memberList) {
|
if (ctx.memberList) {
|
||||||
auto it = ctx.memberList->find(ctx.hostname);
|
auto it = ctx.memberList->find(ctx.hostname);
|
||||||
if (it != ctx.memberList->end()) {
|
if (it != ctx.memberList->end()) {
|
||||||
JsonObject labelsObj = doc["labels"].to<JsonObject>();
|
labels = it->second.labels;
|
||||||
for (const auto& kv : it->second.labels) {
|
|
||||||
labelsObj[kv.first.c_str()] = kv.second;
|
|
||||||
}
|
|
||||||
} else if (!ctx.self.labels.empty()) {
|
} else if (!ctx.self.labels.empty()) {
|
||||||
JsonObject labelsObj = doc["labels"].to<JsonObject>();
|
labels = ctx.self.labels;
|
||||||
for (const auto& kv : ctx.self.labels) {
|
|
||||||
labelsObj[kv.first.c_str()] = kv.second;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String json;
|
response.buildCompleteResponse(labels);
|
||||||
serializeJson(doc, json);
|
request->send(200, "application/json", response.toJsonString());
|
||||||
request->send(200, "application/json", json);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
|
void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
|
||||||
bool success = !Update.hasError();
|
bool success = !Update.hasError();
|
||||||
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
|
NodeOperationResponse response;
|
||||||
success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
|
response.setSuccess(success ? "OK" : "FAIL");
|
||||||
response->addHeader("Connection", "close");
|
|
||||||
request->send(response);
|
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
|
||||||
|
httpResponse->addHeader("Connection", "close");
|
||||||
|
request->send(httpResponse);
|
||||||
request->onDisconnect([this]() {
|
request->onDisconnect([this]() {
|
||||||
LOG_INFO("API", "Restart device");
|
LOG_INFO("API", "Restart device");
|
||||||
delay(10);
|
delay(10);
|
||||||
@@ -108,10 +104,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
|
void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
|
||||||
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
|
NodeOperationResponse response;
|
||||||
"{\"status\": \"restarting\"}");
|
response.setSuccess("restarting");
|
||||||
response->addHeader("Connection", "close");
|
|
||||||
request->send(response);
|
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
|
||||||
|
httpResponse->addHeader("Connection", "close");
|
||||||
|
request->send(httpResponse);
|
||||||
request->onDisconnect([this]() {
|
request->onDisconnect([this]() {
|
||||||
LOG_INFO("API", "Restart device");
|
LOG_INFO("API", "Restart device");
|
||||||
delay(10);
|
delay(10);
|
||||||
@@ -120,36 +118,7 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NodeService::handleEndpointsRequest(AsyncWebServerRequest* request) {
|
void NodeService::handleEndpointsRequest(AsyncWebServerRequest* request) {
|
||||||
JsonDocument doc;
|
NodeEndpointsResponse response;
|
||||||
JsonArray endpointsArr = doc["endpoints"].to<JsonArray>();
|
response.addEndpoints(apiServer.getEndpoints());
|
||||||
|
request->send(200, "application/json", response.toJsonString());
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
#include "spore/services/TaskService.h"
|
#include "spore/services/TaskService.h"
|
||||||
#include "spore/core/ApiServer.h"
|
#include "spore/core/ApiServer.h"
|
||||||
|
#include "spore/types/TaskResponse.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
using spore::types::TaskStatusResponse;
|
||||||
|
using spore::types::TaskControlResponse;
|
||||||
|
|
||||||
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
|
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
|
||||||
|
|
||||||
void TaskService::registerEndpoints(ApiServer& api) {
|
void TaskService::registerEndpoints(ApiServer& api) {
|
||||||
api.addEndpoint("/api/tasks/status", HTTP_GET,
|
api.addEndpoint("/api/tasks/status", HTTP_GET,
|
||||||
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
||||||
std::vector<ParamSpec>{});
|
std::vector<ParamSpec>{}, "TaskService");
|
||||||
|
|
||||||
api.addEndpoint("/api/tasks/control", HTTP_POST,
|
api.addEndpoint("/api/tasks/control", HTTP_POST,
|
||||||
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
[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("enable"), String("disable"), String("start"), String("stop"), String("status")},
|
||||||
String("")
|
String("")
|
||||||
}
|
}
|
||||||
});
|
}, "TaskService");
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
|
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||||
|
TaskStatusResponse response;
|
||||||
|
|
||||||
|
// Get task statuses using a separate document to avoid reference issues
|
||||||
JsonDocument scratch;
|
JsonDocument scratch;
|
||||||
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
|
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
|
||||||
|
|
||||||
JsonDocument doc;
|
// Build the complete response with the task data
|
||||||
JsonObject summaryObj = doc["summary"].to<JsonObject>();
|
response.buildCompleteResponse(taskStatuses);
|
||||||
summaryObj["totalTasks"] = taskStatuses.size();
|
request->send(200, "application/json", response.toJsonString());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
|
void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
|
||||||
@@ -88,50 +75,27 @@ void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
|
|||||||
success = true;
|
success = true;
|
||||||
message = "Task status retrieved";
|
message = "Task status retrieved";
|
||||||
|
|
||||||
JsonDocument statusDoc;
|
TaskControlResponse response;
|
||||||
statusDoc["success"] = success;
|
response.setResponse(success, message, taskName, action);
|
||||||
statusDoc["message"] = message;
|
response.addTaskDetails(taskName,
|
||||||
statusDoc["task"] = taskName;
|
taskManager.isTaskEnabled(taskName.c_str()),
|
||||||
statusDoc["action"] = action;
|
taskManager.isTaskRunning(taskName.c_str()),
|
||||||
|
taskManager.getTaskInterval(taskName.c_str()));
|
||||||
|
|
||||||
statusDoc["taskDetails"] = JsonObject();
|
request->send(200, "application/json", response.toJsonString());
|
||||||
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);
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
success = false;
|
success = false;
|
||||||
message = "Invalid action. Use: enable, disable, start, stop, or status";
|
message = "Invalid action. Use: enable, disable, start, stop, or status";
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonDocument doc;
|
TaskControlResponse response;
|
||||||
doc["success"] = success;
|
response.setResponse(success, message, taskName, action);
|
||||||
doc["message"] = message;
|
request->send(success ? 200 : 400, "application/json", response.toJsonString());
|
||||||
doc["task"] = taskName;
|
|
||||||
doc["action"] = action;
|
|
||||||
|
|
||||||
String json;
|
|
||||||
serializeJson(doc, json);
|
|
||||||
request->send(success ? 200 : 400, "application/json", json);
|
|
||||||
} else {
|
} else {
|
||||||
JsonDocument doc;
|
TaskControlResponse response;
|
||||||
doc["success"] = false;
|
response.setError("Missing parameters. Required: task, action",
|
||||||
doc["message"] = "Missing parameters. Required: task, action";
|
"{\"task\": \"discovery_send\", \"action\": \"status\"}");
|
||||||
doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}";
|
request->send(400, "application/json", response.toJsonString());
|
||||||
|
|
||||||
String json;
|
|
||||||
serializeJson(doc, json);
|
|
||||||
request->send(400, "application/json", json);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
185
src/spore/util/CpuUsage.cpp
Normal file
185
src/spore/util/CpuUsage.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user