diff --git a/build_legacy.sh b/build_legacy.sh index fd1d65eb5..59d6270ab 100755 --- a/build_legacy.sh +++ b/build_legacy.sh @@ -506,6 +506,8 @@ for section, options in data: Ebyte_EoRa-S3_companion_radio_ble Tbeam_SX1262_companion_radio_ble Tbeam_SX1276_companion_radio_ble + Tbeam_companion_radio_ble + LilyGo_T3S3_companion_radio_ble T_Beam_S3_Supreme_SX1262_companion_radio_ble heltec_v4_expansionkit_tft_companion_radio_ble_femon ) @@ -2482,8 +2484,8 @@ get_unified_full_infrastructure_target() { # 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|\ + tbeam_repeater|tbeam_sx1262_repeater|tbeam_sx1276_repeater|\ + tbeam_room_server|tbeam_sx1262_room_server|tbeam_sx1276_room_server|\ lilygo_tlora_v2_1_1_6_repeater) return 1 ;; esac @@ -3100,12 +3102,13 @@ requires_dram_limited_neighbors() { heltec_v2_repeater_lora_ota_no_external_sensors|\ meshadventurer_sx1262_repeater_lora_ota_no_external_sensors|\ meshadventurer_sx1268_repeater_lora_ota_no_external_sensors|\ + tbeam_repeater_lora_ota_no_external_sensors|\ tbeam_sx1262_repeater_lora_ota_no_external_sensors|\ tbeam_sx1276_repeater_lora_ota_no_external_sensors) return 0 ;; generic_e22_sx1262_repeater|generic_e22_sx1268_repeater|\ heltec_v2_repeater|meshadventurer_sx1262_repeater|\ - meshadventurer_sx1268_repeater|tbeam_sx1262_repeater|\ + meshadventurer_sx1268_repeater|tbeam_repeater|tbeam_sx1262_repeater|\ tbeam_sx1276_repeater) if [ "$ESP32_FULL_BUILD" = "1" ]; then return 0; fi ;; esac @@ -3123,6 +3126,7 @@ requires_dram_limited_neighbors() { |lilygo_tlora_v2_1_1_6_repeater_bridge_rs232 \ |meshadventurer_sx1262_repeater_bridge_espnow \ |meshadventurer_sx1268_repeater_bridge_espnow \ + |tbeam_repeater_bridge_espnow \ |tbeam_sx1262_repeater_bridge_espnow \ |tbeam_sx1276_repeater_bridge_espnow) return 0 @@ -3132,7 +3136,7 @@ requires_dram_limited_neighbors() { case "${1,,}" in lilygo_tlora_v2_1_1_6_repeater_observer_mqtt_) return 0 ;; - tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt|rak_3x72_repeater|tiny_relay_repeater|wio-e5-mini_repeater|wio-e5-repeater_bridge_rs232|wio-e5_repeater) return 0 ;; + tbeam_repeater_observer_mqtt|tbeam_room_server_observer_mqtt|tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt|rak_3x72_repeater|tiny_relay_repeater|wio-e5-mini_repeater|wio-e5-repeater_bridge_rs232|wio-e5_repeater) return 0 ;; *) return 1 ;; esac } @@ -3438,6 +3442,7 @@ is_esp32_partition_migration_full_target() { ebyte_eora-s3_repeater|ebyte_eora-s3_room_server|\ generic_e22_sx1262_repeater|generic_e22_sx1268_repeater|\ heltec_ct62_repeater|heltec_ct62_sensor|\ + lilygo_t3s3_repeater|lilygo_t3s3_room_server|\ lilygo_t3s3_sx1262_repeater|lilygo_t3s3_sx1262_room_server|\ lilygo_t3s3_sx1276_repeater|lilygo_t3s3_sx1276_room_server|\ station_g2_repeater|station_g2_logging_repeater|station_g2_room_server|\ @@ -3846,6 +3851,8 @@ apply_esp32_constrained_companion_size_profile() { station_g2_companion_radio_ble \ |thinknode_m2_companion_radio_ble \ |thinknode_m2_companion_radio_wifi \ + |lilygo_t3s3_companion_radio_ble \ + |lilygo_t3s3_companion_radio_ble_ps \ |lilygo_t3s3_sx1262_companion_radio_ble \ |lilygo_t3s3_sx1262_companion_radio_ble_ps \ |lilygo_t3s3_sx1276_companion_radio_ble \ @@ -3872,8 +3879,8 @@ apply_esp32_iram_advert_storage_profile() { lilygo_tlora_v2_1_1_6_repeater_observer_mqtt|lilygo_tlora_v2_1_1_6_repeater_observer_mqtt_|\ lilygo_tlora_v2_1_1_6_room_server|lilygo_tlora_v2_1_1_6_room_server_|\ lilygo_tlora_v2_1_1_6_room_server_observer_mqtt|lilygo_tlora_v2_1_1_6_room_server_observer_mqtt_|\ - tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt|\ - tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt) + tbeam_repeater_observer_mqtt|tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt|\ + tbeam_room_server_observer_mqtt|tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt) export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFLOOD_ADVERT_IRAM_WORD_STORAGE=1" ;; esac @@ -3909,13 +3916,13 @@ apply_esp32_full_size_profile() { case "${env_name,,}" in lilygo_tlora_v2_1_1_6_room_server|lilygo_tlora_v2_1_1_6_room_server_|\ lilygo_tlora_v2_1_1_6_room_server_observer_mqtt|lilygo_tlora_v2_1_1_6_room_server_observer_mqtt_|\ - tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt) + tbeam_room_server_observer_mqtt|tbeam_sx1262_room_server_observer_mqtt|tbeam_sx1276_room_server_observer_mqtt) # Preserve all 32 Room posts in genuine byte-accessible RTC SLOW memory. # The bounded proof implementation serves the same maximum 4096 leaves # without retaining the legacy 16 KiB leaf cache alongside Room services. export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DROOM_POSTS_RTC_SLOW_STORAGE=1 -DOTA_BOUNDED_SOURCE_PROOFS=1" ;; - tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt) + tbeam_repeater_observer_mqtt|tbeam_sx1262_repeater_observer_mqtt|tbeam_sx1276_repeater_observer_mqtt) # MQTT/TLS plus OTA left only 3.3 KiB of static-region headroom with # 63 rules. Retain the complete rule engine with a 31-entry table. append_platformio_build_unflags "-DFLOOD_PACKET_FILTER_SLOTS=63" @@ -3929,6 +3936,7 @@ apply_esp32_full_size_profile() { lilygo_tlora_v2_1_1_6_repeater_bridge_espnow|\ meshadventurer_sx1262_repeater_bridge_espnow|\ meshadventurer_sx1268_repeater_bridge_espnow|\ + tbeam_repeater_bridge_espnow|\ tbeam_sx1262_repeater_bridge_espnow|\ tbeam_sx1276_repeater_bridge_espnow) if [ "${PACKET_LOGGING_OVERRIDE,,}" = "on" ]; then @@ -7132,7 +7140,7 @@ get_ordinary_full_priority() { case "${base,,}" in *_observer_mqtt) echo 20 ;; *_bridge_espnow) echo 10 ;; - tbeam_sx1262_repeater|tbeam_sx1276_repeater) echo 0 ;; + tbeam_repeater|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/scripts/build_local_release.py b/scripts/build_local_release.py index 0b3228adf..04d173f41 100644 --- a/scripts/build_local_release.py +++ b/scripts/build_local_release.py @@ -578,6 +578,7 @@ 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", + "tbeam_repeater", "tbeam_room_server", } # Capacity exceptions may still need a dedicated bridge. Old plain # Meshadventurer images without the linked driver must not supersede it. diff --git a/scripts/consolidated_profiles.py b/scripts/consolidated_profiles.py index 228ea97bd..171aaf9d6 100644 --- a/scripts/consolidated_profiles.py +++ b/scripts/consolidated_profiles.py @@ -6,6 +6,18 @@ import sys from optional_display_profiles import resolved_defines, spec_for_defines SPECS = ( + dict(key="autoLoRaRadio", capability="radio.autodetect.sx126x_sx127x", + marker="meshcore.capability.radio.autodetect.sx126x_sx127x.v1", + platform="ESP32_PLATFORM", board="ttgo-t-beam", + target=r"Tbeam_(?:repeater(?:_bridge_espnow)?|room_server|kiss_modem|companion_radio_(?:ble|usb|wifi|full)(?:_ps)?)(?:_observer_mqtt)?", + defines={"MESH_RADIO_AUTO_FAMILY": "1", "RADIO_CLASS": "UniversalLoRaRadio", + "WRAPPER_CLASS": "UniversalLoRaRadio"}), + dict(key="autoLoRaRadio", capability="radio.autodetect.sx126x_sx127x", + marker="meshcore.capability.radio.autodetect.sx126x_sx127x.v1", + platform="ESP32_PLATFORM", board="t3_s3_v1_x", + target=r"LilyGo_T3S3_(?:repeater(?:_bridge_espnow)?|room_server|kiss_modem|terminal_chat|companion_radio_(?:ble|usb|wifi|full)(?:_ps)?)(?:_observer_mqtt)?", + defines={"MESH_RADIO_AUTO_FAMILY": "1", "RADIO_CLASS": "UniversalLoRaRadio", + "WRAPPER_CLASS": "UniversalLoRaRadio"}), dict(key="unifiedV4Display", capability="display.heltec_v4.unified", marker="meshcore.capability.display.heltec_v4.unified.v1", platform="ESP32_PLATFORM", board="heltec_v4", diff --git a/src/helpers/LoRaChipProbe.h b/src/helpers/LoRaChipProbe.h new file mode 100644 index 000000000..9cf72c9f9 --- /dev/null +++ b/src/helpers/LoRaChipProbe.h @@ -0,0 +1,45 @@ +#pragma once + +#include "LoRaRadioFamily.h" +#include +#include +#include + +namespace mesh { + +// SX1276/78 and RFM9x share this register layout. SX1272 (0x22) does not. +inline bool isSX1276Version(uint8_t version) { + return version == 0x12 || version == 0x13 || version == 0x11; +} + +// IO supplies reset(), readSX127Version(), ready(), readSX126Version(), +// now(), and pause(). Identification never starts RX, CAD or transmission. +template +LoRaRadioFamily probeLoRaChip(IO& io, uint32_t timeout_ms = 250) { + io.reset(); + const uint8_t version = io.readSX127Version(); + if (isSX1276Version(version) && io.readSX127Version() == version) + return LoRaRadioFamily::SX127x; + + // Discard the unrecognized command/status left on an SX126x by the first + // read. A reset enters RC standby, so no speculative mode-setting is needed. + io.reset(); + const uint32_t started = io.now(); + while (uint32_t(io.now() - started) < timeout_ms) { + if (io.ready()) { + char signature[6] = {}; + char confirm[6] = {}; + // SX1262 intentionally reports the SX1261 family signature in RadioLib. + if (io.readSX126Version(signature, sizeof(signature)) + && memcmp(signature, "SX1261", sizeof(signature)) == 0 + && io.ready() + && io.readSX126Version(confirm, sizeof(confirm)) + && memcmp(signature, confirm, sizeof(signature)) == 0) + return LoRaRadioFamily::SX126x; + } + io.pause(1); + } + return LoRaRadioFamily::None; +} + +} // namespace mesh diff --git a/src/helpers/LoRaRadioFamily.h b/src/helpers/LoRaRadioFamily.h new file mode 100644 index 000000000..be3a83aaf --- /dev/null +++ b/src/helpers/LoRaRadioFamily.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +namespace mesh { +enum class LoRaRadioFamily : uint8_t { None, SX126x, SX127x }; +} diff --git a/src/helpers/esp32/TBeamBoard.cpp b/src/helpers/esp32/TBeamBoard.cpp index 010b2ca32..26ccb8110 100644 --- a/src/helpers/esp32/TBeamBoard.cpp +++ b/src/helpers/esp32/TBeamBoard.cpp @@ -7,6 +7,10 @@ #include //#include +#if defined(MESH_RADIO_AUTO_FAMILY) && MESH_RADIO_AUTO_FAMILY +extern bool prepareUniversalLoRaRadio(); +#endif + uint32_t deviceOnline = 0x00; #if !defined(PORTABLE_MQTT_OBSERVER) || defined(TBEAM_SUPREME_SX1262) @@ -42,17 +46,28 @@ void TBeamBoard::begin() { digitalWrite(P_LORA_TX_LED, HIGH); //inverted pin for SX1276 - HIGH for off #endif - //radiotype_detect(); + // The radio rail must be powered before probing. Selection also supplies + // the actual wake IRQ before regulator/tuning setup runs in the role. +#if defined(MESH_RADIO_AUTO_FAMILY) && MESH_RADIO_AUTO_FAMILY + prepareUniversalLoRaRadio(); +#endif esp_reset_reason_t reason = esp_reset_reason(); if (reason == ESP_RST_DEEPSLEEP) { uint64_t wakeup_source = esp_sleep_get_ext1_wakeup_status(); - if (wakeup_source & (1ULL << P_LORA_DIO_1)) { // received a LoRa packet (while in deep sleep) +#if defined(MESH_RADIO_AUTO_FAMILY) && MESH_RADIO_AUTO_FAMILY + const uint32_t radio_irq = getIRQGpio(); +#else + const uint32_t radio_irq = P_LORA_DIO_1; +#endif + if (radio_irq != static_cast(-1) + && (wakeup_source & (1ULL << radio_irq))) { startup_reason = BD_STARTUP_RX_PACKET; } mesh::esp32::releaseRtcGpioIfSupported((gpio_num_t)P_LORA_NSS); - mesh::esp32::deinitRtcGpioIfSupported((gpio_num_t)P_LORA_DIO_1); + if (radio_irq != static_cast(-1)) + mesh::esp32::deinitRtcGpioIfSupported((gpio_num_t)radio_irq); } } diff --git a/src/helpers/radiolib/ESP32LoRaChipProbe.h b/src/helpers/radiolib/ESP32LoRaChipProbe.h new file mode 100644 index 000000000..e02ff086b --- /dev/null +++ b/src/helpers/radiolib/ESP32LoRaChipProbe.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace mesh { + +struct LoRaChipProbePins { + uint32_t nss; + uint32_t reset; + uint32_t busy; +}; + +class ESP32LoRaChipProbeIO { + SPIClass& spi_; + const LoRaChipProbePins& pins_; + public: + ESP32LoRaChipProbeIO(SPIClass& spi, const LoRaChipProbePins& pins) + : spi_(spi), pins_(pins) {} + uint32_t now() const { return millis(); } + void pause(uint32_t ms) { +#if MESH_USB_CONSOLE_COOPERATIVE + serviceUsbLoggingPort(); + serviceUsbTerminalPort(); +#endif + delay(ms); + } + void reset() { + digitalWrite(pins_.nss, HIGH); + pinMode(pins_.nss, OUTPUT); + pinMode(pins_.busy, INPUT_PULLDOWN); + digitalWrite(pins_.reset, HIGH); + pinMode(pins_.reset, OUTPUT); + digitalWrite(pins_.reset, LOW); + pause(1); + digitalWrite(pins_.reset, HIGH); + pause(10); + } + bool ready() const { return digitalRead(pins_.busy) == LOW; } + uint8_t readSX127Version() { + spi_.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); + digitalWrite(pins_.nss, LOW); + spi_.transfer(0x42); // SX127x RegVersion; read, not write. + const uint8_t version = spi_.transfer(0); + digitalWrite(pins_.nss, HIGH); + spi_.endTransaction(); + return version; + } + bool readSX126Version(char* data, size_t length) { + if (!ready()) return false; + spi_.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0)); + digitalWrite(pins_.nss, LOW); + spi_.transfer(RADIOLIB_SX126X_CMD_READ_REGISTER); + spi_.transfer(RADIOLIB_SX126X_REG_VERSION_STRING >> 8); + spi_.transfer(RADIOLIB_SX126X_REG_VERSION_STRING & 0xff); + spi_.transfer(0); // command status precedes register contents. + for (size_t i = 0; i < length; ++i) data[i] = spi_.transfer(0); + digitalWrite(pins_.nss, HIGH); + spi_.endTransaction(); + delayMicroseconds(1); + return true; + } +}; + +// Caller powers the radio and starts this board's SPI pins first. Leave NSS +// deselected; the chosen chip's normal initialization performs its own reset. +inline LoRaRadioFamily probeUniversalLoRaChip(SPIClass& spi, + const LoRaChipProbePins& pins) { + ESP32LoRaChipProbeIO io(spi, pins); + const auto family = probeLoRaChip(io); + if (family == LoRaRadioFamily::SX127x) pinMode(pins.busy, INPUT); + return family; +} + +} // namespace mesh diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index c8cda76c3..a6d62a840 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -259,7 +259,11 @@ protected: // The T096 SX1262 bench test found RSSI valid and stable by 7 ms after RX // entry, including the 500 kHz/SF5 worst-case profile. Retain the // conservative 20 ms path for the other radio families. -#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LLCC68) +#if defined(MESH_RADIO_AUTO_FAMILY) && MESH_RADIO_AUTO_FAMILY + // A universal image shares this implementation across both families. Keep + // SX127x's settling margin even in TUs that also include the SX126x wrapper. + static constexpr uint32_t NoiseFloorSettleMillis = 20UL; +#elif defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LLCC68) static constexpr uint32_t NoiseFloorSettleMillis = 7UL; #else static constexpr uint32_t NoiseFloorSettleMillis = 20UL; diff --git a/src/helpers/radiolib/UniversalLoRaRadio.cpp b/src/helpers/radiolib/UniversalLoRaRadio.cpp new file mode 100644 index 000000000..7a3e8ed06 --- /dev/null +++ b/src/helpers/radiolib/UniversalLoRaRadio.cpp @@ -0,0 +1,91 @@ +#if defined(MESH_RADIO_AUTO_FAMILY) && MESH_RADIO_AUTO_FAMILY + +#include "UniversalLoRaRadio.h" +#include + +extern const char meshcore_capability_universal_lora_radio[]; +const char meshcore_capability_universal_lora_radio[] __attribute__((used)) = + "meshcore.capability.radio.autodetect.sx126x_sx127x.v1"; + +UniversalLoRaRadio::UniversalLoRaRadio(Module& sx126_module, Module& sx127_module, + mesh::MainBoard& board) + : sx126_module_(sx126_module), sx127_module_(sx127_module), board_(board) {} + +UniversalLoRaRadio::~UniversalLoRaRadio() { + if (family_ == mesh::LoRaRadioFamily::SX126x) { + static_cast(backend_)->~CustomSX1262Wrapper(); + static_cast(physical_)->~CustomSX1262(); + } else if (family_ == mesh::LoRaRadioFamily::SX127x) { + static_cast(backend_)->~SX127Backend(); + static_cast(physical_)->~CustomSX1276(); + } +} + +bool UniversalLoRaRadio::selectFamily(mesh::LoRaRadioFamily family) { + volatile const char* capability = meshcore_capability_universal_lora_radio; + (void)*capability; + // Selection never touches SPI. The board must positively identify the family + // before calling this; a later hardware failure cannot rebind active IRQs. + if (family != mesh::LoRaRadioFamily::SX126x + && family != mesh::LoRaRadioFamily::SX127x) return false; + if (backend_) return family_ == family; + if (family == mesh::LoRaRadioFamily::SX126x) { + auto* chip = new (&chip_storage_) CustomSX1262(&sx126_module_); + physical_ = chip; + backend_ = new (&driver_storage_) CustomSX1262Wrapper(*chip, board_); + } else { + auto* chip = new (&chip_storage_) CustomSX1276(&sx127_module_); + physical_ = chip; + backend_ = new (&driver_storage_) SX127Backend(*chip, board_); + } + family_ = family; + return true; +} + +bool UniversalLoRaRadio::stdInit(SPIClass* spi) { + if (family_ == mesh::LoRaRadioFamily::SX126x) + return static_cast(physical_)->std_init(spi); + if (family_ == mesh::LoRaRadioFamily::SX127x) + return static_cast(physical_)->std_init(spi); + return false; +} + +const char* UniversalLoRaRadio::familyName() const { + if (family_ == mesh::LoRaRadioFamily::SX126x) return "SX126x"; + if (family_ == mesh::LoRaRadioFamily::SX127x) return "SX127x"; + return "none"; +} + +void UniversalLoRaRadio::powerOff() { + if (family_ == mesh::LoRaRadioFamily::SX126x) + static_cast(backend_)->powerOff(); + else if (family_ == mesh::LoRaRadioFamily::SX127x) + static_cast(backend_)->powerOff(); +} + +bool UniversalLoRaRadio::SX127Backend::validateProfile( + const mesh::RadioProfileParams& params) const { + // The common .cpp is compiled once for a universal image. Keep SX127x's + // actual SF/range and protocol capabilities instead of its SX126x defaults. + if (!mesh::RadioProfiles::valid(params, false) + || params.freq < 137.0f || params.freq > 1020.0f + || params.sf < 7 || params.bw > 500.0f) return false; + if (params.protocol == mesh::RadioProfileProtocol::Meshtext && !supportsMeshtext()) return false; + if (params.protocol == mesh::RadioProfileProtocol::Ethernet && !supportsEthernetTunnel()) return false; +#if MESH_ENABLE_HOST_KISS + if (params.protocol == mesh::RadioProfileProtocol::Kiss && !supportsHostKiss()) return false; +#else + if (params.protocol == mesh::RadioProfileProtocol::Kiss) return false; +#endif + return true; +} + +bool UniversalLoRaRadio::SX127Backend::checkBeaconOutputPower( + float frequency_mhz, int8_t power) const { + (void)frequency_mhz; + // Match SX127x::setOutputPower(): values below 2 dBm select RFO, not PA_BOOST. + return static_cast(_radio)->checkOutputPower(power, nullptr, power < 2) + == RADIOLIB_ERR_NONE; +} + +#endif diff --git a/src/helpers/radiolib/UniversalLoRaRadio.h b/src/helpers/radiolib/UniversalLoRaRadio.h new file mode 100644 index 000000000..8830c6d9c --- /dev/null +++ b/src/helpers/radiolib/UniversalLoRaRadio.h @@ -0,0 +1,243 @@ +#pragma once + +#if defined(MESH_RADIO_AUTO_FAMILY) && MESH_RADIO_AUTO_FAMILY + +#include +#include +#include "RadioLibWrappers.h" +#include "CustomSX1262Wrapper.h" +#include "CustomSX1276Wrapper.h" + +// Mesh and Dispatcher retain this address before setup(). Only the selected +// backend is constructed, after the board powers and identifies its radio. +// Existing wrappers continue to own IRQs, recovery, profiles and modem state. +class UniversalLoRaRadio : public mesh::Radio { + class SX127Backend : public CustomSX1276Wrapper { + public: + SX127Backend(CustomSX1276& radio, mesh::MainBoard& board) + : CustomSX1276Wrapper(radio, board) {} + bool validateProfile(const mesh::RadioProfileParams& params) const override; + protected: + bool checkBeaconOutputPower(float frequency_mhz, int8_t power) const override; + }; + + using ChipStorage = typename std::aligned_union<0, CustomSX1262, CustomSX1276>::type; + using DriverStorage = typename std::aligned_union<0, CustomSX1262Wrapper, SX127Backend>::type; + ChipStorage chip_storage_; + DriverStorage driver_storage_; + Module& sx126_module_; + Module& sx127_module_; + mesh::MainBoard& board_; + PhysicalLayer* physical_ = nullptr; + RadioLibWrapper* backend_ = nullptr; + mesh::LoRaRadioFamily family_ = mesh::LoRaRadioFamily::None; + + public: + // Compile both policies; the runtime capability still rejects RXPS on SX127x. + static constexpr bool SupportsRxPowerSaving = true; + UniversalLoRaRadio(Module& sx126_module, Module& sx127_module, mesh::MainBoard& board); + ~UniversalLoRaRadio(); + UniversalLoRaRadio(const UniversalLoRaRadio&) = delete; + UniversalLoRaRadio& operator=(const UniversalLoRaRadio&) = delete; + bool selectFamily(mesh::LoRaRadioFamily family); + bool stdInit(SPIClass* spi); + mesh::LoRaRadioFamily family() const { return family_; } + PhysicalLayer* physicalLayer() { return physical_; } + const PhysicalLayer* physicalLayer() const { return physical_; } + const char* familyName() const; + + // These small forwarding methods preserve the complete mesh::Radio contract. + // An unidentified radio never transmits or reports successful reconfiguration. +#define MESH_AUTO_LORA_RET(type, method, params, args, fallback) \ + type method params override { return backend_ ? backend_->method args : fallback; } +#define MESH_AUTO_LORA_CONST(type, method, params, args, fallback) \ + type method params const override { return backend_ ? static_cast(backend_)->method args : fallback; } +#define MESH_AUTO_LORA_VOID(method, params, args) \ + void method params override { if (backend_) backend_->method args; } +#define MESH_AUTO_LORA_EXTRA(type, method, params, args, fallback) \ + type method params { return backend_ ? backend_->method args : fallback; } +#define MESH_AUTO_LORA_EXTRA_CONST(type, method, params, args, fallback) \ + type method params const { return backend_ ? static_cast(backend_)->method args : fallback; } + + MESH_AUTO_LORA_CONST(uint8_t, regulatorModes, (), (), 0) + MESH_AUTO_LORA_CONST(mesh::power::RegulatorMode, defaultRegulatorMode, (), (), mesh::power::RegulatorMode::Default) + MESH_AUTO_LORA_RET(bool, configureBootRegulator, (mesh::power::RegulatorMode mode), (mode), false) + MESH_AUTO_LORA_CONST(mesh::power::RegulatorMode, activeRegulatorMode, (), (), mesh::power::RegulatorMode::Default) + MESH_AUTO_LORA_CONST(bool, supportsCarrierWave, (), (), false) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, setCarrierWave, (uint8_t profile, uint32_t duration_ms), (profile, duration_ms), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_CONST(bool, isCarrierWaveActive, (), (), false) + MESH_AUTO_LORA_CONST(uint8_t, carrierWaveProfile, (), (), 0) + MESH_AUTO_LORA_CONST(uint32_t, carrierWaveRemainingMillis, (), (), 0) + MESH_AUTO_LORA_CONST(bool, supportsMorseTx, (), (), false) + MESH_AUTO_LORA_RET(mesh::MorseTxResult, startMorseTx, (float frequency_mhz, uint8_t wpm, const char* text), (frequency_mhz, wpm, text), mesh::MorseTxResult::Unsupported) + MESH_AUTO_LORA_RET(mesh::MorseTxResult, stopMorseTx, (), (), mesh::MorseTxResult::Unsupported) + MESH_AUTO_LORA_CONST(bool, isMorseTxActive, (), (), false) + MESH_AUTO_LORA_CONST(float, morseTxFrequency, (), (), mesh::MorseTx::DefaultFrequencyMHz) + MESH_AUTO_LORA_CONST(uint8_t, morseTxWpm, (), (), mesh::MorseTx::DefaultWpm) + MESH_AUTO_LORA_CONST(uint32_t, morseTxRemainingMillis, (), (), 0) + MESH_AUTO_LORA_CONST(bool, supportsMorseRx, (), (), false) + MESH_AUTO_LORA_RET(mesh::MorseRxResult, startMorseRx, (float frequency_mhz, uint8_t wpm, uint32_t duration_ms), (frequency_mhz, wpm, duration_ms), mesh::MorseRxResult::Unsupported) + MESH_AUTO_LORA_RET(mesh::MorseRxResult, stopMorseRx, (), (), mesh::MorseRxResult::Unsupported) + MESH_AUTO_LORA_CONST(bool, isMorseRxActive, (), (), false) + MESH_AUTO_LORA_CONST(bool, morseRxRecovering, (), (), false) + MESH_AUTO_LORA_CONST(bool, morseRxLearning, (), (), false) + MESH_AUTO_LORA_CONST(float, morseRxFrequency, (), (), mesh::MorseTx::DefaultFrequencyMHz) + MESH_AUTO_LORA_CONST(float, morseRxBandwidthKHz, (), (), 0.0f) + MESH_AUTO_LORA_CONST(uint32_t, morseRxRemainingMillis, (), (), 0) + MESH_AUTO_LORA_CONST(uint32_t, morseCaptureGeneration, (), (), 0) + bool morseRxDiagnostics(mesh::MorseRxDiagnostics& out) const override { + if (backend_) return backend_->morseRxDiagnostics(out); + out = {}; + return false; + } + MESH_AUTO_LORA_CONST(size_t, morseCaptureLength, (), (), 0) + MESH_AUTO_LORA_CONST(bool, morseCaptureTruncated, (), (), false) + MESH_AUTO_LORA_CONST(const char*, morseCaptureText, (), (), "") + size_t readMorseCapture(size_t offset, char* out, size_t capacity) const override { + if (backend_) return backend_->readMorseCapture(offset, out, capacity); + if (out && capacity) out[0] = 0; + return 0; + } + MESH_AUTO_LORA_VOID(clearMorseCapture, (), ()) +#if defined(MESH_MORSE_HIL) && MESH_MORSE_HIL + MESH_AUTO_LORA_CONST(size_t, morseTraceLength, (), (), 0) + MESH_AUTO_LORA_CONST(uint32_t, morseTraceTotal, (), (), 0) + MESH_AUTO_LORA_CONST(bool, morseTraceTruncated, (), (), false) + MESH_AUTO_LORA_CONST(bool, readMorseTrace, (size_t offset, mesh::MorseRssiTraceEntry& out), (offset, out), false) +#endif + + MESH_AUTO_LORA_VOID(begin, (), ()) + MESH_AUTO_LORA_RET(mesh::RadioProfiles*, profiles, (), (), nullptr) + MESH_AUTO_LORA_CONST(const mesh::RadioProfiles*, profiles, (), (), nullptr) +#if MESH_RADIO_SWITCH_TUNING + MESH_AUTO_LORA_CONST(mesh::RadioSwitchCapabilities, switchTuningCapabilities, (), (), mesh::RadioSwitchCapabilities{}) + MESH_AUTO_LORA_CONST(mesh::RadioSwitchTuning, switchTuning, (), (), mesh::RadioSwitchTuning{}) + MESH_AUTO_LORA_CONST(uint32_t, switchSpiHz, (), (), 0) + MESH_AUTO_LORA_RET(bool, configureBootSwitchTuning, (const mesh::RadioSwitchTuning& tuning), (tuning), false) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, trySetSwitchTuning, + (const mesh::RadioSwitchTuning& tuning, + bool (*persist)(void*, const mesh::RadioSwitchTuning&) = nullptr, void* context = nullptr), + (tuning, persist, context), mesh::RadioParamApplyResult::FAILED) +#endif + MESH_AUTO_LORA_CONST(bool, validateProfile, (const mesh::RadioProfileParams& params), (params), false) + MESH_AUTO_LORA_CONST(bool, supportsMeshtext, (), (), false) + MESH_AUTO_LORA_CONST(bool, supportsEthernetTunnel, (), (), false) +#if MESH_ENABLE_HOST_KISS + MESH_AUTO_LORA_CONST(bool, supportsHostKiss, (), (), false) +#endif +#if MESH_ENABLE_TX_BEACONS + MESH_AUTO_LORA_CONST(bool, validateBeaconPower, (float frequency_mhz, int8_t power), (frequency_mhz, power), false) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, prepareBeaconTransmit, (const mesh::RadioProfileParams& params, int8_t power), (params, power), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, restoreBeaconReceive, (), (), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_CONST(bool, isBeaconRadioOwned, (), (), false) + MESH_AUTO_LORA_RET(bool, isBeaconChannelBusy, (), (), true) + MESH_AUTO_LORA_RET(uint32_t, getBeaconAirtime, (const mesh::RadioProfileParams& params, int length), (params, length), 0) +#endif + MESH_AUTO_LORA_CONST(uint8_t, receiveProfile, (), (), 0) + MESH_AUTO_LORA_CONST(uint32_t, receiveProfileGeneration, (), (), 0) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, prepareTransmitProfile, (uint8_t profile), (profile), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, prepareTransmitProfile, (uint8_t profile, bool reply_rx_override), (profile, reply_rx_override), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_CONST(uint16_t, profilePreamble, (uint8_t profile), (profile), 0) + MESH_AUTO_LORA_RET(uint32_t, getProfileAirtime, (uint8_t profile, int len_bytes, uint8_t cr = 0), (profile, len_bytes, cr), 0) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, trySetPrimaryParams, (const mesh::RadioProfileParams& params, bool temporary, const uint32_t* timings = nullptr), (params, temporary, timings), mesh::RadioParamApplyResult::FAILED) + + MESH_AUTO_LORA_RET(int, recvRaw, (uint8_t* bytes, int size), (bytes, size), 0) + MESH_AUTO_LORA_VOID(onReceiveProcessed, (), ()) + MESH_AUTO_LORA_RET(uint32_t, getEstAirtimeFor, (int len_bytes), (len_bytes), 0) + MESH_AUTO_LORA_RET(float, packetScore, (float snr, int packet_len), (snr, packet_len), 0.0f) + MESH_AUTO_LORA_RET(bool, startSendRaw, (const uint8_t* bytes, int length), (bytes, length), false) + MESH_AUTO_LORA_RET(bool, setCodingRate, (uint8_t cr), (cr), false) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, tryRestoreCodingRate, (uint8_t cr), (cr), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_RET(bool, isSendComplete, (), (), false) + MESH_AUTO_LORA_VOID(onSendFinished, (), ()) + MESH_AUTO_LORA_VOID(loop, (), ()) + MESH_AUTO_LORA_VOID(idle, (), ()) + MESH_AUTO_LORA_VOID(startRecv, (), ()) + MESH_AUTO_LORA_RET(bool, setTrackerSleep, (bool sleeping), (sleeping), false) + MESH_AUTO_LORA_RET(bool, recoverRadio, (bool hard), (hard), false) + + MESH_AUTO_LORA_CONST(int, getNoiseFloor, (), (), 0) + MESH_AUTO_LORA_CONST(float, getNoiseFloorDbm, (), (), 0.0f) + MESH_AUTO_LORA_CONST(float, getNoiseFloorDbm, (uint8_t profile), (profile), 0.0f) +#if MESH_ENABLE_TELEMETRY_HISTORY + MESH_AUTO_LORA_CONST(bool, getNoiseFloorReading, (uint8_t profile, float& dbm, uint32_t& published_at_ms), (profile, dbm, published_at_ms), false) + MESH_AUTO_LORA_VOID(requestNoiseFloorMeasurement, (), ()) +#endif + MESH_AUTO_LORA_CONST(float, getNoiseFloorCalibrationSecondsRemaining, (), (), 0.0f) + MESH_AUTO_LORA_CONST(float, getNoiseFloorCalibrationSecondsRemaining, (uint8_t profile), (profile), 0.0f) + MESH_AUTO_LORA_CONST(bool, isCalibratingNoiseFloor, (), (), false) + MESH_AUTO_LORA_CONST(bool, getNoiseFloorWakeDelay, (uint32_t now, uint32_t& delay_ms), (now, delay_ms), false) + MESH_AUTO_LORA_CONST(bool, getReceiveRetryDelay, (uint32_t now, uint32_t& delay_ms), (now, delay_ms), false) + MESH_AUTO_LORA_CONST(bool, getProfileIdleDelay, (uint32_t& delay_ms), (delay_ms), false) + MESH_AUTO_LORA_VOID(triggerNoiseFloorCalibrate, (int threshold), (threshold)) + MESH_AUTO_LORA_VOID(recalibrateNoiseFloor, (), ()) + MESH_AUTO_LORA_VOID(setCADEnabled, (bool enabled), (enabled)) + MESH_AUTO_LORA_EXTRA(bool, setCADScanTimeoutMillis, (uint32_t timeout_ms), (timeout_ms), false) + MESH_AUTO_LORA_EXTRA_CONST(uint32_t, getCADScanTimeoutMillis, (), (), 0) + MESH_AUTO_LORA_VOID(resetAGC, (), ()) + MESH_AUTO_LORA_RET(bool, tryResetAGC, (), (), false) + MESH_AUTO_LORA_CONST(uint32_t, getRecoveryCount, (), (), 0) + MESH_AUTO_LORA_CONST(uint8_t, getRadioState, (), (), 0) + MESH_AUTO_LORA_CONST(unsigned long, getLastRecvMillis, (), (), 0) + MESH_AUTO_LORA_CONST(unsigned long, getLastRadioInterruptMillis, (), (), 0) + MESH_AUTO_LORA_CONST(bool, isInRecvMode, (), (), false) + MESH_AUTO_LORA_CONST(bool, supportsRxPowerSaving, (), (), false) + MESH_AUTO_LORA_RET(bool, setRxPowerSaving, (bool enabled, uint32_t rx_us, uint32_t sleep_us), (enabled, rx_us, sleep_us), false) + MESH_AUTO_LORA_CONST(bool, isRxPowerSavingContinuousFallback, (), (), false) + MESH_AUTO_LORA_CONST(bool, supportsRxPowerSavingRfRxDisable, (), (), false) + MESH_AUTO_LORA_RET(bool, setRxPowerSavingRfRxDisabled, (bool disabled), (disabled), false) + MESH_AUTO_LORA_CONST(bool, isRxPowerSavingRfRxDisabled, (), (), false) + MESH_AUTO_LORA_RET(mesh::RadioParamApplyResult, trySetParams, (float frequency, float bandwidth, uint8_t sf, uint8_t cr, const uint32_t* timings = nullptr), (frequency, bandwidth, sf, cr, timings), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_RET(bool, isReceiving, (), (), false) + MESH_AUTO_LORA_RET(bool, isReceivingPassive, (int interference_margin_db), (interference_margin_db), false) + MESH_AUTO_LORA_CONST(float, getLastRSSI, (), (), 0.0f) + MESH_AUTO_LORA_CONST(float, getLastSNR, (), (), 0.0f) + MESH_AUTO_LORA_CONST(uint32_t, getPacketsRecvErrors, (), (), 0) + + // Existing roles use these RadioLibWrapper helpers in addition to mesh::Radio. + MESH_AUTO_LORA_EXTRA(mesh::RadioParamApplyResult, trySetRxPowerSaving, (bool enabled, uint32_t rx_us, uint32_t sleep_us), (enabled, rx_us, sleep_us), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_EXTRA_CONST(RxPowerSavingStatus, getRxPowerSavingStatus, (), (), RxPowerSavingStatus{}) + uint32_t rxPowerSavingTransitionUs() const { + return backend_ ? backend_->rxPowerSavingTransitionUs() : rxPowerSavingDefaultTransitionUs(); + } + void getRxPowerSavingTimings(uint32_t& rx_us, uint32_t& sleep_us) const { + if (backend_) backend_->getRxPowerSavingTimings(rx_us, sleep_us); + else { rx_us = 0; sleep_us = 0; } + } + MESH_AUTO_LORA_EXTRA(mesh::RadioParamApplyResult, trySetRxPowerSavingRfRxDisabled, (bool disabled), (disabled), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_EXTRA(bool, isChannelActive, (), (), false) + MESH_AUTO_LORA_EXTRA(bool, setParams, (float frequency, float bandwidth, uint8_t sf, uint8_t cr, const uint32_t* timings = nullptr), (frequency, bandwidth, sf, cr, timings), false) + MESH_AUTO_LORA_EXTRA(uint32_t, getRngSeed, (), (), 0) + MESH_AUTO_LORA_EXTRA_CONST(bool, isTxPowerValid, (int8_t power), (power), false) + MESH_AUTO_LORA_EXTRA_CONST(bool, isTxPowerApplied, (int8_t power), (power), false) + MESH_AUTO_LORA_EXTRA(bool, setTxPower, (int8_t power), (power), false) + MESH_AUTO_LORA_EXTRA(mesh::RadioParamApplyResult, trySetTxPower, (int8_t power), (power), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_EXTRA(float, getCurrentRSSI, (), (), NAN) + MESH_AUTO_LORA_EXTRA_CONST(uint8_t, getSpreadingFactor, (), (), 0) + MESH_AUTO_LORA_EXTRA_CONST(uint16_t, preambleLengthForParams, (uint8_t sf, float bandwidth), (sf, bandwidth), 0) + MESH_AUTO_LORA_EXTRA_CONST(uint16_t, currentPreambleLength, (), (), 0) + MESH_AUTO_LORA_EXTRA(bool, updatePreamble, (uint8_t sf, float bandwidth), (sf, bandwidth), false) + MESH_AUTO_LORA_EXTRA(PacketMillis, calcMaxPacketMillis, (uint8_t sf, float bandwidth, uint8_t cr, uint16_t preamble), (sf, bandwidth, cr, preamble), (PacketMillis{0, 0})) + MESH_AUTO_LORA_EXTRA(int16_t, performChannelScan, (), (), RADIOLIB_ERR_UNSUPPORTED) + MESH_AUTO_LORA_EXTRA_CONST(uint32_t, getPacketsRecv, (), (), 0) + MESH_AUTO_LORA_EXTRA_CONST(uint32_t, getPacketsSent, (), (), 0) + MESH_AUTO_LORA_EXTRA_CONST(uint32_t, getRxPsWatchdogSoftCount, (), (), 0) + MESH_AUTO_LORA_EXTRA_CONST(uint32_t, getRxPsWatchdogHardCount, (), (), 0) + MESH_AUTO_LORA_EXTRA_CONST(bool, isWatchdogObserving, (), (), false) + void resetStats() { if (backend_) backend_->resetStats(); } + MESH_AUTO_LORA_EXTRA(bool, setRxBoostedGainMode, (bool enabled), (enabled), false) + MESH_AUTO_LORA_EXTRA(mesh::RadioParamApplyResult, trySetRxBoostedGainMode, (bool enabled), (enabled), mesh::RadioParamApplyResult::FAILED) + MESH_AUTO_LORA_EXTRA_CONST(bool, supportsRxBoostedGainMode, (), (), false) + MESH_AUTO_LORA_EXTRA_CONST(bool, getRxBoostedGainMode, (), (), false) + MESH_AUTO_LORA_EXTRA(bool, configSideDetectors, (const uint8_t* spreading_factors, uint8_t count, float bandwidth), (spreading_factors, count, bandwidth), false) + MESH_AUTO_LORA_EXTRA(mesh::RadioParamApplyResult, tryConfigSideDetectors, (const uint8_t* spreading_factors, uint8_t count, float bandwidth), (spreading_factors, count, bandwidth), mesh::RadioParamApplyResult::FAILED) + void powerOff(); + +#undef MESH_AUTO_LORA_RET +#undef MESH_AUTO_LORA_CONST +#undef MESH_AUTO_LORA_VOID +#undef MESH_AUTO_LORA_EXTRA +#undef MESH_AUTO_LORA_EXTRA_CONST +}; + +#endif diff --git a/variants/lilygo_t3s3_universal/platformio.ini b/variants/lilygo_t3s3_universal/platformio.ini new file mode 100644 index 000000000..bdc3865b8 --- /dev/null +++ b/variants/lilygo_t3s3_universal/platformio.ini @@ -0,0 +1,224 @@ +; Startup-detected SX126x/SX127x radio. Legacy chip-specific targets retain +; their original wiring, names, and OTA identities. + +[LilyGo_T3S3] +extends = esp32_base +board = t3_s3_v1_x +build_flags = + ${esp32_base.build_flags} + -I variants/lilygo_t3s3_universal + -D LILYGO_T3S3 + -D P_LORA_DIO_1=33 + -D P_LORA_NSS=7 + -D P_LORA_RESET=8 + -D P_LORA_BUSY=34 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=3 + -D P_LORA_MOSI=6 + -D P_LORA_TX_LED=37 + -D PIN_VBAT_READ=1 + -D PIN_USER_BTN=0 + -D PIN_BOARD_SDA=18 + -D PIN_BOARD_SCL=17 + -D PIN_OLED_RESET=-1 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D USE_SX1262 + -D RADIO_CLASS=UniversalLoRaRadio + -D WRAPPER_CLASS=UniversalLoRaRadio + -D LORA_TX_POWER=20 + -D SX126X_RX_BOOSTED_GAIN=1 + -D USE_SX1276 + -D MESH_RADIO_AUTO_FAMILY=1 + -D SX127X_CURRENT_LIMIT=120 + -D P_LORA_DIO_0=9 +build_src_filter = + ${esp32_base.build_src_filter} + +<../variants/lilygo_t3s3_universal> +lib_deps = + ${esp32_base.lib_deps} + adafruit/Adafruit SSD1306 @ ^2.5.13 + +[env:LilyGo_T3S3_repeater] +extends = env:LilyGo_T3S3_sx1262_repeater +build_flags = + ${LilyGo_T3S3.build_flags} + -D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI and the original dual-OTA partition map + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"T3 S3 Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T3S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T3S3_repeater_bridge_espnow] +extends = env:LilyGo_T3S3_sx1262_repeater_bridge_espnow +build_flags = + ${LilyGo_T3S3.build_flags} + -D LIGHTWEIGHT_WIFI_OTA=1 ; retain WebUI and the original dual-OTA partition map + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_ESPNOW_BRIDGE=1 +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T3S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T3S3_repeater_observer_mqtt] +extends = env:LilyGo_T3S3_sx1262_repeater_observer_mqtt +build_flags = + ${LilyGo_T3S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D MQTT_MAX_PACKET_SIZE=1024 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_T3S3.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + +[env:LilyGo_T3S3_room_server_observer_mqtt] +extends = env:LilyGo_T3S3_sx1262_room_server_observer_mqtt +build_flags = + ${LilyGo_T3S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"T3 S3 Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D WITH_MQTT_BRIDGE=1 + -D MAX_NEIGHBOURS=50 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y + -D ESP32_CPU_FREQ=160 +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T3S3.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + +[env:LilyGo_T3S3_terminal_chat] +extends = env:LilyGo_T3S3_sx1262_terminal_chat +build_flags = + ${LilyGo_T3S3.build_flags} + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=1 +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + +<../examples/simple_secure_chat/main.cpp> +lib_deps = + ${LilyGo_T3S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T3S3_room_server] +extends = env:LilyGo_T3S3_sx1262_room_server +build_flags = + ${LilyGo_T3S3.build_flags} + -D DISPLAY_CLASS=SSD1306Display + -D ADVERT_NAME='"T3 S3 Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_T3S3.lib_deps} + ${esp32_ota.lib_deps} + +[env:LilyGo_T3S3_companion_radio_usb] +extends = env:LilyGo_T3S3_sx1262_companion_radio_usb +build_flags = + ${LilyGo_T3S3.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D ENABLE_USB_INTERFACE +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T3S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T3S3_companion_radio_ble] +extends = env:LilyGo_T3S3_sx1262_companion_radio_ble +build_flags = + ${LilyGo_T3S3.build_flags} + -I examples/companion_radio/ui-new + -D DISPLAY_CLASS=SSD1306Display + -D MAX_CONTACTS=350 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D BLE_DEBUG_LOGGING=1 + -D OFFLINE_QUEUE_SIZE=512 +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + + + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_T3S3.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:LilyGo_T3S3_companion_radio_ble_ps] +extends = env:LilyGo_T3S3_companion_radio_ble + +[env:LilyGo_T3S3_kiss_modem] +extends = env:LilyGo_T3S3_sx1262_kiss_modem +build_src_filter = + ${LilyGo_T3S3.build_src_filter} + +<../examples/kiss_modem/> diff --git a/variants/lilygo_t3s3_universal/target.cpp b/variants/lilygo_t3s3_universal/target.cpp new file mode 100644 index 000000000..d95535ce9 --- /dev/null +++ b/variants/lilygo_t3s3_universal/target.cpp @@ -0,0 +1,48 @@ +#include +#include "target.h" +#include +#include +#include + +UniversalT3S3Board board; + +static SPIClass spi; +static SX126xBufferedRadioHal sx126x_hal(spi); +static TunableRadioHal sx127x_hal(spi, 2000000); +static Module sx126x_module(&sx126x_hal, P_LORA_NSS, P_LORA_DIO_1, + P_LORA_RESET, P_LORA_BUSY); +static Module sx127x_module(&sx127x_hal, P_LORA_NSS, P_LORA_DIO_0, + P_LORA_RESET, P_LORA_DIO_1); +UniversalLoRaRadio radio_driver(sx126x_module, sx127x_module, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); +SensorManager sensors; + +#ifdef DISPLAY_CLASS +DISPLAY_CLASS display; +MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +mesh::LoRaRadioFamily universalLoRaRadioFamily() { + return radio_driver.family(); +} + +bool prepareUniversalLoRaRadio() { + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + const mesh::LoRaChipProbePins pins{P_LORA_NSS, P_LORA_RESET, P_LORA_BUSY}; + return radio_driver.selectFamily(mesh::probeUniversalLoRaChip(spi, pins)); +} + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + return radio_driver.stdInit(&spi); +} + +mesh::LocalIdentity radio_new_identity() { + auto* physical = radio_driver.physicalLayer(); + if (!physical) return mesh::LocalIdentity(); + RadioNoiseListener rng(*physical); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/lilygo_t3s3_universal/target.h b/variants/lilygo_t3s3_universal/target.h new file mode 100644 index 000000000..c87928c26 --- /dev/null +++ b/variants/lilygo_t3s3_universal/target.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#ifdef DISPLAY_CLASS +#include +#include +#endif + +bool prepareUniversalLoRaRadio(); +mesh::LoRaRadioFamily universalLoRaRadioFamily(); + +class UniversalT3S3Board : public ESP32Board { +public: + void begin() { + ESP32Board::begin(); + prepareUniversalLoRaRadio(); + } + + uint32_t getIRQGpio() override { + switch (universalLoRaRadioFamily()) { + case mesh::LoRaRadioFamily::SX126x: return P_LORA_DIO_1; + case mesh::LoRaRadioFamily::SX127x: return P_LORA_DIO_0; + default: return static_cast(-1); + } + } + + const char* getManufacturerName() const override { + switch (universalLoRaRadioFamily()) { + case mesh::LoRaRadioFamily::SX126x: return "LilyGo T3 S3 SX126x"; + case mesh::LoRaRadioFamily::SX127x: return "LilyGo T3 S3 SX127x"; + default: return "LilyGo T3 S3 (radio undetected)"; + } + } +}; + +extern UniversalT3S3Board board; +extern UniversalLoRaRadio radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern SensorManager sensors; + +#ifdef DISPLAY_CLASS +extern DISPLAY_CLASS display; +extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity(); diff --git a/variants/lilygo_tbeam_universal/platformio.ini b/variants/lilygo_tbeam_universal/platformio.ini new file mode 100644 index 000000000..28e060c5a --- /dev/null +++ b/variants/lilygo_tbeam_universal/platformio.ini @@ -0,0 +1,194 @@ +; Startup-detected SX126x/SX127x radio. Legacy chip-specific targets retain +; their original wiring, names, and OTA identities. + +[LilyGo_TBeam] +extends = esp32_base +board = ttgo-t-beam +build_unflags = + -fexceptions +build_flags = + ${esp32_base.build_flags} + -fno-exceptions + -D PORTABLE_ESP32_ROM_NANO_FORMAT=1 + -I variants/lilygo_tbeam_universal + -D TBEAM_SX1262 + -D P_LORA_DIO_0=26 + -D P_LORA_DIO_1=33 + -D P_LORA_BUSY=32 + -D P_LORA_NSS=18 + -D P_LORA_RESET=23 + -D P_LORA_SCLK=5 + -D P_LORA_MISO=19 + -D P_LORA_MOSI=27 + -D SX126X_DIO2_AS_RF_SWITCH=true + -D SX126X_DIO3_TCXO_VOLTAGE=1.8 + -D SX126X_CURRENT_LIMIT=140 + -D SX126X_RX_BOOSTED_GAIN=1 + -D USE_SX1262 + -D RADIO_CLASS=UniversalLoRaRadio + -D WRAPPER_CLASS=UniversalLoRaRadio + -D DISPLAY_CLASS=SSD1306Display + -D LORA_TX_POWER=20 + -D P_LORA_TX_LED=4 + -D PIN_BOARD_SDA=21 + -D PIN_BOARD_SCL=22 + -D PIN_GPS_RX=12 + -D PIN_GPS_TX=34 + -D PIN_USER_BTN=38 + -D ENV_INCLUDE_GPS=1 + -D USE_SX1276 + -D MESH_RADIO_AUTO_FAMILY=1 + -D SX127X_CURRENT_LIMIT=120 +build_src_filter = + ${esp32_base.build_src_filter} + +<../variants/lilygo_tbeam_universal> + + + + + + +board_build.partitions = min_spiffs.csv ; get around 4mb flash limit +lib_deps = + ${esp32_base.lib_deps} + lewisxhe/XPowersLib@^0.2.7 + adafruit/Adafruit SSD1306 @ ^2.5.13 + stevemarple/MicroNMEA @ ^2.0.6 + boschsensortec/BSEC Software Library @ ^1.8.1492 + +[env:Tbeam_companion_radio_ble] +extends = env:Tbeam_SX1262_companion_radio_ble +build_flags = + ${LilyGo_TBeam.build_flags} + -I examples/companion_radio/ui-new + -D MAX_CONTACTS=160 + -D MAX_GROUP_CHANNELS=40 + -D BLE_PIN_CODE=123456 + -D OFFLINE_QUEUE_SIZE=128 +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + + + + + +<../examples/companion_radio/*.cpp> + +<../examples/companion_radio/ui-new/*.cpp> +lib_deps = + ${LilyGo_TBeam.lib_deps} + densaugeo/base64 @ ~1.4.0 + +[env:Tbeam_companion_radio_ble_ps] +extends = env:Tbeam_companion_radio_ble + +[env:Tbeam_repeater] +extends = env:Tbeam_SX1262_repeater +build_flags = + ${LilyGo_TBeam.build_flags} + -D ADVERT_NAME='"T-Beam Repeater"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D FLOOD_CHANNEL_SCOPE_SLOTS=31 ; classic ESP32 DRAM cannot fit the default 255 with LoRa OTA +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam.lib_deps} + ${esp32_ota.lib_deps} + +[env:Tbeam_repeater_bridge_espnow] +extends = env:Tbeam_SX1262_repeater_bridge_espnow +build_flags = + ${LilyGo_TBeam.build_flags} + -D ADVERT_NAME='"ESPNow Bridge"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D FLOOD_CHANNEL_SCOPE_SLOTS=31 ; classic ESP32 DRAM cannot fit the default 255 with ESP-NOW + -D WITH_ESPNOW_BRIDGE=1 + -D DISABLE_LORA_OTA=1 ; ESP-NOW plus LoRa OTA overflows this board's internal DRAM +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam.lib_deps} + ${esp32_ota.lib_deps} + +[env:Tbeam_room_server] +extends = env:Tbeam_SX1262_room_server +build_flags = + ${LilyGo_TBeam.build_flags} + -D ADVERT_NAME='"T-Beam Room"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TBeam.lib_deps} + ${esp32_ota.lib_deps} + +[env:Tbeam_kiss_modem] +extends = env:Tbeam_SX1262_kiss_modem +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + +<../examples/kiss_modem/> + +[env:Tbeam_repeater_observer_mqtt] +extends = env:Tbeam_SX1262_repeater_observer_mqtt +build_flags = + ${LilyGo_TBeam.build_flags} + -D ADVERT_NAME='"T-Beam MQTT Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D MAX_NEIGHBOURS=50 + -D WITH_MQTT_BRIDGE=1 + -D FLOOD_CHANNEL_SCOPE_SLOTS=31 ; retain scope support within this observer's tight internal DRAM + -D MQTT_MAX_PACKET_SIZE=1024 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + + + + + + + + + +<../examples/simple_repeater> +lib_deps = + ${LilyGo_TBeam.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 + +[env:Tbeam_room_server_observer_mqtt] +extends = env:Tbeam_SX1262_room_server_observer_mqtt +build_flags = + ${LilyGo_TBeam.build_flags} + -D ADVERT_NAME='"T-Beam Room Observer"' + -D ADVERT_LAT=0.0 + -D ADVERT_LON=0.0 + -D ADMIN_PASSWORD='"password"' + -D ROOM_PASSWORD='"hello"' + -D WITH_MQTT_BRIDGE=1 + -D MAX_NEIGHBOURS=50 + -D MQTT_MAX_PACKET_SIZE=1024 + -D MQTT_DEBUG=1 + -D CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +build_src_filter = + ${LilyGo_TBeam.build_src_filter} + + + + + + + + + +<../examples/simple_room_server> +lib_deps = + ${LilyGo_TBeam.lib_deps} + ${esp32_ota.lib_deps} + elims/PsychicMqttClient@^0.2.4 + bblanchon/ArduinoJson @ 7.4.3 + arduino-libraries/NTPClient + JChristensen/Timezone + paulstoffregen/Time@1.6.1 diff --git a/variants/lilygo_tbeam_universal/target.cpp b/variants/lilygo_tbeam_universal/target.cpp new file mode 100644 index 000000000..27fb14b6f --- /dev/null +++ b/variants/lilygo_tbeam_universal/target.cpp @@ -0,0 +1,57 @@ +#include +#include "target.h" +#include +#include +#include + +UniversalTBeamBoard board; + +static SPIClass spi; +static SX126xBufferedRadioHal sx126x_hal(spi); +static TunableRadioHal sx127x_hal(spi, 2000000); +static Module sx126x_module(&sx126x_hal, P_LORA_NSS, P_LORA_DIO_1, + P_LORA_RESET, P_LORA_BUSY); +static Module sx127x_module(&sx127x_hal, P_LORA_NSS, P_LORA_DIO_0, + P_LORA_RESET, P_LORA_DIO_1); +UniversalLoRaRadio radio_driver(sx126x_module, sx127x_module, board); + +ESP32RTCClock fallback_clock; +AutoDiscoverRTCClock rtc_clock(fallback_clock); + +#if ENV_INCLUDE_GPS +#include +MicroNMEALocationProvider nmea(Serial1, &rtc_clock); +EnvironmentSensorManager sensors(nmea); +#else +EnvironmentSensorManager sensors; +#endif + +#ifdef DISPLAY_CLASS +DISPLAY_CLASS display; +MomentaryButton user_btn(PIN_USER_BTN, 1000, true); +#endif + +mesh::LoRaRadioFamily universalLoRaRadioFamily() { + return radio_driver.family(); +} + +bool prepareUniversalLoRaRadio() { + // TBeamBoard calls this after enabling the PMU's radio rail. Probe before + // regulator policy or any modem initialization can touch the wrong chip. + spi.begin(P_LORA_SCLK, P_LORA_MISO, P_LORA_MOSI); + const mesh::LoRaChipProbePins pins{P_LORA_NSS, P_LORA_RESET, P_LORA_BUSY}; + return radio_driver.selectFamily(mesh::probeUniversalLoRaChip(spi, pins)); +} + +bool radio_init() { + fallback_clock.begin(); + rtc_clock.begin(Wire); + return radio_driver.stdInit(&spi); +} + +mesh::LocalIdentity radio_new_identity() { + auto* physical = radio_driver.physicalLayer(); + if (!physical) return mesh::LocalIdentity(); + RadioNoiseListener rng(*physical); + return mesh::LocalIdentity(&rng); +} diff --git a/variants/lilygo_tbeam_universal/target.h b/variants/lilygo_tbeam_universal/target.h new file mode 100644 index 000000000..049db887d --- /dev/null +++ b/variants/lilygo_tbeam_universal/target.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#ifdef DISPLAY_CLASS +#include +#include +#endif + +bool prepareUniversalLoRaRadio(); +mesh::LoRaRadioFamily universalLoRaRadioFamily(); + +class UniversalTBeamBoard : public TBeamBoard { +public: + uint32_t getIRQGpio() override { + switch (universalLoRaRadioFamily()) { + case mesh::LoRaRadioFamily::SX126x: return P_LORA_DIO_1; + case mesh::LoRaRadioFamily::SX127x: return P_LORA_DIO_0; + default: return static_cast(-1); + } + } + + const char* getManufacturerName() const override { + switch (universalLoRaRadioFamily()) { + case mesh::LoRaRadioFamily::SX126x: return "LilyGo T-Beam SX126x"; + case mesh::LoRaRadioFamily::SX127x: return "LilyGo T-Beam SX127x"; + default: return "LilyGo T-Beam (radio undetected)"; + } + } +}; + +extern UniversalTBeamBoard board; +extern UniversalLoRaRadio radio_driver; +extern AutoDiscoverRTCClock rtc_clock; +extern EnvironmentSensorManager sensors; + +#ifdef DISPLAY_CLASS +extern DISPLAY_CLASS display; +extern MomentaryButton user_btn; +#endif + +bool radio_init(); +mesh::LocalIdentity radio_new_identity();