#include "NeoPixelService.h" #include "spore/core/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{}); api.addEndpoint("/api/neopixel/patterns", HTTP_GET, [this](AsyncWebServerRequest* request) { handlePatternsRequest(request); }, std::vector{}); api.addEndpoint("/api/neopixel", HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ 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(); 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 NeoPixelService::patternNamesVector() const { std::vector 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; } }