From 01f4494f00eb799ae153d577e9b08e66199f02fe Mon Sep 17 00:00:00 2001 From: acidburns Date: Sun, 1 Feb 2026 21:04:34 +0100 Subject: [PATCH] expand web ui with batch table and manual --- include/web_server.h | 2 + src/main.cpp | 9 ++-- src/web_server.cpp | 98 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/include/web_server.h b/include/web_server.h index ee9a980..9acaec5 100644 --- a/include/web_server.h +++ b/include/web_server.h @@ -7,4 +7,6 @@ void web_server_begin_ap(const SenderStatus *statuses, uint8_t count); void web_server_begin_sta(const SenderStatus *statuses, uint8_t count); void web_server_set_config(const WifiMqttConfig &config); +void web_server_set_sender_faults(const FaultCounters *faults, const FaultType *last_errors); +void web_server_set_last_batch(uint8_t sender_index, const MeterData *samples, size_t count); void web_server_loop(); diff --git a/src/main.cpp b/src/main.cpp index b06655c..eb12144 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -595,6 +595,7 @@ void setup() { time_receiver_init(g_cfg.ntp_server_1.c_str(), g_cfg.ntp_server_2.c_str()); mqtt_init(g_cfg, g_device_id); web_server_set_config(g_cfg); + web_server_set_sender_faults(g_sender_faults_remote, g_sender_last_error_remote); web_server_begin_sta(g_sender_statuses, NUM_SENDERS); } else { g_ap_mode = true; @@ -608,6 +609,7 @@ void setup() { g_cfg.ntp_server_2 = "time.nist.gov"; } web_server_set_config(g_cfg); + web_server_set_sender_faults(g_sender_faults_remote, g_sender_last_error_remote); web_server_begin_ap(g_sender_statuses, NUM_SENDERS); } } @@ -787,10 +789,11 @@ static void receiver_loop() { } } 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 if (jsonToMeterBatch(json, samples, METER_BATCH_MAX_SAMPLES, count)) { + if (duplicate) { + send_batch_ack(batch_id, pkt.device_id_short); + } else if (jsonToMeterBatch(json, samples, METER_BATCH_MAX_SAMPLES, count)) { 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].link_valid = true; samples[s].link_rssi_dbm = pkt.rssi_dbm; diff --git a/src/web_server.cpp b/src/web_server.cpp index 4a83854..e56bd6b 100644 --- a/src/web_server.cpp +++ b/src/web_server.cpp @@ -1,12 +1,17 @@ #include "web_server.h" #include #include "wifi_manager.h" +#include "config.h" static WebServer server(80); static const SenderStatus *g_statuses = nullptr; static uint8_t g_status_count = 0; static WifiMqttConfig g_config; static bool g_is_ap = false; +static const FaultCounters *g_sender_faults = nullptr; +static const FaultType *g_sender_last_errors = nullptr; +static MeterData g_last_batch[NUM_SENDERS][METER_BATCH_MAX_SAMPLES]; +static uint8_t g_last_batch_count[NUM_SENDERS] = {}; static String html_header(const String &title) { String h = ""; @@ -19,10 +24,40 @@ static String html_footer() { return ""; } +static String format_faults(uint8_t idx) { + if (!g_sender_faults || !g_sender_last_errors || idx >= g_status_count) { + return ""; + } + String s; + s += " faults m:"; + s += String(g_sender_faults[idx].meter_read_fail); + s += " d:"; + s += String(g_sender_faults[idx].decode_fail); + s += " tx:"; + s += String(g_sender_faults[idx].lora_tx_fail); + s += " last:"; + s += String(static_cast(g_sender_last_errors[idx])); + return s; +} + static String render_sender_block(const SenderStatus &status) { String s; s += "
"; - s += "" + String(status.last_data.device_id) + "
"; + uint8_t idx = 0; + if (g_statuses) { + for (uint8_t i = 0; i < g_status_count; ++i) { + if (&g_statuses[i] == &status) { + idx = i; + break; + } + } + } + s += "" + String(status.last_data.device_id) + ""; + if (status.has_data && status.last_data.link_valid) { + s += " R:" + String(status.last_data.link_rssi_dbm) + " S:" + String(status.last_data.link_snr_db, 1); + } + s += format_faults(idx); + s += "
"; if (!status.has_data) { s += "No data"; } else { @@ -45,6 +80,7 @@ static void handle_root() { } html += "

Configure WiFi/MQTT/NTP

"; + html += "

Manual

"; html += html_footer(); server.send(200, "text/html", html); } @@ -100,6 +136,31 @@ static void handle_sender() { if (device_id.equalsIgnoreCase(g_statuses[i].last_data.device_id)) { String html = html_header("Sender " + device_id); html += render_sender_block(g_statuses[i]); + if (g_last_batch_count[i] > 0) { + html += "

Last batch (" + String(g_last_batch_count[i]) + " samples)

"; + html += ""; + html += ""; + html += ""; + for (uint8_t r = 0; r < g_last_batch_count[i]; ++r) { + const MeterData &d = g_last_batch[i][r]; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + html += ""; + } + html += "
#tse_kwhp_wp1_wp2_wp3_wbat_vbat_pctrssisnrerr_txerr_last
" + String(r) + "" + String(d.ts_utc) + "" + String(d.energy_total_kwh, 3) + "" + String(d.total_power_w, 1) + "" + String(d.phase_power_w[0], 1) + "" + String(d.phase_power_w[1], 1) + "" + String(d.phase_power_w[2], 1) + "" + String(d.battery_voltage_v, 2) + "" + String(d.battery_percent) + "" + String(d.link_rssi_dbm) + "" + String(d.link_snr_db, 1) + "" + String(d.err_lora_tx) + "" + String(static_cast(d.last_error)) + "
"; + } html += html_footer(); server.send(200, "text/html", html); return; @@ -108,16 +169,50 @@ static void handle_sender() { server.send(404, "text/plain", "Not found"); } +static void handle_manual() { + String html = html_header("DD3 Manual"); + html += "
    "; + html += "
  • Energy: total kWh since meter start.
  • "; + html += "
  • Power: total active power in W.
  • "; + html += "
  • P1/P2/P3: phase power in W.
  • "; + html += "
  • bat_v: battery voltage (V), bat_pct: estimated percent.
  • "; + html += "
  • RSSI/SNR: LoRa link quality from last packet.
  • "; + html += "
  • err_tx: LoRa TX error count; err_last: last error code.
  • "; + html += "
  • faults m/d/tx: meter read/decode/tx counters.
  • "; + html += "
"; + html += html_footer(); + server.send(200, "text/html", html); +} + void web_server_set_config(const WifiMqttConfig &config) { g_config = config; } +void web_server_set_sender_faults(const FaultCounters *faults, const FaultType *last_errors) { + g_sender_faults = faults; + g_sender_last_errors = last_errors; +} + +void web_server_set_last_batch(uint8_t sender_index, const MeterData *samples, size_t count) { + if (!samples || sender_index >= NUM_SENDERS) { + return; + } + if (count > METER_BATCH_MAX_SAMPLES) { + count = METER_BATCH_MAX_SAMPLES; + } + g_last_batch_count[sender_index] = static_cast(count); + for (size_t i = 0; i < count; ++i) { + g_last_batch[sender_index][i] = samples[i]; + } +} + void web_server_begin_ap(const SenderStatus *statuses, uint8_t count) { g_statuses = statuses; g_status_count = count; g_is_ap = true; server.on("/", handle_root); + server.on("/manual", handle_manual); server.on("/wifi", HTTP_GET, handle_wifi_get); server.on("/wifi", HTTP_POST, handle_wifi_post); server.on("/sender/", handle_sender); @@ -137,6 +232,7 @@ void web_server_begin_sta(const SenderStatus *statuses, uint8_t count) { g_is_ap = false; server.on("/", handle_root); + server.on("/manual", handle_manual); server.on("/sender/", handle_sender); server.on("/wifi", HTTP_GET, handle_wifi_get); server.on("/wifi", HTTP_POST, handle_wifi_post);