mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
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:
parent
d869bb53d5
commit
dc2f1478b6
29 changed files with 1087 additions and 670 deletions
|
|
@ -15,155 +15,155 @@
|
|||
|
||||
// Message types: 1-15 control, 16-127 client ops, 128-255 device events. A
|
||||
// number is never reused; a retired type stays reserved.
|
||||
// T(name, number, sender): sender is 'c' client, 'd' device, 'b' both.
|
||||
#define PROTO_TYPES(T) \
|
||||
T(hello, 1, 'd') \
|
||||
T(reply, 2, 'd') \
|
||||
T(next, 3, 'c') \
|
||||
T(sync, 4, 'b') \
|
||||
T(syncEnd, 5, 'd') \
|
||||
T(subscribe, 6, 'c') \
|
||||
T(unsubscribe, 7, 'c') \
|
||||
T(shadeState, 128, 'd') \
|
||||
T(shade, 129, 'd') \
|
||||
T(shadeRemoved, 130, 'd') \
|
||||
T(group, 131, 'd') \
|
||||
T(groupState, 132, 'd') \
|
||||
T(groupRemoved, 133, 'd') \
|
||||
T(room, 134, 'd') \
|
||||
T(roomRemoved, 135, 'd') \
|
||||
T(roomOrder, 136, 'd') \
|
||||
T(shadeOrder, 137, 'd') \
|
||||
T(groupOrder, 138, 'd') \
|
||||
T(shadeCommand, 139, 'd') \
|
||||
T(memory, 140, 'd') \
|
||||
T(wifi, 141, 'd') \
|
||||
T(ethernet, 142, 'd') \
|
||||
T(frame, 143, 'd') \
|
||||
T(frequencyScan, 144, 'd') \
|
||||
T(repeaters, 145, 'd')
|
||||
// TYPE(name, number, sender): sender is 'c' client, 'd' device, 'b' both.
|
||||
#define PROTO_TYPES(TYPE) \
|
||||
TYPE(hello, 1, 'd') \
|
||||
TYPE(reply, 2, 'd') \
|
||||
TYPE(next, 3, 'c') \
|
||||
TYPE(sync, 4, 'b') \
|
||||
TYPE(syncEnd, 5, 'd') \
|
||||
TYPE(subscribe, 6, 'c') \
|
||||
TYPE(unsubscribe, 7, 'c') \
|
||||
TYPE(shadeState, 128, 'd') \
|
||||
TYPE(shade, 129, 'd') \
|
||||
TYPE(shadeRemoved, 130, 'd') \
|
||||
TYPE(group, 131, 'd') \
|
||||
TYPE(groupState, 132, 'd') \
|
||||
TYPE(groupRemoved, 133, 'd') \
|
||||
TYPE(room, 134, 'd') \
|
||||
TYPE(roomRemoved, 135, 'd') \
|
||||
TYPE(roomOrder, 136, 'd') \
|
||||
TYPE(shadeOrder, 137, 'd') \
|
||||
TYPE(groupOrder, 138, 'd') \
|
||||
TYPE(shadeCommand, 139, 'd') \
|
||||
TYPE(memory, 140, 'd') \
|
||||
TYPE(wifi, 141, 'd') \
|
||||
TYPE(ethernet, 142, 'd') \
|
||||
TYPE(frame, 143, 'd') \
|
||||
TYPE(frequencyScan, 144, 'd') \
|
||||
TYPE(repeaters, 145, 'd')
|
||||
|
||||
// Fields, in one dictionary for every type. F(name, key, kind, default):
|
||||
// Fields, in one dictionary for every type. FIELD(name, key, kind, default):
|
||||
// kind is b bool (sent as 1/0), i integer, n number with a fraction, s string,
|
||||
// l list, o object. A field at its default is left out; a string's default is
|
||||
// "" and a list's is empty. hello's and the schema's own keys (fw, sh, T, F,
|
||||
// E, Q) are fixed, as a client needs them before it has the schema.
|
||||
#define PROTO_FIELDS(F) \
|
||||
#define PROTO_FIELDS(FIELD) \
|
||||
/* the schema and the sync */ \
|
||||
F(types, "T", 'l', 0) \
|
||||
F(fields, "F", 'l', 0) \
|
||||
F(errors, "E", 'l', 0) \
|
||||
F(feeds, "Q", 'l', 0) \
|
||||
F(header, "h", 'o', 0) \
|
||||
F(rooms, "R", 'l', 0) \
|
||||
F(shades, "S", 'l', 0) \
|
||||
F(groups, "G", 'l', 0) \
|
||||
F(repeaters, "X", 'l', 0) \
|
||||
F(status, "Z", 'o', 0) \
|
||||
F(fw, "fw", 's', 0) \
|
||||
F(schemaHash, "sh", 's', 0) \
|
||||
FIELD(types, "T", 'l', 0) \
|
||||
FIELD(fields, "F", 'l', 0) \
|
||||
FIELD(errors, "E", 'l', 0) \
|
||||
FIELD(feeds, "Q", 'l', 0) \
|
||||
FIELD(header, "h", 'o', 0) \
|
||||
FIELD(rooms, "R", 'l', 0) \
|
||||
FIELD(shades, "S", 'l', 0) \
|
||||
FIELD(groups, "G", 'l', 0) \
|
||||
FIELD(repeaters, "X", 'l', 0) \
|
||||
FIELD(status, "Z", 'o', 0) \
|
||||
FIELD(fw, "fw", 's', 0) \
|
||||
FIELD(schemaHash, "sh", 's', 0) \
|
||||
/* replies and control */ \
|
||||
F(ok, "k", 'b', 0) \
|
||||
F(code, "e", 'i', 0) \
|
||||
F(desc, "x", 's', 0) \
|
||||
F(feed, "fe", 'i', 0) \
|
||||
F(ids, "is", 'l', 0) \
|
||||
FIELD(ok, "k", 'b', 0) \
|
||||
FIELD(code, "e", 'i', 0) \
|
||||
FIELD(desc, "x", 's', 0) \
|
||||
FIELD(feed, "fe", 'i', 0) \
|
||||
FIELD(ids, "is", 'l', 0) \
|
||||
/* rooms, shades and groups */ \
|
||||
F(shadeId, "s", 'i', 0) \
|
||||
F(groupId, "g", 'i', 0) \
|
||||
F(roomId, "r", 'i', 0) \
|
||||
F(name, "n", 's', 0) \
|
||||
F(remoteAddress, "a", 'i', 0) \
|
||||
F(lastRollingCode, "c", 'i', 0) \
|
||||
F(sortOrder, "o", 'i', 0) \
|
||||
F(flags, "f", 'i', 0) \
|
||||
F(position, "p", 'i', 0) \
|
||||
F(target, "t", 'i', 0) \
|
||||
F(direction, "d", 'i', 0) \
|
||||
F(myPos, "m", 'i', -1) \
|
||||
F(tiltType, "y", 'i', 0) \
|
||||
F(tiltPosition, "q", 'i', 0) \
|
||||
F(tiltTarget, "u", 'i', 0) \
|
||||
F(tiltDirection, "w", 'i', 0) \
|
||||
F(myTiltPos, "M", 'i', -1) \
|
||||
F(upTime, "U", 'i', 0) \
|
||||
F(downTime, "D", 'i', 0) \
|
||||
F(tiltTime, "Y", 'i', 0) \
|
||||
F(stepSize, "z", 'i', 100) \
|
||||
F(paired, "i", 'b', 0) \
|
||||
F(shadeType, "st", 'i', 0) \
|
||||
F(bitLength, "bl", 'i', 56) \
|
||||
F(proto, "pr", 'i', 0) \
|
||||
F(flipCommands, "fc", 'b', 0) \
|
||||
F(flipPosition, "fp", 'b', 0) \
|
||||
F(inGroup, "ig", 'b', 0) \
|
||||
F(sunSensor, "ss", 'b', 0) \
|
||||
F(light, "li", 'b', 0) \
|
||||
F(simMy, "sm", 'b', 0) \
|
||||
F(repeats, "rp", 'i', 1) \
|
||||
F(gpioUp, "gu", 'i', 0) \
|
||||
F(gpioDown, "gd", 'i', 0) \
|
||||
F(gpioMy, "gm", 'i', 0) \
|
||||
F(gpioLLTrigger, "gl", 'b', 0) \
|
||||
F(linkedRemotes, "lk", 'l', 0) \
|
||||
F(linkedShades, "ls", 'l', 0) \
|
||||
FIELD(shadeId, "s", 'i', 0) \
|
||||
FIELD(groupId, "g", 'i', 0) \
|
||||
FIELD(roomId, "r", 'i', 0) \
|
||||
FIELD(name, "n", 's', 0) \
|
||||
FIELD(remoteAddress, "a", 'i', 0) \
|
||||
FIELD(lastRollingCode, "c", 'i', 0) \
|
||||
FIELD(sortOrder, "o", 'i', 0) \
|
||||
FIELD(flags, "f", 'i', 0) \
|
||||
FIELD(position, "p", 'i', 0) \
|
||||
FIELD(target, "t", 'i', 0) \
|
||||
FIELD(direction, "d", 'i', 0) \
|
||||
FIELD(myPos, "m", 'i', -1) \
|
||||
FIELD(tiltType, "y", 'i', 0) \
|
||||
FIELD(tiltPosition, "q", 'i', 0) \
|
||||
FIELD(tiltTarget, "u", 'i', 0) \
|
||||
FIELD(tiltDirection, "w", 'i', 0) \
|
||||
FIELD(myTiltPos, "M", 'i', -1) \
|
||||
FIELD(upTime, "U", 'i', 0) \
|
||||
FIELD(downTime, "D", 'i', 0) \
|
||||
FIELD(tiltTime, "Y", 'i', 0) \
|
||||
FIELD(stepSize, "z", 'i', 100) \
|
||||
FIELD(paired, "i", 'b', 0) \
|
||||
FIELD(shadeType, "st", 'i', 0) \
|
||||
FIELD(bitLength, "bl", 'i', 56) \
|
||||
FIELD(proto, "pr", 'i', 0) \
|
||||
FIELD(flipCommands, "fc", 'b', 0) \
|
||||
FIELD(flipPosition, "fp", 'b', 0) \
|
||||
FIELD(inGroup, "ig", 'b', 0) \
|
||||
FIELD(sunSensor, "ss", 'b', 0) \
|
||||
FIELD(light, "li", 'b', 0) \
|
||||
FIELD(simMy, "sm", 'b', 0) \
|
||||
FIELD(repeats, "rp", 'i', 1) \
|
||||
FIELD(gpioUp, "gu", 'i', 0) \
|
||||
FIELD(gpioDown, "gd", 'i', 0) \
|
||||
FIELD(gpioMy, "gm", 'i', 0) \
|
||||
FIELD(gpioLLTrigger, "gl", 'b', 0) \
|
||||
FIELD(linkedRemotes, "lk", 'l', 0) \
|
||||
FIELD(linkedShades, "ls", 'l', 0) \
|
||||
/* shadeCommand */ \
|
||||
F(command, "cm", 's', 0) \
|
||||
F(source, "so", 's', 0) \
|
||||
F(sourceAddress, "sr", 'i', 0) \
|
||||
FIELD(command, "cm", 's', 0) \
|
||||
FIELD(source, "so", 's', 0) \
|
||||
FIELD(sourceAddress, "sr", 'i', 0) \
|
||||
/* the header */ \
|
||||
F(maxRooms, "xr", 'i', 0) \
|
||||
F(maxShades, "xs", 'i', 0) \
|
||||
F(maxGroups, "xg", 'i', 0) \
|
||||
F(maxGroupedShades, "xm", 'i', 0) \
|
||||
F(maxLinkedRemotes, "xl", 'i', 0) \
|
||||
F(startingAddress, "sa", 'i', 0) \
|
||||
F(serverId, "sv", 's', 0) \
|
||||
F(hostname, "hn", 's', 0) \
|
||||
F(model, "md", 's', 0) \
|
||||
F(chipModel, "cp", 's', 0) \
|
||||
F(connType, "ct", 's', 0) \
|
||||
FIELD(maxRooms, "xr", 'i', 0) \
|
||||
FIELD(maxShades, "xs", 'i', 0) \
|
||||
FIELD(maxGroups, "xg", 'i', 0) \
|
||||
FIELD(maxGroupedShades, "xm", 'i', 0) \
|
||||
FIELD(maxLinkedRemotes, "xl", 'i', 0) \
|
||||
FIELD(startingAddress, "sa", 'i', 0) \
|
||||
FIELD(serverId, "sv", 's', 0) \
|
||||
FIELD(hostname, "hn", 's', 0) \
|
||||
FIELD(model, "md", 's', 0) \
|
||||
FIELD(chipModel, "cp", 's', 0) \
|
||||
FIELD(connType, "ct", 's', 0) \
|
||||
/* memory, wifi and ethernet */ \
|
||||
F(freeHeap, "fr", 'i', 0) \
|
||||
F(maxHeap, "mx", 'i', 0) \
|
||||
F(minHeap, "mn", 'i', 0) \
|
||||
F(totalHeap, "to", 'i', 0) \
|
||||
F(uptime, "up", 'i', 0) \
|
||||
F(loopRate, "lp", 'i', 0) \
|
||||
F(loopMaxUs, "lx", 'i', 0) \
|
||||
F(ssid, "sd", 's', 0) \
|
||||
F(strength, "sg", 'i', 0) \
|
||||
F(channel, "ch", 'i', 0) \
|
||||
F(connected, "co", 'b', 0) \
|
||||
F(speed, "sp", 'i', 0) \
|
||||
F(fullDuplex, "fd", 'b', 0) \
|
||||
FIELD(freeHeap, "fr", 'i', 0) \
|
||||
FIELD(maxHeap, "mx", 'i', 0) \
|
||||
FIELD(minHeap, "mn", 'i', 0) \
|
||||
FIELD(totalHeap, "to", 'i', 0) \
|
||||
FIELD(uptime, "up", 'i', 0) \
|
||||
FIELD(loopRate, "lp", 'i', 0) \
|
||||
FIELD(loopMaxUs, "lx", 'i', 0) \
|
||||
FIELD(ssid, "sd", 's', 0) \
|
||||
FIELD(strength, "sg", 'i', 0) \
|
||||
FIELD(channel, "ch", 'i', 0) \
|
||||
FIELD(connected, "co", 'b', 0) \
|
||||
FIELD(speed, "sp", 'i', 0) \
|
||||
FIELD(fullDuplex, "fd", 'b', 0) \
|
||||
/* the frame and frequency scan feeds */ \
|
||||
F(encKey, "ek", 'i', 0) \
|
||||
F(rawCmd, "rw", 'i', 0) \
|
||||
F(rssi, "rs", 'i', 0) \
|
||||
F(bits, "bt", 'i', 0) \
|
||||
F(valid, "va", 'b', 0) \
|
||||
F(hwsync, "hs", 'i', 0) \
|
||||
F(pulses, "pu", 'l', 0) \
|
||||
F(scanning, "sc", 'b', 0) \
|
||||
F(testFreq, "tf", 'n', 0) \
|
||||
F(testRssi, "tr", 'i', 0) \
|
||||
F(frequency, "fq", 'n', 0)
|
||||
FIELD(encKey, "ek", 'i', 0) \
|
||||
FIELD(rawCmd, "rw", 'i', 0) \
|
||||
FIELD(rssi, "rs", 'i', 0) \
|
||||
FIELD(bits, "bt", 'i', 0) \
|
||||
FIELD(valid, "va", 'b', 0) \
|
||||
FIELD(hwsync, "hs", 'i', 0) \
|
||||
FIELD(pulses, "pu", 'l', 0) \
|
||||
FIELD(scanning, "sc", 'b', 0) \
|
||||
FIELD(testFreq, "tf", 'n', 0) \
|
||||
FIELD(testRssi, "tr", 'i', 0) \
|
||||
FIELD(frequency, "fq", 'n', 0)
|
||||
|
||||
// Error codes in a reply. E(name, number)
|
||||
#define PROTO_ERRORS(E) \
|
||||
E(badRequest, 1) \
|
||||
E(unknownOp, 2) \
|
||||
E(unauthorized, 3) \
|
||||
E(notFound, 4) \
|
||||
E(busy, 5) \
|
||||
E(radioUnavailable, 6) \
|
||||
E(internal, 7)
|
||||
// Error codes in a reply. ERR(name, number)
|
||||
#define PROTO_ERRORS(ERR) \
|
||||
ERR(badRequest, 1) \
|
||||
ERR(unknownOp, 2) \
|
||||
ERR(unauthorized, 3) \
|
||||
ERR(notFound, 4) \
|
||||
ERR(busy, 5) \
|
||||
ERR(radioUnavailable, 6) \
|
||||
ERR(internal, 7)
|
||||
|
||||
// Feeds a client can subscribe to. Q(name, number)
|
||||
#define PROTO_FEEDS(Q) \
|
||||
Q(frames, 1) \
|
||||
Q(frequencyScan, 2)
|
||||
// Feeds a client can subscribe to. FEED(name, number)
|
||||
#define PROTO_FEEDS(FEED) \
|
||||
FEED(frames, 1) \
|
||||
FEED(frequencyScan, 2)
|
||||
|
||||
#define PROTO_ENUM_ENTRY(name, number, ...) name = number,
|
||||
enum class proto_type_t : uint8_t { PROTO_TYPES(PROTO_ENUM_ENTRY) };
|
||||
|
|
@ -171,7 +171,7 @@ enum class proto_error_t : uint8_t { none = 0, PROTO_ERRORS(PROTO_ENUM_ENTRY) };
|
|||
enum class proto_feed_t : uint8_t { PROTO_FEEDS(PROTO_ENUM_ENTRY) };
|
||||
#undef PROTO_ENUM_ENTRY
|
||||
#define PROTO_FIELD_ENTRY(name, ...) name,
|
||||
enum class pf : uint8_t { PROTO_FIELDS(PROTO_FIELD_ENTRY) count };
|
||||
enum class proto_field_t : uint8_t { PROTO_FIELDS(PROTO_FIELD_ENTRY) count };
|
||||
#undef PROTO_FIELD_ENTRY
|
||||
|
||||
struct proto_field_info_t {
|
||||
|
|
@ -185,7 +185,7 @@ struct proto_named_t {
|
|||
const char *name;
|
||||
char sender; // types only
|
||||
};
|
||||
const proto_field_info_t &protoField(pf field);
|
||||
const proto_field_info_t &protoField(proto_field_t field);
|
||||
const proto_field_info_t *protoFieldByKey(const char *key);
|
||||
// The tables as the schema lists them, in declaration order.
|
||||
uint8_t protoTypeCount();
|
||||
|
|
@ -205,7 +205,7 @@ const char *protoSchemaHash();
|
|||
class ProtoWriter {
|
||||
public:
|
||||
explicit ProtoWriter(JsonFormatter &json) : json(json) {}
|
||||
template<typename T> void add(pf field, T value) {
|
||||
template<typename T> void add(proto_field_t field, T value) {
|
||||
const proto_field_info_t &f = protoField(field);
|
||||
if constexpr(std::is_same<T, bool>::value) {
|
||||
if((value ? 1 : 0) == f.def) return;
|
||||
|
|
@ -224,9 +224,9 @@ class ProtoWriter {
|
|||
this->json.addElem(f.key, s);
|
||||
}
|
||||
}
|
||||
void beginList(pf field) { this->json.beginArray(protoField(field).key); }
|
||||
void beginList(proto_field_t field) { this->json.beginArray(protoField(field).key); }
|
||||
void endList() { this->json.endArray(); }
|
||||
void beginObject(pf field) { this->json.beginObject(protoField(field).key); }
|
||||
void beginObject(proto_field_t field) { this->json.beginObject(protoField(field).key); }
|
||||
void endObject() { this->json.endObject(); }
|
||||
// An object or a value inside a list.
|
||||
void beginItem() { this->json.beginObject(); }
|
||||
|
|
@ -236,12 +236,4 @@ class ProtoWriter {
|
|||
private:
|
||||
void addInt(const proto_field_info_t &f, int64_t value);
|
||||
};
|
||||
|
||||
// A frame: {"v":2,"t":<type>,"id":<id>,"d":{ ... }}. id 0 leaves it out.
|
||||
void beginFrame(JsonFormatter &json, proto_type_t type, uint32_t id = 0);
|
||||
void endFrame(JsonFormatter &json);
|
||||
// A reply carrying an error.
|
||||
void writeErrorReply(JsonFormatter &json, uint32_t id, proto_error_t code, const char *desc);
|
||||
// hello: the firmware version and the schema hash.
|
||||
void writeHello(JsonFormatter &json, const char *fw);
|
||||
#endif
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue