feat: command ops for socket protocol 2

shadeCommand, groupCommand, tiltCommand, repeatCommand, setPositions,
setSensor, setMyPosition, unpairShade, setRollingCode (types 16-24) are
read on async_tcp by ProtoOps and handed to loop() through the
LoopQueue, which replies once the request has run: ok, not found or
radio unavailable. A bad payload, a paused radio or a full queue is
answered straight away. sendRemoteCommand (25) touches nothing of the
shade model, so it goes to the radio from async_tcp like its route.

ShadeRequests::run() now says how a request went, not only whether
the radio took it. An op reads only the fields that were sent, so
setPositions can leave the tilt as it is. The shadeCommand event is
renamed shadeCommanded, since the op has the route's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 11:21:55 +02:00
parent 9fe05105ff
commit 0b54264ee3
18 changed files with 812 additions and 50 deletions

View file

@ -21,69 +21,71 @@ static bool inRange(int8_t pos) { return pos >= 0 && pos <= 100; }
static int8_t storedPosition(int8_t pos, bool flip) { return displayPosition(static_cast<float>(pos), flip); }
static uint8_t repeatsOr(int8_t repeat, uint8_t own) { return repeat >= 0 ? static_cast<uint8_t>(repeat) : own; }
bool ShadeRequests::run(const shade_request_t &req) {
static shade_request_result_t sent(bool taken) { return taken ? shade_request_result_t::done : shade_request_result_t::radio_refused; }
shade_request_result_t ShadeRequests::run(const shade_request_t &req) {
shade_info_t shade;
uint8_t groupRepeats = 0;
switch(req.type) {
case shade_request_type_t::group_command:
case shade_request_type_t::group_repeat:
case shade_request_type_t::group_sensor:
if(!this->ops.findGroup(req.id, groupRepeats)) return true;
if(!this->ops.findGroup(req.id, groupRepeats)) return shade_request_result_t::not_found;
break;
default:
if(!this->ops.findShade(req.id, shade)) return true;
if(!this->ops.findShade(req.id, shade)) return shade_request_result_t::not_found;
break;
}
const bool flip = shade.flipPosition;
switch(req.type) {
case shade_request_type_t::shade_command:
if(inRange(req.target)) return this->ops.moveToTarget(req.id, storedPosition(req.target, flip));
return this->ops.sendCommand(req.id, req.command, repeatsOr(req.repeat, shade.repeats), req.stepSize);
if(inRange(req.target)) return sent(this->ops.moveToTarget(req.id, storedPosition(req.target, flip)));
return sent(this->ops.sendCommand(req.id, req.command, repeatsOr(req.repeat, shade.repeats), req.stepSize));
case shade_request_type_t::group_command:
return this->ops.sendGroupCommand(req.id, req.command, repeatsOr(req.repeat, groupRepeats), req.stepSize);
return sent(this->ops.sendGroupCommand(req.id, req.command, repeatsOr(req.repeat, groupRepeats), req.stepSize));
case shade_request_type_t::tilt_command:
if(inRange(req.target)) return this->ops.moveToTiltTarget(req.id, storedPosition(req.target, flip));
return this->ops.sendTiltCommand(req.id, req.command);
if(inRange(req.target)) return sent(this->ops.moveToTiltTarget(req.id, storedPosition(req.target, flip)));
return sent(this->ops.sendTiltCommand(req.id, req.command));
case shade_request_type_t::shade_repeat: {
// A single button garage door has no Prog, its button is Toggle.
somfy_commands cmd = req.command;
if(shade.shadeType == shade_types::garage1 && cmd == somfy_commands::Prog) cmd = somfy_commands::Toggle;
const uint8_t repeat = repeatsOr(req.repeat, shade.repeats);
if(this->ops.isLastCommand(req.id, cmd)) return this->ops.repeatFrame(req.id, repeat);
return this->ops.sendCommand(req.id, cmd, repeat, req.stepSize);
if(this->ops.isLastCommand(req.id, cmd)) return sent(this->ops.repeatFrame(req.id, repeat));
return sent(this->ops.sendCommand(req.id, cmd, repeat, req.stepSize));
}
case shade_request_type_t::group_repeat: {
const uint8_t repeat = repeatsOr(req.repeat, groupRepeats);
if(this->ops.isLastGroupCommand(req.id, req.command)) return this->ops.repeatGroupFrame(req.id, repeat);
return this->ops.sendGroupCommand(req.id, req.command, repeat, req.stepSize);
if(this->ops.isLastGroupCommand(req.id, req.command)) return sent(this->ops.repeatGroupFrame(req.id, repeat));
return sent(this->ops.sendGroupCommand(req.id, req.command, repeat, req.stepSize));
}
case shade_request_type_t::set_positions: {
const float pos = inRange(req.position) ? storedPosition(req.position, flip) : -1.0f;
const float tilt = inRange(req.tiltPosition) && shade.tiltType != tilt_types::none ? storedPosition(req.tiltPosition, flip) : -1.0f;
if(pos >= 0.0f || tilt >= 0.0f) this->ops.setPositions(req.id, pos, tilt);
return true;
return shade_request_result_t::done;
}
case shade_request_type_t::shade_sensor:
return this->ops.sendSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, shade.repeats));
return sent(this->ops.sendSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, shade.repeats)));
case shade_request_type_t::group_sensor:
return this->ops.sendGroupSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, groupRepeats));
return sent(this->ops.sendGroupSensorCommand(req.id, req.windy, req.sunny, repeatsOr(req.repeat, groupRepeats)));
case shade_request_type_t::set_favorite: {
const int8_t tilt = requestedFavoriteTilt(req.tiltPosition, shade.tiltType, shade.myTiltPos, flip);
// Without a lift position only the tilt changes: the lift favorite stays,
// or is set where the shade is when it has none.
int8_t pos = req.position;
if(!inRange(pos)) pos = displayPosition(shade.myPos >= 0.0f ? shade.myPos : shade.currentPos, flip);
return this->ops.setMyPosition(req.id, storedPosition(pos, flip), storedPosition(tilt, flip));
return sent(this->ops.setMyPosition(req.id, storedPosition(pos, flip), storedPosition(tilt, flip)));
}
case shade_request_type_t::unpair: {
// A 56-bit motor needs a longer Prog press than an 80-bit one.
const bool sent = this->ops.sendCommand(req.id, somfy_commands::Prog, shade.bitLength == 56 ? 7 : 1, 0);
if(sent) this->ops.setUnpaired(req.id);
return sent;
const bool taken = this->ops.sendCommand(req.id, somfy_commands::Prog, shade.bitLength == 56 ? 7 : 1, 0);
if(taken) this->ops.setUnpaired(req.id);
return sent(taken);
}
case shade_request_type_t::set_rolling_code:
this->ops.setRollingCode(req.id, req.rollingCode);
return true;
return shade_request_result_t::done;
}
return true;
return shade_request_result_t::done;
}