feat: services (#2)

This commit is contained in:
2025-09-13 13:45:24 +02:00
parent fe045804cb
commit 12caeb0be6
33 changed files with 3293 additions and 785 deletions

View File

@@ -0,0 +1,291 @@
#include "NeoPixelService.h"
#include "ApiServer.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;
}
}