fix: protocol 2 core review findings

From the review of #99:
- A sync could stall when a client took a slot the moment another left:
  loop() used up the new client's sync on the old one. The slots move
  to ProtoSlots, a pure module with native tests, and loop() takes a
  slot's wants only for the client it read (takeWants).
- Deleting a group sends its shades, whose inGroup changed. Deleting a
  shade unlinks it from its groups and sends them, so a new shade given
  its id doesn't join them.
- next and sync with an id get an ok reply, and a message in pieces
  gets one bad request, not one per piece.
- Feeds check for loop() before they write the event buffer, and log
  an event too large, as events do.
- editChanges() (ModelEditor, native tests) says what clients get after
  an edit; the edit result carries the shade a group gained or lost.
  order() takes a sort_list_t.
- The frame writers move to ProtoFrame. Renames: pf to proto_field_t,
  the X-macro letters to TYPE/FIELD/ERR/FEED, proto_request_t to
  proto_message_t, REQ_* to WANTS_*, fmt to eventJson. The event
  bodies share sendItem(), the connection type name lives in
  ESPNetwork, and the loop() task check in SocketEmitter.
- e2e: the heap floor check skips when the low since boot is already
  under it; tests for replies to next and sync and a long message.
- SECURITY_PLAN.md splits step 1 into 1a-1d.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 13:17:15 +02:00
parent d869bb53d5
commit dc2f1478b6
29 changed files with 1087 additions and 670 deletions

View file

