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