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
This commit is contained in:
agessaman committed 2026-01-02 13:36:41 -08:00
1 parent 0cd0c36ab9
commit efc15eb643
8 files changed
+376 -2701

No files matched your search

+17 -405
View File
@@ -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 <name>` - Set device origin name
- `set mqtt.iata <code>` - 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 <ms>` - Set status publish interval (1000-3600000 ms)
- `set mqtt.server <hostname>` - Set MQTT server hostname
- `set mqtt.port <port>` - Set MQTT server port (1-65535)
- `set mqtt.username <username>` - Set MQTT username
- `set mqtt.password <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 <ssid>` - Set WiFi SSID
- `set wifi.pwd <password>` - 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 <string>` - Set timezone string (IANA format or abbreviation)
- `set timezone.offset <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
+3 -41
View File
@@ -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
+180 -336
View File
@@ -3,24 +3,6 @@
#include "TxtDataHelpers.h"
#include "AdvertDataHelpers.h"
#include <RTClib.h>
#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
+32 -168
View File
@@ -1,8 +1,6 @@
#include "MQTTMessageBuilder.h"
#include <ArduinoJson.h>
#include <time.h>
#include <Timezone.h>
#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<JsonObject>();
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<JsonObject>();
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;
}
}
+1 -32
View File
@@ -3,7 +3,6 @@
#include "MeshCore.h"
#include <ArduinoJson.h>
#include <Mesh.h>
#include <Timezone.h>
/**
* @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
);
File diff suppressed because it is too large. Load diff
+7 -153
View File
@@ -2,12 +2,10 @@
#include "MeshCore.h"
#include "helpers/bridges/BridgeBase.h"
#include <PsychicMqttClient.h>
#include <PubSubClient.h>
#include <WiFi.h>
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Timezone.h>
#include "helpers/JWTHelper.h"
#if defined(MQTT_DEBUG) && defined(ARDUINO)
#include <Arduino.h>
@@ -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
+10 -56
View File
@@ -51,9 +51,6 @@ build_flags =
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/ui/SSD1306Display.cpp>
+<../examples/simple_repeater>
-<helpers/bridges/MQTTBridge.cpp>
-<helpers/MQTTMessageBuilder.cpp>
-<helpers/JWTHelper.cpp>
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}
+<helpers/bridges/MQTTBridge.cpp>
+<helpers/MQTTMessageBuilder.cpp>
+<helpers/JWTHelper.cpp>
+<helpers/ui/SSD1306Display.cpp>
+<../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}
+<helpers/ui/SSD1306Display.cpp>
+<helpers/bridges/MQTTBridge.cpp>
+<helpers/MQTTMessageBuilder.cpp>
+<helpers/JWTHelper.cpp>
+<../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>
-<helpers/bridges/MQTTBridge.cpp>
-<helpers/MQTTMessageBuilder.cpp>
-<helpers/JWTHelper.cpp>
lib_deps =
${Heltec_lora32_v3.lib_deps}
${esp32_ota.lib_deps}