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

@ -2,6 +2,7 @@
#define PROTO_SOCKET_H
#include <ESPAsyncWebServer.h>
#include "ProtoSchema.h"
#include "ModelEditor.h"
class SomfyShade;
class SomfyGroup;
@ -13,11 +14,8 @@ class SomfyRoom;
//
// async_tcp only reads what clients send and answers with fixed frames that
// hold nothing of the shade model (hello, errors, ok). Everything formatted
// from the model goes out from loop(), in the socket event buffer.
#define PROTO_MAX_CLIENTS 5
// How long a client may take to ask for the next sync frame.
#define PROTO_SYNC_TIMEOUT_MS 30000
// from the model goes out from loop(), in the socket event buffer. The
// clients' slots and syncs are in ProtoSlots.
class ProtoSocket {
public:
// Adds /ws to the server. From setup(), on loop()'s task.
@ -34,24 +32,23 @@ class ProtoSocket {
void groupRemoved(uint8_t groupId);
void room(SomfyRoom &room);
void roomRemoved(uint8_t roomId);
// roomOrder, shadeOrder or groupOrder: the ids, in sort order.
void order(proto_type_t type);
// roomOrder, shadeOrder or groupOrder: the list's ids, in sort order.
void order(sort_list_t list);
void repeaters();
void shadeCommand(SomfyShade &shade, const char *command, const char *source, uint32_t sourceAddress);
void memory();
void wifi();
void ethernet();
// A feed event: write its fields into the writer between the two. Only
// when someone subscribed, so check subscribed() first.
// when subscribed() says someone is, which is also only on loop().
bool subscribed(proto_feed_t feed) const;
ProtoWriter beginFeed(proto_type_t type);
void endFeed(proto_feed_t feed);
static void wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
private:
TaskHandle_t _owner = nullptr;
bool onLoopTask() const;
JsonFormatter &beginEvent(proto_type_t type);
void endEvent();
// On loop()'s task, with a client to send to: nothing is formatted for
// nobody, or off the task that owns the event buffer.
bool sending() const;
};
extern ProtoSocket protoSocket;
#endif