Added support for MeshCoreTel broker.

This commit is contained in:
Valentin V. Bartenev
2026-04-27 03:15:00 +03:00
rodzic 41a29975a4
commit f1912ff184
6 zmienionych plików z 216 dodań i 46 usunięć
+5 -2
Wyświetl plik
@@ -14,7 +14,7 @@ These commands are available on `*_repeater_mqtt` firmware targets.
- `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.iata`: shows the IATA/location code used in MQTT topics. - `get mqtt.iata`: shows the IATA/location code used in MQTT topics.
- `set mqtt.iata <code>`: sets the IATA/location code, for example `MEL`. - `set mqtt.iata <code>`: sets the IATA/location code, for example `MOW`.
- `set mqtt.iata UNSET`: marks MQTT IATA as not configured yet. While it is `UNSET`, enabled MQTT brokers stay disconnected until a real code is saved. - `set mqtt.iata UNSET`: marks MQTT IATA as not configured yet. While it is `UNSET`, enabled MQTT brokers stay disconnected until a real code is saved.
### MQTT Identity ### MQTT Identity
@@ -38,6 +38,9 @@ These commands are available on `*_repeater_mqtt` firmware targets.
### MQTT Endpoints ### MQTT Endpoints
- `set mqtt.meshcoretel on|off`
- `get mqtt.meshcoretel`
- `set mqtt.letsmesh-us on|off`
- `get mqtt.eastmesh-au` - `get mqtt.eastmesh-au`
- `set mqtt.eastmesh-au on|off` - `set mqtt.eastmesh-au on|off`
- `get mqtt.letsmesh-eu` - `get mqtt.letsmesh-eu`
@@ -49,7 +52,7 @@ Notes:
- new repeater MQTT installs default `mqtt.iata` to `UNSET` - new repeater MQTT 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`, `eastmesh-au`, `letsmesh-eu`, and `letsmesh-us` will not connect even if they are toggled on - if `mqtt.iata` is `UNSET`, `meshcoretel`, `eastmesh-au`, `letsmesh-eu`, and `letsmesh-us` will not connect even if they are toggled on
Legacy dotted aliases are also accepted: Legacy dotted aliases are also accepted:
+8
Wyświetl plik
@@ -2308,6 +2308,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x02) ? "on" : "off"); sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x02) ? "on" : "off");
} else if (memcmp(command, "get mqtt.letsmesh-us", 21) == 0 || memcmp(command, "get mqtt.letsmesh.us", 21) == 0) { } else if (memcmp(command, "get mqtt.letsmesh-us", 21) == 0 || memcmp(command, "get mqtt.letsmesh.us", 21) == 0) {
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x04) ? "on" : "off"); sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x04) ? "on" : "off");
} else if (memcmp(command, "get mqtt.meshcoretel", 20) == 0) {
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x08) ? "on" : "off");
} else if (memcmp(command, "set mqtt.tx ", 12) == 0) { } else if (memcmp(command, "set mqtt.tx ", 12) == 0) {
mqtt.setTxEnabled(memcmp(&command[12], "on", 2) == 0); mqtt.setTxEnabled(memcmp(&command[12], "on", 2) == 0);
strcpy(reply, "OK"); strcpy(reply, "OK");
@@ -2356,6 +2358,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else { } else {
strcpy(reply, "Err - max 2 mqtt brokers"); strcpy(reply, "Err - max 2 mqtt brokers");
} }
} else if (memcmp(command, "set mqtt.meshcoretel ", 21) == 0) {
if (mqtt.setEndpointEnabled(0x08, memcmp(&command[21], "on", 2) == 0)) {
strcpy(reply, "OK");
} else {
strcpy(reply, "Err - max 2 mqtt brokers");
}
#endif #endif
} else{ } else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands _cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
+1 -1
Wyświetl plik
@@ -67,7 +67,7 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
StrHelper::strncpy(prefs.iata, MQTT_DEFAULT_IATA, sizeof(prefs.iata)); StrHelper::strncpy(prefs.iata, MQTT_DEFAULT_IATA, sizeof(prefs.iata));
} }
prefs.status_interval_ms = kFixedStatusIntervalMs; prefs.status_interval_ms = kFixedStatusIntervalMs;
prefs.enabled_mask &= 0x07; prefs.enabled_mask &= 0x0F;
return true; return true;
} }
+50 -30
Wyświetl plik
@@ -17,7 +17,7 @@
#include <time.h> #include <time.h>
#ifndef FIRMWARE_VERSION #ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.14.1" #define FIRMWARE_VERSION "v1.15.0 (VBart EastMesh)"
#endif #endif
#ifndef FIRMWARE_BUILD_DATE #ifndef FIRMWARE_BUILD_DATE
@@ -95,12 +95,15 @@ void logMqttMemorySnapshot(const char*, const char* = nullptr) {
} }
const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[3] = { const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[4] = {
{"eastmesh-au", "eastmesh-au", "mqtt2.eastmesh.au", "wss://mqtt2.eastmesh.au:443/mqtt", kEastmeshBit}, {"eastmesh-au", "eastmesh-au", "mqtt2.eastmesh.au", "wss://mqtt2.eastmesh.au:443/mqtt",
kEastmeshBit, false, nullptr, nullptr},
{"letsmesh-eu", "letsmesh-eu", "mqtt-eu-v1.letsmesh.net", "wss://mqtt-eu-v1.letsmesh.net:443/mqtt", {"letsmesh-eu", "letsmesh-eu", "mqtt-eu-v1.letsmesh.net", "wss://mqtt-eu-v1.letsmesh.net:443/mqtt",
kLetsmeshEuBit}, kLetsmeshEuBit, false, nullptr, nullptr},
{"letsmesh-us", "letsmesh-us", "mqtt-us-v1.letsmesh.net", "wss://mqtt-us-v1.letsmesh.net:443/mqtt", {"letsmesh-us", "letsmesh-us", "mqtt-us-v1.letsmesh.net", "wss://mqtt-us-v1.letsmesh.net:443/mqtt",
kLetsmeshUsBit}, kLetsmeshUsBit, false, nullptr, nullptr},
{"meshcoretel", "meshcoretel", "meshcoretel.ru", "mqtt://meshcoretel.ru:1883",
kMeshcoretelBit, true, "meshcore", "meshcore"},
}; };
MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity) MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
@@ -109,7 +112,7 @@ MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
{ {
memset(_device_id, 0, sizeof(_device_id)); memset(_device_id, 0, sizeof(_device_id));
MQTTPrefsStore::setDefaults(_prefs); MQTTPrefsStore::setDefaults(_prefs);
for (size_t i = 0; i < 3; ++i) { for (size_t i = 0; i < 4; ++i) {
memset(&_brokers[i], 0, sizeof(_brokers[i])); memset(&_brokers[i], 0, sizeof(_brokers[i]));
_brokers[i].spec = &kBrokerSpecs[i]; _brokers[i].spec = &kBrokerSpecs[i];
} }
@@ -125,7 +128,7 @@ bool MQTTUplink::savePrefs() {
} }
bool MQTTUplink::hasEnabledBroker() const { bool MQTTUplink::hasEnabledBroker() const {
return (_prefs.enabled_mask & 0x07) != 0; return (_prefs.enabled_mask & 0x0F) != 0;
} }
bool MQTTUplink::isUnsetIataValue(const char* iata) { bool MQTTUplink::isUnsetIataValue(const char* iata) {
@@ -277,7 +280,11 @@ void MQTTUplink::refreshBrokerIdentity(BrokerState& broker) {
if (broker.spec == nullptr) { if (broker.spec == nullptr) {
return; return;
} }
snprintf(broker.username, sizeof(broker.username), "v1_%s", _device_id); if (broker.spec->username != nullptr) {
snprintf(broker.username, sizeof(broker.username), "%s", broker.spec->username);
} else {
snprintf(broker.username, sizeof(broker.username), "v1_%s", _device_id);
}
snprintf(broker.client_id, sizeof(broker.client_id), "mqtt_%s-%.6s", broker.spec->key, _device_id); snprintf(broker.client_id, sizeof(broker.client_id), "mqtt_%s-%.6s", broker.spec->key, _device_id);
formatTopic(broker.status_topic, sizeof(broker.status_topic), "status"); formatTopic(broker.status_topic, sizeof(broker.status_topic), "status");
} }
@@ -621,9 +628,11 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
return; return;
} }
time_t now = time(nullptr); if (!broker.spec->plain_tcp) {
if (broker.client != nullptr && broker.token_expires_at > 0 && now + kTokenRefreshSlackSecs >= broker.token_expires_at) { time_t now = time(nullptr);
destroyBroker(broker, false); if (broker.client != nullptr && broker.token_expires_at > 0 && now + kTokenRefreshSlackSecs >= broker.token_expires_at) {
destroyBroker(broker, false);
}
} }
unsigned long now_ms = millis(); unsigned long now_ms = millis();
@@ -646,26 +655,32 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
broker.last_connect_attempt = now_ms; broker.last_connect_attempt = now_ms;
broker.reconnect_pending = false; broker.reconnect_pending = false;
if (!refreshToken(broker)) { if (!broker.spec->plain_tcp && !refreshToken(broker)) {
broker.reconnect_pending = true; broker.reconnect_pending = true;
broker.next_connect_attempt = now_ms + kBrokerRetryBaseMillis; broker.next_connect_attempt = now_ms + kBrokerRetryBaseMillis;
return; return;
} }
refreshBrokerState(broker); refreshBrokerState(broker);
MQTT_LOG("%s mqtt init host=%s port=%d path=%s client_id=%s", broker.spec->label, broker.spec->host, 443, "/mqtt", MQTT_LOG("%s mqtt init host=%s uri=%s plain_tcp=%d client_id=%s",
broker.client_id); broker.spec->label, broker.spec->host, broker.spec->uri,
broker.spec->plain_tcp ? 1 : 0, 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 = broker.spec->host; if (broker.spec->plain_tcp) {
cfg.broker.address.port = 443; cfg.broker.address.uri = broker.spec->uri;
cfg.broker.address.transport = MQTT_TRANSPORT_OVER_WSS; cfg.credentials.authentication.password = broker.spec->password;
cfg.broker.address.path = "/mqtt"; } else {
cfg.broker.verification.certificate = mqtt_ca_certs::kCombinedPem; cfg.broker.address.hostname = broker.spec->host;
cfg.broker.address.port = 443;
cfg.broker.address.transport = MQTT_TRANSPORT_OVER_WSS;
cfg.broker.address.path = "/mqtt";
cfg.broker.verification.certificate = mqtt_ca_certs::kCombinedPem;
cfg.credentials.authentication.password = broker.token;
}
cfg.credentials.username = broker.username; cfg.credentials.username = broker.username;
cfg.credentials.client_id = broker.client_id; cfg.credentials.client_id = broker.client_id;
cfg.credentials.authentication.password = broker.token;
cfg.session.keepalive = 30; cfg.session.keepalive = 30;
cfg.session.last_will.topic = broker.status_topic; cfg.session.last_will.topic = broker.status_topic;
cfg.session.last_will.msg = broker.offline_payload; cfg.session.last_will.msg = broker.offline_payload;
@@ -677,10 +692,18 @@ 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 = broker.spec->host; if (broker.spec->plain_tcp) {
cfg.port = 443; cfg.uri = broker.spec->uri;
cfg.password = broker.spec->password;
} else {
cfg.host = broker.spec->host;
cfg.port = 443;
cfg.transport = MQTT_TRANSPORT_OVER_WSS;
cfg.path = "/mqtt";
cfg.cert_pem = mqtt_ca_certs::kCombinedPem;
cfg.password = broker.token;
}
cfg.username = broker.username; cfg.username = broker.username;
cfg.password = broker.token;
cfg.client_id = broker.client_id; cfg.client_id = broker.client_id;
cfg.keepalive = 30; cfg.keepalive = 30;
cfg.buffer_size = 768; cfg.buffer_size = 768;
@@ -688,13 +711,10 @@ 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 = MQTT_TRANSPORT_OVER_WSS;
cfg.cert_pem = mqtt_ca_certs::kCombinedPem;
cfg.lwt_topic = broker.status_topic; cfg.lwt_topic = broker.status_topic;
cfg.lwt_msg = broker.offline_payload; cfg.lwt_msg = broker.offline_payload;
cfg.lwt_qos = 1; cfg.lwt_qos = 1;
cfg.lwt_retain = 1; cfg.lwt_retain = 1;
cfg.path = "/mqtt";
#endif #endif
broker.client = esp_mqtt_client_init(&cfg); broker.client = esp_mqtt_client_init(&cfg);
@@ -721,7 +741,7 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
void MQTTUplink::begin(FILESYSTEM* fs) { void MQTTUplink::begin(FILESYSTEM* fs) {
_fs = fs; _fs = fs;
MQTTPrefsStore::load(_fs, _prefs); MQTTPrefsStore::load(_fs, _prefs);
uint8_t normalized_mask = normalizeEnabledMask(_prefs.enabled_mask & 0x07); uint8_t normalized_mask = normalizeEnabledMask(_prefs.enabled_mask & 0x0F);
if (normalized_mask != _prefs.enabled_mask) { if (normalized_mask != _prefs.enabled_mask) {
_prefs.enabled_mask = normalized_mask; _prefs.enabled_mask = normalized_mask;
savePrefs(); savePrefs();
@@ -865,16 +885,16 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const {
return "retry"; return "retry";
}; };
snprintf(reply, reply_size, "> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s status:%s tx:%s", snprintf(reply, reply_size, "> wifi:%s ntp:%s iata:%s meshcoretel:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%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(kMeshcoretelBit), broker_state(kEastmeshBit), broker_state(kLetsmeshEuBit), broker_state(kLetsmeshUsBit),
_prefs.status_enabled ? "on" : "off", _prefs.tx_enabled ? "on" : "off"); _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) {
uint8_t next_mask = _prefs.enabled_mask & 0x07; uint8_t next_mask = _prefs.enabled_mask & 0x0F;
if (enabled) { if (enabled) {
next_mask = normalizeEnabledMask(next_mask | bit); next_mask = normalizeEnabledMask(next_mask | bit);
if ((next_mask & bit) == 0) { if ((next_mask & bit) == 0) {
+6 -2
Wyświetl plik
@@ -74,6 +74,9 @@ private:
const char* host; const char* host;
const char* uri; const char* uri;
uint8_t bit; uint8_t bit;
bool plain_tcp;
const char* username;
const char* password;
}; };
struct BrokerState { struct BrokerState {
@@ -110,11 +113,12 @@ private:
static constexpr uint8_t kEastmeshBit = 0x01; static constexpr uint8_t kEastmeshBit = 0x01;
static constexpr uint8_t kLetsmeshEuBit = 0x02; static constexpr uint8_t kLetsmeshEuBit = 0x02;
static constexpr uint8_t kLetsmeshUsBit = 0x04; static constexpr uint8_t kLetsmeshUsBit = 0x04;
static constexpr uint8_t kMeshcoretelBit = 0x08;
static constexpr uint8_t kMaxEnabledBrokers = 2; static constexpr uint8_t kMaxEnabledBrokers = 2;
static const BrokerSpec kBrokerSpecs[3]; static const BrokerSpec kBrokerSpecs[4];
static bool isUnsetIataValue(const char* iata); static bool isUnsetIataValue(const char* iata);
BrokerState _brokers[3]; BrokerState _brokers[4];
static void handleMqttEvent(void* handler_args, esp_event_base_t base, int32_t event_id, void* event_data); static void handleMqttEvent(void* handler_args, esp_event_base_t base, int32_t event_id, void* event_data);
bool hasEnabledBroker() const; bool hasEnabledBroker() const;
+146 -11
Wyświetl plik
@@ -828,6 +828,35 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
<optgroup label="Configuration"> <optgroup label="Configuration">
<option value="UNSET">UNSET - To be configured</option> <option value="UNSET">UNSET - To be configured</option>
</optgroup> </optgroup>
<optgroup label="Russia">
<option value="BSK">BSK - Biysk</option>
<option value="CEE">CEE - Cherepovets</option>
<option value="CEK">CEK - Chelyabinsk</option>
<option value="GOJ">GOJ - Nizhny Novgorod</option>
<option value="IKT">IKT - Irkutsk</option>
<option value="IWA">IWA - Ivanovo</option>
<option value="KHV">KHV - Khabarovsk</option>
<option value="KLD">KLD - Tver</option>
<option value="KLF">KLF - Kaluga</option>
<option value="KVX">KVX - Kirov</option>
<option value="KZN">KZN - Kazan</option>
<option value="LPK">LPK - Lipetsk</option>
<option value="MOW">MOW - Moscow</option>
<option value="OVB">OVB - Novosibirsk</option>
<option value="ROV">ROV - Rostov-on-Don</option>
<option value="RZN">RZN - Ryazan</option>
<option value="SVX">SVX - Yekaterinburg</option>
<option value="TBW">TBW - Tambov</option>
<option value="TYA">TYA - Tula</option>
<option value="UFA">UFA - Ufa</option>
<option value="VOG">VOG - Volgograd</option>
<option value="VOZ">VOZ - Voronezh</option>
<option value="VSG">VSG - Lugansk</option>
<option value="VVO">VVO - Vladivostok</option>
</optgroup>
<optgroup label="France">
<option value="CHR">CHR - Chateauroux</option>
</optgroup>
<optgroup label="ACT"> <optgroup label="ACT">
<option value="CBR">CBR - Canberra</option> <option value="CBR">CBR - Canberra</option>
</optgroup> </optgroup>
@@ -913,7 +942,30 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
</div> </div>
<div class="field-card"> <div class="field-card">
<label class="label">MQTT Servers</label> <label class="label">MQTT Servers</label>
<div class="broker-stack"> <div class="broker-stack" style="grid-template-columns:minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)">
<div class="broker-group">
<div class="broker-group-title">Meshcoretel</div>
<div class="broker-grid single">
<div class="broker-card">
<div class="broker-mode">
<div class="broker-row">
<div class="broker-copy">
<div class="broker-title">Meshcoretel RU</div>
<div class="broker-state" id="mqttMeshcoretelState">Off</div>
</div>
</div>
<div class="mode-slider">
<input id="mqttMeshcoretelMode" type="range" min="0" max="1" step="1" value="0" aria-label="Meshcoretel mode">
<div class="mode-labels two" aria-hidden="true">
<div class="mode-label" data-meshcoretel-label="off">Off</div>
<div class="mode-label" data-meshcoretel-label="on">On</div>
</div>
</div>
<input id="mqttMeshcoretel" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
</div>
</div>
</div>
</div>
<div class="broker-group"> <div class="broker-group">
<div class="broker-group-title">EastMesh</div> <div class="broker-group-title">EastMesh</div>
<div class="broker-grid single"> <div class="broker-grid single">
@@ -964,7 +1016,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
</div> </div>
</div> </div>
</div> </div>
<div class="panel-note">If EastMesh is enabled, use only one LetsMesh broker. Enable both LetsMesh brokers only when EastMesh is off.</div> <div class="panel-note">Only two brokers can be enabled at the same time.</div>
<div id="mqttBrokerWarning" class="panel-warning"></div> <div id="mqttBrokerWarning" class="panel-warning"></div>
</div> </div>
</div> </div>
@@ -2074,7 +2126,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
} }
if (inlineWarning) { if (inlineWarning) {
inlineWarning.textContent = showWarning inlineWarning.textContent = showWarning
? "MQTT IATA is unset. Set it before enabling EastMesh or LetsMesh brokers." ? "MQTT IATA is unset. Set it before enabling any MQTT brokers."
: ""; : "";
} }
} }
@@ -2223,13 +2275,25 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
function clampLetsmeshModeIndex(index) { function clampLetsmeshModeIndex(index) {
const eastmesh = document.getElementById("mqttEastmeshAu"); const eastmesh = document.getElementById("mqttEastmeshAu");
const eastmeshEnabled = !!(eastmesh && eastmesh.checked); const eastmeshEnabled = !!(eastmesh && eastmesh.checked);
const meshcoretel = document.getElementById("mqttMeshcoretel");
const meshcoretelEnabled = !!(meshcoretel && meshcoretel.checked);
const bounded = Math.max(0, Math.min(3, index)); const bounded = Math.max(0, Math.min(3, index));
return eastmeshEnabled && bounded === 3 ? 1 : bounded; if ((eastmeshEnabled || meshcoretelEnabled) && bounded === 3) {
return 1;
}
if (eastmeshEnabled && meshcoretelEnabled && bounded > 0) {
return 0;
}
return bounded;
} }
function refreshLetsmeshModeUi() { function refreshLetsmeshModeUi() {
const mode = getLetsmeshMode(); const mode = getLetsmeshMode();
const eastmesh = document.getElementById("mqttEastmeshAu"); const eastmesh = document.getElementById("mqttEastmeshAu");
const eastmeshEnabled = !!(eastmesh && eastmesh.checked); const eastmeshEnabled = !!(eastmesh && eastmesh.checked);
const meshcoretel = document.getElementById("mqttMeshcoretel");
const meshcoretelEnabled = !!(meshcoretel && meshcoretel.checked);
const bothBlocked = eastmeshEnabled || meshcoretelEnabled;
const allBlocked = eastmeshEnabled && meshcoretelEnabled;
const state = document.getElementById("mqttLetsmeshModeState"); const state = document.getElementById("mqttLetsmeshModeState");
const labels = { off:"Off", eu:"EU", us:"US", both:"Both" }; const labels = { off:"Off", eu:"EU", us:"US", both:"Both" };
if (state) { if (state) {
@@ -2240,11 +2304,15 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
if (slider) { if (slider) {
slider.max = "3"; slider.max = "3";
slider.value = String(clampLetsmeshModeIndex(getLetsmeshModeIndex(mode))); slider.value = String(clampLetsmeshModeIndex(getLetsmeshModeIndex(mode)));
slider.disabled = allBlocked;
} }
document.querySelectorAll("[data-letsmesh-label]").forEach((label) => { document.querySelectorAll("[data-letsmesh-label]").forEach((label) => {
const labelMode = label.dataset.letsmeshLabel; const labelMode = label.dataset.letsmeshLabel;
label.classList.toggle("active", labelMode === mode); label.classList.toggle("active", labelMode === mode);
label.classList.toggle("disabled", eastmeshEnabled && labelMode === "both"); label.classList.toggle("disabled",
(bothBlocked && labelMode === "both") ||
(allBlocked && labelMode !== "off")
);
}); });
} }
function setBrokerToggle(inputId, state) { function setBrokerToggle(inputId, state) {
@@ -2262,6 +2330,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
} else if (inputId === "mqttEastmeshAu") { } else if (inputId === "mqttEastmeshAu") {
refreshEastmeshModeUi(); refreshEastmeshModeUi();
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
} else if (inputId === "mqttMeshcoretel") {
refreshMeshcoretelModeUi();
} }
} }
async function loadBrokerState(cmd, inputId, options = {}) { async function loadBrokerState(cmd, inputId, options = {}) {
@@ -2380,8 +2450,14 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
mqttIataSelect.addEventListener("change", refreshMqttIataWarning); mqttIataSelect.addEventListener("change", refreshMqttIataWarning);
} }
async function setEastmeshMode(enabled) { async function setEastmeshMode(enabled) {
if (enabled && getLetsmeshMode() === "both") { if (enabled) {
await setLetsmeshMode("eu"); if (getLetsmeshMode() === "both") {
await setLetsmeshMode("eu");
}
const meshcoretel = document.getElementById("mqttMeshcoretel");
if (meshcoretel && meshcoretel.checked && getLetsmeshMode() !== "off") {
await setLetsmeshMode("off");
}
} }
const result = await runCommand(enabled ? "set mqtt.eastmesh-au on" : "set mqtt.eastmesh-au off"); const result = await runCommand(enabled ? "set mqtt.eastmesh-au on" : "set mqtt.eastmesh-au off");
if (!result.ok) { if (!result.ok) {
@@ -2392,6 +2468,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
setBrokerToggle("mqttEastmeshAu", enabled ? "on" : "off"); setBrokerToggle("mqttEastmeshAu", enabled ? "on" : "off");
refreshEastmeshModeUi(); refreshEastmeshModeUi();
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
refreshMeshcoretelModeUi();
} }
const eastmeshModeSlider = document.getElementById("mqttEastmeshMode"); const eastmeshModeSlider = document.getElementById("mqttEastmeshMode");
if (eastmeshModeSlider) { if (eastmeshModeSlider) {
@@ -2404,9 +2481,22 @@ 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) { const eastmeshEnabled = !!(eastmesh && eastmesh.checked);
refreshLetsmeshModeUi(); const meshcoretel = document.getElementById("mqttMeshcoretel");
return; const meshcoretelEnabled = !!(meshcoretel && meshcoretel.checked);
if (mode === "both") {
if (eastmeshEnabled) {
refreshLetsmeshModeUi();
return;
}
if (meshcoretelEnabled) {
refreshLetsmeshModeUi();
return;
}
}
if (mode !== "off" && meshcoretelEnabled && eastmeshEnabled) {
refreshLetsmeshModeUi();
return;
} }
const desired = { const desired = {
eu: mode === "eu" || mode === "both", eu: mode === "eu" || mode === "both",
@@ -2440,6 +2530,49 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
setLetsmeshMode(modes[index] || "off"); setLetsmeshMode(modes[index] || "off");
}); });
} }
function refreshMeshcoretelModeUi() {
const input = document.getElementById("mqttMeshcoretel");
const enabled = !!(input && input.checked);
const slider = document.getElementById("mqttMeshcoretelMode");
if (slider) slider.value = enabled ? "1" : "0";
const state = document.getElementById("mqttMeshcoretelState");
if (state) {
state.textContent = enabled ? "On" : "Off";
state.classList.toggle("on", enabled);
}
document.querySelectorAll("[data-meshcoretel-label]").forEach((label) => {
label.classList.toggle("active", label.dataset.meshcoretelLabel === (enabled ? "on" : "off"));
});
}
async function setMeshcoretelMode(enabled) {
if (enabled) {
if (getLetsmeshMode() === "both") {
await setLetsmeshMode("eu");
}
const eastmesh = document.getElementById("mqttEastmeshAu");
if (eastmesh && eastmesh.checked && getLetsmeshMode() !== "off") {
await setLetsmeshMode("off");
}
}
const result = await runCommand(enabled ? "set mqtt.meshcoretel on" : "set mqtt.meshcoretel off");
if (!result.ok) {
refreshMeshcoretelModeUi();
return;
}
setBrokerToggle("mqttMeshcoretel", enabled ? "on" : "off");
refreshMeshcoretelModeUi();
refreshLetsmeshModeUi();
refreshEastmeshModeUi();
}
const meshcoretelModeSlider = document.getElementById("mqttMeshcoretelMode");
if (meshcoretelModeSlider) {
meshcoretelModeSlider.addEventListener("input", () => {
meshcoretelModeSlider.value = (Number.parseInt(meshcoretelModeSlider.value, 10) || 0) >= 1 ? "1" : "0";
});
meshcoretelModeSlider.addEventListener("change", () => {
setMeshcoretelMode((Number.parseInt(meshcoretelModeSlider.value, 10) || 0) >= 1);
});
}
document.getElementById("saveOwnerInfo").onclick = () => { document.getElementById("saveOwnerInfo").onclick = () => {
const value = document.getElementById("ownerInfo").value.replace(/\n/g, "|"); const value = document.getElementById("ownerInfo").value.replace(/\n/g, "|");
runCommand("set owner.info " + value); runCommand("set owner.info " + value);
@@ -2568,7 +2701,8 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
() => loadField("get mqtt.email", "mqttEmail", null, quiet), () => loadField("get mqtt.email", "mqttEmail", null, quiet),
() => loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu", quiet), () => loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu", quiet),
() => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet), () => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet),
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet) () => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet),
() => loadBrokerState("get mqtt.meshcoretel", "mqttMeshcoretel", quiet)
]); ]);
statusEl.textContent = "Ready"; statusEl.textContent = "Ready";
} catch (error) { } catch (error) {
@@ -2581,6 +2715,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
} }
refreshEastmeshModeUi(); refreshEastmeshModeUi();
refreshLetsmeshModeUi(); refreshLetsmeshModeUi();
refreshMeshcoretelModeUi();
initApp(); initApp();
</script> </script>
</body> </body>