mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-18 19:44:23 +00:00
634 lines
25 KiB
C++
634 lines
25 KiB
C++
#include "CompanionMqttSetupPortal.h"
|
|
|
|
#if defined(ESP32_PLATFORM) && defined(WIFI_SSID) && defined(WITH_MQTT_BRIDGE)
|
|
|
|
#include <Arduino.h>
|
|
#include <Preferences.h>
|
|
#include <WiFi.h>
|
|
#include <freertos/FreeRTOS.h>
|
|
#include <freertos/task.h>
|
|
#include <helpers/MQTTDefaults.h>
|
|
#include <helpers/MQTTPresets.h>
|
|
#include <strings.h>
|
|
|
|
namespace {
|
|
|
|
static const char* NVS_NAMESPACE = "mesh-mqtt";
|
|
static const char* NVS_VERSION_KEY = "version";
|
|
static const char* NVS_PREFS_KEY = "prefs";
|
|
static const size_t MAX_FORM_BODY = 2048;
|
|
|
|
struct PortalImpl {
|
|
WiFiServer server{80};
|
|
MQTTPrefs* prefs = nullptr;
|
|
MQTTPrefs page_prefs{};
|
|
TaskHandle_t task = nullptr;
|
|
volatile bool* active = nullptr;
|
|
volatile bool saved = false;
|
|
portMUX_TYPE prefs_lock = portMUX_INITIALIZER_UNLOCKED;
|
|
};
|
|
|
|
template <size_t N>
|
|
static bool hasTerminator(const char (&value)[N]) {
|
|
return memchr(value, 0, N) != nullptr;
|
|
}
|
|
|
|
static bool storedStringsAreValid(const MQTTPrefs& prefs) {
|
|
if (!hasTerminator(prefs.mqtt_origin)
|
|
|| !hasTerminator(prefs.mqtt_iata)
|
|
|| !hasTerminator(prefs.wifi_ssid)
|
|
|| !hasTerminator(prefs.wifi_password)
|
|
|| !hasTerminator(prefs.timezone_string)
|
|
|| !hasTerminator(prefs.mqtt_owner_public_key)
|
|
|| !hasTerminator(prefs.mqtt_email)
|
|
|| !hasTerminator(prefs.mqtt_ntp_server)
|
|
|| !hasTerminator(prefs.snmp_community)
|
|
|| !hasTerminator(prefs.alert_psk_hex)
|
|
|| !hasTerminator(prefs.alert_hashtag)
|
|
|| !hasTerminator(prefs.alert_region)) {
|
|
return false;
|
|
}
|
|
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
|
|
if (!hasTerminator(prefs.mqtt_slot_preset[i])
|
|
|| !hasTerminator(prefs.mqtt_slot_host[i])
|
|
|| !hasTerminator(prefs.mqtt_slot_username[i])
|
|
|| !hasTerminator(prefs.mqtt_slot_password[i])
|
|
|| !hasTerminator(prefs.mqtt_slot_token[i])
|
|
|| !hasTerminator(prefs.mqtt_slot_topic[i])
|
|
|| !hasTerminator(prefs.mqtt_slot_audience[i])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool readLine(WiFiClient& client, char* line, size_t capacity) {
|
|
size_t length = 0;
|
|
const uint32_t deadline = millis() + 5000;
|
|
while (client.connected()) {
|
|
while (client.available()) {
|
|
int c = client.read();
|
|
if (c < 0) break;
|
|
if (c == '\n') {
|
|
if (length && line[length - 1] == '\r') length--;
|
|
line[length] = 0;
|
|
return true;
|
|
}
|
|
if (length + 1 < capacity) line[length++] = static_cast<char>(c);
|
|
}
|
|
if (static_cast<int32_t>(millis() - deadline) >= 0) break;
|
|
delay(1);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int hexValue(char c) {
|
|
if (c >= '0' && c <= '9') return c - '0';
|
|
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
|
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
|
|
return -1;
|
|
}
|
|
|
|
static bool decodeFormValue(const char* encoded, size_t encoded_len,
|
|
char* output, size_t output_size) {
|
|
if (!output || output_size == 0) return false;
|
|
size_t out = 0;
|
|
for (size_t i = 0; i < encoded_len; i++) {
|
|
char c = encoded[i];
|
|
if (c == '+') {
|
|
c = ' ';
|
|
} else if (c == '%' && i + 2 < encoded_len) {
|
|
const int hi = hexValue(encoded[i + 1]);
|
|
const int lo = hexValue(encoded[i + 2]);
|
|
if (hi < 0 || lo < 0) return false;
|
|
c = static_cast<char>((hi << 4) | lo);
|
|
i += 2;
|
|
}
|
|
if (c == 0 || out + 1 >= output_size) return false;
|
|
output[out++] = c;
|
|
}
|
|
output[out] = 0;
|
|
return true;
|
|
}
|
|
|
|
static bool getFormField(const char* body, const char* name,
|
|
char* output, size_t output_size) {
|
|
const size_t name_len = strlen(name);
|
|
const char* field = body;
|
|
while (field && *field) {
|
|
const char* end = strchr(field, '&');
|
|
const size_t field_len = end ? static_cast<size_t>(end - field) : strlen(field);
|
|
if (field_len >= name_len + 1 && field[name_len] == '='
|
|
&& strncmp(field, name, name_len) == 0) {
|
|
return decodeFormValue(field + name_len + 1, field_len - name_len - 1,
|
|
output, output_size);
|
|
}
|
|
field = end ? end + 1 : nullptr;
|
|
}
|
|
if (output && output_size) output[0] = 0;
|
|
return false;
|
|
}
|
|
|
|
static bool hasFormField(const char* body, const char* name) {
|
|
char value[8];
|
|
return getFormField(body, name, value, sizeof(value));
|
|
}
|
|
|
|
static void sendHeader(WiFiClient& client, int code, const char* reason,
|
|
const char* content_type = "text/html") {
|
|
client.printf("HTTP/1.1 %d %s\r\nContent-Type: %s\r\n"
|
|
"Connection: close\r\nCache-Control: no-store\r\n\r\n",
|
|
code, reason, content_type);
|
|
}
|
|
|
|
static void printHtmlEscaped(WiFiClient& client, const char* value) {
|
|
if (!value) return;
|
|
while (*value) {
|
|
switch (*value++) {
|
|
case '&': client.print(F("&")); break;
|
|
case '<': client.print(F("<")); break;
|
|
case '>': client.print(F(">")); break;
|
|
case '\"': client.print(F(""")); break;
|
|
case '\'': client.print(F("'")); break;
|
|
default: client.write(static_cast<uint8_t>(value[-1])); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void printValueAttribute(WiFiClient& client, const char* value) {
|
|
client.print(F(" value=\""));
|
|
printHtmlEscaped(client, value);
|
|
client.print('\"');
|
|
}
|
|
|
|
static void snapshotPagePrefs(PortalImpl* impl, MQTTPrefs& snapshot) {
|
|
portENTER_CRITICAL(&impl->prefs_lock);
|
|
snapshot = impl->page_prefs;
|
|
portEXIT_CRITICAL(&impl->prefs_lock);
|
|
}
|
|
|
|
static uint16_t presetPort(const MQTTPresetDef& preset) {
|
|
const char* scheme = strstr(preset.server_url, "://");
|
|
if (!scheme) return 0;
|
|
const char* host = scheme + 3;
|
|
const char* colon = strchr(host, ':');
|
|
if (colon) {
|
|
char* end = nullptr;
|
|
const unsigned long port = strtoul(colon + 1, &end, 10);
|
|
if (end != colon + 1 && port <= 65535) return static_cast<uint16_t>(port);
|
|
}
|
|
if (strncmp(preset.server_url, "mqtt://", 7) == 0) return 1883;
|
|
if (strncmp(preset.server_url, "mqtts://", 8) == 0) return 8883;
|
|
if (strncmp(preset.server_url, "ws://", 5) == 0) return 80;
|
|
return 443;
|
|
}
|
|
|
|
static void sendConfigPage(WiFiClient& client, const MQTTPrefs& prefs) {
|
|
const char* selected_preset = prefs.mqtt_slot_preset[0][0]
|
|
? prefs.mqtt_slot_preset[0] : MQTT_PRESET_CUSTOM;
|
|
sendHeader(client, 200, "OK");
|
|
client.print(F(R"HTML(<!doctype html><html><head>
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>MeshCore MQTT setup</title><style>
|
|
body{font-family:sans-serif;max-width:38rem;margin:2rem auto;padding:0 1rem;color:#222}
|
|
label{display:block;margin-top:1rem}input,select,button{box-sizing:border-box;width:100%;font-size:1rem;padding:.65rem;margin-top:.3rem}
|
|
.checks label{display:inline-block;width:auto;margin-right:1rem}.checks input{width:auto}fieldset{margin-top:1.2rem;border:1px solid #bbb}
|
|
small{color:#555}button{margin-top:1.3rem}#iata.iata-missing{border:2px solid #c00}#iata.iata-ready{border:2px solid #111}</style></head><body>
|
|
<h2>MeshCore companion MQTT setup</h2>
|
|
<p>Configure or update the MQTT destination for this companion.</p>
|
|
<form method="post" action="/save">
|
|
<label>Preset<select id="preset" name="preset">
|
|
)HTML"));
|
|
client.print(F("<option value=\"custom\""));
|
|
if (strcmp(selected_preset, MQTT_PRESET_CUSTOM) == 0) client.print(F(" selected"));
|
|
client.print(F(">Custom broker</option>\n"));
|
|
for (int i = 0; i < MQTT_PRESET_COUNT; i++) {
|
|
const MQTTPresetDef& preset = MQTT_PRESETS[i];
|
|
client.print(F("<option value=\""));
|
|
printHtmlEscaped(client, preset.name);
|
|
client.print(F("\" data-url=\""));
|
|
printHtmlEscaped(client, preset.server_url);
|
|
client.printf("\" data-port=\"%u\" data-user=\"", presetPort(preset));
|
|
printHtmlEscaped(client, preset.userpass_username ? preset.userpass_username : "");
|
|
client.print(F("\" data-audience=\""));
|
|
printHtmlEscaped(client, preset.jwt_audience ? preset.jwt_audience : "");
|
|
client.print('\"');
|
|
if (strcmp(selected_preset, preset.name) == 0) client.print(F(" selected"));
|
|
client.print('>');
|
|
printHtmlEscaped(client, preset.name);
|
|
client.print(F("</option>\n"));
|
|
}
|
|
client.print(F(R"HTML(</select></label>
|
|
<small>Selecting a preset fills its endpoint details. Built-in presets use their compiled endpoint and authentication settings.</small>
|
|
<fieldset><legend>Identity and routing</legend>
|
|
)HTML"));
|
|
client.print(F("<label>Origin name (optional)<input name=\"origin\" maxlength=\"31\" autocomplete=\"off\""));
|
|
printValueAttribute(client, prefs.mqtt_origin);
|
|
client.print(F("></label>\n<label>IATA code<input id=\"iata\" name=\"iata\" maxlength=\"3\" pattern=\"[A-Za-z]{3}\" placeholder=\"SEA\""));
|
|
printValueAttribute(client, prefs.mqtt_iata);
|
|
client.print(F(R"HTML(></label>
|
|
<small>A three-letter IATA code is required unless a custom topic template or MeshRank token supplies the route.</small>
|
|
)HTML"));
|
|
client.print(F("<label>Owner public key (optional)<input name=\"owner\" maxlength=\"64\" autocomplete=\"off\""));
|
|
printValueAttribute(client, prefs.mqtt_owner_public_key);
|
|
client.print(F("></label>\n<label>Email (optional)<input name=\"email\" type=\"email\" maxlength=\"63\" autocomplete=\"off\""));
|
|
printValueAttribute(client, prefs.mqtt_email);
|
|
client.print(F(R"HTML(></label>
|
|
</fieldset>
|
|
<fieldset><legend>Custom broker or preset credentials</legend>
|
|
)HTML"));
|
|
client.print(F("<label>Broker host or URI<input id=\"host\" name=\"host\" maxlength=\"63\" placeholder=\"mqtt://broker.example:1883\""));
|
|
printValueAttribute(client, prefs.mqtt_slot_host[0]);
|
|
client.print(F("></label>\n<label>Port<input id=\"port\" name=\"port\" type=\"number\" min=\"0\" max=\"65535\""));
|
|
client.printf(" value=\"%u\"></label>\n", prefs.mqtt_slot_port[0]);
|
|
client.print(F("<label>Username<input id=\"username\" name=\"username\" maxlength=\"31\" autocomplete=\"username\""));
|
|
printValueAttribute(client, prefs.mqtt_slot_username[0]);
|
|
client.print(F("></label>\n<label>Password<input id=\"password\" name=\"password\" type=\"password\" maxlength=\"63\" autocomplete=\"new-password\" placeholder=\"Leave blank to keep saved password\"></label>\n"));
|
|
if (prefs.mqtt_slot_password[0][0]) {
|
|
client.print(F("<label><input style=\"width:auto\" type=\"checkbox\" name=\"clear_password\" value=\"1\"> Clear saved password</label>\n"));
|
|
}
|
|
client.print(F("<label>Token (optional)<input id=\"token\" name=\"token\" maxlength=\"47\" autocomplete=\"off\""));
|
|
printValueAttribute(client, prefs.mqtt_slot_token[0]);
|
|
client.print(F("></label>\n<label>Topic template (optional)<input id=\"topic\" name=\"topic\" maxlength=\"95\" placeholder=\"meshcore/{device_id}/packets\""));
|
|
printValueAttribute(client, prefs.mqtt_slot_topic[0]);
|
|
client.print(F("></label>\n<label>JWT audience (custom broker only, optional)<input id=\"audience\" name=\"audience\" maxlength=\"63\""));
|
|
printValueAttribute(client, prefs.mqtt_slot_audience[0]);
|
|
client.print(F(R"HTML(></label>
|
|
</fieldset>
|
|
<fieldset class="checks"><legend>Publish</legend>
|
|
)HTML"));
|
|
client.printf("<label><input type=\"checkbox\" name=\"status\" value=\"1\"%s> Status</label>\n",
|
|
prefs.mqtt_status_enabled ? " checked" : "");
|
|
client.printf("<label><input type=\"checkbox\" name=\"packets\" value=\"1\"%s> Packets</label>\n",
|
|
prefs.mqtt_packets_enabled ? " checked" : "");
|
|
client.printf("<label><input type=\"checkbox\" name=\"raw\" value=\"1\"%s> Raw</label>\n",
|
|
prefs.mqtt_raw_enabled ? " checked" : "");
|
|
client.printf("<label><input type=\"checkbox\" name=\"rx\" value=\"1\"%s> RX</label>\n",
|
|
prefs.mqtt_rx_enabled ? " checked" : "");
|
|
client.print(F("<label>TX<select name=\"tx\"><option value=\"2\""));
|
|
if (prefs.mqtt_tx_enabled == 2) client.print(F(" selected"));
|
|
client.print(F(">Self adverts</option><option value=\"1\""));
|
|
if (prefs.mqtt_tx_enabled == 1) client.print(F(" selected"));
|
|
client.print(F(">All TX</option><option value=\"0\""));
|
|
if (prefs.mqtt_tx_enabled == 0) client.print(F(" selected"));
|
|
client.print(F(R"HTML(>Off</option></select></label>
|
|
</fieldset>
|
|
<button type="submit">Save and apply MQTT settings</button></form>
|
|
<p><small>This page remains available on the joined WiFi network at this node's IP address.</small></p>
|
|
<script>
|
|
const preset=document.getElementById('preset');
|
|
const iata=document.getElementById('iata');
|
|
function updateIataBox(){
|
|
const ready=/^[A-Za-z]{3}$/.test(iata.value.trim());
|
|
iata.classList.toggle('iata-ready',ready);
|
|
iata.classList.toggle('iata-missing',!ready);
|
|
}
|
|
function fillPreset(){
|
|
const o=preset.options[preset.selectedIndex], custom=o.value==='custom';
|
|
document.getElementById('host').value=custom?'':(o.dataset.url||'');
|
|
document.getElementById('port').value=custom?'1883':(o.dataset.port||'0');
|
|
document.getElementById('username').value=custom?'':(o.dataset.user||'');
|
|
document.getElementById('password').value='';
|
|
document.getElementById('audience').value=custom?'':(o.dataset.audience||'');
|
|
document.getElementById('topic').value='';
|
|
if(o.value!=='meshrank')document.getElementById('token').value='';
|
|
}
|
|
preset.addEventListener('change',fillPreset);
|
|
iata.addEventListener('input',updateIataBox);
|
|
updateIataBox();
|
|
if(preset.value!=='custom'&&!document.getElementById('host').value)fillPreset();
|
|
</script>
|
|
</body></html>)HTML"));
|
|
}
|
|
|
|
static void sendMessage(WiFiClient& client, int code, const char* reason,
|
|
const char* heading, const char* message) {
|
|
sendHeader(client, code, reason);
|
|
client.printf("<!doctype html><meta name=viewport content='width=device-width'>"
|
|
"<h2>%s</h2><p>%s</p>", heading, message);
|
|
client.print("<p><a href='/'>Return to MQTT settings</a></p>");
|
|
}
|
|
|
|
static bool isThreeLetterCode(const char* value) {
|
|
if (!value || strlen(value) != 3) return false;
|
|
for (int i = 0; i < 3; i++) {
|
|
if (!isalpha(static_cast<unsigned char>(value[i]))) return false;
|
|
}
|
|
return strcasecmp(value, "XXX") != 0;
|
|
}
|
|
|
|
static void copyString(char* dest, size_t dest_size, const char* src) {
|
|
strncpy(dest, src ? src : "", dest_size - 1);
|
|
dest[dest_size - 1] = 0;
|
|
}
|
|
|
|
static bool populatePrefs(const char* body, MQTTPrefs& prefs,
|
|
char* error, size_t error_size) {
|
|
char previous_preset[sizeof(prefs.mqtt_slot_preset[0])];
|
|
char previous_password[sizeof(prefs.mqtt_slot_password[0])];
|
|
char preset[24];
|
|
char origin[32];
|
|
char iata[8];
|
|
char owner[65];
|
|
char email[64];
|
|
char host[64];
|
|
char port_text[8];
|
|
char username[32];
|
|
char password[64];
|
|
char token[48];
|
|
char topic[96];
|
|
char audience[64];
|
|
char tx_text[4];
|
|
|
|
copyString(previous_preset, sizeof(previous_preset), prefs.mqtt_slot_preset[0]);
|
|
copyString(previous_password, sizeof(previous_password), prefs.mqtt_slot_password[0]);
|
|
|
|
if (!getFormField(body, "preset", preset, sizeof(preset))) {
|
|
snprintf(error, error_size, "Select an MQTT preset or custom broker.");
|
|
return false;
|
|
}
|
|
const MQTTPresetDef* preset_def = findMQTTPreset(preset);
|
|
const bool custom = strcmp(preset, MQTT_PRESET_CUSTOM) == 0;
|
|
if (!custom && !preset_def) {
|
|
snprintf(error, error_size, "The selected MQTT preset is not valid.");
|
|
return false;
|
|
}
|
|
|
|
getFormField(body, "origin", origin, sizeof(origin));
|
|
getFormField(body, "iata", iata, sizeof(iata));
|
|
getFormField(body, "owner", owner, sizeof(owner));
|
|
getFormField(body, "email", email, sizeof(email));
|
|
getFormField(body, "host", host, sizeof(host));
|
|
getFormField(body, "port", port_text, sizeof(port_text));
|
|
getFormField(body, "username", username, sizeof(username));
|
|
getFormField(body, "password", password, sizeof(password));
|
|
getFormField(body, "token", token, sizeof(token));
|
|
getFormField(body, "topic", topic, sizeof(topic));
|
|
getFormField(body, "audience", audience, sizeof(audience));
|
|
getFormField(body, "tx", tx_text, sizeof(tx_text));
|
|
if (!password[0] && !hasFormField(body, "clear_password")
|
|
&& strcmp(preset, previous_preset) == 0) {
|
|
copyString(password, sizeof(password), previous_password);
|
|
}
|
|
|
|
char* port_end = nullptr;
|
|
const unsigned long parsed_port = strtoul(port_text, &port_end, 10);
|
|
if (!port_text[0] || !port_end || *port_end || parsed_port > 65535) {
|
|
snprintf(error, error_size, "Enter a valid MQTT port from 0 through 65535.");
|
|
return false;
|
|
}
|
|
if (custom && !host[0]) {
|
|
snprintf(error, error_size, "A broker host or URI is required for a custom broker.");
|
|
return false;
|
|
}
|
|
if (custom && parsed_port == 0 && strstr(host, "://") == nullptr) {
|
|
snprintf(error, error_size, "A custom host without a URI scheme needs a nonzero port.");
|
|
return false;
|
|
}
|
|
if (strcmp(preset, "meshrank") == 0 && !token[0]) {
|
|
snprintf(error, error_size, "The MeshRank preset requires an account token.");
|
|
return false;
|
|
}
|
|
if (preset_def && mqttPresetNeedsSlotCredentials(preset_def)
|
|
&& (!username[0] || !password[0])) {
|
|
snprintf(error, error_size, "That preset requires a username and password.");
|
|
return false;
|
|
}
|
|
const bool preset_needs_iata = preset_def
|
|
&& preset_def->topic_style == MQTT_TOPIC_MESHCORE;
|
|
const bool custom_needs_iata = custom && !topic[0];
|
|
if (preset_needs_iata && !isThreeLetterCode(iata)) {
|
|
snprintf(error, error_size, "That preset requires a valid three-letter IATA code.");
|
|
return false;
|
|
}
|
|
if (custom_needs_iata && !isThreeLetterCode(iata)) {
|
|
snprintf(error, error_size, "Enter a three-letter IATA code, or provide a custom topic template.");
|
|
return false;
|
|
}
|
|
|
|
const uint8_t tx_mode = (strcmp(tx_text, "0") == 0) ? 0
|
|
: (strcmp(tx_text, "1") == 0) ? 1 : 2;
|
|
|
|
char wifi_ssid[sizeof(prefs.wifi_ssid)];
|
|
char wifi_password[sizeof(prefs.wifi_password)];
|
|
copyString(wifi_ssid, sizeof(wifi_ssid), prefs.wifi_ssid);
|
|
copyString(wifi_password, sizeof(wifi_password), prefs.wifi_password);
|
|
|
|
applyMQTTDefaults(&prefs);
|
|
copyString(prefs.wifi_ssid, sizeof(prefs.wifi_ssid), wifi_ssid);
|
|
copyString(prefs.wifi_password, sizeof(prefs.wifi_password), wifi_password);
|
|
for (int i = 0; i < MAX_MQTT_SLOTS; i++) {
|
|
copyString(prefs.mqtt_slot_preset[i], sizeof(prefs.mqtt_slot_preset[i]), MQTT_PRESET_NONE);
|
|
}
|
|
|
|
copyString(prefs.mqtt_origin, sizeof(prefs.mqtt_origin), origin);
|
|
copyString(prefs.mqtt_iata, sizeof(prefs.mqtt_iata), iata);
|
|
for (size_t i = 0; prefs.mqtt_iata[i]; i++) {
|
|
prefs.mqtt_iata[i] = static_cast<char>(toupper(static_cast<unsigned char>(prefs.mqtt_iata[i])));
|
|
}
|
|
copyString(prefs.mqtt_owner_public_key, sizeof(prefs.mqtt_owner_public_key), owner);
|
|
copyString(prefs.mqtt_email, sizeof(prefs.mqtt_email), email);
|
|
copyString(prefs.mqtt_slot_preset[0], sizeof(prefs.mqtt_slot_preset[0]), preset);
|
|
copyString(prefs.mqtt_slot_host[0], sizeof(prefs.mqtt_slot_host[0]), host);
|
|
prefs.mqtt_slot_port[0] = static_cast<uint16_t>(parsed_port);
|
|
copyString(prefs.mqtt_slot_username[0], sizeof(prefs.mqtt_slot_username[0]), username);
|
|
copyString(prefs.mqtt_slot_password[0], sizeof(prefs.mqtt_slot_password[0]), password);
|
|
copyString(prefs.mqtt_slot_token[0], sizeof(prefs.mqtt_slot_token[0]), token);
|
|
copyString(prefs.mqtt_slot_topic[0], sizeof(prefs.mqtt_slot_topic[0]), topic);
|
|
copyString(prefs.mqtt_slot_audience[0], sizeof(prefs.mqtt_slot_audience[0]), audience);
|
|
prefs.mqtt_status_enabled = hasFormField(body, "status") ? 1 : 0;
|
|
prefs.mqtt_packets_enabled = hasFormField(body, "packets") ? 1 : 0;
|
|
prefs.mqtt_raw_enabled = hasFormField(body, "raw") ? 1 : 0;
|
|
prefs.mqtt_rx_enabled = hasFormField(body, "rx") ? 1 : 0;
|
|
prefs.mqtt_tx_enabled = tx_mode;
|
|
return true;
|
|
}
|
|
|
|
static bool handleClient(PortalImpl* impl, WiFiClient& client) {
|
|
char line[256];
|
|
if (!readLine(client, line, sizeof(line))) return false;
|
|
const bool post_save = strncmp(line, "POST /save ", 11) == 0;
|
|
const bool get_request = strncmp(line, "GET ", 4) == 0;
|
|
size_t content_length = 0;
|
|
do {
|
|
if (!readLine(client, line, sizeof(line))) return false;
|
|
if (strncasecmp(line, "Content-Length:", 15) == 0) {
|
|
content_length = static_cast<size_t>(strtoul(line + 15, nullptr, 10));
|
|
}
|
|
} while (line[0]);
|
|
|
|
if (get_request) {
|
|
sendConfigPage(client, impl->page_prefs);
|
|
return false;
|
|
}
|
|
if (!post_save || content_length == 0 || content_length > MAX_FORM_BODY) {
|
|
sendMessage(client, 400, "Bad Request", "Invalid request", "The MQTT form data was missing or too large.");
|
|
return false;
|
|
}
|
|
|
|
char* body = static_cast<char*>(malloc(content_length + 1));
|
|
if (!body) {
|
|
sendMessage(client, 503, "Service Unavailable", "Out of memory", "Reboot the companion and try again.");
|
|
return false;
|
|
}
|
|
const size_t received = client.readBytes(body, content_length);
|
|
body[received] = 0;
|
|
if (received != content_length) {
|
|
free(body);
|
|
sendMessage(client, 408, "Request Timeout", "Incomplete request", "The complete MQTT form was not received.");
|
|
return false;
|
|
}
|
|
|
|
MQTTPrefs candidate;
|
|
snapshotPagePrefs(impl, candidate);
|
|
char error[180];
|
|
const bool valid = populatePrefs(body, candidate, error, sizeof(error));
|
|
free(body);
|
|
if (!valid) {
|
|
sendMessage(client, 400, "Bad Request", "MQTT setup needs attention", error);
|
|
return false;
|
|
}
|
|
if (!CompanionMqttSetupPortal::saveStoredConfig(candidate)) {
|
|
sendMessage(client, 500, "Internal Server Error", "Could not save MQTT setup",
|
|
"The configuration was valid but could not be written to flash.");
|
|
return false;
|
|
}
|
|
|
|
portENTER_CRITICAL(&impl->prefs_lock);
|
|
impl->page_prefs = candidate;
|
|
impl->saved = true;
|
|
portEXIT_CRITICAL(&impl->prefs_lock);
|
|
|
|
sendMessage(client, 200, "OK", "MQTT setup saved",
|
|
"The MQTT client is applying the new settings. This page remains available for later changes.");
|
|
return true;
|
|
}
|
|
|
|
static void portalTask(void* arg) {
|
|
PortalImpl* impl = static_cast<PortalImpl*>(arg);
|
|
while (impl->active && *impl->active
|
|
&& WiFi.status() == WL_CONNECTED && WiFi.getMode() == WIFI_STA) {
|
|
WiFiClient client = impl->server.available();
|
|
if (client) {
|
|
client.setTimeout(5000);
|
|
handleClient(impl, client);
|
|
client.stop();
|
|
}
|
|
delay(5);
|
|
}
|
|
|
|
impl->server.stop();
|
|
impl->task = nullptr;
|
|
if (impl->active) *impl->active = false;
|
|
vTaskDelete(nullptr);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
CompanionMqttSetupPortal::CompanionMqttSetupPortal()
|
|
: _active(false), _impl(nullptr) {}
|
|
|
|
bool CompanionMqttSetupPortal::begin(MQTTPrefs* prefs) {
|
|
if (_active) return true;
|
|
if (!prefs || WiFi.status() != WL_CONNECTED || WiFi.getMode() != WIFI_STA) return false;
|
|
PortalImpl* impl = static_cast<PortalImpl*>(_impl);
|
|
if (!impl) {
|
|
impl = new PortalImpl();
|
|
if (!impl) return false;
|
|
_impl = impl;
|
|
}
|
|
if (impl->task != nullptr) return false;
|
|
impl->prefs = prefs;
|
|
impl->active = &_active;
|
|
impl->page_prefs = *prefs;
|
|
impl->saved = false;
|
|
impl->server.begin();
|
|
impl->server.setNoDelay(true);
|
|
_active = true;
|
|
if (xTaskCreatePinnedToCore(portalTask, "mqtt-setup", 8192, impl, 1,
|
|
&impl->task, 0) != pdPASS) {
|
|
_active = false;
|
|
impl->server.stop();
|
|
impl->task = nullptr;
|
|
return false;
|
|
}
|
|
Serial.printf("MQTT setup: reconnect to the joined WiFi and open http://%s/\n",
|
|
WiFi.localIP().toString().c_str());
|
|
return true;
|
|
}
|
|
|
|
void CompanionMqttSetupPortal::stop() {
|
|
_active = false;
|
|
PortalImpl* impl = static_cast<PortalImpl*>(_impl);
|
|
if (impl) impl->server.stop();
|
|
}
|
|
|
|
bool CompanionMqttSetupPortal::loop() {
|
|
PortalImpl* impl = static_cast<PortalImpl*>(_impl);
|
|
if (!impl) return false;
|
|
bool saved = false;
|
|
portENTER_CRITICAL(&impl->prefs_lock);
|
|
if (impl->saved && impl->prefs) {
|
|
*impl->prefs = impl->page_prefs;
|
|
impl->saved = false;
|
|
saved = true;
|
|
}
|
|
portEXIT_CRITICAL(&impl->prefs_lock);
|
|
return saved;
|
|
}
|
|
|
|
bool CompanionMqttSetupPortal::loadStoredConfig(MQTTPrefs& prefs) {
|
|
Preferences nvs;
|
|
if (!nvs.begin(NVS_NAMESPACE, true)) return false;
|
|
const uint16_t version = nvs.getUShort(NVS_VERSION_KEY, 0);
|
|
const size_t length = nvs.getBytesLength(NVS_PREFS_KEY);
|
|
const bool shape_ok = version == MQTT_PREFS_VERSION && length == sizeof(MQTTPrefs);
|
|
const size_t read = shape_ok ? nvs.getBytes(NVS_PREFS_KEY, &prefs, sizeof(prefs)) : 0;
|
|
nvs.end();
|
|
if (read != sizeof(prefs)) return false;
|
|
|
|
if (!storedStringsAreValid(prefs)
|
|
|| prefs.mqtt_status_enabled > 1
|
|
|| prefs.mqtt_packets_enabled > 1
|
|
|| prefs.mqtt_raw_enabled > 1
|
|
|| prefs.mqtt_rx_enabled > 1
|
|
|| prefs.mqtt_tx_enabled > 2
|
|
|| prefs.wifi_power_save > 2) {
|
|
return false;
|
|
}
|
|
|
|
const char* preset = prefs.mqtt_slot_preset[0];
|
|
const bool custom = strcmp(preset, MQTT_PRESET_CUSTOM) == 0;
|
|
const MQTTPresetDef* preset_def = custom ? nullptr : findMQTTPreset(preset);
|
|
if (!preset[0] || (!custom && !preset_def)) return false;
|
|
if (custom) {
|
|
const char* host = prefs.mqtt_slot_host[0];
|
|
if (!host[0]
|
|
|| (prefs.mqtt_slot_port[0] == 0 && strstr(host, "://") == nullptr)
|
|
|| (!prefs.mqtt_slot_topic[0][0] && !isThreeLetterCode(prefs.mqtt_iata))) {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (preset_def->topic_style == MQTT_TOPIC_MESHCORE
|
|
&& !isThreeLetterCode(prefs.mqtt_iata)) return false;
|
|
if (preset_def->topic_style == MQTT_TOPIC_MESHRANK
|
|
&& !prefs.mqtt_slot_token[0][0]) return false;
|
|
if (mqttPresetNeedsSlotCredentials(preset_def)
|
|
&& (!prefs.mqtt_slot_username[0][0] || !prefs.mqtt_slot_password[0][0])) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CompanionMqttSetupPortal::saveStoredConfig(const MQTTPrefs& prefs) {
|
|
Preferences nvs;
|
|
if (!nvs.begin(NVS_NAMESPACE, false)) return false;
|
|
const size_t written = nvs.putBytes(NVS_PREFS_KEY, &prefs, sizeof(prefs));
|
|
const size_t version_written = nvs.putUShort(NVS_VERSION_KEY, MQTT_PREFS_VERSION);
|
|
nvs.end();
|
|
return written == sizeof(prefs) && version_written == sizeof(uint16_t);
|
|
}
|
|
|
|
#endif
|