Refactor LoRa protocol to batch+ack with ACK-based time bootstrap
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
#include <ArduinoJson.h>
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
#include "config.h"
|
||||
#include "power_manager.h"
|
||||
|
||||
static float round2(float value) {
|
||||
if (isnan(value)) {
|
||||
@@ -12,20 +10,6 @@ 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;
|
||||
@@ -51,31 +35,6 @@ 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;
|
||||
@@ -133,193 +92,3 @@ bool meterDataToJson(const MeterData &data, String &out_json) {
|
||||
size_t len = serializeJson(doc, out_json);
|
||||
return len > 0 && len < 256;
|
||||
}
|
||||
|
||||
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 = 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.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);
|
||||
data.rx_reject_reason = static_cast<uint8_t>(doc["rx_reject"] | 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, uint16_t batch_id, String &out_json, const FaultCounters *faults, FaultType last_error) {
|
||||
if (!samples || count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DynamicJsonDocument doc(8192);
|
||||
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;
|
||||
doc["t_first"] = samples[0].ts_utc;
|
||||
doc["t_last"] = samples[count - 1].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;
|
||||
}
|
||||
if (faults->lora_tx_fail > 0) {
|
||||
doc["err_tx"] = faults->lora_tx_fail;
|
||||
}
|
||||
}
|
||||
doc["err_last"] = static_cast<uint8_t>(last_error);
|
||||
if (!isnan(samples[count - 1].battery_voltage_v)) {
|
||||
char bat_buf[16];
|
||||
format_float_2(bat_buf, sizeof(bat_buf), samples[count - 1].battery_voltage_v);
|
||||
doc["bat_v"] = serialized(bat_buf);
|
||||
}
|
||||
|
||||
JsonArray energy = doc.createNestedArray("e_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) {
|
||||
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 = "";
|
||||
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;
|
||||
}
|
||||
|
||||
const char *id = doc["id"] | "";
|
||||
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);
|
||||
float bat_v = doc["bat_v"] | NAN;
|
||||
|
||||
if (!doc["schema"].isNull()) {
|
||||
if ((doc["schema"] | 0) != 1) {
|
||||
return false;
|
||||
}
|
||||
size_t count = doc["n"] | 0;
|
||||
if (count == 0) {
|
||||
return false;
|
||||
}
|
||||
if (count > max_count) {
|
||||
count = max_count;
|
||||
}
|
||||
|
||||
uint32_t t0 = doc["t0"] | 0;
|
||||
uint32_t t_first = doc["t_first"] | t0;
|
||||
uint32_t t_last = doc["t_last"] | t_first;
|
||||
uint32_t dt_s = doc["dt_s"] | 1;
|
||||
JsonArray energy = doc["e_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");
|
||||
}
|
||||
|
||||
if (count > 1 && t_last >= t_first) {
|
||||
uint32_t span = t_last - t_first;
|
||||
uint32_t step = span / static_cast<uint32_t>(count - 1);
|
||||
data.ts_utc = t_first + static_cast<uint32_t>(idx) * step;
|
||||
} else {
|
||||
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 = bat_v;
|
||||
if (!isnan(bat_v)) {
|
||||
data.battery_percent = battery_percent_from_voltage(bat_v);
|
||||
} else {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
out_count = count;
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user