Files
MeshCore/src/helpers/bridges/MQTTBridge.cpp
T
agessaman 2185523df6 Add sorting functions for neighbour information in MyMesh and optimize MQTTBridge for memory efficiency
- Introduced comparison functions for sorting neighbours by timestamp and signal strength in MyMesh.
- Implemented early exit conditions in MQTTBridge to improve processing efficiency when no neighbours are present.
- Enhanced MQTTBridge to optimize memory usage by adjusting MQTT client configurations and implementing memory pressure checks.
- Reduced processing limits in MQTTBridge to maintain responsiveness and prevent blocking during packet handling.
2026-01-02 13:36:41 -08:00

2410 lines
98 KiB
C++

#include "MQTTBridge.h"
#include "../MQTTMessageBuilder.h"
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Timezone.h>
#ifdef ESP_PLATFORM
#include <esp_wifi.h>
#endif
// Helper function to strip quotes from strings (both single and double quotes)
static void stripQuotes(char* str, size_t max_len) {
if (!str || max_len == 0) return;
size_t len = strlen(str);
if (len == 0) return;
// Remove leading quote (single or double)
if (str[0] == '"' || str[0] == '\'') {
memmove(str, str + 1, len);
len--;
}
// Remove trailing quote (single or double)
if (len > 0 && (str[len-1] == '"' || str[len-1] == '\'')) {
str[len-1] = '\0';
}
}
// Helper function to check if WiFi credentials are valid
static bool isWiFiConfigValid(const NodePrefs* prefs) {
// Check if WiFi SSID is configured (not empty)
if (strlen(prefs->wifi_ssid) == 0) {
return false;
}
// WiFi password can be empty for open networks, so we don't check it
return true;
}
#ifdef WITH_MQTT_BRIDGE
MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity)
: BridgeBase(prefs, mgr, rtc), _mqtt_client(nullptr),
_active_brokers(0), _queue_head(0), _queue_tail(0), _queue_count(0),
_last_status_publish(0), _last_status_retry(0), _status_interval(300000), // 5 minutes default
_ntp_client(_ntp_udp, "pool.ntp.org", 0, 60000), _last_ntp_sync(0), _ntp_synced(false),
_timezone(nullptr), _last_raw_len(0), _last_snr(0), _last_rssi(0), _last_raw_timestamp(0),
_analyzer_us_enabled(false), _analyzer_eu_enabled(false), _identity(identity),
_analyzer_us_client(nullptr), _analyzer_eu_client(nullptr), _config_valid(false),
_last_memory_check(0), _skipped_publishes(0),
_last_no_broker_log(0), _last_config_warning(0), _dispatcher(nullptr), _radio(nullptr), _board(nullptr), _ms(nullptr) {
// Initialize default values
strncpy(_origin, "MeshCore-Repeater", sizeof(_origin) - 1);
strncpy(_iata, "XXX", sizeof(_iata) - 1);
strncpy(_device_id, "DEVICE_ID_PLACEHOLDER", sizeof(_device_id) - 1);
strncpy(_firmware_version, "unknown", sizeof(_firmware_version) - 1);
strncpy(_board_model, "unknown", sizeof(_board_model) - 1);
strncpy(_build_date, "unknown", sizeof(_build_date) - 1);
_status_enabled = true;
_packets_enabled = true;
_raw_enabled = false;
_tx_enabled = false; // Disable TX packets by default
// Initialize MQTT server settings with defaults (empty/null values)
_prefs->mqtt_server[0] = '\0'; // Empty string
_prefs->mqtt_port = 0; // Invalid port
_prefs->mqtt_username[0] = '\0'; // Empty string
_prefs->mqtt_password[0] = '\0'; // Empty string
// Override with build flags if defined
#ifdef MQTT_SERVER
strncpy(_prefs->mqtt_server, MQTT_SERVER, sizeof(_prefs->mqtt_server) - 1);
#endif
#ifdef MQTT_PORT
_prefs->mqtt_port = MQTT_PORT;
#endif
#ifdef MQTT_USERNAME
strncpy(_prefs->mqtt_username, MQTT_USERNAME, sizeof(_prefs->mqtt_username) - 1);
#endif
#ifdef MQTT_PASSWORD
strncpy(_prefs->mqtt_password, MQTT_PASSWORD, sizeof(_prefs->mqtt_password) - 1);
#endif
// Initialize packet queue
memset(_packet_queue, 0, sizeof(_packet_queue));
// Initialize has_raw_data flags
for (int i = 0; i < MAX_QUEUE_SIZE; i++) {
_packet_queue[i].has_raw_data = false;
}
// Initialize throttle log timers
_last_no_broker_log = 0;
_last_analyzer_us_log = 0;
_last_analyzer_eu_log = 0;
// Set default broker configuration
setBrokerDefaults();
}
void MQTTBridge::begin() {
MQTT_DEBUG_PRINTLN("Initializing MQTT Bridge...");
// Check if WiFi credentials are configured first
if (!isWiFiConfigValid(_prefs)) {
MQTT_DEBUG_PRINTLN("MQTT Bridge initialization skipped - WiFi credentials not configured");
return;
}
// Validate custom MQTT broker configuration (optional)
_config_valid = isMQTTConfigValid();
if (!_config_valid) {
MQTT_DEBUG_PRINTLN("No valid custom MQTT server configured - analyzer servers will still work");
} else {
MQTT_DEBUG_PRINTLN("Custom MQTT server configuration is valid");
}
// Update origin and IATA from preferences
strncpy(_origin, _prefs->mqtt_origin, sizeof(_origin) - 1);
_origin[sizeof(_origin) - 1] = '\0';
strncpy(_iata, _prefs->mqtt_iata, sizeof(_iata) - 1);
_iata[sizeof(_iata) - 1] = '\0';
// Strip quotes from MQTT server configuration if present
stripQuotes(_prefs->mqtt_server, sizeof(_prefs->mqtt_server));
stripQuotes(_prefs->mqtt_username, sizeof(_prefs->mqtt_username));
stripQuotes(_prefs->mqtt_password, sizeof(_prefs->mqtt_password));
// Strip quotes from origin if present
MQTT_DEBUG_PRINTLN("Origin before stripping: '%s'", _origin);
stripQuotes(_origin, sizeof(_origin));
MQTT_DEBUG_PRINTLN("Origin after stripping: '%s'", _origin);
// Strip quotes from IATA if present
MQTT_DEBUG_PRINTLN("IATA before stripping: '%s'", _iata);
stripQuotes(_iata, sizeof(_iata));
MQTT_DEBUG_PRINTLN("IATA after stripping: '%s'", _iata);
// Convert IATA code to uppercase (IATA codes are conventionally uppercase)
for (int i = 0; _iata[i]; i++) {
_iata[i] = toupper(_iata[i]);
}
// Update enabled flags from preferences
_status_enabled = _prefs->mqtt_status_enabled;
_packets_enabled = _prefs->mqtt_packets_enabled;
_raw_enabled = _prefs->mqtt_raw_enabled;
_tx_enabled = _prefs->mqtt_tx_enabled;
// Set status interval to 5 minutes (300000 ms), or use preference if set and valid
if (_prefs->mqtt_status_interval >= 1000 && _prefs->mqtt_status_interval <= 3600000) {
_status_interval = _prefs->mqtt_status_interval;
MQTT_DEBUG_PRINTLN("Using preference status interval: %lu ms", _status_interval);
} else {
// Invalid or uninitialized value - fix it in preferences and use default
if (_prefs->mqtt_status_interval > 0 && _prefs->mqtt_status_interval != 300000) {
MQTT_DEBUG_PRINTLN("Invalid preference status interval: %lu ms (fixing to default 300000 ms)",
_prefs->mqtt_status_interval);
}
_prefs->mqtt_status_interval = 300000; // Fix the preference value
_status_interval = 300000; // 5 minutes default
// Note: We don't save preferences here as that should be done by the caller if needed
// This ensures the correct value is used for this session
}
MQTT_DEBUG_PRINTLN("Status publishing: enabled=%s, interval=%lu ms",
_status_enabled ? "true" : "false", _status_interval);
// Check for configuration mismatch: bridge.source=tx but mqtt.tx=off
checkConfigurationMismatch();
MQTT_DEBUG_PRINTLN("Origin: %s, IATA: %s", _origin, _iata);
MQTT_DEBUG_PRINTLN("Device ID: %s", _device_id);
MQTT_DEBUG_PRINTLN("WiFi SSID: %s", _prefs->wifi_ssid);
// Initialize WiFi
MQTT_DEBUG_PRINTLN("Starting WiFi...");
WiFi.mode(WIFI_STA);
// Enable automatic reconnection - ESP32 will handle reconnection automatically
WiFi.setAutoReconnect(true);
WiFi.setAutoConnect(true);
// Set up WiFi event handlers for better diagnostics and immediate disconnection detection
#ifdef ESP_PLATFORM
WiFi.onEvent([this](WiFiEvent_t event, WiFiEventInfo_t info) {
switch(event) {
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
// Log disconnection event - reason code access varies by ESP32 Arduino version
// Just log the event; detailed reason tracking happens in loop() monitoring
MQTT_DEBUG_PRINTLN("WiFi event: DISCONNECTED");
// Auto-reconnect is enabled, but we'll also monitor in loop() for active reconnection
break;
case ARDUINO_EVENT_WIFI_STA_CONNECTED:
MQTT_DEBUG_PRINTLN("WiFi event: CONNECTED to AP");
break;
case ARDUINO_EVENT_WIFI_STA_GOT_IP:
MQTT_DEBUG_PRINTLN("WiFi event: GOT_IP - %s", IPAddress(info.got_ip.ip_info.ip.addr).toString().c_str());
break;
case ARDUINO_EVENT_WIFI_STA_LOST_IP:
MQTT_DEBUG_PRINTLN("WiFi event: LOST_IP");
break;
default:
// Other events not critical for our use case
break;
}
});
#endif
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
// Wait for WiFi connection
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
MQTT_DEBUG_PRINT(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
MQTT_DEBUG_PRINTLN("WiFi connected! IP: %s", WiFi.localIP().toString().c_str());
// Configure WiFi power management for efficient operation
#ifdef ESP_PLATFORM
// Set WiFi power save mode from preferences (0=min, 1=none, 2=max)
wifi_ps_type_t ps_mode;
uint8_t ps_pref = _prefs->wifi_power_save;
if (ps_pref == 1) {
ps_mode = WIFI_PS_NONE;
} else if (ps_pref == 2) {
ps_mode = WIFI_PS_MAX_MODEM;
} else {
ps_mode = WIFI_PS_MIN_MODEM; // Default to min (0) if invalid or unset
}
esp_err_t ps_result = esp_wifi_set_ps(ps_mode);
if (ps_result == ESP_OK) {
const char* ps_name = (ps_mode == WIFI_PS_NONE) ? "none" :
(ps_mode == WIFI_PS_MAX_MODEM) ? "max" : "min";
MQTT_DEBUG_PRINTLN("WiFi power save mode set to: %s", ps_name);
} else {
MQTT_DEBUG_PRINTLN("Failed to set WiFi power save mode: %d", ps_result);
}
// Set WiFi TX power - use build flag if defined, otherwise default to 11dBm
#ifdef MQTT_WIFI_TX_POWER
WiFi.setTxPower(MQTT_WIFI_TX_POWER);
MQTT_DEBUG_PRINTLN("WiFi TX power set via build flag (MQTT_WIFI_TX_POWER)");
#else
WiFi.setTxPower(WIFI_POWER_11dBm);
MQTT_DEBUG_PRINTLN("WiFi TX power set to 11dBm (default)");
#endif
#endif
// Sync time with NTP
syncTimeWithNTP();
} else {
MQTT_DEBUG_PRINTLN("WiFi connection failed! Auto-reconnect enabled, will retry automatically");
// Don't return here - allow MQTT bridge to initialize even if WiFi isn't connected yet
// Auto-reconnect will handle reconnection in the background
}
// Initialize PsychicMqttClient
_mqtt_client = new PsychicMqttClient();
// Optimize MQTT client configuration for memory efficiency
// Main client doesn't use JWT tokens, so can use smaller buffer
optimizeMqttClientConfig(_mqtt_client, false);
// Set up event callbacks for the main MQTT client
_mqtt_client->onConnect([this](bool sessionPresent) {
MQTT_DEBUG_PRINTLN("MQTT client connected, session present: %s", sessionPresent ? "true" : "false");
// Update broker connection status
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && !_brokers[i].connected) {
_brokers[i].connected = true;
_active_brokers++;
MQTT_DEBUG_PRINTLN("Broker %d marked as connected", i);
break;
}
}
});
_mqtt_client->onDisconnect([this](bool sessionPresent) {
MQTT_DEBUG_PRINTLN("MQTT client disconnected, session present: %s", sessionPresent ? "true" : "false");
// Update broker connection status
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].connected) {
_brokers[i].connected = false;
_active_brokers--;
MQTT_DEBUG_PRINTLN("Broker %d marked as disconnected", i);
break;
}
}
});
// Set default broker from preferences or build flags
setBroker(0, _prefs->mqtt_server, _prefs->mqtt_port, _prefs->mqtt_username, _prefs->mqtt_password, true);
// Setup Let's Mesh Analyzer servers
setupAnalyzerServers();
// Setup PsychicMqttClient WebSocket clients for analyzer servers
setupAnalyzerClients();
// Connect to brokers
connectToBrokers();
_initialized = true;
MQTT_DEBUG_PRINTLN("MQTT Bridge initialized");
}
void MQTTBridge::end() {
MQTT_DEBUG_PRINTLN("Stopping MQTT Bridge...");
// Disconnect from all brokers
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
_mqtt_client->disconnect();
_brokers[i].connected = false;
}
}
// Disconnect analyzer clients
if (_analyzer_us_client) {
_analyzer_us_client->disconnect();
delete _analyzer_us_client;
_analyzer_us_client = nullptr;
}
if (_analyzer_eu_client) {
_analyzer_eu_client->disconnect();
delete _analyzer_eu_client;
_analyzer_eu_client = nullptr;
}
// Clean up queued packets to prevent memory leaks
for (int i = 0; i < _queue_count; i++) {
int index = (_queue_head + i) % MAX_QUEUE_SIZE;
if (_packet_queue[index].packet) {
_mgr->free(_packet_queue[index].packet);
_packet_queue[index].packet = nullptr;
}
// Clear the entire structure to free raw_data buffers
memset(&_packet_queue[index], 0, sizeof(QueuedPacket));
}
// Clear packet queue
_queue_count = 0;
_queue_head = 0;
_queue_tail = 0;
// Clear all queue slots for safety
memset(_packet_queue, 0, sizeof(_packet_queue));
// Clean up timezone object to prevent memory leak
if (_timezone) {
delete _timezone;
_timezone = nullptr;
}
// Clean up resources
if (_mqtt_client) {
delete _mqtt_client;
_mqtt_client = nullptr;
}
_initialized = false;
MQTT_DEBUG_PRINTLN("MQTT Bridge stopped");
}
bool MQTTBridge::isConfigValid() const {
return _config_valid;
}
bool MQTTBridge::isConfigValid(const NodePrefs* prefs) {
// Check if MQTT server is configured (not default placeholder)
if (strlen(prefs->mqtt_server) == 0 ||
strcmp(prefs->mqtt_server, "your-mqtt-broker.com") == 0) {
return false;
}
// Check if MQTT port is valid
if (prefs->mqtt_port == 0 || prefs->mqtt_port > 65535) {
return false;
}
// Check if MQTT username is configured (not default placeholder)
if (strlen(prefs->mqtt_username) == 0 ||
strcmp(prefs->mqtt_username, "your-username") == 0) {
return false;
}
// Check if MQTT password is configured (not default placeholder)
if (strlen(prefs->mqtt_password) == 0 ||
strcmp(prefs->mqtt_password, "your-password") == 0) {
return false;
}
return true;
}
void MQTTBridge::checkConfigurationMismatch() {
// Check if bridge.source is set to tx (logTx) but mqtt.tx is disabled
// This would prevent packet publishing since sendPacket() requires both packets_enabled and tx_enabled
if (_prefs->bridge_pkt_src == 0 && _packets_enabled && !_tx_enabled) {
unsigned long now = millis();
// Always log on first detection, then throttle to every 5 minutes to avoid spam
if (_last_config_warning == 0 || (now - _last_config_warning > CONFIG_WARNING_INTERVAL)) {
MQTT_DEBUG_PRINTLN("MQTT: Configuration mismatch detected! bridge.source=tx (logTx) but mqtt.tx=off. Packets will not be published. Run 'set bridge.source rx' or 'set mqtt.tx on' to fix.");
_last_config_warning = now;
}
} else {
// Configuration is correct, reset warning timer so we log immediately if it becomes wrong again
_last_config_warning = 0;
}
}
bool MQTTBridge::isReady() const {
return _initialized && isWiFiConfigValid(_prefs);
}
void MQTTBridge::loop() {
if (!_initialized) return;
// Actively monitor and manage WiFi connection
static unsigned long last_wifi_check = 0;
static unsigned long last_wifi_reconnect_attempt = 0;
static wl_status_t last_wifi_status = WL_DISCONNECTED;
static bool wifi_status_initialized = false;
static unsigned long wifi_disconnected_time = 0;
unsigned long now = millis();
wl_status_t current_wifi_status = WiFi.status();
// Initialize last_wifi_status on first loop() call to avoid false "reconnected" message
if (!wifi_status_initialized) {
last_wifi_status = current_wifi_status;
wifi_status_initialized = true;
if (current_wifi_status == WL_CONNECTED) {
MQTT_DEBUG_PRINTLN("WiFi connected! IP: %s", WiFi.localIP().toString().c_str());
}
}
// Check WiFi status every 10 seconds for faster detection
if (now - last_wifi_check > 10000) {
last_wifi_check = now;
if (current_wifi_status == WL_CONNECTED) {
if (last_wifi_status != WL_CONNECTED) {
// WiFi just reconnected (after being disconnected)
MQTT_DEBUG_PRINTLN("WiFi reconnected! IP: %s", WiFi.localIP().toString().c_str());
wifi_disconnected_time = 0;
}
last_wifi_status = WL_CONNECTED;
} else {
// WiFi is disconnected
if (last_wifi_status == WL_CONNECTED) {
// WiFi just disconnected
MQTT_DEBUG_PRINTLN("WiFi disconnected! Status: %d", current_wifi_status);
wifi_disconnected_time = now;
} else if (wifi_disconnected_time > 0) {
// WiFi has been disconnected for a while
unsigned long disconnected_duration = now - wifi_disconnected_time;
// Try to force reconnection if disconnected for more than 30 seconds
// and we haven't tried in the last 30 seconds
if (disconnected_duration > 30000 && (now - last_wifi_reconnect_attempt) > 30000) {
MQTT_DEBUG_PRINTLN("WiFi still disconnected after %lu ms, attempting manual reconnect...", disconnected_duration);
last_wifi_reconnect_attempt = now;
// Force reconnection attempt
WiFi.disconnect();
delay(100);
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
MQTT_DEBUG_PRINTLN("WiFi.begin() called - waiting for connection...");
} else if (disconnected_duration > 60000) {
// Log every minute if still disconnected
static unsigned long last_disconnect_log = 0;
if (now - last_disconnect_log > 60000) {
MQTT_DEBUG_PRINTLN("WiFi disconnected for %lu seconds, status: %d", disconnected_duration / 1000, current_wifi_status);
last_disconnect_log = now;
}
}
}
last_wifi_status = current_wifi_status;
}
}
// Maintain broker connections
connectToBrokers();
// Maintain analyzer server connections
maintainAnalyzerConnections();
// Process packet queue
processPacketQueue();
// Periodic configuration check (throttled to avoid spam)
checkConfigurationMismatch();
// Periodic NTP sync (every hour) - only when connected
if (WiFi.status() == WL_CONNECTED && millis() - _last_ntp_sync > 3600000) {
syncTimeWithNTP();
}
// Publish status updates (handle millis() overflow correctly)
if (_status_enabled) {
// Check if we have any destinations available before attempting to publish
bool has_custom_brokers = isAnyBrokerConnected() && _config_valid;
bool has_analyzer_servers = (_analyzer_us_enabled && _analyzer_us_client && _analyzer_us_client->connected()) ||
(_analyzer_eu_enabled && _analyzer_eu_client && _analyzer_eu_client->connected());
bool has_destinations = has_custom_brokers || has_analyzer_servers;
// Only attempt to publish if we have destinations available
if (has_destinations) {
unsigned long now = millis();
unsigned long elapsed = (now >= _last_status_publish) ?
(now - _last_status_publish) :
(ULONG_MAX - _last_status_publish + now + 1);
// Check if enough time has passed since last successful publish
bool should_publish = (elapsed >= _status_interval);
// If interval hasn't passed, check if we should retry after a failed publish
// Only check retry if _last_status_retry != 0 (meaning we had a failed publish)
if (!should_publish && _last_status_retry != 0) {
unsigned long retry_elapsed = (now >= _last_status_retry) ?
(now - _last_status_retry) :
(ULONG_MAX - _last_status_retry + now + 1);
// Retry if enough time has passed since last retry attempt
should_publish = (retry_elapsed >= STATUS_RETRY_INTERVAL);
}
if (should_publish) {
MQTT_DEBUG_PRINTLN("Status publish timer expired (elapsed: %lu ms, interval: %lu ms)", elapsed, _status_interval);
_last_status_retry = now; // Update retry timestamp
if (publishStatus()) {
_last_status_publish = now; // Only update timer on successful publication
_last_status_retry = 0; // Reset retry timer on success (0 = no retry needed)
MQTT_DEBUG_PRINTLN("Status published successfully, next publish in %lu ms", _status_interval);
} else {
MQTT_DEBUG_PRINTLN("Status publish failed, will retry in %lu ms", STATUS_RETRY_INTERVAL);
// Retry timer (_last_status_retry) already updated above
}
}
} else {
// No destinations available - reset retry timer to avoid spam
if (_last_status_retry != 0) {
_last_status_retry = 0;
}
}
}
// Memory monitoring (every 5 minutes)
static unsigned long last_memory_log = 0;
if (millis() - last_memory_log > 300000) { // 5 minutes
logMemoryStatus();
// Debug: Log status timer state when memory check happens
if (_status_enabled) {
unsigned long now = millis();
unsigned long elapsed = (now >= _last_status_publish) ?
(now - _last_status_publish) :
(ULONG_MAX - _last_status_publish + now + 1);
unsigned long next_publish = (elapsed < _status_interval) ? (_status_interval - elapsed) : 0;
MQTT_DEBUG_PRINTLN("Memory check: Status timer - elapsed: %lu ms, interval: %lu ms, next: %lu ms",
elapsed, _status_interval, next_publish);
}
last_memory_log = millis();
}
// Critical memory check (every 15 minutes)
static unsigned long last_critical_check = 0;
if (millis() - last_critical_check > 900000) { // 15 minutes
if (ESP.getMaxAllocHeap() < 60000) { // Less than 60KB max alloc
MQTT_DEBUG_PRINTLN("WARNING: Max alloc heap below 60KB - potential memory leak detected!");
MQTT_DEBUG_PRINTLN("Free: %d, Min: %d, Max: %d", ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
// Attempt memory defragmentation by forcing garbage collection
MQTT_DEBUG_PRINTLN("Attempting memory defragmentation...");
// Force a small allocation and immediate free to trigger defrag
void* temp = malloc(1024);
if (temp) {
free(temp);
MQTT_DEBUG_PRINTLN("Defragmentation complete. New Max Alloc: %d", ESP.getMaxAllocHeap());
}
}
// Critical threshold check
if (ESP.getMaxAllocHeap() < 40000) { // Less than 40KB max alloc
MQTT_DEBUG_PRINTLN("CRITICAL: Max alloc heap below 40KB - severe memory leak!");
MQTT_DEBUG_PRINTLN("Free: %d, Min: %d, Max: %d", ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap());
}
last_critical_check = millis();
}
}
void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
if (!_initialized || !_packets_enabled) {
MQTT_DEBUG_PRINTLN("Packet received but not processing - initialized: %s, packets_enabled: %s",
_initialized ? "true" : "false", _packets_enabled ? "true" : "false");
return;
}
// Check if we have any valid brokers to send to
bool has_valid_brokers = _config_valid ||
(_analyzer_us_enabled && _analyzer_us_client) ||
(_analyzer_eu_enabled && _analyzer_eu_client);
if (!has_valid_brokers) {
MQTT_DEBUG_PRINTLN("Packet received but no valid brokers available - discarding");
return;
}
// Early exit if queue is full to avoid unnecessary processing
// queuePacket() will handle dropping oldest, but we can skip debug logging here
if (_queue_count >= MAX_QUEUE_SIZE) {
// Queue full - queuePacket() will handle dropping oldest packet
// Continue to queuePacket() to maintain consistent behavior
}
// Debug logging for packet types that might be getting filtered
uint8_t packet_type = packet->getPayloadType();
if (packet_type == 4 || packet_type == 9) { // ADVERT or TRACE
MQTT_DEBUG_PRINTLN("Packet received: type=%d (ADVERT=%d, TRACE=%d), queuing for transmission",
packet_type, (packet_type == 4), (packet_type == 9));
}
// Queue packet for transmission
queuePacket(packet, false);
}
void MQTTBridge::sendPacket(mesh::Packet *packet) {
if (!_initialized || !_packets_enabled || !_tx_enabled) return;
// Queue packet for transmission (only if TX enabled)
queuePacket(packet, true);
}
bool MQTTBridge::isMQTTConfigValid() {
// Check if MQTT server is configured (not default placeholder)
if (strlen(_prefs->mqtt_server) == 0 ||
strcmp(_prefs->mqtt_server, "your-mqtt-broker.com") == 0) {
return false;
}
// Check if MQTT port is valid
if (_prefs->mqtt_port == 0 || _prefs->mqtt_port > 65535) {
return false;
}
// Check if MQTT username is configured (not default placeholder)
if (strlen(_prefs->mqtt_username) == 0 ||
strcmp(_prefs->mqtt_username, "your-username") == 0) {
return false;
}
// Check if MQTT password is configured (not default placeholder)
if (strlen(_prefs->mqtt_password) == 0 ||
strcmp(_prefs->mqtt_password, "your-password") == 0) {
return false;
}
return true;
}
bool MQTTBridge::isIATAValid() const {
// Check if IATA code is configured (not empty, not default "XXX")
if (strlen(_iata) == 0 || strcmp(_iata, "XXX") == 0) {
return false;
}
return true;
}
void MQTTBridge::connectToBrokers() {
// Check if MQTT configuration is valid before attempting connection
if (!_config_valid) {
return;
}
// For now, connect to the first enabled broker
// TODO: Implement multi-broker support with PsychicMqttClient
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (!_brokers[i].enabled) continue;
// Check if we need to attempt connection
if (!_brokers[i].connected &&
millis() - _brokers[i].last_attempt > _brokers[i].reconnect_interval) {
MQTT_DEBUG_PRINTLN("Connecting to broker %d: %s:%d", i, _brokers[i].host, _brokers[i].port);
// Generate unique client ID
char client_id[32];
snprintf(client_id, sizeof(client_id), "%s_%d_%lu", _origin, i, millis());
// Set broker URI and connect using PsychicMqttClient API
char broker_uri[128];
snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port);
_mqtt_client->setServer(broker_uri);
// Set credentials if provided
if (strlen(_brokers[i].username) > 0) {
_mqtt_client->setCredentials(_brokers[i].username, _brokers[i].password);
}
// Connect to the broker (PsychicMqttClient uses async connection)
_mqtt_client->connect();
// Update attempt timestamp
_brokers[i].last_attempt = millis();
MQTT_DEBUG_PRINTLN("Initiating connection to broker %d", i);
}
// Maintain connection and check for stale connections
if (_brokers[i].connected) {
// Check actual connection state - if it's stale, force reconnection
// This prevents accumulation of stale connections that appear connected but aren't responsive
if (!_mqtt_client->connected()) {
_brokers[i].connected = false;
_active_brokers--;
MQTT_DEBUG_PRINTLN("Lost connection to broker %d", i);
} else {
// Periodic health check: Force reconnection every 4 hours to prevent stale connections
// This addresses the responsiveness degradation issue after long uptime
static unsigned long last_health_check = 0;
const unsigned long HEALTH_CHECK_INTERVAL = 14400000; // 4 hours
unsigned long now = millis();
// Handle millis() overflow
unsigned long elapsed = (now >= last_health_check) ?
(now - last_health_check) :
(ULONG_MAX - last_health_check + now + 1);
if (elapsed >= HEALTH_CHECK_INTERVAL) {
MQTT_DEBUG_PRINTLN("Performing periodic connection health check for broker %d", i);
last_health_check = now;
// Disconnect and reconnect to refresh the connection
// This prevents accumulation of stale TCP connections or internal state
_mqtt_client->disconnect();
delay(100); // Brief delay to allow cleanup
// Reconnect with fresh connection
char broker_uri[128];
snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port);
_mqtt_client->setServer(broker_uri);
if (strlen(_brokers[i].username) > 0) {
_mqtt_client->setCredentials(_brokers[i].username, _brokers[i].password);
}
_mqtt_client->connect();
_brokers[i].last_attempt = now;
MQTT_DEBUG_PRINTLN("Reconnected broker %d for health check", i);
}
}
}
}
}
void MQTTBridge::processPacketQueue() {
if (_queue_count == 0) {
return;
}
// Check if we have any connected brokers (custom or analyzer)
bool has_connected_brokers = isAnyBrokerConnected() ||
(_analyzer_us_enabled && _analyzer_us_client && _analyzer_us_client->connected()) ||
(_analyzer_eu_enabled && _analyzer_eu_client && _analyzer_eu_client->connected());
if (!has_connected_brokers) {
if (_queue_count > 0) {
// Only log this message periodically to avoid spam (every 30 seconds max)
unsigned long now = millis();
if (now - _last_no_broker_log > NO_BROKER_LOG_INTERVAL) {
MQTT_DEBUG_PRINTLN("Queue has %d packets but no brokers connected", _queue_count);
_last_no_broker_log = now;
}
}
return;
}
// Reset the log timer when brokers are connected
_last_no_broker_log = 0;
// MQTT_DEBUG_PRINTLN("Processing packet queue - count: %d", _queue_count);
// Limit processing to 1 packet per loop iteration to maintain responsiveness
// Large packets (like neighbors list responses) can take significant time to process
// (JSON building + multiple broker publishes), so we spread the work across multiple loops
int processed = 0;
int max_per_loop = 1; // Process max 1 packet per loop to keep main loop responsive (reduced from 2)
unsigned long loop_start_time = millis();
const unsigned long MAX_PROCESSING_TIME_MS = 30; // Reduced from 50ms to 30ms for better responsiveness
while (_queue_count > 0 && processed < max_per_loop) {
// Check time budget - if we've spent too long, stop processing to maintain responsiveness
unsigned long elapsed = millis() - loop_start_time;
if (elapsed > MAX_PROCESSING_TIME_MS) {
MQTT_DEBUG_PRINTLN("Packet processing time budget exceeded (%lu ms), deferring remaining %d packets", elapsed, _queue_count);
break;
}
QueuedPacket& queued = _packet_queue[_queue_head];
MQTT_DEBUG_PRINTLN("Processing queued packet (is_tx: %s)", queued.is_tx ? "true" : "false");
// Publish packet (use stored raw data if available)
publishPacket(queued.packet, queued.is_tx,
queued.has_raw_data ? queued.raw_data : nullptr,
queued.has_raw_data ? queued.raw_len : 0,
queued.has_raw_data ? queued.snr : 0.0f,
queued.has_raw_data ? queued.rssi : 0.0f);
// Publish raw if enabled
if (_raw_enabled) {
publishRaw(queued.packet);
}
// Free packet memory before removing from queue
if (queued.packet) {
_mgr->free(queued.packet);
queued.packet = nullptr;
}
// Remove from queue
dequeuePacket();
processed++;
// Yield to main loop after each packet to maintain responsiveness
// This prevents blocking the main loop for extended periods
#ifdef ESP_PLATFORM
vTaskDelay(1); // Yield to other tasks (1 tick = ~10ms on ESP32)
#endif
}
}
bool MQTTBridge::publishStatus() {
// Check if IATA is configured before attempting to publish
if (!isIATAValid()) {
static unsigned long last_iata_warning = 0;
unsigned long now = millis();
// Only log this warning every 5 minutes to avoid spam
if (now - last_iata_warning > 300000) {
MQTT_DEBUG_PRINTLN("MQTT: Cannot publish status - IATA code not configured (current: '%s'). Please set mqtt.iata via CLI.", _iata);
last_iata_warning = now;
}
return false;
}
// Memory pressure check: Skip status publishes when heap is severely fragmented
// Status publishes are less critical than packet publishes, so we can skip them more aggressively
#ifdef ESP32
unsigned long now = millis();
if (now - _last_memory_check > 5000) { // Check every 5 seconds
size_t max_alloc = ESP.getMaxAllocHeap();
if (max_alloc < 70000) { // Less than 70KB max alloc = moderate fragmentation
static unsigned long last_status_skip_log = 0;
if (now - last_status_skip_log > 300000) { // Log every 5 minutes
MQTT_DEBUG_PRINTLN("MQTT: Skipping status publish due to memory pressure (Max alloc: %d)", max_alloc);
last_status_skip_log = now;
}
return false; // Skip status publish
}
_last_memory_check = now;
}
#endif
// Check if we have any valid destinations (custom brokers or analyzer servers)
bool has_custom_brokers = isAnyBrokerConnected() && _config_valid;
bool has_analyzer_servers = (_analyzer_us_enabled && _analyzer_us_client && _analyzer_us_client->connected()) ||
(_analyzer_eu_enabled && _analyzer_eu_client && _analyzer_eu_client->connected());
MQTT_DEBUG_PRINTLN("publishStatus() called - custom_brokers: %s, analyzer_servers: %s",
has_custom_brokers ? "yes" : "no", has_analyzer_servers ? "yes" : "no");
if (!has_custom_brokers && !has_analyzer_servers) {
MQTT_DEBUG_PRINTLN("No destinations available for status publish");
return false; // No destinations available
}
// Status messages with stats can be larger (~400-500 bytes), so increase buffer size
char json_buffer[768]; // Increased from 512 to accommodate stats object
char origin_id[65];
char timestamp[32];
char radio_info[64];
// Get current timestamp in ISO 8601 format
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", &timeinfo);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
}
// Build radio info string (freq,bw,sf,cr)
snprintf(radio_info, sizeof(radio_info), "%.6f,%.1f,%d,%d",
_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr);
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build client version string
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
// Collect stats on-demand if sources are available
int battery_mv = -1;
int uptime_secs = -1;
int errors = -1;
int noise_floor = -999;
int tx_air_secs = -1;
int rx_air_secs = -1;
if (_board) {
battery_mv = _board->getBattMilliVolts();
}
if (_ms) {
uptime_secs = _ms->getMillis() / 1000;
}
if (_dispatcher) {
errors = _dispatcher->getErrFlags();
tx_air_secs = _dispatcher->getTotalAirTime() / 1000;
rx_air_secs = _dispatcher->getReceiveAirTime() / 1000;
}
if (_radio) {
noise_floor = (int16_t)_radio->getNoiseFloor();
}
// Build status message with stats
int len = MQTTMessageBuilder::buildStatusMessage(
_origin,
origin_id,
_board_model, // model - now dynamic!
_firmware_version, // firmware version
radio_info,
client_version, // client version
"online",
timestamp,
json_buffer,
sizeof(json_buffer),
battery_mv,
uptime_secs,
errors,
_queue_count, // Use current queue length
noise_floor,
tx_air_secs,
rx_air_secs
);
if (len > 0) {
bool published = false;
// Build topic string once and reuse (optimization: avoid redundant snprintf calls)
char topic[128];
snprintf(topic, sizeof(topic), "meshcore/%s/%s/status", _iata, _device_id);
size_t json_len = strlen(json_buffer); // Cache length to avoid multiple strlen() calls
// Publish to all connected custom brokers
if (_config_valid && _mqtt_client && _mqtt_client->connected()) {
// Track last broker URI to avoid calling setServer() unnecessarily (memory optimization)
// setServer() may allocate memory, so we only call it when the broker changes
static char last_broker_uri_status[128] = "";
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
MQTT_DEBUG_PRINTLN("Publishing status to topic: %s", topic);
// Build broker URI
char broker_uri[128];
snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port);
// Only call setServer() if broker URI changed (reduces memory allocations)
if (strcmp(broker_uri, last_broker_uri_status) != 0) {
_mqtt_client->setServer(broker_uri);
strncpy(last_broker_uri_status, broker_uri, sizeof(last_broker_uri_status) - 1);
last_broker_uri_status[sizeof(last_broker_uri_status) - 1] = '\0';
}
// Publish with timeout check - don't block if connection is slow
int publish_result = _mqtt_client->publish(topic, 1, true, json_buffer, json_len);
if (publish_result > 0) {
published = true;
} else {
// Publish failed - connection may be stale, mark as disconnected
static unsigned long last_status_publish_fail_log = 0;
unsigned long now = millis();
if (now - last_status_publish_fail_log > 60000) { // Log every minute max
MQTT_DEBUG_PRINTLN("Status publish failed (result=%d), marking broker %d as disconnected", publish_result, i);
last_status_publish_fail_log = now;
}
_brokers[i].connected = false;
_active_brokers--;
}
}
}
}
// Always publish to Let's Mesh Analyzer servers if enabled and connected
// Use shared helper function to publish same JSON to both servers (avoids duplication)
if (has_analyzer_servers) {
#ifdef ESP32
size_t max_alloc = ESP.getMaxAllocHeap();
if (max_alloc >= 70000) { // Only publish to analyzer servers if memory is OK
#endif
publishToAnalyzerServers(topic, json_buffer, true); // retained=true for status
published = true;
#ifdef ESP32
}
#endif
}
// Return true if we successfully published to at least one destination
if (published) {
MQTT_DEBUG_PRINTLN("Status published successfully");
return true;
}
}
MQTT_DEBUG_PRINTLN("Status publish failed - no destinations or build failed");
return false; // Failed to build or publish message
}
void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx,
const uint8_t* raw_data, int raw_len,
float snr, float rssi) {
if (!packet) return;
// Check if IATA is configured before attempting to publish
if (!isIATAValid()) {
static unsigned long last_iata_warning = 0;
unsigned long now = millis();
// Only log this warning every 5 minutes to avoid spam
if (now - last_iata_warning > 300000) {
MQTT_DEBUG_PRINTLN("MQTT: Cannot publish packet - IATA code not configured (current: '%s'). Please set mqtt.iata via CLI.", _iata);
last_iata_warning = now;
}
return;
}
// Memory pressure check: Skip publishes when heap is severely fragmented
// This prevents further fragmentation and allows memory to recover
// Threshold: Max alloc < 60KB indicates severe fragmentation
#ifdef ESP32
unsigned long now = millis();
if (now - _last_memory_check > 5000) { // Check every 5 seconds
size_t max_alloc = ESP.getMaxAllocHeap();
if (max_alloc < 60000) { // Less than 60KB max alloc = severe fragmentation
_skipped_publishes++;
static unsigned long last_skip_log = 0;
if (now - last_skip_log > 60000) { // Log every minute
MQTT_DEBUG_PRINTLN("MQTT: Skipping publish due to memory pressure (Max alloc: %d, skipped: %d)", max_alloc, _skipped_publishes);
last_skip_log = now;
}
return; // Skip this publish to allow memory to recover
}
_last_memory_check = now;
}
#endif
// Size-adaptive buffer: estimate needed size based on packet size
// Most packets are <100 bytes (need ~400 byte JSON), large packets need ~1500 bytes
int packet_size = packet->getRawLength();
size_t json_buffer_size = (packet_size > 150) ? 2048 : 1024;
char json_buffer[2048]; // Always allocate max, but pass actual needed size to builders
char origin_id[65];
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build packet message using raw radio data if provided
// Use size-adaptive buffer size based on actual packet size
size_t buffer_size = (packet->getRawLength() > 150) ? 2048 : 1024;
int len;
if (raw_data && raw_len > 0) {
// Use provided raw radio data
len = MQTTMessageBuilder::buildPacketJSONFromRaw(
raw_data, raw_len, packet, is_tx, _origin, origin_id,
snr, rssi, _timezone, json_buffer, buffer_size
);
} else if (_last_raw_len > 0 && (millis() - _last_raw_timestamp) < 1000) {
// Fallback to global raw radio data (within 1 second of packet)
len = MQTTMessageBuilder::buildPacketJSONFromRaw(
_last_raw_data, _last_raw_len, packet, is_tx, _origin, origin_id,
_last_snr, _last_rssi, _timezone, json_buffer, buffer_size
);
} else {
// Fallback to reconstructed packet data
len = MQTTMessageBuilder::buildPacketJSON(
packet, is_tx, _origin, origin_id, _timezone, json_buffer, buffer_size
);
}
if (len > 0) {
// Build topic string once and reuse (optimization: avoid redundant snprintf calls)
char topic[128];
snprintf(topic, sizeof(topic), "meshcore/%s/%s/packets", _iata, _device_id);
size_t json_len = strlen(json_buffer); // Cache length to avoid multiple strlen() calls
// Publish to custom brokers (only if config is valid)
if (_config_valid && _mqtt_client && _mqtt_client->connected()) {
// Track last broker URI to avoid calling setServer() unnecessarily (memory optimization)
// setServer() may allocate memory, so we only call it when the broker changes
static char last_broker_uri[128] = "";
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
// Build broker URI
char broker_uri[128];
snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port);
// Only call setServer() if broker URI changed (reduces memory allocations)
if (strcmp(broker_uri, last_broker_uri) != 0) {
_mqtt_client->setServer(broker_uri);
strncpy(last_broker_uri, broker_uri, sizeof(last_broker_uri) - 1);
last_broker_uri[sizeof(last_broker_uri) - 1] = '\0';
}
// Publish with timeout check - don't block if connection is slow
// This prevents blocking the main loop when MQTT broker is slow or unresponsive
int publish_result = _mqtt_client->publish(topic, 1, false, json_buffer, json_len); // qos=1, retained=false
if (publish_result <= 0) {
// Publish failed - connection may be stale, mark as disconnected
// This prevents accumulation of failed publishes that block the loop
static unsigned long last_publish_fail_log = 0;
unsigned long now = millis();
if (now - last_publish_fail_log > 60000) { // Log every minute max
MQTT_DEBUG_PRINTLN("Publish failed (result=%d), marking broker %d as disconnected", publish_result, i);
last_publish_fail_log = now;
}
_brokers[i].connected = false;
_active_brokers--;
}
}
}
} else if (_config_valid) {
// Connection state is out of sync - mark all brokers as disconnected
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
_brokers[i].connected = false;
_active_brokers--;
}
}
}
// Always publish to Let's Mesh Analyzer servers (independent of custom broker config)
// Skip analyzer servers if memory is severely fragmented (they're less critical than custom brokers)
#ifdef ESP32
size_t max_alloc = ESP.getMaxAllocHeap();
if (max_alloc >= 60000) { // Only publish to analyzer servers if memory is OK
publishToAnalyzerServers(topic, json_buffer, false);
}
#else
publishToAnalyzerServers(topic, json_buffer, false);
#endif
} else {
// Debug: log when packet message building fails
uint8_t packet_type = packet->getPayloadType();
if (packet_type == 4 || packet_type == 9) { // ADVERT or TRACE
MQTT_DEBUG_PRINTLN("Failed to build packet JSON for type=%d (len=%d), packet not published", packet_type, len);
}
}
}
void MQTTBridge::publishRaw(mesh::Packet* packet) {
if (!packet) return;
// Check if IATA is configured before attempting to publish
if (!isIATAValid()) {
static unsigned long last_iata_warning = 0;
unsigned long now = millis();
// Only log this warning every 5 minutes to avoid spam
if (now - last_iata_warning > 300000) {
MQTT_DEBUG_PRINTLN("MQTT: Cannot publish raw packet - IATA code not configured (current: '%s'). Please set mqtt.iata via CLI.", _iata);
last_iata_warning = now;
}
return;
}
// Large packets need larger buffer for raw JSON too
char json_buffer[2048];
char origin_id[65];
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build raw message
int len = MQTTMessageBuilder::buildRawJSON(
packet, _origin, origin_id, _timezone, json_buffer, sizeof(json_buffer)
);
if (len > 0) {
// Build topic string once and reuse (optimization: avoid redundant snprintf calls)
char topic[128];
snprintf(topic, sizeof(topic), "meshcore/%s/%s/raw", _iata, _device_id);
size_t json_len = strlen(json_buffer); // Cache length to avoid multiple strlen() calls
// Publish to custom brokers (only if config is valid)
if (_config_valid && _mqtt_client && _mqtt_client->connected()) {
// Track last broker URI to avoid calling setServer() unnecessarily (memory optimization)
// setServer() may allocate memory, so we only call it when the broker changes
static char last_broker_uri_raw[128] = "";
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
// Build broker URI
char broker_uri[128];
snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port);
// Only call setServer() if broker URI changed (reduces memory allocations)
if (strcmp(broker_uri, last_broker_uri_raw) != 0) {
_mqtt_client->setServer(broker_uri);
strncpy(last_broker_uri_raw, broker_uri, sizeof(last_broker_uri_raw) - 1);
last_broker_uri_raw[sizeof(last_broker_uri_raw) - 1] = '\0';
}
// Publish with timeout check - don't block if connection is slow
int publish_result = _mqtt_client->publish(topic, 1, false, json_buffer, json_len); // qos=1, retained=false
if (publish_result <= 0) {
// Publish failed - connection may be stale, mark as disconnected
static unsigned long last_raw_publish_fail_log = 0;
unsigned long now = millis();
if (now - last_raw_publish_fail_log > 60000) { // Log every minute max
MQTT_DEBUG_PRINTLN("Raw publish failed (result=%d), marking broker %d as disconnected", publish_result, i);
last_raw_publish_fail_log = now;
}
_brokers[i].connected = false;
_active_brokers--;
}
}
}
}
// Always publish to Let's Mesh Analyzer servers (independent of custom broker config)
// Skip analyzer servers if memory is severely fragmented (they're less critical than custom brokers)
#ifdef ESP32
size_t max_alloc = ESP.getMaxAllocHeap();
if (max_alloc >= 60000) { // Only publish to analyzer servers if memory is OK
publishToAnalyzerServers(topic, json_buffer, false);
}
#else
publishToAnalyzerServers(topic, json_buffer, false);
#endif
}
}
void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) {
if (_queue_count >= MAX_QUEUE_SIZE) {
// Queue full, remove oldest and free its memory
QueuedPacket& oldest = _packet_queue[_queue_head];
if (oldest.packet) {
MQTT_DEBUG_PRINTLN("Queue full, dropping oldest packet (queue size: %d)", _queue_count);
_mgr->free(oldest.packet);
oldest.packet = nullptr;
}
// dequeuePacket() will clear the structure
dequeuePacket();
}
QueuedPacket& queued = _packet_queue[_queue_tail];
// Clear structure first to ensure clean state (removes any stale data)
memset(&queued, 0, sizeof(QueuedPacket));
queued.packet = packet;
queued.timestamp = millis();
queued.is_tx = is_tx;
queued.has_raw_data = false; // Default to false, set true if we have valid data
// Only capture raw radio data for RX packets (TX packets are generated locally, no raw radio data)
// This saves 256 bytes per queued TX packet (memory optimization)
if (!is_tx && _last_raw_len > 0 && (millis() - _last_raw_timestamp) < 1000) {
if (_last_raw_len <= sizeof(queued.raw_data)) {
memcpy(queued.raw_data, _last_raw_data, _last_raw_len);
queued.raw_len = _last_raw_len;
queued.snr = _last_snr;
queued.rssi = _last_rssi;
queued.has_raw_data = true;
}
}
_queue_tail = (_queue_tail + 1) % MAX_QUEUE_SIZE;
_queue_count++;
}
void MQTTBridge::dequeuePacket() {
if (_queue_count == 0) return;
// Clear the dequeued packet structure to free memory and prevent stale data
QueuedPacket& dequeued = _packet_queue[_queue_head];
memset(&dequeued, 0, sizeof(QueuedPacket));
dequeued.has_raw_data = false; // Explicitly set after memset
_queue_head = (_queue_head + 1) % MAX_QUEUE_SIZE;
_queue_count--;
}
bool MQTTBridge::isAnyBrokerConnected() {
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
return true;
}
}
return false;
}
void MQTTBridge::setBrokerDefaults() {
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
memset(&_brokers[i], 0, sizeof(MQTTBroker));
_brokers[i].port = 1883;
_brokers[i].qos = 0;
_brokers[i].enabled = false;
_brokers[i].connected = false;
_brokers[i].reconnect_interval = 5000; // 5 seconds
}
}
void MQTTBridge::setBroker(int broker_index, const char* host, uint16_t port,
const char* username, const char* password, bool enabled) {
if (broker_index < 0 || broker_index >= MAX_MQTT_BROKERS_COUNT) return;
MQTTBroker& broker = _brokers[broker_index];
strncpy(broker.host, host, sizeof(broker.host) - 1);
broker.port = port;
strncpy(broker.username, username, sizeof(broker.username) - 1);
strncpy(broker.password, password, sizeof(broker.password) - 1);
broker.enabled = enabled;
broker.connected = false;
broker.reconnect_interval = 5000;
}
void MQTTBridge::setOrigin(const char* origin) {
strncpy(_origin, origin, sizeof(_origin) - 1);
_origin[sizeof(_origin) - 1] = '\0';
}
void MQTTBridge::setIATA(const char* iata) {
strncpy(_iata, iata, sizeof(_iata) - 1);
_iata[sizeof(_iata) - 1] = '\0';
// Convert IATA code to uppercase (IATA codes are conventionally uppercase)
for (int i = 0; _iata[i]; i++) {
_iata[i] = toupper(_iata[i]);
}
}
void MQTTBridge::setDeviceID(const char* device_id) {
strncpy(_device_id, device_id, sizeof(_device_id) - 1);
_device_id[sizeof(_device_id) - 1] = '\0';
MQTT_DEBUG_PRINTLN("Device ID set to: %s", _device_id);
}
void MQTTBridge::setFirmwareVersion(const char* firmware_version) {
strncpy(_firmware_version, firmware_version, sizeof(_firmware_version) - 1);
_firmware_version[sizeof(_firmware_version) - 1] = '\0';
}
void MQTTBridge::setBoardModel(const char* board_model) {
strncpy(_board_model, board_model, sizeof(_board_model) - 1);
_board_model[sizeof(_board_model) - 1] = '\0';
}
void MQTTBridge::setBuildDate(const char* build_date) {
strncpy(_build_date, build_date, sizeof(_build_date) - 1);
_build_date[sizeof(_build_date) - 1] = '\0';
}
void MQTTBridge::storeRawRadioData(const uint8_t* raw_data, int len, float snr, float rssi) {
if (len > 0 && len <= sizeof(_last_raw_data)) {
memcpy(_last_raw_data, raw_data, len);
_last_raw_len = len;
_last_snr = snr;
_last_rssi = rssi;
_last_raw_timestamp = millis();
MQTT_DEBUG_PRINTLN("Stored raw radio data: %d bytes, SNR=%.1f, RSSI=%.1f", len, snr, rssi);
}
}
void MQTTBridge::setupAnalyzerServers() {
// Update analyzer server settings from preferences
bool previous_us_enabled = _analyzer_us_enabled;
bool previous_eu_enabled = _analyzer_eu_enabled;
_analyzer_us_enabled = _prefs->mqtt_analyzer_us_enabled;
_analyzer_eu_enabled = _prefs->mqtt_analyzer_eu_enabled;
MQTT_DEBUG_PRINTLN("Analyzer servers - US: %s, EU: %s",
_analyzer_us_enabled ? "enabled" : "disabled",
_analyzer_eu_enabled ? "enabled" : "disabled");
// Create authentication token if any analyzer servers are enabled
if (_analyzer_us_enabled || _analyzer_eu_enabled) {
if (createAuthToken()) {
MQTT_DEBUG_PRINTLN("Created authentication token for analyzer servers");
} else {
MQTT_DEBUG_PRINTLN("Failed to create authentication token");
}
}
// If settings changed and bridge is already initialized, recreate clients
// This handles the case where settings change after initialization
if (_initialized && (previous_us_enabled != _analyzer_us_enabled || previous_eu_enabled != _analyzer_eu_enabled)) {
MQTT_DEBUG_PRINTLN("Analyzer server settings changed - recreating clients");
setupAnalyzerClients();
}
}
bool MQTTBridge::createAuthToken() {
if (!_identity) {
MQTT_DEBUG_PRINTLN("No identity available for creating auth token");
return false;
}
// Create username in the format: v1_{UPPERCASE_PUBLIC_KEY}
char public_key_hex[65];
mesh::Utils::toHex(public_key_hex, _identity->pub_key, PUB_KEY_SIZE);
snprintf(_analyzer_username, sizeof(_analyzer_username), "v1_%s", public_key_hex);
MQTT_DEBUG_PRINTLN("Creating auth token for username: %s", _analyzer_username);
bool us_token_created = false;
bool eu_token_created = false;
// Get current time for expiration tracking
unsigned long current_time = time(nullptr);
unsigned long expires_in = 86400; // 24 hours
bool time_synced = (current_time >= 1000000000); // After year 2001
// Prepare owner public key (if set) - convert to uppercase hex
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
// Copy and convert to uppercase
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
}
owner_key = owner_key_uppercase;
MQTT_DEBUG_PRINTLN("Using owner public key: %s", owner_key);
}
// Build client version string (same format as used in status messages)
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
// Get email from preferences (if set)
const char* email = nullptr;
if (_prefs->mqtt_email[0] != '\0') {
email = _prefs->mqtt_email;
MQTT_DEBUG_PRINTLN("Using email: %s", email);
}
// Create JWT token for US server
if (_analyzer_us_enabled) {
MQTT_DEBUG_PRINTLN("Creating JWT token for US server...");
if (JWTHelper::createAuthToken(
*_identity, "mqtt-us-v1.letsmesh.net",
0, expires_in, _auth_token_us, sizeof(_auth_token_us),
owner_key, client_version, email)) {
MQTT_DEBUG_PRINTLN("Created auth token for US server");
us_token_created = true;
// Set expiration time if time is synced, otherwise leave at 0 to be set later
if (time_synced) {
_token_us_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("US token expiration set to: %lu", _token_us_expires_at);
} else {
_token_us_expires_at = 0; // Will be set after NTP sync
MQTT_DEBUG_PRINTLN("US token created, expiration will be set after NTP sync");
}
} else {
MQTT_DEBUG_PRINTLN("Failed to create auth token for US server");
_token_us_expires_at = 0;
}
}
// Create JWT token for EU server
if (_analyzer_eu_enabled) {
MQTT_DEBUG_PRINTLN("Creating JWT token for EU server...");
if (JWTHelper::createAuthToken(
*_identity, "mqtt-eu-v1.letsmesh.net",
0, expires_in, _auth_token_eu, sizeof(_auth_token_eu),
owner_key, client_version, email)) {
MQTT_DEBUG_PRINTLN("Created auth token for EU server");
eu_token_created = true;
// Set expiration time if time is synced, otherwise leave at 0 to be set later
if (time_synced) {
_token_eu_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("EU token expiration set to: %lu", _token_eu_expires_at);
} else {
_token_eu_expires_at = 0; // Will be set after NTP sync
MQTT_DEBUG_PRINTLN("EU token created, expiration will be set after NTP sync");
}
} else {
MQTT_DEBUG_PRINTLN("Failed to create auth token for EU server");
_token_eu_expires_at = 0;
}
}
return us_token_created || eu_token_created;
}
void MQTTBridge::publishToAnalyzerServers(const char* topic, const char* payload, bool retained) {
if (!_analyzer_us_enabled && !_analyzer_eu_enabled) {
MQTT_DEBUG_PRINTLN("No analyzer servers enabled, skipping publish to topic: %s", topic);
return;
}
MQTT_DEBUG_PRINTLN("Publishing to analyzer servers via WebSocket MQTT");
MQTT_DEBUG_PRINTLN("Topic: %s", topic);
MQTT_DEBUG_PRINTLN("Payload length: %d", strlen(payload));
MQTT_DEBUG_PRINTLN("US enabled: %s, EU enabled: %s", _analyzer_us_enabled ? "true" : "false", _analyzer_eu_enabled ? "true" : "false");
// Publish to US server if enabled
if (_analyzer_us_enabled && _analyzer_us_client) {
MQTT_DEBUG_PRINTLN("Publishing to US analyzer server");
publishToAnalyzerClient(_analyzer_us_client, topic, payload, retained);
} else {
MQTT_DEBUG_PRINTLN("US analyzer server not available (enabled: %s, client: %s)",
_analyzer_us_enabled ? "true" : "false", _analyzer_us_client ? "exists" : "null");
}
// Publish to EU server if enabled
if (_analyzer_eu_enabled && _analyzer_eu_client) {
MQTT_DEBUG_PRINTLN("Publishing to EU analyzer server");
publishToAnalyzerClient(_analyzer_eu_client, topic, payload, retained);
} else {
MQTT_DEBUG_PRINTLN("EU analyzer server not available (enabled: %s, client: %s)",
_analyzer_eu_enabled ? "true" : "false", _analyzer_eu_client ? "exists" : "null");
}
}
// Google Trust Services - GTS Root R4
const char* GTS_ROOT_R4 =
"-----BEGIN CERTIFICATE-----\n"
"MIIDejCCAmKgAwIBAgIQf+UwvzMTQ77dghYQST2KGzANBgkqhkiG9w0BAQsFADBX\n"
"MQswCQYDVQQGEwJCRTEZMBcGA1UEChMQR2xvYmFsU2lnbiBudi1zYTEQMA4GA1UE\n"
"CxMHUm9vdCBDQTEbMBkGA1UEAxMSR2xvYmFsU2lnbiBSb290IENBMB4XDTIzMTEx\n"
"NTAzNDMyMVoXDTI4MDEyODAwMDA0MlowRzELMAkGA1UEBhMCVVMxIjAgBgNVBAoT\n"
"GUdvb2dsZSBUcnVzdCBTZXJ2aWNlcyBMTEMxFDASBgNVBAMTC0dUUyBSb290IFI0\n"
"MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE83Rzp2iLYK5DuDXFgTB7S0md+8Fhzube\n"
"Rr1r1WEYNa5A3XP3iZEwWus87oV8okB2O6nGuEfYKueSkWpz6bFyOZ8pn6KY019e\n"
"WIZlD6GEZQbR3IvJx3PIjGov5cSr0R2Ko4H/MIH8MA4GA1UdDwEB/wQEAwIBhjAd\n"
"BgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDwYDVR0TAQH/BAUwAwEB/zAd\n"
"BgNVHQ4EFgQUgEzW63T/STaj1dj8tT7FavCUHYwwHwYDVR0jBBgwFoAUYHtmGkUN\n"
"l8qJUC99BM00qP/8/UswNgYIKwYBBQUHAQEEKjAoMCYGCCsGAQUFBzAChhpodHRw\n"
"Oi8vaS5wa2kuZ29vZy9nc3IxLmNydDAtBgNVHR8EJjAkMCKgIKAehhxodHRwOi8v\n"
"Yy5wa2kuZ29vZy9yL2dzcjEuY3JsMBMGA1UdIAQMMAowCAYGZ4EMAQIBMA0GCSqG\n"
"SIb3DQEBCwUAA4IBAQAYQrsPBtYDh5bjP2OBDwmkoWhIDDkic574y04tfzHpn+cJ\n"
"odI2D4SseesQ6bDrarZ7C30ddLibZatoKiws3UL9xnELz4ct92vID24FfVbiI1hY\n"
"+SW6FoVHkNeWIP0GCbaM4C6uVdF5dTUsMVs/ZbzNnIdCp5Gxmx5ejvEau8otR/Cs\n"
"kGN+hr/W5GvT1tMBjgWKZ1i4//emhA1JG1BbPzoLJQvyEotc03lXjTaCzv8mEbep\n"
"8RqZ7a2CPsgRbuvTPBwcOMBBmuFeU88+FSBX6+7iP0il8b4Z0QFqIwwMHfs/L6K1\n"
"vepuoxtGzi4CZ68zJpiq1UvSqTbFJjtbD4seiMHl\n"
"-----END CERTIFICATE-----\n";
void MQTTBridge::setupAnalyzerClients() {
MQTT_DEBUG_PRINTLN("Setting up PsychicMqttClient WebSocket clients...");
MQTT_DEBUG_PRINTLN("Analyzer servers - US: %s, EU: %s",
_analyzer_us_enabled ? "enabled" : "disabled",
_analyzer_eu_enabled ? "enabled" : "disabled");
// Clean up existing clients if they're no longer enabled
// This handles the case where settings change after initialization
if (!_analyzer_us_enabled && _analyzer_us_client) {
MQTT_DEBUG_PRINTLN("US analyzer disabled - cleaning up client");
_analyzer_us_client->disconnect();
delete _analyzer_us_client;
_analyzer_us_client = nullptr;
}
if (!_analyzer_eu_enabled && _analyzer_eu_client) {
MQTT_DEBUG_PRINTLN("EU analyzer disabled - cleaning up client");
_analyzer_eu_client->disconnect();
delete _analyzer_eu_client;
_analyzer_eu_client = nullptr;
}
if (!_analyzer_us_enabled && !_analyzer_eu_enabled) {
MQTT_DEBUG_PRINTLN("No analyzer servers enabled, skipping PsychicMqttClient setup");
return;
}
// Setup US server client (only if enabled and doesn't already exist)
if (_analyzer_us_enabled && !_analyzer_us_client) {
_analyzer_us_client = new PsychicMqttClient();
// Optimize MQTT client configuration for memory efficiency
// Analyzer clients use 768-byte JWT tokens, need larger buffer for CONNECT message
optimizeMqttClientConfig(_analyzer_us_client, true);
// Set up event callbacks for US server
_analyzer_us_client->onConnect([this](bool sessionPresent) {
MQTT_DEBUG_PRINTLN("Connected to Let's Mesh US server, session present: %s", sessionPresent ? "true" : "false");
// Publish status message when connected
publishStatusToAnalyzerClient(_analyzer_us_client, "mqtt-us-v1.letsmesh.net");
});
_analyzer_us_client->onDisconnect([this](bool sessionPresent) {
MQTT_DEBUG_PRINTLN("Disconnected from Let's Mesh US server, session present: %s", sessionPresent ? "true" : "false");
});
_analyzer_us_client->onError([this](esp_mqtt_error_codes error) {
MQTT_DEBUG_PRINTLN("Let's Mesh US server error - error_type: %d, connect_return_code: %d, sock_errno: %d",
error.error_type, error.connect_return_code, error.esp_transport_sock_errno);
});
// Set up WebSocket MQTT over TLS connection to US server
_analyzer_us_client->setServer("wss://mqtt-us-v1.letsmesh.net:443/mqtt");
MQTT_DEBUG_PRINTLN("US Server - Username: %s", _analyzer_username);
MQTT_DEBUG_PRINTLN("US Server - Auth token length: %d", strlen(_auth_token_us));
MQTT_DEBUG_PRINTLN("US Server - Auth token (first 50 chars): %.50s...", _auth_token_us);
_analyzer_us_client->setCredentials(_analyzer_username, _auth_token_us);
// Configure TLS - use specific GTS Root R4 certificate
MQTT_DEBUG_PRINTLN("Using GTS Root R4 certificate for US server");
_analyzer_us_client->setCACert(GTS_ROOT_R4);
// Only attempt connection if WiFi is connected and NTP is synced
// Otherwise, maintainAnalyzerConnections() will handle it later
if (WiFi.status() == WL_CONNECTED && _ntp_synced) {
// Connect to US server (async connection)
_analyzer_us_client->connect();
MQTT_DEBUG_PRINTLN("Initiating connection to Let's Mesh US server");
} else {
MQTT_DEBUG_PRINTLN("Deferring US server connection - WiFi: %s, NTP: %s",
(WiFi.status() == WL_CONNECTED) ? "connected" : "disconnected",
_ntp_synced ? "synced" : "not synced");
}
}
// Setup EU server client (only if enabled and doesn't already exist)
if (_analyzer_eu_enabled && !_analyzer_eu_client) {
_analyzer_eu_client = new PsychicMqttClient();
// Optimize MQTT client configuration for memory efficiency
// Analyzer clients use 768-byte JWT tokens, need larger buffer for CONNECT message
optimizeMqttClientConfig(_analyzer_eu_client, true);
// Set up event callbacks for EU server
_analyzer_eu_client->onConnect([this](bool sessionPresent) {
MQTT_DEBUG_PRINTLN("Connected to Let's Mesh EU server, session present: %s", sessionPresent ? "true" : "false");
// Publish status message when connected
publishStatusToAnalyzerClient(_analyzer_eu_client, "mqtt-eu-v1.letsmesh.net");
});
_analyzer_eu_client->onDisconnect([this](bool sessionPresent) {
MQTT_DEBUG_PRINTLN("Disconnected from Let's Mesh EU server, session present: %s", sessionPresent ? "true" : "false");
});
_analyzer_eu_client->onError([this](esp_mqtt_error_codes error) {
MQTT_DEBUG_PRINTLN("Let's Mesh EU server error - error_type: %d, connect_return_code: %d, sock_errno: %d",
error.error_type, error.connect_return_code, error.esp_transport_sock_errno);
});
// Set up WebSocket MQTT over TLS connection to EU server
_analyzer_eu_client->setServer("wss://mqtt-eu-v1.letsmesh.net:443/mqtt");
MQTT_DEBUG_PRINTLN("EU Server - Username: %s", _analyzer_username);
MQTT_DEBUG_PRINTLN("EU Server - Auth token length: %d", strlen(_auth_token_eu));
MQTT_DEBUG_PRINTLN("EU Server - Auth token (first 50 chars): %.50s...", _auth_token_eu);
_analyzer_eu_client->setCredentials(_analyzer_username, _auth_token_eu);
// Configure TLS - use specific GTS Root R4 certificate
MQTT_DEBUG_PRINTLN("Using GTS Root R4 certificate for EU server");
_analyzer_eu_client->setCACert(GTS_ROOT_R4);
// Only attempt connection if WiFi is connected and NTP is synced
// Otherwise, maintainAnalyzerConnections() will handle it later
if (WiFi.status() == WL_CONNECTED && _ntp_synced) {
// Connect to EU server (async connection)
_analyzer_eu_client->connect();
MQTT_DEBUG_PRINTLN("Initiating connection to Let's Mesh EU server");
} else {
MQTT_DEBUG_PRINTLN("Deferring EU server connection - WiFi: %s, NTP: %s",
(WiFi.status() == WL_CONNECTED) ? "connected" : "disconnected",
_ntp_synced ? "synced" : "not synced");
}
}
}
void MQTTBridge::publishToAnalyzerClient(PsychicMqttClient* client, const char* topic, const char* payload, bool retained) {
if (!client) {
return; // Don't log null client - this is expected if analyzer is disabled
}
if (!client->connected()) {
// Throttle log spam - only log periodically for each analyzer server
unsigned long now = millis();
bool should_log = false;
if (client == _analyzer_us_client && (now - _last_analyzer_us_log > ANALYZER_LOG_INTERVAL)) {
should_log = true;
_last_analyzer_us_log = now;
} else if (client == _analyzer_eu_client && (now - _last_analyzer_eu_log > ANALYZER_LOG_INTERVAL)) {
should_log = true;
_last_analyzer_eu_log = now;
}
if (should_log) {
MQTT_DEBUG_PRINTLN("PsychicMqttClient not connected - skipping publish to topic: %s", topic);
}
return;
}
// Reset log timer when connected
if (client == _analyzer_us_client) {
_last_analyzer_us_log = 0;
} else if (client == _analyzer_eu_client) {
_last_analyzer_eu_log = 0;
}
MQTT_DEBUG_PRINTLN("Publishing to analyzer client - topic: %s, payload length: %d, retained: %s",
topic, strlen(payload), retained ? "true" : "false");
// Publish message using PsychicMqttClient API
int result = client->publish(topic, 1, retained, payload, strlen(payload));
if (result > 0) {
MQTT_DEBUG_PRINTLN("PsychicMqttClient message published successfully, result=%d", result);
} else {
MQTT_DEBUG_PRINTLN("PsychicMqttClient publish failed, result=%d", result);
}
}
void MQTTBridge::publishStatusToAnalyzerClient(PsychicMqttClient* client, const char* server_name) {
if (!client || !client->connected()) {
return;
}
// Check if IATA is configured before attempting to publish
if (!isIATAValid()) {
static unsigned long last_iata_warning = 0;
unsigned long now = millis();
// Only log this warning every 5 minutes to avoid spam
if (now - last_iata_warning > 300000) {
MQTT_DEBUG_PRINTLN("MQTT: Cannot publish status to analyzer - IATA code not configured (current: '%s'). Please set mqtt.iata via CLI.", _iata);
last_iata_warning = now;
}
return;
}
// Create status message
char status_topic[128];
snprintf(status_topic, sizeof(status_topic), "meshcore/%s/%s/status", _iata, _device_id);
// Build proper status message using MQTTMessageBuilder
// Status messages with stats can be larger (~400-500 bytes)
char json_buffer[768]; // Increased from 512 to accommodate stats object
char origin_id[65];
char timestamp[32];
char radio_info[64];
// Get current timestamp in ISO 8601 format
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", &timeinfo);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
}
// Build radio info string (freq,bw,sf,cr)
snprintf(radio_info, sizeof(radio_info), "%.6f,%.1f,%d,%d",
_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr);
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build client version string
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
// Collect stats on-demand if sources are available
int battery_mv = -1;
int uptime_secs = -1;
int errors = -1;
int noise_floor = -999;
int tx_air_secs = -1;
int rx_air_secs = -1;
if (_board) {
battery_mv = _board->getBattMilliVolts();
}
if (_ms) {
uptime_secs = _ms->getMillis() / 1000;
}
if (_dispatcher) {
errors = _dispatcher->getErrFlags();
tx_air_secs = _dispatcher->getTotalAirTime() / 1000;
rx_air_secs = _dispatcher->getReceiveAirTime() / 1000;
}
if (_radio) {
noise_floor = (int16_t)_radio->getNoiseFloor();
}
// Build status message using MQTTMessageBuilder with stats
int len = MQTTMessageBuilder::buildStatusMessage(
_origin,
origin_id,
_board_model, // model
_firmware_version, // firmware version
radio_info,
client_version, // client version
"online",
timestamp,
json_buffer,
sizeof(json_buffer),
battery_mv,
uptime_secs,
errors,
_queue_count, // Use current queue length
noise_floor,
tx_air_secs,
rx_air_secs
);
if (len > 0) {
MQTT_DEBUG_PRINTLN("Publishing status to %s server", server_name);
MQTT_DEBUG_PRINTLN("Status topic: %s", status_topic);
MQTT_DEBUG_PRINTLN("Status payload: %s", json_buffer);
// Publish status message (retained)
int result = client->publish(status_topic, 1, true, json_buffer, strlen(json_buffer));
if (result > 0) {
MQTT_DEBUG_PRINTLN("Status published to %s server successfully, result=%d", server_name, result);
} else {
MQTT_DEBUG_PRINTLN("Status publish to %s server failed, result=%d", server_name, result);
}
}
}
void MQTTBridge::maintainAnalyzerConnections() {
if (!_identity) {
return;
}
// Check WiFi status first - don't attempt MQTT reconnection if WiFi is disconnected
if (WiFi.status() != WL_CONNECTED) {
// WiFi is not connected - skip MQTT reconnection attempts
// WiFi auto-reconnect will handle WiFi, then we can reconnect MQTT
static unsigned long last_wifi_warning = 0;
unsigned long now = millis();
if (now - last_wifi_warning > 300000) { // Log every 5 minutes max
MQTT_DEBUG_PRINTLN("Skipping MQTT reconnection - WiFi not connected");
last_wifi_warning = now;
}
return;
}
// Check NTP sync status - JWT tokens require valid timestamps
// If NTP hasn't synced yet, wait before attempting connections
if (!_ntp_synced) {
static unsigned long last_ntp_warning = 0;
unsigned long now = millis();
if (now - last_ntp_warning > 300000) { // Log every 5 minutes max
MQTT_DEBUG_PRINTLN("Skipping MQTT reconnection - NTP not synced yet");
last_ntp_warning = now;
}
return;
}
unsigned long current_time = time(nullptr);
// If time is not synced (time() returns 0 or very small value), skip expiration checks
// Tokens will still work but we can't track expiration properly
// If expiration time was set before time sync, it will be a small value, so we'll renew
bool time_synced = (current_time >= 1000000000); // After year 2001
const unsigned long RENEWAL_BUFFER = 3600; // Renew tokens 1 hour before expiration
const unsigned long RENEWAL_THROTTLE_MS = 60000; // Don't attempt renewal more than once per minute
const unsigned long RECONNECT_THROTTLE_MS = 60000; // Don't attempt reconnection more than once per minute
unsigned long now_millis = millis();
// Check and renew US server token if needed
if (_analyzer_us_enabled && _analyzer_us_client) {
// Check if token is expired or will expire soon
// Only check expiration if time is synced - if time isn't synced, we can't validate expiration
// If time wasn't synced when token was created, expiration time will be invalid (< 1000000000), so renew when time syncs
bool token_needs_renewal = false;
if (!time_synced) {
// Time not synced yet - only renew if token is missing (expires_at == 0)
// Don't renew if token exists but expiration is invalid - wait for time sync
token_needs_renewal = (_token_us_expires_at == 0);
} else {
// Time is synced - check if token needs renewal
token_needs_renewal = (_token_us_expires_at == 0) ||
!(_token_us_expires_at >= 1000000000) || // Expiration time invalid (created before time sync)
(current_time >= _token_us_expires_at) ||
(current_time >= (_token_us_expires_at - RENEWAL_BUFFER));
}
// Throttle renewal attempts - don't try more than once per minute to avoid blocking
bool can_attempt_renewal = (now_millis - _last_token_renewal_attempt_us) >= RENEWAL_THROTTLE_MS;
// Check if client is disconnected and needs reconnection with new token
bool needs_reconnect = !_analyzer_us_client->connected();
if (token_needs_renewal && can_attempt_renewal) {
_last_token_renewal_attempt_us = now_millis;
MQTT_DEBUG_PRINTLN("US token expired or expiring soon (expires_at: %lu, current: %lu), renewing...",
_token_us_expires_at, current_time);
// Prepare owner public key (if set) - convert to uppercase hex
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
// Copy and convert to uppercase
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
}
owner_key = owner_key_uppercase;
}
// Build client version string (same format as used in status messages)
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
// Get email from preferences (if set)
const char* email = nullptr;
if (_prefs->mqtt_email[0] != '\0') {
email = _prefs->mqtt_email;
}
// Renew the token
if (JWTHelper::createAuthToken(
*_identity, "mqtt-us-v1.letsmesh.net",
0, 86400, _auth_token_us, sizeof(_auth_token_us),
owner_key, client_version, email)) {
unsigned long expires_in = 86400; // 24 hours
// Only set expiration time if time is synced - otherwise set to 0 to indicate it needs to be set later
if (time_synced) {
_token_us_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("US token renewed, new expiration: %lu", _token_us_expires_at);
} else {
_token_us_expires_at = 0; // Will be set properly after time sync
MQTT_DEBUG_PRINTLN("US token renewed, expiration will be set after time sync");
}
// Update client credentials with new token
_analyzer_us_client->setCredentials(_analyzer_username, _auth_token_us);
// Reconnect to apply new token (whether currently connected or not)
// If connected, disconnect first to ensure new token is used
if (_analyzer_us_client->connected()) {
MQTT_DEBUG_PRINTLN("Disconnecting US server to apply new token...");
_analyzer_us_client->disconnect();
}
MQTT_DEBUG_PRINTLN("Reconnecting to US server with renewed token...");
_last_reconnect_attempt_us = now_millis; // Update reconnect timestamp to throttle subsequent attempts
_analyzer_us_client->connect();
} else {
MQTT_DEBUG_PRINTLN("Failed to renew US token");
_token_us_expires_at = 0;
}
} else if (needs_reconnect) {
// Token is still valid but connection is lost - reconnect with existing token
// Throttle reconnection attempts to avoid spamming
unsigned long reconnect_elapsed = (now_millis >= _last_reconnect_attempt_us) ?
(now_millis - _last_reconnect_attempt_us) :
(ULONG_MAX - _last_reconnect_attempt_us + now_millis + 1);
if (reconnect_elapsed >= RECONNECT_THROTTLE_MS) {
_last_reconnect_attempt_us = now_millis;
MQTT_DEBUG_PRINTLN("US server disconnected but token still valid, reconnecting...");
_analyzer_us_client->connect();
} else {
// Throttled - only log periodically to avoid spam (every 5 minutes max)
static unsigned long last_throttle_log_us = 0;
if (now_millis - last_throttle_log_us > 300000) {
MQTT_DEBUG_PRINTLN("US server reconnection throttled (last attempt %lu ms ago, need %lu ms)",
reconnect_elapsed, RECONNECT_THROTTLE_MS);
last_throttle_log_us = now_millis;
}
}
}
}
// Check and renew EU server token if needed
if (_analyzer_eu_enabled && _analyzer_eu_client) {
// Check if token is expired or will expire soon
// Only check expiration if time is synced - if time isn't synced, we can't validate expiration
// If time wasn't synced when token was created, expiration time will be invalid (< 1000000000), so renew when time syncs
bool token_needs_renewal = false;
if (!time_synced) {
// Time not synced yet - only renew if token is missing (expires_at == 0)
// Don't renew if token exists but expiration is invalid - wait for time sync
token_needs_renewal = (_token_eu_expires_at == 0);
} else {
// Time is synced - check if token needs renewal
token_needs_renewal = (_token_eu_expires_at == 0) ||
!(_token_eu_expires_at >= 1000000000) || // Expiration time invalid (created before time sync)
(current_time >= _token_eu_expires_at) ||
(current_time >= (_token_eu_expires_at - RENEWAL_BUFFER));
}
// Throttle renewal attempts - don't try more than once per minute to avoid blocking
bool can_attempt_renewal = (now_millis - _last_token_renewal_attempt_eu) >= RENEWAL_THROTTLE_MS;
// Check if client is disconnected and needs reconnection with new token
bool needs_reconnect = !_analyzer_eu_client->connected();
if (token_needs_renewal && can_attempt_renewal) {
_last_token_renewal_attempt_eu = now_millis;
MQTT_DEBUG_PRINTLN("EU token expired or expiring soon (expires_at: %lu, current: %lu), renewing...",
_token_eu_expires_at, current_time);
// Prepare owner public key (if set) - convert to uppercase hex
const char* owner_key = nullptr;
char owner_key_uppercase[65];
if (_prefs->mqtt_owner_public_key[0] != '\0') {
// Copy and convert to uppercase
strncpy(owner_key_uppercase, _prefs->mqtt_owner_public_key, sizeof(owner_key_uppercase) - 1);
owner_key_uppercase[sizeof(owner_key_uppercase) - 1] = '\0';
for (int i = 0; owner_key_uppercase[i]; i++) {
owner_key_uppercase[i] = toupper(owner_key_uppercase[i]);
}
owner_key = owner_key_uppercase;
}
// Build client version string
char client_version[64];
getClientVersion(client_version, sizeof(client_version));
// Get email from preferences (if set)
const char* email = nullptr;
if (_prefs->mqtt_email[0] != '\0') {
email = _prefs->mqtt_email;
}
// Renew the token
if (JWTHelper::createAuthToken(
*_identity, "mqtt-eu-v1.letsmesh.net",
0, 86400, _auth_token_eu, sizeof(_auth_token_eu),
owner_key, client_version, email)) {
unsigned long expires_in = 86400; // 24 hours
// Only set expiration time if time is synced - otherwise set to 0 to indicate it needs to be set later
if (time_synced) {
_token_eu_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("EU token renewed, new expiration: %lu", _token_eu_expires_at);
} else {
_token_eu_expires_at = 0; // Will be set properly after time sync
MQTT_DEBUG_PRINTLN("EU token renewed, expiration will be set after time sync");
}
// Update client credentials with new token
_analyzer_eu_client->setCredentials(_analyzer_username, _auth_token_eu);
// Reconnect to apply new token (whether currently connected or not)
// If connected, disconnect first to ensure new token is used
if (_analyzer_eu_client->connected()) {
MQTT_DEBUG_PRINTLN("Disconnecting EU server to apply new token...");
_analyzer_eu_client->disconnect();
}
MQTT_DEBUG_PRINTLN("Reconnecting to EU server with renewed token...");
_last_reconnect_attempt_eu = now_millis; // Update reconnect timestamp to throttle subsequent attempts
_analyzer_eu_client->connect();
} else {
MQTT_DEBUG_PRINTLN("Failed to renew EU token");
_token_eu_expires_at = 0;
}
} else if (needs_reconnect) {
// Token is still valid but connection is lost - reconnect with existing token
// Throttle reconnection attempts to avoid spamming
unsigned long reconnect_elapsed = (now_millis >= _last_reconnect_attempt_eu) ?
(now_millis - _last_reconnect_attempt_eu) :
(ULONG_MAX - _last_reconnect_attempt_eu + now_millis + 1);
if (reconnect_elapsed >= RECONNECT_THROTTLE_MS) {
_last_reconnect_attempt_eu = now_millis;
MQTT_DEBUG_PRINTLN("EU server disconnected but token still valid, reconnecting...");
_analyzer_eu_client->connect();
} else {
// Throttled - only log periodically to avoid spam (every 5 minutes max)
static unsigned long last_throttle_log_eu = 0;
if (now_millis - last_throttle_log_eu > 300000) {
MQTT_DEBUG_PRINTLN("EU server reconnection throttled (last attempt %lu ms ago, need %lu ms)",
reconnect_elapsed, RECONNECT_THROTTLE_MS);
last_throttle_log_eu = now_millis;
}
}
}
}
// Note: PsychicMqttClient handles automatic reconnection internally,
// but we need to ensure tokens are renewed before reconnection attempts
}
void MQTTBridge::setMessageTypes(bool status, bool packets, bool raw) {
_status_enabled = status;
_packets_enabled = packets;
_raw_enabled = raw;
}
int MQTTBridge::getConnectedBrokers() const {
int count = 0;
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
count++;
}
}
return count;
}
int MQTTBridge::getQueueSize() const {
return _queue_count;
}
void MQTTBridge::setStatsSources(mesh::Dispatcher* dispatcher, mesh::Radio* radio,
mesh::MainBoard* board, mesh::MillisecondClock* ms) {
_dispatcher = dispatcher;
_radio = radio;
_board = board;
_ms = ms;
}
void MQTTBridge::syncTimeWithNTP() {
if (!WiFi.isConnected()) {
MQTT_DEBUG_PRINTLN("Cannot sync time - WiFi not connected");
return;
}
MQTT_DEBUG_PRINTLN("Syncing time with NTP...");
// Test DNS resolution before attempting NTP sync
#ifdef ESP_PLATFORM
IPAddress resolved_ip;
if (!WiFi.hostByName("pool.ntp.org", resolved_ip)) {
MQTT_DEBUG_PRINTLN("WARNING: DNS resolution failed for pool.ntp.org - NTP sync may fail");
}
#endif
// Begin NTP client
_ntp_client.begin();
// Force update (blocking call with timeout)
if (_ntp_client.forceUpdate()) {
unsigned long epochTime = _ntp_client.getEpochTime();
// Set system timezone to UTC first
// This ensures time() returns UTC time
configTime(0, 0, "pool.ntp.org");
// Update the device's RTC clock with UTC time (if available)
if (_rtc) {
_rtc->setCurrentTime(epochTime);
}
// Mark NTP as synced regardless of RTC availability
// JWT tokens need valid time, which is now available via time()
bool was_ntp_synced = _ntp_synced;
_ntp_synced = true;
_last_ntp_sync = millis();
MQTT_DEBUG_PRINTLN("Time synced: %lu", epochTime);
// If tokens were created before NTP sync (expires_at == 0), set expiration times now
// Tokens are created during begin(), so they're recent (within last few minutes)
// Set expiration to 24 hours from now to be safe
if (!was_ntp_synced) {
unsigned long current_time = time(nullptr);
unsigned long expires_in = 86400; // 24 hours
if (_analyzer_us_enabled && _token_us_expires_at == 0 && strlen(_auth_token_us) > 0) {
_token_us_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("US token expiration set after NTP sync: %lu", _token_us_expires_at);
}
if (_analyzer_eu_enabled && _token_eu_expires_at == 0 && strlen(_auth_token_eu) > 0) {
_token_eu_expires_at = current_time + expires_in;
MQTT_DEBUG_PRINTLN("EU token expiration set after NTP sync: %lu", _token_eu_expires_at);
}
}
// Set timezone from string (with DST support) - only if changed
static char last_timezone[64] = "";
if (strcmp(_prefs->timezone_string, last_timezone) != 0) {
MQTT_DEBUG_PRINTLN("Setting timezone: %s", _prefs->timezone_string);
// Clean up old timezone object to prevent memory leak
if (_timezone) {
delete _timezone;
_timezone = nullptr;
}
// Create timezone object based on timezone string
Timezone* tz = createTimezoneFromString(_prefs->timezone_string);
if (tz) {
MQTT_DEBUG_PRINTLN("Timezone created successfully");
// Store timezone for later use in message building
_timezone = tz;
} else {
MQTT_DEBUG_PRINTLN("Failed to create timezone, using UTC");
// Create UTC timezone as fallback
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
_timezone = new Timezone(utc, utc);
}
// Remember this timezone string
strncpy(last_timezone, _prefs->timezone_string, sizeof(last_timezone) - 1);
last_timezone[sizeof(last_timezone) - 1] = '\0';
// Force memory defragmentation after timezone recreation
MQTT_DEBUG_PRINTLN("Forcing memory defragmentation after timezone change");
void* temp = malloc(1024);
if (temp) {
free(temp);
MQTT_DEBUG_PRINTLN("Defragmentation complete. Max Alloc: %d", ESP.getMaxAllocHeap());
}
}
// Show current time in both UTC and local
struct tm* utc_timeinfo = gmtime((time_t*)&epochTime);
struct tm* local_timeinfo = localtime((time_t*)&epochTime);
if (utc_timeinfo) {
MQTT_DEBUG_PRINTLN("UTC time: %04d-%02d-%02d %02d:%02d:%02d",
utc_timeinfo->tm_year + 1900, utc_timeinfo->tm_mon + 1, utc_timeinfo->tm_mday,
utc_timeinfo->tm_hour, utc_timeinfo->tm_min, utc_timeinfo->tm_sec);
}
if (local_timeinfo) {
MQTT_DEBUG_PRINTLN("Local time: %04d-%02d-%02d %02d:%02d:%02d",
local_timeinfo->tm_year + 1900, local_timeinfo->tm_mon + 1, local_timeinfo->tm_mday,
local_timeinfo->tm_hour, local_timeinfo->tm_min, local_timeinfo->tm_sec);
}
} else {
MQTT_DEBUG_PRINTLN("NTP sync failed");
}
_ntp_client.end();
}
Timezone* MQTTBridge::createTimezoneFromString(const char* tz_string) {
// Create Timezone objects for common IANA timezone strings
// North America
if (strcmp(tz_string, "America/Los_Angeles") == 0 || strcmp(tz_string, "America/Vancouver") == 0) {
TimeChangeRule pst = {"PST", First, Sun, Nov, 2, -480}; // UTC-8
TimeChangeRule pdt = {"PDT", Second, Sun, Mar, 2, -420}; // UTC-7
return new Timezone(pdt, pst);
} else if (strcmp(tz_string, "America/Denver") == 0) {
TimeChangeRule mst = {"MST", First, Sun, Nov, 2, -420}; // UTC-7
TimeChangeRule mdt = {"MDT", Second, Sun, Mar, 2, -360}; // UTC-6
return new Timezone(mdt, mst);
} else if (strcmp(tz_string, "America/Chicago") == 0) {
TimeChangeRule cst = {"CST", First, Sun, Nov, 2, -360}; // UTC-6
TimeChangeRule cdt = {"CDT", Second, Sun, Mar, 2, -300}; // UTC-5
return new Timezone(cdt, cst);
} else if (strcmp(tz_string, "America/New_York") == 0 || strcmp(tz_string, "America/Toronto") == 0) {
TimeChangeRule est = {"EST", First, Sun, Nov, 2, -300}; // UTC-5
TimeChangeRule edt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC-4
return new Timezone(edt, est);
} else if (strcmp(tz_string, "America/Anchorage") == 0) {
TimeChangeRule akst = {"AKST", First, Sun, Nov, 2, -540}; // UTC-9
TimeChangeRule akdt = {"AKDT", Second, Sun, Mar, 2, -480}; // UTC-8
return new Timezone(akdt, akst);
} else if (strcmp(tz_string, "Pacific/Honolulu") == 0) {
TimeChangeRule hst = {"HST", Last, Sun, Oct, 2, -600}; // UTC-10 (no DST)
return new Timezone(hst, hst);
// Europe
} else if (strcmp(tz_string, "Europe/London") == 0) {
TimeChangeRule gmt = {"GMT", Last, Sun, Oct, 2, 0}; // UTC+0
TimeChangeRule bst = {"BST", Last, Sun, Mar, 1, 60}; // UTC+1
return new Timezone(bst, gmt);
} else if (strcmp(tz_string, "Europe/Paris") == 0 || strcmp(tz_string, "Europe/Berlin") == 0) {
TimeChangeRule cet = {"CET", Last, Sun, Oct, 3, 60}; // UTC+1
TimeChangeRule cest = {"CEST", Last, Sun, Mar, 2, 120}; // UTC+2
return new Timezone(cest, cet);
} else if (strcmp(tz_string, "Europe/Moscow") == 0) {
TimeChangeRule msk = {"MSK", Last, Sun, Oct, 3, 180}; // UTC+3 (no DST since 2014)
return new Timezone(msk, msk);
// Asia
} else if (strcmp(tz_string, "Asia/Tokyo") == 0) {
TimeChangeRule jst = {"JST", Last, Sun, Oct, 2, 540}; // UTC+9 (no DST)
return new Timezone(jst, jst);
} else if (strcmp(tz_string, "Asia/Shanghai") == 0 || strcmp(tz_string, "Asia/Hong_Kong") == 0) {
TimeChangeRule cst = {"CST", Last, Sun, Oct, 2, 480}; // UTC+8 (no DST)
return new Timezone(cst, cst);
} else if (strcmp(tz_string, "Asia/Kolkata") == 0) {
TimeChangeRule ist = {"IST", Last, Sun, Oct, 2, 330}; // UTC+5:30 (no DST)
return new Timezone(ist, ist);
} else if (strcmp(tz_string, "Asia/Dubai") == 0) {
TimeChangeRule gst = {"GST", Last, Sun, Oct, 2, 240}; // UTC+4 (no DST)
return new Timezone(gst, gst);
// Australia
} else if (strcmp(tz_string, "Australia/Sydney") == 0 || strcmp(tz_string, "Australia/Melbourne") == 0) {
TimeChangeRule aest = {"AEST", First, Sun, Apr, 3, 600}; // UTC+10
TimeChangeRule aedt = {"AEDT", First, Sun, Oct, 2, 660}; // UTC+11
return new Timezone(aedt, aest);
} else if (strcmp(tz_string, "Australia/Perth") == 0) {
TimeChangeRule awst = {"AWST", Last, Sun, Oct, 2, 480}; // UTC+8 (no DST)
return new Timezone(awst, awst);
// Timezone abbreviations (with DST handling)
} else if (strcmp(tz_string, "PDT") == 0 || strcmp(tz_string, "PST") == 0) {
// Pacific Time (PST/PDT)
TimeChangeRule pst = {"PST", First, Sun, Nov, 2, -480}; // UTC-8
TimeChangeRule pdt = {"PDT", Second, Sun, Mar, 2, -420}; // UTC-7
return new Timezone(pdt, pst);
} else if (strcmp(tz_string, "MDT") == 0 || strcmp(tz_string, "MST") == 0) {
// Mountain Time (MST/MDT)
TimeChangeRule mst = {"MST", First, Sun, Nov, 2, -420}; // UTC-7
TimeChangeRule mdt = {"MDT", Second, Sun, Mar, 2, -360}; // UTC-6
return new Timezone(mdt, mst);
} else if (strcmp(tz_string, "CDT") == 0 || strcmp(tz_string, "CST") == 0) {
// Central Time (CST/CDT)
TimeChangeRule cst = {"CST", First, Sun, Nov, 2, -360}; // UTC-6
TimeChangeRule cdt = {"CDT", Second, Sun, Mar, 2, -300}; // UTC-5
return new Timezone(cdt, cst);
} else if (strcmp(tz_string, "EDT") == 0 || strcmp(tz_string, "EST") == 0) {
// Eastern Time (EST/EDT)
TimeChangeRule est = {"EST", First, Sun, Nov, 2, -300}; // UTC-5
TimeChangeRule edt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC-4
return new Timezone(edt, est);
} else if (strcmp(tz_string, "BST") == 0 || strcmp(tz_string, "GMT") == 0) {
// British Time (GMT/BST)
TimeChangeRule gmt = {"GMT", Last, Sun, Oct, 2, 0}; // UTC+0
TimeChangeRule bst = {"BST", Last, Sun, Mar, 1, 60}; // UTC+1
return new Timezone(bst, gmt);
} else if (strcmp(tz_string, "CEST") == 0 || strcmp(tz_string, "CET") == 0) {
// Central European Time (CET/CEST)
TimeChangeRule cet = {"CET", Last, Sun, Oct, 3, 60}; // UTC+1
TimeChangeRule cest = {"CEST", Last, Sun, Mar, 2, 120}; // UTC+2
return new Timezone(cest, cet);
// UTC and simple offsets
} else if (strcmp(tz_string, "UTC") == 0) {
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
return new Timezone(utc, utc);
} else if (strncmp(tz_string, "UTC", 3) == 0) {
// Handle UTC+/-X format (UTC-8, UTC+5, etc.)
int offset = atoi(tz_string + 3);
TimeChangeRule utc_offset = {"UTC", Last, Sun, Mar, 0, offset * 60};
return new Timezone(utc_offset, utc_offset);
} else if (strncmp(tz_string, "GMT", 3) == 0) {
// Handle GMT+/-X format (GMT-8, GMT+5, etc.)
int offset = atoi(tz_string + 3);
TimeChangeRule gmt_offset = {"GMT", Last, Sun, Mar, 0, offset * 60};
return new Timezone(gmt_offset, gmt_offset);
} else if (strncmp(tz_string, "+", 1) == 0 || strncmp(tz_string, "-", 1) == 0) {
// Handle simple +/-X format (+5, -8, etc.)
int offset = atoi(tz_string);
TimeChangeRule offset_tz = {"TZ", Last, Sun, Mar, 0, offset * 60};
return new Timezone(offset_tz, offset_tz);
} else {
// Unknown timezone, return null
MQTT_DEBUG_PRINTLN("Unknown timezone: %s", tz_string);
return nullptr;
}
}
void MQTTBridge::getClientVersion(char* buffer, size_t buffer_size) const {
if (!buffer || buffer_size == 0) {
return;
}
// Generate client version string in format "meshcore/{firmware_version}"
snprintf(buffer, buffer_size, "meshcore/%s", _firmware_version);
}
void MQTTBridge::optimizeMqttClientConfig(PsychicMqttClient* client, bool is_analyzer_client) {
if (!client) return;
// Buffer size selection:
// - Analyzer clients: Need 1024 bytes for CONNECT message with 768-byte JWT tokens
// (CONNECT message: ~10 bytes overhead + 70 bytes username + 768 bytes password = ~850+ bytes)
// - Main client: Can use 768 bytes (smaller than default 1024, but safe for regular publishes)
// Most JSON messages are <500 bytes, but we need headroom for CONNECT message too
int buffer_size = is_analyzer_client ? 1024 : 768;
client->setBufferSize(buffer_size);
// Access ESP-IDF config to optimize additional settings
esp_mqtt_client_config_t* config = client->getMqttConfig();
if (config) {
#if defined(ESP_IDF_VERSION_MAJOR) && ESP_IDF_VERSION_MAJOR >= 5
// ESP-IDF v5: Use buffer.size and buffer.out_size
// Set output buffer size to match input buffer
if (config->buffer.out_size == 0 || config->buffer.out_size > buffer_size) {
config->buffer.out_size = buffer_size;
MQTT_DEBUG_PRINTLN("Optimized MQTT client (%s): buffer.size=%d, out_size=%d",
is_analyzer_client ? "analyzer" : "main",
config->buffer.size, config->buffer.out_size);
}
#else
// ESP-IDF v4: buffer_size is set via setBufferSize() above
// Note: out_buffer_size may not be directly accessible in v4 config structure
MQTT_DEBUG_PRINTLN("Optimized MQTT client (%s): buffer_size=%d",
is_analyzer_client ? "analyzer" : "main",
config->buffer_size);
#endif
}
}
void MQTTBridge::logMemoryStatus() {
MQTT_DEBUG_PRINTLN("=== Memory Status ===");
MQTT_DEBUG_PRINTLN("Free heap: %d bytes", ESP.getFreeHeap());
MQTT_DEBUG_PRINTLN("Min free heap: %d bytes", ESP.getMinFreeHeap());
MQTT_DEBUG_PRINTLN("Max alloc heap: %d bytes", ESP.getMaxAllocHeap());
MQTT_DEBUG_PRINTLN("Heap size: %d bytes", ESP.getHeapSize());
MQTT_DEBUG_PRINTLN("Free PSRAM: %d bytes", ESP.getFreePsram());
MQTT_DEBUG_PRINTLN("Queue size: %d/%d packets", _queue_count, MAX_QUEUE_SIZE);
MQTT_DEBUG_PRINTLN("===================");
}
#endif