From d7e98a41fac7cc090c05a9b8773de145e7587283 Mon Sep 17 00:00:00 2001 From: master Date: Thu, 28 Aug 2025 11:17:24 +0200 Subject: [PATCH] feature/capabilities (#1) Reviewed-on: https://git.dcentral.systems/iot/spore/pulls/1 --- examples/relay/main.cpp | 2 +- include/ApiServer.h | 25 +++++++++- src/ApiServer.cpp | 103 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 127 insertions(+), 3 deletions(-) diff --git a/examples/relay/main.cpp b/examples/relay/main.cpp index 5bd1ac8..25fa5d9 100644 --- a/examples/relay/main.cpp +++ b/examples/relay/main.cpp @@ -59,7 +59,7 @@ public: 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() { diff --git a/include/ApiServer.h b/include/ApiServer.h index eb02fd9..7bf88ae 100644 --- a/include/ApiServer.h +++ b/include/ApiServer.h @@ -21,19 +21,42 @@ public: 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 + }; + 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); private: AsyncWebServer server; NodeContext& ctx; TaskManager& taskManager; std::vector> serviceRegistry; + std::vector capabilityRegistry; void onClusterMembersRequest(AsyncWebServerRequest *request); void methodToStr(const std::tuple &endpoint, ArduinoJson::V742PB22::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); }; diff --git a/src/ApiServer.cpp b/src/ApiServer.cpp index 625b0bf..e7059f8 100644 --- a/src/ApiServer.cpp +++ b/src/ApiServer.cpp @@ -27,6 +27,34 @@ void ApiServer::addEndpoint(const String& uri, int method, std::function requestHandler, + const std::vector& params) { + capabilityRegistry.push_back(EndpointCapability{uri, method, params}); + serviceRegistry.push_back(std::make_tuple(uri, method)); + if (ctx.memberList && !ctx.memberList->empty()) { + auto it = ctx.memberList->find(ctx.hostname); + if (it != ctx.memberList->end()) { + it->second.apiEndpoints.push_back(std::make_tuple(uri, method)); + } + } + server.on(uri.c_str(), method, requestHandler); +} + +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}); + serviceRegistry.push_back(std::make_tuple(uri, method)); + if (ctx.memberList && !ctx.memberList->empty()) { + auto it = ctx.memberList->find(ctx.hostname); + if (it != ctx.memberList->end()) { + it->second.apiEndpoints.push_back(std::make_tuple(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)); @@ -38,12 +66,21 @@ void ApiServer::begin() { ); addEndpoint("/api/node/restart", HTTP_POST, std::bind(&ApiServer::onRestartRequest, this, std::placeholders::_1)); + + // 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::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")}} + } + ); server.begin(); } @@ -296,4 +333,68 @@ void ApiServer::onTaskControlRequest(AsyncWebServerRequest *request) { 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; + }; + auto methodStrFromInt = [](int method) -> const char* { + switch (method) { + case HTTP_GET: return "GET"; + case HTTP_POST: return "POST"; + case HTTP_PUT: return "PUT"; + case HTTP_DELETE: return "DELETE"; + case HTTP_PATCH: return "PATCH"; + default: return "UNKNOWN"; + } + }; + + // 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); + } + } + } + } + } + + // 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