fix: reduce repeater web panel pressure and improve mqtt diagnostics

This commit is contained in:
Jared Dohrman
2026-04-13 09:42:32 +10:00
parent 9e0cff9e70
commit de071cbb79
7 changed files with 188 additions and 156 deletions
+2
View File
@@ -163,6 +163,8 @@ Features include:
- light and dark themes - light and dark themes
- optional disable via `set web off` - optional disable via `set web off`
Recommended use is initial setup and occasional troubleshooting. On MQTT repeater deployments, disable the panel again when finished if you want maximum MQTT heap headroom.
Some constrained targets disable the panel to stay within flash limits. For example, `Xiao_C3_repeater_mqtt` builds with `WITH_WEB_PANEL=0`. Some constrained targets disable the panel to stay within flash limits. For example, `Xiao_C3_repeater_mqtt` builds with `WITH_WEB_PANEL=0`.
### Companion WiFi Additions ### Companion WiFi Additions
+1 -1
View File
@@ -93,7 +93,7 @@ It is the traditional way to configure a repeater after flashing, including:
- flood max - flood max
- some advanced repeater settings - some advanced repeater settings
As of `v1.2.1`, the local repeater web panel also includes the same common repeater settings, so many users can continue day-to-day configuration there after initial setup. As of `v1.2.1`, the local repeater web panel also includes the same common repeater settings, so users can complete initial setup there and return for occasional troubleshooting or configuration changes. On MQTT repeaters that need maximum headroom, it is still best to disable the panel again when you are finished.
### Console ### Console
+30
View File
@@ -17,6 +17,12 @@ It gives you:
- editable MQTT settings - editable MQTT settings
- a stats dashboard with Wi-Fi, core, radio, memory, and packet views - a stats dashboard with Wi-Fi, core, radio, memory, and packet views
Operational guidance:
- use it for initial setup, occasional configuration changes, and troubleshooting
- when you are finished, prefer `set web off` on MQTT repeaters that need maximum headroom
- this leaves more internal heap available for MQTT/WSS activity, especially on dual-broker setups
## Screenshot Overview ## Screenshot Overview
The layout below reflects the current panel structure for the repeater web UI. The layout below reflects the current panel structure for the repeater web UI.
@@ -62,6 +68,18 @@ Example:
This is intended for local admin use on a trusted network, not for open internet exposure. This is intended for local admin use on a trusted network, not for open internet exposure.
## Performance Notes
The panel is designed to load more gently than earlier versions. On login it now fetches sections in sequence instead of requesting one large bootstrap payload up front.
Even with that change, the panel still uses HTTPS and internal heap. On boards running one or two WSS MQTT brokers, opening the panel reduces MQTT headroom while the session is active.
Recommended practice for repeater deployments:
- enable the panel for initial configuration
- use it again for occasional checks or troubleshooting
- disable it with `set web off` when finished so MQTT has the most headroom available
## Actions ## Actions
The Actions panel gives you the most common operational controls: The Actions panel gives you the most common operational controls:
@@ -210,6 +228,18 @@ Check:
Use the repeater admin password, not the guest password. Use the repeater admin password, not the guest password.
### MQTT becomes unstable when I log in
The web panel now loads settings section-by-section to reduce startup pressure, but HTTPS still consumes internal heap.
Check:
- whether one or two MQTT brokers are enabled
- `memory` before and after login
- whether stability improves after `set web off`
For fixed installations where MQTT uptime matters more than browser access, use the panel briefly and then disable it again.
### A command says it is not allowlisted ### A command says it is not allowlisted
The panel intentionally limits what can be run from the browser. Use the serial CLI for commands outside the web allowlist. `clock` is included, but most maintenance and debug commands are still serial-only. The panel intentionally limits what can be run from the browser. Use the serial CLI for commands outside the web allowlist. `clock` is included, but most maintenance and debug commands are still serial-only.
+104 -24
View File
@@ -120,6 +120,25 @@ const char* getWifiStateLabel(const MQTTPrefs& prefs, bool wifi_started) {
return "down"; return "down";
} }
#if MQTT_DEBUG
void logMqttMemorySnapshot(const char* phase, const char* broker_label = nullptr) {
MQTT_LOG("mem phase=%s broker=%s uptime_ms=%lu heap_free=%u heap_min=%u heap_max=%u psram_free=%u psram_min=%u "
"psram_max=%u",
phase != nullptr ? phase : "-",
broker_label != nullptr ? broker_label : "-",
millis(),
ESP.getFreeHeap(),
ESP.getMinFreeHeap(),
ESP.getMaxAllocHeap(),
ESP.getFreePsram(),
ESP.getMinFreePsram(),
ESP.getMaxAllocPsram());
}
#else
void logMqttMemorySnapshot(const char*, const char* = nullptr) {
}
#endif
} }
const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[3] = { const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[3] = {
@@ -398,6 +417,14 @@ bool MQTTUplink::refreshToken(BrokerState& broker) {
} }
void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) { void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) {
bool had_runtime_state = broker.client != nullptr || broker.token != nullptr || broker.connected ||
broker.connect_announced || broker.reconnect_pending || broker.next_connect_attempt != 0 ||
broker.last_connect_attempt != 0 || broker.reconnect_failures != 0 ||
broker.token_expires_at != 0;
if (!had_runtime_state) {
return;
}
logMqttMemorySnapshot("destroy-pre", broker.spec != nullptr ? broker.spec->label : nullptr);
if (broker.client != nullptr) { if (broker.client != nullptr) {
MQTT_LOG("%s destroy broker client", broker.spec->label); MQTT_LOG("%s destroy broker client", broker.spec->label);
esp_mqtt_client_stop(broker.client); esp_mqtt_client_stop(broker.client);
@@ -408,6 +435,7 @@ void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) {
broker.token = nullptr; broker.token = nullptr;
broker.connected = false; broker.connected = false;
broker.connect_announced = false; broker.connect_announced = false;
broker.connected_since_ms = 0;
broker.token_expires_at = 0; broker.token_expires_at = 0;
if (reset_retry_state) { if (reset_retry_state) {
broker.reconnect_pending = false; broker.reconnect_pending = false;
@@ -415,6 +443,7 @@ void MQTTUplink::destroyBroker(BrokerState& broker, bool reset_retry_state) {
broker.reconnect_failures = 0; broker.reconnect_failures = 0;
broker.last_connect_attempt = 0; broker.last_connect_attempt = 0;
} }
logMqttMemorySnapshot("destroy-post", broker.spec != nullptr ? broker.spec->label : nullptr);
} }
void MQTTUplink::queuePublish(BrokerState& broker, const char* topic, const char* payload, bool retain) { void MQTTUplink::queuePublish(BrokerState& broker, const char* topic, const char* payload, bool retain) {
@@ -423,7 +452,8 @@ void MQTTUplink::queuePublish(BrokerState& broker, const char* topic, const char
} }
MQTT_LOG("%s publish topic=%s retain=%d bytes=%u", broker.spec->label, topic, retain ? 1 : 0, MQTT_LOG("%s publish topic=%s retain=%d bytes=%u", broker.spec->label, topic, retain ? 1 : 0,
static_cast<unsigned>(strlen(payload))); static_cast<unsigned>(strlen(payload)));
esp_mqtt_client_enqueue(broker.client, topic, payload, 0, 1, retain, true); int enqueue_rc = esp_mqtt_client_enqueue(broker.client, topic, payload, 0, 1, retain, true);
MQTT_LOG("%s enqueue topic=%s rc=%d connected=%d", broker.spec->label, topic, enqueue_rc, broker.connected ? 1 : 0);
} }
int MQTTUplink::buildStatusJson(char* buffer, size_t buffer_size, bool online) const { int MQTTUplink::buildStatusJson(char* buffer, size_t buffer_size, bool online) const {
@@ -565,6 +595,7 @@ void MQTTUplink::publishOnlineStatus(BrokerState& broker) {
} }
void MQTTUplink::publishStatus(bool online) { void MQTTUplink::publishStatus(bool online) {
logMqttMemorySnapshot(online ? "status-pre" : "status-offline-pre");
char* payload = allocScratchBuffer(768); char* payload = allocScratchBuffer(768);
if (payload == nullptr) { if (payload == nullptr) {
return; return;
@@ -580,6 +611,7 @@ void MQTTUplink::publishStatus(bool online) {
} }
} }
freeScratchBuffer(payload); freeScratchBuffer(payload);
logMqttMemorySnapshot(online ? "status-post" : "status-offline-post");
} }
void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t event_id, void* event_data) { void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t event_id, void* event_data) {
@@ -588,6 +620,8 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e
return; return;
} }
auto* event = static_cast<esp_mqtt_event_handle_t>(event_data); auto* event = static_cast<esp_mqtt_event_handle_t>(event_data);
unsigned long now_ms = millis();
unsigned long connected_for_ms = broker->connected_since_ms != 0 ? (now_ms - broker->connected_since_ms) : 0;
switch (event_id) { switch (event_id) {
case MQTT_EVENT_CONNECTED: case MQTT_EVENT_CONNECTED:
@@ -595,33 +629,56 @@ void MQTTUplink::handleMqttEvent(void* handler_args, esp_event_base_t, int32_t e
broker->reconnect_pending = false; broker->reconnect_pending = false;
broker->next_connect_attempt = 0; broker->next_connect_attempt = 0;
broker->reconnect_failures = 0; broker->reconnect_failures = 0;
broker->connected_since_ms = now_ms;
MQTT_LOG("%s connected", broker->spec->label); MQTT_LOG("%s connected", broker->spec->label);
logMqttMemorySnapshot("connected", broker->spec->label);
break; break;
case MQTT_EVENT_DISCONNECTED: case MQTT_EVENT_DISCONNECTED:
MQTT_LOG("%s disconnected", broker->spec->label); broker->connected = false;
broker->connected_since_ms = 0;
MQTT_LOG("%s disconnected wifi_status=%d rssi=%d connected_for_ms=%lu", broker->spec->label,
static_cast<int>(WiFi.status()), WiFi.RSSI(), connected_for_ms);
logMqttMemorySnapshot("disconnected", broker->spec->label);
if (!broker->reconnect_pending) {
if (broker->reconnect_failures < 10) {
broker->reconnect_failures++;
}
broker->reconnect_pending = true;
broker->next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker->reconnect_failures);
MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label,
getBrokerRetryDelayMillis(broker->reconnect_failures),
static_cast<unsigned>(broker->reconnect_failures));
}
break;
case MQTT_EVENT_ERROR: case MQTT_EVENT_ERROR:
broker->connected = false; broker->connected = false;
if (broker->reconnect_failures < 10) { broker->connected_since_ms = 0;
broker->reconnect_failures++; if (event != nullptr && event->error_handle != nullptr) {
MQTT_LOG("%s error type=%d tls_esp=0x%x tls_stack=0x%x cert_flags=0x%x sock_errno=%d conn_refused=%d "
"connected_for_ms=%lu",
broker->spec->label, event->error_handle->error_type, event->error_handle->esp_tls_last_esp_err,
event->error_handle->esp_tls_stack_err, event->error_handle->esp_tls_cert_verify_flags,
event->error_handle->esp_transport_sock_errno, event->error_handle->connect_return_code,
connected_for_ms);
} else {
MQTT_LOG("%s error event connected_for_ms=%lu", broker->spec->label, connected_for_ms);
} }
broker->reconnect_pending = true; MQTT_LOG("%s wifi_status=%d rssi=%d", broker->spec->label, static_cast<int>(WiFi.status()), WiFi.RSSI());
broker->next_connect_attempt = millis() + getBrokerRetryDelayMillis(broker->reconnect_failures); logMqttMemorySnapshot("error", broker->spec->label);
if (event_id == MQTT_EVENT_ERROR) { if (!broker->reconnect_pending) {
if (event != nullptr && event->error_handle != nullptr) { if (broker->reconnect_failures < 10) {
MQTT_LOG("%s error type=%d tls_esp=0x%x tls_stack=0x%x cert_flags=0x%x sock_errno=%d conn_refused=%d", broker->reconnect_failures++;
broker->spec->label, event->error_handle->error_type, event->error_handle->esp_tls_last_esp_err,
event->error_handle->esp_tls_stack_err, event->error_handle->esp_tls_cert_verify_flags,
event->error_handle->esp_transport_sock_errno, event->error_handle->connect_return_code);
} else {
MQTT_LOG("%s error event", broker->spec->label);
} }
broker->reconnect_pending = true;
broker->next_connect_attempt = now_ms + getBrokerRetryDelayMillis(broker->reconnect_failures);
MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label,
getBrokerRetryDelayMillis(broker->reconnect_failures),
static_cast<unsigned>(broker->reconnect_failures));
} }
MQTT_LOG("%s reconnect in %lu ms (failures=%u)", broker->spec->label,
getBrokerRetryDelayMillis(broker->reconnect_failures),
static_cast<unsigned>(broker->reconnect_failures));
break; break;
case MQTT_EVENT_BEFORE_CONNECT: case MQTT_EVENT_BEFORE_CONNECT:
MQTT_LOG("%s before connect", broker->spec->label); MQTT_LOG("%s before connect", broker->spec->label);
logMqttMemorySnapshot("before-connect", broker->spec->label);
break; break;
default: default:
break; break;
@@ -716,13 +773,17 @@ void MQTTUplink::updateTimeSync() {
} }
} }
void MQTTUplink::ensureBroker(BrokerState& broker) { void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
if (broker.spec == nullptr) { if (broker.spec == nullptr) {
return; return;
} }
bool enabled = (_prefs.enabled_mask & broker.spec->bit) != 0; bool enabled = (_prefs.enabled_mask & broker.spec->bit) != 0;
if (!enabled) { if (!enabled) {
destroyBroker(broker); if (broker.client != nullptr || broker.token != nullptr || broker.connected || broker.connect_announced ||
broker.reconnect_pending || broker.next_connect_attempt != 0 || broker.last_connect_attempt != 0 ||
broker.reconnect_failures != 0 || broker.token_expires_at != 0) {
destroyBroker(broker);
}
return; return;
} }
@@ -749,6 +810,9 @@ void MQTTUplink::ensureBroker(BrokerState& broker) {
if (broker.next_connect_attempt != 0 && now_ms < broker.next_connect_attempt) { if (broker.next_connect_attempt != 0 && now_ms < broker.next_connect_attempt) {
return; return;
} }
if (!allow_new_connect) {
return;
}
broker.last_connect_attempt = now_ms; broker.last_connect_attempt = now_ms;
broker.reconnect_pending = false; broker.reconnect_pending = false;
@@ -759,8 +823,9 @@ void MQTTUplink::ensureBroker(BrokerState& broker) {
} }
refreshBrokerState(broker); refreshBrokerState(broker);
MQTT_LOG("%s mqtt init host=%s port=%d path=%s client_id=%s heap_free=%u heap_max=%u", broker.spec->label, MQTT_LOG("%s mqtt init host=%s port=%d path=%s client_id=%s", broker.spec->label, broker.spec->host, 443, "/mqtt",
broker.spec->host, 443, "/mqtt", broker.client_id, ESP.getFreeHeap(), ESP.getMaxAllocHeap()); broker.client_id);
logMqttMemorySnapshot("init-pre", broker.spec->label);
esp_mqtt_client_config_t cfg = {}; esp_mqtt_client_config_t cfg = {};
#if ESP_IDF_VERSION_MAJOR >= 5 #if ESP_IDF_VERSION_MAJOR >= 5
cfg.broker.address.hostname = broker.spec->host; cfg.broker.address.hostname = broker.spec->host;
@@ -805,17 +870,21 @@ void MQTTUplink::ensureBroker(BrokerState& broker) {
broker.client = esp_mqtt_client_init(&cfg); broker.client = esp_mqtt_client_init(&cfg);
if (broker.client == nullptr) { if (broker.client == nullptr) {
MQTT_LOG("%s mqtt init failed", broker.spec->label); MQTT_LOG("%s mqtt init failed", broker.spec->label);
logMqttMemorySnapshot("init-failed", broker.spec->label);
return; return;
} }
logMqttMemorySnapshot("init-post", broker.spec->label);
esp_mqtt_client_register_event(broker.client, MQTT_EVENT_ANY, &MQTTUplink::handleMqttEvent, &broker); esp_mqtt_client_register_event(broker.client, MQTT_EVENT_ANY, &MQTTUplink::handleMqttEvent, &broker);
if (esp_mqtt_client_start(broker.client) != ESP_OK) { if (esp_mqtt_client_start(broker.client) != ESP_OK) {
MQTT_LOG("%s mqtt start failed", broker.spec->label); MQTT_LOG("%s mqtt start failed", broker.spec->label);
logMqttMemorySnapshot("start-failed", broker.spec->label);
broker.reconnect_pending = true; broker.reconnect_pending = true;
broker.next_connect_attempt = now_ms + kBrokerRetryBaseMillis; broker.next_connect_attempt = now_ms + kBrokerRetryBaseMillis;
destroyBroker(broker, false); destroyBroker(broker, false);
} else { } else {
MQTT_LOG("%s mqtt start requested", broker.spec->label); MQTT_LOG("%s mqtt start requested", broker.spec->label);
logMqttMemorySnapshot("start-requested", broker.spec->label);
} }
} }
@@ -865,8 +934,21 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
_web_panel.lockSession(); _web_panel.lockSession();
} }
BrokerState* active_connecting_broker = nullptr;
for (BrokerState& broker : _brokers) { for (BrokerState& broker : _brokers) {
ensureBroker(broker); if (broker.client != nullptr && !broker.connected && !broker.reconnect_pending) {
active_connecting_broker = &broker;
break;
}
}
bool connect_started = false;
for (BrokerState& broker : _brokers) {
bool allow_new_connect = active_connecting_broker == nullptr && !connect_started;
ensureBroker(broker, allow_new_connect);
if (active_connecting_broker == nullptr && broker.client != nullptr && !broker.connected && !broker.reconnect_pending) {
connect_started = true;
}
if (broker.connected && !broker.connect_announced) { if (broker.connected && !broker.connect_announced) {
publishOnlineStatus(broker); publishOnlineStatus(broker);
broker.connect_announced = true; broker.connect_announced = true;
@@ -1041,8 +1123,6 @@ bool MQTTUplink::setWebEnabled(bool enabled) {
bool ok = savePrefs(); bool ok = savePrefs();
if (_prefs.web_enabled != 0) { if (_prefs.web_enabled != 0) {
ensureWebServer(); ensureWebServer();
} else {
stopWebServer();
} }
return ok; return ok;
#else #else
+2 -1
View File
@@ -98,6 +98,7 @@ private:
bool reconnect_pending; bool reconnect_pending;
unsigned long last_connect_attempt; unsigned long last_connect_attempt;
unsigned long next_connect_attempt; unsigned long next_connect_attempt;
unsigned long connected_since_ms;
time_t token_expires_at; time_t token_expires_at;
uint8_t reconnect_failures; uint8_t reconnect_failures;
char username[70]; char username[70];
@@ -144,7 +145,7 @@ private:
void refreshIdentityStrings(); void refreshIdentityStrings();
void refreshBrokerIdentity(BrokerState& broker); void refreshBrokerIdentity(BrokerState& broker);
void refreshBrokerState(BrokerState& broker); void refreshBrokerState(BrokerState& broker);
void ensureBroker(BrokerState& broker); void ensureBroker(BrokerState& broker, bool allow_new_connect);
void destroyBroker(BrokerState& broker, bool reset_retry_state = true); void destroyBroker(BrokerState& broker, bool reset_retry_state = true);
bool refreshToken(BrokerState& broker); bool refreshToken(BrokerState& broker);
void publishStatus(bool online); void publishStatus(bool online);
+49 -129
View File
@@ -1219,8 +1219,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const value = document.getElementById(inputId).value; const value = document.getElementById(inputId).value;
runCommand(prefix + value); runCommand(prefix + value);
} }
async function loadField(cmd, inputId, format) { async function loadField(cmd, inputId, format, options = {}) {
const result = await runCommand(cmd); const result = await runCommand(cmd, options);
if (!result.ok) return; if (!result.ok) return;
let value = parseReplyValue(result.text); let value = parseReplyValue(result.text);
if (format === "multiline") { if (format === "multiline") {
@@ -1285,27 +1285,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
} }
return JSON.parse(text); return JSON.parse(text);
}); });
}
function applyBootstrapData(data) {
if (!data) return;
if (typeof data.name === "string") document.getElementById("nodeName").value = data.name;
if (typeof data.mqtt_iata === "string" && data.mqtt_iata.length) document.getElementById("mqttIata").value = data.mqtt_iata;
if (typeof data.mqtt_owner === "string") document.getElementById("mqttOwner").value = data.mqtt_owner;
if (typeof data.mqtt_email === "string") document.getElementById("mqttEmail").value = data.mqtt_email;
if (typeof data.advert_interval === "string") document.getElementById("advertInterval").value = data.advert_interval;
if (typeof data.flood_interval === "string") document.getElementById("floodInterval").value = data.flood_interval;
if (typeof data.flood_max === "string") document.getElementById("floodMax").value = data.flood_max;
if (typeof data.path_hash_mode === "string") document.getElementById("pathHashMode").value = data.path_hash_mode;
if (typeof data.private_key === "string") document.getElementById("privateKey").value = data.private_key.toUpperCase();
if (typeof data.radio === "string") currentRadioConfig = parseRadioValue(data.radio);
if (typeof data.role === "string") document.getElementById("roleValue").value = data.role;
if (typeof data.clock === "string") document.getElementById("clockUtc").value = data.clock;
if (typeof data.public_key === "string") document.getElementById("publicKey").value = data.public_key.toUpperCase();
if (typeof data.mqtt_eastmesh_au === "string") setBrokerToggle("mqttEastmeshAu", data.mqtt_eastmesh_au);
if (typeof data.mqtt_letsmesh_eu === "string") setBrokerToggle("mqttLetsmeshEu", data.mqtt_letsmesh_eu);
if (typeof data.mqtt_letsmesh_us === "string") setBrokerToggle("mqttLetsmeshUs", data.mqtt_letsmesh_us);
syncRadioPresetUi();
updateBrokerWarning();
} }
function getLetsmeshMode() { function getLetsmeshMode() {
const eu = document.getElementById("mqttLetsmeshEu"); const eu = document.getElementById("mqttLetsmeshEu");
@@ -1376,17 +1355,13 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
} }
} }
function updateBrokerWarning() { async function loadBrokerState(cmd, inputId, options = {}) {
document.getElementById("mqttBrokerWarning").textContent = ""; const result = await runCommand(cmd, options);
}
async function loadBrokerState(cmd, inputId) {
const result = await runCommand(cmd);
if (!result.ok) return; if (!result.ok) return;
setBrokerToggle(inputId, parseReplyValue(result.text)); setBrokerToggle(inputId, parseReplyValue(result.text));
updateBrokerWarning();
} }
async function loadRadioConfig() { async function loadRadioConfig(options = {}) {
const result = await runCommand("get radio"); const result = await runCommand("get radio", options);
if (!result.ok) { if (!result.ok) {
setRadioPresetStatus("Unable to load current radio config.", true); setRadioPresetStatus("Unable to load current radio config.", true);
return; return;
@@ -1400,6 +1375,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
setRadioPresetStatus(""); setRadioPresetStatus("");
syncRadioPresetUi(); syncRadioPresetUi();
} }
function pause(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function loadSection(title, tasks) {
statusEl.textContent = title;
for (const task of tasks) {
await task();
}
await pause(40);
}
async function loadRadioPresets() { async function loadRadioPresets() {
const selectEl = document.getElementById("radioPreset"); const selectEl = document.getElementById("radioPreset");
const applyBtn = document.getElementById("applyRadioPresetBtn"); const applyBtn = document.getElementById("applyRadioPresetBtn");
@@ -1493,13 +1478,11 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
if (!result.ok) { if (!result.ok) {
refreshEastmeshModeUi(); refreshEastmeshModeUi();
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
updateBrokerWarning();
return; return;
} }
setBrokerToggle("mqttEastmeshAu", enabled ? "on" : "off"); setBrokerToggle("mqttEastmeshAu", enabled ? "on" : "off");
refreshEastmeshModeUi(); refreshEastmeshModeUi();
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
updateBrokerWarning();
} }
const eastmeshModeSlider = document.getElementById("mqttEastmeshMode"); const eastmeshModeSlider = document.getElementById("mqttEastmeshMode");
if (eastmeshModeSlider) { if (eastmeshModeSlider) {
@@ -1513,7 +1496,6 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
async function setLetsmeshMode(mode) { async function setLetsmeshMode(mode) {
const eastmesh = document.getElementById("mqttEastmeshAu"); const eastmesh = document.getElementById("mqttEastmeshAu");
if (mode === "both" && eastmesh && eastmesh.checked) { if (mode === "both" && eastmesh && eastmesh.checked) {
updateBrokerWarning();
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
return; return;
} }
@@ -1532,13 +1514,11 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
const result = await runCommand(command); const result = await runCommand(command);
if (!result.ok) { if (!result.ok) {
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
updateBrokerWarning();
return; return;
} }
setBrokerToggle(inputId, nextState); setBrokerToggle(inputId, nextState);
} }
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
updateBrokerWarning();
} }
const letsmeshModeSlider = document.getElementById("mqttLetsmeshMode"); const letsmeshModeSlider = document.getElementById("mqttLetsmeshMode");
if (letsmeshModeSlider) { if (letsmeshModeSlider) {
@@ -1615,30 +1595,43 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
return; return;
} }
showAuthedUi(true); showAuthedUi(true);
const quiet = { recordHistory:false, updateInput:false };
try { try {
applyBootstrapData(await fetchJson("/api/bootstrap")); await loadSection("Loading info...", [
} catch (_) { () => loadField("get role", "roleValue", null, quiet),
() => loadField("clock", "clockUtc", null, quiet),
() => loadField("get public.key", "publicKey", "uppercase", quiet)
]);
await loadSection("Loading repeater settings...", [
() => loadField("get name", "nodeName", null, quiet),
() => loadField("get lat", "nodeLat", null, quiet),
() => loadField("get lon", "nodeLon", null, quiet),
() => loadField("get prv.key", "privateKey", "uppercase", quiet),
() => loadField("get owner.info", "ownerInfo", "multiline", quiet)
]);
await loadSection("Loading radio settings...", [
() => loadRadioConfig(quiet),
() => loadField("get path.hash.mode", "pathHashMode", null, quiet)
]);
await loadSection("Loading advertising...", [
() => loadField("get advert.interval", "advertInterval", null, quiet),
() => loadField("get flood.advert.interval", "floodInterval", null, quiet),
() => loadField("get flood.max", "floodMax", null, quiet)
]);
await loadSection("Loading MQTT settings...", [
() => loadField("get mqtt.iata", "mqttIata", null, quiet),
() => loadField("get mqtt.owner", "mqttOwner", null, quiet),
() => loadField("get mqtt.email", "mqttEmail", null, quiet),
() => loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu", quiet),
() => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet),
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet)
]);
statusEl.textContent = "Ready";
} catch (error) {
if (!token) { if (!token) {
return; return;
} }
await Promise.all([ statusEl.textContent = error && error.message ? error.message : "Unable to load repeater settings.";
loadField("get name", "nodeName"),
loadField("get mqtt.iata", "mqttIata"),
loadField("get mqtt.owner", "mqttOwner"),
loadField("get mqtt.email", "mqttEmail"),
loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu"),
loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu"),
loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs"),
loadField("get advert.interval", "advertInterval"),
loadField("get flood.advert.interval", "floodInterval"),
loadField("get flood.max", "floodMax"),
loadField("get path.hash.mode", "pathHashMode"),
loadField("get prv.key", "privateKey", "uppercase"),
loadField("get role", "roleValue"),
loadField("clock", "clockUtc"),
loadField("get public.key", "publicKey", "uppercase"),
loadRadioConfig()
]);
} }
loadRadioPresets(); loadRadioPresets();
} }
@@ -1697,13 +1690,11 @@ bool WebPanelServer::start() {
httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context}; httpd_uri_t app_uri = {.uri = "/app", .method = HTTP_GET, .handler = &WebPanelServer::handleApp, .user_ctx = &_route_context};
httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context}; httpd_uri_t login_uri = {.uri = "/login", .method = HTTP_POST, .handler = &WebPanelServer::handleLogin, .user_ctx = &_route_context};
httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context}; httpd_uri_t command_uri = {.uri = "/api/command", .method = HTTP_POST, .handler = &WebPanelServer::handleCommand, .user_ctx = &_route_context};
httpd_uri_t bootstrap_uri = {.uri = "/api/bootstrap", .method = HTTP_GET, .handler = &WebPanelServer::handleBootstrap, .user_ctx = &_route_context};
httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context}; httpd_uri_t stats_uri = {.uri = "/api/stats", .method = HTTP_GET, .handler = &WebPanelServer::handleStats, .user_ctx = &_route_context};
httpd_register_uri_handler(_server, &index_uri); httpd_register_uri_handler(_server, &index_uri);
httpd_register_uri_handler(_server, &app_uri); httpd_register_uri_handler(_server, &app_uri);
httpd_register_uri_handler(_server, &login_uri); httpd_register_uri_handler(_server, &login_uri);
httpd_register_uri_handler(_server, &command_uri); httpd_register_uri_handler(_server, &command_uri);
httpd_register_uri_handler(_server, &bootstrap_uri);
httpd_register_uri_handler(_server, &stats_uri); httpd_register_uri_handler(_server, &stats_uri);
WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str()); WEB_PANEL_LOG("server started on https://%s/", WiFi.localIP().toString().c_str());
return true; return true;
@@ -1826,77 +1817,6 @@ esp_err_t WebPanelServer::handleCommand(httpd_req_t* req) {
return rc; return rc;
} }
esp_err_t WebPanelServer::handleBootstrap(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
return httpd_resp_send_500(req);
}
if (!ctx->self->isAuthorized(req)) {
return httpd_resp_send_err(req, HTTPD_401_UNAUTHORIZED, "Unauthorized");
}
ctx->self->noteActivity();
char* reply = allocScratchBuffer(kWebReplyBufferSize);
if (reply == nullptr) {
freeScratchBuffer(reply);
return httpd_resp_send_500(req);
}
const struct {
const char* key;
const char* command;
} fields[] = {
{"name", "get name"},
{"mqtt_iata", "get mqtt.iata"},
{"mqtt_owner", "get mqtt.owner"},
{"mqtt_email", "get mqtt.email"},
{"mqtt_eastmesh_au", "get mqtt.eastmesh-au"},
{"mqtt_letsmesh_eu", "get mqtt.letsmesh-eu"},
{"mqtt_letsmesh_us", "get mqtt.letsmesh-us"},
{"advert_interval", "get advert.interval"},
{"flood_interval", "get flood.advert.interval"},
{"flood_max", "get flood.max"},
{"path_hash_mode", "get path.hash.mode"},
{"private_key", "get prv.key"},
{"role", "get role"},
{"clock", "clock"},
{"public_key", "get public.key"},
{"radio", "get radio"},
};
httpd_resp_set_type(req, "application/json; charset=utf-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store");
if (sendChunk(req, "{") != ESP_OK) {
freeScratchBuffer(reply);
httpd_resp_sendstr_chunk(req, nullptr);
return ESP_FAIL;
}
for (size_t i = 0; i < (sizeof(fields) / sizeof(fields[0])); ++i) {
memset(reply, 0, kWebReplyBufferSize);
ctx->self->_runner->runWebCommand(fields[i].command, reply, kWebReplyBufferSize);
const char* value = reply;
if (value[0] == '>' && value[1] == ' ') {
value += 2;
}
if (strcmp(value, "-") == 0) {
value = "";
}
if (sendJsonFieldChunk(req, fields[i].key, value, i != 0) != ESP_OK) {
freeScratchBuffer(reply);
httpd_resp_sendstr_chunk(req, nullptr);
return ESP_FAIL;
}
}
esp_err_t rc = sendChunk(req, "}");
if (rc == ESP_OK) {
rc = httpd_resp_sendstr_chunk(req, nullptr);
} else {
httpd_resp_sendstr_chunk(req, nullptr);
}
freeScratchBuffer(reply);
return rc;
}
esp_err_t WebPanelServer::handleStats(httpd_req_t* req) { esp_err_t WebPanelServer::handleStats(httpd_req_t* req) {
auto* ctx = static_cast<RouteContext*>(req->user_ctx); auto* ctx = static_cast<RouteContext*>(req->user_ctx);
if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) { if (ctx == nullptr || ctx->self == nullptr || ctx->self->_runner == nullptr) {
-1
View File
@@ -47,7 +47,6 @@ private:
static esp_err_t handleApp(httpd_req_t* req); static esp_err_t handleApp(httpd_req_t* req);
static esp_err_t handleLogin(httpd_req_t* req); static esp_err_t handleLogin(httpd_req_t* req);
static esp_err_t handleCommand(httpd_req_t* req); static esp_err_t handleCommand(httpd_req_t* req);
static esp_err_t handleBootstrap(httpd_req_t* req);
static esp_err_t handleStats(httpd_req_t* req); static esp_err_t handleStats(httpd_req_t* req);
bool readRequestBody(httpd_req_t* req, char* buffer, size_t buffer_size) const; bool readRequestBody(httpd_req_t* req, char* buffer, size_t buffer_size) const;