Split MQTT and ESP-NOW controls in Full ESP32 builds

This commit is contained in:
mikecarper committed 2026-09-17 01:13:10 -07:00
1 parent 9a149eaea1
commit 30067cb5cd
14 files changed
+337 -108

No files matched your search

+7 -4
View File
@@ -1154,9 +1154,9 @@
], "Separate from device sleep and LoRa RXPS. Bluetooth coexistence can constrain the effective mode.");
}
if (info.mqtt) {
toggle(info.espnowBridge ? "MQTT + ESP-NOW bridge" : "MQTT broker connections", "set mqtt.enabled", "get mqtt.enabled",
toggle("MQTT broker connections", "set mqtt.enabled", "get mqtt.enabled",
info.espnowBridge
? "This Full image has one shared bridge switch: On starts WiFi/MQTT and then ESP-NOW after WiFi associates; Off stops both. Configure WiFi and broker slots first, and set bridge.channel to your access point's fixed 2.4 GHz channel."
? "Independent of the ESP-NOW bridge below. Configure WiFi and broker slots first. Use MQTT alone, ESP-NOW alone, or enable both; get mqtt.running checks the MQTT runtime."
: "Configure WiFi and broker slots first, using WebConfig or set mqtt1.preset/settings. Off keeps those settings. get mqtt.running checks the runtime; get mqtt.status shows connections.");
["status", "packets", "raw", "rx"].forEach(function (name) {
toggle("MQTT " + (name === "rx" ? "receive capture" : name + " publication"), "set mqtt." + name, "get mqtt." + name,
@@ -1183,11 +1183,14 @@
}), "Use the UART and pin map for this exact board. Canonical GPS-enabled RAK4631 uses UART2; select it with set bridge.uart 2 while the bridge is stopped. UART1 needs a compatible GPS-free image.");
}
if (info.espnowBridge && infrastructure) {
if (!info.mqtt) toggle("ESP-NOW bridge", "set bridge.enabled", "get bridge.running");
toggle("ESP-NOW bridge", "set bridge.enabled", "get bridge.enabled",
info.mqtt
? "Independent of MQTT in this Full image. set espnow.enabled is an alias. get bridge.running (or get espnow.running) checks its live state."
: "set bridge.enabled controls the ESP-NOW bridge; get bridge.running checks its live state.");
section("ESP-NOW bridge framing", ["wrapped", "raw"].map(function (mode) {
return { label: mode, commands: ["set bridge.format " + mode] };
}), info.mqtt
? "This Full image shares one 2.4 GHz radio between WiFi/MQTT and ESP-NOW. ESP-NOW begins after WiFi connects and only when bridge.channel matches that access point's fixed channel. Primary ESP-NOW mesh uses set espnow.channel instead."
? "This Full image shares one 2.4 GHz radio between WiFi/MQTT and ESP-NOW. When both are enabled, ESP-NOW begins after WiFi connects and only when bridge.channel matches that access point's fixed channel. ESP-NOW-only mode does not need WiFi credentials. Primary ESP-NOW mesh uses set espnow.channel instead."
: "Set bridge.channel to the bridge channel. Primary ESP-NOW mesh uses set espnow.channel instead; see the board guide before changing channels.");
}
if (full && /sensecapindicator/i.test(profile.target)) section("Indicator wireless transport", [
+6 -2
View File
@@ -180,8 +180,12 @@ For commands and option explanations, follow the
An ESP-NOW bridge target keeps LoRa as its primary mesh radio. Expanded ESP32
Full repeater and room-server images combine ESP-NOW with WiFi MQTT in the
same firmware. Both use one 2.4 GHz radio, so the ESP-NOW bridge channel must
match the connected WiFi access point's fixed channel. Its runtime
same firmware. MQTT and ESP-NOW are independent runtime transports: use
`set mqtt.enabled on|off` for MQTT and `set bridge.enabled on|off` (or
`set espnow.enabled on|off`) for ESP-NOW, so either or both can run. Both use
one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must
match the connected WiFi access point's fixed channel. ESP-NOW-only mode does
not require WiFi credentials. Its runtime
`bridge.format` setting chooses the peer protocol: `wrapped` (the
backward-compatible bridge-to-bridge default using `bridge.secret`) or `raw`
(direct MeshCore ESP-NOW LR frames for `Generic_ESPNOW`,
+6 -6
View File
@@ -26,10 +26,10 @@ public:
if (the_mesh.isWebConfigActive()) mask |= mesh::wireless::WiFi;
#endif
#ifdef WITH_MQTT_BRIDGE
if (the_mesh.isBridgeRunning()) mask |= mesh::wireless::WiFi;
if (the_mesh.isMqttBridgeRunning()) mask |= mesh::wireless::WiFi;
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (the_mesh.isBridgeRunning()) mask |= mesh::wireless::EspNow;
if (the_mesh.isEspNowBridgeRunning()) mask |= mesh::wireless::EspNow;
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (radio_driver.isEnabled()) mask |= mesh::wireless::EspNow;
@@ -61,12 +61,12 @@ public:
resume_web_ = the_mesh.isWebConfigActive();
#endif
#ifdef WITH_MQTT_BRIDGE
resume_mqtt_ = the_mesh.isBridgeRunning();
resume_mqtt_ = the_mesh.isMqttBridgeRunning();
#endif
wifi_saved_ = true;
}
#ifdef WITH_MQTT_BRIDGE
if (!the_mesh.setBridgeState(false)) return Result::Failed;
if (!the_mesh.setMqttBridgeState(false)) return Result::Failed;
#endif
#ifdef WITH_WEBCONFIG
if (the_mesh.isWebConfigActive()) {
@@ -102,7 +102,7 @@ public:
|| !(enabled() & mesh::wireless::WiFi)
#endif
) {
if (!the_mesh.setBridgeState(true)) return Result::Failed;
if (!the_mesh.setMqttBridgeState(true)) return Result::Failed;
}
#endif
wifi_saved_ = resume_web_ = resume_mqtt_ = false;
@@ -111,7 +111,7 @@ public:
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (service == mesh::wireless::EspNow)
return the_mesh.setBridgeState(on) ? Result::Done : Result::Failed;
return the_mesh.setEspNowBridgeState(on) ? Result::Done : Result::Failed;
#endif
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
if (service == mesh::wireless::EspNow) {
+10 -4
View File
@@ -1146,7 +1146,7 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
#endif
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed RX packets - bridge decides based on mqtt.rx setting
if (mqtt_bridge) mqtt_bridge->onPacketReceived(pkt);
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->onPacketReceived(pkt);
#ifdef WITH_ESPNOW_BRIDGE
// ESP-NOW follows bridge.source, independently of MQTT's mqtt.rx policy.
ESPNowBridge* espnow = &espnow_bridge;
@@ -1198,7 +1198,7 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed TX packets - bridge decides based on mqtt.tx setting
if (mqtt_bridge) mqtt_bridge->sendPacket(pkt);
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->sendPacket(pkt);
#ifdef WITH_ESPNOW_BRIDGE
ESPNowBridge* espnow = &espnow_bridge;
if (_prefs.bridge_pkt_src == 0 && espnow && espnow->isRunning()) {
@@ -3595,6 +3595,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
#else
_prefs.bridge_enabled = 1; // enabled
#endif
_prefs.espnow_bridge_enabled = 1; // preserves the Full image's former combined default
_prefs.bridge_delay = 500; // milliseconds
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
_prefs.bridge_baud = 115200; // baud rate
@@ -3792,6 +3793,10 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (_prefs.espnow_bridge_enabled) setEspNowBridgeState(true);
#endif
// Wire fault-alert reporter. begin() is safe regardless of bridge state.
// Passing `this` as the callbacks lets the reporter resolve a TransportKey
// scope (alert.region override, falling back to default_scope) so alert
@@ -3806,7 +3811,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
#ifdef WITH_MQTT_BRIDGE
// Preserve the MQTT observer's first-boot setup experience even though the
// persistent WebUI master switch defaults off on infrastructure roles.
start_webui = start_webui || _cli.getObserverPrefs()->wifi_ssid[0] == 0;
start_webui = start_webui || (_prefs.bridge_enabled
&& _cli.getObserverPrefs()->wifi_ssid[0] == 0);
if (start_webui && _cli.getObserverPrefs()->wifi_ssid[0] == 0) {
#if defined(WITH_ESPNOW_BRIDGE)
if (espnow_bridge.isRunning()) espnow_bridge.end();
@@ -10814,7 +10820,7 @@ void MyMesh::onConfigBatchEnd() {
if (_wc_restart_pending) {
_wc_restart_pending = false;
_wc_slot_restart_mask = 0;
if (_prefs.bridge_enabled) restartBridge();
if (_prefs.bridge_enabled) restartMqttBridge();
return;
}
#endif
+76 -30
View File
@@ -604,7 +604,8 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
// up a second WiFi owner before the AP selects the shared channel.
bool startSharedEspNowBridgeIfReady() {
if (espnow_bridge.isRunning()) return true;
if (!mqtt_bridge || !mqtt_bridge->isRunning() || !WiFi.isConnected()) return false;
if (!_prefs.espnow_bridge_enabled) return false;
if (mqtt_bridge && mqtt_bridge->isRunning() && !WiFi.isConnected()) return false;
if (!millisHasNowPassed(shared_espnow_retry_at)) return false;
shared_espnow_retry_at = millis() + 5000;
configureBridgeFilter(&espnow_bridge);
@@ -1246,12 +1247,22 @@ public:
#endif
}
bool setBridgeState(bool enable) override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
// The combined Full bridge is intentionally one saved switch. ESP-NOW is
// started only after MQTT has brought WiFi up, so it can validate and share
// the access point channel instead of retuning the station interface.
if (!enable && !mqtt_bridge && !espnow_bridge.isRunning()) return true;
bool isEspNowBridgeRunning() override {
return espnow_bridge.isRunning();
}
bool setMqttBridgeState(bool enable) override {
if (!enable) {
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
_alerter.setBridge(nullptr);
return !mqtt_bridge || !mqtt_bridge->isRunning();
}
// Give the WiFi station to MQTT while it associates. ESP-NOW is restarted
// after the AP channel is known, which avoids a raw ESP-NOW-only session
// pinning the station to a different channel.
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
if (!mqtt_bridge) {
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
@@ -1265,32 +1276,71 @@ public:
getRTCClock(), &self_id);
if (!mqtt_bridge) return false;
}
if (enable) {
if (!mqtt_bridge->isRunning()) {
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
configureBridgeFilter(mqtt_bridge);
mqtt_bridge->begin();
_alerter.setBridge(mqtt_bridge);
}
if (!mqtt_bridge->isRunning()) return false;
if (!mqtt_bridge->isRunning()) {
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
mqtt_bridge->setDeviceID(device_id);
mqtt_bridge->setFirmwareVersion(getFirmwareVer());
mqtt_bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
configureBridgeFilter(mqtt_bridge);
mqtt_bridge->begin();
_alerter.setBridge(mqtt_bridge);
}
if (_prefs.espnow_bridge_enabled) startSharedEspNowBridgeIfReady();
return mqtt_bridge->isRunning();
}
bool setEspNowBridgeState(bool enable) override {
if (!enable) {
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
return !espnow_bridge.isRunning();
}
if (espnow_bridge.isRunning()) return true;
shared_espnow_retry_at = 0;
if (mqtt_bridge && mqtt_bridge->isRunning()) {
// Association is asynchronous; servicePostMeshLoop retries once the
// MQTT station has joined the access point on its fixed channel.
startSharedEspNowBridgeIfReady();
// MQTT has started successfully; ESP-NOW joins after its WiFi station
// associates and validates the configured shared channel.
return true;
}
configureBridgeFilter(&espnow_bridge);
espnow_bridge.begin();
return espnow_bridge.isRunning();
}
bool restartMqttBridge() override {
#ifdef WITH_WEBCONFIG
if (_wc_batch_active) {
_wc_restart_pending = true;
return true;
}
#endif
if (espnow_bridge.isRunning()) espnow_bridge.end();
if (mqtt_bridge->isRunning()) mqtt_bridge->end();
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
shared_espnow_retry_at = 0;
_alerter.setBridge(nullptr);
return !mqtt_bridge->isRunning() && !espnow_bridge.isRunning();
return !_prefs.bridge_enabled || setMqttBridgeState(true);
}
bool restartEspNowBridge() override {
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
return !_prefs.espnow_bridge_enabled || setEspNowBridgeState(true);
}
#endif
bool setBridgeState(bool enable) override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (!enable) return setEspNowBridgeState(false) && setMqttBridgeState(false);
const bool mqtt_ok = _prefs.bridge_enabled
? setMqttBridgeState(true) : setMqttBridgeState(false);
const bool espnow_ok = _prefs.espnow_bridge_enabled
? setEspNowBridgeState(true) : setEspNowBridgeState(false);
return mqtt_ok && espnow_ok;
#else
// Disabling an already-absent heap-backed bridge is successful and must
// not allocate an instance merely to stop it. The embedded ESP-NOW bridge
@@ -1381,11 +1431,7 @@ public:
return true;
}
#endif
if (espnow_bridge.isRunning()) espnow_bridge.end();
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
shared_espnow_retry_at = 0;
_alerter.setBridge(nullptr);
return setBridgeState(true);
return restartEspNowBridge() && restartMqttBridge();
#else
#ifdef WITH_RS232_BRIDGE
// RS-232 changes must be applied synchronously so the CLI can commit or
+10 -4
View File
@@ -331,7 +331,7 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
#endif
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed RX packets - bridge decides based on mqtt.rx setting
if (_prefs.bridge_enabled && bridge) bridge->onPacketReceived(pkt);
if (_prefs.bridge_enabled && bridge && bridge->isRunning()) bridge->onPacketReceived(pkt);
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (_prefs.bridge_pkt_src == 1 && espnow_bridge.isRunning()) {
@@ -373,7 +373,7 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
#ifdef WITH_MQTT_BRIDGE
// MQTT bridge: always feed TX packets - bridge decides based on mqtt.tx setting
if (_prefs.bridge_enabled && bridge) bridge->sendPacket(pkt);
if (_prefs.bridge_enabled && bridge && bridge->isRunning()) bridge->sendPacket(pkt);
#endif
#ifdef WITH_ESPNOW_BRIDGE
if (_prefs.bridge_pkt_src == 0 && espnow_bridge.isRunning()) {
@@ -1321,6 +1321,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
// bridge defaults (same as repeater)
_prefs.bridge_enabled = 1; // enabled
_prefs.espnow_bridge_enabled = 1; // preserves the Full image's former combined default
_prefs.bridge_delay = 500; // milliseconds
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
_prefs.bridge_baud = 115200; // baud rate
@@ -1458,6 +1459,10 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (_prefs.espnow_bridge_enabled) setEspNowBridgeState(true);
#endif
// Wire fault-alert reporter. begin() is safe regardless of bridge state.
// Passing `this` as the callbacks lets the reporter resolve a TransportKey
// scope (alert.region override, falling back to default_scope) so alert
@@ -1470,7 +1475,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
#if defined(WITH_WEBCONFIG) && !defined(WEBCONFIG_NO_AUTO_AP)
bool start_webui = WebConfigServer::loadEnabled(false);
#ifdef WITH_MQTT_BRIDGE
start_webui = start_webui || _cli.getObserverPrefs()->wifi_ssid[0] == 0;
start_webui = start_webui || (_prefs.bridge_enabled
&& _cli.getObserverPrefs()->wifi_ssid[0] == 0);
if (start_webui && _cli.getObserverPrefs()->wifi_ssid[0] == 0) {
#if defined(WITH_ESPNOW_BRIDGE)
if (espnow_bridge.isRunning()) espnow_bridge.end();
@@ -2008,7 +2014,7 @@ void MyMesh::onConfigBatchEnd() {
if (_wc_restart_pending) {
_wc_restart_pending = false;
_wc_slot_restart_mask = 0;
if (_prefs.bridge_enabled) restartBridge();
if (_prefs.bridge_enabled) restartMqttBridge();
return;
}
const uint8_t mask = _wc_slot_restart_mask;
+89 -42
View File
@@ -564,7 +564,8 @@ public:
// station interface so it can share the AP-selected channel safely.
bool startSharedEspNowBridgeIfReady() {
if (espnow_bridge.isRunning()) return true;
if (!bridge || !bridge->isRunning() || !WiFi.isConnected()) return false;
if (!_prefs.espnow_bridge_enabled) return false;
if (bridge && bridge->isRunning() && !WiFi.isConnected()) return false;
if (!millisHasNowPassed(shared_espnow_retry_at)) return false;
shared_espnow_retry_at = millis() + 5000;
configureBridgeFilter(&espnow_bridge);
@@ -581,20 +582,22 @@ public:
#endif
}
void configureBridgeFilter(AbstractBridge* active_bridge) {
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
active_bridge->setPacketFilter([](void* context, const mesh::Packet* packet) {
return static_cast<MyMesh*>(context)->allowTransportPacket(packet, 2); // RULE_MODE_BRIDGE
}, this);
#else
(void)active_bridge;
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
bool isEspNowBridgeRunning() override {
return espnow_bridge.isRunning();
}
bool setBridgeState(bool enable) override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (!enable && !bridge && !espnow_bridge.isRunning()) return true;
bool setMqttBridgeState(bool enable) override {
if (!enable) {
if (bridge && bridge->isRunning()) bridge->end();
_alerter.setBridge(nullptr);
return !bridge || !bridge->isRunning();
}
// Give the WiFi station to MQTT while it associates. ESP-NOW is restarted
// after the AP channel is known, which avoids a raw ESP-NOW-only session
// pinning the station to a different channel.
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
if (!bridge) {
MQTTNodeInfo node_info;
node_info.node_name = _prefs.node_name;
@@ -608,39 +611,87 @@ public:
getRTCClock(), &self_id);
if (!bridge) return false;
}
if (enable) {
if (!bridge->isRunning()) {
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
bridge->setDeviceID(device_id);
bridge->setFirmwareVersion(getFirmwareVer());
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
bridge->setBuildDate(getBuildDate());
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
if (!bridge->isRunning()) {
char device_id[65];
mesh::LocalIdentity self_id = getSelfId();
mesh::Utils::toHex(device_id, self_id.pub_key, PUB_KEY_SIZE);
bridge->setDeviceID(device_id);
bridge->setFirmwareVersion(getFirmwareVer());
bridge->setBoardModel(_cli.getBoard()->getManufacturerName());
bridge->setBuildDate(getBuildDate());
bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#ifdef WITH_SNMP
if (_cli.getObserverPrefs()->snmp_enabled) {
_snmp_agent.setNodeName(_prefs.node_name);
_snmp_agent.setFirmwareVersion(getFirmwareVer());
bridge->setSNMPAgent(&_snmp_agent);
}
#endif
configureBridgeFilter(bridge);
bridge->begin();
_alerter.setBridge(bridge);
if (_cli.getObserverPrefs()->snmp_enabled) {
_snmp_agent.setNodeName(_prefs.node_name);
_snmp_agent.setFirmwareVersion(getFirmwareVer());
bridge->setSNMPAgent(&_snmp_agent);
}
if (!bridge->isRunning()) return false;
#endif
configureBridgeFilter(bridge);
bridge->begin();
_alerter.setBridge(bridge);
}
if (_prefs.espnow_bridge_enabled) startSharedEspNowBridgeIfReady();
return bridge->isRunning();
}
bool setEspNowBridgeState(bool enable) override {
if (!enable) {
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
return !espnow_bridge.isRunning();
}
if (espnow_bridge.isRunning()) return true;
shared_espnow_retry_at = 0;
if (bridge && bridge->isRunning()) {
startSharedEspNowBridgeIfReady();
// The MQTT task owns association. ESP-NOW starts after it sees the
// active AP channel and accepts the configured bridge channel.
return true;
}
configureBridgeFilter(&espnow_bridge);
espnow_bridge.begin();
return espnow_bridge.isRunning();
}
bool restartMqttBridge() override {
#ifdef WITH_WEBCONFIG
if (_wc_batch_active) {
_wc_restart_pending = true;
return true;
}
#endif
if (espnow_bridge.isRunning()) espnow_bridge.end();
if (bridge->isRunning()) bridge->end();
if (bridge && bridge->isRunning()) bridge->end();
shared_espnow_retry_at = 0;
_alerter.setBridge(nullptr);
return !bridge->isRunning() && !espnow_bridge.isRunning();
return !_prefs.bridge_enabled || setMqttBridgeState(true);
}
bool restartEspNowBridge() override {
if (espnow_bridge.isRunning()) espnow_bridge.end();
shared_espnow_retry_at = 0;
return !_prefs.espnow_bridge_enabled || setEspNowBridgeState(true);
}
#endif
void configureBridgeFilter(AbstractBridge* active_bridge) {
#if MESH_ENABLE_ROOM_FLOOD_RULE_ENGINE
active_bridge->setPacketFilter([](void* context, const mesh::Packet* packet) {
return static_cast<MyMesh*>(context)->allowTransportPacket(packet, 2); // RULE_MODE_BRIDGE
}, this);
#else
(void)active_bridge;
#endif
}
bool setBridgeState(bool enable) override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (!enable) return setEspNowBridgeState(false) && setMqttBridgeState(false);
const bool mqtt_ok = _prefs.bridge_enabled
? setMqttBridgeState(true) : setMqttBridgeState(false);
const bool espnow_ok = _prefs.espnow_bridge_enabled
? setEspNowBridgeState(true) : setEspNowBridgeState(false);
return mqtt_ok && espnow_ok;
#else
// An absent MQTT bridge is already stopped. Do not allocate one solely to
// satisfy an idempotent disable request.
@@ -705,11 +756,7 @@ public:
return true;
}
#endif
if (espnow_bridge.isRunning()) espnow_bridge.end();
if (bridge && bridge->isRunning()) bridge->end();
shared_espnow_retry_at = 0;
_alerter.setBridge(nullptr);
return setBridgeState(true);
return restartEspNowBridge() && restartMqttBridge();
#else
if (!bridge) return false;
#ifdef WITH_WEBCONFIG
+82 -9
View File
@@ -1286,6 +1286,10 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
&& preamble <= mesh::RadioProfiles::MaxPreamble))) {
_prefs->primary_radio_preamble = preamble;
}
if (file.available() >= (int)sizeof(_prefs->espnow_bridge_enabled)) {
file.read((uint8_t *)&_prefs->espnow_bridge_enabled,
sizeof(_prefs->espnow_bridge_enabled));
}
}
}
}
@@ -1359,6 +1363,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
}
#endif
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
_prefs->espnow_bridge_enabled = constrain(_prefs->espnow_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_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD);
@@ -1607,6 +1612,7 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
WRITE_COMMON_PREFS(&prefs->bridge_uart); // 862
WRITE_COMMON_PREFS(&prefs->bridge_format); // 863
WRITE_COMMON_PREFS(&prefs->primary_radio_preamble); // appended primary tuple field
WRITE_COMMON_PREFS(&prefs->espnow_bridge_enabled); // appended Full ESP-NOW intent
#undef WRITE_COMMON_PREFS_BYTES
#undef WRITE_COMMON_PREFS
@@ -1764,6 +1770,7 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
file.write((uint8_t *)&_prefs->bridge_uart, sizeof(_prefs->bridge_uart)); // 862
file.write((uint8_t *)&_prefs->bridge_format, sizeof(_prefs->bridge_format)); // 863
file.write((uint8_t *)&_prefs->primary_radio_preamble, sizeof(_prefs->primary_radio_preamble)); // appended
file.write((uint8_t *)&_prefs->espnow_bridge_enabled, sizeof(_prefs->espnow_bridge_enabled)); // appended
_common_save_succeeded = file.commit();
if (!_common_save_succeeded) {
@@ -3343,7 +3350,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
_prefs->usb_logging_enabled = usb_enabled ? 1 : 0;
_prefs->bridge_enabled = wifi_enabled ? 1 : 0;
mesh::setUsbLoggingEnabled(usb_enabled);
_callbacks->setBridgeState(wifi_enabled);
_callbacks->setMqttBridgeState(wifi_enabled);
savePrefs();
snprintf(reply, 160, "OK - logging.output %s (saved)", value);
return;
@@ -3392,6 +3399,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
#endif
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (memcmp(config, "espnow.enabled ", 15) == 0) {
const char* value = config + 15;
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set espnow.enabled on|off");
} else {
char canonical[28];
snprintf(canonical, sizeof(canonical), "set bridge.enabled %s", value);
handleSetCmd(sender_timestamp, canonical, reply);
}
return;
}
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
if (handleSdCardSetCmd(config, reply)) return;
#endif
@@ -4246,6 +4266,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (!parseOnOffStrict(&config[15], enable)) {
strcpy(reply, "Error: usage set bridge.enabled on|off");
} else {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
// In a combined Full image bridge.enabled retains its ESP-NOW meaning;
// mqtt.enabled is the independent MQTT switch.
_prefs->espnow_bridge_enabled = enable;
const bool applied = _callbacks->setEspNowBridgeState(enable);
savePrefs();
strcpy(reply, applied ? "OK"
: "Error: ESP-NOW runtime change failed; setting saved");
#else
#ifdef WITH_RS232_BRIDGE
if (enable
&& _sensors->gpsSerialTransportMayConflict(_prefs->bridge_uart)
@@ -4290,6 +4319,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
savePrefs();
strcpy(reply, applied ? "OK"
: "Error: bridge runtime change failed; setting saved");
#endif
#endif
}
} else if (memcmp(config, "bridge.delay ", 13) == 0) {
@@ -4331,7 +4361,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
const uint32_t previous_baud = _prefs->bridge_baud;
const bool previous_running = _callbacks->isBridgeRunning();
_prefs->bridge_baud = baud;
const bool applied = !_prefs->bridge_enabled || _callbacks->restartBridge();
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
#endif
if (applied) {
savePrefs();
strcpy(reply, "OK");
@@ -4374,7 +4409,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
const uint8_t previous_uart = _prefs->bridge_uart;
const bool previous_running = _callbacks->isBridgeRunning();
_prefs->bridge_uart = (uint8_t)uart;
const bool applied = !_prefs->bridge_enabled || _callbacks->restartBridge();
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
#endif
if (!applied) {
_prefs->bridge_uart = previous_uart;
const bool restored = previous_running
@@ -4401,7 +4441,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (mesh::bridge::parseEspNowBridgeChannel(
&config[15], _prefs->bridge_format, channel)) {
_prefs->bridge_channel = channel;
const bool applied = !_prefs->bridge_enabled || _callbacks->restartBridge();
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
#endif
savePrefs();
strcpy(reply, applied ? "OK"
: "Error: setting saved; bridge failed to restart");
@@ -4416,7 +4461,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "Error: secret must be 1-%u characters", (unsigned)(sizeof(_prefs->bridge_secret) - 1));
} else {
StrHelper::strncpy(_prefs->bridge_secret, secret, sizeof(_prefs->bridge_secret));
const bool applied = !_prefs->bridge_enabled || _callbacks->restartBridge();
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
#endif
savePrefs();
strcpy(reply, applied ? "OK"
: "Error: setting saved; bridge failed to restart");
@@ -4430,7 +4480,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, "Error: set bridge.channel to 1-13 before changing format");
} else {
_prefs->bridge_format = format;
const bool applied = !_prefs->bridge_enabled || _callbacks->restartBridge();
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
#endif
savePrefs();
if (!applied) {
strcpy(reply, "Error: setting saved; bridge failed to restart");
@@ -4839,7 +4894,9 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", _callbacks->getRole());
} else if (configKeyEquals(config, "bridge.type")) {
sprintf(reply, "> %s",
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
"mqtt+espnow"
#elif defined(WITH_RS232_BRIDGE)
"rs232"
#elif WITH_ESPNOW_BRIDGE
"espnow"
@@ -4851,9 +4908,25 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
);
#ifdef WITH_BRIDGE
} else if (configKeyEquals(config, "bridge.enabled")) {
sprintf(reply, "> %s", _prefs->bridge_enabled ? "on" : "off");
sprintf(reply, "> %s",
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
_prefs->espnow_bridge_enabled ? "on" : "off");
#else
_prefs->bridge_enabled ? "on" : "off");
#endif
} else if (configKeyEquals(config, "bridge.running")) {
sprintf(reply, "> %s", _callbacks->isBridgeRunning() ? "on" : "off");
sprintf(reply, "> %s",
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
_callbacks->isEspNowBridgeRunning() ? "on" : "off");
#else
_callbacks->isBridgeRunning() ? "on" : "off");
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
} else if (configKeyEquals(config, "espnow.enabled")) {
sprintf(reply, "> %s", _prefs->espnow_bridge_enabled ? "on" : "off");
} else if (configKeyEquals(config, "espnow.running")) {
sprintf(reply, "> %s", _callbacks->isEspNowBridgeRunning() ? "on" : "off");
#endif
} else if (configKeyEquals(config, "bridge.delay")) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_delay);
} else if (configKeyEquals(config, "bridge.source")) {
+14
View File
@@ -191,6 +191,11 @@ public:
// Appended after the existing /com_prefs tail. Keep the preamble in the same
// transaction as frequency/modulation; old images adopt /radio_profiles.
uint16_t primary_radio_preamble = 0;
// Full ESP32 images can carry MQTT and ESP-NOW simultaneously. Keep the
// ESP-NOW intent separately so either transport can be selected at runtime.
// Old preference images did not have this byte; they default to enabled to
// retain the previous combined-Full behavior after an upgrade.
uint8_t espnow_bridge_enabled = 1;
uint8_t retry_preset = 0;
uint8_t direct_retry_attempts = 0;
uint16_t direct_retry_base_ms = 0;
@@ -574,6 +579,15 @@ public:
virtual bool isBridgeRunning() const { return false; }
// Most roles have one bridge, so the transport-specific methods retain the
// legacy bridge behavior by default. Combined MQTT + ESP-NOW roles override
// them to let each transport be started, stopped, and queried independently.
virtual bool setMqttBridgeState(bool enable) { return setBridgeState(enable); }
virtual bool setEspNowBridgeState(bool enable) { return setBridgeState(enable); }
virtual bool restartMqttBridge() { return restartBridge(); }
virtual bool restartEspNowBridge() { return restartBridge(); }
virtual bool isEspNowBridgeRunning() { return isBridgeRunning(); }
virtual void restartBridgeSlot(int slot) {
// Default: fall back to full restart
restartBridge();
+8 -5
View File
@@ -187,16 +187,19 @@ bool CommonCLI::handleObserverSetCmd(uint32_t sender_timestamp, const char* conf
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
strcpy(reply, "Error: use set mqtt.enabled on|off");
} else {
char canonical[24];
snprintf(canonical, sizeof(canonical), "set bridge.enabled %s", value);
handleSetCmd(sender_timestamp, canonical, reply);
const bool enable = strcmp(value, "on") == 0;
_prefs->bridge_enabled = enable ? 1 : 0;
const bool applied = _callbacks->setMqttBridgeState(enable);
savePrefs();
strcpy(reply, applied ? "OK"
: "Error: MQTT runtime change failed; setting saved");
}
return true;
}
const auto restart_observer_bridge = [this]() {
return mesh::cli::restartBridgeIfEnabled(
_prefs->bridge_enabled != 0,
[this]() { return _callbacks->restartBridge(); });
[this]() { return _callbacks->restartMqttBridge(); });
};
if (memcmp(config, "snmp.community ", 15) == 0) {
if (valueTooLong(&config[15], sizeof(_mqtt_prefs.snmp_community), reply, "snmp.community")) return true;
@@ -887,7 +890,7 @@ bool CommonCLI::handleObserverGetCmd(uint32_t sender_timestamp, const char* conf
return true;
}
if (strcmp(config, "mqtt.running") == 0) {
snprintf(reply, 160, "> %s", _callbacks->isBridgeRunning() ? "on" : "off");
snprintf(reply, 160, "> %s", _callbacks->isMqttBridgeRunning() ? "on" : "off");
return true;
}
if (memcmp(config, "snmp.community", 14) == 0) {
+3
View File
@@ -48,6 +48,7 @@ def full_command_keys(text):
COMMON_QUERY_EXCEPTIONS = {
'bootloader.ver': 'Installed bootloader identity',
'bridge.running': 'Live bridge state; set bridge.enabled controls intent',
'espnow.running': 'Live ESP-NOW bridge state; set espnow.enabled controls intent',
'bridge.type': 'Build-selected bridge transport',
'password': 'Written by the password command, not set password',
'public.key': 'Derived from identity; written through set prv.key',
@@ -80,6 +81,8 @@ class CLISettingsContractTest(unittest.TestCase):
'esp32_web': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'ADMIN_PASSWORD', 'MESH_USB_LOGGING_AVAILABLE'],
'esp32_mqtt': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE', 'WITH_BRIDGE',
'WITH_MQTT_NEIGHBORS', 'MESH_USB_LOGGING_AVAILABLE'],
'esp32_mqtt_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE',
'WITH_ESPNOW_BRIDGE', 'WITH_BRIDGE'],
'rs232_gps': ['WITH_BRIDGE', 'WITH_RS232_BRIDGE', 'ENV_INCLUDE_GPS'],
'espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE', 'WITH_ESPNOW_BRIDGE', 'MESH_PRIMARY_ESPNOW'],
'lr2021': ['USE_LR2021'],
+19
View File
@@ -113,6 +113,25 @@ int main(){
'''
class EspNowLifecycleTests(unittest.TestCase):
def test_combined_full_transport_controls_are_independent(self):
common = (ROOT/'src/helpers/CommonCLI.cpp').read_text()
observer = (ROOT/'src/helpers/CommonCLI_Observer.cpp').read_text()
prefs = (ROOT/'src/helpers/CommonCLI.h').read_text()
self.assertIn('uint8_t espnow_bridge_enabled = 1;', prefs)
self.assertIn('setEspNowBridgeState(enable)', common)
self.assertIn('_prefs->espnow_bridge_enabled', common)
self.assertIn('setMqttBridgeState(enable)', observer)
self.assertIn('isMqttBridgeRunning()', observer)
for role in ('simple_repeater', 'simple_room_server'):
header = (ROOT/f'examples/{role}/MyMesh.h').read_text()
combined = method(header, 'bool setBridgeState(bool enable) override')
self.assertIn('setMqttBridgeState(true)', combined)
self.assertIn('setEspNowBridgeState(true)', combined)
mqtt = method(header, 'bool setMqttBridgeState(bool enable) override')
espnow = method(header, 'bool setEspNowBridgeState(bool enable) override')
self.assertIn('if (espnow_bridge.isRunning()) espnow_bridge.end();', mqtt)
self.assertIn('if (mqtt_bridge' if role == 'simple_repeater' else 'if (bridge', espnow)
def test_sdk_failures_stop_and_restart(self):
header=(ROOT/'src/helpers/esp32/ESPNOWRadio.h').read_text()
source=(ROOT/'src/helpers/esp32/ESPNOWRadio.cpp').read_text()
+3 -2
View File
@@ -1012,9 +1012,10 @@ const combinedObserver = combinedCatalog.profiles.find(profile => profile.target
assert.deepStrictEqual(picker.profileFieldValues(combinedObserver, 'mode'), ['standard', 'espnow']);
assert(picker.profileMatches(combinedObserver, {mode: 'espnow'}, ['mode']));
const combinedDirections = picker.runtimeDirections(combinedObserver, {logging: 'usb'});
assert(combinedDirections.some(section => section.title === 'MQTT + ESP-NOW bridge'));
assert(!combinedDirections.some(section => section.title === 'ESP-NOW bridge'));
assert(combinedDirections.some(section => section.title === 'MQTT broker connections'));
assert(combinedDirections.some(section => section.title === 'ESP-NOW bridge'));
assert.strictEqual(commands(combinedDirections).filter(command => command === 'set mqtt.enabled on').length, 1);
assert.strictEqual(commands(combinedDirections).filter(command => command === 'set bridge.enabled on').length, 1);
console.log('current release metadata and capacity directions tests passed');
assert.strictEqual(nrf.chipFamily, 'nrf52');
+4
View File
@@ -338,6 +338,10 @@ struct {
bool hasWirelessNetworkClient(){return network;}
bool isBridgeRunning(){return bridge;}
bool setBridgeState(bool on){bridge=on;return true;}
bool isMqttBridgeRunning(){return bridge;}
bool isEspNowBridgeRunning(){return bridge;}
bool setMqttBridgeState(bool on){bridge=on;return true;}
bool setEspNowBridgeState(bool on){bridge=on;return true;}
bool stopWebConfig(char*){web=false;stopping=true;return true;}
bool startWebConfig(bool,char*){web=true;return true;}
} the_mesh;