Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
783a572
docs(refactoring): design cut D of note 0011, the radio globals from …
marcelverdult Oct 21, 2026
6510e4d
test(variants): pin the radio, wrapper and RTC globals of 31 nRF52 va…
marcelverdult Oct 21, 2026
a8dc76a
refactor(variants): define the radio globals once for 31 nRF52 SX1262…
marcelverdult Oct 21, 2026
060481b
docs(refactoring): record cut D1 of note 0011, the radio globals of 3…
marcelverdult Oct 21, 2026
02978dd
test(variants): pin the radio globals and radio_init() of 22 ESP32 va…
marcelverdult Oct 21, 2026
67619b7
refactor(variants): define the radio globals and radio_init() once fo…
marcelverdult Oct 21, 2026
33a5241
docs(refactoring): record cut D2 of note 0011, the radio globals and …
marcelverdult Oct 22, 2026
50a1f74
docs(refactoring): wrap a line in the D2 record
marcelverdult Oct 22, 2026
a051146
docs(refactoring): cut D, the constructors of the moved globals with …
marcelverdult Oct 22, 2026
12e3730
test(esp32): pin the RTC backup of ESP32RTCClock across two translati…
marcelverdult Oct 22, 2026
836678d
fix(esp32): define the RTC backup of ESP32RTCClock once
marcelverdult Oct 22, 2026
e607226
test(esp32): keep the RTC backup suite out of the native environment
marcelverdult Oct 23, 2026
a154984
docs(refactoring): note 0012, one RTC backup for ESP32RTCClock
marcelverdult Oct 23, 2026
24e3ead
test(esp32): include <time.h> in the ESP-IDF stand-ins
marcelverdult Oct 26, 2026
37cd6a0
docs(refactoring): note 0012, the <time.h> fix of the stand-ins
marcelverdult Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run-unit-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
uses: ./.github/actions/setup-build-environment

- name: Run Unit Tests
run: pio test -e native -e native_kiss_modem -e native_core -e native_ofast -e native_chat -e native_identity_store -e native_node_prefs -e native_radio_wrappers -e native_server -e native_sensor -e native_repeater -e native_repeater_full -e native_region -e native_radio_chips -e native_variant_target -e native_variant_target_override -e native_companion -vv
run: pio test -e native -e native_kiss_modem -e native_core -e native_ofast -e native_chat -e native_identity_store -e native_node_prefs -e native_radio_wrappers -e native_server -e native_sensor -e native_repeater -e native_repeater_full -e native_region -e native_radio_chips -e native_variant_target -e native_variant_target_override -e native_companion -e native_esp32_rtc_backup -vv

- name: Upload Test Results
# Upload test results even if the test step failed.
Expand Down
599 changes: 597 additions & 2 deletions docs/refactoring/0011-variant-target.md

Large diffs are not rendered by default.

269 changes: 269 additions & 0 deletions docs/refactoring/0012-esp32-rtc-backup.md

Large diffs are not rendered by default.

135 changes: 135 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,11 @@ test_ignore =
test_region_map
test_variant_target
test_variant_target_override
test_variant_target_globals
test_variant_target_esp32
test_variant_target_esp32_override
test_companion_frames
test_esp32_rtc_backup
build_src_filter =
-<*>
+<../src/Utils.cpp>
Expand Down Expand Up @@ -521,6 +525,137 @@ build_src_filter = ${env:native_variant_target.build_src_filter}
lib_deps =
google/googletest @ 1.17.0

[env:native_variant_target_globals]
; The radio, wrapper and RTC globals of a RadioLib variant (cut D of note 0011):
; the shared code against the target.h of a variant with the real
; AutoDiscoverRTCClock (test/mocks/radiolib_variant_globals, ahead of the other
; stand-ins). The pins and NRF52_PLATFORM are what an nRF52 variant's build
; flags and variant.h set; their values only have to differ from each other.
platform = native
test_framework = googletest
build_flags = -std=c++17
-I test/mocks/radiolib_variant_globals
-I test/mocks/radiolib_variant
-I src
-I test/mocks
-D LORA_SF=8
-D WRAPPER_CLASS=TestRadioWrapper
-D RADIO_CLASS=TestRadio
-D NRF52_PLATFORM
-D P_LORA_NSS=11
-D P_LORA_DIO_1=12
-D P_LORA_RESET=13
-D P_LORA_BUSY=14
test_build_src = yes
test_filter = test_variant_target_globals
build_src_filter = ${env:native_variant_target.build_src_filter}
lib_deps =
google/googletest @ 1.17.0

