Compare commits

...

2 Commits

Author SHA1 Message Date
1738017fd5 chore: refactoring and docs 2025-08-21 18:00:02 +02:00
5870695465 chore: rename ClusterContext to NodeContext 2025-08-21 17:39:43 +02:00
10 changed files with 84 additions and 56 deletions

View File

@@ -1,27 +1,57 @@
# SPORE # SPORE
SProcket ORchestration Engine > SProcket ORchestration Engine
SPORE is a simple cluster engine for ESP8266 microcontrollers.
## Features ## Features
- WiFi STA / AP - WiFi STA / AP
- node auto discovery over UDP - auto discovery over UDP
- service registry - service registry
- pub/sub event system - pub/sub event system
- Over-The-Air updates - Over-The-Air updates
## Supported Hardware ## Supported Hardware
- ESP8266 - ESP-01
## Configuration ## 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 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 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
### Configuration
Choose one of your nodes as the API node to interact with the cluster and configure it in `.env`: Choose one of your nodes as the API node to interact with the cluster and configure it in `.env`:
```sh ```sh
export API_NODE=10.0.1.x export API_NODE=10.0.1.x
``` ```
## Build ### Build
Build the firmware: Build the firmware:
@@ -29,14 +59,14 @@ Build the firmware:
./ctl.sh build ./ctl.sh build
``` ```
## Flash ### Flash
Flash firmware to a connected device: Flash firmware to a connected device:
```sh ```sh
./ctl.sh flash ./ctl.sh flash
``` ```
## OTA ### OTA
Update one nodes: Update one nodes:

View File

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

View File

@@ -7,28 +7,26 @@
#include <vector> #include <vector>
#include <tuple> #include <tuple>
#include "ClusterContext.h" #include "NodeContext.h"
#include "NodeInfo.h" #include "NodeInfo.h"
#define DEBUG_UPDATER 1
using namespace std; using namespace std;
using namespace std::placeholders; using namespace std::placeholders;
class RestApiServer { class ApiServer {
public: public:
RestApiServer(ClusterContext& ctx, uint16_t port = 80); ApiServer(NodeContext& ctx, uint16_t port = 80);
void begin(); 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);
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); std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler);
private: private:
AsyncWebServer server; AsyncWebServer server;
ClusterContext& ctx; NodeContext& ctx;
std::vector<std::tuple<String, int>> serviceRegistry; 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 methodToStr(const std::tuple<String, int> &endpoint, ArduinoJson::V742PB22::JsonObject &apiObj);
void getSystemStatusJson(AsyncWebServerRequest *request); void onSystemStatusRequest(AsyncWebServerRequest *request);
void onFirmwareUpdateRequest(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 onFirmwareUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final);
void onRestartRequest(AsyncWebServerRequest *request); void onRestartRequest(AsyncWebServerRequest *request);

View File

@@ -1,8 +1,8 @@
#include "ClusterManager.h" #include "ClusterManager.h"
ClusterManager::ClusterManager(ClusterContext& ctx) : ctx(ctx) { ClusterManager::ClusterManager(NodeContext& ctx) : ctx(ctx) {
// Register callback for node_discovered event // 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); NodeInfo* node = static_cast<NodeInfo*>(data);
this->addOrUpdateNode(node->hostname, node->ip); this->addOrUpdateNode(node->hostname, node->ip);
}); });
@@ -111,7 +111,7 @@ void ClusterManager::heartbeatTaskCallback() {
node.lastSeen = millis(); node.lastSeen = millis();
node.status = NodeInfo::ACTIVE; node.status = NodeInfo::ACTIVE;
updateLocalNodeResources(); updateLocalNodeResources();
ctx.triggerEvent("node_discovered", &node); ctx.fire("node_discovered", &node);
break; break;
} }
} }

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#include "Globals.h" #include "Globals.h"
#include "ClusterContext.h" #include "NodeContext.h"
#include "NodeInfo.h" #include "NodeInfo.h"
#include <Arduino.h> #include <Arduino.h>
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
@@ -9,7 +9,7 @@
class ClusterManager { class ClusterManager {
public: public:
ClusterManager(ClusterContext& ctx); ClusterManager(NodeContext& ctx);
void sendDiscovery(); void sendDiscovery();
void listenForDiscovery(); void listenForDiscovery();
void addOrUpdateNode(const String& nodeHost, IPAddress nodeIP); void addOrUpdateNode(const String& nodeHost, IPAddress nodeIP);
@@ -22,5 +22,5 @@ public:
void heartbeatTaskCallback(); void heartbeatTaskCallback();
void updateAllMembersInfoTaskCallback(); void updateAllMembersInfoTaskCallback();
private: private:
ClusterContext& ctx; NodeContext& ctx;
}; };

View File

