mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
From the review of 7723711:
- The assembler is locked: a client closed for a full queue is closed
by the task that was sending, loop(), whose disconnect dropped the
buffer while async_tcp could be copying into it. A whole message is
taken out of it, so a disconnect can't free it while it is read.
- loop() expires a message stalled for 10 s, freeing its buffer and
telling its client, instead of holding it until another client came.
A binary message is refused before it can touch another's buffer.
- Each edit takes only the fields its route reads; one it would ignore
is refused by name. linkRemote's rolling code goes in under the
route's name, so applyModelEdit is unchanged again.
- An edit refused for an id that doesn't exist replies not found
(edit_result_t::missing). A reply that doesn't fit its buffer goes
as an internal error, so the op still gets one.
- The TYPE table says what becomes of each op (proto_op_t), which the
socket switches on. The reply address is one proto_reply_to_t. The
routes and the ops start and queue an edit the same way
(newModelEdit, submitModelEdit). The full-list texts and the offered
remote address rule live in ModelEditor.
- e2e: the sort order test skips a list that it would renumber.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
290 lines
10 KiB
C++
290 lines
10 KiB
C++
#ifndef PROTO_SCHEMA_H
|
|
#define PROTO_SCHEMA_H
|
|
#include <stdint.h>
|
|
#include <type_traits>
|
|
#include "JsonFormatter.h"
|
|
|
|
// Socket protocol 2 (docs/PROTOCOL.md, docs/adr/0005-one-websocket-protocol.md):
|
|
// the one table of message types, fields, error codes and feeds. The device
|
|
// sends it to clients as the schema, so it is the only copy anywhere. Writers
|
|
// name fields by proto_field_t, so a field that isn't declared here can't be
|
|
// sent. Kept free of the shade model and the web server so it can be unit
|
|
// tested on the PC (pio test -e native).
|
|
|
|
#define PROTO_VERSION 2
|
|
|
|
// Message types: 1-15 control, 16-127 client ops, 128-255 device events. A
|
|
// number is never reused; a retired type stays reserved.
|
|
// TYPE(name, number, sender, op): sender is 'c' client, 'd' device, 'b' both;
|
|
// op is what becomes of a client op (proto_op_t), '-' for the rest.
|
|
#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(shadeCommand, 16, 'c', 'q') \
|
|
TYPE(groupCommand, 17, 'c', 'q') \
|
|
TYPE(tiltCommand, 18, 'c', 'q') \
|
|
TYPE(repeatCommand, 19, 'c', 'q') \
|
|
TYPE(setPositions, 20, 'c', 'q') \
|
|
TYPE(setSensor, 21, 'c', 'q') \
|
|
TYPE(setMyPosition, 22, 'c', 'q') \
|
|
TYPE(unpairShade, 23, 'c', 'q') \
|
|
TYPE(setRollingCode, 24, 'c', 'q') \
|
|
TYPE(sendRemoteCommand, 25, 'c', 'f') \
|
|
TYPE(addRoom, 26, 'c', 'e') \
|
|
TYPE(addShade, 27, 'c', 'e') \
|
|
TYPE(addGroup, 28, 'c', 'e') \
|
|
TYPE(saveRoom, 29, 'c', 'e') \
|
|
TYPE(saveShade, 30, 'c', 'e') \
|
|
TYPE(saveGroup, 31, 'c', 'e') \
|
|
TYPE(deleteRoom, 32, 'c', 'e') \
|
|
TYPE(deleteShade, 33, 'c', 'e') \
|
|
TYPE(deleteGroup, 34, 'c', 'e') \
|
|
TYPE(linkRemote, 35, 'c', 'e') \
|
|
TYPE(unlinkRemote, 36, 'c', 'e') \
|
|
TYPE(linkToGroup, 37, 'c', 'e') \
|
|
TYPE(unlinkFromGroup, 38, 'c', 'e') \
|
|
TYPE(linkRepeater, 39, 'c', 'e') \
|
|
TYPE(unlinkRepeater, 40, 'c', 'e') \
|
|
TYPE(setPaired, 41, 'c', 'e') \
|
|
TYPE(roomSortOrder, 42, 'c', 'e') \
|
|
TYPE(shadeSortOrder, 43, 'c', 'e') \
|
|
TYPE(groupSortOrder, 44, 'c', 'e') \
|
|
TYPE(getNextRoom, 45, 'c', 'r') \
|
|
TYPE(getNextShade, 46, 'c', 'r') \
|
|
TYPE(getNextGroup, 47, 'c', 'r') \
|
|
TYPE(groupOptions, 48, 'c', 'r') \
|
|
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(shadeCommanded, 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. 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(FIELD) \
|
|
/* the schema and the sync */ \
|
|
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 */ \
|
|
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 */ \
|
|
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) \
|
|
/* the command ops and shadeCommanded */ \
|
|
FIELD(command, "cm", 's', 0) \
|
|
FIELD(source, "so", 's', 0) \
|
|
FIELD(sourceAddress, "sr", 'i', 0) \
|
|
FIELD(repeat, "re", 'i', 0) \
|
|
FIELD(sunny, "sn", 'b', 0) \
|
|
FIELD(windy, "wn", 'b', 0) \
|
|
/* the edit ops */ \
|
|
FIELD(validation, "vc", 'i', 0) \
|
|
/* the header */ \
|
|
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 */ \
|
|
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 */ \
|
|
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. 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. 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) };
|
|
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 proto_field_t : uint8_t { PROTO_FIELDS(PROTO_FIELD_ENTRY) count };
|
|
#undef PROTO_FIELD_ENTRY
|
|
|
|
struct proto_field_info_t {
|
|
const char *name;
|
|
const char *key;
|
|
char kind;
|
|
int32_t def;
|
|
};
|
|
struct proto_named_t {
|
|
uint8_t number;
|
|
const char *name;
|
|
char sender; // types only
|
|
char op; // types only
|
|
};
|
|
// What becomes of a client op, from the TYPE table.
|
|
enum class proto_op_t : char {
|
|
none = '-', // not an op
|
|
shadeRequest = 'q', // a command: a shade request loop() runs
|
|
remoteFrame = 'f', // sendRemoteCommand: a frame that goes to the radio at once
|
|
edit = 'e', // an edit loop() applies
|
|
read = 'r' // a read loop() answers from the shade model
|
|
};
|
|
const proto_field_info_t &protoField(proto_field_t field);
|
|
const proto_field_info_t *protoFieldByKey(const char *key);
|
|
// The field with that key; false when there is none.
|
|
bool protoFieldOfKey(const char *key, proto_field_t &out);
|
|
// The tables as the schema lists them, in declaration order.
|
|
uint8_t protoTypeCount();
|
|
const proto_named_t &protoTypeAt(uint8_t index);
|
|
uint8_t protoErrorCount();
|
|
const proto_named_t &protoErrorAt(uint8_t index);
|
|
uint8_t protoFeedCount();
|
|
const proto_named_t &protoFeedAt(uint8_t index);
|
|
const char *protoTypeName(uint8_t number);
|
|
// What a client may send: control messages it sends and the ops.
|
|
bool protoClientMaySend(uint8_t type);
|
|
proto_op_t protoOpKind(uint8_t type);
|
|
// Hex FNV-1a of every row of the schema: 8 characters and a terminator.
|
|
const char *protoSchemaHash();
|
|
|
|
// Writes fields by their short keys into the open object of json: booleans as
|
|
// 1/0, and nothing at all for a value at the field's default.
|
|
class ProtoWriter {
|
|
public:
|
|
explicit ProtoWriter(JsonFormatter &json) : json(json) {}
|
|
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;
|
|
this->json.addElem(f.key, (uint8_t)(value ? 1 : 0));
|
|
}
|
|
else if constexpr(std::is_floating_point<T>::value) {
|
|
if(value == (T)f.def) return;
|
|
this->json.addElem(f.key, (float)value);
|
|
}
|
|
else if constexpr(std::is_integral<T>::value) {
|
|
this->addInt(f, (int64_t)value);
|
|
}
|
|
else {
|
|
const char *s = value;
|
|
if(!s || !s[0]) return;
|
|
this->json.addElem(f.key, s);
|
|
}
|
|
}
|
|
void beginList(proto_field_t field) { this->json.beginArray(protoField(field).key); }
|
|
void endList() { this->json.endArray(); }
|
|
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(); }
|
|
void endItem() { this->json.endObject(); }
|
|
void item(int64_t value);
|
|
JsonFormatter &json;
|
|
private:
|
|
void addInt(const proto_field_info_t &f, int64_t value);
|
|
};
|
|
#endif
|