diff --git a/Boards.h b/Boards.h index c66a71bc..2e8b6d55 100644 --- a/Boards.h +++ b/Boards.h @@ -114,6 +114,8 @@ #define MODEL_11 0x11 // RAK4631, 433 Mhz #define MODEL_12 0x12 // RAK4631, 868 Mhz + #define BOARD_XIAO_NRF52840 0x52 // Seeed XIAO nRF52840 + Wio-SX1262 + #define PRODUCT_HMBRW 0xF0 #define BOARD_HMBRW 0x32 #define BOARD_HUZZAH32 0x34 @@ -142,6 +144,8 @@ #ifndef MODEM #if BOARD_MODEL == BOARD_RAK4631 #define MODEM SX1262 + #elif BOARD_MODEL == BOARD_XIAO_NRF52840 + #define MODEM SX1262 #elif BOARD_MODEL == BOARD_GENERIC_NRF52 #define MODEM SX1262 #else @@ -898,6 +902,59 @@ const int DISPLAY_BL_PIN = PIN_T114_TFT_BLGT; const int DISPLAY_RST = PIN_T114_TFT_RST; + #elif BOARD_MODEL == BOARD_XIAO_NRF52840 + #define HAS_EEPROM false + #define HAS_DISPLAY false + #define HAS_BLUETOOTH false + #undef HAS_BLE + #define HAS_BLE true + // Seeed platform bootloader does not write image size at the Adafruit + // nRF52 IMG_SIZE_START address, so firmware hash validation is disabled. + #undef VALIDATE_FIRMWARE + #define VALIDATE_FIRMWARE false + #define HAS_CONSOLE false + #define HAS_PMU false + #define HAS_NP false + #define HAS_SD false + #undef HAS_TCXO + #define HAS_TCXO true + #define HAS_RF_SWITCH_RX_TX true + #undef HAS_BUSY + #define HAS_BUSY true + #define DIO2_AS_RF_SWITCH true + #define CONFIG_UART_BUFFER_SIZE 6144 + #define CONFIG_QUEUE_SIZE 6144 + #define CONFIG_QUEUE_MAX_LENGTH 200 + #define EEPROM_SIZE 296 + #define EEPROM_OFFSET EEPROM_SIZE-EEPROM_RESERVED + #define BLE_MANUFACTURER "Seeed Studio" + #define BLE_MODEL "XIAO nRF52840" + + // XIAO nRF52840 onboard LEDs are active-low + // (LOW turns LED on, HIGH turns LED off) + #define LED_ON LOW + #define LED_OFF HIGH + + #undef HAS_SLEEP + #define HAS_SLEEP true + #define PIN_WAKEUP 1 // DIO1 from radio (Arduino D1) + #define WAKEUP_LEVEL HIGH // DIO1 goes high on interrupt + + // Pin mapping for XIAO nRF52840 + Wio-SX1262 kit + // Using Arduino digital pin numbers (D0-D10), not raw GPIO + // The Arduino framework maps these via g_ADigitalPinMap + const int pin_rxen = 5; // D5 → P0.05 (RF_SW) + const int pin_reset = 2; // D2 → P0.28 (RST) + const int pin_cs = 4; // D4 → P0.04 (NSS) + const int pin_sclk = 8; // D8 → P1.13 (SCK) + const int pin_mosi = 10; // D10 → P1.15 (MOSI) + const int pin_miso = 9; // D9 → P1.14 (MISO) + const int pin_busy = 3; // D3 → P0.29 (BUSY) + const int pin_dio = 1; // D1 → P0.03 (DIO1/IRQ) + const int pin_led_rx = LED_RED; + const int pin_led_tx = LED_RED; + const int pin_tcxo_enable = -1; + #else #error An unsupported nRF board was selected. Cannot compile RNode firmware. #endif diff --git a/README.md b/README.md index 1453999e..1ae474e4 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,69 @@ When in `Transport Mode`, the device will display "TRANSPORT" across the top of Note that at the present time, when in TNC mode this firmware does not operate like a regular RNode does when in TNC mode due to logging from the embedded RNS that is output on the serial port. This can clobber KISS communication from the attached machine so do not attempt to attach another RNS to the device while in this mode. On the plus side, there is extensive logging available on the serial port to observe the embedded RNS in action and to aid in troubleshooting. +## Supported Hardware + +### Seeed XIAO nRF52840 + Wio-SX1262 + +The [Seeed XIAO nRF52840](https://wiki.seeedstudio.com/XIAO_BLE/) paired with the [Wio-SX1262 expansion board](https://wiki.seeedstudio.com/wio_sx1262_with_xiao_nrf52840_kit/) is a compact, low-power LoRa node well suited for deployment as a standalone Reticulum transport/repeater. + +**Pin mapping (do not modify — consumed by radio):** + +| Function | Arduino pin | nRF52840 GPIO | +|----------|-------------|---------------| +| RF switch (RXEN) | D5 | P0.05 | +| Radio reset | D2 | P0.28 | +| SPI CS | D4 | P0.04 | +| SPI SCK | D8 | P1.13 | +| SPI MOSI | D10 | P1.15 | +| SPI MISO | D9 | P1.14 | +| BUSY | D3 | P0.29 | +| DIO1 / IRQ | D1 | P0.03 | + +Free GPIO for external use: D0, D6, D7. + +**PlatformIO environments:** + +| Environment | Description | +|-------------|-------------| +| `xiao_nrf52840` | Standard build — continuous RX, ~10 mA | + +**Build and flash:** + +The Seeed platform is downloaded automatically on the first build. +``` +pio run -e xiao_nrf52840 +``` + +Flashing uses the nRF52 UF2 DFU bootloader. Double-tap the reset button on the XIAO to enter bootloader mode (the LED pulses slowly), then upload: +``` +pio run -e xiao_nrf52840 -t upload +``` +The post-upload script automatically provisions the EEPROM and writes the firmware hash via `rnodeconf`. + +**Configuration:** + +For standalone TNC / transport mode: +``` +rnodeconf /dev/ttyACM0 --tnc --freq 869525000 --bw 125000 --sf 8 --cr 5 --txp 14 +``` + +For host-controlled mode (attached RNS / meshchat): +``` +rnodeconf /dev/ttyACM0 --normal +``` + +Verify with: +``` +rnodeconf /dev/ttyACM0 --info +``` + +The onboard red LED indicates radio state: briefly lit on RX/TX activity in HOST mode; lit while the channel is sensed busy (CSMA) in TNC mode. + +**Notes:** +- The Seeed bootloader does not write the image size at the Adafruit nRF52 `IMG_SIZE_START` address, so `VALIDATE_FIRMWARE` is disabled for this board to prevent the firmware hash check from failing. +- No dedicated `rnodeconf` product/model ID exists for the XIAO nRF52840 yet; the RAK4631 bytes (`0x10`/`0x12`) are reused during provisioning. + ## Build Dependencies Build environment is configured for use in [VSCode](https://code.visualstudio.com/) and [PlatformIO](https://platformio.org/). @@ -37,7 +100,7 @@ Build environment is configured for use in [VSCode](https://code.visualstudio.co Building and uploading to hardware is simple through the VSCode PlatformIO IDE - Install VSCode and PlatformIO - Clone this repo -- Lanch PlatformIO and load repo +- Launch PlatformIO and load repo - In PlatformIO, select the environment for intended board - Build, Upload, and Monitor to observe application logging @@ -66,18 +129,21 @@ Build a single environment (board): ``` pio run -e ttgo-t-beam pio run -e wiscore_rak4631 +pio run -e xiao_nrf52840 ``` Build and upload a single environment (board): ``` pio run -e ttgo-t-beam -t upload pio run -e wiscore_rak4631 -t upload +pio run -e xiao_nrf52840 -t upload ``` Build and package a single environment (board): ``` pio run -e ttgo-t-beam -t package pio run -e wiscore_rak4631 -t package +pio run -e xiao_nrf52840 -t package ``` Build all environments (boards): @@ -122,8 +188,8 @@ New firmware release procedure: - [ ] Extend KISS interface to support config/control of the integrated microReticulum stack - [ ] Add interface for easy customization of firmware -- [ ] Add power management and sleep states to extend battery runtime - [x] Add build targets for NRF52 boards +- [x] Add build target for Seeed XIAO nRF52840 + Wio-SX1262 Please open an Issue if you have trouble building ior using the API, and feel free to start a new Discussion for anything else. diff --git a/RNode_Firmware.ino b/RNode_Firmware.ino index 0b8f7f31..54aa9a65 100644 --- a/RNode_Firmware.ino +++ b/RNode_Firmware.ino @@ -378,11 +378,11 @@ void setup() { boot_seq(); #endif - #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 + #if BOARD_MODEL != BOARD_RAK4631 && BOARD_MODEL != BOARD_HELTEC_T114 && BOARD_MODEL != BOARD_TECHO && BOARD_MODEL != BOARD_T3S3 && BOARD_MODEL != BOARD_TBEAM_S_V1 && BOARD_MODEL != BOARD_HELTEC32_V4 && BOARD_MODEL != BOARD_XIAO_NRF52840 // Some boards need to wait until the hardware UART is set up before booting - // the full firmware. In the case of the RAK4631 and Heltec T114, the line below will wait - // until a serial connection is actually established with a master. Thus, it - // is disabled on this platform. + // the full firmware. In the case of the RAK4631, XIAO nRF52840, and Heltec T114, + // the line below will wait until a serial connection is actually established + // with a master. Thus, it is disabled on these platforms. while (!Serial); #endif @@ -1004,6 +1004,7 @@ bool startRadio() { LoRa->enableCrc(); LoRa->onReceive(receive_callback); + lora_receive(); // Flash an info pattern to indicate @@ -1822,6 +1823,7 @@ void serial_callback(uint8_t sbyte) { bool medium_free() { update_modem_status(); + update_noise_floor(); if (avoid_interference && interference_detected) { return false; } return !dcd; } @@ -2295,10 +2297,22 @@ void sleep_now() { delay(2000); analogWrite(PIN_VEXT_EN, 0); delay(100); - #endif + #elif BOARD_MODEL == BOARD_XIAO_NRF52840 + // XIAO nRF52840: put radio to sleep, configure DIO1 as wakeup, enter System OFF + if (radio_online) { + LoRa->sleep(); + } + #if defined(PIN_WAKEUP) + nrf_gpio_cfg_sense_input(g_ADigitalPinMap[PIN_WAKEUP], + NRF_GPIO_PIN_PULLDOWN, + NRF_GPIO_PIN_SENSE_HIGH); + #endif + NRF_POWER->SYSTEMOFF = 1; + #else sd_power_gpregret_set(0, 0x6d); nrf_gpio_cfg_sense_input(pin_btn_usr1, NRF_GPIO_PIN_PULLUP, NRF_GPIO_PIN_SENSE_LOW); NRF_POWER->SYSTEMOFF = 1; + #endif #endif #endif } diff --git a/Utilities.h b/Utilities.h index e5ede163..662aee1a 100644 --- a/Utilities.h +++ b/Utilities.h @@ -24,7 +24,7 @@ using namespace Adafruit_LittleFS_Namespace; #define EEPROM_FILE "eeprom" bool file_exists = false; - int written_bytes = 4; + int written_bytes = 0; File file(InternalFS); #endif #include @@ -376,6 +376,14 @@ extern RNS::Reticulum reticulum; void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); } void led_id_on() { } void led_id_off() { } + #elif BOARD_MODEL == BOARD_XIAO_NRF52840 + // XIAO LEDs are active-low (LED_ON=LOW, LED_OFF=HIGH); rx and tx share LED_RED + void led_rx_on() { digitalWrite(pin_led_rx, LED_ON); } + void led_rx_off() { digitalWrite(pin_led_rx, LED_OFF); } + void led_tx_on() { digitalWrite(pin_led_tx, LED_ON); } + void led_tx_off() { digitalWrite(pin_led_tx, LED_OFF); } + void led_id_on() { } + void led_id_off() { } #endif #endif @@ -1714,6 +1722,8 @@ bool eeprom_model_valid() { #elif BOARD_MODEL == BOARD_HELTEC_T114 if (model == MODEL_C6 || model == MODEL_C7) { #elif BOARD_MODEL == BOARD_RAK4631 + if (model == MODEL_11 || model == MODEL_12) { + #elif BOARD_MODEL == BOARD_XIAO_NRF52840 if (model == MODEL_11 || model == MODEL_12) { #elif BOARD_MODEL == BOARD_HUZZAH32 if (model == MODEL_FF) { @@ -1868,7 +1878,7 @@ void eeprom_conf_load() { } void eeprom_conf_save() { - if (hw_ready && radio_online) { + if (hw_ready) { eeprom_update(eeprom_addr(ADDR_CONF_SF), lora_sf); eeprom_update(eeprom_addr(ADDR_CONF_CR), lora_cr); eeprom_update(eeprom_addr(ADDR_CONF_TXP), lora_txp); diff --git a/extra_script.py b/extra_script.py index 1904d069..cecb5aca 100644 --- a/extra_script.py +++ b/extra_script.py @@ -27,6 +27,9 @@ def pre_upload(source, target, env): print("*** Executing pre_upload steps...") # do some actions +def is_nrf52_platform(platform): + return "nordicnrf52" in platform or "seeedboards" in platform or "platform-seeedboards" in platform + def post_upload(source, target, env): print("*** Executing post_upload steps...") print("Platform:", env.GetProjectOption("platform")) @@ -43,9 +46,8 @@ def post_upload(source, target, env): firmware_hash(source, env) # firmware pacakaging is incomplete due to missing console image #firmware_package(env) - elif ("nordicnrf52" in platform): + elif is_nrf52_platform(platform): time.sleep(5) - # device provisioning is incomplete and only currently appropriate for 915MHz RAK4631 device_provision(env) time.sleep(5) firmware_hash(source, env) @@ -95,6 +97,10 @@ def device_provision(env): env.Execute("rnodeconf --product 10 --model 12 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) case "heltec_t114" | "heltec_t114_local": env.Execute("rnodeconf --product c2 --model c7 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) + case "xiao_nrf52840": + # No dedicated rnodeconf product ID for XIAO nRF52840 yet; reuse RAK4631 + # product/model bytes (0x10/0x12) — same MCU+SX1262 stack, 868/915 MHz band + env.Execute("rnodeconf --product 10 --model 12 --hwrev 1 --rom " + env.subst("$UPLOAD_PORT")) case _: print(f"Unknown board variant {variant}, can not provision device!") @@ -104,10 +110,9 @@ def firmware_hash(source, env): source_file = source[0].get_abspath() platform = env.GetProjectOption("platform") print("Platform:", platform) - if (platform == "nordicnrf52"): + if is_nrf52_platform(platform): build_dir = env.subst("$BUILD_DIR") env.Execute("cd " + build_dir + "; unzip -o " + source_file + " " + env.subst("$PROGNAME") + ".bin") - #source_file.replace(".zip", ".bin") source_file = build_dir + "/" + env.subst("$PROGNAME") + ".bin"; print("source_file:", source_file) firmware_data = open(source_file, "rb").read() @@ -171,7 +176,7 @@ def firmware_package(env): zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".elf " zip_cmd += build_dir + "/" + env.subst("$PROGNAME") + ".map " env.Execute(zip_cmd) - elif (platform == "nordicnrf52"): + elif is_nrf52_platform(platform): env.Execute("cp " + build_dir + "/" + env.subst("$PROGNAME") + ".zip " + project_dir + "/Release/.") env.Execute("python " + project_dir + "/release_hashes.py > " + project_dir + "/Release/release.json") @@ -207,7 +212,7 @@ def firmware_package(env): title="Package", description="Package esp32 firmware for delivery" ) -elif (platform == "nordicnrf52"): +elif is_nrf52_platform(platform): # remove --specs=nano.specs to allow exceptions to work if '--specs=nano.specs' in env['LINKFLAGS']: env['LINKFLAGS'].remove('--specs=nano.specs') diff --git a/platformio.ini b/platformio.ini index ca9d1aad..6636d9a3 100644 --- a/platformio.ini +++ b/platformio.ini @@ -991,3 +991,31 @@ lib_deps = adafruit/Adafruit NeoPixel@^1.12.0 microStore=symlink://../microStore microReticulum=symlink://../microReticulum + +; MCU: Nordic nRF52840 +; SRAM: 256 KB +; Flash: 1 MB internal +; LoRa: Semtech SX1262 (via Wio-SX1262 expansion board) +; Power: Battery/solar optimized with duty-cycle RX support +[env:xiao_nrf52840] +platform = https://github.com/Seeed-Studio/platform-seeedboards.git +board = seeed-xiao-afruitnrf52-nrf52840 +framework = arduino +custom_variant = xiao_nrf52840 +lib_archive = no +build_flags = + ${env.build_flags} + -fexceptions + -DBOARD_MODEL=BOARD_XIAO_NRF52840 + -DUSTORE_USE_INTERNALFS + -DURTN_PATH_TABLE_MAX_RECS=25 + -DRNS_USE_ALLOCATOR=1 + -DRNS_USE_TLSF=1 + -Wl,--allow-multiple-definition +build_unflags = -fno-exceptions +lib_deps = + ${env.lib_deps} + XPowersLib@^0.2.1 + https://github.com/attermann/microStore.git + https://github.com/attermann/microReticulum.git + diff --git a/sx126x.cpp b/sx126x.cpp index 77e4dc3e..bf3c23c2 100644 --- a/sx126x.cpp +++ b/sx126x.cpp @@ -21,6 +21,7 @@ #define OP_STANDBY_6X 0x80 #define OP_TX_6X 0x83 #define OP_RX_6X 0x82 +#define OP_RX_DUTY_CYCLE_6X 0x94 #define OP_PA_CONFIG_6X 0x95 #define OP_SET_IRQ_FLAGS_6X 0x08 // Also provides info such as // preamble detection, etc for @@ -127,7 +128,10 @@ sx126x::sx126x() : _fifo_rx_addr_ptr(0), _packet({0}), _preinit_done(false), - _onReceive(NULL) + _onReceive(NULL), + _rxDutyCycleEnabled(false), + _rxPeriodUs(0), + _sleepPeriodUs(0) { setTimeout(0); } bool sx126x::preInit() { @@ -504,18 +508,29 @@ bool sx126x::dcd() { bool header_detected = false; bool carrier_detected = false; - if ((buf[1] & IRQ_HEADER_DET_MASK_6X) != 0) { header_detected = true; carrier_detected = true; } - else { header_detected = false; } + if ((buf[1] & IRQ_HEADER_DET_MASK_6X) != 0) { + header_detected = true; carrier_detected = true; + // Keep the software timer alive so it doesn't expire mid-reception. + preamble_detected_at = now; + } else { header_detected = false; } if ((buf[1] & IRQ_PREAMBLE_DET_MASK_6X) != 0) { carrier_detected = true; if (preamble_detected_at == 0) { preamble_detected_at = now; } - if (now - preamble_detected_at > lora_preamble_time_ms + lora_header_time_ms) { + // Clear preamble IRQ bit immediately. The SX1262 preamble IRQ is not + // routed to DIO1, so this bit is never cleared by handleDio0Rise(). + // Without immediate clearing, spurious detections latch for up to 147ms + // each; under high-frequency CSMA polling the medium appears perpetually + // busy. The software timer below preserves the correct busy window. + uint8_t clearbuf[2] = {0}; + clearbuf[1] = IRQ_PREAMBLE_DET_MASK_6X; + executeOpcode(OP_CLEAR_IRQ_STATUS_6X, clearbuf, 2); + } else if (!header_detected && preamble_detected_at != 0) { + if (now - preamble_detected_at < (uint32_t)(lora_preamble_time_ms + lora_header_time_ms)) { + carrier_detected = true; // Still within expected preamble+header window + } else { preamble_detected_at = 0; - if (!header_detected) { false_preamble_detected = true; } - uint8_t clearbuf[2] = {0}; - clearbuf[1] = IRQ_PREAMBLE_DET_MASK_6X; - executeOpcode(OP_CLEAR_IRQ_STATUS_6X, clearbuf, 2); + false_preamble_detected = true; } } @@ -692,7 +707,47 @@ void sx126x::standby() { executeOpcode(OP_STANDBY_6X, &byte, 1); } -void sx126x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_6X, &byte, 1); } +void sx126x::sleep() { uint8_t byte = 0x00; executeOpcode(OP_SLEEP_6X, &byte, 1); _rxDutyCycleEnabled = false; } + +void sx126x::setRxDutyCycle(uint32_t rxPeriodUs, uint32_t sleepPeriodUs) { + _rxPeriodUs = rxPeriodUs; + _sleepPeriodUs = sleepPeriodUs; +} + +void sx126x::startRxDutyCycle() { + if (_rxPeriodUs == 0 || _sleepPeriodUs == 0) { + setRxDutyCycleParams(_sf, getSignalBandwidth(), _preambleLength); + } + explicitHeaderMode(); + if (_rxen != -1) { rxAntEnable(); } + // Convert µs to 15.625µs steps (64 kHz RC clock): steps = us * 64 / 1000 + uint32_t rxPeriod = (_rxPeriodUs * 64) / 1000; + uint32_t sleepPeriod = (_sleepPeriodUs * 64) / 1000; + uint8_t buf[6]; + buf[0] = (rxPeriod >> 16) & 0xFF; + buf[1] = (rxPeriod >> 8) & 0xFF; + buf[2] = rxPeriod & 0xFF; + buf[3] = (sleepPeriod >> 16) & 0xFF; + buf[4] = (sleepPeriod >> 8) & 0xFF; + buf[5] = sleepPeriod & 0xFF; + executeOpcode(OP_RX_DUTY_CYCLE_6X, buf, 6); + _rxDutyCycleEnabled = true; +} + +void sx126x::stopRxDutyCycle() { standby(); _rxDutyCycleEnabled = false; } + +bool sx126x::isRxDutyCycleEnabled() { return _rxDutyCycleEnabled; } + +void sx126x::setRxDutyCycleParams(uint8_t sf, long bw, uint16_t preambleSymbols) { + // Symbol time in µs = (2^SF * 1e6) / BW + uint32_t symbolTimeUs = ((uint32_t)1 << sf) * 1000000UL / bw; + // RX window: 2 symbols + 5ms TCXO warmup + _rxPeriodUs = (2 * symbolTimeUs) + 5000; + // Sleep: (preamble - 3) symbols so we always wake during preamble + _sleepPeriodUs = (preambleSymbols > 4) ? (preambleSymbols - 3) * symbolTimeUs : symbolTimeUs; + if (_rxPeriodUs < 5000) _rxPeriodUs = 5000; + if (_sleepPeriodUs < 1000) _sleepPeriodUs = 1000; +} void sx126x::enableTCXO() { #if HAS_TCXO @@ -712,6 +767,8 @@ void sx126x::enableTCXO() { uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; #elif BOARD_MODEL == BOARD_HELTEC32_V4 uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; + #elif BOARD_MODEL == BOARD_XIAO_NRF52840 + uint8_t buf[4] = {MODE_TCXO_1_8V_6X, 0x00, 0x00, 0xFF}; #endif executeOpcode(OP_DIO3_TCXO_CTRL_6X, buf, 4); #endif diff --git a/sx126x.h b/sx126x.h index c94a5528..ae010908 100644 --- a/sx126x.h +++ b/sx126x.h @@ -56,6 +56,13 @@ class sx126x : public Stream { void receive(int size = 0); void standby(); void sleep(); + + // Low Power RX Duty Cycle Mode + void setRxDutyCycle(uint32_t rxPeriodUs, uint32_t sleepPeriodUs); + void startRxDutyCycle(); + void stopRxDutyCycle(); + bool isRxDutyCycleEnabled(); + void setRxDutyCycleParams(uint8_t sf, long bw, uint16_t preambleSymbols); void reset(void); bool preInit(); @@ -140,6 +147,9 @@ class sx126x : public Stream { bool _preinit_done; volatile bool _dio0_pending; void (*_onReceive)(int); + bool _rxDutyCycleEnabled; + uint32_t _rxPeriodUs; + uint32_t _sleepPeriodUs; }; extern sx126x sx126x_modem;