Retain independent RS232 in Full MQTT repeaters

This commit is contained in:
mikecarper committed 2026-10-05 18:21:42 -07:00
1 parent 28023c7302
commit 51d74a200e
37 files changed
+1572 -196

No files matched your search

+2 -2
View File
@@ -120,6 +120,8 @@ jobs:
run: |
python3 -B test/test_transport_filter_wiring.py -v
python3 -B test/test_rs232_runtime.py -v
python3 -B test/test_rs232_mqtt_runtime.py -v
python3 -B test/test_rs232_driver_lifecycle.py -v
- name: Verify saved repeat-off trace exception and relay guards
run: |
@@ -346,8 +348,6 @@ jobs:
python3 -B test/test_companion_wifi_session.py -v
python3 -B test/test_espnow_radio_lifecycle.py -v
python3 -B test/test_espnow_runtime.py -v
# Reports the confirmed HTTP-OTA ownership/stale-server bugs as
# explicit expected failures until the separate firmware fix lands.
python3 -B test/test_wifi_ota_lifecycle.py -v
- name: Verify message reader buttons, touch targets, and footer layouts
+8 -5
View File
@@ -3398,16 +3398,19 @@ declare_build_capability_contract() {
declare_full_logging_application_contract "$env_name"
record_build_capability "profile.${BUILD_PROFILE_FOR_TARGET}"
if [ "$env_name_lc" = mke_s3_repeater ]; then
# Do not remove the dedicated bridge images unless both real transports
# survived linking in the canonical runtime-selectable repeater.
record_build_expectation "bridge.rs232" "_ZN11RS232Bridge5beginEv"
fi
pio_env_name=$(get_pio_build_env "$env_name")
if [ "$BUILD_PROFILE_FOR_TARGET" = "full" ] \
&& is_esp32_canonical_full_release_target "$env_name"; then
pio_env_name=$(get_exact_identity_full_pio_env "$env_name")
fi
if is_repeater_role_target "$env_name" \
&& { pio_env_option_contains "$env_name" build_flags "WITH_RS232_BRIDGE=" \
|| pio_env_option_contains "$pio_env_name" build_flags "WITH_RS232_BRIDGE="; }; then
# A Full observer recipe must retain the UART from its normal repeater.
# Check both the logical and compiled recipes so substituting sources
# cannot silently remove RS-232 while its legacy image is retired.
record_build_expectation "bridge.rs232" "_ZN11RS232Bridge5beginEv"
fi
if [ "$BUILD_PROFILE_FOR_TARGET" = "full" ] \
&& supports_esp32_full_shared_espnow "$pio_env_name"; then
# A capability flag alone cannot retire a dedicated transport artifact.
+8 -4
View File
@@ -1600,17 +1600,21 @@
if (profile.role === "repeater") toggle("Repeat mesh traffic", "set repeat", "get repeat");
if (info.rs232 && infrastructure) {
const mode = chosen.mode;
// MQTT images keep the historical bridge.enabled ESP-NOW alias.
// Their independently saved UART uses the transport-specific controls.
const uartSetting = info.mqtt ? "rs232.enabled" : "bridge.enabled";
const uartRunning = info.mqtt ? "rs232.running" : "bridge.running";
section("RS232 bridge" + (mode === "rs232" ? " - selected" : ""), [
{ label: "On", commands: ["set bridge.enabled on", "get bridge.running"] },
{ label: "Off", commands: ["set bridge.enabled off"] },
{ label: "Set baud", commands: ["set bridge.enabled off", "set bridge.baud 115200", "set bridge.enabled on"] },
{ label: "On", commands: ["set " + uartSetting + " on", "get " + uartRunning] },
{ label: "Off", commands: ["set " + uartSetting + " off"] },
{ label: "Set baud", commands: ["set " + uartSetting + " off", "set bridge.baud 115200", "set " + uartSetting + " on"] },
].sort(function (a, b) {
return mode === "standard" ? Number(b.label === "Off") - Number(a.label === "Off") : 0;
}), "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) {
// Combined images need the transport-specific switch: bridge.enabled
// controls the UART when RS-232 is compiled alongside ESP-NOW. Older
// keeps its historical meaning for the primary transport. Older
// dedicated ESP-NOW releases retain their original bridge.* commands.
const independentEspnow = info.rs232 || info.mqtt;
toggle("ESP-NOW bridge", independentEspnow ? "set espnow.enabled" : "set bridge.enabled",
+12
View File
@@ -204,6 +204,18 @@ available from a canonical image:
target remains directly buildable with its bridge-on default but is omitted
from bulk releases. Browser OTA pauses ESP-NOW while leaving RS-232 running;
`stop ota` restores ESP-NOW when its saved setting is on.
- Full Heltec V3, Wireless Stick Lite V3, RAK3112 and LilyGo T-LoRa V2.1-1.6
MQTT repeaters retain RS-232 alongside MQTT and ESP-NOW. The first three
use UART2 with RX on GPIO5 and TX on GPIO6; T-LoRa uses UART2 with RX on
GPIO34 and TX on GPIO25. Use `set rs232.enabled on|off` and
`get rs232.running` independently of `mqtt.enabled` and `espnow.enabled`.
`bridge.baud` and `bridge.uart` configure the UART. Old MQTT preferences
do not enable this newly added UART; its saved intent needs a validated
UART profile marker. Historical `bridge.enabled` keeps its ESP-NOW meaning
in MQTT images and its UART meaning in non-MQTT images. Full qualification
verifies the linked UART driver even when compiling observer sources for a
normal repeater, and resumed release selection rejects Full images without
that proof.
- MeshTower V2 repeaters use
`Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors` as the one
canonical target. Its SD OTA target `0A9DBBF0`, hardware ID
+14 -2
View File
@@ -4745,6 +4745,15 @@ bridge actually started in this boot; they can differ after a hardware conflict,
missing credentials, or a transient initialization failure. Normal merged
repeater images default to `off`; dedicated bridge images may default to `on`.
Full MQTT repeaters with RS-232 expose three independent transports. Use
`set rs232.enabled on|off` and `get rs232.enabled` for saved UART intent;
`get rs232.running` reports the actual UART state. `mqtt.enabled` and
`espnow.enabled` select the other two transports. In MQTT images the historical
`bridge.enabled` alias controls ESP-NOW; in non-MQTT UART images it controls
RS-232. `bridge.baud` and `bridge.uart` change only UART configuration, without
restarting MQTT or ESP-NOW. The new UART mode starts off when upgrading old
MQTT preferences and retains explicit settings across reboots.
---
#### Add a delay to packets routed through this bridge
@@ -4908,7 +4917,9 @@ Requires WiFi connected and the MQTT bridge running.
**Parameters:**
- `rate`: Integer baud rate from `9600` through the board's compiled
`BRIDGE_MAX_BAUD` (commonly `500000`); for example `115200`. Stop the bridge
with `set bridge.enabled off` before changing it, then enable it again.
with `set rs232.enabled off` in a combined MQTT image, or
`set bridge.enabled off` in a non-MQTT UART image, before changing it.
Enable the UART again with the matching switch.
**Default:** `115200`
@@ -4928,7 +4939,8 @@ Requires WiFi connected and the MQTT bridge running.
The setting is persistent and restarts an enabled bridge immediately. Normal
repeater artifacts start with `bridge.enabled off`; configure the UART and baud
rate before running `set bridge.enabled on`. On the canonical RAK4631 runtime
rate before running `set bridge.enabled on`. Combined MQTT images use
`rs232.enabled` for that independent UART setting. On the canonical RAK4631 runtime
image, UART 1 is reserved even if the bounded boot probe hears no RAK12501.
Silence cannot prove that a cold L76K is physically absent, and that module
remains powered by the shared WB_IO2/3V3_S rail. Use UART 2. UART 1 requires an
+1
View File
@@ -84,6 +84,7 @@ Stopping it first lets you finish configuring both sides before connecting them.
| Option | Choices and purpose |
| --- | --- |
| `espnow.enabled` | `on` starts ESP-NOW; `off` stops it and keeps its saved settings. Independent of MQTT or RS-232. |
| `rs232.enabled` | On combined UART images, starts or stops RS-232 independently of MQTT and ESP-NOW. Use `get rs232.running` to check the live UART. |
| `bridge.channel` | The 2.4 GHz channel, 1-13. Match it on both boards. This is separate from the LoRa frequency in `set radio`. |
| `bridge.format` | `wrapped` is the default for this two-repeater setup and uses the shared secret. `raw` connects to primary-ESP-NOW nodes such as `Generic_ESPNOW` or `SenseCapIndicator-ESPNow`; it ignores the secret. Both peers must use compatible formats. |
| `bridge.secret` | A matching, case-sensitive value of 1-15 characters for `wrapped` mode. It separates bridge groups; it is not strong encryption. MeshCore's own message encryption remains in place. |
+4 -2
View File
@@ -207,8 +207,10 @@ both transports. Use `set espnow.enabled on|off` for ESP-NOW and, where
supported, `set mqtt.enabled on|off` for MQTT. Newly combined ordinary
repeaters start with ESP-NOW off until explicitly enabled, including the first
upgrade from unmarked legacy preferences. Later reboots preserve that setting.
On combined RS-232 boards `bridge.enabled` controls the UART independently. Both use
one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must
On combined RS-232 boards without MQTT, `bridge.enabled` controls the UART.
Full Heltec V3, WSL3, RAK3112 and T-LoRa V2.1 MQTT images also retain RS-232;
use `set rs232.enabled on|off` and `get rs232.running` for their independent
UART. MQTT and ESP-NOW 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
+5 -1
View File
@@ -26,7 +26,11 @@ Classic ESP32 additionally retains its existing 8 KiB **static DRAM** check.
The policy adds allowances for task stacks, radio packet pools, screen objects
and pixel buffers, filesystem/sensor allocations, enabled wireless stacks,
MQTT connections, OTA scratch and transient allocations. A 160x80 ST7735
MQTT connections, OTA scratch and transient allocations. ESP32 images with
RS-232 reserve another 8 KiB even when UART defaults off: 4 KiB for the
source-bounded bridge object and 4 KiB for the UART driver and buffers. This
keeps runtime UART enablement in the budget beside MQTT and ESP-NOW.
A 160x80 ST7735
framebuffer needs 25,602 bytes; an OLED allowance is 4 KiB. nRF52 Full with that
color framebuffer must have at least 72 KiB available before startup allocations.
Headless and OLED devices use their own smaller totals. The JSON lists each
+2
View File
@@ -190,6 +190,8 @@ from some portable builds. The USB browser console still works without it.
| Verbose USB diagnostics | `set usb.debug on` while USB logging is on | `set usb.debug off` | `get usb.debug` reports saved intent |
| Capture RX log to node storage | `log start` | `log stop` | `log` prints the capture locally |
| RS232 / ESP-NOW bridge master | `set bridge.enabled on` | `set bridge.enabled off` | `get bridge.enabled`, `get bridge.running`, `get bridge.type` |
| Independent RS232 in combined Full MQTT images | `set rs232.enabled on` | `set rs232.enabled off` | `get rs232.enabled`, `get rs232.running` |
| Independent ESP-NOW in combined Full images | `set espnow.enabled on` | `set espnow.enabled off` | `get espnow.enabled`, `get espnow.running` |
| MQTT periodic status publication | `set mqtt.status on` | `set mqtt.status off` | `get mqtt.status` shows connection status |
| MQTT packet publication | `set mqtt.packets on` | `set mqtt.packets off` | `get mqtt.packets` |
| SNMP on supported MQTT infrastructure | `set snmp on`, then `reboot` | `set snmp off`, then `reboot` | `get snmp` |
+40 -10
View File
@@ -721,7 +721,7 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
memcpy(&reply_data[4], &now, 4); // include our clock (for easy clock sync, and packet hash uniqueness)
reply_data[8] = 0; // features
#ifdef WITH_RS232_BRIDGE
if (isBridgeRunning()) reply_data[8] |= 0x01; // is bridge, type UART
if (isRs232BridgeRunning()) reply_data[8] |= 0x01; // is bridge, type UART
#ifdef WITH_ESPNOW_BRIDGE
if (isEspNowBridgeRunning()) reply_data[8] |= 0x03; // ESP-NOW may run alongside UART
#endif
@@ -1198,6 +1198,12 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
active_bridge->sendPacket(pkt);
}
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
// UART output remains independent of MQTT's RX/TX and enabled settings.
if (_prefs.bridge_pkt_src == 1 && isRs232BridgeRunning()) {
bridge->sendPacket(pkt);
}
#endif
#if defined(WITH_ESPNOW_BRIDGE) \
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
// ESP-NOW follows bridge.source independently of the other transport's state.
@@ -1250,6 +1256,11 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
active_bridge->sendPacket(pkt);
}
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
if (_prefs.bridge_pkt_src == 0 && isRs232BridgeRunning()) {
bridge->sendPacket(pkt);
}
#endif
#if defined(WITH_ESPNOW_BRIDGE) \
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
if (_prefs.bridge_pkt_src == 0 && espnow_bridge.isRunning()) {
@@ -3464,13 +3475,14 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
, espnow_bridge(&_prefs, _mgr, &rtc)
, shared_espnow_retry_at(0)
#endif
#elif defined(WITH_RS232_BRIDGE)
#endif
#if defined(WITH_RS232_BRIDGE)
, bridge(nullptr)
#if defined(WITH_ESPNOW_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
, espnow_bridge(&_prefs, _mgr, &rtc)
, shared_espnow_retry_at(0)
#endif
#elif defined(WITH_ESPNOW_BRIDGE)
#elif defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
, bridge(&_prefs, _mgr, &rtc)
, shared_espnow_retry_at(0)
#endif
@@ -3655,11 +3667,17 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
// bridge defaults
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
&& !defined(RS232_BRIDGE_DEFAULT_ON) && !defined(WITH_MQTT_BRIDGE)
_prefs.bridge_enabled = 0; // normal repeater until explicitly enabled
#else
_prefs.bridge_enabled = 1; // enabled
#endif
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
_prefs.rs232_bridge_enabled = 0;
#elif defined(WITH_RS232_BRIDGE)
_prefs.rs232_bridge_enabled = 1;
#endif
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
_prefs.espnow_bridge_enabled = 0; // merged normal repeater until explicitly enabled
#else
@@ -3828,7 +3846,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
mqtt_bridge = new MQTTBridge(node_info, _cli.getObserverPrefs(),
getRTCClock(), &self_id);
#endif
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
if (!bridge) {
bridge = createRS232Bridge();
}
@@ -3837,7 +3855,7 @@ void MyMesh::begin(FILESYSTEM *fs) {
// partial object/GPS ownership. Advertisements and bridge.running report
// the actual stopped state rather than the saved preference.
MESH_DEBUG_PRINTLN("RS232 bridge configured on but failed to start");
if (!setBridgeState(false)) {
if (!setRs232BridgeState(false)) {
MESH_DEBUG_PRINTLN(
"RS232 bridge cleanup failed; UART/GPS ownership remains tracked");
}
@@ -3883,6 +3901,14 @@ void MyMesh::begin(FILESYSTEM *fs) {
}
#endif
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
if (rs232BridgeEnabled() && !setRs232BridgeState(true)) {
MESH_DEBUG_PRINTLN("RS232 bridge configured on but failed to start");
if (!setRs232BridgeState(false)) {
MESH_DEBUG_PRINTLN("RS232 bridge cleanup failed; UART/GPS ownership remains tracked");
}
}
#endif
#if defined(WITH_ESPNOW_BRIDGE) \
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
if (_prefs.espnow_bridge_enabled) setEspNowBridgeState(true);
@@ -12845,9 +12871,10 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
else startSharedEspNowBridgeIfReady();
#endif
#endif
#if defined(WITH_RS232_BRIDGE) \
|| (defined(WITH_BRIDGE) && !defined(WITH_MQTT_BRIDGE) \
&& !defined(WITH_ESPNOW_BRIDGE))
#if defined(WITH_RS232_BRIDGE)
if (isRs232BridgeRunning()) bridge->loop();
#elif defined(WITH_BRIDGE) && !defined(WITH_MQTT_BRIDGE) \
&& !defined(WITH_ESPNOW_BRIDGE)
AbstractBridge* active_bridge = activeBridge();
if (active_bridge && active_bridge->isRunning()) active_bridge->loop();
#endif
@@ -13515,6 +13542,9 @@ bool MyMesh::hasPendingWork() const {
#if defined(WITH_BRIDGE)
const AbstractBridge* active_bridge = activeBridge();
if (active_bridge && active_bridge->isRunning()) return true;
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
if (isRs232BridgeRunning()) return true;
#endif
#if defined(WITH_ESPNOW_BRIDGE) \
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
if (espnow_bridge.isRunning()) return true;
+65 -13
View File
@@ -1,6 +1,7 @@
#pragma once
#include <Arduino.h>
#include <new>
#include <Mesh.h>
#if defined(ENABLE_OTA)
#include <helpers/ota/OtaContext.h>
@@ -583,14 +584,15 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
ESPNowBridge espnow_bridge;
uint32_t shared_espnow_retry_at = 0;
#endif
#elif defined(WITH_RS232_BRIDGE)
#endif
#if defined(WITH_RS232_BRIDGE)
RS232Bridge* bridge;
uint8_t active_rs232_bridge_uart = 0;
#if defined(WITH_ESPNOW_BRIDGE)
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
ESPNowBridge espnow_bridge;
uint32_t shared_espnow_retry_at = 0;
#endif
#elif defined(WITH_ESPNOW_BRIDGE)
#elif defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
ESPNowBridge bridge;
uint32_t shared_espnow_retry_at = 0;
#endif
@@ -1207,12 +1209,12 @@ public:
RS232Bridge* createRS232Bridge() {
#ifdef WITH_RS232_BRIDGE_ALT
if (_prefs.bridge_uart == WITH_RS232_BRIDGE_ALT_UART) {
return new RS232Bridge(&_prefs, WITH_RS232_BRIDGE_ALT,
return new (std::nothrow) RS232Bridge(&_prefs, WITH_RS232_BRIDGE_ALT,
WITH_RS232_BRIDGE_ALT_RX,
WITH_RS232_BRIDGE_ALT_TX, _mgr, getRTCClock());
}
#endif
return new RS232Bridge(&_prefs, WITH_RS232_BRIDGE,
return new (std::nothrow) RS232Bridge(&_prefs, WITH_RS232_BRIDGE,
WITH_RS232_BRIDGE_RX, WITH_RS232_BRIDGE_TX,
_mgr, getRTCClock());
}
@@ -1278,11 +1280,55 @@ public:
if (stopped && gps_released) active_rs232_bridge_uart = 0;
return stopped && gps_released;
}
bool rs232BridgeEnabled() const {
#ifdef WITH_MQTT_BRIDGE
return _prefs.rs232_bridge_enabled != 0;
#else
return _prefs.bridge_enabled != 0;
#endif
}
bool isRs232BridgeRunning() const override {
return bridge && bridge->isRunning();
}
bool setRs232BridgeState(bool enable) override {
if (!enable) {
const bool stopped = endRS232Bridge();
if (stopped) {
delete bridge;
bridge = nullptr;
}
return stopped;
}
if (isRs232BridgeRunning()) return true;
// Retry any tracked GPS release before constructing a new UART owner.
if (active_rs232_bridge_uart != 0 && !endRS232Bridge()) return false;
if (!bridge) bridge = createRS232Bridge();
return bridge && beginRS232Bridge();
}
bool restartRs232Bridge() override {
// Apply UART changes synchronously so a failed CLI change can restore
// the previous pins/baud without disturbing MQTT or ESP-NOW.
if (!endRS232Bridge()) return false;
delete bridge;
bridge = nullptr;
return !rs232BridgeEnabled() || setRs232BridgeState(true);
}
#endif
bool isBridgeRunning() const override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
return (mqtt_bridge && mqtt_bridge->isRunning()) || espnow_bridge.isRunning();
#if defined(WITH_MQTT_BRIDGE)
return (mqtt_bridge && mqtt_bridge->isRunning())
#ifdef WITH_ESPNOW_BRIDGE
|| espnow_bridge.isRunning()
#endif
#ifdef WITH_RS232_BRIDGE
|| isRs232BridgeRunning()
#endif
;
#else
const AbstractBridge* active_bridge = activeBridge();
return active_bridge != nullptr && active_bridge->isRunning();
@@ -1385,7 +1431,11 @@ public:
bool setBridgeState(bool enable) override {
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
if (!enable) return setEspNowBridgeState(false) && setMqttBridgeState(false);
if (!enable) {
const bool espnow_stopped = setEspNowBridgeState(false);
const bool mqtt_stopped = setMqttBridgeState(false);
return espnow_stopped && mqtt_stopped;
}
const bool mqtt_ok = _prefs.bridge_enabled
? setMqttBridgeState(true) : setMqttBridgeState(false);
const bool espnow_ok = _prefs.espnow_bridge_enabled
@@ -1417,7 +1467,7 @@ public:
if (!mqtt_bridge) return false;
}
#endif
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
if (enable && !bridge) {
bridge = createRS232Bridge();
if (!bridge) return false;
@@ -1449,7 +1499,7 @@ public:
mqtt_bridge->setBuildDate(getBuildDate());
mqtt_bridge->setStatsSources(this, _radio, _cli.getBoard(), _ms);
#endif
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
const bool started = beginRS232Bridge();
#else
configureBridgeFilter(active_bridge);
@@ -1463,7 +1513,7 @@ public:
}
else
{
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
const bool stopped = endRS232Bridge();
delete bridge;
bridge = nullptr;
@@ -1487,13 +1537,15 @@ public:
return true;
}
#endif
return restartEspNowBridge() && restartMqttBridge();
const bool mqtt_ok = restartMqttBridge();
const bool espnow_ok = restartEspNowBridge();
return mqtt_ok && espnow_ok;
#else
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_RS232_BRIDGE)
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
shared_espnow_retry_at = 0;
#endif
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
// RS-232 changes must be applied synchronously so the CLI can commit or
// roll back the selected pins/baud. This branch also reconstructs a bridge
// after a prior allocation/start failure and is not WebConfig-coalesced.
+33 -1
View File
@@ -139,6 +139,26 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
and check.get("present") is True
for check in manifest.get("verification") or []))
def proven_rs232(manifest: dict) -> bool:
return (qualified_full(manifest)
and "bridge.rs232" in (manifest.get("capabilities") or [])
and any(check.get("capability") == "bridge.rs232"
and check.get("present") is True
for check in manifest.get("verification") or []))
def role_base(target: str) -> str:
base = target.rstrip("_").lower()
if base.endswith("_observer_mqtt"):
base = base[:-len("_observer_mqtt")]
return base
# These normal repeaters already supply a UART. Full MQTT source
# substitution must never remove it while retiring the dedicated image.
uart_full_targets = {"heltec_v3_repeater", "heltec_wsl3_repeater",
"rak_3112_repeater", "lilygo_tlora_v2_1_1_6_repeater"}
combined_rs232 = {role_base(record["manifest"]["target"]) for record in records
if proven_rs232(record["manifest"])} & uart_full_targets
# A resumed build directory can still contain old normal or dedicated
# bridge images. The canonical Full image must prove both runtime bridges
# even when no stale legacy image happens to remain beside it.
@@ -186,6 +206,10 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
# Resume may contain portable legacy images as well as Full ones.
# Retire them only after the matching combined driver is proven.
continue
if (manifest["platform"] == "ESP32_PLATFORM"
and legacy_base.endswith("_repeater_bridge_rs232")
and legacy_base[:-len("_bridge_rs232")] in combined_rs232):
continue
if manifest["platform"] != "ESP32_PLATFORM" or manifest["build_profile"] != "full":
passthrough.append(record)
continue
@@ -221,10 +245,18 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
# A stale normal image must not displace the proven transport
# while simultaneously suppressing its historical dedicated image.
priority = -1
if key in combined_rs232 and not proven_rs232(manifest):
priority = -1
old = chosen.get(key)
if old is None or priority > old[0]:
chosen[key] = (priority, record)
return passthrough + [item[1] for item in chosen.values()]
selected = [item[1] for item in chosen.values()]
for record in selected:
manifest = record["manifest"]
if role_base(manifest["target"]) in uart_full_targets and not proven_rs232(manifest):
raise ValueError(manifest["target"] + ": Full release requires the verified "
"RS232 bridge from its normal repeater")
return passthrough + selected
def stage_release(work: Path, migration_work: Path, destination: Path, version: str,
+5
View File
@@ -74,6 +74,11 @@ def requirements(platform, defines, target):
parts["wireless_stacks"] = max(wifi, ble) if "COMPANION_EXCLUSIVE_WIFI_BLE" in defines else wifi + ble
if "WITH_MQTT_BRIDGE" in defines:
parts["mqtt_connections_buffers"] = 24576
if "WITH_RS232_BRIDGE" in defines:
# The UART bridge is allocated on enable, alongside WiFi/MQTT.
# RS232Bridge.cpp bounds its object to 4 KiB; reserve another
# 4 KiB for the SDK UART task, buffers and allocator overhead.
parts["uart_bridge_and_driver"] = 8192
if "ENABLE_OTA" in defines:
parts["ota_source_scratch"] = 8192
if ("WEBCONFIG_DISABLED" not in defines
+190 -48
View File
@@ -844,7 +844,15 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
is_upgrade = true;
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
if (_prefs->bridge_uart == 0) _prefs->bridge_enabled = 0;
#ifdef WITH_MQTT_BRIDGE
_prefs->rs232_bridge_enabled = 0;
#else
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
#ifdef WITH_RS232_BRIDGE_ALT
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
#endif
) _prefs->bridge_enabled = 0;
#endif
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
#ifdef WITH_RS232_BRIDGE_ALT
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
@@ -1014,6 +1022,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->trace_when_repeat_off = 0;
_prefs->gps_sync_interval_hours = 0;
_prefs->ota_channel = 0;
_prefs->rs232_bridge_enabled = 0;
#if defined(RP2040_PLATFORM)
File file = fs->open(filename, "r");
#else
@@ -1024,9 +1033,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
bool has_runtime_espnow_intent = false;
#endif
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
#ifdef WITH_RS232_BRIDGE
bool has_runtime_rs232_intent = false;
#ifndef WITH_MQTT_BRIDGE
bool has_runtime_bridge_uart = false;
bool has_rs232_intent_tail = false;
#endif
#endif
// Every supported layout contains the fixed 290-byte common core. Reject
// a truncated in-place write before it can leave strings unterminated or
@@ -1392,8 +1404,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->bridge_uart)) {
file.read((uint8_t *)&_prefs->bridge_uart,
sizeof(_prefs->bridge_uart));
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
has_runtime_bridge_uart = true;
#endif
if (file.available() >= (int)sizeof(_prefs->bridge_format)) {
@@ -1422,12 +1433,28 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
if (file.available() >= (int)sizeof(_prefs->ota_channel)) {
file.read((uint8_t *)&_prefs->ota_channel,
sizeof(_prefs->ota_channel));
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
// Reserve the profile byte in every current writer so
// the appended UART intent never depends on build flags.
uint8_t bridge_profile = 0;
if (file.read(&bridge_profile, sizeof(bridge_profile)) == sizeof(bridge_profile)) {
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
has_runtime_espnow_intent = bridge_profile == 0xA1;
}
#endif
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
has_rs232_intent_tail = file.available() > 0;
#endif
if (file.available() >= 2) {
uint8_t rs232_intent = 0;
uint8_t rs232_profile = 0;
file.read(&rs232_intent, sizeof(rs232_intent));
file.read(&rs232_profile, sizeof(rs232_profile));
_prefs->rs232_bridge_enabled = rs232_intent;
#ifdef WITH_RS232_BRIDGE
has_runtime_rs232_intent = rs232_profile == 0xB1
&& rs232_intent <= 1;
#endif
}
}
}
}
}
@@ -1504,19 +1531,51 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_com_prefs_needs_upgrade = true;
}
#endif
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
&& !defined(RS232_BRIDGE_DEFAULT_ON)
if (!has_runtime_bridge_uart || _prefs->bridge_uart == 0) {
// Pre-merge normal repeaters persisted bridge_enabled=1 even though no
// bridge was compiled, with either no UART tail or the no-bridge zero
// sentinel. Fail safe before normalizing the UART instead of claiming
// it unexpectedly; the user can explicitly enable the bridge.
_prefs->bridge_enabled = 0;
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
const bool legacy_uart_intent = has_runtime_bridge_uart
&& (_prefs->bridge_uart == WITH_RS232_BRIDGE_UART
#ifdef WITH_RS232_BRIDGE_ALT
|| _prefs->bridge_uart == WITH_RS232_BRIDGE_ALT_UART
#endif
);
#endif
#ifdef WITH_RS232_BRIDGE
if (!has_runtime_rs232_intent) {
// Old MQTT/normal images had no UART or stored the no-bridge zero
// sentinel. A standalone UART profile can retain supported legacy
// intent; a torn/corrupt new intent never takes that fallback path.
#ifdef WITH_MQTT_BRIDGE
// A nonzero old UART byte alone cannot identify the previous role.
// Preserve MQTT's primary intent and require the independent marker.
_prefs->rs232_bridge_enabled = 0;
#else
_prefs->rs232_bridge_enabled = !has_rs232_intent_tail && legacy_uart_intent
? (_prefs->bridge_enabled == 1 ? 1 : 0) : 0;
if (has_rs232_intent_tail) {
// A partial/corrupt new UART tail cannot inherit another profile's
// primary MQTT flag, including in dedicated/default-on UART images.
_prefs->bridge_enabled = 0;
}
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
_prefs->bridge_enabled = _prefs->rs232_bridge_enabled;
#endif
#endif
_com_prefs_needs_upgrade = true;
}
#ifndef WITH_MQTT_BRIDGE
// A current combined-profile file may carry MQTT on and UART off. Its
// validated UART byte must win before restoring the legacy primary alias.
if (has_runtime_rs232_intent) {
_prefs->bridge_enabled = _prefs->rs232_bridge_enabled;
}
_prefs->rs232_bridge_enabled = _prefs->bridge_enabled == 1 ? 1 : 0;
#endif
#else
_prefs->rs232_bridge_enabled = 0;
#endif
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
_prefs->espnow_bridge_enabled = constrain(_prefs->espnow_bridge_enabled, 0, 1);
_prefs->rs232_bridge_enabled = constrain(_prefs->rs232_bridge_enabled, 0, 1);
#if defined(WITH_ESPNOW_BRIDGE) && defined(ESPNOW_BRIDGE_MERGED) \
&& !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
// An unrelated legacy primary bridge flag cannot enable this new mode.
@@ -1785,8 +1844,17 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
WRITE_COMMON_PREFS(&prefs->ota_channel); // appended WiFi OTA channel; never shift older fields
#ifdef ESPNOW_BRIDGE_MERGED
const uint8_t bridge_profile = 0xA1;
WRITE_COMMON_PREFS(&bridge_profile); // independent ESP-NOW intent belongs to this profile
#else
const uint8_t bridge_profile = 0;
#endif
WRITE_COMMON_PREFS(&bridge_profile); // ESP-NOW profile marker/reserved byte
WRITE_COMMON_PREFS(&prefs->rs232_bridge_enabled); // independent UART intent
#ifdef WITH_RS232_BRIDGE
const uint8_t rs232_profile = 0xB1;
#else
const uint8_t rs232_profile = 0;
#endif
WRITE_COMMON_PREFS(&rs232_profile); // UART intent belongs to this profile
#undef WRITE_COMMON_PREFS_BYTES
#undef WRITE_COMMON_PREFS
@@ -1801,6 +1869,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
_common_save_succeeded = false;
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
_prefs->rs232_bridge_enabled = _prefs->bridge_enabled == 1 ? 1 : 0;
#elif !defined(WITH_RS232_BRIDGE)
_prefs->rs232_bridge_enabled = 0;
#endif
}
#ifdef WITH_MQTT_BRIDGE
// Observer builds use a verified temp/backup transaction for common prefs.
@@ -1952,8 +2025,17 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
file.write((uint8_t *)&_prefs->ota_channel, sizeof(_prefs->ota_channel)); // appended WiFi OTA channel
#ifdef ESPNOW_BRIDGE_MERGED
const uint8_t bridge_profile = 0xA1;
file.write(&bridge_profile, sizeof(bridge_profile)); // independent ESP-NOW intent belongs to this profile
#else
const uint8_t bridge_profile = 0;
#endif
file.write(&bridge_profile, sizeof(bridge_profile)); // ESP-NOW profile marker/reserved byte
file.write((uint8_t *)&_prefs->rs232_bridge_enabled, sizeof(_prefs->rs232_bridge_enabled));
#ifdef WITH_RS232_BRIDGE
const uint8_t rs232_profile = 0xB1;
#else
const uint8_t rs232_profile = 0;
#endif
file.write(&rs232_profile, sizeof(rs232_profile)); // UART intent belongs to this profile
_common_save_succeeded = file.commit();
if (!_common_save_succeeded) {
@@ -3126,7 +3208,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
}
#ifdef WITH_RS232_BRIDGE
if (is_gps_toggle && strcmp(value, "1") == 0
&& _callbacks->isBridgeRunning()
&& _callbacks->isRs232BridgeRunning()
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
strcpy(reply, "saved; UART GPS paused while bridge is enabled");
} else
@@ -3170,11 +3252,12 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
_prefs->gps_enabled = 1;
savePrefs();
if (_callbacks->isBridgeRunning()
if (
#ifdef WITH_RS232_BRIDGE
_callbacks->isRs232BridgeRunning()
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)
#else
&& false
false
#endif
) {
strcpy(reply, "saved; UART GPS paused while bridge is enabled");
@@ -3199,7 +3282,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
if (!_prefs->gps_enabled) {
strcpy(reply, "gps is off");
#ifdef WITH_RS232_BRIDGE
} else if (_callbacks->isBridgeRunning()
} else if (_callbacks->isRs232BridgeRunning()
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
strcpy(reply, "gps paused by RS232 bridge");
#endif
@@ -3781,6 +3864,45 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
return;
}
#endif
#ifdef WITH_RS232_BRIDGE
if (strncmp(config, "rs232.enabled ", 14) == 0) {
bool enable = false;
if (!parseOnOffStrict(config + 14, enable)) {
strcpy(reply, "Error: use set rs232.enabled on|off");
return;
}
if (enable && _sensors->gpsSerialTransportMayConflict(_prefs->bridge_uart)
&& (!_sensors->gpsUsesSerialUart(_prefs->bridge_uart)
|| !_sensors->gpsSerialTransportCanYield(_prefs->bridge_uart))) {
strcpy(reply, "Error: UART may be driven by GPS; use UART 2 or a no-GPS build");
return;
}
#ifdef WITH_MQTT_BRIDGE
uint8_t& intent = _prefs->rs232_bridge_enabled;
#else
uint8_t& intent = _prefs->bridge_enabled;
#endif
const uint8_t previous_enabled = intent;
const bool previous_running = _callbacks->isRs232BridgeRunning();
intent = enable ? 1 : 0;
const bool applied = _callbacks->setRs232BridgeState(enable);
const bool saved = applied && trySavePrefs();
if (!saved) {
intent = previous_enabled;
#ifndef WITH_MQTT_BRIDGE
_prefs->rs232_bridge_enabled = _prefs->bridge_enabled == 1 ? 1 : 0;
#endif
const bool restored = _callbacks->setRs232BridgeState(previous_running);
strcpy(reply, restored
? "Error: RS232 change failed; setting unchanged"
: "Error: RS232 change failed; previous runtime could not be restored");
} else {
strcpy(reply, enable && _sensors->gpsUsesSerialUart(_prefs->bridge_uart)
? "OK - UART GPS paused" : "OK");
}
return;
}
#endif
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
@@ -4674,7 +4796,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
strcpy(reply, applied ? "OK"
: "Error: ESP-NOW runtime change failed; setting saved");
#else
#ifdef WITH_RS232_BRIDGE
#if defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
if (enable
&& _sensors->gpsSerialTransportMayConflict(_prefs->bridge_uart)
&& (!_sensors->gpsUsesSerialUart(_prefs->bridge_uart)
@@ -4683,11 +4805,10 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
const uint8_t previous_enabled = _prefs->bridge_enabled;
const bool previous_running = _callbacks->isBridgeRunning();
const bool previous_running = _callbacks->isRs232BridgeRunning();
_prefs->bridge_enabled = enable;
const bool applied = _callbacks->setBridgeState(enable);
if (applied) {
savePrefs();
const bool applied = _callbacks->setRs232BridgeState(enable);
if (applied && trySavePrefs()) {
#ifdef WITH_RS232_BRIDGE
if (enable && _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
strcpy(reply, "OK - UART GPS paused");
@@ -4698,12 +4819,13 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
}
} else {
_prefs->bridge_enabled = previous_enabled;
_prefs->rs232_bridge_enabled = previous_enabled == 1 ? 1 : 0;
bool restored = true;
if (_callbacks->isBridgeRunning() != previous_running
if (_callbacks->isRs232BridgeRunning() != previous_running
|| !previous_running) {
// The false/false case still calls disable to delete a failed,
// non-running heap-backed bridge instance.
restored = _callbacks->setBridgeState(previous_running);
restored = _callbacks->setRs232BridgeState(previous_running);
}
strcpy(reply, restored
? "Error: bridge state change failed; setting unchanged"
@@ -4758,22 +4880,21 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
if (mesh::cli::parseUnsignedIntegerStrict(&config[12], baud)
&& baud >= 9600 && baud <= BRIDGE_MAX_BAUD) {
const uint32_t previous_baud = _prefs->bridge_baud;
const bool previous_running = _callbacks->isBridgeRunning();
const bool previous_running = _callbacks->isRs232BridgeRunning();
_prefs->bridge_baud = baud;
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#ifdef WITH_MQTT_BRIDGE
!_prefs->rs232_bridge_enabled || _callbacks->restartRs232Bridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
!_prefs->bridge_enabled || _callbacks->restartRs232Bridge();
#endif
if (applied) {
savePrefs();
if (applied && trySavePrefs()) {
strcpy(reply, "OK");
} else {
_prefs->bridge_baud = previous_baud;
const bool restored = previous_running
? _callbacks->restartBridge()
: _callbacks->setBridgeState(false);
? _callbacks->restartRs232Bridge()
: _callbacks->setRs232BridgeState(false);
strcpy(reply, restored
? "Error: bridge failed to restart; baud unchanged"
: "Error: bridge failed to restart; previous baud could not be restored");
@@ -4798,7 +4919,12 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
#endif
}
else {
if (_prefs->bridge_enabled
if (
#ifdef WITH_MQTT_BRIDGE
_prefs->rs232_bridge_enabled
#else
_prefs->bridge_enabled
#endif
&& _sensors->gpsSerialTransportMayConflict((uint8_t)uart)
&& (!_sensors->gpsUsesSerialUart((uint8_t)uart)
|| !_sensors->gpsSerialTransportCanYield((uint8_t)uart))) {
@@ -4806,28 +4932,31 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
return;
}
const uint8_t previous_uart = _prefs->bridge_uart;
const bool previous_running = _callbacks->isBridgeRunning();
const bool previous_running = _callbacks->isRs232BridgeRunning();
_prefs->bridge_uart = (uint8_t)uart;
const bool applied =
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
#ifdef WITH_MQTT_BRIDGE
!_prefs->rs232_bridge_enabled || _callbacks->restartRs232Bridge();
#else
!_prefs->bridge_enabled || _callbacks->restartBridge();
!_prefs->bridge_enabled || _callbacks->restartRs232Bridge();
#endif
if (!applied) {
if (!applied || !trySavePrefs()) {
_prefs->bridge_uart = previous_uart;
const bool restored = previous_running
? _callbacks->restartBridge()
: _callbacks->setBridgeState(false);
? _callbacks->restartRs232Bridge()
: _callbacks->setRs232BridgeState(false);
strcpy(reply, restored
? "Error: bridge failed to restart; UART unchanged"
: "Error: bridge failed to restart; previous UART could not be restored");
} else if (_prefs->bridge_enabled
} else if (
#ifdef WITH_MQTT_BRIDGE
_prefs->rs232_bridge_enabled
#else
_prefs->bridge_enabled
#endif
&& _sensors->gpsUsesSerialUart(_prefs->bridge_uart)) {
savePrefs();
strcpy(reply, "OK - UART GPS paused");
} else {
savePrefs();
strcpy(reply, "OK");
}
}
@@ -5323,7 +5452,11 @@ 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",
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
"mqtt+rs232+espnow"
#elif defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
"mqtt+rs232"
#elif defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
"mqtt+espnow"
#elif defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
"rs232+espnow"
@@ -5370,6 +5503,15 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
sprintf(reply, "> %s", _prefs->bridge_pkt_src ? "rx" : "tx");
#endif
#ifdef WITH_RS232_BRIDGE
} else if (configKeyEquals(config, "rs232.enabled")) {
sprintf(reply, "> %s",
#ifdef WITH_MQTT_BRIDGE
_prefs->rs232_bridge_enabled ? "on" : "off");
#else
_prefs->bridge_enabled ? "on" : "off");
#endif
} else if (configKeyEquals(config, "rs232.running")) {
sprintf(reply, "> %s", _callbacks->isRs232BridgeRunning() ? "on" : "off");
} else if (configKeyEquals(config, "bridge.baud")) {
sprintf(reply, "> %d", (uint32_t)_prefs->bridge_baud);
} else if (configKeyEquals(config, "bridge.uart")) {
+7
View File
@@ -205,6 +205,10 @@ public:
// retain the previous MQTT combined-Full behavior after an upgrade. Merged
// default-off profiles require their appended profile marker instead.
uint8_t espnow_bridge_enabled = 1;
// Independent UART intent in MQTT-combined profiles. Non-MQTT UART roles
// retain bridge_enabled as their legacy alias and mirror it on save.
// Appended after the profile markers, never beside the legacy bridge byte.
uint8_t rs232_bridge_enabled = 0;
// Appended after the ESP-NOW intent. Zero keeps the board's legacy cadence.
uint16_t gps_sync_interval_hours = 0;
uint8_t retry_preset = 0;
@@ -600,9 +604,12 @@ public:
// 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 setRs232BridgeState(bool enable) { return setBridgeState(enable); }
virtual bool restartMqttBridge() { return restartBridge(); }
virtual bool restartEspNowBridge() { return restartBridge(); }
virtual bool restartRs232Bridge() { return restartBridge(); }
virtual bool isEspNowBridgeRunning() { return isBridgeRunning(); }
virtual bool isRs232BridgeRunning() const { return isBridgeRunning(); }
virtual void restartBridgeSlot(int slot) {
// Default: fall back to full restart
+12 -1
View File
@@ -5,6 +5,12 @@
#ifdef WITH_RS232_BRIDGE
#ifdef ESP32
// check_firmware_ram.py reserves this object plus the UART driver's runtime
// allocation even when a combined Full image starts with the UART disabled.
static_assert(sizeof(RS232Bridge) <= 4096, "Update the ESP32 UART heap budget");
#endif
RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
int16_t tx_pin, mesh::PacketManager *mgr,
mesh::RTCClock *rtc)
@@ -12,9 +18,10 @@ RS232Bridge::RS232Bridge(NodePrefs *prefs, Stream &serial, int16_t rx_pin,
_tx_pin(tx_pin) {}
void RS232Bridge::begin() {
_initialized = false;
BRIDGE_DEBUG_PRINTLN("Initializing at %d baud...\n", _prefs->bridge_baud);
#if defined(ESP32)
((HardwareSerial *)_serial)->setPins(_rx_pin, _tx_pin);
if (!((HardwareSerial *)_serial)->setPins(_rx_pin, _tx_pin)) return;
#elif defined(NRF52_PLATFORM)
// Tested with RAK_4631 and T114
// The Adafruit Uart object may already be active on its variant defaults.
@@ -33,7 +40,11 @@ void RS232Bridge::begin() {
((HardwareSerial *)_serial)->begin(_prefs->bridge_baud);
// Update bridge state
#if defined(ESP32)
_initialized = static_cast<bool>(*static_cast<HardwareSerial *>(_serial));
#else
_initialized = true;
#endif
}
void RS232Bridge::end() {
+387
View File
@@ -0,0 +1,387 @@
// Real repeater methods and CLI dispatch are inserted by the Python harness.
#include <cassert>
#include <cstdio>
#include <cstring>
#include <cstdint>
#include <initializer_list>
#include <new>
#include <helpers/CLICommandUtils.h>
#include <helpers/bridges/ESPNowBridgeFormat.h>
#define BRIDGE_MAX_BAUD 115200
#define PUB_KEY_SIZE 32
#define MESH_DEBUG_PRINTLN(...) ((void)0)
namespace mesh {
struct Packet { bool allowed = true; };
struct LocalIdentity { uint8_t pub_key[32] = {}; };
struct Utils { static void toHex(char* out, const uint8_t*, unsigned) { strcpy(out, "mock-node"); } };
}
struct StrHelper {
static void strncpy(char* out, const char* in, size_t size) {
assert(size); std::strncpy(out, in, size - 1); out[size - 1] = 0;
}
};
struct NodePrefs {
uint8_t bridge_enabled = 0, espnow_bridge_enabled = 0, rs232_bridge_enabled = 0;
uint16_t bridge_delay = 0;
uint8_t bridge_pkt_src = 0, bridge_uart = 0, bridge_channel = 0, bridge_format = 0;
uint32_t bridge_baud = 0;
float freq = 915, bw = 125;
uint8_t sf = 7, cr = 5, disable_fwd = 0;
char bridge_secret[32] = {}, node_name[32] = "triple bridge";
};
struct FakeUart {
unsigned begins = 0, ends = 0;
int rx = -1, tx = -1;
uint32_t baud = 0;
bool running = false;
} Serial2;
struct FakeSensors {
unsigned yield_calls = 0;
bool gpsUsesSerialUart(uint8_t uart) const { return uart == 1; }
bool gpsSerialTransportMayConflict(uint8_t uart) const { return uart == 1; }
bool gpsSerialTransportCanYield(uint8_t) const { return true; }
bool setGpsSerialTransportBlocked(uint8_t, bool) { ++yield_calls; return true; }
};
class AbstractBridge {
bool (*filter)(void*, const mesh::Packet*) = nullptr;
void* filter_context = nullptr;
protected:
bool allow(mesh::Packet* packet) { return !filter || filter(filter_context, packet); }
public:
unsigned filters = 0;
virtual ~AbstractBridge() = default;
virtual void begin() = 0;
virtual void end() = 0;
virtual bool isRunning() const = 0;
virtual void loop() = 0;
virtual void sendPacket(mesh::Packet*) = 0;
void setPacketFilter(bool (*check)(void*, const mesh::Packet*), void* context) {
filter = check; filter_context = context; ++filters;
}
};
class RS232Bridge : public AbstractBridge {
NodePrefs* prefs;
FakeUart& uart;
int rx, tx;
bool running = false;
public:
inline static unsigned live = 0, fail_starts = 0, fail_allocations = 0;
static void* operator new(size_t size, const std::nothrow_t&) noexcept {
if (fail_allocations) { --fail_allocations; return nullptr; }
return ::operator new(size, std::nothrow);
}
static void operator delete(void* pointer) noexcept { ::operator delete(pointer); }
unsigned loops = 0, sent = 0;
RS232Bridge(NodePrefs* p, FakeUart& u, int r, int t, void*, void*)
: prefs(p), uart(u), rx(r), tx(t) { ++live; }
~RS232Bridge() override { --live; }
void begin() override {
assert(filters); ++uart.begins;
if (fail_starts) { --fail_starts; return; }
uart.rx = rx; uart.tx = tx; uart.baud = prefs->bridge_baud;
running = uart.running = true;
}
void end() override { ++uart.ends; running = uart.running = false; }
bool isRunning() const override { return running; }
void loop() override { ++loops; }
void sendPacket(mesh::Packet* p) override { if (allow(p)) ++sent; }
};
class ESPNowBridge : public AbstractBridge {
bool running = false;
public:
unsigned begins = 0, ends = 0, fail_starts = 0, loops = 0, sent = 0;
void begin() override {
assert(filters); ++begins;
if (fail_starts) { --fail_starts; return; }
running = true;
}
void end() override { ++ends; running = false; }
bool isRunning() const override { return running; }
void loop() override { ++loops; }
void sendPacket(mesh::Packet* p) override { if (allow(p)) ++sent; }
};
struct MQTTNodeInfo {
const char* node_name;
float* freq;
float* bw;
uint8_t* sf;
uint8_t* cr;
uint8_t* repeat_flag;
bool repeat_when_nonzero;
};
class MQTTBridge : public AbstractBridge {
bool running = false;
public:
inline static unsigned live = 0, fail_starts = 0;
unsigned begins = 0, ends = 0, loops = 0, sent = 0, received = 0;
MQTTBridge(MQTTNodeInfo, void*, void*, mesh::LocalIdentity*) { ++live; }
~MQTTBridge() override { --live; }
void setDeviceID(const char*) {}
void setFirmwareVersion(const char*) {}
void setBoardModel(const char*) {}
void setBuildDate(const char*) {}
void setStatsSources(void*, void*, void*, void*) {}
void begin() override {
assert(filters); ++begins;
if (fail_starts) { --fail_starts; return; }
running = true;
}
void end() override { ++ends; running = false; }
bool isRunning() const override { return running; }
void loop() override { ++loops; }
void sendPacket(mesh::Packet* p) override { if (allow(p)) ++sent; }
void onPacketReceived(mesh::Packet* p) { if (allow(p)) ++received; }
};
static uint32_t fake_millis = 100;
static uint32_t millis() { return fake_millis; }
static bool millisHasNowPassed(uint32_t deadline) {
return static_cast<int32_t>(fake_millis - deadline) >= 0;
}
struct FakeWiFi { bool connected = true; bool isConnected() const { return connected; } } WiFi;
struct FakeBoard {
bool ota = false, allow_start = true, allow_stop = true;
bool isOTAUpdateRunning() const { return ota; }
const char* getManufacturerName() const { return "synthetic ESP32"; }
bool startOTAUpdate(const char*, char* reply, bool) {
ota = allow_start; strcpy(reply, allow_start ? "Started: http://192.168.4.1/update" : "ERR: OTA failed");
return allow_start;
}
bool stopOTAUpdate(char* reply) {
if (allow_stop) ota = false;
strcpy(reply, allow_stop ? "OK - OTA stopped" : "ERR: upload active"); return allow_stop;
}
};
struct FakeCLI {
FakeBoard board;
FakeBoard* getBoard() { return &board; }
void* getObserverPrefs() { return nullptr; }
};
struct FakeAlerter { void setBridge(MQTTBridge*) {} };
struct Callbacks {
virtual ~Callbacks() = default;
virtual bool isBridgeRunning() const = 0;
virtual bool setBridgeState(bool) = 0;
virtual bool restartBridge() = 0;
virtual bool isRs232BridgeRunning() const = 0;
virtual bool setRs232BridgeState(bool) = 0;
virtual bool restartRs232Bridge() = 0;
virtual bool isEspNowBridgeRunning() = 0;
virtual bool setEspNowBridgeState(bool) = 0;
virtual bool restartEspNowBridge() = 0;
virtual bool isMqttBridgeRunning() = 0;
virtual bool setMqttBridgeState(bool) = 0;
virtual bool restartMqttBridge() = 0;
};
class MyMesh : public Callbacks {
public:
NodePrefs _prefs;
RS232Bridge* bridge = nullptr;
MQTTBridge* mqtt_bridge = nullptr;
ESPNowBridge espnow_bridge;
uint8_t active_rs232_bridge_uart = 0;
uint32_t shared_espnow_retry_at = 0;
unsigned filter_checks = 0;
FakeSensors sensors;
FakeCLI _cli;
FakeAlerter _alerter;
mesh::LocalIdentity self_id;
void* _mgr = nullptr; void* _radio = nullptr; void* _ms = nullptr;
void* getRTCClock() { return nullptr; }
mesh::LocalIdentity getSelfId() { return self_id; }
const char* getFirmwareVer() { return "test"; }
const char* getBuildDate() { return "test"; }
bool allowTransportPacket(const mesh::Packet* p, unsigned mode) {
assert(mode == 2); ++filter_checks; return p->allowed;
}
@FILTER@
void defaults() {
@DEFAULTS@
}
void boot() {
@BOOT@
}
void routeRx(mesh::Packet* pkt) {
@ROUTE_RX@
}
void routeTx(mesh::Packet* pkt) {
@ROUTE_TX@
}
void serviceBridges() {
@SERVICE@
}
bool bridgesPreventSleep() const {
@PENDING@
return false;
}
~MyMesh() override {
assert(setRs232BridgeState(false));
assert(setBridgeState(false));
delete mqtt_bridge;
}
@LIFECYCLE@
};
@PARSERS@
class CommonCLI {
public:
NodePrefs* _prefs;
MyMesh* _callbacks;
FakeSensors* _sensors;
FakeBoard* _board;
NodePrefs stored;
unsigned saves = 0;
bool fail_save = false;
explicit CommonCLI(MyMesh& mesh) : _prefs(&mesh._prefs), _callbacks(&mesh),
_sensors(&mesh.sensors), _board(mesh._cli.getBoard()) {}
void savePrefs() { ++saves; stored = *_prefs; }
bool trySavePrefs() { if (fail_save) return false; savePrefs(); return true; }
void set(const char* input, char* reply) {
char config[256] = {}; assert(strlen(input) < sizeof(config)); strcpy(config, input);
@SET_BRANCHES@
}
bool get(const char* config, char* reply) {
@GET_BRANCHES@
return false;
}
void run(const char* command, char* reply) {
if (false) {
@OTA@
}
}
};
static void expect_get(CommonCLI& cli, const char* key, const char* expected) {
char reply[256] = {}; cli.get(key, reply); assert(strcmp(reply, expected) == 0);
}
int main() {
{
MyMesh fresh; fresh.defaults(); CommonCLI cli(fresh);
assert(fresh._prefs.bridge_enabled && fresh._prefs.espnow_bridge_enabled);
assert(!fresh._prefs.rs232_bridge_enabled);
const unsigned before = Serial2.begins;
fresh.boot();
assert(fresh.isMqttBridgeRunning() && fresh.isEspNowBridgeRunning());
assert(!fresh.isRs232BridgeRunning() && !fresh.bridge && Serial2.begins == before);
expect_get(cli, "bridge.type", "> mqtt+rs232+espnow");
expect_get(cli, "rs232.enabled", "> off");
expect_get(cli, "rs232.running", "> off");
}
MyMesh mesh; mesh.defaults(); mesh.boot(); CommonCLI cli(mesh);
char reply[256] = {};
cli.set("rs232.enabled on", reply);
assert(strcmp(reply, "OK") == 0 && mesh.isRs232BridgeRunning());
assert(Serial2.rx == 5 && Serial2.tx == 6 && Serial2.baud == 115200);
assert(mesh.sensors.yield_calls == 0 && mesh.bridgesPreventSleep());
expect_get(cli, "rs232.enabled", "> on"); expect_get(cli, "rs232.running", "> on");
mesh::Packet packet;
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
assert(mesh.espnow_bridge.sent == 1 && mesh.espnow_bridge.loops == 1);
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
assert(mesh.mqtt_bridge->loops == 0); // Its actual task services MQTT independently.
assert(mesh.filter_checks == 4);
packet.allowed = false;
mesh.routeRx(&packet); mesh.routeTx(&packet);
assert(mesh.filter_checks == 8 && mesh.bridge->sent == 1 && mesh.espnow_bridge.sent == 1);
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
packet.allowed = true;
const unsigned wifi_starts = mesh.mqtt_bridge->begins, wireless_starts = mesh.espnow_bridge.begins;
cli.set("bridge.baud 57600", reply);
assert(strcmp(reply, "OK") == 0 && Serial2.baud == 57600);
cli.set("bridge.uart 2", reply);
assert(strcmp(reply, "OK") == 0 && mesh.isRs232BridgeRunning());
assert(mesh.mqtt_bridge->begins == wifi_starts && mesh.espnow_bridge.begins == wireless_starts);
const unsigned saves = cli.saves;
for (const char* invalid : {"rs232.enabled onjunk", "bridge.uart 1", "bridge.uart 2junk",
"bridge.baud 9599", "bridge.baud 115201", "bridge.baud 57600junk"}) {
cli.set(invalid, reply); assert(strncmp(reply, "Error", 5) == 0 && cli.saves == saves);
assert(mesh.isRs232BridgeRunning() && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
}
RS232Bridge::fail_starts = 1;
cli.set("bridge.baud 19200", reply);
assert(strstr(reply, "baud unchanged") && cli.saves == saves);
assert(mesh._prefs.bridge_baud == 57600 && Serial2.baud == 57600 && mesh.isRs232BridgeRunning());
assert(mesh.mqtt_bridge->begins == wifi_starts && mesh.espnow_bridge.begins == wireless_starts);
cli.set("rs232.enabled off", reply); assert(strcmp(reply, "OK") == 0 && !mesh.bridge);
const unsigned disabled_saves = cli.saves;
RS232Bridge::fail_starts = 1;
cli.set("rs232.enabled on", reply);
assert(strstr(reply, "setting unchanged") && cli.saves == disabled_saves && !mesh.bridge);
assert(!mesh._prefs.rs232_bridge_enabled && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
RS232Bridge::fail_allocations = 1;
cli.set("rs232.enabled on", reply);
assert(strstr(reply, "setting unchanged") && !mesh.bridge && !mesh._prefs.rs232_bridge_enabled);
assert(cli.saves == disabled_saves && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
cli.fail_save = true;
cli.set("rs232.enabled on", reply);
assert(strstr(reply, "setting unchanged") && !mesh.bridge && !mesh._prefs.rs232_bridge_enabled);
assert(cli.saves == disabled_saves && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
cli.fail_save = false;
cli.set("rs232.enabled on", reply); assert(strcmp(reply, "OK") == 0);
const unsigned saved_after_enable = cli.saves;
cli.fail_save = true;
cli.set("rs232.enabled off", reply);
assert(strstr(reply, "setting unchanged") && mesh.isRs232BridgeRunning() && mesh._prefs.rs232_bridge_enabled);
cli.set("bridge.baud 19200", reply);
assert(strstr(reply, "baud unchanged") && mesh._prefs.bridge_baud == 57600 && Serial2.baud == 57600);
cli.set("bridge.uart 2", reply);
assert(strstr(reply, "UART unchanged") && mesh._prefs.bridge_uart == 2 && mesh.isRs232BridgeRunning());
assert(cli.saves == saved_after_enable && mesh.mqtt_bridge->begins == wifi_starts);
assert(mesh.espnow_bridge.begins == wireless_starts);
cli.fail_save = false;
// Saved UART settings recreate a running independent bridge after reboot.
{
MyMesh restarted; restarted.defaults(); restarted._prefs = cli.stored; restarted.boot();
assert(restarted.isRs232BridgeRunning() && restarted.isMqttBridgeRunning());
assert(restarted._prefs.bridge_baud == 57600 && restarted.isEspNowBridgeRunning());
}
// ESP-NOW format/channel changes and its legacy alias never restart the UART.
const unsigned uart_before_wireless = Serial2.begins;
cli.set("bridge.channel 6", reply); cli.set("bridge.secret three-transports", reply);
cli.set("bridge.format raw", reply); assert(strncmp(reply, "OK", 2) == 0);
assert(Serial2.begins == uart_before_wireless && mesh.mqtt_bridge->begins == wifi_starts);
cli.set("bridge.enabled off", reply);
assert(strcmp(reply, "OK") == 0 && !mesh.isEspNowBridgeRunning());
assert(mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
cli.set("mqtt.enabled off", reply);
assert(strcmp(reply, "OK") == 0 && !mesh.isMqttBridgeRunning());
assert(mesh.isRs232BridgeRunning() && mesh.bridgesPreventSleep());
const unsigned uart_tx_before = mesh.bridge->sent;
mesh.routeRx(&packet); mesh.serviceBridges();
assert(mesh.bridge->sent == uart_tx_before + 1);
cli.set("espnow.enabled on", reply); assert(strcmp(reply, "OK") == 0);
cli.set("mqtt.enabled on", reply); assert(strcmp(reply, "OK") == 0);
mesh.serviceBridges(); assert(mesh.isEspNowBridgeRunning());
// Browser OTA releases only WiFi users and leaves the enabled UART running.
const unsigned uart_before_ota = Serial2.begins;
cli.run("start ota ap", reply);
assert(mesh._cli.board.ota && !mesh.isMqttBridgeRunning() && !mesh.isEspNowBridgeRunning());
assert(mesh.isRs232BridgeRunning() && Serial2.begins == uart_before_ota);
for (unsigned tick = 0; tick < 5; ++tick) { fake_millis += 5001; mesh.serviceBridges(); }
assert(!mesh.isEspNowBridgeRunning() && mesh.isRs232BridgeRunning());
mesh._cli.board.allow_stop = false; cli.run("stop ota", reply);
assert(strcmp(reply, "ERR: upload active") == 0 && !mesh.isMqttBridgeRunning());
mesh._cli.board.allow_stop = true; cli.run("stop ota", reply); mesh.serviceBridges();
assert(!mesh._cli.board.ota && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
assert(mesh.isRs232BridgeRunning() && Serial2.begins == uart_before_ota);
// Wireless startup failure retries without stopping or reallocating UART.
cli.set("espnow.enabled off", reply); mesh.espnow_bridge.fail_starts = 2;
cli.set("espnow.enabled on", reply); mesh.serviceBridges();
const unsigned retry_starts = mesh.espnow_bridge.begins;
mesh.serviceBridges(); assert(mesh.espnow_bridge.begins == retry_starts);
fake_millis += 5001; mesh.serviceBridges(); assert(!mesh.isEspNowBridgeRunning());
fake_millis += 5001; mesh.serviceBridges(); assert(mesh.isEspNowBridgeRunning());
assert(mesh.isRs232BridgeRunning() && Serial2.begins == uart_before_ota);
// Every switch, parameter change and rollback leaves other transports intact.
for (unsigned cycle = 0; cycle < 100; ++cycle) {
cli.set("rs232.enabled off", reply);
assert(!mesh.bridge && mesh.isMqttBridgeRunning() && mesh.isEspNowBridgeRunning());
cli.set("rs232.enabled on", reply);
assert(mesh.isRs232BridgeRunning() && RS232Bridge::live == 1);
}
cli.set("rs232.enabled off", reply); cli.set("espnow.enabled off", reply); cli.set("mqtt.enabled off", reply);
assert(!mesh.bridgesPreventSleep() && RS232Bridge::live == 0);
cli.run("start ota", reply); cli.run("stop ota", reply); mesh.serviceBridges();
assert(!mesh.isRs232BridgeRunning() && !mesh.isMqttBridgeRunning() && !mesh.isEspNowBridgeRunning());
assert(mesh.sensors.yield_calls == 0);
puts("Three transport runtime checks passed");
}
+21 -3
View File
@@ -6,7 +6,7 @@
#include <helpers/ConfigSerializer.h>
struct NodePrefs : ConfigSerializer {
uint8_t bridge_enabled = 0, bridge_uart = 2, espnow_bridge_enabled = 0;
uint8_t bridge_enabled = 0, bridge_uart = 2, espnow_bridge_enabled = 0, rs232_bridge_enabled = 0;
uint16_t bridge_delay = 500;
uint8_t bridge_pkt_src = 1, bridge_channel = 1, bridge_format = 0;
uint32_t bridge_baud = 115200;
@@ -47,8 +47,14 @@ struct CLI {
}
bool import(FakeFS* fs) {
bool loaded = false, is_upgrade = false;
#ifdef WITH_MQTT_BRIDGE
bool node_prefs_needs_migration = false;
#endif
@IMPORT@
assert(loaded == is_upgrade);
#ifdef WITH_MQTT_BRIDGE
assert(node_prefs_needs_migration == loaded);
#endif
return loaded;
}
};
@@ -60,17 +66,27 @@ int main() {
"{name:\"imported node\",bridge:{en:1,uart:1}}",
"{name:\"imported node\",bridge:{en:1,uart:2}}",
"{name:\"imported node\",bridge:{en:0,uart:2}}",
"{name:\"imported node\",bridge:{en:1,uart:2,rs232:1}}",
}) {
CLI cli;
cli.prefs.espnow_bridge_enabled = 1; // An old unused flag cannot prove intent.
cli.prefs.rs232_bridge_enabled = 1; // Reused objects cannot donate UART intent.
FakeFS fs{json};
assert(cli.import(&fs) && cli.saves == 1);
assert(cli.import(&fs));
#ifdef WITH_MQTT_BRIDGE
assert(cli.saves == 0);
#else
assert(cli.saves == 1);
#endif
assert(strcmp(cli.prefs.node_name, "imported node") == 0);
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON) \
&& !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(cli.prefs.bridge_enabled == 0);
#elif defined(WITH_MQTT_BRIDGE) && defined(WITH_RS232_BRIDGE)
assert(cli.prefs.bridge_enabled == (strstr(json, "en:1") != nullptr));
assert(cli.prefs.rs232_bridge_enabled == 0 && cli.prefs.bridge_uart == 2);
#elif defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
const bool explicit_uart = strstr(json, "uart:1") || strstr(json, "uart:2");
const bool explicit_uart = strstr(json, "uart:2");
const bool enabled = explicit_uart && strstr(json, "en:1");
assert(cli.prefs.bridge_enabled == enabled && cli.prefs.bridge_uart == 2);
#else
@@ -85,8 +101,10 @@ int main() {
#else
assert(cli.prefs.espnow_bridge_enabled == 1);
#endif
#ifndef WITH_MQTT_BRIDGE
assert(cli.saved_uart == cli.prefs.bridge_uart && cli.saved_enabled == cli.prefs.bridge_enabled);
assert(cli.saved_secondary == cli.prefs.espnow_bridge_enabled);
#endif
}
// Malformed JSON may partially apply scalar fields before the parser rejects it.
CLI broken;
+7 -3
View File
@@ -29,7 +29,7 @@ template<typename T> T constrain(T value, int low, int high) {
}
struct NodePrefs {
uint8_t bridge_enabled = 0, espnow_bridge_enabled = 0;
uint8_t bridge_enabled = 0, espnow_bridge_enabled = 0, rs232_bridge_enabled = 0;
uint16_t bridge_delay = 0;
uint8_t bridge_pkt_src = 0, bridge_uart = 0, bridge_channel = 0, bridge_format = 0;
uint32_t bridge_baud = 0;
@@ -117,6 +117,9 @@ struct Callbacks {
virtual bool isBridgeRunning() const = 0;
virtual bool setBridgeState(bool) = 0;
virtual bool restartBridge() = 0;
virtual bool isRs232BridgeRunning() const = 0;
virtual bool setRs232BridgeState(bool) = 0;
virtual bool restartRs232Bridge() = 0;
#ifdef WITH_ESPNOW_BRIDGE
virtual bool isEspNowBridgeRunning() = 0;
virtual bool setEspNowBridgeState(bool) = 0;
@@ -186,6 +189,7 @@ public:
explicit CommonCLI(MyMesh& mesh)
: _prefs(&mesh._prefs), _callbacks(&mesh), _sensors(&mesh.sensors) {}
void savePrefs() { ++saves; stored = *_prefs; }
bool trySavePrefs() { savePrefs(); return true; }
void load(FakeFile file) {
@INITIAL_UART@
@LOAD_UART@
@@ -212,7 +216,7 @@ static void check_upgrade(bool has_tail, uint8_t saved_uart, uint8_t enabled) {
CommonCLI cli(mesh);
cli.load(FakeFile{has_tail, saved_uart});
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
const bool normal_upgrade = !has_tail || saved_uart == 0;
const bool normal_upgrade = !has_tail || saved_uart != 2;
#else
const bool normal_upgrade = false;
#endif
@@ -241,7 +245,7 @@ int main() {
assert(Serial2.begins == begins + (mesh._prefs.bridge_enabled ? 1 : 0));
}
// Missing old tails and current normal-repeater zero sentinels fail safe.
// Legacy dedicated UART1 is intent, and normalizes to the board's UART2.
// Only a supported legacy UART proves intent; unsupported ports fail closed.
for (uint8_t enabled : {uint8_t{0}, uint8_t{1}}) {
check_upgrade(false, 0, enabled);
check_upgrade(true, 0, enabled);
+141 -63
View File
@@ -1,3 +1,4 @@
#include <fstream>
struct Capture {
std::vector<uint8_t> bytes;
std::map<const void*, size_t> offsets;
@@ -9,9 +10,39 @@ struct Capture {
size_t offset(const void* field) const { return offsets.at(field); }
};
int main() {
// The same executable can exchange actual serialized files with another profile.
// This catches role transitions which cannot be represented by a single macro set.
int main(int argc, char** argv) {
if (argc > 1) {
assert(argc == 7);
CommonCLI cli;
const bool writing = strcmp(argv[1], "write") == 0;
if (writing) {
cli.prefs.bridge_enabled = atoi(argv[3]);
cli.prefs.espnow_bridge_enabled = atoi(argv[4]);
cli.prefs.rs232_bridge_enabled = atoi(argv[5]);
cli.prefs.bridge_uart = 2;
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto& image = cli.fs.files.at("/com_prefs");
std::ofstream stream(argv[2], std::ios::binary);
stream.write(reinterpret_cast<const char*>(image.data()), image.size());
assert(stream.good());
} else {
std::ifstream stream(argv[2], std::ios::binary);
assert(stream.good());
cli.fs.files["/com_prefs"] = std::vector<uint8_t>(
std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>());
cli.loadPrefsInt(&cli.fs, "/com_prefs");
assert(cli.prefs.bridge_enabled == atoi(argv[3]));
assert(cli.prefs.espnow_bridge_enabled == atoi(argv[4]));
assert(cli.prefs.rs232_bridge_enabled == atoi(argv[5]));
assert(cli.prefs.bridge_uart == atoi(argv[6]));
}
return 0;
}
CommonCLI writer;
strcpy(writer.prefs.node_name, "MKE combined repeater");
strcpy(writer.prefs.node_name, "Combined repeater");
strcpy(writer.prefs.password, "test-password");
writer.prefs.bridge_uart = 2;
writer.prefs.bridge_baud = 57600;
@@ -22,80 +53,134 @@ int main() {
const size_t uart_offset = initial.offset(&writer.prefs.bridge_uart);
const size_t enabled_offset = initial.offset(&writer.prefs.bridge_enabled);
const size_t channel_offset = initial.offset(&writer.prefs.ota_channel);
const size_t esp_marker_offset = channel_offset + 1;
const size_t rs_intent_offset = initial.offset(&writer.prefs.rs232_bridge_enabled);
const size_t rs_marker_offset = channel_offset + 3;
assert(initial.bytes.size() == 877 && channel_offset == 873);
assert(rs_intent_offset == channel_offset + 2);
#ifdef ESPNOW_BRIDGE_MERGED
assert(initial.bytes.size() == 875 && initial.bytes.back() == 0xA1);
const size_t marker_offset = channel_offset + 1;
assert(marker_offset == initial.bytes.size() - 1);
assert(initial.bytes[esp_marker_offset] == 0xA1);
#else
assert(initial.bytes.size() == 874 && channel_offset == initial.bytes.size() - 1);
assert(initial.bytes[esp_marker_offset] == 0);
#endif
#ifdef WITH_RS232_BRIDGE
assert(initial.bytes[rs_marker_offset] == 0xB1);
#else
assert(initial.bytes[rs_marker_offset] == 0);
#endif
assert(initial.offset(writer.prefs.node_name) == 4);
assert(initial.offset(&writer.prefs.freq) == 72);
assert(initial.offset(&writer.prefs.bridge_enabled) == 127);
assert(enabled_offset == 127);
for (uint8_t primary : {uint8_t{0}, uint8_t{1}}) {
for (uint8_t secondary : {uint8_t{0}, uint8_t{1}}) {
writer.prefs.bridge_enabled = primary;
writer.prefs.espnow_bridge_enabled = secondary;
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
assert(writer._common_save_succeeded);
const auto image = writer.fs.files.at("/com_prefs");
Capture checked;
assert(writeCommonPrefsImage(checked, &writer.prefs));
assert(checked.bytes == image); // Checked and ordinary writers agree.
CommonCLI reader;
reader.fs.files["/com_prefs"] = image;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
for (uint8_t uart_intent : {uint8_t{0}, uint8_t{1}}) {
writer.prefs.bridge_enabled = primary;
writer.prefs.espnow_bridge_enabled = secondary;
writer.prefs.rs232_bridge_enabled = uart_intent;
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
assert(writer._common_save_succeeded);
const auto image = writer.fs.files.at("/com_prefs");
Capture checked;
assert(writeCommonPrefsImage(checked, &writer.prefs));
assert(checked.bytes == image); // Checked and ordinary writers agree.
CommonCLI reader;
reader.fs.files["/com_prefs"] = image;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
// The new sole bridge stores independent ESP-NOW intent and mirrors the
// legacy primary alias, even if an older caller supplied different flags.
assert(reader.prefs.bridge_enabled == secondary);
assert(reader.prefs.bridge_enabled == secondary);
#else
assert(reader.prefs.bridge_enabled == primary);
assert(reader.prefs.bridge_enabled == primary);
#endif
assert(reader.prefs.espnow_bridge_enabled == secondary);
assert(reader.prefs.espnow_bridge_enabled == secondary);
#ifdef WITH_RS232_BRIDGE
assert(reader.prefs.bridge_uart == 2);
assert(reader.prefs.bridge_uart == 2);
#ifdef WITH_MQTT_BRIDGE
assert(reader.prefs.rs232_bridge_enabled == uart_intent);
#else
assert(reader.prefs.bridge_uart == 0);
assert(reader.prefs.rs232_bridge_enabled == primary);
#endif
assert(reader.prefs.bridge_baud == 57600 && reader.prefs.bridge_channel == 6);
assert(strcmp(reader.prefs.node_name, writer.prefs.node_name) == 0);
assert(strcmp(reader.prefs.bridge_secret, "test-secret") == 0);
#else
assert(reader.prefs.bridge_uart == 0 && reader.prefs.rs232_bridge_enabled == 0);
#endif
assert(reader.prefs.bridge_baud == 57600 && reader.prefs.bridge_channel == 6);
assert(strcmp(reader.prefs.node_name, writer.prefs.node_name) == 0);
assert(strcmp(reader.prefs.bridge_secret, "test-secret") == 0);
}
}
}
writer.prefs.bridge_enabled = 1;
writer.prefs.espnow_bridge_enabled = 1;
Capture on;
assert(writeCommonPrefsImage(on, &writer.prefs));
writer.prefs.rs232_bridge_enabled = 1;
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
assert(writer._common_save_succeeded);
const auto on = writer.fs.files.at("/com_prefs");
#ifdef RS232_BRIDGE_MERGED
// Missing UART tails and current no-bridge zero sentinels both fail safe.
for (bool missing_tail : {false, true}) {
auto old = on.bytes;
if (missing_tail) old.resize(uart_offset);
else old[uart_offset] = 0;
assert(old[enabled_offset] == 1);
#ifdef WITH_RS232_BRIDGE
// Unmarked previous MQTT files cannot prove UART intent, even when they
// contain UART2 and primary enabled. Dedicated UART profiles retain context.
for (size_t length : {uart_offset, esp_marker_offset, rs_intent_offset}) {
auto old = on; old.resize(length);
CommonCLI reader;
reader.fs.files["/com_prefs"] = old;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.bridge_enabled == 0 && reader.prefs.bridge_uart == 2);
assert(reader._com_prefs_needs_upgrade);
#ifdef WITH_MQTT_BRIDGE
assert(reader.prefs.bridge_enabled == 1 && reader.prefs.rs232_bridge_enabled == 0);
#elif defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
const bool legacy_uart_present = length > uart_offset;
assert(reader.prefs.bridge_enabled == legacy_uart_present);
assert(reader.prefs.rs232_bridge_enabled == legacy_uart_present);
#endif
assert(reader.prefs.bridge_uart == 2 && reader._com_prefs_needs_upgrade);
}
// Older dedicated MKE builds selected Serial2 but persisted the UART1 fallback.
auto dedicated = on.bytes;
dedicated[uart_offset] = 1;
// A partial new tail, invalid marker or invalid boolean must never fall back
// to a different transport's primary setting. Short reads fail closed too.
for (unsigned invalid = 0; invalid <= 255; ++invalid) {
if (invalid == 0xB1) continue;
CommonCLI reader;
auto corrupt = on; corrupt[rs_marker_offset] = static_cast<uint8_t>(invalid);
reader.fs.files["/com_prefs"] = corrupt;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.rs232_bridge_enabled == 0 && reader._com_prefs_needs_upgrade);
#ifdef WITH_MQTT_BRIDGE
assert(reader.prefs.bridge_enabled == 1);
#else
assert(reader.prefs.bridge_enabled == 0);
#endif
}
for (unsigned invalid = 2; invalid <= 255; ++invalid) {
CommonCLI reader;
auto corrupt = on; corrupt[rs_intent_offset] = static_cast<uint8_t>(invalid);
reader.fs.files["/com_prefs"] = corrupt;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.rs232_bridge_enabled == 0);
}
for (size_t fault : {rs_intent_offset, rs_marker_offset}) {
CommonCLI reader;
reader.fs.files["/com_prefs"] = on;
reader.fs.fail_read_after = static_cast<int>(fault);
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.rs232_bridge_enabled == 0);
}
CommonCLI torn;
torn.fs.files["/com_prefs"] = {on.begin(), on.begin() + rs_marker_offset};
torn.loadPrefsInt(&torn.fs, "/com_prefs");
assert(torn.prefs.rs232_bridge_enabled == 0);
#if defined(RS232_BRIDGE_MERGED) && !defined(WITH_MQTT_BRIDGE)
// Old Serial2 builds saved the generic UART1 fallback; known UART2 remains
// the safe resolved port, without losing a valid new independent intent.
auto dedicated = on; dedicated[uart_offset] = 1;
CommonCLI legacy_reader;
legacy_reader.fs.files["/com_prefs"] = dedicated;
legacy_reader.loadPrefsInt(&legacy_reader.fs, "/com_prefs");
assert(legacy_reader.prefs.bridge_enabled == 1 && legacy_reader.prefs.bridge_uart == 2);
#endif
#endif
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
for (unsigned invalid = 0; invalid <= 255; ++invalid) {
if (invalid == 0xA1) continue;
auto old = on.bytes;
old[marker_offset] = static_cast<uint8_t>(invalid);
auto old = on; old[esp_marker_offset] = static_cast<uint8_t>(invalid);
CommonCLI reader;
reader.fs.files["/com_prefs"] = old;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
@@ -107,30 +192,23 @@ int main() {
#endif
assert(reader._com_prefs_needs_upgrade);
}
// Old 874-byte profiles saved the unused secondary flag as one. It is not intent.
auto old = on.bytes;
old.resize(marker_offset);
auto old = on; old.resize(esp_marker_offset);
CommonCLI reader;
reader.fs.files["/com_prefs"] = old;
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.espnow_bridge_enabled == 0);
#if !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
assert(reader.prefs.bridge_enabled == 0);
#else
assert(reader.prefs.bridge_enabled == 1);
#endif
reader.fs.files["/com_prefs"] = on.bytes;
reader.fs.fail_read_after = static_cast<int>(marker_offset);
reader.fs.files["/com_prefs"] = on;
reader.fs.fail_read_after = static_cast<int>(esp_marker_offset);
reader.loadPrefsInt(&reader.fs, "/com_prefs");
assert(reader.prefs.espnow_bridge_enabled == 0);
// A torn append must retain the previous durable settings image.
writer.savePrefs(&writer.fs, PrefsSaveRouting::Scope::Common);
assert(writer._common_save_succeeded);
const auto previous = writer.fs.files.at("/com_prefs");
writer.fs.fail_write_after = static_cast<int>(marker_offset);
writer.prefs.espnow_bridge_enabled = 0;
assert(!writer.trySavePrefs());
assert(writer.fs.files.at("/com_prefs") == previous);
#endif
// Every append boundary must commit atomically for observer and plain writers.
for (size_t fault : {esp_marker_offset, rs_intent_offset, rs_marker_offset}) {
const auto previous = writer.fs.files.at("/com_prefs");
writer.fs.fail_write_after = static_cast<int>(fault);
writer.prefs.rs232_bridge_enabled = 0;
assert(!writer.trySavePrefs());
assert(writer.fs.files.at("/com_prefs") == previous);
writer.fs.fail_write_after = -1;
}
}
+30
View File
@@ -341,6 +341,23 @@ require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_ALT=Serial1")
require("RAK_4631_repeater", "build_flags", "WITH_RS232_BRIDGE_UART=2")
reject("wio-e5_repeater", "build_flags", "WITH_RS232_BRIDGE=")
# Full source substitution must preserve the normal repeater UART rather
# than retiring its legacy bridge while compiling an MQTT-only recipe.
for env_name, rx, tx in (
("Heltec_v3_repeater_observer_mqtt", 5, 6),
("Heltec_WSL3_repeater_observer_mqtt", 5, 6),
("RAK_3112_repeater_observer_mqtt", 5, 6),
("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", 34, 25),
):
require(env_name, "build_flags", "WITH_MQTT_BRIDGE=1")
require(env_name, "build_flags", "WITH_RS232_BRIDGE=Serial2")
require(env_name, "build_flags", "WITH_RS232_BRIDGE_UART=2")
require(env_name, "build_flags", f"WITH_RS232_BRIDGE_RX={rx}")
require(env_name, "build_flags", f"WITH_RS232_BRIDGE_TX={tx}")
require(env_name, "build_flags", "RS232_BRIDGE_MERGED=1")
require(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp")
reject(env_name, "build_flags", "RS232_BRIDGE_DEFAULT_ON")
# MKE keeps its ordinary role and UART wiring while exposing the existing
# persistent bridge controls. The merged marker selects the disabled default;
# the explicit legacy bridge remains enabled for deliberate direct builds.
@@ -1606,6 +1623,19 @@ if is_redundant_bulk_build_target wio-e5-repeater_bridge_rs232; then
fail "capacity-constrained Wio-E5 RS232 bridge was incorrectly merged"
fi
(
BUILD_PROFILE_FOR_TARGET=full
for uart_target in Heltec_v3_repeater Heltec_WSL3_repeater RAK_3112_repeater \
LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_; do
BUILD_EXPECTATIONS=()
declare_build_capability_contract "$uart_target" ESP32_PLATFORM
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.rs232=_ZN11RS232Bridge5beginEv"* ]] \
|| fail "$uart_target Full contract omitted the linked UART driver"
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.espnow=_ZN12ESPNowBridge5beginEv"* ]] \
|| fail "$uart_target Full contract omitted the linked ESP-NOW driver"
done
)
# A WiFi base's final -UENABLE_OTA must not win over the Full Companion's
# source-only OTA overlay. That mismatch compiles out both the TCP terminal and
# seeder while still leaving a superficially valid binary.
+5
View File
@@ -49,6 +49,7 @@ 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',
'rs232.running': 'Live UART bridge state; set rs232.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',
@@ -87,6 +88,10 @@ class CLISettingsContractTest(unittest.TestCase):
'rs232_gps': ['WITH_BRIDGE', 'WITH_RS232_BRIDGE', 'ENV_INCLUDE_GPS'],
'rs232_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
'WITH_RS232_BRIDGE', 'WITH_ESPNOW_BRIDGE'],
'mqtt_rs232_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
'WITH_MQTT_BRIDGE', 'WITH_RS232_BRIDGE', 'WITH_ESPNOW_BRIDGE'],
'mqtt_rs232': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
'WITH_MQTT_BRIDGE', 'WITH_RS232_BRIDGE'],
'espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE', 'WITH_ESPNOW_BRIDGE', 'MESH_PRIMARY_ESPNOW'],
'lr2021': ['USE_LR2021'],
}
+1 -1
View File
@@ -86,7 +86,7 @@ int main() {
auto original=cli.fs.files["/com_prefs"];
// Appending preferences must not move the established preamble bytes.
const size_t preamble_offset=866;
assert(original.size()==874);
assert(original.size()==877);
assert(original[preamble_offset]==48 && original[preamble_offset+1]==0);
assert(original[preamble_offset+2]==1);
assert(original[preamble_offset+3]==0x50 && original[preamble_offset+4]==0x01);
+1 -1
View File
@@ -159,7 +159,7 @@ int main() {
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto baseline = cli.fs.files["/com_prefs"];
assert(baseline.size() == 874);
assert(baseline.size() == 877);
const size_t debug_offset = 871, trace_offset = 872;
const size_t logging_offset = 863, preamble_offset = 866;
const size_t espnow_offset = 868, gps_offset = 869;
+34
View File
@@ -1284,6 +1284,40 @@ assert(!picker.runtimeDirections(staleBridgeMetadata.profiles.find(profile =>
profile.target === bridgeTargets[0]), {mode: 'espnow'}).some(section => section.title === 'ESP-NOW bridge'));
console.log('combined UART/ESP-NOW picker modes, independent commands and historical compatibility passed');
// MQTT, ESP-NOW and UART coexist in the same qualified Full artifact. UART
// instructions must never use MQTT's historical ESP-NOW bridge alias.
const tripleTargets = ['Heltec_v3_repeater', 'Heltec_WSL3_repeater',
'RAK_3112_repeater', 'LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_']
.map(target => target + '-full-usb-wifi');
const tripleCatalog = picker.buildCatalog([
release(bridgeFamily, '2026-10-05T00:00:00Z', tripleTargets.map(target =>
asset(target + '-ota-' + bridgeFamily + '.bin'))),
], {
familyTag: bridgeFamily,
profiles: Object.fromEntries(tripleTargets.map(target => [target, {
platform: 'ESP32_PLATFORM', rs232: true, mqtt: true, espnowBridge: true,
updateMethods: ['wifi', 'lora'],
}])),
});
for (const profile of tripleCatalog.profiles) {
assert.deepStrictEqual(picker.profileFieldValues(profile, 'mode'),
['standard', 'rs232', 'espnow']);
const directions = picker.runtimeDirections(profile, {mode: 'rs232'});
const uart = directions.find(section => /^RS232 bridge/.test(section.title));
const wireless = directions.find(section => section.title === 'ESP-NOW bridge');
const mqtt = directions.find(section => section.title === 'MQTT broker connections');
assert(uart && wireless && mqtt);
assert.deepStrictEqual(uart.actions.map(action => action.commands), [
['set rs232.enabled on', 'get rs232.running'], ['set rs232.enabled off'],
['set rs232.enabled off', 'set bridge.baud 115200', 'set rs232.enabled on'],
]);
assert(commands([wireless]).includes('set espnow.enabled on'));
assert(commands([mqtt]).includes('set mqtt.enabled on'));
assert(!commands([uart, wireless, mqtt]).some(command => command.includes('bridge.enabled')));
}
assert.strictEqual(tripleCatalog.profiles.length, tripleTargets.length);
console.log('three-transport Full picker uses independent UART, MQTT and ESP-NOW controls');
// Partition expansion uses the packager's lookup and published utility assets,
// not hardware labels or guessed slugs (several historical names differ).
const migrationFamily = currentControls.familyTag;
+15
View File
@@ -173,6 +173,21 @@ class FirmwareRamTest(unittest.TestCase):
}, "v4_companion")
self.assertEqual(default, expanded)
def test_combined_uart_reserves_heap_beside_mqtt_before_enable(self):
defines = {"WITH_MQTT_BRIDGE": 1, "WIFI_OTA_SEEDER": 1}
mqtt = ram.requirements("ESP32_PLATFORM", defines, "v3_repeater")
combined = ram.requirements("ESP32_PLATFORM", {
**defines, "WITH_RS232_BRIDGE": "Serial2", "RS232_BRIDGE_MERGED": 1,
}, "v3_repeater")
self.assertEqual(combined["required_heap_bytes"] - mqtt["required_heap_bytes"], 8192)
self.assertEqual(combined["components"]["uart_bridge_and_driver"], 8192)
# A selectable alternate port does not create two UART owners.
alternate = ram.requirements("ESP32_PLATFORM", {
**defines, "WITH_RS232_BRIDGE": "Serial2", "RS232_BRIDGE_MERGED": 1,
"WITH_RS232_BRIDGE_ALT": "Serial1",
}, "v3_repeater")
self.assertEqual(alternate, combined)
def test_published_image_tables_match_elf_and_use_its_own_reservations(self):
with tempfile.TemporaryDirectory() as temp:
path = Path(temp) / "reference.elf"
+36 -4
View File
@@ -20,6 +20,38 @@ def record(target: str, profile: str = "full", platform: str = "ESP32_PLATFORM",
class ReleaseFullSelectionTest(unittest.TestCase):
def test_full_uart_source_substitution_requires_linked_driver_proof(self):
for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater",
"LilyGo_TLora_V2_1_1_6_repeater"):
for suffix in ("", "_observer_mqtt", "_observer_mqtt_"):
for fault in ("missing_capability", "missing_proof", "false_proof", "unverified"):
image = record(base + suffix, capabilities=("bridge.rs232", "bridge.espnow"))
manifest = image["manifest"]
if fault == "missing_capability":
manifest["capabilities"].remove("bridge.rs232")
elif fault == "missing_proof":
manifest["verification"] = []
elif fault == "false_proof":
manifest["verification"][0]["present"] = False
else:
manifest["verified"] = False
with self.subTest(base=base, suffix=suffix, fault=fault):
with self.assertRaisesRegex(ValueError, "verified RS232 bridge"):
select_ordinary_full_records([image])
def test_new_combined_full_displaces_stale_plain_and_both_dedicated_bridges(self):
for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater",
"LilyGo_TLora_V2_1_1_6_repeater"):
stale = record(base, capabilities=("bridge.espnow",))
primary = record(base + "_observer_mqtt_",
capabilities=("bridge.rs232", "bridge.espnow"))
for profile in ("standard", "full"):
old = [record(base + "_bridge_" + bridge, profile)
for bridge in ("rs232", "espnow")]
for inputs in ([stale, primary] + old, old + [primary, stale]):
with self.subTest(base=base, profile=profile):
self.assertEqual(select_ordinary_full_records(inputs), [primary])
def test_meshtower_sd_primary_filters_stale_internal_images_without_relabeling(self):
primary = "Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors"
variants = []
@@ -63,7 +95,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
inputs = [
record("Heltec_v3_repeater_observer_mqtt"),
record("Heltec_v3_repeater_bridge_espnow"),
record("Heltec_v3_repeater", capabilities=("bridge.espnow",)),
record("Heltec_v3_repeater", capabilities=("bridge.espnow", "bridge.rs232")),
record("Heltec_v3_repeater", "standard"),
record("Heltec_v3_room_server"),
record("Station_G2_repeater"),
@@ -71,7 +103,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
record("Tbeam_SX1262_repeater_bridge_espnow"),
record("Tbeam_SX1262_repeater_observer_mqtt", capabilities=("bridge.espnow",)),
record("LilyGo_TLora_V2_1_1_6_repeater"),
record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_"),
record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", capabilities=("bridge.rs232",)),
record("Meshadventurer_sx1262_repeater"),
record("Meshadventurer_sx1262_repeater_bridge_espnow"),
record("RAK_4631_repeater", platform="NRF52_PLATFORM"),
@@ -158,7 +190,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
self.assertEqual(select_ordinary_full_records(ordered), ordered)
def test_qualified_combined_plain_image_replaces_only_its_matching_espnow_image(self):
primary = record("Heltec_v3_repeater", capabilities=("bridge.espnow",))
primary = record("Heltec_v3_repeater", capabilities=("bridge.espnow", "bridge.rs232"))
other_board = record("heltec_v4_tft_repeater_bridge_espnow", capabilities=("bridge.espnow",))
for profile in ("full", "standard"):
legacy = record("Heltec_v3_repeater_bridge_espnow", profile, capabilities=("bridge.espnow",))
@@ -221,7 +253,7 @@ class ReleaseFullSelectionTest(unittest.TestCase):
def test_combined_observer_with_trailing_underscore_replaces_matching_espnow(self):
observer = record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_",
capabilities=("bridge.espnow",))
capabilities=("bridge.espnow", "bridge.rs232"))
legacy = record("LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow",
capabilities=("bridge.espnow",))
for inputs in ([observer, legacy], [legacy, observer]):
+12 -11
View File
@@ -398,7 +398,7 @@ class Nrf52VariantContractsTest(unittest.TestCase):
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
self.assertIn("bool isBridgeRunning() const override", header)
self.assertIn("if (isBridgeRunning()) reply_data[8] |= 0x01", implementation)
self.assertIn("if (isRs232BridgeRunning()) reply_data[8] |= 0x01", implementation)
self.assertIn("if (isBridgeRunning()) reply_data[8] |= 0x03", implementation)
boot_failure = implementation[
implementation.index("if (!bridge || !beginRS232Bridge())"):
@@ -406,7 +406,7 @@ class Nrf52VariantContractsTest(unittest.TestCase):
"if (!bridge || !beginRS232Bridge())"
))
]
self.assertIn("setBridgeState(false)", boot_failure)
self.assertIn("setRs232BridgeState(false)", boot_failure)
self.assertNotIn("_prefs.bridge_enabled", boot_failure)
self.assertIn("parseOnOffStrict(&config[15], enable)", cli)
self.assertIn("parseUnsignedIntegerStrict(&config[12], baud)", cli)
@@ -417,7 +417,7 @@ class Nrf52VariantContractsTest(unittest.TestCase):
self.assertIn('_prefs->bridge_pkt_src ? "rx" : "tx"', cli)
self.assertIn("!defined(RS232_BRIDGE_DEFAULT_ON)", cli)
def test_default_on_rs232_profile_skips_default_off_tail_migration(self) -> None:
def test_default_on_rs232_profile_retains_legacy_alias_and_checks_new_intent(self) -> None:
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
xiao = (ROOT / "variants/xiao_nrf52/platformio.ini").read_text()
dedicated = ini_section(
@@ -426,17 +426,18 @@ class Nrf52VariantContractsTest(unittest.TestCase):
self.assertIn("extends = env:solarxiao_30S_repeater", dedicated)
self.assertRegex(dedicated, r"-D\s+RS232_BRIDGE_DEFAULT_ON=1\b")
guard = (
"#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \\\n"
" && !defined(RS232_BRIDGE_DEFAULT_ON)"
)
# Declaration, persisted-tail detection, and migration must share the
# same guard. A broader use breaks dedicated default-on bridge builds.
# Dedicated compatibility profiles retain their old primary UART alias;
# every current UART profile checks the independent marker before a
# newer MQTT image can donate intent. Executable nRF52/default-on and
# cross-profile round trips are covered by test_rs232_runtime.py.
loader_start = cli.index("void CommonCLI::loadPrefsInt(")
loader_end = cli.index("static bool writeCommonPrefsImage(", loader_start)
binary_loader = cli[loader_start:loader_end]
self.assertEqual(binary_loader.count(guard), 3)
self.assertEqual(binary_loader.count("has_runtime_bridge_uart"), 3)
self.assertTrue("has_runtime_rs232_intent = rs232_profile == 0xB1" in binary_loader)
self.assertTrue("&& rs232_intent <= 1" in binary_loader)
self.assertTrue("if (!has_runtime_rs232_intent)" in binary_loader)
self.assertTrue("legacy_uart_intent" in binary_loader)
self.assertTrue("_prefs->bridge_enabled = _prefs->rs232_bridge_enabled;" in binary_loader)
def test_gat562_30s_gps_enable_and_buzzer_pins_are_distinct(self) -> None:
recipe = (
+4 -1
View File
@@ -66,7 +66,10 @@ void testRoundTripsAndLayout() {
const size_t channel_offset = original.offset(&cli.prefs.ota_channel);
const size_t trace_offset = original.offset(&cli.prefs.trace_when_repeat_off);
assert(channel_offset == trace_offset + sizeof(cli.prefs.trace_when_repeat_off));
assert(channel_offset + sizeof(cli.prefs.ota_channel) == original.bytes.size());
assert(channel_offset + sizeof(cli.prefs.ota_channel) + 3 == original.bytes.size());
assert(original.bytes[channel_offset + 1] == 0); // reserved ESP-NOW profile
assert(original.offset(&cli.prefs.rs232_bridge_enabled) == channel_offset + 2);
assert(original.bytes[channel_offset + 2] == 0 && original.bytes.back() == 0);
// The established common core remains at its public file offsets.
assert(original.offset(cli.prefs.node_name) == 4);
assert(original.offset(&cli.prefs.freq) == 72);
+1 -1
View File
@@ -21,7 +21,7 @@ int main() {
cli.savePrefs(&cli.fs, PrefsSaveRouting::Scope::Common);
assert(cli._common_save_succeeded);
const auto baseline = cli.fs.files["/com_prefs"];
assert(baseline.size() == 874);
assert(baseline.size() == 877);
const size_t trace_offset = 872, debug_offset = 871, gps_offset = 869;
assert(baseline[trace_offset] == 0 && baseline[debug_offset] == 1);
assert(baseline[866] == 48 && baseline[867] == 0 && baseline[868] == 1);
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Execute the production ESP32 UART begin/end and bridge state checks."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = r'''
#include <cassert>
#include <cstdint>
#define ESP32 1
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
struct NodePrefs { uint32_t bridge_baud = 57600; };
struct Stream { virtual ~Stream() = default; };
struct HardwareSerial : Stream {
bool pins_ok = true, begin_ok = true, installed = false;
unsigned pin_calls = 0, begin_calls = 0, end_calls = 0;
int16_t rx = -1, tx = -1;
uint32_t baud = 0;
bool setPins(int16_t selected_rx, int16_t selected_tx) {
++pin_calls; rx = selected_rx; tx = selected_tx; return pins_ok;
}
void begin(uint32_t selected_baud) { ++begin_calls; baud = selected_baud; installed = begin_ok; }
explicit operator bool() const { return installed; }
void end() { ++end_calls; installed = false; }
};
struct BridgeBase {
bool _initialized = false;
@RUNNING@
};
struct RS232Bridge : BridgeBase {
NodePrefs* _prefs;
Stream* _serial;
int16_t _rx_pin, _tx_pin;
RS232Bridge(NodePrefs& prefs, HardwareSerial& serial)
: _prefs(&prefs), _serial(&serial), _rx_pin(5), _tx_pin(6) {}
void begin();
void end();
};
@BEGIN@
@END@
int main() {
NodePrefs prefs;
HardwareSerial serial;
RS232Bridge bridge(prefs, serial);
assert(!bridge.isRunning());
serial.pins_ok = false;
bridge.begin();
assert(!bridge.isRunning() && serial.pin_calls == 1 && serial.begin_calls == 0);
serial.pins_ok = true; serial.begin_ok = false;
bridge.begin();
assert(!bridge.isRunning() && !serial.installed && serial.begin_calls == 1);
serial.begin_ok = true;
bridge.begin();
assert(bridge.isRunning() && serial.installed && serial.begin_calls == 2);
assert(serial.rx == 5 && serial.tx == 6 && serial.baud == 57600);
bridge.end();
assert(!bridge.isRunning() && !serial.installed && serial.end_calls == 1);
// A failed restart must clear its former state, and a later retry can recover.
bridge.begin(); assert(bridge.isRunning());
serial.pins_ok = false;
bridge.begin(); assert(!bridge.isRunning());
bridge.end(); serial.pins_ok = true; prefs.bridge_baud = 19200;
bridge.begin(); assert(bridge.isRunning() && serial.baud == 19200);
for (unsigned cycle = 0; cycle < 100; ++cycle) {
bridge.end(); assert(!bridge.isRunning());
bridge.begin(); assert(bridge.isRunning());
}
bridge.end(); assert(!bridge.isRunning());
}
'''
class RS232DriverLifecycleTests(unittest.TestCase):
def test_failed_pins_or_driver_start_never_report_running_and_retry_recovers(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
driver = (ROOT / "src/helpers/bridges/RS232Bridge.cpp").read_text(encoding="ascii")
base = (ROOT / "src/helpers/bridges/BridgeBase.cpp").read_text(encoding="ascii")
code = FIXTURE.replace("@RUNNING@", extract_braced(base, "bool BridgeBase::isRunning() const")
.replace("BridgeBase::", "").replace(" override", ""))
code = code.replace("@BEGIN@", extract_braced(driver, "void RS232Bridge::begin()"))
code = code.replace("@END@", extract_braced(driver, "void RS232Bridge::end()"))
with tempfile.TemporaryDirectory(prefix="rs232-driver-lifecycle-") as directory:
work = Path(directory)
source, binary = work / "main.cpp", work / "test"
source.write_text(code, encoding="ascii")
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
str(source), "-o", str(binary)], capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
if __name__ == "__main__":
unittest.main()
+224
View File
@@ -0,0 +1,224 @@
#!/usr/bin/env python3
"""Execute real UART/MQTT/ESP-NOW repeater lifecycle and CLI independently."""
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
import unittest
from test_replay_reset_integration import extract_braced
ROOT = Path(__file__).resolve().parents[1]
class RS232MQTTRuntimeTests(unittest.TestCase):
def test_independent_transports_route_retry_restart_and_survive_browser_ota(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
observer = (ROOT / "src/helpers/CommonCLI_Observer.cpp").read_text(encoding="ascii")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
flags = ["-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
"-DWITH_RS232_BRIDGE_RX=5", "-DWITH_RS232_BRIDGE_TX=6",
"-DRS232_BRIDGE_MERGED=1", "-DWITH_MQTT_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1", "-DWITH_BRIDGE=1",
"-DENV_INCLUDE_GPS=1", "-DLIGHTWEIGHT_WIFI_OTA=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1"]
def preprocess(source, extra=()):
return subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, *extra, "-"],
input=source, capture_output=True, text=True, check=True).stdout
processed_header = preprocess(re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M))
lifecycle = "\n".join(extract_braced(processed_header, signature) for signature in (
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
"bool isBridgeRunning() const override", "bool rs232BridgeEnabled() const", "bool isRs232BridgeRunning()",
"bool setRs232BridgeState(bool enable)", "bool restartRs232Bridge()",
"bool startSharedEspNowBridgeIfReady()", "bool isEspNowBridgeRunning()",
"bool setEspNowBridgeState(bool enable)", "bool restartEspNowBridge()",
"bool isMqttBridgeRunning()", "bool setMqttBridgeState(bool enable)",
"bool restartMqttBridge()", "bool setBridgeState(bool enable)", "bool restartBridge()",
))
defaults_start = implementation.index("// bridge defaults")
defaults_end = implementation.index("// GPS defaults", defaults_start)
boot_start = implementation.index('mesh::hilStartupTrace("mesh_bridge_begin");')
boot_start = implementation.index("#if defined(WITH_BRIDGE)", boot_start)
boot_end = implementation.index('mesh::hilStartupTrace("mesh_bridge_ready");', boot_start)
routing = {}
for name in ("logRx", "logTx"):
start = implementation.index("void MyMesh::" + name + "(")
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", start)
end = implementation.index(" if (_logging)", start)
routing[name] = implementation[start:end]
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)",
implementation.index("MyMesh::servicePostMeshLoop()"))
service_end = implementation.index(" if (next_flood_advert", service_start)
pending_start = implementation.index("#if defined(WITH_BRIDGE)",
implementation.index("bool MyMesh::hasPendingWork()"))
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
commands = preprocess("\n".join(extract_braced(cli, signature) for signature in (
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd(")))
sets = "\nelse ".join(extract_braced(commands, signature) for signature in (
'if (strncmp(config, "mqtt.enabled ", 13) == 0)',
'if (strncmp(config, "rs232.enabled ", 14) == 0)',
'if (memcmp(config, "espnow.enabled ", 15) == 0)',
'if (memcmp(config, "bridge.enabled ", 15) == 0)',
'if (memcmp(config, "bridge.baud ", 12) == 0)',
'if (memcmp(config, "bridge.uart ", 12) == 0)',
'if (memcmp(config, "bridge.channel ", 15) == 0)',
'if (memcmp(config, "bridge.secret ", 14) == 0)',
'if (memcmp(config, "bridge.format ", 14) == 0)',
))
gets = "\nelse ".join(extract_braced(commands, signature) for signature in (
'if (configKeyEquals(config, "bridge.type"))',
'if (configKeyEquals(config, "rs232.enabled"))',
'if (configKeyEquals(config, "rs232.running"))',
'if (configKeyEquals(config, "bridge.enabled"))',
'if (configKeyEquals(config, "bridge.running"))',
'if (configKeyEquals(config, "espnow.enabled"))',
'if (configKeyEquals(config, "espnow.running"))',
'if (configKeyEquals(config, "bridge.baud"))',
'if (configKeyEquals(config, "bridge.uart"))',
))
gets += "\nelse " + "\nelse ".join(extract_braced(observer, signature) for signature in (
'if (strcmp(config, "mqtt.enabled") == 0)', 'if (strcmp(config, "mqtt.running") == 0)'))
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
ota_end = cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
for key, value in {
"LIFECYCLE": lifecycle, "DEFAULTS": implementation[defaults_start:defaults_end],
"FILTER": extract_braced(header, "void configureBridgeFilter(AbstractBridge* active_bridge)"),
"BOOT": implementation[boot_start:boot_end], "ROUTE_RX": routing["logRx"],
"ROUTE_TX": routing["logTx"], "SERVICE": implementation[service_start:service_end],
"PENDING": implementation[pending_start:pending_end], "SET_BRANCHES": sets,
"GET_BRANCHES": gets, "OTA": cli[ota_start:ota_end],
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
}.items():
code = code.replace("@" + key + "@", value)
with tempfile.TemporaryDirectory(prefix="rs232-mqtt-runtime-") as temporary:
work = Path(temporary)
source, binary = work / "main.cpp", work / "test"
source.write_text(code, encoding="ascii")
result = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*flags, "-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertIn("Three transport runtime checks passed", result.stdout)
def test_uart_and_mqtt_without_espnow_preserve_primary_mqtt_alias(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
callback_header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
flags = ["-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
"-DWITH_RS232_BRIDGE_RX=5", "-DWITH_RS232_BRIDGE_TX=6", "-DRS232_BRIDGE_MERGED=1",
"-DWITH_MQTT_BRIDGE=1", "-DWITH_BRIDGE=1", "-DENV_INCLUDE_GPS=1",
"-DLIGHTWEIGHT_WIFI_OTA=1", "-DMESH_ENABLE_FLOOD_RULE_ENGINE=1"]
def preprocess(source):
return subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
input=source, capture_output=True, text=True, check=True).stdout
processed_header = preprocess(re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M))
lifecycle = "\n".join(extract_braced(processed_header, signature) for signature in (
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()", "bool endRS232Bridge()",
"bool rs232BridgeEnabled() const", "bool isBridgeRunning() const override",
"bool isRs232BridgeRunning()", "bool setRs232BridgeState(bool enable)",
"bool restartRs232Bridge()", "bool isMqttBridgeRunning()", "bool setBridgeState(bool enable)",
"bool restartBridge()"))
code = (ROOT / "test/fixtures/rs232_mqtt_runtime.cpp").read_text(encoding="ascii")
code = code[:code.index("int main() {")]
for signature in ("virtual bool isEspNowBridgeRunning() = 0;",
"virtual bool setEspNowBridgeState(bool) = 0;",
"virtual bool restartEspNowBridge() = 0;"):
code = code.replace(signature, "")
for short, full in (("virtual bool setMqttBridgeState(bool) = 0;", "virtual bool setMqttBridgeState(bool enable)"),
("virtual bool restartMqttBridge() = 0;", "virtual bool restartMqttBridge()")):
code = code.replace(short, extract_braced(callback_header, full))
commands = preprocess("\n".join(extract_braced(cli, signature) for signature in (
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd(")))
boot_start = implementation.index('mesh::hilStartupTrace("mesh_bridge_begin");')
boot_start = implementation.index("#if defined(WITH_BRIDGE)", boot_start)
boot_end = implementation.index('mesh::hilStartupTrace("mesh_bridge_ready");', boot_start)
service_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)", implementation.index("MyMesh::servicePostMeshLoop()"))
service_end = implementation.index(" if (next_flood_advert", service_start)
pending_start = implementation.index("#if defined(WITH_BRIDGE)", implementation.index("bool MyMesh::hasPendingWork()"))
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
route = {}
for name in ("logRx", "logTx"):
start = implementation.index("#ifdef WITH_MQTT_BRIDGE\n", implementation.index("void MyMesh::" + name + "("))
route[name] = implementation[start:implementation.index(" if (_logging)", start)]
ota_start = cli.index(' } else if (memcmp(command, "start ota", 9)')
defaults_start = implementation.index("// bridge defaults")
substitutions = {
"LIFECYCLE": lifecycle, "BOOT": implementation[boot_start:boot_end],
"DEFAULTS": implementation[defaults_start:implementation.index("// GPS defaults", defaults_start)],
"FILTER": extract_braced(header, "void configureBridgeFilter(AbstractBridge* active_bridge)"),
"ROUTE_RX": route["logRx"], "ROUTE_TX": route["logTx"],
"SERVICE": implementation[service_start:service_end], "PENDING": implementation[pending_start:pending_end],
"SET_BRANCHES": "\nelse ".join(extract_braced(commands, signature) for signature in (
'if (strncmp(config, "mqtt.enabled ", 13) == 0)', 'if (strncmp(config, "rs232.enabled ", 14) == 0)',
'if (memcmp(config, "bridge.enabled ", 15) == 0)', 'if (memcmp(config, "bridge.baud ", 12) == 0)',
'if (memcmp(config, "bridge.uart ", 12) == 0)')),
"GET_BRANCHES": extract_braced(commands, 'if (configKeyEquals(config, "bridge.type"))'),
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
"OTA": cli[ota_start:cli.index(' } else if (memcmp(command, "clock", 5)', ota_start)],
}
for key, value in substitutions.items():
code = code.replace("@" + key + "@", value)
code += r'''int main() {
assert(millisHasNowPassed(millis()));
MyMesh mesh; mesh.defaults(); mesh.boot(); CommonCLI cli(mesh);
expect_get(cli, "bridge.type", "> mqtt+rs232");
assert(mesh.isMqttBridgeRunning() && !mesh.isRs232BridgeRunning());
char reply[256] = {};
cli.set("rs232.enabled on", reply); assert(strcmp(reply, "OK") == 0);
assert(mesh.isRs232BridgeRunning() && mesh.isMqttBridgeRunning());
const unsigned starts = mesh.mqtt_bridge->begins;
cli.set("bridge.baud 57600", reply); assert(strcmp(reply, "OK") == 0);
assert(mesh.isRs232BridgeRunning() && mesh.mqtt_bridge->begins == starts);
cli.set("bridge.enabled off", reply); assert(strcmp(reply, "OK") == 0);
assert(!mesh._prefs.bridge_enabled && mesh._prefs.rs232_bridge_enabled);
assert(!mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
assert(mesh.isBridgeRunning() && mesh.bridgesPreventSleep());
cli.set("bridge.enabled on", reply); assert(strcmp(reply, "OK") == 0);
assert(mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
mesh::Packet packet;
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
assert(mesh.mqtt_bridge->received == 1 && mesh.mqtt_bridge->sent == 1);
assert(mesh.filter_checks == 3);
const unsigned uart_starts = Serial2.begins;
cli.run("start ota ap", reply);
assert(mesh._cli.board.ota && !mesh.isMqttBridgeRunning() && mesh.isRs232BridgeRunning());
cli.run("stop ota", reply);
assert(!mesh._cli.board.ota && mesh.isMqttBridgeRunning() && Serial2.begins == uart_starts);
cli.set("rs232.enabled off", reply); cli.set("mqtt.enabled off", reply);
assert(!mesh.bridge && !mesh.isMqttBridgeRunning() && !mesh.bridgesPreventSleep());
cli.set("rs232.enabled on", reply); mesh.mqtt_bridge->fail_starts = 1;
cli.set("mqtt.enabled on", reply);
assert(strstr(reply, "setting saved") && !mesh.isMqttBridgeRunning());
assert(mesh.isRs232BridgeRunning() && mesh.bridgesPreventSleep());
cli.set("mqtt.enabled on", reply);
assert(strcmp(reply, "OK") == 0 && mesh.isMqttBridgeRunning());
}'''
with tempfile.TemporaryDirectory(prefix="rs232-mqtt-only-") as directory:
work = Path(directory)
source, binary = work / "main.cpp", work / "test"
source.write_text(code, encoding="ascii")
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*flags, "-I" + str(ROOT / "src"), str(source), "-o", str(binary)],
capture_output=True, text=True, timeout=60)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
checked = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
if __name__ == "__main__":
unittest.main()
+86 -14
View File
@@ -35,6 +35,11 @@ class RS232RuntimeTests(unittest.TestCase):
("merged_composite", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
"ESPNOW_BRIDGE_MERGED=1"]),
("mqtt_rs232", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1"]),
("mqtt_rs232_espnow", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1",
"WITH_ESPNOW_BRIDGE=1"]),
("sole_espnow_merged", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1"]),
("sole_espnow_dedicated", ["WITH_ESPNOW_BRIDGE=1"]),
("sole_espnow_default_on", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1",
@@ -71,11 +76,21 @@ class RS232RuntimeTests(unittest.TestCase):
code += (ROOT / "test/fixtures/rs232_runtime/prefs.cpp").read_text(encoding="ascii")
for profile, macros in (
("ordinary_unmerged", []),
("rs232_dedicated", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2"]),
("rs232_default_on", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
("nrf52_rs232_default_on", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
("rs232_merged", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1"]),
("rs232_espnow_merged", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
"ESPNOW_BRIDGE_MERGED=1"]),
("mqtt_rs232", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1"]),
("mqtt_rs232_espnow", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
"RS232_BRIDGE_MERGED=1", "WITH_MQTT_BRIDGE=1",
"WITH_ESPNOW_BRIDGE=1"]),
("sole_espnow_merged", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1"]),
("sole_espnow_dedicated", ["WITH_ESPNOW_BRIDGE=1"]),
("sole_espnow_default_on", ["WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1",
@@ -89,7 +104,7 @@ class RS232RuntimeTests(unittest.TestCase):
source, binary = work / "main.cpp", work / "test"
source.write_text(code, encoding="ascii")
result = subprocess.run([
compiler, "-std=c++17", "-DESP32_PLATFORM=1", "-DENABLE_OTA=1",
compiler, "-std=c++17", "-D" + ("NRF52_PLATFORM=1" if profile.startswith("nrf52") else "ESP32_PLATFORM=1"), "-DENABLE_OTA=1",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*["-D" + macro for macro in macros], "-I" + str(work),
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"),
@@ -100,27 +115,82 @@ class RS232RuntimeTests(unittest.TestCase):
result = subprocess.run([str(binary)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_uart_intent_survives_cross_profile_upgrades_without_claiming_mqtt_intent(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
base = persistence_harness().replace("#define WITH_RS232_BRIDGE_UART 1", "")
code = base[:base.index("struct Capture {")]
code += (ROOT / "test/fixtures/rs232_runtime/prefs.cpp").read_text(encoding="ascii")
common = ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2", "RS232_BRIDGE_MERGED=1"]
profiles = {"normal": [*common, "WITH_ESPNOW_BRIDGE=1", "ESPNOW_BRIDGE_MERGED=1"],
"dedicated": common[:2],
"triple": [*common, "WITH_ESPNOW_BRIDGE=1", "WITH_MQTT_BRIDGE=1"],
"mqtt_uart": [*common, "WITH_MQTT_BRIDGE=1"]}
with tempfile.TemporaryDirectory(prefix="rs232-profile-upgrade-") as directory:
work = Path(directory)
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text(encoding="ascii")
(work / "ContactFileTransaction.h").write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'), encoding="ascii")
source = work / "main.cpp"
source.write_text(code, encoding="ascii")
for name, macros in profiles.items():
result = subprocess.run([compiler, "-std=c++17", "-DESP32_PLATFORM=1", "-DENABLE_OTA=1",
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
*["-D" + macro for macro in macros], "-I" + str(work),
"-I" + str(ROOT / "test/fixtures/radio_profiles/mocks"),
"-I" + str(ROOT / "src"), "-I" + str(ROOT / "src/helpers"),
str(source), "-o", str(work / name)], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def run(profile, operation, image, primary, espnow, uart, port=2):
result = subprocess.run([str(work / profile), operation, str(image), str(primary),
str(espnow), str(uart), str(port)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
for origin in ("triple", "mqtt_uart"):
for uart in (0, 1):
image = work / f"{origin}-{uart}.bin"
run(origin, "write", image, 1, 0, uart)
self.assertEqual(len(image.read_bytes()), 877)
for target in ("normal", "dedicated"):
with self.subTest(origin=origin, target=target, uart=uart):
run(target, "read", image, uart, 0, uart)
# Old MQTT files persisted primary enabled plus UART2.
# Without the new independent marker this remains MQTT only.
old = work / "old-mqtt.bin"
old.write_bytes(image.read_bytes()[:874])
run(origin, "read", old, 1, 0, 0)
for origin in ("normal", "dedicated"):
for uart in (0, 1):
image = work / f"{origin}-{uart}.bin"
run(origin, "write", image, uart, 0, uart)
for target in ("triple", "mqtt_uart"):
with self.subTest(origin=origin, target=target, uart=uart):
run(target, "read", image, uart, 0, uart)
image = work / "corrupt-triple.bin"
run("triple", "write", image, 1, 0, 1)
saved = image.read_bytes()
for tail in (saved[:875] + bytes([1]), saved[:876], saved[:876] + bytes([0])):
image.write_bytes(tail)
for target in ("normal", "dedicated"):
run(target, "read", image, 0, 0, 0)
for target in ("triple", "mqtt_uart"):
run(target, "read", image, 1, 0, 0)
def test_production_defaults_upgrade_and_runtime_controls(self):
compiler = shutil.which("g++") or shutil.which("clang++")
self.assertIsNotNone(compiler, "a host C++ compiler is required")
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
header = (ROOT / "examples/simple_repeater/MyMesh.h").read_text(encoding="ascii")
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
guard = (
"#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \\\n"
" && !defined(RS232_BRIDGE_DEFAULT_ON)"
)
declaration_start = cli.index(guard, cli.index('if (file) {'))
declaration = cli[declaration_start:cli.index("#endif", declaration_start) + len("#endif")]
esp_guard = "#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)"
esp_declaration = cli.index(esp_guard, cli.index('if (file) {'))
declaration += "\n" + cli[esp_declaration:cli.index("#endif", esp_declaration) + len("#endif")]
declaration_start = cli.index("#if defined(ESPNOW_BRIDGE_MERGED)", cli.index("mesh::hilStartupTrace(\"prefs_image_open_ready\")"))
declaration_end = cli.index(" // Every supported layout", declaration_start)
declaration = cli[declaration_start:declaration_end]
read_start = cli.index("if (file.available() >= (int)sizeof(_prefs->bridge_uart))")
read_end = cli.index("if (file.available() >= (int)sizeof(_prefs->bridge_format))", read_start)
# The fixture supplies a file positioned at the appended UART field.
uart_read = cli[read_start:read_end] + "}\n"
marker_read_start = cli.index(esp_guard, cli.index("if (file.available() >= (int)sizeof(_prefs->ota_channel))"))
uart_read += cli[marker_read_start:cli.index("#endif", marker_read_start) + len("#endif")]
# The old image mock has just its UART byte, so marker reads return
# zero; execute the real marker guard rather than inventing a fallback.
marker_start = cli.index("uint8_t bridge_profile = 0;", cli.index("if (file.available() >= (int)sizeof(_prefs->ota_channel))"))
uart_read += "uint8_t bridge_profile = 0;\n" + extract_braced(cli[marker_start:], "if (file.read(&bridge_profile,")
migrate_start = cli.index("// sanitise bad bridge pref values")
migrate_end = cli.index("if (!mesh::bridge::isValidEspNowFormat", migrate_start)
migration = cli[migrate_start:migrate_end]
@@ -137,7 +207,9 @@ class RS232RuntimeTests(unittest.TestCase):
lifecycle = "\n".join(extract_braced(header, signature) for signature in (
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
"RS232Bridge* createRS232Bridge()", "bool beginRS232Bridge()",
"bool endRS232Bridge()", "bool isBridgeRunning() const override",
"bool endRS232Bridge()", "bool rs232BridgeEnabled() const",
"bool isRs232BridgeRunning() const override", "bool setRs232BridgeState(bool enable)",
"bool restartRs232Bridge()", "bool isBridgeRunning() const override",
"bool setBridgeState(bool enable) override", "bool restartBridge() override",
))
lifecycle += "\n#ifdef WITH_ESPNOW_BRIDGE\n" + "\n".join(
+29 -5
View File
@@ -37,7 +37,10 @@ STUBS = r'''
#define WITH_ESPNOW_BRIDGE 1
#define MESH_DEBUG_PRINTLN(...) ((void)0)
constexpr int WL_CONNECTED = 3, WL_DISCONNECTED = 6, HTTP_GET = 0;
constexpr int WIFI_AP = 2;
enum wifi_mode_t { WIFI_OFF = 0, WIFI_STA = 1, WIFI_AP = 2 };
constexpr wifi_mode_t WIFI_MODE_NULL = WIFI_OFF;
int esp_wifi_stop();
int esp_wifi_deinit();
constexpr int ESP_OK = 0;
constexpr int LISTEN = 10;
@@ -65,7 +68,20 @@ struct MockWiFi {
IPAddress station_ip, ap_ip, configured_ap_ip;
int ap_protocol = 15;
int status() const { return station_status; }
int getMode() const { return sdk_ap_active ? WIFI_AP : 0; }
wifi_mode_t getMode() const {
return started_cache && sdk_started ? (sdk_ap_active ? WIFI_AP : WIFI_STA) : WIFI_OFF;
}
int channel() const { return initialized_cache && sdk_initialized ? 1 : 0; }
bool mode(wifi_mode_t requested) {
if (requested == getMode()) return true;
if (requested == WIFI_OFF) {
esp_wifi_stop(); esp_wifi_deinit();
initialized_cache = started_cache = false;
} else {
sdk_initialized = sdk_started = initialized_cache = started_cache = true;
}
return true;
}
IPAddress localIP() const { return station_ip; }
IPAddress softAPIP() const { return ap_ip; }
bool softAPConfig(IPAddress ip, IPAddress, IPAddress) {
@@ -99,6 +115,11 @@ int esp_wifi_stop() {
return 0;
}
int esp_wifi_deinit() { ++wifi_deinit_calls; sdk_initialized = false; return 0; }
int esp_wifi_get_mode(wifi_mode_t* mode) {
if (!sdk_initialized) return -1;
*mode = sdk_ap_active ? WIFI_AP : WIFI_STA;
return ESP_OK;
}
int server_count = 0, server_begin_calls = 0;
struct AsyncWebServerRequest {
@@ -268,10 +289,12 @@ class WifiOtaLifecycleTests(unittest.TestCase):
source = folder / "lifecycle.cpp"
binary = folder / ("lifecycle.exe" if os.name == "nt" else "lifecycle")
source.write_text(production_harness())
subprocess.run([
built = subprocess.run([
os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I", str(ROOT / "src"), str(source), "-o", str(binary),
], check=True, capture_output=True, text=True, timeout=60)
], capture_output=True, text=True, timeout=60)
if built.returncode:
raise RuntimeError(built.stdout + built.stderr)
cls.observations = {}
for scenario in SCENARIOS:
result = subprocess.run([str(binary), scenario], check=True,
@@ -317,7 +340,8 @@ class WifiOtaLifecycleTests(unittest.TestCase):
self.assertEqual(observation["deinit_calls"], count)
self.assertEqual(observation["sdk_initialized"], 1 - count)
self.assertEqual(observation["sdk_started"], 1 - count)
self.assertTrue(observation["initialized_cache"] and observation["started_cache"])
self.assertEqual(observation["initialized_cache"], 1 - count)
self.assertEqual(observation["started_cache"], 1 - count)
self.assertEqual(observation["endpoint_usable"], 1 - count)
if count == 0:
if scenario == "cleanup_web":
+14
View File
@@ -136,6 +136,12 @@ build_flags =
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_MQTT_BRIDGE=1
; Keep the normal repeater UART in the combined Full image; disabled until enabled.
-D WITH_RS232_BRIDGE=Serial2
-D WITH_RS232_BRIDGE_UART=2
-D WITH_RS232_BRIDGE_RX=5
-D WITH_RS232_BRIDGE_TX=6
-D RS232_BRIDGE_MERGED=1
; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h).
-D MQTT_NEIGHBORS_WITHOUT_PSRAM=1
-D MQTT_MAX_PACKET_SIZE=1024
@@ -159,6 +165,7 @@ build_flags =
-D ENV_PIN_SCL=34
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/bridges/MQTTBridge.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<helpers/MQTTMessageBuilder.cpp>
+<helpers/JWTHelper.cpp>
+<helpers/SNMPAgent.cpp>
@@ -587,6 +594,12 @@ build_flags =
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_MQTT_BRIDGE=1
; Keep the normal repeater UART in the combined Full image; disabled until enabled.
-D WITH_RS232_BRIDGE=Serial2
-D WITH_RS232_BRIDGE_UART=2
-D WITH_RS232_BRIDGE_RX=5
-D WITH_RS232_BRIDGE_TX=6
-D RS232_BRIDGE_MERGED=1
; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h).
-D MQTT_NEIGHBORS_WITHOUT_PSRAM=1
-D MQTT_MAX_PACKET_SIZE=1024
@@ -608,6 +621,7 @@ build_flags =
# -D MQTT_PASSWORD='"your-password"'
build_src_filter = ${Heltec_lora32_v3.build_src_filter}
+<helpers/bridges/MQTTBridge.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<helpers/MQTTMessageBuilder.cpp>
+<helpers/JWTHelper.cpp>
+<helpers/SNMPAgent.cpp>
@@ -191,6 +191,12 @@ build_flags =
-D MAX_NEIGHBOURS=50
-D MAX_RECENT_REPEATERS=256 ; no-PSRAM observer must retain internal DRAM for MQTT/TLS
-D WITH_MQTT_BRIDGE=1
; Keep the normal repeater UART in the combined Full image; disabled until enabled.
-D WITH_RS232_BRIDGE=Serial2
-D WITH_RS232_BRIDGE_UART=2
-D WITH_RS232_BRIDGE_RX=34
-D WITH_RS232_BRIDGE_TX=25
-D RS232_BRIDGE_MERGED=1
-D WEBCONFIG_DISABLED=1 ; async portal exceeds this 4 MB target's dual-OTA app slot; use CLI setup
-D FLOOD_CHANNEL_SCOPE_SLOTS=4 ; no-PSRAM observer must retain internal DRAM for MQTT/TLS
-D MQTT_MAX_PACKET_SIZE=1024
@@ -209,6 +215,7 @@ build_flags =
# -D MQTT_PASSWORD='"your-password"'
build_src_filter = ${LilyGo_TLora_V2_1_1_6_core.build_src_filter}
+<helpers/bridges/MQTTBridge.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<helpers/MQTTMessageBuilder.cpp>
+<helpers/JWTHelper.cpp>
+<helpers/ui/SSD1306Display.cpp>
+7
View File
@@ -121,6 +121,12 @@ build_flags =
-D ADMIN_PASSWORD='"password"'
-D MAX_NEIGHBOURS=50
-D WITH_MQTT_BRIDGE=1
; Keep the normal repeater UART in the combined Full image; disabled until enabled.
-D WITH_RS232_BRIDGE=Serial2
-D WITH_RS232_BRIDGE_UART=2
-D WITH_RS232_BRIDGE_RX=5
-D WITH_RS232_BRIDGE_TX=6
-D RS232_BRIDGE_MERGED=1
; Non-PSRAM board: neighbors table costs ~4 KB of internal DRAM (see MQTTBridge.h).
-D MQTT_NEIGHBORS_WITHOUT_PSRAM=1
-D MQTT_MAX_PACKET_SIZE=1024
@@ -143,6 +149,7 @@ build_flags =
-D ENV_PIN_SCL=18
build_src_filter = ${rak3112.build_src_filter}
+<helpers/bridges/MQTTBridge.cpp>
+<helpers/bridges/RS232Bridge.cpp>
+<helpers/MQTTMessageBuilder.cpp>
+<helpers/JWTHelper.cpp>
+<helpers/SNMPAgent.cpp>