feat: add custom MQTT WSS transport

This commit is contained in:
Jared Dohrman
2026-06-09 19:09:33 +10:00
parent ec88d300c1
commit 9bce46c298
9 changed files with 208 additions and 20 deletions
+6 -2
View File
@@ -34,7 +34,7 @@ No-argument `get` commands must be entered exactly as shown.
### MQTT Status And Routing ### 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.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_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. - `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>` - `set mqtt.custom.host <host>`
- `get mqtt.custom.port` - `get mqtt.custom.port`
- `set mqtt.custom.port <port>` - `set mqtt.custom.port <port>`
- `get mqtt.custom.transport`: shows `tcp` or `wss`.
- `set mqtt.custom.transport tcp|wss`
- `get mqtt.custom.username` - `get mqtt.custom.username`
- `set mqtt.custom.username <username>` - `set mqtt.custom.username <username>`
- `get mqtt.custom.password`: shows `set` when a custom password is configured. - `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` - new observer installs default `mqtt.iata` to `UNSET`
- `letsmesh-eu` and `letsmesh-us` remain off by default unless already configured in saved prefs - `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 - 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 - 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 - 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 - 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
+3 -2
View File
@@ -254,7 +254,7 @@ This section includes:
- `mqtt.owner`: owner public key. - `mqtt.owner`: owner public key.
- `mqtt.email`: owner contact email. - `mqtt.email`: owner contact email.
- MQTT server toggles: `eastmesh-au`, `letsmesh-eu`, `letsmesh-us`, and custom MQTT. - 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. `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 - while `mqtt.iata` is `UNSET`, enabled MQTT brokers do not attempt to connect
- the current MQTT server states are loaded when the page opens - the current MQTT server states are loaded when the page opens
- you can toggle each MQTT server on or off from this panel - 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 - 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 - 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 - at most two MQTT brokers can be enabled at once
+8
View File
@@ -2430,6 +2430,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sprintf(reply, "> %s", mqtt.getCustomHost()[0] ? mqtt.getCustomHost() : "-"); sprintf(reply, "> %s", mqtt.getCustomHost()[0] ? mqtt.getCustomHost() : "-");
} else if (strcmp(command, "get mqtt.custom.port") == 0) { } else if (strcmp(command, "get mqtt.custom.port") == 0) {
sprintf(reply, "> %u", static_cast<unsigned>(mqtt.getCustomPort())); 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) { } else if (strcmp(command, "get mqtt.custom.username") == 0) {
sprintf(reply, "> %s", mqtt.getCustomUsername()[0] ? mqtt.getCustomUsername() : "-"); sprintf(reply, "> %s", mqtt.getCustomUsername()[0] ? mqtt.getCustomUsername() : "-");
} else if (strcmp(command, "get mqtt.custom.password") == 0) { } 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 { } else {
strcpy(reply, "Err - bad mqtt.custom.port"); 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) { } else if (memcmp(command, "set mqtt.custom.username ", 25) == 0) {
if (mqtt.setCustomUsername(&command[25])) { if (mqtt.setCustomUsername(&command[25])) {
strcpy(reply, "OK"); strcpy(reply, "OK");
+48
View File
@@ -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." text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released."
breaking_changes: [] 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 - track: observer-eastmesh-bridge-espnow
version: "2026.5.1" version: "2026.5.1"
tag: "observer-eastmesh-bridge-espnow-v2026.5.1" tag: "observer-eastmesh-bridge-espnow-v2026.5.1"
@@ -287,3 +311,27 @@ releases:
area: web area: web
text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released." text: "Hardened OTA startup by binding before reporting success and falling back to alternate ports when port 80 is still being released."
breaking_changes: [] 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: []
+3
View File
@@ -70,6 +70,9 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
if (prefs.custom_port == 0) { if (prefs.custom_port == 0) {
prefs.custom_port = 1883; prefs.custom_port = 1883;
} }
if (prefs.custom_transport > 1) {
prefs.custom_transport = 0;
}
prefs.status_interval_ms = kFixedStatusIntervalMs; prefs.status_interval_ms = kFixedStatusIntervalMs;
prefs.enabled_mask &= 0x0F; prefs.enabled_mask &= 0x0F;
return true; return true;
+1 -1
View File
@@ -33,7 +33,7 @@ struct MQTTPrefs {
char custom_host[96]; char custom_host[96];
char custom_username[65]; char custom_username[65];
char custom_password[96]; char custom_password[96];
uint8_t reserved[1]; uint8_t custom_transport;
}; };
class MQTTPrefsStore { class MQTTPrefsStore {
+89 -15
View File
@@ -11,8 +11,9 @@
#include <esp_idf_version.h> #include <esp_idf_version.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <esp_system.h> #include <esp_system.h>
#if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE) && !(defined(ARDUINO) && ESP_IDF_VERSION_MAJOR < 5) #if defined(CONFIG_MBEDTLS_CERTIFICATE_BUNDLE)
#include <esp_crt_bundle.h> #include <esp_err.h>
extern "C" esp_err_t esp_crt_bundle_attach(void* conf);
#define MQTT_USE_CRT_BUNDLE 1 #define MQTT_USE_CRT_BUNDLE 1
#define MQTT_CRT_BUNDLE_ATTACH esp_crt_bundle_attach #define MQTT_CRT_BUNDLE_ATTACH esp_crt_bundle_attach
#endif #endif
@@ -214,6 +215,30 @@ uint16_t MQTTUplink::brokerPort(const BrokerState& broker) const {
return 443; 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 { bool MQTTUplink::isBrokerConfigured(const BrokerState& broker) const {
if (broker.spec == nullptr) { if (broker.spec == nullptr) {
return false; return false;
@@ -930,19 +955,27 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
} }
refreshBrokerState(broker); refreshBrokerState(broker);
MQTT_LOG("%s mqtt init host=%s port=%u path=%s client_id=%s", broker.spec->label, brokerHost(broker), const char* uri = brokerUsesWss(broker) ? brokerUri(broker) : nullptr;
static_cast<unsigned>(brokerPort(broker)), broker.spec->custom ? "-" : "/mqtt", broker.client_id); 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); 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 = brokerHost(broker); if (brokerUsesWss(broker)) {
cfg.broker.address.port = brokerPort(broker); cfg.broker.address.uri = uri;
cfg.broker.address.transport = broker.spec->custom ? MQTT_TRANSPORT_OVER_TCP : MQTT_TRANSPORT_OVER_WSS; } else {
if (!broker.spec->custom) { cfg.broker.address.hostname = brokerHost(broker);
cfg.broker.address.path = "/mqtt"; cfg.broker.address.port = brokerPort(broker);
cfg.broker.address.transport = MQTT_TRANSPORT_OVER_TCP;
} }
if (broker.spec->custom) { if (broker.spec->custom) {
cfg.credentials.authentication.password = _prefs.custom_password; 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 { } else {
#if defined(MQTT_USE_CRT_BUNDLE) #if defined(MQTT_USE_CRT_BUNDLE)
cfg.broker.verification.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; 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.size = 768;
cfg.buffer.out_size = 1280; cfg.buffer.out_size = 1280;
#else #else
cfg.host = brokerHost(broker); if (brokerUsesWss(broker)) {
cfg.port = brokerPort(broker); cfg.uri = uri;
} else {
cfg.host = brokerHost(broker);
cfg.port = brokerPort(broker);
cfg.transport = MQTT_TRANSPORT_OVER_TCP;
}
cfg.username = broker.username; cfg.username = broker.username;
cfg.password = broker.spec->custom ? _prefs.custom_password : broker.token; cfg.password = broker.spec->custom ? _prefs.custom_password : broker.token;
cfg.client_id = broker.client_id; 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.reconnect_timeout_ms = 10000;
cfg.network_timeout_ms = 10000; cfg.network_timeout_ms = 10000;
cfg.disable_auto_reconnect = true; 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 (!broker.spec->custom) {
#if defined(MQTT_USE_CRT_BUNDLE) #if defined(MQTT_USE_CRT_BUNDLE)
cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH; cfg.crt_bundle_attach = MQTT_CRT_BUNDLE_ATTACH;
#else #else
cfg.cert_pem = brokerCaCert(*broker.spec); cfg.cert_pem = brokerCaCert(*broker.spec);
#endif #endif
cfg.path = "/mqtt";
} }
cfg.lwt_topic = broker.status_topic; cfg.lwt_topic = broker.status_topic;
cfg.lwt_msg = broker.offline_payload; cfg.lwt_msg = broker.offline_payload;
@@ -1163,12 +1204,13 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const {
}; };
snprintf(reply, reply_size, 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->isWifiConnected()) ? "up" : "down",
(_network != nullptr && _network->hasTimeSync()) ? "up" : "wait", (_network != nullptr && _network->hasTimeSync()) ? "up" : "wait",
_prefs.iata, _prefs.iata,
broker_state(kEastmeshBit), broker_state(kLetsmeshEuBit), broker_state(kLetsmeshUsBit), 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) { bool MQTTUplink::setEndpointEnabled(uint8_t bit, bool enabled) {
@@ -1335,6 +1377,38 @@ bool MQTTUplink::setCustomPort(const char* port) {
return saved; 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) { bool MQTTUplink::setCustomUsername(const char* username) {
if (username == nullptr) { if (username == nullptr) {
return false; return false;
+5
View File
@@ -67,6 +67,8 @@ public:
const char* getCustomHost() const { return _prefs.custom_host; } const char* getCustomHost() const { return _prefs.custom_host; }
bool setCustomPort(const char* port); bool setCustomPort(const char* port);
uint16_t getCustomPort() const { return _prefs.custom_port; } uint16_t getCustomPort() const { return _prefs.custom_port; }
bool setCustomTransport(const char* transport);
const char* getCustomTransport() const;
bool setCustomUsername(const char* username); bool setCustomUsername(const char* username);
const char* getCustomUsername() const { return _prefs.custom_username; } const char* getCustomUsername() const { return _prefs.custom_username; }
bool setCustomPassword(const char* password); bool setCustomPassword(const char* password);
@@ -104,6 +106,7 @@ private:
char username[70]; char username[70];
char* token; char* token;
char client_id[48]; char client_id[48];
char uri[144];
char status_topic[128]; char status_topic[128];
char offline_payload[512]; char offline_payload[512];
}; };
@@ -144,6 +147,8 @@ private:
bool preflightBroker(BrokerState& broker) const; bool preflightBroker(BrokerState& broker) const;
const char* brokerHost(const BrokerState& broker) const; const char* brokerHost(const BrokerState& broker) const;
uint16_t brokerPort(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; bool isBrokerConfigured(const BrokerState& broker) const;
void formatTopic(char* dst, size_t dst_size, const char* leaf) const; void formatTopic(char* dst, size_t dst_size, const char* leaf) const;
void refreshIdentityStrings(); void refreshIdentityStrings();
+45
View File
@@ -1042,6 +1042,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<button id="saveCustomEndpointBtn" class="savebtn">Save</button> <button id="saveCustomEndpointBtn" class="savebtn">Save</button>
</div> </div>
</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="row">
<div class="field-card"> <div class="field-card">
<label class="label" for="mqttCustomUsername">Username</label> <label class="label" for="mqttCustomUsername">Username</label>
@@ -2841,6 +2851,30 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
input.value = host && host !== "-" ? `${host}:${port}` : ""; 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) { function parseCustomEndpoint(value) {
const endpoint = String(value || "").trim(); const endpoint = String(value || "").trim();
const separator = endpoint.lastIndexOf(":"); const separator = endpoint.lastIndexOf(":");
@@ -3052,6 +3086,16 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
} }
document.getElementById("refreshCustomEndpointBtn").onclick = () => loadCustomEndpoint(); document.getElementById("refreshCustomEndpointBtn").onclick = () => loadCustomEndpoint();
document.getElementById("saveCustomEndpointBtn").onclick = () => saveCustomEndpoint(); 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) { 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) {
@@ -3260,6 +3304,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet), () => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet),
() => loadBrokerState("get mqtt.custom", "mqttCustom", quiet), () => loadBrokerState("get mqtt.custom", "mqttCustom", quiet),
() => loadCustomEndpoint(quiet), () => loadCustomEndpoint(quiet),
() => loadCustomTransport(quiet),
() => loadField("get mqtt.custom.username", "mqttCustomUsername", null, quiet) () => loadField("get mqtt.custom.username", "mqttCustomUsername", null, quiet)
]); ]);
if (!isCurrentPageLoad(generation)) return; if (!isCurrentPageLoad(generation)) return;