#include #include #include #include #include #include "Globals.h" #include "NodeContext.h" #include "NetworkManager.h" #include "ClusterManager.h" #include "ApiServer.h" #include "TaskManager.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 // 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); } class NeoPixelService { public: enum class Pattern { Off, ColorWipe, Rainbow, RainbowCycle, TheaterChase, TheaterChaseRainbow }; NeoPixelService(NodeContext &ctx, TaskManager &taskMgr, uint16_t numPixels, uint8_t pin, neoPixelType type) : ctx(ctx), 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 registerApi(ApiServer &api) { api.addEndpoint("/api/neopixel/status", HTTP_GET, [this](AsyncWebServerRequest *request) { JsonDocument doc; doc["pin"] = NEOPIXEL_PIN; 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); }); api.addEndpoint("/api/neopixel/patterns", HTTP_GET, [this](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); }); api.addEndpoint("/api/neopixel", HTTP_POST, [this](AsyncWebServerRequest *request) { String pattern = request->hasParam("pattern", true) ? request->getParam("pattern", true)->value() : ""; if (pattern.length()) { setPatternByName(pattern); } if (request->hasParam("interval_ms", true)) { updateIntervalMs = request->getParam("interval_ms", true)->value().toInt(); if (updateIntervalMs < 1) updateIntervalMs = 1; 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); } // Optional RGB for color_wipe and theater_chase 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); }, std::vector{ ApiServer::ParamSpec{ String("pattern"), false, String("body"), String("string"), patternNamesVector() }, ApiServer::ParamSpec{ String("interval_ms"), false, String("body"), String("number"), {} }, ApiServer::ParamSpec{ String("brightness"), false, String("body"), String("number"), {} }, ApiServer::ParamSpec{ String("r"), false, String("body"), String("number"), {} }, ApiServer::ParamSpec{ String("g"), false, String("body"), String("number"), {} }, ApiServer::ParamSpec{ String("b"), false, String("body"), String("number"), {} }, } ); } void setPatternByName(const String &name) { auto it = patternUpdaters.find(name); if (it != patternUpdaters.end()) { // Map name to enum for status and reset state currentPattern = nameToPattern(name); resetStateForPattern(currentPattern); } } void setBrightness(uint8_t b) { brightness = b; strip.setBrightness(brightness); strip.show(); } private: void 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 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 patternNamesVector() const { std::vector v; v.reserve(patternUpdaters.size()); for (const auto &kv : patternUpdaters) v.push_back(kv.first); return v; } String 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"); } Pattern 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 resetStateForPattern(Pattern p) { // Clear strip by default when changing pattern strip.clear(); strip.show(); // Reset indexes/state variables wipeIndex = 0; rainbowJ = 0; cycleJ = 0; chaseJ = 0; chaseQ = 0; chasePhaseOn = true; lastUpdateMs = 0; // force immediate update on next tick currentPattern = p; } void 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(); } } // Pattern updaters (non-blocking; advance a small step each call) void updateOff() { // Ensure off state strip.clear(); strip.show(); } void updateColorWipe() { if (wipeIndex < strip.numPixels()) { strip.setPixelColor(wipeIndex, wipeColor); ++wipeIndex; strip.show(); } else { // Restart strip.clear(); wipeIndex = 0; } } void updateRainbow() { for (uint16_t i = 0; i < strip.numPixels(); i++) { strip.setPixelColor(i, colorWheel(strip, (i + rainbowJ) & 255)); } strip.show(); rainbowJ = (rainbowJ + 1) & 0xFF; // 0..255 } void 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 updateTheaterChase() { // Phase toggles on/off for the current q offset 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; // move the crawl chaseJ = (chaseJ + 1) % 10; // cycle count kept for status if needed } } void 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; } } private: NodeContext &ctx; TaskManager &taskManager; Adafruit_NeoPixel strip; std::map> 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; }; NodeContext ctx({ {"app", "neopixel"}, {"device", "light"}, {"pixels", String(NEOPIXEL_COUNT)}, {"pin", String(NEOPIXEL_PIN)} }); NetworkManager network(ctx); TaskManager taskManager(ctx); ClusterManager cluster(ctx, taskManager); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); NeoPixelService neoService(ctx, taskManager, NEOPIXEL_COUNT, NEOPIXEL_PIN, NEOPIXEL_TYPE); void setup() { Serial.begin(115200); network.setupWiFi(); taskManager.initialize(); apiServer.begin(); neoService.registerApi(apiServer); taskManager.printTaskStatus(); } void loop() { taskManager.execute(); yield(); }