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 WifiMqttConfig g_cfg;
|
||||
static uint32_t g_boot_ms = 0;
|
||||
static bool g_debug_forced_reboot_done = false;
|
||||
static FaultCounters g_sender_faults = {};
|
||||
static FaultCounters g_receiver_faults = {};
|
||||
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_valid = 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 finish_inflight_batch();
|
||||
|
||||
static void serial_debug_printf(const char *fmt, ...) {
|
||||
if (!SERIAL_DEBUG_MODE) {
|
||||
@@ -391,6 +401,23 @@ static bool float_to_i16_w(float value, int16_t &out) {
|
||||
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) {
|
||||
if (isnan(value) || value <= 0.0f) {
|
||||
return 0;
|
||||
@@ -536,6 +563,7 @@ static bool prepare_inflight_from_queue() {
|
||||
}
|
||||
|
||||
static bool send_inflight_batch(uint32_t ts_for_display) {
|
||||
g_last_tx_build_error = TxBuildError::None;
|
||||
if (!g_inflight_active) {
|
||||
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_last = static_cast<uint8_t>(g_sender_last_error);
|
||||
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) {
|
||||
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]) ||
|
||||
!float_to_i16_w(g_inflight_samples[i].phase_power_w[2], input.p3_w[i])) {
|
||||
return false;
|
||||
if (i > 0 && input.energy_wh[i] < input.energy_wh[i - 1]) {
|
||||
input.energy_wh[i] = input.energy_wh[i - 1];
|
||||
if (energy_regressions < 255) {
|
||||
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];
|
||||
size_t encoded_len = 0;
|
||||
uint32_t encode_start = millis();
|
||||
if (!encode_batch(input, encoded, sizeof(encoded), &encoded_len)) {
|
||||
g_last_tx_build_error = TxBuildError::Encode;
|
||||
return false;
|
||||
}
|
||||
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_batch_ack_pending = true;
|
||||
} 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_count = 0;
|
||||
g_inflight_batch_id = 0;
|
||||
@@ -1193,6 +1253,16 @@ receiver_loop_done:
|
||||
}
|
||||
|
||||
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
|
||||
if (g_role == DeviceRole::Sender) {
|
||||
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_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) {
|
||||
if (isnan(value)) {
|
||||
return 0;
|
||||
@@ -374,6 +400,11 @@ static String render_sender_block(const SenderStatus &status) {
|
||||
if (!status.has_data) {
|
||||
s += "No data";
|
||||
} 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 += "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])) + " / " +
|
||||
|
||||
Reference in New Issue
Block a user