fix: e2e failures on the device: edit replies before their events, /ws closed on reboot

- runModelEdit sent an edit's events (room, roomRemoved, the orders,
  repeaters) before its reply, where PROTOCOL.md says they follow it. A
  client waiting for the reply read past them (test_adding_and_deleting_a_room).
- A restart dropped the /ws connections without a close, so clients never
  saw them go (test_reboot). ESPNetwork::end() now closes them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-25 12:43:06 +01:00
parent 72e99d14c5
commit 330d8fa0de
4 changed files with 8 additions and 1 deletions

View file

@ -57,6 +57,7 @@ void ESPNetwork::end() {
SSDP.end(); SSDP.end();
mqtt.end(); mqtt.end();
sockEmit.end(); sockEmit.end();
protoSocket.end();
delay(100); delay(100);
} }
bool ESPNetwork::setup() { bool ESPNetwork::setup() {

View file

@ -266,9 +266,11 @@ static void markEdited(const model_edit_t &edit, const edit_result_t &result) {
void runModelEdit(model_edit_t *edit) { void runModelEdit(model_edit_t *edit) {
const edit_result_t result = applyModelEdit(*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); if(!result.ok) ESP_LOGW(TAG, "Edit %u refused: %s", static_cast<uint8_t>(edit->type), result.desc);
else markEdited(*edit, result); // The reply first: the edit's events follow it (docs/PROTOCOL.md), so a
// client that waits for the reply doesn't read past them.
if(edit->replyTo.client) protoSocket.replyEdit(edit->replyTo, edit->type, result); if(edit->replyTo.client) protoSocket.replyEdit(edit->replyTo, edit->type, result);
else emitRequestResult(edit->requestId, result); else emitRequestResult(edit->requestId, result);
if(result.ok) markEdited(*edit, result);
cancelModelEdit(edit); cancelModelEdit(edit);
} }

View file

@ -415,6 +415,7 @@ void ProtoSocket::begin(AsyncWebServer &server) {
ws.onEvent(ProtoSocket::wsEvent); ws.onEvent(ProtoSocket::wsEvent);
server.addHandler(&ws); server.addHandler(&ws);
} }
void ProtoSocket::end() { ws.closeAll(1001, "Restarting"); }
bool ProtoSocket::hasClients() const { bool ProtoSocket::hasClients() const {
for(const proto_slot_t &s : slots) for(const proto_slot_t &s : slots)
if(s.clientId.load()) return true; if(s.clientId.load()) return true;

View file

@ -24,6 +24,9 @@ class ProtoSocket {
void begin(AsyncWebServer &server); void begin(AsyncWebServer &server);
// Moves each client's sync on. loop() only. // Moves each client's sync on. loop() only.
void loop(); void loop();
// Closes every client, before a reboot: a restart drops the connections
// without a word, so clients wouldn't know they had gone.
void end();
bool hasClients() const; bool hasClients() const;
// The reply to a command op once loop() has run its shade request. // The reply to a command op once loop() has run its shade request.
void replyRequest(const proto_reply_to_t &to, const shade_request_t &req, shade_request_result_t result); void replyRequest(const proto_reply_to_t &to, const shade_request_t &req, shade_request_result_t result);