feat(cluster, neopattern): add CLUSTER_EVENT and broadcast handling
Add CLUSTER_EVENT message type and end-to-end handling across cluster and NeoPattern example.\n\nCluster protocol / core:\n- Add ClusterProtocol::CLUSTER_EVENT_MSG\n- ClusterManager: register predicate/handler for CLUSTER_EVENT\n- Robust parsing: accept data as string or nested JSON; serialize nested data to string before firing\n- Add defensive null-termination for full UDP reads; log unknown message head if no handler matches\n- Logging: source IP, payload length, missing fields, and pre-fire event details\n\nNeoPatternService:\n- Constructor now accepts NodeContext and registers ctx.on(api/neopattern) handler\n- /api/neopattern: add optional boolean 'broadcast' flag\n- If broadcast=true: build event payload and send CLUSTER_EVENT over UDP (subnet-directed broadcast); log target and payload size; also fire locally so sender applies immediately\n- Implement applyControlParams with robust ArduinoJson is<T>() checks (replaces deprecated containsKey()) and flexible string/number parsing for color, brightness, steps, interval\n- Minor fixes: include Globals for ClusterProtocol, include ESP8266WiFi for broadcast IP calc, safer brightness clamping\n\nExample:\n- Update neopattern main to pass NodeContext into NeoPatternService\n\nResult:\n- Nodes receive and process CLUSTER_EVENT (api/neopattern) via ctx.fire/ctx.on\n- Broadcast reliably reaches peers; parsing handles both stringified and nested JSON data\n- Additional logs aid diagnosis of delivery/format issues and remove deprecation warnings
This commit is contained in:
@@ -41,7 +41,11 @@ void ClusterManager::listen() {
|
||||
if (len <= 0) {
|
||||
return;
|
||||
}
|
||||
incoming[len] = 0;
|
||||
if (len >= (int)ClusterProtocol::UDP_BUF_SIZE) {
|
||||
incoming[ClusterProtocol::UDP_BUF_SIZE - 1] = 0;
|
||||
} else {
|
||||
incoming[len] = 0;
|
||||
}
|
||||
handleIncomingMessage(incoming);
|
||||
}
|
||||
|
||||
@@ -51,6 +55,7 @@ void ClusterManager::initMessageHandlers() {
|
||||
messageHandlers.push_back({ &ClusterManager::isHeartbeatMsg, [this](const char* msg){ this->onHeartbeat(msg); }, "HEARTBEAT" });
|
||||
messageHandlers.push_back({ &ClusterManager::isResponseMsg, [this](const char* msg){ this->onResponse(msg); }, "RESPONSE" });
|
||||
messageHandlers.push_back({ &ClusterManager::isNodeInfoMsg, [this](const char* msg){ this->onNodeInfo(msg); }, "NODE_INFO" });
|
||||
messageHandlers.push_back({ &ClusterManager::isClusterEventMsg, [this](const char* msg){ this->onClusterEvent(msg); }, "CLUSTER_EVENT" });
|
||||
}
|
||||
|
||||
void ClusterManager::handleIncomingMessage(const char* incoming) {
|
||||
@@ -60,6 +65,15 @@ void ClusterManager::handleIncomingMessage(const char* incoming) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Unknown message - log first token
|
||||
const char* colon = strchr(incoming, ':');
|
||||
String head;
|
||||
if (colon) {
|
||||
head = String(incoming).substring(0, colon - incoming);
|
||||
} else {
|
||||
head = String(incoming);
|
||||
}
|
||||
LOG_DEBUG("Cluster", String("Unknown cluster message: ") + head);
|
||||
}
|
||||
|
||||
bool ClusterManager::isDiscoveryMsg(const char* msg) {
|
||||
@@ -78,6 +92,10 @@ bool ClusterManager::isNodeInfoMsg(const char* msg) {
|
||||
return strncmp(msg, ClusterProtocol::NODE_INFO_MSG, strlen(ClusterProtocol::NODE_INFO_MSG)) == 0;
|
||||
}
|
||||
|
||||
bool ClusterManager::isClusterEventMsg(const char* msg) {
|
||||
return strncmp(msg, ClusterProtocol::CLUSTER_EVENT_MSG, strlen(ClusterProtocol::CLUSTER_EVENT_MSG)) == 0;
|
||||
}
|
||||
|
||||
void ClusterManager::onDiscovery(const char* /*msg*/) {
|
||||
ctx.udp->beginPacket(ctx.udp->remoteIP(), ctx.config.udp_port);
|
||||
String response = String(ClusterProtocol::RESPONSE_MSG) + ":" + ctx.hostname;
|
||||
@@ -174,6 +192,50 @@ void ClusterManager::onNodeInfo(const char* msg) {
|
||||
}
|
||||
}
|
||||
|
||||
void ClusterManager::onClusterEvent(const char* msg) {
|
||||
// Message format: CLUSTER_EVENT:{"event":"...","data":"<json string>"}
|
||||
const char* jsonStart = msg + strlen(ClusterProtocol::CLUSTER_EVENT_MSG) + 1; // skip prefix and ':'
|
||||
if (*jsonStart == '\0') {
|
||||
LOG_DEBUG("Cluster", "CLUSTER_EVENT received with empty payload");
|
||||
return;
|
||||
}
|
||||
LOG_INFO("Cluster", String("CLUSTER_EVENT raw from ") + ctx.udp->remoteIP().toString() + " len=" + String(strlen(jsonStart)));
|
||||
JsonDocument doc;
|
||||
DeserializationError err = deserializeJson(doc, jsonStart);
|
||||
if (err) {
|
||||
LOG_DEBUG("Cluster", String("Failed to parse CLUSTER_EVENT JSON from ") + ctx.udp->remoteIP().toString());
|
||||
return;
|
||||
}
|
||||
// Robust extraction of event and data
|
||||
String eventStr;
|
||||
if (doc["event"].is<const char*>()) {
|
||||
eventStr = doc["event"].as<const char*>();
|
||||
} else if (doc["event"].is<String>()) {
|
||||
eventStr = doc["event"].as<String>();
|
||||
}
|
||||
|
||||
String data;
|
||||
if (doc["data"].is<const char*>()) {
|
||||
data = doc["data"].as<const char*>();
|
||||
} else if (doc["data"].is<JsonVariantConst>()) {
|
||||
// If data is a nested JSON object/array, serialize it back to string
|
||||
String tmp;
|
||||
serializeJson(doc["data"], tmp);
|
||||
data = tmp;
|
||||
}
|
||||
|
||||
if (eventStr.length() == 0 || data.length() == 0) {
|
||||
String dbg;
|
||||
serializeJson(doc, dbg);
|
||||
LOG_WARN("Cluster", String("CLUSTER_EVENT missing 'event' or 'data' | payload=") + dbg);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string eventKey(eventStr.c_str());
|
||||
LOG_INFO("Cluster", String("Firing event '") + eventStr + "' with dataLen=" + String(data.length()));
|
||||
ctx.fire(eventKey, &data);
|
||||
}
|
||||
|
||||
void ClusterManager::addOrUpdateNode(const String& nodeHost, IPAddress nodeIP) {
|
||||
auto& memberList = *ctx.memberList;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user