diff --git a/build_legacy.sh b/build_legacy.sh index a0468ea1e..5013bdb7e 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -2460,8 +2460,8 @@ get_unified_full_infrastructure_target() { # These plain Full recipes retain more routing capacity than their MQTT # siblings (T-Beam flood rules / room neighbors, TLora repeater neighbors). - # Keep them available for an explicit exact-identity build; the ordinary - # release chooses the MQTT observer for these board/role groups. + # T-Beam keeps the observer as its ordinary release choice. TLora retains + # both identities: its UART cannot fit alongside MQTT and ESP-NOW. case "${mqtt_base,,}" in tbeam_sx1262_repeater|tbeam_sx1276_repeater|\ tbeam_sx1262_room_server|tbeam_sx1276_room_server|\ @@ -6372,7 +6372,8 @@ get_esp32_full_profile_target() { echo "$target" } -# The ordinary release offers one Full image per physical board and role. +# The ordinary release combines Full images per physical board and role +# where the runtime memory budget permits all transports. # Keep the exact environment names for direct --full-exact builds and for # legacy/partition migration packages; this selection applies only to bulk # Full passes. Include the board in the key so similarly named recipes cannot @@ -6416,6 +6417,12 @@ get_ordinary_full_group_key() { *_repeater|*_room_server) ;; *) role_base=$base ;; esac + # Real qualification found the triple-transport TLora image 6496 bytes + # short of its required internal heap. Preserve UART and MQTT as two Full + # identities, with ESP-NOW available in each. + if [ "${base,,}" = lilygo_tlora_v2_1_1_6_repeater_observer_mqtt ]; then + role_base=$base + fi printf '%s|%s\n' "${PIO_ENV_BOARD_BY_NAME[$target]:-$target}" "${role_base,,}" } @@ -6425,7 +6432,7 @@ get_ordinary_full_priority() { case "${base,,}" in *_observer_mqtt) echo 20 ;; *_bridge_espnow) echo 10 ;; - tbeam_sx1262_repeater|tbeam_sx1276_repeater|lilygo_tlora_v2_1_1_6_repeater) echo 0 ;; + tbeam_sx1262_repeater|tbeam_sx1276_repeater) echo 0 ;; *) # G2's deployed Full identity is the observer. Other audited plain # targets keep their own identity, with observer sources where needed. diff --git a/docs/_javascript/firmware_picker.js b/docs/_javascript/firmware_picker.js index 58e9ced30..33ec970c9 100644 --- a/docs/_javascript/firmware_picker.js +++ b/docs/_javascript/firmware_picker.js @@ -620,6 +620,11 @@ return (profiles || []).filter(function (profile) { const target = String(profile && profile.target || ""); + // TLora cannot fit UART, MQTT and ESP-NOW together. Its normal Full + // UART image and Full MQTT observer are both intentional choices. + if (/^LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_?-full-usb-wifi$/i.test(target)) { + return true; + } // Same-partition ESP32 migrations publish the ordinary target's Full // image with its established mOTA identity. The former observer-named // Full image remains in the release for deployed devices that still use diff --git a/docs/cli_build_matrix.md b/docs/cli_build_matrix.md index f0fbda031..b5d5433d3 100644 --- a/docs/cli_build_matrix.md +++ b/docs/cli_build_matrix.md @@ -49,8 +49,11 @@ The direct observer and Full-matrix paths use the same `full-usb-wifi-ota` artifact and runtime output controls. The ordinary bulk release publishes one Full OTA identity per exact ESP32 board and role. Audited plain targets keep their identity while compiling the combined observer/ESP-NOW sources; G2 keeps -its deployed observer identity. T-Beam SX1262/SX1276 and TLora V2 choose the -observer in the ordinary release. Their plain Full recipes remain available +its deployed observer identity. T-Beam SX1262/SX1276 choose the observer in +the ordinary release. TLora V2 retains two Full images because UART, MQTT +and ESP-NOW together exceed its internal RAM budget: the normal repeater +provides UART plus ESP-NOW, and the observer provides MQTT plus ESP-NOW. +T-Beam plain Full recipes remain available as explicit alternatives with different routing capacities. Build one with `bash build.sh build-firmware --full-exact`, then deliberately install it over LoRa with `ota pull flash` and `ota install` on compatible @@ -204,10 +207,9 @@ available from a canonical image: target remains directly buildable with its bridge-on default but is omitted from bulk releases. Browser OTA pauses ESP-NOW while leaving RS-232 running; `stop ota` restores ESP-NOW when its saved setting is on. -- Full Heltec V3, Wireless Stick Lite V3, RAK3112 and LilyGo T-LoRa V2.1-1.6 - MQTT repeaters retain RS-232 alongside MQTT and ESP-NOW. The first three - use UART2 with RX on GPIO5 and TX on GPIO6; T-LoRa uses UART2 with RX on - GPIO34 and TX on GPIO25. Use `set rs232.enabled on|off` and +- Full Heltec V3, Wireless Stick Lite V3 and RAK3112 MQTT repeaters retain + RS-232 alongside MQTT and ESP-NOW. They use UART2 with RX on GPIO5 and + TX on GPIO6. Use `set rs232.enabled on|off` and `get rs232.running` independently of `mqtt.enabled` and `espnow.enabled`. `bridge.baud` and `bridge.uart` configure the UART. Old MQTT preferences do not enable this newly added UART; its saved intent needs a validated @@ -216,6 +218,12 @@ available from a canonical image: verifies the linked UART driver even when compiling observer sources for a normal repeater, and resumed release selection rejects Full images without that proof. +- LilyGo T-LoRa V2.1-1.6 retains a normal Full UART/ESP-NOW repeater and a + separate Full MQTT/ESP-NOW observer. Real triple-transport qualification + fell 6,496 bytes short of the required internal heap, including the UART + driver allowance. The normal UART remains on RX GPIO34 and TX GPIO25; + use `bridge.enabled` or `rs232.enabled` to control it. Both Full identities + remain in bulk releases instead of reducing routing capacity or RAM margins. - 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 diff --git a/docs/espnow_bridge_setup.md b/docs/espnow_bridge_setup.md index a3baa94eb..0a33c6b55 100644 --- a/docs/espnow_bridge_setup.md +++ b/docs/espnow_bridge_setup.md @@ -26,7 +26,8 @@ Open each board's [USB console](https://flasher.meshcore.io/console) at get bridge.type ``` -The answer can be `espnow`, `mqtt+espnow`, or `rs232+espnow`. Confirm the +The answer can be `espnow`, `mqtt+espnow`, `rs232+espnow`, or +`mqtt+rs232+espnow`, depending on the image. Confirm the independent ESP-NOW control is present with `get espnow.enabled` and `get espnow.running`. The `espnow` commands select this transport without changing another bridge in a combined image. diff --git a/docs/firmware_picker.md b/docs/firmware_picker.md index 5c1f8f25b..3e9505d7d 100644 --- a/docs/firmware_picker.md +++ b/docs/firmware_picker.md @@ -208,9 +208,11 @@ supported, `set mqtt.enabled on|off` for MQTT. Newly combined ordinary repeaters start with ESP-NOW off until explicitly enabled, including the first upgrade from unmarked legacy preferences. Later reboots preserve that setting. On combined RS-232 boards without MQTT, `bridge.enabled` controls the UART. -Full Heltec V3, WSL3, RAK3112 and T-LoRa V2.1 MQTT images also retain RS-232; +Full Heltec V3, WSL3 and RAK3112 MQTT images also retain RS-232; use `set rs232.enabled on|off` and `get rs232.running` for their independent -UART. MQTT and ESP-NOW use one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must +UART. T-LoRa V2.1 keeps two Full choices due to its internal RAM limit: +the normal repeater has UART plus ESP-NOW, and the observer has MQTT plus +ESP-NOW. MQTT and ESP-NOW use one 2.4 GHz radio, so when both are enabled the ESP-NOW bridge channel must match the connected WiFi access point's fixed channel. ESP-NOW-only mode does not require WiFi credentials. Its runtime `bridge.format` setting chooses the peer protocol: `wrapped` (the diff --git a/docs/research/firmware_memory_budget.md b/docs/research/firmware_memory_budget.md index b7c32e989..b48aa88c2 100644 --- a/docs/research/firmware_memory_budget.md +++ b/docs/research/firmware_memory_budget.md @@ -30,6 +30,10 @@ MQTT connections, OTA scratch and transient allocations. ESP32 images with RS-232 reserve another 8 KiB even when UART defaults off: 4 KiB for the source-bounded bridge object and 4 KiB for the UART driver and buffers. This keeps runtime UART enablement in the budget beside MQTT and ESP-NOW. +T-LoRa V2.1-1.6 cannot fit all three transports: the qualified linked triple +image had 190,488 available internal bytes against 196,984 required bytes. +Its ordinary release retains separate UART/ESP-NOW and MQTT/ESP-NOW Full +images; the memory policy and routing capacities remain unchanged. A 160x80 ST7735 framebuffer needs 25,602 bytes; an OLED allowance is 4 KiB. nRF52 Full with that color framebuffer must have at least 72 KiB available before startup allocations. diff --git a/scripts/build_local_release.py b/scripts/build_local_release.py index e9567ba8a..61d4fbc41 100644 --- a/scripts/build_local_release.py +++ b/scripts/build_local_release.py @@ -154,8 +154,17 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]: # These normal repeaters already supply a UART. Full MQTT source # substitution must never remove it while retiring the dedicated image. + # TLora cannot fit all three transports; its normal and observer Full + # images remain separate, and only its normal image requires UART. uart_full_targets = {"heltec_v3_repeater", "heltec_wsl3_repeater", "rak_3112_repeater", "lilygo_tlora_v2_1_1_6_repeater"} + tlora_uart_target = "lilygo_tlora_v2_1_1_6_repeater" + + def requires_uart_full(target: str) -> bool: + base = role_base(target) + return (base in uart_full_targets + and (base != tlora_uart_target or target.rstrip("_").lower() == base)) + combined_rs232 = {role_base(record["manifest"]["target"]) for record in records if proven_rs232(record["manifest"])} & uart_full_targets @@ -227,6 +236,8 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]: base = bridge_base kind = "bridge" key = base.lower() + if key == tlora_uart_target and kind == "observer": + key = target.rstrip("_").lower() # G2's observer is the deployed Full identity. The other listed # observer/bridge recipes offer features that their plain images do # not combine, so keep the richer ordinary release choice. @@ -234,7 +245,6 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]: "station_g2_repeater", "station_g2_room_server", "tbeam_sx1262_repeater", "tbeam_sx1276_repeater", "tbeam_sx1262_room_server", "tbeam_sx1276_room_server", - "lilygo_tlora_v2_1_1_6_repeater", } # Capacity exceptions may still need a dedicated bridge. Old plain # Meshadventurer images without the linked driver must not supersede it. @@ -253,7 +263,7 @@ def select_ordinary_full_records(records: list[dict]) -> list[dict]: selected = [item[1] for item in chosen.values()] for record in selected: manifest = record["manifest"] - if role_base(manifest["target"]) in uart_full_targets and not proven_rs232(manifest): + if requires_uart_full(manifest["target"]) and not proven_rs232(manifest): raise ValueError(manifest["target"] + ": Full release requires the verified " "RS232 bridge from its normal repeater") return passthrough + selected diff --git a/test/test_build_profiles.sh b/test/test_build_profiles.sh index 5dee5289d..eee37059a 100644 --- a/test/test_build_profiles.sh +++ b/test/test_build_profiles.sh @@ -347,7 +347,6 @@ for env_name, rx, tx in ( ("Heltec_v3_repeater_observer_mqtt", 5, 6), ("Heltec_WSL3_repeater_observer_mqtt", 5, 6), ("RAK_3112_repeater_observer_mqtt", 5, 6), - ("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", 34, 25), ): require(env_name, "build_flags", "WITH_MQTT_BRIDGE=1") require(env_name, "build_flags", "WITH_RS232_BRIDGE=Serial2") @@ -358,6 +357,14 @@ for env_name, rx, tx in ( require(env_name, "build_src_filter", "helpers/bridges/RS232Bridge.cpp") reject(env_name, "build_flags", "RS232_BRIDGE_DEFAULT_ON") +# The T-LoRa runtime RAM budget cannot fit MQTT, ESP-NOW and UART together. +# Keep its UART in the normal Full image and its MQTT recipe independent. +tlora_observer = "LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_" +require(tlora_observer, "build_flags", "WITH_MQTT_BRIDGE=1") +reject(tlora_observer, "build_flags", "WITH_RS232_BRIDGE") +reject(tlora_observer, "build_flags", "RS232_BRIDGE_MERGED") +reject(tlora_observer, "build_src_filter", "helpers/bridges/RS232Bridge.cpp") + # 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. @@ -1086,10 +1093,28 @@ done [ "$(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" +tlora_normal=LilyGo_TLora_V2_1_1_6_repeater +tlora_observer=${tlora_normal}_observer_mqtt_ +tlora_espnow=${tlora_normal}_bridge_espnow +[ "$(get_ordinary_full_group_key "$tlora_normal")" \ + != "$(get_ordinary_full_group_key "$tlora_observer")" ] \ + || fail "TLora RAM exception collapsed its independent UART and MQTT Full groups" +for tlora_order in normal-first observer-first bridge-first; do + case "$tlora_order" in + normal-first) tlora_candidates=("$tlora_normal" "$tlora_observer" "$tlora_espnow") ;; + observer-first) tlora_candidates=("$tlora_observer" "$tlora_espnow" "$tlora_normal") ;; + bridge-first) tlora_candidates=("$tlora_espnow" "$tlora_normal" "$tlora_observer") ;; + esac + mapfile -t tlora_selected < <(select_ordinary_full_targets "${tlora_candidates[@]}") + [ "${#tlora_selected[@]}" = 2 ] \ + || fail "TLora $tlora_order did not retain exactly two capacity-safe Full images" + [[ " ${tlora_selected[*]} " == *" $tlora_normal "* ]] \ + || fail "TLora $tlora_order discarded its UART Full image" + [[ " ${tlora_selected[*]} " == *" $tlora_observer "* ]] \ + || fail "TLora $tlora_order discarded its MQTT Full image" + [[ " ${tlora_selected[*]} " != *" $tlora_espnow "* ]] \ + || fail "TLora $tlora_order retained the replaced dedicated ESP-NOW image" +done ESP32_FULL_BUILD=0 PLATFORMIO_BUILD_FLAGS="" PLATFORMIO_BUILD_SRC_FILTER="" @@ -1626,7 +1651,7 @@ fi ( BUILD_PROFILE_FOR_TARGET=full for uart_target in Heltec_v3_repeater Heltec_WSL3_repeater RAK_3112_repeater \ - LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_; do + LilyGo_TLora_V2_1_1_6_repeater; do BUILD_EXPECTATIONS=() declare_build_capability_contract "$uart_target" ESP32_PLATFORM [[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.rs232=_ZN11RS232Bridge5beginEv"* ]] \ @@ -1634,6 +1659,12 @@ fi [[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.espnow=_ZN12ESPNowBridge5beginEv"* ]] \ || fail "$uart_target Full contract omitted the linked ESP-NOW driver" done + BUILD_EXPECTATIONS=() + declare_build_capability_contract LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_ ESP32_PLATFORM + [[ " ${BUILD_EXPECTATIONS[*]} " != *"bridge.rs232="* ]] \ + || fail "TLora MQTT capacity exception falsely promised a linked UART driver" + [[ " ${BUILD_EXPECTATIONS[*]} " == *"bridge.espnow=_ZN12ESPNowBridge5beginEv"* ]] \ + || fail "TLora MQTT capacity exception omitted its linked ESP-NOW driver" ) # A WiFi base's final -UENABLE_OTA must not win over the Full Companion's diff --git a/test/test_firmware_picker.js b/test/test_firmware_picker.js index c197cc35d..1b2125bf0 100644 --- a/test/test_firmware_picker.js +++ b/test/test_firmware_picker.js @@ -1287,7 +1287,7 @@ console.log('combined UART/ESP-NOW picker modes, independent commands and histor // MQTT, ESP-NOW and UART coexist in the same qualified Full artifact. UART // instructions must never use MQTT's historical ESP-NOW bridge alias. const tripleTargets = ['Heltec_v3_repeater', 'Heltec_WSL3_repeater', - 'RAK_3112_repeater', 'LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_'] + 'RAK_3112_repeater'] .map(target => target + '-full-usb-wifi'); const tripleCatalog = picker.buildCatalog([ release(bridgeFamily, '2026-10-05T00:00:00Z', tripleTargets.map(target => @@ -1318,6 +1318,52 @@ for (const profile of tripleCatalog.profiles) { assert.strictEqual(tripleCatalog.profiles.length, tripleTargets.length); console.log('three-transport Full picker uses independent UART, MQTT and ESP-NOW controls'); +// T-LoRa cannot fit all three transports in its runtime RAM budget. Both +// capacity-safe Full images remain selectable with their actual capabilities. +const tloraNormalTarget = 'LilyGo_TLora_V2_1_1_6_repeater-full-usb-wifi'; +const tloraObserverTarget = 'LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_-full-usb-wifi'; +const tloraCatalog = picker.buildCatalog([ + release(bridgeFamily, '2026-10-05T00:00:00Z', [tloraNormalTarget, tloraObserverTarget] + .map(target => asset(target + '-ota-' + bridgeFamily + '.bin'))), +], { + familyTag: bridgeFamily, + profiles: { + [tloraNormalTarget]: { + platform: 'ESP32_PLATFORM', rs232: true, mqtt: false, espnowBridge: true, + updateMethods: ['wifi', 'lora'], + loggingModes: ['none', 'usb'], loggingControl: 'usb.logging', + }, + [tloraObserverTarget]: { + platform: 'ESP32_PLATFORM', rs232: false, mqtt: true, espnowBridge: true, + updateMethods: ['wifi', 'lora'], + loggingModes: ['none', 'usb', 'wifi', 'both'], loggingControl: 'logging.output', + }, + }, +}); +assert.strictEqual(tloraCatalog.profiles.length, 2); +const tloraNormalProfile = tloraCatalog.profiles.find(profile => profile.target === tloraNormalTarget); +const tloraObserverProfile = tloraCatalog.profiles.find(profile => profile.target === tloraObserverTarget); +assert(tloraNormalProfile && tloraObserverProfile); +assert.deepStrictEqual(picker.profileFieldValues(tloraNormalProfile, 'mode'), + ['standard', 'rs232', 'espnow']); +assert.deepStrictEqual(picker.profileFieldValues(tloraObserverProfile, 'mode'), + ['standard', 'espnow']); +assert.deepStrictEqual(picker.profileFieldValues(tloraNormalProfile, 'logging'), ['none', 'usb']); +assert.deepStrictEqual(picker.profileFieldValues(tloraObserverProfile, 'logging'), + ['none', 'usb', 'wifi', 'both']); +assert(!picker.profileMatches(tloraNormalProfile, {logging: 'wifi'}, ['logging'])); +assert(picker.profileMatches(tloraObserverProfile, {logging: 'wifi'}, ['logging'])); +const tloraNormalDirections = picker.runtimeDirections(tloraNormalProfile, {mode: 'rs232'}); +const tloraObserverDirections = picker.runtimeDirections(tloraObserverProfile, {mode: 'espnow'}); +assert(tloraNormalDirections.some(section => /^RS232 bridge/.test(section.title))); +assert(tloraNormalDirections.some(section => section.title === 'ESP-NOW bridge')); +assert(!tloraNormalDirections.some(section => section.title === 'MQTT broker connections')); +assert(tloraObserverDirections.some(section => section.title === 'MQTT broker connections')); +assert(tloraObserverDirections.some(section => section.title === 'ESP-NOW bridge')); +assert(!tloraObserverDirections.some(section => /^RS232 bridge/.test(section.title))); +assert(!commands(tloraObserverDirections).some(command => command.startsWith('set rs232.'))); +console.log('T-LoRa RAM exception preserves separate UART and MQTT Full picker choices'); + // Partition expansion uses the packager's lookup and published utility assets, // not hardware labels or guessed slugs (several historical names differ). const migrationFamily = currentControls.familyTag; diff --git a/test/test_local_release_full_selection.py b/test/test_local_release_full_selection.py index 508910b3d..c5cf85541 100644 --- a/test/test_local_release_full_selection.py +++ b/test/test_local_release_full_selection.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""The ordinary release has one ESP32 Full OTA identity per board and role.""" +"""Full selection preserves qualified transports and board capacity exceptions.""" import sys import unittest @@ -23,7 +23,9 @@ class ReleaseFullSelectionTest(unittest.TestCase): def test_full_uart_source_substitution_requires_linked_driver_proof(self): for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater", "LilyGo_TLora_V2_1_1_6_repeater"): - for suffix in ("", "_observer_mqtt", "_observer_mqtt_"): + suffixes = ("",) if base.startswith("LilyGo_TLora_") else ( + "", "_observer_mqtt", "_observer_mqtt_") + for suffix in suffixes: for fault in ("missing_capability", "missing_proof", "false_proof", "unverified"): image = record(base + suffix, capabilities=("bridge.rs232", "bridge.espnow")) manifest = image["manifest"] @@ -40,8 +42,7 @@ class ReleaseFullSelectionTest(unittest.TestCase): select_ordinary_full_records([image]) def test_new_combined_full_displaces_stale_plain_and_both_dedicated_bridges(self): - for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater", - "LilyGo_TLora_V2_1_1_6_repeater"): + for base in ("Heltec_v3_repeater", "Heltec_WSL3_repeater", "RAK_3112_repeater"): stale = record(base, capabilities=("bridge.espnow",)) primary = record(base + "_observer_mqtt_", capabilities=("bridge.rs232", "bridge.espnow")) @@ -52,6 +53,65 @@ class ReleaseFullSelectionTest(unittest.TestCase): with self.subTest(base=base, profile=profile): self.assertEqual(select_ordinary_full_records(inputs), [primary]) + def test_tlora_ram_exception_keeps_uart_and_mqtt_full_images(self): + base = "LilyGo_TLora_V2_1_1_6_repeater" + normal = record(base, capabilities=("bridge.rs232", "bridge.espnow")) + observer = record(base + "_observer_mqtt_", capabilities=("bridge.espnow",)) + for profile in ("standard", "full"): + legacy = [record(base + "_bridge_" + bridge, profile) + for bridge in ("rs232", "espnow")] + for inputs in ([normal, observer] + legacy, legacy + [observer, normal], + [observer] + legacy + [normal]): + with self.subTest(profile=profile, first=inputs[0]["manifest"]["target"]): + selected = select_ordinary_full_records(inputs) + self.assertCountEqual(selected, [normal, observer]) + self.assertEqual(len(selected), 2) + self.assertTrue(all(item is normal or item is observer for item in selected)) + + def test_tlora_observer_cannot_retire_uart_without_normal_driver_proof(self): + base = "LilyGo_TLora_V2_1_1_6_repeater" + observer = record(base + "_observer_mqtt_", capabilities=("bridge.espnow",)) + for profile in ("standard", "full"): + uart = record(base + "_bridge_rs232", profile, capabilities=("bridge.rs232",)) + wireless = record(base + "_bridge_espnow", profile, capabilities=("bridge.espnow",)) + for inputs in ([observer, uart, wireless], [wireless, uart, observer]): + with self.subTest(profile=profile): + self.assertCountEqual(select_ordinary_full_records(inputs), [observer, uart]) + for fault in ("missing_capability", "missing_proof", "false_proof", "unverified"): + normal = record(base, capabilities=("bridge.rs232", "bridge.espnow")) + manifest = normal["manifest"] + if fault == "missing_capability": + manifest["capabilities"].remove("bridge.rs232") + elif fault == "missing_proof": + manifest["verification"] = [] + elif fault == "false_proof": + manifest["verification"][0]["present"] = False + else: + manifest["verified"] = False + for inputs in ([normal, observer], [observer, normal]): + with self.subTest(fault=fault): + with self.assertRaisesRegex(ValueError, "verified RS232 bridge"): + select_ordinary_full_records(inputs) + + def test_tlora_legacy_espnow_needs_verified_replacement(self): + base = "LilyGo_TLora_V2_1_1_6_repeater" + for fault in ("missing_capability", "missing_proof", "false_proof", "unverified"): + observer = record(base + "_observer_mqtt_", capabilities=("bridge.espnow",)) + manifest = observer["manifest"] + if fault == "missing_capability": + manifest["capabilities"] = [] + elif fault == "missing_proof": + manifest["verification"] = [] + elif fault == "false_proof": + manifest["verification"][0]["present"] = False + else: + manifest["verified"] = False + for profile in ("standard", "full"): + legacy = record(base + "_bridge_espnow", profile, capabilities=("bridge.espnow",)) + with self.subTest(fault=fault, profile=profile): + self.assertCountEqual(select_ordinary_full_records([observer, legacy]), + [observer, legacy]) + def test_meshtower_sd_primary_filters_stale_internal_images_without_relabeling(self): primary = "Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors" variants = [] @@ -102,8 +162,8 @@ class ReleaseFullSelectionTest(unittest.TestCase): record("Station_G2_repeater_observer_mqtt"), record("Tbeam_SX1262_repeater_bridge_espnow"), record("Tbeam_SX1262_repeater_observer_mqtt", capabilities=("bridge.espnow",)), - record("LilyGo_TLora_V2_1_1_6_repeater"), - record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", capabilities=("bridge.rs232",)), + record("LilyGo_TLora_V2_1_1_6_repeater", capabilities=("bridge.rs232", "bridge.espnow")), + record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", capabilities=("bridge.espnow",)), record("Meshadventurer_sx1262_repeater"), record("Meshadventurer_sx1262_repeater_bridge_espnow"), record("RAK_4631_repeater", platform="NRF52_PLATFORM"), @@ -113,11 +173,12 @@ class ReleaseFullSelectionTest(unittest.TestCase): "Heltec_v3_repeater", "Heltec_v3_room_server", "Station_G2_repeater_observer_mqtt", "Tbeam_SX1262_repeater_observer_mqtt", + "LilyGo_TLora_V2_1_1_6_repeater", "LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", "Meshadventurer_sx1262_repeater_bridge_espnow", "RAK_4631_repeater", }) - self.assertEqual(len(selected), 8) # standard V3 remains alongside Full V3 + self.assertEqual(len(selected), 9) # standard V3 and both TLora Full profiles remain def test_mke_combined_repeater_filters_stale_bridge_identities_without_relabeling(self): primary = record("MKE_s3_repeater", capabilities=("bridge.rs232", "bridge.espnow")) @@ -253,7 +314,7 @@ class ReleaseFullSelectionTest(unittest.TestCase): def test_combined_observer_with_trailing_underscore_replaces_matching_espnow(self): observer = record("LilyGo_TLora_V2_1_1_6_repeater_observer_mqtt_", - capabilities=("bridge.espnow", "bridge.rs232")) + capabilities=("bridge.espnow",)) legacy = record("LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow", capabilities=("bridge.espnow",)) for inputs in ([observer, legacy], [legacy, observer]): diff --git a/variants/lilygo_tlora_v2_1/platformio.ini b/variants/lilygo_tlora_v2_1/platformio.ini index d521ae2c6..cbd8fd12b 100644 --- a/variants/lilygo_tlora_v2_1/platformio.ini +++ b/variants/lilygo_tlora_v2_1/platformio.ini @@ -191,12 +191,8 @@ build_flags = -D MAX_NEIGHBOURS=50 -D MAX_RECENT_REPEATERS=256 ; no-PSRAM observer must retain internal DRAM for MQTT/TLS -D WITH_MQTT_BRIDGE=1 - ; Keep the normal repeater UART in the combined Full image; disabled until enabled. - -D WITH_RS232_BRIDGE=Serial2 - -D WITH_RS232_BRIDGE_UART=2 - -D WITH_RS232_BRIDGE_RX=34 - -D WITH_RS232_BRIDGE_TX=25 - -D RS232_BRIDGE_MERGED=1 + ; UART plus MQTT and ESP-NOW exceeds the internal heap budget by 6496 bytes. + ; Keep UART in the separate normal Full repeater; retain MQTT/ESP-NOW here. -D WEBCONFIG_DISABLED=1 ; async portal exceeds this 4 MB target's dual-OTA app slot; use CLI setup -D FLOOD_CHANNEL_SCOPE_SLOTS=4 ; no-PSRAM observer must retain internal DRAM for MQTT/TLS -D MQTT_MAX_PACKET_SIZE=1024 @@ -215,7 +211,6 @@ build_flags = # -D MQTT_PASSWORD='"your-password"' build_src_filter = ${LilyGo_TLora_V2_1_1_6_core.build_src_filter} + - + + + +