Files
DD3-LoRa-Bridge-MultiSender/src/json_codec.cpp

246 lines
7.4 KiB
C++

#include "json_codec.h"
#include <ArduinoJson.h>
#include <math.h>
#include "power_manager.h"
static float round2(float value) {
if (isnan(value)) {
return value;
}
return roundf(value * 100.0f) / 100.0f;
}
static const char *short_id_from_device_id(const char *device_id) {
if (!device_id) {
return "";
}
size_t len = strlen(device_id);
if (len >= 4) {
return device_id + (len - 4);
}
return device_id;
}
static void format_float_2(char *buf, size_t buf_len, float value) {
if (!buf || buf_len == 0) {
return;
}
if (isnan(value)) {
snprintf(buf, buf_len, "null");
return;
}
snprintf(buf, buf_len, "%.2f", round2(value));
}
bool meterDataToJson(const MeterData &data, String &out_json) {
StaticJsonDocument<256> doc;
doc["id"] = short_id_from_device_id(data.device_id);
doc["ts"] = data.ts_utc;
char buf[16];
format_float_2(buf, sizeof(buf), data.energy_total_kwh);
doc["e_kwh"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.total_power_w);
doc["p_w"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.phase_power_w[0]);
doc["p1_w"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.phase_power_w[1]);
doc["p2_w"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.phase_power_w[2]);
doc["p3_w"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.phase_voltage_v[0]);
doc["v1_v"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.phase_voltage_v[1]);
doc["v2_v"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.phase_voltage_v[2]);
doc["v3_v"] = serialized(buf);
format_float_2(buf, sizeof(buf), data.battery_voltage_v);
doc["bat_v"] = serialized(buf);
doc["bat_pct"] = data.battery_percent;
if (data.link_valid) {
doc["rssi"] = data.link_rssi_dbm;
doc["snr"] = data.link_snr_db;
}
if (data.err_meter_read > 0) {
doc["err_m"] = data.err_meter_read;
}
if (data.err_decode > 0) {
doc["err_d"] = data.err_decode;
}
if (data.err_lora_tx > 0) {
doc["err_tx"] = data.err_lora_tx;
}
if (data.last_error != FaultType::None) {
doc["err_last"] = static_cast<uint8_t>(data.last_error);
}
out_json = "";
size_t len = serializeJson(doc, out_json);
return len > 0 && len < 256;
}
static float read_float_or_legacy(JsonDocument &doc, const char *key, const char *legacy_key) {
if (doc[key].isNull()) {
return doc[legacy_key] | NAN;
}
return doc[key] | NAN;
}
bool jsonToMeterData(const String &json, MeterData &data) {
StaticJsonDocument<256> doc;
DeserializationError err = deserializeJson(doc, json);
if (err) {
return false;
}
const char *id = doc["id"] | "";
if (strlen(id) == 4) {
snprintf(data.device_id, sizeof(data.device_id), "dd3-%s", id);
} else {
strncpy(data.device_id, id, sizeof(data.device_id));
}
data.device_id[sizeof(data.device_id) - 1] = '\0';
data.ts_utc = doc["ts"] | 0;
data.energy_total_kwh = read_float_or_legacy(doc, "e_kwh", "energy_kwh");
data.total_power_w = read_float_or_legacy(doc, "p_w", "p_total_w");
data.phase_power_w[0] = doc["p1_w"] | NAN;
data.phase_power_w[1] = doc["p2_w"] | NAN;
data.phase_power_w[2] = doc["p3_w"] | NAN;
data.phase_voltage_v[0] = doc["v1_v"] | NAN;
data.phase_voltage_v[1] = doc["v2_v"] | NAN;
data.phase_voltage_v[2] = doc["v3_v"] | NAN;
data.battery_voltage_v = doc["bat_v"] | NAN;
if (doc["bat_pct"].isNull() && !isnan(data.battery_voltage_v)) {
data.battery_percent = battery_percent_from_voltage(data.battery_voltage_v);
} else {
data.battery_percent = doc["bat_pct"] | 0;
}
data.valid = true;
data.link_valid = false;
data.link_rssi_dbm = 0;
data.link_snr_db = NAN;
data.err_meter_read = doc["err_m"] | 0;
data.err_decode = doc["err_d"] | 0;
data.err_lora_tx = doc["err_tx"] | 0;
data.last_error = static_cast<FaultType>(doc["err_last"] | 0);
if (strlen(data.device_id) >= 8) {
const char *suffix = data.device_id + strlen(data.device_id) - 4;
data.short_id = static_cast<uint16_t>(strtoul(suffix, nullptr, 16));
}
return true;
}
bool meterBatchToJson(const MeterData *samples, size_t count, String &out_json, const FaultCounters *faults, FaultType last_error) {
if (!samples || count == 0) {
return false;
}
DynamicJsonDocument doc(8192);
doc["id"] = short_id_from_device_id(samples[count - 1].device_id);
doc["bat_v"] = round2(samples[count - 1].battery_voltage_v);
doc["bat_pct"] = samples[count - 1].battery_percent;
if (faults) {
if (faults->meter_read_fail > 0) {
doc["err_m"] = faults->meter_read_fail;
}
if (faults->lora_tx_fail > 0) {
doc["err_tx"] = faults->lora_tx_fail;
}
}
if (last_error != FaultType::None) {
doc["err_last"] = static_cast<uint8_t>(last_error);
}
JsonArray arr = doc.createNestedArray("s");
for (size_t i = 0; i < count; ++i) {
JsonArray row = arr.createNestedArray();
row.add(samples[i].ts_utc);
row.add(round2(samples[i].energy_total_kwh));
row.add(round2(samples[i].total_power_w));
row.add(round2(samples[i].phase_power_w[0]));
row.add(round2(samples[i].phase_power_w[1]));
row.add(round2(samples[i].phase_power_w[2]));
row.add(round2(samples[i].phase_voltage_v[0]));
row.add(round2(samples[i].phase_voltage_v[1]));
row.add(round2(samples[i].phase_voltage_v[2]));
row.add(samples[i].valid ? 1 : 0);
}
out_json = "";
size_t len = serializeJson(doc, out_json);
return len > 0;
}
bool jsonToMeterBatch(const String &json, MeterData *out_samples, size_t max_count, size_t &out_count) {
out_count = 0;
if (!out_samples || max_count == 0) {
return false;
}
DynamicJsonDocument doc(8192);
DeserializationError err = deserializeJson(doc, json);
if (err) {
return false;
}
JsonArray arr = doc["s"].as<JsonArray>();
if (arr.isNull()) {
return false;
}
const char *id = doc["id"] | "";
float bat_v = doc["bat_v"] | NAN;
uint8_t bat_pct = doc["bat_pct"] | 0;
uint32_t err_m = doc["err_m"] | 0;
uint32_t err_tx = doc["err_tx"] | 0;
FaultType last_error = static_cast<FaultType>(doc["err_last"] | 0);
size_t idx = 0;
for (JsonArray row : arr) {
if (idx >= max_count) {
break;
}
MeterData &data = out_samples[idx];
data = {};
if (strlen(id) == 4) {
snprintf(data.device_id, sizeof(data.device_id), "dd3-%s", id);
} else {
strncpy(data.device_id, id, sizeof(data.device_id));
}
data.device_id[sizeof(data.device_id) - 1] = '\0';
data.ts_utc = row[0] | 0;
data.energy_total_kwh = row[1] | NAN;
data.total_power_w = row[2] | NAN;
data.phase_power_w[0] = row[3] | NAN;
data.phase_power_w[1] = row[4] | NAN;
data.phase_power_w[2] = row[5] | NAN;
data.phase_voltage_v[0] = row[6] | NAN;
data.phase_voltage_v[1] = row[7] | NAN;
data.phase_voltage_v[2] = row[8] | NAN;
data.valid = (row[9] | 1) != 0;
data.battery_voltage_v = bat_v;
if (doc["bat_pct"].isNull() && !isnan(bat_v)) {
data.battery_percent = battery_percent_from_voltage(bat_v);
} else {
data.battery_percent = bat_pct;
}
data.link_valid = false;
data.link_rssi_dbm = 0;
data.link_snr_db = NAN;
data.err_meter_read = err_m;
data.err_decode = 0;
data.err_lora_tx = err_tx;
data.last_error = last_error;
if (strlen(data.device_id) >= 8) {
const char *suffix = data.device_id + strlen(data.device_id) - 4;
data.short_id = static_cast<uint16_t>(strtoul(suffix, nullptr, 16));
}
idx++;
}
out_count = idx;
return idx > 0;
}