Add web last-update timestamp and debug auto-reboot
This commit is contained in:
78
src/main.cpp
78
src/main.cpp
@@ -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);
|
||||||
|
|||||||
@@ -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])) + " / " +
|
||||||
|
|||||||
Reference in New Issue
Block a user