Combine MKE S3 repeater bridge modes at runtime
This commit is contained in:
1 parent
0e0faa33a4
commit
6cd1289c8b
21 files changed
+1557
-55
No files matched your search
@@ -114,7 +114,9 @@ jobs:
|
||||
run: node scripts/test_filter_tool.js
|
||||
|
||||
- name: Verify bridge filter wiring and shorthand permissions
|
||||
run: python3 -B test/test_transport_filter_wiring.py -v
|
||||
run: |
|
||||
python3 -B test/test_transport_filter_wiring.py -v
|
||||
python3 -B test/test_rs232_runtime.py -v
|
||||
|
||||
- name: Verify saved repeat-off trace exception and relay guards
|
||||
run: |
|
||||
|
||||
+46
-4
@@ -3383,6 +3383,12 @@ 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"
|
||||
record_build_expectation "bridge.espnow" "_ZN12ESPNowBridge5beginEv"
|
||||
fi
|
||||
if is_nrf52_sensor_ota_pair_target "$env_name" \
|
||||
&& [ -n "${NRF52_OTA_SENSOR_PROFILE:-}" ]; then
|
||||
record_build_capability "sensor.profile.${NRF52_OTA_SENSOR_PROFILE}"
|
||||
@@ -5488,6 +5494,9 @@ get_merged_rs232_repeater_replacement() {
|
||||
heltec_wsl3_repeater_bridge_rs232)
|
||||
printf '%s\n' Heltec_WSL3_repeater
|
||||
;;
|
||||
mke_s3_repeater_bridge_rs232)
|
||||
printf '%s\n' MKE_s3_repeater
|
||||
;;
|
||||
lilygo_tlora_v2_1_1_6_repeater_bridge_rs232)
|
||||
printf '%s\n' LilyGo_TLora_V2_1_1_6_repeater
|
||||
;;
|
||||
@@ -5505,7 +5514,15 @@ is_firmware_role_replaced_by_canonical_artifact() {
|
||||
|| get_full_companion_replacement "$1" >/dev/null 2>&1 \
|
||||
|| get_terminal_chat_companion_replacement "$1" >/dev/null \
|
||||
|| get_combined_usb_ble_companion_replacement "$1" >/dev/null \
|
||||
|| get_merged_rs232_repeater_replacement "$1" >/dev/null
|
||||
|| get_merged_rs232_repeater_replacement "$1" >/dev/null \
|
||||
|| get_merged_espnow_repeater_replacement "$1" >/dev/null
|
||||
}
|
||||
|
||||
get_merged_espnow_repeater_replacement() {
|
||||
case "${1,,}" in
|
||||
mke_s3_repeater_bridge_espnow) printf '%s\n' MKE_s3_repeater ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_runtime_setting_alias_target() {
|
||||
@@ -5591,7 +5608,7 @@ resolve_full_companion_firmwares() {
|
||||
resolve_repeater_firmwares() {
|
||||
local env_name
|
||||
while IFS= read -r env_name; do
|
||||
if ! get_unified_meshtower_repeater_replacement "$env_name" >/dev/null; then
|
||||
if ! is_redundant_bulk_build_target "$env_name"; then
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
done < <(get_pio_envs_for_variant_role repeater)
|
||||
@@ -5613,7 +5630,8 @@ resolve_full_esp32_firmwares() {
|
||||
local env_name
|
||||
|
||||
for env_name in "${SUPPORTED_PIO_ENVS[@]}"; do
|
||||
if supports_esp32_full_build "$env_name"; then
|
||||
if supports_esp32_full_build "$env_name" \
|
||||
&& ! is_redundant_bulk_build_target "$env_name"; then
|
||||
printf '%s\n' "$env_name"
|
||||
fi
|
||||
done
|
||||
@@ -6326,7 +6344,31 @@ get_ordinary_full_group_key() {
|
||||
*_repeater_observer_mqtt|*_room_server_observer_mqtt)
|
||||
role_base=${base%_observer_mqtt} ;;
|
||||
*_repeater_bridge_espnow)
|
||||
role_base=${base%_bridge_espnow} ;;
|
||||
# Merged repeaters and observer-backed Full images include ESP-NOW at
|
||||
# runtime. Keep dedicated images for boards without that replacement.
|
||||
local combined_target=""
|
||||
local repeater_base=${base%_bridge_espnow}
|
||||
combined_target=$(get_merged_espnow_repeater_replacement "$target") || combined_target=""
|
||||
if [ -n "$combined_target" ] \
|
||||
&& is_supported_build_env "$combined_target" \
|
||||
&& supports_esp32_full_build "$combined_target" \
|
||||
&& [ -n "${PIO_ENV_BOARD_BY_NAME[$target]:-}" ] \
|
||||
&& [ "${PIO_ENV_BOARD_BY_NAME[$target]}" = "${PIO_ENV_BOARD_BY_NAME[$combined_target]:-}" ]; then
|
||||
role_base=$repeater_base
|
||||
else
|
||||
combined_target=$(get_mqtt_enabled_target "$repeater_base") || combined_target=""
|
||||
if [ -n "$combined_target" ] \
|
||||
&& supports_esp32_full_build "$combined_target" \
|
||||
&& [ -n "${PIO_ENV_BOARD_BY_NAME[$target]:-}" ] \
|
||||
&& [ "${PIO_ENV_BOARD_BY_NAME[$target]}" = "${PIO_ENV_BOARD_BY_NAME[$combined_target]:-}" ]; then
|
||||
role_base=$repeater_base
|
||||
elif is_mqtt_bridge_target "${repeater_base}_observer_mqtt_" \
|
||||
&& supports_esp32_full_build "${repeater_base}_observer_mqtt_" \
|
||||
&& [ -n "${PIO_ENV_BOARD_BY_NAME[$target]:-}" ] \
|
||||
&& [ "${PIO_ENV_BOARD_BY_NAME[$target]}" = "${PIO_ENV_BOARD_BY_NAME[${repeater_base}_observer_mqtt_]:-}" ]; then
|
||||
role_base=$repeater_base
|
||||
fi
|
||||
fi ;;
|
||||
*_repeater|*_room_server) ;;
|
||||
*) role_base=$base ;;
|
||||
esac
|
||||
|
||||
@@ -185,6 +185,25 @@ available from a canonical image:
|
||||
releases. Wio-E5 remains separate because its normal image has only 916
|
||||
bytes free, while the combined image exceeds the fixed 240 KiB application
|
||||
partition by 2,192 bytes.
|
||||
`MKE_s3_repeater` includes the bridge on UART 2, with RX on GPIO16 and TX on
|
||||
GPIO17. Fresh installs leave it disabled at 115200 baud; upgrades from normal
|
||||
repeaters keep it disabled, while existing saved RS-232 settings are retained.
|
||||
Use `set bridge.baud 115200` and `set bridge.enabled on` to enable it, or
|
||||
`set bridge.enabled off` to stop it. `set bridge.uart 2` selects its fixed
|
||||
UART; other UART numbers are rejected. Successful changes take effect
|
||||
immediately and persist across reboots. The historical
|
||||
`MKE_s3_repeater_bridge_rs232` name remains directly buildable with its
|
||||
bridge-on default for compatibility but is omitted from bulk releases.
|
||||
The same MKE image includes ESP-NOW, controlled independently with
|
||||
`set espnow.enabled on|off` and `get espnow.running`. Both bridges can run
|
||||
together and share `bridge.source` and `bridge.delay`; `bridge.channel`,
|
||||
`bridge.secret`, and `bridge.format` apply only to ESP-NOW. Its first boot
|
||||
after an upgrade leaves ESP-NOW off, including upgrades from the dedicated
|
||||
ESP-NOW image: run `set espnow.enabled on` to enable it. Subsequent reboots
|
||||
retain the saved setting. The historical `MKE_s3_repeater_bridge_espnow`
|
||||
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.
|
||||
- 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
|
||||
|
||||
@@ -722,6 +722,9 @@ uint8_t MyMesh::handleAnonClockReq(const mesh::Identity& sender, uint32_t sender
|
||||
reply_data[8] = 0; // features
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
if (isBridgeRunning()) 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
|
||||
#elif WITH_ESPNOW_BRIDGE
|
||||
if (isBridgeRunning()) reply_data[8] |= 0x03; // is bridge, type ESP-NOW
|
||||
#endif
|
||||
@@ -1187,13 +1190,6 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// MQTT bridge: always feed RX packets - bridge decides based on mqtt.rx setting
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->onPacketReceived(pkt);
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
// ESP-NOW follows bridge.source, independently of MQTT's mqtt.rx policy.
|
||||
ESPNowBridge* espnow = &espnow_bridge;
|
||||
if (_prefs.bridge_pkt_src == 1 && espnow && espnow->isRunning()) {
|
||||
espnow->sendPacket(pkt);
|
||||
}
|
||||
#endif
|
||||
#elif defined(WITH_BRIDGE)
|
||||
// Non-MQTT bridge: use bridge.source setting
|
||||
AbstractBridge* active_bridge = activeBridge();
|
||||
@@ -1202,6 +1198,13 @@ void MyMesh::logRx(mesh::Packet *pkt, int len, float score) {
|
||||
active_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.
|
||||
if (_prefs.bridge_pkt_src == 1 && espnow_bridge.isRunning()) {
|
||||
espnow_bridge.sendPacket(pkt);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
@@ -1239,12 +1242,6 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
// MQTT bridge: always feed TX packets - bridge decides based on mqtt.tx setting
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->sendPacket(pkt);
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
ESPNowBridge* espnow = &espnow_bridge;
|
||||
if (_prefs.bridge_pkt_src == 0 && espnow && espnow->isRunning()) {
|
||||
espnow->sendPacket(pkt);
|
||||
}
|
||||
#endif
|
||||
#elif defined(WITH_BRIDGE)
|
||||
// Non-MQTT bridge: use bridge.source setting
|
||||
AbstractBridge* active_bridge = activeBridge();
|
||||
@@ -1253,6 +1250,12 @@ void MyMesh::logTx(mesh::Packet *pkt, int len) {
|
||||
active_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()) {
|
||||
espnow_bridge.sendPacket(pkt);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (_logging) {
|
||||
File f = openAppend(PACKET_LOG_FILE);
|
||||
@@ -3463,6 +3466,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#endif
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
, bridge(nullptr)
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
, espnow_bridge(&_prefs, _mgr, &rtc)
|
||||
, shared_espnow_retry_at(0)
|
||||
#endif
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
, bridge(&_prefs, _mgr, &rtc)
|
||||
#endif
|
||||
@@ -3652,7 +3659,11 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
|
||||
#else
|
||||
_prefs.bridge_enabled = 1; // enabled
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
_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
|
||||
_prefs.bridge_delay = 500; // milliseconds
|
||||
_prefs.bridge_pkt_src = 1; // logRx (RX packets)
|
||||
_prefs.bridge_baud = 115200; // baud rate
|
||||
@@ -3865,7 +3876,8 @@ void MyMesh::begin(FILESYSTEM *fs) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
if (_prefs.espnow_bridge_enabled) setEspNowBridgeState(true);
|
||||
#endif
|
||||
mesh::hilStartupTrace("mesh_bridge_ready");
|
||||
@@ -12818,13 +12830,16 @@ void __attribute__((noinline)) MyMesh::servicePostMeshLoop() {
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
// MQTT runs on Core 0. ESP-NOW remains cooperative, including in the
|
||||
// combined Full image where both transports share the WiFi station radio.
|
||||
#if defined(WITH_MQTT_BRIDGE)
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
||||
if (espnow_bridge.isRunning()) espnow_bridge.loop();
|
||||
else startSharedEspNowBridgeIfReady();
|
||||
#else
|
||||
if (bridge.isRunning()) bridge.loop();
|
||||
#endif
|
||||
#elif defined(WITH_BRIDGE) && !defined(WITH_MQTT_BRIDGE)
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) \
|
||||
|| (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
|
||||
@@ -13492,7 +13507,8 @@ 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_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
if (espnow_bridge.isRunning()) return true;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -586,6 +586,10 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
RS232Bridge* bridge;
|
||||
uint8_t active_rs232_bridge_uart = 0;
|
||||
#if defined(WITH_ESPNOW_BRIDGE)
|
||||
ESPNowBridge espnow_bridge;
|
||||
uint32_t shared_espnow_retry_at = 0;
|
||||
#endif
|
||||
#elif defined(WITH_ESPNOW_BRIDGE)
|
||||
ESPNowBridge bridge;
|
||||
#endif
|
||||
@@ -610,17 +614,19 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
// MQTT creates its WiFi task asynchronously. Do not let ESP-NOW touch the
|
||||
// station interface until that task has associated, otherwise it could bring
|
||||
// up a second WiFi owner before the AP selects the shared channel.
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_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 (!_prefs.espnow_bridge_enabled) return false;
|
||||
// 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;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning() && !WiFi.isConnected()) return false;
|
||||
#endif
|
||||
if (!millisHasNowPassed(shared_espnow_retry_at)) return false;
|
||||
shared_espnow_retry_at = millis() + 5000;
|
||||
configureBridgeFilter(&espnow_bridge);
|
||||
@@ -1277,11 +1283,13 @@ public:
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
bool isEspNowBridgeRunning() override {
|
||||
return espnow_bridge.isRunning();
|
||||
}
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool setMqttBridgeState(bool enable) override {
|
||||
if (!enable) {
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning()) mqtt_bridge->end();
|
||||
@@ -1322,6 +1330,7 @@ public:
|
||||
if (_prefs.espnow_bridge_enabled) startSharedEspNowBridgeIfReady();
|
||||
return mqtt_bridge->isRunning();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool setEspNowBridgeState(bool enable) override {
|
||||
if (!enable) {
|
||||
@@ -1330,18 +1339,22 @@ public:
|
||||
return !espnow_bridge.isRunning();
|
||||
}
|
||||
if (espnow_bridge.isRunning()) return true;
|
||||
if (_cli.getBoard()->isOTAUpdateRunning()) return false;
|
||||
shared_espnow_retry_at = 0;
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
if (mqtt_bridge && mqtt_bridge->isRunning()) {
|
||||
// Association is asynchronous; servicePostMeshLoop retries once the
|
||||
// MQTT station has joined the access point on its fixed channel.
|
||||
startSharedEspNowBridgeIfReady();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
configureBridgeFilter(&espnow_bridge);
|
||||
espnow_bridge.begin();
|
||||
return espnow_bridge.isRunning();
|
||||
}
|
||||
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
bool restartMqttBridge() override {
|
||||
#ifdef WITH_WEBCONFIG
|
||||
if (_wc_batch_active) {
|
||||
@@ -1355,6 +1368,7 @@ public:
|
||||
_alerter.setBridge(nullptr);
|
||||
return !_prefs.bridge_enabled || setMqttBridgeState(true);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool restartEspNowBridge() override {
|
||||
if (espnow_bridge.isRunning()) espnow_bridge.end();
|
||||
|
||||
@@ -122,11 +122,55 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
if targets & tower_legacy and tower_primary not in targets:
|
||||
raise ValueError("MeshTower V2 release requires the SD primary; old internal-only "
|
||||
"artifacts cannot substitute for its distinct OTA identity")
|
||||
|
||||
mke_primary = "mke_s3_repeater"
|
||||
mke_legacy = {mke_primary + "_bridge_rs232", mke_primary + "_bridge_espnow"}
|
||||
mke_bridges = {"bridge.rs232", "bridge.espnow"}
|
||||
|
||||
def qualified_full(manifest: dict) -> bool:
|
||||
return (manifest["platform"] == "ESP32_PLATFORM"
|
||||
and manifest["build_profile"] == "full"
|
||||
and manifest.get("verified") is True)
|
||||
|
||||
# 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.
|
||||
mke_full = [record["manifest"] for record in records
|
||||
if record["manifest"]["target"].lower() == mke_primary
|
||||
and record["manifest"]["build_profile"] == "full"]
|
||||
if mke_full or targets & mke_legacy:
|
||||
if not mke_full or any(
|
||||
not qualified_full(manifest)
|
||||
or not mke_bridges <= set(manifest.get("capabilities") or [])
|
||||
or not mke_bridges <= {
|
||||
check["capability"] for check in manifest.get("verification") or []
|
||||
if check.get("present") is True
|
||||
}
|
||||
for manifest in mke_full
|
||||
):
|
||||
raise ValueError("MKE S3 release requires a qualified combined repeater "
|
||||
"with verified RS232 and ESP-NOW bridges; old normal or "
|
||||
"legacy bridge artifacts cannot replace that canonical image")
|
||||
|
||||
# A dedicated ESP-NOW image is redundant only when the matching normal or
|
||||
# observer Full image really advertises that capability. Filename suffixes
|
||||
# alone previously suppressed V4 TFT's only ESP-NOW implementation.
|
||||
combined_espnow = set()
|
||||
for record in records:
|
||||
manifest = record["manifest"]
|
||||
if not qualified_full(manifest) or "bridge.espnow" not in (manifest.get("capabilities") or []):
|
||||
continue
|
||||
base = manifest["target"].rstrip("_").lower()
|
||||
if base.endswith("_observer_mqtt"):
|
||||
base = base[:-len("_observer_mqtt")]
|
||||
if base.endswith("_repeater"):
|
||||
combined_espnow.add(base)
|
||||
|
||||
chosen: dict[str, tuple[int, dict]] = {}
|
||||
passthrough: list[dict] = []
|
||||
for record in records:
|
||||
manifest = record["manifest"]
|
||||
if manifest["target"].lower() in tower_legacy:
|
||||
if manifest["target"].lower() in tower_legacy | mke_legacy:
|
||||
continue
|
||||
if manifest["platform"] != "ESP32_PLATFORM" or manifest["build_profile"] != "full":
|
||||
passthrough.append(record)
|
||||
@@ -138,7 +182,11 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]:
|
||||
base = base[:-len("_observer_mqtt")]
|
||||
kind = "observer"
|
||||
elif base.lower().endswith("_bridge_espnow"):
|
||||
base = base[:-len("_bridge_espnow")]
|
||||
bridge_base = base[:-len("_bridge_espnow")]
|
||||
if bridge_base.lower() in combined_espnow or bridge_base.lower() in {
|
||||
"meshadventurer_sx1262_repeater", "meshadventurer_sx1268_repeater",
|
||||
}:
|
||||
base = bridge_base
|
||||
kind = "bridge"
|
||||
key = base.lower()
|
||||
# G2's observer is the deployed Full identity. The other listed
|
||||
|
||||
+112
-11
@@ -824,14 +824,49 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
|
||||
File legacy = fs->open("/prefs.json", FILE_O_READ);
|
||||
#else
|
||||
File legacy = fs->open("/prefs.json", "r");
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
||||
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
const uint8_t previous_bridge_uart = _prefs->bridge_uart;
|
||||
const uint8_t previous_bridge_enabled = _prefs->bridge_enabled;
|
||||
// JSON profiles can contain the unused enabled flag without any UART.
|
||||
// Require an explicit nonzero UART before treating it as bridge intent.
|
||||
_prefs->bridge_uart = 0;
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
const uint8_t previous_espnow_enabled = _prefs->espnow_bridge_enabled;
|
||||
#endif
|
||||
if (legacy && _prefs->loadSerial(legacy)) {
|
||||
loaded = true;
|
||||
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;
|
||||
if (_prefs->bridge_uart != WITH_RS232_BRIDGE_UART
|
||||
#ifdef WITH_RS232_BRIDGE_ALT
|
||||
&& _prefs->bridge_uart != WITH_RS232_BRIDGE_ALT_UART
|
||||
#endif
|
||||
) {
|
||||
_prefs->bridge_uart = WITH_RS232_BRIDGE_UART;
|
||||
}
|
||||
#endif
|
||||
#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;
|
||||
#endif
|
||||
#ifdef WITH_MQTT_BRIDGE
|
||||
node_prefs_needs_migration = true;
|
||||
#else
|
||||
savePrefs(fs);
|
||||
#endif
|
||||
} else {
|
||||
#if defined(WITH_RS232_BRIDGE) && defined(RS232_BRIDGE_MERGED) \
|
||||
&& !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
_prefs->bridge_uart = previous_bridge_uart;
|
||||
_prefs->bridge_enabled = previous_bridge_enabled;
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
_prefs->espnow_bridge_enabled = previous_espnow_enabled;
|
||||
#endif
|
||||
}
|
||||
if (legacy) legacy.close();
|
||||
@@ -972,6 +1007,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
#endif
|
||||
mesh::hilStartupTrace("prefs_image_open_ready");
|
||||
if (file) {
|
||||
#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)
|
||||
bool has_runtime_bridge_uart = false;
|
||||
@@ -1370,6 +1408,12 @@ 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)
|
||||
uint8_t bridge_profile = 0;
|
||||
if (file.read(&bridge_profile, sizeof(bridge_profile)) == sizeof(bridge_profile)) {
|
||||
has_runtime_espnow_intent = bridge_profile == 0xA1;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1438,12 +1482,21 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
|
||||
_prefs->loop_detect = constrain(_prefs->loop_detect, 0, 3); // LOOP_DETECT_OFF..LOOP_DETECT_STRICT
|
||||
|
||||
// sanitise bad bridge pref values
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
if (!has_runtime_espnow_intent) {
|
||||
// Older profiles saved an unused ESP-NOW flag as on. Only the appended
|
||||
// combined-profile marker proves that this was an explicit setting.
|
||||
_prefs->espnow_bridge_enabled = 0;
|
||||
_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) {
|
||||
if (!has_runtime_bridge_uart || _prefs->bridge_uart == 0) {
|
||||
// Pre-merge normal repeaters persisted bridge_enabled=1 even though no
|
||||
// bridge was compiled. Fail safe on the first merged boot instead of
|
||||
// unexpectedly claiming a UART; the user can explicitly enable it.
|
||||
// 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;
|
||||
_com_prefs_needs_upgrade = true;
|
||||
}
|
||||
@@ -1710,6 +1763,10 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
|
||||
WRITE_COMMON_PREFS(&prefs->usb_debug_enabled); // appended USB debug intent
|
||||
WRITE_COMMON_PREFS(&prefs->trace_when_repeat_off); // appended repeater trace exception
|
||||
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
|
||||
#endif
|
||||
|
||||
#undef WRITE_COMMON_PREFS_BYTES
|
||||
#undef WRITE_COMMON_PREFS
|
||||
@@ -1873,6 +1930,10 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
|
||||
file.write((uint8_t *)&_prefs->usb_debug_enabled, sizeof(_prefs->usb_debug_enabled)); // appended
|
||||
file.write((uint8_t *)&_prefs->trace_when_repeat_off, sizeof(_prefs->trace_when_repeat_off)); // appended
|
||||
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
|
||||
#endif
|
||||
|
||||
_common_save_succeeded = file.commit();
|
||||
if (!_common_save_succeeded) {
|
||||
@@ -2783,16 +2844,40 @@ 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.
|
||||
const bool espnow_paused = _callbacks->isEspNowBridgeRunning();
|
||||
if (espnow_paused && !_callbacks->setEspNowBridgeState(false)) {
|
||||
strcpy(reply, "ERR: could not pause ESP-NOW for OTA");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
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 (espnow_paused && !_callbacks->setEspNowBridgeState(true)) {
|
||||
const size_t used = strlen(reply);
|
||||
snprintf(reply + used, 160 - used, "; ESP-NOW resume failed");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(LIGHTWEIGHT_WIFI_OTA)
|
||||
else {
|
||||
// Keep WiFi up, but release MQTT/TLS heap while the browser uploader runs.
|
||||
_callbacks->setBridgeState(false);
|
||||
}
|
||||
#endif
|
||||
#if defined(WITH_RS232_BRIDGE) && 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");
|
||||
}
|
||||
#endif
|
||||
if (webconfig_stopped) {
|
||||
const size_t used = strlen(reply);
|
||||
@@ -2810,6 +2895,15 @@ 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) {
|
||||
const bool resumed = _callbacks->setEspNowBridgeState(true);
|
||||
const size_t used = strlen(reply);
|
||||
snprintf(reply + used, 160 - used, resumed
|
||||
? "; ESP-NOW resumed" : "; ESP-NOW resume failed");
|
||||
}
|
||||
#endif
|
||||
} else if (memcmp(command, "clock", 5) == 0) {
|
||||
uint32_t now = getRTCClock()->getCurrentTime();
|
||||
@@ -3668,15 +3762,19 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#endif
|
||||
// Observer/MQTT/WiFi/timezone/alert/SNMP commands live in CommonCLI_Observer.cpp.
|
||||
if (handleObserverSetCmd(sender_timestamp, config, reply)) return;
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
if (memcmp(config, "espnow.enabled ", 15) == 0) {
|
||||
const char* value = config + 15;
|
||||
if (strcmp(value, "on") != 0 && strcmp(value, "off") != 0) {
|
||||
strcpy(reply, "Error: use set espnow.enabled on|off");
|
||||
} else {
|
||||
char canonical[28];
|
||||
snprintf(canonical, sizeof(canonical), "set bridge.enabled %s", value);
|
||||
handleSetCmd(sender_timestamp, canonical, reply);
|
||||
const bool enable = strcmp(value, "on") == 0;
|
||||
_prefs->espnow_bridge_enabled = enable;
|
||||
const bool applied = _callbacks->setEspNowBridgeState(enable);
|
||||
savePrefs();
|
||||
strcpy(reply, applied ? "OK"
|
||||
: "Error: ESP-NOW runtime change failed; setting saved");
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -4711,7 +4809,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
&config[15], _prefs->bridge_format, channel)) {
|
||||
_prefs->bridge_channel = channel;
|
||||
const bool applied =
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
||||
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
||||
#else
|
||||
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
||||
@@ -4731,7 +4829,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
StrHelper::strncpy(_prefs->bridge_secret, secret, sizeof(_prefs->bridge_secret));
|
||||
const bool applied =
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
||||
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
||||
#else
|
||||
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
||||
@@ -4750,7 +4848,7 @@ void CommonCLI::handleSetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
} else {
|
||||
_prefs->bridge_format = format;
|
||||
const bool applied =
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE)
|
||||
!_prefs->espnow_bridge_enabled || _callbacks->restartEspNowBridge();
|
||||
#else
|
||||
!_prefs->bridge_enabled || _callbacks->restartBridge();
|
||||
@@ -5195,6 +5293,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
sprintf(reply, "> %s",
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
"mqtt+espnow"
|
||||
#elif defined(WITH_RS232_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
"rs232+espnow"
|
||||
#elif defined(WITH_RS232_BRIDGE)
|
||||
"rs232"
|
||||
#elif WITH_ESPNOW_BRIDGE
|
||||
@@ -5220,7 +5320,8 @@ void CommonCLI::handleGetCmd(uint32_t sender_timestamp, char* command, char* rep
|
||||
#else
|
||||
_callbacks->isBridgeRunning() ? "on" : "off");
|
||||
#endif
|
||||
#if defined(WITH_MQTT_BRIDGE) && defined(WITH_ESPNOW_BRIDGE)
|
||||
#if defined(WITH_ESPNOW_BRIDGE) \
|
||||
&& (defined(WITH_MQTT_BRIDGE) || defined(WITH_RS232_BRIDGE))
|
||||
} else if (configKeyEquals(config, "espnow.enabled")) {
|
||||
sprintf(reply, "> %s", _prefs->espnow_bridge_enabled ? "on" : "off");
|
||||
} else if (configKeyEquals(config, "espnow.running")) {
|
||||
|
||||
@@ -197,10 +197,11 @@ public:
|
||||
// Appended after the existing /com_prefs tail. Keep the preamble in the same
|
||||
// transaction as frequency/modulation; old images adopt /radio_profiles.
|
||||
uint16_t primary_radio_preamble = 0;
|
||||
// Full ESP32 images can carry MQTT and ESP-NOW simultaneously. Keep the
|
||||
// Full ESP32 images can carry another bridge and ESP-NOW simultaneously. Keep the
|
||||
// ESP-NOW intent separately so either transport can be selected at runtime.
|
||||
// Old preference images did not have this byte; they default to enabled to
|
||||
// retain the previous combined-Full behavior after an upgrade.
|
||||
// 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;
|
||||
// Appended after the ESP-NOW intent. Zero keeps the board's legacy cadence.
|
||||
uint16_t gps_sync_interval_hours = 0;
|
||||
@@ -593,7 +594,7 @@ public:
|
||||
virtual bool isBridgeRunning() const { return false; }
|
||||
|
||||
// Most roles have one bridge, so the transport-specific methods retain the
|
||||
// legacy bridge behavior by default. Combined MQTT + ESP-NOW roles override
|
||||
// legacy bridge behavior by default. Combined bridge roles override
|
||||
// them to let each transport be started, stopped, and queried independently.
|
||||
virtual bool setMqttBridgeState(bool enable) { return setBridgeState(enable); }
|
||||
virtual bool setEspNowBridgeState(bool enable) { return setBridgeState(enable); }
|
||||
|
||||
@@ -303,7 +303,8 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
const IPAddress ap_ip(192, 168, 4, 1);
|
||||
const IPAddress ap_mask(255, 255, 255, 0);
|
||||
ota_started_ap = WiFi.softAPConfig(ap_ip, ap_ip, ap_mask)
|
||||
&& WiFi.softAP("MeshCore-OTA", nullptr);
|
||||
&& WiFi.softAP("MeshCore-OTA", nullptr,
|
||||
mesh::wifi::accessPointChannel());
|
||||
}
|
||||
// A valid IP does not imply a discoverable hotspot. An LR protocol bit
|
||||
// left by ESP-NOW makes AP beacons incompatible with ordinary clients.
|
||||
@@ -398,7 +399,8 @@ bool ESP32Board::startOTAUpdate(const char* id, char reply[], bool force_ap) {
|
||||
const IPAddress ap_ip(192, 168, 4, 1);
|
||||
const IPAddress ap_mask(255, 255, 255, 0);
|
||||
ota_started_ap = WiFi.softAPConfig(ap_ip, ap_ip, ap_mask)
|
||||
&& WiFi.softAP("MeshCore-OTA", NULL);
|
||||
&& WiFi.softAP("MeshCore-OTA", NULL,
|
||||
mesh::wifi::accessPointChannel());
|
||||
}
|
||||
if (ota_started_ap && mesh::wifi::applyAccessPointProtocolMask() != ESP_OK) {
|
||||
WiFi.softAPdisconnect(true);
|
||||
|
||||
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
// Production JSON parser, bridge schema, and legacy import block are inserted
|
||||
// by test_rs232_runtime.py. Only file access and save observation are faked.
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <helpers/ConfigSerializer.h>
|
||||
|
||||
struct NodePrefs : ConfigSerializer {
|
||||
uint8_t bridge_enabled = 0, bridge_uart = 2, espnow_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;
|
||||
uint8_t usb_logging_enabled = 0, usb_debug_enabled = 0;
|
||||
char bridge_secret[16] = "test-secret", node_name[32] = "before import";
|
||||
@BRIDGE_SCHEMA@
|
||||
BridgePrefs bridge;
|
||||
NodePrefs() : bridge(this) {}
|
||||
void structure() override {
|
||||
@NAME_SCHEMA@
|
||||
@BRIDGE_ROOT@
|
||||
}
|
||||
};
|
||||
struct File : Stream {
|
||||
std::string text;
|
||||
size_t pos = 0;
|
||||
bool open = true;
|
||||
explicit File(std::string value) : text(value) {}
|
||||
explicit operator bool() const { return open; }
|
||||
int available() override { return static_cast<int>(text.size() - pos); }
|
||||
int read() override { return pos < text.size() ? text[pos++] : -1; }
|
||||
int peek() override { return pos < text.size() ? text[pos] : -1; }
|
||||
void close() { open = false; }
|
||||
};
|
||||
struct FakeFS {
|
||||
std::string text;
|
||||
bool exists(const char*) const { return true; }
|
||||
File open(const char*, const char*) { return File(text); }
|
||||
};
|
||||
struct CLI {
|
||||
NodePrefs prefs;
|
||||
NodePrefs* _prefs = &prefs;
|
||||
int saves = 0;
|
||||
uint8_t saved_uart = 0, saved_enabled = 0, saved_secondary = 0;
|
||||
void savePrefs(FakeFS*) {
|
||||
++saves; saved_uart = prefs.bridge_uart;
|
||||
saved_enabled = prefs.bridge_enabled; saved_secondary = prefs.espnow_bridge_enabled;
|
||||
}
|
||||
bool import(FakeFS* fs) {
|
||||
bool loaded = false, is_upgrade = false;
|
||||
@IMPORT@
|
||||
assert(loaded == is_upgrade);
|
||||
return loaded;
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
for (const char* json : {
|
||||
"{name:\"imported node\",bridge:{en:1}}",
|
||||
"{name:\"imported node\",bridge:{en:1,uart:0}}",
|
||||
"{name:\"imported node\",bridge:{en:1,uart:1}}",
|
||||
"{name:\"imported node\",bridge:{en:1,uart:2}}",
|
||||
"{name:\"imported node\",bridge:{en:0,uart:2}}",
|
||||
}) {
|
||||
CLI cli;
|
||||
cli.prefs.espnow_bridge_enabled = 1; // An old unused flag cannot prove intent.
|
||||
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)
|
||||
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);
|
||||
#else
|
||||
assert(cli.prefs.bridge_enabled == (strstr(json, "en:1") != nullptr));
|
||||
assert(cli.prefs.bridge_uart == (strstr(json, "uart:0") ? 0
|
||||
: strstr(json, "uart:1") ? 1 : 2));
|
||||
#endif
|
||||
#if defined(ESPNOW_BRIDGE_MERGED) && !defined(ESPNOW_BRIDGE_DEFAULT_ON)
|
||||
assert(cli.prefs.espnow_bridge_enabled == 0);
|
||||
#else
|
||||
assert(cli.prefs.espnow_bridge_enabled == 1);
|
||||
#endif
|
||||
assert(cli.saved_uart == cli.prefs.bridge_uart && cli.saved_enabled == cli.prefs.bridge_enabled);
|
||||
assert(cli.saved_secondary == cli.prefs.espnow_bridge_enabled);
|
||||
}
|
||||
// Malformed JSON may partially apply scalar fields before the parser rejects it.
|
||||
CLI broken;
|
||||
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
|
||||
}
|
||||
Vendored
+440
@@ -0,0 +1,440 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <helpers/CLICommandUtils.h>
|
||||
#include <helpers/bridges/ESPNowBridgeFormat.h>
|
||||
|
||||
#define WITH_BRIDGE 1
|
||||
#define WITH_RS232_BRIDGE Serial2
|
||||
#define WITH_RS232_BRIDGE_UART 2
|
||||
#define WITH_RS232_BRIDGE_RX 16
|
||||
#define WITH_RS232_BRIDGE_TX 17
|
||||
#define ENV_INCLUDE_GPS 1
|
||||
#define BRIDGE_MAX_BAUD 115200
|
||||
#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 != 0);
|
||||
std::strncpy(out, in, size - 1); out[size - 1] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T> T constrain(T value, int low, int high) {
|
||||
return value < static_cast<T>(low) ? static_cast<T>(low)
|
||||
: value > static_cast<T>(high) ? static_cast<T>(high) : value;
|
||||
}
|
||||
|
||||
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] = {};
|
||||
};
|
||||
|
||||
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 {
|
||||
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 RS232Bridge : public AbstractBridge {
|
||||
NodePrefs* prefs;
|
||||
FakeUart& uart;
|
||||
int rx, tx;
|
||||
bool running = false;
|
||||
public:
|
||||
inline static unsigned live = 0;
|
||||
inline static unsigned fail_starts = 0;
|
||||
unsigned loops = 0, sent = 0;
|
||||
RS232Bridge(NodePrefs* prefs_, FakeUart& uart_, int rx_, int tx_, void*, void*)
|
||||
: prefs(prefs_), uart(uart_), rx(rx_), tx(tx_) { ++live; }
|
||||
~RS232Bridge() override { --live; }
|
||||
void begin() override {
|
||||
++uart.begins;
|
||||
if (fail_starts != 0) { --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*) override { ++sent; }
|
||||
};
|
||||
|
||||
class ESPNowBridge : public AbstractBridge {
|
||||
bool running = false;
|
||||
public:
|
||||
unsigned begins = 0, ends = 0, fail_starts = 0;
|
||||
unsigned loops = 0, sent = 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; }
|
||||
};
|
||||
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
static uint32_t fake_millis = 100;
|
||||
static uint32_t millis() { return fake_millis; }
|
||||
static bool millisHasNowPassed(uint32_t deadline) { return fake_millis >= deadline; }
|
||||
#endif
|
||||
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;
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
virtual bool isEspNowBridgeRunning() = 0;
|
||||
virtual bool setEspNowBridgeState(bool) = 0;
|
||||
virtual bool restartEspNowBridge() = 0;
|
||||
#endif
|
||||
};
|
||||
|
||||
class MyMesh : public Callbacks {
|
||||
public:
|
||||
NodePrefs _prefs;
|
||||
RS232Bridge* bridge = nullptr;
|
||||
uint8_t active_rs232_bridge_uart = 0;
|
||||
FakeSensors sensors;
|
||||
FakeCLI _cli;
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
ESPNowBridge espnow_bridge;
|
||||
uint32_t shared_espnow_retry_at = 0;
|
||||
#endif
|
||||
void* _mgr = nullptr;
|
||||
void* getRTCClock() { return nullptr; }
|
||||
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;
|
||||
}
|
||||
~MyMesh() override {
|
||||
assert(setBridgeState(false));
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
assert(setEspNowBridgeState(false));
|
||||
#endif
|
||||
}
|
||||
@LIFECYCLE@
|
||||
};
|
||||
|
||||
struct FakeFile {
|
||||
bool present;
|
||||
uint8_t value;
|
||||
int available() const { return present ? 1 : 0; }
|
||||
size_t read(uint8_t* out, size_t size) {
|
||||
assert(size == 1);
|
||||
if (!present) return 0;
|
||||
*out = value; present = false; return 1;
|
||||
}
|
||||
};
|
||||
|
||||
@PARSERS@
|
||||
|
||||
class CommonCLI {
|
||||
public:
|
||||
NodePrefs* _prefs;
|
||||
MyMesh* _callbacks;
|
||||
FakeSensors* _sensors;
|
||||
bool _com_prefs_needs_upgrade = false;
|
||||
unsigned saves = 0;
|
||||
NodePrefs stored;
|
||||
explicit CommonCLI(MyMesh& mesh)
|
||||
: _prefs(&mesh._prefs), _callbacks(&mesh), _sensors(&mesh.sensors) {}
|
||||
void savePrefs() { ++saves; stored = *_prefs; }
|
||||
void load(FakeFile file) {
|
||||
@INITIAL_UART@
|
||||
@LOAD_UART@
|
||||
}
|
||||
void set(const char* input, char* reply) {
|
||||
// Production dispatch receives a fixed-size command buffer.
|
||||
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 check_upgrade(bool has_tail, uint8_t saved_uart, uint8_t enabled) {
|
||||
MyMesh mesh; mesh.defaults();
|
||||
mesh._prefs.bridge_enabled = 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;
|
||||
#else
|
||||
const bool normal_upgrade = false;
|
||||
#endif
|
||||
assert(mesh._prefs.bridge_enabled == (normal_upgrade ? 0 : enabled));
|
||||
assert(mesh._prefs.bridge_uart == 2);
|
||||
if (normal_upgrade || (has_tail && saved_uart != 2))
|
||||
assert(cli._com_prefs_needs_upgrade);
|
||||
const unsigned begins = Serial2.begins;
|
||||
mesh.boot();
|
||||
assert(mesh.isBridgeRunning() == (mesh._prefs.bridge_enabled != 0));
|
||||
assert(Serial2.begins == begins + (mesh._prefs.bridge_enabled ? 1 : 0));
|
||||
assert(mesh.sensors.yield_calls == 0);
|
||||
}
|
||||
|
||||
int main() {
|
||||
{
|
||||
MyMesh mesh; mesh.defaults();
|
||||
#if defined(RS232_BRIDGE_MERGED) && !defined(RS232_BRIDGE_DEFAULT_ON)
|
||||
assert(mesh._prefs.bridge_enabled == 0);
|
||||
#else
|
||||
assert(mesh._prefs.bridge_enabled == 1);
|
||||
#endif
|
||||
assert(mesh._prefs.bridge_uart == 2 && mesh._prefs.bridge_baud == 115200);
|
||||
const unsigned begins = Serial2.begins;
|
||||
mesh.boot();
|
||||
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.
|
||||
for (uint8_t enabled : {uint8_t{0}, uint8_t{1}}) {
|
||||
check_upgrade(false, 0, enabled);
|
||||
check_upgrade(true, 0, enabled);
|
||||
check_upgrade(true, 1, enabled);
|
||||
check_upgrade(true, 2, enabled);
|
||||
}
|
||||
{
|
||||
MyMesh mesh; mesh.defaults(); mesh._prefs.bridge_enabled = 0;
|
||||
CommonCLI cli(mesh);
|
||||
char reply[256] = {};
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
expect_get(cli, "bridge.type", "> rs232+espnow");
|
||||
#else
|
||||
expect_get(cli, "bridge.type", "> rs232");
|
||||
#endif
|
||||
expect_get(cli, "bridge.enabled", "> off");
|
||||
expect_get(cli, "bridge.running", "> off");
|
||||
expect_get(cli, "bridge.uart", "> 2");
|
||||
const unsigned begins = Serial2.begins;
|
||||
assert(mesh.setBridgeState(false) && RS232Bridge::live == 0);
|
||||
cli.set("bridge.baud 57600", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && cli.saves == 1);
|
||||
assert(Serial2.begins == begins && RS232Bridge::live == 0);
|
||||
expect_get(cli, "bridge.baud", "> 57600");
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && cli.saves == 2);
|
||||
assert(Serial2.running && RS232Bridge::live == 1);
|
||||
assert(Serial2.rx == 16 && Serial2.tx == 17 && Serial2.baud == 57600);
|
||||
assert(mesh.sensors.yield_calls == 0);
|
||||
mesh::Packet packet;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
|
||||
assert(mesh.bridgesPreventSleep());
|
||||
expect_get(cli, "bridge.enabled", "> on");
|
||||
expect_get(cli, "bridge.running", "> on");
|
||||
// Persistence reconstructs the same enabled runtime after reboot.
|
||||
{
|
||||
MyMesh restarted; restarted.defaults(); restarted._prefs = cli.stored;
|
||||
CommonCLI restart_cli(restarted);
|
||||
restart_cli.load(FakeFile{true, cli.stored.bridge_uart});
|
||||
restarted.boot();
|
||||
assert(restarted.isBridgeRunning() && restarted._prefs.bridge_baud == 57600);
|
||||
}
|
||||
cli.set("bridge.baud 38400", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && Serial2.baud == 38400);
|
||||
const unsigned saves = cli.saves;
|
||||
for (const char* invalid : {"bridge.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.isBridgeRunning() && mesh._prefs.bridge_baud == 38400);
|
||||
assert(mesh._prefs.bridge_uart == 2);
|
||||
}
|
||||
RS232Bridge::fail_starts = 1;
|
||||
cli.set("bridge.baud 19200", reply);
|
||||
assert(strstr(reply, "baud unchanged") != nullptr && cli.saves == saves);
|
||||
assert(mesh._prefs.bridge_baud == 38400 && mesh.isBridgeRunning());
|
||||
assert(Serial2.baud == 38400 && RS232Bridge::live == 1);
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && !mesh.isBridgeRunning());
|
||||
assert(RS232Bridge::live == 0 && !Serial2.running);
|
||||
const unsigned disabled_saves = cli.saves;
|
||||
RS232Bridge::fail_starts = 1;
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(strstr(reply, "setting unchanged") != nullptr);
|
||||
assert(cli.saves == disabled_saves && mesh._prefs.bridge_enabled == 0);
|
||||
assert(!mesh.isBridgeRunning() && RS232Bridge::live == 0);
|
||||
// Repeated runtime switches must release the object and hardware UART.
|
||||
for (unsigned cycle = 0; cycle < 100; ++cycle) {
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isBridgeRunning());
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && RS232Bridge::live == 0);
|
||||
}
|
||||
expect_get(cli, "bridge.enabled", "> off");
|
||||
expect_get(cli, "bridge.running", "> off");
|
||||
assert(mesh.sensors.yield_calls == 0);
|
||||
}
|
||||
#ifdef WITH_ESPNOW_BRIDGE
|
||||
{
|
||||
MyMesh mesh; mesh.defaults();
|
||||
CommonCLI cli(mesh);
|
||||
char reply[256] = {};
|
||||
mesh::Packet packet;
|
||||
assert(mesh._prefs.espnow_bridge_enabled == 0);
|
||||
assert(!mesh.bridgesPreventSleep());
|
||||
expect_get(cli, "espnow.enabled", "> off");
|
||||
expect_get(cli, "espnow.running", "> off");
|
||||
const unsigned initial_uart_begins = Serial2.begins;
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isEspNowBridgeRunning());
|
||||
assert(!mesh.isBridgeRunning() && RS232Bridge::live == 0);
|
||||
assert(Serial2.begins == initial_uart_begins && mesh.bridgesPreventSleep());
|
||||
expect_get(cli, "bridge.running", "> off");
|
||||
expect_get(cli, "espnow.enabled", "> on");
|
||||
expect_get(cli, "espnow.running", "> on");
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.espnow_bridge.sent == 1 && mesh.espnow_bridge.loops == 1);
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isBridgeRunning());
|
||||
assert(mesh.isEspNowBridgeRunning() && RS232Bridge::live == 1);
|
||||
const unsigned secondary_begins = mesh.espnow_bridge.begins;
|
||||
mesh.routeRx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == 1 && mesh.espnow_bridge.sent == 2);
|
||||
assert(mesh.bridge->loops == 1 && mesh.espnow_bridge.loops == 2);
|
||||
// UART changes and rollback must leave the secondary transport running.
|
||||
cli.set("bridge.baud 57600", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.espnow_bridge.begins == secondary_begins);
|
||||
RS232Bridge::fail_starts = 1;
|
||||
cli.set("bridge.baud 38400", reply);
|
||||
assert(strstr(reply, "baud unchanged") != nullptr && mesh.isBridgeRunning());
|
||||
assert(mesh._prefs.bridge_baud == 57600 && mesh.isEspNowBridgeRunning());
|
||||
assert(mesh.espnow_bridge.begins == secondary_begins);
|
||||
const unsigned primary_begins = Serial2.begins;
|
||||
cli.set("bridge.channel 6", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.espnow_bridge.begins == secondary_begins + 1);
|
||||
cli.set("bridge.secret wired-and-wireless", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.espnow_bridge.begins == secondary_begins + 2);
|
||||
cli.set("bridge.format raw", reply);
|
||||
assert(strncmp(reply, "OK", 2) == 0 && mesh.espnow_bridge.begins == secondary_begins + 3);
|
||||
assert(Serial2.begins == primary_begins && mesh.isBridgeRunning());
|
||||
expect_get(cli, "bridge.channel", "> 6");
|
||||
expect_get(cli, "bridge.secret", "> wired-and-wireless");
|
||||
expect_get(cli, "bridge.format", "> raw");
|
||||
const unsigned saves = cli.saves;
|
||||
for (const char* invalid : {"espnow.enabled onjunk", "bridge.channel 0",
|
||||
"bridge.channel 14", "bridge.channel 6junk", "bridge.secret ",
|
||||
"bridge.format unsupported"}) {
|
||||
cli.set(invalid, reply);
|
||||
assert(strncmp(reply, "Error", 5) == 0 && cli.saves == saves);
|
||||
assert(mesh.isBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
}
|
||||
// TX-source mode feeds both transports once and ignores RX.
|
||||
mesh._prefs.bridge_pkt_src = 0;
|
||||
const unsigned primary_sent = mesh.bridge->sent;
|
||||
const unsigned secondary_sent = mesh.espnow_bridge.sent;
|
||||
mesh.routeRx(&packet); mesh.routeTx(&packet);
|
||||
assert(mesh.bridge->sent == primary_sent + 1);
|
||||
assert(mesh.espnow_bridge.sent == secondary_sent + 1);
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && RS232Bridge::live == 0);
|
||||
assert(mesh.isEspNowBridgeRunning() && mesh.bridgesPreventSleep());
|
||||
mesh.routeTx(&packet); // Secondary-only routing cannot dereference primary.
|
||||
assert(mesh.espnow_bridge.sent == secondary_sent + 2);
|
||||
cli.set("bridge.enabled on", reply);
|
||||
cli.set("espnow.enabled off", reply);
|
||||
assert(strcmp(reply, "OK") == 0 && mesh.isBridgeRunning());
|
||||
assert(!mesh.isEspNowBridgeRunning() && mesh.bridgesPreventSleep());
|
||||
const unsigned secondary_when_off = mesh.espnow_bridge.sent;
|
||||
mesh.routeTx(&packet); mesh.serviceBridges();
|
||||
assert(mesh.bridge->sent == 1 && mesh.bridge->loops == 1);
|
||||
assert(mesh.espnow_bridge.sent == secondary_when_off);
|
||||
// An OTA pause cannot be undone by a command or cooperative retry.
|
||||
mesh._cli.board.ota = true;
|
||||
const unsigned before_ota = mesh.espnow_bridge.begins;
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(strstr(reply, "setting saved") != nullptr);
|
||||
assert(mesh._prefs.espnow_bridge_enabled == 1 && !mesh.isEspNowBridgeRunning());
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.espnow_bridge.begins == before_ota && mesh.isBridgeRunning());
|
||||
mesh._cli.board.ota = false;
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.isEspNowBridgeRunning() && mesh.espnow_bridge.begins == before_ota + 1);
|
||||
// Failed ESP-NOW startup is rate-limited, without stopping the UART bridge.
|
||||
cli.set("espnow.enabled off", reply);
|
||||
mesh.espnow_bridge.fail_starts = 2;
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(strstr(reply, "setting saved") != nullptr && !mesh.isEspNowBridgeRunning());
|
||||
mesh.serviceBridges();
|
||||
const unsigned retry_begins = mesh.espnow_bridge.begins;
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.espnow_bridge.begins == retry_begins && mesh.isBridgeRunning());
|
||||
fake_millis += 5001;
|
||||
mesh.serviceBridges();
|
||||
assert(mesh.isEspNowBridgeRunning() && mesh.espnow_bridge.begins == retry_begins + 1);
|
||||
// Independent switches repeatedly leave the other transport intact.
|
||||
for (unsigned cycle = 0; cycle < 100; ++cycle) {
|
||||
cli.set("bridge.enabled off", reply);
|
||||
assert(mesh.isEspNowBridgeRunning() && RS232Bridge::live == 0);
|
||||
cli.set("bridge.enabled on", reply);
|
||||
assert(mesh.isEspNowBridgeRunning() && mesh.isBridgeRunning());
|
||||
cli.set("espnow.enabled off", reply);
|
||||
assert(mesh.isBridgeRunning() && !mesh.isEspNowBridgeRunning());
|
||||
cli.set("espnow.enabled on", reply);
|
||||
assert(mesh.isBridgeRunning() && mesh.isEspNowBridgeRunning());
|
||||
}
|
||||
cli.set("bridge.enabled off", reply);
|
||||
cli.set("espnow.enabled off", reply);
|
||||
assert(!mesh.bridgesPreventSleep() && RS232Bridge::live == 0);
|
||||
assert(mesh.sensors.yield_calls == 0);
|
||||
}
|
||||
#endif
|
||||
assert(RS232Bridge::live == 0);
|
||||
puts("RS232 runtime checks passed");
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
struct Capture {
|
||||
std::vector<uint8_t> bytes;
|
||||
std::map<const void*, size_t> offsets;
|
||||
size_t write(const uint8_t* data, size_t size) {
|
||||
offsets[data] = bytes.size();
|
||||
bytes.insert(bytes.end(), data, data + size);
|
||||
return size;
|
||||
}
|
||||
size_t offset(const void* field) const { return offsets.at(field); }
|
||||
};
|
||||
|
||||
int main() {
|
||||
CommonCLI writer;
|
||||
strcpy(writer.prefs.node_name, "MKE combined repeater");
|
||||
strcpy(writer.prefs.password, "test-password");
|
||||
writer.prefs.bridge_uart = 2;
|
||||
writer.prefs.bridge_baud = 57600;
|
||||
writer.prefs.bridge_channel = 6;
|
||||
strcpy(writer.prefs.bridge_secret, "test-secret");
|
||||
Capture initial;
|
||||
assert(writeCommonPrefsImage(initial, &writer.prefs));
|
||||
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);
|
||||
#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);
|
||||
#else
|
||||
assert(initial.bytes.size() == 874 && channel_offset == initial.bytes.size() - 1);
|
||||
#endif
|
||||
assert(initial.offset(writer.prefs.node_name) == 4);
|
||||
assert(initial.offset(&writer.prefs.freq) == 72);
|
||||
assert(initial.offset(&writer.prefs.bridge_enabled) == 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");
|
||||
assert(reader.prefs.bridge_enabled == primary);
|
||||
assert(reader.prefs.espnow_bridge_enabled == secondary);
|
||||
#ifdef WITH_RS232_BRIDGE
|
||||
assert(reader.prefs.bridge_uart == 2);
|
||||
#else
|
||||
assert(reader.prefs.bridge_uart == 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));
|
||||
|
||||
#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);
|
||||
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);
|
||||
}
|
||||
// Older dedicated MKE builds selected Serial2 but persisted the UART1 fallback.
|
||||
auto dedicated = on.bytes;
|
||||
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
|
||||
|
||||
#ifdef ESPNOW_BRIDGE_MERGED
|
||||
for (unsigned invalid = 0; invalid <= 255; ++invalid) {
|
||||
if (invalid == 0xA1) continue;
|
||||
auto old = on.bytes;
|
||||
old[marker_offset] = static_cast<uint8_t>(invalid);
|
||||
CommonCLI reader;
|
||||
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);
|
||||
}
|
||||
// Old 874-byte profiles saved the unused secondary flag as one. It is not intent.
|
||||
auto old = on.bytes;
|
||||
old.resize(marker_offset);
|
||||
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);
|
||||
reader.fs.files["/com_prefs"] = on.bytes;
|
||||
reader.fs.fail_read_after = static_cast<int>(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
|
||||
}
|
||||
Vendored
+11
-2
@@ -18,6 +18,7 @@ constexpr uint8_t WIFI_PROTOCOL_11B = 1, WIFI_PROTOCOL_11G = 2;
|
||||
constexpr uint8_t WIFI_PROTOCOL_11N = 4, WIFI_PROTOCOL_LR = 8;
|
||||
using esp_err_t = int;
|
||||
bool allow_protocol_reset = true;
|
||||
bool constrain_espnow_channel = true;
|
||||
struct IPAddress {
|
||||
std::string value;
|
||||
IPAddress() = default;
|
||||
@@ -31,6 +32,7 @@ struct IPAddress {
|
||||
struct WiFiFake {
|
||||
bool connected = true, ap = false, allow_ap = true;
|
||||
unsigned starts = 0;
|
||||
int ap_channel = 0;
|
||||
uint8_t ap_protocol = 15; // Valid driver state, but LR beacons hide the AP from ordinary clients.
|
||||
int status() const { return connected ? WL_CONNECTED : 0; }
|
||||
int getMode() const { return ap ? WIFI_AP : 0; }
|
||||
@@ -41,8 +43,9 @@ struct WiFiFake {
|
||||
assert(mask.value == "255.255.255.0");
|
||||
return allow_ap;
|
||||
}
|
||||
bool softAP(const char* ssid, const char* password) {
|
||||
bool softAP(const char* ssid, const char* password, int channel = 1) {
|
||||
assert(strcmp(ssid, "MeshCore-OTA") == 0 && password == nullptr);
|
||||
ap_channel = channel;
|
||||
++starts; ap = allow_ap; return ap;
|
||||
}
|
||||
void softAPdisconnect(bool) { ap = false; }
|
||||
@@ -54,6 +57,8 @@ esp_err_t esp_wifi_set_protocol(int interface_id, uint8_t protocols) {
|
||||
return ESP_OK;
|
||||
}
|
||||
namespace mesh { namespace wifi {
|
||||
bool espNowChannelConstrained() { return constrain_espnow_channel; }
|
||||
uint8_t activeEspNowChannel() { return 6; }
|
||||
@AP_PROTOCOL_POLICY@
|
||||
} }
|
||||
static unsigned server_starts = 0;
|
||||
@@ -123,7 +128,10 @@ int main() {
|
||||
+ "/update";
|
||||
assert(strncmp(reply.text, url.c_str(), url.size()) == 0);
|
||||
assert(strstr(reply.text, instruction));
|
||||
if (ap || !WiFi.connected) assert(WiFi.ap_protocol == 7);
|
||||
if (ap || !WiFi.connected) {
|
||||
assert(WiFi.ap_protocol == 7);
|
||||
assert(WiFi.ap_channel == (constrain_espnow_channel ? 6 : 1));
|
||||
}
|
||||
assert(strlen(reply.text) < 160 && reply.guard == 0x12345678);
|
||||
};
|
||||
// Normal start reports the actual LAN address and does not disconnect it.
|
||||
@@ -174,6 +182,7 @@ int main() {
|
||||
assert(board.stopOTAUpdate(reply.text));
|
||||
assert(!board.inhibit_sleep && !board.ota_server && !WiFi.ap && WiFi.connected);
|
||||
|
||||
constrain_espnow_channel = false;
|
||||
WiFi.connected = false;
|
||||
allow_protocol_reset = false;
|
||||
assert(!board.startOTAUpdate("test node", reply.text, false));
|
||||
|
||||
@@ -132,6 +132,7 @@ fi
|
||||
# and BLE constraints cannot silently disappear through that inheritance.
|
||||
pio project config --json-output | python3 -c '
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
sections = {section: dict(options) for section, options in json.load(sys.stdin)}
|
||||
@@ -325,6 +326,7 @@ for env_name in (
|
||||
"solarxiao_33S_repeater",
|
||||
"Heltec_v3_repeater",
|
||||
"Heltec_WSL3_repeater",
|
||||
"MKE_s3_repeater",
|
||||
"Heltec_t096_repeater",
|
||||
"Heltec_t096_repeater_lora_ota_no_external_sensors",
|
||||
"RAK_4631_repeater",
|
||||
@@ -339,6 +341,55 @@ 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=")
|
||||
|
||||
# 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.
|
||||
for env_name in ("MKE_s3_repeater", "MKE_s3_repeater_bridge_rs232"):
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE=Serial2")
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE_UART=2")
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE_RX=16")
|
||||
require(env_name, "build_flags", "WITH_RS232_BRIDGE_TX=17")
|
||||
require(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp")
|
||||
reject("MKE_s3_repeater", "build_flags", "RS232_BRIDGE_DEFAULT_ON")
|
||||
reject("MKE_s3_repeater_bridge_rs232", "build_flags", "RS232_BRIDGE_MERGED")
|
||||
require("MKE_s3_repeater", "build_flags", "WITH_ESPNOW_BRIDGE=1")
|
||||
require("MKE_s3_repeater", "build_flags", "ESPNOW_BRIDGE_MERGED=1")
|
||||
require("MKE_s3_repeater", "build_src_filter", "helpers/bridges/ESPNowBridge.cpp")
|
||||
reject("MKE_s3_repeater_bridge_espnow", "build_flags", "ESPNOW_BRIDGE_MERGED")
|
||||
for env_name in (
|
||||
"MKE_s3",
|
||||
"MKE_s3_repeater_bridge_espnow",
|
||||
"MKE_s3_room_server",
|
||||
"MKE_s3_sensor",
|
||||
"MKE_s3_terminal_chat",
|
||||
"MKE_s3_companion_radio_usb",
|
||||
"MKE_s3_companion_radio_ble",
|
||||
"MKE_s3_companion_radio_wifi",
|
||||
):
|
||||
reject(env_name, "build_flags", "WITH_RS232_BRIDGE")
|
||||
reject(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp")
|
||||
require("MKE_s3_repeater_bridge_espnow", "build_flags", "WITH_ESPNOW_BRIDGE=1")
|
||||
require("MKE_s3_repeater_bridge_espnow", "build_src_filter", "helpers/bridges/ESPNowBridge.cpp")
|
||||
|
||||
# GPIO5 is the radio reset line. Guard the complete board wiring rather than
|
||||
# accepting a bridge pin assignment that compiles but disables a peripheral.
|
||||
mke_pins = {
|
||||
name: int(value)
|
||||
for name, value in re.findall(
|
||||
r"(?:^|\s)-D\s*([A-Z0-9_]+)=([0-9]+)(?=\s|$)",
|
||||
option_text("MKE_s3_repeater", "build_flags"),
|
||||
)
|
||||
}
|
||||
bridge_pins = {mke_pins["WITH_RS232_BRIDGE_RX"], mke_pins["WITH_RS232_BRIDGE_TX"]}
|
||||
for pin_name in (
|
||||
"P_LORA_NSS", "P_LORA_MOSI", "P_LORA_MISO", "P_LORA_SCLK",
|
||||
"P_LORA_BUSY", "P_LORA_RESET", "P_LORA_DIO_1",
|
||||
"PIN_BOARD_SDA", "PIN_BOARD_SCL", "PIN_USER_BTN",
|
||||
"PIN_GPS_RX", "PIN_GPS_TX", "PIN_GPS_EN",
|
||||
):
|
||||
if mke_pins[pin_name] in bridge_pins:
|
||||
raise SystemExit(f"test_build_profiles: MKE bridge UART conflicts with {pin_name}")
|
||||
|
||||
for env_name in (
|
||||
"t1000e_companion_radio_usb",
|
||||
"MeshTracker_X1_companion_radio_usb",
|
||||
@@ -918,6 +969,39 @@ apply_esp32_full_shared_bridge_profile Station_G2_repeater_observer_mqtt
|
||||
[[ "$(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
|
||||
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"
|
||||
[ "$(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"
|
||||
done
|
||||
[ "$(get_unified_full_infrastructure_target Heltec_v3_repeater_bridge_espnow)" \
|
||||
= Heltec_v3_repeater_observer_mqtt ] \
|
||||
|| fail "Heltec V3 lost its same-board combined observer recipe"
|
||||
[ "$(select_ordinary_full_targets Heltec_v3_repeater \
|
||||
Heltec_v3_repeater_bridge_espnow Heltec_v3_repeater_observer_mqtt)" \
|
||||
= Heltec_v3_repeater ] \
|
||||
|| fail "Heltec V3 retained duplicate ESP-NOW/observer Full images"
|
||||
[ "$(select_ordinary_full_targets MKE_s3_repeater MKE_s3_repeater_bridge_espnow)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 retained a duplicate ESP-NOW image after merging its sources"
|
||||
[ "$(select_ordinary_full_targets MKE_s3_repeater_bridge_espnow MKE_s3_repeater)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 bridge input order changed its canonical Full image"
|
||||
[ "$(select_ordinary_full_targets LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow \
|
||||
LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_)" \
|
||||
= LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_ ] \
|
||||
|| fail "TLora trailing-underscore observer failed to replace its ESP-NOW sibling"
|
||||
ESP32_FULL_BUILD=0
|
||||
PLATFORMIO_BUILD_FLAGS=""
|
||||
PLATFORMIO_BUILD_SRC_FILTER=""
|
||||
@@ -1380,6 +1464,73 @@ fi
|
||||
|| fail "T096 RS232 bridge did not map to merged repeater"
|
||||
is_redundant_bulk_build_target Heltec_v3_repeater_bridge_rs232 \
|
||||
|| fail "merged RS232 bridge remained in canonical bulk builds"
|
||||
[ "$(get_merged_rs232_repeater_replacement MKE_s3_repeater_bridge_rs232)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 RS232 bridge did not map to its merged repeater"
|
||||
is_supported_build_env MKE_s3_repeater_bridge_rs232 \
|
||||
|| fail "MKE S3 legacy RS232 target stopped being directly buildable"
|
||||
[ "$(get_pio_build_env MKE_s3_repeater_bridge_rs232)" \
|
||||
= MKE_s3_repeater_bridge_rs232 ] \
|
||||
|| fail "MKE S3 direct legacy build lost its bridge-on recipe"
|
||||
is_redundant_bulk_build_target MKE_s3_repeater_bridge_rs232 \
|
||||
|| fail "MKE S3 dedicated RS232 image remained in canonical bulk builds"
|
||||
if is_redundant_bulk_build_target MKE_s3_repeater; then
|
||||
fail "MKE S3 canonical repeater was omitted"
|
||||
fi
|
||||
is_redundant_bulk_build_target MKE_s3_repeater_bridge_espnow \
|
||||
|| fail "MKE S3 dedicated ESP-NOW image remained beside its combined repeater"
|
||||
[ "$(get_merged_espnow_repeater_replacement MKE_s3_repeater_bridge_espnow)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 ESP-NOW bridge did not map to its merged repeater"
|
||||
is_supported_build_env MKE_s3_repeater_bridge_espnow \
|
||||
|| fail "MKE S3 legacy ESP-NOW target stopped being directly buildable"
|
||||
[ "$(get_pio_build_env MKE_s3_repeater_bridge_espnow)" \
|
||||
= MKE_s3_repeater_bridge_espnow ] \
|
||||
|| fail "MKE S3 direct legacy ESP-NOW build lost its bridge-on recipe"
|
||||
(
|
||||
SUPPORTED_PIO_ENVS=(
|
||||
MKE_s3_repeater
|
||||
MKE_s3_repeater_bridge_rs232
|
||||
MKE_s3_repeater_bridge_espnow
|
||||
)
|
||||
[ "$(resolve_all_firmwares)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 bulk inventory did not consolidate both bridge images"
|
||||
[ "$(resolve_repeater_firmwares)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 repeater inventory retained dedicated bridge images"
|
||||
[ "$(resolve_full_esp32_firmwares)" \
|
||||
= MKE_s3_repeater ] \
|
||||
|| fail "MKE S3 Full ESP32 inventory retained dedicated bridge images"
|
||||
)
|
||||
|
||||
# A hardcoded merged-role recommendation is insufficient if its normal
|
||||
# recipe no longer exists, cannot supply Full, or belongs to another board.
|
||||
# Isolate these metadata faults so a future observer cannot mask the guard.
|
||||
for replacement_fault in missing full board; do
|
||||
(
|
||||
normal_target=MKE_s3_repeater
|
||||
bridge_target=MKE_s3_repeater_bridge_espnow
|
||||
PIO_ENV_PLATFORM_BY_NAME[$normal_target]=ESP32_PLATFORM
|
||||
PIO_ENV_FULL_BUILD_BY_NAME[$normal_target]=1
|
||||
PIO_ENV_BOARD_BY_NAME[$normal_target]=mke-test-board
|
||||
PIO_ENV_PLATFORM_BY_NAME[$bridge_target]=ESP32_PLATFORM
|
||||
PIO_ENV_FULL_BUILD_BY_NAME[$bridge_target]=1
|
||||
PIO_ENV_BOARD_BY_NAME[$bridge_target]=mke-test-board
|
||||
for candidate in "$normal_target" "${normal_target}_mqtt" \
|
||||
"${normal_target}_observer_mqtt" "${normal_target}_observer_mqtt_"; do
|
||||
PIO_ENV_MQTT_BY_NAME[$candidate]=0
|
||||
done
|
||||
case "$replacement_fault" in
|
||||
missing) unset 'PIO_ENV_PLATFORM_BY_NAME[MKE_s3_repeater]' ;;
|
||||
full) PIO_ENV_FULL_BUILD_BY_NAME[$normal_target]=0 ;;
|
||||
board) PIO_ENV_BOARD_BY_NAME[$normal_target]=different-board ;;
|
||||
esac
|
||||
[ "$(get_ordinary_full_group_key "$bridge_target")" \
|
||||
= 'mke-test-board|mke_s3_repeater_bridge_espnow' ] \
|
||||
|| fail "MKE S3 $replacement_fault replacement incorrectly suppressed its bridge"
|
||||
)
|
||||
done
|
||||
if is_redundant_bulk_build_target wio-e5-repeater_bridge_rs232; then
|
||||
fail "capacity-constrained Wio-E5 RS232 bridge was incorrectly merged"
|
||||
fi
|
||||
|
||||
@@ -85,6 +85,8 @@ class CLISettingsContractTest(unittest.TestCase):
|
||||
'esp32_mqtt_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_MQTT_BRIDGE',
|
||||
'WITH_ESPNOW_BRIDGE', 'WITH_BRIDGE'],
|
||||
'rs232_gps': ['WITH_BRIDGE', 'WITH_RS232_BRIDGE', 'ENV_INCLUDE_GPS'],
|
||||
'rs232_espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE',
|
||||
'WITH_RS232_BRIDGE', 'WITH_ESPNOW_BRIDGE'],
|
||||
'espnow': ['ESP_PLATFORM', 'ESP32_PLATFORM', 'WITH_BRIDGE', 'WITH_ESPNOW_BRIDGE', 'MESH_PRIMARY_ESPNOW'],
|
||||
'lr2021': ['USE_LR2021'],
|
||||
}
|
||||
|
||||
@@ -9,8 +9,13 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
from build_local_release import select_ordinary_full_records
|
||||
|
||||
|
||||
def record(target: str, profile: str = "full", platform: str = "ESP32_PLATFORM") -> dict:
|
||||
return {"manifest": {"target": target, "build_profile": profile, "platform": platform},
|
||||
def record(target: str, profile: str = "full", platform: str = "ESP32_PLATFORM",
|
||||
capabilities: tuple[str, ...] = ()) -> dict:
|
||||
return {"manifest": {"target": target, "build_profile": profile, "platform": platform,
|
||||
"verified": True, "capabilities": list(capabilities),
|
||||
"verification": [{"capability": capability, "present": True,
|
||||
"source": "linked image", "evidence": capability}
|
||||
for capability in capabilities]},
|
||||
"files": []}
|
||||
|
||||
|
||||
@@ -58,13 +63,13 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
inputs = [
|
||||
record("Heltec_v3_repeater_observer_mqtt"),
|
||||
record("Heltec_v3_repeater_bridge_espnow"),
|
||||
record("Heltec_v3_repeater"),
|
||||
record("Heltec_v3_repeater", capabilities=("bridge.espnow",)),
|
||||
record("Heltec_v3_repeater", "standard"),
|
||||
record("Heltec_v3_room_server"),
|
||||
record("Station_G2_repeater"),
|
||||
record("Station_G2_repeater_observer_mqtt"),
|
||||
record("Tbeam_SX1262_repeater_bridge_espnow"),
|
||||
record("Tbeam_SX1262_repeater_observer_mqtt"),
|
||||
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("Meshadventurer_sx1262_repeater"),
|
||||
@@ -82,6 +87,97 @@ class ReleaseFullSelectionTest(unittest.TestCase):
|
||||
})
|
||||
self.assertEqual(len(selected), 8) # standard V3 remains alongside Full V3
|
||||
|
||||
def test_mke_combined_repeater_filters_stale_bridge_identities_without_relabeling(self):
|
||||
primary = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
legacy = [record("MKE_s3_repeater_bridge_" + bridge, profile)
|
||||
for bridge in ("rs232", "espnow") for profile in ("full", "standard")]
|
||||
for inputs in ([primary], legacy + [primary], [primary] + legacy):
|
||||
with self.subTest(primary_first=inputs[0] is primary):
|
||||
selected = select_ordinary_full_records(inputs)
|
||||
self.assertEqual(selected, [primary])
|
||||
self.assertIs(selected[0], primary)
|
||||
self.assertEqual(selected[0]["manifest"]["target"], "MKE_s3_repeater")
|
||||
|
||||
def test_mke_legacy_images_require_a_qualified_combined_primary(self):
|
||||
incomplete = []
|
||||
incomplete.append([])
|
||||
incomplete.append([record("MKE_s3_repeater", "standard",
|
||||
capabilities=("bridge.rs232", "bridge.espnow"))])
|
||||
for capability in ("bridge.rs232", "bridge.espnow"):
|
||||
incomplete.append([record("MKE_s3_repeater", capabilities=(capability,))])
|
||||
unqualified = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
unqualified["manifest"]["verified"] = False
|
||||
incomplete.append([unqualified])
|
||||
for capability in ("bridge.rs232", "bridge.espnow"):
|
||||
missing = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
next(check for check in missing["manifest"]["verification"]
|
||||
if check["capability"] == capability)["present"] = False
|
||||
incomplete.append([missing])
|
||||
unproven = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
unproven["manifest"]["verification"] = []
|
||||
incomplete.append([unproven])
|
||||
for bridge in ("rs232", "espnow"):
|
||||
for normal in incomplete:
|
||||
with self.subTest(bridge=bridge, normal=normal):
|
||||
with self.assertRaisesRegex(ValueError, "qualified combined repeater"):
|
||||
select_ordinary_full_records(normal + [record("MKE_s3_repeater_bridge_" + bridge)])
|
||||
|
||||
def test_mke_full_primary_requires_verified_bridges_without_legacy_images(self):
|
||||
incomplete = [record("MKE_s3_repeater", capabilities=capabilities)
|
||||
for capabilities in ((), ("bridge.rs232",), ("bridge.espnow",))]
|
||||
unqualified = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
unqualified["manifest"]["verified"] = False
|
||||
incomplete.append(unqualified)
|
||||
unproven = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
unproven["manifest"]["verification"] = []
|
||||
incomplete.append(unproven)
|
||||
wrong_platform = record("MKE_s3_repeater", platform="NRF52_PLATFORM",
|
||||
capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
incomplete.append(wrong_platform)
|
||||
for capability in ("bridge.rs232", "bridge.espnow"):
|
||||
missing = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow"))
|
||||
next(check for check in missing["manifest"]["verification"]
|
||||
if check["capability"] == capability)["present"] = False
|
||||
incomplete.append(missing)
|
||||
for primary in incomplete:
|
||||
with self.subTest(primary=primary):
|
||||
with self.assertRaisesRegex(ValueError, "qualified combined repeater"):
|
||||
select_ordinary_full_records([primary])
|
||||
|
||||
def test_mke_full_contract_does_not_apply_to_other_platforms_or_standard_profiles(self):
|
||||
other_platform = record("RAK_4631_repeater", platform="NRF52_PLATFORM")
|
||||
other_platform["manifest"].update(verified=None, capabilities=None, verification=None)
|
||||
standard_mke = record("MKE_s3_repeater", "standard")
|
||||
inputs = [other_platform, standard_mke]
|
||||
self.assertEqual(select_ordinary_full_records(inputs), inputs)
|
||||
|
||||
def test_dedicated_espnow_survives_when_plain_image_lacks_the_capability(self):
|
||||
inputs = [record("heltec_v4_tft_repeater"),
|
||||
record("heltec_v4_tft_repeater_bridge_espnow", capabilities=("bridge.espnow",))]
|
||||
for ordered in (inputs, list(reversed(inputs))):
|
||||
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",))
|
||||
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])
|
||||
|
||||
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",))
|
||||
legacy = record("LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow",
|
||||
capabilities=("bridge.espnow",))
|
||||
for inputs in ([observer, legacy], [legacy, observer]):
|
||||
self.assertEqual(select_ordinary_full_records(inputs), [observer])
|
||||
|
||||
def test_unqualified_plain_image_cannot_replace_dedicated_espnow(self):
|
||||
primary = record("heltec_v4_tft_repeater", capabilities=("bridge.espnow",))
|
||||
primary["manifest"]["verified"] = False
|
||||
legacy = record("heltec_v4_tft_repeater_bridge_espnow", capabilities=("bridge.espnow",))
|
||||
self.assertEqual(select_ordinary_full_records([primary, legacy]), [primary, legacy])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -432,8 +432,11 @@ class Nrf52VariantContractsTest(unittest.TestCase):
|
||||
)
|
||||
# Declaration, persisted-tail detection, and migration must share the
|
||||
# same guard. A broader use breaks dedicated default-on bridge builds.
|
||||
self.assertEqual(cli.count(guard), 3)
|
||||
self.assertEqual(cli.count("has_runtime_bridge_uart"), 3)
|
||||
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)
|
||||
|
||||
def test_gat562_30s_gps_enable_and_buzzer_pins_are_distinct(self) -> None:
|
||||
recipe = (
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Execute production RS-232 defaults, preference migration, and CLI lifecycle."""
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from test_replay_reset_integration import extract_braced
|
||||
from test_ota_channel_persistence import persistence_harness
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURE = ROOT / "test/fixtures/rs232_runtime/main.cpp"
|
||||
|
||||
|
||||
class RS232RuntimeTests(unittest.TestCase):
|
||||
def test_legacy_json_import_requires_explicit_uart_before_enabling(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text(encoding="ascii")
|
||||
header = (ROOT / "src/helpers/CommonCLI.h").read_text(encoding="ascii")
|
||||
code = (ROOT / "test/fixtures/rs232_runtime/json.cpp").read_text(encoding="ascii")
|
||||
code = code.replace("@BRIDGE_SCHEMA@", extract_braced(header, "class BridgePrefs") + ";")
|
||||
code = code.replace("@NAME_SCHEMA@", 'def("name", node_name, sizeof(node_name));')
|
||||
self.assertIn('def("name", node_name, sizeof(node_name));', header)
|
||||
code = code.replace("@BRIDGE_ROOT@", 'def("bridge", bridge);')
|
||||
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"]),
|
||||
):
|
||||
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="rs232-json-") as directory:
|
||||
work = Path(directory)
|
||||
(work / "Utils.h").write_text(
|
||||
'#pragma once\n#include <Arduino.h>\nnamespace mesh { struct Utils {\n'
|
||||
'static void printHex(Stream&, uint8_t*, size_t) {}\n'
|
||||
'static bool fromHex(uint8_t*, size_t, const char*) { return true; }\n'
|
||||
'}; }\n', encoding="ascii")
|
||||
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",
|
||||
"-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"),
|
||||
str(source_file), str(ROOT / "src/helpers/ConfigSerializer.cpp"), "-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)
|
||||
|
||||
def test_complete_production_preference_images(self):
|
||||
compiler = shutil.which("g++") or shutil.which("clang++")
|
||||
self.assertIsNotNone(compiler, "a host C++ compiler is required")
|
||||
base = persistence_harness()
|
||||
code = base[:base.index("struct Capture {")]
|
||||
# The generic constants extraction includes the header's fallback UART1.
|
||||
# Use the resolved MKE UART2 flag instead, as a real build does.
|
||||
code = code.replace("#define WITH_RS232_BRIDGE_UART 1", "")
|
||||
code += (ROOT / "test/fixtures/rs232_runtime/prefs.cpp").read_text(encoding="ascii")
|
||||
for profile, macros in (
|
||||
("ordinary_unmerged", []),
|
||||
("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"]),
|
||||
):
|
||||
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="rs232-prefs-") 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, 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",
|
||||
"-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(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)
|
||||
|
||||
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")]
|
||||
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")]
|
||||
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]
|
||||
defaults_start = implementation.index("// bridge defaults")
|
||||
defaults_end = implementation.index("StrHelper::strncpy(_prefs.bridge_secret", defaults_start)
|
||||
defaults = implementation[defaults_start:defaults_end]
|
||||
initial_start = cli.index("#ifdef WITH_RS232_BRIDGE", cli.index("bool is_fresh_install"))
|
||||
initial = cli[initial_start:cli.index("#endif", initial_start) + len("#endif")]
|
||||
boot_source = implementation[implementation.index('mesh::hilStartupTrace("mesh_bridge_begin")'):]
|
||||
boot = extract_braced(boot_source, "if (_prefs.bridge_enabled) {")
|
||||
secondary_boot = "if (_prefs.espnow_bridge_enabled) setEspNowBridgeState(true);"
|
||||
self.assertIn(secondary_boot, boot_source)
|
||||
boot += "\n#ifdef WITH_ESPNOW_BRIDGE\n" + secondary_boot + "\n#endif\n"
|
||||
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 setBridgeState(bool enable) override", "bool restartBridge() override",
|
||||
))
|
||||
lifecycle += "\n#ifdef WITH_ESPNOW_BRIDGE\n" + "\n".join(
|
||||
extract_braced(header, signature) for signature in (
|
||||
"bool startSharedEspNowBridgeIfReady()", "bool isEspNowBridgeRunning() override",
|
||||
"bool setEspNowBridgeState(bool enable) override", "bool restartEspNowBridge() override",
|
||||
)) + "\n#endif\n"
|
||||
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]
|
||||
loop_start = implementation.index("#if defined(WITH_ESPNOW_BRIDGE)",
|
||||
implementation.index("MyMesh::servicePostMeshLoop()"))
|
||||
loop_end = implementation.index(" if (next_flood_advert", loop_start)
|
||||
pending_start = implementation.index("#if defined(WITH_BRIDGE)",
|
||||
implementation.index("bool MyMesh::hasPendingWork()"))
|
||||
pending_end = implementation.index(" if (radio_driver.isWatchdogObserving()", pending_start)
|
||||
# Preprocess complete dispatch methods before extracting branches.
|
||||
# Their feature guards cross the closing braces between else-if cases.
|
||||
commands = "\n".join(extract_braced(cli, signature) for signature in (
|
||||
"void CommonCLI::handleSetCmd(", "void CommonCLI::handleGetCmd("))
|
||||
code = FIXTURE.read_text(encoding="ascii")
|
||||
for name, value in {
|
||||
"PARSERS": "\n".join(extract_braced(cli, signature) for signature in (
|
||||
"static bool parseOnOffStrict(", "static bool configKeyEquals(")),
|
||||
"DEFAULTS": defaults, "INITIAL_UART": initial,
|
||||
"LOAD_UART": declaration + "\n" + uart_read + "\n" + migration,
|
||||
"LIFECYCLE": lifecycle, "BOOT": boot,
|
||||
"ROUTE_RX": routing["logRx"], "ROUTE_TX": routing["logTx"],
|
||||
"SERVICE": implementation[loop_start:loop_end],
|
||||
"PENDING": implementation[pending_start:pending_end],
|
||||
}.items():
|
||||
code = code.replace("@" + name + "@", value)
|
||||
for profile, macros in (
|
||||
("merged", ["RS232_BRIDGE_MERGED=1"]),
|
||||
("merged_default_on", ["RS232_BRIDGE_MERGED=1", "RS232_BRIDGE_DEFAULT_ON=1"]),
|
||||
("legacy_dedicated", []),
|
||||
("merged_rs232_espnow", ["RS232_BRIDGE_MERGED=1", "WITH_ESPNOW_BRIDGE=1",
|
||||
"ESPNOW_BRIDGE_MERGED=1"]),
|
||||
):
|
||||
with self.subTest(profile=profile), tempfile.TemporaryDirectory(prefix="rs232-runtime-") as directory:
|
||||
work = Path(directory)
|
||||
source, binary = work / "main.cpp", work / "test"
|
||||
processed = subprocess.run([
|
||||
compiler, "-E", "-P", "-x", "c++", "-DWITH_BRIDGE=1",
|
||||
"-DWITH_RS232_BRIDGE=Serial2", "-DWITH_RS232_BRIDGE_UART=2",
|
||||
"-DENV_INCLUDE_GPS=1", *["-D" + macro for macro in macros], "-",
|
||||
], input=commands, capture_output=True, text=True, check=True).stdout
|
||||
set_branches = "\nelse ".join(extract_braced(processed, signature) for signature in (
|
||||
'if (memcmp(config, "bridge.enabled ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.baud ", 12) == 0)',
|
||||
'if (memcmp(config, "bridge.uart ", 12) == 0)',
|
||||
))
|
||||
get_branches = "\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, "bridge.baud"))',
|
||||
'if (configKeyEquals(config, "bridge.uart"))',
|
||||
))
|
||||
if "WITH_ESPNOW_BRIDGE=1" in macros:
|
||||
set_branches = extract_braced(processed, 'if (memcmp(config, "espnow.enabled ", 15) == 0)') \
|
||||
+ "\nelse " + set_branches
|
||||
set_branches += "\nelse " + "\nelse ".join(extract_braced(processed, signature) for signature in (
|
||||
'if (memcmp(config, "bridge.channel ", 15) == 0)',
|
||||
'if (memcmp(config, "bridge.secret ", 14) == 0)',
|
||||
'if (memcmp(config, "bridge.format ", 14) == 0)',
|
||||
))
|
||||
get_branches += "\nelse " + "\nelse ".join(extract_braced(processed, signature) for signature in (
|
||||
'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.write_text(code.replace("@SET_BRANCHES@", set_branches)
|
||||
.replace("@GET_BRANCHES@", get_branches), encoding="ascii")
|
||||
result = subprocess.run([
|
||||
compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
|
||||
*["-D" + macro for macro in macros], "-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("RS232 runtime checks passed", result.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -72,7 +72,8 @@ struct MockWiFi {
|
||||
configured_ap_ip = ip;
|
||||
return ap_config_ok;
|
||||
}
|
||||
bool softAP(const char*, const char*) {
|
||||
bool softAP(const char*, const char*, int channel = 1) {
|
||||
assert(channel == 1);
|
||||
++ap_start_calls;
|
||||
if (ap_start_ok) {
|
||||
sdk_initialized = sdk_started = sdk_ap_active = true;
|
||||
@@ -85,6 +86,7 @@ struct MockWiFi {
|
||||
} WiFi;
|
||||
namespace mesh { namespace wifi {
|
||||
int applyAccessPointProtocolMask() { WiFi.ap_protocol = 7; return ESP_OK; }
|
||||
int accessPointChannel() { return 1; }
|
||||
} }
|
||||
|
||||
int wifi_stop_calls = 0, wifi_deinit_calls = 0;
|
||||
|
||||
+109
-1
@@ -39,7 +39,8 @@ class WiFiOtaStartTest(unittest.TestCase):
|
||||
mask = re.search(r"static constexpr uint8_t kAccessPointProtocolMask =.*?;",
|
||||
policy, re.DOTALL).group(0)
|
||||
fixture = fixture.replace("@AP_PROTOCOL_POLICY@", mask + "\n" + extract_braced(
|
||||
policy, "inline esp_err_t applyAccessPointProtocolMask()"))
|
||||
policy, "inline esp_err_t applyAccessPointProtocolMask()") + "\n"
|
||||
+ extract_braced(policy, "inline int accessPointChannel()"))
|
||||
for name, implementation, flags in (
|
||||
("lightweight infrastructure", lightweight, ["-DLIGHTWEIGHT_WIFI_OTA=1"]),
|
||||
("lightweight companion", lightweight, ["-DLIGHTWEIGHT_WIFI_OTA=1", "-DCOMPANION_RADIO_FULL=1"]),
|
||||
@@ -177,6 +178,113 @@ 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):
|
||||
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)
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
bool ota = false;
|
||||
struct Callbacks {
|
||||
bool web = false, espnow = true, uart = true;
|
||||
bool allow_pause = true, allow_resume = true;
|
||||
int pauses = 0, resumes = 0, uart_changes = 0;
|
||||
bool isWebConfigActive() const { return web; }
|
||||
bool stopWebConfigForOTA(char*) { web = false; return true; }
|
||||
bool isEspNowBridgeRunning() const { return espnow; }
|
||||
bool setEspNowBridgeState(bool enabled) {
|
||||
if (enabled) {
|
||||
++resumes; assert(!ota);
|
||||
if (!allow_resume) return false;
|
||||
} else {
|
||||
++pauses;
|
||||
if (!allow_pause) return false;
|
||||
}
|
||||
espnow = enabled; return true;
|
||||
}
|
||||
bool setBridgeState(bool enabled) { ++uart_changes; uart = enabled; return true; }
|
||||
} callbacks;
|
||||
struct Board {
|
||||
bool allow_start = true, allow_stop = true;
|
||||
int starts = 0;
|
||||
bool startOTAUpdate(const char*, char* reply, bool) {
|
||||
assert(!callbacks.espnow && !callbacks.web); ++starts;
|
||||
ota = allow_start;
|
||||
strcpy(reply, allow_start ? "Started: http://192.168.4.1/update"
|
||||
: "ERR: OTA WiFi failed");
|
||||
return allow_start;
|
||||
}
|
||||
bool stopOTAUpdate(char* reply) {
|
||||
if (allow_stop) ota = false;
|
||||
strcpy(reply, allow_stop ? "OK - OTA stopped" : "ERR: OTA upload active");
|
||||
return allow_stop;
|
||||
}
|
||||
} board;
|
||||
struct Prefs {
|
||||
const char* node_name = "test";
|
||||
bool bridge_enabled = true, espnow_bridge_enabled = true;
|
||||
} prefs;
|
||||
struct CLI {
|
||||
Board* _board = &board;
|
||||
Prefs* _prefs = &prefs;
|
||||
Callbacks* _callbacks = &callbacks;
|
||||
void run(const char* command, char* reply) {
|
||||
if (false) {
|
||||
@CLI@
|
||||
}
|
||||
}
|
||||
};
|
||||
int main() {
|
||||
CLI cli;
|
||||
struct { char text[160] = {}; unsigned guard = 42; } reply;
|
||||
cli.run("start ota invalid", reply.text);
|
||||
assert(strstr(reply.text, "usage") && callbacks.pauses == 0 && board.starts == 0);
|
||||
callbacks.allow_pause = false;
|
||||
cli.run("start ota", reply.text);
|
||||
assert(strcmp(reply.text, "ERR: could not pause ESP-NOW for OTA") == 0);
|
||||
assert(board.starts == 0 && callbacks.espnow && callbacks.uart);
|
||||
callbacks.allow_pause = true;
|
||||
callbacks.web = true;
|
||||
cli.run("start ota ap", reply.text);
|
||||
assert(ota && !callbacks.espnow && callbacks.uart);
|
||||
assert(strstr(reply.text, "; ESP-NOW paused") && strstr(reply.text, "; WebConfig stopped"));
|
||||
assert(prefs.bridge_enabled && prefs.espnow_bridge_enabled);
|
||||
const int resumed_before_failure = callbacks.resumes;
|
||||
board.allow_stop = false;
|
||||
cli.run("stop ota", reply.text);
|
||||
assert(strcmp(reply.text, "ERR: OTA upload active") == 0);
|
||||
assert(callbacks.resumes == resumed_before_failure && !callbacks.espnow);
|
||||
board.allow_stop = true;
|
||||
cli.run("stop ota", reply.text);
|
||||
assert(!ota && callbacks.espnow && callbacks.uart);
|
||||
assert(strstr(reply.text, "; ESP-NOW resumed"));
|
||||
board.allow_start = false;
|
||||
cli.run("start ota", reply.text);
|
||||
assert(strcmp(reply.text, "ERR: OTA WiFi failed") == 0 && callbacks.espnow);
|
||||
callbacks.allow_resume = false;
|
||||
cli.run("start ota", reply.text);
|
||||
assert(strcmp(reply.text, "ERR: OTA WiFi failed; ESP-NOW resume failed") == 0);
|
||||
assert(!callbacks.espnow && callbacks.uart);
|
||||
cli.run("stop ota", reply.text);
|
||||
assert(strstr(reply.text, "; ESP-NOW resume failed"));
|
||||
// A disabled secondary must stay disabled after the upload stops.
|
||||
prefs.espnow_bridge_enabled = false;
|
||||
callbacks.allow_resume = true;
|
||||
board.allow_start = true;
|
||||
const int resumes = callbacks.resumes;
|
||||
cli.run("start ota", reply.text);
|
||||
assert(!strstr(reply.text, "ESP-NOW") && !callbacks.espnow);
|
||||
cli.run("stop ota", reply.text);
|
||||
assert(callbacks.resumes == resumes && !callbacks.espnow);
|
||||
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")
|
||||
|
||||
def test_background_bridge_retry_waits_until_ota_stops(self):
|
||||
fixture = r'''
|
||||
#include <cassert>
|
||||
|
||||
@@ -52,9 +52,20 @@ build_flags =
|
||||
-D ADVERT_LON=0.0
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
; Runtime bridge is off until enabled with `set bridge.enabled on`.
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
; GPIO5 is the LoRa reset line; keep the bridge on GPIO16/17.
|
||||
-D WITH_RS232_BRIDGE_RX=16
|
||||
-D WITH_RS232_BRIDGE_TX=17
|
||||
-D RS232_BRIDGE_MERGED=1
|
||||
-D WITH_ESPNOW_BRIDGE=1
|
||||
-D ESPNOW_BRIDGE_MERGED=1
|
||||
; -D MESH_PACKET_LOGGING=1
|
||||
; -D MESH_DEBUG=1
|
||||
build_src_filter = ${MKE_s3.build_src_filter}
|
||||
+<helpers/bridges/RS232Bridge.cpp>
|
||||
+<helpers/bridges/ESPNowBridge.cpp>
|
||||
+<helpers/ui/SSD1306Display.cpp>
|
||||
+<../examples/simple_repeater>
|
||||
lib_deps =
|
||||
@@ -73,6 +84,7 @@ build_flags =
|
||||
-D ADMIN_PASSWORD='"password"'
|
||||
-D MAX_NEIGHBOURS=50
|
||||
-D WITH_RS232_BRIDGE=Serial2
|
||||
-D WITH_RS232_BRIDGE_UART=2
|
||||
; Wire the bridge UART to GPIO16/17; GPIO5 is the LoRa reset line.
|
||||
-D WITH_RS232_BRIDGE_RX=16
|
||||
-D WITH_RS232_BRIDGE_TX=17
|
||||
|
||||
Reference in new issue
Block a user