diff --git a/README.md b/README.md index 8988e468..b78c119b 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,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` diff --git a/eastmesh-docs/custom-cli.md b/eastmesh-docs/custom-cli.md index 0e9be13e..dee72e8a 100644 --- a/eastmesh-docs/custom-cli.md +++ b/eastmesh-docs/custom-cli.md @@ -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 diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 1dd57add..15cf375e 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -2454,6 +2454,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) { @@ -2512,6 +2514,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"); diff --git a/release-notes.yml b/release-notes.yml index 258c34b3..8824f3ed 100644 --- a/release-notes.yml +++ b/release-notes.yml @@ -242,6 +242,30 @@ releases: - "`get mqtt.status` output changed from per-broker fields to `p:`/`s:` broker slots; anything parsing the previous format must be updated." - "Saved LetsMesh EU/US broker selections are cleared once on upgrade; re-enable a broker such as MeshMapper if a second uplink is required." + - track: observer-eastmesh + version: "2026.6.5" + tag: "observer-eastmesh-v2026.6.5" + date: "2026-06-26" + previous_version: "2026.6.4" + summary: "Adds the Waev curated MQTT broker and makes the Custom MQTT settings panel full width when Custom is selected." + changes: + - type: added + area: mqtt + text: "Added the `waev` curated broker (`wss://mqtt.waev.app:443/mqtt`) using WSS, verified TLS, and MeshCore JWT authentication with the broker host as the token audience." + - type: added + area: cli + text: "Added `get mqtt.waev` and `set mqtt.waev on|off` to toggle the Waev broker." + - type: added + area: mqtt + text: "Added per-broker JWT token lifetimes so Waev receives a 1-hour token to match its shorter maximum token lifetime." + - type: fixed + area: web + text: "Made the Custom MQTT settings panel span the full width of the MQTT Servers card when Custom is selected." + - type: docs + area: docs + text: "Documented the Waev broker in the README and custom CLI guides." + breaking_changes: [] + - track: observer-eastmesh-bridge-espnow version: "2026.5.1" tag: "observer-eastmesh-bridge-espnow-v2026.5.1" @@ -395,6 +419,7 @@ releases: - "`get mqtt.status` output changed from per-broker fields to `p:`/`s:` broker slots; anything parsing the previous format must be updated." - "Saved LetsMesh EU/US broker selections are cleared once on upgrade; re-enable a broker such as MeshMapper if a second uplink is required." +<<<<<<< HEAD - track: observer-eastmesh-bridge-mqtt version: "2026.7.0" tag: "observer-eastmesh-bridge-mqtt-v2026.7.0" @@ -417,4 +442,28 @@ releases: - type: added area: boards text: "Added `Xiao_S3_WIO_repeater_observer_mqtt_bridge` as the first supported observer MQTT bridge target." +======= + - track: observer-eastmesh-bridge-espnow + version: "2026.6.5" + tag: "observer-eastmesh-bridge-espnow-v2026.6.5" + date: "2026-06-26" + previous_version: "2026.6.4" + summary: "Adds the Waev curated MQTT broker and makes the Custom MQTT settings panel full width in Observer ESP-NOW bridge builds." + changes: + - type: added + area: mqtt + text: "Added the `waev` curated broker (`wss://mqtt.waev.app:443/mqtt`) using WSS, verified TLS, and MeshCore JWT authentication with the broker host as the token audience." + - type: added + area: cli + text: "Added `get mqtt.waev` and `set mqtt.waev on|off` to toggle the Waev broker." + - type: added + area: mqtt + text: "Added per-broker JWT token lifetimes so Waev receives a 1-hour token to match its shorter maximum token lifetime." + - type: fixed + area: web + text: "Made the Custom MQTT settings panel span the full width of the MQTT Servers card when Custom is selected." + - type: docs + area: docs + text: "Documented the Waev broker in the README and custom CLI guides." +>>>>>>> cdf2cc6fa283615a04c488ca3ad324a33b72966d breaking_changes: [] diff --git a/src/helpers/mqtt/MQTTPrefs.cpp b/src/helpers/mqtt/MQTTPrefs.cpp index 93a279aa..4bc7e282 100644 --- a/src/helpers/mqtt/MQTTPrefs.cpp +++ b/src/helpers/mqtt/MQTTPrefs.cpp @@ -80,7 +80,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. diff --git a/src/helpers/mqtt/MQTTUplink.cpp b/src/helpers/mqtt/MQTTUplink.cpp index 352c58ce..74c60438 100644 --- a/src/helpers/mqtt/MQTTUplink.cpp +++ b/src/helpers/mqtt/MQTTUplink.cpp @@ -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(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(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(expires_at), static_cast(ttl), + owner != nullptr ? "yes" : "no", email != nullptr ? "yes" : "no"); return true; } diff --git a/src/helpers/mqtt/MQTTUplink.h b/src/helpers/mqtt/MQTTUplink.h index 0f106004..82f421fb 100644 --- a/src/helpers/mqtt/MQTTUplink.h +++ b/src/helpers/mqtt/MQTTUplink.h @@ -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); diff --git a/src/helpers/web/WebPanelServer.cpp b/src/helpers/web/WebPanelServer.cpp index 2d051923..1b8bef7b 100644 --- a/src/helpers/web/WebPanelServer.cpp +++ b/src/helpers/web/WebPanelServer.cpp @@ -977,12 +977,13 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML( +