Refactor LoRa protocol to batch+ack with ACK-based time bootstrap
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
bool compressBuffer(const uint8_t *in, size_t in_len, uint8_t *out, size_t out_max, size_t &out_len);
|
|
||||||
bool decompressBuffer(const uint8_t *in, size_t in_len, uint8_t *out, size_t out_max, size_t &out_len);
|
|
||||||
@@ -7,21 +7,11 @@ enum class DeviceRole : uint8_t {
|
|||||||
Receiver = 1
|
Receiver = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class PayloadType : uint8_t {
|
|
||||||
MeterData = 0,
|
|
||||||
TestCode = 1,
|
|
||||||
TimeSync = 2,
|
|
||||||
MeterBatch = 3,
|
|
||||||
Ack = 4
|
|
||||||
};
|
|
||||||
|
|
||||||
enum class BatchRetryPolicy : uint8_t {
|
enum class BatchRetryPolicy : uint8_t {
|
||||||
Keep = 0,
|
Keep = 0,
|
||||||
Drop = 1
|
Drop = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr uint8_t PROTOCOL_VERSION = 1;
|
|
||||||
|
|
||||||
// Pin definitions
|
// Pin definitions
|
||||||
constexpr uint8_t PIN_LORA_SCK = 5;
|
constexpr uint8_t PIN_LORA_SCK = 5;
|
||||||
constexpr uint8_t PIN_LORA_MISO = 19;
|
constexpr uint8_t PIN_LORA_MISO = 19;
|
||||||
@@ -57,17 +47,7 @@ constexpr uint8_t LORA_PREAMBLE_LEN = 8;
|
|||||||
|
|
||||||
// Timing
|
// Timing
|
||||||
constexpr uint32_t SENDER_WAKE_INTERVAL_SEC = 30;
|
constexpr uint32_t SENDER_WAKE_INTERVAL_SEC = 30;
|
||||||
constexpr uint32_t TIME_SYNC_INTERVAL_SEC = 60;
|
constexpr uint32_t SYNC_REQUEST_INTERVAL_MS = 15000;
|
||||||
constexpr uint32_t TIME_SYNC_SLOW_INTERVAL_SEC = 3600;
|
|
||||||
constexpr uint32_t TIME_SYNC_FAST_WINDOW_MS = 10UL * 60UL * 1000UL;
|
|
||||||
constexpr uint32_t SENDER_TIMESYNC_WINDOW_MS = 300;
|
|
||||||
constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_FAST = 60;
|
|
||||||
constexpr uint32_t SENDER_TIMESYNC_CHECK_SEC_SLOW = 3600;
|
|
||||||
constexpr uint32_t TIME_SYNC_DRIFT_THRESHOLD_SEC = 10;
|
|
||||||
constexpr uint32_t TIME_SYNC_BURST_INTERVAL_MS = 10000;
|
|
||||||
constexpr uint32_t TIME_SYNC_BURST_DURATION_MS = 10UL * 60UL * 1000UL;
|
|
||||||
constexpr uint32_t TIME_SYNC_ERROR_TIMEOUT_MS = 2UL * 24UL * 60UL * 60UL * 1000UL;
|
|
||||||
constexpr bool ENABLE_DS3231 = true;
|
|
||||||
constexpr uint32_t OLED_PAGE_INTERVAL_MS = 4000;
|
constexpr uint32_t OLED_PAGE_INTERVAL_MS = 4000;
|
||||||
constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL;
|
constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL;
|
||||||
constexpr uint32_t SENDER_OLED_READ_MS = 10000;
|
constexpr uint32_t SENDER_OLED_READ_MS = 10000;
|
||||||
@@ -107,6 +87,7 @@ constexpr const char *WEB_AUTH_DEFAULT_USER = "admin";
|
|||||||
constexpr const char *WEB_AUTH_DEFAULT_PASS = "admin";
|
constexpr const char *WEB_AUTH_DEFAULT_PASS = "admin";
|
||||||
|
|
||||||
constexpr uint8_t NUM_SENDERS = 1;
|
constexpr uint8_t NUM_SENDERS = 1;
|
||||||
|
constexpr uint32_t MIN_ACCEPTED_EPOCH_UTC = 1769904000UL; // 2026-02-01 00:00:00 UTC
|
||||||
inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = {
|
inline constexpr uint16_t EXPECTED_SENDER_IDS[NUM_SENDERS] = {
|
||||||
0xF19C //433mhz sender
|
0xF19C //433mhz sender
|
||||||
//0x7EB4 //868mhz sender
|
//0x7EB4 //868mhz sender
|
||||||
|
|||||||
@@ -6,19 +6,16 @@ enum class FaultType : uint8_t {
|
|||||||
None = 0,
|
None = 0,
|
||||||
MeterRead = 1,
|
MeterRead = 1,
|
||||||
Decode = 2,
|
Decode = 2,
|
||||||
LoraTx = 3,
|
LoraTx = 3
|
||||||
TimeSync = 4
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class RxRejectReason : uint8_t {
|
enum class RxRejectReason : uint8_t {
|
||||||
None = 0,
|
None = 0,
|
||||||
CrcFail = 1,
|
CrcFail = 1,
|
||||||
BadProtocol = 2,
|
InvalidMsgKind = 2,
|
||||||
WrongRole = 3,
|
LengthMismatch = 3,
|
||||||
WrongPayloadType = 4,
|
DeviceIdMismatch = 4,
|
||||||
LengthMismatch = 5,
|
BatchIdMismatch = 5
|
||||||
DeviceIdMismatch = 6,
|
|
||||||
BatchIdMismatch = 7
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FaultCounters {
|
struct FaultCounters {
|
||||||
|
|||||||
@@ -4,7 +4,3 @@
|
|||||||
#include "data_model.h"
|
#include "data_model.h"
|
||||||
|
|
||||||
bool meterDataToJson(const MeterData &data, String &out_json);
|
bool meterDataToJson(const MeterData &data, String &out_json);
|
||||||
bool jsonToMeterData(const String &json, MeterData &data);
|
|
||||||
bool meterBatchToJson(const MeterData *samples, size_t count, uint16_t batch_id, String &out_json,
|
|
||||||
const FaultCounters *faults = nullptr, FaultType last_error = FaultType::None);
|
|
||||||
bool jsonToMeterBatch(const String &json, MeterData *out_samples, size_t max_count, size_t &out_count);
|
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
constexpr size_t LORA_MAX_PAYLOAD = 230;
|
constexpr size_t LORA_MAX_PAYLOAD = 230;
|
||||||
|
|
||||||
|
enum class LoraMsgKind : uint8_t {
|
||||||
|
BatchUp = 0,
|
||||||
|
AckDown = 1
|
||||||
|
};
|
||||||
|
|
||||||
struct LoraPacket {
|
struct LoraPacket {
|
||||||
uint8_t protocol_version;
|
LoraMsgKind msg_kind;
|
||||||
DeviceRole role;
|
|
||||||
uint16_t device_id_short;
|
uint16_t device_id_short;
|
||||||
PayloadType payload_type;
|
|
||||||
uint8_t payload[LORA_MAX_PAYLOAD];
|
uint8_t payload[LORA_MAX_PAYLOAD];
|
||||||
size_t payload_len;
|
size_t payload_len;
|
||||||
int16_t rssi_dbm;
|
int16_t rssi_dbm;
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <Arduino.h>
|
|
||||||
|
|
||||||
bool rtc_ds3231_init();
|
|
||||||
bool rtc_ds3231_is_present();
|
|
||||||
bool rtc_ds3231_read_epoch(uint32_t &epoch_utc);
|
|
||||||
bool rtc_ds3231_set_epoch(uint32_t epoch_utc);
|
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "lora_transport.h"
|
|
||||||
|
|
||||||
void time_receiver_init(const char *ntp_server_1, const char *ntp_server_2);
|
void time_receiver_init(const char *ntp_server_1, const char *ntp_server_2);
|
||||||
uint32_t time_get_utc();
|
uint32_t time_get_utc();
|
||||||
bool time_is_synced();
|
bool time_is_synced();
|
||||||
void time_set_utc(uint32_t epoch);
|
void time_set_utc(uint32_t epoch);
|
||||||
bool time_send_timesync(uint16_t device_id_short);
|
|
||||||
bool time_handle_timesync_payload(const uint8_t *payload, size_t len);
|
|
||||||
void time_get_local_hhmm(char *out, size_t out_len);
|
void time_get_local_hhmm(char *out, size_t out_len);
|
||||||
void time_rtc_init();
|
|
||||||
bool time_try_load_from_rtc();
|
|
||||||
bool time_rtc_present();
|
|
||||||
uint32_t time_get_last_sync_utc();
|
uint32_t time_get_last_sync_utc();
|
||||||
uint32_t time_get_last_sync_age_sec();
|
uint32_t time_get_last_sync_age_sec();
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
#include "compressor.h"
|
|
||||||
|
|
||||||
static constexpr uint8_t RLE_MARKER = 0xFF;
|
|
||||||
|
|
||||||
bool compressBuffer(const uint8_t *in, size_t in_len, uint8_t *out, size_t out_max, size_t &out_len) {
|
|
||||||
out_len = 0;
|
|
||||||
if (!in || !out) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t i = 0;
|
|
||||||
while (i < in_len) {
|
|
||||||
uint8_t value = in[i];
|
|
||||||
size_t run = 1;
|
|
||||||
while (i + run < in_len && in[i + run] == value && run < 255) {
|
|
||||||
run++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == RLE_MARKER || run >= 4) {
|
|
||||||
if (out_len + 3 > out_max) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
out[out_len++] = RLE_MARKER;
|
|
||||||
out[out_len++] = static_cast<uint8_t>(run);
|
|
||||||
out[out_len++] = value;
|
|
||||||
} else {
|
|
||||||
if (out_len + run > out_max) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (size_t j = 0; j < run; ++j) {
|
|
||||||
out[out_len++] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
i += run;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool decompressBuffer(const uint8_t *in, size_t in_len, uint8_t *out, size_t out_max, size_t &out_len) {
|
|
||||||
out_len = 0;
|
|
||||||
if (!in || !out) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t i = 0;
|
|
||||||
while (i < in_len) {
|
|
||||||
uint8_t value = in[i++];
|
|
||||||
if (value == RLE_MARKER) {
|
|
||||||
if (i + 1 >= in_len) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
uint8_t run = in[i++];
|
|
||||||
uint8_t data = in[i++];
|
|
||||||
if (out_len + run > out_max) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (uint8_t j = 0; j < run; ++j) {
|
|
||||||
out[out_len++] = data;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (out_len + 1 > out_max) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
out[out_len++] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
@@ -13,12 +13,8 @@ const char *rx_reject_reason_text(RxRejectReason reason) {
|
|||||||
switch (reason) {
|
switch (reason) {
|
||||||
case RxRejectReason::CrcFail:
|
case RxRejectReason::CrcFail:
|
||||||
return "crc_fail";
|
return "crc_fail";
|
||||||
case RxRejectReason::BadProtocol:
|
case RxRejectReason::InvalidMsgKind:
|
||||||
return "bad_protocol_version";
|
return "invalid_msg_kind";
|
||||||
case RxRejectReason::WrongRole:
|
|
||||||
return "wrong_role";
|
|
||||||
case RxRejectReason::WrongPayloadType:
|
|
||||||
return "wrong_payload_type";
|
|
||||||
case RxRejectReason::LengthMismatch:
|
case RxRejectReason::LengthMismatch:
|
||||||
return "length_mismatch";
|
return "length_mismatch";
|
||||||
case RxRejectReason::DeviceIdMismatch:
|
case RxRejectReason::DeviceIdMismatch:
|
||||||
|
|||||||
@@ -188,8 +188,6 @@ static bool render_last_error_line(uint8_t y) {
|
|||||||
label = "decode";
|
label = "decode";
|
||||||
} else if (g_last_error == FaultType::LoraTx) {
|
} else if (g_last_error == FaultType::LoraTx) {
|
||||||
label = "lora";
|
label = "lora";
|
||||||
} else if (g_last_error == FaultType::TimeSync) {
|
|
||||||
label = "timesync";
|
|
||||||
}
|
}
|
||||||
display.setCursor(0, y);
|
display.setCursor(0, y);
|
||||||
display.printf("Err: %s %lus", label, static_cast<unsigned long>(age_seconds(g_last_error_ts, g_last_error_ms)));
|
display.printf("Err: %s %lus", label, static_cast<unsigned long>(age_seconds(g_last_error_ts, g_last_error_ms)));
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include "config.h"
|
|
||||||
#include "power_manager.h"
|
|
||||||
|
|
||||||
static float round2(float value) {
|
static float round2(float value) {
|
||||||
if (isnan(value)) {
|
if (isnan(value)) {
|
||||||
@@ -12,20 +10,6 @@ static float round2(float value) {
|
|||||||
return roundf(value * 100.0f) / 100.0f;
|
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) {
|
static int32_t round_to_i32(float value) {
|
||||||
if (isnan(value)) {
|
if (isnan(value)) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -51,31 +35,6 @@ static const char *short_id_from_device_id(const char *device_id) {
|
|||||||
return 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) {
|
static void format_float_2(char *buf, size_t buf_len, float value) {
|
||||||
if (!buf || buf_len == 0) {
|
if (!buf || buf_len == 0) {
|
||||||
return;
|
return;
|
||||||
@@ -133,193 +92,3 @@ bool meterDataToJson(const MeterData &data, String &out_json) {
|
|||||||
size_t len = serializeJson(doc, out_json);
|
size_t len = serializeJson(doc, out_json);
|
||||||
return len > 0 && len < 256;
|
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -65,13 +65,11 @@ bool lora_send(const LoraPacket &pkt) {
|
|||||||
if (SERIAL_DEBUG_MODE) {
|
if (SERIAL_DEBUG_MODE) {
|
||||||
t1 = millis();
|
t1 = millis();
|
||||||
}
|
}
|
||||||
uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2];
|
uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2];
|
||||||
size_t idx = 0;
|
size_t idx = 0;
|
||||||
buffer[idx++] = pkt.protocol_version;
|
buffer[idx++] = static_cast<uint8_t>(pkt.msg_kind);
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.role);
|
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short >> 8);
|
buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short >> 8);
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short & 0xFF);
|
buffer[idx++] = static_cast<uint8_t>(pkt.device_id_short & 0xFF);
|
||||||
buffer[idx++] = static_cast<uint8_t>(pkt.payload_type);
|
|
||||||
|
|
||||||
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
||||||
return false;
|
return false;
|
||||||
@@ -111,7 +109,7 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
|||||||
while (true) {
|
while (true) {
|
||||||
int packet_size = LoRa.parsePacket();
|
int packet_size = LoRa.parsePacket();
|
||||||
if (packet_size > 0) {
|
if (packet_size > 0) {
|
||||||
if (packet_size < 7) {
|
if (packet_size < 5) {
|
||||||
while (LoRa.available()) {
|
while (LoRa.available()) {
|
||||||
LoRa.read();
|
LoRa.read();
|
||||||
}
|
}
|
||||||
@@ -119,13 +117,13 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2];
|
uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2];
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
while (LoRa.available() && len < sizeof(buffer)) {
|
while (LoRa.available() && len < sizeof(buffer)) {
|
||||||
buffer[len++] = LoRa.read();
|
buffer[len++] = LoRa.read();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (len < 7) {
|
if (len < 5) {
|
||||||
note_reject(RxRejectReason::LengthMismatch);
|
note_reject(RxRejectReason::LengthMismatch);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -136,21 +134,20 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) {
|
|||||||
note_reject(RxRejectReason::CrcFail);
|
note_reject(RxRejectReason::CrcFail);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (buffer[0] != PROTOCOL_VERSION) {
|
uint8_t msg_kind = buffer[0];
|
||||||
note_reject(RxRejectReason::BadProtocol);
|
if (msg_kind > static_cast<uint8_t>(LoraMsgKind::AckDown)) {
|
||||||
|
note_reject(RxRejectReason::InvalidMsgKind);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pkt.protocol_version = buffer[0];
|
pkt.msg_kind = static_cast<LoraMsgKind>(msg_kind);
|
||||||
pkt.role = static_cast<DeviceRole>(buffer[1]);
|
pkt.device_id_short = static_cast<uint16_t>(buffer[1] << 8) | buffer[2];
|
||||||
pkt.device_id_short = static_cast<uint16_t>(buffer[2] << 8) | buffer[3];
|
pkt.payload_len = len - 5;
|
||||||
pkt.payload_type = static_cast<PayloadType>(buffer[4]);
|
|
||||||
pkt.payload_len = len - 7;
|
|
||||||
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
if (pkt.payload_len > LORA_MAX_PAYLOAD) {
|
||||||
note_reject(RxRejectReason::LengthMismatch);
|
note_reject(RxRejectReason::LengthMismatch);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
memcpy(pkt.payload, &buffer[5], pkt.payload_len);
|
memcpy(pkt.payload, &buffer[3], pkt.payload_len);
|
||||||
pkt.rssi_dbm = static_cast<int16_t>(LoRa.packetRssi());
|
pkt.rssi_dbm = static_cast<int16_t>(LoRa.packetRssi());
|
||||||
pkt.snr_db = LoRa.packetSnr();
|
pkt.snr_db = LoRa.packetSnr();
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
434
src/main.cpp
434
src/main.cpp
@@ -1,9 +1,7 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "data_model.h"
|
#include "data_model.h"
|
||||||
#include "json_codec.h"
|
|
||||||
#include "payload_codec.h"
|
#include "payload_codec.h"
|
||||||
#include "compressor.h"
|
|
||||||
#include "lora_transport.h"
|
#include "lora_transport.h"
|
||||||
#include "meter_driver.h"
|
#include "meter_driver.h"
|
||||||
#include "power_manager.h"
|
#include "power_manager.h"
|
||||||
@@ -28,8 +26,6 @@ static char g_device_id[16] = "";
|
|||||||
static SenderStatus g_sender_statuses[NUM_SENDERS];
|
static SenderStatus g_sender_statuses[NUM_SENDERS];
|
||||||
static bool g_ap_mode = false;
|
static bool g_ap_mode = false;
|
||||||
static WifiMqttConfig g_cfg;
|
static WifiMqttConfig g_cfg;
|
||||||
static uint32_t g_last_timesync_ms = 0;
|
|
||||||
static constexpr uint32_t TIME_SYNC_OFFSET_MS = 15000;
|
|
||||||
static uint32_t g_boot_ms = 0;
|
static uint32_t g_boot_ms = 0;
|
||||||
static FaultCounters g_sender_faults = {};
|
static FaultCounters g_sender_faults = {};
|
||||||
static FaultCounters g_receiver_faults = {};
|
static FaultCounters g_receiver_faults = {};
|
||||||
@@ -50,18 +46,6 @@ static uint32_t g_sender_last_error_remote_ms[NUM_SENDERS] = {};
|
|||||||
static bool g_sender_discovery_sent[NUM_SENDERS] = {};
|
static bool g_sender_discovery_sent[NUM_SENDERS] = {};
|
||||||
static bool g_receiver_discovery_sent = false;
|
static bool g_receiver_discovery_sent = false;
|
||||||
|
|
||||||
struct TimeSyncBurstState {
|
|
||||||
bool active;
|
|
||||||
uint32_t start_ms;
|
|
||||||
uint32_t last_send_ms;
|
|
||||||
uint32_t last_drift_check_ms;
|
|
||||||
bool last_drift_ok;
|
|
||||||
};
|
|
||||||
|
|
||||||
static TimeSyncBurstState g_timesync_burst[NUM_SENDERS] = {};
|
|
||||||
static uint32_t g_sender_last_timesync_rx_ms = 0;
|
|
||||||
static bool g_sender_timesync_error = false;
|
|
||||||
|
|
||||||
static constexpr size_t BATCH_HEADER_SIZE = 6;
|
static constexpr size_t BATCH_HEADER_SIZE = 6;
|
||||||
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
|
static constexpr size_t BATCH_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE;
|
||||||
static constexpr size_t BATCH_MAX_COMPRESSED = 4096;
|
static constexpr size_t BATCH_MAX_COMPRESSED = 4096;
|
||||||
@@ -99,21 +83,19 @@ static MeterData g_inflight_samples[METER_BATCH_MAX_SAMPLES];
|
|||||||
static uint8_t g_inflight_count = 0;
|
static uint8_t g_inflight_count = 0;
|
||||||
static uint16_t g_inflight_batch_id = 0;
|
static uint16_t g_inflight_batch_id = 0;
|
||||||
static bool g_inflight_active = false;
|
static bool g_inflight_active = false;
|
||||||
|
static bool g_inflight_sync_request = false;
|
||||||
static uint32_t g_last_debug_log_ms = 0;
|
static uint32_t g_last_debug_log_ms = 0;
|
||||||
static uint32_t g_sender_last_timesync_check_ms = 0;
|
|
||||||
static uint32_t g_sender_rx_window_ms = 0;
|
static uint32_t g_sender_rx_window_ms = 0;
|
||||||
static uint32_t g_sender_sleep_ms = 0;
|
static uint32_t g_sender_sleep_ms = 0;
|
||||||
static uint32_t g_sender_power_log_ms = 0;
|
static uint32_t g_sender_power_log_ms = 0;
|
||||||
static uint8_t g_sender_timesync_mode = 0;
|
|
||||||
static RxRejectReason g_sender_rx_reject_reason = RxRejectReason::None;
|
static RxRejectReason g_sender_rx_reject_reason = RxRejectReason::None;
|
||||||
static uint32_t g_sender_rx_reject_log_ms = 0;
|
static uint32_t g_sender_rx_reject_log_ms = 0;
|
||||||
static MeterData g_last_meter_data = {};
|
static MeterData g_last_meter_data = {};
|
||||||
static bool g_last_meter_valid = false;
|
static bool g_last_meter_valid = false;
|
||||||
static uint32_t g_last_meter_rx_ms = 0;
|
static uint32_t g_last_meter_rx_ms = 0;
|
||||||
static uint32_t g_meter_stale_seconds = 0;
|
static uint32_t g_meter_stale_seconds = 0;
|
||||||
static constexpr uint32_t SENDER_TIMESYNC_ACQUIRE_MS = 10UL * 60UL * 1000UL;
|
static bool g_time_acquired = false;
|
||||||
static constexpr uint32_t SENDER_TIMESYNC_ACQUIRE_INTERVAL_SEC = 20;
|
static uint32_t g_last_sync_request_ms = 0;
|
||||||
static constexpr uint32_t SENDER_TIMESYNC_ACQUIRE_WINDOW_MS = 3000;
|
|
||||||
|
|
||||||
static void watchdog_kick();
|
static void watchdog_kick();
|
||||||
|
|
||||||
@@ -129,17 +111,6 @@ static void serial_debug_printf(const char *fmt, ...) {
|
|||||||
Serial.println(buf);
|
Serial.println(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void sender_set_timesync_mode(uint8_t mode) {
|
|
||||||
if (g_sender_timesync_mode == mode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
g_sender_timesync_mode = mode;
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
const char *label = mode == 2 ? "acquire" : (mode == 1 ? "slow" : "fast");
|
|
||||||
serial_debug_printf("timesync: mode=%s", label);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint16_t g_last_batch_id_rx[NUM_SENDERS] = {};
|
static uint16_t g_last_batch_id_rx[NUM_SENDERS] = {};
|
||||||
|
|
||||||
struct BatchRxState {
|
struct BatchRxState {
|
||||||
@@ -185,37 +156,6 @@ static bool battery_sample_due(uint32_t now_ms) {
|
|||||||
return g_last_battery_ms == 0 || now_ms - g_last_battery_ms >= BATTERY_SAMPLE_INTERVAL_MS;
|
return g_last_battery_ms == 0 || now_ms - g_last_battery_ms >= BATTERY_SAMPLE_INTERVAL_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool sender_timesync_window_due() {
|
|
||||||
uint32_t interval_sec = SENDER_TIMESYNC_CHECK_SEC_FAST;
|
|
||||||
bool in_acquire = (g_sender_last_timesync_rx_ms == 0) && (millis() - g_boot_ms < SENDER_TIMESYNC_ACQUIRE_MS);
|
|
||||||
bool allow_slow = (millis() - g_boot_ms >= 60000UL) && time_is_synced() && time_rtc_present() &&
|
|
||||||
(g_sender_last_timesync_rx_ms > 0);
|
|
||||||
// RTC boot time is not evidence of receiving network TimeSync.
|
|
||||||
if (in_acquire) {
|
|
||||||
interval_sec = SENDER_TIMESYNC_ACQUIRE_INTERVAL_SEC;
|
|
||||||
sender_set_timesync_mode(2);
|
|
||||||
} else if (allow_slow) {
|
|
||||||
interval_sec = SENDER_TIMESYNC_CHECK_SEC_SLOW;
|
|
||||||
sender_set_timesync_mode(1);
|
|
||||||
} else {
|
|
||||||
sender_set_timesync_mode(0);
|
|
||||||
}
|
|
||||||
static uint32_t last_interval_sec = 0;
|
|
||||||
if (last_interval_sec != interval_sec) {
|
|
||||||
last_interval_sec = interval_sec;
|
|
||||||
g_sender_last_timesync_check_ms = millis();
|
|
||||||
}
|
|
||||||
if (g_sender_last_timesync_check_ms == 0) {
|
|
||||||
g_sender_last_timesync_check_ms = millis() - interval_sec * 1000UL;
|
|
||||||
}
|
|
||||||
uint32_t now_ms = millis();
|
|
||||||
if (now_ms - g_sender_last_timesync_check_ms >= interval_sec * 1000UL) {
|
|
||||||
g_sender_last_timesync_check_ms = now_ms;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool batch_queue_drop_oldest() {
|
static bool batch_queue_drop_oldest() {
|
||||||
if (g_batch_count == 0) {
|
if (g_batch_count == 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -228,33 +168,13 @@ static bool batch_queue_drop_oldest() {
|
|||||||
g_inflight_active = false;
|
g_inflight_active = false;
|
||||||
g_inflight_count = 0;
|
g_inflight_count = 0;
|
||||||
g_inflight_batch_id = 0;
|
g_inflight_batch_id = 0;
|
||||||
|
g_inflight_sync_request = false;
|
||||||
}
|
}
|
||||||
g_batch_tail = (g_batch_tail + 1) % BATCH_QUEUE_DEPTH;
|
g_batch_tail = (g_batch_tail + 1) % BATCH_QUEUE_DEPTH;
|
||||||
g_batch_count--;
|
g_batch_count--;
|
||||||
return dropped_inflight;
|
return dropped_inflight;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void receiver_note_timesync_drift(uint8_t sender_idx, uint32_t sender_ts_utc) {
|
|
||||||
if (sender_idx >= NUM_SENDERS) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!time_is_synced() || sender_ts_utc == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
uint32_t now_utc = time_get_utc();
|
|
||||||
uint32_t diff = now_utc > sender_ts_utc ? now_utc - sender_ts_utc : sender_ts_utc - now_utc;
|
|
||||||
TimeSyncBurstState &state = g_timesync_burst[sender_idx];
|
|
||||||
state.last_drift_check_ms = millis();
|
|
||||||
state.last_drift_ok = diff <= TIME_SYNC_DRIFT_THRESHOLD_SEC;
|
|
||||||
if (!state.last_drift_ok) {
|
|
||||||
if (!state.active) {
|
|
||||||
state.active = true;
|
|
||||||
state.start_ms = millis();
|
|
||||||
state.last_send_ms = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void sender_note_rx_reject(RxRejectReason reason, const char *context) {
|
static void sender_note_rx_reject(RxRejectReason reason, const char *context) {
|
||||||
if (reason == RxRejectReason::None) {
|
if (reason == RxRejectReason::None) {
|
||||||
return;
|
return;
|
||||||
@@ -370,6 +290,29 @@ static uint16_t read_u16_le(const uint8_t *src) {
|
|||||||
return static_cast<uint16_t>(src[0]) | (static_cast<uint16_t>(src[1]) << 8);
|
return static_cast<uint16_t>(src[0]) | (static_cast<uint16_t>(src[1]) << 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void write_u16_be(uint8_t *dst, uint16_t value) {
|
||||||
|
dst[0] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||||
|
dst[1] = static_cast<uint8_t>(value & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t read_u16_be(const uint8_t *src) {
|
||||||
|
return static_cast<uint16_t>(src[0] << 8) | static_cast<uint16_t>(src[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_u32_be(uint8_t *dst, uint32_t value) {
|
||||||
|
dst[0] = static_cast<uint8_t>((value >> 24) & 0xFF);
|
||||||
|
dst[1] = static_cast<uint8_t>((value >> 16) & 0xFF);
|
||||||
|
dst[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||||
|
dst[3] = static_cast<uint8_t>(value & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t read_u32_be(const uint8_t *src) {
|
||||||
|
return (static_cast<uint32_t>(src[0]) << 24) |
|
||||||
|
(static_cast<uint32_t>(src[1]) << 16) |
|
||||||
|
(static_cast<uint32_t>(src[2]) << 8) |
|
||||||
|
static_cast<uint32_t>(src[3]);
|
||||||
|
}
|
||||||
|
|
||||||
static uint16_t sender_id_from_short_id(uint16_t short_id) {
|
static uint16_t sender_id_from_short_id(uint16_t short_id) {
|
||||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
||||||
if (EXPECTED_SENDER_IDS[i] == short_id) {
|
if (EXPECTED_SENDER_IDS[i] == short_id) {
|
||||||
@@ -433,7 +376,7 @@ static uint32_t compute_batch_rx_timeout_ms(uint16_t total_len, uint8_t chunk_co
|
|||||||
}
|
}
|
||||||
size_t max_chunk_payload = total_len > BATCH_CHUNK_PAYLOAD ? BATCH_CHUNK_PAYLOAD : total_len;
|
size_t max_chunk_payload = total_len > BATCH_CHUNK_PAYLOAD ? BATCH_CHUNK_PAYLOAD : total_len;
|
||||||
size_t payload_len = BATCH_HEADER_SIZE + max_chunk_payload;
|
size_t payload_len = BATCH_HEADER_SIZE + max_chunk_payload;
|
||||||
size_t packet_len = 5 + payload_len + 2;
|
size_t packet_len = 3 + payload_len + 2;
|
||||||
uint32_t per_chunk_toa_ms = lora_airtime_ms(packet_len);
|
uint32_t per_chunk_toa_ms = lora_airtime_ms(packet_len);
|
||||||
uint32_t timeout_ms = static_cast<uint32_t>(chunk_count) * per_chunk_toa_ms + BATCH_RX_MARGIN_MS;
|
uint32_t timeout_ms = static_cast<uint32_t>(chunk_count) * per_chunk_toa_ms + BATCH_RX_MARGIN_MS;
|
||||||
return timeout_ms < 10000 ? 10000 : timeout_ms;
|
return timeout_ms < 10000 ? 10000 : timeout_ms;
|
||||||
@@ -444,7 +387,7 @@ static uint32_t compute_batch_ack_timeout_ms(size_t payload_len) {
|
|||||||
return 10000;
|
return 10000;
|
||||||
}
|
}
|
||||||
uint8_t chunk_count = static_cast<uint8_t>((payload_len + BATCH_CHUNK_PAYLOAD - 1) / BATCH_CHUNK_PAYLOAD);
|
uint8_t chunk_count = static_cast<uint8_t>((payload_len + BATCH_CHUNK_PAYLOAD - 1) / BATCH_CHUNK_PAYLOAD);
|
||||||
size_t packet_len = 5 + BATCH_HEADER_SIZE + (payload_len > BATCH_CHUNK_PAYLOAD ? BATCH_CHUNK_PAYLOAD : payload_len) + 2;
|
size_t packet_len = 3 + BATCH_HEADER_SIZE + (payload_len > BATCH_CHUNK_PAYLOAD ? BATCH_CHUNK_PAYLOAD : payload_len) + 2;
|
||||||
uint32_t per_chunk_toa_ms = lora_airtime_ms(packet_len);
|
uint32_t per_chunk_toa_ms = lora_airtime_ms(packet_len);
|
||||||
uint32_t timeout_ms = static_cast<uint32_t>(chunk_count) * per_chunk_toa_ms + BATCH_RX_MARGIN_MS;
|
uint32_t timeout_ms = static_cast<uint32_t>(chunk_count) * per_chunk_toa_ms + BATCH_RX_MARGIN_MS;
|
||||||
return timeout_ms < 10000 ? 10000 : timeout_ms;
|
return timeout_ms < 10000 ? 10000 : timeout_ms;
|
||||||
@@ -467,10 +410,8 @@ static bool send_batch_payload(const uint8_t *data, size_t len, uint32_t ts_for_
|
|||||||
chunk_len = BATCH_CHUNK_PAYLOAD;
|
chunk_len = BATCH_CHUNK_PAYLOAD;
|
||||||
}
|
}
|
||||||
LoraPacket pkt = {};
|
LoraPacket pkt = {};
|
||||||
pkt.protocol_version = PROTOCOL_VERSION;
|
pkt.msg_kind = LoraMsgKind::BatchUp;
|
||||||
pkt.role = DeviceRole::Sender;
|
|
||||||
pkt.device_id_short = g_short_id;
|
pkt.device_id_short = g_short_id;
|
||||||
pkt.payload_type = PayloadType::MeterBatch;
|
|
||||||
pkt.payload_len = chunk_len + BATCH_HEADER_SIZE;
|
pkt.payload_len = chunk_len + BATCH_HEADER_SIZE;
|
||||||
|
|
||||||
uint8_t *payload = pkt.payload;
|
uint8_t *payload = pkt.payload;
|
||||||
@@ -501,27 +442,31 @@ static bool send_batch_payload(const uint8_t *data, size_t len, uint32_t ts_for_
|
|||||||
return all_ok;
|
return all_ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void send_batch_ack(uint16_t batch_id, uint16_t sender_id) {
|
static void send_batch_ack(uint16_t batch_id, uint8_t sample_count) {
|
||||||
|
uint32_t epoch = time_get_utc();
|
||||||
|
uint8_t time_valid = (time_is_synced() && epoch >= MIN_ACCEPTED_EPOCH_UTC) ? 1 : 0;
|
||||||
|
if (!time_valid) {
|
||||||
|
epoch = 0;
|
||||||
|
}
|
||||||
LoraPacket ack = {};
|
LoraPacket ack = {};
|
||||||
ack.protocol_version = PROTOCOL_VERSION;
|
ack.msg_kind = LoraMsgKind::AckDown;
|
||||||
ack.role = DeviceRole::Receiver;
|
|
||||||
ack.device_id_short = g_short_id;
|
ack.device_id_short = g_short_id;
|
||||||
ack.payload_type = PayloadType::Ack;
|
ack.payload_len = 7;
|
||||||
ack.payload_len = 6;
|
ack.payload[0] = time_valid;
|
||||||
write_u16_le(&ack.payload[0], batch_id);
|
write_u16_be(&ack.payload[1], batch_id);
|
||||||
write_u16_le(&ack.payload[2], sender_id);
|
write_u32_be(&ack.payload[3], epoch);
|
||||||
write_u16_le(&ack.payload[4], g_short_id);
|
|
||||||
uint8_t repeats = ACK_REPEAT_COUNT == 0 ? 1 : ACK_REPEAT_COUNT;
|
uint8_t repeats = ACK_REPEAT_COUNT == 0 ? 1 : ACK_REPEAT_COUNT;
|
||||||
for (uint8_t i = 0; i < repeats; ++i) {
|
for (uint8_t i = 0; i < repeats; ++i) {
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
serial_debug_printf("ack: tx repeat %u/%u batch_id=%u", static_cast<unsigned>(i + 1),
|
|
||||||
static_cast<unsigned>(repeats), batch_id);
|
|
||||||
}
|
|
||||||
lora_send(ack);
|
lora_send(ack);
|
||||||
if (i + 1 < repeats && ACK_REPEAT_DELAY_MS > 0) {
|
if (i + 1 < repeats && ACK_REPEAT_DELAY_MS > 0) {
|
||||||
delay(ACK_REPEAT_DELAY_MS);
|
delay(ACK_REPEAT_DELAY_MS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
serial_debug_printf("ack: tx batch_id=%u time_valid=%u epoch=%lu samples=%u",
|
||||||
|
batch_id,
|
||||||
|
static_cast<unsigned>(time_valid),
|
||||||
|
static_cast<unsigned long>(epoch),
|
||||||
|
static_cast<unsigned>(sample_count));
|
||||||
lora_receive_continuous();
|
lora_receive_continuous();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,23 +492,25 @@ static bool prepare_inflight_from_queue() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool send_inflight_batch(uint32_t ts_for_display) {
|
static bool send_inflight_batch(uint32_t ts_for_display) {
|
||||||
if (!g_inflight_active || g_inflight_count == 0) {
|
if (!g_inflight_active) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
BatchInput input = {};
|
BatchInput input = {};
|
||||||
input.sender_id = sender_id_from_short_id(g_short_id);
|
input.sender_id = sender_id_from_short_id(g_short_id);
|
||||||
input.batch_id = g_inflight_batch_id;
|
input.batch_id = g_inflight_batch_id;
|
||||||
input.t_last = g_inflight_samples[g_inflight_count - 1].ts_utc;
|
input.t_last = g_inflight_sync_request ? time_get_utc() :
|
||||||
|
g_inflight_samples[g_inflight_count - 1].ts_utc;
|
||||||
uint32_t dt_s = METER_SAMPLE_INTERVAL_MS / 1000;
|
uint32_t dt_s = METER_SAMPLE_INTERVAL_MS / 1000;
|
||||||
input.dt_s = dt_s > 0 ? static_cast<uint8_t>(dt_s) : 1;
|
input.dt_s = dt_s > 0 ? static_cast<uint8_t>(dt_s) : 1;
|
||||||
input.n = g_inflight_count;
|
input.n = g_inflight_sync_request ? 0 : g_inflight_count;
|
||||||
input.battery_mV = battery_mv_from_voltage(g_inflight_samples[g_inflight_count - 1].battery_voltage_v);
|
input.battery_mV = g_inflight_sync_request ? battery_mv_from_voltage(g_last_battery_voltage_v) :
|
||||||
|
battery_mv_from_voltage(g_inflight_samples[g_inflight_count - 1].battery_voltage_v);
|
||||||
input.err_m = g_sender_faults.meter_read_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.meter_read_fail);
|
input.err_m = g_sender_faults.meter_read_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.meter_read_fail);
|
||||||
input.err_d = g_sender_faults.decode_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.decode_fail);
|
input.err_d = g_sender_faults.decode_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.decode_fail);
|
||||||
input.err_tx = g_sender_faults.lora_tx_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.lora_tx_fail);
|
input.err_tx = g_sender_faults.lora_tx_fail > 255 ? 255 : static_cast<uint8_t>(g_sender_faults.lora_tx_fail);
|
||||||
input.err_last = static_cast<uint8_t>(g_sender_last_error);
|
input.err_last = static_cast<uint8_t>(g_sender_last_error);
|
||||||
input.err_rx_reject = static_cast<uint8_t>(g_sender_rx_reject_reason);
|
input.err_rx_reject = static_cast<uint8_t>(g_sender_rx_reject_reason);
|
||||||
for (uint8_t i = 0; i < g_inflight_count; ++i) {
|
for (uint8_t i = 0; i < input.n; ++i) {
|
||||||
input.energy_wh[i] = kwh_to_wh_from_float(g_inflight_samples[i].energy_total_kwh);
|
input.energy_wh[i] = kwh_to_wh_from_float(g_inflight_samples[i].energy_total_kwh);
|
||||||
if (!float_to_i16_w(g_inflight_samples[i].phase_power_w[0], input.p1_w[i]) ||
|
if (!float_to_i16_w(g_inflight_samples[i].phase_power_w[0], input.p1_w[i]) ||
|
||||||
!float_to_i16_w(g_inflight_samples[i].phase_power_w[1], input.p2_w[i]) ||
|
!float_to_i16_w(g_inflight_samples[i].phase_power_w[1], input.p2_w[i]) ||
|
||||||
@@ -580,7 +527,7 @@ static bool send_inflight_batch(uint32_t ts_for_display) {
|
|||||||
}
|
}
|
||||||
uint32_t encode_ms = millis() - encode_start;
|
uint32_t encode_ms = millis() - encode_start;
|
||||||
if (SERIAL_DEBUG_MODE) {
|
if (SERIAL_DEBUG_MODE) {
|
||||||
serial_debug_printf("tx: batch_id=%u count=%u bin_len=%u", g_inflight_batch_id, g_inflight_count,
|
serial_debug_printf("tx: batch_id=%u count=%u bin_len=%u", g_inflight_batch_id, input.n,
|
||||||
static_cast<unsigned>(encoded_len));
|
static_cast<unsigned>(encoded_len));
|
||||||
if (encode_ms > 200) {
|
if (encode_ms > 200) {
|
||||||
serial_debug_printf("tx: encode took %lums", static_cast<unsigned long>(encode_ms));
|
serial_debug_printf("tx: encode took %lums", static_cast<unsigned long>(encode_ms));
|
||||||
@@ -596,10 +543,18 @@ static bool send_inflight_batch(uint32_t ts_for_display) {
|
|||||||
}
|
}
|
||||||
if (ok) {
|
if (ok) {
|
||||||
g_last_batch_send_ms = millis();
|
g_last_batch_send_ms = millis();
|
||||||
|
if (g_inflight_sync_request) {
|
||||||
|
serial_debug_printf("sync: request tx batch_id=%u", g_inflight_batch_id);
|
||||||
|
} else {
|
||||||
serial_debug_printf("tx: sent batch_id=%u len=%u", g_inflight_batch_id, static_cast<unsigned>(encoded_len));
|
serial_debug_printf("tx: sent batch_id=%u len=%u", g_inflight_batch_id, static_cast<unsigned>(encoded_len));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (g_inflight_sync_request) {
|
||||||
|
serial_debug_printf("sync: request tx failed batch_id=%u", g_inflight_batch_id);
|
||||||
} else {
|
} else {
|
||||||
serial_debug_printf("tx: send failed batch_id=%u", g_inflight_batch_id);
|
serial_debug_printf("tx: send failed batch_id=%u", g_inflight_batch_id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,6 +562,7 @@ static bool send_meter_batch(uint32_t ts_for_display) {
|
|||||||
if (!prepare_inflight_from_queue()) {
|
if (!prepare_inflight_from_queue()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
g_inflight_sync_request = false;
|
||||||
bool ok = send_inflight_batch(ts_for_display);
|
bool ok = send_inflight_batch(ts_for_display);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
g_last_sent_batch_id = g_inflight_batch_id;
|
g_last_sent_batch_id = g_inflight_batch_id;
|
||||||
@@ -615,12 +571,36 @@ static bool send_meter_batch(uint32_t ts_for_display) {
|
|||||||
g_inflight_active = false;
|
g_inflight_active = false;
|
||||||
g_inflight_count = 0;
|
g_inflight_count = 0;
|
||||||
g_inflight_batch_id = 0;
|
g_inflight_batch_id = 0;
|
||||||
|
g_inflight_sync_request = false;
|
||||||
|
}
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool send_sync_request() {
|
||||||
|
if (g_batch_ack_pending) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (battery_sample_due(millis())) {
|
||||||
|
update_battery_cache();
|
||||||
|
}
|
||||||
|
g_inflight_active = true;
|
||||||
|
g_inflight_sync_request = true;
|
||||||
|
g_inflight_count = 0;
|
||||||
|
g_inflight_batch_id = g_batch_id;
|
||||||
|
bool ok = send_inflight_batch(time_get_utc());
|
||||||
|
if (ok) {
|
||||||
|
g_last_sent_batch_id = g_inflight_batch_id;
|
||||||
|
g_batch_ack_pending = true;
|
||||||
|
} else {
|
||||||
|
g_inflight_active = false;
|
||||||
|
g_inflight_sync_request = false;
|
||||||
|
g_inflight_batch_id = 0;
|
||||||
}
|
}
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool resend_inflight_batch(uint32_t ts_for_display) {
|
static bool resend_inflight_batch(uint32_t ts_for_display) {
|
||||||
if (!g_batch_ack_pending || !g_inflight_active || g_inflight_count == 0) {
|
if (!g_batch_ack_pending || !g_inflight_active || (!g_inflight_sync_request && g_inflight_count == 0)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return send_inflight_batch(ts_for_display);
|
return send_inflight_batch(ts_for_display);
|
||||||
@@ -635,6 +615,7 @@ static void finish_inflight_batch() {
|
|||||||
g_inflight_active = false;
|
g_inflight_active = false;
|
||||||
g_inflight_count = 0;
|
g_inflight_count = 0;
|
||||||
g_inflight_batch_id = 0;
|
g_inflight_batch_id = 0;
|
||||||
|
g_inflight_sync_request = false;
|
||||||
g_batch_id++;
|
g_batch_id++;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,8 +712,6 @@ void setup() {
|
|||||||
|
|
||||||
lora_init();
|
lora_init();
|
||||||
display_init();
|
display_init();
|
||||||
time_rtc_init();
|
|
||||||
time_try_load_from_rtc();
|
|
||||||
display_set_self_ids(g_short_id, g_device_id);
|
display_set_self_ids(g_short_id, g_device_id);
|
||||||
|
|
||||||
if (g_role == DeviceRole::Sender) {
|
if (g_role == DeviceRole::Sender) {
|
||||||
@@ -741,6 +720,8 @@ void setup() {
|
|||||||
meter_init();
|
meter_init();
|
||||||
g_last_sample_ms = millis() - METER_SAMPLE_INTERVAL_MS;
|
g_last_sample_ms = millis() - METER_SAMPLE_INTERVAL_MS;
|
||||||
g_last_send_ms = millis();
|
g_last_send_ms = millis();
|
||||||
|
g_last_sync_request_ms = millis() - SYNC_REQUEST_INTERVAL_MS;
|
||||||
|
g_time_acquired = false;
|
||||||
update_battery_cache();
|
update_battery_cache();
|
||||||
} else {
|
} else {
|
||||||
power_receiver_init();
|
power_receiver_init();
|
||||||
@@ -794,6 +775,7 @@ static void sender_loop() {
|
|||||||
g_batch_retry_count);
|
g_batch_retry_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (g_time_acquired) {
|
||||||
const char *frame = nullptr;
|
const char *frame = nullptr;
|
||||||
size_t frame_len = 0;
|
size_t frame_len = 0;
|
||||||
if (meter_poll_frame(frame, frame_len)) {
|
if (meter_poll_frame(frame, frame_len)) {
|
||||||
@@ -840,9 +822,7 @@ static void sender_loop() {
|
|||||||
data.battery_voltage_v = g_last_battery_voltage_v;
|
data.battery_voltage_v = g_last_battery_voltage_v;
|
||||||
data.battery_percent = g_last_battery_percent;
|
data.battery_percent = g_last_battery_percent;
|
||||||
data.rx_reject_reason = static_cast<uint8_t>(g_sender_rx_reject_reason);
|
data.rx_reject_reason = static_cast<uint8_t>(g_sender_rx_reject_reason);
|
||||||
|
data.ts_utc = time_get_utc();
|
||||||
uint32_t now_utc = time_get_utc();
|
|
||||||
data.ts_utc = now_utc > 0 ? now_utc : millis() / 1000;
|
|
||||||
data.valid = meter_ok;
|
data.valid = meter_ok;
|
||||||
|
|
||||||
g_last_sample_ts_utc = data.ts_utc;
|
g_last_sample_ts_utc = data.ts_utc;
|
||||||
@@ -859,10 +839,16 @@ static void sender_loop() {
|
|||||||
g_last_send_ms = now_ms;
|
g_last_send_ms = now_ms;
|
||||||
send_meter_batch(last_sample_ts());
|
send_meter_batch(last_sample_ts());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (!g_batch_ack_pending && now_ms - g_last_sync_request_ms >= SYNC_REQUEST_INTERVAL_MS) {
|
||||||
|
g_last_sync_request_ms = now_ms;
|
||||||
|
send_sync_request();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (g_batch_ack_pending) {
|
if (g_batch_ack_pending) {
|
||||||
LoraPacket ack_pkt = {};
|
LoraPacket ack_pkt = {};
|
||||||
const uint32_t ack_len = 5 + 6 + 2;
|
const uint32_t ack_len = 3 + 7 + 2;
|
||||||
uint32_t ack_air_ms = lora_airtime_ms(ack_len);
|
uint32_t ack_air_ms = lora_airtime_ms(ack_len);
|
||||||
uint32_t ack_window_ms = ack_air_ms + 300;
|
uint32_t ack_window_ms = ack_air_ms + 300;
|
||||||
if (ack_window_ms < 1200) {
|
if (ack_window_ms < 1200) {
|
||||||
@@ -886,74 +872,35 @@ static void sender_loop() {
|
|||||||
}
|
}
|
||||||
if (!got_ack) {
|
if (!got_ack) {
|
||||||
sender_note_rx_reject(lora_get_last_rx_reject_reason(), "ack");
|
sender_note_rx_reject(lora_get_last_rx_reject_reason(), "ack");
|
||||||
} else if (ack_pkt.role != DeviceRole::Receiver) {
|
} else if (ack_pkt.msg_kind != LoraMsgKind::AckDown) {
|
||||||
sender_note_rx_reject(RxRejectReason::WrongRole, "ack");
|
sender_note_rx_reject(RxRejectReason::InvalidMsgKind, "ack");
|
||||||
} else if (ack_pkt.payload_type != PayloadType::Ack) {
|
} else if (ack_pkt.payload_len < 7) {
|
||||||
sender_note_rx_reject(RxRejectReason::WrongPayloadType, "ack");
|
|
||||||
} else if (ack_pkt.payload_len < 6) {
|
|
||||||
sender_note_rx_reject(RxRejectReason::LengthMismatch, "ack");
|
sender_note_rx_reject(RxRejectReason::LengthMismatch, "ack");
|
||||||
} else {
|
} else {
|
||||||
uint16_t ack_id = read_u16_le(ack_pkt.payload);
|
uint8_t time_valid = ack_pkt.payload[0] & 0x01;
|
||||||
uint16_t ack_sender = read_u16_le(&ack_pkt.payload[2]);
|
uint16_t ack_id = read_u16_be(&ack_pkt.payload[1]);
|
||||||
uint16_t ack_receiver = read_u16_le(&ack_pkt.payload[4]);
|
uint32_t ack_epoch = read_u32_be(&ack_pkt.payload[3]);
|
||||||
if (ack_sender == g_short_id && ack_receiver == ack_pkt.device_id_short &&
|
bool set_time = false;
|
||||||
g_batch_ack_pending && ack_id == g_last_sent_batch_id) {
|
if (g_batch_ack_pending && ack_id == g_last_sent_batch_id) {
|
||||||
|
if (time_valid == 1 && ack_epoch >= MIN_ACCEPTED_EPOCH_UTC) {
|
||||||
|
time_set_utc(ack_epoch);
|
||||||
|
g_time_acquired = true;
|
||||||
|
set_time = true;
|
||||||
|
}
|
||||||
g_last_acked_batch_id = ack_id;
|
g_last_acked_batch_id = ack_id;
|
||||||
serial_debug_printf("ack: rx ok batch_id=%u", ack_id);
|
serial_debug_printf("ack: rx ok batch_id=%u time_valid=%u epoch=%lu set=%u",
|
||||||
|
ack_id,
|
||||||
|
static_cast<unsigned>(time_valid),
|
||||||
|
static_cast<unsigned long>(ack_epoch),
|
||||||
|
set_time ? 1 : 0);
|
||||||
finish_inflight_batch();
|
finish_inflight_batch();
|
||||||
} else {
|
} else {
|
||||||
if (ack_sender != g_short_id || ack_receiver != ack_pkt.device_id_short) {
|
if (ack_id != g_last_sent_batch_id) {
|
||||||
sender_note_rx_reject(RxRejectReason::DeviceIdMismatch, "ack");
|
|
||||||
} else if (ack_id != g_last_sent_batch_id) {
|
|
||||||
sender_note_rx_reject(RxRejectReason::BatchIdMismatch, "ack");
|
sender_note_rx_reject(RxRejectReason::BatchIdMismatch, "ack");
|
||||||
}
|
}
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
serial_debug_printf("ack: reject batch_id=%u sender=%u receiver=%u exp_batch=%u exp_sender=%u",
|
|
||||||
ack_id, ack_sender, ack_receiver, g_last_sent_batch_id, g_short_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bool timesync_due = (!g_batch_ack_pending && sender_timesync_window_due());
|
|
||||||
if (timesync_due) {
|
|
||||||
LoraPacket rx = {};
|
|
||||||
uint32_t rx_start = millis();
|
|
||||||
uint32_t window_ms = (g_sender_timesync_mode == 2) ? SENDER_TIMESYNC_ACQUIRE_WINDOW_MS : SENDER_TIMESYNC_WINDOW_MS;
|
|
||||||
bool got = lora_receive_window(rx, window_ms);
|
|
||||||
uint32_t rx_elapsed = millis() - rx_start;
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
g_sender_rx_window_ms += rx_elapsed;
|
|
||||||
}
|
|
||||||
if (!got) {
|
|
||||||
sender_note_rx_reject(lora_get_last_rx_reject_reason(), "timesync");
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
serial_debug_printf("timesync: rx miss window_ms=%lu", static_cast<unsigned long>(window_ms));
|
|
||||||
}
|
|
||||||
} else if (rx.role != DeviceRole::Receiver) {
|
|
||||||
sender_note_rx_reject(RxRejectReason::WrongRole, "timesync");
|
|
||||||
} else if (rx.payload_type != PayloadType::TimeSync) {
|
|
||||||
sender_note_rx_reject(RxRejectReason::WrongPayloadType, "timesync");
|
|
||||||
} else if (time_handle_timesync_payload(rx.payload, rx.payload_len)) {
|
|
||||||
g_sender_last_timesync_rx_ms = now_ms;
|
|
||||||
if (g_sender_timesync_error) {
|
|
||||||
g_sender_timesync_error = false;
|
|
||||||
display_set_last_error(FaultType::None, 0, 0);
|
|
||||||
}
|
|
||||||
serial_debug_printf("timesync: rx ok window_ms=%lu", static_cast<unsigned long>(window_ms));
|
|
||||||
} else {
|
|
||||||
sender_note_rx_reject(RxRejectReason::LengthMismatch, "timesync");
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
serial_debug_printf("timesync: rx miss window_ms=%lu", static_cast<unsigned long>(window_ms));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
uint32_t timesync_age_ms = (g_sender_last_timesync_rx_ms > 0) ? (now_ms - g_sender_last_timesync_rx_ms)
|
|
||||||
: (now_ms - g_boot_ms);
|
|
||||||
if (!g_sender_timesync_error && timesync_age_ms > TIME_SYNC_ERROR_TIMEOUT_MS) {
|
|
||||||
g_sender_timesync_error = true;
|
|
||||||
display_set_last_error(FaultType::TimeSync, time_get_utc(), now_ms);
|
|
||||||
}
|
|
||||||
if (!g_batch_ack_pending) {
|
if (!g_batch_ack_pending) {
|
||||||
lora_sleep();
|
lora_sleep();
|
||||||
}
|
}
|
||||||
@@ -974,6 +921,7 @@ static void sender_loop() {
|
|||||||
g_inflight_active = false;
|
g_inflight_active = false;
|
||||||
g_inflight_count = 0;
|
g_inflight_count = 0;
|
||||||
g_inflight_batch_id = 0;
|
g_inflight_batch_id = 0;
|
||||||
|
g_inflight_sync_request = false;
|
||||||
}
|
}
|
||||||
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::LoraTx);
|
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::LoraTx);
|
||||||
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
|
||||||
@@ -982,9 +930,14 @@ static void sender_loop() {
|
|||||||
|
|
||||||
display_tick();
|
display_tick();
|
||||||
|
|
||||||
uint32_t next_sample_due = g_last_sample_ms + METER_SAMPLE_INTERVAL_MS;
|
uint32_t next_due = g_time_acquired ? (g_last_sample_ms + METER_SAMPLE_INTERVAL_MS) :
|
||||||
|
(g_last_sync_request_ms + SYNC_REQUEST_INTERVAL_MS);
|
||||||
|
if (g_time_acquired) {
|
||||||
uint32_t next_send_due = g_last_send_ms + METER_SEND_INTERVAL_MS;
|
uint32_t next_send_due = g_last_send_ms + METER_SEND_INTERVAL_MS;
|
||||||
uint32_t next_due = next_sample_due < next_send_due ? next_sample_due : next_send_due;
|
if (next_send_due < next_due) {
|
||||||
|
next_due = next_send_due;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!g_batch_ack_pending && next_due > now_ms) {
|
if (!g_batch_ack_pending && next_due > now_ms) {
|
||||||
watchdog_kick();
|
watchdog_kick();
|
||||||
if (SERIAL_DEBUG_MODE) {
|
if (SERIAL_DEBUG_MODE) {
|
||||||
@@ -1002,64 +955,13 @@ static void sender_loop() {
|
|||||||
|
|
||||||
static void receiver_loop() {
|
static void receiver_loop() {
|
||||||
watchdog_kick();
|
watchdog_kick();
|
||||||
if (g_last_timesync_ms == 0) {
|
|
||||||
g_last_timesync_ms = millis() - (TIME_SYNC_INTERVAL_SEC * 1000UL - TIME_SYNC_OFFSET_MS);
|
|
||||||
}
|
|
||||||
LoraPacket pkt = {};
|
LoraPacket pkt = {};
|
||||||
if (lora_receive(pkt, 0) && pkt.protocol_version == PROTOCOL_VERSION) {
|
if (lora_receive(pkt, 0)) {
|
||||||
if (pkt.payload_type == PayloadType::MeterData) {
|
if (pkt.msg_kind == LoraMsgKind::BatchUp) {
|
||||||
uint8_t decompressed[256];
|
|
||||||
size_t decompressed_len = 0;
|
|
||||||
if (!decompressBuffer(pkt.payload, pkt.payload_len, decompressed, sizeof(decompressed) - 1, decompressed_len)) {
|
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
|
||||||
} else {
|
|
||||||
if (decompressed_len >= sizeof(decompressed)) {
|
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
decompressed[decompressed_len] = '\0';
|
|
||||||
MeterData data = {};
|
|
||||||
if (jsonToMeterData(String(reinterpret_cast<const char *>(decompressed)), data)) {
|
|
||||||
data.link_valid = true;
|
|
||||||
data.link_rssi_dbm = pkt.rssi_dbm;
|
|
||||||
data.link_snr_db = pkt.snr_db;
|
|
||||||
sd_logger_log_sample(data, data.last_error != FaultType::None);
|
|
||||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
|
||||||
if (pkt.device_id_short == EXPECTED_SENDER_IDS[i]) {
|
|
||||||
data.short_id = pkt.device_id_short;
|
|
||||||
g_sender_statuses[i].last_data = data;
|
|
||||||
g_sender_statuses[i].last_update_ts_utc = data.ts_utc;
|
|
||||||
g_sender_statuses[i].has_data = true;
|
|
||||||
g_sender_faults_remote[i].meter_read_fail = data.err_meter_read;
|
|
||||||
g_sender_faults_remote[i].lora_tx_fail = data.err_lora_tx;
|
|
||||||
receiver_note_timesync_drift(i, data.ts_utc);
|
|
||||||
g_sender_last_error_remote[i] = data.last_error;
|
|
||||||
g_sender_last_error_remote_utc[i] = time_get_utc();
|
|
||||||
g_sender_last_error_remote_ms[i] = millis();
|
|
||||||
mqtt_publish_state(data);
|
|
||||||
if (ENABLE_HA_DISCOVERY && !g_sender_discovery_sent[i]) {
|
|
||||||
g_sender_discovery_sent[i] = mqtt_publish_discovery(data.device_id);
|
|
||||||
}
|
|
||||||
publish_faults_if_needed(data.device_id, g_sender_faults_remote[i], g_sender_faults_remote_published[i],
|
|
||||||
g_sender_last_error_remote[i], g_sender_last_error_remote_published[i],
|
|
||||||
g_sender_last_error_remote_utc[i], g_sender_last_error_remote_ms[i]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (pkt.payload_type == PayloadType::MeterBatch) {
|
|
||||||
BatchInput batch = {};
|
BatchInput batch = {};
|
||||||
bool decode_error = false;
|
bool decode_error = false;
|
||||||
uint16_t batch_id = 0;
|
uint16_t batch_id = 0;
|
||||||
if (process_batch_packet(pkt, batch, decode_error, batch_id)) {
|
if (process_batch_packet(pkt, batch, decode_error, batch_id)) {
|
||||||
MeterData samples[METER_BATCH_MAX_SAMPLES];
|
|
||||||
size_t count = 0;
|
|
||||||
int8_t sender_idx = -1;
|
int8_t sender_idx = -1;
|
||||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
||||||
if (pkt.device_id_short == EXPECTED_SENDER_IDS[i]) {
|
if (pkt.device_id_short == EXPECTED_SENDER_IDS[i]) {
|
||||||
@@ -1067,29 +969,43 @@ static void receiver_loop() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool duplicate = sender_idx >= 0 && g_last_batch_id_rx[sender_idx] == batch_id;
|
bool duplicate = sender_idx >= 0 && g_last_batch_id_rx[sender_idx] == batch_id;
|
||||||
|
send_batch_ack(batch_id, batch.n);
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
send_batch_ack(batch_id, pkt.device_id_short);
|
goto receiver_loop_done;
|
||||||
} else {
|
}
|
||||||
|
if (sender_idx >= 0) {
|
||||||
g_last_batch_id_rx[sender_idx] = batch_id;
|
g_last_batch_id_rx[sender_idx] = batch_id;
|
||||||
send_batch_ack(batch_id, pkt.device_id_short);
|
}
|
||||||
count = batch.n;
|
if (batch.n == 0) {
|
||||||
if (count == 0 || count > METER_BATCH_MAX_SAMPLES) {
|
goto receiver_loop_done;
|
||||||
|
}
|
||||||
|
if (batch.n > METER_BATCH_MAX_SAMPLES) {
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
||||||
return;
|
goto receiver_loop_done;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t count = batch.n;
|
||||||
uint16_t short_id = pkt.device_id_short;
|
uint16_t short_id = pkt.device_id_short;
|
||||||
if (short_id == 0) {
|
if (short_id == 0) {
|
||||||
short_id = short_id_from_sender_id(batch.sender_id);
|
short_id = short_id_from_sender_id(batch.sender_id);
|
||||||
}
|
}
|
||||||
uint64_t span = static_cast<uint64_t>(batch.dt_s) * static_cast<uint64_t>(count - 1);
|
uint64_t span = static_cast<uint64_t>(batch.dt_s) * static_cast<uint64_t>(count - 1);
|
||||||
if (batch.t_last < span) {
|
if (batch.t_last < span || batch.t_last < MIN_ACCEPTED_EPOCH_UTC) {
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
||||||
return;
|
goto receiver_loop_done;
|
||||||
}
|
}
|
||||||
uint32_t t_first = batch.t_last - static_cast<uint32_t>(span);
|
uint32_t t_first = batch.t_last - static_cast<uint32_t>(span);
|
||||||
|
if (t_first < MIN_ACCEPTED_EPOCH_UTC) {
|
||||||
|
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
||||||
|
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
||||||
|
goto receiver_loop_done;
|
||||||
|
}
|
||||||
|
|
||||||
|
MeterData samples[METER_BATCH_MAX_SAMPLES];
|
||||||
float bat_v = batch.battery_mV > 0 ? static_cast<float>(batch.battery_mV) / 1000.0f : NAN;
|
float bat_v = batch.battery_mV > 0 ? static_cast<float>(batch.battery_mV) / 1000.0f : NAN;
|
||||||
for (size_t s = 0; s < count; ++s) {
|
for (size_t s = 0; s < count; ++s) {
|
||||||
MeterData &data = samples[s];
|
MeterData &data = samples[s];
|
||||||
@@ -1107,11 +1023,7 @@ static void receiver_loop() {
|
|||||||
data.phase_power_w[2] = static_cast<float>(batch.p3_w[s]);
|
data.phase_power_w[2] = static_cast<float>(batch.p3_w[s]);
|
||||||
data.total_power_w = data.phase_power_w[0] + data.phase_power_w[1] + data.phase_power_w[2];
|
data.total_power_w = data.phase_power_w[0] + data.phase_power_w[1] + data.phase_power_w[2];
|
||||||
data.battery_voltage_v = bat_v;
|
data.battery_voltage_v = bat_v;
|
||||||
if (!isnan(bat_v)) {
|
data.battery_percent = !isnan(bat_v) ? battery_percent_from_voltage(bat_v) : 0;
|
||||||
data.battery_percent = battery_percent_from_voltage(bat_v);
|
|
||||||
} else {
|
|
||||||
data.battery_percent = 0;
|
|
||||||
}
|
|
||||||
data.valid = true;
|
data.valid = true;
|
||||||
data.link_valid = true;
|
data.link_valid = true;
|
||||||
data.link_rssi_dbm = pkt.rssi_dbm;
|
data.link_rssi_dbm = pkt.rssi_dbm;
|
||||||
@@ -1127,16 +1039,13 @@ static void receiver_loop() {
|
|||||||
if (sender_idx >= 0) {
|
if (sender_idx >= 0) {
|
||||||
web_server_set_last_batch(static_cast<uint8_t>(sender_idx), samples, count);
|
web_server_set_last_batch(static_cast<uint8_t>(sender_idx), samples, count);
|
||||||
for (size_t s = 0; s < count; ++s) {
|
for (size_t s = 0; s < count; ++s) {
|
||||||
samples[s].short_id = pkt.device_id_short;
|
|
||||||
mqtt_publish_state(samples[s]);
|
mqtt_publish_state(samples[s]);
|
||||||
}
|
}
|
||||||
if (count > 0) {
|
|
||||||
g_sender_statuses[sender_idx].last_data = samples[count - 1];
|
g_sender_statuses[sender_idx].last_data = samples[count - 1];
|
||||||
g_sender_statuses[sender_idx].last_update_ts_utc = samples[count - 1].ts_utc;
|
g_sender_statuses[sender_idx].last_update_ts_utc = samples[count - 1].ts_utc;
|
||||||
g_sender_statuses[sender_idx].has_data = true;
|
g_sender_statuses[sender_idx].has_data = true;
|
||||||
g_sender_faults_remote[sender_idx].meter_read_fail = samples[count - 1].err_meter_read;
|
g_sender_faults_remote[sender_idx].meter_read_fail = samples[count - 1].err_meter_read;
|
||||||
g_sender_faults_remote[sender_idx].lora_tx_fail = samples[count - 1].err_lora_tx;
|
g_sender_faults_remote[sender_idx].lora_tx_fail = samples[count - 1].err_lora_tx;
|
||||||
receiver_note_timesync_drift(static_cast<uint8_t>(sender_idx), samples[count - 1].ts_utc);
|
|
||||||
g_sender_last_error_remote[sender_idx] = samples[count - 1].last_error;
|
g_sender_last_error_remote[sender_idx] = samples[count - 1].last_error;
|
||||||
g_sender_last_error_remote_utc[sender_idx] = time_get_utc();
|
g_sender_last_error_remote_utc[sender_idx] = time_get_utc();
|
||||||
g_sender_last_error_remote_ms[sender_idx] = millis();
|
g_sender_last_error_remote_ms[sender_idx] = millis();
|
||||||
@@ -1147,8 +1056,6 @@ static void receiver_loop() {
|
|||||||
g_sender_last_error_remote[sender_idx], g_sender_last_error_remote_published[sender_idx],
|
g_sender_last_error_remote[sender_idx], g_sender_last_error_remote_published[sender_idx],
|
||||||
g_sender_last_error_remote_utc[sender_idx], g_sender_last_error_remote_ms[sender_idx]);
|
g_sender_last_error_remote_utc[sender_idx], g_sender_last_error_remote_ms[sender_idx]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (decode_error) {
|
} else if (decode_error) {
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::Decode);
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
||||||
@@ -1156,42 +1063,7 @@ static void receiver_loop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t interval_sec = TIME_SYNC_INTERVAL_SEC;
|
receiver_loop_done:
|
||||||
uint32_t now_ms = millis();
|
|
||||||
if (!g_ap_mode) {
|
|
||||||
bool burst_sent = false;
|
|
||||||
for (uint8_t i = 0; i < NUM_SENDERS; ++i) {
|
|
||||||
TimeSyncBurstState &state = g_timesync_burst[i];
|
|
||||||
if (state.active) {
|
|
||||||
if (now_ms - state.start_ms >= TIME_SYNC_BURST_DURATION_MS) {
|
|
||||||
state.active = false;
|
|
||||||
} else if (state.last_send_ms == 0 || now_ms - state.last_send_ms >= TIME_SYNC_BURST_INTERVAL_MS) {
|
|
||||||
state.last_send_ms = now_ms;
|
|
||||||
burst_sent = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (burst_sent) {
|
|
||||||
if (!time_send_timesync(g_short_id)) {
|
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::LoraTx);
|
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
|
||||||
}
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
serial_debug_printf("timesync: tx burst");
|
|
||||||
}
|
|
||||||
g_last_timesync_ms = now_ms;
|
|
||||||
} else if (now_ms - g_last_timesync_ms > interval_sec * 1000UL) {
|
|
||||||
g_last_timesync_ms = now_ms;
|
|
||||||
if (!time_send_timesync(g_short_id)) {
|
|
||||||
note_fault(g_receiver_faults, g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms, FaultType::LoraTx);
|
|
||||||
display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms);
|
|
||||||
}
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
serial_debug_printf("timesync: tx normal");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mqtt_loop();
|
mqtt_loop();
|
||||||
web_server_loop();
|
web_server_loop();
|
||||||
if (ENABLE_HA_DISCOVERY && !g_receiver_discovery_sent) {
|
if (ENABLE_HA_DISCOVERY && !g_receiver_discovery_sent) {
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ static const char *fault_text(FaultType fault) {
|
|||||||
return "decode";
|
return "decode";
|
||||||
case FaultType::LoraTx:
|
case FaultType::LoraTx:
|
||||||
return "loratx";
|
return "loratx";
|
||||||
case FaultType::TimeSync:
|
|
||||||
return "timesync";
|
|
||||||
default:
|
default:
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou
|
|||||||
if (!out || !out_len) {
|
if (!out || !out_len) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (in.n == 0 || in.n > kMaxSamples) {
|
if (in.n > kMaxSamples) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (in.dt_s == 0) {
|
if (in.dt_s == 0) {
|
||||||
@@ -131,6 +131,11 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou
|
|||||||
out[pos++] = in.err_last;
|
out[pos++] = in.err_last;
|
||||||
out[pos++] = in.err_rx_reject;
|
out[pos++] = in.err_rx_reject;
|
||||||
|
|
||||||
|
if (in.n == 0) {
|
||||||
|
*out_len = pos;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!ensure_capacity(4, out_cap, pos)) {
|
if (!ensure_capacity(4, out_cap, pos)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -210,9 +215,18 @@ bool decode_batch(const uint8_t *buf, size_t len, BatchInput *out) {
|
|||||||
out->err_last = buf[pos++];
|
out->err_last = buf[pos++];
|
||||||
out->err_rx_reject = buf[pos++];
|
out->err_rx_reject = buf[pos++];
|
||||||
|
|
||||||
if (out->n == 0 || out->n > kMaxSamples || out->dt_s == 0) {
|
if (out->n > kMaxSamples || out->dt_s == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (out->n == 0) {
|
||||||
|
for (uint8_t i = 0; i < kMaxSamples; ++i) {
|
||||||
|
out->energy_wh[i] = 0;
|
||||||
|
out->p1_w[i] = 0;
|
||||||
|
out->p2_w[i] = 0;
|
||||||
|
out->p3_w[i] = 0;
|
||||||
|
}
|
||||||
|
return pos == len;
|
||||||
|
}
|
||||||
if (pos + 4 > len) {
|
if (pos + 4 > len) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
#include "rtc_ds3231.h"
|
|
||||||
#include "config.h"
|
|
||||||
#include <Wire.h>
|
|
||||||
#include <string>
|
|
||||||
#include <time.h>
|
|
||||||
|
|
||||||
static constexpr uint8_t DS3231_ADDR = 0x68;
|
|
||||||
|
|
||||||
static uint8_t bcd_to_dec(uint8_t val) {
|
|
||||||
return static_cast<uint8_t>((val >> 4) * 10 + (val & 0x0F));
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t dec_to_bcd(uint8_t val) {
|
|
||||||
return static_cast<uint8_t>(((val / 10) << 4) | (val % 10));
|
|
||||||
}
|
|
||||||
|
|
||||||
static time_t timegm_fallback(struct tm *tm_utc) {
|
|
||||||
if (!tm_utc) {
|
|
||||||
return static_cast<time_t>(-1);
|
|
||||||
}
|
|
||||||
const char *old_tz = getenv("TZ");
|
|
||||||
// getenv() may return a pointer into mutable storage that becomes invalid after setenv().
|
|
||||||
std::string old_tz_copy = old_tz ? old_tz : "";
|
|
||||||
setenv("TZ", "UTC0", 1);
|
|
||||||
tzset();
|
|
||||||
time_t t = mktime(tm_utc);
|
|
||||||
if (!old_tz_copy.empty()) {
|
|
||||||
setenv("TZ", old_tz_copy.c_str(), 1);
|
|
||||||
} else {
|
|
||||||
unsetenv("TZ");
|
|
||||||
}
|
|
||||||
tzset();
|
|
||||||
return t;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool read_registers(uint8_t start_reg, uint8_t *out, size_t len) {
|
|
||||||
if (!out || len == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Wire.beginTransmission(DS3231_ADDR);
|
|
||||||
Wire.write(start_reg);
|
|
||||||
if (Wire.endTransmission(false) != 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
size_t read = Wire.requestFrom(DS3231_ADDR, static_cast<uint8_t>(len));
|
|
||||||
if (read != len) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (size_t i = 0; i < len; ++i) {
|
|
||||||
out[i] = Wire.read();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool write_registers(uint8_t start_reg, const uint8_t *data, size_t len) {
|
|
||||||
if (!data || len == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Wire.beginTransmission(DS3231_ADDR);
|
|
||||||
Wire.write(start_reg);
|
|
||||||
for (size_t i = 0; i < len; ++i) {
|
|
||||||
Wire.write(data[i]);
|
|
||||||
}
|
|
||||||
return Wire.endTransmission() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rtc_ds3231_init() {
|
|
||||||
Wire.begin(PIN_OLED_SDA, PIN_OLED_SCL);
|
|
||||||
Wire.setClock(100000);
|
|
||||||
return rtc_ds3231_is_present();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rtc_ds3231_is_present() {
|
|
||||||
Wire.beginTransmission(DS3231_ADDR);
|
|
||||||
return Wire.endTransmission() == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rtc_ds3231_read_epoch(uint32_t &epoch_utc) {
|
|
||||||
uint8_t regs[7] = {};
|
|
||||||
if (!read_registers(0x00, regs, sizeof(regs))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t sec = bcd_to_dec(regs[0] & 0x7F);
|
|
||||||
uint8_t min = bcd_to_dec(regs[1] & 0x7F);
|
|
||||||
uint8_t hour = bcd_to_dec(regs[2] & 0x3F);
|
|
||||||
uint8_t day = bcd_to_dec(regs[4] & 0x3F);
|
|
||||||
uint8_t month = bcd_to_dec(regs[5] & 0x1F);
|
|
||||||
uint16_t year = 2000 + bcd_to_dec(regs[6]);
|
|
||||||
|
|
||||||
struct tm tm_utc = {};
|
|
||||||
tm_utc.tm_sec = sec;
|
|
||||||
tm_utc.tm_min = min;
|
|
||||||
tm_utc.tm_hour = hour;
|
|
||||||
tm_utc.tm_mday = day;
|
|
||||||
tm_utc.tm_mon = month - 1;
|
|
||||||
tm_utc.tm_year = year - 1900;
|
|
||||||
tm_utc.tm_isdst = 0;
|
|
||||||
|
|
||||||
time_t t = timegm_fallback(&tm_utc);
|
|
||||||
if (t <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
epoch_utc = static_cast<uint32_t>(t);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool rtc_ds3231_set_epoch(uint32_t epoch_utc) {
|
|
||||||
time_t t = static_cast<time_t>(epoch_utc);
|
|
||||||
struct tm tm_utc = {};
|
|
||||||
if (!gmtime_r(&t, &tm_utc)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t regs[7] = {};
|
|
||||||
regs[0] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_sec));
|
|
||||||
regs[1] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_min));
|
|
||||||
regs[2] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_hour));
|
|
||||||
regs[3] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_wday + 1));
|
|
||||||
regs[4] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_mday));
|
|
||||||
regs[5] = dec_to_bcd(static_cast<uint8_t>(tm_utc.tm_mon + 1));
|
|
||||||
regs[6] = dec_to_bcd(static_cast<uint8_t>((tm_utc.tm_year + 1900) - 2000));
|
|
||||||
|
|
||||||
return write_registers(0x00, regs, sizeof(regs));
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,6 @@ static const char *fault_text(FaultType fault) {
|
|||||||
return "decode";
|
return "decode";
|
||||||
case FaultType::LoraTx:
|
case FaultType::LoraTx:
|
||||||
return "loratx";
|
return "loratx";
|
||||||
case FaultType::TimeSync:
|
|
||||||
return "timesync";
|
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
|
|
||||||
#ifdef ENABLE_TEST_MODE
|
#ifdef ENABLE_TEST_MODE
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "compressor.h"
|
|
||||||
#include "lora_transport.h"
|
#include "lora_transport.h"
|
||||||
#include "json_codec.h"
|
|
||||||
#include "time_manager.h"
|
#include "time_manager.h"
|
||||||
#include "display_ui.h"
|
#include "display_ui.h"
|
||||||
#include "mqtt_client.h"
|
#include "mqtt_client.h"
|
||||||
@@ -14,16 +12,9 @@
|
|||||||
|
|
||||||
static uint32_t g_last_test_ms = 0;
|
static uint32_t g_last_test_ms = 0;
|
||||||
static uint16_t g_test_code_counter = 1000;
|
static uint16_t g_test_code_counter = 1000;
|
||||||
static uint32_t g_last_timesync_ms = 0;
|
|
||||||
static constexpr uint32_t TEST_SEND_INTERVAL_MS = 30000;
|
static constexpr uint32_t TEST_SEND_INTERVAL_MS = 30000;
|
||||||
static constexpr uint32_t TEST_TIMESYNC_OFFSET_MS = 15000;
|
|
||||||
|
|
||||||
void test_sender_loop(uint16_t short_id, const char *device_id) {
|
void test_sender_loop(uint16_t short_id, const char *device_id) {
|
||||||
LoraPacket rx = {};
|
|
||||||
if (lora_receive(rx, 0) && rx.payload_type == PayloadType::TimeSync) {
|
|
||||||
time_handle_timesync_payload(rx.payload, rx.payload_len);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (millis() - g_last_test_ms < TEST_SEND_INTERVAL_MS) {
|
if (millis() - g_last_test_ms < TEST_SEND_INTERVAL_MS) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -60,48 +51,34 @@ void test_sender_loop(uint16_t short_id, const char *device_id) {
|
|||||||
data.ts_utc = ts;
|
data.ts_utc = ts;
|
||||||
display_set_last_meter(data);
|
display_set_last_meter(data);
|
||||||
|
|
||||||
uint8_t compressed[LORA_MAX_PAYLOAD];
|
if (json.length() > LORA_MAX_PAYLOAD) {
|
||||||
size_t compressed_len = 0;
|
|
||||||
if (!compressBuffer(reinterpret_cast<const uint8_t *>(json.c_str()), json.length(), compressed, sizeof(compressed), compressed_len)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LoraPacket pkt = {};
|
LoraPacket pkt = {};
|
||||||
pkt.protocol_version = PROTOCOL_VERSION;
|
pkt.msg_kind = LoraMsgKind::BatchUp;
|
||||||
pkt.role = DeviceRole::Sender;
|
|
||||||
pkt.device_id_short = short_id;
|
pkt.device_id_short = short_id;
|
||||||
pkt.payload_type = PayloadType::TestCode;
|
pkt.payload_len = json.length();
|
||||||
pkt.payload_len = compressed_len;
|
memcpy(pkt.payload, json.c_str(), pkt.payload_len);
|
||||||
memcpy(pkt.payload, compressed, compressed_len);
|
|
||||||
lora_send(pkt);
|
lora_send(pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
void test_receiver_loop(SenderStatus *statuses, uint8_t count, uint16_t self_short_id) {
|
void test_receiver_loop(SenderStatus *statuses, uint8_t count, uint16_t self_short_id) {
|
||||||
if (g_last_timesync_ms == 0) {
|
(void)self_short_id;
|
||||||
g_last_timesync_ms = millis() - (TIME_SYNC_INTERVAL_SEC * 1000UL - TEST_TIMESYNC_OFFSET_MS);
|
|
||||||
}
|
|
||||||
if (millis() - g_last_timesync_ms > TIME_SYNC_INTERVAL_SEC * 1000UL) {
|
|
||||||
g_last_timesync_ms = millis();
|
|
||||||
time_send_timesync(self_short_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
LoraPacket pkt = {};
|
LoraPacket pkt = {};
|
||||||
if (!lora_receive(pkt, 0)) {
|
if (!lora_receive(pkt, 0)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pkt.payload_type != PayloadType::TestCode) {
|
if (pkt.msg_kind != LoraMsgKind::BatchUp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t decompressed[160];
|
uint8_t decompressed[160];
|
||||||
size_t decompressed_len = 0;
|
if (pkt.payload_len >= sizeof(decompressed)) {
|
||||||
if (!decompressBuffer(pkt.payload, pkt.payload_len, decompressed, sizeof(decompressed) - 1, decompressed_len)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (decompressed_len >= sizeof(decompressed)) {
|
memcpy(decompressed, pkt.payload, pkt.payload_len);
|
||||||
return;
|
decompressed[pkt.payload_len] = '\0';
|
||||||
}
|
|
||||||
decompressed[decompressed_len] = '\0';
|
|
||||||
|
|
||||||
StaticJsonDocument<128> doc;
|
StaticJsonDocument<128> doc;
|
||||||
if (deserializeJson(doc, reinterpret_cast<const char *>(decompressed)) != DeserializationError::Ok) {
|
if (deserializeJson(doc, reinterpret_cast<const char *>(decompressed)) != DeserializationError::Ok) {
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
#include "time_manager.h"
|
#include "time_manager.h"
|
||||||
#include "compressor.h"
|
|
||||||
#include "config.h"
|
|
||||||
#include "rtc_ds3231.h"
|
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
static bool g_time_synced = false;
|
static bool g_time_synced = false;
|
||||||
static bool g_tz_set = false;
|
static bool g_tz_set = false;
|
||||||
static bool g_rtc_present = false;
|
|
||||||
static uint32_t g_last_sync_utc = 0;
|
static uint32_t g_last_sync_utc = 0;
|
||||||
static constexpr uint32_t kMinValidEpoch = 1672531200UL; // 2023-01-01
|
|
||||||
static constexpr uint32_t kMaxValidEpoch = 4102444800UL; // 2100-01-01
|
|
||||||
|
|
||||||
static void note_last_sync(uint32_t epoch) {
|
static void note_last_sync(uint32_t epoch) {
|
||||||
if (epoch == 0) {
|
if (epoch == 0) {
|
||||||
@@ -57,63 +51,6 @@ void time_set_utc(uint32_t epoch) {
|
|||||||
settimeofday(&tv, nullptr);
|
settimeofday(&tv, nullptr);
|
||||||
g_time_synced = true;
|
g_time_synced = true;
|
||||||
note_last_sync(epoch);
|
note_last_sync(epoch);
|
||||||
|
|
||||||
if (g_rtc_present) {
|
|
||||||
rtc_ds3231_set_epoch(epoch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool time_send_timesync(uint16_t device_id_short) {
|
|
||||||
uint32_t epoch = time_get_utc();
|
|
||||||
if (epoch == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
char payload_str[32];
|
|
||||||
snprintf(payload_str, sizeof(payload_str), "T:%lu", static_cast<unsigned long>(epoch));
|
|
||||||
|
|
||||||
uint8_t compressed[LORA_MAX_PAYLOAD];
|
|
||||||
size_t compressed_len = 0;
|
|
||||||
if (!compressBuffer(reinterpret_cast<const uint8_t *>(payload_str), strlen(payload_str), compressed, sizeof(compressed), compressed_len)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LoraPacket pkt = {};
|
|
||||||
pkt.protocol_version = PROTOCOL_VERSION;
|
|
||||||
pkt.role = DeviceRole::Receiver;
|
|
||||||
pkt.device_id_short = device_id_short;
|
|
||||||
pkt.payload_type = PayloadType::TimeSync;
|
|
||||||
pkt.payload_len = compressed_len;
|
|
||||||
memcpy(pkt.payload, compressed, compressed_len);
|
|
||||||
bool ok = lora_send(pkt);
|
|
||||||
if (ok) {
|
|
||||||
lora_receive_continuous();
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool time_handle_timesync_payload(const uint8_t *payload, size_t len) {
|
|
||||||
uint8_t decompressed[64];
|
|
||||||
size_t decompressed_len = 0;
|
|
||||||
if (!decompressBuffer(payload, len, decompressed, sizeof(decompressed), decompressed_len)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (decompressed_len >= sizeof(decompressed)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
decompressed[decompressed_len] = '\0';
|
|
||||||
|
|
||||||
if (decompressed_len < 3 || decompressed[0] != 'T' || decompressed[1] != ':') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t epoch = static_cast<uint32_t>(strtoul(reinterpret_cast<const char *>(decompressed + 2), nullptr, 10));
|
|
||||||
if (epoch == 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
time_set_utc(epoch);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void time_get_local_hhmm(char *out, size_t out_len) {
|
void time_get_local_hhmm(char *out, size_t out_len) {
|
||||||
@@ -127,43 +64,6 @@ void time_get_local_hhmm(char *out, size_t out_len) {
|
|||||||
snprintf(out, out_len, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
snprintf(out, out_len, "%02d:%02d", timeinfo.tm_hour, timeinfo.tm_min);
|
||||||
}
|
}
|
||||||
|
|
||||||
void time_rtc_init() {
|
|
||||||
if (!ENABLE_DS3231) {
|
|
||||||
g_rtc_present = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
g_rtc_present = rtc_ds3231_init();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool time_try_load_from_rtc() {
|
|
||||||
if (!g_rtc_present) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (time_is_synced()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
uint32_t epoch = 0;
|
|
||||||
if (!rtc_ds3231_read_epoch(epoch) || epoch == 0) {
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
Serial.println("rtc: read failed");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bool valid = epoch >= kMinValidEpoch && epoch <= kMaxValidEpoch;
|
|
||||||
if (SERIAL_DEBUG_MODE) {
|
|
||||||
Serial.printf("rtc: epoch=%lu %s\n", static_cast<unsigned long>(epoch), valid ? "accepted" : "rejected");
|
|
||||||
}
|
|
||||||
if (!valid) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
time_set_utc(epoch);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool time_rtc_present() {
|
|
||||||
return g_rtc_present;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t time_get_last_sync_utc() {
|
uint32_t time_get_last_sync_utc() {
|
||||||
return g_last_sync_utc;
|
return g_last_sync_utc;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,6 @@ static const char *fault_text(FaultType fault) {
|
|||||||
return "decode";
|
return "decode";
|
||||||
case FaultType::LoraTx:
|
case FaultType::LoraTx:
|
||||||
return "loratx";
|
return "loratx";
|
||||||
case FaultType::TimeSync:
|
|
||||||
return "timesync";
|
|
||||||
default:
|
default:
|
||||||
return "none";
|
return "none";
|
||||||
}
|
}
|
||||||
@@ -651,8 +649,8 @@ static void handle_manual() {
|
|||||||
html += "<li>Battery: percent with voltage in V.</li>";
|
html += "<li>Battery: percent with voltage in V.</li>";
|
||||||
html += "<li>RSSI/SNR: LoRa link quality from last packet.</li>";
|
html += "<li>RSSI/SNR: LoRa link quality from last packet.</li>";
|
||||||
html += "<li>err_tx: sender-side LoRa TX error counter.</li>";
|
html += "<li>err_tx: sender-side LoRa TX error counter.</li>";
|
||||||
html += "<li>err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx, 4=TimeSync).</li>";
|
html += "<li>err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx).</li>";
|
||||||
html += "<li>rx_reject: last RX reject reason (0=None, 1=crc_fail, 2=bad_protocol_version, 3=wrong_role, 4=wrong_payload_type, 5=length_mismatch, 6=device_id_mismatch, 7=batch_id_mismatch).</li>";
|
html += "<li>rx_reject: last RX reject reason (0=None, 1=crc_fail, 2=invalid_msg_kind, 3=length_mismatch, 4=device_id_mismatch, 5=batch_id_mismatch).</li>";
|
||||||
html += "<li>faults m/d/tx: receiver-side counters (meter read fails, decode fails, LoRa TX fails).</li>";
|
html += "<li>faults m/d/tx: receiver-side counters (meter read fails, decode fails, LoRa TX fails).</li>";
|
||||||
html += "<li>faults last: last receiver-side error code (same mapping as err_last).</li>";
|
html += "<li>faults last: last receiver-side error code (same mapping as err_last).</li>";
|
||||||
html += "</ul>";
|
html += "</ul>";
|
||||||
|
|||||||
Reference in New Issue
Block a user