ProtoSettings.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include <stdio.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "ProtoSettings.h" | ||
| 5 | #include "ProtoParse.h" | ||
| 6 | #include "EthPhy.h" | ||
| 7 | |||
| 8 | static const char *const SECTIONS[] = {"general", "network", "mqtt", "security", "radio"}; | ||
| 9 | 16 | const char *settingsSectionName(settings_section_t section) { return SECTIONS[static_cast<uint8_t>(section)]; } | |
| 10 | |||
| 11 | 717 | static settings_section_t sectionOf(settings_part_t part) { | |
| 12 | 717 | switch(part) { | |
| 13 | 62 | case settings_part_t::general: | |
| 14 | 62 | case settings_part_t::ntp: return settings_section_t::general; | |
| 15 | 263 | case settings_part_t::conn: | |
| 16 | case settings_part_t::wifi: | ||
| 17 | case settings_part_t::ethernet: | ||
| 18 | 263 | case settings_part_t::ip: return settings_section_t::network; | |
| 19 | 125 | case settings_part_t::mqtt: return settings_section_t::mqtt; | |
| 20 | 57 | case settings_part_t::security: return settings_section_t::security; | |
| 21 | 210 | default: return settings_section_t::radio; | |
| 22 | } | ||
| 23 | } | ||
| 24 | |||
| 25 | // What a save may set. lo..hi is a whole number's range, a number's in | ||
| 26 | // hundredths, and hi a string's longest length: what the setting holds, so the | ||
| 27 | // route's fromJSON neither cuts it short nor wraps it. The has* fields and | ||
| 28 | // radioInit are only ever sent. | ||
| 29 | struct settings_field_t { | ||
| 30 | settings_part_t part; | ||
| 31 | proto_field_t field; | ||
| 32 | const char *route; // the name the route reads; nullptr when it is the field's | ||
| 33 | int32_t lo; | ||
| 34 | int32_t hi; | ||
| 35 | }; | ||
| 36 | static const settings_field_t FIELDS[] = { | ||
| 37 | {settings_part_t::general, proto_field_t::hostname, nullptr, 0, 31}, | ||
| 38 | {settings_part_t::general, proto_field_t::ssdpBroadcast, nullptr, 0, 0}, | ||
| 39 | {settings_part_t::ntp, proto_field_t::ntpServer, nullptr, 0, 64}, | ||
| 40 | {settings_part_t::ntp, proto_field_t::posixZone, nullptr, 0, 63}, | ||
| 41 | // wifi, ethernet and ethernet with WiFi as the fallback (conn_types_t). | ||
| 42 | {settings_part_t::conn, proto_field_t::connMode, "connType", 1, 3}, | ||
| 43 | {settings_part_t::wifi, proto_field_t::ssid, nullptr, 0, 64}, | ||
| 44 | {settings_part_t::wifi, proto_field_t::passphrase, nullptr, 0, 64}, | ||
| 45 | {settings_part_t::wifi, proto_field_t::roaming, nullptr, 0, 0}, | ||
| 46 | {settings_part_t::wifi, proto_field_t::hidden, nullptr, 0, 0}, | ||
| 47 | {settings_part_t::ethernet, proto_field_t::boardType, nullptr, 0, 255}, | ||
| 48 | {settings_part_t::ethernet, proto_field_t::phyType, nullptr, 0, 255}, | ||
| 49 | {settings_part_t::ethernet, proto_field_t::CLKMode, nullptr, 0, ETH_CLOCK_ID_MAX}, | ||
| 50 | {settings_part_t::ethernet, proto_field_t::phyAddress, nullptr, -1, 31}, | ||
| 51 | // Pins run to GPIO 54, the P4's last. | ||
| 52 | {settings_part_t::ethernet, proto_field_t::PWRPin, nullptr, -1, 54}, | ||
| 53 | {settings_part_t::ethernet, proto_field_t::MDCPin, nullptr, 0, 54}, | ||
| 54 | {settings_part_t::ethernet, proto_field_t::MDIOPin, nullptr, 0, 54}, | ||
| 55 | {settings_part_t::ip, proto_field_t::dhcp, nullptr, 0, 0}, | ||
| 56 | {settings_part_t::ip, proto_field_t::ip, nullptr, 0, 15}, | ||
| 57 | {settings_part_t::ip, proto_field_t::gateway, nullptr, 0, 15}, | ||
| 58 | {settings_part_t::ip, proto_field_t::subnet, nullptr, 0, 15}, | ||
| 59 | {settings_part_t::ip, proto_field_t::dns1, nullptr, 0, 15}, | ||
| 60 | {settings_part_t::ip, proto_field_t::dns2, nullptr, 0, 15}, | ||
| 61 | {settings_part_t::mqtt, proto_field_t::enabled, nullptr, 0, 0}, | ||
| 62 | {settings_part_t::mqtt, proto_field_t::pubDisco, nullptr, 0, 0}, | ||
| 63 | {settings_part_t::mqtt, proto_field_t::protocol, nullptr, 0, 9}, | ||
| 64 | {settings_part_t::mqtt, proto_field_t::hostname, nullptr, 0, 64}, | ||
| 65 | {settings_part_t::mqtt, proto_field_t::port, nullptr, 1, 65535}, | ||
| 66 | {settings_part_t::mqtt, proto_field_t::username, nullptr, 0, 32}, | ||
| 67 | {settings_part_t::mqtt, proto_field_t::password, nullptr, 0, 32}, | ||
| 68 | {settings_part_t::mqtt, proto_field_t::rootTopic, nullptr, 0, 64}, | ||
| 69 | {settings_part_t::mqtt, proto_field_t::discoTopic, nullptr, 0, 64}, | ||
| 70 | // None, PIN or password (security_types). | ||
| 71 | {settings_part_t::security, proto_field_t::securityType, "type", 0, 2}, | ||
| 72 | {settings_part_t::security, proto_field_t::username, nullptr, 0, 32}, | ||
| 73 | {settings_part_t::security, proto_field_t::password, nullptr, 0, 32}, | ||
| 74 | {settings_part_t::security, proto_field_t::pin, nullptr, 0, 4}, | ||
| 75 | // 56 or 80, checked on its own. | ||
| 76 | {settings_part_t::radio, proto_field_t::bitLength, "type", 56, 80}, | ||
| 77 | // RTS, RTW, RTV, GP Relay or GP Remote (radio_proto), checked on its own. | ||
| 78 | {settings_part_t::radio, proto_field_t::proto, nullptr, 0, 9}, | ||
| 79 | {settings_part_t::radio, proto_field_t::enabled, nullptr, 0, 0}, | ||
| 80 | {settings_part_t::radio, proto_field_t::TXPin, nullptr, 0, 54}, | ||
| 81 | {settings_part_t::radio, proto_field_t::RXPin, nullptr, 0, 54}, | ||
| 82 | {settings_part_t::radio, proto_field_t::SCKPin, nullptr, 0, 54}, | ||
| 83 | {settings_part_t::radio, proto_field_t::MOSIPin, nullptr, 0, 54}, | ||
| 84 | {settings_part_t::radio, proto_field_t::MISOPin, nullptr, 0, 54}, | ||
| 85 | {settings_part_t::radio, proto_field_t::CSNPin, nullptr, 0, 54}, | ||
| 86 | // What the CC1101 can be set to, in MHz and kHz. | ||
| 87 | {settings_part_t::radio, proto_field_t::frequency, nullptr, 30000, 92800}, | ||
| 88 | {settings_part_t::radio, proto_field_t::rxBandwidth, nullptr, 5803, 81250}, | ||
| 89 | {settings_part_t::radio, proto_field_t::deviation, nullptr, 158, 38085}, | ||
| 90 | {settings_part_t::radio, proto_field_t::txPower, nullptr, -30, 12}, | ||
| 91 | {settings_part_t::radio, proto_field_t::noiseDetection, nullptr, 0, 0} | ||
| 92 | }; | ||
| 93 | 252 | static const char *routeName(const settings_field_t &s) { return s.route ? s.route : protoField(s.field).name; } | |
| 94 | 83 | static const settings_field_t *saved(settings_section_t section, proto_field_t field) { | |
| 95 | 2321 | for(const settings_field_t &s : FIELDS) | |
| 96 | 2312 | if(s.field == field && sectionOf(s.part) == section) return &s; | |
| 97 | 9 | return nullptr; | |
| 98 | } | ||
| 99 | |||
| 100 | 79 | const char *parseSettingsSection(JsonObjectConst d, settings_section_t §ion) { | |
| 101 | 79 | const char *name = protoString(d, proto_field_t::section); | |
| 102 | 79 | if(!name) return "section is missing"; | |
| 103 | 251 | for(uint8_t i = 0; i < sizeof(SECTIONS) / sizeof(SECTIONS[0]); i++) { | |
| 104 | 249 | if(strcmp(SECTIONS[i], name) != 0) continue; | |
| 105 | 74 | section = static_cast<settings_section_t>(i); | |
| 106 | 74 | return nullptr; | |
| 107 | } | ||
| 108 | 2 | return "Unknown section"; | |
| 109 | } | ||
| 110 | |||
| 111 | // Four numbers 0-255 between dots. | ||
| 112 | 8 | static bool isAddress(const char *s) { | |
| 113 | 31 | for(int part = 0; part < 4; part++) { | |
| 114 | 30 | if(part > 0 && *s++ != '.') return false; | |
| 115 | 26 | if(*s < '0' || *s > '9') return false; | |
| 116 | char *end; | ||
| 117 | 24 | const long n = strtol(s, &end, 10); | |
| 118 | 24 | if(n > 255 || end - s > 3) return false; | |
| 119 | 23 | s = end; | |
| 120 | } | ||
| 121 | 4 | return *s == '\0'; | |
| 122 | } | ||
| 123 | |||
| 124 | // What the reply's desc points at; every parse runs on async_tcp, one at a | ||
| 125 | // time. | ||
| 126 | static char why[64]; | ||
| 127 | // A value already of its field's kind: nullptr when it is in range. | ||
| 128 | 69 | static const char *checkValue(const settings_field_t &s, const proto_field_info_t &f, JsonVariant value) { | |
| 129 | 69 | if(f.kind == 'i') { | |
| 130 | 29 | const bool inRange = value.is<int64_t>() && value.as<int64_t>() >= s.lo && value.as<int64_t>() <= s.hi; | |
| 131 | 29 | if(s.field == proto_field_t::bitLength) { | |
| 132 | 5 | if(inRange && (value.as<int>() == 56 || value.as<int>() == 80)) return nullptr; | |
| 133 | 2 | snprintf(why, sizeof(why), "%s must be 56 or 80", f.name); | |
| 134 | 2 | return why; | |
| 135 | } | ||
| 136 | 24 | if(s.field == proto_field_t::proto) { | |
| 137 | 4 | if(inRange && (value.as<int>() <= 2 || value.as<int>() >= 8)) return nullptr; | |
| 138 | 2 | snprintf(why, sizeof(why), "%s must be 0, 1, 2, 8 or 9", f.name); | |
| 139 | 2 | return why; | |
| 140 | } | ||
| 141 | 20 | if(inRange) return nullptr; | |
| 142 | 9 | if(s.lo == s.hi) snprintf(why, sizeof(why), "%s must be %ld", f.name, (long)s.lo); | |
| 143 | 9 | else snprintf(why, sizeof(why), "%s must be %ld-%ld", f.name, (long)s.lo, (long)s.hi); | |
| 144 | 9 | return why; | |
| 145 | } | ||
| 146 | 40 | if(f.kind == 'n') { | |
| 147 | // Half a hundredth either way, as 58.03 is a little under it in a float. | ||
| 148 | 11 | const double hundredths = value.as<double>() * 100; | |
| 149 | 11 | if(hundredths > s.lo - 0.5 && hundredths < s.hi + 0.5) return nullptr; | |
| 150 | 3 | snprintf(why, sizeof(why), "%s must be %ld.%02ld-%ld.%02ld", f.name, (long)(s.lo / 100), (long)(s.lo % 100), | |
| 151 | 3 | (long)(s.hi / 100), (long)(s.hi % 100)); | |
| 152 | 3 | return why; | |
| 153 | } | ||
| 154 | 29 | if(f.kind != 's') return nullptr; | |
| 155 | 23 | const char *text = value.as<const char *>(); | |
| 156 | 23 | if(strlen(text) > static_cast<size_t>(s.hi)) { | |
| 157 | 3 | snprintf(why, sizeof(why), "%s is longer than %ld characters", f.name, (long)s.hi); | |
| 158 | 3 | return why; | |
| 159 | } | ||
| 160 | 20 | if(s.part != settings_part_t::ip) return nullptr; | |
| 161 | // The settings send 0.0.0.0 as "", so "" sets it back. | ||
| 162 | 10 | if(!text[0]) value.set("0.0.0.0"); | |
| 163 | 8 | else if(!isAddress(text)) { | |
| 164 | 5 | snprintf(why, sizeof(why), "%s is not an IP address", f.name); | |
| 165 | 5 | return why; | |
| 166 | } | ||
| 167 | 5 | return nullptr; | |
| 168 | } | ||
| 169 | |||
| 170 | 70 | const char *expandSettingsPayload(JsonObjectConst d, bool hasEthernet, settings_section_t §ion, JsonDocument &doc) { | |
| 171 | 70 | doc.clear(); | |
| 172 | 70 | const char *bad = parseSettingsSection(d, section); | |
| 173 | 70 | if(bad) return bad; | |
| 174 | 69 | JsonObject obj = doc.to<JsonObject>(); | |
| 175 | 297 | for(JsonPairConst kv : d) { | |
| 176 | proto_field_t field; | ||
| 177 | 154 | if(!protoFieldOfKey(kv.key().c_str(), field)) { | |
| 178 | 2 | snprintf(why, sizeof(why), "Unknown field %s", kv.key().c_str()); | |
| 179 | 40 | return why; | |
| 180 | } | ||
| 181 | 152 | if(field == proto_field_t::section) continue; | |
| 182 | 83 | const proto_field_info_t &f = protoField(field); | |
| 183 | 83 | const settings_field_t *s = saved(section, field); | |
| 184 | 83 | if(!s) { | |
| 185 | 9 | snprintf(why, sizeof(why), "%s can't be saved in %s", f.name, settingsSectionName(section)); | |
| 186 | 9 | return why; | |
| 187 | } | ||
| 188 | 74 | if(s->part == settings_part_t::ethernet && !hasEthernet) return "This device has no Ethernet"; | |
| 189 | 73 | JsonVariant value = obj[routeName(*s)].to<JsonVariant>(); | |
| 190 | 73 | if(!protoRouteValue(f, kv.value(), value)) { | |
| 191 | 4 | snprintf(why, sizeof(why), "%s is not a %s", f.name, protoKindName(f.kind)); | |
| 192 | 4 | return why; | |
| 193 | } | ||
| 194 | 69 | bad = checkValue(*s, f, value); | |
| 195 | 69 | if(bad) return bad; | |
| 196 | } | ||
| 197 | 29 | if(obj.size() == 0) return "Nothing to save"; | |
| 198 | 28 | return doc.overflowed() ? "Too large" : nullptr; | |
| 199 | } | ||
| 200 | |||
| 201 | 8 | uint16_t settingsParts(settings_section_t section, JsonObjectConst request) { | |
| 202 | 8 | uint16_t parts = 0; | |
| 203 | 21 | for(JsonPairConst kv : request) | |
| 204 | 650 | for(const settings_field_t &s : FIELDS) | |
| 205 | 637 | if(sectionOf(s.part) == section && strcmp(routeName(s), kv.key().c_str()) == 0) | |
| 206 | 13 | parts |= 1u << static_cast<uint8_t>(s.part); | |
| 207 | 8 | return parts; | |
| 208 | } | ||
| 209 |