Compare commits
2 Commits
83d87159fc
...
4727405be1
| Author | SHA1 | Date | |
|---|---|---|---|
| 4727405be1 | |||
| 93f09c3bb4 |
@@ -2,10 +2,6 @@
|
||||
* 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
|
||||
@@ -28,7 +24,8 @@ enum pattern
|
||||
FADE = 5,
|
||||
FIRE = 6
|
||||
};
|
||||
// Patern directions supported:
|
||||
|
||||
// Pattern directions supported:
|
||||
enum direction
|
||||
{
|
||||
FORWARD,
|
||||
@@ -52,8 +49,8 @@ class NeoPattern : public Adafruit_NeoPixel
|
||||
uint16_t Index; // current step within the pattern
|
||||
uint16_t completed = 0;
|
||||
|
||||
// FIXME return current NeoPatternState
|
||||
void (*OnComplete)(int); // Callback on completion of pattern
|
||||
// Callback on completion of pattern
|
||||
void (*OnComplete)(int);
|
||||
|
||||
uint8_t *frameBuffer;
|
||||
int bufferSize = 0;
|
||||
@@ -76,6 +73,14 @@ class NeoPattern : public Adafruit_NeoPixel
|
||||
begin();
|
||||
}
|
||||
|
||||
~NeoPattern() {
|
||||
if (frameBuffer) {
|
||||
free(frameBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Implementation starts here (inline)
|
||||
void handleStream(uint8_t *data, size_t len)
|
||||
{
|
||||
//const uint16_t *data16 = (uint16_t *)data;
|
||||
@@ -129,6 +134,9 @@ class NeoPattern : public Adafruit_NeoPixel
|
||||
case FIRE:
|
||||
Fire(50, 120);
|
||||
break;
|
||||
case NONE:
|
||||
// For NONE pattern, just maintain current state
|
||||
break;
|
||||
default:
|
||||
if (bufferSize > 0)
|
||||
{
|
||||
@@ -461,10 +469,11 @@ class NeoPattern : public Adafruit_NeoPixel
|
||||
{
|
||||
setPixelColor(Pixel, Color(red, green, blue));
|
||||
}
|
||||
|
||||
void showStrip()
|
||||
{
|
||||
show();
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,56 +1,90 @@
|
||||
#include "NeoPatternService.h"
|
||||
#include "spore/core/ApiServer.h"
|
||||
#include "spore/util/Logging.h"
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
NeoPatternService::NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type)
|
||||
NeoPatternService::NeoPatternService(TaskManager& taskMgr, const NeoPixelConfig& config)
|
||||
: taskManager(taskMgr),
|
||||
pixels(numPixels, pin, type),
|
||||
updateIntervalMs(100),
|
||||
brightness(48) {
|
||||
pixels.setBrightness(brightness);
|
||||
pixels.show();
|
||||
|
||||
config(config),
|
||||
activePattern(NeoPatternType::RAINBOW_CYCLE),
|
||||
direction(NeoDirection::FORWARD),
|
||||
updateIntervalMs(config.updateInterval),
|
||||
lastUpdateMs(0),
|
||||
initialized(false) {
|
||||
|
||||
// Initialize NeoPattern
|
||||
neoPattern = new NeoPattern(config.length, config.pin, NEO_GRB + NEO_KHZ800);
|
||||
neoPattern->setBrightness(config.brightness);
|
||||
|
||||
// Initialize state
|
||||
currentState.pattern = static_cast<uint>(activePattern);
|
||||
currentState.color = 0xFF0000; // Red
|
||||
currentState.color2 = 0x0000FF; // Blue
|
||||
currentState.totalSteps = 16;
|
||||
currentState.brightness = config.brightness;
|
||||
|
||||
// Set initial pattern
|
||||
neoPattern->Color1 = currentState.color;
|
||||
neoPattern->Color2 = currentState.color2;
|
||||
neoPattern->TotalSteps = currentState.totalSteps;
|
||||
neoPattern->ActivePattern = static_cast<::pattern>(activePattern);
|
||||
neoPattern->Direction = static_cast<::direction>(direction);
|
||||
|
||||
registerPatterns();
|
||||
registerTasks();
|
||||
setPatternByName("rainbow_cycle");
|
||||
initialized = true;
|
||||
|
||||
LOG_INFO("NeoPattern", "Service initialized");
|
||||
}
|
||||
|
||||
NeoPatternService::~NeoPatternService() {
|
||||
if (neoPattern) {
|
||||
delete neoPattern;
|
||||
}
|
||||
}
|
||||
|
||||
void NeoPatternService::registerEndpoints(ApiServer& api) {
|
||||
// Status endpoint
|
||||
api.addEndpoint("/api/neopattern/status", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
|
||||
// Patterns list endpoint
|
||||
api.addEndpoint("/api/neopattern/patterns", HTTP_GET,
|
||||
[this](AsyncWebServerRequest* request) { handlePatternsRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
|
||||
// Control endpoint
|
||||
api.addEndpoint("/api/neopattern", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
|
||||
std::vector<ParamSpec>{
|
||||
ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()},
|
||||
ParamSpec{String("interval_ms"), false, String("body"), String("number"), {}, String("100")},
|
||||
ParamSpec{String("brightness"), false, String("body"), String("number"), {}, String("50")},
|
||||
ParamSpec{String("color"), false, String("body"), String("color"), {}},
|
||||
ParamSpec{String("color2"), false, String("body"), String("color"), {}},
|
||||
ParamSpec{String("r"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("g"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("b"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("r2"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("g2"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("b2"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("total_steps"), false, String("body"), String("number"), {}},
|
||||
ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}}
|
||||
ParamSpec{String("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("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}},
|
||||
ParamSpec{String("interval"), false, String("body"), String("number"), {}, String("100")}
|
||||
});
|
||||
|
||||
// State endpoint for complex state updates
|
||||
api.addEndpoint("/api/neopattern/state", HTTP_POST,
|
||||
[this](AsyncWebServerRequest* request) { handleStateRequest(request); },
|
||||
std::vector<ParamSpec>{});
|
||||
}
|
||||
|
||||
void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
doc["pin"] = pixels.getPin();
|
||||
doc["count"] = pixels.numPixels();
|
||||
doc["interval_ms"] = updateIntervalMs;
|
||||
doc["brightness"] = brightness;
|
||||
doc["pin"] = config.pin;
|
||||
doc["length"] = config.length;
|
||||
doc["brightness"] = currentState.brightness;
|
||||
doc["pattern"] = currentPatternName();
|
||||
doc["total_steps"] = pixels.TotalSteps;
|
||||
doc["color1"] = pixels.Color1;
|
||||
doc["color2"] = pixels.Color2;
|
||||
doc["color"] = String(currentState.color, HEX);
|
||||
doc["color2"] = String(currentState.color2, HEX);
|
||||
doc["total_steps"] = currentState.totalSteps;
|
||||
doc["direction"] = (direction == NeoDirection::FORWARD) ? "forward" : "reverse";
|
||||
doc["interval"] = updateIntervalMs;
|
||||
doc["active"] = initialized;
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
@@ -60,7 +94,9 @@ void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) {
|
||||
void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
|
||||
JsonDocument doc;
|
||||
JsonArray arr = doc.to<JsonArray>();
|
||||
for (auto& kv : patternSetters) arr.add(kv.first);
|
||||
for (const auto& kv : patternUpdaters) {
|
||||
arr.add(kv.first);
|
||||
}
|
||||
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
@@ -68,121 +104,265 @@ void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) {
|
||||
}
|
||||
|
||||
void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) {
|
||||
bool updated = false;
|
||||
|
||||
if (request->hasParam("pattern", true)) {
|
||||
String name = request->getParam("pattern", true)->value();
|
||||
setPatternByName(name);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (request->hasParam("interval_ms", true)) {
|
||||
unsigned long v = request->getParam("interval_ms", true)->value().toInt();
|
||||
if (v < 1) v = 1;
|
||||
updateIntervalMs = v;
|
||||
taskManager.setTaskInterval("neopattern_update", updateIntervalMs);
|
||||
if (request->hasParam("color", true)) {
|
||||
String colorStr = request->getParam("color", true)->value();
|
||||
uint32_t color = parseColor(colorStr);
|
||||
setColor(color);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (request->hasParam("color2", true)) {
|
||||
String colorStr = request->getParam("color2", true)->value();
|
||||
uint32_t color = parseColor(colorStr);
|
||||
setColor2(color);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (request->hasParam("brightness", true)) {
|
||||
int b = request->getParam("brightness", true)->value().toInt();
|
||||
if (b < 0) b = 0;
|
||||
if (b > 255) b = 255;
|
||||
setBrightness((uint8_t)b);
|
||||
}
|
||||
|
||||
// Accept packed color ints or r,g,b triplets
|
||||
if (request->hasParam("color", true)) {
|
||||
pixels.Color1 = (uint32_t)strtoul(request->getParam("color", true)->value().c_str(), nullptr, 0);
|
||||
}
|
||||
if (request->hasParam("color2", true)) {
|
||||
pixels.Color2 = (uint32_t)strtoul(request->getParam("color2", true)->value().c_str(), nullptr, 0);
|
||||
}
|
||||
if (request->hasParam("r", true) || request->hasParam("g", true) || request->hasParam("b", true)) {
|
||||
int r = request->hasParam("r", true) ? request->getParam("r", true)->value().toInt() : 0;
|
||||
int g = request->hasParam("g", true) ? request->getParam("g", true)->value().toInt() : 0;
|
||||
int b = request->hasParam("b", true) ? request->getParam("b", true)->value().toInt() : 0;
|
||||
pixels.Color1 = pixels.Color(r, g, b);
|
||||
}
|
||||
if (request->hasParam("r2", true) || request->hasParam("g2", true) || request->hasParam("b2", true)) {
|
||||
int r = request->hasParam("r2", true) ? request->getParam("r2", true)->value().toInt() : 0;
|
||||
int g = request->hasParam("g2", true) ? request->getParam("g2", true)->value().toInt() : 0;
|
||||
int b = request->hasParam("b2", true) ? request->getParam("b2", true)->value().toInt() : 0;
|
||||
pixels.Color2 = pixels.Color(r, g, b);
|
||||
setBrightness(static_cast<uint8_t>(b));
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (request->hasParam("total_steps", true)) {
|
||||
pixels.TotalSteps = request->getParam("total_steps", true)->value().toInt();
|
||||
int steps = request->getParam("total_steps", true)->value().toInt();
|
||||
if (steps > 0) {
|
||||
setTotalSteps(static_cast<uint16_t>(steps));
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (request->hasParam("direction", true)) {
|
||||
String dir = request->getParam("direction", true)->value();
|
||||
if (dir.equalsIgnoreCase("forward")) pixels.Direction = FORWARD;
|
||||
else if (dir.equalsIgnoreCase("reverse")) pixels.Direction = REVERSE;
|
||||
String dirStr = request->getParam("direction", true)->value();
|
||||
NeoDirection dir = (dirStr.equalsIgnoreCase("reverse")) ? NeoDirection::REVERSE : NeoDirection::FORWARD;
|
||||
setDirection(dir);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (request->hasParam("interval", true)) {
|
||||
unsigned long interval = request->getParam("interval", true)->value().toInt();
|
||||
if (interval > 0) {
|
||||
setUpdateInterval(interval);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Return current state
|
||||
JsonDocument resp;
|
||||
resp["ok"] = true;
|
||||
resp["pattern"] = currentPatternName();
|
||||
resp["interval_ms"] = updateIntervalMs;
|
||||
resp["brightness"] = brightness;
|
||||
resp["color"] = String(currentState.color, HEX);
|
||||
resp["color2"] = String(currentState.color2, HEX);
|
||||
resp["brightness"] = currentState.brightness;
|
||||
resp["total_steps"] = currentState.totalSteps;
|
||||
resp["direction"] = (direction == NeoDirection::FORWARD) ? "forward" : "reverse";
|
||||
resp["interval"] = updateIntervalMs;
|
||||
resp["updated"] = updated;
|
||||
|
||||
String json;
|
||||
serializeJson(resp, json);
|
||||
request->send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void NeoPatternService::setBrightness(uint8_t b) {
|
||||
brightness = b;
|
||||
pixels.setBrightness(brightness);
|
||||
pixels.show();
|
||||
void NeoPatternService::handleStateRequest(AsyncWebServerRequest* request) {
|
||||
if (request->contentType() != "application/json") {
|
||||
request->send(400, "application/json", "{\"error\":\"Content-Type must be application/json\"}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: AsyncWebServerRequest doesn't have getBody() method
|
||||
// For now, we'll skip JSON body parsing and use form parameters
|
||||
request->send(400, "application/json", "{\"error\":\"JSON body parsing not implemented\"}");
|
||||
return;
|
||||
|
||||
// JSON body parsing not implemented for AsyncWebServerRequest
|
||||
// This would need to be implemented differently
|
||||
request->send(501, "application/json", "{\"error\":\"Not implemented\"}");
|
||||
}
|
||||
|
||||
void NeoPatternService::setPattern(NeoPatternType pattern) {
|
||||
activePattern = pattern;
|
||||
currentState.pattern = static_cast<uint>(pattern);
|
||||
neoPattern->ActivePattern = static_cast<::pattern>(pattern);
|
||||
resetStateForPattern(pattern);
|
||||
}
|
||||
|
||||
void NeoPatternService::setPatternByName(const String& name) {
|
||||
NeoPatternType pattern = nameToPattern(name);
|
||||
setPattern(pattern);
|
||||
}
|
||||
|
||||
void NeoPatternService::setColor(uint32_t color) {
|
||||
currentState.color = color;
|
||||
neoPattern->Color1 = color;
|
||||
if (activePattern == NeoPatternType::NONE) {
|
||||
neoPattern->ColorSet(color);
|
||||
}
|
||||
}
|
||||
|
||||
void NeoPatternService::setColor2(uint32_t color) {
|
||||
currentState.color2 = color;
|
||||
neoPattern->Color2 = color;
|
||||
}
|
||||
|
||||
void NeoPatternService::setBrightness(uint8_t brightness) {
|
||||
currentState.brightness = brightness;
|
||||
neoPattern->setBrightness(brightness);
|
||||
neoPattern->show();
|
||||
}
|
||||
|
||||
void NeoPatternService::setTotalSteps(uint16_t steps) {
|
||||
currentState.totalSteps = steps;
|
||||
neoPattern->TotalSteps = steps;
|
||||
}
|
||||
|
||||
void NeoPatternService::setDirection(NeoDirection dir) {
|
||||
direction = dir;
|
||||
neoPattern->Direction = static_cast<::direction>(dir);
|
||||
}
|
||||
|
||||
void NeoPatternService::setUpdateInterval(unsigned long interval) {
|
||||
updateIntervalMs = interval;
|
||||
taskManager.setTaskInterval("neopattern_update", interval);
|
||||
}
|
||||
|
||||
void NeoPatternService::setState(const NeoPatternState& state) {
|
||||
currentState = state;
|
||||
|
||||
// Apply state to NeoPattern
|
||||
neoPattern->Index = 0;
|
||||
neoPattern->Color1 = state.color;
|
||||
neoPattern->Color2 = state.color2;
|
||||
neoPattern->TotalSteps = state.totalSteps;
|
||||
neoPattern->ActivePattern = static_cast<::pattern>(state.pattern);
|
||||
neoPattern->Direction = FORWARD;
|
||||
|
||||
setBrightness(state.brightness);
|
||||
setPattern(static_cast<NeoPatternType>(state.pattern));
|
||||
}
|
||||
|
||||
NeoPatternState NeoPatternService::getState() const {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
void NeoPatternService::registerTasks() {
|
||||
taskManager.registerTask("neopattern_update", updateIntervalMs, [this]() { update(); });
|
||||
taskManager.registerTask("neopattern_status_print", 10000, [this]() {
|
||||
Serial.printf("[NeoPattern] pattern=%s interval=%lu ms brightness=%u\n",
|
||||
currentPatternName().c_str(), updateIntervalMs, brightness);
|
||||
LOG_INFO("NeoPattern", "Status update");
|
||||
});
|
||||
}
|
||||
|
||||
void NeoPatternService::registerPatterns() {
|
||||
patternSetters["off"] = [this]() { pixels.ActivePattern = NONE; };
|
||||
patternSetters["rainbow_cycle"] = [this]() { pixels.RainbowCycle(updateIntervalMs); };
|
||||
patternSetters["theater_chase"] = [this]() { pixels.TheaterChase(pixels.Color1 ? pixels.Color1 : pixels.Color(127,127,127), pixels.Color2, updateIntervalMs); };
|
||||
patternSetters["color_wipe"] = [this]() { pixels.ColorWipe(pixels.Color1 ? pixels.Color1 : pixels.Color(255,0,0), updateIntervalMs); };
|
||||
patternSetters["scanner"] = [this]() { pixels.Scanner(pixels.Color1 ? pixels.Color1 : pixels.Color(0,0,255), updateIntervalMs); };
|
||||
patternSetters["fade"] = [this]() { pixels.Fade(pixels.Color1, pixels.Color2, pixels.TotalSteps ? pixels.TotalSteps : 32, updateIntervalMs); };
|
||||
patternSetters["fire"] = [this]() { pixels.ActivePattern = FIRE; pixels.Interval = updateIntervalMs; };
|
||||
// Register 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 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;
|
||||
}
|
||||
|
||||
std::vector<String> NeoPatternService::patternNamesVector() {
|
||||
if (patternSetters.empty()) registerPatterns();
|
||||
std::vector<String> v;
|
||||
v.reserve(patternSetters.size());
|
||||
for (const auto& kv : patternSetters) v.push_back(kv.first);
|
||||
return v;
|
||||
}
|
||||
|
||||
String NeoPatternService::currentPatternName() {
|
||||
switch (pixels.ActivePattern) {
|
||||
case NONE: return String("off");
|
||||
case RAINBOW_CYCLE: return String("rainbow_cycle");
|
||||
case THEATER_CHASE: return String("theater_chase");
|
||||
case COLOR_WIPE: return String("color_wipe");
|
||||
case SCANNER: return String("scanner");
|
||||
case FADE: return String("fade");
|
||||
case FIRE: return String("fire");
|
||||
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 String("off");
|
||||
return names;
|
||||
}
|
||||
|
||||
void NeoPatternService::setPatternByName(const String& name) {
|
||||
if (patternSetters.empty()) registerPatterns();
|
||||
auto it = patternSetters.find(name);
|
||||
if (it != patternSetters.end()) {
|
||||
pixels.Index = 0;
|
||||
pixels.Direction = FORWARD;
|
||||
it->second();
|
||||
String NeoPatternService::currentPatternName() const {
|
||||
for (const auto& kv : nameToPatternMap) {
|
||||
if (kv.second == activePattern) {
|
||||
return kv.first;
|
||||
}
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
NeoPatternService::NeoPatternType NeoPatternService::nameToPattern(const String& name) const {
|
||||
auto it = nameToPatternMap.find(name);
|
||||
return (it != nameToPatternMap.end()) ? it->second : NeoPatternType::NONE;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
void NeoPatternService::update() {
|
||||
pixels.Update();
|
||||
if (!initialized) return;
|
||||
|
||||
//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 {
|
||||
updateNone();
|
||||
}
|
||||
}
|
||||
|
||||
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,87 @@
|
||||
#pragma once
|
||||
#include "spore/Service.h"
|
||||
#include "spore/core/TaskManager.h"
|
||||
#include "NeoPattern.cpp"
|
||||
#include "NeoPattern.h"
|
||||
#include "NeoPatternState.h"
|
||||
#include "NeoPixelConfig.h"
|
||||
#include <map>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
class NeoPatternService : public Service {
|
||||
public:
|
||||
NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type);
|
||||
enum class NeoPatternType {
|
||||
NONE = 0,
|
||||
RAINBOW_CYCLE = 1,
|
||||
THEATER_CHASE = 2,
|
||||
COLOR_WIPE = 3,
|
||||
SCANNER = 4,
|
||||
FADE = 5,
|
||||
FIRE = 6
|
||||
};
|
||||
|
||||
enum class NeoDirection {
|
||||
FORWARD,
|
||||
REVERSE
|
||||
};
|
||||
|
||||
NeoPatternService(TaskManager& taskMgr, const NeoPixelConfig& config);
|
||||
~NeoPatternService();
|
||||
|
||||
void registerEndpoints(ApiServer& api) override;
|
||||
const char* getName() const override { return "NeoPattern"; }
|
||||
|
||||
void setBrightness(uint8_t b);
|
||||
// Pattern control methods
|
||||
void setPattern(NeoPatternType pattern);
|
||||
void setPatternByName(const String& name);
|
||||
void setColor(uint32_t color);
|
||||
void setColor2(uint32_t color2);
|
||||
void setBrightness(uint8_t brightness);
|
||||
void setTotalSteps(uint16_t steps);
|
||||
void setDirection(NeoDirection direction);
|
||||
void setUpdateInterval(unsigned long interval);
|
||||
|
||||
// State management
|
||||
void setState(const NeoPatternState& state);
|
||||
NeoPatternState getState() const;
|
||||
|
||||
private:
|
||||
void registerTasks();
|
||||
void registerPatterns();
|
||||
std::vector<String> patternNamesVector();
|
||||
String currentPatternName();
|
||||
void update();
|
||||
|
||||
// Pattern updaters
|
||||
void updateRainbowCycle();
|
||||
void updateTheaterChase();
|
||||
void updateColorWipe();
|
||||
void updateScanner();
|
||||
void updateFade();
|
||||
void updateFire();
|
||||
void updateNone();
|
||||
|
||||
// Handlers
|
||||
// API handlers
|
||||
void handleStatusRequest(AsyncWebServerRequest* request);
|
||||
void handlePatternsRequest(AsyncWebServerRequest* request);
|
||||
void handleControlRequest(AsyncWebServerRequest* request);
|
||||
void handleStateRequest(AsyncWebServerRequest* request);
|
||||
|
||||
// Utility methods
|
||||
std::vector<String> patternNamesVector() const;
|
||||
String currentPatternName() const;
|
||||
NeoPatternType nameToPattern(const String& name) const;
|
||||
void resetStateForPattern(NeoPatternType pattern);
|
||||
uint32_t parseColor(const String& colorStr) const;
|
||||
|
||||
TaskManager& taskManager;
|
||||
NeoPattern pixels;
|
||||
NeoPattern* neoPattern;
|
||||
NeoPixelConfig config;
|
||||
NeoPatternState currentState;
|
||||
|
||||
std::map<String, std::function<void()>> patternUpdaters;
|
||||
std::map<String, NeoPatternType> nameToPatternMap;
|
||||
|
||||
NeoPatternType activePattern;
|
||||
NeoDirection direction;
|
||||
unsigned long updateIntervalMs;
|
||||
uint8_t brightness;
|
||||
std::map<String, std::function<void()>> patternSetters;
|
||||
unsigned long lastUpdateMs;
|
||||
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
|
||||
130
examples/neopattern/README.md
Normal file
130
examples/neopattern/README.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 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 `config.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
|
||||
```
|
||||
|
||||
## 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 +1,22 @@
|
||||
#ifndef __DEVICE_CONFIG__
|
||||
#define __DEVICE_CONFIG__
|
||||
#ifndef __NPX_CONFIG__
|
||||
#define __NPX_CONFIG__
|
||||
|
||||
// Scheduler config
|
||||
#define _TASK_SLEEP_ON_IDLE_RUN
|
||||
#define _TASK_STD_FUNCTION
|
||||
#define _TASK_PRIORITY
|
||||
// NeoPixel Configuration
|
||||
#define NEOPIXEL_PIN 2
|
||||
#define NEOPIXEL_LENGTH 4
|
||||
#define NEOPIXEL_BRIGHTNESS 100
|
||||
#define NEOPIXEL_UPDATE_INTERVAL 100
|
||||
|
||||
// Chip config
|
||||
#define SPROCKET_TYPE "SPROCKET"
|
||||
#define SERIAL_BAUD_RATE 115200
|
||||
#define STARTUP_DELAY 1000
|
||||
// Spore Framework Configuration
|
||||
#define SPORE_APP_NAME "neopattern"
|
||||
#define SPORE_DEVICE_TYPE "led_strip"
|
||||
|
||||
// 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
|
||||
// Network Configuration (if needed)
|
||||
#define WIFI_SSID "your_wifi_ssid"
|
||||
#define WIFI_PASSWORD "your_wifi_password"
|
||||
|
||||
// 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
|
||||
// Debug Configuration
|
||||
#define DEBUG_SERIAL_BAUD 115200
|
||||
#define DEBUG_ENABLED true
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2,25 +2,31 @@
|
||||
#include "spore/Spore.h"
|
||||
#include "spore/util/Logging.h"
|
||||
#include "NeoPatternService.h"
|
||||
#include "NeoPixelConfig.h"
|
||||
|
||||
#ifndef LED_STRIP_PIN
|
||||
#define LED_STRIP_PIN 2
|
||||
// Configuration constants
|
||||
#ifndef NEOPIXEL_PIN
|
||||
#define NEOPIXEL_PIN 2
|
||||
#endif
|
||||
|
||||
#ifndef LED_STRIP_LENGTH
|
||||
#define LED_STRIP_LENGTH 8
|
||||
#ifndef NEOPIXEL_LENGTH
|
||||
#define NEOPIXEL_LENGTH 8
|
||||
#endif
|
||||
|
||||
#ifndef LED_STRIP_TYPE
|
||||
#define LED_STRIP_TYPE (NEO_GRB + NEO_KHZ800)
|
||||
#ifndef NEOPIXEL_BRIGHTNESS
|
||||
#define NEOPIXEL_BRIGHTNESS 100
|
||||
#endif
|
||||
|
||||
#ifndef NEOPIXEL_UPDATE_INTERVAL
|
||||
#define NEOPIXEL_UPDATE_INTERVAL 100
|
||||
#endif
|
||||
|
||||
// Create Spore instance with custom labels
|
||||
Spore spore({
|
||||
{"app", "neopattern"},
|
||||
{"device", "light"},
|
||||
{"pixels", String(LED_STRIP_LENGTH)},
|
||||
{"pin", String(LED_STRIP_PIN)}
|
||||
{"device", "led_strip"},
|
||||
{"pixels", String(NEOPIXEL_LENGTH)},
|
||||
{"pin", String(NEOPIXEL_PIN)}
|
||||
});
|
||||
|
||||
// Create custom service
|
||||
@@ -30,17 +36,25 @@ void setup() {
|
||||
// Initialize the Spore framework
|
||||
spore.setup();
|
||||
|
||||
// Create configuration
|
||||
NeoPixelConfig config(
|
||||
NEOPIXEL_PIN,
|
||||
NEOPIXEL_LENGTH,
|
||||
NEOPIXEL_BRIGHTNESS,
|
||||
NEOPIXEL_UPDATE_INTERVAL
|
||||
);
|
||||
|
||||
// Create and add custom service
|
||||
neoPatternService = new NeoPatternService(spore.getTaskManager(), LED_STRIP_LENGTH, LED_STRIP_PIN, LED_STRIP_TYPE);
|
||||
neoPatternService = new NeoPatternService(spore.getTaskManager(), config);
|
||||
spore.addService(neoPatternService);
|
||||
|
||||
// Start the API server and complete initialization
|
||||
spore.begin();
|
||||
|
||||
LOG_INFO( "Main", "NeoPattern service registered and ready!");
|
||||
LOG_INFO("Main", "NeoPattern service registered and ready!");
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Run the Spore framework loop
|
||||
spore.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 "Service.h"
|
||||
#include "util/Logging.h"
|
||||
#include "util/CpuUsage.h"
|
||||
|
||||
class Spore {
|
||||
public:
|
||||
@@ -33,6 +34,11 @@ public:
|
||||
TaskManager& getTaskManager() { return taskManager; }
|
||||
ClusterManager& getCluster() { return cluster; }
|
||||
ApiServer& getApiServer() { return apiServer; }
|
||||
|
||||
// CPU usage monitoring
|
||||
CpuUsage& getCpuUsage() { return cpuUsage; }
|
||||
float getCurrentCpuUsage() const { return cpuUsage.getCpuUsage(); }
|
||||
float getAverageCpuUsage() const { return cpuUsage.getAverageCpuUsage(); }
|
||||
|
||||
|
||||
private:
|
||||
@@ -45,6 +51,7 @@ private:
|
||||
TaskManager taskManager;
|
||||
ClusterManager cluster;
|
||||
ApiServer apiServer;
|
||||
CpuUsage cpuUsage;
|
||||
|
||||
std::vector<std::shared_ptr<Service>> services;
|
||||
bool initialized;
|
||||
|
||||
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;
|
||||
};
|
||||
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);
|
||||
};
|
||||
107
platformio.ini
107
platformio.ini
@@ -9,7 +9,7 @@
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[platformio]
|
||||
;default_envs = esp01_1m
|
||||
default_envs = base
|
||||
src_dir = .
|
||||
data_dir = ${PROJECT_DIR}/examples/${PIOENV}/data
|
||||
|
||||
@@ -80,69 +80,7 @@ build_src_filter =
|
||||
+<src/spore/util/*.cpp>
|
||||
+<src/internal/*.cpp>
|
||||
|
||||
[env:d1_mini_relay]
|
||||
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]
|
||||
[env:neopattern]
|
||||
platform = platformio/espressif8266@^4.2.1
|
||||
board = esp01_1m
|
||||
framework = arduino
|
||||
@@ -162,44 +100,3 @@ build_src_filter =
|
||||
+<src/spore/types/*.cpp>
|
||||
+<src/spore/util/*.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/TaskService.h"
|
||||
#include "spore/services/StaticFileService.h"
|
||||
#include "spore/services/MonitoringService.h"
|
||||
#include <Arduino.h>
|
||||
|
||||
Spore::Spore() : ctx(), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
||||
apiServer(ctx, taskManager, ctx.config.api_server_port),
|
||||
initialized(false), apiServerStarted(false) {
|
||||
cpuUsage(), initialized(false), apiServerStarted(false) {
|
||||
}
|
||||
|
||||
Spore::Spore(std::initializer_list<std::pair<String, String>> initialLabels)
|
||||
: ctx(initialLabels), network(ctx), taskManager(ctx), cluster(ctx, taskManager),
|
||||
apiServer(ctx, taskManager, ctx.config.api_server_port),
|
||||
initialized(false), apiServerStarted(false) {
|
||||
cpuUsage(), initialized(false), apiServerStarted(false) {
|
||||
}
|
||||
|
||||
Spore::~Spore() {
|
||||
@@ -33,6 +34,9 @@ void Spore::setup() {
|
||||
// Initialize core components
|
||||
initializeCore();
|
||||
|
||||
// Initialize CPU usage monitoring
|
||||
cpuUsage.begin();
|
||||
|
||||
// Register core services
|
||||
registerCoreServices();
|
||||
|
||||
@@ -68,7 +72,16 @@ void Spore::loop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start CPU usage measurement
|
||||
cpuUsage.startMeasurement();
|
||||
|
||||
// Execute main tasks
|
||||
taskManager.execute();
|
||||
|
||||
// End CPU usage measurement before yield
|
||||
cpuUsage.endMeasurement();
|
||||
|
||||
// Yield to allow other tasks to run
|
||||
yield();
|
||||
}
|
||||
|
||||
@@ -121,6 +134,7 @@ void Spore::registerCoreServices() {
|
||||
auto clusterService = std::make_shared<ClusterService>(ctx);
|
||||
auto taskService = std::make_shared<TaskService>(taskManager);
|
||||
auto staticFileService = std::make_shared<StaticFileService>(ctx, apiServer);
|
||||
auto monitoringService = std::make_shared<MonitoringService>(cpuUsage);
|
||||
|
||||
// Add to services list
|
||||
services.push_back(nodeService);
|
||||
@@ -128,6 +142,7 @@ void Spore::registerCoreServices() {
|
||||
services.push_back(clusterService);
|
||||
services.push_back(taskService);
|
||||
services.push_back(staticFileService);
|
||||
services.push_back(monitoringService);
|
||||
|
||||
LOG_INFO("Spore", "Core services registered");
|
||||
}
|
||||
|
||||
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::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();
|
||||
}
|
||||
}
|
||||
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