feat: services (#2)

This commit is contained in:
2025-09-13 13:45:24 +02:00
parent fe045804cb
commit 12caeb0be6
33 changed files with 3293 additions and 785 deletions

View File

@@ -1,8 +1,8 @@
#include "ApiServer.h"
#include "services/Service.h"
#include <algorithm>
// 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<void(As
// Overloads that also record minimal capability specs
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& 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,343 +47,22 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function<void(As
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const std::vector<ParamSpec>& 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>{
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<JsonArray>();
for (const auto& entry : serviceRegistry) {
JsonObject apiObj = apiArr.add<JsonObject>();
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<JsonObject>();
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<JsonObject>();
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<JsonArray>();
for (const auto& pair : *ctx.memberList) {
const NodeInfo& node = pair.second;
JsonObject obj = arr.add<JsonObject>();
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<JsonArray>();
for (const auto& endpoint : node.apiEndpoints) {
JsonObject apiObj = apiArr.add<JsonObject>();
apiObj["uri"] = std::get<0>(endpoint);
methodToStr(endpoint, apiObj);
}
// Add labels if present
if (!node.labels.empty()) {
JsonObject labelsObj = obj["labels"].to<JsonObject>();
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<String, int> &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<JsonObject>();
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<JsonArray>();
for (const auto& taskPair : taskStatuses) {
JsonObject taskObj = tasksArr.add<JsonObject>();
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<JsonObject>();
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<JsonArray>();
// Track seen (uri|method) to avoid duplicates
std::vector<String> 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<JsonObject>();
obj["uri"] = cap.uri;
obj["method"] = methodStrFromInt(cap.method);
if (!cap.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& ps : cap.params) {
JsonObject p = paramsArr.add<JsonObject>();
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<JsonArray>();
for (const auto& v : ps.values) {
allowed.add(v);
}
}
}
}
}
// 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<JsonObject>();
obj["uri"] = uri;
obj["method"] = methodStrFromInt(method);
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -2,8 +2,69 @@
// 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<AccessPoint> NetworkManager::getAccessPoints() const {
return accessPoints;
}
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);

View File

@@ -0,0 +1,42 @@
#include "services/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<ParamSpec>{});
}
void ClusterService::handleMembersRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray arr = doc["members"].to<JsonArray>();
for (const auto& pair : *ctx.memberList) {
const NodeInfo& node = pair.second;
JsonObject obj = arr.add<JsonObject>();
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<JsonObject>();
for (const auto& kv : node.labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
}

View File

@@ -0,0 +1,132 @@
#include "services/NetworkService.h"
#include <ArduinoJson.h>
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<ParamSpec>{});
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
std::vector<ParamSpec>{});
// Network status and configuration endpoints
api.addEndpoint("/api/network/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
std::vector<ParamSpec>{
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<JsonArray>();
for (const auto& ap : accessPoints) {
JsonObject apObj = doc["access_points"].add<JsonObject>();
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);
}

View File

@@ -0,0 +1,157 @@
#include "services/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<ParamSpec>{});
// 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>{
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<ParamSpec>{});
// Capabilities endpoint
api.addEndpoint("/api/node/capabilities", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleCapabilitiesRequest(request); },
std::vector<ParamSpec>{});
}
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<JsonObject>();
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<JsonObject>();
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<JsonArray>();
// Add all registered capabilities
for (const auto& cap : ctx.capabilities) {
JsonObject obj = endpointsArr.add<JsonObject>();
obj["uri"] = cap.uri;
obj["method"] = ApiServer::methodToStr(cap.method);
if (!cap.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& ps : cap.params) {
JsonObject p = paramsArr.add<JsonObject>();
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<JsonArray>();
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);
}

View File

@@ -0,0 +1,137 @@
#include "services/TaskService.h"
#include "ApiServer.h"
#include <algorithm>
TaskService::TaskService(TaskManager& taskManager) : taskManager(taskManager) {}
void TaskService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/tasks/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
api.addEndpoint("/api/tasks/control", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
std::vector<ParamSpec>{
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("")
}
});
}
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
JsonDocument scratch;
auto taskStatuses = taskManager.getAllTaskStatuses(scratch);
JsonDocument doc;
JsonObject summaryObj = doc["summary"].to<JsonObject>();
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<JsonArray>();
for (const auto& taskPair : taskStatuses) {
JsonObject taskObj = tasksArr.add<JsonObject>();
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<JsonObject>();
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);
}
}