ProtoOps.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include <stdio.h> | ||
| 2 | #include <strings.h> | ||
| 3 | #include "ProtoOps.h" | ||
| 4 | |||
| 5 | // An op reads only the fields that were sent: unlike what the device sends, a | ||
| 6 | // left-out field is not its schema default but not given, so a client can | ||
| 7 | // leave a shade's tilt or its sun flag as they are. | ||
| 8 | class OpReader { | ||
| 9 | public: | ||
| 10 | 42 | explicit OpReader(JsonObjectConst d) : d(d) {} | |
| 11 | 145 | bool has(proto_field_t field) const { return !this->d[protoField(field).key].isNull(); } | |
| 12 | // The field when it was sent, as an integer in lo..hi. False when it | ||
| 13 | // wasn't; error is set when it was but isn't one. | ||
| 14 | 114 | bool get(proto_field_t field, int32_t lo, int32_t hi, int32_t &out) { | |
| 15 | 114 | JsonVariantConst v = this->d[protoField(field).key]; | |
| 16 | 114 | if(v.isNull()) return false; | |
| 17 | 64 | if(!v.is<int32_t>() || v.as<int32_t>() < lo || v.as<int32_t>() > hi) { | |
| 18 | 11 | this->fail("%s must be %ld-%ld", protoField(field).name, (long)lo, (long)hi); | |
| 19 | 11 | return false; | |
| 20 | } | ||
| 21 | 53 | out = v.as<int32_t>(); | |
| 22 | 53 | return true; | |
| 23 | } | ||
| 24 | // The same for a field the op can't do without. | ||
| 25 | 47 | bool need(proto_field_t field, int32_t lo, int32_t hi, int32_t &out) { | |
| 26 | 47 | if(!this->has(field)) { | |
| 27 | 5 | this->fail("%s is missing", protoField(field).name); | |
| 28 | 5 | return false; | |
| 29 | } | ||
| 30 | 42 | return this->get(field, lo, hi, out); | |
| 31 | } | ||
| 32 | 19 | bool command(somfy_commands &cmd) { | |
| 33 | 19 | if(!this->has(proto_field_t::command)) { | |
| 34 | 1 | this->fail("command is missing"); | |
| 35 | 1 | return false; | |
| 36 | } | ||
| 37 | 18 | if(!protoCommand(protoString(proto_field_t::command), cmd)) { | |
| 38 | 2 | this->fail("Not a command"); | |
| 39 | 2 | return false; | |
| 40 | } | ||
| 41 | 16 | return true; | |
| 42 | } | ||
| 43 | // Exactly one of the two. | ||
| 44 | 22 | bool either(proto_field_t a, proto_field_t b) { | |
| 45 | 22 | if(this->has(a) == this->has(b)) { | |
| 46 | 4 | this->fail("Give %s or %s", protoField(a).name, protoField(b).name); | |
| 47 | 4 | return false; | |
| 48 | } | ||
| 49 | 18 | return true; | |
| 50 | } | ||
| 51 | template<typename... Args> | ||
| 52 | 27 | void fail(const char *fmt, Args... args) { | |
| 53 | // Only the first problem is reported. | ||
| 54 | 27 | if(this->error) return; | |
| 55 | 25 | snprintf(this->why, sizeof(this->why), fmt, args...); | |
| 56 | 25 | this->error = this->why; | |
| 57 | } | ||
| 58 | const char *error = nullptr; | ||
| 59 | private: | ||
| 60 | JsonObjectConst d; | ||
| 61 | char why[64]; | ||
| 62 | 18 | const char *protoString(proto_field_t field) const { | |
| 63 | 18 | JsonVariantConst v = this->d[protoField(field).key]; | |
| 64 | 36 | return v.is<const char *>() ? v.as<const char *>() : nullptr; | |
| 65 | } | ||
| 66 | }; | ||
| 67 | // The reader's problem, kept once the reader has gone so the reply can send | ||
| 68 | // it. One buffer for every parse: they all run on async_tcp, one at a time. | ||
| 69 | static char lastError[64]; | ||
| 70 | 42 | static const char *errorOf(OpReader &r) { | |
| 71 | 42 | if(!r.error) return nullptr; | |
| 72 | 25 | snprintf(lastError, sizeof(lastError), "%s", r.error); | |
| 73 | 25 | return lastError; | |
| 74 | } | ||
| 75 | |||
| 76 | struct command_name_t { | ||
| 77 | const char *name; | ||
| 78 | somfy_commands cmd; | ||
| 79 | }; | ||
| 80 | // The names, and the spellings shadeCommanded reports, so they read back. | ||
| 81 | static const command_name_t COMMANDS[] = { | ||
| 82 | {"My", somfy_commands::My}, {"Up", somfy_commands::Up}, {"Down", somfy_commands::Down}, | ||
| 83 | {"MyUp", somfy_commands::MyUp}, {"My+Up", somfy_commands::MyUp}, | ||
| 84 | {"MyDown", somfy_commands::MyDown}, {"My+Down", somfy_commands::MyDown}, | ||
| 85 | {"UpDown", somfy_commands::UpDown}, {"Up+Down", somfy_commands::UpDown}, | ||
| 86 | {"MyUpDown", somfy_commands::MyUpDown}, {"My+Up+Down", somfy_commands::MyUpDown}, | ||
| 87 | {"Prog", somfy_commands::Prog}, | ||
| 88 | {"SunFlag", somfy_commands::SunFlag}, {"Sun Flag", somfy_commands::SunFlag}, | ||
| 89 | {"Flag", somfy_commands::Flag}, | ||
| 90 | {"StepUp", somfy_commands::StepUp}, {"Step Up", somfy_commands::StepUp}, | ||
| 91 | {"StepDown", somfy_commands::StepDown}, {"Step Down", somfy_commands::StepDown}, | ||
| 92 | {"Toggle", somfy_commands::Toggle}, {"Sensor", somfy_commands::Sensor}, | ||
| 93 | {"Favorite", somfy_commands::Favorite}, {"Stop", somfy_commands::Stop} | ||
| 94 | }; | ||
| 95 | 24 | bool protoCommand(const char *name, somfy_commands &cmd) { | |
| 96 | 24 | if(!name) return false; | |
| 97 | 170 | for(const command_name_t &c : COMMANDS) { | |
| 98 | 167 | if(strcasecmp(c.name, name) == 0) { | |
| 99 | 19 | cmd = c.cmd; | |
| 100 | 19 | return true; | |
| 101 | } | ||
| 102 | } | ||
| 103 | 3 | return false; | |
| 104 | } | ||
| 105 | |||
| 106 | // The repeat count and step size a command may carry. | ||
| 107 | 23 | static void readRepeat(OpReader &r, shade_request_t &req) { | |
| 108 | int32_t n; | ||
| 109 | 23 | if(r.get(proto_field_t::repeat, 0, INT8_MAX, n)) req.repeat = static_cast<int8_t>(n); | |
| 110 | 23 | } | |
| 111 | 19 | static void readStep(OpReader &r, shade_request_t &req) { | |
| 112 | int32_t n; | ||
| 113 | // Steps are 7 bits on air. | ||
| 114 | 19 | if(r.get(proto_field_t::stepSize, 0, 127, n)) req.stepSize = static_cast<uint8_t>(n); | |
| 115 | 19 | } | |
| 116 | 34 | static void readId(OpReader &r, proto_field_t field, shade_request_t &req) { | |
| 117 | int32_t n; | ||
| 118 | 34 | if(r.need(field, 0, 255, n)) req.id = static_cast<uint8_t>(n); | |
| 119 | 34 | } | |
| 120 | 26 | static void readShade(OpReader &r, shade_request_t &req) { readId(r, proto_field_t::shadeId, req); } | |
| 121 | // A shade or a group; req.type becomes the one that was given. | ||
| 122 | 8 | static void readShadeOrGroup(OpReader &r, shade_request_t &req, shade_request_type_t shade, shade_request_type_t group) { | |
| 123 | 8 | if(!r.either(proto_field_t::shadeId, proto_field_t::groupId)) return; | |
| 124 | 6 | const bool isShade = r.has(proto_field_t::shadeId); | |
| 125 | 6 | req.type = isShade ? shade : group; | |
| 126 | 6 | readId(r, isShade ? proto_field_t::shadeId : proto_field_t::groupId, req); | |
| 127 | } | ||
| 128 | // A command, or a position 0-100 to move to. | ||
| 129 | 14 | static void readCommandOrTarget(OpReader &r, shade_request_t &req) { | |
| 130 | 14 | if(!r.either(proto_field_t::command, proto_field_t::target)) return; | |
| 131 | int32_t n; | ||
| 132 | 12 | if(r.has(proto_field_t::command)) r.command(req.command); | |
| 133 | 4 | else if(r.get(proto_field_t::target, 0, 100, n)) req.target = static_cast<int8_t>(n); | |
| 134 | } | ||
| 135 | // Two values in lo..hi of which at least one is given; the other is left as | ||
| 136 | // it was. | ||
| 137 | 11 | 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) { | |
| 138 | 11 | if(!r.has(a) && !r.has(b)) { | |
| 139 | 3 | r.fail("Give %s, %s or both", protoField(a).name, protoField(b).name); | |
| 140 | 3 | return; | |
| 141 | } | ||
| 142 | int32_t n; | ||
| 143 | 8 | if(r.get(a, lo, hi, n)) aOut = static_cast<int8_t>(n); | |
| 144 | 8 | if(r.get(b, lo, hi, n)) bOut = static_cast<int8_t>(n); | |
| 145 | } | ||
| 146 | |||
| 147 | 37 | const char *parseShadeOp(uint8_t type, JsonObjectConst d, shade_request_t &req) { | |
| 148 | 37 | req = shade_request_t(); | |
| 149 | 37 | OpReader r(d); | |
| 150 | int32_t n; | ||
| 151 | 37 | switch(static_cast<proto_type_t>(type)) { | |
| 152 | 13 | case proto_type_t::shadeCommand: | |
| 153 | 13 | req.type = shade_request_type_t::shade_command; | |
| 154 | 13 | readShade(r, req); | |
| 155 | 13 | readCommandOrTarget(r, req); | |
| 156 | 13 | readRepeat(r, req); | |
| 157 | 13 | readStep(r, req); | |
| 158 | 13 | break; | |
| 159 | 1 | case proto_type_t::tiltCommand: | |
| 160 | 1 | req.type = shade_request_type_t::tilt_command; | |
| 161 | 1 | readShade(r, req); | |
| 162 | 1 | readCommandOrTarget(r, req); | |
| 163 | 1 | break; | |
| 164 | 2 | case proto_type_t::groupCommand: | |
| 165 | 2 | req.type = shade_request_type_t::group_command; | |
| 166 | 2 | readId(r, proto_field_t::groupId, req); | |
| 167 | 2 | r.command(req.command); | |
| 168 | 2 | readRepeat(r, req); | |
| 169 | 2 | readStep(r, req); | |
| 170 | 2 | break; | |
| 171 | 4 | case proto_type_t::repeatCommand: | |
| 172 | 4 | readShadeOrGroup(r, req, shade_request_type_t::shade_repeat, shade_request_type_t::group_repeat); | |
| 173 | 4 | r.command(req.command); | |
| 174 | 4 | readRepeat(r, req); | |
| 175 | 4 | readStep(r, req); | |
| 176 | 4 | break; | |
| 177 | 4 | case proto_type_t::setPositions: | |
| 178 | 4 | req.type = shade_request_type_t::set_positions; | |
| 179 | 4 | readShade(r, req); | |
| 180 | 4 | readOneOrBoth(r, proto_field_t::position, proto_field_t::tiltPosition, 0, 100, req.position, req.tiltPosition); | |
| 181 | 4 | break; | |
| 182 | 4 | case proto_type_t::setSensor: | |
| 183 | 4 | readShadeOrGroup(r, req, shade_request_type_t::shade_sensor, shade_request_type_t::group_sensor); | |
| 184 | 4 | readOneOrBoth(r, proto_field_t::sunny, proto_field_t::windy, 0, 1, req.sunny, req.windy); | |
| 185 | 4 | readRepeat(r, req); | |
| 186 | 4 | break; | |
| 187 | 3 | case proto_type_t::setMyPosition: | |
| 188 | 3 | req.type = shade_request_type_t::set_favorite; | |
| 189 | 3 | readShade(r, req); | |
| 190 | 3 | readOneOrBoth(r, proto_field_t::myPos, proto_field_t::myTiltPos, 0, 100, req.position, req.tiltPosition); | |
| 191 | 3 | break; | |
| 192 | 2 | case proto_type_t::unpairShade: | |
| 193 | 2 | req.type = shade_request_type_t::unpair; | |
| 194 | 2 | readShade(r, req); | |
| 195 | 2 | break; | |
| 196 | 3 | case proto_type_t::setRollingCode: | |
| 197 | 3 | req.type = shade_request_type_t::set_rolling_code; | |
| 198 | 3 | readShade(r, req); | |
| 199 | 3 | if(r.need(proto_field_t::lastRollingCode, 0, UINT16_MAX, n)) req.rollingCode = static_cast<uint16_t>(n); | |
| 200 | 3 | break; | |
| 201 | 1 | default: | |
| 202 | 1 | r.fail("Not a shade op"); | |
| 203 | 1 | break; | |
| 204 | } | ||
| 205 | 74 | return errorOf(r); | |
| 206 | } | ||
| 207 | |||
| 208 | 5 | const char *parseRemoteCommandOp(JsonObjectConst d, somfy_frame_t &frame, uint8_t &repeats) { | |
| 209 | 5 | frame = somfy_frame_t(); | |
| 210 | 5 | repeats = 0; | |
| 211 | 5 | OpReader r(d); | |
| 212 | int32_t n; | ||
| 213 | // A remote address is 24 bits, and 0 is no remote. | ||
| 214 | 5 | if(r.need(proto_field_t::remoteAddress, 1, 0xFFFFFF, n)) frame.remoteAddress = static_cast<uint32_t>(n); | |
| 215 | 5 | r.command(frame.cmd); | |
| 216 | 5 | if(r.need(proto_field_t::lastRollingCode, 1, UINT16_MAX, n)) frame.rollingCode = static_cast<uint16_t>(n); | |
| 217 | 5 | if(r.get(proto_field_t::encKey, 0, UINT8_MAX, n)) frame.encKey = static_cast<uint8_t>(n); | |
| 218 | 5 | if(r.get(proto_field_t::repeats, 0, UINT8_MAX, n)) repeats = static_cast<uint8_t>(n); | |
| 219 | 10 | return errorOf(r); | |
| 220 | } | ||
| 221 | |||
| 222 | 5 | proto_error_t protoRequestError(shade_request_result_t result) { | |
| 223 | 5 | switch(result) { | |
| 224 | 1 | case shade_request_result_t::not_found: return proto_error_t::notFound; | |
| 225 | 1 | case shade_request_result_t::radio_refused: return proto_error_t::radioUnavailable; | |
| 226 | // At rest it would take it: the client may try again. | ||
| 227 | 1 | case shade_request_result_t::shade_moving: return proto_error_t::busy; | |
| 228 | 1 | case shade_request_result_t::no_tilt: return proto_error_t::badRequest; | |
| 229 | 1 | default: return proto_error_t::none; | |
| 230 | } | ||
| 231 | } | ||
| 232 | 6 | const char *protoRequestErrorDesc(const shade_request_t &req, shade_request_result_t result) { | |
| 233 | 6 | switch(result) { | |
| 234 | 2 | case shade_request_result_t::not_found: return shadeRequestIsForGroup(req.type) ? "No group with that id" : "No shade with that id"; | |
| 235 | 1 | case shade_request_result_t::radio_refused: return PROTO_RADIO_REFUSED; | |
| 236 | 1 | case shade_request_result_t::shade_moving: return "The shade is moving"; | |
| 237 | 1 | case shade_request_result_t::no_tilt: return "The shade has no tilt"; | |
| 238 | 1 | default: return nullptr; | |
| 239 | } | ||
| 240 | } | ||
| 241 |