[env:native_variant_target_esp32]
; The radio globals and radio_init() of an ESP32 variant of cut D2 of note 0011,
; with an SX1262 on its own SPI bus (P_LORA_SCLK) on the default host, and a
; variant with its own radio_init() (test_variant_target_esp32_override); the
; three below are the other board descriptions that occur. ESP32 and the pins
; are what the variants' build flags set; the values only have to differ.
; ICACHE_RAM_ATTR is what the ESP32 core defines for RadioLibWrappers.cpp.
platform = native
test_framework = googletest
build_flags = -std=c++17
-I test/mocks/radiolib_variant_esp32
-I test/mocks/radiolib_variant
-I src
-I test/mocks
-D LORA_SF=8
-D WRAPPER_CLASS=TestRadioWrapper
-D RADIO_CLASS=TestRadio
-D ESP32
-D ICACHE_RAM_ATTR=
-D P_LORA_NSS=11
-D P_LORA_DIO_1=12
-D P_LORA_RESET=13
-D P_LORA_BUSY=14
-D P_LORA_DIO_0=15
-D P_LORA_SCLK=16
test_build_src = yes
test_filter = test_variant_target_esp32*
build_src_filter = ${env:native_variant_target.build_src_filter}
lib_deps =
google/googletest @ 1.17.0

[env:native_variant_target_esp32_no_sclk]
; The same without P_LORA_SCLK: no own bus, RadioLib starts the default one.
platform = native
test_framework = googletest
build_flags = -std=c++17
-I test/mocks/radiolib_variant_esp32
-I test/mocks/radiolib_variant
-I src
-I test/mocks
-D LORA_SF=8
-D WRAPPER_CLASS=TestRadioWrapper
-D RADIO_CLASS=TestRadio
-D ESP32
-D ICACHE_RAM_ATTR=
-D P_LORA_NSS=11
-D P_LORA_DIO_1=12
-D P_LORA_RESET=13
-D P_LORA_BUSY=14
-D P_LORA_DIO_0=15
test_build_src = yes
test_filter = test_variant_target_esp32
build_src_filter = ${env:native_variant_target.build_src_filter}
lib_deps =
google/googletest @ 1.17.0

[env:native_variant_target_esp32_sx1276]
; The same with an SX1276, whose Module takes DIO0 and DIO1 instead of DIO1
; and BUSY (USE_SX1276 is what CustomSX1276Wrapper.h defines).
platform = native
test_framework = googletest
build_flags = ${env:native_variant_target_esp32.build_flags}
-D USE_SX1276
test_build_src = yes
test_filter = test_variant_target_esp32
build_src_filter = ${env:native_variant_target.build_src_filter}
lib_deps =
google/googletest @ 1.17.0

[env:native_esp32_rtc_backup]
; The RTC backup of ESP32RTCClock: the real src/helpers/ESP32Board.h and
; ESP32Board.cpp, which defines the backup, against stand-ins for the ESP-IDF
; headers and the variant's target.h, used from two translation units
; (test/test_esp32_rtc_backup/other_unit.cpp). The pins are what ESP32Board
; uses; their values are not.
platform = native
test_framework = googletest
build_flags = -std=c++17
-I test/mocks/esp32_idf
-I src
-I test/mocks
-D ESP_PLATFORM
-D P_LORA_DIO_1=1
-D P_LORA_NSS=2
test_build_src = yes
test_filter = test_esp32_rtc_backup
build_src_filter =
-<*>
+<helpers/ESP32Board.cpp>
lib_deps =
google/googletest @ 1.17.0

[env:native_variant_target_esp32_spi_host]
; The same with the own bus on another host than the SPIClass default.
platform = native
test_framework = googletest
build_flags = ${env:native_variant_target_esp32.build_flags}
-D P_LORA_SPI_HOST=FSPI
test_build_src = yes
test_filter = test_variant_target_esp32
build_src_filter = ${env:native_variant_target.build_src_filter}
lib_deps =
google/googletest @ 1.17.0

[env:native_region]
; RegionMap on the host. It reads packets and keys and writes nothing but its own
; table, so it builds against the small stubs in test/test_region_map/stubs, which are
Expand Down
4 changes: 4 additions & 0 deletions src/helpers/ESP32Board.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
#include "ESP32Board.h"
#include <target.h>

// The RTC backup of ESP32RTCClock, declared in ESP32Board.h.
RTC_NOINIT_ATTR uint32_t _rtc_backup_time;
RTC_NOINIT_ATTR uint32_t _rtc_backup_magic;

#if defined(ADMIN_PASSWORD) && !defined(DISABLE_WIFI_OTA) // Repeater or Room Server only
#include <WiFi.h>
#include <AsyncTCP.h>
Expand Down
7 changes: 5 additions & 2 deletions src/helpers/ESP32Board.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,11 @@ class ESP32Board : public mesh::MainBoard {
}
};

