From 995d9730116aca4b5b7ee8ec1437eaa0d9cd75f2 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Sat, 13 Sep 2025 12:35:52 +0200 Subject: [PATCH] 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); +}