ProtoEdits.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include <stdio.h> | ||
| 2 | #include <string.h> | ||
| 3 | #include "ProtoEdits.h" | ||
| 4 | #include "ProtoParse.h" | ||
| 5 | |||
| 6 | struct edit_op_t { | ||
| 7 | proto_type_t type; | ||
| 8 | model_edit_type_t edit; | ||
| 9 | }; | ||
| 10 | static const edit_op_t EDIT_OPS[] = { | ||
| 11 | {proto_type_t::addRoom, model_edit_type_t::add_room}, | ||
| 12 | {proto_type_t::addShade, model_edit_type_t::add_shade}, | ||
| 13 | {proto_type_t::addGroup, model_edit_type_t::add_group}, | ||
| 14 | {proto_type_t::saveRoom, model_edit_type_t::save_room}, | ||
| 15 | {proto_type_t::saveShade, model_edit_type_t::save_shade}, | ||
| 16 | {proto_type_t::saveGroup, model_edit_type_t::save_group}, | ||
| 17 | {proto_type_t::deleteRoom, model_edit_type_t::delete_room}, | ||
| 18 | {proto_type_t::deleteShade, model_edit_type_t::delete_shade}, | ||
| 19 | {proto_type_t::deleteGroup, model_edit_type_t::delete_group}, | ||
| 20 | {proto_type_t::linkRemote, model_edit_type_t::link_remote}, | ||
| 21 | {proto_type_t::unlinkRemote, model_edit_type_t::unlink_remote}, | ||
| 22 | {proto_type_t::linkToGroup, model_edit_type_t::link_to_group}, | ||
| 23 | {proto_type_t::unlinkFromGroup, model_edit_type_t::unlink_from_group}, | ||
| 24 | {proto_type_t::linkRepeater, model_edit_type_t::link_repeater}, | ||
| 25 | {proto_type_t::unlinkRepeater, model_edit_type_t::unlink_repeater}, | ||
| 26 | {proto_type_t::setPaired, model_edit_type_t::set_paired}, | ||
| 27 | {proto_type_t::roomSortOrder, model_edit_type_t::room_order}, | ||
| 28 | {proto_type_t::shadeSortOrder, model_edit_type_t::shade_order}, | ||
| 29 | {proto_type_t::groupSortOrder, model_edit_type_t::group_order} | ||
| 30 | }; | ||
| 31 | 374 | bool protoEditType(uint8_t type, model_edit_type_t &out) { | |
| 32 | 7077 | for(const edit_op_t &op : EDIT_OPS) { | |
| 33 | 6744 | if(static_cast<uint8_t>(op.type) != type) continue; | |
| 34 | 41 | out = op.edit; | |
| 35 | 41 | return true; | |
| 36 | } | ||
| 37 | 333 | return false; | |
| 38 | } | ||
| 39 | 7 | const char *parseReadPayload(uint8_t type, JsonObjectConst d, uint8_t &groupId) { | |
| 40 | 7 | groupId = 0; | |
| 41 | 7 | if(type != static_cast<uint8_t>(proto_type_t::groupOptions)) return nullptr; | |
| 42 | 4 | JsonVariantConst v = d[protoField(proto_field_t::groupId).key]; | |
| 43 | 4 | if(v.isNull()) return "groupId is missing"; | |
| 44 | 3 | if(!v.is<uint8_t>()) return "groupId must be 0-255"; | |
| 45 | 1 | groupId = v.as<uint8_t>(); | |
| 46 | 1 | return nullptr; | |
| 47 | } | ||
| 48 | |||
| 49 | // The fields each edit's route reads, so a field it would ignore is refused | ||
| 50 | // rather than dropped without a word. A shade's flags aren't among them: | ||
| 51 | // they hold light, which is set on its own, and the sun and wind state, which | ||
| 52 | // setSensor sets. A group's sunSensor is worked out from its shades. | ||
| 53 | static const proto_field_t ROOM_FIELDS[] = {proto_field_t::name, proto_field_t::sortOrder}; | ||
| 54 | static const proto_field_t SHADE_FIELDS[] = { | ||
| 55 | proto_field_t::name, proto_field_t::roomId, proto_field_t::remoteAddress, proto_field_t::shadeType, | ||
| 56 | proto_field_t::tiltType, proto_field_t::bitLength, proto_field_t::proto, proto_field_t::upTime, | ||
| 57 | proto_field_t::downTime, proto_field_t::tiltTime, proto_field_t::stepSize, proto_field_t::flipCommands, | ||
| 58 | proto_field_t::flipPosition, proto_field_t::sunSensor, proto_field_t::light, proto_field_t::simMy, | ||
| 59 | proto_field_t::repeats, proto_field_t::gpioUp, proto_field_t::gpioDown, proto_field_t::gpioMy, | ||
| 60 | proto_field_t::gpioLLTrigger | ||
| 61 | }; | ||
| 62 | static const proto_field_t GROUP_FIELDS[] = { | ||
| 63 | proto_field_t::name, proto_field_t::roomId, proto_field_t::remoteAddress, proto_field_t::bitLength, | ||
| 64 | proto_field_t::proto, proto_field_t::flipCommands, proto_field_t::repeats | ||
| 65 | }; | ||
| 66 | template<size_t N> | ||
| 67 | 32 | static bool in(proto_field_t field, const proto_field_t (&fields)[N]) { | |
| 68 | 361 | for(proto_field_t f : fields) | |
| 69 | 355 | if(f == field) return true; | |
| 70 | 6 | return false; | |
| 71 | } | ||
| 72 | 59 | static bool takes(model_edit_type_t type, proto_field_t f) { | |
| 73 | 59 | switch(type) { | |
| 74 | ✗ | case model_edit_type_t::add_room: return in(f, ROOM_FIELDS); | |
| 75 | 4 | case model_edit_type_t::save_room: return f == proto_field_t::roomId || in(f, ROOM_FIELDS); | |
| 76 | 1 | case model_edit_type_t::add_shade: return in(f, SHADE_FIELDS); | |
| 77 | 34 | case model_edit_type_t::save_shade: return f == proto_field_t::shadeId || in(f, SHADE_FIELDS); | |
| 78 | ✗ | case model_edit_type_t::add_group: return in(f, GROUP_FIELDS); | |
| 79 | 4 | case model_edit_type_t::save_group: return f == proto_field_t::groupId || in(f, GROUP_FIELDS); | |
| 80 | 2 | case model_edit_type_t::delete_room: return f == proto_field_t::roomId; | |
| 81 | 1 | case model_edit_type_t::delete_shade: return f == proto_field_t::shadeId; | |
| 82 | ✗ | case model_edit_type_t::delete_group: return f == proto_field_t::groupId; | |
| 83 | 3 | case model_edit_type_t::link_remote: | |
| 84 | 3 | return f == proto_field_t::shadeId || f == proto_field_t::remoteAddress || f == proto_field_t::lastRollingCode; | |
| 85 | 3 | case model_edit_type_t::unlink_remote: return f == proto_field_t::shadeId || f == proto_field_t::remoteAddress; | |
| 86 | ✗ | case model_edit_type_t::link_to_group: | |
| 87 | ✗ | case model_edit_type_t::unlink_from_group: return f == proto_field_t::groupId || f == proto_field_t::shadeId; | |
| 88 | ✗ | case model_edit_type_t::link_repeater: | |
| 89 | ✗ | case model_edit_type_t::unlink_repeater: return f == proto_field_t::remoteAddress; | |
| 90 | 7 | case model_edit_type_t::set_paired: return f == proto_field_t::shadeId || f == proto_field_t::paired; | |
| 91 | ✗ | default: return false; | |
| 92 | } | ||
| 93 | } | ||
| 94 | // The name a field has in the edit route's request. Only linkRemote's rolling | ||
| 95 | // code is named apart from the dictionary. | ||
| 96 | 51 | static const char *routeName(model_edit_type_t type, proto_field_t f) { | |
| 97 | 51 | if(type == model_edit_type_t::link_remote && f == proto_field_t::lastRollingCode) return "rollingCode"; | |
| 98 | 50 | return protoField(f).name; | |
| 99 | } | ||
| 100 | // What the reply's desc points at; every parse runs on async_tcp, one at a | ||
| 101 | // time. | ||
| 102 | static char why[64]; | ||
| 103 | 26 | const char *expandEditPayload(model_edit_type_t type, JsonObjectConst d, JsonDocument &doc) { | |
| 104 | 26 | doc.clear(); | |
| 105 | 26 | switch(type) { | |
| 106 | 5 | case model_edit_type_t::room_order: | |
| 107 | case model_edit_type_t::shade_order: | ||
| 108 | case model_edit_type_t::group_order: { | ||
| 109 | // The route takes the ids on their own, in their new order. | ||
| 110 | 9 | for(JsonPairConst kv : d) { | |
| 111 | 5 | if(strcmp(kv.key().c_str(), protoField(proto_field_t::ids).key) == 0) continue; | |
| 112 | 1 | return "A sort order takes only ids"; | |
| 113 | } | ||
| 114 | 4 | JsonVariantConst ids = d[protoField(proto_field_t::ids).key]; | |
| 115 | 4 | if(!ids.is<JsonArrayConst>()) return "ids is missing"; | |
| 116 | 8 | for(JsonVariantConst id : ids.as<JsonArrayConst>()) | |
| 117 | 7 | if(!id.is<uint8_t>()) return "ids must be a list of ids"; | |
| 118 | 1 | doc.set(ids); | |
| 119 | 1 | return doc.overflowed() ? "Too large" : nullptr; | |
| 120 | } | ||
| 121 | 21 | default: | |
| 122 | 21 | break; | |
| 123 | } | ||
| 124 | 21 | JsonObject obj = doc.to<JsonObject>(); | |
| 125 | 68 | for(JsonPairConst kv : d) { | |
| 126 | proto_field_t field; | ||
| 127 | 60 | if(!protoFieldOfKey(kv.key().c_str(), field)) { | |
| 128 | 1 | snprintf(why, sizeof(why), "Unknown field %s", kv.key().c_str()); | |
| 129 | 13 | return why; | |
| 130 | } | ||
| 131 | 59 | const proto_field_info_t &f = protoField(field); | |
| 132 | 59 | if(!takes(type, field)) { | |
| 133 | 8 | snprintf(why, sizeof(why), "%s can't be set by this edit", f.name); | |
| 134 | 8 | return why; | |
| 135 | } | ||
| 136 | 51 | if(!protoRouteValue(f, kv.value(), obj[routeName(type, field)].to<JsonVariant>())) { | |
| 137 | 4 | snprintf(why, sizeof(why), "%s is not a %s", f.name, protoKindName(f.kind)); | |
| 138 | 4 | return why; | |
| 139 | } | ||
| 140 | } | ||
| 141 | // A left-out field is not given, so it can't stand for unpaired. | ||
| 142 | 8 | if(type == model_edit_type_t::set_paired && obj["paired"].isNull()) return "paired is missing"; | |
| 143 | 7 | return doc.overflowed() ? "Too large" : nullptr; | |
| 144 | } | ||
| 145 |