#include "NeoPatternService.h" #include "spore/core/ApiServer.h" #include "spore/util/Logging.h" #include "spore/internal/Globals.h" #include #include NeoPatternService::NeoPatternService(NodeContext& ctx, TaskManager& taskMgr, const NeoPixelConfig& config) : taskManager(taskMgr), ctx(ctx), 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(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(); registerEventHandlers(); 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{}); // Patterns list endpoint api.addEndpoint("/api/neopattern/patterns", HTTP_GET, [this](AsyncWebServerRequest* request) { handlePatternsRequest(request); }, std::vector{}); // Control endpoint api.addEndpoint("/api/neopattern", HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ ParamSpec{String("pattern"), false, String("body"), String("string"), patternNamesVector()}, ParamSpec{String("color"), false, String("body"), String("color"), {}}, ParamSpec{String("color2"), false, String("body"), String("color"), {}}, ParamSpec{String("brightness"), false, String("body"), String("numberRange"), {}, String("80")}, ParamSpec{String("total_steps"), false, String("body"), String("numberRange"), {}, String("16")}, ParamSpec{String("direction"), false, String("body"), String("string"), {String("forward"), String("reverse")}}, ParamSpec{String("interval"), false, String("body"), String("number"), {}, String("100")}, ParamSpec{String("broadcast"), false, String("body"), String("boolean"), {}} }); // State endpoint for complex state updates api.addEndpoint("/api/neopattern/state", HTTP_POST, [this](AsyncWebServerRequest* request) { handleStateRequest(request); }, std::vector{}); } void NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) { JsonDocument doc; doc["pin"] = config.pin; doc["length"] = config.length; doc["brightness"] = currentState.brightness; doc["pattern"] = currentPatternName(); 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; // Add pattern metadata String currentPattern = currentPatternName(); doc["pattern_description"] = getPatternDescription(currentPattern); doc["pattern_requires_color2"] = patternRequiresColor2(currentPattern); doc["pattern_supports_direction"] = patternSupportsDirection(currentPattern); String json; serializeJson(doc, json); request->send(200, "application/json", json); } void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) { JsonDocument doc; JsonArray arr = doc.to(); // Get all patterns from registry and include metadata auto patterns = patternRegistry.getAllPatterns(); for (const auto& pattern : patterns) { JsonObject patternObj = arr.add(); patternObj["name"] = pattern.name; patternObj["type"] = pattern.type; patternObj["description"] = pattern.description; patternObj["requires_color2"] = pattern.requiresColor2; patternObj["supports_direction"] = pattern.supportsDirection; } String json; serializeJson(doc, json); request->send(200, "application/json", json); } void NeoPatternService::handleControlRequest(AsyncWebServerRequest* request) { bool updated = false; bool broadcast = false; if (request->hasParam("broadcast", true)) { String b = request->getParam("broadcast", true)->value(); broadcast = b.equalsIgnoreCase("true") || b == "1"; } // Build JSON payload from provided params (single source of truth) JsonDocument payload; bool any = false; if (request->hasParam("pattern", true)) { payload["pattern"] = request->getParam("pattern", true)->value(); any = true; } if (request->hasParam("color", true)) { payload["color"] = request->getParam("color", true)->value(); any = true; } if (request->hasParam("color2", true)) { payload["color2"] = request->getParam("color2", true)->value(); any = true; } if (request->hasParam("brightness", true)) { payload["brightness"] = request->getParam("brightness", true)->value(); any = true; } if (request->hasParam("total_steps", true)) { payload["total_steps"] = request->getParam("total_steps", true)->value(); any = true; } if (request->hasParam("direction", true)) { payload["direction"] = request->getParam("direction", true)->value(); any = true; } if (request->hasParam("interval", true)) { payload["interval"] = request->getParam("interval", true)->value(); any = true; } String payloadStr; serializeJson(payload, payloadStr); // Always apply locally via event so we have a single codepath for updates if (any) { std::string ev = "api/neopattern"; String localData = payloadStr; LOG_INFO("NeoPattern", String("Applying local api/neopattern via event payloadLen=") + String(payloadStr.length())); ctx.fire(ev, &localData); updated = true; } // Broadcast to peers if requested (delegate to core broadcast handler) if (broadcast && any) { JsonDocument eventDoc; eventDoc["event"] = "api/neopattern"; eventDoc["data"] = payloadStr; // data is JSON string String eventJson; serializeJson(eventDoc, eventJson); LOG_INFO("NeoPattern", String("Submitting cluster/broadcast for api/neopattern payloadLen=") + String(payloadStr.length())); std::string ev = "cluster/broadcast"; String eventStr = eventJson; ctx.fire(ev, &eventStr); } // Return current state JsonDocument resp; resp["ok"] = true; resp["pattern"] = currentPatternName(); 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::registerEventHandlers() { ctx.on("api/neopattern", [this](void* dataPtr) { String* jsonStr = static_cast(dataPtr); if (!jsonStr) { LOG_WARN("NeoPattern", "Received api/neopattern with null dataPtr"); return; } LOG_INFO("NeoPattern", String("Received api/neopattern event dataLen=") + String(jsonStr->length())); JsonDocument doc; DeserializationError err = deserializeJson(doc, *jsonStr); if (err) { LOG_WARN("NeoPattern", String("Failed to parse CLUSTER_EVENT data: ") + err.c_str()); return; } JsonObject obj = doc.as(); bool applied = applyControlParams(obj); if (applied) { LOG_INFO("NeoPattern", "Applied control from CLUSTER_EVENT"); } }); } bool NeoPatternService::applyControlParams(const JsonObject& obj) { bool updated = false; if (obj["pattern"].is() || obj["pattern"].is()) { String name = obj["pattern"].as(); if (isValidPattern(name)) { setPatternByName(name); updated = true; } } if (obj["color"].is() || obj["color"].is() || obj["color"].is() || obj["color"].is()) { String colorStr; if (obj["color"].is() || obj["color"].is()) { colorStr = String(obj["color"].as()); } else { colorStr = obj["color"].as(); } uint32_t color = parseColor(colorStr); setColor(color); updated = true; } if (obj["color2"].is() || obj["color2"].is() || obj["color2"].is() || obj["color2"].is()) { String colorStr; if (obj["color2"].is() || obj["color2"].is()) { colorStr = String(obj["color2"].as()); } else { colorStr = obj["color2"].as(); } uint32_t color = parseColor(colorStr); setColor2(color); updated = true; } if (obj["brightness"].is() || obj["brightness"].is() || obj["brightness"].is() || obj["brightness"].is()) { int b = 0; if (obj["brightness"].is() || obj["brightness"].is()) { b = obj["brightness"].as(); } else { b = String(obj["brightness"].as()).toInt(); } if (b < 0) { b = 0; } if (b > 255) { b = 255; } setBrightness(static_cast(b)); updated = true; } if (obj["total_steps"].is() || obj["total_steps"].is() || obj["total_steps"].is() || obj["total_steps"].is()) { int steps = 0; if (obj["total_steps"].is() || obj["total_steps"].is()) { steps = obj["total_steps"].as(); } else { steps = String(obj["total_steps"].as()).toInt(); } if (steps > 0) { setTotalSteps(static_cast(steps)); updated = true; } } if (obj["direction"].is() || obj["direction"].is()) { String dirStr = obj["direction"].as(); NeoDirection dir = (dirStr.equalsIgnoreCase("reverse")) ? NeoDirection::REVERSE : NeoDirection::FORWARD; setDirection(dir); updated = true; } if (obj["interval"].is() || obj["interval"].is() || obj["interval"].is() || obj["interval"].is()) { unsigned long interval = 0; if (obj["interval"].is() || obj["interval"].is()) { interval = obj["interval"].as(); } else { interval = String(obj["interval"].as()).toInt(); } if (interval > 0) { setUpdateInterval(interval); updated = true; } } return updated; } 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(pattern); neoPattern->ActivePattern = static_cast<::pattern>(pattern); resetStateForPattern(pattern); // Initialize the pattern using the registry patternRegistry.initializePattern(static_cast(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(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]() { LOG_INFO("NeoPattern", "Status update"); }); } void NeoPatternService::registerPatterns() { // Register all patterns with their metadata and callbacks patternRegistry.registerPattern( "none", static_cast(NeoPatternType::NONE), "No pattern - solid color", [this]() { /* No initialization needed */ }, [this]() { updateNone(); }, false, // doesn't require color2 false // doesn't support direction ); patternRegistry.registerPattern( "rainbow_cycle", static_cast(NeoPatternType::RAINBOW_CYCLE), "Rainbow cycle pattern", [this]() { neoPattern->RainbowCycle(updateIntervalMs, static_cast<::direction>(direction)); }, [this]() { updateRainbowCycle(); }, false, // doesn't require color2 true // supports direction ); patternRegistry.registerPattern( "theater_chase", static_cast(NeoPatternType::THEATER_CHASE), "Theater chase pattern", [this]() { neoPattern->TheaterChase(currentState.color, currentState.color2, updateIntervalMs, static_cast<::direction>(direction)); }, [this]() { updateTheaterChase(); }, true, // requires color2 true // supports direction ); patternRegistry.registerPattern( "color_wipe", static_cast(NeoPatternType::COLOR_WIPE), "Color wipe pattern", [this]() { neoPattern->ColorWipe(currentState.color, updateIntervalMs, static_cast<::direction>(direction)); }, [this]() { updateColorWipe(); }, false, // doesn't require color2 true // supports direction ); patternRegistry.registerPattern( "scanner", static_cast(NeoPatternType::SCANNER), "Scanner pattern", [this]() { neoPattern->Scanner(currentState.color, updateIntervalMs); }, [this]() { updateScanner(); }, false, // doesn't require color2 false // doesn't support direction ); patternRegistry.registerPattern( "fade", static_cast(NeoPatternType::FADE), "Fade pattern", [this]() { neoPattern->Fade(currentState.color, currentState.color2, currentState.totalSteps, updateIntervalMs, static_cast<::direction>(direction)); }, [this]() { updateFade(); }, true, // requires color2 true // supports direction ); patternRegistry.registerPattern( "fire", static_cast(NeoPatternType::FIRE), "Fire effect pattern", [this]() { neoPattern->Fire(50, 120); }, [this]() { updateFire(); }, false, // doesn't require color2 false // doesn't support direction ); } std::vector NeoPatternService::patternNamesVector() const { return patternRegistry.getAllPatternNames(); } String NeoPatternService::currentPatternName() const { return patternRegistry.getPatternName(static_cast(activePattern)); } NeoPatternService::NeoPatternType NeoPatternService::nameToPattern(const String& name) const { uint8_t type = patternRegistry.getPatternType(name); return static_cast(type); } 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); } } bool NeoPatternService::isValidPattern(const String& name) const { return patternRegistry.isValidPattern(name); } bool NeoPatternService::isValidPattern(NeoPatternType type) const { return patternRegistry.isValidPattern(static_cast(type)); } bool NeoPatternService::patternRequiresColor2(const String& name) const { const PatternInfo* info = patternRegistry.getPattern(name); return info ? info->requiresColor2 : false; } bool NeoPatternService::patternSupportsDirection(const String& name) const { const PatternInfo* info = patternRegistry.getPattern(name); return info ? info->supportsDirection : false; } String NeoPatternService::getPatternDescription(const String& name) const { const PatternInfo* info = patternRegistry.getPattern(name); return info ? info->description : ""; } void NeoPatternService::update() { if (!initialized) return; //unsigned long now = millis(); //if (now - lastUpdateMs < updateIntervalMs) return; //lastUpdateMs = now; // Use pattern registry to execute the current pattern patternRegistry.executePattern(static_cast(activePattern)); } 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); }