Update batch schema and add ACK handling
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
#include "json_codec.h"
|
||||
#include <ArduinoJson.h>
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "power_manager.h"
|
||||
|
||||
static float round2(float value) {
|
||||
@@ -10,6 +12,34 @@ static float round2(float value) {
|
||||
return roundf(value * 100.0f) / 100.0f;
|
||||
}
|
||||
|
||||
static uint32_t kwh_to_wh(float value) {
|
||||
if (isnan(value)) {
|
||||
return 0;
|
||||
}
|
||||
double wh = static_cast<double>(value) * 1000.0;
|
||||
if (wh < 0.0) {
|
||||
wh = 0.0;
|
||||
}
|
||||
if (wh > static_cast<double>(UINT32_MAX)) {
|
||||
wh = static_cast<double>(UINT32_MAX);
|
||||
}
|
||||
return static_cast<uint32_t>(llround(wh));
|
||||
}
|
||||
|
||||
static int32_t round_to_i32(float value) {
|
||||
if (isnan(value)) {
|
||||
return 0;
|
||||
}
|
||||
long rounded = lroundf(value);
|
||||
if (rounded > INT32_MAX) {
|
||||
return INT32_MAX;
|
||||
}
|
||||
if (rounded < INT32_MIN) {
|
||||
return INT32_MIN;
|
||||
}
|
||||
return static_cast<int32_t>(rounded);
|
||||
}
|
||||
|
||||
static const char *short_id_from_device_id(const char *device_id) {
|
||||
if (!device_id) {
|
||||
return "";
|
||||
@@ -21,6 +51,31 @@ static const char *short_id_from_device_id(const char *device_id) {
|
||||
return device_id;
|
||||
}
|
||||
|
||||
static void sender_label_from_short_id(uint16_t short_id, char *out, size_t out_len) {
|
||||
if (!out || out_len == 0) {
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
||||
if (EXPECTED_SENDER_IDS[i] == short_id) {
|
||||
snprintf(out, out_len, "s%02u", static_cast<unsigned>(i + 1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
snprintf(out, out_len, "s00");
|
||||
}
|
||||
|
||||
static uint16_t short_id_from_sender_label(const char *sender_label) {
|
||||
if (!sender_label || strlen(sender_label) < 2 || sender_label[0] != 's') {
|
||||
return 0;
|
||||
}
|
||||
char *end = nullptr;
|
||||
long idx = strtol(sender_label + 1, &end, 10);
|
||||
if (end == sender_label + 1 || idx <= 0 || idx > NUM_SENDERS) {
|
||||
return 0;
|
||||
}
|
||||
return EXPECTED_SENDER_IDS[idx - 1];
|
||||
}
|
||||
|
||||
static void format_float_2(char *buf, size_t buf_len, float value) {
|
||||
if (!buf || buf_len == 0) {
|
||||
return;
|
||||
@@ -47,12 +102,6 @@ bool meterDataToJson(const MeterData &data, String &out_json) {
|
||||
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;
|
||||
@@ -78,13 +127,6 @@ bool meterDataToJson(const MeterData &data, String &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);
|
||||
@@ -101,14 +143,11 @@ bool jsonToMeterData(const String &json, MeterData &data) {
|
||||
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.energy_total_kwh = doc["e_kwh"] | NAN;
|
||||
data.total_power_w = doc["p_w"] | NAN;
|
||||
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);
|
||||
@@ -132,15 +171,21 @@ bool jsonToMeterData(const String &json, MeterData &data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool meterBatchToJson(const MeterData *samples, size_t count, String &out_json, const FaultCounters *faults, FaultType last_error) {
|
||||
bool meterBatchToJson(const MeterData *samples, size_t count, uint16_t batch_id, 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;
|
||||
doc["schema"] = 1;
|
||||
char sender_label[8] = {};
|
||||
sender_label_from_short_id(samples[count - 1].short_id, sender_label, sizeof(sender_label));
|
||||
doc["sender"] = sender_label;
|
||||
doc["batch_id"] = batch_id;
|
||||
doc["t0"] = samples[0].ts_utc;
|
||||
uint32_t dt_s = METER_SAMPLE_INTERVAL_MS / 1000;
|
||||
doc["dt_s"] = dt_s > 0 ? dt_s : 1;
|
||||
doc["n"] = static_cast<uint32_t>(count);
|
||||
if (faults) {
|
||||
if (faults->meter_read_fail > 0) {
|
||||
doc["err_m"] = faults->meter_read_fail;
|
||||
@@ -152,19 +197,18 @@ bool meterBatchToJson(const MeterData *samples, size_t count, String &out_json,
|
||||
if (last_error != FaultType::None) {
|
||||
doc["err_last"] = static_cast<uint8_t>(last_error);
|
||||
}
|
||||
JsonArray arr = doc.createNestedArray("s");
|
||||
|
||||
JsonArray energy = doc.createNestedArray("energy_wh");
|
||||
JsonArray p_w = doc.createNestedArray("p_w");
|
||||
JsonArray p1_w = doc.createNestedArray("p1_w");
|
||||
JsonArray p2_w = doc.createNestedArray("p2_w");
|
||||
JsonArray p3_w = doc.createNestedArray("p3_w");
|
||||
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);
|
||||
energy.add(kwh_to_wh(samples[i].energy_total_kwh));
|
||||
p_w.add(round_to_i32(samples[i].total_power_w));
|
||||
p1_w.add(round_to_i32(samples[i].phase_power_w[0]));
|
||||
p2_w.add(round_to_i32(samples[i].phase_power_w[1]));
|
||||
p3_w.add(round_to_i32(samples[i].phase_power_w[2]));
|
||||
}
|
||||
|
||||
out_json = "";
|
||||
@@ -184,62 +228,72 @@ bool jsonToMeterBatch(const String &json, MeterData *out_samples, size_t max_cou
|
||||
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;
|
||||
const char *sender = doc["sender"] | "";
|
||||
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;
|
||||
if (!doc["schema"].isNull()) {
|
||||
if ((doc["schema"] | 0) != 1) {
|
||||
return false;
|
||||
}
|
||||
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));
|
||||
size_t count = doc["n"] | 0;
|
||||
if (count == 0) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
if (count > max_count) {
|
||||
count = max_count;
|
||||
}
|
||||
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));
|
||||
uint32_t t0 = doc["t0"] | 0;
|
||||
uint32_t dt_s = doc["dt_s"] | 1;
|
||||
JsonArray energy = doc["energy_wh"].as<JsonArray>();
|
||||
JsonArray p_w = doc["p_w"].as<JsonArray>();
|
||||
JsonArray p1_w = doc["p1_w"].as<JsonArray>();
|
||||
JsonArray p2_w = doc["p2_w"].as<JsonArray>();
|
||||
JsonArray p3_w = doc["p3_w"].as<JsonArray>();
|
||||
|
||||
for (size_t idx = 0; idx < count; ++idx) {
|
||||
MeterData &data = out_samples[idx];
|
||||
data = {};
|
||||
uint16_t short_id = short_id_from_sender_label(sender);
|
||||
if (short_id != 0) {
|
||||
snprintf(data.device_id, sizeof(data.device_id), "dd3-%04X", short_id);
|
||||
data.short_id = short_id;
|
||||
} else if (id[0] != '\0') {
|
||||
strncpy(data.device_id, id, sizeof(data.device_id));
|
||||
data.device_id[sizeof(data.device_id) - 1] = '\0';
|
||||
} else {
|
||||
snprintf(data.device_id, sizeof(data.device_id), "dd3-0000");
|
||||
}
|
||||
|
||||
data.ts_utc = t0 + static_cast<uint32_t>(idx) * dt_s;
|
||||
data.energy_total_kwh = static_cast<float>((energy[idx] | 0)) / 1000.0f;
|
||||
data.total_power_w = static_cast<float>(p_w[idx] | 0);
|
||||
data.phase_power_w[0] = static_cast<float>(p1_w[idx] | 0);
|
||||
data.phase_power_w[1] = static_cast<float>(p2_w[idx] | 0);
|
||||
data.phase_power_w[2] = static_cast<float>(p3_w[idx] | 0);
|
||||
data.battery_voltage_v = NAN;
|
||||
data.battery_percent = 0;
|
||||
data.valid = true;
|
||||
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 (data.short_id == 0 && 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 = count;
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
out_count = idx;
|
||||
return idx > 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user