static RTC_NOINIT_ATTR uint32_t _rtc_backup_time;
static RTC_NOINIT_ATTR uint32_t _rtc_backup_magic;
// The time ESP32RTCClock restores after a reset, in RTC memory that a reset does
// not clear. Defined once, in ESP32Board.cpp: the inline members below run in
// every translation unit that uses them and must all see the same backup.
extern uint32_t _rtc_backup_time;
extern uint32_t _rtc_backup_magic;
#define RTC_BACKUP_MAGIC 0xAA55CC33
#define RTC_TIME_MIN 1772323200 // 1 Mar 2026

Expand Down
77 changes: 76 additions & 1 deletion src/helpers/radiolib/RadioLibTarget.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// The parts of a variant's target.cpp that every board with a RadioLib radio
// writes the same way, defined once here. They are weak: a variant (or a
// writes the same way, defined once here. The functions are weak: a variant (or a
// downstream fork's variant) that still defines its own keeps compiling, and its
// definition takes precedence. WRAPPER_CLASS is what tells a RadioLib variant
// apart; the ESP-NOW variants have no LoRa radio and define their own.
Expand All @@ -9,6 +9,81 @@

#include <helpers/radiolib/RadioLibWrappers.h>

// The radio, its wrapper and the RTC clock, for the variants whose target.h
// asks for them with RADIOLIB_TARGET_GLOBALS. Built from the board's
// description: the P_LORA_* pins, the chip, the platform, and on ESP32
// P_LORA_SCLK (an own SPI bus) and P_LORA_SPI_HOST (its host, if not the
// SPIClass default). Not weak, unlike the functions below: a weak object is
// still constructed here, into the variant's object where the variant defines
// its own, so a variant that keeps its globals simply does not define the macro.
#ifdef RADIOLIB_TARGET_GLOBALS

#if !defined(NRF52_PLATFORM) && !defined(ESP32)
#error "RADIOLIB_TARGET_GLOBALS: the radio globals are described for nRF52 and ESP32 only so far; define them in target.cpp"
#endif

#include <SPI.h>
#include <helpers/AutoDiscoverRTCClock.h>
#if defined(ESP32)
#include <helpers/ESP32Board.h>
#else
#include <helpers/ArduinoHelpers.h>
#endif

// The pins in the order the chip's Module takes them.
#if defined(USE_SX1276)
#define RADIOLIB_TARGET_PINS P_LORA_NSS, P_LORA_DIO_0, P_LORA_RESET, P_LORA_DIO_1
#else
#define RADIOLIB_TARGET_PINS P_LORA_NSS, P_LORA_DIO_1, P_LORA_RESET, P_LORA_BUSY
#endif

// On nRF52 the radio is on SPI, passed to RadioLib, so that std_init() and not
// RadioLib starts the bus. On ESP32 it has a bus of its own where the board
// names the SPI pins, and otherwise RadioLib starts its default bus itself.
#if defined(ESP32)
#if defined(P_LORA_SCLK)
#if defined(P_LORA_SPI_HOST)
static SPIClass spi(P_LORA_SPI_HOST);
#else
static SPIClass spi;
#endif
static RADIO_CLASS radio = new Module(RADIOLIB_TARGET_PINS, spi);
#else
static RADIO_CLASS radio = new Module(RADIOLIB_TARGET_PINS);
#endif
#else
static RADIO_CLASS radio = new Module(RADIOLIB_TARGET_PINS, SPI);
#endif

WRAPPER_CLASS radio_driver(radio, board);

#if defined(ESP32)
ESP32RTCClock fallback_clock;
#else
VolatileRTCClock fallback_clock;
#endif
AutoDiscoverRTCClock rtc_clock(fallback_clock);

// The radio_init() of these variants on ESP32 with a chip that has std_init():
// start the fallback clock and the RTC, then the radio on its bus.
#if defined(ESP32) && (defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_SX1276))

#include <Wire.h>

__attribute__((weak)) bool radio_init() {
fallback_clock.begin();
rtc_clock.begin(Wire);
#if defined(P_LORA_SCLK)
return radio.std_init(&spi);
#else
return radio.std_init();
#endif
}

#endif

#endif

