diff --git a/include/compressor.h b/include/compressor.h deleted file mode 100644 index 6d08714..0000000 --- a/include/compressor.h +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include - -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); diff --git a/include/config.h b/include/config.h index 00c9d14..3fff7ab 100644 --- a/include/config.h +++ b/include/config.h @@ -7,21 +7,11 @@ enum class DeviceRole : uint8_t { Receiver = 1 }; -enum class PayloadType : uint8_t { - MeterData = 0, - TestCode = 1, - TimeSync = 2, - MeterBatch = 3, - Ack = 4 -}; - enum class BatchRetryPolicy : uint8_t { Keep = 0, Drop = 1 }; -constexpr uint8_t PROTOCOL_VERSION = 1; - // Pin definitions constexpr uint8_t PIN_LORA_SCK = 5; constexpr uint8_t PIN_LORA_MISO = 19; @@ -57,17 +47,7 @@ constexpr uint8_t LORA_PREAMBLE_LEN = 8; // Timing constexpr uint32_t SENDER_WAKE_INTERVAL_SEC = 30; -constexpr uint32_t TIME_SYNC_INTERVAL_SEC = 60; -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 SYNC_REQUEST_INTERVAL_MS = 15000; constexpr uint32_t OLED_PAGE_INTERVAL_MS = 4000; constexpr uint32_t OLED_AUTO_OFF_MS = 10UL * 60UL * 1000UL; 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 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] = { 0xF19C //433mhz sender //0x7EB4 //868mhz sender diff --git a/include/data_model.h b/include/data_model.h index cb30367..670b799 100644 --- a/include/data_model.h +++ b/include/data_model.h @@ -6,19 +6,16 @@ enum class FaultType : uint8_t { None = 0, MeterRead = 1, Decode = 2, - LoraTx = 3, - TimeSync = 4 + LoraTx = 3 }; enum class RxRejectReason : uint8_t { None = 0, CrcFail = 1, - BadProtocol = 2, - WrongRole = 3, - WrongPayloadType = 4, - LengthMismatch = 5, - DeviceIdMismatch = 6, - BatchIdMismatch = 7 + InvalidMsgKind = 2, + LengthMismatch = 3, + DeviceIdMismatch = 4, + BatchIdMismatch = 5 }; struct FaultCounters { diff --git a/include/json_codec.h b/include/json_codec.h index 86a0fd7..fbfa3b7 100644 --- a/include/json_codec.h +++ b/include/json_codec.h @@ -4,7 +4,3 @@ #include "data_model.h" 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); diff --git a/include/lora_transport.h b/include/lora_transport.h index 97e3b75..9f3e42e 100644 --- a/include/lora_transport.h +++ b/include/lora_transport.h @@ -6,11 +6,14 @@ constexpr size_t LORA_MAX_PAYLOAD = 230; +enum class LoraMsgKind : uint8_t { + BatchUp = 0, + AckDown = 1 +}; + struct LoraPacket { - uint8_t protocol_version; - DeviceRole role; + LoraMsgKind msg_kind; uint16_t device_id_short; - PayloadType payload_type; uint8_t payload[LORA_MAX_PAYLOAD]; size_t payload_len; int16_t rssi_dbm; diff --git a/include/rtc_ds3231.h b/include/rtc_ds3231.h deleted file mode 100644 index 2423036..0000000 --- a/include/rtc_ds3231.h +++ /dev/null @@ -1,8 +0,0 @@ -#pragma once - -#include - -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); diff --git a/include/time_manager.h b/include/time_manager.h index 26892f6..5a636d1 100644 --- a/include/time_manager.h +++ b/include/time_manager.h @@ -1,17 +1,11 @@ #pragma once #include -#include "lora_transport.h" void time_receiver_init(const char *ntp_server_1, const char *ntp_server_2); uint32_t time_get_utc(); bool time_is_synced(); 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_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_age_sec(); diff --git a/src/compressor.cpp b/src/compressor.cpp deleted file mode 100644 index 72b0f57..0000000 --- a/src/compressor.cpp +++ /dev/null @@ -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(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; -} diff --git a/src/data_model.cpp b/src/data_model.cpp index fbdc0e0..0ecdbe9 100644 --- a/src/data_model.cpp +++ b/src/data_model.cpp @@ -13,12 +13,8 @@ const char *rx_reject_reason_text(RxRejectReason reason) { switch (reason) { case RxRejectReason::CrcFail: return "crc_fail"; - case RxRejectReason::BadProtocol: - return "bad_protocol_version"; - case RxRejectReason::WrongRole: - return "wrong_role"; - case RxRejectReason::WrongPayloadType: - return "wrong_payload_type"; + case RxRejectReason::InvalidMsgKind: + return "invalid_msg_kind"; case RxRejectReason::LengthMismatch: return "length_mismatch"; case RxRejectReason::DeviceIdMismatch: diff --git a/src/display_ui.cpp b/src/display_ui.cpp index 7fcc63e..1f0196a 100644 --- a/src/display_ui.cpp +++ b/src/display_ui.cpp @@ -188,8 +188,6 @@ static bool render_last_error_line(uint8_t y) { label = "decode"; } else if (g_last_error == FaultType::LoraTx) { label = "lora"; - } else if (g_last_error == FaultType::TimeSync) { - label = "timesync"; } display.setCursor(0, y); display.printf("Err: %s %lus", label, static_cast(age_seconds(g_last_error_ts, g_last_error_ms))); diff --git a/src/json_codec.cpp b/src/json_codec.cpp index 8f4343c..e188653 100644 --- a/src/json_codec.cpp +++ b/src/json_codec.cpp @@ -2,8 +2,6 @@ #include #include #include -#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(value) * 1000.0; - if (wh < 0.0) { - wh = 0.0; - } - if (wh > static_cast(UINT32_MAX)) { - wh = static_cast(UINT32_MAX); - } - return static_cast(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(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(doc["err_last"] | 0); - data.rx_reject_reason = static_cast(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(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(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(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(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 p_w = doc["p_w"].as(); - JsonArray p1_w = doc["p1_w"].as(); - JsonArray p2_w = doc["p2_w"].as(); - JsonArray p3_w = doc["p3_w"].as(); - - 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(count - 1); - data.ts_utc = t_first + static_cast(idx) * step; - } else { - data.ts_utc = t0 + static_cast(idx) * dt_s; - } - data.energy_total_kwh = static_cast((energy[idx] | 0)) / 1000.0f; - data.total_power_w = static_cast(p_w[idx] | 0); - data.phase_power_w[0] = static_cast(p1_w[idx] | 0); - data.phase_power_w[1] = static_cast(p2_w[idx] | 0); - data.phase_power_w[2] = static_cast(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(strtoul(suffix, nullptr, 16)); - } - } - - out_count = count; - return count > 0; - } - - return false; -} diff --git a/src/lora_transport.cpp b/src/lora_transport.cpp index 80443be..db5a7e6 100644 --- a/src/lora_transport.cpp +++ b/src/lora_transport.cpp @@ -65,13 +65,11 @@ bool lora_send(const LoraPacket &pkt) { if (SERIAL_DEBUG_MODE) { t1 = millis(); } - uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2]; + uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2]; size_t idx = 0; - buffer[idx++] = pkt.protocol_version; - buffer[idx++] = static_cast(pkt.role); + buffer[idx++] = static_cast(pkt.msg_kind); buffer[idx++] = static_cast(pkt.device_id_short >> 8); buffer[idx++] = static_cast(pkt.device_id_short & 0xFF); - buffer[idx++] = static_cast(pkt.payload_type); if (pkt.payload_len > LORA_MAX_PAYLOAD) { return false; @@ -111,7 +109,7 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) { while (true) { int packet_size = LoRa.parsePacket(); if (packet_size > 0) { - if (packet_size < 7) { + if (packet_size < 5) { while (LoRa.available()) { LoRa.read(); } @@ -119,13 +117,13 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) { return false; } - uint8_t buffer[5 + LORA_MAX_PAYLOAD + 2]; + uint8_t buffer[1 + 2 + LORA_MAX_PAYLOAD + 2]; size_t len = 0; while (LoRa.available() && len < sizeof(buffer)) { buffer[len++] = LoRa.read(); } - if (len < 7) { + if (len < 5) { note_reject(RxRejectReason::LengthMismatch); return false; } @@ -136,21 +134,20 @@ bool lora_receive(LoraPacket &pkt, uint32_t timeout_ms) { note_reject(RxRejectReason::CrcFail); return false; } - if (buffer[0] != PROTOCOL_VERSION) { - note_reject(RxRejectReason::BadProtocol); + uint8_t msg_kind = buffer[0]; + if (msg_kind > static_cast(LoraMsgKind::AckDown)) { + note_reject(RxRejectReason::InvalidMsgKind); return false; } - pkt.protocol_version = buffer[0]; - pkt.role = static_cast(buffer[1]); - pkt.device_id_short = static_cast(buffer[2] << 8) | buffer[3]; - pkt.payload_type = static_cast(buffer[4]); - pkt.payload_len = len - 7; + pkt.msg_kind = static_cast(msg_kind); + pkt.device_id_short = static_cast(buffer[1] << 8) | buffer[2]; + pkt.payload_len = len - 5; if (pkt.payload_len > LORA_MAX_PAYLOAD) { note_reject(RxRejectReason::LengthMismatch); return false; } - memcpy(pkt.payload, &buffer[5], pkt.payload_len); + memcpy(pkt.payload, &buffer[3], pkt.payload_len); pkt.rssi_dbm = static_cast(LoRa.packetRssi()); pkt.snr_db = LoRa.packetSnr(); return true; diff --git a/src/main.cpp b/src/main.cpp index 392235f..820477e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,9 +1,7 @@ #include #include "config.h" #include "data_model.h" -#include "json_codec.h" #include "payload_codec.h" -#include "compressor.h" #include "lora_transport.h" #include "meter_driver.h" #include "power_manager.h" @@ -28,8 +26,6 @@ static char g_device_id[16] = ""; static SenderStatus g_sender_statuses[NUM_SENDERS]; static bool g_ap_mode = false; 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 FaultCounters g_sender_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_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_CHUNK_PAYLOAD = LORA_MAX_PAYLOAD - BATCH_HEADER_SIZE; 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 uint16_t g_inflight_batch_id = 0; 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_sender_last_timesync_check_ms = 0; static uint32_t g_sender_rx_window_ms = 0; static uint32_t g_sender_sleep_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 uint32_t g_sender_rx_reject_log_ms = 0; static MeterData g_last_meter_data = {}; static bool g_last_meter_valid = false; static uint32_t g_last_meter_rx_ms = 0; static uint32_t g_meter_stale_seconds = 0; -static constexpr uint32_t SENDER_TIMESYNC_ACQUIRE_MS = 10UL * 60UL * 1000UL; -static constexpr uint32_t SENDER_TIMESYNC_ACQUIRE_INTERVAL_SEC = 20; -static constexpr uint32_t SENDER_TIMESYNC_ACQUIRE_WINDOW_MS = 3000; +static bool g_time_acquired = false; +static uint32_t g_last_sync_request_ms = 0; static void watchdog_kick(); @@ -129,17 +111,6 @@ static void serial_debug_printf(const char *fmt, ...) { 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] = {}; 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; } -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() { if (g_batch_count == 0) { return false; @@ -228,33 +168,13 @@ static bool batch_queue_drop_oldest() { g_inflight_active = false; g_inflight_count = 0; g_inflight_batch_id = 0; + g_inflight_sync_request = false; } g_batch_tail = (g_batch_tail + 1) % BATCH_QUEUE_DEPTH; g_batch_count--; 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) { if (reason == RxRejectReason::None) { return; @@ -370,6 +290,29 @@ static uint16_t read_u16_le(const uint8_t *src) { return static_cast(src[0]) | (static_cast(src[1]) << 8); } +static void write_u16_be(uint8_t *dst, uint16_t value) { + dst[0] = static_cast((value >> 8) & 0xFF); + dst[1] = static_cast(value & 0xFF); +} + +static uint16_t read_u16_be(const uint8_t *src) { + return static_cast(src[0] << 8) | static_cast(src[1]); +} + +static void write_u32_be(uint8_t *dst, uint32_t value) { + dst[0] = static_cast((value >> 24) & 0xFF); + dst[1] = static_cast((value >> 16) & 0xFF); + dst[2] = static_cast((value >> 8) & 0xFF); + dst[3] = static_cast(value & 0xFF); +} + +static uint32_t read_u32_be(const uint8_t *src) { + return (static_cast(src[0]) << 24) | + (static_cast(src[1]) << 16) | + (static_cast(src[2]) << 8) | + static_cast(src[3]); +} + static uint16_t sender_id_from_short_id(uint16_t short_id) { for (uint8_t i = 0; i < NUM_SENDERS; ++i) { 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 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 timeout_ms = static_cast(chunk_count) * per_chunk_toa_ms + BATCH_RX_MARGIN_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; } uint8_t chunk_count = static_cast((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 timeout_ms = static_cast(chunk_count) * per_chunk_toa_ms + BATCH_RX_MARGIN_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; } LoraPacket pkt = {}; - pkt.protocol_version = PROTOCOL_VERSION; - pkt.role = DeviceRole::Sender; + pkt.msg_kind = LoraMsgKind::BatchUp; pkt.device_id_short = g_short_id; - pkt.payload_type = PayloadType::MeterBatch; pkt.payload_len = chunk_len + BATCH_HEADER_SIZE; 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; } -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 = {}; - ack.protocol_version = PROTOCOL_VERSION; - ack.role = DeviceRole::Receiver; + ack.msg_kind = LoraMsgKind::AckDown; ack.device_id_short = g_short_id; - ack.payload_type = PayloadType::Ack; - ack.payload_len = 6; - write_u16_le(&ack.payload[0], batch_id); - write_u16_le(&ack.payload[2], sender_id); - write_u16_le(&ack.payload[4], g_short_id); + ack.payload_len = 7; + ack.payload[0] = time_valid; + write_u16_be(&ack.payload[1], batch_id); + write_u32_be(&ack.payload[3], epoch); uint8_t repeats = ACK_REPEAT_COUNT == 0 ? 1 : ACK_REPEAT_COUNT; 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(i + 1), - static_cast(repeats), batch_id); - } lora_send(ack); if (i + 1 < repeats && ACK_REPEAT_DELAY_MS > 0) { delay(ACK_REPEAT_DELAY_MS); } } + serial_debug_printf("ack: tx batch_id=%u time_valid=%u epoch=%lu samples=%u", + batch_id, + static_cast(time_valid), + static_cast(epoch), + static_cast(sample_count)); lora_receive_continuous(); } @@ -547,23 +492,25 @@ static bool prepare_inflight_from_queue() { } static bool send_inflight_batch(uint32_t ts_for_display) { - if (!g_inflight_active || g_inflight_count == 0) { + if (!g_inflight_active) { return false; } BatchInput input = {}; input.sender_id = sender_id_from_short_id(g_short_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; input.dt_s = dt_s > 0 ? static_cast(dt_s) : 1; - input.n = g_inflight_count; - input.battery_mV = battery_mv_from_voltage(g_inflight_samples[g_inflight_count - 1].battery_voltage_v); + input.n = g_inflight_sync_request ? 0 : g_inflight_count; + 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(g_sender_faults.meter_read_fail); input.err_d = g_sender_faults.decode_fail > 255 ? 255 : static_cast(g_sender_faults.decode_fail); input.err_tx = g_sender_faults.lora_tx_fail > 255 ? 255 : static_cast(g_sender_faults.lora_tx_fail); input.err_last = static_cast(g_sender_last_error); input.err_rx_reject = static_cast(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); 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]) || @@ -580,7 +527,7 @@ static bool send_inflight_batch(uint32_t ts_for_display) { } uint32_t encode_ms = millis() - encode_start; 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(encoded_len)); if (encode_ms > 200) { serial_debug_printf("tx: encode took %lums", static_cast(encode_ms)); @@ -596,9 +543,17 @@ static bool send_inflight_batch(uint32_t ts_for_display) { } if (ok) { g_last_batch_send_ms = millis(); - serial_debug_printf("tx: sent batch_id=%u len=%u", g_inflight_batch_id, static_cast(encoded_len)); + 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(encoded_len)); + } } else { - serial_debug_printf("tx: send failed batch_id=%u", g_inflight_batch_id); + if (g_inflight_sync_request) { + serial_debug_printf("sync: request tx failed batch_id=%u", g_inflight_batch_id); + } else { + serial_debug_printf("tx: send failed batch_id=%u", g_inflight_batch_id); + } } return ok; } @@ -607,6 +562,7 @@ static bool send_meter_batch(uint32_t ts_for_display) { if (!prepare_inflight_from_queue()) { return false; } + g_inflight_sync_request = false; bool ok = send_inflight_batch(ts_for_display); if (ok) { 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_count = 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; } 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 send_inflight_batch(ts_for_display); @@ -635,6 +615,7 @@ static void finish_inflight_batch() { g_inflight_active = false; g_inflight_count = 0; g_inflight_batch_id = 0; + g_inflight_sync_request = false; g_batch_id++; } @@ -731,8 +712,6 @@ void setup() { lora_init(); display_init(); - time_rtc_init(); - time_try_load_from_rtc(); display_set_self_ids(g_short_id, g_device_id); if (g_role == DeviceRole::Sender) { @@ -741,6 +720,8 @@ void setup() { meter_init(); g_last_sample_ms = millis() - METER_SAMPLE_INTERVAL_MS; g_last_send_ms = millis(); + g_last_sync_request_ms = millis() - SYNC_REQUEST_INTERVAL_MS; + g_time_acquired = false; update_battery_cache(); } else { power_receiver_init(); @@ -794,75 +775,80 @@ static void sender_loop() { g_batch_retry_count); } - const char *frame = nullptr; - size_t frame_len = 0; - if (meter_poll_frame(frame, frame_len)) { - MeterData parsed = {}; - parsed.energy_total_kwh = NAN; - parsed.total_power_w = NAN; - parsed.phase_power_w[0] = NAN; - parsed.phase_power_w[1] = NAN; - parsed.phase_power_w[2] = NAN; - parsed.valid = false; - if (meter_parse_frame(frame, frame_len, parsed)) { - g_last_meter_data = parsed; - g_last_meter_valid = true; - g_last_meter_rx_ms = now_ms; - g_meter_stale_seconds = 0; + if (g_time_acquired) { + const char *frame = nullptr; + size_t frame_len = 0; + if (meter_poll_frame(frame, frame_len)) { + MeterData parsed = {}; + parsed.energy_total_kwh = NAN; + parsed.total_power_w = NAN; + parsed.phase_power_w[0] = NAN; + parsed.phase_power_w[1] = NAN; + parsed.phase_power_w[2] = NAN; + parsed.valid = false; + if (meter_parse_frame(frame, frame_len, parsed)) { + g_last_meter_data = parsed; + g_last_meter_valid = true; + g_last_meter_rx_ms = now_ms; + g_meter_stale_seconds = 0; + } } - } - if (now_ms - g_last_sample_ms >= METER_SAMPLE_INTERVAL_MS) { - g_last_sample_ms = now_ms; - MeterData data = {}; - data.short_id = g_short_id; - strncpy(data.device_id, g_device_id, sizeof(data.device_id)); + if (now_ms - g_last_sample_ms >= METER_SAMPLE_INTERVAL_MS) { + g_last_sample_ms = now_ms; + MeterData data = {}; + data.short_id = g_short_id; + strncpy(data.device_id, g_device_id, sizeof(data.device_id)); - bool meter_ok = g_last_meter_valid; - if (meter_ok) { - data.energy_total_kwh = g_last_meter_data.energy_total_kwh; - data.total_power_w = g_last_meter_data.total_power_w; - data.phase_power_w[0] = g_last_meter_data.phase_power_w[0]; - data.phase_power_w[1] = g_last_meter_data.phase_power_w[1]; - data.phase_power_w[2] = g_last_meter_data.phase_power_w[2]; - uint32_t age_ms = now_ms - g_last_meter_rx_ms; - g_meter_stale_seconds = age_ms >= 1000 ? (age_ms / 1000) : 0; - } else { - g_meter_stale_seconds++; + bool meter_ok = g_last_meter_valid; + if (meter_ok) { + data.energy_total_kwh = g_last_meter_data.energy_total_kwh; + data.total_power_w = g_last_meter_data.total_power_w; + data.phase_power_w[0] = g_last_meter_data.phase_power_w[0]; + data.phase_power_w[1] = g_last_meter_data.phase_power_w[1]; + data.phase_power_w[2] = g_last_meter_data.phase_power_w[2]; + uint32_t age_ms = now_ms - g_last_meter_rx_ms; + g_meter_stale_seconds = age_ms >= 1000 ? (age_ms / 1000) : 0; + } else { + g_meter_stale_seconds++; + } + if (!meter_ok) { + note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead); + display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms); + } + if (g_build_count == 0 && battery_sample_due(now_ms)) { + update_battery_cache(); + } + data.battery_voltage_v = g_last_battery_voltage_v; + data.battery_percent = g_last_battery_percent; + data.rx_reject_reason = static_cast(g_sender_rx_reject_reason); + data.ts_utc = time_get_utc(); + data.valid = meter_ok; + + g_last_sample_ts_utc = data.ts_utc; + g_build_samples[g_build_count++] = data; + if (g_build_count >= METER_BATCH_MAX_SAMPLES) { + batch_queue_enqueue(g_build_samples, g_build_count); + g_build_count = 0; + } + display_set_last_meter(data); + display_set_last_read(meter_ok, data.ts_utc); } - if (!meter_ok) { - note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::MeterRead); - display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms); - } - if (g_build_count == 0 && battery_sample_due(now_ms)) { - update_battery_cache(); - } - data.battery_voltage_v = g_last_battery_voltage_v; - data.battery_percent = g_last_battery_percent; - data.rx_reject_reason = static_cast(g_sender_rx_reject_reason); - uint32_t now_utc = time_get_utc(); - data.ts_utc = now_utc > 0 ? now_utc : millis() / 1000; - data.valid = meter_ok; - - g_last_sample_ts_utc = data.ts_utc; - g_build_samples[g_build_count++] = data; - if (g_build_count >= METER_BATCH_MAX_SAMPLES) { - batch_queue_enqueue(g_build_samples, g_build_count); - g_build_count = 0; + if (!g_batch_ack_pending && now_ms - g_last_send_ms >= METER_SEND_INTERVAL_MS) { + g_last_send_ms = now_ms; + 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(); } - display_set_last_meter(data); - display_set_last_read(meter_ok, data.ts_utc); - } - - if (!g_batch_ack_pending && now_ms - g_last_send_ms >= METER_SEND_INTERVAL_MS) { - g_last_send_ms = now_ms; - send_meter_batch(last_sample_ts()); } if (g_batch_ack_pending) { 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_window_ms = ack_air_ms + 300; if (ack_window_ms < 1200) { @@ -886,74 +872,35 @@ static void sender_loop() { } if (!got_ack) { sender_note_rx_reject(lora_get_last_rx_reject_reason(), "ack"); - } else if (ack_pkt.role != DeviceRole::Receiver) { - sender_note_rx_reject(RxRejectReason::WrongRole, "ack"); - } else if (ack_pkt.payload_type != PayloadType::Ack) { - sender_note_rx_reject(RxRejectReason::WrongPayloadType, "ack"); - } else if (ack_pkt.payload_len < 6) { + } else if (ack_pkt.msg_kind != LoraMsgKind::AckDown) { + sender_note_rx_reject(RxRejectReason::InvalidMsgKind, "ack"); + } else if (ack_pkt.payload_len < 7) { sender_note_rx_reject(RxRejectReason::LengthMismatch, "ack"); } else { - uint16_t ack_id = read_u16_le(ack_pkt.payload); - uint16_t ack_sender = read_u16_le(&ack_pkt.payload[2]); - uint16_t ack_receiver = read_u16_le(&ack_pkt.payload[4]); - if (ack_sender == g_short_id && ack_receiver == ack_pkt.device_id_short && - g_batch_ack_pending && ack_id == g_last_sent_batch_id) { + uint8_t time_valid = ack_pkt.payload[0] & 0x01; + uint16_t ack_id = read_u16_be(&ack_pkt.payload[1]); + uint32_t ack_epoch = read_u32_be(&ack_pkt.payload[3]); + bool set_time = false; + 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; - 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(time_valid), + static_cast(ack_epoch), + set_time ? 1 : 0); finish_inflight_batch(); } else { - if (ack_sender != g_short_id || ack_receiver != ack_pkt.device_id_short) { - sender_note_rx_reject(RxRejectReason::DeviceIdMismatch, "ack"); - } else if (ack_id != g_last_sent_batch_id) { + if (ack_id != g_last_sent_batch_id) { 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(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(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(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) { lora_sleep(); } @@ -974,6 +921,7 @@ static void sender_loop() { g_inflight_active = false; g_inflight_count = 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); 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(); - uint32_t next_sample_due = g_last_sample_ms + METER_SAMPLE_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; + 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; + if (next_send_due < next_due) { + next_due = next_send_due; + } + } if (!g_batch_ack_pending && next_due > now_ms) { watchdog_kick(); if (SERIAL_DEBUG_MODE) { @@ -1002,64 +955,13 @@ static void sender_loop() { static void receiver_loop() { watchdog_kick(); - if (g_last_timesync_ms == 0) { - g_last_timesync_ms = millis() - (TIME_SYNC_INTERVAL_SEC * 1000UL - TIME_SYNC_OFFSET_MS); - } LoraPacket pkt = {}; - if (lora_receive(pkt, 0) && pkt.protocol_version == PROTOCOL_VERSION) { - if (pkt.payload_type == PayloadType::MeterData) { - 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(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) { + if (lora_receive(pkt, 0)) { + if (pkt.msg_kind == LoraMsgKind::BatchUp) { BatchInput batch = {}; bool decode_error = false; uint16_t batch_id = 0; 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; for (uint8_t i = 0; i < NUM_SENDERS; ++i) { if (pkt.device_id_short == EXPECTED_SENDER_IDS[i]) { @@ -1067,87 +969,92 @@ static void receiver_loop() { break; } } - bool duplicate = sender_idx >= 0 && g_last_batch_id_rx[sender_idx] == batch_id; - if (duplicate) { - send_batch_ack(batch_id, pkt.device_id_short); - } else { - g_last_batch_id_rx[sender_idx] = batch_id; - send_batch_ack(batch_id, pkt.device_id_short); - count = batch.n; - if (count == 0 || count > 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); - display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms); - return; - } - uint16_t short_id = pkt.device_id_short; - if (short_id == 0) { - short_id = short_id_from_sender_id(batch.sender_id); - } - uint64_t span = static_cast(batch.dt_s) * static_cast(count - 1); - if (batch.t_last < span) { - 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; - } - uint32_t t_first = batch.t_last - static_cast(span); - float bat_v = batch.battery_mV > 0 ? static_cast(batch.battery_mV) / 1000.0f : NAN; - for (size_t s = 0; s < count; ++s) { - MeterData &data = samples[s]; - data = {}; - data.short_id = short_id; - if (short_id != 0) { - snprintf(data.device_id, sizeof(data.device_id), "dd3-%04X", short_id); - } else { - snprintf(data.device_id, sizeof(data.device_id), "dd3-0000"); - } - data.ts_utc = t_first + static_cast(s) * batch.dt_s; - data.energy_total_kwh = static_cast(batch.energy_wh[s]) / 1000.0f; - data.phase_power_w[0] = static_cast(batch.p1_w[s]); - data.phase_power_w[1] = static_cast(batch.p2_w[s]); - data.phase_power_w[2] = static_cast(batch.p3_w[s]); - 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; - if (!isnan(bat_v)) { - data.battery_percent = battery_percent_from_voltage(bat_v); - } else { - data.battery_percent = 0; - } - data.valid = true; - data.link_valid = true; - data.link_rssi_dbm = pkt.rssi_dbm; - data.link_snr_db = pkt.snr_db; - data.err_meter_read = batch.err_m; - data.err_decode = batch.err_d; - data.err_lora_tx = batch.err_tx; - data.last_error = static_cast(batch.err_last); - data.rx_reject_reason = batch.err_rx_reject; - sd_logger_log_sample(data, (s + 1 == count) && data.last_error != FaultType::None); - } - if (sender_idx >= 0) { - web_server_set_last_batch(static_cast(sender_idx), samples, count); - for (size_t s = 0; s < count; ++s) { - samples[s].short_id = pkt.device_id_short; - mqtt_publish_state(samples[s]); - } - if (count > 0) { - 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].has_data = true; - 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; - receiver_note_timesync_drift(static_cast(sender_idx), samples[count - 1].ts_utc); - 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_ms[sender_idx] = millis(); - if (ENABLE_HA_DISCOVERY && !g_sender_discovery_sent[sender_idx]) { - g_sender_discovery_sent[sender_idx] = mqtt_publish_discovery(samples[count - 1].device_id); - } - publish_faults_if_needed(samples[count - 1].device_id, g_sender_faults_remote[sender_idx], g_sender_faults_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]); - } + bool duplicate = sender_idx >= 0 && g_last_batch_id_rx[sender_idx] == batch_id; + send_batch_ack(batch_id, batch.n); + if (duplicate) { + goto receiver_loop_done; + } + if (sender_idx >= 0) { + g_last_batch_id_rx[sender_idx] = batch_id; + } + if (batch.n == 0) { + 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); + display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms); + goto receiver_loop_done; + } + + size_t count = batch.n; + uint16_t short_id = pkt.device_id_short; + if (short_id == 0) { + short_id = short_id_from_sender_id(batch.sender_id); + } + uint64_t span = static_cast(batch.dt_s) * static_cast(count - 1); + 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); + display_set_last_error(g_receiver_last_error, g_receiver_last_error_utc, g_receiver_last_error_ms); + goto receiver_loop_done; + } + uint32_t t_first = batch.t_last - static_cast(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(batch.battery_mV) / 1000.0f : NAN; + for (size_t s = 0; s < count; ++s) { + MeterData &data = samples[s]; + data = {}; + data.short_id = short_id; + if (short_id != 0) { + snprintf(data.device_id, sizeof(data.device_id), "dd3-%04X", short_id); + } else { + snprintf(data.device_id, sizeof(data.device_id), "dd3-0000"); } + data.ts_utc = t_first + static_cast(s) * batch.dt_s; + data.energy_total_kwh = static_cast(batch.energy_wh[s]) / 1000.0f; + data.phase_power_w[0] = static_cast(batch.p1_w[s]); + data.phase_power_w[1] = static_cast(batch.p2_w[s]); + data.phase_power_w[2] = static_cast(batch.p3_w[s]); + 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_percent = !isnan(bat_v) ? battery_percent_from_voltage(bat_v) : 0; + data.valid = true; + data.link_valid = true; + data.link_rssi_dbm = pkt.rssi_dbm; + data.link_snr_db = pkt.snr_db; + data.err_meter_read = batch.err_m; + data.err_decode = batch.err_d; + data.err_lora_tx = batch.err_tx; + data.last_error = static_cast(batch.err_last); + data.rx_reject_reason = batch.err_rx_reject; + sd_logger_log_sample(data, (s + 1 == count) && data.last_error != FaultType::None); + } + + if (sender_idx >= 0) { + web_server_set_last_batch(static_cast(sender_idx), samples, count); + for (size_t s = 0; s < count; ++s) { + mqtt_publish_state(samples[s]); + } + 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].has_data = true; + 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_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_ms[sender_idx] = millis(); + if (ENABLE_HA_DISCOVERY && !g_sender_discovery_sent[sender_idx]) { + g_sender_discovery_sent[sender_idx] = mqtt_publish_discovery(samples[count - 1].device_id); + } + publish_faults_if_needed(samples[count - 1].device_id, g_sender_faults_remote[sender_idx], g_sender_faults_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]); } } 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); @@ -1156,42 +1063,7 @@ static void receiver_loop() { } } - uint32_t interval_sec = TIME_SYNC_INTERVAL_SEC; - 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"); - } - } - } - +receiver_loop_done: mqtt_loop(); web_server_loop(); if (ENABLE_HA_DISCOVERY && !g_receiver_discovery_sent) { diff --git a/src/mqtt_client.cpp b/src/mqtt_client.cpp index 2c6008f..35e09c4 100644 --- a/src/mqtt_client.cpp +++ b/src/mqtt_client.cpp @@ -18,8 +18,6 @@ static const char *fault_text(FaultType fault) { return "decode"; case FaultType::LoraTx: return "loratx"; - case FaultType::TimeSync: - return "timesync"; default: return "none"; } diff --git a/src/payload_codec.cpp b/src/payload_codec.cpp index 6403057..11d0cb6 100644 --- a/src/payload_codec.cpp +++ b/src/payload_codec.cpp @@ -101,7 +101,7 @@ bool encode_batch(const BatchInput &in, uint8_t *out, size_t out_cap, size_t *ou if (!out || !out_len) { return false; } - if (in.n == 0 || in.n > kMaxSamples) { + if (in.n > kMaxSamples) { return false; } 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_rx_reject; + if (in.n == 0) { + *out_len = pos; + return true; + } + if (!ensure_capacity(4, out_cap, pos)) { 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_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; } + 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) { return false; } diff --git a/src/rtc_ds3231.cpp b/src/rtc_ds3231.cpp deleted file mode 100644 index f4103d7..0000000 --- a/src/rtc_ds3231.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "rtc_ds3231.h" -#include "config.h" -#include -#include -#include - -static constexpr uint8_t DS3231_ADDR = 0x68; - -static uint8_t bcd_to_dec(uint8_t val) { - return static_cast((val >> 4) * 10 + (val & 0x0F)); -} - -static uint8_t dec_to_bcd(uint8_t val) { - return static_cast(((val / 10) << 4) | (val % 10)); -} - -static time_t timegm_fallback(struct tm *tm_utc) { - if (!tm_utc) { - return static_cast(-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(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(t); - return true; -} - -bool rtc_ds3231_set_epoch(uint32_t epoch_utc) { - time_t t = static_cast(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(tm_utc.tm_sec)); - regs[1] = dec_to_bcd(static_cast(tm_utc.tm_min)); - regs[2] = dec_to_bcd(static_cast(tm_utc.tm_hour)); - regs[3] = dec_to_bcd(static_cast(tm_utc.tm_wday + 1)); - regs[4] = dec_to_bcd(static_cast(tm_utc.tm_mday)); - regs[5] = dec_to_bcd(static_cast(tm_utc.tm_mon + 1)); - regs[6] = dec_to_bcd(static_cast((tm_utc.tm_year + 1900) - 2000)); - - return write_registers(0x00, regs, sizeof(regs)); -} diff --git a/src/sd_logger.cpp b/src/sd_logger.cpp index 915744a..085e2b4 100644 --- a/src/sd_logger.cpp +++ b/src/sd_logger.cpp @@ -15,8 +15,6 @@ static const char *fault_text(FaultType fault) { return "decode"; case FaultType::LoraTx: return "loratx"; - case FaultType::TimeSync: - return "timesync"; default: return ""; } diff --git a/src/test_mode.cpp b/src/test_mode.cpp index 1cc89f6..d9ce1c7 100644 --- a/src/test_mode.cpp +++ b/src/test_mode.cpp @@ -2,9 +2,7 @@ #ifdef ENABLE_TEST_MODE #include "config.h" -#include "compressor.h" #include "lora_transport.h" -#include "json_codec.h" #include "time_manager.h" #include "display_ui.h" #include "mqtt_client.h" @@ -14,16 +12,9 @@ static uint32_t g_last_test_ms = 0; 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_TIMESYNC_OFFSET_MS = 15000; 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) { return; } @@ -60,48 +51,34 @@ void test_sender_loop(uint16_t short_id, const char *device_id) { data.ts_utc = ts; display_set_last_meter(data); - uint8_t compressed[LORA_MAX_PAYLOAD]; - size_t compressed_len = 0; - if (!compressBuffer(reinterpret_cast(json.c_str()), json.length(), compressed, sizeof(compressed), compressed_len)) { + if (json.length() > LORA_MAX_PAYLOAD) { return; } LoraPacket pkt = {}; - pkt.protocol_version = PROTOCOL_VERSION; - pkt.role = DeviceRole::Sender; + pkt.msg_kind = LoraMsgKind::BatchUp; pkt.device_id_short = short_id; - pkt.payload_type = PayloadType::TestCode; - pkt.payload_len = compressed_len; - memcpy(pkt.payload, compressed, compressed_len); + pkt.payload_len = json.length(); + memcpy(pkt.payload, json.c_str(), pkt.payload_len); lora_send(pkt); } void test_receiver_loop(SenderStatus *statuses, uint8_t count, uint16_t self_short_id) { - if (g_last_timesync_ms == 0) { - 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); - } - + (void)self_short_id; LoraPacket pkt = {}; if (!lora_receive(pkt, 0)) { return; } - if (pkt.payload_type != PayloadType::TestCode) { + if (pkt.msg_kind != LoraMsgKind::BatchUp) { return; } uint8_t decompressed[160]; - size_t decompressed_len = 0; - if (!decompressBuffer(pkt.payload, pkt.payload_len, decompressed, sizeof(decompressed) - 1, decompressed_len)) { + if (pkt.payload_len >= sizeof(decompressed)) { return; } - if (decompressed_len >= sizeof(decompressed)) { - return; - } - decompressed[decompressed_len] = '\0'; + memcpy(decompressed, pkt.payload, pkt.payload_len); + decompressed[pkt.payload_len] = '\0'; StaticJsonDocument<128> doc; if (deserializeJson(doc, reinterpret_cast(decompressed)) != DeserializationError::Ok) { diff --git a/src/time_manager.cpp b/src/time_manager.cpp index 91bcd6a..72f620b 100644 --- a/src/time_manager.cpp +++ b/src/time_manager.cpp @@ -1,15 +1,9 @@ #include "time_manager.h" -#include "compressor.h" -#include "config.h" -#include "rtc_ds3231.h" #include static bool g_time_synced = false; static bool g_tz_set = false; -static bool g_rtc_present = false; 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) { if (epoch == 0) { @@ -57,63 +51,6 @@ void time_set_utc(uint32_t epoch) { settimeofday(&tv, nullptr); g_time_synced = true; 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(epoch)); - - uint8_t compressed[LORA_MAX_PAYLOAD]; - size_t compressed_len = 0; - if (!compressBuffer(reinterpret_cast(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(strtoul(reinterpret_cast(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) { @@ -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); } -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(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() { return g_last_sync_utc; } diff --git a/src/web_server.cpp b/src/web_server.cpp index dd60774..8e37401 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -83,8 +83,6 @@ static const char *fault_text(FaultType fault) { return "decode"; case FaultType::LoraTx: return "loratx"; - case FaultType::TimeSync: - return "timesync"; default: return "none"; } @@ -651,8 +649,8 @@ static void handle_manual() { html += "
  • Battery: percent with voltage in V.
  • "; html += "
  • RSSI/SNR: LoRa link quality from last packet.
  • "; html += "
  • err_tx: sender-side LoRa TX error counter.
  • "; - html += "
  • err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx, 4=TimeSync).
  • "; - html += "
  • 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).
  • "; + html += "
  • err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx).
  • "; + html += "
  • 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).
  • "; html += "
  • faults m/d/tx: receiver-side counters (meter read fails, decode fails, LoRa TX fails).
  • "; html += "
  • faults last: last receiver-side error code (same mapping as err_last).
  • "; html += "";