#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 = ""; h += "" + title + ""; h += "

" + title + "

"; return h; } 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 += "
"; 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; } } } String device_id = status.last_data.device_id; s += "" + device_id + ""; if (status.has_data && status.last_data.link_valid) { s += " RSSI:" + String(status.last_data.link_rssi_dbm) + " SNR:" + String(status.last_data.link_snr_db, 1); } if (status.has_data) { s += " err_tx:" + String(status.last_data.err_lora_tx); s += " err_last:" + String(static_cast(status.last_data.last_error)); } s += format_faults(idx); s += "
"; if (!status.has_data) { s += "No data"; } else { s += "Energy: " + String(status.last_data.energy_total_kwh, 3) + " kWh
"; s += "Power: " + String(status.last_data.total_power_w, 1) + " W
"; s += "P1/P2/P3: " + String(status.last_data.phase_power_w[0], 1) + " / " + String(status.last_data.phase_power_w[1], 1) + " / " + String(status.last_data.phase_power_w[2], 1) + " W
"; s += "Battery: " + String(status.last_data.battery_percent) + "% (" + String(status.last_data.battery_voltage_v, 2) + " V)"; } s += "
"; return s; } static void handle_root() { String html = html_header("DD3 Bridge Status"); html += g_is_ap ? "

Mode: AP

" : "

Mode: STA

"; if (g_statuses) { for (uint8_t i = 0; i < g_status_count; ++i) { html += render_sender_block(g_statuses[i]); } } html += "

Configure WiFi/MQTT/NTP

"; html += "

Manual

"; html += html_footer(); server.send(200, "text/html", html); } static void handle_wifi_get() { String html = html_header("WiFi/MQTT Config"); html += "
"; html += "SSID:
"; html += "Password:
"; html += "MQTT Host:
"; html += "MQTT Port:
"; html += "MQTT User:
"; html += "MQTT Pass:
"; html += "NTP Server 1:
"; html += "NTP Server 2:
"; html += ""; html += "
"; html += html_footer(); server.send(200, "text/html", html); } static void handle_wifi_post() { WifiMqttConfig cfg; cfg.ntp_server_1 = "pool.ntp.org"; cfg.ntp_server_2 = "time.nist.gov"; cfg.ssid = server.arg("ssid"); cfg.password = server.arg("pass"); cfg.mqtt_host = server.arg("mqhost"); cfg.mqtt_port = static_cast(server.arg("mqport").toInt()); cfg.mqtt_user = server.arg("mquser"); cfg.mqtt_pass = server.arg("mqpass"); if (server.arg("ntp1").length() > 0) { cfg.ntp_server_1 = server.arg("ntp1"); } if (server.arg("ntp2").length() > 0) { cfg.ntp_server_2 = server.arg("ntp2"); } cfg.valid = true; wifi_save_config(cfg); server.send(200, "text/html", "Saved. Rebooting..."); delay(1000); ESP.restart(); } static void handle_sender() { if (!g_statuses) { server.send(404, "text/plain", "No senders"); return; } String uri = server.uri(); String device_id = uri.substring(String("/sender/").length()); for (uint8_t i = 0; i < g_status_count; ++i) { 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; } } 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 += "
  • Battery: percent with voltage in V.
  • "; html += "
  • RSSI/SNR: LoRa link quality from last packet.
  • "; html += "
  • err_tx: sender-side LoRa TX error counter.
  • "; html += "
  • err_last: last error code (0=None, 1=MeterRead, 2=Decode, 3=LoraTx).
  • "; html += "
  • faults m/d/tx: receiver-side counters (meter read fails, decode fails, LoRa TX fails).
  • "; html += "
  • faults last: last receiver-side error code (same mapping as err_last).
  • "; 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); server.onNotFound([]() { if (server.uri().startsWith("/sender/")) { handle_sender(); return; } server.send(404, "text/plain", "Not found"); }); server.begin(); } void web_server_begin_sta(const SenderStatus *statuses, uint8_t count) { g_statuses = statuses; g_status_count = 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); server.onNotFound([]() { if (server.uri().startsWith("/sender/")) { handle_sender(); return; } server.send(404, "text/plain", "Not found"); }); server.begin(); } void web_server_loop() { server.handleClient(); }