From 5bf228306349e1ea4591e9f4411817de64019b0b Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Fri, 12 Sep 2025 15:59:21 +0200 Subject: [PATCH 1/9] feat: Add NeoPattern example and update API server functionality - Add new NeoPattern example with pattern-based LED control - Update ApiServer with enhanced functionality - Modify neopixel example - Update platformio configuration --- examples/neopattern/NeoPattern.cpp | 470 +++++++++++++++++++++++++++++ examples/neopattern/config.h | 31 ++ examples/neopattern/main.cpp | 252 ++++++++++++++++ examples/neopixel/main.cpp | 4 +- include/ApiServer.h | 1 + platformio.ini | 32 ++ src/ApiServer.cpp | 3 + 7 files changed, 791 insertions(+), 2 deletions(-) create mode 100644 examples/neopattern/NeoPattern.cpp create mode 100644 examples/neopattern/config.h create mode 100644 examples/neopattern/main.cpp diff --git a/examples/neopattern/NeoPattern.cpp b/examples/neopattern/NeoPattern.cpp new file mode 100644 index 0000000..fc090b2 --- /dev/null +++ b/examples/neopattern/NeoPattern.cpp @@ -0,0 +1,470 @@ +/** + * Original NeoPattern code by Bill Earl + * https://learn.adafruit.com/multi-tasking-the-arduino-part-3/overview + * + * TODO + * - cleanup the mess + * - fnc table for patterns to replace switch case + * + * Custom modifications by 0x1d: + * - default OnComplete callback that sets pattern to reverse + * - separate animation update from timer; Update now updates directly, UpdateScheduled uses timer + */ +#ifndef __NeoPattern_INCLUDED__ +#define __NeoPattern_INCLUDED__ + +#include + +using namespace std; + +// Pattern types supported: +enum pattern +{ + NONE = 0, + RAINBOW_CYCLE = 1, + THEATER_CHASE = 2, + COLOR_WIPE = 3, + SCANNER = 4, + FADE = 5, + FIRE = 6 +}; +// Patern directions supported: +enum direction +{ + FORWARD, + REVERSE +}; + +// NeoPattern Class - derived from the Adafruit_NeoPixel class +class NeoPattern : public Adafruit_NeoPixel +{ + public: + // Member Variables: + pattern ActivePattern = RAINBOW_CYCLE; // which pattern is running + direction Direction = FORWARD; // direction to run the pattern + + unsigned long Interval = 150; // milliseconds between updates + unsigned long lastUpdate = 0; // last update of position + + uint32_t Color1 = 0; + uint32_t Color2 = 0; // What colors are in use + uint16_t TotalSteps = 32; // total number of steps in the pattern + uint16_t Index; // current step within the pattern + uint16_t completed = 0; + + // FIXME return current NeoPatternState + void (*OnComplete)(int); // Callback on completion of pattern + + uint8_t *frameBuffer; + int bufferSize = 0; + + // Constructor - calls base-class constructor to initialize strip + NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type, void (*callback)(int)) + : Adafruit_NeoPixel(pixels, pin, type) + { + frameBuffer = (uint8_t *)malloc(768); + OnComplete = callback; + TotalSteps = numPixels(); + begin(); + } + + NeoPattern(uint16_t pixels, uint8_t pin, uint8_t type) + : Adafruit_NeoPixel(pixels, pin, type) + { + frameBuffer = (uint8_t *)malloc(768); + TotalSteps = numPixels(); + begin(); + } + + void handleStream(uint8_t *data, size_t len) + { + //const uint16_t *data16 = (uint16_t *)data; + bufferSize = len; + memcpy(frameBuffer, data, len); + } + + void drawFrameBuffer(int w, uint8_t *frame, int length) + { + for (int i = 0; i < length; i++) + { + uint8_t r = frame[i]; + uint8_t g = frame[i + 1]; + uint8_t b = frame[i + 2]; + setPixelColor(i, r, g, b); + } + } + + void onCompleteDefault(int pixels) + { + //Serial.println("onCompleteDefault"); + // FIXME no specific code + if (ActivePattern == THEATER_CHASE) + { + return; + } + Reverse(); + //Serial.println("pattern completed"); + } + + // Update the pattern + void Update() + { + switch (ActivePattern) + { + case RAINBOW_CYCLE: + RainbowCycleUpdate(); + break; + case THEATER_CHASE: + TheaterChaseUpdate(); + break; + case COLOR_WIPE: + ColorWipeUpdate(); + break; + case SCANNER: + ScannerUpdate(); + break; + case FADE: + FadeUpdate(); + break; + case FIRE: + Fire(50, 120); + break; + default: + if (bufferSize > 0) + { + drawFrameBuffer(TotalSteps, frameBuffer, bufferSize); + } + break; + } + } + + void UpdateScheduled() + { + if ((millis() - lastUpdate) > Interval) // time to update + { + lastUpdate = millis(); + Update(); + } + } + + // Increment the Index and reset at the end + void Increment() + { + completed = 0; + if (Direction == FORWARD) + { + Index++; + if (Index >= TotalSteps) + { + Index = 0; + completed = 1; + if (OnComplete != NULL) + { + OnComplete(numPixels()); // call the comlpetion callback + } + else + { + onCompleteDefault(numPixels()); + } + } + } + else // Direction == REVERSE + { + --Index; + if (Index <= 0) + { + Index = TotalSteps - 1; + completed = 1; + if (OnComplete != NULL) + { + OnComplete(numPixels()); // call the comlpetion callback + } + else + { + onCompleteDefault(numPixels()); + } + } + } + } + + // Reverse pattern direction + void Reverse() + { + if (Direction == FORWARD) + { + Direction = REVERSE; + Index = TotalSteps - 1; + } + else + { + Direction = FORWARD; + Index = 0; + } + } + + // Initialize for a RainbowCycle + void RainbowCycle(uint8_t interval, direction dir = FORWARD) + { + ActivePattern = RAINBOW_CYCLE; + Interval = interval; + TotalSteps = 255; + Index = 0; + Direction = dir; + } + + // Update the Rainbow Cycle Pattern + void RainbowCycleUpdate() + { + for (int i = 0; i < numPixels(); i++) + { + setPixelColor(i, Wheel(((i * 256 / numPixels()) + Index) & 255)); + } + show(); + Increment(); + } + + // Initialize for a Theater Chase + void TheaterChase(uint32_t color1, uint32_t color2, uint16_t interval, direction dir = FORWARD) + { + ActivePattern = THEATER_CHASE; + Interval = interval; + TotalSteps = numPixels(); + Color1 = color1; + Color2 = color2; + Index = 0; + Direction = dir; + } + + // Update the Theater Chase Pattern + void TheaterChaseUpdate() + { + for (int i = 0; i < numPixels(); i++) + { + if ((i + Index) % 3 == 0) + { + setPixelColor(i, Color1); + } + else + { + setPixelColor(i, Color2); + } + } + show(); + Increment(); + } + + // Initialize for a ColorWipe + void ColorWipe(uint32_t color, uint8_t interval, direction dir = FORWARD) + { + ActivePattern = COLOR_WIPE; + Interval = interval; + TotalSteps = numPixels(); + Color1 = color; + Index = 0; + Direction = dir; + } + + // Update the Color Wipe Pattern + void ColorWipeUpdate() + { + setPixelColor(Index, Color1); + show(); + Increment(); + } + + // Initialize for a SCANNNER + void Scanner(uint32_t color1, uint8_t interval) + { + ActivePattern = SCANNER; + Interval = interval; + TotalSteps = (numPixels() - 1) * 2; + Color1 = color1; + Index = 0; + } + + // Update the Scanner Pattern + void ScannerUpdate() + { + for (int i = 0; i < numPixels(); i++) + { + if (i == Index) // Scan Pixel to the right + { + setPixelColor(i, Color1); + } + else if (i == TotalSteps - Index) // Scan Pixel to the left + { + setPixelColor(i, Color1); + } + else // Fading tail + { + setPixelColor(i, DimColor(getPixelColor(i))); + } + } + show(); + Increment(); + } + + // Initialize for a Fade + void Fade(uint32_t color1, uint32_t color2, uint16_t steps, uint8_t interval, direction dir = FORWARD) + { + ActivePattern = FADE; + Interval = interval; + TotalSteps = steps; + Color1 = color1; + Color2 = color2; + Index = 0; + Direction = dir; + } + + // Update the Fade Pattern + void FadeUpdate() + { + // Calculate linear interpolation between Color1 and Color2 + // Optimise order of operations to minimize truncation error + uint8_t red = ((Red(Color1) * (TotalSteps - Index)) + (Red(Color2) * Index)) / TotalSteps; + uint8_t green = ((Green(Color1) * (TotalSteps - Index)) + (Green(Color2) * Index)) / TotalSteps; + uint8_t blue = ((Blue(Color1) * (TotalSteps - Index)) + (Blue(Color2) * Index)) / TotalSteps; + + ColorSet(Color(red, green, blue)); + show(); + Increment(); + } + + // Calculate 50% dimmed version of a color (used by ScannerUpdate) + uint32_t DimColor(uint32_t color) + { + // Shift R, G and B components one bit to the right + uint32_t dimColor = Color(Red(color) >> 1, Green(color) >> 1, Blue(color) >> 1); + return dimColor; + } + + // Set all pixels to a color (synchronously) + void ColorSet(uint32_t color) + { + for (int i = 0; i < numPixels(); i++) + { + setPixelColor(i, color); + } + show(); + } + + // Returns the Red component of a 32-bit color + uint8_t Red(uint32_t color) + { + return (color >> 16) & 0xFF; + } + + // Returns the Green component of a 32-bit color + uint8_t Green(uint32_t color) + { + return (color >> 8) & 0xFF; + } + + // Returns the Blue component of a 32-bit color + uint8_t Blue(uint32_t color) + { + return color & 0xFF; + } + + // Input a value 0 to 255 to get a color value. + // The colours are a transition r - g - b - back to r. + uint32_t Wheel(uint8_t WheelPos) + { + //if(WheelPos == 0) return Color(0,0,0); + WheelPos = 255 - WheelPos; + if (WheelPos < 85) + { + return Color(255 - WheelPos * 3, 0, WheelPos * 3); + } + else if (WheelPos < 170) + { + WheelPos -= 85; + return Color(0, WheelPos * 3, 255 - WheelPos * 3); + } + else + { + WheelPos -= 170; + return Color(WheelPos * 3, 255 - WheelPos * 3, 0); + } + } + /** + * Effects from https://www.tweaking4all.com/hardware/arduino/adruino-led-strip-effects/ + */ + void Fire(int Cooling, int Sparking) + { + uint8_t heat[numPixels()]; + int cooldown; + + // Step 1. Cool down every cell a little + for (int i = 0; i < numPixels(); i++) + { + cooldown = random(0, ((Cooling * 10) / numPixels()) + 2); + + if (cooldown > heat[i]) + { + heat[i] = 0; + } + else + { + heat[i] = heat[i] - cooldown; + } + } + + // Step 2. Heat from each cell drifts 'up' and diffuses a little + for (int k = numPixels() - 1; k >= 2; k--) + { + heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3; + } + + // Step 3. Randomly ignite new 'sparks' near the bottom + if (random(255) < Sparking) + { + int y = random(7); + heat[y] = heat[y] + random(160, 255); + //heat[y] = random(160,255); + } + + // Step 4. Convert heat to LED colors + for (int j = 0; j < numPixels(); j++) + { + setPixelHeatColor(j, heat[j]); + } + + showStrip(); + } + + void setPixelHeatColor(int Pixel, uint8_t temperature) + { + // Scale 'heat' down from 0-255 to 0-191 + uint8_t t192 = round((temperature / 255.0) * 191); + + // calculate ramp up from + uint8_t heatramp = t192 & 0x3F; // 0..63 + heatramp <<= 2; // scale up to 0..252 + + // figure out which third of the spectrum we're in: + if (t192 > 0x80) + { // hottest + setPixel(Pixel, 255, 255, heatramp); + } + else if (t192 > 0x40) + { // middle + setPixel(Pixel, 255, heatramp, 0); + } + else + { // coolest + setPixel(Pixel, heatramp, 0, 0); + } + } + + void setPixel(int Pixel, uint8_t red, uint8_t green, uint8_t blue) + { + setPixelColor(Pixel, Color(red, green, blue)); + } + void showStrip() + { + show(); + } +}; + +#endif \ No newline at end of file diff --git a/examples/neopattern/config.h b/examples/neopattern/config.h new file mode 100644 index 0000000..80ebd5c --- /dev/null +++ b/examples/neopattern/config.h @@ -0,0 +1,31 @@ +#ifndef __DEVICE_CONFIG__ +#define __DEVICE_CONFIG__ + +// Scheduler config +#define _TASK_SLEEP_ON_IDLE_RUN +#define _TASK_STD_FUNCTION +#define _TASK_PRIORITY + +// Chip config +#define SPROCKET_TYPE "SPROCKET" +#define SERIAL_BAUD_RATE 115200 +#define STARTUP_DELAY 1000 + +// network config +#define SPROCKET_MODE 1 +#define WIFI_CHANNEL 11 +#define AP_SSID "sprocket" +#define AP_PASSWORD "th3r31sn0sp00n" +#define STATION_SSID "MyAP" +#define STATION_PASSWORD "th3r31sn0sp00n" +#define HOSTNAME "sprocket" +#define CONNECT_TIMEOUT 10000 + +// NeoPixel conig +#define LED_STRIP_PIN D2 +#define LED_STRIP_LENGTH 8 +#define LED_STRIP_BRIGHTNESS 48 +#define LED_STRIP_UPDATE_INTERVAL 200 +#define LED_STRIP_DEFAULT_COLOR 100 + +#endif \ No newline at end of file diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp new file mode 100644 index 0000000..a9f2b38 --- /dev/null +++ b/examples/neopattern/main.cpp @@ -0,0 +1,252 @@ +#include +#include +#include +#include + +#include "Globals.h" +#include "NodeContext.h" +#include "NetworkManager.h" +#include "ClusterManager.h" +#include "ApiServer.h" +#include "TaskManager.h" + +// Include the NeoPattern implementation from this example folder +#include "NeoPattern.cpp" + +#ifndef LED_STRIP_PIN +#define LED_STRIP_PIN 2 +#endif + +#ifndef LED_STRIP_LENGTH +#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"}, + {"pixels", String(LED_STRIP_LENGTH)}, + {"pin", String(LED_STRIP_PIN)} +}); +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); + +void setup() { + Serial.begin(115200); + + network.setupWiFi(); + + taskManager.initialize(); + + apiServer.begin(); + neoService.registerApi(apiServer); + + taskManager.printTaskStatus(); +} + +void loop() { + taskManager.execute(); + yield(); +} \ No newline at end of file diff --git a/examples/neopixel/main.cpp b/examples/neopixel/main.cpp index 064d721..2dc9de1 100644 --- a/examples/neopixel/main.cpp +++ b/examples/neopixel/main.cpp @@ -135,8 +135,8 @@ public: }, 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("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"), {} }, diff --git a/include/ApiServer.h b/include/ApiServer.h index 4856c39..71ca6fe 100644 --- a/include/ApiServer.h +++ b/include/ApiServer.h @@ -26,6 +26,7 @@ public: String location; // "query" | "body" | "path" | "header" String type; // e.g. "string", "number", "boolean" std::vector values; // optional allowed values + String defaultValue; // optional default value (stringified) }; struct EndpointCapability { String uri; diff --git a/platformio.ini b/platformio.ini index 14449c3..e2bd428 100644 --- a/platformio.ini +++ b/platformio.ini @@ -92,3 +92,35 @@ build_src_filter = + + + + +[env:esp01_1m_neopattern] +platform = platformio/espressif8266@^4.2.1 +board = esp01_1m +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.partitions = partitions_ota_1M.csv +board_build.flash_mode = dout +board_build.flash_size = 1M +lib_deps = ${common.lib_deps} + adafruit/Adafruit NeoPixel@^1.15.1 +build_flags = -DLED_STRIP_PIN=2 +build_src_filter = + + + + + + + +[env:d1_mini_neopattern] +platform = platformio/espressif8266@^4.2.1 +board = d1_mini +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.flash_mode = dio +board_build.flash_size = 4M +lib_deps = ${common.lib_deps} + adafruit/Adafruit NeoPixel@^1.15.1 +build_src_filter = + + + + + + diff --git a/src/ApiServer.cpp b/src/ApiServer.cpp index 60acb94..7b60059 100644 --- a/src/ApiServer.cpp +++ b/src/ApiServer.cpp @@ -365,6 +365,9 @@ void ApiServer::onCapabilitiesRequest(AsyncWebServerRequest *request) { allowed.add(v); } } + if (ps.defaultValue.length() > 0) { + p["default"] = ps.defaultValue; + } } } } -- 2.49.1 From fe3943b48a60274ab26f45a148d7c4a257758ea4 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Fri, 12 Sep 2025 22:08:47 +0200 Subject: [PATCH 2/9] feat: wifiscan --- .cursor/rules | 2 + ctl.sh | 4 + examples/wifiscan/README.md | 88 +++++++++++++++++ examples/wifiscan/main.cpp | 183 ++++++++++++++++++++++++++++++++++++ include/NetworkManager.h | 12 +++ include/NodeContext.h | 25 +++++ include/TaskManager.h | 4 +- platformio.ini | 29 ++++++ src/NetworkManager.cpp | 153 +++++++++++++++++++++++++++++- src/NodeContext.cpp | 4 + 10 files changed, 502 insertions(+), 2 deletions(-) create mode 100644 .cursor/rules create mode 100644 examples/wifiscan/README.md create mode 100644 examples/wifiscan/main.cpp diff --git a/.cursor/rules b/.cursor/rules new file mode 100644 index 0000000..b82809d --- /dev/null +++ b/.cursor/rules @@ -0,0 +1,2 @@ +## Build +- use ./ctl build target diff --git a/ctl.sh b/ctl.sh index 992ffb9..02c5c0b 100755 --- a/ctl.sh +++ b/ctl.sh @@ -51,4 +51,8 @@ function cluster { ${@:-info} } +function monitor { + pio run -t monitor +} + ${@:-info} diff --git a/examples/wifiscan/README.md b/examples/wifiscan/README.md new file mode 100644 index 0000000..152986e --- /dev/null +++ b/examples/wifiscan/README.md @@ -0,0 +1,88 @@ +# WiFi Scanner Example + +This example demonstrates how to use the async WiFi scanning functionality in SPORE with periodic scanning via tasks. + +## Features + +- **Async WiFi Scanning**: Non-blocking WiFi network discovery via NetworkManager +- **Task-based Periodic Scanning**: Automatically scans for networks every minute using TaskManager +- **Event-driven**: Uses the event system to handle scan events +- **REST API**: HTTP endpoints to manually trigger scans and view results +- **Detailed Results**: Shows SSID, RSSI, channel, and encryption type for each network + +## Usage + +1. Upload this example to your ESP8266 device +2. Open the Serial Monitor at 115200 baud +3. The device will automatically start scanning for WiFi networks every minute +4. Results will be displayed in the Serial Monitor +5. Use the REST API to manually trigger scans or view current results + +## Event System + +The WiFi scanner uses the following events: + +- `wifi/scan/start`: Fired when scan starts (both manual and periodic) +- `wifi/scan/complete`: Fired when scan completes successfully +- `wifi/scan/error`: Fired when scan fails to start +- `wifi/scan/timeout`: Fired when scan times out (10 seconds) + +## Task Management + +The example registers a periodic task: +- **Task Name**: `wifi_scan_periodic` +- **Interval**: 60 seconds (60000ms) +- **Function**: Fires `wifi/scan/start` event and starts WiFi scan + +## REST API + +### Manual Scan Control +- **Start Scan**: `POST /api/wifi/scan` +- **Get Status**: `GET /api/wifi/status` + +### Example API Usage +```bash +# Start a manual scan +curl -X POST http://192.168.1.50/api/wifi/scan + +# Get current scan status and results +curl http://192.168.1.50/api/wifi/status + +# Check task status +curl http://192.168.1.50/api/tasks/status + +# Disable periodic scanning +curl -X POST http://192.168.1.50/api/tasks/control \ + -d task=wifi_scan_periodic -d action=disable +``` + +## Architecture + +- **WiFiScannerService**: Manages periodic scanning and API endpoints +- **NetworkManager**: Handles WiFi scanning operations and callbacks +- **NodeContext**: Stores the WiFi access points data +- **TaskManager**: Schedules periodic scan tasks +- **Event System**: Provides communication between components + +## WiFiAccessPoint Structure + +```cpp +struct WiFiAccessPoint { + String ssid; // Network name + int32_t rssi; // Signal strength + uint8_t encryption; // Encryption type + uint8_t* bssid; // MAC address + int32_t channel; // WiFi channel + bool isHidden; // Hidden network flag +}; +``` + +## Task Configuration + +The periodic scanning task can be controlled via the standard TaskManager API: +- Enable/disable the task +- Change the scan interval +- Monitor task status +- Start/stop the task + +This provides flexibility to adjust scanning behavior based on application needs. \ No newline at end of file diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp new file mode 100644 index 0000000..d82f591 --- /dev/null +++ b/examples/wifiscan/main.cpp @@ -0,0 +1,183 @@ +#include +#include +#include "Globals.h" +#include "NodeContext.h" +#include "NetworkManager.h" +#include "ClusterManager.h" +#include "ApiServer.h" +#include "TaskManager.h" + +using namespace std; + +class WiFiScannerService { +public: + WiFiScannerService(NodeContext& ctx, TaskManager& taskMgr, NetworkManager& networkMgr) + : ctx(ctx), taskManager(taskMgr), network(networkMgr) { + registerTasks(); + } + + void registerApi(ApiServer& api) { + api.addEndpoint("/api/wifi/scan", HTTP_POST, [this](AsyncWebServerRequest* request) { + if (network.isWiFiScanning()) { + JsonDocument resp; + resp["success"] = false; + resp["message"] = "WiFi scan already in progress"; + String json; + serializeJson(resp, json); + request->send(409, "application/json", json); + return; + } + + network.startWiFiScan(); + + JsonDocument resp; + resp["success"] = true; + resp["message"] = "WiFi scan started"; + String json; + serializeJson(resp, json); + request->send(200, "application/json", json); + }); + + api.addEndpoint("/api/wifi/status", HTTP_GET, [this](AsyncWebServerRequest* request) { + JsonDocument doc; + doc["scanning"] = network.isWiFiScanning(); + doc["networks_found"] = ctx.wifiAccessPoints->size(); + + JsonArray networks = doc["networks"].to(); + for (const auto& ap : *ctx.wifiAccessPoints) { + JsonObject network = networks.add(); + network["ssid"] = ap.ssid; + network["rssi"] = ap.rssi; + network["channel"] = ap.channel; + network["encryption"] = ap.encryption; + network["hidden"] = ap.isHidden; + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); + }); + } + +private: + void registerTasks() { + // Register task to start WiFi scan every minute (60000ms) + taskManager.registerTask("wifi_scan_periodic", 60000, [this]() { + if (!network.isWiFiScanning()) { + Serial.println("[WiFiScannerService] Starting periodic WiFi scan..."); + ctx.fire("wifi/scan/start", nullptr); + network.startWiFiScan(); + } else { + Serial.println("[WiFiScannerService] Skipping scan - already in progress"); + } + }); + } + + NodeContext& ctx; + TaskManager& taskManager; + NetworkManager& network; +}; + +NodeContext ctx({ + {"app", "wifiscan"}, + {"role", "demo"} +}); +NetworkManager network(ctx); +TaskManager taskManager(ctx); +ClusterManager cluster(ctx, taskManager); +ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); +WiFiScannerService wifiScanner(ctx, taskManager, network); + +// WiFi scan start callback +void onWiFiScanStart(void* data) { + Serial.println("[WiFi] Scan started..."); +} + +// WiFi scan complete callback +void onWiFiScanComplete(void* data) { + Serial.println("\n=== WiFi Scan Results ==="); + + std::vector* accessPoints = static_cast*>(data); + + if (accessPoints->empty()) { + Serial.println("No networks found"); + return; + } + + Serial.printf("Found %d networks:\n", accessPoints->size()); + Serial.println("SSID\t\t\tRSSI\tChannel\tEncryption"); + Serial.println("----------------------------------------"); + + for (const auto& ap : *accessPoints) { + String encryptionStr = "Unknown"; + switch (ap.encryption) { + case ENC_TYPE_NONE: + encryptionStr = "Open"; + break; + case ENC_TYPE_WEP: + encryptionStr = "WEP"; + break; + case ENC_TYPE_TKIP: + encryptionStr = "WPA"; + break; + case ENC_TYPE_CCMP: + encryptionStr = "WPA2"; + break; + case ENC_TYPE_AUTO: + encryptionStr = "Auto"; + break; + } + + Serial.printf("%-20s\t%d\t%d\t%s\n", + ap.ssid.c_str(), ap.rssi, ap.channel, encryptionStr.c_str()); + } + Serial.println("========================\n"); +} + +// WiFi scan error callback +void onWiFiScanError(void* data) { + Serial.println("[WiFi] Scan failed - error occurred"); +} + +// WiFi scan timeout callback +void onWiFiScanTimeout(void* data) { + Serial.println("[WiFi] Scan failed - timeout"); +} + +void setup() { + Serial.begin(115200); + Serial.println("\n=== WiFi Scanner Example ==="); + + // Initialize task manager first + taskManager.initialize(); + ctx.taskManager = &taskManager; + + // Initialize network tasks + network.initTasks(); + + // Setup WiFi + network.setupWiFi(); + + // Start the API server + apiServer.begin(); + wifiScanner.registerApi(apiServer); + + // Register WiFi scan event callbacks + ctx.on("wifi/scan/start", onWiFiScanStart); + ctx.on("wifi/scan/complete", onWiFiScanComplete); + ctx.on("wifi/scan/error", onWiFiScanError); + ctx.on("wifi/scan/timeout", onWiFiScanTimeout); + + // Start an initial WiFi scan + Serial.println("Starting initial WiFi scan..."); + ctx.fire("wifi/scan/start", nullptr); + network.startWiFiScan(); + + // Print initial task status + taskManager.printTaskStatus(); +} + +void loop() { + taskManager.execute(); + yield(); +} \ No newline at end of file diff --git a/include/NetworkManager.h b/include/NetworkManager.h index 9a98672..6c9042b 100644 --- a/include/NetworkManager.h +++ b/include/NetworkManager.h @@ -7,6 +7,18 @@ public: NetworkManager(NodeContext& ctx); void setupWiFi(); void setHostnameFromMac(); + void startWiFiScan(); + void updateWiFiScan(); + bool isWiFiScanning() const; + void processScanResults(int networksFound); + void initTasks(); // Initialize tasks after TaskManager is ready // Public method to process scan results + private: NodeContext& ctx; + bool wifiScanInProgress; + unsigned long wifiScanStartTime; + static const unsigned long WIFI_SCAN_TIMEOUT_MS = 10000; // 10 seconds timeout + static const char* const WIFI_SCAN_MONITOR_TASK; // Task name for WiFi scan monitoring + + void onWiFiScanComplete(int networksFound); // Keep private for internal use }; diff --git a/include/NodeContext.h b/include/NodeContext.h index 76576de..f41d142 100644 --- a/include/NodeContext.h +++ b/include/NodeContext.h @@ -2,12 +2,33 @@ #include #include +#include #include "NodeInfo.h" #include #include #include #include "Config.h" +// Forward declaration +class TaskManager; + +// WiFi access point structure +struct WiFiAccessPoint { + String ssid; + int32_t rssi; + uint8_t encryption; + uint8_t* bssid; + int32_t channel; + bool isHidden; + + WiFiAccessPoint() : rssi(0), encryption(0), bssid(nullptr), channel(0), isHidden(false) {} + ~WiFiAccessPoint() { + if (bssid) { + delete[] bssid; + } + } +}; + class NodeContext { public: NodeContext(); @@ -19,10 +40,14 @@ public: NodeInfo self; std::map* memberList; Config config; + TaskManager* taskManager; using EventCallback = std::function; std::map> eventRegistry; void on(const std::string& event, EventCallback cb); void fire(const std::string& event, void* data); + + // WiFi access points storage + std::vector* wifiAccessPoints; }; diff --git a/include/TaskManager.h b/include/TaskManager.h index ffe1488..d5947a9 100644 --- a/include/TaskManager.h +++ b/include/TaskManager.h @@ -4,9 +4,11 @@ #include #include #include -#include "NodeContext.h" #include +// Forward declaration +class NodeContext; + // Define our own callback type to avoid conflict with TaskScheduler using TaskFunction = std::function; diff --git a/platformio.ini b/platformio.ini index e2bd428..7a726f2 100644 --- a/platformio.ini +++ b/platformio.ini @@ -124,3 +124,32 @@ build_src_filter = + + + + +[env:esp01_1m_wifiscan] +platform = platformio/espressif8266@^4.2.1 +board = esp01_1m +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.partitions = partitions_ota_1M.csv +board_build.flash_mode = dout +board_build.flash_size = 1M +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + + +[env:d1_mini_wifiscan] +platform = platformio/espressif8266@^4.2.1 +board = d1_mini +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.flash_mode = dio +board_build.flash_size = 4M +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp index cae4ca0..2c50f2d 100644 --- a/src/NetworkManager.cpp +++ b/src/NetworkManager.cpp @@ -1,8 +1,29 @@ #include "NetworkManager.h" +#include "TaskManager.h" // SSID and password are now configured via Config class -NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {} +const char* const NetworkManager::WIFI_SCAN_MONITOR_TASK = "wifi_scan_monitor"; + +NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx), wifiScanInProgress(false), wifiScanStartTime(0) { + // Register scan process callback + ctx.on("wifi/scan/process", [this](void* data) { + int networksFound = reinterpret_cast(data); + this->processScanResults(networksFound); + }); +} + +void NetworkManager::initTasks() { + if (!ctx.taskManager) { + Serial.println("[WiFi] Error: TaskManager not initialized"); + return; + } + + // Register task to check scan status every 100ms (initially disabled) + ctx.taskManager->registerTask(WIFI_SCAN_MONITOR_TASK, 100, [this]() { + this->updateWiFiScan(); + }, false); // false = disabled by default +} void NetworkManager::setHostnameFromMac() { uint8_t mac[6]; @@ -65,3 +86,133 @@ void NetworkManager::setupWiFi() { // Notify listeners that the node is (re)discovered ctx.fire("node_discovered", &ctx.self); } + +void NetworkManager::startWiFiScan() { + if (wifiScanInProgress) { + Serial.println("[WiFi] Scan already in progress"); + return; + } + + // Check if we're in AP mode only + if (WiFi.getMode() == WIFI_AP) { + // Enable STA mode while keeping AP mode + WiFi.mode(WIFI_AP_STA); + delay(100); // Give some time for mode change + } + + // Ensure we have STA mode enabled + if (WiFi.getMode() != WIFI_STA && WiFi.getMode() != WIFI_AP_STA) { + Serial.println("[WiFi] Error: Cannot scan without STA mode enabled"); + ctx.fire("wifi/scan/error", nullptr); + return; + } + + Serial.println("[WiFi] Starting WiFi scan..."); + wifiScanInProgress = true; + wifiScanStartTime = millis(); + + // Enable the monitoring task if TaskManager is available + if (ctx.taskManager) { + ctx.taskManager->enableTask(WIFI_SCAN_MONITOR_TASK); + } + + // Clear previous results safely + if (ctx.wifiAccessPoints) { + ctx.wifiAccessPoints->clear(); + } + + // Disable interrupts briefly during scan start + noInterrupts(); + // Start the scan + WiFi.scanNetworksAsync([this](int networksFound) { + // Schedule callback in main loop + ctx.fire("wifi/scan/process", reinterpret_cast(networksFound)); + }, true); // Show hidden networks + interrupts(); +} + +void NetworkManager::updateWiFiScan() { + if (!wifiScanInProgress) { + return; + } + + // Check for timeout + if (millis() - wifiScanStartTime > WIFI_SCAN_TIMEOUT_MS) { + Serial.println("[WiFi] WiFi scan timeout"); + wifiScanInProgress = false; + if (ctx.taskManager) { + ctx.taskManager->disableTask(WIFI_SCAN_MONITOR_TASK); + } + ctx.fire("wifi/scan/timeout", nullptr); + } +} + +void NetworkManager::processScanResults(int networksFound) { + // This is called from the main loop context via the event system + if (!wifiScanInProgress) { + return; // Ignore if we're not expecting results + } + + wifiScanInProgress = false; + // Disable the monitoring task if TaskManager is available + if (ctx.taskManager) { + ctx.taskManager->disableTask(WIFI_SCAN_MONITOR_TASK); + } + + Serial.printf("[WiFi] Processing scan results, found %d networks\n", networksFound); + + // Create a temporary vector to hold new results + std::vector newAccessPoints; + + if (networksFound > 0) { + newAccessPoints.reserve(networksFound); // Pre-allocate space + + // Process each found network + for (int i = 0; i < networksFound; i++) { + WiFiAccessPoint ap; + ap.ssid = WiFi.SSID(i); + ap.rssi = WiFi.RSSI(i); + ap.encryption = WiFi.encryptionType(i); + ap.channel = WiFi.channel(i); + ap.isHidden = WiFi.isHidden(i); + + // Copy BSSID - with null check and bounds protection + uint8_t* bssid = WiFi.BSSID(i); + ap.bssid = nullptr; // Initialize to null first + if (bssid != nullptr) { + ap.bssid = new (std::nothrow) uint8_t[6]; // Use nothrow to prevent exceptions + if (ap.bssid != nullptr) { + memcpy(ap.bssid, bssid, 6); + } + } + + newAccessPoints.push_back(ap); + + Serial.printf("[WiFi] %d: %s (RSSI: %d, Ch: %d, Enc: %d)\n", + i, ap.ssid.c_str(), ap.rssi, ap.channel, ap.encryption); + } + } + + // Free the scan results from WiFi to prevent memory leaks + WiFi.scanDelete(); + + // Safely swap the new results with the stored results + if (ctx.wifiAccessPoints) { + ctx.wifiAccessPoints->swap(newAccessPoints); + // Fire the scan complete event with a copy of the pointer + auto accessPoints = ctx.wifiAccessPoints; + ctx.fire("wifi/scan/complete", accessPoints); + } else { + Serial.println("[WiFi] Error: wifiAccessPoints is null"); + ctx.fire("wifi/scan/error", nullptr); + } +} + +void NetworkManager::onWiFiScanComplete(int networksFound) { + // Internal callback that schedules processing in the main loop + ctx.fire("wifi/scan/process", reinterpret_cast(networksFound)); +} + +bool NetworkManager::isWiFiScanning() const { + return wifiScanInProgress; +} diff --git a/src/NodeContext.cpp b/src/NodeContext.cpp index e5833d3..4d12998 100644 --- a/src/NodeContext.cpp +++ b/src/NodeContext.cpp @@ -1,8 +1,11 @@ #include "NodeContext.h" +#include "TaskManager.h" NodeContext::NodeContext() { udp = new WiFiUDP(); memberList = new std::map(); + wifiAccessPoints = new std::vector(); + taskManager = nullptr; // Will be set by TaskManager constructor hostname = ""; self.hostname = ""; self.ip = IPAddress(); @@ -19,6 +22,7 @@ NodeContext::NodeContext(std::initializer_list> initia NodeContext::~NodeContext() { delete udp; delete memberList; + delete wifiAccessPoints; } void NodeContext::on(const std::string& event, EventCallback cb) { -- 2.49.1 From 5d4d68ca2ddbb9a5abce5afbff2591c4c98a9112 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 10:40:15 +0200 Subject: [PATCH 3/9] Revert "feat: wifiscan" This reverts commit fe3943b48a60274ab26f45a148d7c4a257758ea4. --- .cursor/rules | 2 - ctl.sh | 4 - examples/wifiscan/README.md | 88 ----------------- examples/wifiscan/main.cpp | 183 ------------------------------------ include/NetworkManager.h | 12 --- include/NodeContext.h | 25 ----- include/TaskManager.h | 4 +- platformio.ini | 29 ------ src/NetworkManager.cpp | 153 +----------------------------- src/NodeContext.cpp | 4 - 10 files changed, 2 insertions(+), 502 deletions(-) delete mode 100644 .cursor/rules delete mode 100644 examples/wifiscan/README.md delete mode 100644 examples/wifiscan/main.cpp diff --git a/.cursor/rules b/.cursor/rules deleted file mode 100644 index b82809d..0000000 --- a/.cursor/rules +++ /dev/null @@ -1,2 +0,0 @@ -## Build -- use ./ctl build target diff --git a/ctl.sh b/ctl.sh index 02c5c0b..992ffb9 100755 --- a/ctl.sh +++ b/ctl.sh @@ -51,8 +51,4 @@ function cluster { ${@:-info} } -function monitor { - pio run -t monitor -} - ${@:-info} diff --git a/examples/wifiscan/README.md b/examples/wifiscan/README.md deleted file mode 100644 index 152986e..0000000 --- a/examples/wifiscan/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# WiFi Scanner Example - -This example demonstrates how to use the async WiFi scanning functionality in SPORE with periodic scanning via tasks. - -## Features - -- **Async WiFi Scanning**: Non-blocking WiFi network discovery via NetworkManager -- **Task-based Periodic Scanning**: Automatically scans for networks every minute using TaskManager -- **Event-driven**: Uses the event system to handle scan events -- **REST API**: HTTP endpoints to manually trigger scans and view results -- **Detailed Results**: Shows SSID, RSSI, channel, and encryption type for each network - -## Usage - -1. Upload this example to your ESP8266 device -2. Open the Serial Monitor at 115200 baud -3. The device will automatically start scanning for WiFi networks every minute -4. Results will be displayed in the Serial Monitor -5. Use the REST API to manually trigger scans or view current results - -## Event System - -The WiFi scanner uses the following events: - -- `wifi/scan/start`: Fired when scan starts (both manual and periodic) -- `wifi/scan/complete`: Fired when scan completes successfully -- `wifi/scan/error`: Fired when scan fails to start -- `wifi/scan/timeout`: Fired when scan times out (10 seconds) - -## Task Management - -The example registers a periodic task: -- **Task Name**: `wifi_scan_periodic` -- **Interval**: 60 seconds (60000ms) -- **Function**: Fires `wifi/scan/start` event and starts WiFi scan - -## REST API - -### Manual Scan Control -- **Start Scan**: `POST /api/wifi/scan` -- **Get Status**: `GET /api/wifi/status` - -### Example API Usage -```bash -# Start a manual scan -curl -X POST http://192.168.1.50/api/wifi/scan - -# Get current scan status and results -curl http://192.168.1.50/api/wifi/status - -# Check task status -curl http://192.168.1.50/api/tasks/status - -# Disable periodic scanning -curl -X POST http://192.168.1.50/api/tasks/control \ - -d task=wifi_scan_periodic -d action=disable -``` - -## Architecture - -- **WiFiScannerService**: Manages periodic scanning and API endpoints -- **NetworkManager**: Handles WiFi scanning operations and callbacks -- **NodeContext**: Stores the WiFi access points data -- **TaskManager**: Schedules periodic scan tasks -- **Event System**: Provides communication between components - -## WiFiAccessPoint Structure - -```cpp -struct WiFiAccessPoint { - String ssid; // Network name - int32_t rssi; // Signal strength - uint8_t encryption; // Encryption type - uint8_t* bssid; // MAC address - int32_t channel; // WiFi channel - bool isHidden; // Hidden network flag -}; -``` - -## Task Configuration - -The periodic scanning task can be controlled via the standard TaskManager API: -- Enable/disable the task -- Change the scan interval -- Monitor task status -- Start/stop the task - -This provides flexibility to adjust scanning behavior based on application needs. \ No newline at end of file diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp deleted file mode 100644 index d82f591..0000000 --- a/examples/wifiscan/main.cpp +++ /dev/null @@ -1,183 +0,0 @@ -#include -#include -#include "Globals.h" -#include "NodeContext.h" -#include "NetworkManager.h" -#include "ClusterManager.h" -#include "ApiServer.h" -#include "TaskManager.h" - -using namespace std; - -class WiFiScannerService { -public: - WiFiScannerService(NodeContext& ctx, TaskManager& taskMgr, NetworkManager& networkMgr) - : ctx(ctx), taskManager(taskMgr), network(networkMgr) { - registerTasks(); - } - - void registerApi(ApiServer& api) { - api.addEndpoint("/api/wifi/scan", HTTP_POST, [this](AsyncWebServerRequest* request) { - if (network.isWiFiScanning()) { - JsonDocument resp; - resp["success"] = false; - resp["message"] = "WiFi scan already in progress"; - String json; - serializeJson(resp, json); - request->send(409, "application/json", json); - return; - } - - network.startWiFiScan(); - - JsonDocument resp; - resp["success"] = true; - resp["message"] = "WiFi scan started"; - String json; - serializeJson(resp, json); - request->send(200, "application/json", json); - }); - - api.addEndpoint("/api/wifi/status", HTTP_GET, [this](AsyncWebServerRequest* request) { - JsonDocument doc; - doc["scanning"] = network.isWiFiScanning(); - doc["networks_found"] = ctx.wifiAccessPoints->size(); - - JsonArray networks = doc["networks"].to(); - for (const auto& ap : *ctx.wifiAccessPoints) { - JsonObject network = networks.add(); - network["ssid"] = ap.ssid; - network["rssi"] = ap.rssi; - network["channel"] = ap.channel; - network["encryption"] = ap.encryption; - network["hidden"] = ap.isHidden; - } - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); - }); - } - -private: - void registerTasks() { - // Register task to start WiFi scan every minute (60000ms) - taskManager.registerTask("wifi_scan_periodic", 60000, [this]() { - if (!network.isWiFiScanning()) { - Serial.println("[WiFiScannerService] Starting periodic WiFi scan..."); - ctx.fire("wifi/scan/start", nullptr); - network.startWiFiScan(); - } else { - Serial.println("[WiFiScannerService] Skipping scan - already in progress"); - } - }); - } - - NodeContext& ctx; - TaskManager& taskManager; - NetworkManager& network; -}; - -NodeContext ctx({ - {"app", "wifiscan"}, - {"role", "demo"} -}); -NetworkManager network(ctx); -TaskManager taskManager(ctx); -ClusterManager cluster(ctx, taskManager); -ApiServer apiServer(ctx, taskManager, ctx.config.api_server_port); -WiFiScannerService wifiScanner(ctx, taskManager, network); - -// WiFi scan start callback -void onWiFiScanStart(void* data) { - Serial.println("[WiFi] Scan started..."); -} - -// WiFi scan complete callback -void onWiFiScanComplete(void* data) { - Serial.println("\n=== WiFi Scan Results ==="); - - std::vector* accessPoints = static_cast*>(data); - - if (accessPoints->empty()) { - Serial.println("No networks found"); - return; - } - - Serial.printf("Found %d networks:\n", accessPoints->size()); - Serial.println("SSID\t\t\tRSSI\tChannel\tEncryption"); - Serial.println("----------------------------------------"); - - for (const auto& ap : *accessPoints) { - String encryptionStr = "Unknown"; - switch (ap.encryption) { - case ENC_TYPE_NONE: - encryptionStr = "Open"; - break; - case ENC_TYPE_WEP: - encryptionStr = "WEP"; - break; - case ENC_TYPE_TKIP: - encryptionStr = "WPA"; - break; - case ENC_TYPE_CCMP: - encryptionStr = "WPA2"; - break; - case ENC_TYPE_AUTO: - encryptionStr = "Auto"; - break; - } - - Serial.printf("%-20s\t%d\t%d\t%s\n", - ap.ssid.c_str(), ap.rssi, ap.channel, encryptionStr.c_str()); - } - Serial.println("========================\n"); -} - -// WiFi scan error callback -void onWiFiScanError(void* data) { - Serial.println("[WiFi] Scan failed - error occurred"); -} - -// WiFi scan timeout callback -void onWiFiScanTimeout(void* data) { - Serial.println("[WiFi] Scan failed - timeout"); -} - -void setup() { - Serial.begin(115200); - Serial.println("\n=== WiFi Scanner Example ==="); - - // Initialize task manager first - taskManager.initialize(); - ctx.taskManager = &taskManager; - - // Initialize network tasks - network.initTasks(); - - // Setup WiFi - network.setupWiFi(); - - // Start the API server - apiServer.begin(); - wifiScanner.registerApi(apiServer); - - // Register WiFi scan event callbacks - ctx.on("wifi/scan/start", onWiFiScanStart); - ctx.on("wifi/scan/complete", onWiFiScanComplete); - ctx.on("wifi/scan/error", onWiFiScanError); - ctx.on("wifi/scan/timeout", onWiFiScanTimeout); - - // Start an initial WiFi scan - Serial.println("Starting initial WiFi scan..."); - ctx.fire("wifi/scan/start", nullptr); - network.startWiFiScan(); - - // Print initial task status - taskManager.printTaskStatus(); -} - -void loop() { - taskManager.execute(); - yield(); -} \ No newline at end of file diff --git a/include/NetworkManager.h b/include/NetworkManager.h index 6c9042b..9a98672 100644 --- a/include/NetworkManager.h +++ b/include/NetworkManager.h @@ -7,18 +7,6 @@ public: NetworkManager(NodeContext& ctx); void setupWiFi(); void setHostnameFromMac(); - void startWiFiScan(); - void updateWiFiScan(); - bool isWiFiScanning() const; - void processScanResults(int networksFound); - void initTasks(); // Initialize tasks after TaskManager is ready // Public method to process scan results - private: NodeContext& ctx; - bool wifiScanInProgress; - unsigned long wifiScanStartTime; - static const unsigned long WIFI_SCAN_TIMEOUT_MS = 10000; // 10 seconds timeout - static const char* const WIFI_SCAN_MONITOR_TASK; // Task name for WiFi scan monitoring - - void onWiFiScanComplete(int networksFound); // Keep private for internal use }; diff --git a/include/NodeContext.h b/include/NodeContext.h index f41d142..76576de 100644 --- a/include/NodeContext.h +++ b/include/NodeContext.h @@ -2,33 +2,12 @@ #include #include -#include #include "NodeInfo.h" #include #include #include #include "Config.h" -// Forward declaration -class TaskManager; - -// WiFi access point structure -struct WiFiAccessPoint { - String ssid; - int32_t rssi; - uint8_t encryption; - uint8_t* bssid; - int32_t channel; - bool isHidden; - - WiFiAccessPoint() : rssi(0), encryption(0), bssid(nullptr), channel(0), isHidden(false) {} - ~WiFiAccessPoint() { - if (bssid) { - delete[] bssid; - } - } -}; - class NodeContext { public: NodeContext(); @@ -40,14 +19,10 @@ public: NodeInfo self; std::map* memberList; Config config; - TaskManager* taskManager; using EventCallback = std::function; std::map> eventRegistry; void on(const std::string& event, EventCallback cb); void fire(const std::string& event, void* data); - - // WiFi access points storage - std::vector* wifiAccessPoints; }; diff --git a/include/TaskManager.h b/include/TaskManager.h index d5947a9..ffe1488 100644 --- a/include/TaskManager.h +++ b/include/TaskManager.h @@ -4,11 +4,9 @@ #include #include #include +#include "NodeContext.h" #include -// Forward declaration -class NodeContext; - // Define our own callback type to avoid conflict with TaskScheduler using TaskFunction = std::function; diff --git a/platformio.ini b/platformio.ini index 7a726f2..e2bd428 100644 --- a/platformio.ini +++ b/platformio.ini @@ -124,32 +124,3 @@ build_src_filter = + + + - -[env:esp01_1m_wifiscan] -platform = platformio/espressif8266@^4.2.1 -board = esp01_1m -framework = arduino -upload_speed = 115200 -monitor_speed = 115200 -board_build.partitions = partitions_ota_1M.csv -board_build.flash_mode = dout -board_build.flash_size = 1M -lib_deps = ${common.lib_deps} -build_src_filter = - + - + - + - -[env:d1_mini_wifiscan] -platform = platformio/espressif8266@^4.2.1 -board = d1_mini -framework = arduino -upload_speed = 115200 -monitor_speed = 115200 -board_build.flash_mode = dio -board_build.flash_size = 4M -lib_deps = ${common.lib_deps} -build_src_filter = - + - + - + diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp index 2c50f2d..cae4ca0 100644 --- a/src/NetworkManager.cpp +++ b/src/NetworkManager.cpp @@ -1,29 +1,8 @@ #include "NetworkManager.h" -#include "TaskManager.h" // SSID and password are now configured via Config class -const char* const NetworkManager::WIFI_SCAN_MONITOR_TASK = "wifi_scan_monitor"; - -NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx), wifiScanInProgress(false), wifiScanStartTime(0) { - // Register scan process callback - ctx.on("wifi/scan/process", [this](void* data) { - int networksFound = reinterpret_cast(data); - this->processScanResults(networksFound); - }); -} - -void NetworkManager::initTasks() { - if (!ctx.taskManager) { - Serial.println("[WiFi] Error: TaskManager not initialized"); - return; - } - - // Register task to check scan status every 100ms (initially disabled) - ctx.taskManager->registerTask(WIFI_SCAN_MONITOR_TASK, 100, [this]() { - this->updateWiFiScan(); - }, false); // false = disabled by default -} +NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {} void NetworkManager::setHostnameFromMac() { uint8_t mac[6]; @@ -86,133 +65,3 @@ void NetworkManager::setupWiFi() { // Notify listeners that the node is (re)discovered ctx.fire("node_discovered", &ctx.self); } - -void NetworkManager::startWiFiScan() { - if (wifiScanInProgress) { - Serial.println("[WiFi] Scan already in progress"); - return; - } - - // Check if we're in AP mode only - if (WiFi.getMode() == WIFI_AP) { - // Enable STA mode while keeping AP mode - WiFi.mode(WIFI_AP_STA); - delay(100); // Give some time for mode change - } - - // Ensure we have STA mode enabled - if (WiFi.getMode() != WIFI_STA && WiFi.getMode() != WIFI_AP_STA) { - Serial.println("[WiFi] Error: Cannot scan without STA mode enabled"); - ctx.fire("wifi/scan/error", nullptr); - return; - } - - Serial.println("[WiFi] Starting WiFi scan..."); - wifiScanInProgress = true; - wifiScanStartTime = millis(); - - // Enable the monitoring task if TaskManager is available - if (ctx.taskManager) { - ctx.taskManager->enableTask(WIFI_SCAN_MONITOR_TASK); - } - - // Clear previous results safely - if (ctx.wifiAccessPoints) { - ctx.wifiAccessPoints->clear(); - } - - // Disable interrupts briefly during scan start - noInterrupts(); - // Start the scan - WiFi.scanNetworksAsync([this](int networksFound) { - // Schedule callback in main loop - ctx.fire("wifi/scan/process", reinterpret_cast(networksFound)); - }, true); // Show hidden networks - interrupts(); -} - -void NetworkManager::updateWiFiScan() { - if (!wifiScanInProgress) { - return; - } - - // Check for timeout - if (millis() - wifiScanStartTime > WIFI_SCAN_TIMEOUT_MS) { - Serial.println("[WiFi] WiFi scan timeout"); - wifiScanInProgress = false; - if (ctx.taskManager) { - ctx.taskManager->disableTask(WIFI_SCAN_MONITOR_TASK); - } - ctx.fire("wifi/scan/timeout", nullptr); - } -} - -void NetworkManager::processScanResults(int networksFound) { - // This is called from the main loop context via the event system - if (!wifiScanInProgress) { - return; // Ignore if we're not expecting results - } - - wifiScanInProgress = false; - // Disable the monitoring task if TaskManager is available - if (ctx.taskManager) { - ctx.taskManager->disableTask(WIFI_SCAN_MONITOR_TASK); - } - - Serial.printf("[WiFi] Processing scan results, found %d networks\n", networksFound); - - // Create a temporary vector to hold new results - std::vector newAccessPoints; - - if (networksFound > 0) { - newAccessPoints.reserve(networksFound); // Pre-allocate space - - // Process each found network - for (int i = 0; i < networksFound; i++) { - WiFiAccessPoint ap; - ap.ssid = WiFi.SSID(i); - ap.rssi = WiFi.RSSI(i); - ap.encryption = WiFi.encryptionType(i); - ap.channel = WiFi.channel(i); - ap.isHidden = WiFi.isHidden(i); - - // Copy BSSID - with null check and bounds protection - uint8_t* bssid = WiFi.BSSID(i); - ap.bssid = nullptr; // Initialize to null first - if (bssid != nullptr) { - ap.bssid = new (std::nothrow) uint8_t[6]; // Use nothrow to prevent exceptions - if (ap.bssid != nullptr) { - memcpy(ap.bssid, bssid, 6); - } - } - - newAccessPoints.push_back(ap); - - Serial.printf("[WiFi] %d: %s (RSSI: %d, Ch: %d, Enc: %d)\n", - i, ap.ssid.c_str(), ap.rssi, ap.channel, ap.encryption); - } - } - - // Free the scan results from WiFi to prevent memory leaks - WiFi.scanDelete(); - - // Safely swap the new results with the stored results - if (ctx.wifiAccessPoints) { - ctx.wifiAccessPoints->swap(newAccessPoints); - // Fire the scan complete event with a copy of the pointer - auto accessPoints = ctx.wifiAccessPoints; - ctx.fire("wifi/scan/complete", accessPoints); - } else { - Serial.println("[WiFi] Error: wifiAccessPoints is null"); - ctx.fire("wifi/scan/error", nullptr); - } -} - -void NetworkManager::onWiFiScanComplete(int networksFound) { - // Internal callback that schedules processing in the main loop - ctx.fire("wifi/scan/process", reinterpret_cast(networksFound)); -} - -bool NetworkManager::isWiFiScanning() const { - return wifiScanInProgress; -} diff --git a/src/NodeContext.cpp b/src/NodeContext.cpp index 4d12998..e5833d3 100644 --- a/src/NodeContext.cpp +++ b/src/NodeContext.cpp @@ -1,11 +1,8 @@ #include "NodeContext.h" -#include "TaskManager.h" NodeContext::NodeContext() { udp = new WiFiUDP(); memberList = new std::map(); - wifiAccessPoints = new std::vector(); - taskManager = nullptr; // Will be set by TaskManager constructor hostname = ""; self.hostname = ""; self.ip = IPAddress(); @@ -22,7 +19,6 @@ NodeContext::NodeContext(std::initializer_list> initia NodeContext::~NodeContext() { delete udp; delete memberList; - delete wifiAccessPoints; } void NodeContext::on(const std::string& event, EventCallback cb) { -- 2.49.1 From 995d9730116aca4b5b7ee8ec1437eaa0d9cd75f2 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 12:35:52 +0200 Subject: [PATCH 4/9] feat: services --- examples/wifiscan/main.cpp | 55 ++++++ include/ApiServer.h | 36 +--- include/ApiTypes.h | 18 ++ include/ClusterService.h | 16 ++ include/NetworkManager.h | 18 ++ include/NodeContext.h | 2 + include/NodeService.h | 21 +++ include/Service.h | 9 + include/TaskService.h | 17 ++ include/WifiScanService.h | 17 ++ platformio.ini | 29 +++ src/ApiServer.cpp | 352 ++----------------------------------- src/ClusterService.cpp | 42 +++++ src/NetworkManager.cpp | 53 ++++++ src/NodeService.cpp | 157 +++++++++++++++++ src/TaskService.cpp | 123 +++++++++++++ src/WifiScanService.cpp | 57 ++++++ 17 files changed, 656 insertions(+), 366 deletions(-) create mode 100644 examples/wifiscan/main.cpp create mode 100644 include/ApiTypes.h create mode 100644 include/ClusterService.h create mode 100644 include/NodeService.h create mode 100644 include/Service.h create mode 100644 include/TaskService.h create mode 100644 include/WifiScanService.h create mode 100644 src/ClusterService.cpp create mode 100644 src/NodeService.cpp create mode 100644 src/TaskService.cpp create mode 100644 src/WifiScanService.cpp diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp new file mode 100644 index 0000000..da4340f --- /dev/null +++ b/examples/wifiscan/main.cpp @@ -0,0 +1,55 @@ +#include +#include "NodeContext.h" +#include "NetworkManager.h" +#include "ApiServer.h" +#include "TaskManager.h" +#include "ClusterManager.h" + +// Services +#include "NodeService.h" +#include "ClusterService.h" +#include "TaskService.h" +#include "WifiScanService.h" + +NodeContext ctx; +TaskManager taskManager(ctx); +NetworkManager networkManager(ctx); +ApiServer apiServer(ctx, taskManager); +ClusterManager cluster(ctx, taskManager); + +// Create services +NodeService nodeService(ctx); +ClusterService clusterService(ctx); +TaskService taskService(taskManager); +WifiScanService wifiScanService(networkManager); + +void setup() { + Serial.begin(115200); + Serial.println("\n[WiFiScan] Starting..."); + + // Initialize networking + networkManager.setupWiFi(); + + taskManager.initialize(); + + // Add all services to API server + apiServer.addService(nodeService); + apiServer.addService(clusterService); + apiServer.addService(taskService); + apiServer.addService(wifiScanService); + + // Start API server + 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() { + taskManager.execute(); + yield(); +} \ No newline at end of file diff --git a/include/ApiServer.h b/include/ApiServer.h index 71ca6fe..2b8fd59 100644 --- a/include/ApiServer.h +++ b/include/ApiServer.h @@ -10,53 +10,33 @@ #include "NodeContext.h" #include "NodeInfo.h" #include "TaskManager.h" +#include "ApiTypes.h" + +class Service; // Forward declaration class ApiServer { public: ApiServer(NodeContext& ctx, TaskManager& taskMgr, uint16_t port = 80); void begin(); + void addService(Service& service); void addEndpoint(const String& uri, int method, std::function requestHandler); void addEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler); - // Minimal capability spec types and registration overloads - struct ParamSpec { - String name; - bool required; - String location; // "query" | "body" | "path" | "header" - String type; // e.g. "string", "number", "boolean" - std::vector values; // optional allowed values - String defaultValue; // optional default value (stringified) - }; - struct EndpointCapability { - String uri; - int method; - std::vector params; - }; void addEndpoint(const String& uri, int method, std::function requestHandler, const std::vector& params); void addEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler, const std::vector& params); + + static const char* methodToStr(int method); + private: AsyncWebServer server; NodeContext& ctx; TaskManager& taskManager; + std::vector> services; std::vector> serviceRegistry; - std::vector capabilityRegistry; - void onClusterMembersRequest(AsyncWebServerRequest *request); - void methodToStr(const std::tuple &endpoint, JsonObject &apiObj); - void onSystemStatusRequest(AsyncWebServerRequest *request); - void onFirmwareUpdateRequest(AsyncWebServerRequest *request); - void onFirmwareUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final); - void onRestartRequest(AsyncWebServerRequest *request); - - // Task management endpoints - void onTaskStatusRequest(AsyncWebServerRequest *request); - void onTaskControlRequest(AsyncWebServerRequest *request); - - // Capabilities endpoint - void onCapabilitiesRequest(AsyncWebServerRequest *request); // Internal helpers void registerServiceForLocalNode(const String& uri, int method); diff --git a/include/ApiTypes.h b/include/ApiTypes.h new file mode 100644 index 0000000..34a333d --- /dev/null +++ b/include/ApiTypes.h @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +struct ParamSpec { + String name; + bool required; + String location; // "query" | "body" | "path" | "header" + String type; // e.g. "string", "number", "boolean" + std::vector values; // optional allowed values + String defaultValue; // optional default value (stringified) +}; + +struct EndpointCapability { + String uri; + int method; + std::vector params; +}; diff --git a/include/ClusterService.h b/include/ClusterService.h new file mode 100644 index 0000000..70b3415 --- /dev/null +++ b/include/ClusterService.h @@ -0,0 +1,16 @@ +#pragma once +#include "Service.h" +#include "NodeContext.h" +#include + +class ClusterService : public Service { +public: + ClusterService(NodeContext& ctx); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "Cluster"; } + +private: + NodeContext& ctx; + + void handleMembersRequest(AsyncWebServerRequest* request); +}; diff --git a/include/NetworkManager.h b/include/NetworkManager.h index 9a98672..de76b93 100644 --- a/include/NetworkManager.h +++ b/include/NetworkManager.h @@ -1,12 +1,30 @@ #pragma once #include "NodeContext.h" #include +#include + +struct AccessPoint { + String ssid; + int32_t rssi; + uint8_t encryptionType; + uint8_t* bssid; + int32_t channel; + bool isHidden; +}; class NetworkManager { public: NetworkManager(NodeContext& ctx); void setupWiFi(); void setHostnameFromMac(); + + // WiFi scanning methods + void scanWifi(); + void processAccessPoints(); + std::vector getAccessPoints() const; + private: NodeContext& ctx; + std::vector accessPoints; + bool isScanning = false; }; diff --git a/include/NodeContext.h b/include/NodeContext.h index 76576de..e4c3348 100644 --- a/include/NodeContext.h +++ b/include/NodeContext.h @@ -7,6 +7,7 @@ #include #include #include "Config.h" +#include "ApiTypes.h" class NodeContext { public: @@ -19,6 +20,7 @@ public: NodeInfo self; std::map* memberList; Config config; + std::vector capabilities; using EventCallback = std::function; std::map> eventRegistry; diff --git a/include/NodeService.h b/include/NodeService.h new file mode 100644 index 0000000..f5a89ff --- /dev/null +++ b/include/NodeService.h @@ -0,0 +1,21 @@ +#pragma once +#include "Service.h" +#include "NodeContext.h" +#include +#include + +class NodeService : public Service { +public: + NodeService(NodeContext& ctx); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "Node"; } + +private: + NodeContext& ctx; + + void handleStatusRequest(AsyncWebServerRequest* request); + void handleUpdateRequest(AsyncWebServerRequest* request); + void handleUpdateUpload(AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final); + void handleRestartRequest(AsyncWebServerRequest* request); + void handleCapabilitiesRequest(AsyncWebServerRequest* request); +}; diff --git a/include/Service.h b/include/Service.h new file mode 100644 index 0000000..f78d598 --- /dev/null +++ b/include/Service.h @@ -0,0 +1,9 @@ +#pragma once +#include "ApiServer.h" + +class Service { +public: + virtual ~Service() = default; + virtual void registerEndpoints(ApiServer& api) = 0; + virtual const char* getName() const = 0; +}; diff --git a/include/TaskService.h b/include/TaskService.h new file mode 100644 index 0000000..823c1c1 --- /dev/null +++ b/include/TaskService.h @@ -0,0 +1,17 @@ +#pragma once +#include "Service.h" +#include "TaskManager.h" +#include + +class TaskService : public Service { +public: + TaskService(TaskManager& taskManager); + void registerEndpoints(ApiServer& api) override; + const char* getName() const override { return "Task"; } + +private: + TaskManager& taskManager; + + void handleStatusRequest(AsyncWebServerRequest* request); + void handleControlRequest(AsyncWebServerRequest* request); +}; diff --git a/include/WifiScanService.h b/include/WifiScanService.h new file mode 100644 index 0000000..b824ad6 --- /dev/null +++ b/include/WifiScanService.h @@ -0,0 +1,17 @@ +#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 e2bd428..1b24ccb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -110,6 +110,35 @@ build_src_filter = + + +[env:esp01_1m_wifiscan] +platform = platformio/espressif8266@^4.2.1 +board = esp01_1m +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.partitions = partitions_ota_1M.csv +board_build.flash_mode = dout +board_build.flash_size = 1M +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + + +[env:d1_mini_wifiscan] +platform = platformio/espressif8266@^4.2.1 +board = d1_mini +framework = arduino +upload_speed = 115200 +monitor_speed = 115200 +board_build.flash_mode = dio +board_build.flash_size = 4M +lib_deps = ${common.lib_deps} +build_src_filter = + + + + + + + [env:d1_mini_neopattern] platform = platformio/espressif8266@^4.2.1 board = d1_mini diff --git a/src/ApiServer.cpp b/src/ApiServer.cpp index 7b60059..3a274ab 100644 --- a/src/ApiServer.cpp +++ b/src/ApiServer.cpp @@ -1,8 +1,8 @@ #include "ApiServer.h" +#include "Service.h" #include -// Shared helper for HTTP method to string -static const char* methodStrFromInt(int method) { +const char* ApiServer::methodToStr(int method) { switch (method) { case HTTP_GET: return "GET"; case HTTP_POST: return "POST"; @@ -39,7 +39,7 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, const std::vector& params) { - capabilityRegistry.push_back(EndpointCapability{uri, method, params}); + ctx.capabilities.push_back(EndpointCapability{uri, method, params}); registerServiceForLocalNode(uri, method); server.on(uri.c_str(), method, requestHandler); } @@ -47,346 +47,22 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, std::function uploadHandler, const std::vector& params) { - capabilityRegistry.push_back(EndpointCapability{uri, method, params}); + ctx.capabilities.push_back(EndpointCapability{uri, method, params}); registerServiceForLocalNode(uri, method); server.on(uri.c_str(), method, requestHandler, uploadHandler); } -void ApiServer::begin() { - addEndpoint("/api/node/status", HTTP_GET, - std::bind(&ApiServer::onSystemStatusRequest, this, std::placeholders::_1)); - addEndpoint("/api/cluster/members", HTTP_GET, - std::bind(&ApiServer::onClusterMembersRequest, this, std::placeholders::_1)); - addEndpoint("/api/node/update", HTTP_POST, - std::bind(&ApiServer::onFirmwareUpdateRequest, this, std::placeholders::_1), - std::bind(&ApiServer::onFirmwareUpload, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6) - ); - addEndpoint("/api/node/restart", HTTP_POST, - std::bind(&ApiServer::onRestartRequest, this, std::placeholders::_1)); +void ApiServer::addService(Service& service) { + services.push_back(service); + Serial.printf("[API] Added service: %s\n", service.getName()); +} - // New: Capabilities endpoint - addEndpoint("/api/capabilities", HTTP_GET, - std::bind(&ApiServer::onCapabilitiesRequest, this, std::placeholders::_1)); - - // Task management endpoints - addEndpoint("/api/tasks/status", HTTP_GET, - std::bind(&ApiServer::onTaskStatusRequest, this, std::placeholders::_1)); - addEndpoint("/api/tasks/control", HTTP_POST, - std::bind(&ApiServer::onTaskControlRequest, this, std::placeholders::_1), - std::vector{ - ParamSpec{String("task"), true, String("body"), String("string"), {}}, - ParamSpec{String("action"), true, String("body"), String("string"), {String("enable"), String("disable"), String("start"), String("stop"), String("status")}} - } - ); +void ApiServer::begin() { + // Register all service endpoints + for (auto& service : services) { + service.get().registerEndpoints(*this); + Serial.printf("[API] Registered endpoints for service: %s\n", service.get().getName()); + } server.begin(); } - -void ApiServer::onSystemStatusRequest(AsyncWebServerRequest *request) { - JsonDocument doc; - doc["freeHeap"] = ESP.getFreeHeap(); - doc["chipId"] = ESP.getChipId(); - doc["sdkVersion"] = ESP.getSdkVersion(); - doc["cpuFreqMHz"] = ESP.getCpuFreqMHz(); - doc["flashChipSize"] = ESP.getFlashChipSize(); - JsonArray apiArr = doc["api"].to(); - for (const auto& entry : serviceRegistry) { - JsonObject apiObj = apiArr.add(); - apiObj["uri"] = std::get<0>(entry); - apiObj["method"] = std::get<1>(entry); - } - // Include local node labels if present - if (ctx.memberList) { - auto it = ctx.memberList->find(ctx.hostname); - if (it != ctx.memberList->end()) { - JsonObject labelsObj = doc["labels"].to(); - for (const auto& kv : it->second.labels) { - labelsObj[kv.first.c_str()] = kv.second; - } - } else if (!ctx.self.labels.empty()) { - JsonObject labelsObj = doc["labels"].to(); - for (const auto& kv : ctx.self.labels) { - labelsObj[kv.first.c_str()] = kv.second; - } - } - } - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); -} - -void ApiServer::onClusterMembersRequest(AsyncWebServerRequest *request) { - JsonDocument doc; - JsonArray arr = doc["members"].to(); - for (const auto& pair : *ctx.memberList) { - const NodeInfo& node = pair.second; - JsonObject obj = arr.add(); - obj["hostname"] = node.hostname; - obj["ip"] = node.ip.toString(); - obj["lastSeen"] = node.lastSeen; - obj["latency"] = node.latency; - obj["status"] = statusToStr(node.status); - obj["resources"]["freeHeap"] = node.resources.freeHeap; - obj["resources"]["chipId"] = node.resources.chipId; - obj["resources"]["sdkVersion"] = node.resources.sdkVersion; - obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz; - obj["resources"]["flashChipSize"] = node.resources.flashChipSize; - JsonArray apiArr = obj["api"].to(); - for (const auto& endpoint : node.apiEndpoints) { - JsonObject apiObj = apiArr.add(); - apiObj["uri"] = std::get<0>(endpoint); - methodToStr(endpoint, apiObj); - } - // Add labels if present - if (!node.labels.empty()) { - JsonObject labelsObj = obj["labels"].to(); - for (const auto& kv : node.labels) { - labelsObj[kv.first.c_str()] = kv.second; - } - } - } - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); -} - -void ApiServer::methodToStr(const std::tuple &endpoint, JsonObject &apiObj) -{ - int method = std::get<1>(endpoint); - apiObj["method"] = methodStrFromInt(method); -} - -void ApiServer::onFirmwareUpdateRequest(AsyncWebServerRequest *request) { - bool success = !Update.hasError(); - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}"); - response->addHeader("Connection", "close"); - request->send(response); - request->onDisconnect([]() { - Serial.println("[API] Restart device"); - delay(10); - ESP.restart(); - }); -} - -void ApiServer::onFirmwareUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) { - if (!index) { - Serial.print("[OTA] Update Start "); - Serial.println(filename); - Update.runAsync(true); - if(!Update.begin(request->contentLength(), U_FLASH)) { - Serial.println("[OTA] Update failed: not enough space"); - Update.printError(Serial); - AsyncWebServerResponse *response = request->beginResponse(500, "application/json", "{\"status\": \"FAIL\"}"); - response->addHeader("Connection", "close"); - request->send(response); - return; - } - } - if (!Update.hasError()){ - if (Update.write(data, len) != len) { - Update.printError(Serial); - } - } - if (final) { - if (Update.end(true)) { - if(Update.isFinished()) { - Serial.print("[OTA] Update Success with "); - Serial.print(index + len); - Serial.println("B"); - } else { - Serial.println("[OTA] Update not finished"); - } - } else { - Serial.print("[OTA] Update failed: "); - Update.printError(Serial); - } - } - return; -} - -void ApiServer::onRestartRequest(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"status\": \"restarting\"}"); - response->addHeader("Connection", "close"); - request->send(response); - request->onDisconnect([]() { - Serial.println("[API] Restart device"); - delay(10); - ESP.restart(); - }); -} - -void ApiServer::onTaskStatusRequest(AsyncWebServerRequest *request) { - // Use a separate document as scratch space for task statuses to avoid interfering with the response root - JsonDocument scratch; - - // Get comprehensive task status from TaskManager - auto taskStatuses = taskManager.getAllTaskStatuses(scratch); - - // Build response document - JsonDocument doc; - - // Add summary information - JsonObject summaryObj = doc["summary"].to(); - summaryObj["totalTasks"] = taskStatuses.size(); - summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(), - [](const auto& pair) { return pair.second["enabled"]; }); - - // Add detailed task information - JsonArray tasksArr = doc["tasks"].to(); - for (const auto& taskPair : taskStatuses) { - JsonObject taskObj = tasksArr.add(); - taskObj["name"] = taskPair.first; - taskObj["interval"] = taskPair.second["interval"]; - taskObj["enabled"] = taskPair.second["enabled"]; - taskObj["running"] = taskPair.second["running"]; - taskObj["autoStart"] = taskPair.second["autoStart"]; - } - - // Add system information - JsonObject systemObj = doc["system"].to(); - systemObj["freeHeap"] = ESP.getFreeHeap(); - systemObj["uptime"] = millis(); - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); -} - -void ApiServer::onTaskControlRequest(AsyncWebServerRequest *request) { - // Parse the request body for task control commands - if (request->hasParam("task", true) && request->hasParam("action", true)) { - String taskName = request->getParam("task", true)->value(); - String action = request->getParam("action", true)->value(); - - bool success = false; - String message = ""; - - if (action == "enable") { - taskManager.enableTask(taskName.c_str()); - success = true; - message = "Task enabled"; - } else if (action == "disable") { - taskManager.disableTask(taskName.c_str()); - success = true; - message = "Task disabled"; - } else if (action == "start") { - taskManager.startTask(taskName.c_str()); - success = true; - message = "Task started"; - } else if (action == "stop") { - taskManager.stopTask(taskName.c_str()); - success = true; - message = "Task stopped"; - } else if (action == "status") { - // Get detailed status for a specific task - success = true; - message = "Task status retrieved"; - - // Create JsonDocument for status response - JsonDocument statusDoc; - statusDoc["success"] = success; - statusDoc["message"] = message; - statusDoc["task"] = taskName; - statusDoc["action"] = action; - - // Add task details to response - statusDoc["taskDetails"] = JsonObject(); - JsonObject taskDetails = statusDoc["taskDetails"]; - taskDetails["name"] = taskName; - taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str()); - taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str()); - taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str()); - - // Add system context - taskDetails["system"] = JsonObject(); - JsonObject systemInfo = taskDetails["system"]; - systemInfo["freeHeap"] = ESP.getFreeHeap(); - systemInfo["uptime"] = millis(); - - String statusJson; - serializeJson(statusDoc, statusJson); - request->send(200, "application/json", statusJson); - return; // Early return since we've already sent the response - } else { - success = false; - message = "Invalid action. Use: enable, disable, start, stop, or status"; - } - - JsonDocument doc; - doc["success"] = success; - doc["message"] = message; - doc["task"] = taskName; - doc["action"] = action; - - String json; - serializeJson(doc, json); - request->send(success ? 200 : 400, "application/json", json); - } else { - // Missing parameters - JsonDocument doc; - doc["success"] = false; - doc["message"] = "Missing parameters. Required: task, action"; - doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}"; - - String json; - serializeJson(doc, json); - request->send(400, "application/json", json); - } -} - -void ApiServer::onCapabilitiesRequest(AsyncWebServerRequest *request) { - JsonDocument doc; - JsonArray endpointsArr = doc["endpoints"].to(); - - // Track seen (uri|method) to avoid duplicates - std::vector seen; - auto makeKey = [](const String& uri, int method) { - String k = uri; k += "|"; k += method; return k; - }; - - // Rich entries first - for (const auto& cap : capabilityRegistry) { - String key = makeKey(cap.uri, cap.method); - seen.push_back(key); - JsonObject obj = endpointsArr.add(); - obj["uri"] = cap.uri; - obj["method"] = methodStrFromInt(cap.method); - if (!cap.params.empty()) { - JsonArray paramsArr = obj["params"].to(); - for (const auto& ps : cap.params) { - JsonObject p = paramsArr.add(); - p["name"] = ps.name; - p["location"] = ps.location; - p["required"] = ps.required; - p["type"] = ps.type; - if (!ps.values.empty()) { - JsonArray allowed = p["values"].to(); - for (const auto& v : ps.values) { - allowed.add(v); - } - } - if (ps.defaultValue.length() > 0) { - p["default"] = ps.defaultValue; - } - } - } - } - - // Then any endpoints without explicit param specs - for (const auto& entry : serviceRegistry) { - const String& uri = std::get<0>(entry); - int method = std::get<1>(entry); - String key = makeKey(uri, method); - bool exists = false; - for (const auto& s : seen) { if (s == key) { exists = true; break; } } - if (!exists) { - JsonObject obj = endpointsArr.add(); - obj["uri"] = uri; - obj["method"] = methodStrFromInt(method); - } - } - - String json; - serializeJson(doc, json); - request->send(200, "application/json", json); -} \ No newline at end of file diff --git a/src/ClusterService.cpp b/src/ClusterService.cpp new file mode 100644 index 0000000..f6fdb0c --- /dev/null +++ b/src/ClusterService.cpp @@ -0,0 +1,42 @@ +#include "ClusterService.h" +#include "ApiServer.h" + +ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {} + +void ClusterService::registerEndpoints(ApiServer& api) { + api.addEndpoint("/api/cluster/members", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleMembersRequest(request); }, + std::vector{}); +} + +void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + JsonArray arr = doc["members"].to(); + + for (const auto& pair : *ctx.memberList) { + const NodeInfo& node = pair.second; + JsonObject obj = arr.add(); + obj["hostname"] = node.hostname; + obj["ip"] = node.ip.toString(); + obj["lastSeen"] = node.lastSeen; + obj["latency"] = node.latency; + obj["status"] = statusToStr(node.status); + obj["resources"]["freeHeap"] = node.resources.freeHeap; + obj["resources"]["chipId"] = node.resources.chipId; + obj["resources"]["sdkVersion"] = node.resources.sdkVersion; + obj["resources"]["cpuFreqMHz"] = node.resources.cpuFreqMHz; + obj["resources"]["flashChipSize"] = node.resources.flashChipSize; + + // Add labels if present + if (!node.labels.empty()) { + JsonObject labelsObj = obj["labels"].to(); + for (const auto& kv : node.labels) { + labelsObj[kv.first.c_str()] = kv.second; + } + } + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} diff --git a/src/NetworkManager.cpp b/src/NetworkManager.cpp index cae4ca0..b89252e 100644 --- a/src/NetworkManager.cpp +++ b/src/NetworkManager.cpp @@ -2,6 +2,59 @@ // SSID and password are now configured via Config class +void NetworkManager::scanWifi() { + if (!isScanning) { + isScanning = true; + Serial.println("[WiFi] Starting WiFi scan..."); + // Start async WiFi scan + WiFi.scanNetworksAsync([this](int networksFound) { + Serial.printf("[WiFi] Scan completed, found %d networks\n", networksFound); + this->processAccessPoints(); + this->isScanning = false; + }, true); + } else { + Serial.println("[WiFi] Scan already in progress..."); + } +} + +void NetworkManager::processAccessPoints() { + int numNetworks = WiFi.scanComplete(); + if (numNetworks <= 0) { + Serial.println("[WiFi] No networks found or scan not complete"); + return; + } + + // Clear existing access points + accessPoints.clear(); + + // Process each network found + for (int i = 0; i < numNetworks; i++) { + AccessPoint ap; + ap.ssid = WiFi.SSID(i); + ap.rssi = WiFi.RSSI(i); + ap.encryptionType = WiFi.encryptionType(i); + ap.channel = WiFi.channel(i); + ap.isHidden = ap.ssid.length() == 0; + + // Copy BSSID + uint8_t* newBssid = new uint8_t[6]; + memcpy(newBssid, WiFi.BSSID(i), 6); + ap.bssid = newBssid; + + accessPoints.push_back(ap); + + Serial.printf("[WiFi] Found network %d: %s, Ch: %d, RSSI: %d\n", + i + 1, ap.ssid.c_str(), ap.channel, ap.rssi); + } + + // Free the memory used by the scan + WiFi.scanDelete(); +} + +std::vector NetworkManager::getAccessPoints() const { + return accessPoints; +} + NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {} void NetworkManager::setHostnameFromMac() { diff --git a/src/NodeService.cpp b/src/NodeService.cpp new file mode 100644 index 0000000..6e506d7 --- /dev/null +++ b/src/NodeService.cpp @@ -0,0 +1,157 @@ +#include "NodeService.h" +#include "ApiServer.h" + +NodeService::NodeService(NodeContext& ctx) : ctx(ctx) {} + +void NodeService::registerEndpoints(ApiServer& api) { + // Status endpoint + api.addEndpoint("/api/node/status", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }, + std::vector{}); + + // Update endpoint with file upload + api.addEndpoint("/api/node/update", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleUpdateRequest(request); }, + [this](AsyncWebServerRequest* request, const String& filename, size_t index, uint8_t* data, size_t len, bool final) { + handleUpdateUpload(request, filename, index, data, len, final); + }, + std::vector{ + ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")} + }); + + // Restart endpoint + api.addEndpoint("/api/node/restart", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleRestartRequest(request); }, + std::vector{}); + + // Capabilities endpoint + api.addEndpoint("/api/node/capabilities", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleCapabilitiesRequest(request); }, + std::vector{}); +} + +void NodeService::handleStatusRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + doc["freeHeap"] = ESP.getFreeHeap(); + doc["chipId"] = ESP.getChipId(); + doc["sdkVersion"] = ESP.getSdkVersion(); + doc["cpuFreqMHz"] = ESP.getCpuFreqMHz(); + doc["flashChipSize"] = ESP.getFlashChipSize(); + + // Include local node labels if present + if (ctx.memberList) { + auto it = ctx.memberList->find(ctx.hostname); + if (it != ctx.memberList->end()) { + JsonObject labelsObj = doc["labels"].to(); + for (const auto& kv : it->second.labels) { + labelsObj[kv.first.c_str()] = kv.second; + } + } else if (!ctx.self.labels.empty()) { + JsonObject labelsObj = doc["labels"].to(); + for (const auto& kv : ctx.self.labels) { + labelsObj[kv.first.c_str()] = kv.second; + } + } + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void NodeService::handleUpdateRequest(AsyncWebServerRequest* request) { + bool success = !Update.hasError(); + AsyncWebServerResponse* response = request->beginResponse(200, "application/json", + success ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}"); + response->addHeader("Connection", "close"); + request->send(response); + request->onDisconnect([]() { + Serial.println("[API] Restart device"); + delay(10); + ESP.restart(); + }); +} + +void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const String& filename, + size_t index, uint8_t* data, size_t len, bool final) { + if (!index) { + Serial.print("[OTA] Update Start "); + Serial.println(filename); + Update.runAsync(true); + if(!Update.begin(request->contentLength(), U_FLASH)) { + Serial.println("[OTA] Update failed: not enough space"); + Update.printError(Serial); + AsyncWebServerResponse* response = request->beginResponse(500, "application/json", + "{\"status\": \"FAIL\"}"); + response->addHeader("Connection", "close"); + request->send(response); + return; + } + } + if (!Update.hasError()) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + } + } + if (final) { + if (Update.end(true)) { + if(Update.isFinished()) { + Serial.print("[OTA] Update Success with "); + Serial.print(index + len); + Serial.println("B"); + } else { + Serial.println("[OTA] Update not finished"); + } + } else { + Serial.print("[OTA] Update failed: "); + Update.printError(Serial); + } + } +} + +void NodeService::handleRestartRequest(AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse(200, "application/json", + "{\"status\": \"restarting\"}"); + response->addHeader("Connection", "close"); + request->send(response); + request->onDisconnect([]() { + Serial.println("[API] Restart device"); + delay(10); + ESP.restart(); + }); +} + +void NodeService::handleCapabilitiesRequest(AsyncWebServerRequest* request) { + JsonDocument doc; + JsonArray endpointsArr = doc["endpoints"].to(); + + // Add all registered capabilities + for (const auto& cap : ctx.capabilities) { + JsonObject obj = endpointsArr.add(); + obj["uri"] = cap.uri; + obj["method"] = ApiServer::methodToStr(cap.method); + if (!cap.params.empty()) { + JsonArray paramsArr = obj["params"].to(); + for (const auto& ps : cap.params) { + JsonObject p = paramsArr.add(); + p["name"] = ps.name; + p["location"] = ps.location; + p["required"] = ps.required; + p["type"] = ps.type; + if (!ps.values.empty()) { + JsonArray allowed = p["values"].to(); + for (const auto& v : ps.values) { + allowed.add(v); + } + } + if (ps.defaultValue.length() > 0) { + p["default"] = ps.defaultValue; + } + } + } + } + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} diff --git a/src/TaskService.cpp b/src/TaskService.cpp new file mode 100644 index 0000000..f9f6eac --- /dev/null +++ b/src/TaskService.cpp @@ -0,0 +1,123 @@ +#include "TaskService.h" +#include "ApiServer.h" +#include + +TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {} + +void TaskService::registerEndpoints(ApiServer& api) { + api.addEndpoint("/api/tasks/status", HTTP_GET, + [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }); + + api.addEndpoint("/api/tasks/control", HTTP_POST, + [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, + std::vector{ + ParamSpec{String("task"), true, String("body"), String("string"), {}}, + ParamSpec{String("action"), true, String("body"), String("string"), + {String("enable"), String("disable"), String("start"), String("stop"), String("status")}} + }); +} + +void TaskService::handleStatusRequest(AsyncWebServerRequest* request) { + JsonDocument scratch; + auto taskStatuses = taskManager.getAllTaskStatuses(scratch); + + JsonDocument doc; + JsonObject summaryObj = doc["summary"].to(); + summaryObj["totalTasks"] = taskStatuses.size(); + summaryObj["activeTasks"] = std::count_if(taskStatuses.begin(), taskStatuses.end(), + [](const auto& pair) { return pair.second["enabled"]; }); + + JsonArray tasksArr = doc["tasks"].to(); + for (const auto& taskPair : taskStatuses) { + JsonObject taskObj = tasksArr.add(); + taskObj["name"] = taskPair.first; + taskObj["interval"] = taskPair.second["interval"]; + taskObj["enabled"] = taskPair.second["enabled"]; + taskObj["running"] = taskPair.second["running"]; + taskObj["autoStart"] = taskPair.second["autoStart"]; + } + + JsonObject systemObj = doc["system"].to(); + systemObj["freeHeap"] = ESP.getFreeHeap(); + systemObj["uptime"] = millis(); + + String json; + serializeJson(doc, json); + request->send(200, "application/json", json); +} + +void TaskService::handleControlRequest(AsyncWebServerRequest* request) { + if (request->hasParam("task", true) && request->hasParam("action", true)) { + String taskName = request->getParam("task", true)->value(); + String action = request->getParam("action", true)->value(); + + bool success = false; + String message = ""; + + if (action == "enable") { + taskManager.enableTask(taskName.c_str()); + success = true; + message = "Task enabled"; + } else if (action == "disable") { + taskManager.disableTask(taskName.c_str()); + success = true; + message = "Task disabled"; + } else if (action == "start") { + taskManager.startTask(taskName.c_str()); + success = true; + message = "Task started"; + } else if (action == "stop") { + taskManager.stopTask(taskName.c_str()); + success = true; + message = "Task stopped"; + } else if (action == "status") { + success = true; + message = "Task status retrieved"; + + JsonDocument statusDoc; + statusDoc["success"] = success; + statusDoc["message"] = message; + statusDoc["task"] = taskName; + statusDoc["action"] = action; + + statusDoc["taskDetails"] = JsonObject(); + JsonObject taskDetails = statusDoc["taskDetails"]; + taskDetails["name"] = taskName; + taskDetails["enabled"] = taskManager.isTaskEnabled(taskName.c_str()); + taskDetails["running"] = taskManager.isTaskRunning(taskName.c_str()); + taskDetails["interval"] = taskManager.getTaskInterval(taskName.c_str()); + + taskDetails["system"] = JsonObject(); + JsonObject systemInfo = taskDetails["system"]; + systemInfo["freeHeap"] = ESP.getFreeHeap(); + systemInfo["uptime"] = millis(); + + String statusJson; + serializeJson(statusDoc, statusJson); + request->send(200, "application/json", statusJson); + return; + } else { + success = false; + message = "Invalid action. Use: enable, disable, start, stop, or status"; + } + + JsonDocument doc; + doc["success"] = success; + doc["message"] = message; + doc["task"] = taskName; + doc["action"] = action; + + String json; + serializeJson(doc, json); + request->send(success ? 200 : 400, "application/json", json); + } else { + JsonDocument doc; + doc["success"] = false; + doc["message"] = "Missing parameters. Required: task, action"; + doc["example"] = "{\"task\": \"discovery_send\", \"action\": \"status\"}"; + + String json; + serializeJson(doc, json); + request->send(400, "application/json", json); + } +} diff --git a/src/WifiScanService.cpp b/src/WifiScanService.cpp new file mode 100644 index 0000000..c5d410e --- /dev/null +++ b/src/WifiScanService.cpp @@ -0,0 +1,57 @@ +#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); +} -- 2.49.1 From 00aff9df3e6f95687afb213b18db70c1d2b87f74 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 12:59:37 +0200 Subject: [PATCH 5/9] refactor: implement all core services --- ctl.sh | 7 +- examples/base/main.cpp | 18 +- examples/neopattern/NeoPatternService.cpp | 187 +++++++++++ examples/neopattern/NeoPatternService.h | 34 ++ examples/neopattern/main.cpp | 225 ++----------- examples/neopixel/NeoPixelService.cpp | 291 +++++++++++++++++ examples/neopixel/NeoPixelService.h | 67 ++++ examples/neopixel/main.cpp | 369 ++-------------------- examples/relay/RelayService.cpp | 89 ++++++ examples/relay/RelayService.h | 25 ++ examples/relay/main.cpp | 107 ++----- examples/wifiscan/main.cpp | 21 +- include/NetworkManager.h | 19 ++ include/NetworkService.h | 22 ++ include/WifiScanService.h | 17 - platformio.ini | 10 +- src/NetworkManager.cpp | 8 + src/NetworkService.cpp | 132 ++++++++ src/WifiScanService.cpp | 57 ---- 19 files changed, 987 insertions(+), 718 deletions(-) create mode 100644 examples/neopattern/NeoPatternService.cpp create mode 100644 examples/neopattern/NeoPatternService.h create mode 100644 examples/neopixel/NeoPixelService.cpp create mode 100644 examples/neopixel/NeoPixelService.h create mode 100644 examples/relay/RelayService.cpp create mode 100644 examples/relay/RelayService.h create mode 100644 include/NetworkService.h delete mode 100644 include/WifiScanService.h create mode 100644 src/NetworkService.cpp delete mode 100644 src/WifiScanService.cpp 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); -} -- 2.49.1 From 2971d863d093fab855904768d934985b631d4d0f Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 13:11:32 +0200 Subject: [PATCH 6/9] refactor: service folder structure --- examples/base/main.cpp | 8 ++++---- examples/neopattern/NeoPatternService.h | 2 +- examples/neopattern/main.cpp | 8 ++++---- examples/neopixel/NeoPixelService.h | 2 +- examples/neopixel/main.cpp | 8 ++++---- examples/relay/RelayService.h | 2 +- examples/relay/main.cpp | 8 ++++---- examples/wifiscan/main.cpp | 8 ++++---- include/{ => services}/ClusterService.h | 2 +- include/{ => services}/NetworkService.h | 2 +- include/{ => services}/NodeService.h | 2 +- include/{ => services}/Service.h | 0 include/{ => services}/TaskService.h | 2 +- platformio.ini | 11 ++++++++++- src/ApiServer.cpp | 2 +- src/{ => services}/ClusterService.cpp | 2 +- src/{ => services}/NetworkService.cpp | 4 ++-- src/{ => services}/NodeService.cpp | 2 +- src/{ => services}/TaskService.cpp | 2 +- 19 files changed, 43 insertions(+), 34 deletions(-) rename include/{ => services}/ClusterService.h (92%) rename include/{ => services}/NetworkService.h (95%) rename include/{ => services}/NodeService.h (95%) rename include/{ => services}/Service.h (100%) rename include/{ => services}/TaskService.h (93%) rename src/{ => services}/ClusterService.cpp (97%) rename src/{ => services}/NetworkService.cpp (98%) rename src/{ => services}/NodeService.cpp (99%) rename src/{ => services}/TaskService.cpp (99%) diff --git a/examples/base/main.cpp b/examples/base/main.cpp index cd48d90..518e384 100644 --- a/examples/base/main.cpp +++ b/examples/base/main.cpp @@ -8,10 +8,10 @@ #include "TaskManager.h" // Services -#include "NodeService.h" -#include "NetworkService.h" -#include "ClusterService.h" -#include "TaskService.h" +#include "services/NodeService.h" +#include "services/NetworkService.h" +#include "services/ClusterService.h" +#include "services/TaskService.h" using namespace std; diff --git a/examples/neopattern/NeoPatternService.h b/examples/neopattern/NeoPatternService.h index 2407574..26d0226 100644 --- a/examples/neopattern/NeoPatternService.h +++ b/examples/neopattern/NeoPatternService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "TaskManager.h" #include "NeoPattern.cpp" #include diff --git a/examples/neopattern/main.cpp b/examples/neopattern/main.cpp index 9523c5c..3171d83 100644 --- a/examples/neopattern/main.cpp +++ b/examples/neopattern/main.cpp @@ -8,10 +8,10 @@ #include "TaskManager.h" // Services -#include "NodeService.h" -#include "NetworkService.h" -#include "ClusterService.h" -#include "TaskService.h" +#include "services/NodeService.h" +#include "services/NetworkService.h" +#include "services/ClusterService.h" +#include "services/TaskService.h" #include "NeoPatternService.h" #ifndef LED_STRIP_PIN diff --git a/examples/neopixel/NeoPixelService.h b/examples/neopixel/NeoPixelService.h index a52c355..b0aee58 100644 --- a/examples/neopixel/NeoPixelService.h +++ b/examples/neopixel/NeoPixelService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "TaskManager.h" #include #include diff --git a/examples/neopixel/main.cpp b/examples/neopixel/main.cpp index 6e0d3b0..78dc4ec 100644 --- a/examples/neopixel/main.cpp +++ b/examples/neopixel/main.cpp @@ -8,10 +8,10 @@ #include "TaskManager.h" // Services -#include "NodeService.h" -#include "NetworkService.h" -#include "ClusterService.h" -#include "TaskService.h" +#include "services/NodeService.h" +#include "services/NetworkService.h" +#include "services/ClusterService.h" +#include "services/TaskService.h" #include "NeoPixelService.h" #ifndef NEOPIXEL_PIN diff --git a/examples/relay/RelayService.h b/examples/relay/RelayService.h index b41fbc2..03913b3 100644 --- a/examples/relay/RelayService.h +++ b/examples/relay/RelayService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "TaskManager.h" #include diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 0cb0474..b342ec1 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -8,10 +8,10 @@ #include "TaskManager.h" // Services -#include "NodeService.h" -#include "NetworkService.h" -#include "ClusterService.h" -#include "TaskService.h" +#include "services/NodeService.h" +#include "services/NetworkService.h" +#include "services/ClusterService.h" +#include "services/TaskService.h" #include "RelayService.h" using namespace std; diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp index fa7a60a..c5f8651 100644 --- a/examples/wifiscan/main.cpp +++ b/examples/wifiscan/main.cpp @@ -6,10 +6,10 @@ #include "ClusterManager.h" // Services -#include "NodeService.h" -#include "ClusterService.h" -#include "TaskService.h" -#include "NetworkService.h" +#include "services/NodeService.h" +#include "services/ClusterService.h" +#include "services/TaskService.h" +#include "services/NetworkService.h" NodeContext ctx({ {"app", "network"}, diff --git a/include/ClusterService.h b/include/services/ClusterService.h similarity index 92% rename from include/ClusterService.h rename to include/services/ClusterService.h index 70b3415..41c0145 100644 --- a/include/ClusterService.h +++ b/include/services/ClusterService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "NodeContext.h" #include diff --git a/include/NetworkService.h b/include/services/NetworkService.h similarity index 95% rename from include/NetworkService.h rename to include/services/NetworkService.h index c703bd7..fa4aef8 100644 --- a/include/NetworkService.h +++ b/include/services/NetworkService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "NetworkManager.h" #include "NodeContext.h" diff --git a/include/NodeService.h b/include/services/NodeService.h similarity index 95% rename from include/NodeService.h rename to include/services/NodeService.h index f5a89ff..c113fc2 100644 --- a/include/NodeService.h +++ b/include/services/NodeService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "NodeContext.h" #include #include diff --git a/include/Service.h b/include/services/Service.h similarity index 100% rename from include/Service.h rename to include/services/Service.h diff --git a/include/TaskService.h b/include/services/TaskService.h similarity index 93% rename from include/TaskService.h rename to include/services/TaskService.h index 823c1c1..9e3791c 100644 --- a/include/TaskService.h +++ b/include/services/TaskService.h @@ -1,5 +1,5 @@ #pragma once -#include "Service.h" +#include "services/Service.h" #include "TaskManager.h" #include diff --git a/platformio.ini b/platformio.ini index dee922c..5b060cc 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] @@ -32,6 +32,7 @@ build_src_filter = + + + + + [env:d1_mini] platform = platformio/espressif8266@^4.2.1 @@ -46,6 +47,7 @@ build_src_filter = + + + + + [env:esp01_1m_relay] platform = platformio/espressif8266@^4.2.1 @@ -61,6 +63,7 @@ build_src_filter = + + + + + [env:esp01_1m_neopixel] platform = platformio/espressif8266@^4.2.1 @@ -77,6 +80,7 @@ build_src_filter = + + + + + [env:d1_mini_neopixel] platform = platformio/espressif8266@^4.2.1 @@ -92,6 +96,7 @@ build_src_filter = + + + + + [env:esp01_1m_neopattern] platform = platformio/espressif8266@^4.2.1 @@ -109,6 +114,7 @@ build_src_filter = + + + + + [env:esp01_1m_wifiscan] platform = platformio/espressif8266@^4.2.1 @@ -124,6 +130,7 @@ build_src_filter = + + + + + [env:d1_mini_wifiscan] platform = platformio/espressif8266@^4.2.1 @@ -138,6 +145,7 @@ build_src_filter = + + + + + [env:d1_mini_neopattern] platform = platformio/espressif8266@^4.2.1 @@ -153,3 +161,4 @@ build_src_filter = + + + + + diff --git a/src/ApiServer.cpp b/src/ApiServer.cpp index 3a274ab..fb3c0ee 100644 --- a/src/ApiServer.cpp +++ b/src/ApiServer.cpp @@ -1,5 +1,5 @@ #include "ApiServer.h" -#include "Service.h" +#include "services/Service.h" #include const char* ApiServer::methodToStr(int method) { diff --git a/src/ClusterService.cpp b/src/services/ClusterService.cpp similarity index 97% rename from src/ClusterService.cpp rename to src/services/ClusterService.cpp index f6fdb0c..7b88d20 100644 --- a/src/ClusterService.cpp +++ b/src/services/ClusterService.cpp @@ -1,4 +1,4 @@ -#include "ClusterService.h" +#include "services/ClusterService.h" #include "ApiServer.h" ClusterService::ClusterService(NodeContext& ctx) : ctx(ctx) {} diff --git a/src/NetworkService.cpp b/src/services/NetworkService.cpp similarity index 98% rename from src/NetworkService.cpp rename to src/services/NetworkService.cpp index 8aa6d3b..237b0a4 100644 --- a/src/NetworkService.cpp +++ b/src/services/NetworkService.cpp @@ -1,4 +1,4 @@ -#include "NetworkService.h" +#include "services/NetworkService.h" #include NetworkService::NetworkService(NetworkManager& networkManager) @@ -10,7 +10,7 @@ void NetworkService::registerEndpoints(ApiServer& api) { [this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); }, std::vector{}); - api.addEndpoint("/api/network/wifi/networks", HTTP_GET, + api.addEndpoint("/api/network/wifi/scan", HTTP_GET, [this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); }, std::vector{}); diff --git a/src/NodeService.cpp b/src/services/NodeService.cpp similarity index 99% rename from src/NodeService.cpp rename to src/services/NodeService.cpp index 6e506d7..ce420ec 100644 --- a/src/NodeService.cpp +++ b/src/services/NodeService.cpp @@ -1,4 +1,4 @@ -#include "NodeService.h" +#include "services/NodeService.h" #include "ApiServer.h" NodeService::NodeService(NodeContext& ctx) : ctx(ctx) {} diff --git a/src/TaskService.cpp b/src/services/TaskService.cpp similarity index 99% rename from src/TaskService.cpp rename to src/services/TaskService.cpp index f9f6eac..baa45f1 100644 --- a/src/TaskService.cpp +++ b/src/services/TaskService.cpp @@ -1,4 +1,4 @@ -#include "TaskService.h" +#include "services/TaskService.h" #include "ApiServer.h" #include -- 2.49.1 From 10f1e21ca7e62cb711d1262db4968a100b1b36a6 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 13:13:57 +0200 Subject: [PATCH 7/9] chore: remove obsolete examples --- examples/wifiscan/main.cpp | 50 -------------------------------------- platformio.ini | 31 ----------------------- 2 files changed, 81 deletions(-) delete mode 100644 examples/wifiscan/main.cpp diff --git a/examples/wifiscan/main.cpp b/examples/wifiscan/main.cpp deleted file mode 100644 index c5f8651..0000000 --- a/examples/wifiscan/main.cpp +++ /dev/null @@ -1,50 +0,0 @@ -#include -#include "NodeContext.h" -#include "NetworkManager.h" -#include "ApiServer.h" -#include "TaskManager.h" -#include "ClusterManager.h" - -// Services -#include "services/NodeService.h" -#include "services/ClusterService.h" -#include "services/TaskService.h" -#include "services/NetworkService.h" - -NodeContext ctx({ - {"app", "network"}, - {"role", "demo"} -}); -TaskManager taskManager(ctx); -NetworkManager networkManager(ctx); -ApiServer apiServer(ctx, taskManager); -ClusterManager cluster(ctx, taskManager); - -// Create services -NodeService nodeService(ctx); -ClusterService clusterService(ctx); -TaskService taskService(taskManager); -NetworkService networkService(networkManager); - -void setup() { - Serial.begin(115200); - - // Initialize networking - networkManager.setupWiFi(); - - taskManager.initialize(); - - // Setup API - apiServer.addService(nodeService); - apiServer.addService(clusterService); - apiServer.addService(taskService); - apiServer.addService(networkService); - apiServer.begin(); - - taskManager.printTaskStatus(); -} - -void loop() { - taskManager.execute(); - yield(); -} \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 5b060cc..5266d1b 100644 --- a/platformio.ini +++ b/platformio.ini @@ -116,37 +116,6 @@ build_src_filter = + + -[env:esp01_1m_wifiscan] -platform = platformio/espressif8266@^4.2.1 -board = esp01_1m -framework = arduino -upload_speed = 115200 -monitor_speed = 115200 -board_build.partitions = partitions_ota_1M.csv -board_build.flash_mode = dout -board_build.flash_size = 1M -lib_deps = ${common.lib_deps} -build_src_filter = - + - + - + - + - -[env:d1_mini_wifiscan] -platform = platformio/espressif8266@^4.2.1 -board = d1_mini -framework = arduino -upload_speed = 115200 -monitor_speed = 115200 -board_build.flash_mode = dio -board_build.flash_size = 4M -lib_deps = ${common.lib_deps} -build_src_filter = - + - + - + - + - [env:d1_mini_neopattern] platform = platformio/espressif8266@^4.2.1 board = d1_mini -- 2.49.1 From 84cde697725d19bc5d20a6fd75d0ec6d0a9b21bd Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 13:24:45 +0200 Subject: [PATCH 8/9] fix: task status capability --- src/services/TaskService.cpp | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/services/TaskService.cpp b/src/services/TaskService.cpp index baa45f1..52b616f 100644 --- a/src/services/TaskService.cpp +++ b/src/services/TaskService.cpp @@ -6,14 +6,28 @@ TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {} void TaskService::registerEndpoints(ApiServer& api) { api.addEndpoint("/api/tasks/status", HTTP_GET, - [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }); + [this](AsyncWebServerRequest* request) { handleStatusRequest(request); }, + std::vector{}); api.addEndpoint("/api/tasks/control", HTTP_POST, [this](AsyncWebServerRequest* request) { handleControlRequest(request); }, std::vector{ - ParamSpec{String("task"), true, String("body"), String("string"), {}}, - ParamSpec{String("action"), true, String("body"), String("string"), - {String("enable"), String("disable"), String("start"), String("stop"), String("status")}} + ParamSpec{ + String("task"), + true, + String("body"), + String("string"), + {}, + String("") + }, + ParamSpec{ + String("action"), + true, + String("body"), + String("string"), + {String("enable"), String("disable"), String("start"), String("stop"), String("status")}, + String("") + } }); } -- 2.49.1 From b4b0a7dc8409889c790b5dc4187ab4facd532cf0 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 13:32:26 +0200 Subject: [PATCH 9/9] docs: update --- .cursor/rules/cpp.mdc | 68 ++++ README.md | 6 + api/openapi.yaml | 843 ++++++++++++++++++++++++++++++++++++++++++ docs/API.md | 301 +++++++++++++++ 4 files changed, 1218 insertions(+) create mode 100644 .cursor/rules/cpp.mdc diff --git a/.cursor/rules/cpp.mdc b/.cursor/rules/cpp.mdc new file mode 100644 index 0000000..a480c7e --- /dev/null +++ b/.cursor/rules/cpp.mdc @@ -0,0 +1,68 @@ +--- +description: C++ Development Rules +globs: +alwaysApply: true +--- + +# C++ Development Rules + +You are a senior C++ developer with expertise in modern C++ (C++17/20), STL, and system-level programming. + +## Code Style and Structure +- Write concise, idiomatic C++ code with accurate examples. +- Follow modern C++ conventions and best practices. +- Use object-oriented, procedural, or functional programming patterns as appropriate. +- Leverage STL and standard algorithms for collection operations. +- Use descriptive variable and method names (e.g., 'isUserSignedIn', 'calculateTotal'). +- Structure files into headers (*.hpp) and implementation files (*.cpp) with logical separation of concerns. + +## Naming Conventions +- Use PascalCase for class names. +- Use camelCase for variable names and methods. +- Use SCREAMING_SNAKE_CASE for constants and macros. +- Prefix member variables with an underscore or m_ (e.g., `_userId`, `m_userId`). +- Use namespaces to organize code logically. +## C++ Features Usage + +- Prefer modern C++ features (e.g., auto, range-based loops, smart pointers). +- Use `std::unique_ptr` and `std::shared_ptr` for memory management. +- Prefer `std::optional`, `std::variant`, and `std::any` for type-safe alternatives. +- Use `constexpr` and `const` to optimize compile-time computations. +- Use `std::string_view` for read-only string operations to avoid unnecessary copies. + +## Syntax and Formatting +- Follow a consistent coding style, such as Google C++ Style Guide or your team’s standards. +- Place braces on the same line for control structures and methods. +- Use clear and consistent commenting practices. + +## Error Handling and Validation +- Use exceptions for error handling (e.g., `std::runtime_error`, `std::invalid_argument`). +- Use RAII for resource management to avoid memory leaks. +- Validate inputs at function boundaries. +- Log errors using a logging library (e.g., spdlog, Boost.Log). + +## Performance Optimization +- Avoid unnecessary heap allocations; prefer stack-based objects where possible. +- Use `std::move` to enable move semantics and avoid copies. +- Optimize loops with algorithms from `` (e.g., `std::sort`, `std::for_each`). +- Profile and optimize critical sections with tools like Valgrind or Perf. + +## Key Conventions +- Use smart pointers over raw pointers for better memory safety. +- Avoid global variables; use singletons sparingly. +- Use `enum class` for strongly typed enumerations. +- Separate interface from implementation in classes. +- Use templates and metaprogramming judiciously for generic solutions. + +## Security +- Use secure coding practices to avoid vulnerabilities (e.g., buffer overflows, dangling pointers). +- Prefer `std::array` or `std::vector` over raw arrays. +- Avoid C-style casts; use `static_cast`, `dynamic_cast`, or `reinterpret_cast` when necessary. +- Enforce const-correctness in functions and member variables. + +## Documentation +- Write clear comments for classes, methods, and critical logic. +- Use Doxygen for generating API documentation. +- Document assumptions, constraints, and expected behavior of code. + +Follow the official ISO C++ standards and guidelines for best practices in modern C++ development. diff --git a/README.md b/README.md index 5428fdc..97d3f1c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ SPORE is a cluster engine for ESP8266 microcontrollers that provides automatic n - **Event System**: Local and cluster-wide event publishing/subscription - **Over-The-Air Updates**: Seamless firmware updates across the cluster - **REST API**: HTTP-based cluster management and monitoring +- **Capability Discovery**: Automatic API endpoint and service capability detection ## Supported Hardware @@ -66,11 +67,15 @@ The system provides a comprehensive RESTful API for monitoring and controlling t | Endpoint | Method | Description | |----------|--------|-------------| | `/api/node/status` | GET | System resources and API endpoint registry | +| `/api/node/capabilities` | GET | API endpoint capabilities and parameters | | `/api/cluster/members` | GET | Cluster membership and health status | | `/api/node/update` | POST | OTA firmware updates | | `/api/node/restart` | POST | System restart | | `/api/tasks/status` | GET | Task management and monitoring | | `/api/tasks/control` | POST | Task control operations | +| `/api/network/status` | GET | WiFi and network status information | +| `/api/network/wifi/scan` | GET/POST | WiFi network scanning and discovery | +| `/api/network/wifi/config` | POST | WiFi configuration management | **Response Format:** All endpoints return JSON with standardized error handling and HTTP status codes. @@ -122,6 +127,7 @@ The project uses PlatformIO with Arduino framework and supports multiple ESP8266 - No persistent storage for configuration - Task monitoring and system health metrics - Task execution history and performance analytics not yet implemented +- No authentication or security features implemented ## Troubleshooting diff --git a/api/openapi.yaml b/api/openapi.yaml index ef3e105..72459eb 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -212,6 +212,9 @@ paths: sdkVersion: "3.1.2" cpuFreqMHz: 80 flashChipSize: 1048576 + labels: + location: "kitchen" + type: "sensor" api: - uri: "/api/node/status" method: 1 @@ -220,6 +223,41 @@ paths: - uri: "/api/tasks/control" method: 3 + /api/node/capabilities: + get: + summary: Get API endpoint capabilities + description: | + Returns detailed information about all available API endpoints, + including their parameters, types, and validation rules. + + tags: + - System Status + + responses: + '200': + description: Capabilities retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/CapabilitiesResponse' + examples: + default: + summary: Default response + value: + endpoints: + - uri: "/api/tasks/control" + method: "POST" + params: + - name: "task" + location: "body" + required: true + type: "string" + - name: "action" + location: "body" + required: true + type: "string" + values: ["enable", "disable", "start", "stop", "status"] + /api/cluster/members: get: summary: Get cluster membership information @@ -325,6 +363,425 @@ paths: value: status: "restarting" + /api/network/status: + get: + summary: Get network status information + description: | + Returns comprehensive WiFi and network status information + including connection details, signal strength, and mode. + + tags: + - Network Management + + responses: + '200': + description: Network status retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/NetworkStatusResponse' + examples: + default: + summary: Default response + value: + wifi: + connected: true + mode: "STA" + ssid: "MyNetwork" + ip: "192.168.1.100" + mac: "AA:BB:CC:DD:EE:FF" + hostname: "spore-node-1" + rssi: -45 + + /api/network/wifi/scan: + get: + summary: Get available WiFi networks + description: | + Returns a list of available WiFi networks discovered + during the last scan. + + tags: + - Network Management + + responses: + '200': + description: WiFi networks retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/WifiScanResponse' + examples: + default: + summary: Default response + value: + access_points: + - ssid: "MyNetwork" + rssi: -45 + channel: 6 + encryption_type: 4 + hidden: false + bssid: "AA:BB:CC:DD:EE:FF" + + post: + summary: Trigger WiFi network scan + description: | + Initiates a new WiFi network scan to discover + available networks. + + tags: + - Network Management + + responses: + '200': + description: WiFi scan initiated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/WifiScanInitResponse' + examples: + default: + summary: Scan initiated + value: + status: "scanning" + message: "WiFi scan started" + + /api/network/wifi/config: + post: + summary: Configure WiFi connection + description: | + Configures WiFi connection with new credentials and + attempts to connect to the specified network. + + tags: + - Network Management + + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - ssid + - password + properties: + ssid: + type: string + description: Network SSID + example: "MyNetwork" + password: + type: string + description: Network password + example: "mypassword" + connect_timeout_ms: + type: integer + description: Connection timeout in milliseconds + default: 10000 + example: 10000 + retry_delay_ms: + type: integer + description: Retry delay in milliseconds + default: 500 + example: 500 + + responses: + '200': + description: WiFi configuration updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/WifiConfigResponse' + examples: + default: + summary: Configuration updated + value: + status: "success" + message: "WiFi configuration updated" + connected: true + ip: "192.168.1.100" + + '400': + description: Invalid parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/neopixel/status: + get: + summary: Get NeoPixel LED strip status + description: | + Returns current NeoPixel LED strip status and configuration + including pin, count, brightness, and current pattern. + + tags: + - Hardware Services + + responses: + '200': + description: NeoPixel status retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/NeoPixelStatusResponse' + + /api/neopixel/patterns: + get: + summary: Get available LED patterns + description: | + Returns a list of available LED patterns for NeoPixel strips. + + tags: + - Hardware Services + + responses: + '200': + description: Patterns retrieved successfully + content: + application/json: + schema: + type: array + items: + type: string + example: ["off", "color_wipe", "rainbow", "rainbow_cycle", "theater_chase", "theater_chase_rainbow"] + + /api/neopixel: + post: + summary: Control NeoPixel LED strip + description: | + Controls NeoPixel LED strip patterns, colors, and brightness. + + tags: + - Hardware Services + + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + pattern: + type: string + description: Pattern name + example: "rainbow" + interval_ms: + type: integer + description: Update interval in milliseconds + example: 100 + brightness: + type: integer + minimum: 0 + maximum: 255 + description: Brightness level + example: 50 + color: + type: string + description: Primary color (hex value) + example: "0xFF0000" + r: + type: integer + minimum: 0 + maximum: 255 + description: Red component + g: + type: integer + minimum: 0 + maximum: 255 + description: Green component + b: + type: integer + minimum: 0 + maximum: 255 + description: Blue component + + responses: + '200': + description: NeoPixel control successful + content: + application/json: + schema: + $ref: '#/components/schemas/NeoPixelControlResponse' + + /api/relay/status: + get: + summary: Get relay status + description: | + Returns current relay status and configuration. + + tags: + - Hardware Services + + responses: + '200': + description: Relay status retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/RelayStatusResponse' + + /api/relay: + post: + summary: Control relay + description: | + Controls relay state (on/off/toggle). + + tags: + - Hardware Services + + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - state + properties: + state: + type: string + enum: [on, off, toggle] + description: Desired relay state + example: "on" + + responses: + '200': + description: Relay control successful + content: + application/json: + schema: + $ref: '#/components/schemas/RelayControlResponse' + + '400': + description: Invalid state parameter + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/neopattern/status: + get: + summary: Get NeoPattern service status + description: | + Returns NeoPattern service status and configuration. + + tags: + - Hardware Services + + responses: + '200': + description: NeoPattern status retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/NeoPatternStatusResponse' + + /api/neopattern/patterns: + get: + summary: Get available pattern types + description: | + Returns a list of available pattern types for NeoPattern service. + + tags: + - Hardware Services + + responses: + '200': + description: Patterns retrieved successfully + content: + application/json: + schema: + type: array + items: + type: string + example: ["off", "rainbow_cycle", "theater_chase", "color_wipe", "scanner", "fade", "fire"] + + /api/neopattern: + post: + summary: Control NeoPattern service + description: | + Controls advanced LED patterns with multiple parameters. + + tags: + - Hardware Services + + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + pattern: + type: string + description: Pattern name + example: "rainbow_cycle" + interval_ms: + type: integer + description: Update interval in milliseconds + example: 100 + brightness: + type: integer + minimum: 0 + maximum: 255 + description: Brightness level + example: 50 + color: + type: string + description: Primary color (hex value) + example: "0xFF0000" + color2: + type: string + description: Secondary color (hex value) + example: "0x0000FF" + r: + type: integer + minimum: 0 + maximum: 255 + description: Red component + g: + type: integer + minimum: 0 + maximum: 255 + description: Green component + b: + type: integer + minimum: 0 + maximum: 255 + description: Blue component + r2: + type: integer + minimum: 0 + maximum: 255 + description: Secondary red component + g2: + type: integer + minimum: 0 + maximum: 255 + description: Secondary green component + b2: + type: integer + minimum: 0 + maximum: 255 + description: Secondary blue component + total_steps: + type: integer + description: Pattern step count + example: 32 + direction: + type: string + enum: [forward, reverse] + description: Pattern direction + example: "forward" + + responses: + '200': + description: NeoPattern control successful + content: + application/json: + schema: + $ref: '#/components/schemas/NeoPatternControlResponse' + components: schemas: TaskStatusResponse: @@ -501,11 +958,393 @@ components: type: integer description: Flash chip size in bytes example: 1048576 + labels: + type: object + description: Node labels and metadata + additionalProperties: + type: string + example: + location: "kitchen" + type: "sensor" api: type: array items: $ref: '#/components/schemas/ApiEndpoint' + CapabilitiesResponse: + type: object + required: + - endpoints + properties: + endpoints: + type: array + items: + $ref: '#/components/schemas/EndpointCapability' + + EndpointCapability: + type: object + required: + - uri + - method + properties: + uri: + type: string + description: Endpoint URI path + example: "/api/tasks/control" + method: + type: string + description: HTTP method + example: "POST" + params: + type: array + items: + $ref: '#/components/schemas/ParameterSpec' + + ParameterSpec: + type: object + required: + - name + - location + - required + - type + properties: + name: + type: string + description: Parameter name + example: "task" + location: + type: string + description: Parameter location + example: "body" + required: + type: boolean + description: Whether parameter is required + example: true + type: + type: string + description: Parameter data type + example: "string" + values: + type: array + items: + type: string + description: Allowed values for enum types + example: ["enable", "disable", "start", "stop", "status"] + default: + type: string + description: Default value + example: "10000" + + NetworkStatusResponse: + type: object + required: + - wifi + properties: + wifi: + type: object + required: + - connected + - mode + properties: + connected: + type: boolean + description: Whether WiFi is connected + example: true + mode: + type: string + enum: [STA, AP] + description: WiFi mode + example: "STA" + ssid: + type: string + description: Connected network SSID + example: "MyNetwork" + ip: + type: string + format: ipv4 + description: Local IP address + example: "192.168.1.100" + mac: + type: string + description: MAC address + example: "AA:BB:CC:DD:EE:FF" + hostname: + type: string + description: Device hostname + example: "spore-node-1" + rssi: + type: integer + description: Signal strength in dBm + example: -45 + ap_ip: + type: string + format: ipv4 + description: Access point IP (if in AP mode) + example: "192.168.4.1" + ap_mac: + type: string + description: Access point MAC (if in AP mode) + example: "AA:BB:CC:DD:EE:FF" + stations_connected: + type: integer + description: Number of connected stations (if in AP mode) + example: 2 + + WifiScanResponse: + type: object + required: + - access_points + properties: + access_points: + type: array + items: + $ref: '#/components/schemas/AccessPoint' + + AccessPoint: + type: object + required: + - ssid + - rssi + - channel + - encryption_type + - hidden + - bssid + properties: + ssid: + type: string + description: Network name + example: "MyNetwork" + rssi: + type: integer + description: Signal strength in dBm + example: -45 + channel: + type: integer + description: WiFi channel + example: 6 + encryption_type: + type: integer + description: Security type + example: 4 + hidden: + type: boolean + description: Whether network is hidden + example: false + bssid: + type: string + description: Network MAC address + example: "AA:BB:CC:DD:EE:FF" + + WifiScanInitResponse: + type: object + required: + - status + - message + properties: + status: + type: string + description: Scan status + example: "scanning" + message: + type: string + description: Status message + example: "WiFi scan started" + + WifiConfigResponse: + type: object + required: + - status + - message + - connected + properties: + status: + type: string + description: Configuration status + example: "success" + message: + type: string + description: Status message + example: "WiFi configuration updated" + connected: + type: boolean + description: Whether connection was successful + example: true + ip: + type: string + format: ipv4 + description: Assigned IP address (if connected) + example: "192.168.1.100" + + NeoPixelStatusResponse: + type: object + required: + - pin + - count + - interval_ms + - brightness + - pattern + properties: + pin: + type: integer + description: GPIO pin number + example: 2 + count: + type: integer + description: Number of LEDs in strip + example: 16 + interval_ms: + type: integer + description: Update interval in milliseconds + example: 100 + brightness: + type: integer + minimum: 0 + maximum: 255 + description: Current brightness level + example: 50 + pattern: + type: string + description: Current pattern name + example: "rainbow" + + NeoPixelControlResponse: + type: object + required: + - ok + - pattern + - interval_ms + - brightness + properties: + ok: + type: boolean + description: Whether control was successful + example: true + pattern: + type: string + description: Current pattern name + example: "rainbow" + interval_ms: + type: integer + description: Update interval in milliseconds + example: 100 + brightness: + type: integer + description: Current brightness level + example: 50 + + RelayStatusResponse: + type: object + required: + - pin + - state + - uptime + properties: + pin: + type: integer + description: GPIO pin number + example: 5 + state: + type: string + enum: [on, off] + description: Current relay state + example: "off" + uptime: + type: integer + description: System uptime in milliseconds + example: 12345 + + RelayControlResponse: + type: object + required: + - success + - state + properties: + success: + type: boolean + description: Whether control was successful + example: true + state: + type: string + enum: [on, off] + description: Current relay state + example: "on" + message: + type: string + description: Error message (if unsuccessful) + example: "Invalid state. Use: on, off, or toggle" + + NeoPatternStatusResponse: + type: object + required: + - pin + - count + - interval_ms + - brightness + - pattern + - total_steps + - color1 + - color2 + properties: + pin: + type: integer + description: GPIO pin number + example: 2 + count: + type: integer + description: Number of LEDs + example: 16 + interval_ms: + type: integer + description: Update interval in milliseconds + example: 100 + brightness: + type: integer + minimum: 0 + maximum: 255 + description: Current brightness level + example: 50 + pattern: + type: string + description: Current pattern name + example: "rainbow_cycle" + total_steps: + type: integer + description: Pattern step count + example: 32 + color1: + type: integer + description: Primary color value + example: 16711680 + color2: + type: integer + description: Secondary color value + example: 255 + + NeoPatternControlResponse: + type: object + required: + - ok + - pattern + - interval_ms + - brightness + properties: + ok: + type: boolean + description: Whether control was successful + example: true + pattern: + type: string + description: Current pattern name + example: "rainbow_cycle" + interval_ms: + type: integer + description: Update interval in milliseconds + example: 100 + brightness: + type: integer + description: Current brightness level + example: 50 + ApiEndpoint: type: object required: @@ -663,6 +1502,10 @@ tags: description: Multi-node cluster coordination and health monitoring - name: System Management description: System-level operations like updates and restarts + - name: Network Management + description: WiFi and network configuration and monitoring + - name: Hardware Services + description: Hardware control services for LEDs, relays, and other peripherals externalDocs: description: SPORE Project Documentation diff --git a/docs/API.md b/docs/API.md index 28fa33c..415c011 100644 --- a/docs/API.md +++ b/docs/API.md @@ -16,10 +16,33 @@ The SPORE system provides a comprehensive RESTful API for monitoring and control | Endpoint | Method | Description | Response | |----------|--------|-------------|----------| | `/api/node/status` | GET | System resource information and API endpoint registry | System metrics and API catalog | +| `/api/node/capabilities` | GET | API endpoint capabilities and parameters | Detailed endpoint specifications | | `/api/cluster/members` | GET | Cluster membership and node health information | Cluster topology and health status | | `/api/node/update` | POST | Handle firmware updates via OTA | Update progress and status | | `/api/node/restart` | POST | Trigger system restart | Restart confirmation | +### Network Management API + +| Endpoint | Method | Description | Response | +|----------|--------|-------------|----------| +| `/api/network/status` | GET | WiFi and network status information | Network configuration and status | +| `/api/network/wifi/scan` | GET | Get available WiFi networks | List of discovered networks | +| `/api/network/wifi/scan` | POST | Trigger WiFi network scan | Scan initiation confirmation | +| `/api/network/wifi/config` | POST | Configure WiFi connection | Connection status and result | + +### Hardware Services API + +| Endpoint | Method | Description | Response | +|----------|--------|-------------|----------| +| `/api/neopixel/status` | GET | NeoPixel LED strip status | LED configuration and state | +| `/api/neopixel` | POST | NeoPixel pattern and color control | Control confirmation | +| `/api/neopixel/patterns` | GET | Available LED patterns | List of supported patterns | +| `/api/relay/status` | GET | Relay state and configuration | Relay pin and state | +| `/api/relay` | POST | Relay control (on/off/toggle) | Control result | +| `/api/neopattern/status` | GET | NeoPattern service status | Pattern configuration | +| `/api/neopattern` | POST | Advanced LED pattern control | Control confirmation | +| `/api/neopattern/patterns` | GET | Available pattern types | List of supported patterns | + ## Detailed API Reference ### Task Management @@ -125,8 +148,67 @@ Returns comprehensive system resource information including memory usage, chip d - `sdkVersion`: ESP8266 SDK version - `cpuFreqMHz`: CPU frequency in MHz - `flashChipSize`: Flash chip size in bytes +- `labels`: Node labels and metadata (if available) - `api`: Array of registered API endpoints +**Example Response:** +```json +{ + "freeHeap": 48748, + "chipId": 12345678, + "sdkVersion": "3.1.2", + "cpuFreqMHz": 80, + "flashChipSize": 1048576, + "labels": { + "location": "kitchen", + "type": "sensor" + } +} +``` + +#### GET /api/node/capabilities + +Returns detailed information about all available API endpoints, including their parameters, types, and validation rules. + +**Response Fields:** +- `endpoints[]`: Array of endpoint capability objects +- `uri`: Endpoint URI path +- `method`: HTTP method (GET, POST, etc.) +- `params[]`: Parameter specifications (if applicable) +- `name`: Parameter name +- `location`: Parameter location (body, query, etc.) +- `required`: Whether parameter is required +- `type`: Parameter data type +- `values[]`: Allowed values (for enums) +- `default`: Default value + +**Example Response:** +```json +{ + "endpoints": [ + { + "uri": "/api/tasks/control", + "method": "POST", + "params": [ + { + "name": "task", + "location": "body", + "required": true, + "type": "string" + }, + { + "name": "action", + "location": "body", + "required": true, + "type": "string", + "values": ["enable", "disable", "start", "stop", "status"] + } + ] + } + ] +} +``` + #### GET /api/cluster/members Returns information about all nodes in the cluster, including their health status, resources, and API endpoints. @@ -154,6 +236,225 @@ Initiates an over-the-air firmware update. The firmware file should be uploaded Triggers a system restart. The response will be sent before the restart occurs. +### Network Management + +#### GET /api/network/status + +Returns comprehensive WiFi and network status information. + +**Response Fields:** +- `wifi.connected`: Whether WiFi is connected +- `wifi.mode`: WiFi mode (STA or AP) +- `wifi.ssid`: Connected network SSID +- `wifi.ip`: Local IP address +- `wifi.mac`: MAC address +- `wifi.hostname`: Device hostname +- `wifi.rssi`: Signal strength +- `wifi.ap_ip`: Access point IP (if in AP mode) +- `wifi.ap_mac`: Access point MAC (if in AP mode) +- `wifi.stations_connected`: Number of connected stations (if in AP mode) + +**Example Response:** +```json +{ + "wifi": { + "connected": true, + "mode": "STA", + "ssid": "MyNetwork", + "ip": "192.168.1.100", + "mac": "AA:BB:CC:DD:EE:FF", + "hostname": "spore-node-1", + "rssi": -45 + } +} +``` + +#### GET /api/network/wifi/scan + +Returns a list of available WiFi networks discovered during the last scan. + +**Response Fields:** +- `access_points[]`: Array of discovered networks +- `ssid`: Network name +- `rssi`: Signal strength +- `channel`: WiFi channel +- `encryption_type`: Security type +- `hidden`: Whether network is hidden +- `bssid`: Network MAC address + +**Example Response:** +```json +{ + "access_points": [ + { + "ssid": "MyNetwork", + "rssi": -45, + "channel": 6, + "encryption_type": 4, + "hidden": false, + "bssid": "AA:BB:CC:DD:EE:FF" + } + ] +} +``` + +#### POST /api/network/wifi/scan + +Initiates a new WiFi network scan. + +**Response:** +```json +{ + "status": "scanning", + "message": "WiFi scan started" +} +``` + +#### POST /api/network/wifi/config + +Configures WiFi connection with new credentials. + +**Parameters:** +- `ssid` (required): Network SSID +- `password` (required): Network password +- `connect_timeout_ms` (optional): Connection timeout in milliseconds (default: 10000) +- `retry_delay_ms` (optional): Retry delay in milliseconds (default: 500) + +**Response:** +```json +{ + "status": "success", + "message": "WiFi configuration updated", + "connected": true, + "ip": "192.168.1.100" +} +``` + +### Hardware Services + +#### NeoPixel LED Control + +##### GET /api/neopixel/status + +Returns current NeoPixel LED strip status and configuration. + +**Response Fields:** +- `pin`: GPIO pin number +- `count`: Number of LEDs in strip +- `interval_ms`: Update interval in milliseconds +- `brightness`: Current brightness (0-255) +- `pattern`: Current pattern name + +**Example Response:** +```json +{ + "pin": 2, + "count": 16, + "interval_ms": 100, + "brightness": 50, + "pattern": "rainbow" +} +``` + +##### GET /api/neopixel/patterns + +Returns list of available LED patterns. + +**Response:** +```json +["off", "color_wipe", "rainbow", "rainbow_cycle", "theater_chase", "theater_chase_rainbow"] +``` + +##### POST /api/neopixel + +Controls NeoPixel LED strip patterns and colors. + +**Parameters:** +- `pattern` (optional): Pattern name +- `interval_ms` (optional): Update interval in milliseconds +- `brightness` (optional): Brightness level (0-255) +- `color` (optional): Primary color (hex value) +- `r`, `g`, `b` (optional): RGB color values +- `color2` (optional): Secondary color (hex value) +- `r2`, `g2`, `b2` (optional): Secondary RGB values + +**Example Request:** +```bash +curl -X POST http://192.168.1.100/api/neopixel \ + -d "pattern=rainbow&brightness=100&interval_ms=50" +``` + +#### Relay Control + +##### GET /api/relay/status + +Returns current relay status and configuration. + +**Response Fields:** +- `pin`: GPIO pin number +- `state`: Current state (on/off) +- `uptime`: System uptime in milliseconds + +**Example Response:** +```json +{ + "pin": 5, + "state": "off", + "uptime": 12345 +} +``` + +##### POST /api/relay + +Controls relay state. + +**Parameters:** +- `state` (required): Desired state (on, off, toggle) + +**Example Request:** +```bash +curl -X POST http://192.168.1.100/api/relay \ + -d "state=on" +``` + +#### NeoPattern Service + +##### GET /api/neopattern/status + +Returns NeoPattern service status and configuration. + +**Response Fields:** +- `pin`: GPIO pin number +- `count`: Number of LEDs +- `interval_ms`: Update interval +- `brightness`: Current brightness +- `pattern`: Current pattern name +- `total_steps`: Pattern step count +- `color1`: Primary color +- `color2`: Secondary color + +##### GET /api/neopattern/patterns + +Returns available pattern types. + +**Response:** +```json +["off", "rainbow_cycle", "theater_chase", "color_wipe", "scanner", "fade", "fire"] +``` + +##### POST /api/neopattern + +Controls advanced LED patterns. + +**Parameters:** +- `pattern` (optional): Pattern name +- `interval_ms` (optional): Update interval +- `brightness` (optional): Brightness level +- `color`, `color2` (optional): Color values +- `r`, `g`, `b`, `r2`, `g2`, `b2` (optional): RGB values +- `total_steps` (optional): Pattern step count +- `direction` (optional): Pattern direction (forward/reverse) + ## HTTP Status Codes | Code | Description | Use Case | -- 2.49.1