chore: refactoring and docs

This commit is contained in:
2025-08-21 18:00:02 +02:00
parent 5870695465
commit 1738017fd5
8 changed files with 51 additions and 37 deletions

View File

@@ -1,30 +1,46 @@
# SPORE
SProcket ORchestration Engine
> SProcket ORchestration Engine
SPORE is a simple cluster engine for ESP8266 microcontrollers.
## Features
- WiFi STA / AP
- node auto discovery over UDP
- auto discovery over UDP
- service registry
- pub/sub event system
- Over-The-Air updates
## Supported Hardware
- ESP8266
- ESP-01
## Architecture
### Components
The core architecture consists for following components:
- Network Manager: WiFi connection handling
- Cluster Manager: node discovery and memberlist management
- API Server: HTTP API for interacting with node and cluster
- Task Scheduler: internal scheduler used for system and user defined tasks
### Auto Discovery
A node periodically executes 2 tasks responsible for auto discovers:
A node periodically executes 2 tasks responsible for auto discovery:
- send discovery: send UDP packet on broadcast address to discover nodes
- listen for discovery: receive UDP packets and send response back to the node who initiated discovery
Discovered nodes are added to the so cluster memberlist.
Another periodic task will then call the `/api/node/status` endpoint over HTTP on each node in the memberlist to get detailed informations about the node (e.g. freeHeap, available API endpoints).
Discovered nodes are added to the so clusters memberlist.
Another periodic task will then call the `/api/node/status` endpoint over HTTP on each node in the memberlist to get system resources and available API endpoints.
### Event System
The `NodeContext` implements an event system for publishing and subscribing to local and cluster wide events (TODO).
It is used internally for communication between different components and tasks.
## Develop

View File

