fix: command op replies that said ok when nothing was done

From the review of #100:
- setMyPosition on a moving shade replies busy ("The shade is moving"),
  and setPositions with only a tilt, for a shade without one, replies
  bad request ("The shade has no tilt"). Both said ok before.
- PROTOCOL.md lists where the ops differ from the routes, and how
  sendRemoteCommand is answered.
- The LoopQueue hold comment covers protocol 2 replies and an unpair
  held through a frequency scan.
- One shadeRequestIsForGroup() for the group cases; ProtoOps shares one
  reader for "one or both" fields and one for ids, and the radio
  refused text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-24 12:49:23 +02:00
parent 598d708b12
commit 5737a58790
12 changed files with 103 additions and 49 deletions

View file

@ -64,9 +64,10 @@ class OpReader {
return v.is<const char *>() ? v.as<const char *>() : nullptr;
}
};
// What the reply's desc points at once parseShadeOp has returned.
// The reader's problem, kept once the reader has gone so the reply can send
// it. One buffer for every parse: they all run on async_tcp, one at a time.
static char lastError[64];
static const char *result(OpReader &r) {
static const char *errorOf(OpReader &r) {
if(!r.error) return nullptr;
snprintf(lastError, sizeof(lastError), "%s", r.error);
return lastError;
@ -116,17 +117,17 @@ static void readStep(OpReader &r, shade_request_t &req) {
// Steps are 7 bits on air.
if(r.get(proto_field_t::stepSize, 0, 127, n)) req.stepSize = static_cast<uint8_t>(n);
}
static void readShade(OpReader &r, shade_request_t &req) {
static void readId(OpReader &r, proto_field_t field, shade_request_t &req) {
int32_t n;
if(r.need(proto_field_t::shadeId, 0, 255, n)) req.id = static_cast<uint8_t>(n);
if(r.need(field, 0, 255, n)) req.id = static_cast<uint8_t>(n);
}
static void readShade(OpReader &r, shade_request_t &req) { readId(r, proto_field_t::shadeId, req); }
// A shade or a group; req.type becomes the one that was given.
static void readShadeOrGroup(OpReader &r, shade_request_t &req, shade_request_type_t shade, shade_request_type_t group) {
if(!r.either(proto_field_t::shadeId, proto_field_t::groupId)) return;
int32_t n;
const bool isShade = r.has(proto_field_t::shadeId);
req.type = isShade ? shade : group;
if(r.need(isShade ? proto_field_t::shadeId : proto_field_t::groupId, 0, 255, n)) req.id = static_cast<uint8_t>(n);
readId(r, isShade ? proto_field_t::shadeId : proto_field_t::groupId, req);
}
// A command, or a position 0-100 to move to.
static void readCommandOrTarget(OpReader &r, shade_request_t &req) {
@ -135,15 +136,16 @@ static void readCommandOrTarget(OpReader &r, shade_request_t &req) {
if(r.has(proto_field_t::command)) r.command(req.command);
else if(r.get(proto_field_t::target, 0, 100, n)) req.target = static_cast<int8_t>(n);
}
// Two positions 0-100 of which at least one is given; -1 for the other.
static void readPositions(OpReader &r, proto_field_t lift, proto_field_t tilt, int8_t &liftOut, int8_t &tiltOut) {
if(!r.has(lift) && !r.has(tilt)) {
r.fail("Give %s, %s or both", protoField(lift).name, protoField(tilt).name);
// Two values in lo..hi of which at least one is given; the other is left as
// it was.
static void readOneOrBoth(OpReader &r, proto_field_t a, proto_field_t b, int32_t lo, int32_t hi, int8_t &aOut, int8_t &bOut) {
if(!r.has(a) && !r.has(b)) {
r.fail("Give %s, %s or both", protoField(a).name, protoField(b).name);
return;
}
int32_t n;
if(r.get(lift, 0, 100, n)) liftOut = static_cast<int8_t>(n);
if(r.get(tilt, 0, 100, n)) tiltOut = static_cast<int8_t>(n);
if(r.get(a, lo, hi, n)) aOut = static_cast<int8_t>(n);
if(r.get(b, lo, hi, n)) bOut = static_cast<int8_t>(n);
}
const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req) {
@ -165,7 +167,7 @@ const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req)
break;
case proto_type_t::groupCommand:
req.type = shade_request_type_t::group_command;
if(r.need(proto_field_t::groupId, 0, 255, n)) req.id = static_cast<uint8_t>(n);
readId(r, proto_field_t::groupId, req);
r.command(req.command);
readRepeat(r, req);
readStep(r, req);
@ -179,19 +181,17 @@ const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req)
case proto_type_t::setPositions:
req.type = shade_request_type_t::set_positions;
readShade(r, req);
readPositions(r, proto_field_t::position, proto_field_t::tiltPosition, req.position, req.tiltPosition);
readOneOrBoth(r, proto_field_t::position, proto_field_t::tiltPosition, 0, 100, req.position, req.tiltPosition);
break;
case proto_type_t::setSensor:
readShadeOrGroup(r, req, shade_request_type_t::shade_sensor, shade_request_type_t::group_sensor);
if(!r.has(proto_field_t::sunny) && !r.has(proto_field_t::windy)) r.fail("Give sunny, windy or both");
if(r.get(proto_field_t::sunny, 0, 1, n)) req.sunny = static_cast<int8_t>(n);
if(r.get(proto_field_t::windy, 0, 1, n)) req.windy = static_cast<int8_t>(n);
readOneOrBoth(r, proto_field_t::sunny, proto_field_t::windy, 0, 1, req.sunny, req.windy);
readRepeat(r, req);
break;
case proto_type_t::setMyPosition:
req.type = shade_request_type_t::set_favorite;
readShade(r, req);
readPositions(r, proto_field_t::myPos, proto_field_t::myTiltPos, req.position, req.tiltPosition);
readOneOrBoth(r, proto_field_t::myPos, proto_field_t::myTiltPos, 0, 100, req.position, req.tiltPosition);
break;
case proto_type_t::unpairShade:
req.type = shade_request_type_t::unpair;
@ -206,7 +206,7 @@ const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req)
r.fail("Not a shade op");
break;
}
return result(r);
return errorOf(r);
}
const char *parseRemoteCommandOp(JsonObjectConst d, somfy_frame_t &frame, uint8_t &repeats) {
@ -220,28 +220,25 @@ const char *parseRemoteCommandOp(JsonObjectConst d, somfy_frame_t &frame, uint8_
if(r.need(proto_field_t::lastRollingCode, 1, UINT16_MAX, n)) frame.rollingCode = static_cast<uint16_t>(n);
if(r.get(proto_field_t::encKey, 0, UINT8_MAX, n)) frame.encKey = static_cast<uint8_t>(n);
if(r.get(proto_field_t::repeats, 0, UINT8_MAX, n)) repeats = static_cast<uint8_t>(n);
return result(r);
return errorOf(r);
}
proto_error_t protoRequestError(shade_request_result_t result) {
switch(result) {
case shade_request_result_t::not_found: return proto_error_t::notFound;
case shade_request_result_t::radio_refused: return proto_error_t::radioUnavailable;
// At rest it would take it: the client may try again.
case shade_request_result_t::shade_moving: return proto_error_t::busy;
case shade_request_result_t::no_tilt: return proto_error_t::badRequest;
default: return proto_error_t::none;
}
}
const char *protoRequestErrorDesc(const shade_request_t &req, shade_request_result_t result) {
switch(result) {
case shade_request_result_t::not_found:
switch(req.type) {
case shade_request_type_t::group_command:
case shade_request_type_t::group_repeat:
case shade_request_type_t::group_sensor:
return "No group with that id";
default:
return "No shade with that id";
}
case shade_request_result_t::radio_refused: return "The radio didn't take the command";
case shade_request_result_t::not_found: return shadeRequestIsForGroup(req.type) ? "No group with that id" : "No shade with that id";
case shade_request_result_t::radio_refused: return PROTO_RADIO_REFUSED;
case shade_request_result_t::shade_moving: return "The shade is moving";
case shade_request_result_t::no_tilt: return "The shade has no tilt";
default: return nullptr;
}
}