♻️ Replaces JsonObject with JsonVariant

This commit is contained in:
Rune Harlyk
2025-07-10 17:34:52 +02:00
committed by Rune Harlyk
parent 144b99c180
commit e3cfe89e19
19 changed files with 75 additions and 79 deletions
+3 -3
View File
@@ -81,8 +81,8 @@ esp_err_t EventSocket::onFrame(PsychicWebSocketRequest *request, httpd_ws_frame
ESP_LOGV("EventSocket", "Disconnect: %s", event);
client_subscriptions[event].remove(request->client()->socket());
} else if (message_type == EVENT) {
JsonObject jsonObject = msg[2].as<JsonObject>();
handleEventCallbacks(event, jsonObject, request->client()->socket());
JsonVariant payload = msg[2].as<JsonVariant>();
handleEventCallbacks(event, payload, request->client()->socket());
return ESP_OK;
}
return ESP_OK;
@@ -149,7 +149,7 @@ void EventSocket::send(PsychicWebSocketClient *client, const char *data, size_t
#endif
}
void EventSocket::handleEventCallbacks(String event, JsonObject &jsonObject, int originId) {
void EventSocket::handleEventCallbacks(String event, JsonVariant &jsonObject, int originId) {
for (auto &callback : event_callbacks[event]) {
callback(jsonObject, originId);
}
+2 -2
View File
@@ -9,7 +9,7 @@
enum message_type_t { CONNECT = 0, DISCONNECT = 1, EVENT = 2, PING = 3, PONG = 4, BINARY_EVENT = 5 };
typedef std::function<void(JsonObject &root, int originId)> EventCallback;
typedef std::function<void(JsonVariant &root, int originId)> EventCallback;
typedef std::function<void(const String &originId, bool sync)> SubscribeCallback;
class EventSocket {
@@ -32,7 +32,7 @@ class EventSocket {
std::map<String, std::list<int>> client_subscriptions;
std::map<String, std::list<EventCallback>> event_callbacks;
std::map<String, std::list<SubscribeCallback>> subscribe_callbacks;
void handleEventCallbacks(String event, JsonObject &jsonObject, int originId);
void handleEventCallbacks(String event, JsonVariant &jsonObject, int originId);
void send(PsychicWebSocketClient *client, const char *data, size_t len);
void handleSubscribeCallbacks(String event, const String &originId);
+4 -4
View File
@@ -67,12 +67,12 @@ void MDNSService::addServices() {
esp_err_t MDNSService::getStatus(PsychicRequest *request) {
PsychicJsonResponse response = PsychicJsonResponse(request, false);
JsonObject root = response.getRoot();
JsonVariant root = response.getRoot();
getStatus(root);
return response.send();
}
void MDNSService::getStatus(JsonObject &root) {
void MDNSService::getStatus(JsonVariant &root) {
state().read(state(), root);
root["started"] = _started;
}
@@ -82,7 +82,7 @@ esp_err_t MDNSService::queryServices(PsychicRequest *request, JsonVariant &json)
String proto = json["protocol"].as<String>();
PsychicJsonResponse response = PsychicJsonResponse(request, false);
JsonObject root = response.getRoot();
JsonVariant root = response.getRoot();
ESP_LOGI(TAG, "Querying for service: %s, protocol: %s", service.c_str(), proto.c_str());
@@ -91,7 +91,7 @@ esp_err_t MDNSService::queryServices(PsychicRequest *request, JsonVariant &json)
JsonArray servicesArray = root["services"].to<JsonArray>();
for (int i = 0; i < n; i++) {
JsonObject serviceObj = servicesArray.add<JsonObject>();
JsonVariant serviceObj = servicesArray.add<JsonVariant>();
serviceObj["name"] = MDNS.hostname(i);
serviceObj["ip"] = MDNS.IP(i);
serviceObj["port"] = MDNS.port(i);
+1 -1
View File
@@ -25,7 +25,7 @@ class MDNSService : public StatefulService<MDNSSettings> {
void begin();
esp_err_t getStatus(PsychicRequest *request);
void getStatus(JsonObject &root);
void getStatus(JsonVariant &root);
static esp_err_t queryServices(PsychicRequest *request, JsonVariant &json);
+12 -12
View File
@@ -24,13 +24,13 @@ class MotionService {
MotionService(ServoController *servoController) : _servoController(servoController) {}
void begin() {
socket.onEvent(INPUT_EVENT, [&](JsonObject &root, int originId) { handleInput(root, originId); });
socket.onEvent(INPUT_EVENT, [&](JsonVariant &root, int originId) { handleInput(root, originId); });
socket.onEvent(MODE_EVENT, [&](JsonObject &root, int originId) { handleMode(root, originId); });
socket.onEvent(MODE_EVENT, [&](JsonVariant &root, int originId) { handleMode(root, originId); });
socket.onEvent(ANGLES_EVENT, [&](JsonObject &root, int originId) { anglesEvent(root, originId); });
socket.onEvent(ANGLES_EVENT, [&](JsonVariant &root, int originId) { anglesEvent(root, originId); });
socket.onEvent(POSITION_EVENT, [&](JsonObject &root, int originId) { positionEvent(root, originId); });
socket.onEvent(POSITION_EVENT, [&](JsonVariant &root, int originId) { positionEvent(root, originId); });
socket.onSubscribe(ANGLES_EVENT,
std::bind(&MotionService::syncAngles, this, std::placeholders::_1, std::placeholders::_2));
@@ -38,16 +38,16 @@ class MotionService {
body_state.updateFeet(kinematics.default_feet_positions);
}
void anglesEvent(JsonObject &root, int originId) {
JsonArray array = root["data"].as<JsonArray>();
void anglesEvent(JsonVariant &root, int originId) {
JsonArray array = root.as<JsonArray>();
for (int i = 0; i < 12; i++) {
angles[i] = array[i];
}
syncAngles(String(originId));
}
void positionEvent(JsonObject &root, int originId) {
JsonArray array = root["data"].as<JsonArray>();
void positionEvent(JsonVariant &root, int originId) {
JsonArray array = root.as<JsonArray>();
body_state.omega = array[0];
body_state.phi = array[1];
body_state.psi = array[2];
@@ -56,8 +56,8 @@ class MotionService {
body_state.zm = array[5];
}
void handleInput(JsonObject &root, int originId) {
JsonArray array = root["data"].as<JsonArray>();
void handleInput(JsonVariant &root, int originId) {
JsonArray array = root.as<JsonArray>();
command.lx = array[1];
command.lx = array[1];
command.ly = array[2];
@@ -81,8 +81,8 @@ class MotionService {
}
}
void handleMode(JsonObject &root, int originId) {
motionState = static_cast<MOTION_STATE>(root["data"].as<int>());
void handleMode(JsonVariant &root, int originId) {
motionState = static_cast<MOTION_STATE>(root.as<int>());
ESP_LOGV("MotionService", "Mode %d", motionState);
motionState == MOTION_STATE::DEACTIVATED ? _servoController->deactivate() : _servoController->activate();
JsonDocument doc;
@@ -55,7 +55,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
_eventEndpoint.begin();
_persistence.readFromFS();
socket.onEvent(EVENT_I2C_SCAN, [&](JsonObject &root, int originId) {
socket.onEvent(EVENT_I2C_SCAN, [&](JsonVariant &root, int originId) {
scanI2C();
emitI2C();
});
@@ -33,12 +33,12 @@ class ServoController : public StatefulService<ServoSettings> {
void begin() {
socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
[&](JsonObject &root, int originId) { servoEvent(root, originId); });
socket.onEvent(EVENT_SERVO_STATE, [&](JsonObject &root, int originId) { stateUpdate(root, originId); });
[&](JsonVariant &root, int originId) { servoEvent(root, originId); });
socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) { stateUpdate(root, originId); });
_persistence.readFromFS();
initializePCA();
socket.onEvent(EVENT_SERVO_STATE, [&](JsonObject &root, int originId) {
socket.onEvent(EVENT_SERVO_STATE, [&](JsonVariant &root, int originId) {
is_active = root["active"] | false;
is_active ? activate() : deactivate();
});
@@ -66,13 +66,13 @@ class ServoController : public StatefulService<ServoSettings> {
_pca.sleep();
}
void stateUpdate(JsonObject &root, int originId) {
void stateUpdate(JsonVariant &root, int originId) {
bool active = root["active"].as<bool>();
ESP_LOGI("SERVOCONTROLLER", "Setting state %d", active);
active ? activate() : deactivate();
}
void servoEvent(JsonObject &root, int originId) {
void servoEvent(JsonVariant &root, int originId) {
control_state = SERVO_CONTROL_STATE::PWM;
int8_t servo_id = root["servo_id"];
uint16_t pwm = root["pwm"].as<uint16_t>();
@@ -75,7 +75,7 @@ class APSettings {
localIP == settings.localIP && gatewayIP == settings.gatewayIP && subnetMask == settings.subnetMask;
}
static void read(APSettings &settings, JsonObject &root) {
static void read(APSettings &settings, JsonVariant &root) {
root["provision_mode"] = settings.provisionMode;
root["ssid"] = settings.ssid;
root["password"] = settings.password;
@@ -87,7 +87,7 @@ class APSettings {
root["subnet_mask"] = settings.subnetMask.toString();
}
static StateUpdateResult update(JsonObject &root, APSettings &settings) {
static StateUpdateResult update(JsonVariant &root, APSettings &settings) {
APSettings newSettings = {};
newSettings.provisionMode = root["provision_mode"] | FACTORY_AP_PROVISION_MODE;
switch (settings.provisionMode) {
@@ -39,7 +39,7 @@ class CameraSettings {
uint8_t dcw;
uint8_t colorbar;
static void read(CameraSettings &settings, JsonObject &root) {
static void read(CameraSettings &settings, JsonVariant &root) {
root["pixformat"] = settings.pixformat;
root["framesize"] = settings.framesize;
root["quality"] = settings.quality;
@@ -71,7 +71,7 @@ class CameraSettings {
root["colorbar"] = settings.colorbar;
}
static StateUpdateResult update(JsonObject &root, CameraSettings &settings) {
static StateUpdateResult update(JsonVariant &root, CameraSettings &settings) {
settings.pixformat = root["pixformat"];
settings.framesize = root["framesize"];
settings.brightness = root["brightness"];
@@ -18,12 +18,12 @@ typedef struct {
String key;
String value;
void serialize(JsonObject &json) const {
void serialize(JsonVariant &json) const {
json["key"] = key;
json["value"] = value;
}
bool deserialize(const JsonObject &json) {
bool deserialize(const JsonVariant &json) {
key = json["key"].as<String>();
value = json["value"].as<String>();
@@ -37,7 +37,7 @@ typedef struct {
uint16_t port;
std::vector<mdns_txt_record_t> txtRecords;
void serialize(JsonObject &json) const {
void serialize(JsonVariant &json) const {
json["service"] = service;
json["protocol"] = protocol;
json["port"] = port;
@@ -45,13 +45,13 @@ typedef struct {
if (txtRecords.size() > 0) {
JsonArray txtArray = json["txt_records"].to<JsonArray>();
for (const auto &txt : txtRecords) {
JsonObject txtObj = txtArray.add<JsonObject>();
JsonVariant txtObj = txtArray.add<JsonVariant>();
txt.serialize(txtObj);
}
}
}
bool deserialize(const JsonObject &json) {
bool deserialize(const JsonVariant &json) {
service = json["service"].as<String>();
protocol = json["protocol"].as<String>();
port = json["port"] | 0;
@@ -59,7 +59,7 @@ typedef struct {
txtRecords.clear();
if (json["txt_records"].is<JsonArray>()) {
JsonArray txtArray = json["txt_records"];
for (JsonObject txtObj : txtArray) {
for (JsonVariant txtObj : txtArray) {
mdns_txt_record_t txt;
if (txt.deserialize(txtObj)) {
txtRecords.push_back(txt);
@@ -78,31 +78,31 @@ class MDNSSettings {
std::vector<mdns_service_t> services;
std::vector<mdns_txt_record_t> globalTxtRecords;
static void read(MDNSSettings &settings, JsonObject &root) {
static void read(MDNSSettings &settings, JsonVariant &root) {
root["hostname"] = settings.hostname;
root["instance"] = settings.instance;
JsonArray servicesArray = root["services"].to<JsonArray>();
for (const auto &service : settings.services) {
JsonObject serviceObj = servicesArray.add<JsonObject>();
JsonVariant serviceObj = servicesArray.add<JsonVariant>();
service.serialize(serviceObj);
}
JsonArray txtArray = root["global_txt_records"].to<JsonArray>();
for (const auto &txt : settings.globalTxtRecords) {
JsonObject txtObj = txtArray.add<JsonObject>();
JsonVariant txtObj = txtArray.add<JsonVariant>();
txt.serialize(txtObj);
}
}
static StateUpdateResult update(JsonObject &root, MDNSSettings &settings) {
static StateUpdateResult update(JsonVariant &root, MDNSSettings &settings) {
settings.hostname = root["hostname"] | FACTORY_MDNS_HOSTNAME;
settings.instance = root["instance"] | FACTORY_MDNS_INSTANCE;
settings.services.clear();
if (root["services"].is<JsonArray>()) {
JsonArray servicesArray = root["services"];
for (JsonObject serviceObj : servicesArray) {
for (JsonVariant serviceObj : servicesArray) {
mdns_service_t service;
if (service.deserialize(serviceObj)) {
settings.services.push_back(service);
@@ -121,7 +121,7 @@ class MDNSSettings {
settings.globalTxtRecords.clear();
if (root["global_txt_records"].is<JsonArray>()) {
JsonArray txtArray = root["global_txt_records"];
for (JsonObject txtObj : txtArray) {
for (JsonVariant txtObj : txtArray) {
mdns_txt_record_t txt;
if (txt.deserialize(txtObj)) {
settings.globalTxtRecords.push_back(txt);
@@ -25,14 +25,14 @@ class NTPSettings {
String tzFormat;
String server;
static void read(NTPSettings &settings, JsonObject &root) {
static void read(NTPSettings &settings, JsonVariant &root) {
root["enabled"] = settings.enabled;
root["server"] = settings.server;
root["tz_label"] = settings.tzLabel;
root["tz_format"] = settings.tzFormat;
}
static StateUpdateResult update(JsonObject &root, NTPSettings &settings) {
static StateUpdateResult update(JsonVariant &root, NTPSettings &settings) {
settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED;
settings.server = root["server"] | FACTORY_NTP_SERVER;
settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL;
@@ -34,13 +34,13 @@ class PeripheralsConfiguration {
long frequency = I2C_FREQUENCY;
std::vector<PinConfig> pins;
static void read(PeripheralsConfiguration &settings, JsonObject &root) {
static void read(PeripheralsConfiguration &settings, JsonVariant &root) {
root["sda"] = settings.sda;
root["scl"] = settings.scl;
root["frequency"] = settings.frequency;
}
static StateUpdateResult update(JsonObject &root, PeripheralsConfiguration &settings) {
static StateUpdateResult update(JsonVariant &root, PeripheralsConfiguration &settings) {
settings.sda = root["sda"] | SDA_PIN;
settings.scl = root["scl"] | SCL_PIN;
settings.frequency = root["frequency"] | I2C_FREQUENCY;
@@ -19,22 +19,22 @@ class ServoSettings {
{306, -1, 0, 2.2, "Servo4"}, {306, -1, 45, 2.1055555, "Servo5"}, {306, -1, -90, 1.96923, "Servo6"},
{306, 1, 0, 2.2, "Servo7"}, {306, 1, -45, 2.1055555, "Servo8"}, {306, 1, 90, 1.96923, "Servo9"},
{306, 1, 0, 2.2, "Servo10"}, {306, -1, 45, 2.1055555, "Servo11"}, {306, -1, -90, 1.96923, "Servo12"}};
static void read(ServoSettings &settings, JsonObject &root) {
static void read(ServoSettings &settings, JsonVariant &root) {
JsonArray servos = root["servos"].to<JsonArray>();
for (auto &servo : settings.servos) {
JsonObject newServo = servos.add<JsonObject>();
JsonVariant newServo = servos.add<JsonVariant>();
newServo["center_pwm"] = servo.centerPwm;
newServo["direction"] = servo.direction;
newServo["center_angle"] = servo.centerAngle;
newServo["conversion"] = servo.conversion;
}
}
static StateUpdateResult update(JsonObject &root, ServoSettings &settings) {
static StateUpdateResult update(JsonVariant &root, ServoSettings &settings) {
if (root["servos"].is<JsonArray>()) {
JsonArray servosJson = root["servos"];
int i = 0;
for (auto servo : servosJson) {
JsonObject servoObject = servo.as<JsonObject>();
JsonVariant servoObject = servo.as<JsonVariant>();
uint8_t servoId = i; // servoObject["id"].as<uint8_t>();
settings.servos[servoId].centerPwm = servoObject["center_pwm"].as<float>();
settings.servos[servoId].centerAngle = servoObject["center_angle"].as<float>();
@@ -37,7 +37,7 @@ typedef struct {
IPAddress dnsIP2;
bool available;
void serialize(JsonObject &json) const {
void serialize(JsonVariant &json) const {
json["ssid"] = ssid;
json["password"] = password;
json["static_ip_config"] = staticIPConfig;
@@ -50,7 +50,7 @@ typedef struct {
}
}
bool deserialize(const JsonObject &json) {
bool deserialize(const JsonVariant &json) {
String newSsid = json["ssid"].as<String>();
String newPassword = json["password"].as<String>();
if (newSsid.length() < 1 || newSsid.length() > 31 || newPassword.length() > 64) {
@@ -98,24 +98,24 @@ class WiFiSettings {
String hostname;
bool priorityBySignalStrength;
std::vector<wifi_settings_t> wifiSettings;
static void read(WiFiSettings &settings, JsonObject &root) {
static void read(WiFiSettings &settings, JsonVariant &root) {
root["hostname"] = settings.hostname;
root["priority_RSSI"] = settings.priorityBySignalStrength;
JsonArray wifiNetworks = root["wifi_networks"].to<JsonArray>();
for (const auto &wifi : settings.wifiSettings) {
JsonObject wifiNetwork = wifiNetworks.add<JsonObject>();
JsonVariant wifiNetwork = wifiNetworks.add<JsonVariant>();
wifi.serialize(wifiNetwork);
}
ESP_LOGV("WiFiSettings", "WiFi Settings read");
}
static StateUpdateResult update(JsonObject &root, WiFiSettings &settings) {
static StateUpdateResult update(JsonVariant &root, WiFiSettings &settings) {
settings.hostname = root["hostname"] | FACTORY_WIFI_HOSTNAME;
settings.priorityBySignalStrength = root["priority_RSSI"] | true;
settings.wifiSettings.clear();
if (root["wifi_networks"].is<JsonArray>()) {
JsonArray wifiNetworks = root["wifi_networks"];
int networkCount = 0;
for (JsonObject wifiNetwork : wifiNetworks) {
for (JsonVariant wifiNetwork : wifiNetworks) {
if (networkCount >= 5) {
ESP_LOGE("WiFiSettings", "Too many wifi networks");
break;
@@ -21,9 +21,7 @@ class StatefulHttpEndpoint {
: _stateReader(stateReader), _stateUpdater(stateUpdater), _statefulService(statefulService) {}
esp_err_t handleStateUpdate(PsychicRequest *request, JsonVariant &json) {
if (!json.is<JsonObject>()) return request->reply(400);
JsonObject jsonObject = json.as<JsonObject>();
JsonVariant jsonObject = json.as<JsonVariant>();
StateUpdateResult outcome = _statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
if (outcome == StateUpdateResult::ERROR)
@@ -43,7 +41,7 @@ class StatefulHttpEndpoint {
esp_err_t getState(PsychicRequest *request) {
PsychicJsonResponse response = PsychicJsonResponse(request, false);
JsonObject jsonObject = response.getRoot();
JsonVariant jsonObject = response.getRoot();
_statefulService->read(jsonObject, _stateReader);
return response.send();
}
@@ -39,8 +39,8 @@ class FSPersistence {
if (settingsFile) {
JsonDocument jsonDocument;
DeserializationError error = deserializeJson(jsonDocument, settingsFile);
if (error == DeserializationError::Ok && jsonDocument.is<JsonObject>()) {
JsonObject jsonObject = jsonDocument.as<JsonObject>();
if (error == DeserializationError::Ok) {
JsonVariant jsonObject = jsonDocument.as<JsonVariant>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
settingsFile.close();
return;
@@ -59,7 +59,7 @@ class FSPersistence {
bool writeToFS() {
JsonDocument jsonDocument;
JsonObject jsonObject = jsonDocument.to<JsonObject>();
JsonVariant jsonObject = jsonDocument.to<JsonVariant>();
_statefulService->read(jsonObject, _stateReader);
mkdirs();
@@ -112,7 +112,7 @@ class FSPersistence {
// is supplied, this virtual function allows that to be changed.
virtual void applyDefaults() {
JsonDocument jsonDocument;
JsonObject jsonObject = jsonDocument.as<JsonObject>();
JsonVariant jsonObject = jsonDocument.as<JsonVariant>();
_statefulService->updateWithoutPropagation(jsonObject, _stateUpdater);
}
};
@@ -11,10 +11,10 @@
#include <template/state_result.h>
template <typename T>
using JsonStateUpdater = std::function<StateUpdateResult(JsonObject &root, T &settings)>;
using JsonStateUpdater = std::function<StateUpdateResult(JsonVariant &root, T &settings)>;
template <typename T>
using JsonStateReader = std::function<void(T &settings, JsonObject &root)>;
using JsonStateReader = std::function<void(T &settings, JsonVariant &root)>;
using HandlerId = size_t;
using StateUpdateCallback = std::function<void(const String &originId)>;
@@ -98,7 +98,7 @@ class StatefulService {
return result;
}
StateUpdateResult update(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater, const String &originId) {
StateUpdateResult update(JsonVariant &jsonObject, JsonStateUpdater<T> stateUpdater, const String &originId) {
lock();
StateUpdateResult result = stateUpdater(jsonObject, state_);
unlock();
@@ -106,7 +106,7 @@ class StatefulService {
return result;
}
StateUpdateResult updateWithoutPropagation(JsonObject &jsonObject, JsonStateUpdater<T> stateUpdater) {
StateUpdateResult updateWithoutPropagation(JsonVariant &jsonObject, JsonStateUpdater<T> stateUpdater) {
lock();
StateUpdateResult result = stateUpdater(jsonObject, state_);
unlock();
@@ -119,7 +119,7 @@ class StatefulService {
unlock();
}
void read(JsonObject &jsonObject, JsonStateReader<T> stateReader) {
void read(JsonVariant &jsonObject, JsonStateReader<T> stateReader) {
lock();
stateReader(state_, jsonObject);
unlock();
@@ -27,16 +27,14 @@ class EventEndpoint {
StatefulService<T> *_statefulService;
const char *_event;
void updateState(JsonObject &root, int originId) {
void updateState(JsonVariant &root, int originId) {
_statefulService->update(root, _stateUpdater, String(originId));
}
void syncState(const String &originId, bool sync = false) {
JsonDocument jsonDocument;
JsonObject root = jsonDocument.to<JsonObject>();
String output;
JsonVariant root = jsonDocument.to<JsonVariant>();
_statefulService->read(root, _stateReader);
JsonVariant obj = jsonDocument.as<JsonVariant>();
socket.emit(_event, obj, originId.c_str(), sync);
socket.emit(_event, root, originId.c_str(), sync);
}
};
+3 -3
View File
@@ -7,7 +7,7 @@
class JsonUtils {
public:
static void readIP(const JsonObject &root, const String &key, IPAddress &ip, const String &def) {
static void readIP(const JsonVariant &root, const String &key, IPAddress &ip, const String &def) {
IPAddress defaultIp = {};
if (!defaultIp.fromString(def)) {
defaultIp = INADDR_NONE;
@@ -15,14 +15,14 @@ class JsonUtils {
readIP(root, key, ip, defaultIp);
}
static void readIP(const JsonObject &root, const String &key, IPAddress &ip,
static void readIP(const JsonVariant &root, const String &key, IPAddress &ip,
const IPAddress &defaultIp = INADDR_NONE) {
if (!root[key].is<String>() || !ip.fromString(root[key].as<String>())) {
ip = defaultIp;
}
}
static void writeIP(JsonObject &root, const String &key, const IPAddress &ip) {
static void writeIP(JsonVariant &root, const String &key, const IPAddress &ip) {
if (IPUtils::isSet(ip)) {
root[key] = ip.toString();
}