ESPSomfy-RTS/src/ProtoParse.cpp
cjkas 2517f40709 feat: settings ops, the WiFi and frequency scans, and reboot for socket protocol 2
Step 1d of the protocol 2 core (docs/SECURITY_PLAN.md).

- getSettings/saveSettings by section: general, network, mqtt, security,
  radio. A save is the settings route's request, so loop() applies it with
  the settings' own fromJSON(). ProtoSettings checks every field first:
  kind, range, string length, IP addresses.
- Secrets are only reported as has*. A security save replies with the
  client's new token and closes the other sockets; other saves send
  settingsChanged to the other clients.
- scanWifi runs an async scan from ESPNetwork, kept apart from the roaming
  scan, and wifiNetworks goes to the clients that asked.
- beginFrequencyScan, endFrequencyScan and reboot run at once on async_tcp,
  as their routes do: a scan's end mustn't wait in the loop queue behind an
  unpair that waits for the scan to end (new op kind 'a').
- e2e: test_settings.py and a --reboot flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:52:09 +01:00

67 lines
2.6 KiB
C++

#include "ProtoParse.h"
proto_error_t parseClientFrame(const char *text, size_t len, JsonDocument &doc, proto_message_t &out) {
out = proto_message_t();
if(len > PROTO_MAX_CLIENT_FRAME) return proto_error_t::badRequest;
if(deserializeJson(doc, text, len) != DeserializationError::Ok) return proto_error_t::badRequest;
if(!doc.is<JsonObject>()) return proto_error_t::badRequest;
JsonVariantConst id = doc["id"];
if(id.is<uint32_t>()) out.id = id.as<uint32_t>();
else if(!id.isNull()) return proto_error_t::badRequest;
if(!doc["v"].is<int>() || doc["v"].as<int>() != PROTO_VERSION) return proto_error_t::badRequest;
JsonVariantConst t = doc["t"];
if(!t.is<int>() || t.as<int>() < 1 || t.as<int>() > 255) return proto_error_t::badRequest;
out.type = static_cast<uint8_t>(t.as<int>());
if(!protoClientMaySend(out.type)) return proto_error_t::unknownOp;
if(protoIsOp(out.type) && out.id == 0) return proto_error_t::badRequest;
JsonVariantConst d = doc["d"];
if(!d.isNull() && !d.is<JsonObjectConst>()) return proto_error_t::badRequest;
return proto_error_t::none;
}
int32_t protoInt(JsonObjectConst d, proto_field_t field) {
const proto_field_info_t &f = protoField(field);
JsonVariantConst v = d[f.key];
return v.is<int32_t>() ? v.as<int32_t>() : f.def;
}
bool protoBool(JsonObjectConst d, proto_field_t field) {
const proto_field_info_t &f = protoField(field);
JsonVariantConst v = d[f.key];
return v.is<int>() ? v.as<int>() != 0 : f.def != 0;
}
const char *protoString(JsonObjectConst d, proto_field_t field) {
JsonVariantConst v = d[protoField(field).key];
return v.is<const char *>() ? v.as<const char *>() : nullptr;
}
bool protoHas(JsonObjectConst d, proto_field_t field) { return !d[protoField(field).key].isNull(); }
bool protoRouteValue(const proto_field_info_t &f, JsonVariantConst v, JsonVariant out) {
switch(f.kind) {
case 'b':
// Booleans are 1 and 0; the routes read true and false.
if(!v.is<int>() || (v.as<int>() != 0 && v.as<int>() != 1)) return false;
out.set(v.as<int>() == 1);
return true;
case 'i':
if(v.is<int64_t>() || v.is<uint64_t>()) return out.set(v);
return false;
case 'n':
if(v.is<float>()) return out.set(v);
return false;
case 's':
if(v.is<const char *>()) return out.set(v);
return false;
default:
return false;
}
}
const char *protoKindName(char kind) {
switch(kind) {
case 'b': return "boolean (1 or 0)";
case 'i': return "whole number";
case 'n': return "number";
case 's': return "string";
case 'l': return "list";
default: return "object";
}
}