Added support for MeshCoreTel broker.
This commit is contained in:
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user