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:
cjkas 2026-10-24 13:17:15 +02:00
parent d869bb53d5
commit dc2f1478b6
29 changed files with 1087 additions and 670 deletions

View file

@ -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();
}