diff --git a/ctl.sh b/ctl.sh index 992ffb9..bf851a2 100755 --- a/ctl.sh +++ b/ctl.sh @@ -14,8 +14,7 @@ function build { pio run -e $1 } function all { - target esp01_1m - target d1_mini + pio run } ${@:-all} } @@ -51,4 +50,8 @@ function cluster { ${@:-info} } +function monitor { + pio run --target monitor +} + ${@:-info} diff --git a/examples/base/main.cpp b/examples/base/main.cpp index 942c4d3..cd48d90 100644 --- a/examples/base/main.cpp +++ b/examples/base/main.cpp @@ -7,6 +7,12 @@ #include "ApiServer.h" #include "TaskManager.h" +// Services +#include "NodeService.h" +#include "NetworkService.h" +#include "ClusterService.h" +#include "TaskService.h" + using namespace std; NodeContext ctx({ @@ -18,6 +24,12 @@ TaskManager taskManager(ctx); ClusterManager cluster(ctx, taskManager); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); +// Create services +NodeService nodeService(ctx); +NetworkService networkService(network); +ClusterService clusterService(ctx); +TaskService taskService(taskManager); + void setup() { Serial.begin(115200); @@ -27,7 +39,11 @@ void setup() { // Initialize and start all tasks taskManager.initialize(); - // Start the API server + // Register services and start API server + apiServer.addService(nodeService); + apiServer.addService(networkService); + apiServer.addService(clusterService); + apiServer.addService(taskService); apiServer.begin(); // Print initial task status diff --git a/examples/neopattern/NeoPatternService.cpp b/examples/neopattern/NeoPatternService.cpp new file mode 100644 index 0000000..0862453 --- /dev/null +++ b/examples/neopattern/NeoPatternService.cpp @@ -0,0 +1,187 @@ +#include "NeoPatternService.h" +#include "ApiServer.h" + +NeoPatternService::NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type) + : taskManager(taskMgr), + pixels(numPixels, pin, type), + updateIntervalMs(100), + brightness(48) { + pixels.setBrightness(brightness); + pixels.show(); + + registerTasks(); + setPatternByName("rainbow_cycle"); +} + +void NeoPatternService::registerEndpoints(ApiServer& api) { + api.addEndpoint("/api/neopattern/status", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }, + std::vector{}); + + api.addEndpoint("/api/neopattern/patterns", HTTP_GET, + [this](AsyncWebServerRequest* request) { handlePatternsRequest(request); }, + std::vector{}); + + api.addEndpoint("/api/neopattern", 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 NeoPatternService::handleStatusRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + doc["pin"] = pixels.getPin(); + doc["count"] = pixels.numPixels(); + doc["interval_ms"] = updateIntervalMs; + doc["brightness"] = brightness; + doc["pattern"] = currentPatternName(); + doc["total_steps"] = pixels.TotalSteps; + doc["color1"] = pixels.Color1; + doc["color2"] = pixels.Color2; + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void NeoPatternService::handlePatternsRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + JsonArray arr = doc.to(); + for (auto& kv : patternSetters) arr.add(kv.first); + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void NeoPatternService::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("neopattern_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)) { + 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); + } + + if (request->hasParam("total_steps", true)) { + pixels.TotalSteps = request->getParam("total_steps", true)->value().toInt(); + } + + 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; + } + + 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 NeoPatternService::setBrightness(uint8_t b) { + brightness = b; + pixels.setBrightness(brightness); + pixels.show(); +} + +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); + }); +} + +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; }; +} + +std::vector NeoPatternService::patternNamesVector() { + if (patternSetters.empty()) registerPatterns(); + std::vector 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"); + } + return String("off"); +} + +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(); + } +} + +void NeoPatternService::update() { + pixels.Update(); +} diff --git a/examples/neopattern/NeoPatternService.h b/examples/neopattern/NeoPatternService.h new file mode 100644 index 0000000..2407574 --- /dev/null +++ b/examples/neopattern/NeoPatternService.h @@ -0,0 +1,34 @@ +#pragma once +#include "Service.h" +#include "TaskManager.h" +#include "NeoPattern.cpp" +#include +#include + +class NeoPatternService : public Service { +public: + NeoPatternService(TaskManager& taskMgr, uint16_t numPixels, uint8_t pin, uint8_t type); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "NeoPattern"; } + + void setBrightness(uint8_t b); + void setPatternByName(const String& name); + +private: + void registerTasks(); + void registerPatterns(); + std::vector patternNamesVector(); + String currentPatternName(); + void update(); + + // Handlers + void handleStatusRequest(AsyncWebServerRequest* request); + void handlePatternsRequest(AsyncWebServerRequest* request); + void handleControlRequest(AsyncWebServerRequest* request); + + TaskManager& taskManager; + NeoPattern pixels; + unsigned long updateIntervalMs; + uint8_t brightness; + std::map> patternSetters; +}; diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp index a9f2b38..9523c5c 100644 --- a/examples/neopattern/main.cpp +++ b/examples/neopattern/main.cpp @@ -1,8 +1,5 @@ #include #include -#include -#include - #include "Globals.h" #include "NodeContext.h" #include "NetworkManager.h" @@ -10,8 +7,12 @@ #include "ApiServer.h" #include "TaskManager.h" -// Include the NeoPattern implementation from this example folder -#include "NeoPattern.cpp" +// Services +#include "NodeService.h" +#include "NetworkService.h" +#include "ClusterService.h" +#include "TaskService.h" +#include "NeoPatternService.h" #ifndef LED_STRIP_PIN #define LED_STRIP_PIN 2 @@ -21,206 +22,10 @@ #define LED_STRIP_LENGTH 8 #endif -#ifndef LED_STRIP_BRIGHTNESS -#define LED_STRIP_BRIGHTNESS 48 -#endif - -#ifndef LED_STRIP_UPDATE_INTERVAL -#define LED_STRIP_UPDATE_INTERVAL 100 -#endif - #ifndef LED_STRIP_TYPE #define LED_STRIP_TYPE (NEO_GRB + NEO_KHZ800) #endif -class NeoPatternService { -public: - NeoPatternService(NodeContext &ctx, TaskManager &taskMgr, - uint16_t numPixels, - uint8_t pin, - uint8_t type) - : ctx(ctx), taskManager(taskMgr), - pixels(numPixels, pin, type), - updateIntervalMs(LED_STRIP_UPDATE_INTERVAL), - brightness(LED_STRIP_BRIGHTNESS) { - pixels.setBrightness(brightness); - pixels.show(); - - registerTasks(); - setPatternByName("rainbow_cycle"); - } - - void registerApi(ApiServer &api) { - api.addEndpoint("/api/neopattern/status", HTTP_GET, [this](AsyncWebServerRequest *request) { - JsonDocument doc; - doc["pin"] = LED_STRIP_PIN; - doc["count"] = pixels.numPixels(); - doc["interval_ms"] = updateIntervalMs; - doc["brightness"] = brightness; - doc["pattern"] = currentPatternName(); - doc["total_steps"] = pixels.TotalSteps; - doc["color1"] = pixels.Color1; - doc["color2"] = pixels.Color2; - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); - }); - - api.addEndpoint("/api/neopattern/patterns", HTTP_GET, [this](AsyncWebServerRequest *request) { - JsonDocument doc; - JsonArray arr = doc.to(); - for (auto &kv : patternSetters) arr.add(kv.first); - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); - }); - - api.addEndpoint("/api/neopattern", HTTP_POST, - [this](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("neopattern_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)) { - 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); - } - - if (request->hasParam("total_steps", true)) { - pixels.TotalSteps = request->getParam("total_steps", true)->value().toInt(); - } - - 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; - } - - 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"), {}, String("100") }, - ApiServer::ParamSpec{ String("brightness"), false, String("body"), String("number"), {}, String("50") }, - ApiServer::ParamSpec{ String("color"), false, String("body"), String("color"), {} }, - ApiServer::ParamSpec{ String("color2"), false, String("body"), String("color"), {} }, - 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"), {} }, - ApiServer::ParamSpec{ String("r2"), false, String("body"), String("number"), {} }, - ApiServer::ParamSpec{ String("g2"), false, String("body"), String("number"), {} }, - ApiServer::ParamSpec{ String("b2"), false, String("body"), String("number"), {} }, - ApiServer::ParamSpec{ String("total_steps"), false, String("body"), String("number"), {} }, - ApiServer::ParamSpec{ String("direction"), false, String("body"), String("string"), { String("forward"), String("reverse") } }, - } - ); - } - - void setBrightness(uint8_t b) { - brightness = b; - pixels.setBrightness(brightness); - pixels.show(); - } - -private: - void 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); - }); - } - - void 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; }; - } - - std::vector patternNamesVector() { - if (patternSetters.empty()) registerPatterns(); - std::vector v; - v.reserve(patternSetters.size()); - for (const auto &kv : patternSetters) v.push_back(kv.first); - return v; - } - - String 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"); - } - return String("off"); - } - - void 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(); - } - } - - void update() { - pixels.Update(); - } - -private: - NodeContext &ctx; - TaskManager &taskManager; - NeoPattern pixels; - unsigned long updateIntervalMs; - uint8_t brightness; - std::map> patternSetters; -}; - NodeContext ctx({ {"app", "neopattern"}, {"device", "light"}, @@ -231,18 +36,32 @@ NetworkManager network(ctx); TaskManager taskManager(ctx); ClusterManager cluster(ctx, taskManager); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); -NeoPatternService neoService(ctx, taskManager, LED_STRIP_LENGTH, LED_STRIP_PIN, LED_STRIP_TYPE); + +// Create services +NodeService nodeService(ctx); +NetworkService networkService(network); +ClusterService clusterService(ctx); +TaskService taskService(taskManager); +NeoPatternService neoPatternService(taskManager, LED_STRIP_LENGTH, LED_STRIP_PIN, LED_STRIP_TYPE); void setup() { Serial.begin(115200); + // Setup WiFi first network.setupWiFi(); + // Initialize and start all tasks taskManager.initialize(); + // Register services and start API server + apiServer.addService(nodeService); + apiServer.addService(networkService); + apiServer.addService(clusterService); + apiServer.addService(taskService); + apiServer.addService(neoPatternService); apiServer.begin(); - neoService.registerApi(apiServer); + // Print initial task status taskManager.printTaskStatus(); } diff --git a/examples/neopixel/NeoPixelService.cpp b/examples/neopixel/NeoPixelService.cpp new file mode 100644 index 0000000..a66fb1e --- /dev/null +++ b/examples/neopixel/NeoPixelService.cpp @@ -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{}); + + 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; + } +} diff --git a/examples/neopixel/NeoPixelService.h b/examples/neopixel/NeoPixelService.h new file mode 100644 index 0000000..a52c355 --- /dev/null +++ b/examples/neopixel/NeoPixelService.h @@ -0,0 +1,67 @@ +#pragma once +#include "Service.h" +#include "TaskManager.h" +#include +#include +#include + +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 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> 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; +}; diff --git a/examples/neopixel/main.cpp b/examples/neopixel/main.cpp index 2dc9de1..6e0d3b0 100644 --- a/examples/neopixel/main.cpp +++ b/examples/neopixel/main.cpp @@ -1,9 +1,5 @@ #include #include -#include -#include -#include - #include "Globals.h" #include "NodeContext.h" #include "NetworkManager.h" @@ -11,6 +7,13 @@ #include "ApiServer.h" #include "TaskManager.h" +// Services +#include "NodeService.h" +#include "NetworkService.h" +#include "ClusterService.h" +#include "TaskService.h" +#include "NeoPixelService.h" + #ifndef NEOPIXEL_PIN #define NEOPIXEL_PIN 2 #endif @@ -23,348 +26,46 @@ #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"), {}, String("100") }, - ApiServer::ParamSpec{ String("brightness"), false, String("body"), String("number"), {}, String("50") }, - 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)} + {"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); + +// Create services +NodeService nodeService(ctx); +NetworkService networkService(network); +ClusterService clusterService(ctx); +TaskService taskService(taskManager); +NeoPixelService neoPixelService(taskManager, NEOPIXEL_COUNT, NEOPIXEL_PIN, NEOPIXEL_TYPE); void setup() { - Serial.begin(115200); + Serial.begin(115200); - network.setupWiFi(); + // Setup WiFi first + network.setupWiFi(); - taskManager.initialize(); + // Initialize and start all tasks + taskManager.initialize(); - apiServer.begin(); - neoService.registerApi(apiServer); + // Register services and start API server + apiServer.addService(nodeService); + apiServer.addService(networkService); + apiServer.addService(clusterService); + apiServer.addService(taskService); + apiServer.addService(neoPixelService); + apiServer.begin(); - taskManager.printTaskStatus(); + // Print initial task status + taskManager.printTaskStatus(); } void loop() { - taskManager.execute(); - yield(); -} + taskManager.execute(); + yield(); +} \ No newline at end of file diff --git a/examples/relay/RelayService.cpp b/examples/relay/RelayService.cpp new file mode 100644 index 0000000..9d0200a --- /dev/null +++ b/examples/relay/RelayService.cpp @@ -0,0 +1,89 @@ +#include "RelayService.h" +#include "ApiServer.h" + +RelayService::RelayService(TaskManager& taskMgr, int pin) + : taskManager(taskMgr), relayPin(pin), relayOn(false) { + pinMode(relayPin, OUTPUT); + // Many relay modules are active LOW. Start in OFF state (relay de-energized). + digitalWrite(relayPin, HIGH); + registerTasks(); +} + +void RelayService::registerEndpoints(ApiServer& api) { + api.addEndpoint("/api/relay/status", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }, + std::vector{}); + + api.addEndpoint("/api/relay", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + ParamSpec{String("state"), true, String("body"), String("string"), + {String("on"), String("off"), String("toggle")}} + }); +} + +void RelayService::handleStatusRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + doc["pin"] = relayPin; + doc["state"] = relayOn ? "on" : "off"; + doc["uptime"] = millis(); + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void RelayService::handleControlRequest(AsyncWebServerRequest* request) { + String state = request->hasParam("state", true) ? request->getParam("state", true)->value() : ""; + bool ok = false; + + if (state.equalsIgnoreCase("on")) { + turnOn(); + ok = true; + } else if (state.equalsIgnoreCase("off")) { + turnOff(); + ok = true; + } else if (state.equalsIgnoreCase("toggle")) { + toggle(); + ok = true; + } + + JsonDocument resp; + resp["success"] = ok; + resp["state"] = relayOn ? "on" : "off"; + if (!ok) { + resp["message"] = "Invalid state. Use: on, off, or toggle"; + } + + String json; + serializeJson(resp, json); + request->send(ok ? 200 : 400, "application/json", json); +} + +void RelayService::turnOn() { + relayOn = true; + // Active LOW relay + digitalWrite(relayPin, LOW); + Serial.println("[RelayService] Relay ON"); +} + +void RelayService::turnOff() { + relayOn = false; + digitalWrite(relayPin, HIGH); + Serial.println("[RelayService] Relay OFF"); +} + +void RelayService::toggle() { + if (relayOn) { + turnOff(); + } else { + turnOn(); + } +} + +void RelayService::registerTasks() { + taskManager.registerTask("relay_status_print", 5000, [this]() { + Serial.printf("[RelayService] Status - pin: %d, state: %s\n", + relayPin, relayOn ? "ON" : "OFF"); + }); +} diff --git a/examples/relay/RelayService.h b/examples/relay/RelayService.h new file mode 100644 index 0000000..b41fbc2 --- /dev/null +++ b/examples/relay/RelayService.h @@ -0,0 +1,25 @@ +#pragma once +#include "Service.h" +#include "TaskManager.h" +#include + +class RelayService : public Service { +public: + RelayService(TaskManager& taskMgr, int pin); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "Relay"; } + + void turnOn(); + void turnOff(); + void toggle(); + +private: + void registerTasks(); + + TaskManager& taskManager; + int relayPin; + bool relayOn; + + void handleStatusRequest(AsyncWebServerRequest* request); + void handleControlRequest(AsyncWebServerRequest* request); +}; diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 29e15b3..0cb0474 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -7,6 +7,13 @@ #include "ApiServer.h" #include "TaskManager.h" +// Services +#include "NodeService.h" +#include "NetworkService.h" +#include "ClusterService.h" +#include "TaskService.h" +#include "RelayService.h" + using namespace std; // Choose a default relay pin. For ESP-01 this is GPIO0. Adjust as needed for your board. @@ -14,88 +21,6 @@ using namespace std; #define RELAY_PIN 0 #endif -class RelayService { -public: - RelayService(NodeContext& ctx, TaskManager& taskMgr, int pin) - : ctx(ctx), taskManager(taskMgr), relayPin(pin), relayOn(false) { - pinMode(relayPin, OUTPUT); - // Many relay modules are active LOW. Start in OFF state (relay de-energized). - digitalWrite(relayPin, HIGH); - - registerTasks(); - } - - void registerApi(ApiServer& api) { - api.addEndpoint("/api/relay/status", HTTP_GET, [this](AsyncWebServerRequest* request) { - JsonDocument doc; - doc["pin"] = relayPin; - doc["state"] = relayOn ? "on" : "off"; - doc["uptime"] = millis(); - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); - }); - - api.addEndpoint("/api/relay", HTTP_POST, [this](AsyncWebServerRequest* request) { - String state = request->hasParam("state", true) ? request->getParam("state", true)->value() : ""; - bool ok = false; - if (state.equalsIgnoreCase("on")) { - turnOn(); - ok = true; - } else if (state.equalsIgnoreCase("off")) { - turnOff(); - ok = true; - } else if (state.equalsIgnoreCase("toggle")) { - toggle(); - ok = true; - } - - JsonDocument resp; - resp["success"] = ok; - resp["state"] = relayOn ? "on" : "off"; - if (!ok) { - resp["message"] = "Invalid state. Use: on, off, or toggle"; - } - String json; - serializeJson(resp, json); - request->send(ok ? 200 : 400, "application/json", json); - }, std::vector{ ApiServer::ParamSpec{ String("state"), true, String("body"), String("string"), { String("on"), String("off"), String("toggle") } } }); - } - - void turnOn() { - relayOn = true; - // Active LOW relay - digitalWrite(relayPin, LOW); - Serial.println("[RelayService] Relay ON"); - } - - void turnOff() { - relayOn = false; - digitalWrite(relayPin, HIGH); - Serial.println("[RelayService] Relay OFF"); - } - - void toggle() { - if (relayOn) { - turnOff(); - } else { - turnOn(); - } - } - -private: - void registerTasks() { - taskManager.registerTask("relay_status_print", 5000, [this]() { - Serial.printf("[RelayService] Status - pin: %d, state: %s\n", relayPin, relayOn ? "ON" : "OFF"); - }); - } - - NodeContext& ctx; - TaskManager& taskManager; - int relayPin; - bool relayOn; -}; - NodeContext ctx({ {"app", "relay"}, {"device", "actuator"}, @@ -105,7 +30,13 @@ NetworkManager network(ctx); TaskManager taskManager(ctx); ClusterManager cluster(ctx, taskManager); ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); -RelayService relayService(ctx, taskManager, RELAY_PIN); + +// Create services +NodeService nodeService(ctx); +NetworkService networkService(network); +ClusterService clusterService(ctx); +TaskService taskService(taskManager); +RelayService relayService(taskManager, RELAY_PIN); void setup() { Serial.begin(115200); @@ -116,9 +47,13 @@ void setup() { // Initialize and start all tasks taskManager.initialize(); - // Start the API server and expose relay endpoints + // Register services and start API server + apiServer.addService(nodeService); + apiServer.addService(networkService); + apiServer.addService(clusterService); + apiServer.addService(taskService); + apiServer.addService(relayService); apiServer.begin(); - relayService.registerApi(apiServer); // Print initial task status taskManager.printTaskStatus(); @@ -127,4 +62,4 @@ void setup() { void loop() { taskManager.execute(); yield(); -} +} \ No newline at end of file diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp index da4340f..fa7a60a 100644 --- a/examples/wifiscan/main.cpp +++ b/examples/wifiscan/main.cpp @@ -9,9 +9,12 @@ #include "NodeService.h" #include "ClusterService.h" #include "TaskService.h" -#include "WifiScanService.h" +#include "NetworkService.h" -NodeContext ctx; +NodeContext ctx({ + {"app", "network"}, + {"role", "demo"} +}); TaskManager taskManager(ctx); NetworkManager networkManager(ctx); ApiServer apiServer(ctx, taskManager); @@ -21,32 +24,24 @@ ClusterManager cluster(ctx, taskManager); NodeService nodeService(ctx); ClusterService clusterService(ctx); TaskService taskService(taskManager); -WifiScanService wifiScanService(networkManager); +NetworkService networkService(networkManager); void setup() { Serial.begin(115200); - Serial.println("\n[WiFiScan] Starting..."); // Initialize networking networkManager.setupWiFi(); taskManager.initialize(); - // Add all services to API server + // Setup API apiServer.addService(nodeService); apiServer.addService(clusterService); apiServer.addService(taskService); - apiServer.addService(wifiScanService); - - // Start API server + apiServer.addService(networkService); apiServer.begin(); taskManager.printTaskStatus(); - - Serial.println("[WiFiScan] Ready!"); - Serial.println("[WiFiScan] Available endpoints:"); - Serial.println(" POST /api/wifi/scan - Start WiFi scan"); - Serial.println(" GET /api/wifi - Get scan results"); } void loop() { diff --git a/include/NetworkManager.h b/include/NetworkManager.h index de76b93..38a1d4d 100644 --- a/include/NetworkManager.h +++ b/include/NetworkManager.h @@ -23,6 +23,25 @@ public: void processAccessPoints(); std::vector getAccessPoints() const; + // WiFi configuration methods + void setWiFiConfig(const String& ssid, const String& password, + uint32_t connect_timeout_ms = 10000, + uint32_t retry_delay_ms = 500); + + // Network status methods + bool isConnected() const { return WiFi.isConnected(); } + String getSSID() const { return WiFi.SSID(); } + IPAddress getLocalIP() const { return WiFi.localIP(); } + String getMacAddress() const { return WiFi.macAddress(); } + String getHostname() const { return WiFi.hostname(); } + int32_t getRSSI() const { return WiFi.RSSI(); } + WiFiMode_t getMode() const { return WiFi.getMode(); } + + // AP mode specific methods + IPAddress getAPIP() const { return WiFi.softAPIP(); } + String getAPMacAddress() const { return WiFi.softAPmacAddress(); } + uint8_t getConnectedStations() const { return WiFi.softAPgetStationNum(); } + private: NodeContext& ctx; std::vector accessPoints; diff --git a/include/NetworkService.h b/include/NetworkService.h new file mode 100644 index 0000000..c703bd7 --- /dev/null +++ b/include/NetworkService.h @@ -0,0 +1,22 @@ +#pragma once +#include "Service.h" +#include "NetworkManager.h" +#include "NodeContext.h" + +class NetworkService : public Service { +public: + NetworkService(NetworkManager& networkManager); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "Network"; } + +private: + NetworkManager& networkManager; + + // WiFi scanning endpoints + void handleWifiScanRequest(AsyncWebServerRequest* request); + void handleGetWifiNetworks(AsyncWebServerRequest* request); + + // Network status endpoints + void handleNetworkStatus(AsyncWebServerRequest* request); + void handleSetWifiConfig(AsyncWebServerRequest* request); +}; diff --git a/include/WifiScanService.h b/include/WifiScanService.h deleted file mode 100644 index b824ad6..0000000 --- a/include/WifiScanService.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "Service.h" -#include "NetworkManager.h" -#include "NodeContext.h" - -class WifiScanService : public Service { -public: - WifiScanService(NetworkManager& networkManager); - void registerEndpoints(ApiServer& api) override; - const char* getName() const override { return "WifiScan"; } - -private: - NetworkManager& networkManager; - - void handleScanRequest(AsyncWebServerRequest* request); - void handleGetAccessPoints(AsyncWebServerRequest* request); -}; diff --git a/platformio.ini b/platformio.ini index 1b24ccb..dee922c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,7 +9,7 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = esp01_1m +;default_envs = esp01_1m src_dir = . [common] @@ -106,7 +106,7 @@ lib_deps = ${common.lib_deps} adafruit/Adafruit NeoPixel@^1.15.1 build_flags = -DLED_STRIP_PIN=2 build_src_filter = - + + + + + @@ -121,7 +121,7 @@ board_build.flash_mode = dout board_build.flash_size = 1M lib_deps = ${common.lib_deps} build_src_filter = - + + + + + @@ -135,7 +135,7 @@ board_build.flash_mode = dio board_build.flash_size = 4M lib_deps = ${common.lib_deps} build_src_filter = - + + + + + @@ -150,6 +150,6 @@ board_build.flash_size = 4M lib_deps = ${common.lib_deps} adafruit/Adafruit NeoPixel@^1.15.1 build_src_filter = - + + + + + diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp index b89252e..c06cb30 100644 --- a/src/NetworkManager.cpp +++ b/src/NetworkManager.cpp @@ -57,6 +57,14 @@ std::vector NetworkManager::getAccessPoints() const { NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {} +void NetworkManager::setWiFiConfig(const String& ssid, const String& password, + uint32_t connect_timeout_ms, uint32_t retry_delay_ms) { + ctx.config.wifi_ssid = ssid; + ctx.config.wifi_password = password; + ctx.config.wifi_connect_timeout_ms = connect_timeout_ms; + ctx.config.wifi_retry_delay_ms = retry_delay_ms; +} + void NetworkManager::setHostnameFromMac() { uint8_t mac[6]; WiFi.macAddress(mac); diff --git a/src/NetworkService.cpp b/src/NetworkService.cpp new file mode 100644 index 0000000..8aa6d3b --- /dev/null +++ b/src/NetworkService.cpp @@ -0,0 +1,132 @@ +#include "NetworkService.h" +#include + +NetworkService::NetworkService(NetworkManager& networkManager) + : networkManager(networkManager) {} + +void NetworkService::registerEndpoints(ApiServer& api) { + // WiFi scanning endpoints + api.addEndpoint("/api/network/wifi/scan", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); }, + std::vector{}); + + api.addEndpoint("/api/network/wifi/networks", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); }, + std::vector{}); + + // Network status and configuration endpoints + api.addEndpoint("/api/network/status", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleNetworkStatus(request); }, + std::vector{}); + + api.addEndpoint("/api/network/wifi/config", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); }, + std::vector{ + ParamSpec{String("ssid"), true, String("body"), String("string"), {}, String("")}, + ParamSpec{String("password"), true, String("body"), String("string"), {}, String("")}, + ParamSpec{String("connect_timeout_ms"), false, String("body"), String("number"), {}, String("10000")}, + ParamSpec{String("retry_delay_ms"), false, String("body"), String("number"), {}, String("500")} + }); +} + +void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) { + networkManager.scanWifi(); + + JsonDocument doc; + doc["status"] = "scanning"; + doc["message"] = "WiFi scan started"; + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void NetworkService::handleGetWifiNetworks(AsyncWebServerRequest* request) { + auto accessPoints = networkManager.getAccessPoints(); + + JsonDocument doc; + doc["access_points"].to(); + + for (const auto& ap : accessPoints) { + JsonObject apObj = doc["access_points"].add(); + apObj["ssid"] = ap.ssid; + apObj["rssi"] = ap.rssi; + apObj["channel"] = ap.channel; + apObj["encryption_type"] = ap.encryptionType; + apObj["hidden"] = ap.isHidden; + + // Convert BSSID to string + char bssid[18]; + sprintf(bssid, "%02X:%02X:%02X:%02X:%02X:%02X", + ap.bssid[0], ap.bssid[1], ap.bssid[2], + ap.bssid[3], ap.bssid[4], ap.bssid[5]); + apObj["bssid"] = bssid; + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void NetworkService::handleNetworkStatus(AsyncWebServerRequest* request) { + JsonDocument doc; + + // WiFi status + doc["wifi"]["connected"] = networkManager.isConnected(); + doc["wifi"]["mode"] = networkManager.getMode() == WIFI_AP ? "AP" : "STA"; + doc["wifi"]["ssid"] = networkManager.getSSID(); + doc["wifi"]["ip"] = networkManager.getLocalIP().toString(); + doc["wifi"]["mac"] = networkManager.getMacAddress(); + doc["wifi"]["hostname"] = networkManager.getHostname(); + doc["wifi"]["rssi"] = networkManager.getRSSI(); + + // If in AP mode, add AP specific info + if (networkManager.getMode() == WIFI_AP) { + doc["wifi"]["ap_ip"] = networkManager.getAPIP().toString(); + doc["wifi"]["ap_mac"] = networkManager.getAPMacAddress(); + doc["wifi"]["stations_connected"] = networkManager.getConnectedStations(); + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void NetworkService::handleSetWifiConfig(AsyncWebServerRequest* request) { + if (!request->hasParam("ssid", true) || !request->hasParam("password", true)) { + request->send(400, "application/json", "{\"error\": \"Missing required parameters\"}"); + return; + } + + String ssid = request->getParam("ssid", true)->value(); + String password = request->getParam("password", true)->value(); + + // Optional parameters with defaults + uint32_t connect_timeout_ms = 10000; // Default 10 seconds + uint32_t retry_delay_ms = 500; // Default 500ms + + if (request->hasParam("connect_timeout_ms", true)) { + connect_timeout_ms = request->getParam("connect_timeout_ms", true)->value().toInt(); + } + if (request->hasParam("retry_delay_ms", true)) { + retry_delay_ms = request->getParam("retry_delay_ms", true)->value().toInt(); + } + + // Update configuration + networkManager.setWiFiConfig(ssid, password, connect_timeout_ms, retry_delay_ms); + + // Attempt to connect with new settings + networkManager.setupWiFi(); + + JsonDocument doc; + doc["status"] = "success"; + doc["message"] = "WiFi configuration updated"; + doc["connected"] = WiFi.isConnected(); + if (WiFi.isConnected()) { + doc["ip"] = WiFi.localIP().toString(); + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} diff --git a/src/WifiScanService.cpp b/src/WifiScanService.cpp deleted file mode 100644 index c5d410e..0000000 --- a/src/WifiScanService.cpp +++ /dev/null @@ -1,57 +0,0 @@ -#include "WifiScanService.h" -#include - -WifiScanService::WifiScanService(NetworkManager& networkManager) - : networkManager(networkManager) {} - -void WifiScanService::registerEndpoints(ApiServer& api) { - // POST /api/wifi/scan - Start WiFi scan - api.addEndpoint("/api/wifi/scan", HTTP_POST, - [this](AsyncWebServerRequest* request) { this->handleScanRequest(request); }, - std::vector{}); - - // GET /api/wifi - Get all access points - api.addEndpoint("/api/wifi", HTTP_GET, - [this](AsyncWebServerRequest* request) { this->handleGetAccessPoints(request); }, - std::vector{}); -} - -void WifiScanService::handleScanRequest(AsyncWebServerRequest* request) { - networkManager.scanWifi(); - - AsyncResponseStream *response = request->beginResponseStream("application/json"); - JsonDocument doc; - doc["status"] = "scanning"; - doc["message"] = "WiFi scan started"; - - serializeJson(doc, *response); - request->send(response); -} - -void WifiScanService::handleGetAccessPoints(AsyncWebServerRequest* request) { - auto accessPoints = networkManager.getAccessPoints(); - - JsonDocument doc; - - doc["access_points"].to(); - - for (const auto& ap : accessPoints) { - JsonObject apObj = doc["access_points"].add(); - apObj["ssid"] = ap.ssid; - apObj["rssi"] = ap.rssi; - apObj["channel"] = ap.channel; - apObj["encryption_type"] = ap.encryptionType; - apObj["hidden"] = ap.isHidden; - - // Convert BSSID to string - char bssid[18]; - sprintf(bssid, "%02X:%02X:%02X:%02X:%02X:%02X", - ap.bssid[0], ap.bssid[1], ap.bssid[2], - ap.bssid[3], ap.bssid[4], ap.bssid[5]); - apObj["bssid"] = bssid; - } - - AsyncResponseStream *response = request->beginResponseStream("application/json"); - serializeJson(doc, *response); - request->send(response); -}