Compare commits

3 Commits

Author SHA1 Message Date
025ac7b810 fix: endpoints result 2025-09-23 21:29:18 +02:00
a9f56c1279 fix: tasks endpoint response 2025-09-23 21:03:13 +02:00
594b5e3af6 feat: implement complete JSON serialization system with response classes
- Add abstract JsonSerializable base class with toJson/fromJson methods
- Create comprehensive response classes for complete JSON document handling:
  * ClusterMembersResponse for cluster member data
  * TaskStatusResponse and TaskControlResponse for task operations
  * NodeStatusResponse, NodeEndpointsResponse, NodeOperationResponse for node data
- Implement concrete serializable classes for all data types:
  * NodeInfoSerializable, TaskInfoSerializable, SystemInfoSerializable
  * TaskSummarySerializable, EndpointInfoSerializable
- Refactor all service classes to use new serialization system
- Reduce service method complexity from 20-30 lines to 2-3 lines
- Eliminate manual JsonDocument creation and field mapping
- Ensure type safety and compile-time validation
- Maintain backward compatibility while improving maintainability

Breaking change: Service classes now use response objects instead of manual JSON creation
2025-09-22 21:55:25 +02:00
15 changed files with 915 additions and 199 deletions

View File

@@ -11,6 +11,7 @@
#include "spore/types/NodeInfo.h"
#include "spore/core/TaskManager.h"
#include "spore/types/ApiTypes.h"
#include "spore/util/Logging.h"
class Service; // Forward declaration
@@ -19,15 +20,17 @@ 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<void(AsyncWebServerRequest*)> requestHandler);
void 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);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params);
const String& serviceName = "unknown");
void 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);
const String& serviceName = "unknown");
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const std::vector<ParamSpec>& params, const String& serviceName = "unknown");
void 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, const String& serviceName = "unknown");
// Static file serving
void serveStatic(const String& uri, fs::FS& fs, const String& path, const String& cache_header = "");

View File