@@ -3,7 +3,7 @@
const char* STA_SSID = "shroud"; const char* STA_SSID = "shroud";
const char* STA_PASS = "th3r31sn0sp00n"; const char* STA_PASS = "th3r31sn0sp00n";
NetworkManager::NetworkManager(ClusterContext& ctx) : ctx(ctx) {} NetworkManager::NetworkManager(NodeContext& ctx) : ctx(ctx) {}
void NetworkManager::setHostnameFromMac() { void NetworkManager::setHostnameFromMac() {
uint8_t mac[6]; uint8_t mac[6];
@@ -55,5 +55,5 @@ void NetworkManager::setupWiFi() {
} }
self.lastSeen = millis(); self.lastSeen = millis();
self.status = NodeInfo::ACTIVE; self.status = NodeInfo::ACTIVE;
ctx.triggerEvent("node_discovered", &self); ctx.fire("node_discovered", &self);
} }

View File

@@ -1,12 +1,12 @@
#pragma once #pragma once
#include "ClusterContext.h" #include "NodeContext.h"
#include <ESP8266WiFi.h> #include <ESP8266WiFi.h>
class NetworkManager { class NetworkManager {
public: public:
NetworkManager(ClusterContext& ctx); NetworkManager(NodeContext& ctx);
void setupWiFi(); void setupWiFi();
void setHostnameFromMac(); void setHostnameFromMac();
private: private:
ClusterContext& ctx; NodeContext& ctx;
}; };

View File

@@ -1,23 +1,23 @@
#include "ClusterContext.h" #include "NodeContext.h"
ClusterContext::ClusterContext() { NodeContext::NodeContext() {
scheduler = new Scheduler(); scheduler = new Scheduler();
udp = new WiFiUDP(); udp = new WiFiUDP();
memberList = new std::vector<NodeInfo>(); memberList = new std::vector<NodeInfo>();
hostname = ""; hostname = "";
} }
ClusterContext::~ClusterContext() { NodeContext::~NodeContext() {
delete scheduler; delete scheduler;
delete udp; delete udp;
delete memberList; delete memberList;
} }
void ClusterContext::registerEvent(const std::string& event, EventCallback cb) { void NodeContext::on(const std::string& event, EventCallback cb) {
eventRegistry[event].push_back(cb); eventRegistry[event].push_back(cb);
} }
void ClusterContext::triggerEvent(const std::string& event, void* data) { void NodeContext::fire(const std::string& event, void* data) {
for (auto& cb : eventRegistry[event]) { for (auto& cb : eventRegistry[event]) {
cb(data); cb(data);
} }

View File

@@ -7,10 +7,10 @@
#include <map> #include <map>
#include <string> #include <string>
class ClusterContext { class NodeContext {
public: public:
ClusterContext(); NodeContext();
~ClusterContext(); ~NodeContext();
Scheduler* scheduler; Scheduler* scheduler;
WiFiUDP* udp; WiFiUDP* udp;
String hostname; String hostname;
@@ -20,6 +20,6 @@ public:
using EventCallback = std::function<void(void*)>; using EventCallback = std::function<void(void*)>;
std::map<std::string, std::vector<EventCallback>> eventRegistry; std::map<std::string, std::vector<EventCallback>> eventRegistry;
void registerEvent(const std::string& event, EventCallback cb); void on(const std::string& event, EventCallback cb);
void triggerEvent(const std::string& event, void* data); void fire(const std::string& event, void* data);
}; };

View File

@@ -1,14 +1,14 @@
#include <Arduino.h> #include <Arduino.h>
#include "Globals.h" #include "Globals.h"
#include "ClusterContext.h" #include "NodeContext.h"
#include "NetworkManager.h" #include "NetworkManager.h"
#include "ClusterManager.h" #include "ClusterManager.h"
#include "RestApiServer.h" #include "ApiServer.h"
ClusterContext ctx; NodeContext ctx;
NetworkManager network(ctx); NetworkManager network(ctx);
ClusterManager cluster(ctx); ClusterManager cluster(ctx);
RestApiServer apiServer(ctx); ApiServer apiServer(ctx);
Task tSendDiscovery(TaskIntervals::SEND_DISCOVERY, TASK_FOREVER, [](){ cluster.sendDiscovery(); }); Task tSendDiscovery(TaskIntervals::SEND_DISCOVERY, TASK_FOREVER, [](){ cluster.sendDiscovery(); });
Task tListenForDiscovery(TaskIntervals::LISTEN_FOR_DISCOVERY, TASK_FOREVER, [](){ cluster.listenForDiscovery(); }); Task tListenForDiscovery(TaskIntervals::LISTEN_FOR_DISCOVERY, TASK_FOREVER, [](){ cluster.listenForDiscovery(); });