@ -1,10 +1,12 @@
#include <Arduino.h>
#include <WiFi.h>
#include <atomic>
#include <algorithm>
#include "esp_log.h"
#include "ProtoSocket.h"
#include "ProtoSync.h"
#include "ProtoFrame.h"
#include "ProtoParse.h"
#include "ProtoSlots.h"
#include "SomfySnapshot.h"
#include "Somfy.h"
#include "ConfigSettings.h"
@ -23,87 +25,62 @@ extern ConfigSettings settings;
extern ESPNetwork net;
extern Web webServer;
extern LoopStats loopStats;
extern SocketEmitter sockEmit;
ProtoSocket protoSocket;
static AsyncWebSocket ws("/ws");
static_assert(MAX_SOCK_RESPONSE >= PROTO_MAX_FRAME, "protocol 2 frames share the socket event buffer");
// What a client asked for, from async_tcp to loop().
enum : uint32_t {
REQ_START = 0x01, // just connected
REQ_NEXT = 0x02,
REQ_RESYNC = 0x04
};
struct proto_slot_t {
// Set and cleared on async_tcp; 0 when the slot is free.
std::atomic<uint32_t> asyncId{0};
std::atomic<uint32_t> requests{0};
std::atomic<uint8_t> feeds{0}; // a bit per feed number
std::atomic<bool> withSchema{true};
// loop() only.
uint32_t loopId = 0; // the client loop() is serving here
sync_cursor_t cur;
bool awaitingNext = false;
uint32_t sentAt = 0;
};
static proto_slot_t slots[PROTO_MAX_CLIENTS];
static BufferedJsonFormatter fmt;
static proto_slot_t *slotOf(uint32_t asyncId) {
for(proto_slot_t &s : slots)
if(s.asyncId.load() == asyncId) return &s;
return nullptr;
}
// What loop() formats events and syncEnd with, into the socket event buffer.
static BufferedJsonFormatter eventJson;
/*********************************************************************
* The shade model as a sync source
********************************************************************/
static void writeHeader(ProtoWriter &w) {
w.add(pf::maxRooms, (uint8_t)SOMFY_MAX_ROOMS);
w.add(pf::maxShades, (uint8_t)SOMFY_MAX_SHADES);
w.add(pf::maxGroups, (uint8_t)SOMFY_MAX_GROUPS);
w.add(pf::maxGroupedShades, (uint8_t)SOMFY_MAX_GROUPED_SHADES);
w.add(pf::maxLinkedRemotes, (uint8_t)SOMFY_MAX_LINKED_REMOTES);
w.add(pf::startingAddress, somfy.startingAddress);
w.add(proto_field_t::maxRooms, (uint8_t)SOMFY_MAX_ROOMS);
w.add(proto_field_t::maxShades, (uint8_t)SOMFY_MAX_SHADES);
w.add(proto_field_t::maxGroups, (uint8_t)SOMFY_MAX_GROUPS);
w.add(proto_field_t::maxGroupedShades, (uint8_t)SOMFY_MAX_GROUPED_SHADES);
w.add(proto_field_t::maxLinkedRemotes, (uint8_t)SOMFY_MAX_LINKED_REMOTES);
w.add(proto_field_t::startingAddress, somfy.startingAddress);
// What a new shade or group gets.
w.add(pf::bitLength, somfy.transceiver.config.type);
w.add(pf::proto, static_cast<uint8_t>(somfy.transceiver.config.proto));
w.add(pf::serverId, settings.serverId);
w.add(pf::hostname, settings.hostname);
w.add(pf::model, "ESPSomfyRTS");
w.add(pf::chipModel, settings.chipModel);
w.add(pf::fw, settings.fwVersion.name);
const char *connType = "Unknown";
if(net.connType == conn_types_t::ethernet) connType = "Ethernet";
else if(net.connType == conn_types_t::wifi) connType = "Wifi";
w.add(pf::connType, connType);
w.add(proto_field_t::bitLength, somfy.transceiver.config.type);
w.add(proto_field_t::proto, static_cast<uint8_t>(somfy.transceiver.config.proto));
w.add(proto_field_t::serverId, settings.serverId);
w.add(proto_field_t::hostname, settings.hostname);
w.add(proto_field_t::model, "ESPSomfyRTS");
w.add(proto_field_t::chipModel, settings.chipModel);
w.add(proto_field_t::fw, settings.fwVersion.name);
w.add(proto_field_t::connType, net.connTypeName());
}
static void writeMemory(ProtoWriter &w) {
w.add(pf::freeHeap, ESP.getFreeHeap());
w.add(pf::maxHeap, ESP.getMaxAllocHeap());
w.add(pf::minHeap, ESP.getMinFreeHeap());
w.add(pf::totalHeap, ESP.getHeapSize());
w.add(pf::uptime, (uint32_t)millis());
w.add(pf::loopRate, loopStats.publishedRate());
w.add(pf::loopMaxUs, loopStats.publishedMaxUs());
w.add(proto_field_t::freeHeap, ESP.getFreeHeap());
w.add(proto_field_t::maxHeap, ESP.getMaxAllocHeap());
w.add(proto_field_t::minHeap, ESP.getMinFreeHeap());
w.add(proto_field_t::totalHeap, ESP.getHeapSize());
w.add(proto_field_t::uptime, (uint32_t)millis());
w.add(proto_field_t::loopRate, loopStats.publishedRate());
w.add(proto_field_t::loopMaxUs, loopStats.publishedMaxUs());
}
static void writeWifi(ProtoWriter &w) {
if(WiFi.status() == WL_CONNECTED) {
w.add(pf::ssid, WiFi.SSID().c_str());
w.add(pf::strength, (int32_t)WiFi.RSSI());
w.add(pf::channel, (int32_t)net.channel);
w.add(proto_field_t::ssid, WiFi.SSID().c_str());
w.add(proto_field_t::strength, (int32_t)WiFi.RSSI());
w.add(proto_field_t::channel, (int32_t)net.channel);
}
else {
w.add(pf::strength, (int32_t)-100);
w.add(pf::channel, (int32_t)-1);
w.add(proto_field_t::strength, (int32_t)-100);
w.add(proto_field_t::channel, (int32_t)-1);
}
}
static void writeEthernet(ProtoWriter &w) {
#if HAS_ETHERNET
if(net.connType == conn_types_t::ethernet) {
w.add(pf::connected, net.connected());
w.add(pf::speed, (uint32_t)ETH.linkSpeed());
w.add(pf::fullDuplex, ETH.fullDuplex());
w.add(proto_field_t::connected, net.connected());
w.add(proto_field_t::speed, (uint32_t)ETH.linkSpeed());
w.add(proto_field_t::fullDuplex, ETH.fullDuplex());
}
#else
(void)w;
@ -162,13 +139,10 @@ static void replyError(AsyncWebSocketClient *client, uint32_t id, proto_error_t
writeErrorReply(json, e.id, e.code, e.desc);
}, &e);
}
// Only a message with an id wants a reply.
static void replyOk(AsyncWebSocketClient *client, uint32_t id) {
if(id == 0) return;
sendFixed(client, [](JsonFormatter &json, void *ctx) {
beginFrame(json, proto_type_t::reply, *static_cast<uint32_t *>(ctx));
ProtoWriter(json).add(pf::ok, true);
endFrame(json);
}, &id);
sendFixed(client, [](JsonFormatter &json, void *ctx) { writeOkReply(json, *static_cast<uint32_t *>(ctx)); }, &id);
}
static bool authorized(AsyncWebServerRequest *request) {
if(settings.Security.type == security_types::None) return true;
@ -180,68 +154,65 @@ static bool authorized(AsyncWebServerRequest *request) {
}
static void onConnect(AsyncWebSocketClient *client, AsyncWebServerRequest *request) {
const uint32_t id = client->id();
proto_slot_t *slot = nullptr;
for(proto_slot_t &s : slots) {
uint32_t expected = 0;
if(s.asyncId.compare_exchange_strong(expected, id)) { slot = &s; break; }
}
const AsyncWebParameter *schema = request ? request->getParam("schema") : nullptr;
const bool withSchema = !(schema && schema->value() == protoSchemaHash());
proto_slot_t *slot = claimSlot(slots, PROTO_MAX_CLIENTS, id, withSchema);
if(!slot) {
ESP_LOGW(TAG, "Socket [%lu] refused: all %u clients connected", (unsigned long)id, PROTO_MAX_CLIENTS);
client->close();
return;
}
const AsyncWebParameter *schema = request ? request->getParam("schema") : nullptr;
slot->withSchema = !(schema && schema->value() == protoSchemaHash());
slot->feeds = 0;
client->setCloseClientOnQueueFull(true);
sendFixed(client, [](JsonFormatter &json, void *) { writeHello(json, settings.fwVersion.name); }, nullptr);
slot->requests.fetch_or(REQ_START);
startSync(*slot);
IPAddress ip = client->remoteIP();
ESP_LOGI(TAG, "Socket [%lu] connected from %u.%u.%u.%u%s", (unsigned long)id, ip[0], ip[1], ip[2], ip[3], slot->withSchema ? "" : " (has the schema)");
ESP_LOGI(TAG, "Socket [%lu] connected from %u.%u.%u.%u%s", (unsigned long)id, ip[0], ip[1], ip[2], ip[3], withSchema ? "" : " (has the schema)");
}
static void onDisconnect(AsyncWebSocketClient *client) {
proto_slot_t *slot = slotOf(client->id());
proto_slot_t *slot = slotOf(slots, PROTO_MAX_CLIENTS, client->id());
if(!slot) return;
slot->requests = 0;
slot->feeds = 0;
slot->asyncId = 0;
freeSlot(*slot);
ESP_LOGI(TAG, "Socket [%lu] disconnected", (unsigned long)client->id());
}
static void onFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *data, size_t len) {
if(!(info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT)) {
replyError(client, 0, proto_error_t::badRequest, "Send each message as one text frame");
// One reply for the whole message: its first piece gets it, the rest are
// dropped. A frame larger than a TCP segment arrives in pieces too.
if(info->num == 0 && info->index == 0) replyError(client, 0, proto_error_t::badRequest, "Send each message as one text frame");
return;
}
proto_slot_t *slot = slotOf(client->id());
proto_slot_t *slot = slotOf(slots, PROTO_MAX_CLIENTS, client->id());
if(!slot) return;
JsonDocument doc;
proto_request_t req;
const proto_error_t err = parseClientFrame(reinterpret_cast<const char *>(data), len, doc, req);
proto_message_t msg;
const proto_error_t err = parseClientFrame(reinterpret_cast<const char *>(data), len, doc, msg);
if(err != proto_error_t::none) {
replyError(client, req.id, err, err == proto_error_t::unknownOp ? "Unknown message type" : "Malformed message");
replyError(client, msg.id, err, err == proto_error_t::unknownOp ? "Unknown message type" : "Malformed message");
return;
}
switch(static_cast<proto_type_t>(req.type)) {
switch(static_cast<proto_type_t>(msg.type)) {
case proto_type_t::next:
slot->requests.fetch_or(REQ_NEXT);
slot->wants.fetch_or(WANTS_NEXT);
replyOk(client, msg.id);
return;
case proto_type_t::sync:
slot->requests.fetch_or(REQ_RESYNC);
slot->wants.fetch_or(WANTS_SYNC_AGAIN);
replyOk(client, msg.id);
return;
case proto_type_t::subscribe:
case proto_type_t::unsubscribe: {
const int32_t feed = protoInt(doc["d"], pf::feed);
const int32_t feed = protoInt(doc["d"], proto_field_t::feed);
bool known = false;
for(uint8_t i = 0; i < protoFeedCount(); i++) known |= protoFeedAt(i).number == feed;
if(!known) { replyError(client, req.id, proto_error_t::badRequest, "Unknown feed"); return; }
if(req.type == static_cast<uint8_t>(proto_type_t::subscribe)) slot->feeds.fetch_or(1 << feed);
if(!known) { replyError(client, msg.id, proto_error_t::badRequest, "Unknown feed"); return; }
if(msg.type == static_cast<uint8_t>(proto_type_t::subscribe)) slot->feeds.fetch_or(1 << feed);
else slot->feeds.fetch_and(~(1 << feed));
replyOk(client, req.id);
replyOk(client, msg.id);
return;
}
default:
// The ops arrive with the protocol core's later PRs.
replyError(client, req.id, proto_error_t::unknownOp, "Unknown message type");
replyError(client, msg.id, proto_error_t::unknownOp, "Unknown message type");
return;
}
}
@ -256,17 +227,16 @@ void ProtoSocket::wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
}
void ProtoSocket::begin(AsyncWebServer &server) {
this->_owner = xTaskGetCurrentTaskHandle();
ws.handleHandshake(authorized);
ws.onEvent(ProtoSocket::wsEvent);
server.addHandler(&ws);
}
bool ProtoSocket::onLoopTask() const { return emitAllowed(xTaskGetCurrentTaskHandle(), this->_owner); }
bool ProtoSocket::hasClients() const {
for(const proto_slot_t &s : slots)
if(s.asyncId.load()) return true;
if(s.clientId.load()) return true;
return false;
}
bool ProtoSocket::sending() const { return sockEmit.onLoopTask() && this->hasClients(); }
/*********************************************************************
* loop(): syncs
@ -276,39 +246,26 @@ static void sendSyncFrame(proto_slot_t &s) {
char *buf = socketFrameBuffer();
const size_t len = writeSyncFrame(src, s.cur, buf, PROTO_MAX_FRAME);
if(len) ws.text(s.loopId, buf, len);
if(s.cur.active()) {
s.awaitingNext = true;
s.sentAt = millis();
return;
}
fmt.setBuffer(buf, PROTO_MAX_FRAME);
beginFrame(fmt, proto_type_t::syncEnd);
endFrame(fmt);
ws.text(s.loopId, buf, fmt.length());
s.awaitingNext = false;
syncFrameSent(s, millis());
if(s.cur.active()) return;
eventJson.setBuffer(buf, PROTO_MAX_FRAME);
beginFrame(eventJson, proto_type_t::syncEnd);
endFrame(eventJson);
ws.text(s.loopId, buf, eventJson.length());
}
void ProtoSocket::loop() {
if(!this->onLoopTask()) return;
if(!sockEmit.onLoopTask()) return;
for(proto_slot_t &s : slots) {
const uint32_t id = s.asyncId.load();
if(id != s.loopId) {
// A client left or another took the slot.
s.loopId = id;
s.cur = sync_cursor_t();
s.awaitingNext = false;
}
if(!id) continue;
const uint32_t req = s.requests.exchange(0);
if(req & (REQ_START | REQ_RESYNC)) {
s.cur = beginSync((req & REQ_START) && s.withSchema.load());
sendSyncFrame(s);
}
else if((req & REQ_NEXT) && s.awaitingNext) sendSyncFrame(s);
else if(s.awaitingNext && millis() - s.sentAt > PROTO_SYNC_TIMEOUT_MS) {
ESP_LOGW(TAG, "Socket [%lu] sent no next for %us, closing it", (unsigned long)id, PROTO_SYNC_TIMEOUT_MS / 1000);
s.awaitingNext = false;
s.cur = sync_cursor_t();
ws.close(id);
switch(nextSyncStep(s, millis())) {
case sync_step_t::send:
sendSyncFrame(s);
break;
case sync_step_t::timedOut:
ESP_LOGW(TAG, "Socket [%lu] sent no next for %us, closing it", (unsigned long)s.loopId, PROTO_SYNC_TIMEOUT_MS / 1000);
ws.close(s.loopId);
break;
case sync_step_t::none:
break;
}
}
ws.cleanupClients(PROTO_MAX_CLIENTS);
@ -317,154 +274,148 @@ void ProtoSocket::loop() {
/*********************************************************************
* loop(): events
********************************************************************/
JsonFormatter &ProtoSocket::beginEvent(proto_type_t type) {
fmt.setBuffer(socketFrameBuffer(), PROTO_MAX_FRAME);
beginFrame(fmt, type);
return fmt;
static JsonFormatter &beginEvent(proto_type_t type) {
eventJson.setBuffer(socketFrameBuffer(), PROTO_MAX_FRAME);
beginFrame(eventJson, type);
return eventJson;
}
void ProtoSocket::endEvent() {
endFrame(fmt);
if(fmt.overflowed()) {
ESP_LOGE(TAG, "Event too large for %u bytes, not sent", PROTO_MAX_FRAME);
return;
}
ws.textAll(socketFrameBuffer(), fmt.length());
}
// Each event checks the task and the clients first, so nothing is formatted
// for nobody.
#define PROTO_EVENT_GUARD() if(!this->onLoopTask() || !this->hasClients()) return
void ProtoSocket::shadeState(SomfyShade &shade) {
PROTO_EVENT_GUARD();
shade_snapshot_t s;
snapshotShade(shade, s);
ProtoWriter w(this->beginEvent(proto_type_t::shadeState));
protoShadeState(w, s);
this->endEvent();
}
void ProtoSocket::shade(SomfyShade &shade) {
PROTO_EVENT_GUARD();
shade_snapshot_t s;
snapshotShade(shade, s);
ProtoWriter w(this->beginEvent(proto_type_t::shade));
protoShade(w, s);
this->endEvent();
}
void ProtoSocket::shadeRemoved(uint8_t shadeId) {
PROTO_EVENT_GUARD();
ProtoWriter(this->beginEvent(proto_type_t::shadeRemoved)).add(pf::shadeId, shadeId);
this->endEvent();
}
void ProtoSocket::group(SomfyGroup &group) {
PROTO_EVENT_GUARD();
group_snapshot_t g;
snapshotGroup(group, g);
ProtoWriter w(this->beginEvent(proto_type_t::group));
protoGroup(w, g);
this->endEvent();
}
void ProtoSocket::groupState(SomfyGroup &group) {
PROTO_EVENT_GUARD();
group_snapshot_t g;
snapshotGroup(group, g);
ProtoWriter w(this->beginEvent(proto_type_t::groupState));
protoGroupState(w, g.groupId, g.flags, group.direction);
this->endEvent();
}
void ProtoSocket::groupRemoved(uint8_t groupId) {
PROTO_EVENT_GUARD();
ProtoWriter(this->beginEvent(proto_type_t::groupRemoved)).add(pf::groupId, groupId);
this->endEvent();
}
void ProtoSocket::room(SomfyRoom &room) {
PROTO_EVENT_GUARD();
room_snapshot_t r;
snapshotRoom(room, r);
ProtoWriter w(this->beginEvent(proto_type_t::room));
protoRoom(w, r);
this->endEvent();
}
void ProtoSocket::roomRemoved(uint8_t roomId) {
PROTO_EVENT_GUARD();
ProtoWriter(this->beginEvent(proto_type_t::roomRemoved)).add(pf::roomId, roomId);
this->endEvent();
}
void ProtoSocket::order(proto_type_t type) {
PROTO_EVENT_GUARD();
struct entry_t { int8_t order; uint8_t id; };
entry_t items[SOMFY_MAX_SHADES];
uint8_t n = 0;
if(type == proto_type_t::roomOrder) {
for(SomfyRoom &r : somfy.rooms) if(r.roomId != 0) items[n++] = {r.sortOrder, r.roomId};
}
else if(type == proto_type_t::shadeOrder) {
for(SomfyShade &s : somfy.shades) if(s.getShadeId() != 255) items[n++] = {s.sortOrder, s.getShadeId()};
}
else {
for(SomfyGroup &g : somfy.groups) if(g.getGroupId() != 255) items[n++] = {g.sortOrder, g.getGroupId()};
}
for(uint8_t i = 1; i < n; i++)
for(uint8_t j = i; j > 0 && items[j].order < items[j - 1].order; j--) {
const entry_t t = items[j];
items[j] = items[j - 1];
items[j - 1] = t;
}
ProtoWriter w(this->beginEvent(type));
w.beginList(pf::ids);
for(uint8_t i = 0; i < n; i++) w.item(items[i].id);
w.endList();
this->endEvent();
}
void ProtoSocket::repeaters() {
PROTO_EVENT_GUARD();
ProtoWriter w(this->beginEvent(proto_type_t::repeaters));
w.beginList(pf::repeaters);
for(uint32_t address : somfy.repeaters) if(address != 0) w.item(address);
w.endList();
this->endEvent();
}
void ProtoSocket::shadeCommand(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress) {
PROTO_EVENT_GUARD();
ProtoWriter w(this->beginEvent(proto_type_t::shadeCommand));
w.add(pf::shadeId, shade.getShadeId());
w.add(pf::remoteAddress, shade.getRemoteAddress());
w.add(pf::command, command);
w.add(pf::source, source);
w.add(pf::lastRollingCode, shade.lastRollingCode);
w.add(pf::sourceAddress, sourceAddress);
this->endEvent();
}
void ProtoSocket::memory() {
PROTO_EVENT_GUARD();
ProtoWriter w(this->beginEvent(proto_type_t::memory));
writeMemory(w);
this->endEvent();
}
void ProtoSocket::wifi() {
PROTO_EVENT_GUARD();
ProtoWriter w(this->beginEvent(proto_type_t::wifi));
writeWifi(w);
this->endEvent();
}
void ProtoSocket::ethernet() {
PROTO_EVENT_GUARD();
ProtoWriter w(this->beginEvent(proto_type_t::ethernet));
writeEthernet(w);
this->endEvent();
}
bool ProtoSocket::subscribed(proto_feed_t feed) const {
const uint8_t bit = 1 << static_cast<uint8_t>(feed);
for(const proto_slot_t &s : slots)
if(s.asyncId.load() && (s.feeds.load() & bit)) return true;
// Closes the event; false when it didn't fit, which is logged.
static bool finishEvent() {
endFrame(eventJson);
if(!eventJson.overflowed()) return true;
ESP_LOGE(TAG, "Event too large for %u bytes, not sent", PROTO_MAX_FRAME);
return false;
}
ProtoWriter ProtoSocket::beginFeed(proto_type_t type) { return ProtoWriter(this->beginEvent(type)); }
static void sendEvent() {
if(finishEvent()) ws.textAll(socketFrameBuffer(), eventJson.length());
}
// An item whole, or only its state: a snapshot of it, written by write.
template<typename Model, typename Snapshot>
static void sendItem(proto_type_t type, Model &item, void (*snapshot)(Model &, Snapshot &), void (*write)(ProtoWriter &, const Snapshot &)) {
Snapshot s;
snapshot(item, s);
ProtoWriter w(beginEvent(type));
write(w, s);
sendEvent();
}
void ProtoSocket::shadeState(SomfyShade &shade) {
if(this->sending()) sendItem(proto_type_t::shadeState, shade, snapshotShade, protoShadeState);
}
void ProtoSocket::shade(SomfyShade &shade) {
if(this->sending()) sendItem(proto_type_t::shade, shade, snapshotShade, protoShade);
}
void ProtoSocket::shadeRemoved(uint8_t shadeId) {
if(!this->sending()) return;
ProtoWriter(beginEvent(proto_type_t::shadeRemoved)).add(proto_field_t::shadeId, shadeId);
sendEvent();
}
void ProtoSocket::group(SomfyGroup &group) {
if(this->sending()) sendItem(proto_type_t::group, group, snapshotGroup, protoGroup);
}
void ProtoSocket::groupState(SomfyGroup &group) {
if(!this->sending()) return;
group_snapshot_t g;
snapshotGroup(group, g);
ProtoWriter w(beginEvent(proto_type_t::groupState));
protoGroupState(w, g.groupId, g.flags, group.direction);
sendEvent();
}
void ProtoSocket::groupRemoved(uint8_t groupId) {
if(!this->sending()) return;
ProtoWriter(beginEvent(proto_type_t::groupRemoved)).add(proto_field_t::groupId, groupId);
sendEvent();
}
void ProtoSocket::room(SomfyRoom &room) {
if(this->sending()) sendItem(proto_type_t::room, room, snapshotRoom, protoRoom);
}
void ProtoSocket::roomRemoved(uint8_t roomId) {
if(!this->sending()) return;
ProtoWriter(beginEvent(proto_type_t::roomRemoved)).add(proto_field_t::roomId, roomId);
sendEvent();
}
void ProtoSocket::order(sort_list_t list) {
if(!this->sending()) return;
struct entry_t { int8_t order; uint8_t id; };
static constexpr uint8_t MOST = std::max({SOMFY_MAX_ROOMS, SOMFY_MAX_SHADES, SOMFY_MAX_GROUPS});
entry_t items[MOST];
uint8_t n = 0;
proto_type_t type;
switch(list) {
case sort_list_t::rooms:
type = proto_type_t::roomOrder;
for(SomfyRoom &r : somfy.rooms) if(r.roomId != 0) items[n++] = {r.sortOrder, r.roomId};
break;
case sort_list_t::shades:
type = proto_type_t::shadeOrder;
for(SomfyShade &s : somfy.shades) if(s.getShadeId() != 255) items[n++] = {s.sortOrder, s.getShadeId()};
break;
case sort_list_t::groups:
type = proto_type_t::groupOrder;
for(SomfyGroup &g : somfy.groups) if(g.getGroupId() != 255) items[n++] = {g.sortOrder, g.getGroupId()};
break;
default:
return;
}
for(uint8_t i = 1; i < n; i++)
for(uint8_t j = i; j > 0 && items[j].order < items[j - 1].order; j--) std::swap(items[j], items[j - 1]);
ProtoWriter w(beginEvent(type));
w.beginList(proto_field_t::ids);
for(uint8_t i = 0; i < n; i++) w.item(items[i].id);
w.endList();
sendEvent();
}
void ProtoSocket::repeaters() {
if(!this->sending()) return;
ProtoWriter w(beginEvent(proto_type_t::repeaters));
w.beginList(proto_field_t::repeaters);
for(uint32_t address : somfy.repeaters) if(address != 0) w.item(address);
w.endList();
sendEvent();
}
void ProtoSocket::shadeCommand(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress) {
if(!this->sending()) return;
ProtoWriter w(beginEvent(proto_type_t::shadeCommand));
w.add(proto_field_t::shadeId, shade.getShadeId());
w.add(proto_field_t::remoteAddress, shade.getRemoteAddress());
w.add(proto_field_t::command, command);
w.add(proto_field_t::source, source);
w.add(proto_field_t::lastRollingCode, shade.lastRollingCode);
w.add(proto_field_t::sourceAddress, sourceAddress);
sendEvent();
}
void ProtoSocket::memory() {
if(!this->sending()) return;
ProtoWriter w(beginEvent(proto_type_t::memory));
writeMemory(w);
sendEvent();
}
void ProtoSocket::wifi() {
if(!this->sending()) return;
ProtoWriter w(beginEvent(proto_type_t::wifi));
writeWifi(w);
sendEvent();
}
void ProtoSocket::ethernet() {
if(!this->sending()) return;
ProtoWriter w(beginEvent(proto_type_t::ethernet));
writeEthernet(w);
sendEvent();
}
bool ProtoSocket::subscribed(proto_feed_t feed) const {
// The feed is formatted into the event buffer, which is loop()'s.
if(!sockEmit.onLoopTask()) return false;
const uint8_t bit = 1 << static_cast<uint8_t>(feed);
for(const proto_slot_t &s : slots)
if(s.clientId.load() && (s.feeds.load() & bit)) return true;
return false;
}
ProtoWriter ProtoSocket::beginFeed(proto_type_t type) { return ProtoWriter(beginEvent(type)); }
void ProtoSocket::endFeed(proto_feed_t feed) {
endFrame(fmt);
if(fmt.overflowed() || !this->onLoopTask()) return;
if(!finishEvent()) return;
const uint8_t bit = 1 << static_cast<uint8_t>(feed);
for(proto_slot_t &s : slots) {
const uint32_t id = s.asyncId.load();
if(id && (s.feeds.load() & bit)) ws.text(id, socketFrameBuffer(), fmt.length());
const uint32_t id = s.clientId.load();
if(id && (s.feeds.load() & bit)) ws.text(id, socketFrameBuffer(), eventJson.length());
}
}