From 8ceaca34f78a8b12fa8c3bb8b01aad64e13bae9f Mon Sep 17 00:00:00 2001 From: Gerard Hickey Date: Tue, 21 Jul 2026 18:47:32 -0400 Subject: [PATCH 1/5] feat: Add MQTT bridging support Signed-off-by: Gerard Hickey --- examples/simple_repeater/MyMesh.cpp | 13 +- examples/simple_repeater/MyMesh.h | 10 +- src/helpers/CommonCLI.h | 2 +- src/helpers/bridges/MQTTBridge.cpp | 188 ++++++++++++++++++++ src/helpers/bridges/MQTTBridge.h | 79 ++++++++ variants/lilygo_tbeam_SX1262/platformio.ini | 30 ++++ 6 files changed, 315 insertions(+), 7 deletions(-) create mode 100644 src/helpers/bridges/MQTTBridge.cpp create mode 100644 src/helpers/bridges/MQTTBridge.h diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index b66e19522a..3360b7a665 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -199,8 +199,10 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender reply_data[8] = 0; // features #ifdef WITH_RS232_BRIDGE reply_data[8] |= 0x01; // is bridge, type UART -#elif WITH_ESPNOW_BRIDGE +#elif defined(WITH_ESPNOW_BRIDGE) reply_data[8] |= 0x03; // is bridge, type ESP-NOW +#elif defined(WITH_MQTT_BRIDGE) + reply_data[8] |= MQTT_BRIDGE; // is bridge, type MQTT #endif if (_prefs.disable_fwd) { // is this repeater currently disabled reply_data[8] |= 0x80; // is disabled @@ -344,7 +346,7 @@ int MyMesh::handleRequest(ClientInfo *sender, uint32_t sender_timestamp, uint8_t int results_offset = 0; uint8_t results_buffer[130]; for(int index = 0; index < count && index + offset < neighbours_count; index++){ - + // stop if we can't fit another entry in results int entry_size = pubkey_prefix_length + 4 + 1; if(results_offset + entry_size > sizeof(results_buffer)){ @@ -853,8 +855,9 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc anon_limiter(4, 180) // max 4 every 3 minutes #if defined(WITH_RS232_BRIDGE) , bridge(&_prefs, WITH_RS232_BRIDGE, _mgr, &rtc) -#endif -#if defined(WITH_ESPNOW_BRIDGE) +#elif defined(WITH_ESPNOW_BRIDGE) + , bridge(&_prefs, _mgr, &rtc) +#elif defined(WITH_MQTT_BRIDGE) , bridge(&_prefs, _mgr, &rtc) #endif { @@ -1148,7 +1151,7 @@ void MyMesh::formatRadioStatsReply(char *reply) { } void MyMesh::formatPacketStatsReply(char *reply) { - StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), + StatsFormatHelper::formatPacketStats(reply, radio_driver, getNumSentFlood(), getNumSentDirect(), getNumRecvFlood(), getNumRecvDirect()); } diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 0b2e7491b7..095c87ed0e 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -23,6 +23,11 @@ #define WITH_BRIDGE #endif +#ifdef WITH_MQTT_BRIDGE +#include "helpers/bridges/MQTTBridge.h" +#define WITH_BRIDGE +#endif + #include #include #include @@ -37,6 +42,7 @@ #ifdef WITH_BRIDGE extern AbstractBridge* bridge; +#define MQTT_BRIDGE 0x06 #endif struct RepeaterStats { @@ -117,6 +123,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { RS232Bridge bridge; #elif defined(WITH_ESPNOW_BRIDGE) ESPNowBridge bridge; +#elif defined(WITH_MQTT_BRIDGE) + MQTTBridge bridge; #endif void putNeighbour(const mesh::Identity& id, uint32_t timestamp, float snr); @@ -236,7 +244,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { { bridge.begin(); } - else + else { bridge.end(); } diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f3abcf4772..f236fbe0f6 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -6,7 +6,7 @@ #include #include -#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) +#if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_MQTT_BRIDGE) #define WITH_BRIDGE #endif diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp new file mode 100644 index 0000000000..0262623f11 --- /dev/null +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -0,0 +1,188 @@ +#include "MQTTBridge.h" + +#ifdef WITH_MQTT_BRIDGE + +#ifndef MQTT_HOST + #error "WITH_MQTT_BRIDGE requires MQTT_HOST to be defined" +#endif + +#ifndef WIFI_SSID + #warning "WITH_MQTT_BRIDGE: WIFI_SSID not defined — WiFi must be started externally before bridge.begin()" +#endif + +#include + +MQTTBridge *MQTTBridge::_instance = nullptr; + +MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc) + : BridgeBase(prefs, mgr, rtc), _mqttClient(_wifiClient), _lastReconnectAttempt(0) { + _instance = this; +} + +void MQTTBridge::begin() { +#if defined(WIFI_SSID) + if (WiFi.status() != WL_CONNECTED) { + WIFI_DEBUG_PRINTLN("Starting WiFi SSID=%s", WIFI_SSID); + WiFi.mode(WIFI_STA); + WiFi.setAutoReconnect(true); +#if defined(WIFI_PWD) + WiFi.begin(WIFI_SSID, WIFI_PWD); +#else + WiFi.begin(WIFI_SSID); +#endif + } +#else + if (WiFi.status() != WL_CONNECTED) { + WIFI_DEBUG_PRINTLN("WiFi not connected — will retry when WiFi is available"); + } +#endif + + _mqttClient.setServer(MQTT_HOST, MQTT_PORT); + _mqttClient.setCallback(mqttCallback); + + _initialized = true; + MQTT_DEBUG_PRINTLN("Initialized, broker=%s:%d topic=%s", MQTT_HOST, MQTT_PORT, MQTT_TOPIC); +} + +void MQTTBridge::end() { + if (_mqttClient.connected()) { + _mqttClient.disconnect(); + } + _initialized = false; + MQTT_DEBUG_PRINTLN("Stopped"); +} + +bool MQTTBridge::reconnect() { + if (WiFi.status() != WL_CONNECTED) { + MQTT_DEBUG_PRINTLN("WiFi not connected, skipping MQTT reconnect"); + return false; + } + + MQTT_DEBUG_PRINTLN("Connecting to %s:%d...", MQTT_HOST, MQTT_PORT); + + const char *clientId = "meshcore-bridge"; + bool ok; + +#if defined(MQTT_USERNAME) && defined(MQTT_PASSWORD) + ok = _mqttClient.connect(clientId, MQTT_USERNAME, MQTT_PASSWORD); +#elif defined(MQTT_USERNAME) + ok = _mqttClient.connect(clientId, MQTT_USERNAME, nullptr); +#else + ok = _mqttClient.connect(clientId); +#endif + + if (ok) { + _mqttClient.subscribe(MQTT_TOPIC); + MQTT_DEBUG_PRINTLN("Connected, subscribed to %s", MQTT_TOPIC); + } else { + MQTT_DEBUG_PRINTLN("Connect failed, rc=%d", _mqttClient.state()); + } + return ok; +} + +void MQTTBridge::loop() { + if (!_initialized) return; + + if (!_mqttClient.connected()) { + unsigned long now = millis(); + // Retry every 5 seconds + if (now - _lastReconnectAttempt >= 5000) { + _lastReconnectAttempt = now; + reconnect(); + } + } else { + _mqttClient.loop(); + } +} + +void MQTTBridge::sendPacket(mesh::Packet *packet) { + if (!_initialized || !packet) return; + + if (!_mqttClient.connected()) return; + + if (!_seen_packets.hasSeen(packet)) { + uint8_t buf[MAX_TRANS_UNIT + 1]; + uint16_t len = packet->writeTo(buf); + + if (len == 0 || len > sizeof(buf)) { + BRIDGE_DEBUG_PRINTLN("TX invalid packet length %d", len); + return; + } + + // Encode as uppercase hex + static const char *hex_chars = "0123456789ABCDEF"; + for (uint16_t i = 0; i < len; i++) { + _hexBuf[i * 2] = hex_chars[(buf[i] >> 4) & 0x0F]; + _hexBuf[i * 2 + 1] = hex_chars[buf[i] & 0x0F]; + } + _hexBuf[len * 2] = '\0'; + + if (_mqttClient.publish(MQTT_TOPIC, _hexBuf)) { + MQTT_DEBUG_PRINTLN("TX len=%d hex_len=%d", len, len * 2); + } else { + MQTT_DEBUG_PRINTLN("TX publish failed len=%d", len); + } + } +} + +void MQTTBridge::onPacketReceived(mesh::Packet *packet) { + handleReceivedPacket(packet); +} + +void MQTTBridge::mqttCallback(char *topic, uint8_t *payload, unsigned int length) { + if (_instance) { + _instance->onMqttMessage(topic, payload, length); + } +} + +void MQTTBridge::onMqttMessage(char *topic, uint8_t *payload, unsigned int length) { + if (!_initialized) return; + + // Expect even number of hex chars + if (length == 0 || length % 2 != 0) { + MQTT_DEBUG_PRINTLN("RX invalid hex length %u", length); + return; + } + + uint16_t byte_len = length / 2; + if (byte_len > MAX_TRANS_UNIT + 1) { + MQTT_DEBUG_PRINTLN("RX packet too large %u bytes", byte_len); + return; + } + + // Decode hex into a temporary buffer + uint8_t buf[MAX_TRANS_UNIT + 1]; + for (uint16_t i = 0; i < byte_len; i++) { + char hi = (char)payload[i * 2]; + char lo = (char)payload[i * 2 + 1]; + + uint8_t hi_val, lo_val; + if (hi >= '0' && hi <= '9') hi_val = hi - '0'; + else if (hi >= 'A' && hi <= 'F') hi_val = hi - 'A' + 10; + else if (hi >= 'a' && hi <= 'f') hi_val = hi - 'a' + 10; + else { MQTT_DEBUG_PRINTLN("RX invalid hex char '%c'", hi); return; } + + if (lo >= '0' && lo <= '9') lo_val = lo - '0'; + else if (lo >= 'A' && lo <= 'F') lo_val = lo - 'A' + 10; + else if (lo >= 'a' && lo <= 'f') lo_val = lo - 'a' + 10; + else { MQTT_DEBUG_PRINTLN("RX invalid hex char '%c'", lo); return; } + + buf[i] = (hi_val << 4) | lo_val; + } + + mesh::Packet *pkt = _mgr->allocNew(); + if (!pkt) { + MQTT_DEBUG_PRINTLN("RX alloc failed"); + return; + } + + if (pkt->readFrom(buf, byte_len)) { + MQTT_DEBUG_PRINTLN("RX len=%u", byte_len); + onPacketReceived(pkt); + } else { + MQTT_DEBUG_PRINTLN("RX parse failed len=%u", byte_len); + _mgr->free(pkt); + } +} + +#endif diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h new file mode 100644 index 0000000000..61e89a9771 --- /dev/null +++ b/src/helpers/bridges/MQTTBridge.h @@ -0,0 +1,79 @@ +#pragma once + +#ifdef WITH_MQTT_BRIDGE + +#ifndef MQTT_PORT + #define MQTT_PORT 1883 +#endif + +#ifndef MQTT_TOPIC + #define MQTT_TOPIC "meshcore/packets" +#endif + +// MQTT_MAX_PACKET_SIZE must fit: MQTT headers + topic + hex payload +// Max hex payload = (MAX_TRANS_UNIT+1)*2 = 370 chars; add topic and MQTT overhead +#ifndef MQTT_MAX_PACKET_SIZE + #define MQTT_MAX_PACKET_SIZE 512 +#endif + +#include "helpers/bridges/BridgeBase.h" +#include +#include + +#if MQTT_DEBUG && ARDUINO + #define MQTT_DEBUG_PRINTLN(F, ...) Serial.printf("%s MQTT: " F, getLogDateTime(), ##__VA_ARGS__) +#else + #define MQTT_DEBUG_PRINTLN(...) {} +#endif + +/** + * @brief Bridge implementation using MQTT over WiFi for packet transport + * + * Publishes and subscribes to an MQTT topic, bridging LoRa mesh packets + * to/from the MQTT broker. Packets are encoded as uppercase ASCII hex strings. + * + * Configuration build defines: + * WITH_MQTT_BRIDGE — enable this bridge + * MQTT_HOST — broker hostname or IP (required) + * MQTT_PORT — broker port (default 1883) + * MQTT_USERNAME — broker username (optional, omit to disable auth) + * MQTT_PASSWORD — broker password (optional) + * MQTT_TOPIC — topic to publish/subscribe (default "meshcore/packets") + * MQTT_DEBUG — set to 1 to enable debug output on Serial + * + * WiFi must be configured separately via WIFI_SSID / WIFI_PWD build defines, + * or by calling WiFi.begin() before this bridge's begin() is invoked. The + * bridge will not attempt an MQTT connection until WiFi is associated. + * + * Packet format: raw wire bytes encoded as uppercase hex, e.g. + * "1180EF4C22171A42..." + * Incoming messages on the topic are decoded and injected into the mesh. + * Duplicate detection via SimpleMeshTables prevents re-forwarding seen packets. + */ +class MQTTBridge : public BridgeBase { +public: + MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc); + + void begin() override; + void end() override; + void loop() override; + void sendPacket(mesh::Packet *packet) override; + void onPacketReceived(mesh::Packet *packet) override; + +private: + static MQTTBridge *_instance; + + WiFiClient _wifiClient; + PubSubClient _mqttClient; + unsigned long _lastReconnectAttempt; + + // Buffer sized for maximum hex-encoded mesh packet + static constexpr size_t HEX_BUF_SIZE = (MAX_TRANS_UNIT + 1) * 2 + 1; + char _hexBuf[HEX_BUF_SIZE]; + + static void mqttCallback(char *topic, uint8_t *payload, unsigned int length); + void onMqttMessage(char *topic, uint8_t *payload, unsigned int length); + bool reconnect(); +}; + +#endif diff --git a/variants/lilygo_tbeam_SX1262/platformio.ini b/variants/lilygo_tbeam_SX1262/platformio.ini index 62ac09f875..fc43402c01 100644 --- a/variants/lilygo_tbeam_SX1262/platformio.ini +++ b/variants/lilygo_tbeam_SX1262/platformio.ini @@ -104,6 +104,36 @@ lib_deps = ; ${LilyGo_TBeam_SX1262.lib_deps} ; ${esp32_ota.lib_deps} +[env:Tbeam_SX1262_repeater_bridge_mqtt] +extends = LilyGo_TBeam_SX1262 +build_flags = + ${LilyGo_TBeam_SX1262.build_flags} + -D ADVERT_NAME='"Tbeam SX1262 Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D MESH_PACKET_LOGGING=1 + -D MESH_DEBUG=1 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_HOST='"mqtt.fqdn"' + -D MQTT_PORT=1883 + -D MQTT_USERNAME='"username"' + -D MQTT_PASSWORD='"password"' + -D MQTT_TOPIC='"meshcore/packets"' + -D MQTT_DEBUG=1 + -D WIFI_DEBUG_LOGGING=1 + -D WIFI_SSID='"myssid"' + -D WIFI_PWD='"mypasswd"' + -D BRIDGE_DEBUG=1 +build_src_filter = ${LilyGo_TBeam_SX1262.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam_SX1262.lib_deps} + ${esp32_ota.lib_deps} + knolleary/PubSubClient @ ^2.8 + [env:Tbeam_SX1262_repeater_bridge_espnow] extends = LilyGo_TBeam_SX1262 build_flags = From 679955a8229b4c3e5cfa9374397f821b82f04e32 Mon Sep 17 00:00:00 2001 From: Gerard Hickey Date: Wed, 22 Jul 2026 00:35:03 -0400 Subject: [PATCH 2/5] Add mqtt command to CLI Signed-off-by: Gerard Hickey --- examples/simple_repeater/MyMesh.cpp | 4 +-- src/helpers/CommonCLI.cpp | 42 ++++++++++++++++++++++++++--- src/helpers/CommonCLI.h | 6 +++-- src/helpers/bridges/MQTTBridge.cpp | 38 ++++++++++++++++++-------- src/helpers/bridges/MQTTBridge.h | 12 +++++++-- 5 files changed, 82 insertions(+), 20 deletions(-) diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 3360b7a665..23bfdf4c6a 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -849,7 +849,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc mesh::RTCClock &rtc, mesh::MeshTables &tables) : mesh::Mesh(radio, ms, rng, rtc, *new StaticPoolPacketManager(32), tables), region_map(key_store), temp_map(key_store), - _cli(board, rtc, sensors, region_map, acl, &_prefs, this), + _cli(board, rtc, bridge, sensors, region_map, acl, &_prefs, this), telemetry(MAX_PACKET_PAYLOAD - 4), discover_limiter(4, 120), // max 4 every 2 minutes anon_limiter(4, 180) // max 4 every 3 minutes @@ -858,7 +858,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) + , bridge(&_prefs, _mgr, &rtc, self_id.pub_key) #endif { last_millis = 0; diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c95e3e34b0..67a716e1f6 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -5,6 +5,10 @@ #include "TxtDataHelpers.h" #include +#ifdef WITH_MQTT_BRIDGE + #include +#endif + #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 #endif @@ -654,7 +658,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "OK"); } else { strcpy(reply, "Error, max 64"); - } + } } else if (memcmp(config, "flood.max.advert ", 17) == 0) { uint8_t m = atoi(&config[17]); if (m <= 64) { @@ -779,6 +783,28 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep _callbacks->restartBridge(); savePrefs(); strcpy(reply, "OK"); +#endif +#ifdef WITH_MQTT_BRIDGE + } else if (memcmp(config, "mqtt.host ", 10) == 0) { + static_cast(_bridge)->end(); + StrHelper::strncpy(static_cast(_bridge)->mqtt_host, + &config[10], 64); + static_cast(_bridge)->reconnect(); + strcpy(reply, "OK"); + } else if (memcmp(config, "mqtt.port ", 10) == 0) { + static_cast(_bridge)->end(); + static_cast(_bridge)->mqtt_port = atoi(&config[10]); + static_cast(_bridge)->reconnect(); + strcpy(reply, "OK"); + } else if (memcmp(config, "mqtt.topic ", 11) == 0) { + static_cast(_bridge)->end(); + StrHelper::strncpy(static_cast(_bridge)->mqtt_topic, + &config[11], 64); + if (static_cast(_bridge)->reconnect()) { + strcpy(reply, "OK"); + } else { + strcpy(reply, "Error: failed to subscribe to new topic"); + } #endif } else if (memcmp(config, "adc.multiplier ", 15) == 0) { _prefs->adc_multiplier = atof(&config[15]); @@ -899,6 +925,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep "rs232" #elif WITH_ESPNOW_BRIDGE "espnow" +#elif WITH_MQTT_BRIDGE + "mqtt" #else "none" #endif @@ -920,6 +948,14 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep sprintf(reply, "> %d", (uint32_t)_prefs->bridge_channel); } else if (memcmp(config, "bridge.secret", 13) == 0) { sprintf(reply, "> %s", _prefs->bridge_secret); +#endif +#ifdef WITH_MQTT_BRIDGE + } else if (memcmp(config, "mqtt.host", 9) == 0) { + sprintf(reply, "> %s", static_cast(_bridge)->mqtt_host); + } else if (memcmp(config, "mqtt.port", 9) == 0) { + sprintf(reply, "> %s", static_cast(_bridge)->mqtt_port); + } else if (memcmp(config, "mqtt.topic", 10) == 0) { + sprintf(reply, "> %s", static_cast(_bridge)->mqtt_topic); #endif } else if (memcmp(config, "bootloader.ver", 14) == 0) { #ifdef NRF52_PLATFORM @@ -1138,7 +1174,7 @@ void CommonCLI::handleRegionCmd(char* command, char* reply) { } else if (n >= 3 && strcmp(parts[1], "list") == 0) { uint8_t mask = 0; bool invert = false; - + if (strcmp(parts[2], "allowed") == 0) { mask = REGION_DENY_FLOOD; invert = false; // list regions that DON'T have DENY flag @@ -1149,7 +1185,7 @@ void CommonCLI::handleRegionCmd(char* command, char* reply) { strcpy(reply, "Err - use 'allowed' or 'denied'"); return; } - + int len = _region_map->exportNamesTo(reply, 160, mask, invert); if (len == 0) { strcpy(reply, "-none-"); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f236fbe0f6..b4f0a97486 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -1,6 +1,7 @@ #pragma once #include "Mesh.h" +#include "AbstractBridge.h" #include #include #include @@ -121,6 +122,7 @@ class CommonCLI { NodePrefs* _prefs; CommonCLICallbacks* _callbacks; mesh::MainBoard* _board; + AbstractBridge* _bridge; SensorManager* _sensors; RegionMap* _region_map; ClientACL* _acl; @@ -135,8 +137,8 @@ class CommonCLI { void handleSetCmd(uint32_t sender_timestamp, char* command, char* reply); public: - CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks) - : _board(&board), _rtc(&rtc), _sensors(&sensors), _region_map(®ion_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { } + CommonCLI(mesh::MainBoard& board, mesh::RTCClock& rtc, AbstractBridge& bridge, SensorManager& sensors, RegionMap& region_map, ClientACL& acl, NodePrefs* prefs, CommonCLICallbacks* callbacks) + : _board(&board), _rtc(&rtc), _bridge(&bridge), _sensors(&sensors), _region_map(®ion_map), _acl(&acl), _prefs(prefs), _callbacks(callbacks) { } void loadPrefs(FILESYSTEM* _fs); void savePrefs(FILESYSTEM* _fs); diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 0262623f11..3b7a2d31c6 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -1,4 +1,8 @@ #include "MQTTBridge.h" +#include "Identity.h" +#include +#include +#include #ifdef WITH_MQTT_BRIDGE @@ -14,8 +18,8 @@ MQTTBridge *MQTTBridge::_instance = nullptr; -MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc) - : BridgeBase(prefs, mgr, rtc), _mqttClient(_wifiClient), _lastReconnectAttempt(0) { +MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, const uint8_t *pubKey) + : BridgeBase(prefs, mgr, rtc), _mqttClient(_wifiClient), _lastReconnectAttempt(0), _pubKey(pubKey) { _instance = this; } @@ -37,11 +41,21 @@ void MQTTBridge::begin() { } #endif - _mqttClient.setServer(MQTT_HOST, MQTT_PORT); + // ensure that all strings are null terminated after the copy + mqtt_host[65] = _mqtt_username[33] = _mqtt_password[65] = mqtt_topic[65] = '\0'; + + // save MQTT settings for access from CLI + StrHelper::strzcpy(mqtt_host, MQTT_HOST, 64); + mqtt_port = MQTT_PORT; + StrHelper::strzcpy(_mqtt_username, MQTT_USERNAME, 32); + StrHelper::strzcpy(_mqtt_password, MQTT_PASSWORD, 64); + StrHelper::strzcpy(mqtt_topic, MQTT_TOPIC, 64); + + _mqttClient.setServer(mqtt_host, mqtt_port); _mqttClient.setCallback(mqttCallback); _initialized = true; - MQTT_DEBUG_PRINTLN("Initialized, broker=%s:%d topic=%s", MQTT_HOST, MQTT_PORT, MQTT_TOPIC); + MQTT_DEBUG_PRINTLN("Initialized, broker=%s:%d topic=%s", mqtt_host, mqtt_port, mqtt_topic); } void MQTTBridge::end() { @@ -58,22 +72,24 @@ bool MQTTBridge::reconnect() { return false; } - MQTT_DEBUG_PRINTLN("Connecting to %s:%d...", MQTT_HOST, MQTT_PORT); + MQTT_DEBUG_PRINTLN("Connecting to %s:%d...", mqtt_host, mqtt_port); - const char *clientId = "meshcore-bridge"; + char clientId[32]; // "meshcore-mqtt-bridge-" (21) + 6 hex chars + null + snprintf(clientId, sizeof(clientId), "meshcore-mqtt-bridge-%02X%02X%02X", + _pubKey[0], _pubKey[1], _pubKey[2]); bool ok; #if defined(MQTT_USERNAME) && defined(MQTT_PASSWORD) - ok = _mqttClient.connect(clientId, MQTT_USERNAME, MQTT_PASSWORD); + ok = _mqttClient.connect(clientId, _mqtt_username, _mqtt_password); #elif defined(MQTT_USERNAME) - ok = _mqttClient.connect(clientId, MQTT_USERNAME, nullptr); + ok = _mqttClient.connect(clientId, _mqtt_username, nullptr); #else ok = _mqttClient.connect(clientId); #endif if (ok) { - _mqttClient.subscribe(MQTT_TOPIC); - MQTT_DEBUG_PRINTLN("Connected, subscribed to %s", MQTT_TOPIC); + _mqttClient.subscribe(mqtt_topic); + MQTT_DEBUG_PRINTLN("Connected, subscribed to %s", mqtt_topic); } else { MQTT_DEBUG_PRINTLN("Connect failed, rc=%d", _mqttClient.state()); } @@ -117,7 +133,7 @@ void MQTTBridge::sendPacket(mesh::Packet *packet) { } _hexBuf[len * 2] = '\0'; - if (_mqttClient.publish(MQTT_TOPIC, _hexBuf)) { + if (_mqttClient.publish(mqtt_topic, _hexBuf)) { MQTT_DEBUG_PRINTLN("TX len=%d hex_len=%d", len, len * 2); } else { MQTT_DEBUG_PRINTLN("TX publish failed len=%d", len); diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 61e89a9771..0d273301b7 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -52,13 +52,18 @@ */ class MQTTBridge : public BridgeBase { public: - MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc); + MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, const uint8_t *pubKey); + + char mqtt_host[65]; + uint mqtt_port; + char mqtt_topic[65]; void begin() override; void end() override; void loop() override; void sendPacket(mesh::Packet *packet) override; void onPacketReceived(mesh::Packet *packet) override; + bool reconnect(); private: static MQTTBridge *_instance; @@ -66,6 +71,10 @@ class MQTTBridge : public BridgeBase { WiFiClient _wifiClient; PubSubClient _mqttClient; unsigned long _lastReconnectAttempt; + const uint8_t *_pubKey; + + char _mqtt_username[33]; + char _mqtt_password[65]; // Buffer sized for maximum hex-encoded mesh packet static constexpr size_t HEX_BUF_SIZE = (MAX_TRANS_UNIT + 1) * 2 + 1; @@ -73,7 +82,6 @@ class MQTTBridge : public BridgeBase { static void mqttCallback(char *topic, uint8_t *payload, unsigned int length); void onMqttMessage(char *topic, uint8_t *payload, unsigned int length); - bool reconnect(); }; #endif From f3fd749f19667c79b143e337b16d35bc0611785f Mon Sep 17 00:00:00 2001 From: Gerard Hickey Date: Wed, 22 Jul 2026 00:35:56 -0400 Subject: [PATCH 3/5] Add mqtt CLI commands to documentation Signed-off-by: Gerard Hickey --- docs/cli_commands.md | 129 ++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 45 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index 5598dba550..fef236f59e 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -20,13 +20,14 @@ This document provides an overview of CLI commands that can be sent to MeshCore - [Sensors](#sensors-when-sensor-support-is-compiled-in) - [Bridge](#bridge-when-bridge-support-is-compiled-in) - [Ethernet](#ethernet-when-ethernet-support-is-compiled-in) +- [Board Status](#board-status) --- ## Operational ### Reboot the node -**Usage:** +**Usage:** - `reboot` **Note:** No reply is sent. @@ -51,7 +52,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- ### Sync the clock with the remote device -**Usage:** +**Usage:** - `clock sync` --- @@ -63,7 +64,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- ### Set the time to a specific timestamp -**Usage:** +**Usage:** - `time ` **Parameters:** @@ -72,7 +73,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- ### Send a flood advert -**Usage:** +**Usage:** - `advert` --- @@ -102,7 +103,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore ## Neighbors (Repeater Only) ### List nearby neighbors -**Usage:** +**Usage:** - `neighbors` **Note:** The output of this command is limited to the 8 most recent adverts. @@ -112,10 +113,10 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- ### Remove a neighbor -**Usage:** +**Usage:** - `neighbor.remove ` -**Parameters:** +**Parameters:** - `pubkey_prefix`: The public key of the node to remove from the neighbors list. This can be a short prefix or the full key. All neighbors matching the provided prefix will be removed. **Note:** You can remove all neighbors by sending a space character as the prefix. The space indicates an empty prefix, which matches all existing neighbors. @@ -124,7 +125,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Discover zero hop neighbors -**Usage:** +**Usage:** - `discover.neighbors` --- @@ -137,7 +138,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- ### System Stats - Battery, Uptime, Queue Length and Debug Flags -**Usage:** +**Usage:** - `stats-core` **Serial Only:** Yes @@ -234,7 +235,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### Change the radio parameters for a set duration -**Usage:** +**Usage:** - `tempradio ,,,,` **Parameters:** @@ -435,7 +436,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `powersaving on` - `powersaving off` -**Parameters:** +**Parameters:** - `on`: enable power saving - `off`: disable power saving @@ -469,13 +470,13 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `0`: 1 Byte hash size (256 unique ids)[64 max flood] - `1`: 2 Byte hash size (65,536 unique ids)[32 max flood] - `2`: 3 Byte hash size (16,777,216 unique ids)[21 max flood] - - `3`: DO NOT USE (Reserved) + - `3`: DO NOT USE (Reserved) **Default:** `0` **Note:** the 'path.hash.mode' sets the low-level ID/hash encoding size used when the repeater adverts. This setting has no impact on what packet ID/hash size this repeater forwards, all sizes should be forwarded on firmware >= 1.14. This feature was added in firmware 1.14 -**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. +**Temporary Note:** adverts with ID/hash sizes of 2 or 3 bytes may have limited flood propagation in your network while this feature is new as v1.13.0 firmware and older will drop packets with multibyte path ID/hashes as only 1-byte hashes are supported. Consider your install base of firmware >=1.14 has reached a criticality for effective network flooding before implementing higher ID/hash sizes. --- @@ -485,17 +486,17 @@ This document provides an overview of CLI commands that can be sent to MeshCore - `set loop.detect ` **Parameters:** -- `state`: +- `state`: - `off`: no loop detection is performed - `minimal`: packets are dropped if repeater's ID/hash appears 4 or more times (1-byte), 2 or more (2-byte), 1 or more (3-byte) - `moderate`: packets are dropped if repeater's ID/hash appears 2 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) - `strict`: packets are dropped if repeater's ID/hash appears 1 or more times (1-byte), 1 or more (2-byte), 1 or more (3-byte) - + **Default:** `off` **Note:** When it is enabled, repeaters will now reject flood packets which look like they are in a loop. This has been happening recently in some meshes when there is just a single 'bad' repeater firmware out there (probably some forked or custom firmware). If the payload is messed with, then forwarded, the same packet ends up causing a packet storm, repeated up to the max 64 hops. This feature was added in firmware 1.14 -**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. +**Example:** If preference is `loop.detect minimal`, and a 1-byte path size packet is received, the repeater will see if its own ID/hash is already in the path. If it's already encoded 4 times, it will reject the packet. If the packet uses 2-byte path size, and repeater's own ID/hash is already encoded 2 times, it rejects. If the packet uses 3-byte path size, and the repeater's own ID/hash is already encoded 1 time, it rejects. --- @@ -695,12 +696,12 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### ACL #### Add, update or remove permissions for a companion -**Usage:** +**Usage:** - `setperm ` **Parameters:** - `pubkey`: Companion public key -- `permissions`: +- `permissions`: - `0`: Guest - `1`: Read-only - `2`: Read-write @@ -711,7 +712,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### View the current ACL -**Usage:** +**Usage:** - `get acl` **Serial Only:** Yes @@ -733,7 +734,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore ### Region Management (v1.10.+) #### Bulk-load region lists -**Usage:** +**Usage:** - `region load` - `region load [flood_flag]` @@ -749,16 +750,16 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### Save any changes to regions made since reboot -**Usage:** +**Usage:** - `region save` --- #### Allow a region -**Usage:** +**Usage:** - `region allowf ` -**Parameters:** +**Parameters:** - `name`: Region name (or `*` for wildcard) **Note:** Setting on wildcard `*` allows packets without region transport codes @@ -766,10 +767,10 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### Block a region -**Usage:** +**Usage:** - `region denyf ` -**Parameters:** +**Parameters:** - `name`: Region name (or `*` for wildcard) **Note:** Setting on wildcard `*` drops packets without region transport codes @@ -777,7 +778,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### Show information for a region -**Usage:** +**Usage:** - `region get ` **Parameters:** @@ -786,7 +787,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### View or change the home region for this node -**Usage:** +**Usage:** - `region home` - `region home ` @@ -796,7 +797,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### View or change the default scope region for this node -**Usage:** +**Usage:** - `region default` - `region default {name|}` @@ -806,7 +807,7 @@ This document provides an overview of CLI commands that can be sent to MeshCore --- #### Create a new region -**Usage:** +**Usage:** - `region put [parent_name]` **Parameters:** @@ -857,18 +858,18 @@ region save --- #### Remove a region -**Usage:** +**Usage:** - `region remove ` **Parameters:** - `name`: Region name -**Note:** Must remove all child regions before the region can be removed +**Note:** Must remove all child regions before the region can be removed --- #### View all regions -**Usage:** +**Usage:** - `region list ` **Serial Only:** Yes @@ -881,7 +882,7 @@ region save --- #### Dump all defined regions and flood permissions -**Usage:** +**Usage:** - `region` **Serial Only:** For firmware older than 1.12.0 @@ -906,7 +907,7 @@ region save **Example 2: Using Wildcard with F Flag** ``` -region load +region load * F region save @@ -921,7 +922,7 @@ region save **Example 3: Using Wildcard Without F Flag** ``` -region load +region load * region save @@ -935,7 +936,7 @@ region save **Example 4: Nested Public Region with F Flag** ``` -region load +region load #Europe F #UK #London @@ -956,7 +957,7 @@ region save **Example 5: Wildcard with Nested Public Regions** ``` -region load +region load * F #NorthAmerica #USA @@ -995,13 +996,13 @@ region save --- #### Sync this node's clock with GPS time -**Usage:** +**Usage:** - `gps sync` --- #### Set this node's location based on the GPS coordinates -**Usage:** +**Usage:** - `gps setloc` --- @@ -1011,8 +1012,8 @@ region save - `gps advert` - `gps advert ` -**Parameters:** -- `policy`: `none`|`share`|`prefs` +**Parameters:** +- `policy`: `none`|`share`|`prefs` - `none`: don't include location in adverts - `share`: share gps location (from SensorManager) - `prefs`: location stored in node's lat and lon settings @@ -1034,7 +1035,7 @@ region save --- #### View or change the value of a sensor -**Usage:** +**Usage:** - `sensor get ` - `sensor set ` @@ -1081,7 +1082,7 @@ region save - `set bridge.source ` **Parameters:** -- `source`: +- `source`: - `logRx`: bridges received packets - `logTx`: bridges transmitted packets @@ -1111,8 +1112,8 @@ region save --- -#### Set the ESP-Now secret -**Usage:** +#### Set the ESP-Now secret (ESPNow only) +**Usage:** - `get bridge.secret` - `set bridge.secret ` @@ -1123,6 +1124,44 @@ region save --- +#### Set MQTT host (MQTT bridge only) +**Usage:** +- `get mqtt.host` +- `set mqtt.host ` + +**Parameters:** +- `host`: Hostname or IP of MQTT server + +**Default:** Set at compile time. If changed reverts to compile time value. + +--- + +#### Set MQTT port (MQTT bridge only) +**Usage:** +- `get mqtt.port` +- `set mqtt.port ` + +**Parameters:** +- `port_number`: MQTT server port for connection + +**Default:** Set at compile time. If changed reverts to compile time value. + +--- + +#### Set MQTT topic (MQTT bridge only) +**Usage:** +- `get mqtt.topic` +- `set mqtt.topic ` + +**Parameters:** +- `new_topic`: MQTT topic for publication and subscription + +**Default:** Set at compile time. If changed reverts to compile time value. + +--- + +## Board Status + #### View the bootloader version (nRF52 only) **Usage:** `get bootloader.ver` From 7c39190b9cae3183f4b17857aaf8d3274536643b Mon Sep 17 00:00:00 2001 From: Gerard Hickey Date: Wed, 22 Jul 2026 08:26:43 -0400 Subject: [PATCH 4/5] Properly reinitialize MQTT connection after changes Signed-off-by: Gerard Hickey --- src/helpers/CommonCLI.cpp | 3 +++ src/helpers/bridges/MQTTBridge.cpp | 27 +++++++++++++++++---------- src/helpers/bridges/MQTTBridge.h | 11 ++++++----- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 67a716e1f6..2aa497ab5a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -789,17 +789,20 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep static_cast(_bridge)->end(); StrHelper::strncpy(static_cast(_bridge)->mqtt_host, &config[10], 64); + static_cast(_bridge)->initialize(); static_cast(_bridge)->reconnect(); strcpy(reply, "OK"); } else if (memcmp(config, "mqtt.port ", 10) == 0) { static_cast(_bridge)->end(); static_cast(_bridge)->mqtt_port = atoi(&config[10]); + static_cast(_bridge)->initialize(); static_cast(_bridge)->reconnect(); strcpy(reply, "OK"); } else if (memcmp(config, "mqtt.topic ", 11) == 0) { static_cast(_bridge)->end(); StrHelper::strncpy(static_cast(_bridge)->mqtt_topic, &config[11], 64); + static_cast(_bridge)->initialize(); if (static_cast(_bridge)->reconnect()) { strcpy(reply, "OK"); } else { diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index 3b7a2d31c6..bcd1c30397 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -41,16 +41,17 @@ void MQTTBridge::begin() { } #endif - // ensure that all strings are null terminated after the copy - mqtt_host[65] = _mqtt_username[33] = _mqtt_password[65] = mqtt_topic[65] = '\0'; - // save MQTT settings for access from CLI - StrHelper::strzcpy(mqtt_host, MQTT_HOST, 64); + StrHelper::strzcpy(mqtt_host, MQTT_HOST, sizeof(mqtt_host)); mqtt_port = MQTT_PORT; - StrHelper::strzcpy(_mqtt_username, MQTT_USERNAME, 32); - StrHelper::strzcpy(_mqtt_password, MQTT_PASSWORD, 64); - StrHelper::strzcpy(mqtt_topic, MQTT_TOPIC, 64); + StrHelper::strzcpy(_mqtt_username, MQTT_USERNAME, sizeof(_mqtt_username)); + StrHelper::strzcpy(_mqtt_password, MQTT_PASSWORD, sizeof(_mqtt_password)); + StrHelper::strzcpy(mqtt_topic, MQTT_TOPIC, sizeof(mqtt_topic)); + + initialize(); +} +void MQTTBridge::initialize() { _mqttClient.setServer(mqtt_host, mqtt_port); _mqttClient.setCallback(mqttCallback); @@ -112,16 +113,22 @@ void MQTTBridge::loop() { } void MQTTBridge::sendPacket(mesh::Packet *packet) { - if (!_initialized || !packet) return; + if (!_initialized || !packet) { + MQTT_DEBUG_PRINTLN("connection not initialized"); + return; + } - if (!_mqttClient.connected()) return; + if (!_mqttClient.connected()) { + MQTT_DEBUG_PRINTLN("connection is not connected"); + return; + } if (!_seen_packets.hasSeen(packet)) { uint8_t buf[MAX_TRANS_UNIT + 1]; uint16_t len = packet->writeTo(buf); if (len == 0 || len > sizeof(buf)) { - BRIDGE_DEBUG_PRINTLN("TX invalid packet length %d", len); + MQTT_DEBUG_PRINTLN("TX invalid packet length %d", len); return; } diff --git a/src/helpers/bridges/MQTTBridge.h b/src/helpers/bridges/MQTTBridge.h index 0d273301b7..1311e2c50e 100644 --- a/src/helpers/bridges/MQTTBridge.h +++ b/src/helpers/bridges/MQTTBridge.h @@ -21,7 +21,7 @@ #include #if MQTT_DEBUG && ARDUINO - #define MQTT_DEBUG_PRINTLN(F, ...) Serial.printf("%s MQTT: " F, getLogDateTime(), ##__VA_ARGS__) + #define MQTT_DEBUG_PRINTLN(F, ...) Serial.printf("%s MQTT: " F "\n", getLogDateTime(), ##__VA_ARGS__) #else #define MQTT_DEBUG_PRINTLN(...) {} #endif @@ -54,15 +54,16 @@ class MQTTBridge : public BridgeBase { public: MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc, const uint8_t *pubKey); - char mqtt_host[65]; + char mqtt_host[64]; uint mqtt_port; - char mqtt_topic[65]; + char mqtt_topic[64]; void begin() override; void end() override; void loop() override; void sendPacket(mesh::Packet *packet) override; void onPacketReceived(mesh::Packet *packet) override; + void initialize(); bool reconnect(); private: @@ -73,8 +74,8 @@ class MQTTBridge : public BridgeBase { unsigned long _lastReconnectAttempt; const uint8_t *_pubKey; - char _mqtt_username[33]; - char _mqtt_password[65]; + char _mqtt_username[32]; + char _mqtt_password[64]; // Buffer sized for maximum hex-encoded mesh packet static constexpr size_t HEX_BUF_SIZE = (MAX_TRANS_UNIT + 1) * 2 + 1; From 96cc91dc0b063fdd1f116cafd0a9b437c1d9a6b3 Mon Sep 17 00:00:00 2001 From: Gerard Hickey Date: Wed, 22 Jul 2026 10:04:33 -0400 Subject: [PATCH 5/5] Add support for bridge source of rx and tx Signed-off-by: Gerard Hickey --- docs/cli_commands.md | 1 + examples/simple_repeater/MyMesh.cpp | 6 ++--- src/helpers/CommonCLI.cpp | 22 +++++++++++++--- src/helpers/CommonCLI.h | 4 ++- src/helpers/bridges/MQTTBridge.cpp | 39 ++++++++++++++--------------- 5 files changed, 44 insertions(+), 28 deletions(-) diff --git a/docs/cli_commands.md b/docs/cli_commands.md index fef236f59e..2bf5512b9a 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -1085,6 +1085,7 @@ region save - `source`: - `logRx`: bridges received packets - `logTx`: bridges transmitted packets + - `logBoth`: bridges both transmitted and received packets **Default:** `logTx` diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 23bfdf4c6a..98fe2ed5f7 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -476,7 +476,7 @@ void MyMesh::logRxRaw(float snr, float rssi, const uint8_t raw[], int len) { void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { #ifdef WITH_BRIDGE - if (_prefs.bridge_pkt_src == 1) { + if (_prefs.bridge_pkt_src & BRIDGE_SOURCE_RX) { bridge.sendPacket(pkt); } #endif @@ -502,7 +502,7 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) { void MyMesh::logTx(mesh::Packet *pkt, int len) { #ifdef WITH_BRIDGE - if (_prefs.bridge_pkt_src == 0) { + if (_prefs.bridge_pkt_src & BRIDGE_SOURCE_TX) { bridge.sendPacket(pkt); } #endif @@ -900,7 +900,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 = 0; // logTx + _prefs.bridge_pkt_src = BRIDGE_SOURCE_TX; // logTx _prefs.bridge_baud = 115200; // baud rate _prefs.bridge_channel = 1; // channel 1 diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 2aa497ab5a..42eb96e2f3 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -116,7 +116,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // sanitise bad bridge pref values _prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1); _prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000); - _prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1); + _prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 3); // log Rx and Tx = 3 _prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD); _prefs->bridge_channel = constrain(_prefs->bridge_channel, 0, 14); @@ -751,9 +751,23 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep strcpy(reply, "Error: delay must be between 0-10000 ms"); } } else if (memcmp(config, "bridge.source ", 14) == 0) { - _prefs->bridge_pkt_src = memcmp(&config[14], "rx", 2) == 0; - savePrefs(); - strcpy(reply, "OK"); + if (memcmp(&config[14], "log", 3) == 0) { + if (memcmp(&config[17], "Rx", 2) == 0 || memcmp(&config[17], "rx", 2) == 0) { + _prefs->bridge_pkt_src = BRIDGE_SOURCE_RX; + strcpy(reply, "OK"); + } else if (memcmp(&config[17], "Tx", 2) == 0 || memcmp(&config[17], "tx", 2) == 0) { + _prefs->bridge_pkt_src = BRIDGE_SOURCE_TX; + strcpy(reply, "OK"); + } else if (memcmp(&config[17], "Both", 4) == 0 || memcmp(&config[17], "both", 4) == 0) { + _prefs->bridge_pkt_src = BRIDGE_SOURCE_TX | BRIDGE_SOURCE_RX; + strcpy(reply, "OK"); + } + } + if (memcmp(reply, "OK", 2) == 0) { + savePrefs(); + } else { + strcpy(reply, "Error: source must be logRx, logTx or logBoth."); + } #endif #ifdef WITH_RS232_BRIDGE } else if (memcmp(config, "bridge.baud ", 12) == 0) { diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index b4f0a97486..2000dd242a 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -9,6 +9,8 @@ #if defined(WITH_RS232_BRIDGE) || defined(WITH_ESPNOW_BRIDGE) || defined(WITH_MQTT_BRIDGE) #define WITH_BRIDGE +#define BRIDGE_SOURCE_RX 0x01 +#define BRIDGE_SOURCE_TX 0x02 #endif #define ADVERT_LOC_NONE 0 @@ -48,7 +50,7 @@ struct NodePrefs { // persisted to file // Bridge settings uint8_t bridge_enabled; // boolean uint16_t bridge_delay; // milliseconds (default 500 ms) - uint8_t bridge_pkt_src; // 0 = logTx, 1 = logRx (default logTx) + uint8_t bridge_pkt_src; // 1 = logTx, 2 = logRx (default logTx) uint32_t bridge_baud; // 9600, 19200, 38400, 57600, 115200 (default 115200) uint8_t bridge_channel; // 1-14 (ESP-NOW only) char bridge_secret[16]; // for XOR encryption of bridge packets (ESP-NOW only) diff --git a/src/helpers/bridges/MQTTBridge.cpp b/src/helpers/bridges/MQTTBridge.cpp index bcd1c30397..a3eadddc62 100644 --- a/src/helpers/bridges/MQTTBridge.cpp +++ b/src/helpers/bridges/MQTTBridge.cpp @@ -54,6 +54,7 @@ void MQTTBridge::begin() { void MQTTBridge::initialize() { _mqttClient.setServer(mqtt_host, mqtt_port); _mqttClient.setCallback(mqttCallback); + _mqttClient.setBufferSize(2*MAX_TRANS_UNIT); _initialized = true; MQTT_DEBUG_PRINTLN("Initialized, broker=%s:%d topic=%s", mqtt_host, mqtt_port, mqtt_topic); @@ -75,7 +76,7 @@ bool MQTTBridge::reconnect() { MQTT_DEBUG_PRINTLN("Connecting to %s:%d...", mqtt_host, mqtt_port); - char clientId[32]; // "meshcore-mqtt-bridge-" (21) + 6 hex chars + null + char clientId[28]; // "meshcore-mqtt-bridge-" (21) + 6 hex chars + null snprintf(clientId, sizeof(clientId), "meshcore-mqtt-bridge-%02X%02X%02X", _pubKey[0], _pubKey[1], _pubKey[2]); bool ok; @@ -113,22 +114,16 @@ void MQTTBridge::loop() { } void MQTTBridge::sendPacket(mesh::Packet *packet) { - if (!_initialized || !packet) { - MQTT_DEBUG_PRINTLN("connection not initialized"); - return; - } + if (!_initialized || !packet) return; - if (!_mqttClient.connected()) { - MQTT_DEBUG_PRINTLN("connection is not connected"); - return; - } + if (!_mqttClient.connected()) return; if (!_seen_packets.hasSeen(packet)) { uint8_t buf[MAX_TRANS_UNIT + 1]; uint16_t len = packet->writeTo(buf); if (len == 0 || len > sizeof(buf)) { - MQTT_DEBUG_PRINTLN("TX invalid packet length %d", len); + MQTT_DEBUG_PRINTLN("PUB invalid packet length %d", len); return; } @@ -141,9 +136,9 @@ void MQTTBridge::sendPacket(mesh::Packet *packet) { _hexBuf[len * 2] = '\0'; if (_mqttClient.publish(mqtt_topic, _hexBuf)) { - MQTT_DEBUG_PRINTLN("TX len=%d hex_len=%d", len, len * 2); + MQTT_DEBUG_PRINTLN("PUB raw=%s", _hexBuf); } else { - MQTT_DEBUG_PRINTLN("TX publish failed len=%d", len); + MQTT_DEBUG_PRINTLN("PUB failed len=%d", len); } } } @@ -163,13 +158,13 @@ void MQTTBridge::onMqttMessage(char *topic, uint8_t *payload, unsigned int lengt // Expect even number of hex chars if (length == 0 || length % 2 != 0) { - MQTT_DEBUG_PRINTLN("RX invalid hex length %u", length); + MQTT_DEBUG_PRINTLN("SUB invalid hex length %u", length); return; } uint16_t byte_len = length / 2; if (byte_len > MAX_TRANS_UNIT + 1) { - MQTT_DEBUG_PRINTLN("RX packet too large %u bytes", byte_len); + MQTT_DEBUG_PRINTLN("SUB packet too large %u bytes", byte_len); return; } @@ -183,27 +178,31 @@ void MQTTBridge::onMqttMessage(char *topic, uint8_t *payload, unsigned int lengt if (hi >= '0' && hi <= '9') hi_val = hi - '0'; else if (hi >= 'A' && hi <= 'F') hi_val = hi - 'A' + 10; else if (hi >= 'a' && hi <= 'f') hi_val = hi - 'a' + 10; - else { MQTT_DEBUG_PRINTLN("RX invalid hex char '%c'", hi); return; } + else { MQTT_DEBUG_PRINTLN("SUB invalid hex char '%c'", hi); return; } if (lo >= '0' && lo <= '9') lo_val = lo - '0'; else if (lo >= 'A' && lo <= 'F') lo_val = lo - 'A' + 10; else if (lo >= 'a' && lo <= 'f') lo_val = lo - 'a' + 10; - else { MQTT_DEBUG_PRINTLN("RX invalid hex char '%c'", lo); return; } + else { MQTT_DEBUG_PRINTLN("SUB invalid hex char '%c'", lo); return; } buf[i] = (hi_val << 4) | lo_val; } mesh::Packet *pkt = _mgr->allocNew(); if (!pkt) { - MQTT_DEBUG_PRINTLN("RX alloc failed"); + MQTT_DEBUG_PRINTLN("SUB alloc failed"); return; } if (pkt->readFrom(buf, byte_len)) { - MQTT_DEBUG_PRINTLN("RX len=%u", byte_len); - onPacketReceived(pkt); + if (_seen_packets.hasSeen(pkt)) { + _mgr->free(pkt); + } else { + MQTT_DEBUG_PRINTLN("SUB raw=%.*s", payload); + onPacketReceived(pkt); + } } else { - MQTT_DEBUG_PRINTLN("RX parse failed len=%u", byte_len); + MQTT_DEBUG_PRINTLN("SUB parse failed len=%u", byte_len); _mgr->free(pkt); } }