feat: add custom MQTT WSS transport
This commit is contained in:
@@ -34,7 +34,7 @@ No-argument `get` commands must be entered exactly as shown.
|
||||
|
||||
### MQTT Status And Routing
|
||||
|
||||
- `get mqtt.status`: shows Wi-Fi, NTP, IATA, endpoint status, status publishing state, and TX state.
|
||||
- `get mqtt.status`: shows Wi-Fi, NTP, IATA, endpoint status, custom endpoint transport, status publishing state, and TX state.
|
||||
- `get mqtt.statuscfg`: shows whether periodic status messages are enabled as a simple `on` or `off` value. Most users can just use `get mqtt.status`.
|
||||
- `get mqtt.client_version`: shows the MQTT `client_version` string published by the repeater.
|
||||
- `get mqtt.client_env`: shows the PlatformIO env used to build the repeater firmware.
|
||||
@@ -75,6 +75,8 @@ No-argument `get` commands must be entered exactly as shown.
|
||||
- `set mqtt.custom.host <host>`
|
||||
- `get mqtt.custom.port`
|
||||
- `set mqtt.custom.port <port>`
|
||||
- `get mqtt.custom.transport`: shows `tcp` or `wss`.
|
||||
- `set mqtt.custom.transport tcp|wss`
|
||||
- `get mqtt.custom.username`
|
||||
- `set mqtt.custom.username <username>`
|
||||
- `get mqtt.custom.password`: shows `set` when a custom password is configured.
|
||||
@@ -85,7 +87,9 @@ Notes:
|
||||
- new observer installs default `mqtt.iata` to `UNSET`
|
||||
- `letsmesh-eu` and `letsmesh-us` remain off by default unless already configured in saved prefs
|
||||
- if `mqtt.iata` is `UNSET`, enabled MQTT brokers will not connect
|
||||
- custom MQTT uses normal MQTT over TCP with the configured username and password, not JWT authentication
|
||||
- custom MQTT uses the configured username and password, not JWT authentication
|
||||
- custom MQTT defaults to TCP; set `mqtt.custom.transport wss` for MQTT over secure WebSockets using the fixed `/mqtt` websocket path and the ESP-IDF x509 root CA bundle
|
||||
- in `get mqtt.status`, custom MQTT is shown as `custom:<transport>:<state>`, for example `custom:wss:up`; `conn` means connecting and `up` means connected
|
||||
- custom MQTT uses the same `meshcore/<IATA>/<device>/<leaf>` topics as the curated brokers
|
||||
- turning off a connected broker publishes a retained MQTT status update with `"status":"offline"` before the client disconnects
|
||||
- changing `mqtt.iata` away from a configured value also publishes retained offline status to the old status topic, restarts connected broker clients, and reconnects under the new topic path
|
||||
|
||||
@@ -254,7 +254,7 @@ This section includes:
|
||||
- `mqtt.owner`: owner public key.
|
||||
- `mqtt.email`: owner contact email.
|
||||
- MQTT server toggles: `eastmesh-au`, `letsmesh-eu`, `letsmesh-us`, and custom MQTT.
|
||||
- custom MQTT `host:port`, username, and password fields.
|
||||
- custom MQTT `host:port`, TCP/WSS transport, username, and password fields.
|
||||
|
||||
`UNSET - To be configured` is the default for new observer installs until a real saved value exists.
|
||||
|
||||
@@ -264,7 +264,8 @@ Notes:
|
||||
- while `mqtt.iata` is `UNSET`, enabled MQTT brokers do not attempt to connect
|
||||
- the current MQTT server states are loaded when the page opens
|
||||
- you can toggle each MQTT server on or off from this panel
|
||||
- custom MQTT uses normal MQTT over TCP with the configured username and password, not JWT authentication
|
||||
- custom MQTT uses the configured username and password, not JWT authentication
|
||||
- custom MQTT defaults to TCP; choose WSS for MQTT over secure WebSockets using the fixed `/mqtt` websocket path and the ESP-IDF x509 root CA bundle
|
||||
- turning off a connected MQTT server publishes retained offline status before the client disconnects
|
||||
- changing `mqtt.iata` away from a configured value publishes retained offline status to the old status topic, restarts connected broker clients, and reconnects under the new topic path
|
||||
- at most two MQTT brokers can be enabled at once
|
||||
|
||||
@@ -2430,6 +2430,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
sprintf(reply, "> %s", mqtt.getCustomHost()[0] ? mqtt.getCustomHost() : "-");
|
||||
} else if (strcmp(command, "get mqtt.custom.port") == 0) {
|
||||
sprintf(reply, "> %u", static_cast<unsigned>(mqtt.getCustomPort()));
|
||||
} else if (strcmp(command, "get mqtt.custom.transport") == 0) {
|
||||
sprintf(reply, "> %s", mqtt.getCustomTransport());
|
||||
} else if (strcmp(command, "get mqtt.custom.username") == 0) {
|
||||
sprintf(reply, "> %s", mqtt.getCustomUsername()[0] ? mqtt.getCustomUsername() : "-");
|
||||
} else if (strcmp(command, "get mqtt.custom.password") == 0) {
|
||||
@@ -2500,6 +2502,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
} else {
|
||||
strcpy(reply, "Err - bad mqtt.custom.port");
|
||||
}
|
||||
} else if (memcmp(command, "set mqtt.custom.transport ", 26) == 0) {
|
||||
if (mqtt.setCustomTransport(&command[26])) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - bad mqtt.custom.transport");
|
||||
}
|
||||
} else if (memcmp(command, "set mqtt.custom.username ", 25) == 0) {
|
||||
if (mqtt.setCustomUsername(&command[25])) {
|
||||
strcpy(reply, "OK");
|
||||
|
||||
@@ -188,6 +188,30 @@ releases:
|
||||
text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released."
|
||||
breaking_changes: []
|
||||
|
||||
- track: observer-eastmesh
|
||||
version: "2026.6.3"
|
||||
tag: "observer-eastmesh-v2026.6.3"
|
||||
date: "2026-06-09"
|
||||
previous_version: "2026.6.2"
|
||||
summary: "Adds secure WebSocket transport support for custom MQTT brokers from the Observer web panel and CLI."
|
||||
changes:
|
||||
- type: added
|
||||
area: mqtt
|
||||
text: "Added TCP/WSS transport selection for custom MQTT brokers while keeping custom broker authentication to username and password only."
|
||||
- type: fixed
|
||||
area: mqtt
|
||||
text: "Verified custom MQTT WSS connections with the ESP-IDF x509 root CA bundle and the fixed `/mqtt` WebSocket path."
|
||||
- type: fixed
|
||||
area: mqtt
|
||||
text: "Configured custom MQTT WSS using a full `wss://host:port/mqtt` URI so ESP-MQTT handles the WebSocket connection consistently."
|
||||
- type: changed
|
||||
area: cli
|
||||
text: "Updated `get mqtt.status` to show the custom MQTT transport and state, for example `custom:wss:up`."
|
||||
- type: docs
|
||||
area: docs
|
||||
text: "Documented custom MQTT TCP/WSS configuration in the web panel and custom CLI guides."
|
||||
breaking_changes: []
|
||||
|
||||
- track: observer-eastmesh-bridge-espnow
|
||||
version: "2026.5.1"
|
||||
tag: "observer-eastmesh-bridge-espnow-v2026.5.1"
|
||||
@@ -287,3 +311,27 @@ releases:
|
||||
area: web
|
||||
text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released."
|
||||
breaking_changes: []
|
||||
|
||||
- track: observer-eastmesh-bridge-espnow
|
||||
version: "2026.6.3"
|
||||
tag: "observer-eastmesh-bridge-espnow-v2026.6.3"
|
||||
date: "2026-06-09"
|
||||
previous_version: "2026.6.2"
|
||||
summary: "Adds secure WebSocket transport support for custom MQTT brokers in Observer ESP-NOW bridge builds."
|
||||
changes:
|
||||
- type: added
|
||||
area: mqtt
|
||||
text: "Added TCP/WSS transport selection for custom MQTT brokers while keeping custom broker authentication to username and password only."
|
||||
- type: fixed
|
||||
area: mqtt
|
||||
text: "Verified custom MQTT WSS connections with the ESP-IDF x509 root CA bundle and the fixed `/mqtt` WebSocket path."
|
||||
- type: fixed
|
||||
area: mqtt
|
||||
text: "Configured custom MQTT WSS using a full `wss://host:port/mqtt` URI so ESP-MQTT handles the WebSocket connection consistently."
|
||||
- type: changed
|
||||
area: cli
|
||||
text: "Updated `get mqtt.status` to show the custom MQTT transport and state, for example `custom:wss:up`."
|
||||
- type: docs
|
||||
area: docs
|
||||
text: "Documented custom MQTT TCP/WSS configuration in the web panel and custom CLI guides."
|
||||
breaking_changes: []
|
||||
|
||||
@@ -70,6 +70,9 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
|
||||
if (prefs.custom_port == 0) {
|
||||
prefs.custom_port = 1883;
|
||||
}
|
||||
if (prefs.custom_transport > 1) {
|
||||
prefs.custom_transport = 0;
|
||||
}
|
||||
prefs.status_interval_ms = kFixedStatusIntervalMs;
|
||||
prefs.enabled_mask &= 0x0F;
|
||||
return true;
|
||||
|
||||
@@ -33,7 +33,7 @@ struct MQTTPrefs {
|
||||
char custom_host[96];
|
||||
char custom_username[65];
|
||||
char custom_password[96];
|
||||
uint8_t reserved[1];
|
||||
uint8_t custom_transport;
|
||||
};
|
||||
|
||||
class MQTTPrefsStore {
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_system.h>
|
||||
#if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE) && !(defined(ARDUINO) && ESP_IDF_VERSION_MAJOR < 5)
|
||||
#include <esp_crt_bundle.h>
|
||||
#if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE)
|
||||
#include <esp_err.h>
|
||||
extern "C" esp_err_t esp_crt_bundle_attach(void* conf);
|
||||
#define MQTT_USE_CRT_BUNDLE 1
|
||||
#define MQTT_CRT_BUNDLE_ATTACH esp_crt_bundle_attach
|
||||
#endif
|
||||
@@ -214,6 +215,30 @@ uint16_t MQTTUplink::brokerPort(const BrokerState& broker) const {
|
||||
return 443;
|
||||
}
|
||||
|
||||
bool MQTTUplink::brokerUsesWss(const BrokerState& broker) const {
|
||||
if (broker.spec == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return !broker.spec->custom || _prefs.custom_transport == 1;
|
||||
}
|
||||
|
||||
const char* MQTTUplink::brokerUri(BrokerState& broker) const {
|
||||
if (broker.spec == nullptr) {
|
||||
return "";
|
||||
}
|
||||
if (broker.spec->uri != nullptr) {
|
||||
return broker.spec->uri;
|
||||
}
|
||||
if (brokerUsesWss(broker)) {
|
||||
snprintf(broker.uri, sizeof(broker.uri), "wss://%s:%u/mqtt", brokerHost(broker),
|
||||
static_cast<unsigned>(brokerPort(broker)));
|
||||
} else {
|
||||
snprintf(broker.uri, sizeof(broker.uri), "mqtt://%s:%u", brokerHost(broker),
|
||||
static_cast<unsigned>(brokerPort(broker)));
|
||||
}
|
||||
return broker.uri;
|
||||
}
|
||||
|
||||
bool MQTTUplink::isBrokerConfigured(const BrokerState& broker) const {
|
||||
if (broker.spec == nullptr) {
|
||||
return false;
|
||||
@@ -930,19 +955,27 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
|
||||
}
|
||||
|
||||
refreshBrokerState(broker);
|
||||
MQTT_LOG("%s mqtt init host=%s port=%u path=%s client_id=%s", broker.spec->label, brokerHost(broker),
|
||||
static_cast<unsigned>(brokerPort(broker)), broker.spec->custom ? "-" : "/mqtt", broker.client_id);
|
||||
const char* uri = brokerUsesWss(broker) ? brokerUri(broker) : nullptr;
|
||||
MQTT_LOG("%s mqtt init host=%s port=%u transport=%s path=%s uri=%s client_id=%s", broker.spec->label, brokerHost(broker),
|
||||
static_cast<unsigned>(brokerPort(broker)), brokerUsesWss(broker) ? "wss" : "tcp",
|
||||
brokerUsesWss(broker) ? "/mqtt" : "-", uri != nullptr ? uri : "-", broker.client_id);
|
||||
logMqttMemorySnapshot("init-pre", broker.spec->label);
|
||||
esp_mqtt_client_config_t cfg = {};
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
cfg.broker.address.hostname = brokerHost(broker);
|
||||
cfg.broker.address.port = brokerPort(broker);
|
||||
cfg.broker.address.transport = broker.spec->custom ? MQTT_TRANSPORT_OVER_TCP : MQTT_TRANSPORT_OVER_WSS;
|
||||
if (!broker.spec->custom) {
|
||||
cfg.broker.address.path = "/mqtt";
|
||||
if (brokerUsesWss(broker)) {
|
||||
cfg.broker.address.uri = uri;
|
||||
} else {
|
||||
cfg.broker.address.hostname = brokerHost(broker);
|
||||
cfg.broker.address.port = brokerPort(broker);
|
||||
cfg.broker.address.transport = MQTT_TRANSPORT_OVER_TCP;
|
||||
}
|
||||
if (broker.spec->custom) {
|
||||
cfg.credentials.authentication.password = _prefs.custom_password;
|
||||
if (brokerUsesWss(broker)) {
|
||||
#if defined(MQTT_USE_CRT_BUNDLE)
|
||||
cfg.broker.verification.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH;
|
||||
#endif
|
||||
}
|
||||
} else {
|
||||
#if defined(MQTT_USE_CRT_BUNDLE)
|
||||
cfg.broker.verification.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH;
|
||||
@@ -964,8 +997,13 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
|
||||
cfg.buffer.size = 768;
|
||||
cfg.buffer.out_size = 1280;
|
||||
#else
|
||||
cfg.host = brokerHost(broker);
|
||||
cfg.port = brokerPort(broker);
|
||||
if (brokerUsesWss(broker)) {
|
||||
cfg.uri = uri;
|
||||
} else {
|
||||
cfg.host = brokerHost(broker);
|
||||
cfg.port = brokerPort(broker);
|
||||
cfg.transport = MQTT_TRANSPORT_OVER_TCP;
|
||||
}
|
||||
cfg.username = broker.username;
|
||||
cfg.password = broker.spec->custom ? _prefs.custom_password : broker.token;
|
||||
cfg.client_id = broker.client_id;
|
||||
@@ -975,14 +1013,17 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
|
||||
cfg.reconnect_timeout_ms = 10000;
|
||||
cfg.network_timeout_ms = 10000;
|
||||
cfg.disable_auto_reconnect = true;
|
||||
cfg.transport = broker.spec->custom ? MQTT_TRANSPORT_OVER_TCP : MQTT_TRANSPORT_OVER_WSS;
|
||||
if (broker.spec->custom && brokerUsesWss(broker)) {
|
||||
#if defined(MQTT_USE_CRT_BUNDLE)
|
||||
cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH;
|
||||
#endif
|
||||
}
|
||||
if (!broker.spec->custom) {
|
||||
#if defined(MQTT_USE_CRT_BUNDLE)
|
||||
cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH;
|
||||
#else
|
||||
cfg.cert_pem = brokerCaCert(*broker.spec);
|
||||
#endif
|
||||
cfg.path = "/mqtt";
|
||||
}
|
||||
cfg.lwt_topic = broker.status_topic;
|
||||
cfg.lwt_msg = broker.offline_payload;
|
||||
@@ -1163,12 +1204,13 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const {
|
||||
};
|
||||
|
||||
snprintf(reply, reply_size,
|
||||
"> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s custom:%s status:%s tx:%s",
|
||||
"> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s custom:%s:%s status:%s tx:%s",
|
||||
(_network != nullptr && _network->isWifiConnected()) ? "up" : "down",
|
||||
(_network != nullptr && _network->hasTimeSync()) ? "up" : "wait",
|
||||
_prefs.iata,
|
||||
broker_state(kEastmeshBit), broker_state(kLetsmeshEuBit), broker_state(kLetsmeshUsBit),
|
||||
broker_state(kCustomBit), _prefs.status_enabled ? "on" : "off", _prefs.tx_enabled ? "on" : "off");
|
||||
getCustomTransport(), broker_state(kCustomBit), _prefs.status_enabled ? "on" : "off",
|
||||
_prefs.tx_enabled ? "on" : "off");
|
||||
}
|
||||
|
||||
bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) {
|
||||
@@ -1335,6 +1377,38 @@ bool MQTTUplink::setCustomPort(const char* port) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
const char* MQTTUplink::getCustomTransport() const {
|
||||
return _prefs.custom_transport == 1 ? "wss" : "tcp";
|
||||
}
|
||||
|
||||
bool MQTTUplink::setCustomTransport(const char* transport) {
|
||||
if (transport == nullptr) {
|
||||
return false;
|
||||
}
|
||||
uint8_t parsed;
|
||||
if (strcasecmp(transport, "tcp") == 0) {
|
||||
parsed = 0;
|
||||
} else if (strcasecmp(transport, "wss") == 0) {
|
||||
#if !defined(MQTT_USE_CRT_BUNDLE)
|
||||
return false;
|
||||
#endif
|
||||
parsed = 1;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
bool changed = _prefs.custom_transport != parsed;
|
||||
_prefs.custom_transport = parsed;
|
||||
bool saved = savePrefs();
|
||||
if (saved && changed) {
|
||||
for (BrokerState& broker : _brokers) {
|
||||
if (broker.spec != nullptr && broker.spec->bit == kCustomBit) {
|
||||
destroyBroker(broker);
|
||||
}
|
||||
}
|
||||
}
|
||||
return saved;
|
||||
}
|
||||
|
||||
bool MQTTUplink::setCustomUsername(const char* username) {
|
||||
if (username == nullptr) {
|
||||
return false;
|
||||
|
||||
@@ -67,6 +67,8 @@ public:
|
||||
const char* getCustomHost() const { return _prefs.custom_host; }
|
||||
bool setCustomPort(const char* port);
|
||||
uint16_t getCustomPort() const { return _prefs.custom_port; }
|
||||
bool setCustomTransport(const char* transport);
|
||||
const char* getCustomTransport() const;
|
||||
bool setCustomUsername(const char* username);
|
||||
const char* getCustomUsername() const { return _prefs.custom_username; }
|
||||
bool setCustomPassword(const char* password);
|
||||
@@ -104,6 +106,7 @@ private:
|
||||
char username[70];
|
||||
char* token;
|
||||
char client_id[48];
|
||||
char uri[144];
|
||||
char status_topic[128];
|
||||
char offline_payload[512];
|
||||
};
|
||||
@@ -144,6 +147,8 @@ private:
|
||||
bool preflightBroker(BrokerState& broker) const;
|
||||
const char* brokerHost(const BrokerState& broker) const;
|
||||
uint16_t brokerPort(const BrokerState& broker) const;
|
||||
bool brokerUsesWss(const BrokerState& broker) const;
|
||||
const char* brokerUri(BrokerState& broker) const;
|
||||
bool isBrokerConfigured(const BrokerState& broker) const;
|
||||
void formatTopic(char* dst, size_t dst_size, const char* leaf) const;
|
||||
void refreshIdentityStrings();
|
||||
|
||||
@@ -1042,6 +1042,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
<button id="saveCustomEndpointBtn" class="savebtn">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-card">
|
||||
<label class="label" for="mqttCustomTransport">Transport</label>
|
||||
<div class="mode-slider">
|
||||
<input id="mqttCustomTransport" type="range" min="0" max="1" step="1" value="0" aria-label="Custom MQTT transport">
|
||||
<div class="mode-labels two" aria-hidden="true">
|
||||
<div class="mode-label" data-custom-transport-label="tcp">TCP</div>
|
||||
<div class="mode-label" data-custom-transport-label="wss">WSS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="field-card">
|
||||
<label class="label" for="mqttCustomUsername">Username</label>
|
||||
@@ -2841,6 +2851,30 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
input.value = host && host !== "-" ? `${host}:${port}` : "";
|
||||
}
|
||||
}
|
||||
function refreshCustomTransportUi(transport) {
|
||||
const mode = transport === "wss" ? "wss" : "tcp";
|
||||
const slider = document.getElementById("mqttCustomTransport");
|
||||
if (slider) {
|
||||
slider.value = mode === "wss" ? "1" : "0";
|
||||
}
|
||||
document.querySelectorAll("[data-custom-transport-label]").forEach((label) => {
|
||||
label.classList.toggle("active", label.dataset.customTransportLabel === mode);
|
||||
});
|
||||
}
|
||||
async function loadCustomTransport(options = {}) {
|
||||
const result = await runCommand("get mqtt.custom.transport", options);
|
||||
if (!result.ok) return;
|
||||
refreshCustomTransportUi(parseReplyValue(result.text));
|
||||
}
|
||||
async function setCustomTransport(transport) {
|
||||
const mode = transport === "wss" ? "wss" : "tcp";
|
||||
const result = await runCommand("set mqtt.custom.transport " + mode);
|
||||
if (!result.ok) {
|
||||
await loadCustomTransport({ recordHistory:false });
|
||||
return;
|
||||
}
|
||||
refreshCustomTransportUi(mode);
|
||||
}
|
||||
function parseCustomEndpoint(value) {
|
||||
const endpoint = String(value || "").trim();
|
||||
const separator = endpoint.lastIndexOf(":");
|
||||
@@ -3052,6 +3086,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
document.getElementById("refreshCustomEndpointBtn").onclick = () => loadCustomEndpoint();
|
||||
document.getElementById("saveCustomEndpointBtn").onclick = () => saveCustomEndpoint();
|
||||
const customTransportSlider = document.getElementById("mqttCustomTransport");
|
||||
if (customTransportSlider) {
|
||||
customTransportSlider.addEventListener("input", () => {
|
||||
customTransportSlider.value = (Number.parseInt(customTransportSlider.value, 10) || 0) >= 1 ? "1" : "0";
|
||||
refreshCustomTransportUi(customTransportSlider.value === "1" ? "wss" : "tcp");
|
||||
});
|
||||
customTransportSlider.addEventListener("change", () => {
|
||||
setCustomTransport((Number.parseInt(customTransportSlider.value, 10) || 0) >= 1 ? "wss" : "tcp");
|
||||
});
|
||||
}
|
||||
async function setLetsmeshMode(mode) {
|
||||
const eastmesh = document.getElementById("mqttEastmeshAu");
|
||||
if (mode === "both" && eastmesh && eastmesh.checked) {
|
||||
@@ -3260,6 +3304,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet),
|
||||
() => loadBrokerState("get mqtt.custom", "mqttCustom", quiet),
|
||||
() => loadCustomEndpoint(quiet),
|
||||
() => loadCustomTransport(quiet),
|
||||
() => loadField("get mqtt.custom.username", "mqttCustomUsername", null, quiet)
|
||||
]);
|
||||
if (!isCurrentPageLoad(generation)) return;
|
||||
|
||||
Reference in New Issue
Block a user