@@ -0,0 +1,114 @@
#pragma once
#include "spore/util/JsonSerializable.h"
#include "spore/util/Logging.h"
#include <ArduinoJson.h>
namespace spore {
namespace types {
/**
* Base class for API responses that can be serialized to JSON
* Handles complete JsonDocument creation and serialization
*/
class ApiResponse {
protected:
mutable JsonDocument doc;
public:
virtual ~ApiResponse() = default;
/**
* Get the complete JSON string representation of this response
* @return JSON string ready to send as HTTP response
*/
virtual String toJsonString() const {
String json;
serializeJson(doc, json);
return json;
}
/**
* Get the JsonDocument for direct manipulation if needed
* @return Reference to the internal JsonDocument
*/
JsonDocument& getDocument() { return doc; }
const JsonDocument& getDocument() const { return doc; }
/**
* Clear the document and reset for reuse
*/
virtual void clear() {
doc.clear();
}
};
/**
* Base class for API responses that contain a collection of serializable items
*/
template<typename ItemType>
class CollectionResponse : public ApiResponse {
protected:
String collectionKey;
public:
explicit CollectionResponse(const String& key) : collectionKey(key) {}
/**
* Add a serializable item to the collection
* @param item The serializable item to add
*/
void addItem(const util::JsonSerializable& item) {
// Ensure the array exists and get a reference to it
if (!doc[collectionKey].is<JsonArray>()) {
doc[collectionKey] = JsonArray();
}
JsonArray arr = doc[collectionKey].as<JsonArray>();
JsonObject obj = arr.add<JsonObject>();
item.toJson(obj);
}
/**
* Add a serializable item to the collection (move version)
* @param item The serializable item to add
*/
void addItem(util::JsonSerializable&& item) {
// Ensure the array exists and get a reference to it
if (!doc[collectionKey].is<JsonArray>()) {
doc[collectionKey] = JsonArray();
}
JsonArray arr = doc[collectionKey].as<JsonArray>();
JsonObject obj = arr.add<JsonObject>();
item.toJson(obj);
}
/**
* Add multiple items from a container
* @param items Container of serializable items
*/
template<typename Container>
void addItems(const Container& items) {
// Ensure the array exists and get a reference to it
if (!doc[collectionKey].is<JsonArray>()) {
doc[collectionKey] = JsonArray();
}
JsonArray arr = doc[collectionKey].as<JsonArray>();
for (const auto& item : items) {
JsonObject obj = arr.add<JsonObject>();
item.toJson(obj);
}
}
/**
* Get the current number of items in the collection
* @return Number of items
*/
size_t getItemCount() const {
if (doc[collectionKey].is<JsonArray>()) {
return doc[collectionKey].as<JsonArray>().size();
}
return 0;
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,47 @@
#pragma once
#include "ApiResponse.h"
#include "NodeInfoSerializable.h"
#include <map>
namespace spore {
namespace types {
/**
* Response class for cluster members endpoint
* Handles complete JSON document creation for cluster member data
*/
class ClusterMembersResponse : public CollectionResponse<NodeInfoSerializable> {
public:
ClusterMembersResponse() : CollectionResponse("members") {}
/**
* Add a single node to the response
* @param node The NodeInfo to add
*/
void addNode(const NodeInfo& node) {
addItem(NodeInfoSerializable(const_cast<NodeInfo&>(node)));
}
/**
* Add multiple nodes from a member list
* @param memberList Map of hostname to NodeInfo
*/
void addNodes(const std::map<String, NodeInfo>& memberList) {
for (const auto& pair : memberList) {
addNode(pair.second);
}
}
/**
* Add nodes from a pointer to member list
* @param memberList Pointer to map of hostname to NodeInfo
*/
void addNodes(const std::map<String, NodeInfo>* memberList) {
if (memberList) {
addNodes(*memberList);
}
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,76 @@
#pragma once
#include "ApiTypes.h"
#include "spore/util/JsonSerializable.h"
#include <ArduinoJson.h>
namespace spore {
namespace types {
/**
* Serializable wrapper for EndpointInfo that implements JsonSerializable interface
* Handles conversion between EndpointInfo struct and JSON representation
*/
class EndpointInfoSerializable : public util::JsonSerializable {
private:
const EndpointInfo& endpoint;
public:
explicit EndpointInfoSerializable(const EndpointInfo& ep) : endpoint(ep) {}
/**
* Serialize EndpointInfo to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["uri"] = endpoint.uri;
obj["method"] = endpoint.method;
// Add parameters if present
if (!endpoint.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& param : endpoint.params) {
JsonObject paramObj = paramsArr.add<JsonObject>();
paramObj["name"] = param.name;
paramObj["location"] = param.location;
paramObj["required"] = param.required;
paramObj["type"] = param.type;
if (!param.values.empty()) {
JsonArray valuesArr = paramObj["values"].to<JsonArray>();
for (const auto& value : param.values) {
valuesArr.add(value);
}
}
if (param.defaultValue.length() > 0) {
paramObj["default"] = param.defaultValue;
}
}
}
}
/**
* Deserialize EndpointInfo from JsonObject
*/
void fromJson(const JsonObject& obj) override {
// Note: This would require modifying the EndpointInfo struct to be mutable
// For now, this is a placeholder as EndpointInfo is typically read-only
// in the context where this serialization is used
}
};
/**
* Convenience function to create a JsonArray from a collection of EndpointInfo objects
*/
template<typename Container>
JsonArray endpointInfoToJsonArray(JsonDocument& doc, const Container& endpoints) {
JsonArray arr = doc.to<JsonArray>();
for (const auto& endpoint : endpoints) {
EndpointInfoSerializable serializable(endpoint);
JsonObject obj = arr.add<JsonObject>();
serializable.toJson(obj);
}
return arr;
}
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,141 @@
#pragma once
#include "NodeInfo.h"
#include "ApiTypes.h"
#include "spore/util/JsonSerializable.h"
#include <ArduinoJson.h>
namespace spore {
namespace types {
/**
* Serializable wrapper for NodeInfo that implements JsonSerializable interface
* Handles conversion between NodeInfo struct and JSON representation
*/
class NodeInfoSerializable : public util::JsonSerializable {
private:
NodeInfo& nodeInfo;
public:
explicit NodeInfoSerializable(NodeInfo& node) : nodeInfo(node) {}
/**
* Serialize NodeInfo to JsonObject
* Maps all NodeInfo fields to appropriate JSON structure
*/
void toJson(JsonObject& obj) const override {
obj["hostname"] = nodeInfo.hostname;
obj["ip"] = nodeInfo.ip.toString();
obj["lastSeen"] = nodeInfo.lastSeen;
obj["latency"] = nodeInfo.latency;
obj["status"] = statusToStr(nodeInfo.status);
// Serialize resources
JsonObject resources = obj["resources"].to<JsonObject>();
resources["freeHeap"] = nodeInfo.resources.freeHeap;
resources["chipId"] = nodeInfo.resources.chipId;
resources["sdkVersion"] = nodeInfo.resources.sdkVersion;
resources["cpuFreqMHz"] = nodeInfo.resources.cpuFreqMHz;
resources["flashChipSize"] = nodeInfo.resources.flashChipSize;
// Serialize labels if present
if (!nodeInfo.labels.empty()) {
JsonObject labels = obj["labels"].to<JsonObject>();
for (const auto& kv : nodeInfo.labels) {
labels[kv.first.c_str()] = kv.second;
}
}
// Serialize endpoints if present
if (!nodeInfo.endpoints.empty()) {
JsonArray endpoints = obj["api"].to<JsonArray>();
for (const auto& endpoint : nodeInfo.endpoints) {
JsonObject endpointObj = endpoints.add<JsonObject>();
endpointObj["uri"] = endpoint.uri;
endpointObj["method"] = endpoint.method;
}
}
}
/**
* Deserialize NodeInfo from JsonObject
* Populates NodeInfo fields from JSON structure
*/
void fromJson(const JsonObject& obj) override {
nodeInfo.hostname = obj["hostname"].as<String>();
// Parse IP address
const char* ipStr = obj["ip"];
if (ipStr) {
nodeInfo.ip.fromString(ipStr);
}
nodeInfo.lastSeen = obj["lastSeen"].as<unsigned long>();
nodeInfo.latency = obj["latency"].as<unsigned long>();
// Parse status
const char* statusStr = obj["status"];
if (statusStr) {
if (strcmp(statusStr, "ACTIVE") == 0) {
nodeInfo.status = NodeInfo::ACTIVE;
} else if (strcmp(statusStr, "INACTIVE") == 0) {
nodeInfo.status = NodeInfo::INACTIVE;
} else if (strcmp(statusStr, "DEAD") == 0) {
nodeInfo.status = NodeInfo::DEAD;
}
}
// Parse resources
if (obj["resources"].is<JsonObject>()) {
JsonObject resources = obj["resources"].as<JsonObject>();
nodeInfo.resources.freeHeap = resources["freeHeap"].as<uint32_t>();
nodeInfo.resources.chipId = resources["chipId"].as<uint32_t>();
nodeInfo.resources.sdkVersion = resources["sdkVersion"].as<String>();
nodeInfo.resources.cpuFreqMHz = resources["cpuFreqMHz"].as<uint32_t>();
nodeInfo.resources.flashChipSize = resources["flashChipSize"].as<uint32_t>();
}
// Parse labels
nodeInfo.labels.clear();
if (obj["labels"].is<JsonObject>()) {
JsonObject labels = obj["labels"].as<JsonObject>();
for (JsonPair kvp : labels) {
nodeInfo.labels[kvp.key().c_str()] = kvp.value().as<String>();
}
}
// Parse endpoints
nodeInfo.endpoints.clear();
if (obj["api"].is<JsonArray>()) {
JsonArray endpoints = obj["api"].as<JsonArray>();
for (JsonObject endpointObj : endpoints) {
EndpointInfo endpoint;
endpoint.uri = endpointObj["uri"].as<String>();
endpoint.method = endpointObj["method"].as<int>();
endpoint.isLocal = false;
endpoint.serviceName = "remote";
nodeInfo.endpoints.push_back(std::move(endpoint));
}
}
}
};
/**
* Convenience function to create a JsonArray from a collection of NodeInfo objects
* @param doc The JsonDocument to create the array in
* @param nodes Collection of NodeInfo objects
* @return A JsonArray containing all serialized NodeInfo objects
*/
template<typename Container>
JsonArray nodeInfoToJsonArray(JsonDocument& doc, const Container& nodes) {
JsonArray arr = doc.to<JsonArray>();
for (const auto& pair : nodes) {
const NodeInfo& node = pair.second;
NodeInfoSerializable serializable(const_cast<NodeInfo&>(node));
JsonObject obj = arr.add<JsonObject>();
serializable.toJson(obj);
}
return arr;
}
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,102 @@
#pragma once
#include "ApiResponse.h"
#include "EndpointInfoSerializable.h"
#include "NodeInfo.h"
#include "spore/util/Logging.h"
#include <vector>
namespace spore {
namespace types {
/**
* Response class for node status endpoint
* Handles complete JSON document creation for node status data
*/
class NodeStatusResponse : public ApiResponse {
public:
/**
* Set basic system information
*/
void setSystemInfo() {
doc["freeHeap"] = ESP.getFreeHeap();
doc["chipId"] = ESP.getChipId();
doc["sdkVersion"] = ESP.getSdkVersion();
doc["cpuFreqMHz"] = ESP.getCpuFreqMHz();
doc["flashChipSize"] = ESP.getFlashChipSize();
}
/**
* Add labels to the response
* @param labels Map of label key-value pairs
*/
void addLabels(const std::map<String, String>& labels) {
if (!labels.empty()) {
JsonObject labelsObj = doc["labels"].to<JsonObject>();
for (const auto& kv : labels) {
labelsObj[kv.first.c_str()] = kv.second;
}
}
}
/**
* Build complete response with system info and labels
* @param labels Optional labels to include
*/
void buildCompleteResponse(const std::map<String, String>& labels = {}) {
setSystemInfo();
addLabels(labels);
}
};
/**
* Response class for node endpoints endpoint
* Handles complete JSON document creation for endpoint data
*/
class NodeEndpointsResponse : public CollectionResponse<EndpointInfoSerializable> {
public:
NodeEndpointsResponse() : CollectionResponse("endpoints") {}
/**
* Add a single endpoint to the response
* @param endpoint The EndpointInfo to add
*/
void addEndpoint(const EndpointInfo& endpoint) {
addItem(EndpointInfoSerializable(endpoint));
}
/**
* Add multiple endpoints from a container
* @param endpoints Container of EndpointInfo objects
*/
void addEndpoints(const std::vector<EndpointInfo>& endpoints) {
for (const auto& endpoint : endpoints) {
addEndpoint(endpoint);
}
}
};
/**
* Response class for simple status operations (update, restart)
* Handles simple JSON responses for node operations
*/
class NodeOperationResponse : public ApiResponse {
public:
/**
* Set success response
* @param status Status message
*/
void setSuccess(const String& status) {
doc["status"] = status;
}
/**
* Set error response
* @param status Error status message
*/
void setError(const String& status) {
doc["status"] = status;
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,99 @@
#pragma once
#include "spore/util/JsonSerializable.h"
#include <ArduinoJson.h>
#include <Arduino.h>
namespace spore {
namespace types {
/**
* Serializable wrapper for task information that implements JsonSerializable interface
* Handles conversion between task data and JSON representation
*/
class TaskInfoSerializable : public util::JsonSerializable {
private:
String taskName;
const JsonObject& taskData;
public:
TaskInfoSerializable(const String& name, const JsonObject& data)
: taskName(name), taskData(data) {}
TaskInfoSerializable(const std::string& name, const JsonObject& data)
: taskName(name.c_str()), taskData(data) {}
/**
* Serialize task info to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["name"] = taskName;
obj["interval"] = taskData["interval"];
obj["enabled"] = taskData["enabled"];
obj["running"] = taskData["running"];
obj["autoStart"] = taskData["autoStart"];
}
/**
* Deserialize task info from JsonObject
* Note: This is read-only for task status, so fromJson is not implemented
*/
void fromJson(const JsonObject& obj) override {
// Task info is typically read-only in this context
// Implementation would go here if needed
}
};
/**
* Serializable wrapper for system information
*/
class SystemInfoSerializable : public util::JsonSerializable {
public:
/**
* Serialize system info to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["freeHeap"] = ESP.getFreeHeap();
obj["uptime"] = millis();
}
/**
* Deserialize system info from JsonObject
* Note: System info is typically read-only, so fromJson is not implemented
*/
void fromJson(const JsonObject& obj) override {
// System info is typically read-only
// Implementation would go here if needed
}
};
/**
* Serializable wrapper for task summary information
*/
class TaskSummarySerializable : public util::JsonSerializable {
private:
size_t totalTasks;
size_t activeTasks;
public:
TaskSummarySerializable(size_t total, size_t active)
: totalTasks(total), activeTasks(active) {}
/**
* Serialize task summary to JsonObject
*/
void toJson(JsonObject& obj) const override {
obj["totalTasks"] = totalTasks;
obj["activeTasks"] = activeTasks;
}
/**
* Deserialize task summary from JsonObject
*/
void fromJson(const JsonObject& obj) override {
totalTasks = obj["totalTasks"].as<size_t>();
activeTasks = obj["activeTasks"].as<size_t>();
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,194 @@
#pragma once
#include "ApiResponse.h"
#include "TaskInfoSerializable.h"
#include <map>
namespace spore {
namespace types {
/**
* Response class for task status endpoint
* Handles complete JSON document creation for task status data
*/
class TaskStatusResponse : public ApiResponse {
public:
/**
* Set the task summary information
* @param totalTasks Total number of tasks
* @param activeTasks Number of active tasks
*/
void setSummary(size_t totalTasks, size_t activeTasks) {
TaskSummarySerializable summary(totalTasks, activeTasks);
JsonObject summaryObj = doc["summary"].to<JsonObject>();
summary.toJson(summaryObj);
}
/**
* Add a single task to the response
* @param taskName Name of the task
* @param taskData Task data as JsonObject
*/
void addTask(const String& taskName, const JsonObject& taskData) {
TaskInfoSerializable serializable(taskName, taskData);
if (!doc["tasks"].is<JsonArray>()) {
doc["tasks"] = JsonArray();
}
JsonArray tasksArr = doc["tasks"].as<JsonArray>();
JsonObject taskObj = tasksArr.add<JsonObject>();
serializable.toJson(taskObj);
}
/**
* Add a single task to the response (std::string version)
* @param taskName Name of the task
* @param taskData Task data as JsonObject
*/
void addTask(const std::string& taskName, const JsonObject& taskData) {
TaskInfoSerializable serializable(taskName, taskData);
if (!doc["tasks"].is<JsonArray>()) {
doc["tasks"] = JsonArray();
}
JsonArray tasksArr = doc["tasks"].as<JsonArray>();
JsonObject taskObj = tasksArr.add<JsonObject>();
serializable.toJson(taskObj);
}
/**
* Add multiple tasks from a task statuses map
* @param taskStatuses Map of task name to task data
*/
void addTasks(const std::map<String, JsonObject>& taskStatuses) {
for (const auto& pair : taskStatuses) {
addTask(pair.first, pair.second);
}
}
/**
* Set the system information
*/
void setSystemInfo() {
SystemInfoSerializable systemInfo;
JsonObject systemObj = doc["system"].to<JsonObject>();
systemInfo.toJson(systemObj);
}
/**
* Build complete response with all components
* @param taskStatuses Map of task name to task data
*/
void buildCompleteResponse(const std::map<String, JsonObject>& taskStatuses) {
// Set summary
size_t totalTasks = taskStatuses.size();
size_t activeTasks = 0;
for (const auto& pair : taskStatuses) {
if (pair.second["enabled"]) {
activeTasks++;
}
}
setSummary(totalTasks, activeTasks);
// Add all tasks
addTasks(taskStatuses);
// Set system info
setSystemInfo();
}
/**
* Build complete response with all components from vector
* @param taskStatuses Vector of pairs of task name to task data
*/
void buildCompleteResponse(const std::vector<std::pair<std::string, JsonObject>>& taskStatuses) {
// Clear the document first since getAllTaskStatuses creates a root array
doc.clear();
// Set summary
size_t totalTasks = taskStatuses.size();
size_t activeTasks = 0;
for (const auto& pair : taskStatuses) {
if (pair.second["enabled"]) {
activeTasks++;
}
}
setSummary(totalTasks, activeTasks);
// Add all tasks - extract data before clearing to avoid invalid references
JsonArray tasksArr = doc["tasks"].to<JsonArray>();
for (const auto& pair : taskStatuses) {
// Extract data from JsonObject before it becomes invalid
String taskName = pair.first.c_str();
unsigned long interval = pair.second["interval"];
bool enabled = pair.second["enabled"];
bool running = pair.second["running"];
bool autoStart = pair.second["autoStart"];
// Create new JsonObject in our document
JsonObject taskObj = tasksArr.add<JsonObject>();
taskObj["name"] = taskName;
taskObj["interval"] = interval;
taskObj["enabled"] = enabled;
taskObj["running"] = running;
taskObj["autoStart"] = autoStart;
}
// Set system info
setSystemInfo();
}
};
/**
* Response class for task control operations
* Handles JSON responses for task enable/disable/start/stop operations
*/
class TaskControlResponse : public ApiResponse {
public:
/**
* Set the response data for a task control operation
* @param success Whether the operation was successful
* @param message Response message
* @param taskName Name of the task
* @param action Action performed
*/
void setResponse(bool success, const String& message, const String& taskName, const String& action) {
doc["success"] = success;
doc["message"] = message;
doc["task"] = taskName;
doc["action"] = action;
}
/**
* Add detailed task information to the response
* @param taskName Name of the task
* @param enabled Whether task is enabled
* @param running Whether task is running
* @param interval Task interval
*/
void addTaskDetails(const String& taskName, bool enabled, bool running, unsigned long interval) {
JsonObject taskDetails = doc["taskDetails"].to<JsonObject>();
taskDetails["name"] = taskName;
taskDetails["enabled"] = enabled;
taskDetails["running"] = running;
taskDetails["interval"] = interval;
// Add system info
SystemInfoSerializable systemInfo;
JsonObject systemObj = taskDetails["system"].to<JsonObject>();
systemInfo.toJson(systemObj);
}
/**
* Set error response
* @param message Error message
* @param example Optional example for correct usage
*/
void setError(const String& message, const String& example = "") {
doc["success"] = false;
doc["message"] = message;
if (example.length() > 0) {
doc["example"] = example;
}
}
};
} // namespace types
} // namespace spore

View File

@@ -0,0 +1,56 @@
#pragma once
#include <ArduinoJson.h>
namespace spore {
namespace util {
/**
* Abstract base class for objects that can be serialized to/from JSON
* Provides a clean interface for converting objects to JsonObject and back
*/
class JsonSerializable {
public:
virtual ~JsonSerializable() = default;
/**
* Serialize this object to a JsonObject
* @param obj The JsonObject to populate with this object's data
*/
virtual void toJson(JsonObject& obj) const = 0;
/**
* Deserialize this object from a JsonObject
* @param obj The JsonObject containing the data to populate this object
*/
virtual void fromJson(const JsonObject& obj) = 0;
/**
* Convenience method to create a JsonObject from this object
* @param doc The JsonDocument to create the object in
* @return A JsonObject containing this object's serialized data
*/
JsonObject toJsonObject(JsonDocument& doc) const {
JsonObject obj = doc.to<JsonObject>();
toJson(obj);
return obj;
}
/**
* Convenience method to create a JsonArray from a collection of serializable objects
* @param doc The JsonDocument to create the array in
* @param objects Collection of objects implementing JsonSerializable
* @return A JsonArray containing all serialized objects
*/
template<typename Container>
static JsonArray toJsonArray(JsonDocument& doc, const Container& objects) {
JsonArray arr = doc.to<JsonArray>();
for (const auto& obj : objects) {
JsonObject item = arr.add<JsonObject>();
obj.toJson(item);
}
return arr;
}
};
} // namespace util
} // namespace spore

View File

@@ -21,57 +21,31 @@ void ApiServer::registerEndpoint(const String& uri, int method,
const String& serviceName) {
// Add to local endpoints
endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
// Update cluster if needed
if (ctx.memberList && !ctx.memberList->empty()) {
auto it = ctx.memberList->find(ctx.hostname);
if (it != ctx.memberList->end()) {
it->second.endpoints.push_back(EndpointInfo{uri, method, params, serviceName, true});
}
}
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
const String& serviceName) {
registerEndpoint(uri, method, {}, serviceName);
server.on(uri.c_str(), method, requestHandler);
}
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) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler,
const String& serviceName) {
registerEndpoint(uri, method, {}, serviceName);
server.on(uri.c_str(), method, requestHandler, uploadHandler);
}
// 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) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
const std::vector<ParamSpec>& params, const String& serviceName) {
registerEndpoint(uri, method, params, serviceName);
server.on(uri.c_str(), method, requestHandler);
}
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) {
// Get current service name if available
String serviceName = "unknown";
if (!services.empty()) {
serviceName = services.back().get().getName();
}
const std::vector<ParamSpec>& params, const String& serviceName) {
registerEndpoint(uri, method, params, serviceName);
server.on(uri.c_str(), method, requestHandler, uploadHandler);
}

View File

@@ -1,42 +1,19 @@
#include "spore/services/ClusterService.h"
#include "spore/core/ApiServer.h"
#include "spore/types/ClusterResponse.h"
using spore::types::ClusterMembersResponse;
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>{});
std::vector<ParamSpec>{}, "ClusterService");
}
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);
ClusterMembersResponse response;
response.addNodes(ctx.memberList);
request->send(200, "application/json", response.toJsonString());
}

View File

@@ -12,7 +12,7 @@ MonitoringService::MonitoringService(CpuUsage& cpuUsage)
void MonitoringService::registerEndpoints(ApiServer& api) {
api.addEndpoint("/api/monitoring/resources", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleResourcesRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "MonitoringService");
}
MonitoringService::SystemResources MonitoringService::getSystemResources() const {

View File

@@ -8,16 +8,16 @@ void NetworkService::registerEndpoints(ApiServer& api) {
// WiFi scanning endpoints
api.addEndpoint("/api/network/wifi/scan", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleWifiScanRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NetworkService");
api.addEndpoint("/api/network/wifi/scan", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleGetWifiNetworks(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NetworkService");
// Network status and configuration endpoints
api.addEndpoint("/api/network/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleNetworkStatus(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NetworkService");
api.addEndpoint("/api/network/wifi/config", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleSetWifiConfig(request); },
@@ -26,7 +26,7 @@ void NetworkService::registerEndpoints(ApiServer& api) {
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")}
});
}, "NetworkService");
}
void NetworkService::handleWifiScanRequest(AsyncWebServerRequest* request) {

View File

@@ -1,6 +1,11 @@
#include "spore/services/NodeService.h"
#include "spore/core/ApiServer.h"
#include "spore/util/Logging.h"
#include "spore/types/NodeResponse.h"
using spore::types::NodeStatusResponse;
using spore::types::NodeOperationResponse;
using spore::types::NodeEndpointsResponse;
NodeService::NodeService(NodeContext& ctx, ApiServer& apiServer) : ctx(ctx), apiServer(apiServer) {}
@@ -8,7 +13,7 @@ void NodeService::registerEndpoints(ApiServer& api) {
// Status endpoint
api.addEndpoint("/api/node/status", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleStatusRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NodeService");
// Update endpoint with file upload
api.addEndpoint("/api/node/update", HTTP_POST,
@@ -18,54 +23,45 @@ void NodeService::registerEndpoints(ApiServer& api) {
},
std::vector<ParamSpec>{
ParamSpec{String("firmware"), true, String("body"), String("file"), {}, String("")}
});
}, "NodeService");
// Restart endpoint
api.addEndpoint("/api/node/restart", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleRestartRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NodeService");
// Endpoints endpoint
api.addEndpoint("/api/node/endpoints", HTTP_GET,
[this](AsyncWebServerRequest* request) { handleEndpointsRequest(request); },
std::vector<ParamSpec>{});
std::vector<ParamSpec>{}, "NodeService");
}
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
NodeStatusResponse response;
// Get labels from member list or self
std::map<String, String> labels;
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;
}
labels = it->second.labels;
} 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;
}
labels = ctx.self.labels;
}
}
String json;
serializeJson(doc, json);
request->send(200, "application/json", json);
response.buildCompleteResponse(labels);
request->send(200, "application/json", response.toJsonString());
}
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);
NodeOperationResponse response;
response.setSuccess(success ? "OK" : "FAIL");
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
httpResponse->addHeader("Connection", "close");
request->send(httpResponse);
request->onDisconnect([this]() {
LOG_INFO("API", "Restart device");
delay(10);
@@ -108,10 +104,12 @@ void NodeService::handleUpdateUpload(AsyncWebServerRequest* request, const Strin
}
void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
AsyncWebServerResponse* response = request->beginResponse(200, "application/json",
"{\"status\": \"restarting\"}");
response->addHeader("Connection", "close");
request->send(response);
NodeOperationResponse response;
response.setSuccess("restarting");
AsyncWebServerResponse* httpResponse = request->beginResponse(200, "application/json", response.toJsonString());
httpResponse->addHeader("Connection", "close");
request->send(httpResponse);
request->onDisconnect([this]() {
LOG_INFO("API", "Restart device");
delay(10);
@@ -120,36 +118,7 @@ void NodeService::handleRestartRequest(AsyncWebServerRequest* request) {
}
void NodeService::handleEndpointsRequest(AsyncWebServerRequest* request) {
JsonDocument doc;
JsonArray endpointsArr = doc["endpoints"].to<JsonArray>();
// Add all registered endpoints from ApiServer
for (const auto& endpoint : apiServer.getEndpoints()) {
JsonObject obj = endpointsArr.add<JsonObject>();
obj["uri"] = endpoint.uri;
obj["method"] = ApiServer::methodToStr(endpoint.method);
if (!endpoint.params.empty()) {
JsonArray paramsArr = obj["params"].to<JsonArray>();
for (const auto& ps : endpoint.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);
NodeEndpointsResponse response;
response.addEndpoints(apiServer.getEndpoints());
request->send(200, "application/json", response.toJsonString());
}

View File

@@ -1,13 +1,17 @@
#include "spore/services/TaskService.h"
#include "spore/core/ApiServer.h"
#include "spore/types/TaskResponse.h"
#include <algorithm>
using spore::types::TaskStatusResponse;
using spore::types::TaskControlResponse;
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>{});
std::vector<ParamSpec>{}, "TaskService");
api.addEndpoint("/api/tasks/control", HTTP_POST,
[this](AsyncWebServerRequest* request) { handleControlRequest(request); },
@@ -28,36 +32,19 @@ void TaskService::registerEndpoints(ApiServer& api) {
{String("enable"), String("disable"), String("start"), String("stop"), String("status")},
String("")
}
});
}, "TaskService");
}
void TaskService::handleStatusRequest(AsyncWebServerRequest* request) {
TaskStatusResponse response;
// Get task statuses using a separate document to avoid reference issues
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);
// Build the complete response with the task data
response.buildCompleteResponse(taskStatuses);
request->send(200, "application/json", response.toJsonString());
}
void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
@@ -88,50 +75,27 @@ void TaskService::handleControlRequest(AsyncWebServerRequest* request) {
success = true;
message = "Task status retrieved";
JsonDocument statusDoc;
statusDoc["success"] = success;
statusDoc["message"] = message;
statusDoc["task"] = taskName;
statusDoc["action"] = action;
TaskControlResponse response;
response.setResponse(success, message, taskName, action);
response.addTaskDetails(taskName,
taskManager.isTaskEnabled(taskName.c_str()),
taskManager.isTaskRunning(taskName.c_str()),
taskManager.getTaskInterval(taskName.c_str()));
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);
request->send(200, "application/json", response.toJsonString());
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);
TaskControlResponse response;
response.setResponse(success, message, taskName, action);
request->send(success ? 200 : 400, "application/json", response.toJsonString());
} 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);
TaskControlResponse response;
response.setError("Missing parameters. Required: task, action",
"{\"task\": \"discovery_send\", \"action\": \"status\"}");
request->send(400, "application/json", response.toJsonString());
}
}