Compare commits
6 Commits
4727405be1
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
| 025ac7b810 | |||
| a9f56c1279 | |||
| 594b5e3af6 | |||
| c11652c123 | |||
| 51d4d4bc94 | |||
| e95eb09a11 |
376
examples/neopattern/NeoPattern.cpp
Normal file
376
examples/neopattern/NeoPattern.cpp
Normal file
@@ -0,0 +1,376 @@
|
||||
#include "NeoPattern.h"
|
||||
|
||||
// Constructor - calls base-class constructor to initialize strip
|
||||
NeoPattern::NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int))
|
||||
: Adafruit_NeoPixel(pixels, pin, type)
|
||||
{
|
||||
frameBuffer = (uint8_t *)malloc(768);
|
||||
OnComplete = callback;
|
||||
TotalSteps = numPixels();
|
||||
begin();
|
||||
}
|
||||
|
||||
NeoPattern::NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
|
||||
: Adafruit_NeoPixel(pixels, pin, type)
|
||||
{
|
||||
frameBuffer = (uint8_t *)malloc(768);
|
||||
TotalSteps = numPixels();
|
||||
begin();
|
||||
}
|
||||
|
||||
NeoPattern::~NeoPattern() {
|
||||
if (frameBuffer) {
|
||||
free(frameBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
void NeoPattern::handleStream(uint8_t *data, size_t len)
|
||||
{
|
||||
//const uint16_t *data16 = (uint16_t *)data;
|
||||
bufferSize = len;
|
||||
memcpy(frameBuffer, data, len);
|
||||
}
|
||||
|
||||
void NeoPattern::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 NeoPattern::onCompleteDefault(int pixels)
|
||||
{
|
||||
//Serial.println("onCompleteDefault");
|
||||
// FIXME no specific code
|
||||
if (ActivePattern == THEATER_CHASE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Reverse();
|
||||
//Serial.println("pattern completed");
|
||||
}
|
||||
|
||||
// Increment the Index and reset at the end
|
||||
void NeoPattern::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 NeoPattern::Reverse()
|
||||
{
|
||||
if (Direction == FORWARD)
|
||||
{
|
||||
Direction = REVERSE;
|
||||
Index = TotalSteps - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Direction = FORWARD;
|
||||
Index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize for a RainbowCycle
|
||||
void NeoPattern::RainbowCycle(uint8_t interval, direction dir)
|
||||
{
|
||||
ActivePattern = RAINBOW_CYCLE;
|
||||
Interval = interval;
|
||||
TotalSteps = 255;
|
||||
Index = 0;
|
||||
Direction = dir;
|
||||
}
|
||||
|
||||
// Update the Rainbow Cycle Pattern
|
||||
void NeoPattern::RainbowCycleUpdate()
|
||||
{
|
||||
for (int i = 0; i < numPixels(); i++)
|
||||
{
|
||||
setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255));
|
||||
}
|
||||
show();
|
||||
Increment();
|
||||
}
|
||||
|
||||
// Initialize for a Theater Chase
|
||||
void NeoPattern::TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir)
|
||||
{
|
||||
ActivePattern = THEATER_CHASE;
|
||||
Interval = interval;
|
||||
TotalSteps = numPixels();
|
||||
Color1 = color1;
|
||||
Color2 = color2;
|
||||
Index = 0;
|
||||
Direction = dir;
|
||||
}
|
||||
|
||||
// Update the Theater Chase Pattern
|
||||
void NeoPattern::TheaterChaseUpdate()
|
||||
{
|
||||
for (int i = 0; i < numPixels(); i++)
|
||||
{
|
||||
if ((i + Index) % 3 == 0)
|
||||
{
|
||||
setPixelColor(i, Color1);
|
||||
}
|
||||
else
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 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();
|
||||
}
|
||||
@@ -56,424 +56,54 @@ class NeoPattern : public Adafruit_NeoPixel
|
||||
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);
|
||||
OnComplete = callback;
|
||||
TotalSteps = numPixels();
|
||||
begin();
|
||||
}
|
||||
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int));
|
||||
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type);
|
||||
~NeoPattern();
|
||||
|
||||
NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type)
|
||||
: Adafruit_NeoPixel(pixels, pin, type)
|
||||
{
|
||||
frameBuffer = (uint8_t *)malloc(768);
|
||||
TotalSteps = numPixels();
|
||||
begin();
|
||||
}
|
||||
// Stream handling
|
||||
void handleStream(uint8_t *data, size_t len);
|
||||
void drawFrameBuffer(int w, uint8_t *frame, int length);
|
||||
|
||||
~NeoPattern() {
|
||||
if (frameBuffer) {
|
||||
free(frameBuffer);
|
||||
}
|
||||
}
|
||||
// Pattern completion
|
||||
void onCompleteDefault(int pixels);
|
||||
|
||||
// Pattern control
|
||||
void Increment();
|
||||
void Reverse();
|
||||
|
||||
// Implementation starts here (inline)
|
||||
void handleStream(uint8_t *data, size_t len)
|
||||
{
|
||||
//const uint16_t *data16 = (uint16_t *)data;
|
||||
bufferSize = len;
|
||||
memcpy(frameBuffer, data, len);
|
||||
}
|
||||
// Rainbow Cycle
|
||||
void RainbowCycle(uint8_t interval, direction dir = FORWARD);
|
||||
void RainbowCycleUpdate();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Theater Chase
|
||||
void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD);
|
||||
void TheaterChaseUpdate();
|
||||
|
||||
void onCompleteDefault(int pixels)
|
||||
{
|
||||
//Serial.println("onCompleteDefault");
|
||||
// FIXME no specific code
|
||||
if (ActivePattern == THEATER_CHASE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Reverse();
|
||||
//Serial.println("pattern completed");
|
||||
}
|
||||
// Color Wipe
|
||||
void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD);
|
||||
void ColorWipeUpdate();
|
||||
|
||||
// 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;
|
||||
case NONE:
|
||||
// For NONE pattern, just maintain current state
|
||||
break;
|
||||
default:
|
||||
if (bufferSize > 0)
|
||||
{
|
||||
drawFrameBuffer(TotalSteps, frameBuffer, bufferSize);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Scanner
|
||||
void Scanner(uint32_t color1, uint8_t interval);
|
||||
void ScannerUpdate();
|
||||
|
||||
void UpdateScheduled()
|
||||
{
|
||||
if ((millis() - lastUpdate) > Interval) // time to update
|
||||
{
|
||||
lastUpdate = millis();
|
||||
Update();
|
||||
}
|
||||
}
|
||||
// Fade
|
||||
void Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir = FORWARD);
|
||||
void FadeUpdate();
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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();
|
||||
|
||||
// Reverse pattern direction
|
||||
void Reverse()
|
||||
{
|
||||
if (Direction == FORWARD)
|
||||
{
|
||||
Direction = REVERSE;
|
||||
Index = TotalSteps - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Direction = FORWARD;
|
||||
Index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize for a RainbowCycle
|
||||
void RainbowCycle(uint8_t interval, direction dir = FORWARD)
|
||||
{
|
||||
ActivePattern = RAINBOW_CYCLE;
|
||||
Interval = interval;
|
||||
TotalSteps = 255;
|
||||
Index = 0;
|
||||
Direction = dir;
|
||||
}
|
||||
|
||||
// Update the Rainbow Cycle Pattern
|
||||
void RainbowCycleUpdate()
|
||||
{
|
||||
for (int i = 0; i < numPixels(); i++)
|
||||
{
|
||||
setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255));
|
||||
}
|
||||
show();
|
||||
Increment();
|
||||
}
|
||||
|
||||
// Initialize for a Theater Chase
|
||||
void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD)
|
||||
{
|
||||
ActivePattern = THEATER_CHASE;
|
||||
Interval = interval;
|
||||
TotalSteps = numPixels();
|
||||
Color1 = color1;
|
||||
Color2 = color2;
|
||||
Index = 0;
|
||||
Direction = dir;
|
||||
}
|
||||
|
||||
// Update the Theater Chase Pattern
|
||||
void TheaterChaseUpdate()
|
||||
{
|
||||
for (int i = 0; i < numPixels(); i++)
|
||||
{
|
||||
if ((i + Index) % 3 == 0)
|
||||
{
|
||||
setPixelColor(i, Color1);
|
||||
}
|
||||
else
|
||||
{
|
||||
setPixelColor(i, Color2);
|
||||
}
|
||||
}
|
||||
show();
|
||||
Increment();
|
||||
}
|
||||
|
||||
// Initialize for a ColorWipe
|
||||
void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD)
|
||||
{
|
||||
ActivePattern = COLOR_WIPE;
|
||||
Interval = interval;
|
||||
TotalSteps = numPixels();
|
||||
Color1 = color;
|
||||
Index = 0;
|
||||
Direction = dir;
|
||||
}
|
||||
|
||||
// Update the Color Wipe Pattern
|
||||
void ColorWipeUpdate()
|
||||
{
|
||||
setPixelColor(Index, Color1);
|
||||
show();
|
||||
Increment();
|
||||
}
|
||||
|
||||
// Initialize for a SCANNNER
|
||||
void Scanner(uint32_t color1, uint8_t interval)
|
||||
{
|
||||
ActivePattern = SCANNER;
|
||||
Interval = interval;
|
||||
TotalSteps = (numPixels() - 1) * 2;
|
||||
Color1 = color1;
|
||||
Index = 0;
|
||||
}
|
||||
|
||||
// Update the Scanner Pattern
|
||||
void ScannerUpdate()
|
||||
{
|
||||
for (int i = 0; i < numPixels(); i++)
|
||||
{
|
||||
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 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
|
||||
{
|
||||
heat[i] = heat[i] - cooldown;
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
// 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 setPixel(int Pixel, uint8_t red, uint8_t green, uint8_t blue)
|
||||
{
|
||||
setPixelColor(Pixel, Color(red, green, blue));
|
||||
}
|
||||
|
||||
void showStrip()
|
||||
{
|
||||
show();
|
||||
}
|
||||
// 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
|
||||
@@ -59,10 +59,10 @@ void NeoPatternService::registerEndpoints(ApiServer& api) {
|
||||
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
||||
std::vector<ParamSpec>{
|
||||
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
|
||||
ParamSpec{String("color"), false, String("body"), String("string"), {}},
|
||||
ParamSpec{String("color2"), false, String("body"), String("string"), {}},
|
||||
ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("100")},
|
||||
ParamSpec{String("total_steps"), false, String("body"), String("number"), {}, String("16")},
|
||||
ParamSpec{String("color"), false, String("body"), String("color"), {}},
|
||||
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
|
||||
ParamSpec{String("brightness"), false, String("body"), String("numberRange"), {}, String("80")},
|
||||
ParamSpec{String("total_steps"), false, String("body"), String("numberRange"), {}, String("16")},
|
||||
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}},
|
||||
ParamSpec{String("interval"), false, String("body"), String("number"), {}, String("100")}
|
||||
});
|
||||
@@ -86,6 +86,12 @@ void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||
doc["interval"] = updateIntervalMs;
|
||||
doc["active"] = initialized;
|
||||
|
||||
// Add pattern metadata
|
||||
String currentPattern = currentPatternName();
|
||||
doc["pattern_description"] = getPatternDescription(currentPattern);
|
||||
doc["pattern_requires_color2"] = patternRequiresColor2(currentPattern);
|
||||
doc["pattern_supports_direction"] = patternSupportsDirection(currentPattern);
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
@@ -94,8 +100,16 @@ void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||
void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
JsonArray arr = doc.to<JsonArray>();
|
||||
for (const auto& kv : patternUpdaters) {
|
||||
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;
|
||||
@@ -108,8 +122,13 @@ void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
|
||||
|
||||
if (request->hasParam("pattern", true)) {
|
||||
String name = request->getParam("pattern", true)->value();
|
||||
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("color", true)) {
|
||||
@@ -195,6 +214,9 @@ void NeoPatternService::setPattern(NeoPatternType 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) {
|
||||
@@ -263,46 +285,89 @@ void NeoPatternService::registerTasks() {
|
||||
}
|
||||
|
||||
void NeoPatternService::registerPatterns() {
|
||||
// Register pattern updaters
|
||||
patternUpdaters["none"] = [this]() { updateNone(); };
|
||||
patternUpdaters["rainbow_cycle"] = [this]() { updateRainbowCycle(); };
|
||||
patternUpdaters["theater_chase"] = [this]() { updateTheaterChase(); };
|
||||
patternUpdaters["color_wipe"] = [this]() { updateColorWipe(); };
|
||||
patternUpdaters["scanner"] = [this]() { updateScanner(); };
|
||||
patternUpdaters["fade"] = [this]() { updateFade(); };
|
||||
patternUpdaters["fire"] = [this]() { updateFire(); };
|
||||
// Register all patterns with their metadata and callbacks
|
||||
patternRegistry.registerPattern(
|
||||
"none",
|
||||
static_cast<uint8_t>(NeoPatternType::NONE),
|
||||
"No pattern - solid color",
|
||||
[this]() { /* No initialization needed */ },
|
||||
[this]() { updateNone(); },
|
||||
false, // doesn't require color2
|
||||
false // doesn't support direction
|
||||
);
|
||||
|
||||
// Register name to pattern mapping
|
||||
nameToPatternMap["none"] = NeoPatternType::NONE;
|
||||
nameToPatternMap["rainbow_cycle"] = NeoPatternType::RAINBOW_CYCLE;
|
||||
nameToPatternMap["theater_chase"] = NeoPatternType::THEATER_CHASE;
|
||||
nameToPatternMap["color_wipe"] = NeoPatternType::COLOR_WIPE;
|
||||
nameToPatternMap["scanner"] = NeoPatternType::SCANNER;
|
||||
nameToPatternMap["fade"] = NeoPatternType::FADE;
|
||||
nameToPatternMap["fire"] = NeoPatternType::FIRE;
|
||||
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() const {
|
||||
std::vector<String> names;
|
||||
names.reserve(patternUpdaters.size());
|
||||
for (const auto& kv : patternUpdaters) {
|
||||
names.push_back(kv.first);
|
||||
}
|
||||
return names;
|
||||
return patternRegistry.getAllPatternNames();
|
||||
}
|
||||
|
||||
String NeoPatternService::currentPatternName() const {
|
||||
for (const auto& kv : nameToPatternMap) {
|
||||
if (kv.second == activePattern) {
|
||||
return kv.first;
|
||||
}
|
||||
}
|
||||
return "none";
|
||||
return patternRegistry.getPatternName(static_cast<uint8_t>(activePattern));
|
||||
}
|
||||
|
||||
NeoPatternService::NeoPatternType NeoPatternService::nameToPattern(const String& name) const {
|
||||
auto it = nameToPatternMap.find(name);
|
||||
return (it != nameToPatternMap.end()) ? it->second : NeoPatternType::NONE;
|
||||
uint8_t type = patternRegistry.getPatternType(name);
|
||||
return static_cast<NeoPatternType>(type);
|
||||
}
|
||||
|
||||
void NeoPatternService::resetStateForPattern(NeoPatternType pattern) {
|
||||
@@ -322,6 +387,29 @@ uint32_t NeoPatternService::parseColor(const String& colorStr) const {
|
||||
}
|
||||
}
|
||||
|
||||
bool NeoPatternService::isValidPattern(const String& name) const {
|
||||
return patternRegistry.isValidPattern(name);
|
||||
}
|
||||
|
||||
bool NeoPatternService::isValidPattern(NeoPatternType type) const {
|
||||
return patternRegistry.isValidPattern(static_cast<uint8_t>(type));
|
||||
}
|
||||
|
||||
bool NeoPatternService::patternRequiresColor2(const String& name) const {
|
||||
const PatternInfo* info = patternRegistry.getPattern(name);
|
||||
return info ? info->requiresColor2 : false;
|
||||
}
|
||||
|
||||
bool NeoPatternService::patternSupportsDirection(const String& name) const {
|
||||
const PatternInfo* info = patternRegistry.getPattern(name);
|
||||
return info ? info->supportsDirection : false;
|
||||
}
|
||||
|
||||
String NeoPatternService::getPatternDescription(const String& name) const {
|
||||
const PatternInfo* info = patternRegistry.getPattern(name);
|
||||
return info ? info->description : "";
|
||||
}
|
||||
|
||||
void NeoPatternService::update() {
|
||||
if (!initialized) return;
|
||||
|
||||
@@ -329,13 +417,8 @@ void NeoPatternService::update() {
|
||||
//if (now - lastUpdateMs < updateIntervalMs) return;
|
||||
//lastUpdateMs = now;
|
||||
|
||||
const String name = currentPatternName();
|
||||
auto it = patternUpdaters.find(name);
|
||||
if (it != patternUpdaters.end()) {
|
||||
it->second();
|
||||
} else {
|
||||
updateNone();
|
||||
}
|
||||
// Use pattern registry to execute the current pattern
|
||||
patternRegistry.executePattern(static_cast<uint8_t>(activePattern));
|
||||
}
|
||||
|
||||
void NeoPatternService::updateRainbowCycle() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "NeoPattern.h"
|
||||
#include "NeoPatternState.h"
|
||||
#include "NeoPixelConfig.h"
|
||||
#include "PatternRegistry.h"
|
||||
#include <map>
|
||||
#include <functional>
|
||||
|
||||
@@ -71,13 +72,20 @@ private:
|
||||
void resetStateForPattern(NeoPatternType pattern);
|
||||
uint32_t parseColor(const String& colorStr) const;
|
||||
|
||||
// Pattern validation methods
|
||||
bool isValidPattern(const String& name) const;
|
||||
bool isValidPattern(NeoPatternType type) const;
|
||||
bool patternRequiresColor2(const String& name) const;
|
||||
bool patternSupportsDirection(const String& name) const;
|
||||
String getPatternDescription(const String& name) const;
|
||||
|
||||
TaskManager& taskManager;
|
||||
NeoPattern* neoPattern;
|
||||
NeoPixelConfig config;
|
||||
NeoPatternState currentState;
|
||||
|
||||
std::map<String, std::function<void()>> patternUpdaters;
|
||||
std::map<String, NeoPatternType> nameToPatternMap;
|
||||
// Pattern registry for centralized pattern management
|
||||
PatternRegistry patternRegistry;
|
||||
|
||||
NeoPatternType activePattern;
|
||||
NeoDirection direction;
|
||||
|
||||
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();
|
||||
};
|
||||
@@ -18,13 +18,14 @@ This example demonstrates how to integrate a NeoPixel pattern service with the S
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `config.h` to configure your setup:
|
||||
Edit `NeoPixelConfig.h` to configure your setup:
|
||||
|
||||
```cpp
|
||||
#define NEOPIXEL_PIN 4 // GPIO pin connected to LED strip
|
||||
#define NEOPIXEL_LENGTH 8 // Number of LEDs in your strip
|
||||
#define NEOPIXEL_BRIGHTNESS 100 // Initial brightness (0-255)
|
||||
#define NEOPIXEL_UPDATE_INTERVAL 100 // Update interval in milliseconds
|
||||
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
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#ifndef __NPX_CONFIG__
|
||||
#define __NPX_CONFIG__
|
||||
|
||||
// NeoPixel Configuration
|
||||
#define NEOPIXEL_PIN 2
|
||||
#define NEOPIXEL_LENGTH 4
|
||||
#define NEOPIXEL_BRIGHTNESS 100
|
||||
#define NEOPIXEL_UPDATE_INTERVAL 100
|
||||
|
||||
// Spore Framework Configuration
|
||||
#define SPORE_APP_NAME "neopattern"
|
||||
#define SPORE_DEVICE_TYPE "led_strip"
|
||||
|
||||
// Network Configuration (if needed)
|
||||
#define WIFI_SSID "your_wifi_ssid"
|
||||
#define WIFI_PASSWORD "your_wifi_password"
|
||||
|
||||
// Debug Configuration
|
||||
#define DEBUG_SERIAL_BAUD 115200
|
||||
#define DEBUG_ENABLED true
|
||||
|
||||
#endif
|
||||
@@ -24,7 +24,7 @@
|
||||
// Create Spore instance with custom labels
|
||||
Spore spore({
|
||||
{"app", "neopattern"},
|
||||
{"device", "led_strip"},
|
||||
{"role", "led"},
|
||||
{"pixels", String(NEOPIXEL_LENGTH)},
|
||||
{"pin", String(NEOPIXEL_PIN)}
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "spore/types/NodeInfo.h"
|
||||
#include "spore/core/TaskManager.h"
|
||||
#include "spore/types/ApiTypes.h"
|
||||
#include "spore/util/Logging.h"
|
||||
|
||||
class Service; // Forward declaration
|
||||
|
||||
@@ -19,15 +20,17 @@ public:
|
||||
ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80);
|
||||
void begin();
|
||||
void addService(Service& service);
|
||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler);
|
||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler);
|
||||
|
||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
const std::vector<ParamSpec>& params);
|
||||
const String& serviceName = "unknown");
|
||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
||||
const std::vector<ParamSpec>& params);
|
||||
const String& serviceName = "unknown");
|
||||
|
||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
const std::vector<ParamSpec>& params, const String& serviceName = "unknown");
|
||||
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
||||
const std::vector<ParamSpec>& params, const String& serviceName = "unknown");
|
||||
|
||||
// Static file serving
|
||||
void serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header = "");
|
||||
|
||||
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
|
||||
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
|
||||
@@ -21,57 +21,31 @@ void ApiServer::registerEndpoint(const String& uri, int method,
|
||||
const String& serviceName) {
|
||||
// Add to local endpoints
|
||||
endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
|
||||
|
||||
// Update cluster if needed
|
||||
if (ctx.memberList && !ctx.memberList->empty()) {
|
||||
auto it = ctx.memberList->find(ctx.hostname);
|
||||
if (it != ctx.memberList->end()) {
|
||||
it->second.endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
|
||||
// Get current service name if available
|
||||
String serviceName = "unknown";
|
||||
if (!services.empty()) {
|
||||
serviceName = services.back().get().getName();
|
||||
}
|
||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
const String& serviceName) {
|
||||
registerEndpoint(uri, method, {}, serviceName);
|
||||
server.on(uri.c_str(), method, requestHandler);
|
||||
}
|
||||
|
||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler) {
|
||||
// Get current service name if available
|
||||
String serviceName = "unknown";
|
||||
if (!services.empty()) {
|
||||
serviceName = services.back().get().getName();
|
||||
}
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
||||
const String& serviceName) {
|
||||
registerEndpoint(uri, method, {}, serviceName);
|
||||
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
||||
}
|
||||
|
||||
// Overloads that also record minimal capability specs
|
||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
const std::vector<ParamSpec>& params) {
|
||||
// Get current service name if available
|
||||
String serviceName = "unknown";
|
||||
if (!services.empty()) {
|
||||
serviceName = services.back().get().getName();
|
||||
}
|
||||
const std::vector<ParamSpec>& params, const String& serviceName) {
|
||||
registerEndpoint(uri, method, params, serviceName);
|
||||
server.on(uri.c_str(), method, requestHandler);
|
||||
}
|
||||
|
||||
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
|
||||
const std::vector<ParamSpec>& params) {
|
||||
// Get current service name if available
|
||||
String serviceName = "unknown";
|
||||
if (!services.empty()) {
|
||||
serviceName = services.back().get().getName();
|
||||
}
|
||||
const std::vector<ParamSpec>& params, const String& serviceName) {
|
||||
registerEndpoint(uri, method, params, serviceName);
|
||||
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,6 @@ void NetworkManager::setupWiFi() {
|
||||
}
|
||||
ctx.self.lastSeen = millis();
|
||||
ctx.self.status = NodeInfo::ACTIVE;
|
||||
ctx.self.labels["hostname"] = ctx.hostname;
|
||||
|
||||
// Ensure member list has an entry for this node
|
||||
auto &memberList = *ctx.memberList;
|
||||
|
||||
@@ -1,42 +1,19 @@
|
||||
#include "spore/services/ClusterService.h"
|
||||
#include "spore/core/ApiServer.h"
|
||||
#include "spore/types/ClusterResponse.h"
|
||||
|
||||
using spore::types::ClusterMembersResponse;
|
||||
|
||||
ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {}
|
||||
|
||||
void ClusterService::registerEndpoints(ApiServer& api) {
|
||||
api.addEndpoint("/api/cluster/members", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleMembersRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "ClusterService");
|
||||
}
|
||||
|
||||
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
JsonArray arr = doc["members"].to<JsonArray>();
|
||||
|
||||
for (const auto& pair : *ctx.memberList) {
|
||||
const NodeInfo& node = pair.second;
|
||||
JsonObject obj = arr.add<JsonObject>();
|
||||
obj["hostname"] = node.hostname;
|
||||
obj["ip"] = node.ip.toString();
|
||||
obj["lastSeen"] = node.lastSeen;
|
||||
obj["latency"] = node.latency;
|
||||
obj["status"] = statusToStr(node.status);
|
||||
obj["resources"]["freeHeap"] = node.resources.freeHeap;
|
||||
obj["resources"]["chipId"] = node.resources.chipId;
|
||||
obj["resources"]["sdkVersion"] = node.resources.sdkVersion;
|
||||
obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz;
|
||||
obj["resources"]["flashChipSize"] = node.resources.flashChipSize;
|
||||
|
||||
// Add labels if present
|
||||
if (!node.labels.empty()) {
|
||||
JsonObject labelsObj = obj["labels"].to<JsonObject>();
|
||||
for (const auto& kv : node.labels) {
|
||||
labelsObj[kv.first.c_str()] = kv.second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
ClusterMembersResponse response;
|
||||
response.addNodes(ctx.memberList);
|
||||
request->send(200, "application/json", response.toJsonString());
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ MonitoringService::MonitoringService(CpuUsage& cpuUsage)
|
||||
void MonitoringService::registerEndpoints(ApiServer& api) {
|
||||
api.addEndpoint("/api/monitoring/resources", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleResourcesRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "MonitoringService");
|
||||
}
|
||||
|
||||
MonitoringService::SystemResources MonitoringService::getSystemResources() const {
|
||||
|
||||
@@ -8,16 +8,16 @@ void NetworkService::registerEndpoints(ApiServer& api) {
|
||||
// WiFi scanning endpoints
|
||||
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "NetworkService");
|
||||
|
||||
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "NetworkService");
|
||||
|
||||
// Network status and configuration endpoints
|
||||
api.addEndpoint("/api/network/status", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "NetworkService");
|
||||
|
||||
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
|
||||
@@ -26,7 +26,7 @@ void NetworkService::registerEndpoints(ApiServer& api) {
|
||||
ParamSpec{String("password"), true, String("body"), String("string"), {}, String("")},
|
||||
ParamSpec{String("connect_timeout_ms"), false, String("body"), String("number"), {}, String("10000")},
|
||||
ParamSpec{String("retry_delay_ms"), false, String("body"), String("number"), {}, String("500")}
|
||||
});
|
||||
}, "NetworkService");
|
||||
}
|
||||
|
||||
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#include "spore/services/NodeService.h"
|
||||
#include "spore/core/ApiServer.h"
|
||||
#include "spore/util/Logging.h"
|
||||
#include "spore/types/NodeResponse.h"
|
||||
|
||||
using spore::types::NodeStatusResponse;
|
||||
using spore::types::NodeOperationResponse;
|
||||
using spore::types::NodeEndpointsResponse;
|
||||
|
||||
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
|
||||
|
||||
@@ -8,7 +13,7 @@ void NodeService::registerEndpoints(ApiServer& api) {
|
||||
// Status endpoint
|
||||
api.addEndpoint("/api/node/status", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "NodeService");
|
||||
|
||||
// Update endpoint with file upload
|
||||
api.addEndpoint("/api/node/update", HTTP_POST,
|
||||
@@ -18,54 +23,45 @@ void NodeService::registerEndpoints(ApiServer& api) {
|
||||
},
|
||||
std::vector<ParamSpec>{
|
||||
ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")}
|
||||
});
|
||||
}, "NodeService");
|
||||
|
||||
// Restart endpoint
|
||||
api.addEndpoint("/api/node/restart", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "NodeService");
|
||||
|
||||
// Endpoints endpoint
|
||||
api.addEndpoint("/api/node/endpoints", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "NodeService");
|
||||
}
|
||||
|
||||
void NodeService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
doc["freeHeap"] = ESP.getFreeHeap();
|
||||
doc["chipId"] = ESP.getChipId();
|
||||
doc["sdkVersion"] = ESP.getSdkVersion();
|
||||
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
|
||||
doc["flashChipSize"] = ESP.getFlashChipSize();
|
||||
NodeStatusResponse response;
|
||||
|
||||
// Include local node labels if present
|
||||
// Get labels from member list or self
|
||||
std::map<String, String> labels;
|
||||
if (ctx.memberList) {
|
||||
auto it = ctx.memberList->find(ctx.hostname);
|
||||
if (it != ctx.memberList->end()) {
|
||||
JsonObject labelsObj = doc["labels"].to<JsonObject>();
|
||||
for (const auto& kv : it->second.labels) {
|
||||
labelsObj[kv.first.c_str()] = kv.second;
|
||||
}
|
||||
labels = it->second.labels;
|
||||
} else if (!ctx.self.labels.empty()) {
|
||||
JsonObject labelsObj = doc["labels"].to<JsonObject>();
|
||||
for (const auto& kv : ctx.self.labels) {
|
||||
labelsObj[kv.first.c_str()] = kv.second;
|
||||
}
|
||||
labels = ctx.self.labels;
|
||||
}
|
||||
}
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
response.buildCompleteResponse(labels);
|
||||
request->send(200, "application/json", response.toJsonString());
|
||||
}
|
||||
|
||||
void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) {
|
||||
bool success = !Update.hasError();
|
||||
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
|
||||
success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
|
||||
response->addHeader("Connection", "close");
|
||||
request->send(response);
|
||||
NodeOperationResponse response;
|
||||
response.setSuccess(success ? "OK" : "FAIL");
|
||||
|
||||
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
|
||||
httpResponse->addHeader("Connection", "close");
|
||||
request->send(httpResponse);
|
||||
request->onDisconnect([this]() {
|
||||
LOG_INFO("API", "Restart device");
|
||||
delay(10);
|
||||
@@ -108,10 +104,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
|
||||
}
|
||||
|
||||
void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
|
||||
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
|
||||
"{\"status\": \"restarting\"}");
|
||||
response->addHeader("Connection", "close");
|
||||
request->send(response);
|
||||
NodeOperationResponse response;
|
||||
response.setSuccess("restarting");
|
||||
|
||||
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
|
||||
httpResponse->addHeader("Connection", "close");
|
||||
request->send(httpResponse);
|
||||
request->onDisconnect([this]() {
|
||||
LOG_INFO("API", "Restart device");
|
||||
delay(10);
|
||||
@@ -120,36 +118,7 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
|
||||
}
|
||||
|
||||
void NodeService::handleEndpointsRequest(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
JsonArray endpointsArr = doc["endpoints"].to<JsonArray>();
|
||||
|
||||
// Add all registered endpoints from ApiServer
|
||||
for (const auto& endpoint : apiServer.getEndpoints()) {
|
||||
JsonObject obj = endpointsArr.add<JsonObject>();
|
||||
obj["uri"] = endpoint.uri;
|
||||
obj["method"] = ApiServer::methodToStr(endpoint.method);
|
||||
if (!endpoint.params.empty()) {
|
||||
JsonArray paramsArr = obj["params"].to<JsonArray>();
|
||||
for (const auto& ps : endpoint.params) {
|
||||
JsonObject p = paramsArr.add<JsonObject>();
|
||||
p["name"] = ps.name;
|
||||
p["location"] = ps.location;
|
||||
p["required"] = ps.required;
|
||||
p["type"] = ps.type;
|
||||
if (!ps.values.empty()) {
|
||||
JsonArray allowed = p["values"].to<JsonArray>();
|
||||
for (const auto& v : ps.values) {
|
||||
allowed.add(v);
|
||||
}
|
||||
}
|
||||
if (ps.defaultValue.length() > 0) {
|
||||
p["default"] = ps.defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
NodeEndpointsResponse response;
|
||||
response.addEndpoints(apiServer.getEndpoints());
|
||||
request->send(200, "application/json", response.toJsonString());
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
#include "spore/services/TaskService.h"
|
||||
#include "spore/core/ApiServer.h"
|
||||
#include "spore/types/TaskResponse.h"
|
||||
#include <algorithm>
|
||||
|
||||
using spore::types::TaskStatusResponse;
|
||||
using spore::types::TaskControlResponse;
|
||||
|
||||
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
|
||||
|
||||
void TaskService::registerEndpoints(ApiServer& api) {
|
||||
api.addEndpoint("/api/tasks/status", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
std::vector<ParamSpec>{}, "TaskService");
|
||||
|
||||
api.addEndpoint("/api/tasks/control", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
||||
@@ -28,36 +32,19 @@ void TaskService::registerEndpoints(ApiServer& api) {
|
||||
{String("enable"), String("disable"), String("start"), String("stop"), String("status")},
|
||||
String("")
|
||||
}
|
||||
});
|
||||
}, "TaskService");
|
||||
}
|
||||
|
||||
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||
TaskStatusResponse response;
|
||||
|
||||
// Get task statuses using a separate document to avoid reference issues
|
||||
JsonDocument scratch;
|
||||
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
|
||||
|
||||
JsonDocument doc;
|
||||
JsonObject summaryObj = doc["summary"].to<JsonObject>();
|
||||
summaryObj["totalTasks"] = taskStatuses.size();
|
||||
summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(),
|
||||
[](const auto& pair) { return pair.second["enabled"]; });
|
||||
|
||||
JsonArray tasksArr = doc["tasks"].to<JsonArray>();
|
||||
for (const auto& taskPair : taskStatuses) {
|
||||
JsonObject taskObj = tasksArr.add<JsonObject>();
|
||||
taskObj["name"] = taskPair.first;
|
||||
taskObj["interval"] = taskPair.second["interval"];
|
||||
taskObj["enabled"] = taskPair.second["enabled"];
|
||||
taskObj["running"] = taskPair.second["running"];
|
||||
taskObj["autoStart"] = taskPair.second["autoStart"];
|
||||
}
|
||||
|
||||
JsonObject systemObj = doc["system"].to<JsonObject>();
|
||||
systemObj["freeHeap"] = ESP.getFreeHeap();
|
||||
systemObj["uptime"] = millis();
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
// Build the complete response with the task data
|
||||
response.buildCompleteResponse(taskStatuses);
|
||||
request->send(200, "application/json", response.toJsonString());
|
||||
}
|
||||
|
||||
void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
|
||||
@@ -88,50 +75,27 @@ void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
|
||||
success = true;
|
||||
message = "Task status retrieved";
|
||||
|
||||
JsonDocument statusDoc;
|
||||
statusDoc["success"] = success;
|
||||
statusDoc["message"] = message;
|
||||
statusDoc["task"] = taskName;
|
||||
statusDoc["action"] = action;
|
||||
TaskControlResponse response;
|
||||
response.setResponse(success, message, taskName, action);
|
||||
response.addTaskDetails(taskName,
|
||||
taskManager.isTaskEnabled(taskName.c_str()),
|
||||
taskManager.isTaskRunning(taskName.c_str()),
|
||||
taskManager.getTaskInterval(taskName.c_str()));
|
||||
|
||||
statusDoc["taskDetails"] = JsonObject();
|
||||
JsonObject taskDetails = statusDoc["taskDetails"];
|
||||
taskDetails["name"] = taskName;
|
||||
taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str());
|
||||
taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str());
|
||||
taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str());
|
||||
|
||||
taskDetails["system"] = JsonObject();
|
||||
JsonObject systemInfo = taskDetails["system"];
|
||||
systemInfo["freeHeap"] = ESP.getFreeHeap();
|
||||
systemInfo["uptime"] = millis();
|
||||
|
||||
String statusJson;
|
||||
serializeJson(statusDoc, statusJson);
|
||||
request->send(200, "application/json", statusJson);
|
||||
request->send(200, "application/json", response.toJsonString());
|
||||
return;
|
||||
} else {
|
||||
success = false;
|
||||
message = "Invalid action. Use: enable, disable, start, stop, or status";
|
||||
}
|
||||
|
||||
JsonDocument doc;
|
||||
doc["success"] = success;
|
||||
doc["message"] = message;
|
||||
doc["task"] = taskName;
|
||||
doc["action"] = action;
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(success ? 200 : 400, "application/json", json);
|
||||
TaskControlResponse response;
|
||||
response.setResponse(success, message, taskName, action);
|
||||
request->send(success ? 200 : 400, "application/json", response.toJsonString());
|
||||
} else {
|
||||
JsonDocument doc;
|
||||
doc["success"] = false;
|
||||
doc["message"] = "Missing parameters. Required: task, action";
|
||||
doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}";
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(400, "application/json", json);
|
||||
TaskControlResponse response;
|
||||
response.setError("Missing parameters. Required: task, action",
|
||||
"{\"task\": \"discovery_send\", \"action\": \"status\"}");
|
||||
request->send(400, "application/json", response.toJsonString());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user