mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 19:41:59 +00:00
Include runtime ESP-NOW in Full ESP32 repeaters
This commit is contained in:
@@ -342,6 +342,7 @@ jobs:
|
||||
python3 -B test/test_serial_ethernet_sessions.py -v
|
||||
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
|
||||
|
||||
+59
-14
@@ -2536,19 +2536,34 @@ get_exact_identity_full_migration_target() {
|
||||
printf '%s\n' "$successor"
|
||||
}
|
||||
|
||||
# Full repeaters carry ESP-NOW as a runtime transport. Plain room servers and
|
||||
# companions use different bridge wiring; only their existing MQTT recipes
|
||||
# support this overlay. Capacity exceptions keep their dedicated bridge image.
|
||||
supports_esp32_full_shared_espnow() {
|
||||
local env_name=$1
|
||||
[ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" = "ESP32_PLATFORM" ] \
|
||||
&& supports_esp32_full_build "$env_name" \
|
||||
&& ! is_esp32_companion_build "$env_name" \
|
||||
&& { is_mqtt_bridge_target "$env_name" || is_repeater_role_target "$env_name"; } || return 1
|
||||
# Primary ESP-NOW radios already own the SDK callbacks and packet transport.
|
||||
# A second bridge instance would conflict with that radio, not add a mode.
|
||||
! pio_env_option_contains "$env_name" build_flags "MESH_PRIMARY_ESPNOW" \
|
||||
&& ! pio_env_option_contains "$env_name" build_flags "MESH_ESPNOW_RADIO"
|
||||
}
|
||||
|
||||
apply_esp32_full_shared_bridge_profile() {
|
||||
local env_name=$1
|
||||
|
||||
# Full infrastructure images use the MQTT observer recipe for WiFi/TLS and
|
||||
# add the cooperative ESP-NOW bridge. Companion Full is a different client
|
||||
# transport role, not an infrastructure packet bridge.
|
||||
if [ "$ESP32_FULL_BUILD" != "1" ] \
|
||||
|| [ "${PIO_ENV_PLATFORM_BY_NAME[$env_name]:-}" != "ESP32_PLATFORM" ] \
|
||||
|| is_esp32_companion_build "$env_name" \
|
||||
|| ! is_mqtt_bridge_target "$env_name"; then
|
||||
return 0
|
||||
fi
|
||||
[ "$ESP32_FULL_BUILD" = "1" ] \
|
||||
&& supports_esp32_full_shared_espnow "$env_name" || return 0
|
||||
|
||||
# Keep historical dedicated bridge defaults and MQTT observer behavior.
|
||||
# Newly combined ordinary repeaters start with ESP-NOW off, including when
|
||||
# upgrading preferences written before an ESP-NOW runtime setting existed.
|
||||
if ! is_mqtt_bridge_target "$env_name" \
|
||||
&& ! pio_env_option_contains "$env_name" build_flags "WITH_ESPNOW_BRIDGE"; then
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DESPNOW_BRIDGE_MERGED=1"
|
||||
fi
|
||||
export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DWITH_ESPNOW_BRIDGE=1"
|
||||
append_platformio_build_src_filter "+<helpers/bridges/ESPNowBridge.cpp>"
|
||||
record_build_capability "bridge.espnow"
|
||||
@@ -3387,6 +3402,19 @@ declare_build_capability_contract() {
|
||||
# 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 [ "$BUILD_PROFILE_FOR_TARGET" = "full" ] \
|
||||
&& supports_esp32_full_shared_espnow "$pio_env_name"; then
|
||||
# A capability flag alone cannot retire a dedicated transport artifact.
|
||||
# Verify that the actual ESP-NOW driver survived linking in every Full
|
||||
# repeater/observer that claims it, including exact-identity source bases.
|
||||
record_build_expectation "bridge.espnow" "_ZN12ESPNowBridge5beginEv"
|
||||
elif [ "$env_name_lc" = mke_s3_repeater ]; then
|
||||
record_build_expectation "bridge.espnow" "_ZN12ESPNowBridge5beginEv"
|
||||
fi
|
||||
if is_nrf52_sensor_ota_pair_target "$env_name" \
|
||||
@@ -5519,10 +5547,23 @@ is_firmware_role_replaced_by_canonical_artifact() {
|
||||
}
|
||||
|
||||
get_merged_espnow_repeater_replacement() {
|
||||
case "${1,,}" in
|
||||
mke_s3_repeater_bridge_espnow) printf '%s\n' MKE_s3_repeater ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
local target=$1
|
||||
local base=${target%_}
|
||||
local candidate
|
||||
[[ "${base,,}" == *_repeater_bridge_espnow ]] || return 1
|
||||
base=${base%_bridge_espnow}
|
||||
# Match resolved exact hardware, including older capitalized roles and
|
||||
# trailing underscores. Do not fold different radios, displays or storage.
|
||||
for candidate in "$base" "${base}_" "${!PIO_ENV_PLATFORM_BY_NAME[@]}"; do
|
||||
[ "${candidate,,}" = "${base,,}" ] \
|
||||
|| [ "${candidate,,}" = "${base,,}_" ] || continue
|
||||
supports_esp32_full_shared_espnow "$candidate" || continue
|
||||
[ -n "${PIO_ENV_BOARD_BY_NAME[$target]:-}" ] \
|
||||
&& [ "${PIO_ENV_BOARD_BY_NAME[$target]}" = "${PIO_ENV_BOARD_BY_NAME[$candidate]:-}" ] || continue
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
is_runtime_setting_alias_target() {
|
||||
@@ -6381,11 +6422,15 @@ get_ordinary_full_priority() {
|
||||
case "${base,,}" in
|
||||
*_observer_mqtt) echo 20 ;;
|
||||
*_bridge_espnow) echo 10 ;;
|
||||
tbeam_sx1262_repeater|tbeam_sx1276_repeater|lilygo_tlora_v2_1_1_6_repeater) echo 0 ;;
|
||||
*)
|
||||
# G2's deployed Full identity is the observer. Other audited plain
|
||||
# targets keep their own identity, with observer sources where needed.
|
||||
if is_ordinary_partition_migration_full_target "$target" \
|
||||
|| is_esp32_full_only_bulk_target "$target"; then
|
||||
|| is_esp32_full_only_bulk_target "$target" \
|
||||
|| { supports_esp32_full_shared_espnow "$target" \
|
||||
&& ! get_unified_full_infrastructure_target "$target" >/dev/null \
|
||||
&& ! is_mqtt_bridge_target "$target"; }; then
|
||||
echo 30
|
||||
else
|
||||
echo 0
|
||||
|
||||
@@ -110,7 +110,7 @@ getters; relaying a command over LoRa keeps its remote restrictions.
|
||||
| nRF52 repeater/room-server/sensor release pair | `full-ota` retains the complete supported sensor recipe; `reduced-ota` omits declared optional sensor drivers. Both include verified LoRa OTA, use the same logical target identity and exact storage/layout contract within the pair, and retain the role's normal commands. Repeaters/room servers also verify `retry.preset`. RAK3401 and RAK4631 reduced builds retain GPS where compatible and INA219/INA226/INA260/INA3221 I2C voltage/current monitors. External QSPI/SD targets also publish both choices. Companion/Terminal Chat and KISS roles do not enter this policy. |
|
||||
| ESP32 MQTT observer or ESP-NOW bridge | Always uses the expanded FULL partition profile. The build never substitutes a reduced CLI to fit the legacy application slot. |
|
||||
| FULL ESP32 USB + WiFi | Uses the matching MQTT target with packet logging, USB debug defaulting off, and the complete command surface supported by that role and hardware. Debug verbosity can be enabled only for diagnostics compiled into that image. `get/set logging.output off\|usb\|wifi\|both` selects and persists the active output paths; `get/set usb.debug` separately saves USB verbosity. |
|
||||
| FULL ESP32 logging fallback | Uses the matching non-MQTT target only when no WiFi MQTT sibling exists, with debug and packet support compiled and the complete command surface supported by that role and hardware. USB debug defaults off; `get/set usb.debug` selects verbose output independently of packet capture. Its persistent USB master also covers output-off operation, avoiding a second FULL ESP-NOW image. |
|
||||
| FULL ESP32 logging fallback | Uses the matching non-MQTT target when no compatible MQTT sibling exists, preserving its hardware and role CLI. Full LoRa repeaters also include runtime ESP-NOW with `set espnow.enabled on\|off` and `get espnow.running`; newly combined modes default off. Dedicated images remain only for measured capacity exceptions. USB debug defaults off; `get/set usb.debug` selects verbose output independently of packet capture. |
|
||||
| nRF52 dual-CDC Full Companion | Fresh installs expose only interface `00`; it starts as an ASCII terminal and automatically hands a complete `<` frame to framed Companion. The same interface also carries exclusive serial mOTA traffic. `set usb.logging on reboot` adds output-only interface `02` for ASCII RAW/RX/TX records and optional debug. Alternatively, `set usb.logging stream reboot` keeps CLI replies and packet records on interface `00` for unmodified one-port serial bridges; Binary Companion and serial mOTA require logging off on that port. BLE and source-only LoRa OTA remain available. `get/set usb.logging` persistently controls whether the logging interface is present; `get/set usb.debug` changes verbosity without a reboot. Send CLI/control queries to interface `00`, not the log endpoint. |
|
||||
| ESP32 single-TTY Full Companion | Every ESP32 Full image starts with the ASCII terminal on its one USB TTY and automatically hands a complete `<` frame to framed Companion. On 1.17.1.5, run `set powersaving off` first. `set usb.logging on` switches that TTY to an input-capable plaintext RAW/RX/TX logging terminal and makes framed Companion unavailable on USB; `set usb.logging off` stops logging but leaves the TTY in normal ASCII mode. `get/set usb.debug` separately controls saved verbose diagnostics, defaulting off. The terminal stop token or a valid incoming framed probe then performs the ordinary switch to Binary Companion. A saved logging-on setting starts directly in that logging terminal and disables automatic frame detection. BLE, WiFi, and source-only LoRa OTA remain available. ESP32 Full uses Arduino-ESP32 2.x where supported; RC32 and ESP32-C6 keep their board-required Arduino 3.x platform but still expose only one TTY. |
|
||||
| `no_external_sensors` | Trims selected optional environmental/ranging drivers and their settings; it does not remove generic I2C, core repeater discovery, routing, or runtime RS-232 commands. RAK3401 and RAK4631 profiles retain the four common INA I2C voltage/current monitors. GPS-preserving RAK nRF52 OTA profiles retain their GPS commands and provider; RAK4631 defaults the bridge to UART 2 because RAK12501/L76K GPS uses UART 1. Legacy target suffixes remain stable for OTA identity compatibility. |
|
||||
|
||||
+25
-14
@@ -11,10 +11,13 @@ LoRa network A <--> Repeater A <-- ESP-NOW --> Repeater B <--> LoRa network B
|
||||
|
||||
## 1. Choose the firmware
|
||||
|
||||
Install the exact board's **Full ESP-NOW bridge repeater** image on each board.
|
||||
The target name ends in `_repeater_bridge_espnow`; a standard Heltec V4 uses
|
||||
`heltec_v4_repeater_bridge_espnow`. Full/MQTT and Full Companion images are
|
||||
different choices. See the [firmware picker guide](firmware_picker.md).
|
||||
Install the exact board's **Full repeater** image on each board. ESP-NOW is
|
||||
included as a runtime transport on supported ESP32 Full repeaters. Newly
|
||||
combined ordinary repeaters leave it off until explicitly enabled; that
|
||||
choice is saved across reboot. Boards with measured capacity exceptions keep
|
||||
separate `_repeater_bridge_espnow` images. Historical dedicated target names
|
||||
remain available for direct builds with their original bridge-on defaults.
|
||||
Full Companion is a different role. See the [firmware picker guide](firmware_picker.md).
|
||||
|
||||
Open each board's [USB console](https://flasher.meshcore.io/console) at
|
||||
115200 baud and run:
|
||||
@@ -23,8 +26,10 @@ Open each board's [USB console](https://flasher.meshcore.io/console) at
|
||||
get bridge.type
|
||||
```
|
||||
|
||||
The answer must be `espnow`. `mqtt`, `rs232`, or `none` means that image does
|
||||
not provide the ESP-NOW bridge described here.
|
||||
The answer can be `espnow`, `mqtt+espnow`, or `rs232+espnow`. Confirm the
|
||||
independent ESP-NOW control is present with `get espnow.enabled` and
|
||||
`get espnow.running`. The `espnow` commands select this transport without
|
||||
changing another bridge in a combined image.
|
||||
|
||||
## 2. Set each board's LoRa network
|
||||
|
||||
@@ -33,7 +38,7 @@ the radio settings used by the network on that board's side. Use `Bridge-A`
|
||||
for the first board and `Bridge-B` for the second, or choose your own names.
|
||||
|
||||
```text
|
||||
set bridge.enabled off
|
||||
set espnow.enabled off
|
||||
set name Bridge-A
|
||||
set radio <freq>,<bw>,<sf>,<cr>
|
||||
reboot
|
||||
@@ -60,14 +65,14 @@ case-sensitive secret on each board, using 1-15 characters. Channel `6` is
|
||||
an example; both boards must use the same channel.
|
||||
|
||||
```text
|
||||
set bridge.enabled off
|
||||
set espnow.enabled off
|
||||
set bridge.channel 6
|
||||
set bridge.format wrapped
|
||||
set bridge.secret YourSharedKey
|
||||
set bridge.source tx
|
||||
set bridge.delay 500
|
||||
set repeat on
|
||||
set bridge.enabled on
|
||||
set espnow.enabled on
|
||||
```
|
||||
|
||||
These settings are saved across reboot. Bridge changes apply immediately;
|
||||
@@ -78,15 +83,15 @@ Stopping it first lets you finish configuring both sides before connecting them.
|
||||
|
||||
| Option | Choices and purpose |
|
||||
| --- | --- |
|
||||
| `bridge.enabled` | `on` starts the bridge; `off` stops it and keeps its saved settings. |
|
||||
| `espnow.enabled` | `on` starts ESP-NOW; `off` stops it and keeps its saved settings. Independent of MQTT or RS-232. |
|
||||
| `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. |
|
||||
| `bridge.source` | `tx` crosses packets this repeater transmits on LoRa, after its forwarding decisions; use this for the setup above. `rx` crosses received packets before those decisions and can pass traffic the local repeater would not forward. |
|
||||
| `bridge.delay` | Wait before processing a packet received from the other bridge, in milliseconds. Range: 0-10000. Start with the default `500`. |
|
||||
| `repeat` | `on` enables normal LoRa forwarding; `off` disables forwarding. Keep it `on` on both bridge repeaters. Normal routing, scope, and packet-filter rules still apply. |
|
||||
| `bridge.type` | Read-only firmware capability. It must report `espnow` for this guide. |
|
||||
| `bridge.running` | Read-only runtime state. `on` means the local bridge started; it does not prove the other board is reachable. |
|
||||
| `bridge.type` | Read-only firmware capability, including combined transports. |
|
||||
| `espnow.running` | Read-only ESP-NOW runtime state. `on` means the local bridge started; it does not prove the other board is reachable. |
|
||||
|
||||
This fork uses **`set bridge.source tx`** or **`rx`**. Some upstream guides
|
||||
use `logTx` and `logRx`; those are not the command values used here.
|
||||
@@ -99,8 +104,8 @@ LoRa repeater, see [raw ESP-NOW compatibility](WiFi.md#wifi-companion-setup).
|
||||
Run on both boards:
|
||||
|
||||
```text
|
||||
get bridge.enabled
|
||||
get bridge.running
|
||||
get espnow.enabled
|
||||
get espnow.running
|
||||
get bridge.channel
|
||||
get bridge.format
|
||||
get bridge.secret
|
||||
@@ -127,6 +132,12 @@ If it does not work:
|
||||
and check the networks' forwarding and scope rules.
|
||||
|
||||
A running bridge keeps the device awake, even with device power saving enabled.
|
||||
ESP-NOW shares the WiFi radio: use the connected WiFi network's channel when
|
||||
WiFi is active. Browser OTA pauses ESP-NOW on ordinary non-MQTT repeaters,
|
||||
reports that pause, and resumes saved-enabled ESP-NOW when OTA is stopped.
|
||||
A failed OTA start restores the bridge. The RS-232 transport remains available
|
||||
on combined boards while ESP-NOW is paused.
|
||||
|
||||
For every command's full details, see the [bridge CLI reference](cli_commands.md#bridge-when-bridge-support-is-compiled-in).
|
||||
|
||||
For a broader comparison with a Linux-based bridge, see the
|
||||
|
||||
+12
-8
@@ -193,17 +193,21 @@ Normal repeater firmware includes runtime-controlled RS-232 support where the
|
||||
board has room; use `set bridge.enabled on` after configuring `bridge.uart` and
|
||||
`bridge.baud`. The Wio-E5 remains the capacity exception and offers a separate
|
||||
RS-232 image. Choose Wi-Fi MQTT under **Logging / MQTT**; it is an output mode,
|
||||
not a second connection choice. Repeaters may still offer separate ESP-NOW or
|
||||
Ethernet bridge firmware because those paths use different compiled drivers.
|
||||
not a second connection choice. Full ESP32 LoRa repeaters include runtime
|
||||
ESP-NOW when capacity permits; dedicated ESP-NOW images remain for measured
|
||||
capacity exceptions. Ethernet still needs its exact hardware recipe.
|
||||
|
||||
For commands and option explanations, follow the
|
||||
[ESP-NOW bridge setup guide](espnow_bridge_setup.md).
|
||||
|
||||
An ESP-NOW bridge target keeps LoRa as its primary mesh radio. Expanded ESP32
|
||||
Full repeater and room-server images combine ESP-NOW with WiFi MQTT in the
|
||||
same firmware. MQTT and ESP-NOW are independent runtime transports: use
|
||||
`set mqtt.enabled on|off` for MQTT and `set bridge.enabled on|off` (or
|
||||
`set espnow.enabled on|off`) for ESP-NOW, so either or both can run. Both use
|
||||
Full LoRa repeaters include ESP-NOW, including boards without an MQTT
|
||||
recipe. Existing MQTT-backed Full repeater and room-server images combine
|
||||
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
|
||||
match the connected WiFi access point's fixed channel. ESP-NOW-only mode does
|
||||
not require WiFi credentials. Its runtime
|
||||
@@ -214,8 +218,8 @@ backward-compatible bridge-to-bridge default using `bridge.secret`) or `raw`
|
||||
firmware choice, not two board images. Match `bridge.channel` to the primary
|
||||
nodes' `espnow.channel` before selecting `set bridge.format raw`.
|
||||
For Heltec V4 specifically, `companion_radio_full` is still a LoRa-primary
|
||||
Companion; choose the existing `heltec_v4_repeater_bridge_espnow` firmware to
|
||||
make that board the LoRa/ESP-NOW gateway. Use its exact merged artifact when
|
||||
Companion; choose the combined `heltec_v4_repeater` Full firmware and enable
|
||||
ESP-NOW to make that board the LoRa/ESP-NOW gateway. Use its exact merged artifact when
|
||||
changing roles or partition layouts.
|
||||
|
||||
## Share a selection
|
||||
|
||||
@@ -3472,6 +3472,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#endif
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
, bridge(&_prefs, _mgr, &rtc)
|
||||
, shared_espnow_retry_at(0)
|
||||
#endif
|
||||
{
|
||||
// Global constructors run before setup(), while the heap is still
|
||||
@@ -3663,6 +3664,12 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
_prefs.espnow_bridge_enabled = 0; // merged normal repeater until explicitly enabled
|
||||
#else
|
||||
_prefs.espnow_bridge_enabled = 1; // preserves the Full image's former combined default
|
||||
#endif
|
||||
#if defined(WITH_ESPNOW_BRIDGE) && defined(ESPNOW_BRIDGE_MERGED) \
|
||||
&& !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
// The sole transport keeps its legacy primary lifecycle, while merged
|
||||
// profiles persist the same explicit ESP-NOW intent as combined profiles.
|
||||
_prefs.bridge_enabled = _prefs.espnow_bridge_enabled;
|
||||
#endif
|
||||
_prefs.bridge_delay = 500; // milliseconds
|
||||
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
|
||||
@@ -12835,6 +12842,7 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
|
||||
else startSharedEspNowBridgeIfReady();
|
||||
#else
|
||||
if (bridge.isRunning()) bridge.loop();
|
||||
else startSharedEspNowBridgeIfReady();
|
||||
#endif
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) \
|
||||
|
||||
@@ -592,6 +592,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#endif
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
ESPNowBridge bridge;
|
||||
uint32_t shared_espnow_retry_at = 0;
|
||||
#endif
|
||||
#ifdef WITH_BRIDGE
|
||||
AbstractBridge* activeBridge() {
|
||||
@@ -614,13 +615,18 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
// Retry ESP-NOW without taking over an active OTA session. MQTT creates its
|
||||
// WiFi task asynchronously, so it must first associate on the shared channel.
|
||||
bool startSharedEspNowBridgeIfReady() {
|
||||
if (espnow_bridge.isRunning()) return true;
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
||||
ESPNowBridge& transport = espnow_bridge;
|
||||
if (!_prefs.espnow_bridge_enabled) return false;
|
||||
#else
|
||||
ESPNowBridge& transport = bridge;
|
||||
if (!_prefs.bridge_enabled) return false;
|
||||
#endif
|
||||
if (transport.isRunning()) return true;
|
||||
// Browser OTA pauses the bridges for the entire upload session. The
|
||||
// periodic retry must not undo that pause on its next loop iteration.
|
||||
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
|
||||
@@ -629,9 +635,9 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#endif
|
||||
if (!millisHasNowPassed(shared_espnow_retry_at)) return false;
|
||||
shared_espnow_retry_at = millis() + 5000;
|
||||
configureBridgeFilter(&espnow_bridge);
|
||||
espnow_bridge.begin();
|
||||
return espnow_bridge.isRunning();
|
||||
configureBridgeFilter(&transport);
|
||||
transport.begin();
|
||||
return transport.isRunning();
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_SNMP
|
||||
@@ -1386,6 +1392,12 @@ public:
|
||||
? setEspNowBridgeState(true) : setEspNowBridgeState(false);
|
||||
return mqtt_ok && espnow_ok;
|
||||
#else
|
||||
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
// A standalone bridge shares its WiFi driver with the browser uploader.
|
||||
// Preserve saved intent, but do not restart it until OTA has stopped.
|
||||
if (enable && _cli.getBoard()->isOTAUpdateRunning()) return false;
|
||||
shared_espnow_retry_at = 0;
|
||||
#endif
|
||||
// Disabling an already-absent heap-backed bridge is successful and must
|
||||
// not allocate an instance merely to stop it. The embedded ESP-NOW bridge
|
||||
// is always present, so it continues through the normal state check.
|
||||
@@ -1477,6 +1489,10 @@ public:
|
||||
#endif
|
||||
return restartEspNowBridge() && restartMqttBridge();
|
||||
#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
|
||||
// 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
|
||||
|
||||
@@ -132,6 +132,13 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
and manifest["build_profile"] == "full"
|
||||
and manifest.get("verified") is True)
|
||||
|
||||
def proven_espnow(manifest: dict) -> bool:
|
||||
return (qualified_full(manifest)
|
||||
and "bridge.espnow" in (manifest.get("capabilities") or [])
|
||||
and any(check.get("capability") == "bridge.espnow"
|
||||
and check.get("present") is True
|
||||
for check in manifest.get("verification") or []))
|
||||
|
||||
# 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.
|
||||
@@ -158,7 +165,7 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
combined_espnow = set()
|
||||
for record in records:
|
||||
manifest = record["manifest"]
|
||||
if not qualified_full(manifest) or "bridge.espnow" not in (manifest.get("capabilities") or []):
|
||||
if not proven_espnow(manifest):
|
||||
continue
|
||||
base = manifest["target"].rstrip("_").lower()
|
||||
if base.endswith("_observer_mqtt"):
|
||||
@@ -172,6 +179,13 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
manifest = record["manifest"]
|
||||
if manifest["target"].lower() in tower_legacy | mke_legacy:
|
||||
continue
|
||||
legacy_base = manifest["target"].rstrip("_").lower()
|
||||
if (manifest["platform"] == "ESP32_PLATFORM"
|
||||
and legacy_base.endswith("_repeater_bridge_espnow")
|
||||
and legacy_base[:-len("_bridge_espnow")] in combined_espnow):
|
||||
# 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" or manifest["build_profile"] != "full":
|
||||
passthrough.append(record)
|
||||
continue
|
||||
@@ -197,9 +211,16 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
"tbeam_sx1262_repeater", "tbeam_sx1276_repeater",
|
||||
"tbeam_sx1262_room_server", "tbeam_sx1276_room_server",
|
||||
"lilygo_tlora_v2_1_1_6_repeater",
|
||||
"meshadventurer_sx1262_repeater", "meshadventurer_sx1268_repeater",
|
||||
}
|
||||
# Capacity exceptions may still need a dedicated bridge. Old plain
|
||||
# Meshadventurer images without the linked driver must not supersede it.
|
||||
if key in {"meshadventurer_sx1262_repeater", "meshadventurer_sx1268_repeater"}:
|
||||
special = key not in combined_espnow
|
||||
priority = {"observer": 20, "bridge": 10, "plain": 0 if special else 30}[kind]
|
||||
if key in combined_espnow and not proven_espnow(manifest):
|
||||
# A stale normal image must not displace the proven transport
|
||||
# while simultaneously suppressing its historical dedicated image.
|
||||
priority = -1
|
||||
old = chosen.get(key)
|
||||
if old is None or priority > old[0]:
|
||||
chosen[key] = (priority, record)
|
||||
|
||||
+56
-19
@@ -835,6 +835,9 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
const uint8_t previous_espnow_enabled = _prefs->espnow_bridge_enabled;
|
||||
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
const uint8_t previous_bridge_enabled = _prefs->bridge_enabled;
|
||||
#endif
|
||||
#endif
|
||||
if (legacy && _prefs->loadSerial(legacy)) {
|
||||
loaded = true;
|
||||
@@ -853,6 +856,14 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
// The legacy JSON schema has no independent ESP-NOW intent marker.
|
||||
_prefs->espnow_bridge_enabled = 0;
|
||||
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
_prefs->bridge_enabled = 0;
|
||||
#endif
|
||||
#elif defined(WITH_ESPNOW_BRIDGE) && defined(ESPNOW_BRIDGE_MERGED) \
|
||||
&& !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
// An explicitly default-on compatibility profile imports its legacy
|
||||
// single-bridge intent into the new independent ESP-NOW setting.
|
||||
_prefs->espnow_bridge_enabled = _prefs->bridge_enabled;
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
node_prefs_needs_migration = true;
|
||||
@@ -867,6 +878,9 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
_prefs->espnow_bridge_enabled = previous_espnow_enabled;
|
||||
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
_prefs->bridge_enabled = previous_bridge_enabled;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
if (legacy) legacy.close();
|
||||
@@ -1503,6 +1517,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
#endif
|
||||
_prefs->bridge_enabled = constrain(_prefs->bridge_enabled, 0, 1);
|
||||
_prefs->espnow_bridge_enabled = constrain(_prefs->espnow_bridge_enabled, 0, 1);
|
||||
#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.
|
||||
// Only the separately persisted and marker-validated ESP-NOW intent wins.
|
||||
_prefs->bridge_enabled = _prefs->espnow_bridge_enabled;
|
||||
#endif
|
||||
_prefs->bridge_delay = constrain(_prefs->bridge_delay, 0, 10000);
|
||||
_prefs->bridge_pkt_src = constrain(_prefs->bridge_pkt_src, 0, 1);
|
||||
_prefs->bridge_baud = constrain(_prefs->bridge_baud, 9600, BRIDGE_MAX_BAUD);
|
||||
@@ -2845,10 +2865,9 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
webconfig_stopped = true;
|
||||
}
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& !defined(WITH_MQTT_BRIDGE)
|
||||
// Browser OTA owns the shared WiFi driver, but the independent UART
|
||||
// bridge can keep running throughout the upload.
|
||||
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
// Browser OTA owns the shared WiFi driver. An independent UART bridge
|
||||
// can keep running throughout the upload.
|
||||
const bool espnow_paused = _callbacks->isEspNowBridgeRunning();
|
||||
if (espnow_paused && !_callbacks->setEspNowBridgeState(false)) {
|
||||
strcpy(reply, "ERR: could not pause ESP-NOW for OTA");
|
||||
@@ -2858,8 +2877,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
reply[0] = 0;
|
||||
if (!_board->startOTAUpdate(_prefs->node_name, reply, force_ap)) {
|
||||
if (!reply[0]) strcpy(reply, "Error");
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& !defined(WITH_MQTT_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
if (espnow_paused && !_callbacks->setEspNowBridgeState(true)) {
|
||||
const size_t used = strlen(reply);
|
||||
snprintf(reply + used, 160 - used, "; ESP-NOW resume failed");
|
||||
@@ -2872,8 +2890,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
_callbacks->setBridgeState(false);
|
||||
}
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& !defined(WITH_MQTT_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
else if (espnow_paused) {
|
||||
const size_t used = strlen(reply);
|
||||
snprintf(reply + used, 160 - used, "; ESP-NOW paused");
|
||||
@@ -2896,9 +2913,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
|
||||
_callbacks->setBridgeState(true);
|
||||
}
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& !defined(WITH_MQTT_BRIDGE)
|
||||
else if (_prefs->espnow_bridge_enabled) {
|
||||
#if defined(WITH_ESPNOW_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
else if (
|
||||
#if defined(WITH_RS232_BRIDGE) || defined(ESPNOW_BRIDGE_MERGED)
|
||||
_prefs->espnow_bridge_enabled
|
||||
#else
|
||||
_prefs->bridge_enabled
|
||||
#endif
|
||||
) {
|
||||
const bool resumed = _callbacks->setEspNowBridgeState(true);
|
||||
const size_t used = strlen(reply);
|
||||
snprintf(reply + used, 160 - used, resumed
|
||||
@@ -3762,15 +3784,20 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#endif
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
if (memcmp(config, "espnow.enabled ", 15) == 0) {
|
||||
const char* value = config + 15;
|
||||
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
||||
strcpy(reply, "Error: use set espnow.enabled on|off");
|
||||
} else {
|
||||
const bool enable = strcmp(value, "on") == 0;
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE) \
|
||||
|| defined(ESPNOW_BRIDGE_MERGED)
|
||||
_prefs->espnow_bridge_enabled = enable;
|
||||
#endif
|
||||
#if !defined(WITH_MQTT_BRIDGE) && !defined(WITH_RS232_BRIDGE)
|
||||
_prefs->bridge_enabled = enable;
|
||||
#endif
|
||||
const bool applied = _callbacks->setEspNowBridgeState(enable);
|
||||
savePrefs();
|
||||
strcpy(reply, applied ? "OK"
|
||||
@@ -4633,10 +4660,15 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
if (!parseOnOffStrict(&config[15], enable)) {
|
||||
strcpy(reply, "Error: usage set bridge.enabled on|off");
|
||||
} else {
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
// In a combined Full image bridge.enabled retains its ESP-NOW meaning;
|
||||
// mqtt.enabled is the independent MQTT switch.
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) \
|
||||
|| (defined(ESPNOW_BRIDGE_MERGED) && !defined(WITH_RS232_BRIDGE)))
|
||||
// bridge.enabled retains its ESP-NOW meaning; mqtt.enabled is the
|
||||
// independent MQTT switch when the profile also has that transport.
|
||||
_prefs->espnow_bridge_enabled = enable;
|
||||
#if !defined(WITH_MQTT_BRIDGE)
|
||||
_prefs->bridge_enabled = enable;
|
||||
#endif
|
||||
const bool applied = _callbacks->setEspNowBridgeState(enable);
|
||||
savePrefs();
|
||||
strcpy(reply, applied ? "OK"
|
||||
@@ -5320,10 +5352,15 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#else
|
||||
_callbacks->isBridgeRunning() ? "on" : "off");
|
||||
#endif
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
} else if (configKeyEquals(config, "espnow.enabled")) {
|
||||
sprintf(reply, "> %s", _prefs->espnow_bridge_enabled ? "on" : "off");
|
||||
sprintf(reply, "> %s",
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE) \
|
||||
|| defined(ESPNOW_BRIDGE_MERGED)
|
||||
_prefs->espnow_bridge_enabled ? "on" : "off");
|
||||
#else
|
||||
_prefs->bridge_enabled ? "on" : "off");
|
||||
#endif
|
||||
} else if (configKeyEquals(config, "espnow.running")) {
|
||||
sprintf(reply, "> %s", _callbacks->isEspNowBridgeRunning() ? "on" : "off");
|
||||
#endif
|
||||
|
||||
@@ -197,8 +197,10 @@ public:
|
||||
// Appended after the existing /com_prefs tail. Keep the preamble in the same
|
||||
// transaction as frequency/modulation; old images adopt /radio_profiles.
|
||||
uint16_t primary_radio_preamble = 0;
|
||||
// Full ESP32 images can carry another bridge and ESP-NOW simultaneously. Keep the
|
||||
// Full ESP32 images can carry another bridge and ESP-NOW simultaneously. Keep
|
||||
// ESP-NOW intent separately so either transport can be selected at runtime.
|
||||
// Sole merged ESP-NOW profiles also use this intent and mirror bridge_enabled
|
||||
// for their legacy single-bridge lifecycle and CLI alias.
|
||||
// Old preference images did not have this byte; they default to enabled to
|
||||
// retain the previous MQTT combined-Full behavior after an upgrade. Merged
|
||||
// default-off profiles require their appended profile marker instead.
|
||||
|
||||
@@ -5,11 +5,29 @@
|
||||
#include <helpers/esp32/WiFiRadioPolicy.h>
|
||||
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
#include <WiFi.h>
|
||||
|
||||
static void stopBridgeWiFiIfUnused() {
|
||||
if (!(mesh::wireless::control().enabled() & mesh::wireless::WiFi)) {
|
||||
esp_wifi_stop();
|
||||
esp_wifi_deinit();
|
||||
// WebConfig/OTA may have adopted an initially IDF-only bridge driver.
|
||||
// Shut down through Arduino first so its private initialized/started
|
||||
// caches cannot outlive the SDK driver and break the next WiFi startup.
|
||||
// In the pinned Arduino 2/3 cores, channel() is gated by initialized but
|
||||
// getMode() is also gated by started. A failed facade start can therefore
|
||||
// report OFF despite retaining initialized state. Complete that start
|
||||
// before stopping it, without allocating a facade for a pure IDF owner.
|
||||
if (WiFi.getMode() == WIFI_OFF && WiFi.channel() > 0) {
|
||||
if (!WiFi.mode(WIFI_STA)) return;
|
||||
}
|
||||
if (!WiFi.mode(WIFI_OFF)) return;
|
||||
|
||||
// Arduino OFF is a no-op when the bridge alone initialized the SDK.
|
||||
// Probe the SDK separately rather than trusting the facade's mode/cache.
|
||||
wifi_mode_t mode = WIFI_MODE_NULL;
|
||||
if (esp_wifi_get_mode(&mode) == ESP_OK) {
|
||||
esp_wifi_stop();
|
||||
esp_wifi_deinit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+219
@@ -0,0 +1,219 @@
|
||||
// Production methods and dispatch branches are inserted by test_espnow_runtime.py.
|
||||
// The bridge driver is mocked to inject SDK failures without ESP32 hardware.
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/bridges/ESPNowBridgeFormat.h>
|
||||
#define MESH_DEBUG_PRINTLN(...) ((void)0)
|
||||
|
||||
namespace mesh { struct Packet {}; }
|
||||
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;
|
||||
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;
|
||||
char bridge_secret[32] = {};
|
||||
};
|
||||
class AbstractBridge {
|
||||
public:
|
||||
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;
|
||||
};
|
||||
class ESPNowBridge : public AbstractBridge {
|
||||
bool running = false;
|
||||
public:
|
||||
unsigned begins = 0, ends = 0, fail_starts = 0, sent = 0, loops = 0;
|
||||
void begin() override {
|
||||
++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*) override { ++sent; }
|
||||
};
|
||||
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 FakeBoard { bool ota = false; bool isOTAUpdateRunning() const { return ota; } };
|
||||
struct FakeCLI { FakeBoard board; FakeBoard* getBoard() { return &board; } };
|
||||
struct Callbacks {
|
||||
virtual ~Callbacks() = default;
|
||||
virtual bool isBridgeRunning() const = 0;
|
||||
virtual bool setBridgeState(bool) = 0;
|
||||
virtual bool restartBridge() = 0;
|
||||
@CALLBACK_FORWARDS@
|
||||
};
|
||||
class MyMesh : public Callbacks {
|
||||
public:
|
||||
NodePrefs _prefs;
|
||||
ESPNowBridge bridge;
|
||||
uint32_t shared_espnow_retry_at = 0;
|
||||
FakeCLI _cli;
|
||||
void configureBridgeFilter(AbstractBridge*) {}
|
||||
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;
|
||||
}
|
||||
@LIFECYCLE@
|
||||
};
|
||||
@PARSERS@
|
||||
class CommonCLI {
|
||||
public:
|
||||
NodePrefs* _prefs;
|
||||
MyMesh* _callbacks;
|
||||
NodePrefs stored;
|
||||
unsigned saves = 0;
|
||||
explicit CommonCLI(MyMesh& mesh) : _prefs(&mesh._prefs), _callbacks(&mesh) {}
|
||||
void savePrefs() { ++saves; stored = *_prefs; }
|
||||
void set(const char* input, char* reply) {
|
||||
char config[256] = {};
|
||||
assert(strlen(input) < sizeof(config)); strcpy(config, input);
|
||||
@SET_BRANCHES@
|
||||
}
|
||||
void get(const char* config, char* reply) { @GET_BRANCHES@ }
|
||||
};
|
||||
static void expect_get(CommonCLI& cli, const char* key, const char* expected) {
|
||||
char reply[256] = {}; cli.get(key, reply); assert(strcmp(reply, expected) == 0);
|
||||
}
|
||||
static void assert_aliases(CommonCLI& cli, bool enabled, bool running) {
|
||||
expect_get(cli, "bridge.enabled", enabled ? "> on" : "> off");
|
||||
expect_get(cli, "espnow.enabled", enabled ? "> on" : "> off");
|
||||
expect_get(cli, "bridge.running", running ? "> on" : "> off");
|
||||
expect_get(cli, "espnow.running", running ? "> on" : "> off");
|
||||
}
|
||||
int main() {
|
||||
{
|
||||
MyMesh mesh; mesh.defaults();
|
||||
CommonCLI cli(mesh);
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
const bool initial_enabled = false;
|
||||
#else
|
||||
const bool initial_enabled = true;
|
||||
#endif
|
||||
assert(mesh._prefs.bridge_enabled == initial_enabled);
|
||||
assert(mesh._prefs.espnow_bridge_enabled == initial_enabled);
|
||||
mesh.boot();
|
||||
assert(mesh.bridge.begins == (initial_enabled ? 1 : 0));
|
||||
assert_aliases(cli, initial_enabled, initial_enabled);
|
||||
expect_get(cli, "bridge.type", "> espnow");
|
||||
}
|
||||
MyMesh mesh; mesh.defaults();
|
||||
CommonCLI cli(mesh);
|
||||
char reply[256] = {};
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.bridgesPreventSleep());
|
||||
assert_aliases(cli, false, false);
|
||||
const unsigned initial_begins = mesh.bridge.begins;
|
||||
cli.set("bridge.channel 6", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.bridge.begins == initial_begins);
|
||||
cli.set("bridge.secret combined-secret", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.bridge.begins == initial_begins);
|
||||
cli.set("bridge.format raw", reply);
|
||||
assert(strncmp(reply, "OK", 2) == 0 && mesh.bridge.begins == initial_begins);
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.bridgesPreventSleep());
|
||||
assert_aliases(cli, true, true);
|
||||
mesh::Packet packet;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge.sent == 1 && mesh.bridge.loops == 1); // RX is routed once.
|
||||
cli.set("bridge.source tx", reply);
|
||||
assert(strcmp(reply, "OK") == 0);
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet);
|
||||
assert(mesh.bridge.sent == 2); // TX is routed once, RX no longer feeds bridge.
|
||||
const unsigned configured_begins = mesh.bridge.begins;
|
||||
cli.set("bridge.channel 11", reply);
|
||||
cli.set("bridge.secret updated-secret", reply);
|
||||
cli.set("bridge.format wrapped", reply);
|
||||
assert(mesh.bridge.begins == configured_begins + 3);
|
||||
expect_get(cli, "bridge.channel", "> 11");
|
||||
expect_get(cli, "bridge.secret", "> updated-secret");
|
||||
expect_get(cli, "bridge.format", "> wrapped");
|
||||
const unsigned saves = cli.saves;
|
||||
for (const char* invalid : {"bridge.enabled onjunk", "espnow.enabled onjunk",
|
||||
"bridge.channel 0", "bridge.channel 14", "bridge.channel 6junk",
|
||||
"bridge.secret ", "bridge.format unsupported", "bridge.source rxjunk"}) {
|
||||
cli.set(invalid, reply);
|
||||
assert(strncmp(reply, "Error", 5) == 0 && cli.saves == saves);
|
||||
assert_aliases(cli, true, true);
|
||||
}
|
||||
// The persisted aliases reconstruct exactly the enabled sole runtime.
|
||||
MyMesh restarted; restarted.defaults(); restarted._prefs = cli.stored;
|
||||
restarted.boot();
|
||||
assert(restarted.bridge.begins == 1 && restarted.isBridgeRunning());
|
||||
// Failed SDK initialization preserves saved intent and retries cooperatively.
|
||||
cli.set("espnow.enabled off", reply);
|
||||
mesh.bridge.fail_starts = 2;
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(strstr(reply, "setting saved") && !mesh.isBridgeRunning());
|
||||
assert_aliases(cli, true, false);
|
||||
mesh.serviceBridges();
|
||||
const unsigned after_first_retry = mesh.bridge.begins;
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.bridge.begins == after_first_retry && !mesh.bridgesPreventSleep());
|
||||
fake_millis += 5001;
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.bridge.begins == after_first_retry + 1 && mesh.isBridgeRunning());
|
||||
// Paused browser OTA must block direct enable, restart, and timed retries.
|
||||
assert(mesh.setEspNowBridgeState(false));
|
||||
mesh._cli.board.ota = true;
|
||||
const unsigned before_ota = mesh.bridge.begins;
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(strstr(reply, "setting saved") && !mesh.isEspNowBridgeRunning());
|
||||
cli.set("bridge.channel 3", reply);
|
||||
assert(strstr(reply, "setting saved") && !mesh.isBridgeRunning());
|
||||
for (unsigned tick = 0; tick < 10; ++tick) {
|
||||
fake_millis += 5001;
|
||||
mesh.serviceBridges();
|
||||
}
|
||||
assert(mesh.bridge.begins == before_ota && !mesh.bridgesPreventSleep());
|
||||
mesh._cli.board.ota = false;
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.bridge.begins == before_ota + 1 && mesh.isBridgeRunning());
|
||||
// Runtime switches through either spelling must not leave the radio running.
|
||||
for (unsigned cycle = 0; cycle < 100; ++cycle) {
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.bridgesPreventSleep());
|
||||
assert_aliases(cli, false, false);
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.bridgesPreventSleep());
|
||||
assert_aliases(cli, true, true);
|
||||
cli.set("espnow.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.bridgesPreventSleep());
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isBridgeRunning());
|
||||
}
|
||||
cli.set("espnow.enabled off", reply);
|
||||
const unsigned sent_when_off = mesh.bridge.sent;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge.sent == sent_when_off && !mesh.bridgesPreventSleep());
|
||||
puts("ESP-NOW runtime checks passed");
|
||||
}
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
// Production bridge methods are injected by test_espnow_radio_lifecycle.py.
|
||||
// Keep Arduino's private caches distinct from the underlying SDK driver.
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <initializer_list>
|
||||
#include <helpers/WirelessControl.h>
|
||||
#include <helpers/ESPNowRawFragmentation.h>
|
||||
#include <helpers/bridges/ESPNowBridgeFormat.h>
|
||||
|
||||
#define ESP_IDF_VERSION_VAL(a,b,c) ((a)*10000+(b)*100+(c))
|
||||
#define BRIDGE_DEBUG_PRINTLN(...) ((void)0)
|
||||
#define portENTER_CRITICAL(x) ((void)0)
|
||||
#define portEXIT_CRITICAL(x) ((void)0)
|
||||
#define ESP_NOW_ETH_ALEN 6
|
||||
#define WIFI_INIT_CONFIG_DEFAULT() 0
|
||||
using wifi_init_config_t = int;
|
||||
using wifi_second_chan_t = int;
|
||||
using esp_err_t = int;
|
||||
enum wifi_mode_t { WIFI_OFF=0, WIFI_STA=1, WIFI_AP=2, WIFI_AP_STA=3 };
|
||||
constexpr wifi_mode_t WIFI_MODE_NULL = WIFI_OFF, WIFI_MODE_STA = WIFI_STA;
|
||||
constexpr int ESP_OK=0, WIFI_STORAGE_RAM=0, WIFI_IF_STA=0, WIFI_SECOND_CHAN_NONE=0;
|
||||
constexpr int WIFI_PROTOCOL_11B=1, WIFI_PROTOCOL_11G=2, WIFI_PROTOCOL_11N=4, WIFI_PROTOCOL_LR=8;
|
||||
constexpr int WIFI_PHY_MODE_LR=1, WIFI_PHY_RATE_LORA_250K=2;
|
||||
enum esp_now_send_status_t { ESP_NOW_SEND_SUCCESS=0, ESP_NOW_SEND_FAIL=1 };
|
||||
struct esp_now_peer_info_t { uint8_t peer_addr[6]; int channel, ifidx; bool encrypt; };
|
||||
struct esp_now_rate_config_t { int phymode, rate; bool ersu, dcm; };
|
||||
|
||||
int failure=0, wifi_starts=0, wifi_stops=0, wifi_deinits=0, facade_initializations=0;
|
||||
bool sdk_initialized=false, sdk_started=false, espnow_active=false;
|
||||
uint8_t wifi_channel=6;
|
||||
wifi_mode_t sdk_mode=WIFI_OFF;
|
||||
int esp_wifi_init(wifi_init_config_t*) {
|
||||
if (sdk_initialized) return ESP_OK; // esp-idf init is idempotent.
|
||||
if (failure==11) return -1;
|
||||
sdk_initialized=true;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_set_storage(int) { return !sdk_initialized || failure==12 ? -1 : ESP_OK; }
|
||||
int esp_wifi_set_mode(wifi_mode_t mode) {
|
||||
if (!sdk_initialized || failure==13) return -1;
|
||||
sdk_mode=mode;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_get_mode(wifi_mode_t* mode) {
|
||||
if (!sdk_initialized) return -1;
|
||||
*mode=sdk_mode;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_start() {
|
||||
if (!sdk_initialized || failure==14) return -1;
|
||||
sdk_started=true;
|
||||
++wifi_starts;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_stop() {
|
||||
if (!sdk_initialized || failure==16) return -1;
|
||||
assert(!espnow_active);
|
||||
sdk_started=false;
|
||||
++wifi_stops;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_deinit() {
|
||||
if (!sdk_initialized || sdk_started || failure==17) return -1;
|
||||
assert(!espnow_active);
|
||||
sdk_initialized=false;
|
||||
sdk_mode=WIFI_OFF;
|
||||
++wifi_deinits;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_get_channel(uint8_t* channel, wifi_second_chan_t*) {
|
||||
if (!sdk_initialized) return -1;
|
||||
*channel=wifi_channel;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_set_channel(int channel, int) {
|
||||
if (!sdk_started || failure==15) return -1;
|
||||
wifi_channel=channel;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_wifi_set_protocol(int, int) { return !sdk_started || failure==2 ? -1 : ESP_OK; }
|
||||
int esp_now_init() {
|
||||
if (!sdk_started || failure==4) return -1;
|
||||
assert(!espnow_active);
|
||||
espnow_active=true;
|
||||
return ESP_OK;
|
||||
}
|
||||
int esp_now_deinit() { assert(espnow_active); espnow_active=false; return ESP_OK; }
|
||||
template<typename T> int esp_now_register_send_cb(T) { return failure==7 ? -1 : ESP_OK; }
|
||||
template<typename T> int esp_now_register_recv_cb(T) { return failure==8 ? -1 : ESP_OK; }
|
||||
int esp_now_add_peer(esp_now_peer_info_t* peer) {
|
||||
assert(peer->channel==6);
|
||||
return failure==9 ? -1 : ESP_OK;
|
||||
}
|
||||
int esp_now_del_peer(const uint8_t*) { return ESP_OK; }
|
||||
int esp_now_set_peer_rate_config(const uint8_t*, esp_now_rate_config_t*) { return failure==10 ? -1 : ESP_OK; }
|
||||
int esp_wifi_config_espnow_rate(int, int) { return failure==10 ? -1 : ESP_OK; }
|
||||
|
||||
struct Facade {
|
||||
bool initialized=false, started=false;
|
||||
wifi_mode_t getMode() const {
|
||||
// Pinned Arduino 2.x and 3.x both gate this getter on both caches.
|
||||
if (!initialized || !started) return WIFI_OFF;
|
||||
wifi_mode_t mode=WIFI_OFF;
|
||||
return esp_wifi_get_mode(&mode)==ESP_OK ? mode : WIFI_OFF;
|
||||
}
|
||||
int channel() const {
|
||||
// channel() only checks initialized, not started, in both pinned cores.
|
||||
if (!initialized) return 0;
|
||||
uint8_t channel=0;
|
||||
wifi_second_chan_t secondary=0;
|
||||
return esp_wifi_get_channel(&channel, &secondary)==ESP_OK ? channel : 0;
|
||||
}
|
||||
bool mode(wifi_mode_t requested) {
|
||||
const wifi_mode_t current=getMode();
|
||||
if (current==requested) return true;
|
||||
if (!current && requested && !initialized) {
|
||||
wifi_init_config_t config=0;
|
||||
if (esp_wifi_init(&config)!=ESP_OK) return false;
|
||||
initialized=true;
|
||||
++facade_initializations;
|
||||
if (esp_wifi_set_storage(WIFI_STORAGE_RAM)!=ESP_OK) {
|
||||
initialized=false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (current && !requested) {
|
||||
if (!started) return true;
|
||||
started=false;
|
||||
if (esp_wifi_stop()!=ESP_OK) { started=true; return false; }
|
||||
if (initialized && esp_wifi_deinit()!=ESP_OK) return false;
|
||||
initialized=false;
|
||||
return true;
|
||||
}
|
||||
if (esp_wifi_set_mode(requested)!=ESP_OK) return false;
|
||||
if (started) return true;
|
||||
started=true;
|
||||
if (esp_wifi_start()!=ESP_OK) { started=false; return false; }
|
||||
return true;
|
||||
}
|
||||
} WiFi;
|
||||
|
||||
namespace mesh { namespace wifi {
|
||||
std::atomic<uint8_t>& bridgeEspNowChannel() { static std::atomic<uint8_t> channel{0}; return channel; }
|
||||
} }
|
||||
struct Backend : mesh::wireless::Backend {
|
||||
bool wifi=false;
|
||||
uint8_t available() const override { return mesh::wireless::WiFi|mesh::wireless::EspNow; }
|
||||
uint8_t enabled() const override { return wifi ? mesh::wireless::WiFi : 0; }
|
||||
uint8_t clients() const override { return mesh::wireless::Independent; }
|
||||
mesh::wireless::Result set(uint8_t, bool) override { return mesh::wireless::Result::Done; }
|
||||
};
|
||||
class Bridge {
|
||||
public:
|
||||
struct Prefs { int bridge_format=mesh::bridge::ESPNOW_FORMAT_RAW; uint8_t bridge_channel=6; } prefs;
|
||||
Prefs* _prefs=&prefs;
|
||||
bool _initialized=false;
|
||||
int _active_format=0, _rx_mux=0, _rx_head=0, _rx_tail=0, _rx_count=0, _rx_dropped=0, _rx_dropped_reported=0;
|
||||
int _tx_mux=0, _tx_head=0, _tx_tail=0, _tx_count=0, _tx_waiting=0, _tx_callback_done=0;
|
||||
int _tx_callback_status=0, _tx_dropped=0, _tx_dropped_reported=0;
|
||||
mesh::espnow::ESPNowRawReassembler _raw_reassembler;
|
||||
struct QueuedTransmit { bool started=false; int packet=0; };
|
||||
static constexpr int TX_QUEUE_DEPTH=2;
|
||||
QueuedTransmit _tx_queue[TX_QUEUE_DEPTH];
|
||||
struct { void clear(const int*) {} } _seen_packets;
|
||||
static void recv_cb() {}
|
||||
static void send_cb() {}
|
||||
void begin();
|
||||
void end();
|
||||
};
|
||||
|
||||
@METHODS@
|
||||
|
||||
void coherent() {
|
||||
// SDK ownership may exist without Arduino, but never the reverse.
|
||||
assert(!WiFi.initialized || sdk_initialized);
|
||||
assert(!WiFi.started || (sdk_initialized && sdk_started));
|
||||
}
|
||||
void off() {
|
||||
coherent();
|
||||
assert(!sdk_initialized && !sdk_started && !espnow_active);
|
||||
assert(!WiFi.initialized && !WiFi.started);
|
||||
assert(mesh::wifi::bridgeEspNowChannel().load()==0);
|
||||
}
|
||||
int main() {
|
||||
Backend backend;
|
||||
mesh::wireless::control().begin(backend);
|
||||
Bridge bridge;
|
||||
// Stopping a bridge that only used IDF must not allocate the Arduino facade.
|
||||
for (int cycle=0; cycle<100; ++cycle) {
|
||||
bridge.begin();
|
||||
assert(bridge._initialized && sdk_started && !WiFi.initialized);
|
||||
bridge.end();
|
||||
bridge.end();
|
||||
off();
|
||||
assert(facade_initializations==0);
|
||||
}
|
||||
// Every driver startup failure releases pure-IDF resources without a facade.
|
||||
for (int stage : {11,12,13,14,15,2,4,7,8,9,10}) {
|
||||
failure=stage;
|
||||
bridge.begin();
|
||||
assert(!bridge._initialized);
|
||||
bridge.end();
|
||||
off();
|
||||
assert(facade_initializations==0);
|
||||
}
|
||||
failure=0;
|
||||
for (int cycle=0; cycle<100; ++cycle) {
|
||||
// ESP-NOW -> WebConfig -> stop WebConfig -> stop ESP-NOW -> WebConfig.
|
||||
bridge.begin();
|
||||
assert(bridge._initialized);
|
||||
assert(WiFi.mode(WIFI_AP_STA));
|
||||
backend.wifi=true;
|
||||
coherent();
|
||||
assert(WiFi.mode(WIFI_STA)); // WebConfig keeps STA for the running bridge.
|
||||
backend.wifi=false;
|
||||
bridge.end();
|
||||
off();
|
||||
assert(WiFi.mode(WIFI_AP_STA)); // Must initialize again, rather than use stale caches.
|
||||
coherent();
|
||||
assert(WiFi.mode(WIFI_OFF));
|
||||
off();
|
||||
|
||||
// Browser OTA pauses ESP-NOW, initializes Arduino, then leaves STA alive.
|
||||
bridge.begin();
|
||||
bridge.end();
|
||||
off();
|
||||
assert(WiFi.mode(WIFI_AP_STA));
|
||||
backend.wifi=true;
|
||||
coherent();
|
||||
assert(WiFi.mode(WIFI_STA));
|
||||
backend.wifi=false;
|
||||
bridge.begin();
|
||||
assert(bridge._initialized && WiFi.started);
|
||||
bridge.end();
|
||||
off();
|
||||
}
|
||||
// An active infrastructure owner survives all bridge failures and stops.
|
||||
assert(WiFi.mode(WIFI_AP_STA));
|
||||
backend.wifi=true;
|
||||
const int stops=wifi_stops, deinits=wifi_deinits;
|
||||
for (int stage : {2,4,7,8,9,10}) {
|
||||
failure=stage;
|
||||
bridge.begin();
|
||||
assert(!bridge._initialized && !espnow_active);
|
||||
bridge.end();
|
||||
coherent();
|
||||
assert(sdk_started && WiFi.started && WiFi.getMode()==WIFI_AP_STA);
|
||||
assert(wifi_stops==stops && wifi_deinits==deinits);
|
||||
}
|
||||
failure=0;
|
||||
bridge.begin();
|
||||
bridge.end();
|
||||
assert(wifi_stops==stops && wifi_deinits==deinits);
|
||||
backend.wifi=false;
|
||||
assert(WiFi.mode(WIFI_OFF));
|
||||
off();
|
||||
|
||||
// A failed Arduino start retains initialized=true and started=false.
|
||||
// Driver teardown must repair this partial cache, including its own failures.
|
||||
for (int stage : {0,12,13,14,15,2,4,7,8,9,10}) {
|
||||
failure=14;
|
||||
assert(!WiFi.mode(WIFI_STA));
|
||||
assert(WiFi.initialized && !WiFi.started && sdk_initialized);
|
||||
assert(WiFi.getMode()==WIFI_OFF && WiFi.channel()==6);
|
||||
failure=stage;
|
||||
bridge.begin();
|
||||
if (!stage) assert(bridge._initialized);
|
||||
else assert(!bridge._initialized);
|
||||
bridge.end();
|
||||
coherent(); // A failed repair must not destroy the SDK under a cached facade.
|
||||
failure=0;
|
||||
stopBridgeWiFiIfUnused();
|
||||
off();
|
||||
assert(WiFi.mode(WIFI_AP_STA));
|
||||
coherent();
|
||||
assert(WiFi.mode(WIFI_OFF));
|
||||
off();
|
||||
}
|
||||
// SDK shutdown errors also leave facade caches coherent for a safe retry.
|
||||
for (int stage : {16,17}) {
|
||||
assert(WiFi.mode(WIFI_STA));
|
||||
bridge.begin();
|
||||
assert(bridge._initialized);
|
||||
failure=stage;
|
||||
bridge.end();
|
||||
assert(!espnow_active);
|
||||
coherent();
|
||||
failure=0;
|
||||
stopBridgeWiFiIfUnused();
|
||||
off();
|
||||
}
|
||||
}
|
||||
Vendored
+13
-1
@@ -66,7 +66,10 @@ int main() {
|
||||
FakeFS fs{json};
|
||||
assert(cli.import(&fs) && cli.saves == 1);
|
||||
assert(strcmp(cli.prefs.node_name, "imported node") == 0);
|
||||
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
#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(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
const bool explicit_uart = strstr(json, "uart:1") || strstr(json, "uart:2");
|
||||
const bool enabled = explicit_uart && strstr(json, "en:1");
|
||||
assert(cli.prefs.bridge_enabled == enabled && cli.prefs.bridge_uart == 2);
|
||||
@@ -77,6 +80,8 @@ int main() {
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
assert(cli.prefs.espnow_bridge_enabled == 0);
|
||||
#elif defined(ESPNOW_BRIDGE_MERGED) && !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
assert(cli.prefs.espnow_bridge_enabled == cli.prefs.bridge_enabled);
|
||||
#else
|
||||
assert(cli.prefs.espnow_bridge_enabled == 1);
|
||||
#endif
|
||||
@@ -85,9 +90,16 @@ int main() {
|
||||
}
|
||||
// Malformed JSON may partially apply scalar fields before the parser rejects it.
|
||||
CLI broken;
|
||||
broken.prefs.espnow_bridge_enabled = 1;
|
||||
FakeFS malformed{"{bridge:{en:1,uart:1}"};
|
||||
assert(!broken.import(&malformed) && broken.saves == 0);
|
||||
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
assert(broken.prefs.bridge_enabled == 0 && broken.prefs.bridge_uart == 2);
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
assert(broken.prefs.espnow_bridge_enabled == 1);
|
||||
#if !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
assert(broken.prefs.bridge_enabled == 0);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
+19
-3
@@ -46,7 +46,13 @@ int main() {
|
||||
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);
|
||||
#else
|
||||
assert(reader.prefs.bridge_enabled == primary);
|
||||
#endif
|
||||
assert(reader.prefs.espnow_bridge_enabled == secondary);
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
assert(reader.prefs.bridge_uart == 2);
|
||||
@@ -85,7 +91,7 @@ int main() {
|
||||
assert(legacy_reader.prefs.bridge_enabled == 1 && legacy_reader.prefs.bridge_uart == 2);
|
||||
#endif
|
||||
|
||||
#ifdef ESPNOW_BRIDGE_MERGED
|
||||
#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;
|
||||
@@ -94,7 +100,12 @@ int main() {
|
||||
reader.fs.files["/com_prefs"] = old;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.espnow_bridge_enabled == 0);
|
||||
assert(reader.prefs.bridge_enabled == 1 && reader._com_prefs_needs_upgrade);
|
||||
#if !defined(WITH_RS232_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
assert(reader.prefs.bridge_enabled == 0);
|
||||
#else
|
||||
assert(reader.prefs.bridge_enabled == 1);
|
||||
#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;
|
||||
@@ -102,7 +113,12 @@ int main() {
|
||||
CommonCLI reader;
|
||||
reader.fs.files["/com_prefs"] = old;
|
||||
reader.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
assert(reader.prefs.espnow_bridge_enabled == 0 && reader.prefs.bridge_enabled == 1);
|
||||
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.loadPrefsInt(&reader.fs, "/com_prefs");
|
||||
|
||||
@@ -966,24 +966,95 @@ apply_esp32_full_shared_bridge_profile Station_G2_repeater_observer_mqtt
|
||||
|| fail "Station G2 Full MQTT image did not include the ESP-NOW bridge"
|
||||
[[ " ${BUILD_CAPABILITIES[*]} " == *" bridge.espnow "* ]] \
|
||||
|| fail "Station G2 Full MQTT image did not report ESP-NOW capability"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" != *"ESPNOW_BRIDGE_MERGED"* ]] \
|
||||
|| fail "Station G2 existing MQTT default changed during ESP-NOW composition"
|
||||
[[ "$(get_unified_full_infrastructure_target Station_G2_repeater_bridge_espnow)" \
|
||||
= "Station_G2_repeater_observer_mqtt" ]] \
|
||||
|| fail "Station G2 Full ESP-NOW target did not resolve to the combined MQTT recipe"
|
||||
|
||||
# Heltec V4 TFT has no combined observer recipe, so its dedicated ESP-NOW
|
||||
# image must survive release selection in either input order. MKE now owns
|
||||
# both bridges directly and is checked against its canonical alias below.
|
||||
for plain_target in heltec_v4_tft_repeater; do
|
||||
# Full repeaters without an observer compile the real bridge directly. Both
|
||||
# historical ESP-NOW defaults and unsupported role boundaries stay explicit.
|
||||
for plain_target in heltec_v4_tft_repeater Heltec_v2_repeater \
|
||||
Heltec_ct62_repeater ThinkNode_M2_Repeater ThinkNode_M5_Repeater \
|
||||
Meshadventurer_sx1262_repeater Meshadventurer_sx1268_repeater \
|
||||
Generic_E22_sx1262_repeater Generic_E22_sx1268_repeater \
|
||||
Tenstar_C3_sx1262_repeater Tenstar_C3_sx1268_repeater \
|
||||
nibble_zero_connect_repeater_ nibble_screen_connect_repeater_ \
|
||||
Station_G2_logging_repeater; do
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
BUILD_CAPABILITIES=()
|
||||
apply_esp32_full_shared_bridge_profile "$plain_target"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DWITH_ESPNOW_BRIDGE=1"* ]] \
|
||||
|| fail "$plain_target Full image omitted the runtime ESP-NOW bridge"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DESPNOW_BRIDGE_MERGED=1"* ]] \
|
||||
|| fail "$plain_target Full image omitted safe default-off migration"
|
||||
[[ "$PLATFORMIO_BUILD_SRC_FILTER" == *"helpers/bridges/ESPNowBridge.cpp"* ]] \
|
||||
|| fail "$plain_target Full image omitted the actual ESP-NOW driver source"
|
||||
[[ " ${BUILD_CAPABILITIES[*]} " == *" bridge.espnow "* ]] \
|
||||
|| fail "$plain_target Full image omitted the ESP-NOW capability"
|
||||
(
|
||||
BUILD_PROFILE_FOR_TARGET=full
|
||||
BUILD_EXPECTATIONS=()
|
||||
declare_build_capability_contract "$plain_target" ESP32_PLATFORM
|
||||
[[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.espnow=_ZN12ESPNowBridge5beginEv"* ]] \
|
||||
|| fail "$plain_target Full image does not require a linked ESP-NOW driver"
|
||||
)
|
||||
done
|
||||
for existing_target in Heltec_v2_repeater_bridge_espnow \
|
||||
Station_G2_repeater_observer_mqtt; do
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
apply_esp32_full_shared_bridge_profile "$existing_target"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" == *"-DWITH_ESPNOW_BRIDGE=1"* ]] \
|
||||
|| fail "$existing_target existing bridge source was dropped"
|
||||
[[ "$PLATFORMIO_BUILD_FLAGS" != *"ESPNOW_BRIDGE_MERGED"* ]] \
|
||||
|| fail "$existing_target lost historical bridge-on behavior"
|
||||
done
|
||||
for excluded_target in heltec_v4_sensor heltec_v4_tft_room_server \
|
||||
heltec_v4_tft_companion_radio_full RAK_4631_repeater; do
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
apply_esp32_full_shared_bridge_profile "$excluded_target"
|
||||
[ -z "$PLATFORMIO_BUILD_FLAGS$PLATFORMIO_BUILD_SRC_FILTER" ] \
|
||||
|| fail "$excluded_target acquired unsupported ESP32 repeater bridge wiring"
|
||||
done
|
||||
(
|
||||
ESP32_FULL_BUILD=0
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
apply_esp32_full_shared_bridge_profile heltec_v4_tft_repeater
|
||||
[ -z "$PLATFORMIO_BUILD_FLAGS$PLATFORMIO_BUILD_SRC_FILTER" ] \
|
||||
|| fail "standard repeater acquired the expanded Full ESP-NOW overlay"
|
||||
)
|
||||
for primary_flag in MESH_PRIMARY_ESPNOW MESH_ESPNOW_RADIO; do
|
||||
(
|
||||
pio_env_option_contains() { [ "$3" = "$primary_flag" ]; }
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
if supports_esp32_full_shared_espnow heltec_v4_tft_repeater; then
|
||||
fail "$primary_flag radio acquired a second conflicting SDK owner"
|
||||
fi
|
||||
apply_esp32_full_shared_bridge_profile heltec_v4_tft_repeater
|
||||
[ -z "$PLATFORMIO_BUILD_FLAGS$PLATFORMIO_BUILD_SRC_FILTER" ] \
|
||||
|| fail "$primary_flag primary radio acquired the secondary bridge overlay"
|
||||
)
|
||||
done
|
||||
|
||||
# The TFT's normal Full image now owns ESP-NOW without an observer recipe.
|
||||
# Canonical selection must replace the sibling in either input order.
|
||||
for plain_target in heltec_v4_tft_repeater Meshadventurer_sx1262_repeater; do
|
||||
espnow_target=${plain_target}_bridge_espnow
|
||||
if get_unified_full_infrastructure_target "$espnow_target" >/dev/null; then
|
||||
fail "$espnow_target unexpectedly acquired a combined observer recipe"
|
||||
fi
|
||||
[ "$(select_ordinary_full_targets "$plain_target" "$espnow_target")" \
|
||||
= "$(printf "%s\n" "$plain_target" "$espnow_target")" ] \
|
||||
|| fail "$plain_target incorrectly replaced its dedicated ESP-NOW image"
|
||||
= "$plain_target" ] \
|
||||
|| fail "$plain_target did not replace its dedicated ESP-NOW image"
|
||||
[ "$(select_ordinary_full_targets "$espnow_target" "$plain_target")" \
|
||||
= "$(printf "%s\n" "$espnow_target" "$plain_target")" ] \
|
||||
|| fail "$plain_target lost its dedicated ESP-NOW image with reversed inputs"
|
||||
= "$plain_target" ] \
|
||||
|| fail "$plain_target input order changed the combined ESP-NOW selection"
|
||||
[ "$(get_merged_espnow_repeater_replacement "$espnow_target")" = "$plain_target" ] \
|
||||
|| fail "$plain_target ESP-NOW legacy target did not map to its combined Full image"
|
||||
done
|
||||
[ "$(get_unified_full_infrastructure_target Heltec_v3_repeater_bridge_espnow)" \
|
||||
= Heltec_v3_repeater_observer_mqtt ] \
|
||||
|
||||
@@ -34,6 +34,7 @@ struct FakeWiFi {
|
||||
wifi_mode_t current=WIFI_OFF;bool autoreconnect=true;
|
||||
void persistent(bool){}
|
||||
wifi_mode_t getMode(){return current;}
|
||||
int channel(){return current==WIFI_OFF?0:6;}
|
||||
void setAutoReconnect(bool value){autoreconnect=value;}
|
||||
bool mode(wifi_mode_t value){if(failure==1)return false;current=value;return true;}
|
||||
} WiFi;
|
||||
@@ -167,14 +168,16 @@ class EspNowLifecycleTests(unittest.TestCase):
|
||||
using wifi_init_config_t=int;
|
||||
using wifi_second_chan_t=int;
|
||||
constexpr int WIFI_STORAGE_RAM=0,WIFI_MODE_STA=1,WIFI_SECOND_CHAN_NONE=0;
|
||||
constexpr wifi_mode_t WIFI_MODE_NULL=WIFI_OFF;
|
||||
constexpr int WIFI_PROTOCOL_11B=1,WIFI_PROTOCOL_11G=2,WIFI_PROTOCOL_11N=4;
|
||||
int wifi_stops=0,wifi_starts=0;uint8_t wifi_channel=6;
|
||||
int esp_wifi_init(wifi_init_config_t*){return 0;}
|
||||
int wifi_stops=0,wifi_starts=0;uint8_t wifi_channel=6;bool wifi_initialized=false;
|
||||
int esp_wifi_init(wifi_init_config_t*){wifi_initialized=true;return 0;}
|
||||
int esp_wifi_set_storage(int){return 0;}
|
||||
int esp_wifi_set_mode(int){return 0;}
|
||||
int esp_wifi_start(){++wifi_starts;return 0;}
|
||||
int esp_wifi_stop(){++wifi_stops;return 0;}
|
||||
int esp_wifi_deinit(){return 0;}
|
||||
int esp_wifi_deinit(){wifi_initialized=false;return 0;}
|
||||
int esp_wifi_get_mode(wifi_mode_t* mode){*mode=WIFI_STA;return wifi_initialized?0:-1;}
|
||||
int esp_wifi_get_channel(uint8_t* channel,wifi_second_chan_t*){*channel=wifi_channel;return 0;}
|
||||
int esp_wifi_set_channel(int channel,int){wifi_channel=channel;return 0;}
|
||||
int esp_now_del_peer(const uint8_t*){return 0;}
|
||||
@@ -294,5 +297,28 @@ struct {
|
||||
'-I',str(folder),'-I',str(ROOT/'src'),str(folder/'channel.cpp'),'-o',str(binary)],check=True)
|
||||
subprocess.run([str(binary)],check=True)
|
||||
|
||||
def test_bridge_arduino_sdk_handoffs_and_partial_startup(self):
|
||||
source = (ROOT/'src/helpers/bridges/ESPNowBridge.cpp').read_text()
|
||||
methods = method(source, 'static void stopBridgeWiFiIfUnused(') + '\n'
|
||||
methods += '\n'.join(
|
||||
method(source, f'void ESPNowBridge::{name}(').replace('ESPNowBridge::', 'Bridge::')
|
||||
for name in ('begin', 'end')
|
||||
)
|
||||
fixture = (ROOT/'test/fixtures/espnow_wifi_handoff.cpp').read_text()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
folder = Path(tmp)
|
||||
(folder/'handoff.cpp').write_text(fixture.replace('@METHODS@', methods))
|
||||
for arduino, idf in [(2,40400), (3,50200), (3,50500)]:
|
||||
with self.subTest(arduino=arduino, idf=idf):
|
||||
binary = folder/f'handoff-{idf}'
|
||||
subprocess.run([
|
||||
os.environ.get('CXX', 'g++'), '-std=c++17',
|
||||
'-fsanitize=address,undefined', '-fno-omit-frame-pointer', '-fno-pie', '-no-pie',
|
||||
'-I', str(ROOT/'src'), f'-DESP_IDF_VERSION={idf}',
|
||||
f'-DESP_ARDUINO_VERSION_MAJOR={arduino}',
|
||||
str(folder/'handoff.cpp'), '-o', str(binary),
|
||||
], check=True)
|
||||
subprocess.run([str(binary)], check=True)
|
||||
|
||||
if __name__=='__main__':
|
||||
unittest.main()
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the sole repeater ESP-NOW production lifecycle and CLI with fault injection."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import re
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ESPNowRuntimeTests(unittest.TestCase):
|
||||
def test_production_defaults_aliases_routing_retries_and_ota_exclusion(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")
|
||||
callback_header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
|
||||
implementation = (ROOT / "examples/simple_repeater/MyMesh.cpp").read_text(encoding="ascii")
|
||||
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)
|
||||
lifecycle_signatures = (
|
||||
"AbstractBridge* activeBridge() {", "const AbstractBridge* activeBridge() const {",
|
||||
"bool startSharedEspNowBridgeIfReady()", "bool isBridgeRunning() const override",
|
||||
"bool setBridgeState(bool enable) override",
|
||||
"bool restartBridge() override",
|
||||
)
|
||||
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 = "\n".join(extract_braced(cli, signature) for signature in (
|
||||
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd("))
|
||||
template = (ROOT / "test/fixtures/espnow_runtime.cpp").read_text(encoding="ascii")
|
||||
for key, value in {
|
||||
"DEFAULTS": implementation[defaults_start:defaults_end],
|
||||
"BOOT": implementation[boot_start:boot_end],
|
||||
"CALLBACK_FORWARDS": "\n".join(extract_braced(callback_header, signature) for signature in (
|
||||
"virtual bool setEspNowBridgeState(bool enable)",
|
||||
"virtual bool restartEspNowBridge()", "virtual bool isEspNowBridgeRunning()")),
|
||||
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
|
||||
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
|
||||
"ROUTE_RX": routing["logRx"], "ROUTE_TX": routing["logTx"],
|
||||
"SERVICE": implementation[service_start:service_end],
|
||||
"PENDING": implementation[pending_start:pending_end],
|
||||
}.items():
|
||||
template = template.replace("@" + key + "@", value)
|
||||
for profile, macros in (
|
||||
("merged", ["ESPNOW_BRIDGE_MERGED=1"]),
|
||||
("legacy_dedicated", []),
|
||||
("merged_default_on", ["ESPNOW_BRIDGE_MERGED=1", "ESPNOW_BRIDGE_DEFAULT_ON=1"]),
|
||||
):
|
||||
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="espnow-runtime-") as directory:
|
||||
work = Path(directory)
|
||||
flags = ["-DWITH_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1",
|
||||
*["-D" + macro for macro in macros]]
|
||||
processed = subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
|
||||
input=commands, capture_output=True, text=True, check=True).stdout
|
||||
processed_header = subprocess.run([compiler, "-E", "-P", "-x", "c++", *flags, "-"],
|
||||
input=re.sub(r'^\s*#include[^\n]*', '', header, flags=re.M),
|
||||
capture_output=True, text=True, check=True).stdout
|
||||
lifecycle = "\n".join(extract_braced(processed_header, signature)
|
||||
for signature in lifecycle_signatures)
|
||||
sets = "\nelse ".join(extract_braced(processed, signature) for signature in (
|
||||
'if (memcmp(config, "espnow.enabled ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.enabled ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.source ", 14) == 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(processed, signature) for signature in (
|
||||
'if (configKeyEquals(config, "bridge.type"))',
|
||||
'if (configKeyEquals(config, "bridge.enabled"))',
|
||||
'if (configKeyEquals(config, "bridge.running"))',
|
||||
'if (configKeyEquals(config, "espnow.enabled"))',
|
||||
'if (configKeyEquals(config, "espnow.running"))',
|
||||
'if (configKeyEquals(config, "bridge.channel"))',
|
||||
'if (configKeyEquals(config, "bridge.secret"))',
|
||||
'if (configKeyEquals(config, "bridge.format"))',
|
||||
))
|
||||
source, binary = work / "main.cpp", work / "test"
|
||||
source.write_text(template.replace("@SET_BRANCHES@", sets)
|
||||
.replace("@GET_BRANCHES@", gets)
|
||||
.replace("@LIFECYCLE@", lifecycle), 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("ESP-NOW runtime checks passed", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -159,10 +159,65 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
|
||||
def test_qualified_combined_plain_image_replaces_only_its_matching_espnow_image(self):
|
||||
primary = record("Heltec_v3_repeater", capabilities=("bridge.espnow",))
|
||||
legacy = record("Heltec_v3_repeater_bridge_espnow", capabilities=("bridge.espnow",))
|
||||
other_board = record("heltec_v4_tft_repeater_bridge_espnow", capabilities=("bridge.espnow",))
|
||||
for inputs in ([primary, legacy, other_board], [legacy, primary, other_board]):
|
||||
self.assertEqual(select_ordinary_full_records(inputs), [primary, other_board])
|
||||
for profile in ("full", "standard"):
|
||||
legacy = record("Heltec_v3_repeater_bridge_espnow", profile, capabilities=("bridge.espnow",))
|
||||
for inputs in ([primary, legacy, other_board], [legacy, primary, other_board]):
|
||||
with self.subTest(profile=profile, primary_first=inputs[0] is primary):
|
||||
self.assertEqual(select_ordinary_full_records(inputs), [primary, other_board])
|
||||
|
||||
def test_matching_dedicated_images_survive_all_missing_driver_proof_cases(self):
|
||||
for profile in ("full", "standard"):
|
||||
for fault in ("missing", "false", "wrong_capability", "false_verified"):
|
||||
primary = record("heltec_v4_tft_repeater", capabilities=("bridge.espnow",))
|
||||
if fault == "missing":
|
||||
primary["manifest"]["verification"] = []
|
||||
elif fault == "false":
|
||||
primary["manifest"]["verification"][0]["present"] = False
|
||||
elif fault == "wrong_capability":
|
||||
primary["manifest"]["verification"][0]["capability"] = "bridge.rs232"
|
||||
else:
|
||||
primary["manifest"]["verified"] = False
|
||||
legacy = record("heltec_v4_tft_repeater_bridge_espnow", profile,
|
||||
capabilities=("bridge.espnow",))
|
||||
for inputs in ([primary, legacy], [legacy, primary]):
|
||||
with self.subTest(profile=profile, fault=fault, primary_first=inputs[0] is primary):
|
||||
self.assertCountEqual(select_ordinary_full_records(inputs), inputs)
|
||||
|
||||
def test_stale_plain_cannot_displace_the_observer_proving_the_combined_driver(self):
|
||||
for profile in ("full", "standard"):
|
||||
for fault in ("no_capability", "no_proof", "failed_proof", "unverified"):
|
||||
stale = record("heltec_v4_tft_repeater", capabilities=("bridge.espnow",))
|
||||
if fault == "no_capability":
|
||||
stale["manifest"]["capabilities"] = []
|
||||
elif fault == "no_proof":
|
||||
stale["manifest"]["verification"] = []
|
||||
elif fault == "failed_proof":
|
||||
stale["manifest"]["verification"][0]["present"] = False
|
||||
else:
|
||||
stale["manifest"]["verified"] = False
|
||||
observer = record("heltec_v4_tft_repeater_observer_mqtt",
|
||||
capabilities=("bridge.espnow",))
|
||||
legacy = record("heltec_v4_tft_repeater_bridge_espnow", profile,
|
||||
capabilities=("bridge.espnow",))
|
||||
for inputs in ([stale, observer, legacy], [legacy, observer, stale],
|
||||
[observer, legacy, stale], [legacy, stale, observer]):
|
||||
with self.subTest(profile=profile, fault=fault, first=inputs[0]["manifest"]["target"]):
|
||||
self.assertEqual(select_ordinary_full_records(inputs), [observer])
|
||||
|
||||
def test_new_non_mqtt_full_repeater_replaces_case_and_underscore_legacy_names(self):
|
||||
for primary_name, legacy_name in (
|
||||
("heltec_v4_tft_repeater", "heltec_v4_tft_repeater_bridge_espnow"),
|
||||
("ThinkNode_M2_repeater", "ThinkNode_M2_Repeater_bridge_espnow"),
|
||||
("nibble_zero_connect_repeater_", "nibble_zero_connect_repeater_bridge_espnow_"),
|
||||
("Meshadventurer_sx1262_repeater", "Meshadventurer_sx1262_repeater_bridge_espnow"),
|
||||
):
|
||||
primary = record(primary_name, capabilities=("bridge.espnow",))
|
||||
for profile in ("full", "standard"):
|
||||
legacy = record(legacy_name, profile, capabilities=("bridge.espnow",))
|
||||
for inputs in ([primary, legacy], [legacy, primary]):
|
||||
with self.subTest(primary=primary_name, profile=profile):
|
||||
self.assertEqual(select_ordinary_full_records(inputs), [primary])
|
||||
|
||||
def test_combined_observer_with_trailing_underscore_replaces_matching_espnow(self):
|
||||
observer = record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_",
|
||||
|
||||
@@ -27,10 +27,18 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
self.assertIn('def("bridge", bridge);', header)
|
||||
code = code.replace("@IMPORT@", extract_braced(source, 'if (fs->exists("/prefs.json"))'))
|
||||
for profile, macros in (
|
||||
("legacy_dedicated", []),
|
||||
("merged_default_on", ["RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
|
||||
("merged", ["RS232_BRIDGE_MERGED=1"]),
|
||||
("merged_composite", ["RS232_BRIDGE_MERGED=1", "ESPNOW_BRIDGE_MERGED=1"]),
|
||||
("legacy_dedicated", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2"]),
|
||||
("merged_default_on", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
|
||||
("merged", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1"]),
|
||||
("merged_composite", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
|
||||
"ESPNOW_BRIDGE_MERGED=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",
|
||||
"ESPNOW_BRIDGE_DEFAULT_ON=1"]),
|
||||
):
|
||||
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="rs232-json-") as directory:
|
||||
work = Path(directory)
|
||||
@@ -42,7 +50,7 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
source_file, binary = work / "main.cpp", work / "test"
|
||||
source_file.write_text(code, encoding="ascii")
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
|
||||
compiler, "-std=c++17",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*["-D" + macro for macro in macros], "-I" + str(work),
|
||||
"-I" + str(ROOT / "test/mocks"), "-I" + str(ROOT / "src"),
|
||||
@@ -68,6 +76,10 @@ class RS232RuntimeTests(unittest.TestCase):
|
||||
("rs232_espnow_merged", ["WITH_RS232_BRIDGE=Serial2", "WITH_RS232_BRIDGE_UART=2",
|
||||
"RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
|
||||
"ESPNOW_BRIDGE_MERGED=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",
|
||||
"ESPNOW_BRIDGE_DEFAULT_ON=1"]),
|
||||
):
|
||||
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="rs232-prefs-") as directory:
|
||||
work = Path(directory)
|
||||
|
||||
+30
-14
@@ -178,7 +178,7 @@ int main() {
|
||||
fixture = (ROOT / "test/fixtures/webconfig_ota_handoff.cpp").read_text()
|
||||
self.compile_and_run(fixture.replace("@METHODS@", methods))
|
||||
|
||||
def test_rs232_espnow_ota_pauses_only_wireless_bridge_and_restores_failures(self):
|
||||
def test_non_mqtt_espnow_ota_pauses_wireless_and_restores_saved_intent(self):
|
||||
cli = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
|
||||
start = cli.index(' } else if (memcmp(command, "start ota", 9)')
|
||||
end = cli.index(' } else if (memcmp(command, "clock", 5)', start)
|
||||
@@ -271,6 +271,9 @@ int main() {
|
||||
assert(strstr(reply.text, "; ESP-NOW resume failed"));
|
||||
// A disabled secondary must stay disabled after the upload stops.
|
||||
prefs.espnow_bridge_enabled = false;
|
||||
#if !defined(WITH_RS232_BRIDGE)
|
||||
prefs.bridge_enabled = false;
|
||||
#endif
|
||||
callbacks.allow_resume = true;
|
||||
board.allow_start = true;
|
||||
const int resumes = callbacks.resumes;
|
||||
@@ -281,9 +284,14 @@ int main() {
|
||||
assert(callbacks.uart_changes == 0 && callbacks.uart && reply.guard == 42);
|
||||
}
|
||||
'''
|
||||
self.compile_and_run(fixture.replace("@CLI@", cli[start:end]),
|
||||
"-DESP_PLATFORM=1", "-DADMIN_PASSWORD=1",
|
||||
"-DWITH_RS232_BRIDGE=1", "-DWITH_ESPNOW_BRIDGE=1")
|
||||
for profile, flags in (
|
||||
("rs232_composite", ["-DWITH_RS232_BRIDGE=1"]),
|
||||
("merged_sole", ["-DESPNOW_BRIDGE_MERGED=1"]),
|
||||
("legacy_dedicated_sole", []),
|
||||
):
|
||||
with self.subTest(profile=profile):
|
||||
self.compile_and_run(fixture.replace("@CLI@", cli[start:end]),
|
||||
"-DESP_PLATFORM=1", "-DADMIN_PASSWORD=1", "-DWITH_ESPNOW_BRIDGE=1", *flags)
|
||||
|
||||
def test_background_bridge_retry_waits_until_ota_stops(self):
|
||||
fixture = r'''
|
||||
@@ -303,11 +311,13 @@ struct Bridge {
|
||||
bool isRunning() const { return running; }
|
||||
void begin() { ++starts; running = true; }
|
||||
};
|
||||
using ESPNowBridge = Bridge;
|
||||
struct WiFiMock { bool isConnected() { return true; } } WiFi;
|
||||
struct Mesh {
|
||||
Cli _cli;
|
||||
struct { bool espnow_bridge_enabled = true; } _prefs;
|
||||
struct { bool bridge_enabled = true, espnow_bridge_enabled = true; } _prefs;
|
||||
Bridge espnow_bridge;
|
||||
@SOLE_MEMBER@
|
||||
Bridge* @MQTT_MEMBER@ = nullptr;
|
||||
uint32_t shared_espnow_retry_at = 0;
|
||||
void configureBridgeFilter(Bridge*) {}
|
||||
@@ -318,27 +328,33 @@ int main() {
|
||||
// Simulate repeated servicePostMeshLoop calls, spanning several retry windows.
|
||||
for (now = 0; now <= 30000; now += 100) {
|
||||
assert(!mesh.startSharedEspNowBridgeIfReady());
|
||||
assert(mesh.espnow_bridge.starts == 0);
|
||||
assert(mesh.@TRANSPORT@.starts == 0);
|
||||
}
|
||||
board.ota = false;
|
||||
assert(mesh.startSharedEspNowBridgeIfReady());
|
||||
assert(mesh.espnow_bridge.starts == 1);
|
||||
assert(mesh.@TRANSPORT@.starts == 1);
|
||||
assert(mesh.startSharedEspNowBridgeIfReady());
|
||||
assert(mesh.espnow_bridge.starts == 1);
|
||||
mesh.espnow_bridge.running = false;
|
||||
assert(mesh.@TRANSPORT@.starts == 1);
|
||||
mesh.@TRANSPORT@.running = false;
|
||||
mesh._prefs.espnow_bridge_enabled = false;
|
||||
mesh._prefs.bridge_enabled = false;
|
||||
now += 30000;
|
||||
assert(!mesh.startSharedEspNowBridgeIfReady());
|
||||
assert(mesh.espnow_bridge.starts == 1);
|
||||
assert(mesh.@TRANSPORT@.starts == 1);
|
||||
}
|
||||
'''
|
||||
for role, mqtt_member in (("simple_repeater", "mqtt_bridge"),
|
||||
("simple_room_server", "bridge")):
|
||||
with self.subTest(role=role):
|
||||
for role, mqtt_member, transport, flags in (
|
||||
("simple_repeater", "mqtt_bridge", "espnow_bridge", ["-DWITH_MQTT_BRIDGE=1"]),
|
||||
("simple_repeater", "mqtt_bridge", "espnow_bridge", ["-DWITH_RS232_BRIDGE=1"]),
|
||||
("simple_repeater", "mqtt_bridge", "bridge", []),
|
||||
("simple_room_server", "bridge", "espnow_bridge", ["-DWITH_MQTT_BRIDGE=1"]),
|
||||
):
|
||||
with self.subTest(role=role, flags=flags):
|
||||
source = (ROOT / "examples" / role / "MyMesh.h").read_text()
|
||||
retry = extract_braced(source, "bool startSharedEspNowBridgeIfReady()")
|
||||
self.compile_and_run(fixture.replace("@RETRY@", retry)
|
||||
.replace("@MQTT_MEMBER@", mqtt_member))
|
||||
.replace("@MQTT_MEMBER@", mqtt_member).replace("@TRANSPORT@", transport)
|
||||
.replace("@SOLE_MEMBER@", "Bridge bridge;" if role == "simple_repeater" else ""), *flags)
|
||||
|
||||
def test_full_companion_cli_reports_webconfig_stop_and_hardware_errors(self):
|
||||
source = (ROOT / "examples/companion_radio/MyMesh.cpp").read_text()
|
||||
|
||||
Reference in New Issue
Block a user