basic functionality
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
.pio
|
||||
.vscode/.browse.c_cpp.db*
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
10
.vscode/extensions.json
vendored
Normal file
10
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
]
|
||||
}
|
||||
63
.vscode/settings.json
vendored
Normal file
63
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"chrono": "cpp",
|
||||
"functional": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"bitset": "cpp",
|
||||
"cctype": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"condition_variable": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"deque": "cpp",
|
||||
"list": "cpp",
|
||||
"map": "cpp",
|
||||
"set": "cpp",
|
||||
"unordered_map": "cpp",
|
||||
"vector": "cpp",
|
||||
"exception": "cpp",
|
||||
"algorithm": "cpp",
|
||||
"iterator": "cpp",
|
||||
"memory": "cpp",
|
||||
"memory_resource": "cpp",
|
||||
"numeric": "cpp",
|
||||
"optional": "cpp",
|
||||
"random": "cpp",
|
||||
"ratio": "cpp",
|
||||
"regex": "cpp",
|
||||
"string": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"utility": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"ostream": "cpp",
|
||||
"ranges": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"stop_token": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"thread": "cpp",
|
||||
"cinttypes": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp"
|
||||
}
|
||||
}
|
||||
51
README.md
Normal file
51
README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# SPORE
|
||||
|
||||
SProcket ORchestration Engine
|
||||
|
||||
## Features
|
||||
|
||||
- WiFi STA / AP
|
||||
- node auto discovery over UDP
|
||||
- service registry
|
||||
- pub/sub event system
|
||||
- Over-The-Air updates
|
||||
|
||||
## Supported Hardware
|
||||
|
||||
- ESP8266
|
||||
|
||||
## Configuration
|
||||
|
||||
Choose one of your nodes as the API node to interact with the cluster and configure it in `.env`:
|
||||
```sh
|
||||
export API_NODE=10.0.1.x
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
Build the firmware:
|
||||
|
||||
```sh
|
||||
./ctl.sh build
|
||||
```
|
||||
|
||||
## Flash
|
||||
|
||||
Flash firmware to a connected device:
|
||||
|
||||
```sh
|
||||
./ctl.sh flash
|
||||
```
|
||||
## OTA
|
||||
|
||||
Update one nodes:
|
||||
|
||||
```sh
|
||||
./ctl.sh ota update 10.0.1.x
|
||||
```
|
||||
|
||||
Update all nodes:
|
||||
|
||||
```sh
|
||||
./ctl.sh ota all
|
||||
```
|
||||
204
bastel/main.cpp
Normal file
204
bastel/main.cpp
Normal file
@@ -0,0 +1,204 @@
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h> // or #include <WiFi.h> for ESP32
|
||||
#include <ESP8266mDNS.h> // or #include <ESPmDNS.h> for ESP32
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <AsyncWebSocket.h>
|
||||
#include <ArduinoJson.h>
|
||||
|
||||
// Node metadata structure
|
||||
struct Node {
|
||||
String name;
|
||||
IPAddress ip;
|
||||
unsigned long lastSeen;
|
||||
String status;
|
||||
};
|
||||
|
||||
// Service structure
|
||||
struct Service {
|
||||
String name;
|
||||
String endpoint;
|
||||
void (*callback)();
|
||||
};
|
||||
|
||||
// MemberList and ServiceRegistry
|
||||
Node memberList[10]; // Example size
|
||||
Service serviceRegistry[10];
|
||||
|
||||
AsyncWebServer server(80);
|
||||
AsyncWebSocket ws("/ws");
|
||||
|
||||
void onServiceCall() {
|
||||
// Placeholder for service callback
|
||||
}
|
||||
|
||||
// Helper to update node status
|
||||
String getNodeStatus(unsigned long lastSeen) {
|
||||
unsigned long now = millis();
|
||||
if (now - lastSeen < 10000) return "active";
|
||||
if (now - lastSeen < 60000) return "inactive";
|
||||
return "dead";
|
||||
}
|
||||
|
||||
// Discover other nodes via mDNS
|
||||
void discoverNodes() {
|
||||
int n = MDNS.queryService("_ws", "tcp");
|
||||
if (n == 0) {
|
||||
Serial.println("No mDNS services found");
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < n && i < 10; i++) {
|
||||
IPAddress ip = MDNS.answerIP(i);
|
||||
String name = MDNS.answerHostname(i);
|
||||
if (ip == INADDR_NONE || name.isEmpty()) continue; // Skip invalid entries
|
||||
Serial.printf("Discovered node: %s (%s)\n", name.c_str(), ip.toString().c_str());
|
||||
// Add/update node in memberList
|
||||
bool found = false;
|
||||
for (int j = 0; j < 10; j++) {
|
||||
if (memberList[j].ip == ip) {
|
||||
memberList[j].lastSeen = millis();
|
||||
String oldStatus = memberList[j].status;
|
||||
memberList[j].status = getNodeStatus(memberList[j].lastSeen);
|
||||
found = true;
|
||||
if (oldStatus != memberList[j].status) {
|
||||
Serial.printf("Node %s (%s) status changed: %s -> %s\n", memberList[j].name.c_str(), memberList[j].ip.toString().c_str(), oldStatus.c_str(), memberList[j].status.c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
if (memberList[j].name == "") {
|
||||
memberList[j].name = name;
|
||||
memberList[j].ip = ip;
|
||||
memberList[j].lastSeen = millis();
|
||||
memberList[j].status = "active";
|
||||
Serial.printf("Discovered new node: %s (%s)\n", name.c_str(), ip.toString().c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast event to all nodes
|
||||
void broadcastEvent(const String& event) {
|
||||
DynamicJsonDocument doc(256);
|
||||
doc["type"] = "event";
|
||||
doc["data"] = event;
|
||||
String msg;
|
||||
serializeJson(doc, msg);
|
||||
ws.textAll(msg);
|
||||
}
|
||||
|
||||
// Handle incoming WebSocket events
|
||||
void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
|
||||
if (type == WS_EVT_DATA) {
|
||||
String msg = String((char*)data);
|
||||
DynamicJsonDocument doc(256);
|
||||
DeserializationError err = deserializeJson(doc, msg);
|
||||
if (!err) {
|
||||
String type = doc["type"];
|
||||
if (type == "event") {
|
||||
// Handle pub/sub event
|
||||
Serial.println("Received event: " + doc["data"].as<String>());
|
||||
} else if (type == "service_call") {
|
||||
// Handle service call
|
||||
String endpoint = doc["endpoint"];
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (serviceRegistry[i].endpoint == endpoint && serviceRegistry[i].callback) {
|
||||
serviceRegistry[i].callback();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
|
||||
void onNewClient(System_Event_t *event) {
|
||||
if (event->event == EVENT_SOFTAPMODE_STACONNECTED) {
|
||||
uint8_t *mac = event->event_info.sta_connected.mac;
|
||||
Serial.printf("New WiFi client connected: MAC %02x:%02x:%02x:%02x:%02x:%02x\n",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
}
|
||||
}
|
||||
|
||||
String getMacHostname() {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
char hostname[32];
|
||||
snprintf(hostname, sizeof(hostname), "esp-%02x%02x%02x%02x%02x%02x",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
return String(hostname);
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
delay(100);
|
||||
String myHostname = getMacHostname();
|
||||
WiFi.hostname(myHostname);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin("contraption", "sprocket");
|
||||
Serial.printf("Attempting to connect to AP 'contraption' as %s...\n", myHostname.c_str());
|
||||
unsigned long startAttemptTime = millis();
|
||||
bool connected = false;
|
||||
while (millis() - startAttemptTime < 10000) { // try for 10 seconds
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
connected = true;
|
||||
break;
|
||||
}
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
if (connected) {
|
||||
Serial.printf("\nConnected to AP 'contraption' as %s.\n", myHostname.c_str());
|
||||
} else {
|
||||
Serial.println("\nFailed to connect. Starting AP mode.");
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("contraption", "sprocket");
|
||||
WiFi.hostname(myHostname.c_str());
|
||||
Serial.printf("Access Point 'contraption' started with password 'sprocket' and hostname %s.\n", myHostname.c_str());
|
||||
wifi_set_event_handler_cb(onNewClient);
|
||||
}
|
||||
|
||||
// mDNS setup
|
||||
if (MDNS.begin(myHostname)) {
|
||||
Serial.println("mDNS responder started");
|
||||
// Register WebSocket service on mDNS
|
||||
MDNS.addService("_ws", "tcp", 80); // 80 is the default WebSocket port
|
||||
Serial.println("mDNS WebSocket service '_ws._tcp' registered on port 80");
|
||||
}
|
||||
|
||||
// WebSocket server setup
|
||||
ws.onEvent(onWsEvent);
|
||||
server.addHandler(&ws);
|
||||
server.begin();
|
||||
|
||||
// Register example service
|
||||
serviceRegistry[0] = {"status", "/api/status", onServiceCall};
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Periodically discover nodes
|
||||
static unsigned long lastDiscovery = 0;
|
||||
if (millis() - lastDiscovery > 5000) {
|
||||
discoverNodes();
|
||||
lastDiscovery = millis();
|
||||
}
|
||||
// Update node statuses
|
||||
for (int i = 0; i < 10; i++) {
|
||||
if (memberList[i].name != "") {
|
||||
String oldStatus = memberList[i].status;
|
||||
memberList[i].status = getNodeStatus(memberList[i].lastSeen);
|
||||
if (oldStatus != memberList[i].status) {
|
||||
Serial.printf("Node %s (%s) status changed: %s -> %s\n", memberList[i].name.c_str(), memberList[i].ip.toString().c_str(), oldStatus.c_str(), memberList[i].status.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
// No need for webSocket.loop() with ESPAsyncWebServer
|
||||
// Node discovery, memberlist update, service registry logic to be added
|
||||
}
|
||||
44
ctl.sh
Executable file
44
ctl.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
source .env
|
||||
|
||||
function info {
|
||||
sed -n 's/^##//p' ctl.sh
|
||||
}
|
||||
|
||||
function build {
|
||||
echo "Building project..."
|
||||
pio run
|
||||
}
|
||||
|
||||
function flash {
|
||||
echo "Flashing firmware..."
|
||||
pio run --target upload
|
||||
}
|
||||
|
||||
function ota {
|
||||
function update {
|
||||
echo "Updating node at $1"
|
||||
curl -X POST \
|
||||
-F "file=@.pio/build/esp01_1m/firmware.bin" \
|
||||
http://$1/api/node/update | jq -r '.status'
|
||||
}
|
||||
function all {
|
||||
echo "Updating all nodes..."
|
||||
curl -s http://$API_NODE/api/cluster/members | jq -r '.members.[].ip' | while read -r ip; do
|
||||
ota update $ip
|
||||
done
|
||||
}
|
||||
${@:-info}
|
||||
}
|
||||
|
||||
function cluster {
|
||||
function members {
|
||||
curl -s http://$API_NODE/api/cluster/members | jq -r '.members[] | "\(.hostname) \(.ip)"'
|
||||
}
|
||||
${@:-info}
|
||||
}
|
||||
|
||||
${@:-info}
|
||||
27
include/Globals.h
Normal file
27
include/Globals.h
Normal file
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Cluster protocol and API constants
|
||||
namespace ClusterProtocol {
|
||||
constexpr const char* DISCOVERY_MSG = "CLUSTER_DISCOVERY";
|
||||
constexpr const char* RESPONSE_MSG = "CLUSTER_RESPONSE";
|
||||
constexpr uint16_t UDP_PORT = 4210;
|
||||
constexpr size_t UDP_BUF_SIZE = 64;
|
||||
constexpr const char* API_NODE_STATUS = "/api/node/status";
|
||||
}
|
||||
|
||||
namespace TaskIntervals {
|
||||
constexpr unsigned long SEND_DISCOVERY = 1000;
|
||||
constexpr unsigned long LISTEN_FOR_DISCOVERY = 100;
|
||||
constexpr unsigned long UPDATE_STATUS = 1000;
|
||||
constexpr unsigned long PRINT_MEMBER_LIST = 5000;
|
||||
constexpr unsigned long HEARTBEAT = 2000;
|
||||
constexpr unsigned long UPDATE_ALL_MEMBERS_INFO = 10000;
|
||||
}
|
||||
|
||||
constexpr unsigned long NODE_ACTIVE_THRESHOLD = 10000;
|
||||
constexpr unsigned long NODE_INACTIVE_THRESHOLD = 60000;
|
||||
constexpr unsigned long NODE_DEAD_THRESHOLD = 120000;
|
||||
|
||||
37
include/README
Normal file
37
include/README
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
This directory is intended for project header files.
|
||||
|
||||
A header file is a file containing C declarations and macro definitions
|
||||
to be shared between several project source files. You request the use of a
|
||||
header file in your project source file (C, C++, etc) located in `src` folder
|
||||
by including it, with the C preprocessing directive `#include'.
|
||||
|
||||
```src/main.c
|
||||
|
||||
#include "header.h"
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
Including a header file produces the same results as copying the header file
|
||||
into each source file that needs it. Such copying would be time-consuming
|
||||
and error-prone. With a header file, the related declarations appear
|
||||
in only one place. If they need to be changed, they can be changed in one
|
||||
place, and programs that include the header file will automatically use the
|
||||
new version when next recompiled. The header file eliminates the labor of
|
||||
finding and changing all the copies as well as the risk that a failure to
|
||||
find one copy will result in inconsistencies within a program.
|
||||
|
||||
In C, the convention is to give header files names that end with `.h'.
|
||||
|
||||
Read more about using header files in official GCC documentation:
|
||||
|
||||
* Include Syntax
|
||||
* Include Operation
|
||||
* Once-Only Headers
|
||||
* Computed Includes
|
||||
|
||||
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||
46
lib/README
Normal file
46
lib/README
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
6
partitions_ota_1M.csv
Normal file
6
partitions_ota_1M.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
# Name, Type, SubType, Offset, Size, Flags
|
||||
nvs, data, nvs, , 0x4000,
|
||||
otadata, data, ota, , 0x2000,
|
||||
app0, app, ota_0, , 0x3E000,
|
||||
app1, app, ota_1, , 0x3E000,
|
||||
spiffs, data, spiffs, , 0x1C000,
|
||||
|
23
platformio.ini
Normal file
23
platformio.ini
Normal file
@@ -0,0 +1,23 @@
|
||||
; PlatformIO Project Configuration File
|
||||
;
|
||||
; Build options: build flags, source filter
|
||||
; Upload options: custom upload port, speed and extra flags
|
||||
; Library options: dependencies, extra library storages
|
||||
; Advanced options: extra scripting
|
||||
;
|
||||
; Please visit documentation for the other options and examples
|
||||
; https://docs.platformio.org/page/projectconf.html
|
||||
|
||||
[env:esp01_1m]
|
||||
platform = platformio/espressif8266@^4.2.1
|
||||
board = esp01_1m
|
||||
framework = arduino
|
||||
upload_speed = 115200
|
||||
monitor_speed = 115200
|
||||
board_build.partitions = partitions_ota_1M.csv
|
||||
board_build.flash_mode = dout ; ESP‑01S uses DOUT on 1 Mbit flash
|
||||
board_build.flash_size = 1M
|
||||
lib_deps =
|
||||
esp32async/ESPAsyncWebServer@^3.8.0
|
||||
bblanchon/ArduinoJson@^7.4.2
|
||||
arkhipenko/TaskScheduler@^3.8.5
|
||||
24
src/ClusterContext.cpp
Normal file
24
src/ClusterContext.cpp
Normal file
@@ -0,0 +1,24 @@
|
||||
#include "ClusterContext.h"
|
||||
|
||||
ClusterContext::ClusterContext() {
|
||||
scheduler = new Scheduler();
|
||||
udp = new WiFiUDP();
|
||||
memberList = new std::vector<NodeInfo>();
|
||||
hostname = "";
|
||||
}
|
||||
|
||||
ClusterContext::~ClusterContext() {
|
||||
delete scheduler;
|
||||
delete udp;
|
||||
delete memberList;
|
||||
}
|
||||
|
||||
void ClusterContext::registerEvent(const std::string& event, EventCallback cb) {
|
||||
eventRegistry[event].push_back(cb);
|
||||
}
|
||||
|
||||
void ClusterContext::triggerEvent(const std::string& event, void* data) {
|
||||
for (auto& cb : eventRegistry[event]) {
|
||||
cb(data);
|
||||
}
|
||||
}
|
||||
25
src/ClusterContext.h
Normal file
25
src/ClusterContext.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
#include <TaskSchedulerDeclarations.h>
|
||||
#include <WiFiUdp.h>
|
||||
#include <vector>
|
||||
#include "NodeInfo.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
class ClusterContext {
|
||||
public:
|
||||
ClusterContext();
|
||||
~ClusterContext();
|
||||
Scheduler* scheduler;
|
||||
WiFiUDP* udp;
|
||||
String hostname;
|
||||
IPAddress localIP;
|
||||
std::vector<NodeInfo>* memberList;
|
||||
|
||||
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);
|
||||
};
|
||||
175
src/ClusterManager.cpp
Normal file
175
src/ClusterManager.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include "ClusterManager.h"
|
||||
|
||||
ClusterManager::ClusterManager(ClusterContext& ctx) : ctx(ctx) {
|
||||
// Register callback for node_discovered event
|
||||
ctx.registerEvent("node_discovered", [this](void* data) {
|
||||
NodeInfo* node = static_cast<NodeInfo*>(data);
|
||||
this->addOrUpdateNode(node->hostname, node->ip);
|
||||
});
|
||||
}
|
||||
|
||||
void ClusterManager::sendDiscovery() {
|
||||
//Serial.println("[Cluster] Sending discovery packet...");
|
||||
ctx.udp->beginPacket("255.255.255.255", ClusterProtocol::UDP_PORT);
|
||||
ctx.udp->write(ClusterProtocol::DISCOVERY_MSG);
|
||||
ctx.udp->endPacket();
|
||||
}
|
||||
|
||||
void ClusterManager::listenForDiscovery() {
|
||||
int packetSize = ctx.udp->parsePacket();
|
||||
if (packetSize) {
|
||||
char incoming[ClusterProtocol::UDP_BUF_SIZE];
|
||||
int len = ctx.udp->read(incoming, ClusterProtocol::UDP_BUF_SIZE);
|
||||
if (len > 0) {
|
||||
incoming[len] = 0;
|
||||
}
|
||||
//Serial.printf("[UDP] Packet received: %s\n", incoming);
|
||||
if (strcmp(incoming, ClusterProtocol::DISCOVERY_MSG) == 0) {
|
||||
//Serial.printf("[UDP] Discovery request from: %s\n", ctx.udp->remoteIP().toString().c_str());
|
||||
ctx.udp->beginPacket(ctx.udp->remoteIP(), ClusterProtocol::UDP_PORT);
|
||||
String response = String(ClusterProtocol::RESPONSE_MSG) + ":" + ctx.hostname;
|
||||
ctx.udp->write(response.c_str());
|
||||
ctx.udp->endPacket();
|
||||
//Serial.printf("[UDP] Sent response with hostname: %s\n", ctx.hostname.c_str());
|
||||
} else if (strncmp(incoming, ClusterProtocol::RESPONSE_MSG, strlen(ClusterProtocol::RESPONSE_MSG)) == 0) {
|
||||
char* hostPtr = incoming + strlen(ClusterProtocol::RESPONSE_MSG) + 1;
|
||||
String nodeHost = String(hostPtr);
|
||||
addOrUpdateNode(nodeHost, ctx.udp->remoteIP());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::addOrUpdateNode(const String& nodeHost, IPAddress nodeIP) {
|
||||
auto& memberList = *ctx.memberList;
|
||||
for (auto& node : memberList) {
|
||||
if (node.hostname == nodeHost) {
|
||||
node.ip = nodeIP;
|
||||
//fetchNodeInfo(nodeIP); // Do not fetch here, handled by periodic task
|
||||
return;
|
||||
}
|
||||
}
|
||||
NodeInfo newNode;
|
||||
newNode.hostname = nodeHost;
|
||||
newNode.ip = nodeIP;
|
||||
newNode.lastSeen = millis();
|
||||
updateNodeStatus(newNode, newNode.lastSeen);
|
||||
memberList.push_back(newNode);
|
||||
Serial.printf("[Cluster] Added node: %s @ %s | Status: %s | last update: 0\n",
|
||||
nodeHost.c_str(),
|
||||
newNode.ip.toString().c_str(),
|
||||
statusToStr(newNode.status));
|
||||
//fetchNodeInfo(nodeIP); // Do not fetch here, handled by periodic task
|
||||
}
|
||||
|
||||
void ClusterManager::fetchNodeInfo(const IPAddress& ip) {
|
||||
if(ip == ctx.localIP) {
|
||||
Serial.println("[Cluster] Skipping fetch for local node");
|
||||
return;
|
||||
}
|
||||
HTTPClient http;
|
||||
WiFiClient client;
|
||||
String url = "http://" + ip.toString() + ClusterProtocol::API_NODE_STATUS;
|
||||
http.begin(client, url);
|
||||
int httpCode = http.GET();
|
||||
if (httpCode == 200) {
|
||||
String payload = http.getString();
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, payload);
|
||||
if (!err) {
|
||||
auto& memberList = *ctx.memberList;
|
||||
for (auto& node : memberList) {
|
||||
if (node.ip == ip) {
|
||||
node.resources.freeHeap = doc["freeHeap"];
|
||||
node.resources.chipId = doc["chipId"];
|
||||
node.resources.sdkVersion = (const char*)doc["sdkVersion"];
|
||||
node.resources.cpuFreqMHz = doc["cpuFreqMHz"];
|
||||
node.resources.flashChipSize = doc["flashChipSize"];
|
||||
node.status = NodeInfo::ACTIVE;
|
||||
node.lastSeen = millis();
|
||||
node.apiEndpoints.clear();
|
||||
if (doc["api"].is<JsonArray>()) {
|
||||
JsonArray apiArr = doc["api"].as<JsonArray>();
|
||||
for (JsonObject apiObj : apiArr) {
|
||||
String uri = (const char*)apiObj["uri"];
|
||||
int method = apiObj["method"];
|
||||
node.apiEndpoints.push_back(std::make_tuple(uri, method));
|
||||
}
|
||||
}
|
||||
Serial.printf("[Cluster] Fetched info for node: %s @ %s\n", node.hostname.c_str(), ip.toString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Serial.printf("[Cluster] Failed to fetch info for node @ %s, HTTP code: %d\n", ip.toString().c_str(), httpCode);
|
||||
}
|
||||
http.end();
|
||||
}
|
||||
|
||||
void ClusterManager::heartbeatTaskCallback() {
|
||||
for (auto& node : *ctx.memberList) {
|
||||
if (node.hostname == ctx.hostname) {
|
||||
node.lastSeen = millis();
|
||||
node.status = NodeInfo::ACTIVE;
|
||||
updateLocalNodeResources();
|
||||
ctx.triggerEvent("node_discovered", &node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::updateAllMembersInfoTaskCallback() {
|
||||
auto& memberList = *ctx.memberList;
|
||||
for (const auto& node : memberList) {
|
||||
if (node.ip != ctx.localIP) {
|
||||
fetchNodeInfo(node.ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::updateAllNodeStatuses() {
|
||||
auto& memberList = *ctx.memberList;
|
||||
unsigned long now = millis();
|
||||
for (auto& node : memberList) {
|
||||
updateNodeStatus(node, now);
|
||||
node.latency = now - node.lastSeen;
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::removeDeadNodes() {
|
||||
auto& memberList = *ctx.memberList;
|
||||
unsigned long now = millis();
|
||||
for (size_t i = 0; i < memberList.size(); ) {
|
||||
unsigned long diff = now - memberList[i].lastSeen;
|
||||
if (memberList[i].status == NodeInfo::DEAD && diff > NODE_DEAD_THRESHOLD) {
|
||||
Serial.printf("[Cluster] Removing node: %s\n", memberList[i].hostname.c_str());
|
||||
memberList.erase(memberList.begin() + i);
|
||||
} else {
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::printMemberList() {
|
||||
auto& memberList = *ctx.memberList;
|
||||
if (memberList.empty()) {
|
||||
Serial.println("[Cluster] Member List: empty");
|
||||
return;
|
||||
}
|
||||
Serial.println("[Cluster] Member List:");
|
||||
for (const auto& node : memberList) {
|
||||
Serial.printf(" %s @ %s | Status: %s | last seen: %lu\n", node.hostname.c_str(), node.ip.toString().c_str(), statusToStr(node.status), millis() - node.lastSeen);
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::updateLocalNodeResources() {
|
||||
for (auto& node : *ctx.memberList) {
|
||||
if (node.hostname == ctx.hostname) {
|
||||
node.resources.freeHeap = ESP.getFreeHeap();
|
||||
node.resources.chipId = ESP.getChipId();
|
||||
node.resources.sdkVersion = String(ESP.getSdkVersion());
|
||||
node.resources.cpuFreqMHz = ESP.getCpuFreqMHz();
|
||||
node.resources.flashChipSize = ESP.getFlashChipSize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/ClusterManager.h
Normal file
26
src/ClusterManager.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "Globals.h"
|
||||
#include "ClusterContext.h"
|
||||
#include "NodeInfo.h"
|
||||
#include <Arduino.h>
|
||||
#include <ESP8266WiFi.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <ESP8266HTTPClient.h>
|
||||
|
||||
class ClusterManager {
|
||||
public:
|
||||
ClusterManager(ClusterContext& ctx);
|
||||
void sendDiscovery();
|
||||
void listenForDiscovery();
|
||||
void addOrUpdateNode(const String& nodeHost, IPAddress nodeIP);
|
||||
void updateAllNodeStatuses();
|
||||
void removeDeadNodes();
|
||||
void printMemberList();
|
||||
const std::vector<NodeInfo>& getMemberList() const { return *ctx.memberList; }
|
||||
void fetchNodeInfo(const IPAddress& ip);
|
||||
void updateLocalNodeResources();
|
||||
void heartbeatTaskCallback();
|
||||
void updateAllMembersInfoTaskCallback();
|
||||
private:
|
||||
ClusterContext& ctx;
|
||||
};
|
||||
54
src/NetworkManager.cpp
Normal file
54
src/NetworkManager.cpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "NetworkManager.h"
|
||||
|
||||
const char* STA_SSID = "shroud";
|
||||
const char* STA_PASS = "th3r31sn0sp00n";
|
||||
|
||||
NetworkManager::NetworkManager(ClusterContext& ctx) : ctx(ctx) {}
|
||||
|
||||
void NetworkManager::setHostnameFromMac() {
|
||||
uint8_t mac[6];
|
||||
WiFi.macAddress(mac);
|
||||
char buf[32];
|
||||
sprintf(buf, "esp-%02X%02X%02X%02X%02X%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
|
||||
WiFi.hostname(buf);
|
||||
ctx.hostname = String(buf);
|
||||
}
|
||||
|
||||
void NetworkManager::setupWiFi() {
|
||||
Serial.begin(115200);
|
||||
WiFi.mode(WIFI_STA);
|
||||
WiFi.begin(STA_SSID, STA_PASS);
|
||||
Serial.println("[WiFi] Connecting to AP...");
|
||||
unsigned long startAttemptTime = millis();
|
||||
while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < 15000) {
|
||||
delay(500);
|
||||
Serial.print(".");
|
||||
}
|
||||
if (WiFi.status() == WL_CONNECTED) {
|
||||
Serial.println();
|
||||
Serial.print("[WiFi] Connected to AP, IP: ");
|
||||
Serial.println(WiFi.localIP());
|
||||
} else {
|
||||
Serial.println();
|
||||
Serial.println("[WiFi] Failed to connect to AP. Creating AP...");
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP(STA_SSID, STA_PASS);
|
||||
Serial.print("[WiFi] AP created, IP: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
}
|
||||
setHostnameFromMac();
|
||||
ctx.udp->begin(4210);
|
||||
ctx.localIP = WiFi.localIP();
|
||||
ctx.hostname = WiFi.hostname();
|
||||
Serial.print("[WiFi] Hostname set to: ");
|
||||
Serial.println(ctx.hostname);
|
||||
Serial.print("[WiFi] UDP listening on port 4210\n");
|
||||
|
||||
// Register this node in the memberlist via event system
|
||||
NodeInfo self;
|
||||
self.hostname = ctx.hostname;
|
||||
self.ip = WiFi.localIP();
|
||||
self.lastSeen = millis();
|
||||
self.status = NodeInfo::ACTIVE;
|
||||
ctx.triggerEvent("node_discovered", &self);
|
||||
}
|
||||
12
src/NetworkManager.h
Normal file
12
src/NetworkManager.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include "ClusterContext.h"
|
||||
#include <ESP8266WiFi.h>
|
||||
|
||||
class NetworkManager {
|
||||
public:
|
||||
NetworkManager(ClusterContext& ctx);
|
||||
void setupWiFi();
|
||||
void setHostnameFromMac();
|
||||
private:
|
||||
ClusterContext& ctx;
|
||||
};
|
||||
21
src/NodeInfo.cpp
Normal file
21
src/NodeInfo.cpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#include "NodeInfo.h"
|
||||
|
||||
const char* statusToStr(NodeInfo::Status status) {
|
||||
switch (status) {
|
||||
case NodeInfo::ACTIVE: return "active";
|
||||
case NodeInfo::INACTIVE: return "inactive";
|
||||
case NodeInfo::DEAD: return "dead";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
void updateNodeStatus(NodeInfo &node, unsigned long now) {
|
||||
unsigned long diff = now - node.lastSeen;
|
||||
if (diff < NODE_INACTIVE_THRESHOLD) {
|
||||
node.status = NodeInfo::ACTIVE;
|
||||
} else if (diff < NODE_INACTIVE_THRESHOLD) {
|
||||
node.status = NodeInfo::INACTIVE;
|
||||
} else {
|
||||
node.status = NodeInfo::DEAD;
|
||||
}
|
||||
}
|
||||
24
src/NodeInfo.h
Normal file
24
src/NodeInfo.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
#include "Globals.h"
|
||||
#include <IPAddress.h>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
|
||||
struct NodeInfo {
|
||||
String hostname;
|
||||
IPAddress ip;
|
||||
unsigned long lastSeen;
|
||||
enum Status { ACTIVE, INACTIVE, DEAD } status;
|
||||
struct Resources {
|
||||
uint32_t freeHeap = 0;
|
||||
uint32_t chipId = 0;
|
||||
String sdkVersion;
|
||||
uint32_t cpuFreqMHz = 0;
|
||||
uint32_t flashChipSize = 0;
|
||||
} resources;
|
||||
unsigned long latency = 0; // ms since lastSeen
|
||||
std::vector<std::tuple<String, int>> apiEndpoints; // List of registered endpoints
|
||||
};
|
||||
|
||||
const char* statusToStr(NodeInfo::Status status);
|
||||
void updateNodeStatus(NodeInfo &node, unsigned long now = millis());
|
||||
169
src/RestApiServer.cpp
Normal file
169
src/RestApiServer.cpp
Normal file
@@ -0,0 +1,169 @@
|
||||
#include "RestApiServer.h"
|
||||
|
||||
RestApiServer::RestApiServer(ClusterContext& ctx, uint16_t port) : server(port), ctx(ctx) {}
|
||||
|
||||
void RestApiServer::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()) {
|
||||
(*ctx.memberList)[0].apiEndpoints.push_back(std::make_tuple(uri, method));
|
||||
}
|
||||
server.on(uri.c_str(), method, requestHandler);
|
||||
}
|
||||
|
||||
void RestApiServer::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()) {
|
||||
(*ctx.memberList)[0].apiEndpoints.push_back(std::make_tuple(uri, method));
|
||||
}
|
||||
server.on(uri.c_str(), method, requestHandler, uploadHandler);
|
||||
}
|
||||
|
||||
void RestApiServer::begin() {
|
||||
addEndpoint("/api/node/status", HTTP_GET,
|
||||
std::bind(&RestApiServer::getSystemStatusJson, this, std::placeholders::_1));
|
||||
addEndpoint("/api/cluster/members", HTTP_GET,
|
||||
std::bind(&RestApiServer::getClusterMembersJson, 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)
|
||||
);
|
||||
addEndpoint("/api/node/restart", HTTP_POST,
|
||||
std::bind(&RestApiServer::onRestartRequest, this, std::placeholders::_1));
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void RestApiServer::getSystemStatusJson(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);
|
||||
}
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void RestApiServer::getClusterMembersJson(AsyncWebServerRequest *request) {
|
||||
JsonDocument doc;
|
||||
JsonArray arr = doc["members"].to<JsonArray>();
|
||||
for (const auto& node : *ctx.memberList) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
String json;
|
||||
serializeJson(doc, json);
|
||||
request->send(200, "application/json", json);
|
||||
}
|
||||
|
||||
void RestApiServer::methodToStr(const std::tuple<String, int> &endpoint, ArduinoJson::V742PB22::JsonObject &apiObj)
|
||||
{
|
||||
int method = std::get<1>(endpoint);
|
||||
const char *methodStr = nullptr;
|
||||
switch (method)
|
||||
{
|
||||
case HTTP_GET:
|
||||
methodStr = "GET";
|
||||
break;
|
||||
case HTTP_POST:
|
||||
methodStr = "POST";
|
||||
break;
|
||||
case HTTP_PUT:
|
||||
methodStr = "PUT";
|
||||
break;
|
||||
case HTTP_DELETE:
|
||||
methodStr = "DELETE";
|
||||
break;
|
||||
case HTTP_PATCH:
|
||||
methodStr = "PATCH";
|
||||
break;
|
||||
default:
|
||||
methodStr = "UNKNOWN";
|
||||
break;
|
||||
}
|
||||
apiObj["method"] = methodStr;
|
||||
}
|
||||
|
||||
void RestApiServer::onFirmwareUpdateRequest(AsyncWebServerRequest *request) {
|
||||
bool hasError = !Update.hasError();
|
||||
AsyncWebServerResponse *response = request->beginResponse(200, "application/json", hasError ? "{\"status\": \"OK\"}" : "{\"status\": \"FAIL\"}");
|
||||
response->addHeader("Connection", "close");
|
||||
request->send(response);
|
||||
request->onDisconnect([]() {
|
||||
Serial.println("[API] Restart device");
|
||||
delay(10);
|
||||
ESP.restart();
|
||||
});
|
||||
}
|
||||
|
||||
void RestApiServer::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;
|
||||
} else {
|
||||
Update.printError(Serial);
|
||||
}
|
||||
}
|
||||
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 RestApiServer::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();
|
||||
});
|
||||
}
|
||||
35
src/RestApiServer.h
Normal file
35
src/RestApiServer.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
#include <Arduino.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include <Updater.h>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
#include <tuple>
|
||||
|
||||
#include "ClusterContext.h"
|
||||
#include "NodeInfo.h"
|
||||
|
||||
#define DEBUG_UPDATER 1
|
||||
|
||||
using namespace std;
|
||||
using namespace std::placeholders;
|
||||
|
||||
class RestApiServer {
|
||||
public:
|
||||
RestApiServer(ClusterContext& 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,
|
||||
std::function<void(AsyncWebServerRequest*, const String&, size_t, uint8_t*, size_t, bool)> uploadHandler);
|
||||
private:
|
||||
AsyncWebServer server;
|
||||
ClusterContext& ctx;
|
||||
std::vector<std::tuple<String, int>> serviceRegistry;
|
||||
void getClusterMembersJson(AsyncWebServerRequest *request);
|
||||
void methodToStr(const std::tuple<String, int> &endpoint, ArduinoJson::V742PB22::JsonObject &apiObj);
|
||||
void getSystemStatusJson(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);
|
||||
};
|
||||
5
src/TaskScheduler.cpp
Normal file
5
src/TaskScheduler.cpp
Normal file
@@ -0,0 +1,5 @@
|
||||
/*
|
||||
* https://github.com/arkhipenko/TaskScheduler/tree/master/examples/Scheduler_example16_Multitab
|
||||
*/
|
||||
|
||||
#include <TaskScheduler.h>
|
||||
40
src/main.cpp
Normal file
40
src/main.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include <Arduino.h>
|
||||
#include "Globals.h"
|
||||
#include "ClusterContext.h"
|
||||
#include "NetworkManager.h"
|
||||
#include "ClusterManager.h"
|
||||
#include "RestApiServer.h"
|
||||
|
||||
ClusterContext ctx;
|
||||
NetworkManager network(ctx);
|
||||
ClusterManager cluster(ctx);
|
||||
RestApiServer apiServer(ctx);
|
||||
|
||||
Task tSendDiscovery(TaskIntervals::SEND_DISCOVERY, TASK_FOREVER, [](){ cluster.sendDiscovery(); });
|
||||
Task tListenForDiscovery(TaskIntervals::LISTEN_FOR_DISCOVERY, TASK_FOREVER, [](){ cluster.listenForDiscovery(); });
|
||||
Task tUpdateStatus(TaskIntervals::UPDATE_STATUS, TASK_FOREVER, [](){ cluster.updateAllNodeStatuses(); cluster.removeDeadNodes(); });
|
||||
Task tPrintMemberList(TaskIntervals::PRINT_MEMBER_LIST, TASK_FOREVER, [](){ cluster.printMemberList(); });
|
||||
Task tHeartbeat(TaskIntervals::HEARTBEAT, TASK_FOREVER, [](){ cluster.heartbeatTaskCallback(); });
|
||||
Task tUpdateAllMembersInfo(TaskIntervals::UPDATE_ALL_MEMBERS_INFO, TASK_FOREVER, [](){ cluster.updateAllMembersInfoTaskCallback(); });
|
||||
|
||||
void setup() {
|
||||
network.setupWiFi();
|
||||
ctx.scheduler->init();
|
||||
ctx.scheduler->addTask(tSendDiscovery);
|
||||
ctx.scheduler->addTask(tListenForDiscovery);
|
||||
ctx.scheduler->addTask(tUpdateStatus);
|
||||
ctx.scheduler->addTask(tPrintMemberList);
|
||||
ctx.scheduler->addTask(tHeartbeat);
|
||||
ctx.scheduler->addTask(tUpdateAllMembersInfo);
|
||||
tSendDiscovery.enable();
|
||||
tListenForDiscovery.enable();
|
||||
tUpdateStatus.enable();
|
||||
tPrintMemberList.enable();
|
||||
tHeartbeat.enable();
|
||||
tUpdateAllMembersInfo.enable();
|
||||
apiServer.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
ctx.scheduler->execute();
|
||||
}
|
||||
11
test/README
Normal file
11
test/README
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
This directory is intended for PlatformIO Test Runner and project tests.
|
||||
|
||||
Unit Testing is a software testing method by which individual units of
|
||||
source code, sets of one or more MCU program modules together with associated
|
||||
control data, usage procedures, and operating procedures, are tested to
|
||||
determine whether they are fit for use. Unit testing finds problems early
|
||||
in the development cycle.
|
||||
|
||||
More information about PlatformIO Unit Testing:
|
||||
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||
Reference in New Issue
Block a user