mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-22 12:42:27 +00:00
Retain independent RS232 in Full MQTT repeaters
This commit is contained in:
1 parent
28023c7302
commit
51d74a200e
37 files changed
+1572
-196
No files matched your search
@@ -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
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Vendored
+387
@@ -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");
|
||||
}
|
||||
Vendored
+21
-3
@@ -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;
|
||||
|
||||
Vendored
+7
-3
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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'],
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]):
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user