mirror of
https://github.com/agessaman/MeshCore.git
synced 2026-08-28 18:08:16 +00:00
Refactor MQTT bridge implementation to support up to 3 configurable connection slots with built-in presets for LetsMesh Analyzer (US/EU) and MeshMapper. Update CLI commands for slot management and enhance configuration migration from legacy settings. Adjust related documentation and code structure for improved clarity and functionality.
This commit is contained in:
+167
-278
@@ -7,9 +7,6 @@ This document describes the MQTT bridge implementation that allows MeshCore repe
|
||||
### 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
|
||||
@@ -17,7 +14,7 @@ set wifi.ssid YourWiFiNetwork
|
||||
set wifi.pwd YourWiFiPassword
|
||||
```
|
||||
|
||||
If you wish to upload to the MeshCore Analyzer, also `set mqtt.iata XXX` to a valid IATA airport code
|
||||
If you wish to upload to the MeshCore Analyzer, also `set mqtt.iata XXX` to a valid IATA airport code.
|
||||
|
||||
**3. Reboot to Connect to WiFi**
|
||||
```bash
|
||||
@@ -36,6 +33,9 @@ get bridge.enabled
|
||||
get bridge.source
|
||||
get mqtt.origin
|
||||
get mqtt.iata
|
||||
get mqtt1.preset
|
||||
get mqtt2.preset
|
||||
get mqtt3.preset
|
||||
```
|
||||
|
||||
**6. Restart Bridge (if needed)**
|
||||
@@ -50,8 +50,8 @@ 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
|
||||
- Start uplinking mesh packets to configured MQTT brokers
|
||||
- By default, publish to Let's Mesh Analyzer US (slot 1) and EU (slot 2)
|
||||
- Use device name as MQTT origin (set automatically)
|
||||
|
||||
---
|
||||
@@ -59,131 +59,167 @@ reboot
|
||||
## Overview
|
||||
|
||||
The MQTT bridge implementation provides:
|
||||
- Multiple MQTT broker support (up to 3 brokers)
|
||||
- Up to 3 concurrent MQTT connection slots with built-in presets
|
||||
- Built-in presets for LetsMesh Analyzer (US/EU) and MeshMapper
|
||||
- Custom broker support with username/password authentication
|
||||
- JWT (Ed25519 device signing) authentication for preset brokers
|
||||
- WSS (WebSocket Secure) and direct MQTT transport
|
||||
- Automatic reconnection with exponential backoff
|
||||
- JSON message formatting for status, packets, and raw data
|
||||
- Configurable topics and QoS levels
|
||||
- Packet queuing during connection issues
|
||||
- Automatic migration from old configuration format
|
||||
|
||||
## Files Added
|
||||
## Architecture
|
||||
|
||||
### Core Implementation
|
||||
### Slot-Based Preset System
|
||||
|
||||
The MQTT bridge uses a slot-based architecture with up to 3 concurrent connections. Each slot can be configured with a built-in preset or custom broker settings.
|
||||
|
||||
**Built-in Presets:**
|
||||
|
||||
| Preset | Server | Auth | Transport |
|
||||
|--------|--------|------|-----------|
|
||||
| `analyzer-us` | mqtt-us-v1.letsmesh.net:443 | JWT (Ed25519) | WSS |
|
||||
| `analyzer-eu` | mqtt-eu-v1.letsmesh.net:443 | JWT (Ed25519) | WSS |
|
||||
| `meshmapper` | mqtt.meshmapper.cc:443 | JWT (Ed25519) | WSS |
|
||||
| `custom` | User-configured | Username/Password | MQTT or WSS |
|
||||
| `none` | (disabled) | — | — |
|
||||
|
||||
**Default Configuration:**
|
||||
- Slot 1: `analyzer-us`
|
||||
- Slot 2: `analyzer-eu`
|
||||
- Slot 3: `none`
|
||||
|
||||
### Files
|
||||
|
||||
#### Core Implementation
|
||||
- `src/helpers/bridges/MQTTBridge.h` - MQTT bridge class definition
|
||||
- `src/helpers/bridges/MQTTBridge.cpp` - MQTT bridge implementation
|
||||
- `src/helpers/MQTTPresets.h` - Preset definitions, CA certificates, and lookup functions
|
||||
- `src/helpers/MQTTMessageBuilder.h` - JSON message formatting utilities
|
||||
- `src/helpers/MQTTMessageBuilder.cpp` - JSON message formatting implementation
|
||||
- `src/helpers/JWTHelper.h` - JWT token generation for Ed25519-based authentication
|
||||
|
||||
### Integration
|
||||
#### 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 `variants/heltec_v3/platformio.ini` - Added MQTT build configuration
|
||||
- Updated `variants/station_g2/platformio.ini` - Added MQTT build configuration for Station G2
|
||||
- Updated `src/helpers/CommonCLI.h` - MQTT slot preferences, WiFi, and timezone fields
|
||||
- Updated `src/helpers/CommonCLI.cpp` - MQTT slot CLI commands, migration logic
|
||||
|
||||
## Build Configuration
|
||||
|
||||
To build the MQTT bridge firmware:
|
||||
|
||||
### Heltec V3
|
||||
```bash
|
||||
# Heltec V3
|
||||
pio run -e Heltec_v3_repeater_observer_mqtt
|
||||
```
|
||||
|
||||
### Station G2
|
||||
```bash
|
||||
# Heltec V4
|
||||
pio run -e heltec_v4_repeater_observer_mqtt
|
||||
|
||||
# Station G2
|
||||
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
|
||||
### Build Flags
|
||||
- `WITH_MQTT_BRIDGE=1` - Enable MQTT bridge (required)
|
||||
- `MQTT_DEBUG=1` - Enable debug logging (optional)
|
||||
- `MQTT_WIFI_TX_POWER` - WiFi TX power level (default: `WIFI_POWER_11dBm`)
|
||||
- Available values: `WIFI_POWER_19_5dBm`, `WIFI_POWER_19dBm`, `WIFI_POWER_18_5dBm`, `WIFI_POWER_17_5dBm`, `WIFI_POWER_15dBm`, `WIFI_POWER_13dBm`, `WIFI_POWER_11dBm`, `WIFI_POWER_8_5dBm`, `WIFI_POWER_7dBm`, `WIFI_POWER_5dBm`, `WIFI_POWER_2dBm`, `WIFI_POWER_MINUS_1dBm`
|
||||
- Example: `-D MQTT_WIFI_TX_POWER=WIFI_POWER_19_5dBm` for maximum power
|
||||
- **Note**: These power levels are appropriate for ESP32 and ESP32-S3. ESP32-C3 and ESP32-C6 may have different maximum power capabilities. If an invalid constant is used for your chip, the compiler will report an error. Check your specific ESP32 variant's datasheet for maximum supported TX power.
|
||||
- `MQTT_WIFI_POWER_SAVE_DEFAULT` - Default WiFi power save mode (0=min, 1=none, 2=max)
|
||||
|
||||
## Default Configuration
|
||||
|
||||
The MQTT bridge comes with the following defaults:
|
||||
- **Origin**: "MeshCore-Repeater"
|
||||
- **IATA**: "SEA"
|
||||
- **Origin**: Device name (set automatically)
|
||||
- **IATA**: (must be configured)
|
||||
- **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)
|
||||
- **Slot 1**: `analyzer-us` (mqtt-us-v1.letsmesh.net:443)
|
||||
- **Slot 2**: `analyzer-eu` (mqtt-eu-v1.letsmesh.net:443)
|
||||
- **Slot 3**: `none` (disabled)
|
||||
- **WiFi SSID**: "ssid_here" (must be configured)
|
||||
- **WiFi Password**: "password_here" (must be configured)
|
||||
- **WiFi Power Save**: "min" (minimum power saving, balanced performance and power)
|
||||
- **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
|
||||
### MQTT Slot Commands
|
||||
|
||||
Each slot (1-3) supports the following commands:
|
||||
|
||||
#### Get Commands
|
||||
- `get mqtt1.preset` - Get slot 1 preset name
|
||||
- `get mqtt2.preset` - Get slot 2 preset name
|
||||
- `get mqtt3.preset` - Get slot 3 preset name
|
||||
- `get mqttN.server` - Get custom server hostname for slot N
|
||||
- `get mqttN.port` - Get custom server port for slot N
|
||||
- `get mqttN.username` - Get custom username for slot N
|
||||
- `get mqttN.password` - Get custom password for slot N
|
||||
|
||||
#### Set Commands
|
||||
- `set mqtt1.preset analyzer-us` - Set slot 1 to LetsMesh Analyzer US
|
||||
- `set mqtt1.preset analyzer-eu` - Set slot 1 to LetsMesh Analyzer EU
|
||||
- `set mqtt1.preset meshmapper` - Set slot 1 to MeshMapper
|
||||
- `set mqtt1.preset custom` - Set slot 1 to custom broker (configure server/port/username/password)
|
||||
- `set mqtt1.preset none` - Disable slot 1
|
||||
- `set mqttN.server <hostname>` - Set custom server hostname for slot N
|
||||
- `set mqttN.port <port>` - Set custom server port for slot N (1-65535)
|
||||
- `set mqttN.username <username>` - Set custom username for slot N
|
||||
- `set mqttN.password <password>` - Set custom password for slot N
|
||||
|
||||
**Note:** Custom server/port/username/password settings only apply when the slot's preset is `custom`.
|
||||
|
||||
#### Example: Configure MeshMapper on Slot 3
|
||||
```bash
|
||||
set mqtt3.preset meshmapper
|
||||
```
|
||||
|
||||
#### Example: Configure Custom Broker on Slot 3
|
||||
```bash
|
||||
set mqtt3.preset custom
|
||||
set mqtt3.server your-broker.example.com
|
||||
set mqtt3.port 1883
|
||||
set mqtt3.username your-username
|
||||
set mqtt3.password your-password
|
||||
```
|
||||
|
||||
### MQTT Shared Commands
|
||||
|
||||
These settings apply across all MQTT slots:
|
||||
|
||||
#### 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.status` - Get MQTT status summary (connection info per slot)
|
||||
- `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)
|
||||
- `get mqtt.owner` - Get owner public key (64 hex characters)
|
||||
- **Note**: Available via serial console only (not via LoRa repeater console)
|
||||
- `get mqtt.email` - Get owner email address
|
||||
- **Note**: Available via serial console only (not via LoRa repeater console)
|
||||
- `get mqtt.interval` - Get status publish interval
|
||||
- `get mqtt.owner` - Get owner public key (serial console only)
|
||||
- `get mqtt.email` - Get owner email address (serial console only)
|
||||
|
||||
#### Set Commands
|
||||
- `set mqtt.origin <name>` - Set device origin name
|
||||
- `set mqtt.iata <code>` - Set IATA code
|
||||
- `set mqtt.iata <code>` - Set IATA code (auto-uppercased)
|
||||
- `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
|
||||
- `set mqtt.owner <64-hex-char-public-key>` - Set owner public key (64 hex characters, 32 bytes)
|
||||
- `set mqtt.email <email>` - Set owner email address for matching nodes with owners
|
||||
- `set mqtt.interval <minutes>` - Set status publish interval (1-60 minutes)
|
||||
- `set mqtt.owner <64-hex-char-public-key>` - Set owner public key
|
||||
- `set mqtt.email <email>` - Set owner email address
|
||||
|
||||
### WiFi Commands
|
||||
|
||||
#### Get Commands
|
||||
- `get wifi.ssid` - Get WiFi SSID
|
||||
- `get wifi.pwd` - Get WiFi password
|
||||
- `get wifi.status` - Get WiFi connection status, IP, RSSI, and uptime
|
||||
- `get wifi.powersave` - Get WiFi power save mode (none/min/max)
|
||||
|
||||
#### Set Commands
|
||||
@@ -228,14 +264,13 @@ The CLI commands are organized into two levels:
|
||||
- `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.*`)
|
||||
### Bridge-Specific Commands (`mqtt.*`, `mqttN.*`, `wifi.*`, `timezone.*`)
|
||||
**Implementation-specific settings** - These only apply to the MQTT bridge:
|
||||
- `mqtt.*` - MQTT broker configuration, message types, and formatting
|
||||
- `mqttN.*` - Per-slot MQTT broker configuration (N = 1, 2, or 3)
|
||||
- `mqtt.*` - Shared MQTT settings (message types, origin, IATA, etc.)
|
||||
- `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:
|
||||
@@ -302,6 +337,14 @@ Minimal raw packet data for map integration.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Slot-Based Preset System
|
||||
- Up to 3 concurrent MQTT connections
|
||||
- Built-in presets for LetsMesh Analyzer (US/EU) and MeshMapper
|
||||
- Custom broker support with username/password auth
|
||||
- JWT (Ed25519) authentication for preset brokers
|
||||
- Automatic reconnection with exponential backoff per slot
|
||||
- JWT token buffers only allocated for JWT-auth slots (memory efficient)
|
||||
|
||||
### Raw Radio Data Capture
|
||||
- Captures actual raw radio transmission data (including radio headers)
|
||||
- Uses proper MeshCore packet hashing (SHA256-based)
|
||||
@@ -324,259 +367,105 @@ Minimal raw packet data for map integration.
|
||||
- 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)
|
||||
### Authentication
|
||||
- **JWT Authentication**: Ed25519-signed tokens for secure MQTT authentication (used by all built-in presets)
|
||||
- **Username/Password**: Standard MQTT authentication for custom brokers
|
||||
- **Username Format** (JWT): `v1_{UPPERCASE_PUBLIC_KEY}`
|
||||
- **Automatic Token Renewal**: Tokens are renewed before expiration
|
||||
|
||||
## Migration from Old Configuration
|
||||
|
||||
When upgrading from a firmware version that used the old MQTT configuration format (`mqtt.analyzer.us`, `mqtt.analyzer.eu`, `mqtt.server`, `mqtt.port`, `mqtt.username`, `mqtt.password`), the device automatically migrates settings:
|
||||
|
||||
- `mqtt.analyzer.us = on` → Slot 1 preset: `analyzer-us`
|
||||
- `mqtt.analyzer.eu = on` → Slot 2 preset: `analyzer-eu`
|
||||
- Custom server configured → Slot 3 preset: `custom` with host/port/username/password preserved
|
||||
- All other settings (origin, IATA, message types, WiFi, timezone) are preserved as-is
|
||||
|
||||
The migration happens automatically on first boot after firmware update. No manual intervention is needed.
|
||||
|
||||
## 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:**
|
||||
### Step 1: Configure WiFi
|
||||
```
|
||||
# Set your WiFi credentials
|
||||
set wifi.ssid "YourWiFiNetwork"
|
||||
set wifi.pwd "YourWiFiPassword"
|
||||
|
||||
# Optionally configure WiFi power saving (default: min)
|
||||
# Use "none" for best performance, "min" for balanced (default), "max" for lowest power
|
||||
set wifi.powersave min
|
||||
|
||||
# Verify WiFi settings
|
||||
get wifi.ssid
|
||||
get wifi.pwd
|
||||
get wifi.powersave
|
||||
set wifi.ssid YourWiFiNetwork
|
||||
set wifi.pwd YourWiFiPassword
|
||||
reboot
|
||||
```
|
||||
|
||||
### Step 4: Configure Device Identity
|
||||
Set up your device's identity for MQTT topics and status messages.
|
||||
|
||||
**Via LoRa Repeater Console:**
|
||||
### Step 2: Configure Device Identity
|
||||
```
|
||||
# Set IATA code for topic structure (e.g., airport code)
|
||||
set mqtt.iata "SEA"
|
||||
|
||||
# Verify settings (origin is set automatically to device name)
|
||||
set mqtt.iata SEA
|
||||
get mqtt.origin
|
||||
get mqtt.iata
|
||||
```
|
||||
|
||||
**Via Serial Console (Optional - Owner Configuration):**
|
||||
### Step 3: Verify Slot Configuration
|
||||
```
|
||||
# Set owner public key (64 hex characters, 32 bytes)
|
||||
# This is used for matching nodes with owners in MQTT messages
|
||||
set mqtt.owner A1B2C3D4E5F6789012345678901234567890123456789012345678901234567890
|
||||
|
||||
# Set owner email address
|
||||
set mqtt.email owner@example.com
|
||||
|
||||
# Verify owner settings
|
||||
get mqtt.owner
|
||||
get mqtt.email
|
||||
get mqtt1.preset # Should show: analyzer-us
|
||||
get mqtt2.preset # Should show: analyzer-eu
|
||||
get mqtt3.preset # Should show: none
|
||||
```
|
||||
|
||||
### Step 5: Configure Timezone
|
||||
Set your local timezone for accurate timestamps.
|
||||
|
||||
**Via LoRa Repeater Console:**
|
||||
### Step 4: (Optional) Add MeshMapper
|
||||
```
|
||||
# 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
|
||||
set mqtt3.preset meshmapper
|
||||
```
|
||||
|
||||
### Step 6: Configure MQTT Settings
|
||||
Customize which messages to publish and how often.
|
||||
|
||||
**Via LoRa Repeater Console:**
|
||||
### Step 5: (Optional) Configure Custom Broker
|
||||
```
|
||||
# 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
|
||||
set mqtt3.preset custom
|
||||
set mqtt3.server your-broker.example.com
|
||||
set mqtt3.port 1883
|
||||
set mqtt3.username your-username
|
||||
set mqtt3.password your-password
|
||||
```
|
||||
|
||||
### Step 7: Verify MQTT Broker Connection
|
||||
Check that the device can connect to MQTT brokers.
|
||||
|
||||
**Via LoRa Repeater Console:**
|
||||
### Step 6: Verify Connection
|
||||
```
|
||||
# Check bridge status
|
||||
set bridge.source rx
|
||||
get bridge.enabled
|
||||
|
||||
# If disabled, enable it
|
||||
set bridge.enabled on
|
||||
|
||||
# Check MQTT analyzer servers (optional)
|
||||
get mqtt.analyzer.us
|
||||
get mqtt.analyzer.eu
|
||||
get mqtt.status
|
||||
get wifi.status
|
||||
```
|
||||
|
||||
### 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
|
||||
### Troubleshooting
|
||||
|
||||
#### Device Won't Connect to WiFi
|
||||
**Via LoRa Repeater Console:**
|
||||
```
|
||||
# Check WiFi settings
|
||||
get wifi.ssid
|
||||
get wifi.pwd
|
||||
get wifi.powersave
|
||||
|
||||
# Reset WiFi settings
|
||||
set wifi.ssid ""
|
||||
set wifi.pwd ""
|
||||
|
||||
# Reconfigure with correct credentials
|
||||
set wifi.ssid "YourWiFiNetwork"
|
||||
set wifi.pwd "YourWiFiPassword"
|
||||
|
||||
# If connection issues persist, try disabling power saving for better reliability
|
||||
set wifi.powersave none
|
||||
set wifi.powersave none # Try disabling power saving
|
||||
reboot
|
||||
```
|
||||
|
||||
#### 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
|
||||
get mqtt.status # Check per-slot connection status
|
||||
get mqtt1.preset # Verify slots are configured
|
||||
get mqtt.iata # IATA must be set for Analyzer presets
|
||||
```
|
||||
|
||||
#### 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
|
||||
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
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **PubSubClient**: MQTT client library
|
||||
- **ArduinoJson**: JSON message formatting (v6.17.3)
|
||||
- **PsychicMqttClient**: MQTT client library (supports WSS and direct MQTT)
|
||||
- **ArduinoJson**: JSON message formatting
|
||||
- **NTPClient**: Network time protocol client
|
||||
- **Timezone**: Timezone conversion library (JChristensen/Timezone)
|
||||
- **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)
|
||||
- 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
|
||||
- **JWTHelper**: Custom JWT token generation for device authentication
|
||||
|
||||
@@ -949,9 +949,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
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
|
||||
// MQTT slot presets (analyzer-us and analyzer-eu enabled by default)
|
||||
StrHelper::strncpy(_prefs.mqtt_slot_preset[0], "analyzer-us", sizeof(_prefs.mqtt_slot_preset[0]));
|
||||
StrHelper::strncpy(_prefs.mqtt_slot_preset[1], "analyzer-eu", sizeof(_prefs.mqtt_slot_preset[1]));
|
||||
StrHelper::strncpy(_prefs.mqtt_slot_preset[2], "none", sizeof(_prefs.mqtt_slot_preset[2]));
|
||||
|
||||
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
|
||||
|
||||
|
||||
@@ -681,9 +681,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
StrHelper::strncpy(_prefs.timezone_string, "America/Los_Angeles", sizeof(_prefs.timezone_string));
|
||||
_prefs.timezone_offset = -8; // fallback
|
||||
|
||||
// Let's Mesh Analyzer defaults (same as repeater - both enabled by default)
|
||||
_prefs.mqtt_analyzer_us_enabled = 1; // enabled
|
||||
_prefs.mqtt_analyzer_eu_enabled = 1; // enabled
|
||||
// MQTT slot presets (analyzer-us and analyzer-eu enabled by default)
|
||||
StrHelper::strncpy(_prefs.mqtt_slot_preset[0], "analyzer-us", sizeof(_prefs.mqtt_slot_preset[0]));
|
||||
StrHelper::strncpy(_prefs.mqtt_slot_preset[1], "analyzer-eu", sizeof(_prefs.mqtt_slot_preset[1]));
|
||||
StrHelper::strncpy(_prefs.mqtt_slot_preset[2], "none", sizeof(_prefs.mqtt_slot_preset[2]));
|
||||
|
||||
next_post_idx = 0;
|
||||
next_client_idx = 0;
|
||||
|
||||
+144
-71
@@ -22,11 +22,10 @@ static size_t getMQTTFieldsSize(const NodePrefs* prefs) {
|
||||
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);
|
||||
sizeof(prefs->timezone_offset) + sizeof(prefs->mqtt_slot_preset) +
|
||||
sizeof(prefs->mqtt_slot_host) + sizeof(prefs->mqtt_slot_port) +
|
||||
sizeof(prefs->mqtt_slot_username) + sizeof(prefs->mqtt_slot_password) +
|
||||
sizeof(prefs->mqtt_owner_public_key) + sizeof(prefs->mqtt_email);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -141,17 +140,16 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
size_t mqtt_fields_size = getMQTTFieldsSize(_prefs);
|
||||
#else
|
||||
// If MQTT bridge not enabled, still skip these fields for file format compatibility
|
||||
size_t mqtt_fields_size =
|
||||
size_t mqtt_fields_size =
|
||||
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);
|
||||
sizeof(_prefs->timezone_offset) + sizeof(_prefs->mqtt_slot_preset) +
|
||||
sizeof(_prefs->mqtt_slot_host) + sizeof(_prefs->mqtt_slot_port) +
|
||||
sizeof(_prefs->mqtt_slot_username) + sizeof(_prefs->mqtt_slot_password) +
|
||||
sizeof(_prefs->mqtt_owner_public_key) + sizeof(_prefs->mqtt_email);
|
||||
#endif
|
||||
uint8_t skip_buffer[512]; // Large enough buffer
|
||||
size_t remaining = mqtt_fields_size;
|
||||
@@ -255,17 +253,16 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
|
||||
size_t mqtt_fields_size = getMQTTFieldsSize(_prefs);
|
||||
#else
|
||||
// If MQTT bridge not enabled, still write zeros for file format compatibility
|
||||
size_t mqtt_fields_size =
|
||||
size_t mqtt_fields_size =
|
||||
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);
|
||||
sizeof(_prefs->timezone_offset) + sizeof(_prefs->mqtt_slot_preset) +
|
||||
sizeof(_prefs->mqtt_slot_host) + sizeof(_prefs->mqtt_slot_port) +
|
||||
sizeof(_prefs->mqtt_slot_username) + sizeof(_prefs->mqtt_slot_password) +
|
||||
sizeof(_prefs->mqtt_owner_public_key) + sizeof(_prefs->mqtt_email);
|
||||
#endif
|
||||
memset(pad, 0, sizeof(pad));
|
||||
size_t remaining = mqtt_fields_size;
|
||||
@@ -294,8 +291,13 @@ static void setMQTTPrefsDefaults(MQTTPrefs* prefs) {
|
||||
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
|
||||
// Slot presets: analyzer-us and analyzer-eu enabled by default, slot 3 = none
|
||||
strncpy(prefs->mqtt_slot_preset[0], "analyzer-us", sizeof(prefs->mqtt_slot_preset[0]) - 1);
|
||||
prefs->mqtt_slot_preset[0][sizeof(prefs->mqtt_slot_preset[0]) - 1] = '\0';
|
||||
strncpy(prefs->mqtt_slot_preset[1], "analyzer-eu", sizeof(prefs->mqtt_slot_preset[1]) - 1);
|
||||
prefs->mqtt_slot_preset[1][sizeof(prefs->mqtt_slot_preset[1]) - 1] = '\0';
|
||||
strncpy(prefs->mqtt_slot_preset[2], "none", sizeof(prefs->mqtt_slot_preset[2]) - 1);
|
||||
prefs->mqtt_slot_preset[2][sizeof(prefs->mqtt_slot_preset[2]) - 1] = '\0';
|
||||
#ifdef MQTT_WIFI_POWER_SAVE_DEFAULT
|
||||
prefs->wifi_power_save = MQTT_WIFI_POWER_SAVE_DEFAULT; // 0=min, 1=none, 2=max
|
||||
#else
|
||||
@@ -329,6 +331,57 @@ void CommonCLI::loadMQTTPrefs(FILESYSTEM* fs) {
|
||||
setMQTTPrefsDefaults(&_mqtt_prefs);
|
||||
}
|
||||
file.close();
|
||||
|
||||
// Migration: check if legacy fields have data but new slot fields are empty
|
||||
bool slots_empty = (_mqtt_prefs.mqtt_slot_preset[0][0] == '\0' &&
|
||||
_mqtt_prefs.mqtt_slot_preset[1][0] == '\0' &&
|
||||
_mqtt_prefs.mqtt_slot_preset[2][0] == '\0');
|
||||
bool has_legacy = (_mqtt_prefs._legacy_analyzer_us_enabled != 0 ||
|
||||
_mqtt_prefs._legacy_analyzer_eu_enabled != 0 ||
|
||||
_mqtt_prefs._legacy_mqtt_server[0] != '\0');
|
||||
if (slots_empty && has_legacy) {
|
||||
MESH_DEBUG_PRINTLN("MQTT: Migrating legacy prefs to slot-based presets");
|
||||
// Migrate analyzer US
|
||||
if (_mqtt_prefs._legacy_analyzer_us_enabled == 1) {
|
||||
strncpy(_mqtt_prefs.mqtt_slot_preset[0], "analyzer-us", sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_preset[0][sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1] = '\0';
|
||||
} else {
|
||||
strncpy(_mqtt_prefs.mqtt_slot_preset[0], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_preset[0][sizeof(_mqtt_prefs.mqtt_slot_preset[0]) - 1] = '\0';
|
||||
}
|
||||
// Migrate analyzer EU
|
||||
if (_mqtt_prefs._legacy_analyzer_eu_enabled == 1) {
|
||||
strncpy(_mqtt_prefs.mqtt_slot_preset[1], "analyzer-eu", sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_preset[1][sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1] = '\0';
|
||||
} else {
|
||||
strncpy(_mqtt_prefs.mqtt_slot_preset[1], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_preset[1][sizeof(_mqtt_prefs.mqtt_slot_preset[1]) - 1] = '\0';
|
||||
}
|
||||
// Migrate custom server to slot 3
|
||||
if (_mqtt_prefs._legacy_mqtt_server[0] != '\0' && _mqtt_prefs._legacy_mqtt_port > 0) {
|
||||
strncpy(_mqtt_prefs.mqtt_slot_preset[2], "custom", sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_preset[2][sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1] = '\0';
|
||||
strncpy(_mqtt_prefs.mqtt_slot_host[2], _mqtt_prefs._legacy_mqtt_server, sizeof(_mqtt_prefs.mqtt_slot_host[2]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_host[2][sizeof(_mqtt_prefs.mqtt_slot_host[2]) - 1] = '\0';
|
||||
_mqtt_prefs.mqtt_slot_port[2] = _mqtt_prefs._legacy_mqtt_port;
|
||||
strncpy(_mqtt_prefs.mqtt_slot_username[2], _mqtt_prefs._legacy_mqtt_username, sizeof(_mqtt_prefs.mqtt_slot_username[2]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_username[2][sizeof(_mqtt_prefs.mqtt_slot_username[2]) - 1] = '\0';
|
||||
strncpy(_mqtt_prefs.mqtt_slot_password[2], _mqtt_prefs._legacy_mqtt_password, sizeof(_mqtt_prefs.mqtt_slot_password[2]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_password[2][sizeof(_mqtt_prefs.mqtt_slot_password[2]) - 1] = '\0';
|
||||
} else {
|
||||
strncpy(_mqtt_prefs.mqtt_slot_preset[2], "none", sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1);
|
||||
_mqtt_prefs.mqtt_slot_preset[2][sizeof(_mqtt_prefs.mqtt_slot_preset[2]) - 1] = '\0';
|
||||
}
|
||||
// Clear legacy fields
|
||||
_mqtt_prefs._legacy_analyzer_us_enabled = 0;
|
||||
_mqtt_prefs._legacy_analyzer_eu_enabled = 0;
|
||||
memset(_mqtt_prefs._legacy_mqtt_server, 0, sizeof(_mqtt_prefs._legacy_mqtt_server));
|
||||
_mqtt_prefs._legacy_mqtt_port = 0;
|
||||
memset(_mqtt_prefs._legacy_mqtt_username, 0, sizeof(_mqtt_prefs._legacy_mqtt_username));
|
||||
memset(_mqtt_prefs._legacy_mqtt_password, 0, sizeof(_mqtt_prefs._legacy_mqtt_password));
|
||||
// Save migrated prefs
|
||||
saveMQTTPrefs(fs);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Migration: Try to read from old /com_prefs file if it exists
|
||||
@@ -412,12 +465,14 @@ void CommonCLI::syncMQTTPrefsToNodePrefs() {
|
||||
_prefs->wifi_power_save = _mqtt_prefs.wifi_power_save;
|
||||
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;
|
||||
// Slot-based fields
|
||||
for (int i = 0; i < 3; i++) {
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_preset[i], _mqtt_prefs.mqtt_slot_preset[i], sizeof(_prefs->mqtt_slot_preset[i]));
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_host[i], _mqtt_prefs.mqtt_slot_host[i], sizeof(_prefs->mqtt_slot_host[i]));
|
||||
_prefs->mqtt_slot_port[i] = _mqtt_prefs.mqtt_slot_port[i];
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_username[i], _mqtt_prefs.mqtt_slot_username[i], sizeof(_prefs->mqtt_slot_username[i]));
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_password[i], _mqtt_prefs.mqtt_slot_password[i], sizeof(_prefs->mqtt_slot_password[i]));
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -437,12 +492,14 @@ void CommonCLI::syncNodePrefsToMQTTPrefs() {
|
||||
_mqtt_prefs.wifi_power_save = _prefs->wifi_power_save;
|
||||
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;
|
||||
// Slot-based fields
|
||||
for (int i = 0; i < 3; i++) {
|
||||
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_preset[i], _prefs->mqtt_slot_preset[i], sizeof(_mqtt_prefs.mqtt_slot_preset[i]));
|
||||
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_host[i], _prefs->mqtt_slot_host[i], sizeof(_mqtt_prefs.mqtt_slot_host[i]));
|
||||
_mqtt_prefs.mqtt_slot_port[i] = _prefs->mqtt_slot_port[i];
|
||||
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_username[i], _prefs->mqtt_slot_username[i], sizeof(_mqtt_prefs.mqtt_slot_username[i]));
|
||||
StrHelper::strncpy(_mqtt_prefs.mqtt_slot_password[i], _prefs->mqtt_slot_password[i], sizeof(_mqtt_prefs.mqtt_slot_password[i]));
|
||||
}
|
||||
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));
|
||||
}
|
||||
@@ -687,14 +744,24 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
// 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);
|
||||
} else if (config[0] == 'm' && config[1] == 'q' && config[2] == 't' && config[3] == 't' &&
|
||||
config[4] >= '1' && config[4] <= '3' && config[5] == '.') {
|
||||
// Slot-based commands: get mqtt1.preset, get mqtt1.server, etc.
|
||||
int slot = config[4] - '1'; // 0-2
|
||||
const char* subcmd = &config[6];
|
||||
if (memcmp(subcmd, "preset", 6) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->mqtt_slot_preset[slot]);
|
||||
} else if (memcmp(subcmd, "server", 6) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->mqtt_slot_host[slot]);
|
||||
} else if (memcmp(subcmd, "port", 4) == 0) {
|
||||
sprintf(reply, "> %d", _prefs->mqtt_slot_port[slot]);
|
||||
} else if (memcmp(subcmd, "username", 8) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->mqtt_slot_username[slot]);
|
||||
} else if (memcmp(subcmd, "password", 8) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->mqtt_slot_password[slot]);
|
||||
} else {
|
||||
sprintf(reply, "??: %s", config);
|
||||
}
|
||||
} else if (memcmp(config, "wifi.ssid", 9) == 0) {
|
||||
sprintf(reply, "> %s", _prefs->wifi_ssid);
|
||||
} else if (memcmp(config, "wifi.pwd", 8) == 0) {
|
||||
@@ -745,10 +812,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
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);
|
||||
@@ -761,9 +824,6 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
} 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 if (memcmp(config, "bootloader.ver", 14) == 0) {
|
||||
#ifdef NRF52_PLATFORM
|
||||
@@ -1157,35 +1217,48 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
} 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;
|
||||
} else if (config[0] == 'm' && config[1] == 'q' && config[2] == 't' && config[3] == 't' &&
|
||||
config[4] >= '1' && config[4] <= '3' && config[5] == '.') {
|
||||
// Slot-based commands: set mqtt1.preset <name>, set mqtt1.server <host>, etc.
|
||||
int slot = config[4] - '1'; // 0-2
|
||||
const char* subcmd = &config[6];
|
||||
if (memcmp(subcmd, "preset ", 7) == 0) {
|
||||
const char* preset_name = &subcmd[7];
|
||||
// Validate preset name
|
||||
if (findMQTTPreset(preset_name) != nullptr ||
|
||||
strcmp(preset_name, MQTT_PRESET_CUSTOM) == 0 ||
|
||||
strcmp(preset_name, MQTT_PRESET_NONE) == 0) {
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_preset[slot], preset_name, sizeof(_prefs->mqtt_slot_preset[slot]));
|
||||
savePrefs();
|
||||
_callbacks->restartBridge();
|
||||
sprintf(reply, "OK - slot %d preset: %s", slot + 1, preset_name);
|
||||
} else {
|
||||
strcpy(reply, "Error: valid presets are: analyzer-us, analyzer-eu, meshmapper, custom, none");
|
||||
}
|
||||
} else if (memcmp(subcmd, "server ", 7) == 0) {
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_host[slot], &subcmd[7], sizeof(_prefs->mqtt_slot_host[slot]));
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(subcmd, "port ", 5) == 0) {
|
||||
int port = atoi(&subcmd[5]);
|
||||
if (port > 0 && port <= 65535) {
|
||||
_prefs->mqtt_slot_port[slot] = port;
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error: port must be between 1 and 65535");
|
||||
}
|
||||
} else if (memcmp(subcmd, "username ", 9) == 0) {
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_username[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_username[slot]));
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else if (memcmp(subcmd, "password ", 9) == 0) {
|
||||
StrHelper::strncpy(_prefs->mqtt_slot_password[slot], &subcmd[9], sizeof(_prefs->mqtt_slot_password[slot]));
|
||||
savePrefs();
|
||||
strcpy(reply, "OK");
|
||||
} else {
|
||||
strcpy(reply, "Error: port must be between 1 and 65535");
|
||||
sprintf(reply, "unknown config: %s", config);
|
||||
}
|
||||
} 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];
|
||||
|
||||
+36
-24
@@ -77,18 +77,19 @@ struct NodePrefs { // persisted to file
|
||||
char timezone_string[32]; // Timezone string (e.g., "America/Los_Angeles")
|
||||
int8_t timezone_offset; // Timezone offset in hours (-12 to +14) - fallback
|
||||
|
||||
// MQTT server settings
|
||||
char mqtt_server[64]; // MQTT server hostname
|
||||
uint16_t mqtt_port; // MQTT server port
|
||||
char mqtt_username[32]; // MQTT username
|
||||
char mqtt_password[64]; // MQTT password
|
||||
|
||||
// Let's Mesh Analyzer settings
|
||||
uint8_t mqtt_analyzer_us_enabled; // Enable US analyzer server
|
||||
uint8_t mqtt_analyzer_eu_enabled; // Enable EU analyzer server
|
||||
// MQTT slot presets (3 slots, each can be a preset name or "custom"/"none")
|
||||
char mqtt_slot_preset[3][24]; // e.g. "analyzer-us", "meshmapper", "custom", "none"
|
||||
|
||||
// Per-slot custom broker settings (only used when slot preset is "custom")
|
||||
char mqtt_slot_host[3][64];
|
||||
uint16_t mqtt_slot_port[3];
|
||||
char mqtt_slot_username[3][32];
|
||||
char mqtt_slot_password[3][64];
|
||||
|
||||
// Shared MQTT authentication
|
||||
char mqtt_owner_public_key[65]; // Owner public key (hex string, same length as repeater public key)
|
||||
char mqtt_email[64]; // Owner email address for matching nodes with owners
|
||||
|
||||
|
||||
uint8_t loop_detect;
|
||||
};
|
||||
|
||||
@@ -103,27 +104,38 @@ struct MQTTPrefs {
|
||||
uint8_t mqtt_raw_enabled; // Enable raw messages
|
||||
uint8_t mqtt_tx_enabled; // Enable TX packet uplinking
|
||||
uint32_t mqtt_status_interval; // Status publish interval (ms)
|
||||
|
||||
|
||||
// WiFi settings
|
||||
char wifi_ssid[32]; // WiFi SSID
|
||||
char wifi_password[64]; // WiFi password
|
||||
uint8_t wifi_power_save; // WiFi power save mode: 0=min, 1=none, 2=max (default: 0=min)
|
||||
|
||||
|
||||
// Timezone settings
|
||||
char timezone_string[32]; // Timezone string (e.g., "America/Los_Angeles")
|
||||
int8_t timezone_offset; // Timezone offset in hours (-12 to +14) - fallback
|
||||
|
||||
// MQTT server settings
|
||||
char mqtt_server[64]; // MQTT server hostname
|
||||
uint16_t mqtt_port; // MQTT server port
|
||||
char mqtt_username[32]; // MQTT username
|
||||
char mqtt_password[64]; // MQTT password
|
||||
|
||||
// Let's Mesh Analyzer settings
|
||||
uint8_t mqtt_analyzer_us_enabled; // Enable US analyzer server
|
||||
uint8_t mqtt_analyzer_eu_enabled; // Enable EU analyzer server
|
||||
char mqtt_owner_public_key[65]; // Owner public key (hex string, same length as repeater public key)
|
||||
char mqtt_email[64]; // Owner email address for matching nodes with owners
|
||||
|
||||
// Slot presets (3 slots)
|
||||
char mqtt_slot_preset[3][24]; // e.g. "analyzer-us", "meshmapper", "custom", "none"
|
||||
|
||||
// Per-slot custom broker settings (only used when preset is "custom")
|
||||
char mqtt_slot_host[3][64];
|
||||
uint16_t mqtt_slot_port[3];
|
||||
char mqtt_slot_username[3][32];
|
||||
char mqtt_slot_password[3][64];
|
||||
|
||||
// Shared authentication
|
||||
char mqtt_owner_public_key[65]; // Owner public key (hex string)
|
||||
char mqtt_email[64]; // Owner email address
|
||||
|
||||
// --- Legacy fields for migration detection ---
|
||||
// These are read during loadMQTTPrefs to detect old-format prefs and auto-migrate.
|
||||
// After migration they are zeroed out and not used again.
|
||||
uint8_t _legacy_analyzer_us_enabled;
|
||||
uint8_t _legacy_analyzer_eu_enabled;
|
||||
char _legacy_mqtt_server[64];
|
||||
uint16_t _legacy_mqtt_port;
|
||||
char _legacy_mqtt_username[32];
|
||||
char _legacy_mqtt_password[64];
|
||||
};
|
||||
#endif
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
|
||||
enum MQTTAuthType : uint8_t {
|
||||
MQTT_AUTH_NONE, // No authentication
|
||||
MQTT_AUTH_USERPASS, // Username/password
|
||||
MQTT_AUTH_JWT // Ed25519-signed JWT (device identity)
|
||||
};
|
||||
|
||||
struct MQTTPresetDef {
|
||||
const char* name; // Preset identifier: "analyzer-us", "analyzer-eu", "meshmapper"
|
||||
const char* server_url; // Full URL including scheme: "wss://host:port/path" or "mqtt://host:port"
|
||||
const char* jwt_audience; // JWT audience field (only for MQTT_AUTH_JWT)
|
||||
const char* ca_cert; // PEM CA certificate (nullptr to skip cert pinning)
|
||||
MQTTAuthType auth_type;
|
||||
};
|
||||
|
||||
// Google Trust Services - GTS Root R4 (used by LetsMesh Analyzer)
|
||||
static const char GTS_ROOT_R4[] PROGMEM =
|
||||
"-----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";
|
||||
|
||||
// ISRG Root X1 (used by MeshMapper - Let's Encrypt root CA)
|
||||
static const char ISRG_ROOT_X1[] PROGMEM =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
"MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n"
|
||||
"TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n"
|
||||
"cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n"
|
||||
"WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n"
|
||||
"ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n"
|
||||
"MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n"
|
||||
"h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n"
|
||||
"0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n"
|
||||
"A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n"
|
||||
"T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n"
|
||||
"B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n"
|
||||
"B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n"
|
||||
"KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n"
|
||||
"OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n"
|
||||
"jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n"
|
||||
"qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n"
|
||||
"rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n"
|
||||
"HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n"
|
||||
"hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n"
|
||||
"ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n"
|
||||
"3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n"
|
||||
"NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n"
|
||||
"ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n"
|
||||
"TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n"
|
||||
"jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n"
|
||||
"oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n"
|
||||
"4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n"
|
||||
"mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n"
|
||||
"emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n"
|
||||
"-----END CERTIFICATE-----\n";
|
||||
|
||||
// Number of built-in presets
|
||||
static const int MQTT_PRESET_COUNT = 3;
|
||||
|
||||
// Built-in preset definitions (stored in flash)
|
||||
static const MQTTPresetDef MQTT_PRESETS[MQTT_PRESET_COUNT] = {
|
||||
{ "analyzer-us", "wss://mqtt-us-v1.letsmesh.net:443/mqtt", "mqtt-us-v1.letsmesh.net", GTS_ROOT_R4, MQTT_AUTH_JWT },
|
||||
{ "analyzer-eu", "wss://mqtt-eu-v1.letsmesh.net:443/mqtt", "mqtt-eu-v1.letsmesh.net", GTS_ROOT_R4, MQTT_AUTH_JWT },
|
||||
{ "meshmapper", "wss://mqtt.meshmapper.cc:443/mqtt", "mqtt.meshmapper.cc", ISRG_ROOT_X1, MQTT_AUTH_JWT },
|
||||
};
|
||||
|
||||
// Find a preset by name, returns nullptr if not found
|
||||
static const MQTTPresetDef* findMQTTPreset(const char* name) {
|
||||
if (!name || name[0] == '\0') return nullptr;
|
||||
for (int i = 0; i < MQTT_PRESET_COUNT; i++) {
|
||||
if (strcmp(name, MQTT_PRESETS[i].name) == 0) {
|
||||
return &MQTT_PRESETS[i];
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Maximum number of concurrent MQTT connection slots
|
||||
static const int MAX_MQTT_SLOTS = 3;
|
||||
|
||||
// Slot preset name constants
|
||||
static const char MQTT_PRESET_NONE[] = "none";
|
||||
static const char MQTT_PRESET_CUSTOM[] = "custom";
|
||||
|
||||
#endif
|
||||
+1442
-2336
File diff suppressed because it is too large
Load Diff
+103
-291
@@ -8,6 +8,7 @@
|
||||
#include <WiFiUdp.h>
|
||||
#include <Timezone.h>
|
||||
#include "helpers/JWTHelper.h"
|
||||
#include "helpers/MQTTPresets.h"
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <freertos/FreeRTOS.h>
|
||||
@@ -37,45 +38,52 @@
|
||||
* uplink packet data to multiple MQTT brokers for monitoring and analysis.
|
||||
*
|
||||
* Features:
|
||||
* - Multiple MQTT broker support
|
||||
* - Up to 3 configurable MQTT connection slots
|
||||
* - Built-in presets for LetsMesh Analyzer (US/EU) and MeshMapper
|
||||
* - Custom broker support with username/password auth
|
||||
* - JWT authentication with Ed25519 device signing
|
||||
* - Automatic reconnection with exponential backoff
|
||||
* - JSON message formatting for status, packets, and raw data
|
||||
* - Configurable topics and QoS levels
|
||||
* - Packet queuing during connection issues
|
||||
*
|
||||
* Message Types:
|
||||
* - Status: Device connection status and metadata
|
||||
* - Packets: Full packet data with RF characteristics
|
||||
* - Raw: Minimal raw packet data for map integration
|
||||
*
|
||||
* Configuration:
|
||||
* - Define WITH_MQTT_BRIDGE to enable this bridge
|
||||
* - Configure brokers via CLI commands
|
||||
* - Set origin name and IATA code for topic structure
|
||||
* - Configure slots via: set mqtt1.preset <name>, set mqtt2.preset <name>, etc.
|
||||
* - Available presets: analyzer-us, analyzer-eu, meshmapper, custom, none
|
||||
*/
|
||||
class MQTTBridge : public BridgeBase {
|
||||
private:
|
||||
PsychicMqttClient* _mqtt_client;
|
||||
|
||||
// MQTT broker configuration
|
||||
struct MQTTBroker {
|
||||
// Connection slot - each slot holds one MQTT connection
|
||||
struct MQTTSlot {
|
||||
PsychicMqttClient* client;
|
||||
const MQTTPresetDef* preset; // Points to MQTT_PRESETS[] entry, nullptr for custom/none
|
||||
bool enabled; // true when preset is not "none"
|
||||
bool connected; // Updated in callbacks
|
||||
bool initial_connect_done; // True after first connect() call
|
||||
|
||||
// JWT auth state (only used when preset->auth_type == MQTT_AUTH_JWT)
|
||||
char* auth_token; // PSRAM-allocated, AUTH_TOKEN_SIZE bytes
|
||||
unsigned long token_expires_at;
|
||||
unsigned long last_token_renewal;
|
||||
|
||||
// Custom broker settings (only used when preset_name is "custom")
|
||||
char host[64];
|
||||
uint16_t port;
|
||||
char username[32];
|
||||
char password[64];
|
||||
char client_id[32];
|
||||
uint8_t qos;
|
||||
bool enabled;
|
||||
bool connected;
|
||||
bool initial_connect_done; // True after first connect() call - auto-reconnect handles the rest
|
||||
unsigned long last_attempt;
|
||||
unsigned long reconnect_interval;
|
||||
|
||||
// Reconnect backoff
|
||||
uint8_t reconnect_backoff; // 0..4 index into backoff table
|
||||
unsigned long last_reconnect_attempt;
|
||||
unsigned long last_log_time; // Throttle disconnect log messages
|
||||
};
|
||||
|
||||
static const int MAX_MQTT_BROKERS_COUNT = 3;
|
||||
MQTTBroker _brokers[MAX_MQTT_BROKERS_COUNT];
|
||||
int _active_brokers;
|
||||
|
||||
|
||||
static const size_t AUTH_TOKEN_SIZE = 768;
|
||||
MQTTSlot _slots[MAX_MQTT_SLOTS];
|
||||
|
||||
// JWT username shared across all JWT-auth slots (same device identity)
|
||||
char _jwt_username[70]; // Format: v1_{UPPERCASE_PUBLIC_KEY}
|
||||
|
||||
// Message configuration
|
||||
char _origin[32];
|
||||
char _iata[8];
|
||||
@@ -89,7 +97,7 @@ private:
|
||||
bool _tx_enabled;
|
||||
unsigned long _last_status_publish;
|
||||
unsigned long _status_interval;
|
||||
|
||||
|
||||
// Packet queue for offline scenarios
|
||||
struct QueuedPacket {
|
||||
mesh::Packet* packet;
|
||||
@@ -102,18 +110,18 @@ private:
|
||||
float rssi;
|
||||
bool has_raw_data;
|
||||
};
|
||||
|
||||
|
||||
static const int MAX_QUEUE_SIZE = 10;
|
||||
|
||||
|
||||
// FreeRTOS queue for thread-safe packet queuing
|
||||
#ifdef ESP_PLATFORM
|
||||
QueueHandle_t _packet_queue_handle;
|
||||
TaskHandle_t _mqtt_task_handle;
|
||||
SemaphoreHandle_t _raw_data_mutex; // Mutex for raw radio data
|
||||
// PSRAM-backed task stack (plan §3); TCB kept in internal RAM
|
||||
// PSRAM-backed task stack; TCB kept in internal RAM
|
||||
StackType_t* _mqtt_task_stack; // nullptr if using dynamic task creation
|
||||
StaticTask_t _mqtt_task_tcb;
|
||||
// PSRAM-backed packet queue storage (plan §5)
|
||||
// PSRAM-backed packet queue storage
|
||||
uint8_t* _packet_queue_storage; // nullptr if using dynamic queue
|
||||
StaticQueue_t _packet_queue_struct;
|
||||
#else
|
||||
@@ -123,350 +131,154 @@ private:
|
||||
int _queue_tail;
|
||||
#endif
|
||||
int _queue_count; // Protected by queue operations or mutex
|
||||
|
||||
|
||||
// NTP time sync
|
||||
WiFiUDP _ntp_udp;
|
||||
NTPClient _ntp_client;
|
||||
unsigned long _last_ntp_sync;
|
||||
bool _ntp_synced;
|
||||
bool _ntp_sync_pending; // Flag to trigger NTP sync from loop() instead of event handler
|
||||
|
||||
bool _slots_setup_done; // Deferred: slots set up after NTP sync
|
||||
|
||||
// Timezone handling
|
||||
Timezone* _timezone;
|
||||
|
||||
// Raw radio data storage (plan §6: PSRAM when BOARD_HAS_PSRAM)
|
||||
|
||||
// Raw radio data storage (PSRAM when BOARD_HAS_PSRAM)
|
||||
static const size_t LAST_RAW_DATA_SIZE = 256;
|
||||
uint8_t* _last_raw_data;
|
||||
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;
|
||||
static const size_t AUTH_TOKEN_SIZE = 768;
|
||||
char* _auth_token_us; // JWT token for US server (PSRAM when BOARD_HAS_PSRAM)
|
||||
char* _auth_token_eu; // JWT token for EU server (PSRAM when BOARD_HAS_PSRAM)
|
||||
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;
|
||||
|
||||
|
||||
// Memory pressure monitoring
|
||||
unsigned long _last_memory_check;
|
||||
int _skipped_publishes; // Count of skipped publishes due to memory pressure
|
||||
unsigned long _last_fragmentation_recovery; // Throttle: 5 min between recovery runs (task + loop)
|
||||
unsigned long _fragmentation_pressure_since; // 0 = not under pressure; else first time max_alloc < threshold
|
||||
unsigned long _last_fragmentation_recovery; // Throttle: 5 min between recovery runs
|
||||
unsigned long _fragmentation_pressure_since; // 0 = not under pressure
|
||||
unsigned long _last_critical_check_run; // Throttle: run unified check at most every 60s
|
||||
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;
|
||||
|
||||
// Cached broker connection status (updated in callbacks to avoid redundant checks)
|
||||
bool _cached_has_brokers;
|
||||
bool _cached_has_analyzer_servers;
|
||||
|
||||
// Throttle logging for disconnected broker messages
|
||||
|
||||
// Cached connection status (updated in callbacks to avoid redundant checks)
|
||||
bool _cached_has_connected_slots;
|
||||
|
||||
// Throttle logging
|
||||
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
|
||||
static const unsigned long SLOT_LOG_INTERVAL = 30000; // Log every 30 seconds max
|
||||
unsigned long _last_config_warning; // Throttle configuration mismatch warnings
|
||||
static const unsigned long CONFIG_WARNING_INTERVAL = 300000; // Log every 5 minutes max
|
||||
|
||||
// WiFi connection state and exponential backoff (one place for mqttTaskLoop + loop())
|
||||
// WiFi connection state and exponential backoff
|
||||
unsigned long _last_wifi_check;
|
||||
wl_status_t _last_wifi_status;
|
||||
bool _wifi_status_initialized;
|
||||
unsigned long _wifi_disconnected_time; // 0 when connected
|
||||
unsigned long _last_wifi_reconnect_attempt;
|
||||
uint8_t _wifi_reconnect_backoff_attempt; // 0..5 → 15s, 30s, 60s, 120s, 300s; reset on connect
|
||||
// Main broker reconnect backoff (reset in onConnect)
|
||||
uint8_t _main_broker_reconnect_backoff_attempt; // 0..5 → 15s, 30s, 60s, 120s, 300s
|
||||
// Analyzer reconnect backoff (reset when that client is connected)
|
||||
uint8_t _analyzer_us_reconnect_backoff_attempt; // 0..4 → 60s, 120s, 240s, 300000
|
||||
uint8_t _analyzer_eu_reconnect_backoff_attempt;
|
||||
|
||||
|
||||
// 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 ensureMainMqttClient(); // Create main MQTT client if _config_valid and _mqtt_client is null (e.g. after reinit)
|
||||
|
||||
// Internal methods - slot management
|
||||
void setupSlot(int index); // Create/destroy client for a slot based on its preset
|
||||
void teardownSlot(int index); // Disconnect and free slot resources
|
||||
void maintainSlotConnections(); // Maintain all slot connections (token renewal, reconnect)
|
||||
void maintainSlotConnection(int index, unsigned long now_millis, unsigned long current_time, bool time_synced);
|
||||
bool createSlotAuthToken(int index); // Create/renew JWT token for a slot
|
||||
bool publishToSlot(int index, const char* topic, const char* payload, bool retained = false);
|
||||
bool publishToAllSlots(const char* topic, const char* payload, bool retained = false);
|
||||
void publishStatusToSlot(int index);
|
||||
void updateCachedConnectionStatus();
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
void runCriticalMemoryCheckAndRecovery(); // Unified heap check, pressure timer, optional recovery
|
||||
void runCriticalMemoryCheckAndRecovery();
|
||||
#endif
|
||||
void recreateMqttClientsForFragmentationRecovery(); // Disconnect, delete, recreate all MQTT clients to recover max_alloc
|
||||
void connectToBrokers();
|
||||
void recreateMqttClientsForFragmentationRecovery();
|
||||
void processPacketQueue();
|
||||
bool publishStatus(); // Returns true if status was successfully published
|
||||
// Single place for WiFi monitoring: disconnect analyzers on drop, force reconnect with exponential backoff.
|
||||
// Returns true if we transitioned to connected this call (e.g. for NTP sync in loop()).
|
||||
bool handleWiFiConnection(unsigned long now);
|
||||
|
||||
|
||||
// FreeRTOS task function (runs on Core 0)
|
||||
#ifdef ESP_PLATFORM
|
||||
static void mqttTask(void* parameter);
|
||||
void mqttTaskLoop(); // Main loop for MQTT task
|
||||
void initializeWiFiInTask(); // WiFi initialization moved to task
|
||||
#endif
|
||||
void publishPacket(mesh::Packet* packet, bool is_tx,
|
||||
const uint8_t* raw_data = nullptr, int raw_len = 0,
|
||||
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 publishRaw(mesh::Packet* packet);
|
||||
void queuePacket(mesh::Packet* packet, bool is_tx);
|
||||
void dequeuePacket();
|
||||
bool isAnyBrokerConnected();
|
||||
void setBrokerDefaults();
|
||||
bool isAnySlotConnected();
|
||||
void syncTimeWithNTP();
|
||||
Timezone* createTimezoneFromString(const char* tz_string);
|
||||
bool isMQTTConfigValid();
|
||||
void checkConfigurationMismatch(); // Check for bridge.source/mqtt.tx mismatch
|
||||
bool isIATAValid() const; // Check if IATA code is configured
|
||||
|
||||
void checkConfigurationMismatch();
|
||||
bool isIATAValid() const;
|
||||
|
||||
void optimizeMqttClientConfig(PsychicMqttClient* client, bool needs_large_buffer = false);
|
||||
void getClientVersion(char* buffer, size_t buffer_size) const;
|
||||
void logMemoryStatus();
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs an MQTTBridge instance
|
||||
*
|
||||
* @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);
|
||||
|
||||
/**
|
||||
* Initializes the MQTT bridge
|
||||
*
|
||||
* - Sets up default broker configuration
|
||||
* - Initializes WiFi client
|
||||
* - Prepares MQTT clients for each broker
|
||||
*/
|
||||
void begin() override;
|
||||
|
||||
/**
|
||||
* Stops the MQTT bridge
|
||||
*
|
||||
* - Disconnects from all brokers
|
||||
* - Clears packet queue
|
||||
* - Releases resources
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* Time (millis()) when WiFi was last seen connected. 0 when disconnected or unknown.
|
||||
* Used by get wifi.status to report uptime when WITH_MQTT_BRIDGE is defined.
|
||||
*/
|
||||
static unsigned long getWifiConnectedAtMillis();
|
||||
|
||||
/**
|
||||
* Format an informative MQTT status line for get mqtt.status (msgs, broker, analyzers, queue).
|
||||
* Writes into buf, at most bufsize bytes. Requires bridge to have been initialized (begin() called).
|
||||
*/
|
||||
static void formatMqttStatusReply(char* buf, size_t bufsize, 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
|
||||
* - Processes packet queue
|
||||
* - Publishes status updates
|
||||
*/
|
||||
void loop() override;
|
||||
|
||||
/**
|
||||
* Called when a packet is received via mesh
|
||||
* Queues the packet for MQTT publishing if enabled
|
||||
*
|
||||
* @param packet The received mesh packet
|
||||
*/
|
||||
void onPacketReceived(mesh::Packet *packet) override;
|
||||
|
||||
/**
|
||||
* Called when a packet needs to be transmitted via MQTT
|
||||
* Publishes the packet to all connected brokers
|
||||
*
|
||||
* @param packet The mesh packet to transmit
|
||||
*/
|
||||
void sendPacket(mesh::Packet *packet) override;
|
||||
|
||||
/**
|
||||
* Configure MQTT broker settings
|
||||
* Configure a slot with a preset name. Call this when the user runs
|
||||
* "set mqttN.preset <name>". Handles teardown of old connection and
|
||||
* setup of new one.
|
||||
*
|
||||
* @param broker_index Broker index (0-2)
|
||||
* @param slot_index Slot index (0-2)
|
||||
* @param preset_name Preset name: "analyzer-us", "analyzer-eu", "meshmapper", "custom", "none"
|
||||
*/
|
||||
void setSlotPreset(int slot_index, const char* preset_name);
|
||||
|
||||
/**
|
||||
* Configure custom broker settings for a slot. Only applies when the
|
||||
* slot's preset is "custom".
|
||||
*
|
||||
* @param slot_index Slot index (0-2)
|
||||
* @param host Broker hostname
|
||||
* @param port Broker port
|
||||
* @param username MQTT username
|
||||
* @param password MQTT password
|
||||
* @param enabled Whether broker is enabled
|
||||
* @param username MQTT username (empty for anonymous)
|
||||
* @param password MQTT password (empty for anonymous)
|
||||
*/
|
||||
void setBroker(int broker_index, const char* host, uint16_t port,
|
||||
const char* username, const char* password, bool enabled);
|
||||
void setSlotCustomBroker(int slot_index, const char* host, uint16_t port,
|
||||
const char* username, const char* password);
|
||||
|
||||
/**
|
||||
* Set device origin name for MQTT topics
|
||||
*
|
||||
* @param origin Device name
|
||||
*/
|
||||
void setOrigin(const char* origin);
|
||||
|
||||
/**
|
||||
* Set IATA code for MQTT topics
|
||||
*
|
||||
* @param iata Airport code
|
||||
*/
|
||||
void setIATA(const char* iata);
|
||||
|
||||
/**
|
||||
* Set device public key for MQTT topics
|
||||
*
|
||||
* @param device_id Device public key (hex string)
|
||||
*/
|
||||
void setDeviceID(const char* device_id);
|
||||
|
||||
/**
|
||||
* Set firmware version for status messages
|
||||
*
|
||||
* @param firmware_version Firmware version string
|
||||
*/
|
||||
void setFirmwareVersion(const char* firmware_version);
|
||||
|
||||
/**
|
||||
* Set board model for status messages
|
||||
*
|
||||
* @param board_model Board model string
|
||||
*/
|
||||
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();
|
||||
bool publishToAnalyzerServers(const char* topic, const char* payload, bool retained = false); // Returns true if at least one publish succeeded
|
||||
|
||||
// PsychicMqttClient WebSocket methods
|
||||
void setupAnalyzerClients();
|
||||
void maintainAnalyzerConnections();
|
||||
bool publishToAnalyzerClient(PsychicMqttClient* client, const char* topic, const char* payload, bool retained = false); // Returns true if publish succeeded
|
||||
void publishStatusToAnalyzerClient(PsychicMqttClient* client, const char* server_name);
|
||||
|
||||
/**
|
||||
* Optimize MQTT client configuration for memory efficiency
|
||||
* Reduces buffer sizes to minimize memory usage while maintaining functionality
|
||||
*
|
||||
* @param client MQTT client to optimize
|
||||
* @param is_analyzer_client If true, uses larger buffer for JWT tokens (768 bytes)
|
||||
*/
|
||||
void optimizeMqttClientConfig(PsychicMqttClient* client, bool is_analyzer_client = false);
|
||||
|
||||
/**
|
||||
* Enable/disable message types
|
||||
*
|
||||
* @param status Enable status messages
|
||||
* @param packets Enable packet messages
|
||||
* @param raw Enable raw messages
|
||||
*/
|
||||
void setMessageTypes(bool status, bool packets, bool raw);
|
||||
|
||||
/**
|
||||
* Get connection status for all brokers
|
||||
*
|
||||
* @return Number of connected brokers
|
||||
*/
|
||||
int getConnectedBrokers() const;
|
||||
|
||||
/**
|
||||
* Get queue status
|
||||
*
|
||||
* @return Number of queued packets
|
||||
*/
|
||||
int getQueueSize() const;
|
||||
bool isReady() 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,
|
||||
static unsigned long getWifiConnectedAtMillis();
|
||||
static void formatMqttStatusReply(char* buf, size_t bufsize, const NodePrefs* prefs);
|
||||
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user