expand web ui with batch table and manual

This commit is contained in:
2026-02-01 21:04:34 +01:00
parent 50436cd0bb
commit 01f4494f00
3 changed files with 105 additions and 4 deletions

View File

@@ -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();

View File

@@ -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);
}
}
@@ -791,6 +793,7 @@ static void receiver_loop() {
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<uint8_t>(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;

View File

@@ -1,12 +1,17 @@
#include "web_server.h"
#include <WebServer.h>
#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 = "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>";
@@ -19,10 +24,40 @@ static String html_footer() {
return "</body></html>";
}
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<uint8_t>(g_sender_last_errors[idx]));
return s;
}
static String render_sender_block(const SenderStatus &status) {
String s;
s += "<div style='margin-bottom:10px;padding:6px;border:1px solid #ccc'>";
s += "<strong>" + String(status.last_data.device_id) + "</strong><br>";
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 += "<strong>" + String(status.last_data.device_id) + "</strong>";
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 += "<br>";
if (!status.has_data) {
s += "No data";
} else {
@@ -45,6 +80,7 @@ static void handle_root() {
}
html += "<p><a href='/wifi'>Configure WiFi/MQTT/NTP</a></p>";
html += "<p><a href='/manual'>Manual</a></p>";
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 += "<h3>Last batch (" + String(g_last_batch_count[i]) + " samples)</h3>";
html += "<table border='1' cellspacing='0' cellpadding='3'>";
html += "<tr><th>#</th><th>ts</th><th>e_kwh</th><th>p_w</th><th>p1_w</th><th>p2_w</th><th>p3_w</th>";
html += "<th>bat_v</th><th>bat_pct</th><th>rssi</th><th>snr</th><th>err_tx</th><th>err_last</th></tr>";
for (uint8_t r = 0; r < g_last_batch_count[i]; ++r) {
const MeterData &d = g_last_batch[i][r];
html += "<tr>";
html += "<td>" + String(r) + "</td>";
html += "<td>" + String(d.ts_utc) + "</td>";
html += "<td>" + String(d.energy_total_kwh, 3) + "</td>";
html += "<td>" + String(d.total_power_w, 1) + "</td>";
html += "<td>" + String(d.phase_power_w[0], 1) + "</td>";
html += "<td>" + String(d.phase_power_w[1], 1) + "</td>";
html += "<td>" + String(d.phase_power_w[2], 1) + "</td>";
html += "<td>" + String(d.battery_voltage_v, 2) + "</td>";
html += "<td>" + String(d.battery_percent) + "</td>";
html += "<td>" + String(d.link_rssi_dbm) + "</td>";
html += "<td>" + String(d.link_snr_db, 1) + "</td>";
html += "<td>" + String(d.err_lora_tx) + "</td>";
html += "<td>" + String(static_cast<uint8_t>(d.last_error)) + "</td>";
html += "</tr>";
}
html += "</table>";
}
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 += "<ul>";
html += "<li>Energy: total kWh since meter start.</li>";
html += "<li>Power: total active power in W.</li>";
html += "<li>P1/P2/P3: phase power in W.</li>";
html += "<li>bat_v: battery voltage (V), bat_pct: estimated percent.</li>";
html += "<li>RSSI/SNR: LoRa link quality from last packet.</li>";
html += "<li>err_tx: LoRa TX error count; err_last: last error code.</li>";
html += "<li>faults m/d/tx: meter read/decode/tx counters.</li>";
html += "</ul>";
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<uint8_t>(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);