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
|
### 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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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: []
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user