@@ -1,8 +1,8 @@
#include "RestApiServer.h"
#include "ApiServer.h"
RestApiServer::RestApiServer(NodeContext& ctx, uint16_t port) : server(port), ctx(ctx) {}
ApiServer::ApiServer(NodeContext& ctx, uint16_t port) : server(port), ctx(ctx) {}
void RestApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
void ApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler) {
serviceRegistry.push_back(std::make_tuple(uri, method));
// Store in NodeInfo for local node
if (ctx.memberList && !ctx.memberList->empty()) {
@@ -11,7 +11,7 @@ void RestApiServer::addEndpoint(const String& uri, int method, std::function<voi
server.on(uri.c_str(), method, requestHandler);
}
void RestApiServer::addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> 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) {
serviceRegistry.push_back(std::make_tuple(uri, method));
if (ctx.memberList && !ctx.memberList->empty()) {
@@ -20,21 +20,21 @@ void RestApiServer::addEndpoint(const String& uri, int method, std::function<voi
server.on(uri.c_str(), method, requestHandler, uploadHandler);
}
void RestApiServer::begin() {
void ApiServer::begin() {
addEndpoint("/api/node/status", HTTP_GET,
std::bind(&RestApiServer::getSystemStatusJson, this, std::placeholders::_1));
std::bind(&ApiServer::onSystemStatusRequest, this, std::placeholders::_1));
addEndpoint("/api/cluster/members", HTTP_GET,
std::bind(&RestApiServer::getClusterMembersJson, this, std::placeholders::_1));
std::bind(&ApiServer::onClusterMembersRequest, this, std::placeholders::_1));
addEndpoint("/api/node/update", HTTP_POST,
std::bind(&RestApiServer::onFirmwareUpdateRequest, this, std::placeholders::_1),
std::bind(&RestApiServer::onFirmwareUpload, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5, std::placeholders::_6)
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(&RestApiServer::onRestartRequest, this, std::placeholders::_1));
std::bind(&ApiServer::onRestartRequest, this, std::placeholders::_1));
server.begin();
}
void RestApiServer::getSystemStatusJson(AsyncWebServerRequest *request) {
void ApiServer::onSystemStatusRequest(AsyncWebServerRequest *request) {
JsonDocument doc;
doc["freeHeap"] = ESP.getFreeHeap();
doc["chipId"] = ESP.getChipId();
@@ -52,7 +52,7 @@ void RestApiServer::getSystemStatusJson(AsyncWebServerRequest *request) {
request->send(200, "application/json", json);
}
void RestApiServer::getClusterMembersJson(AsyncWebServerRequest *request) {
void ApiServer::onClusterMembersRequest(AsyncWebServerRequest *request) {
JsonDocument doc;
JsonArray arr = doc["members"].to<JsonArray>();
for (const auto& node : *ctx.memberList) {
@@ -79,7 +79,7 @@ void RestApiServer::getClusterMembersJson(AsyncWebServerRequest *request) {
request->send(200, "application/json", json);
}
void RestApiServer::methodToStr(const std::tuple<String, int> &endpoint, ArduinoJson::V742PB22::JsonObject &apiObj)
void ApiServer::methodToStr(const std::tuple<String, int> &endpoint, ArduinoJson::V742PB22::JsonObject &apiObj)
{
int method = std::get<1>(endpoint);
const char *methodStr = nullptr;
@@ -107,7 +107,7 @@ void RestApiServer::methodToStr(const std::tuple<String, int> &endpoint, Arduino
apiObj["method"] = methodStr;
}
void RestApiServer::onFirmwareUpdateRequest(AsyncWebServerRequest *request) {
void ApiServer::onFirmwareUpdateRequest(AsyncWebServerRequest *request) {
bool hasError = !Update.hasError();
AsyncWebServerResponse *response = request->beginResponse(200, "application/json", hasError ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
response->addHeader("Connection", "close");
@@ -119,7 +119,7 @@ void RestApiServer::onFirmwareUpdateRequest(AsyncWebServerRequest *request) {
});
}
void RestApiServer::onFirmwareUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) {
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);
@@ -157,7 +157,7 @@ void RestApiServer::onFirmwareUpload(AsyncWebServerRequest *request, const Strin
return;
}
void RestApiServer::onRestartRequest(AsyncWebServerRequest *request) {
void ApiServer::onRestartRequest(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response = request->beginResponse(200, "application/json", "{\"status\": \"restarting\"}");
response->addHeader("Connection", "close");
request->send(response);

View File

@@ -10,14 +10,12 @@
#include "NodeContext.h"
#include "NodeInfo.h"
#define DEBUG_UPDATER 1
using namespace std;
using namespace std::placeholders;
class RestApiServer {
class ApiServer {
public:
RestApiServer(NodeContext& ctx, uint16_t port = 80);
ApiServer(NodeContext& ctx, uint16_t port = 80);
void begin();
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler);
void addEndpoint(const String& uri, int method, std::function<void(AsyncWebServerRequest*)> requestHandler,
@@ -26,9 +24,9 @@ private:
AsyncWebServer server;
NodeContext& ctx;
std::vector<std::tuple<String, int>> serviceRegistry;
void getClusterMembersJson(AsyncWebServerRequest *request);
void onClusterMembersRequest(AsyncWebServerRequest *request);
void methodToStr(const std::tuple<String, int> &endpoint, ArduinoJson::V742PB22::JsonObject &apiObj);
void getSystemStatusJson(AsyncWebServerRequest *request);
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);

View File

@@ -2,7 +2,7 @@
ClusterManager::ClusterManager(NodeContext& ctx) : ctx(ctx) {
// Register callback for node_discovered event
ctx.registerEvent("node_discovered", [this](void* data) {
ctx.on("node_discovered", [this](void* data) {
NodeInfo* node = static_cast<NodeInfo*>(data);
this->addOrUpdateNode(node->hostname, node->ip);
});
@@ -111,7 +111,7 @@ void ClusterManager::heartbeatTaskCallback() {
node.lastSeen = millis();
node.status = NodeInfo::ACTIVE;
updateLocalNodeResources();
ctx.triggerEvent("node_discovered", &node);
ctx.fire("node_discovered", &node);
break;
}
}

View File

@@ -55,5 +55,5 @@ void NetworkManager::setupWiFi() {
}
self.lastSeen = millis();
self.status = NodeInfo::ACTIVE;
ctx.triggerEvent("node_discovered", &self);
ctx.fire("node_discovered", &self);
}

View File

@@ -13,11 +13,11 @@ NodeContext::~NodeContext() {
delete memberList;
}
void NodeContext::registerEvent(const std::string& event, EventCallback cb) {
void NodeContext::on(const std::string& event, EventCallback cb) {
eventRegistry[event].push_back(cb);
}
void NodeContext::triggerEvent(const std::string& event, void* data) {
void NodeContext::fire(const std::string& event, void* data) {
for (auto& cb : eventRegistry[event]) {
cb(data);
}

View File

@@ -20,6 +20,6 @@ public:
using EventCallback = std::function<void(void*)>;
std::map<std::string, std::vector<EventCallback>> eventRegistry;
void registerEvent(const std::string& event, EventCallback cb);
void triggerEvent(const std::string& event, void* data);
void on(const std::string& event, EventCallback cb);
void fire(const std::string& event, void* data);
};

View File

@@ -3,12 +3,12 @@
#include "NodeContext.h"
#include "NetworkManager.h"
#include "ClusterManager.h"
#include "RestApiServer.h"
#include "ApiServer.h"
NodeContext ctx;
NetworkManager network(ctx);
ClusterManager cluster(ctx);
RestApiServer apiServer(ctx);
ApiServer apiServer(ctx);
Task tSendDiscovery(TaskIntervals::SEND_DISCOVERY, TASK_FOREVER, [](){ cluster.sendDiscovery(); });
Task tListenForDiscovery(TaskIntervals::LISTEN_FOR_DISCOVERY, TASK_FOREVER, [](){ cluster.listenForDiscovery(); });