// A new random identity, from the noise of the radio radio_driver wraps (which
// is the variant's global radio). Slow, so only for creating the node's identity.
__attribute__((weak)) mesh::LocalIdentity radio_new_identity() {
Expand Down
10 changes: 10 additions & 0 deletions test/mocks/esp32_idf/Wire.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#pragma once

// Stand-in for the Arduino Wire library, as far as ESP32Board::begin() uses it.
class TwoWire {
public:
bool begin() { return true; }
bool begin(int sda, int scl) { return true; }
};

inline TwoWire Wire;
4 changes: 4 additions & 0 deletions test/mocks/esp32_idf/driver/rtc_io.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once

// Stand-in for the ESP-IDF header of the same name, see esp32_idf_stubs.h.
#include "../esp32_idf_stubs.h"
72 changes: 72 additions & 0 deletions test/mocks/esp32_idf/esp32_idf_stubs.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#pragma once

// Stand-in for the parts of the ESP32 Arduino core and ESP-IDF that
// src/helpers/ESP32Board.h and ESP32Board.cpp name, so both compile on the host.
// Only ESP32RTCClock is exercised: esp_reset_reason() returns what a test sets,
// and settimeofday() is redirected to a recorder, so a test sees which time
// begin() and setCurrentTime() set without touching the host's clock. The other
// declarations only have to compile.

#include <stdint.h>
#include <sys/time.h>
#include <time.h> // time(), which the ESP32 core declares through Arduino.h

// esp_attr.h: on the ESP32 a section in RTC memory that a reset does not clear;
// on the host ordinary (zero initialized) memory.
#define RTC_NOINIT_ATTR

typedef enum {
ESP_RST_UNKNOWN,
ESP_RST_POWERON,
ESP_RST_EXT,
ESP_RST_SW,
ESP_RST_PANIC,
ESP_RST_INT_WDT,
ESP_RST_TASK_WDT,
ESP_RST_WDT,
ESP_RST_DEEPSLEEP,
ESP_RST_BROWNOUT,
ESP_RST_SDIO,
} esp_reset_reason_t;

inline esp_reset_reason_t g_esp_reset_reason = ESP_RST_POWERON;
inline esp_reset_reason_t esp_reset_reason() { return g_esp_reset_reason; }
inline void esp_restart() { }

// settimeofday() as ESP32RTCClock calls it: records the time instead of setting
// the host's clock. <sys/time.h> is included above, so the header's own include
// of it does not see the macro.
inline int g_settimeofday_calls = 0;
inline time_t g_settimeofday_sec = 0;
inline int esp32_test_settimeofday(const struct timeval* tv, const void*) {
g_settimeofday_calls++;
g_settimeofday_sec = tv->tv_sec;
return 0;
}
#define settimeofday esp32_test_settimeofday

typedef int gpio_num_t;
enum { GPIO_INTR_POSEDGE = 1, GPIO_INTR_HIGH_LEVEL = 5 };
#ifndef HIGH
#define HIGH 1
#endif
inline int gpio_get_level(gpio_num_t) { return 0; }
inline void gpio_wakeup_enable(gpio_num_t, int) { }
inline void gpio_wakeup_disable(gpio_num_t) { }
inline void gpio_set_intr_type(gpio_num_t, int) { }
inline void esp_sleep_enable_timer_wakeup(uint64_t) { }
inline void esp_sleep_enable_gpio_wakeup() { }
inline void esp_light_sleep_start() { }
enum { ESP_SLEEP_WAKEUP_ALL = 0 };
inline void esp_sleep_disable_wakeup_source(int) { }
inline void esp_deep_sleep_start() { }
inline void rtc_gpio_hold_en(gpio_num_t) { }
inline void gpio_hold_en(gpio_num_t) { }
inline void digitalWrite(int, int) { }

typedef int portMUX_TYPE;
#define portMUX_INITIALIZER_UNLOCKED 0
#define portENTER_CRITICAL(mux) ((void)(mux))
#define portEXIT_CRITICAL(mux) ((void)(mux))

inline float temperatureRead() { return 0.0f; }
4 changes: 4 additions & 0 deletions test/mocks/esp32_idf/esp_system.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once

// Stand-in for the ESP-IDF header of the same name, see esp32_idf_stubs.h.
#include "esp32_idf_stubs.h"
4 changes: 4 additions & 0 deletions test/mocks/esp32_idf/rom/rtc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once

// Stand-in for the ESP-IDF header of the same name, see esp32_idf_stubs.h.
#include "../esp32_idf_stubs.h"
4 changes: 4 additions & 0 deletions test/mocks/esp32_idf/soc/rtc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once

// Stand-in for the ESP-IDF header of the same name, see esp32_idf_stubs.h.
#include "../esp32_idf_stubs.h"
19 changes: 19 additions & 0 deletions test/mocks/esp32_idf/target.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

// Stand-in for a variant's target.h, as far as src/helpers/ESP32Board.cpp uses
// it: ESP32Board::enterDeepSleep() powers off the radio and stops the GPS.

#include <helpers/ESP32Board.h>

struct TestRadioDriver {
void powerOff() { }
};
inline TestRadioDriver radio_driver;

struct TestLocationProvider {
void stop() { }
};
struct TestSensors {
TestLocationProvider* getLocationProvider() { return nullptr; }
};
inline TestSensors sensors;
Loading
Loading