Feature-flagged WiFi/MQTT capability (WITH_MQTT) that bridges LoRa mesh packets to MQTT topics over TLS (port 443). Includes: - WiFiManager: connection handling with AP fallback mode - MQTTBridge: TLS-secured pub/sub with FNV-1a deduplication - WebConfig: REST API for WiFi/MQTT settings - Embedded web UI dashboard for configuration Default broker: meshqt.l.supported.systems:443 (MQTTS) Build with: pio run -e heltec_v3_mqtt
285 lines
8.2 KiB
C++
285 lines
8.2 KiB
C++
#ifdef WITH_MQTT
|
|
|
|
#include "WebConfig.h"
|
|
#include "web_ui.h"
|
|
#include <helpers/CommonCLI.h>
|
|
|
|
WebConfig::WebConfig(WiFiManager& wifi, MQTTBridge& mqtt, uint16_t port)
|
|
: _server(port),
|
|
_wifi(wifi),
|
|
_mqtt(mqtt),
|
|
_wifi_config(nullptr),
|
|
_mqtt_config(nullptr),
|
|
_prefs(nullptr),
|
|
_node_name(nullptr),
|
|
_firmware_version(nullptr),
|
|
_save_callback(nullptr),
|
|
_reboot_callback(nullptr),
|
|
_initialized(false) {
|
|
memset(&_stats, 0, sizeof(_stats));
|
|
memset(_node_id, 0, sizeof(_node_id));
|
|
}
|
|
|
|
void WebConfig::begin(WiFiConfig* wifi_config, MQTTConfig* mqtt_config, NodePrefs* prefs) {
|
|
_wifi_config = wifi_config;
|
|
_mqtt_config = mqtt_config;
|
|
_prefs = prefs;
|
|
|
|
setupRoutes();
|
|
_server.begin();
|
|
_initialized = true;
|
|
|
|
Serial.printf("[WebConfig] Server started on port 80\n");
|
|
}
|
|
|
|
void WebConfig::setNodeInfo(const uint8_t* pubkey, const char* name, const char* version) {
|
|
for (int i = 0; i < 8; i++) {
|
|
sprintf(&_node_id[i * 2], "%02x", pubkey[i]);
|
|
}
|
|
_node_name = name;
|
|
_firmware_version = version;
|
|
}
|
|
|
|
void WebConfig::updateStats(const WebConfigStats& stats) {
|
|
_stats = stats;
|
|
}
|
|
|
|
void WebConfig::end() {
|
|
if (_initialized) {
|
|
_server.end();
|
|
_initialized = false;
|
|
}
|
|
}
|
|
|
|
void WebConfig::setupRoutes() {
|
|
// Serve main page
|
|
_server.on("/", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
|
handleRoot(request);
|
|
});
|
|
|
|
// API endpoints
|
|
_server.on("/api/status", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
|
handleStatus(request);
|
|
});
|
|
|
|
_server.on("/api/wifi", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
|
handleWiFiGet(request);
|
|
});
|
|
|
|
// WiFi POST - use body handler for JSON
|
|
_server.on("/api/wifi", HTTP_POST,
|
|
// Request handler (called after body is received)
|
|
[this](AsyncWebServerRequest* request) {},
|
|
// Upload handler (not used)
|
|
nullptr,
|
|
// Body handler
|
|
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
|
|
if (index == 0 && len == total) {
|
|
// Complete body received
|
|
JsonDocument doc;
|
|
DeserializationError error = deserializeJson(doc, data, len);
|
|
|
|
if (error) {
|
|
sendErrorResponse(request, "Invalid JSON");
|
|
return;
|
|
}
|
|
|
|
if (doc["ssid"].is<const char*>()) {
|
|
strncpy(_wifi_config->ssid, doc["ssid"].as<const char*>(), sizeof(_wifi_config->ssid) - 1);
|
|
}
|
|
if (doc["password"].is<const char*>()) {
|
|
strncpy(_wifi_config->password, doc["password"].as<const char*>(), sizeof(_wifi_config->password) - 1);
|
|
}
|
|
|
|
if (_save_callback) _save_callback();
|
|
|
|
JsonDocument resp;
|
|
resp["success"] = true;
|
|
resp["message"] = "WiFi config saved. Reconnecting...";
|
|
sendJsonResponse(request, resp);
|
|
|
|
// Schedule reconnect after response
|
|
_wifi.reconnect();
|
|
}
|
|
}
|
|
);
|
|
|
|
_server.on("/api/mqtt", HTTP_GET, [this](AsyncWebServerRequest* request) {
|
|
handleMQTTGet(request);
|
|
});
|
|
|
|
// MQTT POST - use body handler for JSON
|
|
_server.on("/api/mqtt", HTTP_POST,
|
|
// Request handler
|
|
[this](AsyncWebServerRequest* request) {},
|
|
// Upload handler
|
|
nullptr,
|
|
// Body handler
|
|
[this](AsyncWebServerRequest* request, uint8_t* data, size_t len, size_t index, size_t total) {
|
|
if (index == 0 && len == total) {
|
|
JsonDocument doc;
|
|
DeserializationError error = deserializeJson(doc, data, len);
|
|
|
|
if (error) {
|
|
sendErrorResponse(request, "Invalid JSON");
|
|
return;
|
|
}
|
|
|
|
if (doc["enabled"].is<bool>()) {
|
|
_mqtt_config->enabled = doc["enabled"].as<bool>() ? 1 : 0;
|
|
}
|
|
if (doc["broker"].is<const char*>()) {
|
|
strncpy(_mqtt_config->broker, doc["broker"].as<const char*>(), sizeof(_mqtt_config->broker) - 1);
|
|
}
|
|
if (doc["port"].is<int>()) {
|
|
_mqtt_config->port = doc["port"].as<uint16_t>();
|
|
}
|
|
if (doc["user"].is<const char*>()) {
|
|
strncpy(_mqtt_config->user, doc["user"].as<const char*>(), sizeof(_mqtt_config->user) - 1);
|
|
}
|
|
if (doc["password"].is<const char*>()) {
|
|
strncpy(_mqtt_config->password, doc["password"].as<const char*>(), sizeof(_mqtt_config->password) - 1);
|
|
}
|
|
if (doc["topic_prefix"].is<const char*>()) {
|
|
strncpy(_mqtt_config->topic_prefix, doc["topic_prefix"].as<const char*>(), sizeof(_mqtt_config->topic_prefix) - 1);
|
|
}
|
|
|
|
if (_save_callback) _save_callback();
|
|
|
|
// Update MQTT bridge config
|
|
_mqtt.updateConfig(*_mqtt_config);
|
|
|
|
JsonDocument resp;
|
|
resp["success"] = true;
|
|
resp["message"] = "MQTT config saved";
|
|
sendJsonResponse(request, resp);
|
|
}
|
|
}
|
|
);
|
|
|
|
_server.on("/api/reboot", HTTP_POST, [this](AsyncWebServerRequest* request) {
|
|
handleReboot(request);
|
|
});
|
|
|
|
// Handle 404
|
|
_server.onNotFound([this](AsyncWebServerRequest* request) {
|
|
handleNotFound(request);
|
|
});
|
|
}
|
|
|
|
void WebConfig::handleRoot(AsyncWebServerRequest* request) {
|
|
request->send_P(200, "text/html", WEB_UI_HTML);
|
|
}
|
|
|
|
void WebConfig::handleStatus(AsyncWebServerRequest* request) {
|
|
JsonDocument doc;
|
|
|
|
// Node info
|
|
doc["node_id"] = _node_id;
|
|
doc["node_name"] = _node_name ? _node_name : "MeshCore";
|
|
doc["firmware"] = _firmware_version ? _firmware_version : "unknown";
|
|
|
|
// WiFi status
|
|
JsonObject wifi = doc["wifi"].to<JsonObject>();
|
|
wifi["connected"] = _wifi.isConnected();
|
|
wifi["ap_mode"] = _wifi.isAPMode();
|
|
wifi["ip"] = _wifi.getLocalIP().toString();
|
|
wifi["rssi"] = _wifi.getRSSI();
|
|
wifi["ssid"] = _wifi.getSSID();
|
|
wifi["mac"] = _wifi.getMacAddress();
|
|
|
|
// MQTT status
|
|
JsonObject mqtt = doc["mqtt"].to<JsonObject>();
|
|
mqtt["connected"] = _mqtt.isConnected();
|
|
mqtt["enabled"] = _mqtt.isEnabled();
|
|
mqtt["broker"] = _mqtt.getBroker();
|
|
mqtt["port"] = _mqtt.getPort();
|
|
mqtt["msgs_sent"] = _mqtt.getMessagesSent();
|
|
mqtt["msgs_recv"] = _mqtt.getMessagesReceived();
|
|
|
|
// Mesh stats
|
|
JsonObject mesh = doc["mesh"].to<JsonObject>();
|
|
mesh["uptime_secs"] = _stats.uptime_secs;
|
|
mesh["packets_rx"] = _stats.packets_rx;
|
|
mesh["packets_tx"] = _stats.packets_tx;
|
|
mesh["air_time_secs"] = _stats.air_time_secs;
|
|
mesh["noise_floor"] = _stats.noise_floor;
|
|
mesh["last_rssi"] = _stats.last_rssi;
|
|
mesh["last_snr"] = _stats.last_snr;
|
|
mesh["tx_queue_len"] = _stats.tx_queue_len;
|
|
mesh["batt_mv"] = _stats.batt_mv;
|
|
|
|
// System info
|
|
doc["free_heap"] = ESP.getFreeHeap();
|
|
doc["uptime_ms"] = millis();
|
|
|
|
sendJsonResponse(request, doc);
|
|
}
|
|
|
|
void WebConfig::handleWiFiGet(AsyncWebServerRequest* request) {
|
|
if (!_wifi_config) {
|
|
sendErrorResponse(request, "Config not available");
|
|
return;
|
|
}
|
|
|
|
JsonDocument doc;
|
|
doc["ssid"] = _wifi_config->ssid;
|
|
// Don't send password for security
|
|
doc["password_set"] = strlen(_wifi_config->password) > 0;
|
|
doc["ap_ssid"] = _wifi_config->ap_ssid;
|
|
|
|
sendJsonResponse(request, doc);
|
|
}
|
|
|
|
void WebConfig::handleMQTTGet(AsyncWebServerRequest* request) {
|
|
if (!_mqtt_config) {
|
|
sendErrorResponse(request, "Config not available");
|
|
return;
|
|
}
|
|
|
|
JsonDocument doc;
|
|
doc["enabled"] = _mqtt_config->enabled != 0;
|
|
doc["broker"] = _mqtt_config->broker;
|
|
doc["port"] = _mqtt_config->port;
|
|
doc["user"] = _mqtt_config->user;
|
|
// Don't send password for security
|
|
doc["password_set"] = strlen(_mqtt_config->password) > 0;
|
|
doc["topic_prefix"] = _mqtt_config->topic_prefix;
|
|
|
|
sendJsonResponse(request, doc);
|
|
}
|
|
|
|
void WebConfig::handleReboot(AsyncWebServerRequest* request) {
|
|
JsonDocument doc;
|
|
doc["success"] = true;
|
|
doc["message"] = "Rebooting...";
|
|
sendJsonResponse(request, doc);
|
|
|
|
// Schedule reboot after response is sent
|
|
delay(100);
|
|
if (_reboot_callback) {
|
|
_reboot_callback();
|
|
} else {
|
|
ESP.restart();
|
|
}
|
|
}
|
|
|
|
void WebConfig::handleNotFound(AsyncWebServerRequest* request) {
|
|
request->send(404, "text/plain", "Not found");
|
|
}
|
|
|
|
void WebConfig::sendJsonResponse(AsyncWebServerRequest* request, JsonDocument& doc, int code) {
|
|
String response;
|
|
serializeJson(doc, response);
|
|
request->send(code, "application/json", response);
|
|
}
|
|
|
|
void WebConfig::sendErrorResponse(AsyncWebServerRequest* request, const char* message, int code) {
|
|
JsonDocument doc;
|
|
doc["success"] = false;
|
|
doc["error"] = message;
|
|
sendJsonResponse(request, doc, code);
|
|
}
|
|
|
|
#endif // WITH_MQTT
|