mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 13:52:15 +01:00
feat: socket protocol 2 core on ws://host/ws
The /ws endpoint runs next to the old socket on port 8080. The handshake checks the token under security, the device sends hello, then a sync of the schema (unless the client has it), header, rooms, shades, groups, repeaters and status, paced by the client's next. Events go out with the old ones: shadeState or the whole shade on a config change, groups, rooms, removals, orders, repeaters, shadeCommand, memory, wifi, ethernet, and the frames and frequencyScan feeds. The table of types, fields, errors and feeds is defined once and is sent as the schema. The writer, item writers, sync and client frame parser are pure and natively tested. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a36888f8a1
commit
482e495678
24 changed files with 1839 additions and 5 deletions
248
src/ProtoSync.cpp
Normal file
248
src/ProtoSync.cpp
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
#include "esp_log.h"
|
||||
#include "ProtoSync.h"
|
||||
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
if(shade.linkedRemoteCount == 0) return;
|
||||
w.beginList(pf::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.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);
|
||||
if(group.linkedShadeCount == 0) return;
|
||||
w.beginList(pf::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);
|
||||
}
|
||||
|
||||
/*********************************************************************
|
||||
* Sync
|
||||
********************************************************************/
|
||||
// What closing a frame cut short takes: the list's ], the }} of d and the
|
||||
// frame, and the terminator.
|
||||
#define SYNC_CLOSE_RESERVE 4
|
||||
|
||||
sync_cursor_t beginSync(bool withSchema) {
|
||||
sync_cursor_t cur;
|
||||
cur.section = withSchema ? sync_section_t::types : sync_section_t::header;
|
||||
cur.index = 0;
|
||||
return cur;
|
||||
}
|
||||
|
||||
static pf 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;
|
||||
}
|
||||
}
|
||||
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::errors: return protoErrorCount();
|
||||
case sync_section_t::feeds: return protoFeedCount();
|
||||
case sync_section_t::rooms: return SOMFY_MAX_ROOMS;
|
||||
case sync_section_t::shades: return SOMFY_MAX_SHADES;
|
||||
case sync_section_t::groups: return SOMFY_MAX_GROUPS;
|
||||
case sync_section_t::repeaters: return SOMFY_MAX_REPEATERS;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
static void namedRow(JsonFormatter &json, const proto_named_t &row) {
|
||||
json.beginArray();
|
||||
json.addElem(row.number);
|
||||
json.addElem(row.name);
|
||||
json.endArray();
|
||||
}
|
||||
// Writes the item in slot of a list section. False when the slot is empty.
|
||||
static bool writeItem(SyncSource &src, ProtoWriter &w, sync_section_t section, uint8_t slot) {
|
||||
JsonFormatter &json = w.json;
|
||||
switch(section) {
|
||||
case sync_section_t::types: namedRow(json, protoTypeAt(slot)); return true;
|
||||
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 char kind[2] = {f.kind, '\0'};
|
||||
json.beginArray();
|
||||
json.addElem(f.key);
|
||||
json.addElem(f.name);
|
||||
json.addElem(kind);
|
||||
json.addElem(f.def);
|
||||
json.endArray();
|
||||
return true;
|
||||
}
|
||||
case sync_section_t::rooms: {
|
||||
room_snapshot_t room;
|
||||
if(!src.room(slot, room)) return false;
|
||||
w.beginItem();
|
||||
protoRoom(w, room);
|
||||
w.endItem();
|
||||
return true;
|
||||
}
|
||||
case sync_section_t::shades: {
|
||||
shade_snapshot_t shade;
|
||||
if(!src.shade(slot, shade)) return false;
|
||||
w.beginItem();
|
||||
protoShade(w, shade);
|
||||
w.endItem();
|
||||
return true;
|
||||
}
|
||||
case sync_section_t::groups: {
|
||||
group_snapshot_t group;
|
||||
if(!src.group(slot, group)) return false;
|
||||
w.beginItem();
|
||||
protoGroup(w, group);
|
||||
w.endItem();
|
||||
return true;
|
||||
}
|
||||
case sync_section_t::repeaters: {
|
||||
uint32_t address = 0;
|
||||
if(!src.repeater(slot, address)) return false;
|
||||
w.item(address);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
static void nextSection(sync_cursor_t &cur) {
|
||||
cur.section = static_cast<sync_section_t>(static_cast<uint8_t>(cur.section) + 1);
|
||||
cur.index = 0;
|
||||
}
|
||||
|
||||
size_t writeSyncFrame(SyncSource &src, sync_cursor_t &cur, char *buff, size_t size) {
|
||||
if(!cur.active()) return 0;
|
||||
BufferedJsonFormatter json;
|
||||
json.setBuffer(buff, size);
|
||||
json.logOverflow(false);
|
||||
ProtoWriter w(json);
|
||||
beginFrame(json, proto_type_t::sync);
|
||||
bool any = false;
|
||||
auto fits = [&json, size]() { return !json.overflowed() && json.length() + SYNC_CLOSE_RESERVE < size; };
|
||||
while(cur.active()) {
|
||||
if(cur.section == sync_section_t::header || cur.section == sync_section_t::status) {
|
||||
const json_mark_t m = json.mark();
|
||||
w.beginObject(sectionField(cur.section));
|
||||
if(cur.section == sync_section_t::header) src.header(w);
|
||||
else src.status(w);
|
||||
w.endObject();
|
||||
if(!fits()) {
|
||||
json.rollback(m);
|
||||
// Only fixed size values, so this can't happen short of a bug.
|
||||
if(!any) { ESP_LOGE(TAG, "Sync section %u doesn't fit in %u bytes", static_cast<uint8_t>(cur.section), (unsigned)size); nextSection(cur); continue; }
|
||||
break;
|
||||
}
|
||||
any = true;
|
||||
nextSection(cur);
|
||||
continue;
|
||||
}
|
||||
bool open = false;
|
||||
bool full = false;
|
||||
const uint8_t slots = sectionSlots(cur.section);
|
||||
while(cur.index < slots) {
|
||||
const json_mark_t m = json.mark();
|
||||
if(!open) w.beginList(sectionField(cur.section));
|
||||
if(!writeItem(src, w, cur.section, cur.index)) {
|
||||
json.rollback(m);
|
||||
cur.index++;
|
||||
continue;
|
||||
}
|
||||
if(!fits()) {
|
||||
json.rollback(m);
|
||||
if(!any) {
|
||||
ESP_LOGE(TAG, "Sync item %u of section %u doesn't fit in %u bytes, leaving it out", cur.index, static_cast<uint8_t>(cur.section), (unsigned)size);
|
||||
cur.index++;
|
||||
continue;
|
||||
}
|
||||
full = true;
|
||||
break;
|
||||
}
|
||||
open = true;
|
||||
any = true;
|
||||
cur.index++;
|
||||
}
|
||||
if(open) w.endList();
|
||||
if(full) break;
|
||||
nextSection(cur);
|
||||
}
|
||||
endFrame(json);
|
||||
return json.length();
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue