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
|
|
@ -38,6 +38,12 @@ class ESPNetwork {
|
|||
bool connecting();
|
||||
void clearConnecting();
|
||||
conn_types_t preferredConnType();
|
||||
// How the device is connected, as clients are told: Ethernet, Wifi or Unknown.
|
||||
const char *connTypeName() const {
|
||||
if(this->connType == conn_types_t::ethernet) return "Ethernet";
|
||||
if(this->connType == conn_types_t::wifi) return "Wifi";
|
||||
return "Unknown";
|
||||
}
|
||||
String ssid;
|
||||
String mac;
|
||||
int channel;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,44 @@
|
|||
#include "ModelEditor.h"
|
||||
|
||||
edit_changes_t editChanges(model_edit_type_t type, const edit_result_t &result) {
|
||||
edit_changes_t c;
|
||||
switch(type) {
|
||||
case model_edit_type_t::add_shade:
|
||||
case model_edit_type_t::save_shade:
|
||||
case model_edit_type_t::set_paired:
|
||||
case model_edit_type_t::link_remote:
|
||||
case model_edit_type_t::unlink_remote:
|
||||
c.shadeId = result.id;
|
||||
break;
|
||||
case model_edit_type_t::add_group:
|
||||
case model_edit_type_t::save_group:
|
||||
c.groupId = result.id;
|
||||
break;
|
||||
// The shade's inGroup goes with it.
|
||||
case model_edit_type_t::link_to_group:
|
||||
case model_edit_type_t::unlink_from_group:
|
||||
c.groupId = result.id;
|
||||
c.shadeId = result.shadeId;
|
||||
break;
|
||||
case model_edit_type_t::add_room:
|
||||
case model_edit_type_t::save_room:
|
||||
c.roomId = result.id;
|
||||
break;
|
||||
case model_edit_type_t::room_order: c.order = sort_list_t::rooms; break;
|
||||
case model_edit_type_t::shade_order: c.order = sort_list_t::shades; break;
|
||||
case model_edit_type_t::group_order: c.order = sort_list_t::groups; break;
|
||||
case model_edit_type_t::link_repeater:
|
||||
case model_edit_type_t::unlink_repeater:
|
||||
c.repeaters = true;
|
||||
break;
|
||||
case model_edit_type_t::delete_room:
|
||||
case model_edit_type_t::delete_shade:
|
||||
case model_edit_type_t::delete_group:
|
||||
break;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
uint8_t firstFreeId(const uint8_t *usedIds, size_t count, uint8_t lastId) {
|
||||
for(uint16_t id = 1; id <= lastId; id++) {
|
||||
bool used = false;
|
||||
|
|
|
|||
|
|
@ -24,11 +24,13 @@ enum class model_edit_type_t : uint8_t {
|
|||
room_order, shade_order, group_order
|
||||
};
|
||||
// What an edit did, sent to the clients as requestResult. id is the room,
|
||||
// shade or group it added or changed, 0 when none. code is the validation
|
||||
// error of a shade that wasn't saved, 0 otherwise.
|
||||
// shade or group it added or changed, 0 when none. shadeId is the shade a
|
||||
// group gained or lost. code is the validation error of a shade that wasn't
|
||||
// saved, 0 otherwise.
|
||||
struct edit_result_t {
|
||||
bool ok = true;
|
||||
uint8_t id = 0;
|
||||
uint8_t shadeId = 0;
|
||||
int8_t code = 0;
|
||||
const char *desc = "";
|
||||
static edit_result_t error(const char *desc, int8_t code = 0) {
|
||||
|
|
@ -40,6 +42,20 @@ struct edit_result_t {
|
|||
}
|
||||
};
|
||||
|
||||
// The lists an edit can put in a new order.
|
||||
enum class sort_list_t : uint8_t { none, rooms, shades, groups };
|
||||
// What protocol 2 clients are sent once an edit has gone through: the whole
|
||||
// items it changed (0 for none), a list's new order, the repeaters. Deletes
|
||||
// send their removals as they happen, so they need nothing here.
|
||||
struct edit_changes_t {
|
||||
uint8_t shadeId = 0;
|
||||
uint8_t groupId = 0;
|
||||
uint8_t roomId = 0;
|
||||
sort_list_t order = sort_list_t::none;
|
||||
bool repeaters = false;
|
||||
};
|
||||
edit_changes_t editChanges(model_edit_type_t type, const edit_result_t &result);
|
||||
|
||||
// The lowest id from 1 to lastId that isn't in usedIds, or 0 when every one
|
||||
// is taken.
|
||||
uint8_t firstFreeId(const uint8_t *usedIds, size_t count, uint8_t lastId);
|
||||
|
|
|
|||
33
src/ProtoFrame.cpp
Normal file
33
src/ProtoFrame.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#include "ProtoFrame.h"
|
||||
|
||||
void beginFrame(JsonFormatter &json, proto_type_t type, uint32_t id) {
|
||||
json.beginObject();
|
||||
json.addElem("v", (uint8_t)PROTO_VERSION);
|
||||
json.addElem("t", static_cast<uint8_t>(type));
|
||||
if(id) json.addElem("id", id);
|
||||
json.beginObject("d");
|
||||
}
|
||||
void endFrame(JsonFormatter &json) {
|
||||
json.endObject();
|
||||
json.endObject();
|
||||
}
|
||||
void writeOkReply(JsonFormatter &json, uint32_t id) {
|
||||
beginFrame(json, proto_type_t::reply, id);
|
||||
ProtoWriter(json).add(proto_field_t::ok, true);
|
||||
endFrame(json);
|
||||
}
|
||||
void writeErrorReply(JsonFormatter &json, uint32_t id, proto_error_t code, const char *desc) {
|
||||
beginFrame(json, proto_type_t::reply, id);
|
||||
ProtoWriter w(json);
|
||||
w.add(proto_field_t::ok, false);
|
||||
w.add(proto_field_t::code, static_cast<uint8_t>(code));
|
||||
w.add(proto_field_t::desc, desc);
|
||||
endFrame(json);
|
||||
}
|
||||
void writeHello(JsonFormatter &json, const char *fw) {
|
||||
beginFrame(json, proto_type_t::hello);
|
||||
ProtoWriter w(json);
|
||||
w.add(proto_field_t::fw, fw);
|
||||
w.add(proto_field_t::schemaHash, protoSchemaHash());
|
||||
endFrame(json);
|
||||
}
|
||||
20
src/ProtoFrame.h
Normal file
20
src/ProtoFrame.h
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
#ifndef PROTO_FRAME_H
|
||||
#define PROTO_FRAME_H
|
||||
#include <stdint.h>
|
||||
#include "JsonFormatter.h"
|
||||
#include "ProtoSchema.h"
|
||||
|
||||
// The envelope of every socket protocol 2 frame, and the frames that hold
|
||||
// nothing of the shade model (docs/PROTOCOL.md). Kept free of the shade model
|
||||
// and the web server so it can be unit tested on the PC (pio test -e native).
|
||||
|
||||
// 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 that says ok.
|
||||
void writeOkReply(JsonFormatter &json, uint32_t id);
|
||||
// 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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#include "ProtoParse.h"
|
||||
|
||||
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_request_t &out) {
|
||||
out = proto_request_t();
|
||||
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_message_t &out) {
|
||||
out = proto_message_t();
|
||||
if(len > PROTO_MAX_CLIENT_FRAME) return proto_error_t::badRequest;
|
||||
if(deserializeJson(doc, text, len) != DeserializationError::Ok) return proto_error_t::badRequest;
|
||||
if(!doc.is<JsonObject>()) return proto_error_t::badRequest;
|
||||
|
|
@ -19,18 +19,18 @@ proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc,
|
|||
return proto_error_t::none;
|
||||
}
|
||||
|
||||
int32_t protoInt(JsonObjectConst d, pf field) {
|
||||
int32_t protoInt(JsonObjectConst d, proto_field_t field) {
|
||||
const proto_field_info_t &f = protoField(field);
|
||||
JsonVariantConst v = d[f.key];
|
||||
return v.is<int32_t>() ? v.as<int32_t>() : f.def;
|
||||
}
|
||||
bool protoBool(JsonObjectConst d, pf field) {
|
||||
bool protoBool(JsonObjectConst d, proto_field_t field) {
|
||||
const proto_field_info_t &f = protoField(field);
|
||||
JsonVariantConst v = d[f.key];
|
||||
return v.is<int>() ? v.as<int>() != 0 : f.def != 0;
|
||||
}
|
||||
const char *protoString(JsonObjectConst d, pf field) {
|
||||
const char *protoString(JsonObjectConst d, proto_field_t field) {
|
||||
JsonVariantConst v = d[protoField(field).key];
|
||||
return v.is<const char *>() ? v.as<const char *>() : nullptr;
|
||||
}
|
||||
bool protoHas(JsonObjectConst d, pf field) { return !d[protoField(field).key].isNull(); }
|
||||
bool protoHas(JsonObjectConst d, proto_field_t field) { return !d[protoField(field).key].isNull(); }
|
||||
|
|
|
|||
|
|
@ -12,21 +12,21 @@
|
|||
// The largest frame a client may send; a longer one is refused unread.
|
||||
#define PROTO_MAX_CLIENT_FRAME 4096
|
||||
|
||||
struct proto_request_t {
|
||||
struct proto_message_t {
|
||||
uint8_t type = 0;
|
||||
uint32_t id = 0; // 0 when the frame had none
|
||||
};
|
||||
// Reads one frame into doc and out. none when it is a message a client may
|
||||
// send; otherwise the error to reply with. out.id is set whenever the frame
|
||||
// had one, so the reply can name it. The payload is doc["d"].
|
||||
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_request_t &out);
|
||||
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_message_t &out);
|
||||
// An op (16-127) as opposed to a control message.
|
||||
inline bool protoIsOp(uint8_t type) { return type >= 16 && type <= 127; }
|
||||
|
||||
// A field of a payload, or its default when it was left out.
|
||||
int32_t protoInt(JsonObjectConst d, pf field);
|
||||
bool protoBool(JsonObjectConst d, pf field);
|
||||
int32_t protoInt(JsonObjectConst d, proto_field_t field);
|
||||
bool protoBool(JsonObjectConst d, proto_field_t field);
|
||||
// nullptr when it was left out, as a string's default is "".
|
||||
const char *protoString(JsonObjectConst d, pf field);
|
||||
bool protoHas(JsonObjectConst d, pf field);
|
||||
const char *protoString(JsonObjectConst d, proto_field_t field);
|
||||
bool protoHas(JsonObjectConst d, proto_field_t field);
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ static const proto_named_t ERRORS[] = { PROTO_ERRORS(PROTO_NAMED_ROW) };
|
|||
static const proto_named_t FEEDS[] = { PROTO_FEEDS(PROTO_NAMED_ROW) };
|
||||
#undef PROTO_SENDER
|
||||
#undef PROTO_NAMED_ROW
|
||||
static_assert(sizeof(FIELDS) / sizeof(FIELDS[0]) == static_cast<size_t>(pf::count), "a field without a row");
|
||||
static_assert(sizeof(FIELDS) / sizeof(FIELDS[0]) == static_cast<size_t>(proto_field_t::count), "a field without a row");
|
||||
|
||||
const proto_field_info_t &protoField(pf field) { return FIELDS[static_cast<uint8_t>(field)]; }
|
||||
const proto_field_info_t &protoField(proto_field_t field) { return FIELDS[static_cast<uint8_t>(field)]; }
|
||||
const proto_field_info_t *protoFieldByKey(const char *key) {
|
||||
for(const proto_field_info_t &f : FIELDS)
|
||||
if(strcmp(f.key, key) == 0) return &f;
|
||||
|
|
@ -77,30 +77,3 @@ void ProtoWriter::item(int64_t value) {
|
|||
if(value >= INT32_MIN && value <= INT32_MAX) this->json.addElem((int32_t)value);
|
||||
else this->json.addElem((uint32_t)value);
|
||||
}
|
||||
|
||||
void beginFrame(JsonFormatter &json, proto_type_t type, uint32_t id) {
|
||||
json.beginObject();
|
||||
json.addElem("v", (uint8_t)PROTO_VERSION);
|
||||
json.addElem("t", static_cast<uint8_t>(type));
|
||||
if(id) json.addElem("id", id);
|
||||
json.beginObject("d");
|
||||
}
|
||||
void endFrame(JsonFormatter &json) {
|
||||
json.endObject();
|
||||
json.endObject();
|
||||
}
|
||||
void writeErrorReply(JsonFormatter &json, uint32_t id, proto_error_t code, const char *desc) {
|
||||
beginFrame(json, proto_type_t::reply, id);
|
||||
ProtoWriter w(json);
|
||||
w.add(pf::ok, false);
|
||||
w.add(pf::code, static_cast<uint8_t>(code));
|
||||
w.add(pf::desc, desc);
|
||||
endFrame(json);
|
||||
}
|
||||
void writeHello(JsonFormatter &json, const char *fw) {
|
||||
beginFrame(json, proto_type_t::hello);
|
||||
ProtoWriter w(json);
|
||||
w.add(pf::fw, fw);
|
||||
w.add(pf::schemaHash, protoSchemaHash());
|
||||
endFrame(json);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
62
src/ProtoSlots.cpp
Normal file
62
src/ProtoSlots.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
#include "ProtoSlots.h"
|
||||
|
||||
proto_slot_t *claimSlot(proto_slot_t *slots, uint8_t count, uint32_t clientId, bool withSchema) {
|
||||
for(uint8_t i = 0; i < count; i++) {
|
||||
uint32_t expected = 0;
|
||||
if(!slots[i].clientId.compare_exchange_strong(expected, clientId)) continue;
|
||||
slots[i].withSchema = withSchema;
|
||||
slots[i].feeds = 0;
|
||||
return &slots[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
void startSync(proto_slot_t &slot) { slot.wants.fetch_or(WANTS_SYNC); }
|
||||
proto_slot_t *slotOf(proto_slot_t *slots, uint8_t count, uint32_t clientId) {
|
||||
for(uint8_t i = 0; i < count; i++)
|
||||
if(slots[i].clientId.load() == clientId) return &slots[i];
|
||||
return nullptr;
|
||||
}
|
||||
void freeSlot(proto_slot_t &slot) {
|
||||
// Cleared before the slot is free, so the next client starts clean.
|
||||
slot.wants = 0;
|
||||
slot.feeds = 0;
|
||||
slot.clientId = 0;
|
||||
}
|
||||
|
||||
uint32_t takeWants(proto_slot_t &slot, uint32_t seenId) {
|
||||
const uint32_t wants = slot.wants.exchange(0);
|
||||
if(slot.clientId.load() == seenId) return wants;
|
||||
// The client left and another took the slot after loop() read it: what was
|
||||
// taken may be the new client's, so it goes back for the next pass. A bit
|
||||
// the old client left behind does no harm, as the new one wants a sync.
|
||||
if(wants) slot.wants.fetch_or(wants);
|
||||
return 0;
|
||||
}
|
||||
sync_step_t nextSyncStep(proto_slot_t &slot, uint32_t now) {
|
||||
const uint32_t id = slot.clientId.load();
|
||||
if(id != slot.loopId) {
|
||||
// A client left or another took the slot.
|
||||
slot.loopId = id;
|
||||
slot.cur = sync_cursor_t();
|
||||
slot.awaitingNext = false;
|
||||
}
|
||||
if(!id) return sync_step_t::none;
|
||||
const uint32_t wants = takeWants(slot, id);
|
||||
if(wants & (WANTS_SYNC | WANTS_SYNC_AGAIN)) {
|
||||
// A client that just connected may skip the schema; one that asks again
|
||||
// never gets it.
|
||||
slot.cur = beginSync((wants & WANTS_SYNC) && slot.withSchema.load());
|
||||
return sync_step_t::send;
|
||||
}
|
||||
if((wants & WANTS_NEXT) && slot.awaitingNext) return sync_step_t::send;
|
||||
if(slot.awaitingNext && now - slot.sentAt > PROTO_SYNC_TIMEOUT_MS) {
|
||||
slot.awaitingNext = false;
|
||||
slot.cur = sync_cursor_t();
|
||||
return sync_step_t::timedOut;
|
||||
}
|
||||
return sync_step_t::none;
|
||||
}
|
||||
void syncFrameSent(proto_slot_t &slot, uint32_t now) {
|
||||
slot.awaitingNext = slot.cur.active();
|
||||
slot.sentAt = now;
|
||||
}
|
||||
59
src/ProtoSlots.h
Normal file
59
src/ProtoSlots.h
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#ifndef PROTO_SLOTS_H
|
||||
#define PROTO_SLOTS_H
|
||||
#include <stdint.h>
|
||||
#include <atomic>
|
||||
#include "ProtoSync.h"
|
||||
|
||||
// The protocol 2 clients and their syncs (docs/PROTOCOL.md). async_tcp
|
||||
// claims and frees a slot and records what its client asked for; loop() moves
|
||||
// each client's sync on. The two sides share only the atomics. Kept free of
|
||||
// the web server so it can be unit tested on the PC (pio test -e native).
|
||||
|
||||
#define PROTO_MAX_CLIENTS 5
|
||||
// How long a client may take to ask for the next sync frame.
|
||||
#define PROTO_SYNC_TIMEOUT_MS 30000
|
||||
|
||||
// What a client asked loop() for, as bits.
|
||||
enum : uint32_t {
|
||||
WANTS_SYNC = 0x01, // just connected: a sync, with the schema unless it has it
|
||||
WANTS_NEXT = 0x02, // the next sync frame
|
||||
WANTS_SYNC_AGAIN = 0x04 // a sync without the schema
|
||||
};
|
||||
struct proto_slot_t {
|
||||
// Set and cleared on async_tcp; clientId is 0 when the slot is free.
|
||||
std::atomic<uint32_t> clientId{0};
|
||||
std::atomic<uint32_t> wants{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;
|
||||
};
|
||||
|
||||
// async_tcp: the slot a new client takes, or nullptr when every slot is
|
||||
// taken.
|
||||
proto_slot_t *claimSlot(proto_slot_t *slots, uint8_t count, uint32_t clientId, bool withSchema);
|
||||
// async_tcp: hello has gone out, so loop() may start the client's sync.
|
||||
void startSync(proto_slot_t &slot);
|
||||
proto_slot_t *slotOf(proto_slot_t *slots, uint8_t count, uint32_t clientId);
|
||||
// async_tcp: the client has gone.
|
||||
void freeSlot(proto_slot_t &slot);
|
||||
|
||||
// What loop() does for a slot on this pass.
|
||||
enum class sync_step_t : uint8_t {
|
||||
none,
|
||||
send, // send the sync frame at slot.cur to slot.loopId
|
||||
timedOut // close slot.loopId: it sent no next in time
|
||||
};
|
||||
// loop(): picks up a new client, takes what it asked for and moves its sync
|
||||
// on. When the step is send, slot.cur is where the frame starts.
|
||||
sync_step_t nextSyncStep(proto_slot_t &slot, uint32_t now);
|
||||
// loop(): a sync frame went out, and more follow while slot.cur is active.
|
||||
void syncFrameSent(proto_slot_t &slot, uint32_t now);
|
||||
// loop(): what the slot's client asked for, provided it is still the client
|
||||
// seenId that loop() read. When another client took the slot in between,
|
||||
// what it asked for stays for the next pass and 0 is returned.
|
||||
uint32_t takeWants(proto_slot_t &slot, uint32_t seenId);
|
||||
#endif
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
#include "esp_log.h"
|
||||
#include "ProtoSync.h"
|
||||
#include "ProtoFrame.h"
|
||||
|
||||
static const char *TAG = "Proto";
|
||||
|
||||
|
|
@ -7,81 +8,81 @@ static const char *TAG = "Proto";
|
|||
* Items
|
||||
********************************************************************/
|
||||
void protoRoom(ProtoWriter &w, const room_snapshot_t &room) {
|
||||
w.add(pf::roomId, room.roomId);
|
||||
w.add(pf::name, room.name);
|
||||
w.add(pf::sortOrder, room.sortOrder);
|
||||
w.add(proto_field_t::roomId, room.roomId);
|
||||
w.add(proto_field_t::name, room.name);
|
||||
w.add(proto_field_t::sortOrder, room.sortOrder);
|
||||
}
|
||||
void protoShadeState(ProtoWriter &w, const shade_snapshot_t &shade) {
|
||||
w.add(pf::shadeId, shade.shadeId);
|
||||
w.add(pf::position, shade.position);
|
||||
w.add(pf::target, shade.target);
|
||||
w.add(pf::direction, shade.direction);
|
||||
w.add(pf::myPos, shade.myPos);
|
||||
w.add(pf::tiltPosition, shade.tiltPosition);
|
||||
w.add(pf::tiltTarget, shade.tiltTarget);
|
||||
w.add(pf::tiltDirection, shade.tiltDirection);
|
||||
w.add(pf::myTiltPos, shade.myTiltPos);
|
||||
w.add(pf::flags, shade.flags);
|
||||
w.add(pf::lastRollingCode, shade.lastRollingCode);
|
||||
w.add(proto_field_t::shadeId, shade.shadeId);
|
||||
w.add(proto_field_t::position, shade.position);
|
||||
w.add(proto_field_t::target, shade.target);
|
||||
w.add(proto_field_t::direction, shade.direction);
|
||||
w.add(proto_field_t::myPos, shade.myPos);
|
||||
w.add(proto_field_t::tiltPosition, shade.tiltPosition);
|
||||
w.add(proto_field_t::tiltTarget, shade.tiltTarget);
|
||||
w.add(proto_field_t::tiltDirection, shade.tiltDirection);
|
||||
w.add(proto_field_t::myTiltPos, shade.myTiltPos);
|
||||
w.add(proto_field_t::flags, shade.flags);
|
||||
w.add(proto_field_t::lastRollingCode, shade.lastRollingCode);
|
||||
}
|
||||
void protoShade(ProtoWriter &w, const shade_snapshot_t &shade) {
|
||||
protoShadeState(w, shade);
|
||||
w.add(pf::roomId, shade.roomId);
|
||||
w.add(pf::name, shade.name);
|
||||
w.add(pf::remoteAddress, shade.remoteAddress);
|
||||
w.add(pf::upTime, shade.upTime);
|
||||
w.add(pf::downTime, shade.downTime);
|
||||
w.add(pf::tiltTime, shade.tiltTime);
|
||||
w.add(pf::stepSize, shade.stepSize);
|
||||
w.add(pf::paired, shade.paired);
|
||||
w.add(pf::tiltType, shade.tiltType);
|
||||
w.add(pf::shadeType, shade.shadeType);
|
||||
w.add(pf::bitLength, shade.bitLength);
|
||||
w.add(pf::proto, shade.proto);
|
||||
w.add(pf::flipCommands, shade.flipCommands);
|
||||
w.add(pf::flipPosition, shade.flipPosition);
|
||||
w.add(pf::inGroup, shade.inGroup);
|
||||
w.add(pf::sunSensor, shade.sunSensor);
|
||||
w.add(pf::light, shade.light);
|
||||
w.add(pf::simMy, shade.simMy);
|
||||
w.add(pf::repeats, shade.repeats);
|
||||
w.add(pf::sortOrder, shade.sortOrder);
|
||||
w.add(pf::gpioUp, shade.gpioUp);
|
||||
w.add(pf::gpioDown, shade.gpioDown);
|
||||
w.add(pf::gpioMy, shade.gpioMy);
|
||||
w.add(pf::gpioLLTrigger, shade.gpioLLTrigger);
|
||||
w.add(proto_field_t::roomId, shade.roomId);
|
||||
w.add(proto_field_t::name, shade.name);
|
||||
w.add(proto_field_t::remoteAddress, shade.remoteAddress);
|
||||
w.add(proto_field_t::upTime, shade.upTime);
|
||||
w.add(proto_field_t::downTime, shade.downTime);
|
||||
w.add(proto_field_t::tiltTime, shade.tiltTime);
|
||||
w.add(proto_field_t::stepSize, shade.stepSize);
|
||||
w.add(proto_field_t::paired, shade.paired);
|
||||
w.add(proto_field_t::tiltType, shade.tiltType);
|
||||
w.add(proto_field_t::shadeType, shade.shadeType);
|
||||
w.add(proto_field_t::bitLength, shade.bitLength);
|
||||
w.add(proto_field_t::proto, shade.proto);
|
||||
w.add(proto_field_t::flipCommands, shade.flipCommands);
|
||||
w.add(proto_field_t::flipPosition, shade.flipPosition);
|
||||
w.add(proto_field_t::inGroup, shade.inGroup);
|
||||
w.add(proto_field_t::sunSensor, shade.sunSensor);
|
||||
w.add(proto_field_t::light, shade.light);
|
||||
w.add(proto_field_t::simMy, shade.simMy);
|
||||
w.add(proto_field_t::repeats, shade.repeats);
|
||||
w.add(proto_field_t::sortOrder, shade.sortOrder);
|
||||
w.add(proto_field_t::gpioUp, shade.gpioUp);
|
||||
w.add(proto_field_t::gpioDown, shade.gpioDown);
|
||||
w.add(proto_field_t::gpioMy, shade.gpioMy);
|
||||
w.add(proto_field_t::gpioLLTrigger, shade.gpioLLTrigger);
|
||||
if(shade.linkedRemoteCount == 0) return;
|
||||
w.beginList(pf::linkedRemotes);
|
||||
w.beginList(proto_field_t::linkedRemotes);
|
||||
for(uint8_t i = 0; i < shade.linkedRemoteCount; i++) {
|
||||
w.beginItem();
|
||||
w.add(pf::remoteAddress, shade.linkedRemotes[i].remoteAddress);
|
||||
w.add(pf::lastRollingCode, shade.linkedRemotes[i].lastRollingCode);
|
||||
w.add(proto_field_t::remoteAddress, shade.linkedRemotes[i].remoteAddress);
|
||||
w.add(proto_field_t::lastRollingCode, shade.linkedRemotes[i].lastRollingCode);
|
||||
w.endItem();
|
||||
}
|
||||
w.endList();
|
||||
}
|
||||
void protoGroup(ProtoWriter &w, const group_snapshot_t &group) {
|
||||
w.add(pf::groupId, group.groupId);
|
||||
w.add(pf::roomId, group.roomId);
|
||||
w.add(pf::name, group.name);
|
||||
w.add(pf::remoteAddress, group.remoteAddress);
|
||||
w.add(pf::lastRollingCode, group.lastRollingCode);
|
||||
w.add(pf::bitLength, group.bitLength);
|
||||
w.add(pf::proto, group.proto);
|
||||
w.add(pf::sunSensor, group.sunSensor);
|
||||
w.add(pf::flipCommands, group.flipCommands);
|
||||
w.add(pf::flags, group.flags);
|
||||
w.add(pf::repeats, group.repeats);
|
||||
w.add(pf::sortOrder, group.sortOrder);
|
||||
w.add(proto_field_t::groupId, group.groupId);
|
||||
w.add(proto_field_t::roomId, group.roomId);
|
||||
w.add(proto_field_t::name, group.name);
|
||||
w.add(proto_field_t::remoteAddress, group.remoteAddress);
|
||||
w.add(proto_field_t::lastRollingCode, group.lastRollingCode);
|
||||
w.add(proto_field_t::bitLength, group.bitLength);
|
||||
w.add(proto_field_t::proto, group.proto);
|
||||
w.add(proto_field_t::sunSensor, group.sunSensor);
|
||||
w.add(proto_field_t::flipCommands, group.flipCommands);
|
||||
w.add(proto_field_t::flags, group.flags);
|
||||
w.add(proto_field_t::repeats, group.repeats);
|
||||
w.add(proto_field_t::sortOrder, group.sortOrder);
|
||||
if(group.linkedShadeCount == 0) return;
|
||||
w.beginList(pf::linkedShades);
|
||||
w.beginList(proto_field_t::linkedShades);
|
||||
for(uint8_t i = 0; i < group.linkedShadeCount; i++) w.item(group.linkedShades[i]);
|
||||
w.endList();
|
||||
}
|
||||
void protoGroupState(ProtoWriter &w, uint8_t groupId, uint8_t flags, int8_t direction) {
|
||||
w.add(pf::groupId, groupId);
|
||||
w.add(pf::flags, flags);
|
||||
w.add(pf::direction, direction);
|
||||
w.add(proto_field_t::groupId, groupId);
|
||||
w.add(proto_field_t::flags, flags);
|
||||
w.add(proto_field_t::direction, direction);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
|
|
@ -98,24 +99,24 @@ sync_cursor_t beginSync(bool withSchema) {
|
|||
return cur;
|
||||
}
|
||||
|
||||
static pf sectionField(sync_section_t section) {
|
||||
static proto_field_t sectionField(sync_section_t section) {
|
||||
switch(section) {
|
||||
case sync_section_t::types: return pf::types;
|
||||
case sync_section_t::fields: return pf::fields;
|
||||
case sync_section_t::errors: return pf::errors;
|
||||
case sync_section_t::feeds: return pf::feeds;
|
||||
case sync_section_t::header: return pf::header;
|
||||
case sync_section_t::rooms: return pf::rooms;
|
||||
case sync_section_t::shades: return pf::shades;
|
||||
case sync_section_t::groups: return pf::groups;
|
||||
case sync_section_t::repeaters: return pf::repeaters;
|
||||
default: return pf::status;
|
||||
case sync_section_t::types: return proto_field_t::types;
|
||||
case sync_section_t::fields: return proto_field_t::fields;
|
||||
case sync_section_t::errors: return proto_field_t::errors;
|
||||
case sync_section_t::feeds: return proto_field_t::feeds;
|
||||
case sync_section_t::header: return proto_field_t::header;
|
||||
case sync_section_t::rooms: return proto_field_t::rooms;
|
||||
case sync_section_t::shades: return proto_field_t::shades;
|
||||
case sync_section_t::groups: return proto_field_t::groups;
|
||||
case sync_section_t::repeaters: return proto_field_t::repeaters;
|
||||
default: return proto_field_t::status;
|
||||
}
|
||||
}
|
||||
static uint8_t sectionSlots(sync_section_t section) {
|
||||
switch(section) {
|
||||
case sync_section_t::types: return protoTypeCount();
|
||||
case sync_section_t::fields: return static_cast<uint8_t>(pf::count);
|
||||
case sync_section_t::fields: return static_cast<uint8_t>(proto_field_t::count);
|
||||
case sync_section_t::errors: return protoErrorCount();
|
||||
case sync_section_t::feeds: return protoFeedCount();
|
||||
case sync_section_t::rooms: return SOMFY_MAX_ROOMS;
|
||||
|
|
@ -139,7 +140,7 @@ static bool writeItem(SyncSource &src, ProtoWriter &w, sync_section_t section, u
|
|||
case sync_section_t::errors: namedRow(json, protoErrorAt(slot)); return true;
|
||||
case sync_section_t::feeds: namedRow(json, protoFeedAt(slot)); return true;
|
||||
case sync_section_t::fields: {
|
||||
const proto_field_info_t &f = protoField(static_cast<pf>(slot));
|
||||
const proto_field_info_t &f = protoField(static_cast<proto_field_t>(slot));
|
||||
const char kind[2] = {f.kind, '\0'};
|
||||
json.beginArray();
|
||||
json.addElem(f.key);
|
||||
|
|
|
|||
|
|
@ -287,6 +287,10 @@ bool SomfyShadeController::deleteShade(uint8_t shadeId) {
|
|||
this->shades[i].clear();
|
||||
}
|
||||
}
|
||||
// Its groups lose it, or a new shade given its id would join them.
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
if(this->groups[i].getGroupId() != 255 && this->groups[i].unlinkShade(shadeId)) this->groups[i].configChanged();
|
||||
}
|
||||
this->commit();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -298,14 +302,14 @@ bool SomfyShadeController::deleteRoom(uint8_t roomId) {
|
|||
if(shades[j].roomId == roomId) {
|
||||
shades[j].roomId = 0;
|
||||
shades[j].emitState();
|
||||
shades[j].changed |= SHADE_CHANGED_CONFIG;
|
||||
shades[j].configChanged();
|
||||
}
|
||||
}
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPS; j++) {
|
||||
if(groups[j].roomId == roomId) {
|
||||
groups[j].roomId = 0;
|
||||
groups[j].emitState();
|
||||
groups[j].changed |= GROUP_CHANGED_CONFIG;
|
||||
groups[j].configChanged();
|
||||
}
|
||||
}
|
||||
rooms[i].emitState("roomRemoved");
|
||||
|
|
@ -319,6 +323,11 @@ bool SomfyShadeController::deleteRoom(uint8_t roomId) {
|
|||
bool SomfyShadeController::deleteGroup(uint8_t groupId) {
|
||||
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++) {
|
||||
if(this->groups[i].getGroupId() == groupId) {
|
||||
// Its shades are no longer in a group, which their inGroup shows.
|
||||
for(uint8_t j = 0; j < SOMFY_MAX_GROUPED_SHADES && groups[i].linkedShades[j] != 0; j++) {
|
||||
SomfyShade *shade = this->getShadeById(groups[i].linkedShades[j]);
|
||||
if(shade) shade->configChanged();
|
||||
}
|
||||
groups[i].emitState("groupRemoved");
|
||||
protoSocket.groupRemoved(groupId);
|
||||
groups[i].unpublish();
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ class SomfyShade : public SomfyRemote, public ShadeSettings, private MotorLink,
|
|||
void emitState(uint8_t num, const char *evt = "shadeState");
|
||||
// What changed since the fan-out last sent the shade (SHADE_CHANGED_* bits).
|
||||
uint16_t changed = 0;
|
||||
// A setting changed: protocol 2 clients get the whole shade.
|
||||
void configChanged() { this->changed |= SHADE_CHANGED_CONFIG; }
|
||||
void emitCommand(somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
|
||||
void emitCommand(uint8_t num, somfy_commands cmd, const char *source, uint32_t sourceAddress, const char *evt = "shadeCommand");
|
||||
// False when the radio refused a command it sent.
|
||||
|
|
@ -215,6 +217,8 @@ class SomfyGroup : public SomfyRemote {
|
|||
void emitState(uint8_t num, const char *evt = "groupState");
|
||||
// What changed since the fan-out last sent the group (GROUP_CHANGED_* bits).
|
||||
uint8_t changed = 0;
|
||||
// A setting or its shades changed: protocol 2 clients get the whole group.
|
||||
void configChanged() { this->changed |= GROUP_CHANGED_CONFIG; }
|
||||
bool sendCommand(somfy_commands cmd);
|
||||
bool sendCommand(somfy_commands cmd, uint8_t repeat, uint8_t stepSize = 0);
|
||||
int8_t p_direction(int8_t dir);
|
||||
|
|
|
|||
|
|
@ -228,48 +228,19 @@ void cancelModelEdit(model_edit_t *edit) {
|
|||
delete edit;
|
||||
editBusy.store(false);
|
||||
}
|
||||
// What protocol 2 clients get after an edit: the whole item it changed, by its
|
||||
// config bit for shades and groups, so the change fan-out sends it once.
|
||||
// Deletes send their removal from the controller as they happen.
|
||||
// What protocol 2 clients get after an edit (editChanges). A shade or group
|
||||
// goes out whole from the change fan-out, by its config bit, so it is sent
|
||||
// once with whatever else changed; the rest goes out now.
|
||||
static void markEdited(const model_edit_t &edit, const edit_result_t &result) {
|
||||
auto shade = [](uint8_t id) {
|
||||
SomfyShade *s = somfy.getShadeById(id);
|
||||
if(s) s->changed |= SHADE_CHANGED_CONFIG;
|
||||
};
|
||||
auto group = [](uint8_t id) {
|
||||
SomfyGroup *g = somfy.getGroupById(id);
|
||||
if(g) g->changed |= GROUP_CHANGED_CONFIG;
|
||||
};
|
||||
switch(edit.type) {
|
||||
case model_edit_type_t::add_shade:
|
||||
case model_edit_type_t::save_shade:
|
||||
case model_edit_type_t::set_paired:
|
||||
case model_edit_type_t::link_remote:
|
||||
case model_edit_type_t::unlink_remote:
|
||||
shade(result.id);
|
||||
break;
|
||||
case model_edit_type_t::add_group:
|
||||
case model_edit_type_t::save_group:
|
||||
group(result.id);
|
||||
break;
|
||||
case model_edit_type_t::link_to_group:
|
||||
case model_edit_type_t::unlink_from_group:
|
||||
group(result.id);
|
||||
shade(edit.doc["shadeId"] | (uint8_t)0);
|
||||
break;
|
||||
case model_edit_type_t::add_room:
|
||||
case model_edit_type_t::save_room: {
|
||||
SomfyRoom *room = somfy.getRoomById(result.id);
|
||||
if(room) protoSocket.room(*room);
|
||||
break;
|
||||
}
|
||||
case model_edit_type_t::room_order: protoSocket.order(proto_type_t::roomOrder); break;
|
||||
case model_edit_type_t::shade_order: protoSocket.order(proto_type_t::shadeOrder); break;
|
||||
case model_edit_type_t::group_order: protoSocket.order(proto_type_t::groupOrder); break;
|
||||
case model_edit_type_t::link_repeater:
|
||||
case model_edit_type_t::unlink_repeater: protoSocket.repeaters(); break;
|
||||
default: break;
|
||||
}
|
||||
const edit_changes_t c = editChanges(edit.type, result);
|
||||
SomfyShade *shade = c.shadeId ? somfy.getShadeById(c.shadeId) : nullptr;
|
||||
if(shade) shade->configChanged();
|
||||
SomfyGroup *group = c.groupId ? somfy.getGroupById(c.groupId) : nullptr;
|
||||
if(group) group->configChanged();
|
||||
SomfyRoom *room = c.roomId ? somfy.getRoomById(c.roomId) : nullptr;
|
||||
if(room) protoSocket.room(*room);
|
||||
if(c.order != sort_list_t::none) protoSocket.order(c.order);
|
||||
if(c.repeaters) protoSocket.repeaters();
|
||||
}
|
||||
void runModelEdit(model_edit_t *edit) {
|
||||
const edit_result_t result = applyModelEdit(*edit);
|
||||
|
|
@ -404,7 +375,9 @@ edit_result_t applyModelEdit(model_edit_t &edit) {
|
|||
else if(!group->linkShade(shadeId)) return edit_result_t::error("This group already has the most shades it can.");
|
||||
somfy.commit();
|
||||
group->emitState();
|
||||
return done(groupId);
|
||||
edit_result_t r = done(groupId);
|
||||
r.shadeId = shadeId;
|
||||
return r;
|
||||
}
|
||||
case model_edit_type_t::link_repeater:
|
||||
case model_edit_type_t::unlink_repeater: {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ void SomfyShadeOps::setUnpaired(uint8_t shadeId) {
|
|||
SomfyShade *shade = somfy.getShadeById(shadeId);
|
||||
shade->paired = false;
|
||||
shade->save();
|
||||
shade->changed |= SHADE_CHANGED_CONFIG;
|
||||
shade->configChanged();
|
||||
}
|
||||
void SomfyShadeOps::setRollingCode(uint8_t shadeId, uint16_t code) { somfy.getShadeById(shadeId)->setRollingCode(code); }
|
||||
bool SomfyShadeOps::sendGroupCommand(uint8_t groupId, somfy_commands cmd, uint8_t repeat, uint8_t stepSize) {
|
||||
|
|
|
|||
|
|
@ -322,11 +322,11 @@ void Transceiver::emitFrequencyScan(uint8_t num) {
|
|||
sockEmit.endEmit(num);
|
||||
if(num == 255 && protoSocket.subscribed(proto_feed_t::frequencyScan)) {
|
||||
ProtoWriter w = protoSocket.beginFeed(proto_type_t::frequencyScan);
|
||||
w.add(pf::scanning, rxmode == 3);
|
||||
w.add(pf::testFreq, currFreq);
|
||||
w.add(pf::testRssi, (int32_t)currRSSI);
|
||||
w.add(pf::frequency, markFreq);
|
||||
w.add(pf::rssi, (int32_t)markRSSI);
|
||||
w.add(proto_field_t::scanning, rxmode == 3);
|
||||
w.add(proto_field_t::testFreq, currFreq);
|
||||
w.add(proto_field_t::testRssi, (int32_t)currRSSI);
|
||||
w.add(proto_field_t::frequency, markFreq);
|
||||
w.add(proto_field_t::rssi, (int32_t)markRSSI);
|
||||
protoSocket.endFeed(proto_feed_t::frequencyScan);
|
||||
}
|
||||
}
|
||||
|
|
@ -377,19 +377,19 @@ void Transceiver::emitFrame(somfy_frame_t *frame, somfy_rx_t *rx) {
|
|||
}
|
||||
if(protoSocket.subscribed(proto_feed_t::frames)) {
|
||||
ProtoWriter w = protoSocket.beginFeed(proto_type_t::frame);
|
||||
w.add(pf::encKey, frame->encKey);
|
||||
w.add(pf::remoteAddress, (uint32_t)frame->remoteAddress);
|
||||
w.add(pf::lastRollingCode, (uint32_t)frame->rollingCode);
|
||||
w.add(pf::command, translateSomfyCommand(frame->cmd).c_str());
|
||||
w.add(pf::rawCmd, frame->rawCmd);
|
||||
w.add(pf::rssi, (int32_t)frame->rssi);
|
||||
w.add(pf::bits, rx ? rx->bit_length : (uint8_t)0);
|
||||
w.add(pf::proto, static_cast<uint8_t>(frame->proto));
|
||||
w.add(pf::valid, frame->valid);
|
||||
w.add(pf::hwsync, frame->hwsync);
|
||||
if(frame->cmd == somfy_commands::StepUp || frame->cmd == somfy_commands::StepDown) w.add(pf::stepSize, frame->stepSize);
|
||||
w.add(proto_field_t::encKey, frame->encKey);
|
||||
w.add(proto_field_t::remoteAddress, (uint32_t)frame->remoteAddress);
|
||||
w.add(proto_field_t::lastRollingCode, (uint32_t)frame->rollingCode);
|
||||
w.add(proto_field_t::command, translateSomfyCommand(frame->cmd).c_str());
|
||||
w.add(proto_field_t::rawCmd, frame->rawCmd);
|
||||
w.add(proto_field_t::rssi, (int32_t)frame->rssi);
|
||||
w.add(proto_field_t::bits, rx ? rx->bit_length : (uint8_t)0);
|
||||
w.add(proto_field_t::proto, static_cast<uint8_t>(frame->proto));
|
||||
w.add(proto_field_t::valid, frame->valid);
|
||||
w.add(proto_field_t::hwsync, frame->hwsync);
|
||||
if(frame->cmd == somfy_commands::StepUp || frame->cmd == somfy_commands::StepDown) w.add(proto_field_t::stepSize, frame->stepSize);
|
||||
if(rx && rx->pulseCount) {
|
||||
w.beginList(pf::pulses);
|
||||
w.beginList(proto_field_t::pulses);
|
||||
for(uint16_t i = 0; i < rx->pulseCount; i++) w.item(rx->pulses[i]);
|
||||
w.endList();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -231,9 +231,6 @@ static void controllerHeader(JsonFormatter &json, const model_snapshot_t &snap)
|
|||
// The parts of /discovery that aren't in the shade model.
|
||||
static void discoveryHeader(JsonFormatter &json, const model_snapshot_t &snap) {
|
||||
(void)snap;
|
||||
const char *connType = "Unknown";
|
||||
if(net.connType == conn_types_t::ethernet) connType = "Ethernet";
|
||||
else if(net.connType == conn_types_t::wifi) connType = "Wifi";
|
||||
json.addElem("serverId", settings.serverId);
|
||||
json.addElem("version", settings.fwVersion.name);
|
||||
json.addElem("model", "ESPSomfyRTS");
|
||||
|
|
@ -241,7 +238,7 @@ static void discoveryHeader(JsonFormatter &json, const model_snapshot_t &snap) {
|
|||
json.addElem("authType", static_cast<uint8_t>(settings.Security.type));
|
||||
json.addElem("permissions", settings.Security.permissions);
|
||||
json.addElem("chipModel", settings.chipModel);
|
||||
json.addElem("connType", connType);
|
||||
json.addElem("connType", net.connTypeName());
|
||||
json.beginObject("memory");
|
||||
json.addElem("max", ESP.getMaxAllocHeap());
|
||||
json.addElem("free", ESP.getFreeHeap());
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ struct room_t {
|
|||
bool leave(uint8_t num);
|
||||
void clear();
|
||||
};
|
||||
// The old socket on port 8080. It goes with step 4 of docs/SECURITY_PLAN.md,
|
||||
// and with it every sockEmit call that sits next to a protoSocket one.
|
||||
class SocketEmitter {
|
||||
protected:
|
||||
uint8_t newclients = 0;
|
||||
|
|
@ -29,9 +31,11 @@ class SocketEmitter {
|
|||
TaskHandle_t _owner = nullptr;
|
||||
// What beginEmit() hands any other task, so the event goes nowhere.
|
||||
JsonSockEvent _discard;
|
||||
bool onLoopTask() const;
|
||||
void delayInit(uint8_t num);
|
||||
public:
|
||||
// Whether this runs on loop()'s task, the only one that may send socket
|
||||
// events: this socket's and protocol 2's.
|
||||
bool onLoopTask() const;
|
||||
JsonSockEvent json;
|
||||
room_t rooms[SOCK_MAX_ROOMS];
|
||||
uint8_t activeClients(uint8_t room);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue