fix: extend MQTT JWT lifetime and suppress reconnect noise during planned refresh

This commit is contained in:
Jared Dohrman committed 2026-04-26 18:10:33 +10:00
1 parent b003cdc9d9
commit c729a58c81
7 files changed
+45 -8

No files matched your search

+2
View File
@@ -229,6 +229,8 @@ The `/stats` page currently shows:
- `Neighbours`: current neighbour table with ID, SNR, heard age, and advert age
- `Events`: current boot/session events
Planned MQTT JWT rotations are shown as `mqtt_token_refreshed` events instead of a short `mqtt_disconnected` / `mqtt_connected` pair.
For boards that expose extra telemetry, the optional `Environment` summary card can show current values such as GPS fix state, latitude, longitude, GPS altitude, voltage, sensor temperature, humidity, barometer, pressure-derived altitude, and MCU temperature.
Metrics with no current value are hidden rather than showing placeholder rows, so the cards vary by board and by current sensor state.
+17 -2
View File
@@ -1793,8 +1793,12 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
const bool wifi_connected = network.isWifiConnected();
#ifdef WITH_MQTT_UPLINK
const bool mqtt_connected = mqtt.isAnyBrokerConnected();
const uint32_t mqtt_token_refresh_count = mqtt.getTokenRefreshCount();
const bool mqtt_token_refreshing = mqtt.isTokenRefreshInProgress();
#else
const bool mqtt_connected = false;
const uint32_t mqtt_token_refresh_count = 0;
const bool mqtt_token_refreshing = false;
#endif
const bool web_panel_up = web.isPanelRunning();
const bool archive_mounted = (_archive != nullptr) && _archive->isMounted();
@@ -1821,11 +1825,22 @@ void MyMesh::updateStatsHistory(unsigned long now_ms) {
_stats_state.web_panel_up = web_panel_up;
_stats_state.archive_mounted = archive_mounted;
_stats_state.low_memory = low_memory;
_stats_state.mqtt_token_refresh_count = mqtt_token_refresh_count;
_stats_state.last_low_memory_event_uptime_secs = 0;
} else {
if (_stats_state.mqtt_token_refresh_count != mqtt_token_refresh_count) {
const uint32_t refresh_delta = mqtt_token_refresh_count - _stats_state.mqtt_token_refresh_count;
recordStatsEvent(HISTORY_EVENT_MQTT_TOKEN_REFRESHED,
static_cast<int16_t>(min<uint32_t>(refresh_delta, 32767)));
_stats_state.mqtt_token_refresh_count = mqtt_token_refresh_count;
}
if (_stats_state.mqtt_connected != mqtt_connected) {
recordStatsEvent(mqtt_connected ? HISTORY_EVENT_MQTT_CONNECTED : HISTORY_EVENT_MQTT_DISCONNECTED);
_stats_state.mqtt_connected = mqtt_connected;
if (_stats_state.mqtt_connected && !mqtt_connected && mqtt_token_refreshing) {
// Planned JWT rotation briefly drops MQTT; keep the event stream focused on real outages.
} else {
recordStatsEvent(mqtt_connected ? HISTORY_EVENT_MQTT_CONNECTED : HISTORY_EVENT_MQTT_DISCONNECTED);
_stats_state.mqtt_connected = mqtt_connected;
}
}
if (_stats_state.web_panel_up != web_panel_up) {
recordStatsEvent(web_panel_up ? HISTORY_EVENT_WEB_STARTED : HISTORY_EVENT_WEB_STOPPED);
+1
View File
@@ -185,6 +185,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks, public WebPanelComm
bool web_panel_up;
bool archive_mounted;
bool low_memory;
uint32_t mqtt_token_refresh_count;
uint32_t last_low_memory_event_uptime_secs;
} _stats_state;
+3
View File
@@ -396,6 +396,7 @@ uint8_t eventTypeFromName(const char* name) {
if (strcmp(name, "low_memory") == 0) return HISTORY_EVENT_LOW_MEMORY;
if (strcmp(name, "stats_enabled") == 0) return HISTORY_EVENT_STATS_ENABLED;
if (strcmp(name, "stats_disabled") == 0) return HISTORY_EVENT_STATS_DISABLED;
if (strcmp(name, "mqtt_token_refreshed") == 0) return HISTORY_EVENT_MQTT_TOKEN_REFRESHED;
return 0;
}
@@ -1369,6 +1370,8 @@ const char* StatsHistory::getEventTypeName(uint8_t type) {
return "stats_enabled";
case HISTORY_EVENT_STATS_DISABLED:
return "stats_disabled";
case HISTORY_EVENT_MQTT_TOKEN_REFRESHED:
return "mqtt_token_refreshed";
default:
return "unknown";
}
+1
View File
@@ -86,6 +86,7 @@ enum HistoryEventType : uint8_t {
HISTORY_EVENT_LOW_MEMORY = 8,
HISTORY_EVENT_STATS_ENABLED = 9,
HISTORY_EVENT_STATS_DISABLED = 10,
HISTORY_EVENT_MQTT_TOKEN_REFRESHED = 11,
};
class StatsHistory {
+17 -6
View File
@@ -43,8 +43,9 @@
namespace {
constexpr unsigned long kBrokerRetryBaseMillis = 10000;
constexpr unsigned long kBrokerRetryMaxMillis = 300000;
constexpr unsigned long kTokenRefreshEventWindowMs = 2UL * 60UL * 1000UL;
constexpr size_t kBrokerTokenSize = 640;
constexpr time_t kTokenLifetimeSecs = 3600;
constexpr time_t kTokenLifetimeSecs = 6UL * 60UL * 60UL;
constexpr time_t kTokenRefreshSlackSecs = 300;
constexpr time_t kMinSaneEpoch = 1735689600; // 2025-01-01T00:00:00Z
@@ -104,8 +105,8 @@ const MQTTUplink::BrokerSpec MQTTUplink::kBrokerSpecs[3] = {
};
MQTTUplink::MQTTUplink(mesh::RTCClock& rtc, mesh::LocalIdentity& identity)
: _fs(nullptr), _rtc(&rtc), _identity(&identity), _running(false), _last_status_publish(0), _last_status{},
_node_name(nullptr), _network(nullptr)
: _fs(nullptr), _rtc(&rtc), _identity(&identity), _running(false), _last_status_publish(0),
_token_refresh_count(0), _token_refresh_active_until_ms(0), _last_status{}, _node_name(nullptr), _network(nullptr)
{
memset(_device_id, 0, sizeof(_device_id));
MQTTPrefsStore::setDefaults(_prefs);
@@ -124,6 +125,11 @@ bool MQTTUplink::savePrefs() {
return MQTTPrefsStore::save(_fs, _prefs);
}
bool MQTTUplink::isTokenRefreshInProgress() const {
return _token_refresh_active_until_ms != 0 &&
static_cast<long>(millis() - _token_refresh_active_until_ms) < 0;
}
bool MQTTUplink::hasEnabledBroker() const {
return (_prefs.enabled_mask & 0x07) != 0;
}
@@ -622,11 +628,15 @@ void MQTTUplink::ensureBroker(BrokerState& broker, bool allow_new_connect) {
}
time_t now = time(nullptr);
unsigned long now_ms = millis();
if (broker.client != nullptr && broker.token_expires_at > 0 && now + kTokenRefreshSlackSecs >= broker.token_expires_at) {
_token_refresh_count++;
_token_refresh_active_until_ms = now_ms + kTokenRefreshEventWindowMs;
MQTT_LOG("%s token refresh reconnect count=%lu", broker.spec->label,
static_cast<unsigned long>(_token_refresh_count));
destroyBroker(broker, false);
}
unsigned long now_ms = millis();
if (broker.client != nullptr) {
if (broker.connected) {
return;
@@ -1000,8 +1010,8 @@ const char* MQTTUplink::getAggregateBrokerState() const {
#else
MQTTUplink::MQTTUplink(mesh::RTCClock&, mesh::LocalIdentity&)
: _fs(nullptr), _rtc(nullptr), _identity(nullptr), _running(false), _last_status_publish(0), _last_status{},
_node_name(nullptr), _network(nullptr) {
: _fs(nullptr), _rtc(nullptr), _identity(nullptr), _running(false), _last_status_publish(0),
_token_refresh_count(0), _token_refresh_active_until_ms(0), _last_status{}, _node_name(nullptr), _network(nullptr) {
MQTTPrefsStore::setDefaults(_prefs);
}
@@ -1024,6 +1034,7 @@ bool MQTTUplink::setOwnerEmail(const char*) { return false; }
bool MQTTUplink::sendStatusNow() { return false; }
bool MQTTUplink::isAnyBrokerConnected() const { return false; }
const char* MQTTUplink::getAggregateBrokerState() const { return "down"; }
bool MQTTUplink::isTokenRefreshInProgress() const { return false; }
#endif
+4
View File
@@ -65,6 +65,8 @@ public:
bool isAnyBrokerConnected() const;
const char* getAggregateBrokerState() const;
void setNetworkStateProvider(NetworkStateProvider* network) { _network = network; }
uint32_t getTokenRefreshCount() const { return _token_refresh_count; }
bool isTokenRefreshInProgress() const;
private:
#if defined(ESP_PLATFORM)
@@ -101,6 +103,8 @@ private:
MQTTPrefs _prefs;
bool _running;
unsigned long _last_status_publish;
uint32_t _token_refresh_count;
unsigned long _token_refresh_active_until_ms;
MQTTStatusSnapshot _last_status;
char _device_id[65];
const char* _node_name;