From efc15eb6436ff5c5ec91157f574fc67f818075aa Mon Sep 17 00:00:00 2001 From: agessaman Date: Sat, 25 Oct 2025 21:35:11 -0700 Subject: [PATCH] Add MQTT Bridge implementation with WiFi CLI configuration - Implement MQTT bridge for packet uplink to multiple brokers - Add JSON message formatting for status, packets, and raw data - Add WiFi SSID/password CLI configuration (get/set wifi.ssid, wifi.pwd) - Add persistent storage for WiFi credentials - Add NTP time synchronization for accurate MQTT timestamps - Add CLI commands for MQTT configuration (origin, iata, status, packets, raw) - Support multiple MQTT brokers with automatic reconnection - Integrate with existing MeshCore bridge architecture --- MQTT_IMPLEMENTATION.md | 422 +------ examples/simple_repeater/MyMesh.cpp | 44 +- src/helpers/CommonCLI.cpp | 516 +++------ src/helpers/MQTTMessageBuilder.cpp | 200 +--- src/helpers/MQTTMessageBuilder.h | 33 +- src/helpers/bridges/MQTTBridge.cpp | 1636 +++------------------------ src/helpers/bridges/MQTTBridge.h | 160 +-- variants/heltec_v3/platformio.ini | 66 +- 8 files changed, 376 insertions(+), 2701 deletions(-) diff --git a/MQTT_IMPLEMENTATION.md b/MQTT_IMPLEMENTATION.md index 28f1169c..9a1b7455 100644 --- a/MQTT_IMPLEMENTATION.md +++ b/MQTT_IMPLEMENTATION.md @@ -2,53 +2,6 @@ This document describes the MQTT bridge implementation that allows MeshCore repeaters to uplink packet data to multiple MQTT brokers. -## Quick Start Guide - -### Essential Commands to Get MQTT Repeater Running - -**1. Connect to device console via repeater login or serial console (115200 baud)** -```bash -# Connect to device via serial -``` - -**2. Configure WiFi Credentials** -```bash -set wifi.ssid YourWiFiNetwork -set wifi.pwd YourWiFiPassword -``` - -**3. Reboot to Connect to WiFi** -```bash -reboot -``` - -**4. Verify Configuration** -```bash -get wifi.ssid -get bridge.enabled -get mqtt.origin -get mqtt.iata -``` -If you wish to change mqtt.iata, use `set mqtt.iata XXX` - -**5. Restart Bridge (if needed)** -```bash -# Option A: Toggle bridge off then on -set bridge.enabled off -set bridge.enabled on - -# Option B: Full device reboot -reboot -``` - -**That's it!** The device will now: -- Connect to WiFi automatically -- Start uplinking mesh packets to Let's Mesh Analyzer -- Publish to both custom MQTT broker and Let's Mesh Analyzer servers -- Use device name as MQTT origin (set automatically) - ---- - ## Overview The MQTT bridge implementation provides: @@ -68,47 +21,19 @@ The MQTT bridge implementation provides: ### Integration - Updated `examples/simple_repeater/MyMesh.h` - Added MQTT bridge support -- Updated `examples/simple_repeater/MyMesh.cpp` - Added MQTT bridge integration and raw radio data capture -- Updated `src/helpers/CommonCLI.h` - Added MQTT, WiFi, and timezone configuration fields -- Updated `src/helpers/CommonCLI.cpp` - Added MQTT, WiFi, and timezone CLI commands +- Updated `examples/simple_repeater/MyMesh.cpp` - Added MQTT bridge integration +- Updated `src/helpers/CommonCLI.h` - Added MQTT configuration fields +- Updated `src/helpers/CommonCLI.cpp` - Added MQTT CLI commands - Updated `variants/heltec_v3/platformio.ini` - Added MQTT build configuration -- Updated `variants/station_g2/platformio.ini` - Added MQTT build configuration for Station G2 ## Build Configuration To build the MQTT bridge firmware: -### Heltec V3 ```bash -pio run -e Heltec_v3_repeater_observer_mqtt +pio run -e Heltec_v3_repeater_bridge_mqtt ``` -### Station G2 -```bash -pio run -e Station_G2_repeater_observer_mqtt -``` - -### Custom MQTT Server Configuration - -You can configure a custom MQTT server using build flags in `platformio.ini`: - -```ini -[env:Heltec_v3_repeater_observer_mqtt] -build_flags = - ${Heltec_lora32_v3.build_flags} - -D WITH_MQTT_BRIDGE=1 - -D MQTT_SERVER='"your-mqtt-broker.com"' - -D MQTT_PORT=1883 - -D MQTT_USERNAME='"your-username"' - -D MQTT_PASSWORD='"your-password"' -``` - -**Build Flags:** -- `MQTT_SERVER` - MQTT broker hostname -- `MQTT_PORT` - MQTT broker port (default: 1883) -- `MQTT_USERNAME` - MQTT username -- `MQTT_PASSWORD` - MQTT password - ## Default Configuration The MQTT bridge comes with the following defaults: @@ -117,117 +42,40 @@ The MQTT bridge comes with the following defaults: - **Status Messages**: Enabled - **Packet Messages**: Enabled - **Raw Messages**: Disabled -- **TX Messages**: Disabled (RX only by default) - **Status Interval**: 5 minutes (300000 ms) - **Default Broker**: meshtastic.pugetmesh.org:1883 (username: meshdev, password: large4cats) -- **WiFi SSID**: "ssid_here" (must be configured) -- **WiFi Password**: "password_here" (must be configured) -- **Timezone**: "America/Los_Angeles" (Pacific Time with DST support) -- **Timezone Offset**: -8 hours (fallback) -- **Let's Mesh Analyzer US**: Enabled (mqtt-us-v1.letsmesh.net:443) -- **Let's Mesh Analyzer EU**: Enabled (mqtt-eu-v1.letsmesh.net:443) ## CLI Commands -### MQTT Commands - -#### Get Commands +### Get Commands - `get mqtt.origin` - Get device origin name - `get mqtt.iata` - Get IATA code - `get mqtt.status` - Get status message setting (on/off) - `get mqtt.packets` - Get packet message setting (on/off) - `get mqtt.raw` - Get raw message setting (on/off) -- `get mqtt.tx` - Get TX message setting (on/off) - `get mqtt.interval` - Get status publish interval (ms) -- `get mqtt.server` - Get MQTT server hostname -- `get mqtt.port` - Get MQTT server port -- `get mqtt.username` - Get MQTT username -- `get mqtt.password` - Get MQTT password -- `get mqtt.analyzer.us` - Get US Let's Mesh Analyzer server setting (on/off) -- `get mqtt.analyzer.eu` - Get EU Let's Mesh Analyzer server setting (on/off) -#### Set Commands +### Set Commands - `set mqtt.origin ` - Set device origin name - `set mqtt.iata ` - Set IATA code - `set mqtt.status on|off` - Enable/disable status messages - `set mqtt.packets on|off` - Enable/disable packet messages - `set mqtt.raw on|off` - Enable/disable raw messages -- `set mqtt.tx on|off` - Enable/disable TX packet messages - `set mqtt.interval ` - Set status publish interval (1000-3600000 ms) -- `set mqtt.server ` - Set MQTT server hostname -- `set mqtt.port ` - Set MQTT server port (1-65535) -- `set mqtt.username ` - Set MQTT username -- `set mqtt.password ` - Set MQTT password -- `set mqtt.analyzer.us on|off` - Enable/disable US Let's Mesh Analyzer server -- `set mqtt.analyzer.eu on|off` - Enable/disable EU Let's Mesh Analyzer server - -### WiFi Commands - -#### Get Commands -- `get wifi.ssid` - Get WiFi SSID -- `get wifi.pwd` - Get WiFi password - -#### Set Commands -- `set wifi.ssid ` - Set WiFi SSID -- `set wifi.pwd ` - Set WiFi password - -### Timezone Commands - -#### Get Commands -- `get timezone` - Get timezone string (e.g., "America/Los_Angeles") -- `get timezone.offset` - Get timezone offset in hours (-12 to +14) - -#### Set Commands -- `set timezone ` - Set timezone string (IANA format or abbreviation) -- `set timezone.offset ` - Set timezone offset in hours (-12 to +14) - -#### Supported Timezone Formats -- **IANA strings**: `America/Los_Angeles`, `Europe/London`, `Asia/Tokyo`, etc. -- **Common abbreviations**: `PDT`, `PST`, `MDT`, `MST`, `CDT`, `CST`, `EDT`, `EST`, `BST`, `GMT`, `CEST`, `CET` -- **UTC offsets**: `UTC-8`, `UTC+5`, `+5`, `-8`, etc. - -### Bridge Commands - -#### Get Commands -- `get bridge.source` - Get packet source (rx/tx) -- `get bridge.enabled` - Get bridge enabled status (on/off) - -#### Set Commands -- `set bridge.source rx|tx` - Set packet source (rx for received, tx for transmitted) -- `set bridge.enabled on|off` - Enable/disable bridge - -## Command Architecture - -The CLI commands are organized into two levels: - -### Bridge Commands (`bridge.*`) -**Low-level bridge control** - These settings apply to all bridge types (MQTT, RS232, ESP-NOW, etc.): -- `bridge.enabled` - Master switch for the entire bridge system -- `bridge.source` - Controls which packet events to capture (RX vs TX) - -### Bridge-Specific Commands (`mqtt.*`, `wifi.*`, `timezone.*`) -**Implementation-specific settings** - These only apply to the MQTT bridge: -- `mqtt.*` - MQTT broker configuration, message types, and formatting -- `wifi.*` - WiFi connection settings for MQTT connectivity -- `timezone.*` - Timezone configuration for accurate timestamps - -This design allows MeshCore to support multiple bridge types simultaneously while keeping configuration clean and logical. ## MQTT Topics -The bridge publishes to three main topics with the following structure: +The bridge publishes to three main topics: -### Status Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/status` +### Status Topic: `meshcore/{IATA}/status` Device connection status and metadata (retained messages). -### Packets Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/packets` +### Packets Topic: `meshcore/{IATA}/packets` Full packet data with RF characteristics and metadata. -### Raw Topic: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/raw` +### Raw Topic: `meshcore/{IATA}/raw` Minimal raw packet data for map integration. -**Note**: `{DEVICE_PUBLIC_KEY}` is the device's public key in hexadecimal format (64 characters). - ## JSON Message Formats ### Status Message @@ -240,7 +88,7 @@ Minimal raw packet data for map integration. "model": "device_model", "firmware_version": "firmware_version", "radio": "radio_info", - "client_version": "meshcore-custom-repeater/{build_date}" + "client_version": "packet_capture_version" } ``` @@ -277,260 +125,24 @@ Minimal raw packet data for map integration. } ``` -## Key Features - -### Raw Radio Data Capture -- Captures actual raw radio transmission data (including radio headers) -- Uses proper MeshCore packet hashing (SHA256-based) -- Provides accurate SNR/RSSI values from actual radio reception -- Supports both RX and TX packet uplinking (configurable) - -### Timezone Support -- Full timezone support with automatic DST handling -- Supports IANA timezone strings, common abbreviations, and UTC offsets -- Separates local time (for timestamps) and UTC time (for time/date fields) -- Uses JChristensen/Timezone library for accurate timezone conversions - -### WiFi Configuration -- Runtime WiFi credential management via CLI -- Persistent storage across reboots -- Automatic reconnection with exponential backoff - -### NTP Time Synchronization -- Automatic time synchronization with NTP servers -- Periodic time updates (every hour) -- Proper UTC system time handling - -### Let's Mesh Analyzer Integration -- **JWT Authentication**: Ed25519-signed tokens for secure MQTT authentication -- **WebSocket MQTT**: Support for MQTT over WebSocket connections (TLS/SSL) -- **Dual Server Support**: Both US and EU servers enabled by default -- **Automatic Token Generation**: Creates authentication tokens using device's Ed25519 keys -- **Username Format**: `v1_{UPPERCASE_PUBLIC_KEY}` (e.g., `v1_7E7662676F7F0850A8A355BAAFBFC1EB7B4174C340442D7D7161C9474A2C9400`) -- **Server Configuration**: - - US Server: `mqtt-us-v1.letsmesh.net:443` (WebSocket with TLS) - - EU Server: `mqtt-eu-v1.letsmesh.net:443` (WebSocket with TLS) - -## First-Time Setup - -### Prerequisites -- MeshCore device with MQTT bridge firmware flashed -- WiFi network credentials -- MQTT broker (optional - default broker is provided) -- LoRa-capable device for configuration (repeater console) -- MeshCore network access - -### Step 1: Initial Boot and Network Connection -1. **Flash the firmware** to your device using PlatformIO or the build script -2. **Deploy the device** in your mesh network location -3. **Ensure WiFi connectivity** - the device will automatically connect to WiFi if credentials are pre-configured -4. **Verify mesh network access** - device should be discoverable by other mesh nodes - -### Step 2: Connect via LoRa Repeater Console -Use a MeshCore companion device to configure the Repeater's MQTT bridge. - -1. **Connect to the mesh** using your companion -2. **Locate the MQTT bridge device** in your contacts -3. **Log into your Repeater** using the default password (password) or whatever you configured via serial console -3. **Tap on the repeater console** on your repeater's settings -4. **Send configuration commands** via LoRa to the MQTT bridge device - -### Step 3: Configure WiFi Connection -The device needs internet connectivity to publish to MQTT brokers. - -**Via LoRa Repeater Console:** -``` -# Set your WiFi credentials -set wifi.ssid "YourWiFiNetwork" -set wifi.pwd "YourWiFiPassword" - -# Verify WiFi settings -get wifi.ssid -get wifi.pwd -``` - -### Step 4: Configure Device Identity -Set up your device's identity for MQTT topics and status messages. - -**Via LoRa Repeater Console:** -``` -# Set IATA code for topic structure (e.g., airport code) -set mqtt.iata "SEA" - -# Verify settings (origin is set automatically to device name) -get mqtt.origin -get mqtt.iata -``` - -### Step 5: Configure Timezone -Set your local timezone for accurate timestamps. - -**Via LoRa Repeater Console:** -``` -# Set timezone (choose one method) -set timezone "America/Los_Angeles" # IANA format -set timezone "PDT" # Abbreviation -set timezone "UTC-8" # UTC offset - -# Verify timezone -get timezone -``` - -### Step 6: Configure MQTT Settings -Customize which messages to publish and how often. - -**Via LoRa Repeater Console:** -``` -# Configure MQTT server (optional - uses defaults if not set) -set mqtt.server "your-mqtt-broker.com" -set mqtt.port 1883 -set mqtt.username "your-username" -set mqtt.password "your-password" - -# Enable/disable message types -set mqtt.status on # Device status messages -set mqtt.packets on # Packet data messages -set mqtt.raw off # Raw packet data (optional) -set mqtt.tx off # Transmitted packets (optional) - -# Set status publish interval (default: 5 minutes) -set mqtt.interval 300000 - -# Verify settings -get mqtt.server -get mqtt.port -get mqtt.username -get mqtt.status -get mqtt.packets -get mqtt.interval -``` - -### Step 7: Verify MQTT Broker Connection -Check that the device can connect to MQTT brokers. - -**Via LoRa Repeater Console:** -``` -# Check bridge status -get bridge.enabled - -# If disabled, enable it -set bridge.enabled on - -# Check MQTT analyzer servers (optional) -get mqtt.analyzer.us -get mqtt.analyzer.eu -``` - -### Step 8: Monitor MQTT Messages -Once configured, the device will automatically publish messages to MQTT brokers. - -**Default MQTT Broker**: `meshtastic.pugetmesh.org:1883` -- Username: `meshdev` -- Password: `large4cats` - -**Topic Structure**: -- Status: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/status` -- Packets: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/packets` -- Raw: `meshcore/{IATA}/{DEVICE_PUBLIC_KEY}/raw` - -**Example Topics**: -- `meshcore/SEA/7E7662676F7F0850A8A355BAAFBFC1EB7B4174C340442D7D7161C9474A2C9400/status` -- `meshcore/SEA/7E7662676F7F0850A8A355BAAFBFC1EB7B4174C340442D7D7161C9474A2C9400/packets` - -### Step 9: Troubleshooting - -#### Device Won't Connect to WiFi -**Via LoRa Repeater Console:** -``` -# Check WiFi settings -get wifi.ssid -get wifi.pwd - -# Reset WiFi settings -set wifi.ssid "" -set wifi.pwd "" - -# Reconfigure with correct credentials -set wifi.ssid "YourWiFiNetwork" -set wifi.pwd "YourWiFiPassword" -``` - -#### No MQTT Messages Appearing -**Via LoRa Repeater Console:** -``` -# Check bridge status -get bridge.enabled - -# Check message types -get mqtt.status -get mqtt.packets - -# Check device identity (origin is set automatically) -get mqtt.origin -get mqtt.iata - -# Enable bridge if needed -set bridge.enabled on -``` - -#### Timezone Issues -**Via LoRa Repeater Console:** -``` -# Check current timezone -get timezone - -# Try different timezone formats -set timezone "America/New_York" # IANA format -set timezone "EST" # Abbreviation -set timezone "UTC-5" # UTC offset -``` - -#### LoRa Configuration Issues -- **Device not responding**: Ensure both devices are on the same mesh network -- **Commands not working**: Check that the target device is reachable via LoRa -- **No response to get commands**: Verify the device is powered and in range - -### Step 10: Advanced Configuration (Optional) - -#### Custom MQTT Broker -If you want to use your own MQTT broker instead of the default: - -``` -# Note: Custom broker configuration requires code modification -# The default broker is: meshtastic.pugetmesh.org:1883 -# Username: meshdev, Password: large4cats -``` - -#### Let's Mesh Analyzer Servers -The device automatically connects to Let's Mesh Analyzer servers for additional monitoring: - -- **US Server**: `mqtt-us-v1.letsmesh.net:443` (WebSocket with TLS) -- **EU Server**: `mqtt-eu-v1.letsmesh.net:443` (WebSocket with TLS) - -These are enabled by default and use JWT authentication with your device's Ed25519 keys. - ## Testing 1. Flash the MQTT bridge firmware to your device -2. Follow the first-time setup instructions above -3. Monitor MQTT broker for incoming messages -4. Verify message formats match the JSON schemas in this document +2. Connect to the device via serial console +3. Configure WiFi connection (if not already connected) +4. Check MQTT settings: `get mqtt.origin` +5. Monitor MQTT broker for incoming messages ## Dependencies - **PubSubClient**: MQTT client library -- **ArduinoJson**: JSON message formatting (v6.17.3) -- **NTPClient**: Network time protocol client -- **Timezone**: Timezone conversion library (JChristensen/Timezone) +- **ArduinoJson**: JSON message formatting - **WiFi**: ESP32 WiFi functionality -- **Ed25519**: Cryptographic library for JWT token signing -- **JWTHelper**: Custom JWT token generation for Let's Mesh Analyzer authentication ## Future Enhancements -- Full WebSocket MQTT implementation (currently JWT tokens are generated but WebSocket publishing is pending) +- Device-signed JWTs for WebSocket MQTT servers - Multiple broker configuration via CLI - Advanced packet filtering - Custom topic templates - TLS/SSL support for secure connections -- Real-time WebSocket MQTT publishing to Let's Mesh Analyzer servers diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 4c1e0700..a0413184 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -343,19 +343,12 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { mesh::Utils::printHex(Serial, raw, len); Serial.println(); #endif - -#ifdef WITH_BRIDGE - if (_prefs.bridge_enabled) { - // Store raw radio data for MQTT messages - bridge.storeRawRadioData(raw, len, snr, rssi); - } -#endif } void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { #ifdef WITH_BRIDGE if (_prefs.bridge_pkt_src == 1) { - bridge.onPacketReceived(pkt); + bridge.sendPacket(pkt); } #endif @@ -681,7 +674,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc #elif defined(WITH_ESPNOW_BRIDGE) , bridge(&_prefs, _mgr, &rtc) #elif defined(WITH_MQTT_BRIDGE) - , bridge(&_prefs, _mgr, &rtc, &self_id) + , bridge(&_prefs, _mgr, &rtc) #endif { last_millis = 0; @@ -719,7 +712,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc // bridge defaults _prefs.bridge_enabled = 1; // enabled _prefs.bridge_delay = 500; // milliseconds - _prefs.bridge_pkt_src = 1; // logRx (RX packets) + _prefs.bridge_pkt_src = 0; // logTx _prefs.bridge_baud = 115200; // baud rate _prefs.bridge_channel = 1; // channel 1 @@ -736,20 +729,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.mqtt_status_enabled = 1; // enabled _prefs.mqtt_packets_enabled = 1; // enabled _prefs.mqtt_raw_enabled = 0; // disabled - _prefs.mqtt_tx_enabled = 0; // disabled (RX only for now) _prefs.mqtt_status_interval = 300000; // 5 minutes // WiFi defaults StrHelper::strncpy(_prefs.wifi_ssid, "ssid_here", sizeof(_prefs.wifi_ssid)); StrHelper::strncpy(_prefs.wifi_password, "password_here", sizeof(_prefs.wifi_password)); - - // Timezone defaults (Pacific Time with DST support) - StrHelper::strncpy(_prefs.timezone_string, "America/Los_Angeles", sizeof(_prefs.timezone_string)); - _prefs.timezone_offset = -8; // fallback - - // Let's Mesh Analyzer defaults (both enabled by default) - _prefs.mqtt_analyzer_us_enabled = 1; // enabled - _prefs.mqtt_analyzer_eu_enabled = 1; // enabled } void MyMesh::begin(FILESYSTEM *fs) { @@ -757,18 +741,6 @@ void MyMesh::begin(FILESYSTEM *fs) { _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); - - // Ensure analyzer servers are enabled by default (in case no prefs were loaded) - if (_prefs.mqtt_analyzer_us_enabled == 0 && _prefs.mqtt_analyzer_eu_enabled == 0) { - _prefs.mqtt_analyzer_us_enabled = 1; // enabled - _prefs.mqtt_analyzer_eu_enabled = 1; // enabled - MESH_DEBUG_PRINTLN("Setting analyzer servers to enabled by default"); - } - - // Set MQTT origin to actual device name (not build-time ADVERT_NAME) - StrHelper::strncpy(_prefs.mqtt_origin, _prefs.node_name, sizeof(_prefs.mqtt_origin)); - MESH_DEBUG_PRINTLN("MQTT origin set to device name: %s", _prefs.mqtt_origin); - acl.load(_fs); // TODO: key_store.begin(); region_map.load(_fs); @@ -788,16 +760,6 @@ void MyMesh::begin(FILESYSTEM *fs) { // Set board model bridge.setBoardModel(_cli.getBoard()->getManufacturerName()); - // Set build date - bridge.setBuildDate(getBuildDate()); - -#ifdef WITH_MQTT_BRIDGE - // Set stats sources for automatic stats collection (optional - can be done in custom initialization) - // This enables stats to be included in status messages automatically - // this (Mesh*) inherits from Dispatcher, so it can be passed as Dispatcher* - bridge.setStatsSources(this, _radio, _cli.getBoard(), _ms); -#endif - bridge.begin(); } #endif diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index e415e700..2c7a8f69 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -3,24 +3,6 @@ #include "TxtDataHelpers.h" #include "AdvertDataHelpers.h" #include -#ifdef WITH_MQTT_BRIDGE -#include "bridges/MQTTBridge.h" - -// Helper function to calculate total size of MQTT fields for file format compatibility -// Uses NodePrefs struct to get accurate field sizes -static size_t getMQTTFieldsSize(const NodePrefs* prefs) { - return sizeof(prefs->mqtt_origin) + sizeof(prefs->mqtt_iata) + - sizeof(prefs->mqtt_status_enabled) + sizeof(prefs->mqtt_packets_enabled) + - sizeof(prefs->mqtt_raw_enabled) + sizeof(prefs->mqtt_tx_enabled) + - sizeof(prefs->mqtt_status_interval) + sizeof(prefs->wifi_ssid) + - sizeof(prefs->wifi_password) + sizeof(prefs->timezone_string) + - sizeof(prefs->timezone_offset) + sizeof(prefs->mqtt_server) + - sizeof(prefs->mqtt_port) + sizeof(prefs->mqtt_username) + - sizeof(prefs->mqtt_password) + sizeof(prefs->mqtt_analyzer_us_enabled) + - sizeof(prefs->mqtt_analyzer_eu_enabled) + sizeof(prefs->mqtt_owner_public_key) + - sizeof(prefs->mqtt_email); -} -#endif // Believe it or not, this std C function is busted on some platforms! static uint32_t _atoi(const char* sp) { @@ -40,12 +22,6 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) { savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old } -#ifdef WITH_MQTT_BRIDGE - // Load MQTT preferences from separate file - loadMQTTPrefs(fs); - // Sync MQTT prefs to NodePrefs so existing code (like MQTTBridge) can access them - syncMQTTPrefsToNodePrefs(); -#endif } void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { @@ -227,33 +203,6 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write(pad, to_write); remaining -= to_write; } - file.write((uint8_t *)&_prefs->mqtt_iata, sizeof(_prefs->mqtt_iata)); // 194 - file.write((uint8_t *)&_prefs->mqtt_status_enabled, sizeof(_prefs->mqtt_status_enabled)); // 202 - file.write((uint8_t *)&_prefs->mqtt_packets_enabled, sizeof(_prefs->mqtt_packets_enabled)); // 203 - file.write((uint8_t *)&_prefs->mqtt_raw_enabled, sizeof(_prefs->mqtt_raw_enabled)); // 204 - file.write((uint8_t *)&_prefs->mqtt_tx_enabled, sizeof(_prefs->mqtt_tx_enabled)); // 205 - file.write((uint8_t *)&_prefs->mqtt_status_interval, sizeof(_prefs->mqtt_status_interval)); // 206 - - // WiFi settings - file.write((uint8_t *)&_prefs->wifi_ssid, sizeof(_prefs->wifi_ssid)); // 209 - file.write((uint8_t *)&_prefs->wifi_password, sizeof(_prefs->wifi_password)); // 241 - - // Timezone settings - file.write((uint8_t *)&_prefs->timezone_string, sizeof(_prefs->timezone_string)); // 305 - file.write((uint8_t *)&_prefs->timezone_offset, sizeof(_prefs->timezone_offset)); // 337 - - // MQTT server settings - file.write((uint8_t *)&_prefs->mqtt_server, sizeof(_prefs->mqtt_server)); // 340 - file.write((uint8_t *)&_prefs->mqtt_port, sizeof(_prefs->mqtt_port)); // 341 - file.write((uint8_t *)&_prefs->mqtt_username, sizeof(_prefs->mqtt_username)); // 342 - file.write((uint8_t *)&_prefs->mqtt_password, sizeof(_prefs->mqtt_password)); // 343 - - // Let's Mesh Analyzer settings - file.write((uint8_t *)&_prefs->mqtt_analyzer_us_enabled, sizeof(_prefs->mqtt_analyzer_us_enabled)); // 344 - file.write((uint8_t *)&_prefs->mqtt_analyzer_eu_enabled, sizeof(_prefs->mqtt_analyzer_eu_enabled)); // 345 - file.write((uint8_t *)&_prefs->mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); // 346 - file.write((uint8_t *)&_prefs->mqtt_email, sizeof(_prefs->mqtt_email)); // 347 - // 209 file.close(); } @@ -264,162 +213,6 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { #endif } -#ifdef WITH_MQTT_BRIDGE -// Set default values for MQTT preferences (used when file doesn't exist or is corrupted) -static void setMQTTPrefsDefaults(MQTTPrefs* prefs) { - memset(prefs, 0, sizeof(MQTTPrefs)); - // Set sensible defaults matching MQTTBridge expectations - prefs->mqtt_status_enabled = 1; // enabled by default - prefs->mqtt_packets_enabled = 1; // enabled by default - prefs->mqtt_raw_enabled = 0; // disabled by default - prefs->mqtt_tx_enabled = 0; // disabled by default (RX only) - prefs->mqtt_status_interval = 300000; // 5 minutes default - prefs->mqtt_analyzer_us_enabled = 1; // enabled by default - prefs->mqtt_analyzer_eu_enabled = 1; // enabled by default - // String fields are already zero-initialized by memset -} - -void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) { - // Initialize with defaults first - setMQTTPrefsDefaults(&_mqtt_prefs); - - if (fs->exists("/mqtt_prefs")) { - // Load from separate MQTT prefs file -#if defined(RP2040_PLATFORM) - File file = fs->open("/mqtt_prefs", "r"); -#else - File file = fs->open("/mqtt_prefs"); -#endif - if (file) { - // Verify file size is correct before reading - if (file.size() >= sizeof(_mqtt_prefs)) { - size_t bytes_read = file.read((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); - if (bytes_read != sizeof(_mqtt_prefs)) { - // File read incomplete - reinitialize to defaults - setMQTTPrefsDefaults(&_mqtt_prefs); - } - } else { - // File too small - reinitialize to defaults - setMQTTPrefsDefaults(&_mqtt_prefs); - } - file.close(); - } - } else { - // Migration: Try to read from old /com_prefs file if it exists - // This handles the case where MQTT settings were previously stored in /com_prefs - if (fs->exists("/com_prefs")) { -#if defined(RP2040_PLATFORM) - File file = fs->open("/com_prefs", "r"); -#else - File file = fs->open("/com_prefs"); -#endif - if (file) { - // Skip to MQTT section (after advert_loc_policy at offset 161) - // Calculate offset: we need to skip everything up to and including advert_loc_policy - size_t offset_to_mqtt = - sizeof(_prefs->airtime_factor) + sizeof(_prefs->node_name) + 4 + // pad - sizeof(_prefs->node_lat) + sizeof(_prefs->node_lon) + - sizeof(_prefs->password) + sizeof(_prefs->freq) + - sizeof(_prefs->tx_power_dbm) + sizeof(_prefs->disable_fwd) + - sizeof(_prefs->advert_interval) + 1 + // pad - sizeof(_prefs->rx_delay_base) + sizeof(_prefs->tx_delay_factor) + - sizeof(_prefs->guest_password) + sizeof(_prefs->direct_tx_delay_factor) + 4 + // pad - sizeof(_prefs->sf) + sizeof(_prefs->cr) + - sizeof(_prefs->allow_read_only) + sizeof(_prefs->multi_acks) + - sizeof(_prefs->bw) + sizeof(_prefs->agc_reset_interval) + 3 + // pad - sizeof(_prefs->flood_max) + sizeof(_prefs->flood_advert_interval) + - sizeof(_prefs->interference_threshold) + sizeof(_prefs->bridge_enabled) + - sizeof(_prefs->bridge_delay) + sizeof(_prefs->bridge_pkt_src) + - sizeof(_prefs->bridge_baud) + sizeof(_prefs->bridge_channel) + - sizeof(_prefs->bridge_secret) + 4 + // pad - sizeof(_prefs->gps_enabled) + sizeof(_prefs->gps_interval) + - sizeof(_prefs->advert_loc_policy); - - // Check if file is large enough and seek succeeded - if (file.size() >= offset_to_mqtt + sizeof(_mqtt_prefs)) { - if (file.seek(offset_to_mqtt)) { - size_t bytes_read = file.read((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); - if (bytes_read == sizeof(_mqtt_prefs)) { - // Successfully migrated - save to new location for future use - file.close(); - saveMQTTPrefs(fs); - return; // Migration successful - } - } - } - file.close(); - // Migration failed - defaults already set, just return - return; - } - } - // No file exists and migration didn't happen - defaults already set - } -} - -void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) { -#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) - fs->remove("/mqtt_prefs"); - File file = fs->open("/mqtt_prefs", FILE_O_WRITE); -#elif defined(RP2040_PLATFORM) - File file = fs->open("/mqtt_prefs", "w"); -#else - File file = fs->open("/mqtt_prefs", "w", true); -#endif - if (file) { - file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); - file.close(); - } -} - -void CommonCLI::syncMQTTPrefsToNodePrefs() { - // Copy MQTT prefs to NodePrefs so existing code can access them - // Use StrHelper::strncpy to ensure proper null termination - StrHelper::strncpy(_prefs->mqtt_origin, _mqtt_prefs.mqtt_origin, sizeof(_prefs->mqtt_origin)); - StrHelper::strncpy(_prefs->mqtt_iata, _mqtt_prefs.mqtt_iata, sizeof(_prefs->mqtt_iata)); - _prefs->mqtt_status_enabled = _mqtt_prefs.mqtt_status_enabled; - _prefs->mqtt_packets_enabled = _mqtt_prefs.mqtt_packets_enabled; - _prefs->mqtt_raw_enabled = _mqtt_prefs.mqtt_raw_enabled; - _prefs->mqtt_tx_enabled = _mqtt_prefs.mqtt_tx_enabled; - _prefs->mqtt_status_interval = _mqtt_prefs.mqtt_status_interval; - StrHelper::strncpy(_prefs->wifi_ssid, _mqtt_prefs.wifi_ssid, sizeof(_prefs->wifi_ssid)); - StrHelper::strncpy(_prefs->wifi_password, _mqtt_prefs.wifi_password, sizeof(_prefs->wifi_password)); - StrHelper::strncpy(_prefs->timezone_string, _mqtt_prefs.timezone_string, sizeof(_prefs->timezone_string)); - _prefs->timezone_offset = _mqtt_prefs.timezone_offset; - StrHelper::strncpy(_prefs->mqtt_server, _mqtt_prefs.mqtt_server, sizeof(_prefs->mqtt_server)); - _prefs->mqtt_port = _mqtt_prefs.mqtt_port; - StrHelper::strncpy(_prefs->mqtt_username, _mqtt_prefs.mqtt_username, sizeof(_prefs->mqtt_username)); - StrHelper::strncpy(_prefs->mqtt_password, _mqtt_prefs.mqtt_password, sizeof(_prefs->mqtt_password)); - _prefs->mqtt_analyzer_us_enabled = _mqtt_prefs.mqtt_analyzer_us_enabled; - _prefs->mqtt_analyzer_eu_enabled = _mqtt_prefs.mqtt_analyzer_eu_enabled; - StrHelper::strncpy(_prefs->mqtt_owner_public_key, _mqtt_prefs.mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); - StrHelper::strncpy(_prefs->mqtt_email, _mqtt_prefs.mqtt_email, sizeof(_prefs->mqtt_email)); -} - -void CommonCLI::syncNodePrefsToMQTTPrefs() { - // Copy NodePrefs to MQTT prefs (used when saving after changes via CLI) - // Use StrHelper::strncpy to ensure proper null termination - StrHelper::strncpy(_mqtt_prefs.mqtt_origin, _prefs->mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin)); - StrHelper::strncpy(_mqtt_prefs.mqtt_iata, _prefs->mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata)); - _mqtt_prefs.mqtt_status_enabled = _prefs->mqtt_status_enabled; - _mqtt_prefs.mqtt_packets_enabled = _prefs->mqtt_packets_enabled; - _mqtt_prefs.mqtt_raw_enabled = _prefs->mqtt_raw_enabled; - _mqtt_prefs.mqtt_tx_enabled = _prefs->mqtt_tx_enabled; - _mqtt_prefs.mqtt_status_interval = _prefs->mqtt_status_interval; - StrHelper::strncpy(_mqtt_prefs.wifi_ssid, _prefs->wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid)); - StrHelper::strncpy(_mqtt_prefs.wifi_password, _prefs->wifi_password, sizeof(_mqtt_prefs.wifi_password)); - StrHelper::strncpy(_mqtt_prefs.timezone_string, _prefs->timezone_string, sizeof(_mqtt_prefs.timezone_string)); - _mqtt_prefs.timezone_offset = _prefs->timezone_offset; - StrHelper::strncpy(_mqtt_prefs.mqtt_server, _prefs->mqtt_server, sizeof(_mqtt_prefs.mqtt_server)); - _mqtt_prefs.mqtt_port = _prefs->mqtt_port; - StrHelper::strncpy(_mqtt_prefs.mqtt_username, _prefs->mqtt_username, sizeof(_mqtt_prefs.mqtt_username)); - StrHelper::strncpy(_mqtt_prefs.mqtt_password, _prefs->mqtt_password, sizeof(_mqtt_prefs.mqtt_password)); - _mqtt_prefs.mqtt_analyzer_us_enabled = _prefs->mqtt_analyzer_us_enabled; - _mqtt_prefs.mqtt_analyzer_eu_enabled = _prefs->mqtt_analyzer_eu_enabled; - StrHelper::strncpy(_mqtt_prefs.mqtt_owner_public_key, _prefs->mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key)); - StrHelper::strncpy(_mqtt_prefs.mqtt_email, _prefs->mqtt_email, sizeof(_mqtt_prefs.mqtt_email)); -} -#endif - #define MIN_LOCAL_ADVERT_INTERVAL 60 void CommonCLI::savePrefs() { @@ -458,10 +251,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch } else { strcpy(reply, "ERR: clock cannot go backwards"); } - } else if (memcmp(command, "memory", 6) == 0) { - sprintf(reply, "Free: %d, Min: %d, Max: %d, Queue: %d", - ESP.getFreeHeap(), ESP.getMinFreeHeap(), ESP.getMaxAllocHeap(), - _callbacks->getQueueSize()); } else if (memcmp(command, "start ota", 9) == 0) { if (!_board->startOTAUpdate(_prefs->node_name, reply)) { strcpy(reply, "Error"); @@ -610,49 +399,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch sprintf(reply, "> %s", _prefs->mqtt_status_enabled ? "on" : "off"); } else if (memcmp(config, "mqtt.packets", 12) == 0) { sprintf(reply, "> %s", _prefs->mqtt_packets_enabled ? "on" : "off"); - } else if (memcmp(config, "mqtt.raw", 8) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_raw_enabled ? "on" : "off"); - } else if (memcmp(config, "mqtt.tx", 7) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_tx_enabled ? "on" : "off"); + } else if (memcmp(config, "mqtt.raw", 8) == 0) { + sprintf(reply, "> %s", _prefs->mqtt_raw_enabled ? "on" : "off"); } else if (memcmp(config, "mqtt.interval", 13) == 0) { - // Display interval in minutes (rounded) - uint32_t minutes = (_prefs->mqtt_status_interval + 29999) / 60000; // Round up - sprintf(reply, "> %u minutes (%lu ms)", minutes, _prefs->mqtt_status_interval); - } else if (memcmp(config, "mqtt.server", 11) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_server); - } else if (memcmp(config, "mqtt.port", 9) == 0) { - sprintf(reply, "> %d", _prefs->mqtt_port); - } else if (memcmp(config, "mqtt.username", 13) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_username); - } else if (memcmp(config, "mqtt.password", 13) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_password); + sprintf(reply, "> %d", (uint32_t)_prefs->mqtt_status_interval); } else if (memcmp(config, "wifi.ssid", 9) == 0) { sprintf(reply, "> %s", _prefs->wifi_ssid); } else if (memcmp(config, "wifi.pwd", 8) == 0) { sprintf(reply, "> %s", _prefs->wifi_password); - } else if (memcmp(config, "timezone", 8) == 0) { - sprintf(reply, "> %s", _prefs->timezone_string); - } else if (memcmp(config, "timezone.offset", 15) == 0) { - sprintf(reply, "> %d", _prefs->timezone_offset); - } else if (memcmp(config, "mqtt.analyzer.us", 17) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_analyzer_us_enabled ? "on" : "off"); - } else if (memcmp(config, "mqtt.analyzer.eu", 17) == 0) { - sprintf(reply, "> %s", _prefs->mqtt_analyzer_eu_enabled ? "on" : "off"); - } else if (sender_timestamp == 0 && memcmp(config, "mqtt.owner", 10) == 0) { // from serial command line only - if (_prefs->mqtt_owner_public_key[0] != '\0') { - sprintf(reply, "> %s", _prefs->mqtt_owner_public_key); - } else { - strcpy(reply, "> (not set)"); - } - } else if (sender_timestamp == 0 && memcmp(config, "mqtt.email", 10) == 0) { // from serial command line only - if (_prefs->mqtt_email[0] != '\0') { - sprintf(reply, "> %s", _prefs->mqtt_email); - } else { - strcpy(reply, "> (not set)"); - } - } else if (memcmp(config, "mqtt.config.valid", 17) == 0) { - bool valid = MQTTBridge::isConfigValid(_prefs); - sprintf(reply, "> %s", valid ? "valid" : "invalid"); #endif } else { sprintf(reply, "??: %s", config); @@ -863,24 +617,18 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch _prefs->mqtt_packets_enabled = memcmp(&config[13], "on", 2) == 0; savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.raw ", 9) == 0) { - _prefs->mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.tx ", 8) == 0) { - _prefs->mqtt_tx_enabled = memcmp(&config[8], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.interval ", 14) == 0) { - uint32_t minutes = _atoi(&config[14]); - if (minutes >= 1 && minutes <= 60) { // 1 minute to 60 minutes - _prefs->mqtt_status_interval = minutes * 60000; // Convert minutes to milliseconds + } else if (memcmp(config, "mqtt.raw ", 9) == 0) { + _prefs->mqtt_raw_enabled = memcmp(&config[9], "on", 2) == 0; + savePrefs(); + strcpy(reply, "OK"); + } else if (memcmp(config, "mqtt.interval ", 15) == 0) { + uint32_t interval = _atoi(&config[15]); + if (interval >= 1000 && interval <= 3600000) { // 1 second to 1 hour + _prefs->mqtt_status_interval = interval; savePrefs(); - // Restart bridge to pick up new interval value - _callbacks->restartBridge(); - sprintf(reply, "OK - interval set to %u minutes (%lu ms), bridge restarted", minutes, _prefs->mqtt_status_interval); + strcpy(reply, "OK"); } else { - strcpy(reply, "Error: interval must be between 1-60 minutes"); + strcpy(reply, "Error: interval must be between 1000-3600000 ms"); } } else if (memcmp(config, "wifi.ssid ", 10) == 0) { StrHelper::strncpy(_prefs->wifi_ssid, &config[10], sizeof(_prefs->wifi_ssid)); @@ -890,77 +638,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch StrHelper::strncpy(_prefs->wifi_password, &config[9], sizeof(_prefs->wifi_password)); savePrefs(); strcpy(reply, "OK"); - } else if (memcmp(config, "timezone ", 9) == 0) { - StrHelper::strncpy(_prefs->timezone_string, &config[9], sizeof(_prefs->timezone_string)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "timezone.offset ", 16) == 0) { - int8_t offset = _atoi(&config[16]); - if (offset >= -12 && offset <= 14) { - _prefs->timezone_offset = offset; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: timezone offset must be between -12 and +14"); - } - } else if (memcmp(config, "mqtt.server ", 12) == 0) { - StrHelper::strncpy(_prefs->mqtt_server, &config[12], sizeof(_prefs->mqtt_server)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.port ", 10) == 0) { - int port = atoi(&config[10]); - if (port > 0 && port <= 65535) { - _prefs->mqtt_port = port; - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: port must be between 1 and 65535"); - } - } else if (memcmp(config, "mqtt.username ", 14) == 0) { - StrHelper::strncpy(_prefs->mqtt_username, &config[14], sizeof(_prefs->mqtt_username)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.password ", 14) == 0) { - StrHelper::strncpy(_prefs->mqtt_password, &config[14], sizeof(_prefs->mqtt_password)); - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.analyzer.us ", 17) == 0) { - _prefs->mqtt_analyzer_us_enabled = memcmp(&config[17], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.analyzer.eu ", 17) == 0) { - _prefs->mqtt_analyzer_eu_enabled = memcmp(&config[17], "on", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); - } else if (memcmp(config, "mqtt.owner ", 11) == 0) { - // Validate that it's a valid hex string of the correct length (64 hex chars = 32 bytes) - const char* owner_key = &config[11]; - int key_len = strlen(owner_key); - if (key_len == 64) { - // Validate hex characters - bool valid = true; - for (int i = 0; i < key_len; i++) { - if (!((owner_key[i] >= '0' && owner_key[i] <= '9') || - (owner_key[i] >= 'A' && owner_key[i] <= 'F') || - (owner_key[i] >= 'a' && owner_key[i] <= 'f'))) { - valid = false; - break; - } - } - if (valid) { - StrHelper::strncpy(_prefs->mqtt_owner_public_key, owner_key, sizeof(_prefs->mqtt_owner_public_key)); - savePrefs(); - strcpy(reply, "OK"); - } else { - strcpy(reply, "Error: invalid hex characters in public key"); - } - } else { - strcpy(reply, "Error: public key must be 64 hex characters (32 bytes)"); - } - } else if (memcmp(config, "mqtt.email ", 11) == 0) { - StrHelper::strncpy(_prefs->mqtt_email, &config[11], sizeof(_prefs->mqtt_email)); - savePrefs(); - strcpy(reply, "OK"); #endif } else { sprintf(reply, "unknown config: %s", config); @@ -1128,3 +805,170 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch strcpy(reply, "Unknown command"); } } + +#ifdef WITH_MQTT_BRIDGE +// Set default values for MQTT preferences (used when file doesn't exist or is corrupted) +static void setMQTTPrefsDefaults(MQTTPrefs* prefs) { + memset(prefs, 0, sizeof(MQTTPrefs)); + // Set sensible defaults matching MQTTBridge expectations + StrHelper::strncpy(prefs->mqtt_origin, "MeshCore-Repeater", sizeof(prefs->mqtt_origin)); + StrHelper::strncpy(prefs->mqtt_iata, "XXX", sizeof(prefs->mqtt_iata)); + prefs->mqtt_status_enabled = 1; + prefs->mqtt_packets_enabled = 1; + prefs->mqtt_raw_enabled = 0; + prefs->mqtt_tx_enabled = 0; + prefs->mqtt_status_interval = 300000; // 5 minutes + StrHelper::strncpy(prefs->wifi_ssid, "ssid_here", sizeof(prefs->wifi_ssid)); + StrHelper::strncpy(prefs->wifi_password, "password_here", sizeof(prefs->wifi_password)); + StrHelper::strncpy(prefs->timezone_string, "America/Los_Angeles", sizeof(prefs->timezone_string)); + prefs->timezone_offset = -8; + prefs->mqtt_analyzer_us_enabled = 1; + prefs->mqtt_analyzer_eu_enabled = 1; +} + +void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) { + // Initialize with zeros + memset(&_mqtt_prefs, 0, sizeof(_mqtt_prefs)); + + if (fs->exists("/mqtt_prefs")) { + // Load from separate MQTT prefs file +#if defined(RP2040_PLATFORM) + File file = fs->open("/mqtt_prefs", "r"); +#else + File file = fs->open("/mqtt_prefs"); +#endif + if (file) { + // Verify file size is correct before reading + if (file.size() >= sizeof(_mqtt_prefs)) { + size_t bytes_read = file.read((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); + if (bytes_read != sizeof(_mqtt_prefs)) { + // File read incomplete - reinitialize to defaults + setMQTTPrefsDefaults(&_mqtt_prefs); + } + } else { + // File too small - reinitialize to defaults + setMQTTPrefsDefaults(&_mqtt_prefs); + } + file.close(); + } else { + // File open failed - use defaults + setMQTTPrefsDefaults(&_mqtt_prefs); + } + } else { + // Migration: Try to read from old /com_prefs file if it exists + // This handles the case where MQTT settings were previously stored in /com_prefs + if (fs->exists("/com_prefs")) { +#if defined(RP2040_PLATFORM) + File file = fs->open("/com_prefs", "r"); +#else + File file = fs->open("/com_prefs"); +#endif + if (file) { + // Skip to MQTT section (after discovery_mod_timestamp) + // Calculate offset: we need to skip everything up to and including discovery_mod_timestamp + // Note: discovery_mod_timestamp was added in upstream dev branch + size_t offset_to_mqtt = + sizeof(_prefs->airtime_factor) + sizeof(_prefs->node_name) + 4 + // pad + sizeof(_prefs->node_lat) + sizeof(_prefs->node_lon) + + sizeof(_prefs->password) + sizeof(_prefs->freq) + + sizeof(_prefs->tx_power_dbm) + sizeof(_prefs->disable_fwd) + + sizeof(_prefs->advert_interval) + 1 + // pad + sizeof(_prefs->rx_delay_base) + sizeof(_prefs->tx_delay_factor) + + sizeof(_prefs->guest_password) + sizeof(_prefs->direct_tx_delay_factor) + 4 + // pad + sizeof(_prefs->sf) + sizeof(_prefs->cr) + + sizeof(_prefs->allow_read_only) + sizeof(_prefs->multi_acks) + + sizeof(_prefs->bw) + sizeof(_prefs->agc_reset_interval) + 3 + // pad + sizeof(_prefs->flood_max) + sizeof(_prefs->flood_advert_interval) + + sizeof(_prefs->interference_threshold) + sizeof(_prefs->bridge_enabled) + + sizeof(_prefs->bridge_delay) + sizeof(_prefs->bridge_pkt_src) + + sizeof(_prefs->bridge_baud) + sizeof(_prefs->bridge_channel) + + sizeof(_prefs->bridge_secret) + 4 + // pad + sizeof(_prefs->gps_enabled) + sizeof(_prefs->gps_interval) + + sizeof(_prefs->advert_loc_policy) + + sizeof(_prefs->discovery_mod_timestamp); // Added in upstream dev branch + + // Check if file is large enough and seek succeeded + if (file.size() >= offset_to_mqtt + sizeof(_mqtt_prefs)) { + if (file.seek(offset_to_mqtt)) { + size_t bytes_read = file.read((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); + if (bytes_read == sizeof(_mqtt_prefs)) { + // Successfully migrated - save to new location for future use + file.close(); + saveMQTTPrefs(fs); + return; // Migration successful + } + } + } + file.close(); + // Migration failed - defaults already set, just return + return; + } + } + // No file exists and migration didn't happen - defaults already set + setMQTTPrefsDefaults(&_mqtt_prefs); + } +} + +void CommonCLI::saveMQTTPrefs(FILESYSTEM* fs) { +#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) + fs->remove("/mqtt_prefs"); + File file = fs->open("/mqtt_prefs", FILE_O_WRITE); +#elif defined(RP2040_PLATFORM) + File file = fs->open("/mqtt_prefs", "w"); +#else + File file = fs->open("/mqtt_prefs", "w", true); +#endif + if (file) { + file.write((uint8_t *)&_mqtt_prefs, sizeof(_mqtt_prefs)); + file.close(); + } +} + +void CommonCLI::syncMQTTPrefsToNodePrefs() { + // Copy MQTT prefs to NodePrefs so existing code can access them + // Use StrHelper::strncpy to ensure proper null termination + StrHelper::strncpy(_prefs->mqtt_origin, _mqtt_prefs.mqtt_origin, sizeof(_prefs->mqtt_origin)); + StrHelper::strncpy(_prefs->mqtt_iata, _mqtt_prefs.mqtt_iata, sizeof(_prefs->mqtt_iata)); + _prefs->mqtt_status_enabled = _mqtt_prefs.mqtt_status_enabled; + _prefs->mqtt_packets_enabled = _mqtt_prefs.mqtt_packets_enabled; + _prefs->mqtt_raw_enabled = _mqtt_prefs.mqtt_raw_enabled; + _prefs->mqtt_tx_enabled = _mqtt_prefs.mqtt_tx_enabled; + _prefs->mqtt_status_interval = _mqtt_prefs.mqtt_status_interval; + StrHelper::strncpy(_prefs->wifi_ssid, _mqtt_prefs.wifi_ssid, sizeof(_prefs->wifi_ssid)); + StrHelper::strncpy(_prefs->wifi_password, _mqtt_prefs.wifi_password, sizeof(_prefs->wifi_password)); + StrHelper::strncpy(_prefs->timezone_string, _mqtt_prefs.timezone_string, sizeof(_prefs->timezone_string)); + _prefs->timezone_offset = _mqtt_prefs.timezone_offset; + StrHelper::strncpy(_prefs->mqtt_server, _mqtt_prefs.mqtt_server, sizeof(_prefs->mqtt_server)); + _prefs->mqtt_port = _mqtt_prefs.mqtt_port; + StrHelper::strncpy(_prefs->mqtt_username, _mqtt_prefs.mqtt_username, sizeof(_prefs->mqtt_username)); + StrHelper::strncpy(_prefs->mqtt_password, _mqtt_prefs.mqtt_password, sizeof(_prefs->mqtt_password)); + _prefs->mqtt_analyzer_us_enabled = _mqtt_prefs.mqtt_analyzer_us_enabled; + _prefs->mqtt_analyzer_eu_enabled = _mqtt_prefs.mqtt_analyzer_eu_enabled; + StrHelper::strncpy(_prefs->mqtt_owner_public_key, _mqtt_prefs.mqtt_owner_public_key, sizeof(_prefs->mqtt_owner_public_key)); + StrHelper::strncpy(_prefs->mqtt_email, _mqtt_prefs.mqtt_email, sizeof(_prefs->mqtt_email)); +} + +void CommonCLI::syncNodePrefsToMQTTPrefs() { + // Copy NodePrefs to MQTT prefs (used when saving after changes via CLI) + // Use StrHelper::strncpy to ensure proper null termination + StrHelper::strncpy(_mqtt_prefs.mqtt_origin, _prefs->mqtt_origin, sizeof(_mqtt_prefs.mqtt_origin)); + StrHelper::strncpy(_mqtt_prefs.mqtt_iata, _prefs->mqtt_iata, sizeof(_mqtt_prefs.mqtt_iata)); + _mqtt_prefs.mqtt_status_enabled = _prefs->mqtt_status_enabled; + _mqtt_prefs.mqtt_packets_enabled = _prefs->mqtt_packets_enabled; + _mqtt_prefs.mqtt_raw_enabled = _prefs->mqtt_raw_enabled; + _mqtt_prefs.mqtt_tx_enabled = _prefs->mqtt_tx_enabled; + _mqtt_prefs.mqtt_status_interval = _prefs->mqtt_status_interval; + StrHelper::strncpy(_mqtt_prefs.wifi_ssid, _prefs->wifi_ssid, sizeof(_mqtt_prefs.wifi_ssid)); + StrHelper::strncpy(_mqtt_prefs.wifi_password, _prefs->wifi_password, sizeof(_mqtt_prefs.wifi_password)); + StrHelper::strncpy(_mqtt_prefs.timezone_string, _prefs->timezone_string, sizeof(_mqtt_prefs.timezone_string)); + _mqtt_prefs.timezone_offset = _prefs->timezone_offset; + StrHelper::strncpy(_mqtt_prefs.mqtt_server, _prefs->mqtt_server, sizeof(_mqtt_prefs.mqtt_server)); + _mqtt_prefs.mqtt_port = _prefs->mqtt_port; + StrHelper::strncpy(_mqtt_prefs.mqtt_username, _prefs->mqtt_username, sizeof(_mqtt_prefs.mqtt_username)); + StrHelper::strncpy(_mqtt_prefs.mqtt_password, _prefs->mqtt_password, sizeof(_mqtt_prefs.mqtt_password)); + _mqtt_prefs.mqtt_analyzer_us_enabled = _prefs->mqtt_analyzer_us_enabled; + _mqtt_prefs.mqtt_analyzer_eu_enabled = _prefs->mqtt_analyzer_eu_enabled; + StrHelper::strncpy(_mqtt_prefs.mqtt_owner_public_key, _prefs->mqtt_owner_public_key, sizeof(_mqtt_prefs.mqtt_owner_public_key)); + StrHelper::strncpy(_mqtt_prefs.mqtt_email, _prefs->mqtt_email, sizeof(_mqtt_prefs.mqtt_email)); +} +#endif diff --git a/src/helpers/MQTTMessageBuilder.cpp b/src/helpers/MQTTMessageBuilder.cpp index b85e4a68..7f4e390f 100644 --- a/src/helpers/MQTTMessageBuilder.cpp +++ b/src/helpers/MQTTMessageBuilder.cpp @@ -1,8 +1,6 @@ #include "MQTTMessageBuilder.h" #include #include -#include -#include "MeshCore.h" int MQTTMessageBuilder::buildStatusMessage( const char* origin, @@ -14,16 +12,9 @@ int MQTTMessageBuilder::buildStatusMessage( const char* status, const char* timestamp, char* buffer, - size_t buffer_size, - int battery_mv, - int uptime_secs, - int errors, - int queue_len, - int noise_floor, - int tx_air_secs, - int rx_air_secs + size_t buffer_size ) { - DynamicJsonDocument doc(768); // Increased size to accommodate stats + DynamicJsonDocument doc(512); JsonObject root = doc.to(); root["status"] = status; @@ -35,34 +26,6 @@ int MQTTMessageBuilder::buildStatusMessage( root["radio"] = radio; root["client_version"] = client_version; - // Add stats object if any stats are provided - if (battery_mv >= 0 || uptime_secs >= 0 || errors >= 0 || queue_len >= 0 || - noise_floor > -999 || tx_air_secs >= 0 || rx_air_secs >= 0) { - JsonObject stats = root.createNestedObject("stats"); - - if (battery_mv >= 0) { - stats["battery_mv"] = battery_mv; - } - if (uptime_secs >= 0) { - stats["uptime_secs"] = uptime_secs; - } - if (errors >= 0) { - stats["errors"] = errors; - } - if (queue_len >= 0) { - stats["queue_len"] = queue_len; - } - if (noise_floor > -999) { - stats["noise_floor"] = noise_floor; - } - if (tx_air_secs >= 0) { - stats["tx_air_secs"] = tx_air_secs; - } - if (rx_air_secs >= 0) { - stats["rx_air_secs"] = rx_air_secs; - } - } - size_t len = serializeJson(root, buffer, buffer_size); return (len > 0 && len < buffer_size) ? len : 0; } @@ -86,15 +49,7 @@ int MQTTMessageBuilder::buildPacketMessage( char* buffer, size_t buffer_size ) { - // Size-adaptive JSON document: estimate needed size based on raw hex string length - // Base JSON overhead ~200 bytes, raw hex can be up to 510 chars (255 bytes packet) - // Use minimum needed to reduce memory fragmentation - size_t raw_len = raw ? strlen(raw) : 0; - size_t doc_size = 256 + raw_len + 128; // Base + raw + overhead, but cap at buffer_size - if (doc_size > buffer_size) doc_size = buffer_size; - if (doc_size < 512) doc_size = 512; // Minimum for small packets - if (doc_size > 2048) doc_size = 2048; // Maximum cap - DynamicJsonDocument doc(doc_size); + DynamicJsonDocument doc(1024); JsonObject root = doc.to(); root["origin"] = origin; @@ -147,55 +102,45 @@ int MQTTMessageBuilder::buildPacketJSON( bool is_tx, const char* origin, const char* origin_id, - Timezone* timezone, char* buffer, size_t buffer_size ) { if (!packet) return 0; - // Get current device time (should be UTC since system timezone is set to UTC) + // Get current device time time_t now = time(nullptr); + struct tm* timeinfo = localtime(&now); - // Convert to local time using timezone library (for timestamp field only) - time_t local_time = timezone ? timezone->toLocal(now) : now; - struct tm* local_timeinfo = localtime(&local_time); - - // Format timestamp in ISO 8601 format (LOCAL TIME) + // Format timestamp in ISO 8601 format char timestamp[32]; - if (local_timeinfo) { - strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", local_timeinfo); + if (timeinfo) { + strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", timeinfo); } else { strcpy(timestamp, "2024-01-01T12:00:00.000000"); } - // Get UTC time (since system timezone is UTC, time() returns UTC) - struct tm* utc_timeinfo = gmtime(&now); - - // Format time and date (ALWAYS UTC) + // Format time and date char time_str[16]; char date_str[16]; - if (utc_timeinfo) { - strftime(time_str, sizeof(time_str), "%H:%M:%S", utc_timeinfo); - strftime(date_str, sizeof(date_str), "%d/%m/%Y", utc_timeinfo); + if (timeinfo) { + strftime(time_str, sizeof(time_str), "%H:%M:%S", timeinfo); + strftime(date_str, sizeof(date_str), "%d/%m/%Y", timeinfo); } else { strcpy(time_str, "12:00:00"); strcpy(date_str, "01/01/2024"); } // Convert packet to hex - // MAX_TRANS_UNIT is 255 bytes, hex = 510 chars, but allow for larger with headers - char raw_hex[1024]; + char raw_hex[512]; packetToHex(packet, raw_hex, sizeof(raw_hex)); // Get packet characteristics int packet_type = packet->getPayloadType(); const char* route_str = getRouteTypeString(packet->isRouteDirect() ? 1 : 0); - // Create proper packet hash using MeshCore's calculatePacketHash method + // Create hash (simplified - use first 8 bytes of packet) char hash_str[17]; - uint8_t packet_hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(packet_hash); - bytesToHex(packet_hash, MAX_HASH_SIZE, hash_str, sizeof(hash_str)); + bytesToHex(packet->payload, min(8, (int)packet->payload_len), hash_str, sizeof(hash_str)); // Build path string for direct packets char path_str[128] = ""; @@ -220,93 +165,10 @@ int MQTTMessageBuilder::buildPacketJSON( ); } -int MQTTMessageBuilder::buildPacketJSONFromRaw( - const uint8_t* raw_data, - int raw_len, - mesh::Packet* packet, - bool is_tx, - const char* origin, - const char* origin_id, - float snr, - float rssi, - Timezone* timezone, - char* buffer, - size_t buffer_size -) { - if (!packet || !raw_data || raw_len <= 0) return 0; - - // Get current device time (should be UTC since system timezone is set to UTC) - time_t now = time(nullptr); - - // Convert to local time using timezone library (for timestamp field only) - time_t local_time = timezone ? timezone->toLocal(now) : now; - struct tm* local_timeinfo = localtime(&local_time); - - // Format timestamp in ISO 8601 format (LOCAL TIME) - char timestamp[32]; - if (local_timeinfo) { - strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", local_timeinfo); - } else { - strcpy(timestamp, "2024-01-01T12:00:00.000000"); - } - - // Get UTC time (since system timezone is UTC, time() returns UTC) - struct tm* utc_timeinfo = gmtime(&now); - - // Format time and date (ALWAYS UTC) - char time_str[16]; - char date_str[16]; - if (utc_timeinfo) { - strftime(time_str, sizeof(time_str), "%H:%M:%S", utc_timeinfo); - strftime(date_str, sizeof(date_str), "%d/%m/%Y", utc_timeinfo); - } else { - strcpy(time_str, "12:00:00"); - strcpy(date_str, "01/01/2024"); - } - - // Convert raw radio data to hex (this includes radio headers) - // MAX_TRANS_UNIT is 255 bytes, hex = 510 chars, but allow for larger with headers - char raw_hex[1024]; - bytesToHex(raw_data, raw_len, raw_hex, sizeof(raw_hex)); - - // Get packet characteristics from the parsed packet - int packet_type = packet->getPayloadType(); - const char* route_str = getRouteTypeString(packet->isRouteDirect() ? 1 : 0); - - // Create proper packet hash using MeshCore's calculatePacketHash method - char hash_str[17]; - uint8_t packet_hash[MAX_HASH_SIZE]; - packet->calculatePacketHash(packet_hash); - bytesToHex(packet_hash, MAX_HASH_SIZE, hash_str, sizeof(hash_str)); - - // Build path string for direct packets - char path_str[128] = ""; - if (packet->isRouteDirect() && packet->path_len > 0) { - // Simplified path representation - snprintf(path_str, sizeof(path_str), "path_len_%d", packet->path_len); - } - - return buildPacketMessage( - origin, origin_id, timestamp, - is_tx ? "tx" : "rx", - time_str, date_str, - raw_len, // Use actual raw radio data length - packet_type, route_str, - packet->payload_len, - raw_hex, - snr, // Use actual SNR from radio - rssi, // Use actual RSSI from radio - hash_str, - packet->isRouteDirect() ? path_str : nullptr, - buffer, buffer_size - ); -} - int MQTTMessageBuilder::buildRawJSON( mesh::Packet* packet, const char* origin, const char* origin_id, - Timezone* timezone, char* buffer, size_t buffer_size ) { @@ -314,10 +176,7 @@ int MQTTMessageBuilder::buildRawJSON( // Get current device time time_t now = time(nullptr); - - // Convert to local time using timezone library - time_t local_time = timezone ? timezone->toLocal(now) : now; - struct tm* timeinfo = localtime(&local_time); + struct tm* timeinfo = localtime(&now); // Format timestamp in ISO 8601 format char timestamp[32]; @@ -328,8 +187,7 @@ int MQTTMessageBuilder::buildRawJSON( } // Convert packet to hex - // MAX_TRANS_UNIT is 255, so max hex size is 510 chars + null = 511 bytes - char raw_hex[1024]; + char raw_hex[512]; packetToHex(packet, raw_hex, sizeof(raw_hex)); return buildRawMessage(origin, origin_id, timestamp, raw_hex, buffer, buffer_size); @@ -391,15 +249,21 @@ void MQTTMessageBuilder::bytesToHex(const uint8_t* data, size_t len, char* hex, } void MQTTMessageBuilder::packetToHex(mesh::Packet* packet, char* hex, size_t hex_size) { - // Serialize full on-air/wire format using Packet::writeTo() - // This includes header, transport codes (if present), path_len, path, and payload - uint8_t raw_buf[512]; - uint8_t raw_len = packet->writeTo(raw_buf); - if (raw_len == 0 || raw_len > sizeof(raw_buf)) return; + // Convert entire packet to hex string + size_t total_len = packet->path_len + packet->payload_len + 2; + if (hex_size < total_len * 2 + 1) return; - // Check if hex buffer is large enough (2 hex chars per byte + null terminator) - if (hex_size < (size_t)raw_len * 2 + 1) return; + size_t offset = 0; - // Convert serialized packet to hex - bytesToHex(raw_buf, raw_len, hex, hex_size); + // Add path data + if (packet->path_len > 0) { + bytesToHex(packet->path, packet->path_len, hex + offset, hex_size - offset); + offset += packet->path_len * 2; + } + + // Add payload data + if (packet->payload_len > 0) { + bytesToHex(packet->payload, packet->payload_len, hex + offset, hex_size - offset); + offset += packet->payload_len * 2; + } } \ No newline at end of file diff --git a/src/helpers/MQTTMessageBuilder.h b/src/helpers/MQTTMessageBuilder.h index 9106e20c..490e32b9 100644 --- a/src/helpers/MQTTMessageBuilder.h +++ b/src/helpers/MQTTMessageBuilder.h @@ -3,7 +3,6 @@ #include "MeshCore.h" #include #include -#include /** * @brief Utility class for building MQTT JSON messages @@ -30,13 +29,6 @@ public: * @param timestamp ISO 8601 timestamp * @param buffer Output buffer for JSON string * @param buffer_size Size of output buffer - * @param battery_mv Battery voltage in millivolts (optional, -1 to omit) - * @param uptime_secs Uptime in seconds (optional, -1 to omit) - * @param errors Error flags (optional, -1 to omit) - * @param queue_len Queue length (optional, -1 to omit) - * @param noise_floor Noise floor in dBm (optional, -999 to omit) - * @param tx_air_secs TX air time in seconds (optional, -1 to omit) - * @param rx_air_secs RX air time in seconds (optional, -1 to omit) * @return Length of JSON string, or 0 on error */ static int buildStatusMessage( @@ -49,14 +41,7 @@ public: const char* status, const char* timestamp, char* buffer, - size_t buffer_size, - int battery_mv = -1, - int uptime_secs = -1, - int errors = -1, - int queue_len = -1, - int noise_floor = -999, - int tx_air_secs = -1, - int rx_air_secs = -1 + size_t buffer_size ); /** @@ -137,21 +122,6 @@ public: bool is_tx, const char* origin, const char* origin_id, - Timezone* timezone, - char* buffer, - size_t buffer_size - ); - - static int buildPacketJSONFromRaw( - const uint8_t* raw_data, - int raw_len, - mesh::Packet* packet, - bool is_tx, - const char* origin, - const char* origin_id, - float snr, - float rssi, - Timezone* timezone, char* buffer, size_t buffer_size ); @@ -170,7 +140,6 @@ public: mesh::Packet* packet, const char* origin, const char* origin_id, - Timezone* timezone, char* buffer, size_t buffer_size ); diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index c193b81f..c4e8e9e9 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -2,98 +2,27 @@ #include "../MQTTMessageBuilder.h" #include #include -#include - -#ifdef ESP_PLATFORM -#include -#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), +MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc) + : BridgeBase(prefs, mgr, rtc), _mqtt_client(nullptr), _wifi_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_no_broker_log(0), _dispatcher(nullptr), _radio(nullptr), _board(nullptr), _ms(nullptr) { + _last_status_publish(0), _status_interval(300000), // 5 minutes default + _ntp_client(_ntp_udp, "pool.ntp.org", 0, 60000), _last_ntp_sync(0), _ntp_synced(false) { // Initialize default values strncpy(_origin, "MeshCore-Repeater", sizeof(_origin) - 1); - strncpy(_iata, "XXX", sizeof(_iata) - 1); + strncpy(_iata, "SEA", sizeof(_iata) - 1); strncpy(_device_id, "DEVICE_ID_PLACEHOLDER", sizeof(_device_id) - 1); - strncpy(_firmware_version, "unknown", sizeof(_firmware_version) - 1); + strncpy(_firmware_version, "v1.9.1", 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(); @@ -102,64 +31,45 @@ MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCCloc 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)); + size_t origin_len = strlen(_origin); + if (origin_len > 0) { + if (_origin[0] == '"') { + memmove(_origin, _origin + 1, origin_len); + origin_len--; + } + if (origin_len > 0 && _origin[origin_len-1] == '"') { + _origin[origin_len-1] = '\0'; + } + } 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)); + size_t iata_len = strlen(_iata); + if (iata_len > 0) { + if (_iata[0] == '"') { + memmove(_iata, _iata + 1, iata_len); + iata_len--; + } + if (iata_len > 0 && _iata[iata_len-1] == '"') { + _iata[iata_len-1] = '\0'; + } + } MQTT_DEBUG_PRINTLN("IATA after stripping: '%s'", _iata); // 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); + _status_interval = _prefs->mqtt_status_interval; MQTT_DEBUG_PRINTLN("Origin: %s, IATA: %s", _origin, _iata); MQTT_DEBUG_PRINTLN("Device ID: %s", _device_id); @@ -181,21 +91,6 @@ void MQTTBridge::begin() { 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 - // Enable WiFi power save mode to reduce power consumption - esp_err_t ps_result = esp_wifi_set_ps(WIFI_PS_MIN_MODEM); - if (ps_result == ESP_OK) { - MQTT_DEBUG_PRINTLN("WiFi power save mode enabled"); - } else { - MQTT_DEBUG_PRINTLN("Failed to enable WiFi power save mode: %d", ps_result); - } - - // Set WiFi TX power to reasonable level for MQTT connectivity - WiFi.setTxPower(WIFI_POWER_11dBm); - MQTT_DEBUG_PRINTLN("WiFi TX power set to 11dBm"); - #endif - // Sync time with NTP syncTimeWithNTP(); } else { @@ -203,44 +98,12 @@ void MQTTBridge::begin() { return; } - // Initialize PsychicMqttClient - _mqtt_client = new PsychicMqttClient(); + // Initialize WiFi client + _wifi_client = new WiFiClient(); + _mqtt_client = new PubSubClient(*_wifi_client); - // 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(); + // Set default broker (meshtastic.pugetmesh.org) + setBroker(0, "meshtastic.pugetmesh.org", 1883, "meshdev", "large4cats", true); // Connect to brokers connectToBrokers(); @@ -260,97 +123,31 @@ void MQTTBridge::end() { } } - // 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; } + if (_wifi_client) { + delete _wifi_client; + _wifi_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; -} - -bool MQTTBridge::isReady() const { - return _initialized && isWiFiConfigValid(_prefs); -} - void MQTTBridge::loop() { if (!_initialized) return; // Maintain broker connections connectToBrokers(); - // Maintain analyzer server connections - maintainAnalyzerConnections(); - // Process packet queue processPacketQueue(); @@ -359,174 +156,28 @@ void MQTTBridge::loop() { 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 elapsed = (millis() >= _last_status_publish) ? - (millis() - _last_status_publish) : - (ULONG_MAX - _last_status_publish + millis() + 1); - MQTT_DEBUG_PRINTLN("Memory check: Status timer - elapsed: %lu ms, interval: %lu ms, next: %lu ms", - elapsed, _status_interval, _status_interval - elapsed); - } - 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(); + // Publish status updates + if (_status_enabled && millis() - _last_status_publish > _status_interval) { + publishStatus(); + _last_status_publish = 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; - } - - // 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)); - } + if (!_initialized || !_packets_enabled) return; // Queue packet for transmission queuePacket(packet, false); } void MQTTBridge::sendPacket(mesh::Packet *packet) { - if (!_initialized || !_packets_enabled || !_tx_enabled) return; + if (!_initialized || !_packets_enabled) return; - // Queue packet for transmission (only if TX enabled) + // Queue packet for transmission 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; @@ -536,32 +187,42 @@ void MQTTBridge::connectToBrokers() { MQTT_DEBUG_PRINTLN("Connecting to broker %d: %s:%d", i, _brokers[i].host, _brokers[i].port); + // Set broker for this connection + _mqtt_client->setServer(_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); + // Attempt connection + bool connected = _mqtt_client->connect( + client_id, + _brokers[i].username, + _brokers[i].password + ); - // Set credentials if provided - if (strlen(_brokers[i].username) > 0) { - _mqtt_client->setCredentials(_brokers[i].username, _brokers[i].password); + if (connected) { + _brokers[i].connected = true; + _brokers[i].reconnect_interval = 5000; // Reset to 5 seconds + _active_brokers++; + MQTT_DEBUG_PRINTLN("Connected to broker %d", i); + + // Publish initial status + if (_status_enabled) { + publishStatus(); + } + } else { + _brokers[i].connected = false; + _brokers[i].last_attempt = millis(); + // Exponential backoff: 5s, 10s, 20s, 30s max + _brokers[i].reconnect_interval = min(30000UL, _brokers[i].reconnect_interval * 2); + MQTT_DEBUG_PRINTLN("Failed to connect to broker %d", i); } - - // 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 if (_brokers[i].connected) { - // PsychicMqttClient handles connection maintenance internally - // TODO: Implement proper connection state checking with callbacks + _mqtt_client->loop(); if (!_mqtt_client->connected()) { _brokers[i].connected = false; _active_brokers--; @@ -572,92 +233,31 @@ void MQTTBridge::connectToBrokers() { } void MQTTBridge::processPacketQueue() { - if (_queue_count == 0) { - return; - } + if (_queue_count == 0 || !isAnyBrokerConnected()) 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); - - // Process up to MAX_QUEUE_SIZE packets per loop to keep up with packet arrival rate + // Process up to 5 packets per loop to avoid blocking int processed = 0; - int max_per_loop = MAX_QUEUE_SIZE; // Process all queued packets per loop - while (_queue_count > 0 && processed < max_per_loop) { + while (_queue_count > 0 && processed < 5) { 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 packet + publishPacket(queued.packet, queued.is_tx); // 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++; } } -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; - } +void MQTTBridge::publishStatus() { + if (!isAnyBrokerConnected()) return; - // 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 json_buffer[512]; char origin_id[65]; char timestamp[32]; char radio_info[64]; @@ -678,218 +278,69 @@ bool MQTTBridge::publishStatus() { 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 + // Build status message int len = MQTTMessageBuilder::buildStatusMessage( _origin, origin_id, _board_model, // model - now dynamic! _firmware_version, // firmware version radio_info, - client_version, // client version + "custom-mqtt-bridge", "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 + sizeof(json_buffer) ); - if (len > 0) { - bool published = false; - - // Publish to all connected custom brokers - if (_config_valid) { - for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) { - if (_brokers[i].enabled && _brokers[i].connected) { - char topic[128]; - snprintf(topic, sizeof(topic), "meshcore/%s/%s/status", _iata, _device_id); - MQTT_DEBUG_PRINTLN("Publishing status to topic: %s", topic); - - // Set broker for this connection (PsychicMqttClient uses URI format) - 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 (_mqtt_client->publish(topic, 1, true, json_buffer, strlen(json_buffer)) > 0) { - published = true; - } - } - } - } - - // Always publish to Let's Mesh Analyzer servers if enabled and connected - if (has_analyzer_servers) { - char analyzer_topic[128]; - snprintf(analyzer_topic, sizeof(analyzer_topic), "meshcore/%s/%s/status", _iata, _device_id); - - // Try to publish to analyzer servers - bool analyzer_published = false; - if (_analyzer_us_enabled && _analyzer_us_client && _analyzer_us_client->connected()) { - _analyzer_us_client->publish(analyzer_topic, 1, true, json_buffer, strlen(json_buffer)); - analyzer_published = true; - MQTT_DEBUG_PRINTLN("Published status to US analyzer server"); - } - if (_analyzer_eu_enabled && _analyzer_eu_client && _analyzer_eu_client->connected()) { - _analyzer_eu_client->publish(analyzer_topic, 1, true, json_buffer, strlen(json_buffer)); - analyzer_published = true; - MQTT_DEBUG_PRINTLN("Published status to EU analyzer server"); - } - - if (analyzer_published) { - published = true; - } - } - - // 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 + if (len > 0) { + // Publish to all connected brokers + for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) { + if (_brokers[i].enabled && _brokers[i].connected) { + char topic[128]; + snprintf(topic, sizeof(topic), "meshcore/%s/%s/status", _iata, _device_id); + MQTT_DEBUG_PRINTLN("Publishing status to topic: %s", topic); + + _mqtt_client->setServer(_brokers[i].host, _brokers[i].port); + _mqtt_client->publish(topic, json_buffer, true); // retained + } + } + } } -void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx, - const uint8_t* raw_data, int raw_len, - float snr, float rssi) { +void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx) { 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; - } - - // 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 json_buffer[1024]; 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 - ); - } + // Build packet message + int len = MQTTMessageBuilder::buildPacketJSON( + packet, is_tx, _origin, origin_id, json_buffer, sizeof(json_buffer) + ); if (len > 0) { - // Publish to custom brokers (only if config is valid) - if (_config_valid) { - for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) { - if (_brokers[i].enabled && _brokers[i].connected) { - // Double-check that the client is actually connected before publishing - // This prevents race conditions where onConnect fires but connection isn't ready yet - if (!_mqtt_client || !_mqtt_client->connected()) { - // Connection state is out of sync - mark broker as disconnected - _brokers[i].connected = false; - _active_brokers--; - continue; - } - - char topic[128]; - snprintf(topic, sizeof(topic), "meshcore/%s/%s/packets", _iata, _device_id); - // MQTT_DEBUG_PRINTLN("Publishing packet to topic: %s", topic); - - // Set broker for this connection (PsychicMqttClient uses URI format) - char broker_uri[128]; - snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port); - _mqtt_client->setServer(broker_uri); - _mqtt_client->publish(topic, 1, false, json_buffer, strlen(json_buffer)); // qos=1, retained=false - } + // Publish to all connected brokers + for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) { + if (_brokers[i].enabled && _brokers[i].connected) { + char topic[128]; + snprintf(topic, sizeof(topic), "meshcore/%s/%s/packets", _iata, _device_id); + MQTT_DEBUG_PRINTLN("Publishing packet to topic: %s", topic); + + _mqtt_client->setServer(_brokers[i].host, _brokers[i].port); + _mqtt_client->publish(topic, json_buffer); } } - - // Always publish to Let's Mesh Analyzer servers (independent of custom broker config) - char analyzer_topic[128]; - snprintf(analyzer_topic, sizeof(analyzer_topic), "meshcore/%s/%s/packets", _iata, _device_id); - publishToAnalyzerServers(analyzer_topic, json_buffer, false); - } 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 json_buffer[512]; char origin_id[65]; // Use actual device ID @@ -898,65 +349,33 @@ void MQTTBridge::publishRaw(mesh::Packet* packet) { // Build raw message int len = MQTTMessageBuilder::buildRawJSON( - packet, _origin, origin_id, _timezone, json_buffer, sizeof(json_buffer) + packet, _origin, origin_id, json_buffer, sizeof(json_buffer) ); if (len > 0) { - // Publish to custom brokers (only if config is valid) - if (_config_valid) { - for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) { - if (_brokers[i].enabled && _brokers[i].connected) { - char topic[128]; - snprintf(topic, sizeof(topic), "meshcore/%s/%s/raw", _iata, _device_id); - - // Set broker for this connection (PsychicMqttClient uses URI format) - char broker_uri[128]; - snprintf(broker_uri, sizeof(broker_uri), "mqtt://%s:%d", _brokers[i].host, _brokers[i].port); - _mqtt_client->setServer(broker_uri); - _mqtt_client->publish(topic, 1, false, json_buffer, strlen(json_buffer)); // qos=1, retained=false - } + // Publish to all connected brokers + for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) { + if (_brokers[i].enabled && _brokers[i].connected) { + char topic[128]; + snprintf(topic, sizeof(topic), "meshcore/%s/%s/raw", _iata, _device_id); + + _mqtt_client->setServer(_brokers[i].host, _brokers[i].port); + _mqtt_client->publish(topic, json_buffer); } } - - // Always publish to Let's Mesh Analyzer servers (independent of custom broker config) - char analyzer_topic[128]; - snprintf(analyzer_topic, sizeof(analyzer_topic), "meshcore/%s/%s/raw", _iata, _device_id); - publishToAnalyzerServers(analyzer_topic, json_buffer, false); } } 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 + // Queue full, remove oldest 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 - - // Capture current raw radio data if available (within 1 second window) - if (_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++; @@ -965,11 +384,6 @@ void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) { 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--; } @@ -1034,606 +448,6 @@ void MQTTBridge::setBoardModel(const char* board_model) { _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 - _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"); - } - } -} - -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 - - // 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; - } else { - MQTT_DEBUG_PRINTLN("Failed to create auth token for US server"); - } - } - - // 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; - } else { - MQTT_DEBUG_PRINTLN("Failed to create auth token for EU server"); - } - } - - 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() { - if (!_analyzer_us_enabled && !_analyzer_eu_enabled) { - MQTT_DEBUG_PRINTLN("No analyzer servers enabled, skipping PsychicMqttClient setup"); - return; - } - - MQTT_DEBUG_PRINTLN("Setting up PsychicMqttClient WebSocket clients..."); - - // Setup US server client - if (_analyzer_us_enabled) { - _analyzer_us_client = new PsychicMqttClient(); - - // 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", - error.error_type, error.connect_return_code); - }); - - // 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); - - // Connect to US server (async connection) - _analyzer_us_client->connect(); - MQTT_DEBUG_PRINTLN("Initiating connection to Let's Mesh US server"); - } - - // Setup EU server client - if (_analyzer_eu_enabled) { - _analyzer_eu_client = new PsychicMqttClient(); - - // 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", - error.error_type, error.connect_return_code); - }); - - // 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); - - // Connect to EU server (async connection) - _analyzer_eu_client->connect(); - MQTT_DEBUG_PRINTLN("Initiating connection to Let's Mesh EU server"); - } -} - -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; - } - - 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 - // If time wasn't synced when token was created, expiration time will be invalid, so renew - bool token_needs_renewal = (_token_us_expires_at == 0) || - !time_synced || - !(_token_us_expires_at >= 1000000000) || // Expiration time invalid - (time_synced && current_time >= _token_us_expires_at) || - (time_synced && 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 - _token_us_expires_at = current_time + expires_in; - MQTT_DEBUG_PRINTLN("US token renewed, new expiration: %lu", _token_us_expires_at); - - // 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 - // If time wasn't synced when token was created, expiration time will be invalid, so renew - bool token_needs_renewal = (_token_eu_expires_at == 0) || - !time_synced || - !(_token_eu_expires_at >= 1000000000) || // Expiration time invalid - (time_synced && current_time >= _token_eu_expires_at) || - (time_synced && 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 - _token_eu_expires_at = current_time + expires_in; - MQTT_DEBUG_PRINTLN("EU token renewed, new expiration: %lu", _token_eu_expires_at); - - // 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; @@ -1654,14 +468,6 @@ 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"); @@ -1677,11 +483,7 @@ void MQTTBridge::syncTimeWithNTP() { 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 + // Update the device's RTC clock if (_rtc) { _rtc->setCurrentTime(epochTime); _ntp_synced = true; @@ -1689,57 +491,12 @@ void MQTTBridge::syncTimeWithNTP() { MQTT_DEBUG_PRINTLN("Time synced: %lu", epochTime); - // 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); + // Show current time + struct tm* timeinfo = localtime((time_t*)&epochTime); + if (timeinfo) { + MQTT_DEBUG_PRINTLN("Current time: %04d-%02d-%02d %02d:%02d:%02d UTC", + timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday, + timeinfo->tm_hour, timeinfo->tm_min, timeinfo->tm_sec); } } else { MQTT_DEBUG_PRINTLN("No RTC clock available for time sync"); @@ -1751,145 +508,4 @@ void MQTTBridge::syncTimeWithNTP() { _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::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 diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 3c044e63..2396c1ef 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -2,12 +2,10 @@ #include "MeshCore.h" #include "helpers/bridges/BridgeBase.h" -#include +#include #include #include #include -#include -#include "helpers/JWTHelper.h" #if defined(MQTT_DEBUG) && defined(ARDUINO) #include @@ -45,7 +43,8 @@ */ class MQTTBridge : public BridgeBase { private: - PsychicMqttClient* _mqtt_client; + PubSubClient* _mqtt_client; + WiFiClient* _wifi_client; // MQTT broker configuration struct MQTTBroker { @@ -71,11 +70,9 @@ private: char _device_id[65]; // Device public key (hex string) char _firmware_version[64]; // Firmware version string char _board_model[64]; // Board model string - char _build_date[32]; // Build date string bool _status_enabled; bool _packets_enabled; bool _raw_enabled; - bool _tx_enabled; unsigned long _last_status_publish; unsigned long _status_interval; @@ -84,15 +81,9 @@ private: mesh::Packet* packet; unsigned long timestamp; bool is_tx; - // Store raw radio data with each packet to avoid it being overwritten - uint8_t raw_data[256]; - int raw_len; - float snr; - float rssi; - bool has_raw_data; }; - static const int MAX_QUEUE_SIZE = 10; + static const int MAX_QUEUE_SIZE = 50; QueuedPacket _packet_queue[MAX_QUEUE_SIZE]; int _queue_head; int _queue_tail; @@ -104,76 +95,17 @@ private: unsigned long _last_ntp_sync; bool _ntp_synced; - // Timezone handling - Timezone* _timezone; - - // Raw radio data storage - uint8_t _last_raw_data[256]; - int _last_raw_len; - float _last_snr; - float _last_rssi; - unsigned long _last_raw_timestamp; - - // Let's Mesh Analyzer support - bool _analyzer_us_enabled; - bool _analyzer_eu_enabled; - char _auth_token_us[768]; // JWT token for US server authentication (increased for owner/client fields) - char _auth_token_eu[768]; // JWT token for EU server authentication (increased for owner/client fields) - char _analyzer_username[70]; // Username in format v1_{UPPERCASE_PUBLIC_KEY} - - // Token expiration tracking - unsigned long _token_us_expires_at; - unsigned long _token_eu_expires_at; - unsigned long _last_token_renewal_attempt_us; - unsigned long _last_token_renewal_attempt_eu; - unsigned long _last_reconnect_attempt_us; - unsigned long _last_reconnect_attempt_eu; - - // Status publish retry tracking - unsigned long _last_status_retry; // Track last retry attempt (separate from successful publish) - static const unsigned long STATUS_RETRY_INTERVAL = 30000; // Retry every 30 seconds if failed - - // Device identity for JWT token creation - mesh::LocalIdentity *_identity; - - // PsychicMqttClient instances for different brokers - PsychicMqttClient* _analyzer_us_client; - PsychicMqttClient* _analyzer_eu_client; - - // Configuration validation state - bool _config_valid; - - // Throttle logging for disconnected broker messages - unsigned long _last_no_broker_log; - static const unsigned long NO_BROKER_LOG_INTERVAL = 30000; // Log every 30 seconds max - - // Throttle logging for analyzer client disconnected messages - unsigned long _last_analyzer_us_log; - unsigned long _last_analyzer_eu_log; - static const unsigned long ANALYZER_LOG_INTERVAL = 30000; // Log every 30 seconds max - - // Optional pointers for collecting stats internally (set by mesh if available) - mesh::Dispatcher* _dispatcher; // For air times and errors - mesh::Radio* _radio; // For noise floor - mesh::MainBoard* _board; // For battery voltage - mesh::MillisecondClock* _ms; // For uptime - // Internal methods void connectToBrokers(); void processPacketQueue(); - bool publishStatus(); // Returns true if status was successfully published - void publishPacket(mesh::Packet* packet, bool is_tx, - const uint8_t* raw_data = nullptr, int raw_len = 0, - float snr = 0.0f, float rssi = 0.0f); + void publishStatus(); + void publishPacket(mesh::Packet* packet, bool is_tx); void publishRaw(mesh::Packet* packet); void queuePacket(mesh::Packet* packet, bool is_tx); void dequeuePacket(); bool isAnyBrokerConnected(); void setBrokerDefaults(); void syncTimeWithNTP(); - Timezone* createTimezoneFromString(const char* tz_string); - bool isMQTTConfigValid(); - bool isIATAValid() const; // Check if IATA code is configured public: /** @@ -182,9 +114,8 @@ public: * @param prefs Node preferences for configuration settings * @param mgr PacketManager for allocating and queuing packets * @param rtc RTCClock for timestamping debug messages - * @param identity Device identity for JWT token creation */ - MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, mesh::LocalIdentity *identity); + MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc); /** * Initializes the MQTT bridge @@ -204,28 +135,6 @@ public: */ void end() override; - /** - * Checks if MQTT configuration is valid - * - * @return true if all required MQTT settings are properly configured - */ - bool isConfigValid() const; - - /** - * Static method to validate MQTT configuration from preferences - * - * @param prefs Node preferences containing MQTT settings - * @return true if all required MQTT settings are properly configured - */ - static bool isConfigValid(const NodePrefs* prefs); - - /** - * Check if MQTT bridge is ready to operate (has WiFi credentials) - * - * @return true if WiFi credentials are configured and bridge can connect - */ - bool isReady() const; - /** * Main loop handler * - Maintains broker connections @@ -298,34 +207,6 @@ public: */ void setBoardModel(const char* board_model); - /** - * Set build date for client version - * - * @param build_date Build date string - */ - void setBuildDate(const char* build_date); - - /** - * Stores raw radio data for MQTT messages - * - * @param raw_data Raw radio transmission data - * @param len Length of raw data - * @param snr Signal-to-noise ratio - * @param rssi Received signal strength indicator - */ - void storeRawRadioData(const uint8_t* raw_data, int len, float snr, float rssi); - - // Let's Mesh Analyzer methods - void setupAnalyzerServers(); - bool createAuthToken(); - void publishToAnalyzerServers(const char* topic, const char* payload, bool retained = false); - - // PsychicMqttClient WebSocket methods - void setupAnalyzerClients(); - void maintainAnalyzerConnections(); - void publishToAnalyzerClient(PsychicMqttClient* client, const char* topic, const char* payload, bool retained = false); - void publishStatusToAnalyzerClient(PsychicMqttClient* client, const char* server_name); - /** * Enable/disable message types * @@ -348,33 +229,6 @@ public: * @return Number of queued packets */ int getQueueSize() const; - - /** - * Set optional pointers for stats collection. - * If these are set, stats will be collected automatically when publishing status. - * - * @param dispatcher Dispatcher (or Mesh*) for air times and errors - * @param radio Radio for noise floor - * @param board MainBoard for battery voltage - * @param ms MillisecondClock for uptime - */ - void setStatsSources(mesh::Dispatcher* dispatcher, mesh::Radio* radio, - mesh::MainBoard* board, mesh::MillisecondClock* ms); - -private: - /** - * Generate client version string in format "meshcore/{firmware_version}" - * Memory-efficient: writes to provided buffer, no dynamic allocation - * - * @param buffer Buffer to write the client version string to - * @param buffer_size Size of the buffer (must be at least 64 bytes) - */ - void getClientVersion(char* buffer, size_t buffer_size) const; - - /** - * Log memory status for debugging - */ - void logMemoryStatus(); }; #endif diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index 303f10c7..2864f4cb 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -51,9 +51,6 @@ build_flags = build_src_filter = ${Heltec_lora32_v3.build_src_filter} + +<../examples/simple_repeater> - - - - - - lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} @@ -105,13 +102,13 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} -[env:Heltec_v3_repeater_observer_mqtt] +[env:Heltec_v3_repeater_bridge_mqtt] extends = Heltec_lora32_v3 -upload_port = /dev/cu.usbserial-4 +upload_port = /dev/cu.usbserial-2 build_flags = ${Heltec_lora32_v3.build_flags} -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_NAME='"MQTT Bridge"' -D ADVERT_LAT=0.0 -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' @@ -122,27 +119,19 @@ build_flags = -D MQTT_DEBUG=1 -D MESH_PACKET_LOGGING=1 -D MESH_DEBUG=1 - -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y # -D WIFI_SSID='"ssid"' # -D WIFI_PWD='"password"' -# -D MQTT_SERVER='"your-mqtt-broker.com"' -# -D MQTT_PORT=1883 -# -D MQTT_USERNAME='"your-username"' -# -D MQTT_PASSWORD='"your-password"' build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + - + + +<../examples/simple_repeater> lib_deps = - ${Heltec_lora32_v3.lib_deps} - ${esp32_ota.lib_deps} - elims/PsychicMqttClient@^0.2.4 - bblanchon/ArduinoJson - arduino-libraries/NTPClient - JChristensen/Timezone - paulstoffregen/Time + ${Heltec_lora32_v3.lib_deps} + ${esp32_ota.lib_deps} + knolleary/PubSubClient @ ^2.8 + bblanchon/ArduinoJson @ 6.17.3 + arduino-libraries/NTPClient [env:Heltec_v3_room_server] extends = Heltec_lora32_v3 @@ -163,38 +152,6 @@ lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps} -[env:Heltec_v3_room_server_observer_mqtt] -extends = Heltec_lora32_v3 -build_flags = - ${Heltec_lora32_v3.build_flags} - -D DISPLAY_CLASS=SSD1306Display - -D ADVERT_NAME='"Heltec Room Observer"' - -D ADVERT_LAT=0.0 - -D ADVERT_LON=0.0 - -D ADMIN_PASSWORD='"password"' - -D ROOM_PASSWORD='"hello"' - -D WITH_MQTT_BRIDGE=1 - -D MAX_MQTT_BROKERS=3 - -D MQTT_MAX_PACKET_SIZE=1024 -; -D MQTT_DEBUG=1 -; -D MESH_PACKET_LOGGING=1 -; -D MESH_DEBUG=1 - -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y -build_src_filter = ${Heltec_lora32_v3.build_src_filter} - + - + - + - + - +<../examples/simple_room_server> -lib_deps = - ${Heltec_lora32_v3.lib_deps} - ${esp32_ota.lib_deps} - elims/PsychicMqttClient@^0.2.4 - bblanchon/ArduinoJson - arduino-libraries/NTPClient - JChristensen/Timezone - paulstoffregen/Time - [env:Heltec_v3_terminal_chat] extends = Heltec_lora32_v3 build_flags = @@ -261,8 +218,8 @@ build_flags = -D MAX_GROUP_CHANNELS=40 -D DISPLAY_CLASS=SSD1306Display -D WIFI_DEBUG_LOGGING=1 -# -D WIFI_SSID='"ssid"' -# -D WIFI_PWD='"password"' + -D WIFI_SSID='"REMOVED_SSID"' + -D WIFI_PWD='"REMOVED_PASSWORD"' ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} @@ -308,9 +265,6 @@ build_flags = ; -D MESH_DEBUG=1 build_src_filter = ${Heltec_lora32_v3.build_src_filter} +<../examples/simple_repeater> - - - - - - lib_deps = ${Heltec_lora32_v3.lib_deps} ${esp32_ota.lib_deps}