Merge pull request #12 from xJARiD/develop
fix(mqtt): clear legacy prefs, restore status publishing, and expand …
This commit is contained in:
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 139 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path d="M3.232,3.582C2.789,3.582 2.368,3.934 2.289,4.369L0.013,16.964C-0.066,17.399 0.229,17.751 0.671,17.751L3.087,17.751C3.529,17.751 3.951,17.399 4.03,16.964L4.935,11.951L6.592,17.293C6.672,17.572 6.923,17.751 7.235,17.751L10.434,17.751C10.746,17.751 11.062,17.572 11.243,17.293L14.835,11.925L13.924,16.964C13.844,17.399 14.14,17.751 14.583,17.751L16.998,17.751C17.44,17.751 17.862,17.399 17.941,16.964L20.217,4.369C20.298,3.934 20.002,3.582 19.56,3.582L16.46,3.582C16.147,3.582 15.831,3.761 15.65,4.04L9.76,12.872C9.668,13.013 9.446,12.975 9.397,12.81L6.976,4.04C6.895,3.761 6.645,3.582 6.332,3.582L3.232,3.582Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M20.853,17.751C20.853,17.751 32.797,17.751 32.797,17.751C33.063,17.751 33.317,17.538 33.364,17.278L33.863,14.504C33.91,14.242 33.733,14.031 33.467,14.031L25.166,14.031C25.077,14.031 25.019,13.96 25.034,13.873L25.281,12.508C25.296,12.421 25.38,12.35 25.469,12.35L32.146,12.35C32.411,12.35 32.665,12.137 32.712,11.877L33.157,9.421C33.204,9.159 33.027,8.949 32.761,8.949L26.085,8.949C25.996,8.949 25.938,8.877 25.953,8.79L26.216,7.328C26.232,7.241 26.316,7.17 26.405,7.17L34.706,7.17C34.971,7.17 35.226,6.957 35.272,6.695L35.756,4.021C35.804,3.761 35.627,3.548 35.361,3.548L23.417,3.548C22.975,3.548 22.551,3.902 22.473,4.337L20.191,16.962C20.114,17.397 20.409,17.751 20.853,17.751Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M45.291,17.749L45.291,17.751L45.705,17.751C47.783,17.751 49.767,16.095 50.136,14.052L50.375,12.727C50.744,10.685 49.359,9.029 47.28,9.029L40.882,9.029C40.617,9.029 40.44,8.818 40.487,8.556L40.649,7.664C40.696,7.402 40.95,7.191 41.215,7.191L49.87,7.191C50.313,7.191 50.735,6.839 50.814,6.404L51.183,4.368C51.262,3.931 50.966,3.579 50.523,3.579L41.063,3.579C38.985,3.579 37,5.235 36.631,7.278L36.37,8.723C36.001,10.767 37.386,12.422 39.465,12.422L45.863,12.422C46.128,12.422 46.305,12.633 46.258,12.895L46.138,13.565C46.091,13.826 45.837,14.037 45.571,14.037L36.675,14.037C36.233,14.037 35.811,14.389 35.732,14.824L35.346,16.962C35.267,17.397 35.562,17.749 36.005,17.749L45.291,17.749Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M67.068,3.575C67.068,3.575 64.393,3.575 64.393,3.575C63.951,3.575 63.529,3.927 63.45,4.361L62.654,8.766C62.639,8.853 62.554,8.923 62.466,8.923L57.282,8.923C57.193,8.923 57.135,8.853 57.15,8.766L57.946,4.361C58.023,3.927 57.73,3.575 57.287,3.575L54.613,3.575C54.17,3.575 53.748,3.927 53.669,4.361L51.392,16.964C51.313,17.399 51.608,17.751 52.05,17.751L54.725,17.751C55.168,17.751 55.589,17.399 55.668,16.964L56.48,12.478C56.495,12.392 56.58,12.32 56.668,12.32L61.852,12.32C61.941,12.32 61.999,12.39 61.984,12.478L61.174,16.964C61.096,17.399 61.391,17.751 61.834,17.751L64.508,17.751C64.951,17.751 65.372,17.399 65.451,16.964L67.729,4.361C67.804,3.927 67.511,3.575 67.068,3.575Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M71.102,17.751C71.102,17.751 78.96,17.751 78.96,17.751C79.402,17.751 79.824,17.399 79.903,16.964L80.288,14.824C80.367,14.389 80.072,14.037 79.629,14.037L72.808,14.037C72.542,14.037 72.365,13.826 72.412,13.565L73.48,7.686C73.527,7.426 73.781,7.213 74.045,7.213L80.866,7.213C81.309,7.213 81.73,6.861 81.81,6.427L82.188,4.335C82.267,3.9 81.971,3.548 81.529,3.548L73.691,3.548C71.618,3.548 69.638,5.197 69.265,7.234L68.011,14.046C67.639,16.091 69.022,17.751 71.102,17.751Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M95.833,3.529C95.833,3.529 87.654,3.529 87.654,3.529C85.576,3.529 83.592,5.186 83.223,7.228L81.99,14.052C81.621,16.094 83.006,17.751 85.084,17.751L93.263,17.751C95.341,17.751 97.326,16.095 97.695,14.052L98.928,7.228C99.297,5.186 97.911,3.529 95.833,3.529ZM93.488,13.567C93.44,13.828 93.186,14.039 92.921,14.039L86.762,14.039C86.496,14.039 86.319,13.828 86.366,13.567L87.434,7.663C87.481,7.402 87.735,7.191 88,7.191L94.157,7.191C94.423,7.191 94.6,7.402 94.553,7.663L93.488,13.567Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M99.884,17.751L102.557,17.751C102.999,17.751 103.421,17.399 103.5,16.965L103.973,14.348C103.988,14.261 104.073,14.19 104.161,14.19L107.397,14.186C107.557,14.186 107.69,14.265 107.756,14.395L109.281,17.37C109.458,17.722 109.78,17.764 110.751,17.749C111.32,17.756 112.184,17.713 113.577,17.713C114.025,17.713 114.3,17.244 114.079,16.853L112.413,13.953C112.37,13.876 112.417,13.772 112.509,13.727C113.795,13.102 114.814,11.889 115.068,10.487L115.649,7.262C116.02,5.218 114.635,3.562 112.557,3.562L102.448,3.562C102.006,3.562 101.584,3.914 101.505,4.349L99.225,16.964C99.146,17.399 99.442,17.751 99.884,17.751L99.884,17.751ZM105.255,7.268C105.27,7.181 105.354,7.11 105.443,7.11L110.674,7.11C111.069,7.11 111.331,7.424 111.261,7.812L110.877,9.933C110.806,10.319 110.431,10.634 110.038,10.634L104.806,10.634C104.718,10.634 104.66,10.564 104.675,10.475L105.255,7.268Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M116.642,17.751C116.642,17.751 128.586,17.751 128.586,17.751C128.851,17.751 129.105,17.538 129.152,17.278L129.651,14.504C129.698,14.242 129.521,14.031 129.256,14.031L120.955,14.031C120.866,14.031 120.808,13.96 120.823,13.873L121.069,12.508C121.084,12.421 121.169,12.35 121.257,12.35L127.934,12.35C128.2,12.35 128.454,12.137 128.501,11.877L128.945,9.421C128.992,9.159 128.815,8.949 128.55,8.949L121.873,8.949C121.785,8.949 121.726,8.877 121.741,8.79L122.005,7.328C122.02,7.241 122.105,7.17 122.193,7.17L130.495,7.17C130.76,7.17 131.014,6.957 131.061,6.695L131.545,4.021C131.592,3.761 131.415,3.548 131.15,3.548L119.206,3.548C118.763,3.548 118.34,3.902 118.261,4.337L115.98,16.962C115.902,17.397 116.198,17.751 116.642,17.751Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M134.674,0C134.674,0 132.059,0 132.059,0C131.965,0 131.877,0.074 131.86,0.166L131.783,0.594C131.766,0.686 131.828,0.76 131.921,0.76L132.745,0.76C132.764,0.76 132.776,0.775 132.773,0.793L132.406,2.819C132.39,2.91 132.452,2.984 132.545,2.984L133.108,2.984C133.201,2.984 133.29,2.91 133.307,2.819L133.673,0.793C133.676,0.775 133.694,0.76 133.713,0.76L134.536,0.76C134.629,0.76 134.718,0.686 134.735,0.594L134.812,0.166C134.828,0.074 134.767,0 134.674,0Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M135.278,0.002C135.185,0.002 135.096,0.076 135.079,0.167L134.6,2.819C134.583,2.91 134.646,2.984 134.739,2.984L135.247,2.984C135.34,2.984 135.429,2.91 135.446,2.819L135.636,1.763L135.985,2.888C136.002,2.947 136.055,2.984 136.121,2.984L136.794,2.984C136.86,2.984 136.926,2.947 136.964,2.888L137.72,1.758L137.528,2.819C137.512,2.91 137.574,2.984 137.667,2.984L138.176,2.984C138.269,2.984 138.358,2.91 138.374,2.819L138.853,0.167C138.87,0.076 138.808,0.002 138.715,0.002L138.062,0.002C137.997,0.002 137.93,0.039 137.892,0.098L136.652,1.957C136.633,1.987 136.586,1.979 136.575,1.944L136.066,0.098C136.049,0.039 135.996,0.002 135.93,0.002L135.278,0.002Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.1 KiB |
+2
-1
@@ -11,7 +11,7 @@ These commands are available on `*_repeater_mqtt` firmware targets.
|
||||
### MQTT Status And Routing
|
||||
|
||||
- `get mqtt.status`
|
||||
- shows WiFi, NTP, IATA, endpoint status, and TX state
|
||||
- shows WiFi, NTP, IATA, endpoint status, status publishing state, and TX state
|
||||
- `get mqtt.statuscfg`
|
||||
- shows whether periodic status messages are enabled
|
||||
- `get mqtt.iata`
|
||||
@@ -107,6 +107,7 @@ When the repeater web panel is enabled, it only allows a limited command set.
|
||||
|
||||
That allowlist currently includes:
|
||||
|
||||
- `clock`
|
||||
- `get mqtt.status`
|
||||
- `get web`
|
||||
- `get web.status`
|
||||
|
||||
+2
-1
@@ -90,6 +90,7 @@ This is a small terminal for allowlisted commands.
|
||||
- press `Enter` to run the command
|
||||
- command history is shown in the terminal box below
|
||||
- save buttons elsewhere in the page also show the generated command and the reply here
|
||||
- `clock` is available here if you want to check the repeater's current board time
|
||||
|
||||
This makes it easy to see exactly what the panel sent to the repeater.
|
||||
|
||||
@@ -201,7 +202,7 @@ Use the repeater admin password, not the guest password.
|
||||
|
||||
### A command says it is not allowlisted
|
||||
|
||||
The panel intentionally limits what can be run from the browser. Use the serial CLI for commands outside the web allowlist.
|
||||
The panel intentionally limits what can be run from the browser. Use the serial CLI for commands outside the web allowlist. `clock` is included, but most maintenance and debug commands are still serial-only.
|
||||
|
||||
### Stats or settings do not refresh
|
||||
|
||||
|
||||
@@ -1317,7 +1317,15 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
} else {
|
||||
strcpy(reply, "Err - bad mqtt.email");
|
||||
}
|
||||
} else if (memcmp(command, "get mqtt.status", 15) == 0) {
|
||||
} else if (strcmp(command, "send mqtt.status") == 0) {
|
||||
if (mqtt.sendStatusNow()) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - mqtt status unavailable");
|
||||
}
|
||||
} else if (strcmp(command, "get mqtt.statuscfg") == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isStatusEnabled() ? "on" : "off");
|
||||
} else if (strcmp(command, "get mqtt.status") == 0) {
|
||||
mqtt.formatStatusReply(reply, 160);
|
||||
} else if (strcmp(command, "get web.status") == 0 || strcmp(command, "get web") == 0) {
|
||||
mqtt.formatWebStatusReply(reply, 160);
|
||||
@@ -1337,8 +1345,6 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
sprintf(reply, "> %s", mqtt.isPacketsEnabled() ? "on" : "off");
|
||||
} else if (memcmp(command, "get mqtt.raw", 12) == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isRawEnabled() ? "on" : "off");
|
||||
} else if (memcmp(command, "get mqtt.statuscfg", 18) == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isStatusEnabled() ? "on" : "off");
|
||||
} else if (memcmp(command, "get mqtt.tx", 11) == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isTxEnabled() ? "on" : "off");
|
||||
} else if (memcmp(command, "get mqtt.eastmesh-au", 20) == 0 || memcmp(command, "get mqtt.eastmesh.au", 20) == 0) {
|
||||
@@ -1436,6 +1442,7 @@ void MyMesh::runWebCommand(const char* command, char* reply, size_t reply_size)
|
||||
};
|
||||
|
||||
bool allowed =
|
||||
matches_exact("clock") ||
|
||||
matches_exact("get mqtt.status") ||
|
||||
matches_exact("get web.status") ||
|
||||
matches_exact("get web") ||
|
||||
|
||||
@@ -44,14 +44,20 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t bytes_to_read = min(static_cast<size_t>(file.size()), sizeof(prefs));
|
||||
bool ok = bytes_to_read >= sizeof(prefs.magic) &&
|
||||
file.read(reinterpret_cast<uint8_t*>(&prefs), bytes_to_read) == bytes_to_read;
|
||||
MQTTPrefs persisted{};
|
||||
size_t bytes_to_read = min(static_cast<size_t>(file.size()), sizeof(persisted));
|
||||
bool ok = bytes_to_read >= sizeof(persisted.magic) &&
|
||||
file.read(reinterpret_cast<uint8_t*>(&persisted), bytes_to_read) == bytes_to_read;
|
||||
file.close();
|
||||
|
||||
if (!ok || prefs.magic != kMagic) {
|
||||
if (!ok || persisted.magic != kMagic) {
|
||||
// Legacy or corrupt MQTT prefs should not survive migration. Remove the
|
||||
// stale file and persist a clean current-format default copy immediately.
|
||||
fs->remove(kFilename);
|
||||
save(fs, prefs);
|
||||
return false;
|
||||
}
|
||||
prefs = persisted;
|
||||
if (prefs.wifi_powersave > 2) {
|
||||
prefs.wifi_powersave = 0;
|
||||
}
|
||||
|
||||
@@ -164,6 +164,27 @@ void MQTTUplink::reconnectWifi() {
|
||||
_last_wifi_attempt = 0;
|
||||
}
|
||||
|
||||
bool MQTTUplink::sendStatusNow() {
|
||||
if (!_running || !_have_time_sync || WiFi.status() != WL_CONNECTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool any_connected = false;
|
||||
for (const BrokerState& broker : _brokers) {
|
||||
if ((_prefs.enabled_mask & broker.spec->bit) != 0 && broker.connected && broker.client != nullptr) {
|
||||
any_connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!any_connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
publishStatus(true);
|
||||
_last_status_publish = millis();
|
||||
return true;
|
||||
}
|
||||
|
||||
void MQTTUplink::makeSafeToken(const char* input, char* output, size_t output_size) {
|
||||
if (output_size == 0) {
|
||||
return;
|
||||
@@ -334,6 +355,7 @@ void MQTTUplink::destroyBroker(BrokerState& broker) {
|
||||
broker.client = nullptr;
|
||||
}
|
||||
broker.connected = false;
|
||||
broker.connect_announced = false;
|
||||
}
|
||||
|
||||
void MQTTUplink::queuePublish(BrokerState& broker, const char* topic, const char* payload, bool retain) {
|
||||
@@ -728,10 +750,13 @@ void MQTTUplink::loop(const MQTTStatusSnapshot& snapshot) {
|
||||
ensureWebServer();
|
||||
|
||||
for (BrokerState& broker : _brokers) {
|
||||
bool was_connected = broker.connected;
|
||||
ensureBroker(broker);
|
||||
if (!was_connected && broker.connected) {
|
||||
if (broker.connected && !broker.connect_announced) {
|
||||
publishOnlineStatus(broker);
|
||||
broker.connect_announced = true;
|
||||
_last_status_publish = millis();
|
||||
} else if (!broker.connected) {
|
||||
broker.connect_announced = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,10 +833,10 @@ void MQTTUplink::formatStatusReply(char* reply, size_t reply_size) const {
|
||||
return "retry";
|
||||
};
|
||||
|
||||
snprintf(reply, reply_size, "> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s tx:%s",
|
||||
snprintf(reply, reply_size, "> wifi:%s ntp:%s iata:%s eastmesh-au:%s letsmesh-eu:%s letsmesh-us:%s status:%s tx:%s",
|
||||
getWifiStateLabel(_prefs, _wifi_started), _have_time_sync ? "up" : "wait", _prefs.iata,
|
||||
broker_state(_brokers[0]), broker_state(_brokers[1]), broker_state(_brokers[2]),
|
||||
_prefs.tx_enabled ? "on" : "off");
|
||||
_prefs.status_enabled ? "on" : "off", _prefs.tx_enabled ? "on" : "off");
|
||||
}
|
||||
|
||||
void MQTTUplink::formatWebStatusReply(char* reply, size_t reply_size) const {
|
||||
@@ -877,9 +902,7 @@ bool MQTTUplink::setWebEnabled(bool enabled) {
|
||||
#if WITH_WEB_PANEL
|
||||
_prefs.web_enabled = enabled ? 1 : 0;
|
||||
bool ok = savePrefs();
|
||||
if (_prefs.web_enabled == 0) {
|
||||
stopWebServer();
|
||||
} else {
|
||||
if (_prefs.web_enabled != 0) {
|
||||
ensureWebServer();
|
||||
}
|
||||
return ok;
|
||||
@@ -960,9 +983,7 @@ bool MQTTUplink::setOwnerPublicKey(const char* owner_public_key) {
|
||||
|
||||
if (owner_public_key[0] == 0) {
|
||||
_prefs.owner_public_key[0] = 0;
|
||||
bool ok = savePrefs();
|
||||
reconnectWifi();
|
||||
return ok;
|
||||
return savePrefs();
|
||||
}
|
||||
|
||||
if (strlen(owner_public_key) != 64) {
|
||||
@@ -976,9 +997,7 @@ bool MQTTUplink::setOwnerPublicKey(const char* owner_public_key) {
|
||||
_prefs.owner_public_key[i] = toupper(static_cast<unsigned char>(owner_public_key[i]));
|
||||
}
|
||||
_prefs.owner_public_key[64] = 0;
|
||||
bool ok = savePrefs();
|
||||
reconnectWifi();
|
||||
return ok;
|
||||
return savePrefs();
|
||||
}
|
||||
|
||||
bool MQTTUplink::setOwnerEmail(const char* owner_email) {
|
||||
@@ -986,9 +1005,7 @@ bool MQTTUplink::setOwnerEmail(const char* owner_email) {
|
||||
return false;
|
||||
}
|
||||
StrHelper::strncpy(_prefs.owner_email, owner_email, sizeof(_prefs.owner_email));
|
||||
bool ok = savePrefs();
|
||||
reconnectWifi();
|
||||
return ok;
|
||||
return savePrefs();
|
||||
}
|
||||
|
||||
void MQTTUplink::formatWifiStatusReply(char* reply, size_t reply_size) const {
|
||||
@@ -1077,6 +1094,7 @@ bool MQTTUplink::setOwnerPublicKey(const char*) { return false; }
|
||||
bool MQTTUplink::setOwnerEmail(const char*) { return false; }
|
||||
void MQTTUplink::formatWifiStatusReply(char* reply, size_t reply_size) const { snprintf(reply, reply_size, "> unsupported"); }
|
||||
void MQTTUplink::reconnectWifi() {}
|
||||
bool MQTTUplink::sendStatusNow() { return false; }
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ public:
|
||||
const char* getOwnerEmail() const { return _prefs.owner_email; }
|
||||
void formatWifiStatusReply(char* reply, size_t reply_size) const;
|
||||
void reconnectWifi();
|
||||
bool sendStatusNow();
|
||||
|
||||
private:
|
||||
#if defined(ESP_PLATFORM)
|
||||
@@ -92,6 +93,7 @@ private:
|
||||
const BrokerSpec* spec;
|
||||
esp_mqtt_client_handle_t client;
|
||||
bool connected;
|
||||
bool connect_announced;
|
||||
unsigned long last_connect_attempt;
|
||||
time_t token_expires_at;
|
||||
char username[70];
|
||||
|
||||
@@ -159,13 +159,13 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
--button-secondary-text:#e6eaf0;
|
||||
}
|
||||
html { min-height:100%; background:linear-gradient(180deg,var(--background),var(--surface2)); background-repeat:no-repeat; background-attachment:fixed; }
|
||||
body { min-height:100vh; margin:0; font:16px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace; background:transparent; color:var(--text); transition:background .2s ease,color .2s ease; }
|
||||
body { min-height:100vh; margin:0; background:transparent; color:var(--text); font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace; font-size:16px; line-height:1.4; transition:background .2s ease,color .2s ease; }
|
||||
main { max-width:920px; margin:0 auto; padding:24px; }
|
||||
.theme-fab { position:fixed; top:18px; right:18px; width:48px; height:48px; border-radius:999px; display:flex; align-items:center; justify-content:center; z-index:20; box-shadow:0 12px 28px rgba(0,0,0,.18); font-size:22px; line-height:1; }
|
||||
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:14px; padding:18px; margin-bottom:18px; }
|
||||
h1,h2,h3 { margin:0 0 12px; font-size:18px; }
|
||||
p { color:var(--text-muted); margin:8px 0 0; }
|
||||
input, textarea, button, select { width:100%; box-sizing:border-box; border-radius:10px; border:1px solid var(--border); background:var(--input-bg); color:var(--text); padding:12px; font:inherit; }
|
||||
input:not([type="checkbox"]), textarea, button, select { width:100%; min-height:44px; box-sizing:border-box; border-radius:10px; border:1px solid var(--border); background:var(--input-bg); color:var(--text); padding:12px; font-family:inherit; font-size:inherit; line-height:inherit; }
|
||||
input:not([type="checkbox"]), textarea, select, button { -webkit-appearance:none; appearance:none; }
|
||||
textarea { min-height:100px; resize:vertical; }
|
||||
button { width:auto; cursor:pointer; background:var(--accent); color:var(--button-text); border:none; font-weight:700; transition:background .2s ease,color .2s ease,border-color .2s ease; }
|
||||
button:hover { background:var(--accent-hover); }
|
||||
@@ -187,12 +187,26 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
.label { font-size:12px; color:var(--text-muted); margin-bottom:6px; display:block; }
|
||||
.fieldline { display:grid; grid-template-columns:1fr auto; gap:8px; align-items:center; }
|
||||
.iconbtn { width:44px; padding:12px 0; }
|
||||
.placeholder-slot { display:block; width:44px; height:44px; }
|
||||
.savebtn { width:100%; background:var(--accent); color:var(--button-text); border:none; }
|
||||
.savebtn:hover { background:var(--accent-hover); }
|
||||
.broker-grid { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:10px; }
|
||||
.broker-card { background:var(--surface2); border:1px solid var(--border); border-radius:12px; padding:12px; display:grid; gap:10px; }
|
||||
.broker-row { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
||||
.broker-copy { display:grid; gap:4px; min-width:0; }
|
||||
.broker-title { font-size:12px; color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em; }
|
||||
.broker-state { font-size:13px; color:var(--text); }
|
||||
.broker-state.on { color:var(--accent); }
|
||||
.switch { position:relative; display:inline-flex; width:54px; height:32px; flex:0 0 auto; }
|
||||
.switch input { position:absolute; opacity:0; width:0; height:0; min-height:0; padding:0; border:0; -webkit-appearance:none; appearance:none; }
|
||||
.slider { position:absolute; inset:0; border-radius:999px; background:var(--background); border:1px solid var(--border); transition:background .2s ease,border-color .2s ease; cursor:pointer; }
|
||||
.slider::before { content:""; position:absolute; width:24px; height:24px; left:3px; top:3px; border-radius:50%; background:var(--surface1); box-shadow:0 2px 8px rgba(0,0,0,.18); transition:transform .2s ease,background .2s ease; }
|
||||
.switch input:checked + .slider { background:var(--accent); border-color:transparent; }
|
||||
.switch input:checked + .slider::before { transform:translateX(22px); background:#fff; }
|
||||
.panel-warning { min-height:1.4em; font-size:13px; color:var(--status-red); }
|
||||
.themebtn { padding:10px 14px; }
|
||||
#app { display:none; }
|
||||
#status { white-space:pre-wrap; color:var(--text-muted); min-height:1.4em; }
|
||||
.terminal { background:var(--terminal-bg); border:1px solid var(--terminal-border); border-radius:12px; padding:14px; min-height:180px; max-height:320px; overflow:auto; font:14px/1.45 ui-monospace,SFMono-Regular,Menlo,monospace; }
|
||||
.terminal { background:var(--terminal-bg); border:1px solid var(--terminal-border); border-radius:12px; padding:14px; min-height:180px; max-height:320px; overflow:auto; font-family:inherit; font-size:14px; line-height:1.45; }
|
||||
.term-entry { margin:0 0 12px; }
|
||||
.term-cmd { color:var(--terminal-cmd); }
|
||||
.term-out { white-space:pre-wrap; color:var(--text); }
|
||||
@@ -231,7 +245,7 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
body { font-size:15px; }
|
||||
main { padding:16px; }
|
||||
.card { padding:16px; margin-bottom:14px; }
|
||||
.row, .row3, .row-command, .metric-grid, .hud-grid-1, .hud-grid-2, .hud-grid-3, .core-grid, .core-metrics { grid-template-columns:1fr; }
|
||||
.row, .row3, .row-command, .metric-grid, .hud-grid-1, .hud-grid-2, .hud-grid-3, .core-grid, .core-metrics, .broker-grid { grid-template-columns:1fr; }
|
||||
.inline-actions { grid-template-columns:minmax(0,1fr) auto auto; }
|
||||
.fieldline { grid-template-columns:minmax(0,1fr) auto; align-items:center; }
|
||||
.row-command button { width:100%; }
|
||||
@@ -247,6 +261,7 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
.actions-group.center { grid-template-columns:1fr; justify-items:center; }
|
||||
#actionsPanel .actions-group button { width:100%; }
|
||||
#actionsPanel .actions-group.center button { width:auto; min-width:56px; }
|
||||
.broker-row { gap:10px; }
|
||||
.row > div + div, .row3 > div + div { margin-top:4px; }
|
||||
.row > div[style*="align-self:end"] { padding-top:4px; }
|
||||
}
|
||||
@@ -303,7 +318,7 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="cliPanel">
|
||||
<section class="card" id="cliPanel" style="display:none">
|
||||
<h2>Run CLI Command</h2>
|
||||
<div class="row-command">
|
||||
<input id="command" placeholder="get mqtt.status">
|
||||
@@ -350,7 +365,7 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
<label class="label" for="guestPassword">Guest Password</label>
|
||||
<div class="inline-actions">
|
||||
<input id="guestPassword" type="password" placeholder="new guest password">
|
||||
<span></span>
|
||||
<span class="placeholder-slot" aria-hidden="true"></span>
|
||||
<button class="savebtn" data-prefix="set guest.password " data-input="guestPassword">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -358,7 +373,7 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
<label class="label" for="privateKey">Private Key</label>
|
||||
<div class="inline-actions">
|
||||
<input id="privateKey" placeholder="64-hex-char private key">
|
||||
<span></span>
|
||||
<span class="placeholder-slot" aria-hidden="true"></span>
|
||||
<button class="savebtn" data-prefix="set prv.key " data-input="privateKey">Save</button>
|
||||
</div>
|
||||
<p>Changing the private key requires a reboot to apply.</p>
|
||||
@@ -488,16 +503,58 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
<button class="savebtn" data-prefix="set mqtt.owner " data-input="mqttOwner">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-card">
|
||||
<label class="label" for="mqttEmail">MQTT Email</label>
|
||||
<div class="inline-actions">
|
||||
<input id="mqttEmail" type="email" placeholder="owner@example.com">
|
||||
<button class="iconbtn" data-load-cmd="get mqtt.email" data-load-input="mqttEmail" title="Refresh MQTT email">↻</button>
|
||||
<button class="savebtn" data-prefix="set mqtt.email " data-input="mqttEmail">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="field-card">
|
||||
<label class="label" for="mqttEmail">MQTT Email</label>
|
||||
<div class="inline-actions">
|
||||
<input id="mqttEmail" type="email" placeholder="owner@example.com">
|
||||
<button class="iconbtn" data-load-cmd="get mqtt.email" data-load-input="mqttEmail" title="Refresh MQTT email">↻</button>
|
||||
<button class="savebtn" data-prefix="set mqtt.email " data-input="mqttEmail">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-card">
|
||||
<label class="label">MQTT Servers</label>
|
||||
<div class="broker-grid">
|
||||
<div class="broker-card">
|
||||
<div class="broker-row">
|
||||
<div class="broker-copy">
|
||||
<div class="broker-title">EastMesh AU</div>
|
||||
<div class="broker-state" id="mqttEastmeshAuState">Off</div>
|
||||
</div>
|
||||
<label class="switch" aria-label="Toggle EastMesh AU">
|
||||
<input id="mqttEastmeshAu" type="checkbox" data-broker-on="set mqtt.eastmesh-au on" data-broker-off="set mqtt.eastmesh-au off">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="broker-card">
|
||||
<div class="broker-row">
|
||||
<div class="broker-copy">
|
||||
<div class="broker-title">LetsMesh EU</div>
|
||||
<div class="broker-state" id="mqttLetsmeshEuState">Off</div>
|
||||
</div>
|
||||
<label class="switch" aria-label="Toggle LetsMesh EU">
|
||||
<input id="mqttLetsmeshEu" type="checkbox" data-broker-on="set mqtt.letsmesh-eu on" data-broker-off="set mqtt.letsmesh-eu off">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="broker-card">
|
||||
<div class="broker-row">
|
||||
<div class="broker-copy">
|
||||
<div class="broker-title">LetsMesh US</div>
|
||||
<div class="broker-state" id="mqttLetsmeshUsState">Off</div>
|
||||
</div>
|
||||
<label class="switch" aria-label="Toggle LetsMesh US">
|
||||
<input id="mqttLetsmeshUs" type="checkbox" data-broker-on="set mqtt.letsmesh-us on" data-broker-off="set mqtt.letsmesh-us off">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mqttBrokerWarning" class="panel-warning"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" id="statsPanel" style="display:none">
|
||||
<h2>Stats</h2>
|
||||
@@ -846,16 +903,44 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
return JSON.parse(text);
|
||||
});
|
||||
}
|
||||
function applyBootstrapData(data) {
|
||||
if (!data) return;
|
||||
if (typeof data.name === "string") document.getElementById("nodeName").value = data.name;
|
||||
if (typeof data.mqtt_iata === "string" && data.mqtt_iata.length) document.getElementById("mqttIata").value = data.mqtt_iata;
|
||||
if (typeof data.mqtt_owner === "string") document.getElementById("mqttOwner").value = data.mqtt_owner;
|
||||
if (typeof data.mqtt_email === "string") document.getElementById("mqttEmail").value = data.mqtt_email;
|
||||
if (typeof data.advert_interval === "string") document.getElementById("advertInterval").value = data.advert_interval;
|
||||
if (typeof data.flood_interval === "string") document.getElementById("floodInterval").value = data.flood_interval;
|
||||
if (typeof data.flood_max === "string") document.getElementById("floodMax").value = data.flood_max;
|
||||
}
|
||||
function applyBootstrapData(data) {
|
||||
if (!data) return;
|
||||
if (typeof data.name === "string") document.getElementById("nodeName").value = data.name;
|
||||
if (typeof data.mqtt_iata === "string" && data.mqtt_iata.length) document.getElementById("mqttIata").value = data.mqtt_iata;
|
||||
if (typeof data.mqtt_owner === "string") document.getElementById("mqttOwner").value = data.mqtt_owner;
|
||||
if (typeof data.mqtt_email === "string") document.getElementById("mqttEmail").value = data.mqtt_email;
|
||||
if (typeof data.advert_interval === "string") document.getElementById("advertInterval").value = data.advert_interval;
|
||||
if (typeof data.flood_interval === "string") document.getElementById("floodInterval").value = data.flood_interval;
|
||||
if (typeof data.flood_max === "string") document.getElementById("floodMax").value = data.flood_max;
|
||||
if (typeof data.mqtt_eastmesh_au === "string") setBrokerToggle("mqttEastmeshAu", data.mqtt_eastmesh_au);
|
||||
if (typeof data.mqtt_letsmesh_eu === "string") setBrokerToggle("mqttLetsmeshEu", data.mqtt_letsmesh_eu);
|
||||
if (typeof data.mqtt_letsmesh_us === "string") setBrokerToggle("mqttLetsmeshUs", data.mqtt_letsmesh_us);
|
||||
updateBrokerWarning();
|
||||
}
|
||||
function setBrokerToggle(inputId, state) {
|
||||
const input = document.getElementById(inputId);
|
||||
if (!input) return;
|
||||
const enabled = state === "on";
|
||||
input.checked = enabled;
|
||||
const label = document.getElementById(inputId + "State");
|
||||
if (label) {
|
||||
label.textContent = enabled ? "On" : "Off";
|
||||
label.classList.toggle("on", enabled);
|
||||
}
|
||||
}
|
||||
function updateBrokerWarning() {
|
||||
const enabled = ["mqttEastmeshAu", "mqttLetsmeshEu", "mqttLetsmeshUs"].filter((inputId) => {
|
||||
const input = document.getElementById(inputId);
|
||||
return input && input.checked;
|
||||
}).length;
|
||||
document.getElementById("mqttBrokerWarning").textContent = enabled >= 3 ? "Running all 3 MQTT servers is not recommended. Prefer 2 at most." : "";
|
||||
}
|
||||
async function loadBrokerState(cmd, inputId) {
|
||||
const result = await runCommand(cmd);
|
||||
if (!result.ok) return;
|
||||
setBrokerToggle(inputId, parseReplyValue(result.text));
|
||||
updateBrokerWarning();
|
||||
}
|
||||
async function refreshStats() {
|
||||
const getStatsBtn = document.getElementById("getStatsBtn");
|
||||
getStatsBtn.disabled = true;
|
||||
@@ -898,19 +983,22 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
}
|
||||
token = text.trim();
|
||||
showAuthedUi(true);
|
||||
try {
|
||||
applyBootstrapData(await fetchJson("/api/bootstrap"));
|
||||
} catch (_) {
|
||||
await Promise.all([
|
||||
loadField("get name", "nodeName"),
|
||||
loadField("get mqtt.iata", "mqttIata"),
|
||||
loadField("get mqtt.owner", "mqttOwner"),
|
||||
loadField("get mqtt.email", "mqttEmail"),
|
||||
loadField("get advert.interval", "advertInterval"),
|
||||
loadField("get flood.advert.interval", "floodInterval"),
|
||||
loadField("get flood.max", "floodMax")
|
||||
]);
|
||||
}
|
||||
try {
|
||||
applyBootstrapData(await fetchJson("/api/bootstrap"));
|
||||
} catch (_) {
|
||||
await Promise.all([
|
||||
loadField("get name", "nodeName"),
|
||||
loadField("get mqtt.iata", "mqttIata"),
|
||||
loadField("get mqtt.owner", "mqttOwner"),
|
||||
loadField("get mqtt.email", "mqttEmail"),
|
||||
loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu"),
|
||||
loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu"),
|
||||
loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs"),
|
||||
loadField("get advert.interval", "advertInterval"),
|
||||
loadField("get flood.advert.interval", "floodInterval"),
|
||||
loadField("get flood.max", "floodMax")
|
||||
]);
|
||||
}
|
||||
};
|
||||
document.getElementById("password").addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
@@ -926,13 +1014,22 @@ const char kWebPanelHtml[] PROGMEM = R"HTML(
|
||||
document.getElementById("runBtn").click();
|
||||
}
|
||||
});
|
||||
document.querySelectorAll("[data-cmd]").forEach((btn) => btn.onclick = () => runCommand(btn.dataset.cmd));
|
||||
document.querySelectorAll("[data-prefix]").forEach((btn) => btn.onclick = () => runPrefixed(btn.dataset.prefix, btn.dataset.input));
|
||||
document.querySelectorAll("[data-load-cmd]").forEach((btn) => btn.onclick = () => loadField(btn.dataset.loadCmd, btn.dataset.loadInput, btn.dataset.loadFormat));
|
||||
document.getElementById("saveOwnerInfo").onclick = () => {
|
||||
const value = document.getElementById("ownerInfo").value.replace(/\n/g, "|");
|
||||
runCommand("set owner.info " + value);
|
||||
};
|
||||
document.querySelectorAll("[data-cmd]").forEach((btn) => btn.onclick = () => runCommand(btn.dataset.cmd));
|
||||
document.querySelectorAll("[data-prefix]").forEach((btn) => btn.onclick = () => runPrefixed(btn.dataset.prefix, btn.dataset.input));
|
||||
document.querySelectorAll("[data-load-cmd]").forEach((btn) => btn.onclick = () => loadField(btn.dataset.loadCmd, btn.dataset.loadInput, btn.dataset.loadFormat));
|
||||
document.querySelectorAll("[data-broker-on]").forEach((input) => input.addEventListener("change", () => {
|
||||
const label = document.getElementById(input.id + "State");
|
||||
if (label) {
|
||||
label.textContent = input.checked ? "On" : "Off";
|
||||
label.classList.toggle("on", input.checked);
|
||||
}
|
||||
updateBrokerWarning();
|
||||
runCommand(input.checked ? input.dataset.brokerOn : input.dataset.brokerOff);
|
||||
}));
|
||||
document.getElementById("saveOwnerInfo").onclick = () => {
|
||||
const value = document.getElementById("ownerInfo").value.replace(/\n/g, "|");
|
||||
runCommand("set owner.info " + value);
|
||||
};
|
||||
document.getElementById("rebootBtn").onclick = async () => {
|
||||
if (confirm("Reboot the repeater now?")) {
|
||||
await runCommand("reboot");
|
||||
@@ -1127,15 +1224,18 @@ esp_err_t WebPanelServer::handleBootstrap(httpd_req_t* req) {
|
||||
const struct {
|
||||
const char* key;
|
||||
const char* command;
|
||||
} fields[] = {
|
||||
{"name", "get name"},
|
||||
{"mqtt_iata", "get mqtt.iata"},
|
||||
{"mqtt_owner", "get mqtt.owner"},
|
||||
{"mqtt_email", "get mqtt.email"},
|
||||
{"advert_interval", "get advert.interval"},
|
||||
{"flood_interval", "get flood.advert.interval"},
|
||||
{"flood_max", "get flood.max"},
|
||||
};
|
||||
} fields[] = {
|
||||
{"name", "get name"},
|
||||
{"mqtt_iata", "get mqtt.iata"},
|
||||
{"mqtt_owner", "get mqtt.owner"},
|
||||
{"mqtt_email", "get mqtt.email"},
|
||||
{"mqtt_eastmesh_au", "get mqtt.eastmesh-au"},
|
||||
{"mqtt_letsmesh_eu", "get mqtt.letsmesh-eu"},
|
||||
{"mqtt_letsmesh_us", "get mqtt.letsmesh-us"},
|
||||
{"advert_interval", "get advert.interval"},
|
||||
{"flood_interval", "get flood.advert.interval"},
|
||||
{"flood_max", "get flood.max"},
|
||||
};
|
||||
|
||||
size_t offset = 0;
|
||||
json[offset++] = '{';
|
||||
|
||||
@@ -464,6 +464,7 @@ build_flags =
|
||||
-D WIFI_PWD='"mypwd"'
|
||||
-D MQTT_DEFAULT_IATA='"MEL"'
|
||||
-D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=1
|
||||
-D ARDUINO_USB_CDC_ON_BOOT=1
|
||||
build_src_filter = ${heltec_v4_oled.build_src_filter}
|
||||
+<helpers/mqtt/*.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
|
||||
Reference in New Issue
Block a user