feat(mqtt): add Waev broker
Add Waev (mqtt.waev.app) as a curated MQTT broker, selectable from the web-panel Primary/Secondary dropdowns and the CLI. - Broker spec on bit 0x20, audience mqtt.waev.app, Google Trust Services WE1 CA (matches Waev's cert chain); widen broker mask/count to 0x3F/6 - Per-broker JWT lifetime via BrokerSpec.token_ttl_secs: Waev enforces a 1-hour max token TTL and rejects the 6h default with MQTT CONNACK 5, so it mints a 3600s token while other brokers keep the 6h default - CLI get/set mqtt.waev, web-panel dropdown entry + hidden toggle, docs - Log token TTL in the "token ready" line for broker auth debugging fix(web): make Custom MQTT settings full width when Custom is selected Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
dac0923899
commit
6ad3e5224e
7 files changed
+37
-13
No files matched your search
@@ -145,6 +145,7 @@ uv run --group docs zensical build
|
||||
- curated broker support for:
|
||||
- `eastmesh-au`
|
||||
- `meshmapper`
|
||||
- `waev`
|
||||
- `letsmesh-eu` (retired)
|
||||
- `letsmesh-us` (retired)
|
||||
- WSS transport at `/mqtt`
|
||||
|
||||
@@ -67,6 +67,8 @@ No-argument `get` commands must be entered exactly as shown.
|
||||
- `set mqtt.eastmesh-au on|off`
|
||||
- `get mqtt.meshmapper`
|
||||
- `set mqtt.meshmapper on|off`
|
||||
- `get mqtt.waev`
|
||||
- `set mqtt.waev on|off`
|
||||
- `get mqtt.letsmesh-eu` (retired)
|
||||
- `set mqtt.letsmesh-eu on|off` (retired)
|
||||
- `get mqtt.letsmesh-us` (retired)
|
||||
@@ -89,6 +91,7 @@ Notes:
|
||||
- new observer installs default `mqtt.iata` to `UNSET`
|
||||
- a maximum of two MQTT brokers can be enabled at once
|
||||
- `meshmapper` is the curated global broker (`wss://mqtt.meshmapper.net:443/mqtt`); like `eastmesh-au` it uses WSS, verified TLS, and MeshCore JWT auth with the broker host as the token audience
|
||||
- `waev` is a curated broker (`wss://mqtt.waev.app:443/mqtt`); like `eastmesh-au` it uses WSS, verified TLS, and MeshCore JWT auth with the broker host as the token audience, but enforces a 1-hour auth token lifetime (the firmware mints a shorter-lived token for it automatically)
|
||||
- `letsmesh-eu` and `letsmesh-us` are retired (LetsMesh is no longer maintained) and off by default; any saved selections are cleared once on upgrade, though the endpoints stay selectable for legacy use
|
||||
- if `mqtt.iata` is `UNSET`, enabled MQTT brokers will not connect
|
||||
- custom MQTT uses the configured username and password, not JWT authentication
|
||||
|
||||
@@ -2446,6 +2446,8 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x01) ? "on" : "off");
|
||||
} else if (strcmp(command, "get mqtt.meshmapper") == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x10) ? "on" : "off");
|
||||
} else if (strcmp(command, "get mqtt.waev") == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x20) ? "on" : "off");
|
||||
} else if (strcmp(command, "get mqtt.letsmesh-eu") == 0 || strcmp(command, "get mqtt.letsmesh.eu") == 0) {
|
||||
sprintf(reply, "> %s", mqtt.isEndpointEnabled(0x02) ? "on" : "off");
|
||||
} else if (strcmp(command, "get mqtt.letsmesh-us") == 0 || strcmp(command, "get mqtt.letsmesh.us") == 0) {
|
||||
@@ -2504,6 +2506,12 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
||||
} else {
|
||||
strcpy(reply, "Err - max 2 mqtt brokers");
|
||||
}
|
||||
} else if (memcmp(command, "set mqtt.waev ", 14) == 0) {
|
||||
if (mqtt.setEndpointEnabled(0x20, memcmp(&command[14], "on", 2) == 0)) {
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Err - max 2 mqtt brokers");
|
||||
}
|
||||
} else if (memcmp(command, "set mqtt.letsmesh-eu ", 21) == 0 || memcmp(command, "set mqtt.letsmesh.eu ", 21) == 0) {
|
||||
if (mqtt.setEndpointEnabled(0x02, memcmp(&command[21], "on", 2) == 0)) {
|
||||
strcpy(reply, "OK");
|
||||
|
||||
@@ -75,7 +75,7 @@ bool MQTTPrefsStore::load(FILESYSTEM* fs, MQTTPrefs& prefs) {
|
||||
prefs.custom_transport = 0;
|
||||
}
|
||||
prefs.status_interval_ms = kFixedStatusIntervalMs;
|
||||
prefs.enabled_mask &= 0x1F;
|
||||
prefs.enabled_mask &= 0x3F;
|
||||
if (!prefs.brokers_migrated) {
|
||||
// LetsMesh is retired. Clear any saved EU/US selections once so dead brokers
|
||||
// stop retrying; the endpoints remain re-selectable afterwards.
|
||||
|
||||
@@ -128,14 +128,16 @@ void logMqttMemorySnapshot(const char*, const char* = nullptr) {
|
||||
}
|
||||
|
||||
const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[kBrokerCount] = {
|
||||
{"eastmesh-au", "eastmesh-au", "mqtt2.eastmesh.au", "wss://mqtt2.eastmesh.au:443/mqtt", kEastmeshBit, false},
|
||||
{"meshmapper", "meshmapper", "mqtt.meshmapper.net", "wss://mqtt.meshmapper.net:443/mqtt", kMeshmapperBit, false},
|
||||
{"eastmesh-au", "eastmesh-au", "mqtt2.eastmesh.au", "wss://mqtt2.eastmesh.au:443/mqtt", kEastmeshBit, false, 0},
|
||||
{"meshmapper", "meshmapper", "mqtt.meshmapper.net", "wss://mqtt.meshmapper.net:443/mqtt", kMeshmapperBit, false, 0},
|
||||
// waev enforces a shorter max token lifetime than the curated default; match its documented config.
|
||||
{"waev", "waev", "mqtt.waev.app", "wss://mqtt.waev.app:443/mqtt", kWaevBit, false, 3600},
|
||||
// Retired: LetsMesh is no longer maintained. Kept selectable for legacy nodes; new nodes should use meshmapper.
|
||||
{"letsmesh-eu", "letsmesh-eu", "mqtt-eu-v1.letsmesh.net", "wss://mqtt-eu-v1.letsmesh.net:443/mqtt",
|
||||
kLetsmeshEuBit, false},
|
||||
kLetsmeshEuBit, false, 0},
|
||||
{"letsmesh-us", "letsmesh-us", "mqtt-us-v1.letsmesh.net", "wss://mqtt-us-v1.letsmesh.net:443/mqtt",
|
||||
kLetsmeshUsBit, false},
|
||||
{"custom", "custom", nullptr, nullptr, kCustomBit, true},
|
||||
kLetsmeshUsBit, false, 0},
|
||||
{"custom", "custom", nullptr, nullptr, kCustomBit, true, 0},
|
||||
};
|
||||
|
||||
MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
|
||||
@@ -202,7 +204,8 @@ const char* MQTTUplink::brokerCaCert(const BrokerSpec& spec) {
|
||||
if (spec.bit == kEastmeshBit || spec.bit == kMeshmapperBit) {
|
||||
return mqtt_ca_certs::kEastmeshIsrgRootX1Pem; // Let's Encrypt ISRG Root X1
|
||||
}
|
||||
return mqtt_ca_certs::kLetsmeshWe1Pem;
|
||||
// waev (mqtt.waev.app) presents a Google Trust Services WE1 chain, same issuer as LetsMesh.
|
||||
return mqtt_ca_certs::kLetsmeshWe1Pem; // Google Trust Services WE1
|
||||
}
|
||||
|
||||
const char* MQTTUplink::brokerHost(const BrokerState& broker) const {
|
||||
@@ -572,7 +575,8 @@ bool MQTTUplink::refreshToken(BrokerState& broker) {
|
||||
}
|
||||
}
|
||||
|
||||
time_t expires_at = now + kTokenLifetimeSecs;
|
||||
time_t ttl = broker.spec->token_ttl_secs != 0 ? static_cast<time_t>(broker.spec->token_ttl_secs) : kTokenLifetimeSecs;
|
||||
time_t expires_at = now + ttl;
|
||||
const char* owner = _prefs.owner_public_key[0] ? _prefs.owner_public_key : nullptr;
|
||||
const char* email = _prefs.owner_email[0] ? _prefs.owner_email : nullptr;
|
||||
if (!JWTHelper::createAuthToken(*_identity, brokerHost(broker), now, expires_at, broker.token, kBrokerTokenSize,
|
||||
@@ -583,8 +587,9 @@ bool MQTTUplink::refreshToken(BrokerState& broker) {
|
||||
return false;
|
||||
}
|
||||
broker.token_expires_at = expires_at;
|
||||
MQTT_LOG("%s token ready exp=%lu owner=%s email=%s", broker.spec->label,
|
||||
static_cast<unsigned long>(expires_at), owner != nullptr ? "yes" : "no", email != nullptr ? "yes" : "no");
|
||||
MQTT_LOG("%s token ready exp=%lu ttl=%ld owner=%s email=%s", broker.spec->label,
|
||||
static_cast<unsigned long>(expires_at), static_cast<long>(ttl),
|
||||
owner != nullptr ? "yes" : "no", email != nullptr ? "yes" : "no");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,6 +89,9 @@ private:
|
||||
const char* uri;
|
||||
uint8_t bit;
|
||||
bool custom;
|
||||
// Per-broker JWT lifetime in seconds; 0 = use the default kTokenLifetimeSecs. waev enforces a
|
||||
// shorter max token lifetime than the curated default, so it gets its own (matches its config).
|
||||
uint32_t token_ttl_secs;
|
||||
};
|
||||
|
||||
struct BrokerState {
|
||||
@@ -131,9 +134,10 @@ private:
|
||||
static constexpr uint8_t kLetsmeshUsBit = 0x04;
|
||||
static constexpr uint8_t kCustomBit = 0x08;
|
||||
static constexpr uint8_t kMeshmapperBit = 0x10;
|
||||
static constexpr uint8_t kBrokerMask = 0x1F;
|
||||
static constexpr uint8_t kWaevBit = 0x20;
|
||||
static constexpr uint8_t kBrokerMask = 0x3F;
|
||||
static constexpr uint8_t kMaxEnabledBrokers = 2;
|
||||
static constexpr size_t kBrokerCount = 5;
|
||||
static constexpr size_t kBrokerCount = 6;
|
||||
static const BrokerSpec kBrokerSpecs[kBrokerCount];
|
||||
static bool isUnsetIataValue(const char* iata);
|
||||
static const char* brokerCaCert(const BrokerSpec& spec);
|
||||
|
||||
@@ -977,12 +977,13 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
</div>
|
||||
<input id="mqttEastmeshAu" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
|
||||
<input id="mqttMeshmapper" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
|
||||
<input id="mqttWaev" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
|
||||
<input id="mqttLetsmeshEu" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
|
||||
<input id="mqttLetsmeshUs" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
|
||||
<input id="mqttCustom" class="visually-hidden" type="checkbox" tabindex="-1" aria-hidden="true">
|
||||
<div class="broker-group wide" id="mqttCustomConfig" style="display:none">
|
||||
<div class="broker-group-title">Custom MQTT settings</div>
|
||||
<div class="broker-grid">
|
||||
<div class="broker-grid single">
|
||||
<div class="broker-card">
|
||||
<div class="field-card">
|
||||
<label class="label" for="mqttCustomEndpoint">Host:Port</label>
|
||||
@@ -2705,6 +2706,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
const MQTT_BROKERS = [
|
||||
{ key:"eastmesh-au", input:"mqttEastmeshAu", label:"EastMesh AU", set:"set mqtt.eastmesh-au" },
|
||||
{ key:"meshmapper", input:"mqttMeshmapper", label:"MeshMapper", set:"set mqtt.meshmapper" },
|
||||
{ key:"waev", input:"mqttWaev", label:"Waev", set:"set mqtt.waev" },
|
||||
{ key:"custom", input:"mqttCustom", label:"Custom", set:"set mqtt.custom", custom:true },
|
||||
{ key:"letsmesh-eu", input:"mqttLetsmeshEu", label:"LetsMesh EU (retired)", set:"set mqtt.letsmesh-eu" },
|
||||
{ key:"letsmesh-us", input:"mqttLetsmeshUs", label:"LetsMesh US (retired)", set:"set mqtt.letsmesh-us" }
|
||||
@@ -3192,6 +3194,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
||||
() => loadField("get mqtt.email", "mqttEmail", null, quiet),
|
||||
() => loadBrokerState("get mqtt.eastmesh-au", "mqttEastmeshAu", quiet),
|
||||
() => loadBrokerState("get mqtt.meshmapper", "mqttMeshmapper", quiet),
|
||||
() => loadBrokerState("get mqtt.waev", "mqttWaev", quiet),
|
||||
() => loadBrokerState("get mqtt.letsmesh-eu", "mqttLetsmeshEu", quiet),
|
||||
() => loadBrokerState("get mqtt.letsmesh-us", "mqttLetsmeshUs", quiet),
|
||||
() => loadBrokerState("get mqtt.custom", "mqttCustom", quiet),
|
||||
|
||||
Reference in new issue
Block a user