feat: edit ops and the reads their forms need, for socket protocol 2

The edit routes become ops 26-44 (addRoom ... groupSortOrder): ProtoEdits
turns an op's payload into the route's request, full names and real
booleans, and loop() applies it with applyModelEdit() as the route
does. The reply goes to the client that sent it, with an add's new id,
or the route's reason and a shade's validation code; the HTTP routes
keep sending requestResult.

getNextRoom, getNextShade, getNextGroup and groupOptions (45-48) are
answered by loop() from the shade model, through a new LoopQueue item.

Messages that arrive in pieces, a frame over one TCP segment or several
frames, are put back together by ProtoAssembly: one at a time across
clients, up to 4 KB, in a buffer held only while one is on its way.
A message in pieces is now told from its first frame's opcode, which
also stops a fragmented message's last frame drawing a second reply.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 14:29:53 +02:00
parent ea973809e2
commit 77237111ea
22 changed files with 851 additions and 22 deletions

View file

@ -84,6 +84,9 @@ void LoopQueue::drain() {
case loop_item_type_t::model_edit:
runModelEdit(item.edit);
break;
case loop_item_type_t::proto_read:
protoSocket.replyRead(item.replyClient, item.replyId, item.readOp, item.readId);
break;
}
}
}

View file

@ -24,7 +24,8 @@ enum class loop_item_type_t : uint8_t {
restore_shades, // restore from /shades.tmp, then reboot
load_shades, // replace the shades with /shades.tmp
snapshot_fill, // fill a read's ModelSnapshot
model_edit // apply an edit with ModelEditor
model_edit, // apply an edit with ModelEditor
proto_read // answer a protocol 2 read op from the shade model
};
struct model_edit_t;
struct loop_item_t {
@ -33,10 +34,12 @@ struct loop_item_t {
uint8_t restoreParts = 0; // restore_shades: RESTORE_* bits
uint8_t slot = 0; // snapshot_fill: the SnapshotSlots slot
model_edit_t *edit = nullptr; // model_edit: loop() runs and frees it
// shade_request from a protocol 2 client: loop() replies to this socket
// client with replyId once the request has run. 0 for no reply.
// shade_request and proto_read from a protocol 2 client: loop() replies to
// this socket client with replyId once it has run. 0 for no reply.
uint32_t replyClient = 0;
uint32_t replyId = 0;
uint8_t readOp = 0; // proto_read: the op's type
uint8_t readId = 0; // proto_read: the group, for groupOptions
};
// Whether the item sends anything on air.

47
src/ProtoAssembly.cpp Normal file
View file

@ -0,0 +1,47 @@
#include <stdlib.h>
#include <string.h>
#include "ProtoAssembly.h"
void MessageAssembler::free() {
::free(this->buf);
this->buf = nullptr;
this->len = 0;
this->owner = 0;
}
void MessageAssembler::drop(uint32_t clientId) {
if(this->owner == clientId) this->free();
if(this->stalled == clientId) this->stalled = 0;
}
assembly_t MessageAssembler::add(const proto_piece_t &piece, uint32_t now) {
if(piece.first) {
if(this->stalled == piece.clientId) this->stalled = 0;
if(this->owner && this->owner != piece.clientId) {
if(now - this->startedAt < PROTO_ASSEMBLY_TIMEOUT_MS) return assembly_t::busy;
// A message that stalled gives the buffer up after a while.
this->stalled = this->owner;
}
this->free();
if(!piece.text) return assembly_t::notText;
this->buf = static_cast<char *>(malloc(this->limit + 1));
if(!this->buf) return assembly_t::noMemory;
this->owner = piece.clientId;
this->startedAt = now;
}
else if(this->owner != piece.clientId) {
if(this->stalled == piece.clientId) {
this->stalled = 0;
return assembly_t::timedOut;
}
// The rest of a message that was already answered.
return assembly_t::ignored;
}
if(this->len + piece.len > this->limit) {
this->free();
return assembly_t::tooLarge;
}
memcpy(this->buf + this->len, piece.data, piece.len);
this->len += piece.len;
if(!piece.last) return assembly_t::more;
this->buf[this->len] = '\0';
return assembly_t::complete;
}

61
src/ProtoAssembly.h Normal file
View file

@ -0,0 +1,61 @@
#ifndef PROTO_ASSEMBLY_H
#define PROTO_ASSEMBLY_H
#include <stddef.h>
#include <stdint.h>
// Putting a protocol 2 message back together from the pieces the web server
// hands over: a frame larger than a TCP segment, or a message sent as several
// frames (docs/PROTOCOL.md). One message at a time across every client, in
// one buffer taken when its first piece arrives and freed when it is whole, so
// nothing is held while no long message is on its way. async_tcp only. Kept
// free of the web server so it can be unit tested on the PC
// (pio test -e native).
// How long a message may take to arrive in full before another client's may
// take the buffer.
#define PROTO_ASSEMBLY_TIMEOUT_MS 10000
// One piece of a message.
struct proto_piece_t {
uint32_t clientId = 0;
bool first = false; // its first piece: the start of its first frame
bool last = false; // its last piece: the end of its final frame
bool text = true; // a text message rather than binary
const uint8_t *data = nullptr;
size_t len = 0;
};
enum class assembly_t : uint8_t {
more, // wait for the next piece
complete, // the message is whole: text() and length(), then done()
ignored, // a piece of a message that was already answered
busy, // another client's message holds the buffer
tooLarge, // over the limit: the rest of it is ignored
notText, // a binary message: the rest of it is ignored
noMemory, // the buffer couldn't be had: the rest of it is ignored
timedOut // it stalled and another client's took the buffer
};
class MessageAssembler {
public:
explicit MessageAssembler(size_t limit) : limit(limit) {}
~MessageAssembler() { this->free(); }
// Each reply-worthy result (busy, tooLarge, notText, noMemory, timedOut)
// comes once per message, from the piece that caused it.
assembly_t add(const proto_piece_t &piece, uint32_t now);
// The whole message, terminated, until done().
const char *text() const { return this->buf; }
size_t length() const { return this->len; }
void done() { this->free(); }
// The client has gone: its part of a message goes with it.
void drop(uint32_t clientId);
bool holding() const { return this->owner != 0; }
private:
size_t limit;
uint32_t owner = 0;
uint32_t startedAt = 0;
// The client whose stalled message lost the buffer, still to be told.
uint32_t stalled = 0;
char *buf = nullptr;
size_t len = 0;
void free();
};
#endif

108
src/ProtoEdits.cpp Normal file
View file

@ -0,0 +1,108 @@
#include <stdio.h>
#include "ProtoEdits.h"
struct edit_op_t {
proto_type_t type;
model_edit_type_t edit;
};
static const edit_op_t EDIT_OPS[] = {
{proto_type_t::addRoom, model_edit_type_t::add_room},
{proto_type_t::addShade, model_edit_type_t::add_shade},
{proto_type_t::addGroup, model_edit_type_t::add_group},
{proto_type_t::saveRoom, model_edit_type_t::save_room},
{proto_type_t::saveShade, model_edit_type_t::save_shade},
{proto_type_t::saveGroup, model_edit_type_t::save_group},
{proto_type_t::deleteRoom, model_edit_type_t::delete_room},
{proto_type_t::deleteShade, model_edit_type_t::delete_shade},
{proto_type_t::deleteGroup, model_edit_type_t::delete_group},
{proto_type_t::linkRemote, model_edit_type_t::link_remote},
{proto_type_t::unlinkRemote, model_edit_type_t::unlink_remote},
{proto_type_t::linkToGroup, model_edit_type_t::link_to_group},
{proto_type_t::unlinkFromGroup, model_edit_type_t::unlink_from_group},
{proto_type_t::linkRepeater, model_edit_type_t::link_repeater},
{proto_type_t::unlinkRepeater, model_edit_type_t::unlink_repeater},
{proto_type_t::setPaired, model_edit_type_t::set_paired},
{proto_type_t::roomSortOrder, model_edit_type_t::room_order},
{proto_type_t::shadeSortOrder, model_edit_type_t::shade_order},
{proto_type_t::groupSortOrder, model_edit_type_t::group_order}
};
bool protoEditType(uint8_t type, model_edit_type_t &out) {
for(const edit_op_t &op : EDIT_OPS) {
if(static_cast<uint8_t>(op.type) != type) continue;
out = op.edit;
return true;
}
return false;
}
bool protoIsReadOp(uint8_t type) {
switch(static_cast<proto_type_t>(type)) {
case proto_type_t::getNextRoom:
case proto_type_t::getNextShade:
case proto_type_t::getNextGroup:
case proto_type_t::groupOptions:
return true;
default:
return false;
}
}
// What the reply's desc points at; every parse runs on async_tcp, one at a
// time.
static char why[64];
// One value under its field's kind: false when it isn't one.
static bool expandValue(const proto_field_info_t &f, JsonVariantConst v, JsonVariant out) {
switch(f.kind) {
case 'b':
// Booleans are 1 and 0; the edit routes read true and false.
if(!v.is<int>() || (v.as<int>() != 0 && v.as<int>() != 1)) return false;
out.set(v.as<int>() == 1);
return true;
case 'i':
if(v.is<int64_t>() || v.is<uint64_t>()) return out.set(v);
return false;
case 'n':
if(v.is<float>()) return out.set(v);
return false;
case 's':
if(v.is<const char *>()) return out.set(v);
return false;
case 'l':
if(v.is<JsonArrayConst>()) return out.set(v);
return false;
default:
return false;
}
}
const char *expandEditPayload(model_edit_type_t type, JsonObjectConst d, JsonDocument &doc) {
doc.clear();
switch(type) {
case model_edit_type_t::room_order:
case model_edit_type_t::shade_order:
case model_edit_type_t::group_order: {
// The route takes the ids on their own, in their new order.
JsonVariantConst ids = d[protoField(proto_field_t::ids).key];
if(!ids.is<JsonArrayConst>()) return "ids is missing";
for(JsonVariantConst id : ids.as<JsonArrayConst>())
if(!id.is<uint8_t>()) return "ids must be a list of ids";
doc.set(ids);
return doc.overflowed() ? "Too large" : nullptr;
}
default:
break;
}
JsonObject obj = doc.to<JsonObject>();
for(JsonPairConst kv : d) {
const proto_field_info_t *f = protoFieldByKey(kv.key().c_str());
if(!f) {
snprintf(why, sizeof(why), "Unknown field %s", kv.key().c_str());
return why;
}
if(!expandValue(*f, kv.value(), obj[f->name].to<JsonVariant>())) {
snprintf(why, sizeof(why), "%s is not a %s", f->name,
f->kind == 'b' ? "boolean (1 or 0)" : f->kind == 'i' ? "whole number" : f->kind == 'n' ? "number" :
f->kind == 's' ? "string" : "list");
return why;
}
}
return doc.overflowed() ? "Too large" : nullptr;
}

25
src/ProtoEdits.h Normal file
View file

@ -0,0 +1,25 @@
#ifndef PROTO_EDITS_H
#define PROTO_EDITS_H
#include <stdint.h>
#include <ArduinoJson.h>
#include "ProtoSchema.h"
#include "ModelEditor.h"
// Socket protocol 2's edit ops and the reads an edit form needs
// (docs/PROTOCOL.md). An edit op is the edit route of the same name: its
// payload, in full names, is what loop() applies with applyModelEdit(), so the
// rules stay in one place. Kept free of the shade model and the web server so
// it can be unit tested on the PC (pio test -e native).
// The edit an op asks for; false for a type that isn't an edit op.
bool protoEditType(uint8_t type, model_edit_type_t &out);
// The request of an edit route, from the op's payload d: full names for the
// short keys, and booleans as true and false. A sort order's request is its
// list of ids. nullptr when it is good; otherwise what is wrong with it, for
// a bad request reply. As with every op, only what was sent is in it.
const char *expandEditPayload(model_edit_type_t type, JsonObjectConst d, JsonDocument &doc);
// getNextRoom, getNextShade, getNextGroup and groupOptions: answered by
// loop() from the shade model.
bool protoIsReadOp(uint8_t type);
#endif

View file

@ -34,6 +34,29 @@
TYPE(unpairShade, 23, 'c') \
TYPE(setRollingCode, 24, 'c') \
TYPE(sendRemoteCommand, 25, 'c') \
TYPE(addRoom, 26, 'c') \
TYPE(addShade, 27, 'c') \
TYPE(addGroup, 28, 'c') \
TYPE(saveRoom, 29, 'c') \
TYPE(saveShade, 30, 'c') \
TYPE(saveGroup, 31, 'c') \
TYPE(deleteRoom, 32, 'c') \
TYPE(deleteShade, 33, 'c') \
TYPE(deleteGroup, 34, 'c') \
TYPE(linkRemote, 35, 'c') \
TYPE(unlinkRemote, 36, 'c') \
TYPE(linkToGroup, 37, 'c') \
TYPE(unlinkFromGroup, 38, 'c') \
TYPE(linkRepeater, 39, 'c') \
TYPE(unlinkRepeater, 40, 'c') \
TYPE(setPaired, 41, 'c') \
TYPE(roomSortOrder, 42, 'c') \
TYPE(shadeSortOrder, 43, 'c') \
TYPE(groupSortOrder, 44, 'c') \
TYPE(getNextRoom, 45, 'c') \
TYPE(getNextShade, 46, 'c') \
TYPE(getNextGroup, 47, 'c') \
TYPE(groupOptions, 48, 'c') \
TYPE(shadeState, 128, 'd') \
TYPE(shade, 129, 'd') \
TYPE(shadeRemoved, 130, 'd') \
@ -124,6 +147,8 @@
FIELD(repeat, "re", 'i', 0) \
FIELD(sunny, "sn", 'b', 0) \
FIELD(windy, "wn", 'b', 0) \
/* the edit ops */ \
FIELD(validation, "vc", 'i', 0) \
/* the header */ \
FIELD(maxRooms, "xr", 'i', 0) \
FIELD(maxShades, "xs", 'i', 0) \

View file

@ -176,6 +176,15 @@ uint8_t SomfyShadeController::getNextGroupId() {
const uint8_t id = firstFreeId(ids, SOMFY_MAX_GROUPS, SOMFY_MAX_GROUPS);
return id == 0 ? 255 : id;
}
uint32_t SomfyShadeController::getNextRemoteAddress(uint8_t id) {
uint32_t used[SOMFY_MAX_SHADES + SOMFY_MAX_GROUPS];
size_t count = 0;
for(uint8_t i = 0; i < SOMFY_MAX_SHADES; i++)
if(this->shades[i].getShadeId() != 255) used[count++] = this->shades[i].getRemoteAddress();
for(uint8_t i = 0; i < SOMFY_MAX_GROUPS; i++)
if(this->groups[i].getGroupId() != 255) used[count++] = this->groups[i].getRemoteAddress();
return freeRemoteAddress(this->startingAddress + id, used, count);
}
uint8_t SomfyShadeController::getNextRoomId() {
uint8_t ids[SOMFY_MAX_ROOMS];
for(uint8_t i = 0; i < SOMFY_MAX_ROOMS; i++) ids[i] = this->rooms[i].roomId;

View file

@ -239,6 +239,9 @@ class SomfyShadeController {
uint8_t getNextRoomId();
uint8_t getNextShadeId();
uint8_t getNextGroupId();
// The remote address a new shade or group with that id is offered: one no
// shade or group uses.
uint32_t getNextRemoteAddress(uint8_t id);
int8_t getMaxRoomOrder();
int8_t getMaxShadeOrder();
int8_t getMaxGroupOrder();

View file

@ -8,9 +8,14 @@
// the clients how it went.
// One edit on its way to loop(): the edit route and a copy of its request.
// From an edit route its result goes to every client as requestResult; from a
// protocol 2 edit op, as the reply to replyId for the socket client
// replyClient.
struct model_edit_t {
model_edit_type_t type = model_edit_type_t::add_room;
uint32_t requestId = 0;
uint32_t replyClient = 0;
uint32_t replyId = 0;
JsonDocument doc;
};
@ -20,7 +25,7 @@ bool beginModelEdit();
uint32_t nextEditRequestId();
// Gives up an edit that was begun but never queued. Any task.
void cancelModelEdit(model_edit_t *edit);
// Applies the edit, saves it, sends requestResult and frees it. loop() only.
// Applies the edit, saves it, tells how it went and frees it. loop() only.
void runModelEdit(model_edit_t *edit);
// Applies the edit to the shade model and saves it. loop() only.
edit_result_t applyModelEdit(model_edit_t &edit);

View file

@ -246,7 +246,8 @@ void runModelEdit(model_edit_t *edit) {
const edit_result_t result = applyModelEdit(*edit);
if(!result.ok) ESP_LOGW(TAG, "Edit %u refused: %s", static_cast<uint8_t>(edit->type), result.desc);
else markEdited(*edit, result);
emitRequestResult(edit->requestId, result);
if(edit->replyClient) protoSocket.replyEdit(edit->replyClient, edit->replyId, edit->type, result);
else emitRequestResult(edit->requestId, result);
cancelModelEdit(edit);
}
@ -357,8 +358,10 @@ edit_result_t applyModelEdit(model_edit_t &edit) {
if(!shade) return edit_result_t::error("Shade Id not found.");
if(obj["remoteAddress"].isNull()) return edit_result_t::error("Remote address not provided.");
const uint32_t address = obj["remoteAddress"];
// The route's name, or protocol 2's.
const uint16_t rollingCode = obj["rollingCode"] | (obj["lastRollingCode"] | (uint16_t)0);
if(edit.type == model_edit_type_t::unlink_remote) shade->unlinkRemote(address);
else if(!shade->linkRemote(address, obj["rollingCode"] | (uint16_t)0))
else if(!shade->linkRemote(address, rollingCode))
return edit_result_t::error("This shade already has the most linked remotes it can.");
return done(shade->getShadeId());
}

View file

@ -8,6 +8,9 @@
#include "ProtoParse.h"
#include "ProtoSlots.h"
#include "ProtoOps.h"
#include "ProtoEdits.h"
#include "ProtoAssembly.h"
#include "SomfyEditor.h"
#include "LoopQueue.h"
#include "SomfySnapshot.h"
#include "Somfy.h"
@ -37,6 +40,8 @@ static_assert(MAX_SOCK_RESPONSE >= PROTO_MAX_FRAME, "protocol 2 frames share the
static proto_slot_t slots[PROTO_MAX_CLIENTS];
// What loop() formats events and syncEnd with, into the socket event buffer.
static BufferedJsonFormatter eventJson;
// Messages that arrive in pieces. async_tcp only.
static MessageAssembler assembler(PROTO_MAX_CLIENT_FRAME);
/*********************************************************************
* The shade model as a sync source
@ -175,6 +180,50 @@ static void sendRemoteCommand(AsyncWebSocketClient *client, const proto_message_
}
replyOk(client, req.id);
}
// An edit op: loop() applies it as its edit route does, and replies.
static void queueEditOp(AsyncWebSocketClient *client, const proto_message_t &msg, model_edit_type_t type, JsonObjectConst d) {
if(!beginModelEdit()) {
replyError(client, msg.id, proto_error_t::busy, "Another edit is running, send it again");
return;
}
model_edit_t *edit = new (std::nothrow) model_edit_t();
if(!edit) {
cancelModelEdit(nullptr);
replyError(client, msg.id, proto_error_t::internal, "Out of memory");
return;
}
const char *bad = expandEditPayload(type, d, edit->doc);
if(bad) {
replyError(client, msg.id, proto_error_t::badRequest, bad);
cancelModelEdit(edit);
return;
}
edit->type = type;
edit->replyClient = client->id();
edit->replyId = msg.id;
loop_item_t item;
item.type = loop_item_type_t::model_edit;
item.edit = edit;
if(!loopQueue.submit(item)) {
cancelModelEdit(edit);
replyError(client, msg.id, proto_error_t::busy, "Command queue full");
}
}
// A read op: loop() answers it from the shade model.
static void queueReadOp(AsyncWebSocketClient *client, const proto_message_t &msg, JsonObjectConst d) {
loop_item_t item;
item.type = loop_item_type_t::proto_read;
item.readOp = msg.type;
if(msg.type == static_cast<uint8_t>(proto_type_t::groupOptions)) {
JsonVariantConst groupId = d[protoField(proto_field_t::groupId).key];
if(groupId.isNull()) { replyError(client, msg.id, proto_error_t::badRequest, "groupId is missing"); return; }
if(!groupId.is<uint8_t>()) { replyError(client, msg.id, proto_error_t::badRequest, "groupId must be 0-255"); return; }
item.readId = groupId.as<uint8_t>();
}
item.replyClient = client->id();
item.replyId = msg.id;
if(!loopQueue.submit(item)) replyError(client, msg.id, proto_error_t::busy, "Command queue full");
}
static bool authorized(AsyncWebServerRequest *request) {
if(settings.Security.type == security_types::None) return true;
const AsyncWebParameter *key = request->getParam("apikey");
@ -200,23 +249,19 @@ static void onConnect(AsyncWebSocketClient *client, AsyncWebServerRequest *reque
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) {
assembler.drop(client->id());
proto_slot_t *slot = slotOf(slots, PROTO_MAX_CLIENTS, client->id());
if(!slot) return;
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)) {
// 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;
}
// One whole message from a client.
static void onMessage(AsyncWebSocketClient *client, const char *text, size_t len) {
proto_slot_t *slot = slotOf(slots, PROTO_MAX_CLIENTS, client->id());
if(!slot) return;
JsonDocument doc;
proto_message_t msg;
const proto_error_t err = parseClientFrame(reinterpret_cast<const char *>(data), len, doc, msg);
const proto_error_t err = parseClientFrame(text, len, doc, msg);
if(err != proto_error_t::none) {
replyError(client, msg.id, err, err == proto_error_t::unknownOp ? "Unknown message type" : "Malformed message");
return;
@ -244,14 +289,67 @@ static void onFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *d
case proto_type_t::sendRemoteCommand:
sendRemoteCommand(client, msg, doc["d"]);
return;
default:
default: {
if(protoIsShadeOp(msg.type)) {
queueShadeOp(client, msg, doc["d"]);
return;
}
// The edit and settings ops arrive with the protocol core's later PRs.
model_edit_type_t edit;
if(protoEditType(msg.type, edit)) {
queueEditOp(client, msg, edit, doc["d"]);
return;
}
if(protoIsReadOp(msg.type)) {
queueReadOp(client, msg, doc["d"]);
return;
}
// The settings ops arrive with the protocol core's next PR.
replyError(client, msg.id, proto_error_t::unknownOp, "Unknown message type");
return;
}
}
}
static void onFrame(AsyncWebSocketClient *client, AwsFrameInfo *info, uint8_t *data, size_t len) {
// Most messages are a whole text frame in one piece.
if(info->final && info->index == 0 && info->len == len && info->opcode == WS_TEXT) {
onMessage(client, reinterpret_cast<const char *>(data), len);
return;
}
// The rest come in pieces: a frame larger than a TCP segment, or a message
// sent as several frames. Only a message's first frame has a text or
// binary opcode; the frames after it are continuations.
proto_piece_t piece;
piece.clientId = client->id();
piece.first = info->index == 0 && info->opcode != WS_CONTINUATION;
piece.last = info->final && info->index + len == info->len;
piece.text = info->message_opcode == WS_TEXT;
piece.data = data;
piece.len = len;
// Each refusal comes once per message, so the client gets one reply. The
// message couldn't be read, so the reply has no id.
switch(assembler.add(piece, millis())) {
case assembly_t::complete:
onMessage(client, assembler.text(), assembler.length());
assembler.done();
return;
case assembly_t::busy:
replyError(client, 0, proto_error_t::busy, "Another long message is on its way, send it again");
return;
case assembly_t::tooLarge:
replyError(client, 0, proto_error_t::badRequest, "Message too large");
return;
case assembly_t::notText:
replyError(client, 0, proto_error_t::badRequest, "Send text messages");
return;
case assembly_t::noMemory:
replyError(client, 0, proto_error_t::internal, "Out of memory");
return;
case assembly_t::timedOut:
replyError(client, 0, proto_error_t::badRequest, "The message took too long to arrive");
return;
case assembly_t::more:
case assembly_t::ignored:
return;
}
}
void ProtoSocket::wsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) {
@ -321,6 +419,83 @@ void ProtoSocket::replyRequest(uint32_t client, uint32_t id, const shade_request
if(!json.overflowed()) ws.text(client, buf, json.length());
}
// What an edit op's reply says: for an add, the new item's id; for a refused
// edit, the route's reason and the shade's validation code when it has one.
void ProtoSocket::replyEdit(uint32_t client, uint32_t id, model_edit_type_t type, const edit_result_t &result) {
if(!sockEmit.onLoopTask() || id == 0) return;
char buf[256];
BufferedJsonFormatter json;
json.setBuffer(buf, sizeof(buf));
beginFrame(json, proto_type_t::reply, id);
ProtoWriter w(json);
w.add(proto_field_t::ok, result.ok);
if(!result.ok) {
w.add(proto_field_t::code, static_cast<uint8_t>(proto_error_t::badRequest));
w.add(proto_field_t::desc, result.desc);
w.add(proto_field_t::validation, result.code);
}
else if(type == model_edit_type_t::add_room) w.add(proto_field_t::roomId, result.id);
else if(type == model_edit_type_t::add_shade) w.add(proto_field_t::shadeId, result.id);
else if(type == model_edit_type_t::add_group) w.add(proto_field_t::groupId, result.id);
endFrame(json);
// A client that has gone is skipped.
if(!json.overflowed()) ws.text(client, buf, json.length());
}
// The reads an edit form needs: what a new room, shade or group would get,
// and the shades a group could take.
void ProtoSocket::replyRead(uint32_t client, uint32_t id, uint8_t op, uint8_t groupId) {
if(!sockEmit.onLoopTask() || id == 0) return;
char buf[256];
BufferedJsonFormatter json;
json.setBuffer(buf, sizeof(buf));
const char *refused = nullptr;
proto_error_t code = proto_error_t::badRequest;
beginFrame(json, proto_type_t::reply, id);
ProtoWriter w(json);
switch(static_cast<proto_type_t>(op)) {
case proto_type_t::getNextRoom: {
const uint8_t roomId = somfy.getNextRoomId();
if(roomId == 0) { refused = "Maximum number of rooms exceeded."; break; }
w.add(proto_field_t::ok, true);
w.add(proto_field_t::roomId, roomId);
break;
}
case proto_type_t::getNextShade:
case proto_type_t::getNextGroup: {
const bool isShade = op == static_cast<uint8_t>(proto_type_t::getNextShade);
const uint8_t newId = isShade ? somfy.getNextShadeId() : somfy.getNextGroupId();
if(newId == 255) { refused = isShade ? "Maximum number of shades exceeded." : "Maximum number of groups exceeded."; break; }
w.add(proto_field_t::ok, true);
w.add(isShade ? proto_field_t::shadeId : proto_field_t::groupId, newId);
w.add(proto_field_t::remoteAddress, somfy.getNextRemoteAddress(newId));
w.add(proto_field_t::bitLength, somfy.transceiver.config.type);
w.add(proto_field_t::proto, static_cast<uint8_t>(somfy.transceiver.config.proto));
break;
}
case proto_type_t::groupOptions: {
SomfyGroup *group = somfy.getGroupById(groupId);
if(!group) { refused = "No group with that id"; code = proto_error_t::notFound; break; }
w.add(proto_field_t::ok, true);
w.add(proto_field_t::groupId, groupId);
// The shades it could take: every one it doesn't have.
w.beginList(proto_field_t::ids);
for(SomfyShade &shade : somfy.shades)
if(shade.getShadeId() != 255 && !group->hasShadeId(shade.getShadeId())) w.item(shade.getShadeId());
w.endList();
break;
}
default:
refused = "Not a read op";
break;
}
if(refused) {
json.setBuffer(buf, sizeof(buf));
writeErrorReply(json, id, code, refused);
}
else endFrame(json);
if(!json.overflowed()) ws.text(client, buf, json.length());
}
/*********************************************************************
* loop(): events
********************************************************************/

View file

@ -26,6 +26,10 @@ class ProtoSocket {
bool hasClients() const;
// The reply to a command op once loop() has run its shade request.
void replyRequest(uint32_t client, uint32_t id, const shade_request_t &req, shade_request_result_t result);
// The reply to an edit op once loop() has applied it.
void replyEdit(uint32_t client, uint32_t id, model_edit_type_t type, const edit_result_t &result);
// The reply to a read op, from the shade model. loop() only.
void replyRead(uint32_t client, uint32_t id, uint8_t op, uint8_t groupId);
// Events, loop() only.
void shadeState(SomfyShade &shade);
void shade(SomfyShade &shade);