diff --git a/docs/radio_profiles.md b/docs/radio_profiles.md index 4389c29c1..8a355188f 100644 --- a/docs/radio_profiles.md +++ b/docs/radio_profiles.md @@ -556,11 +556,18 @@ download can discard it. ### Meshtext channel chat `MESH_ENABLE_MESHTEXT_CHAT` enables the text-only Meshtext backend in the -Companion. It defaults on for ESP32-S3 **Full Companion** builds using -SX1262 LoRa and for the RAK4631 **Full Companion** starting with 1.17.1.10 -preview3. An ESP-NOW primary and other builds default off. RAK4631 1.17.1.10 -preview2 and earlier builds did not include this backend. This is not -a replacement for stock peer firmware or a cross-protocol bridge. +Companion. It defaults on for **Full Companion** builds on ESP32 and nRF52, +including classic ESP32, C3, C6, S3, T1000-E, XIAO nRF52, RAK3401, Nano G2, +T114 and the other Full families. The installed radio driver must support +Meshtext and the selected frequency/modulation. An ESP-NOW primary, non-Full +builds, and other platforms default off; an explicit build override is retained. +This default replaces the earlier ESP32-S3/SX1262 and RAK4631-only scope. +Older images can therefore lack the backend even when their channel editor +accepts an `mt-` name; a factory reset cannot add code omitted from an image. + +The common default and modem support do not establish physical qualification +for every board. Each release image must still pass its flash and runtime RAM +budgets, and hardware interoperability must be checked separately. In an enabled build, an `mt-` name always requests Meshtext, even if radio2 is off or setup is blocked. It never falls back to sending MeshCore frames. In a diff --git a/src/helpers/MeshtextConfig.h b/src/helpers/MeshtextConfig.h index a1321bcc4..33251a4fb 100644 --- a/src/helpers/MeshtextConfig.h +++ b/src/helpers/MeshtextConfig.h @@ -4,24 +4,14 @@ #include "EthernetTunnelConfig.h" #include "ota/OtaDirectConfig.h" -// Resolve the chip in every translation unit, including Dispatcher.cpp which -// includes this before Arduino.h. Otherwise different class layouts could be -// selected for the same build depending on include order. -#if !defined(MESH_ENABLE_MESHTEXT_CHAT) && defined(ESP32_PLATFORM) && defined(ARDUINO) - #include -#endif - -// Channel chat is an optional Companion transport, never a bridge. Qualified -// families are ESP32-S3 Full/SX1262 and RAK4631 Full; other builds retain their -// features and do not acquire the codec or its bounded raw-frame queue. A new -// family must qualify its modem implementation, flash and runtime RAM first. +// Full Companion includes channel chat on both supported platforms. Keep this +// independent of board/chip macros, which some Arduino variants reveal only +// after shared sources include this header. Every translation unit must select +// the same layouts. The concrete driver still gates modem/profile support; +// release builds must also pass their unchanged flash and runtime RAM budgets. #ifndef MESH_ENABLE_MESHTEXT_CHAT - #if defined(ESP32_PLATFORM) && defined(CONFIG_IDF_TARGET_ESP32S3) \ - && defined(COMPANION_RADIO_FULL) && defined(USE_SX1262) \ - && !(defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW) - #define MESH_ENABLE_MESHTEXT_CHAT 1 - #elif defined(NRF52_PLATFORM) && defined(RAK_4631) \ - && defined(COMPANION_RADIO_FULL) && defined(USE_SX1262) \ + #if (defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM)) \ + && defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL \ && !(defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW) #define MESH_ENABLE_MESHTEXT_CHAT 1 #else diff --git a/test/test_meshtext_config.py b/test/test_meshtext_config.py index afa2acb9a..bc435f268 100644 --- a/test/test_meshtext_config.py +++ b/test/test_meshtext_config.py @@ -38,7 +38,7 @@ class MeshtextConfigTest(unittest.TestCase): if arduino_first: includes.reverse() source = "".join("#include <" + name + ">\n" for name in includes) - source += feature + "\n" + source += "#if " + feature + "\n1\n#else\n0\n#endif\n" result = subprocess.run( [self.compiler, "-E", "-P", "-x", "c++", "-", "-I", str(work), "-I", str(ROOT / "src"), @@ -54,37 +54,47 @@ class MeshtextConfigTest(unittest.TestCase): self.assertEqual(self.effective(defines, arduino_first=arduino_first, **kwargs), str(expected)) - def test_rak4631_full_companion_is_enabled_in_every_translation_unit(self): - self.assert_orders(["NRF52_PLATFORM", "RAK_4631", "COMPANION_RADIO_FULL", "USE_SX1262"], 1) + def test_nrf52_full_companion_families_are_enabled_in_every_translation_unit(self): + for board, modem in ( + ("RAK_4631", "USE_SX1262"), + ("RAK_3401", "USE_SX1262"), + ("T1000_E", "USE_LR1110"), + ("XIAO_NRF52", "USE_SX1262"), + ("NANO_G2_ULTRA", "USE_SX1262"), + ("HELTEC_T114", "USE_SX1262"), + ("MUZI_BASE", "USE_LR1121"), + ): + self.assert_orders(["NRF52_PLATFORM", "COMPANION_RADIO_FULL=1", board, modem], 1) + # Shared sources may not know the board or radio chip yet. + self.assert_orders(["NRF52_PLATFORM", "COMPANION_RADIO_FULL=1"], 1) - def test_nrf52_default_stays_scoped_to_qualified_hardware_and_role(self): - enabled = ["NRF52_PLATFORM", "RAK_4631", "COMPANION_RADIO_FULL", "USE_SX1262"] - for missing in enabled: - self.assert_orders([item for item in enabled if item != missing], 0) - self.assert_orders(["NRF52_PLATFORM", "RAK_3401", "COMPANION_RADIO_FULL", "USE_SX1262"], 0) - self.assert_orders(enabled + ["MESH_PRIMARY_ESPNOW=1"], 0) - self.assert_orders(enabled + ["MESH_PRIMARY_ESPNOW=0"], 1) + def test_esp32_full_companion_does_not_depend_on_sdk_target_or_modem_macros(self): + for target in (None, "CONFIG_IDF_TARGET_ESP32", "CONFIG_IDF_TARGET_ESP32S2", + "CONFIG_IDF_TARGET_ESP32S3", "CONFIG_IDF_TARGET_ESP32C3", + "CONFIG_IDF_TARGET_ESP32C6"): + for modem in (None, "USE_SX1262", "USE_SX1276", "USE_SX1268", + "USE_LR1110", "USE_LR1121", "USE_LR2021", "USE_LLCC68"): + flags = ["ESP32_PLATFORM", "ARDUINO=10819", "COMPANION_RADIO_FULL=1"] + self.assert_orders(flags + ([modem] if modem else []), 1, sdk_target=target) - def test_esp32_s3_full_keeps_effective_sdk_target_enablement(self): - enabled = ["ESP32_PLATFORM", "ARDUINO=10819", "COMPANION_RADIO_FULL", "USE_SX1262"] - self.assert_orders(enabled, 1, sdk_target="CONFIG_IDF_TARGET_ESP32S3") - self.assert_orders(enabled + ["MESH_PRIMARY_ESPNOW=0"], 1, - sdk_target="CONFIG_IDF_TARGET_ESP32S3") - self.assert_orders(enabled + ["MESH_PRIMARY_ESPNOW=1"], 0, - sdk_target="CONFIG_IDF_TARGET_ESP32S3") - for target in (None, "CONFIG_IDF_TARGET_ESP32", "CONFIG_IDF_TARGET_ESP32S2"): - self.assert_orders(enabled, 0, sdk_target=target) - for missing in ("COMPANION_RADIO_FULL", "USE_SX1262"): - self.assert_orders([item for item in enabled if item != missing], 0, - sdk_target="CONFIG_IDF_TARGET_ESP32S3") + def test_defaults_keep_minimal_other_platforms_and_espnow_primary_disabled(self): + for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM"): + for full in ([], ["COMPANION_RADIO_FULL=0"]): + self.assert_orders([platform, "USE_SX1262", *full], 0) + enabled = [platform, "COMPANION_RADIO_FULL=1"] + self.assert_orders(enabled + ["MESH_PRIMARY_ESPNOW=1"], 0) + self.assert_orders(enabled + ["MESH_PRIMARY_ESPNOW=0"], 1) + for platform in ([], ["RP2040_PLATFORM"], ["STM32_PLATFORM"]): + self.assert_orders([*platform, "COMPANION_RADIO_FULL=1", "USE_SX1262"], 0) def test_explicit_enable_and_disable_override_defaults(self): self.assert_orders(["MESH_ENABLE_MESHTEXT_CHAT=1"], 1) - self.assert_orders(["NRF52_PLATFORM", "RAK_4631", "COMPANION_RADIO_FULL", - "USE_SX1262", "MESH_ENABLE_MESHTEXT_CHAT=0"], 0) - self.assert_orders(["ESP32_PLATFORM", "ARDUINO=10819", "COMPANION_RADIO_FULL", - "USE_SX1262", "MESH_ENABLE_MESHTEXT_CHAT=0"], 0, - sdk_target="CONFIG_IDF_TARGET_ESP32S3") + for platform in ("NRF52_PLATFORM", "ESP32_PLATFORM"): + self.assert_orders([platform, "COMPANION_RADIO_FULL=1", + "MESH_ENABLE_MESHTEXT_CHAT=0"], 0) + for overrides in (["COMPANION_RADIO_FULL=0"], + ["COMPANION_RADIO_FULL=1", "MESH_PRIMARY_ESPNOW=1"]): + self.assert_orders([platform, *overrides, "MESH_ENABLE_MESHTEXT_CHAT=1"], 1) def test_tx_beacon_wire_support_does_not_enable_companion_chat(self): for platform in ("ESP32_PLATFORM", "NRF52_PLATFORM", "RP2040_PLATFORM"): @@ -102,23 +112,30 @@ class MeshtextConfigTest(unittest.TestCase): for flags in (["STM32_PLATFORM", "USE_SX1262"], []): self.assert_orders(flags, 0, feature="MESH_ENABLE_MESHTEXT_WIRE") - def test_late_variant_chip_discovery_does_not_change_wire_layout(self): + def test_late_variant_chip_discovery_does_not_change_chat_or_wire_layout(self): with tempfile.TemporaryDirectory(prefix="meshcore-mt-late-variant-") as folder: work = Path(folder) - # Nano G2 Ultra declares USE_SX1262 in its Arduino variant header, - # not in global flags. The codec translation unit omits Arduino. - (work / "Arduino.h").write_text( - "#pragma once\n#define USE_SX1262 1\n", encoding="ascii") - for arduino_first in (False, True): - source = "#include \n" if arduino_first else "" - source += "#include \n" - source += "static_assert(MESH_ENABLE_MESHTEXT_CHAT == 0, \"chat stays disabled\");\n" - source += "static_assert(MESH_ENABLE_MESHTEXT_WIRE == 1, \"consistent wire layout\");\n" - result = subprocess.run( - [self.compiler, "-std=c++17", "-fsyntax-only", "-x", "c++", "-", - "-DNRF52_PLATFORM", "-I", str(work), "-I", str(ROOT / "src")], - input=source, capture_output=True, text=True) - self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + # Some variants expose their chip only through Arduino.h, while + # Dispatcher and codec translation units include this header first. + for platform in ("NRF52_PLATFORM", "ESP32_PLATFORM"): + for modem in ("USE_SX1262", "USE_LR1110", "USE_LR1121", "USE_SX1276"): + (work / "Arduino.h").write_text( + "#pragma once\n#define " + modem + " 1\n", encoding="ascii") + for full in (0, 1): + for includes in (("Arduino.h", "helpers/MeshtextConfig.h"), + ("helpers/MeshtextConfig.h", "Arduino.h"), + ("helpers/MeshtextConfig.h",)): + with self.subTest(platform=platform, modem=modem, + full=full, includes=includes): + source = "".join("#include <" + name + ">\n" for name in includes) + source += f'static_assert(MESH_ENABLE_MESHTEXT_CHAT == {full}, "consistent chat layout");\n' + source += 'static_assert(MESH_ENABLE_MESHTEXT_WIRE == 1, "consistent wire layout");\n' + result = subprocess.run( + [self.compiler, "-std=c++17", "-fsyntax-only", "-x", "c++", "-", + "-D" + platform, f"-DCOMPANION_RADIO_FULL={full}", + "-I", str(work), "-I", str(ROOT / "src")], + input=source, capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_actual_driver_capability_and_mc_only_no_spi(self): source = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text() @@ -184,15 +201,13 @@ int main() { self.assertEqual(result.returncode, 0, result.stdout + result.stderr) def test_chat_keeps_wire_support_with_beacons_disabled(self): - flags = ["NRF52_PLATFORM", "RAK_4631", "COMPANION_RADIO_FULL", "USE_SX1262", - "MESH_ENABLE_TX_BEACONS=0"] - self.assert_orders(flags, 1) - self.assert_orders(flags, 1, feature="MESH_ENABLE_MESHTEXT_WIRE") - flags = ["ESP32_PLATFORM", "ARDUINO=10819", "COMPANION_RADIO_FULL", "USE_SX1262", - "MESH_ENABLE_TX_BEACONS=0"] - self.assert_orders(flags, 1, sdk_target="CONFIG_IDF_TARGET_ESP32S3") - self.assert_orders(flags, 1, feature="MESH_ENABLE_MESHTEXT_WIRE", - sdk_target="CONFIG_IDF_TARGET_ESP32S3") + for platform in ("NRF52_PLATFORM", "ESP32_PLATFORM"): + flags = [platform, "COMPANION_RADIO_FULL=1", "MESH_ENABLE_TX_BEACONS=0"] + self.assert_orders(flags, 1) + self.assert_orders(flags, 1, feature="MESH_ENABLE_MESHTEXT_WIRE") + self.assert_orders(flags, 1, feature="MESH_ENABLE_FOREIGN_RADIO") + self.assert_orders([platform, "COMPANION_RADIO_FULL=0", "MESH_ENABLE_TX_BEACONS=0"], + 0, feature="MESH_ENABLE_MESHTEXT_WIRE") if __name__ == "__main__":