Add web last-update timestamp and debug auto-reboot

This commit is contained in:
2026-02-13 01:51:31 +01:00
parent 3a1de36b75
commit 9d5f5ed513
2 changed files with 105 additions and 4 deletions

View File

@@ -27,6 +27,7 @@ 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_boot_ms = 0; static uint32_t g_boot_ms = 0;
static bool g_debug_forced_reboot_done = false;
static FaultCounters g_sender_faults = {}; static FaultCounters g_sender_faults = {};
static FaultCounters g_receiver_faults = {}; static FaultCounters g_receiver_faults = {};
static FaultCounters g_receiver_faults_published = {}; static FaultCounters g_receiver_faults_published = {};
@@ -105,8 +106,17 @@ static uint32_t g_last_sync_request_ms = 0;
static uint32_t g_build_attempts = 0; static uint32_t g_build_attempts = 0;
static uint32_t g_build_valid = 0; static uint32_t g_build_valid = 0;
static uint32_t g_build_invalid = 0; static uint32_t g_build_invalid = 0;
static constexpr uint32_t DEBUG_FORCED_REBOOT_INTERVAL_MS = 3UL * 60UL * 60UL * 1000UL;
enum class TxBuildError : uint8_t {
None = 0,
Encode = 1
};
static TxBuildError g_last_tx_build_error = TxBuildError::None;
static void watchdog_kick(); static void watchdog_kick();
static void finish_inflight_batch();
static void serial_debug_printf(const char *fmt, ...) { static void serial_debug_printf(const char *fmt, ...) {
if (!SERIAL_DEBUG_MODE) { if (!SERIAL_DEBUG_MODE) {
@@ -391,6 +401,23 @@ static bool float_to_i16_w(float value, int16_t &out) {
return true; return true;
} }
static int16_t float_to_i16_w_clamped(float value, bool &clamped) {
clamped = false;
if (isnan(value)) {
return 0;
}
long rounded = lroundf(value);
if (rounded < INT16_MIN) {
clamped = true;
return INT16_MIN;
}
if (rounded > INT16_MAX) {
clamped = true;
return INT16_MAX;
}
return static_cast<int16_t>(rounded);
}
static uint16_t battery_mv_from_voltage(float value) { static uint16_t battery_mv_from_voltage(float value) {
if (isnan(value) || value <= 0.0f) { if (isnan(value) || value <= 0.0f) {
return 0; return 0;
@@ -536,6 +563,7 @@ 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) {
g_last_tx_build_error = TxBuildError::None;
if (!g_inflight_active) { if (!g_inflight_active) {
return false; return false;
} }
@@ -554,19 +582,44 @@ static bool send_inflight_batch(uint32_t ts_for_display) {
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);
uint8_t energy_regressions = 0;
uint8_t phase_clamps = 0;
for (uint8_t i = 0; i < input.n; ++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 (i > 0 && input.energy_wh[i] < input.energy_wh[i - 1]) {
!float_to_i16_w(g_inflight_samples[i].phase_power_w[1], input.p2_w[i]) || input.energy_wh[i] = input.energy_wh[i - 1];
!float_to_i16_w(g_inflight_samples[i].phase_power_w[2], input.p3_w[i])) { if (energy_regressions < 255) {
return false; energy_regressions++;
}
} }
bool c1 = false;
bool c2 = false;
bool c3 = false;
input.p1_w[i] = float_to_i16_w_clamped(g_inflight_samples[i].phase_power_w[0], c1);
input.p2_w[i] = float_to_i16_w_clamped(g_inflight_samples[i].phase_power_w[1], c2);
input.p3_w[i] = float_to_i16_w_clamped(g_inflight_samples[i].phase_power_w[2], c3);
if (c1 && phase_clamps < 255) {
phase_clamps++;
}
if (c2 && phase_clamps < 255) {
phase_clamps++;
}
if (c3 && phase_clamps < 255) {
phase_clamps++;
}
}
if (SERIAL_DEBUG_MODE && (energy_regressions > 0 || phase_clamps > 0)) {
serial_debug_printf("tx: sanitize batch_id=%u energy_regress=%u phase_clamps=%u",
g_inflight_batch_id,
static_cast<unsigned>(energy_regressions),
static_cast<unsigned>(phase_clamps));
} }
static uint8_t encoded[BATCH_MAX_COMPRESSED]; static uint8_t encoded[BATCH_MAX_COMPRESSED];
size_t encoded_len = 0; size_t encoded_len = 0;
uint32_t encode_start = millis(); uint32_t encode_start = millis();
if (!encode_batch(input, encoded, sizeof(encoded), &encoded_len)) { if (!encode_batch(input, encoded, sizeof(encoded), &encoded_len)) {
g_last_tx_build_error = TxBuildError::Encode;
return false; return false;
} }
uint32_t encode_ms = millis() - encode_start; uint32_t encode_ms = millis() - encode_start;
@@ -612,6 +665,13 @@ static bool send_meter_batch(uint32_t ts_for_display) {
g_last_sent_batch_id = g_inflight_batch_id; g_last_sent_batch_id = g_inflight_batch_id;
g_batch_ack_pending = true; g_batch_ack_pending = true;
} else { } else {
if (g_last_tx_build_error == TxBuildError::Encode) {
serial_debug_printf("tx: encode failed batch_id=%u dropped", g_inflight_batch_id);
note_fault(g_sender_faults, g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms, FaultType::Decode);
display_set_last_error(g_sender_last_error, g_sender_last_error_utc, g_sender_last_error_ms);
finish_inflight_batch();
return false;
}
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;
@@ -1193,6 +1253,16 @@ receiver_loop_done:
} }
void loop() { void loop() {
if (SERIAL_DEBUG_MODE && !g_debug_forced_reboot_done &&
(millis() - g_boot_ms >= DEBUG_FORCED_REBOOT_INTERVAL_MS)) {
g_debug_forced_reboot_done = true;
serial_debug_printf("debug: force reboot after %lu ms uptime",
static_cast<unsigned long>(millis() - g_boot_ms));
#ifdef ARDUINO_ARCH_ESP32
delay(50);
esp_restart();
#endif
}
#ifdef ENABLE_TEST_MODE #ifdef ENABLE_TEST_MODE
if (g_role == DeviceRole::Sender) { if (g_role == DeviceRole::Sender) {
test_sender_loop(g_short_id, g_device_id); test_sender_loop(g_short_id, g_device_id);

View File

@@ -57,6 +57,32 @@ static HistoryJob g_history = {};
static constexpr size_t SD_LIST_MAX_FILES = 200; static constexpr size_t SD_LIST_MAX_FILES = 200;
static constexpr size_t SD_DOWNLOAD_MAX_PATH = 160; static constexpr size_t SD_DOWNLOAD_MAX_PATH = 160;
static String format_utc_timestamp(uint32_t ts_utc) {
if (ts_utc == 0) {
return "n/a";
}
time_t t = static_cast<time_t>(ts_utc);
struct tm tm_utc;
gmtime_r(&t, &tm_utc);
char buf[32];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d UTC",
tm_utc.tm_year + 1900,
tm_utc.tm_mon + 1,
tm_utc.tm_mday,
tm_utc.tm_hour,
tm_utc.tm_min,
tm_utc.tm_sec);
return String(buf);
}
static uint32_t timestamp_age_seconds(uint32_t ts_utc) {
uint32_t now_utc = time_get_utc();
if (ts_utc == 0 || now_utc < ts_utc) {
return 0;
}
return now_utc - ts_utc;
}
static int32_t round_power_w(float value) { static int32_t round_power_w(float value) {
if (isnan(value)) { if (isnan(value)) {
return 0; return 0;
@@ -374,6 +400,11 @@ static String render_sender_block(const SenderStatus &status) {
if (!status.has_data) { if (!status.has_data) {
s += "No data"; s += "No data";
} else { } else {
s += "Last update: " + format_utc_timestamp(status.last_update_ts_utc);
if (time_is_synced()) {
s += " (" + String(timestamp_age_seconds(status.last_update_ts_utc)) + "s ago)";
}
s += "<br>";
s += "Energy: " + String(status.last_data.energy_total_kwh, 2) + " kWh<br>"; s += "Energy: " + String(status.last_data.energy_total_kwh, 2) + " kWh<br>";
s += "Power: " + String(round_power_w(status.last_data.total_power_w)) + " W<br>"; s += "Power: " + String(round_power_w(status.last_data.total_power_w)) + " W<br>";
s += "P1/P2/P3: " + String(round_power_w(status.last_data.phase_power_w[0])) + " / " + s += "P1/P2/P3: " + String(round_power_w(status.last_data.phase_power_w[0])) + " / " +