568 lines
28 KiB
Python
568 lines
28 KiB
Python
"""Exercise RTC guards and real terminal GPIO retention on ESP32 families."""
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
from cpp_source import body
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
HEADER = ROOT / "src/helpers/ESP32GpioHold.h"
|
|
SDK = r'''
|
|
#pragma once
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
using gpio_num_t=int;
|
|
using esp_err_t=int;
|
|
constexpr int ESP_OK=0;
|
|
constexpr int ESP_ERR_INVALID_ARG=258;
|
|
constexpr int GPIO_MODE_OUTPUT=2, GPIO_PULLUP_DISABLE=0;
|
|
constexpr int GPIO_PULLDOWN_DISABLE=0, GPIO_INTR_DISABLE=0;
|
|
struct gpio_config_t { uint64_t pin_bit_mask; int mode,pull_up_en,pull_down_en,intr_type; };
|
|
inline unsigned held=0,released=0,deinitialized=0,digital_held=0;
|
|
inline int api_result=0;
|
|
struct Pad { int latch=0; bool output=false, hold=false; int retained=-1; };
|
|
inline Pad pads[64];
|
|
inline bool deep_hold=false;
|
|
inline bool require_configured_release=false;
|
|
inline unsigned deep_hold_en_calls=0, deep_hold_dis_calls=0;
|
|
inline bool valid_output(int pin) {
|
|
#if CHIP_S3
|
|
return pin>=0 && pin<=48 && pin!=46;
|
|
#else
|
|
return pin>=0 && pin<=33;
|
|
#endif
|
|
}
|
|
#define GPIO_IS_VALID_OUTPUT_GPIO(pin) valid_output(pin)
|
|
inline int physical(int pin) { return pads[pin].hold ? pads[pin].retained : (pads[pin].output ? pads[pin].latch : -1); }
|
|
inline int gpio_set_level(int pin,unsigned level) { assert(valid_output(pin)); pads[pin].latch=level; return api_result; }
|
|
inline int gpio_config(const gpio_config_t* config) {
|
|
assert(config->mode==GPIO_MODE_OUTPUT && config->pull_up_en==0 && config->pull_down_en==0 && config->intr_type==0);
|
|
if (api_result) return api_result;
|
|
for(int pin=0;pin<64;++pin) if(config->pin_bit_mask&(1ULL<<pin)) {
|
|
assert(valid_output(pin)); pads[pin].output=true;
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
|
|
inline bool rtc_gpio_is_valid_gpio(int pin) {
|
|
#if CHIP_S3
|
|
return pin>=0 && pin<=21;
|
|
#else
|
|
return pin==0 || pin==2 || pin==4 || (pin>=12 && pin<=15)
|
|
|| (pin>=25 && pin<=27) || (pin>=32 && pin<=39);
|
|
#endif
|
|
}
|
|
inline int rtc_gpio_hold_en(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); ++held; if(!api_result) { pads[pin].retained=physical(pin); pads[pin].hold=true; } return api_result; }
|
|
inline int rtc_gpio_hold_dis(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); assert(!require_configured_release || !pads[pin].hold || pads[pin].output); ++released; if(!api_result) pads[pin].hold=false; return api_result; }
|
|
inline int rtc_gpio_deinit(int pin) { assert(pin>=0 && rtc_gpio_is_valid_gpio(pin)); ++deinitialized; return api_result; }
|
|
#endif
|
|
inline int gpio_hold_en(int pin) {
|
|
assert(valid_output(pin));
|
|
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
|
|
if(rtc_gpio_is_valid_gpio(pin)) return rtc_gpio_hold_en(pin);
|
|
#endif
|
|
++digital_held;
|
|
if(!api_result) { pads[pin].retained=physical(pin); pads[pin].hold=true; }
|
|
return api_result;
|
|
}
|
|
inline int gpio_hold_dis(int pin) {
|
|
assert(valid_output(pin));
|
|
assert(pads[pin].output); // Mode MUST be restored before release.
|
|
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
|
|
if(rtc_gpio_is_valid_gpio(pin)) return rtc_gpio_hold_dis(pin);
|
|
#endif
|
|
if(!api_result) pads[pin].hold=false;
|
|
return api_result;
|
|
}
|
|
#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
|
inline void gpio_deep_sleep_hold_en() { deep_hold=true; ++deep_hold_en_calls; }
|
|
inline void gpio_deep_sleep_hold_dis() { deep_hold=false; ++deep_hold_dis_calls; }
|
|
#endif
|
|
inline void simulate_deep_sleep() {
|
|
for(int pin=0;pin<64;++pin) {
|
|
bool rtc=false;
|
|
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
|
|
rtc=rtc_gpio_is_valid_gpio(pin);
|
|
#endif
|
|
#if SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP
|
|
if(!rtc && !pads[pin].hold) { pads[pin].output=false; pads[pin].retained=-1; }
|
|
#else
|
|
if(!rtc && !deep_hold) { pads[pin].output=false; pads[pin].retained=-1; }
|
|
#endif
|
|
}
|
|
}
|
|
'''
|
|
HARNESS = r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#include <initializer_list>
|
|
int main() {
|
|
for(int pin : {-1,0,2,4,7,8,15,18,33,42,43,47,48,64}) {
|
|
bool valid=false;
|
|
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
|
|
valid=pin>=0 && rtc_gpio_is_valid_gpio(pin);
|
|
#endif
|
|
const unsigned before=held, release_before=released, deinit_before=deinitialized;
|
|
assert(mesh::esp32::supportsRtcGpio(pin)==valid);
|
|
assert(mesh::esp32::holdRtcGpioIfSupported(pin)==ESP_OK);
|
|
assert(mesh::esp32::releaseRtcGpioIfSupported(pin)==ESP_OK);
|
|
assert(mesh::esp32::deinitRtcGpioIfSupported(pin)==ESP_OK);
|
|
assert(held==before+valid && released==release_before+valid && deinitialized==deinit_before+valid);
|
|
}
|
|
assert(digital_held==0); // No broad digital retention behavior was invented.
|
|
#if SOC_RTCIO_INPUT_OUTPUT_SUPPORTED
|
|
api_result=99;
|
|
assert(mesh::esp32::holdRtcGpioIfSupported(4)==99);
|
|
assert(mesh::esp32::releaseRtcGpioIfSupported(4)==99);
|
|
assert(mesh::esp32::deinitRtcGpioIfSupported(4)==99);
|
|
#endif
|
|
}
|
|
'''
|
|
|
|
|
|
class GpioHoldTests(unittest.TestCase):
|
|
def compile_run(self, source, *, caps=1, s3=0, defines=(), broken=False, legacy_guard=False, helper_mutation=None, extra_source=None, files=None):
|
|
compiler=shutil.which("g++") or shutil.which("clang++")
|
|
self.assertIsNotNone(compiler)
|
|
with tempfile.TemporaryDirectory(prefix="esp32-rtc-guard-") as directory:
|
|
work=Path(directory)
|
|
(work/"driver").mkdir()
|
|
(work/"driver/rtc_io.h").write_text(SDK)
|
|
(work/"driver/gpio.h").write_text('#include "rtc_io.h"\n')
|
|
for name,contents in (files or {}).items():
|
|
path=work/name
|
|
path.parent.mkdir(parents=True,exist_ok=True)
|
|
path.write_text(contents)
|
|
if broken or legacy_guard or helper_mutation:
|
|
(work/"helpers").mkdir()
|
|
helper = HEADER.read_text()
|
|
if broken:
|
|
helper = helper.replace("if (supportsRtcGpio(pin))", "if (true)")
|
|
if legacy_guard:
|
|
guard = "#if defined(SOC_RTCIO_INPUT_OUTPUT_SUPPORTED) && SOC_RTCIO_INPUT_OUTPUT_SUPPORTED"
|
|
self.assertIn(guard, helper)
|
|
helper = helper.replace(guard, "#if !defined(SOC_RTCIO_INPUT_OUTPUT_SUPPORTED) || SOC_RTCIO_INPUT_OUTPUT_SUPPORTED")
|
|
if helper_mutation:
|
|
helper = helper_mutation(helper)
|
|
(work/"helpers/ESP32GpioHold.h").write_text(helper)
|
|
cpp,binary=work/"test.cpp",work/"test"
|
|
cpp.write_text(source)
|
|
extra=[]
|
|
if extra_source:
|
|
other=work/"other.cpp"
|
|
other.write_text(extra_source)
|
|
extra=[str(other)]
|
|
result=subprocess.run([compiler,"-std=c++17","-Wall","-Wextra","-Werror", "-I",str(work),"-I",str(ROOT/"src"),
|
|
*([] if caps is None else [f"-DSOC_RTCIO_INPUT_OUTPUT_SUPPORTED={caps}"]),
|
|
f"-DCHIP_S3={s3}",*("-D"+flag for flag in defines),str(cpp),*extra,"-o",str(binary)],capture_output=True,text=True)
|
|
if legacy_guard:
|
|
self.assertNotEqual(result.returncode, 0, "old undefined-capability guard compiled without RTC APIs")
|
|
self.assertIn("not declared", result.stderr)
|
|
return
|
|
self.assertEqual(result.returncode,0,result.stderr)
|
|
ran=subprocess.run([str(binary)],capture_output=True,text=True)
|
|
if broken or helper_mutation:
|
|
self.assertNotEqual(ran.returncode,0,"negative control concealed unsupported RTC call")
|
|
else:
|
|
self.assertEqual(ran.returncode,0,ran.stderr)
|
|
|
|
def test_chip_specific_and_absent_rtc_apis_no_global_hold(self):
|
|
for caps,s3 in ((1,0),(1,1),(0,0),(None,0)):
|
|
with self.subTest(caps=caps,s3=s3):
|
|
self.compile_run(HARNESS,caps=caps,s3=s3)
|
|
self.compile_run(HARNESS,broken=True)
|
|
# Installed ESP-IDF4.4C3 leaves the macro UNDEFINED and omits RTC API
|
|
# declarations entirely, rather than defining capability0.
|
|
self.compile_run(HARNESS,caps=None,legacy_guard=True)
|
|
|
|
def test_real_shutdown_latches_known_output_without_arming_global_hold(self):
|
|
shutdown=body((ROOT/"src/helpers/ESP32Board.cpp").read_text(),"void ESP32Board::shutdownPeripherals()")
|
|
code=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#include <cstddef>
|
|
#define HIGH 1
|
|
static bool powered_off=false,gps_off=false,flushed=false;
|
|
void delay(unsigned ms) { assert(ms==100); }
|
|
struct Radio { void powerOff(){powered_off=true;} } radio_driver;
|
|
struct GPS { void stop(){gps_off=true;} } gps;
|
|
struct Sensors { GPS* getLocationProvider(){return &gps;} void shutdownGpsTransportForDeepSleep(){assert(gps_off);} } sensors;
|
|
struct SerialPort { void flush(){flushed=true;} } Serial;
|
|
class ESP32Board { public: void shutdownPeripherals(); };
|
|
@SHUTDOWN@
|
|
int main(){ ESP32Board board; board.shutdownPeripherals();
|
|
assert(powered_off && gps_off && flushed);
|
|
const bool rtc=mesh::esp32::supportsRtcGpio(P_LORA_NSS);
|
|
assert(held==rtc && digital_held==!rtc);
|
|
assert(pads[P_LORA_NSS].output && physical(P_LORA_NSS)==HIGH);
|
|
assert(!deep_hold && deep_hold_en_calls==0);
|
|
}
|
|
'''.replace("@SHUTDOWN@",shutdown)
|
|
for caps,s3,definitions in ((1,0,("P_LORA_NSS=18",)),(1,1,("P_LORA_NSS=8",)),
|
|
(1,1,("P_LORA_NSS=43",)),(0,0,("P_LORA_NSS=18","CONFIG_IDF_TARGET_ESP32C3=1")),
|
|
(None,0,("P_LORA_NSS=18","CONFIG_IDF_TARGET_ESP32C3=1")),
|
|
(0,0,("P_LORA_NSS=18","CONFIG_IDF_TARGET_ESP32C6=1"))):
|
|
with self.subTest(definitions=definitions):
|
|
self.compile_run(code,caps=caps,s3=s3,defines=definitions)
|
|
|
|
def test_terminal_hold_retains_output_and_boot_release_preloads_before_unhold(self):
|
|
code=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
int main() {
|
|
using namespace mesh::esp32;
|
|
resetTerminalDeepSleepHolds();
|
|
assert(!deep_hold && !terminalDigitalHoldPending());
|
|
assert(holdOutputForDeepSleep(-1,1)==ESP_ERR_INVALID_ARG);
|
|
assert(holdOutputForDeepSleep(64,1)==ESP_ERR_INVALID_ARG);
|
|
assert(holdOutputForDeepSleep(34,1)==@PIN34_RESULT@);
|
|
resetTerminalDeepSleepHolds();
|
|
assert(holdOutputForDeepSleep(P_LORA_NSS,1)==ESP_OK);
|
|
assert(physical(P_LORA_NSS)==1 && pads[P_LORA_NSS].output);
|
|
assert(!deep_hold); // No global hold in runtime/light sleep.
|
|
commitTerminalDeepSleepHolds();
|
|
assert(deep_hold==!supportsRtcGpio(P_LORA_NSS));
|
|
simulate_deep_sleep();
|
|
assert(physical(P_LORA_NSS)==1); // Negative control detects lost non-RTC retention.
|
|
pads[P_LORA_NSS].latch=0; pads[P_LORA_NSS].output=false; // CPU reset.
|
|
resetTerminalDeepSleepHolds();
|
|
assert(!deep_hold);
|
|
assert(releaseOutputAfterDeepSleep(P_LORA_NSS,1)==ESP_OK);
|
|
assert(!pads[P_LORA_NSS].hold && physical(P_LORA_NSS)==1);
|
|
// Neither an invalid pin nor failed setup is allowed to request auto-hold.
|
|
resetTerminalDeepSleepHolds(); api_result=77;
|
|
assert(holdOutputForDeepSleep(P_LORA_NSS,0)==77);
|
|
commitTerminalDeepSleepHolds(); assert(!deep_hold);
|
|
}
|
|
'''
|
|
for caps,s3,pin in ((1,0,18),(1,1,43),(1,1,8),(None,0,18),(0,0,18)):
|
|
with self.subTest(caps=caps,s3=s3,pin=pin):
|
|
self.compile_run(code.replace('@PIN34_RESULT@','ESP_OK' if s3 else 'ESP_ERR_INVALID_ARG'),
|
|
caps=caps,s3=s3,defines=(f'P_LORA_NSS={pin}',))
|
|
# gpio_hold_en alone is NOT a physical non-RTC deep-sleep fix.
|
|
self.compile_run(code.replace('@PIN34_RESULT@','ESP_ERR_INVALID_ARG'),defines=('P_LORA_NSS=18',),
|
|
helper_mutation=lambda text: text.replace('if (terminalDigitalHoldPending()) gpio_deep_sleep_hold_en();',''))
|
|
|
|
def test_pending_terminal_hold_is_shared_between_board_and_fem_translation_units(self):
|
|
self.compile_run(r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
void femShutdown();
|
|
int main(){ mesh::esp32::resetTerminalDeepSleepHolds(); femShutdown();
|
|
assert(!deep_hold); mesh::esp32::commitTerminalDeepSleepHolds(); assert(deep_hold);
|
|
}
|
|
''',s3=1,extra_source=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
void femShutdown(){ assert(mesh::esp32::holdOutputForDeepSleep(42,1)==ESP_OK); }
|
|
''')
|
|
|
|
def test_single_pad_deep_sleep_hold_omits_unavailable_global_apis(self):
|
|
# IDF5.3 C6 headers omit the global APIs entirely. Per-pad holds are
|
|
# retained directly, including across reset until the pin is released.
|
|
self.compile_run(r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
int main() {
|
|
using namespace mesh::esp32;
|
|
resetTerminalDeepSleepHolds();
|
|
assert(!terminalDigitalHoldPending() && deep_hold_dis_calls==0);
|
|
assert(holdOutputForDeepSleep(18,1)==ESP_OK);
|
|
commitTerminalDeepSleepHolds();
|
|
assert(!deep_hold && deep_hold_en_calls==0);
|
|
simulate_deep_sleep();
|
|
assert(physical(18)==1);
|
|
pads[18].latch=0; pads[18].output=false;
|
|
resetTerminalDeepSleepHolds();
|
|
assert(deep_hold_dis_calls==0);
|
|
assert(releaseOutputAfterDeepSleep(18,1)==ESP_OK);
|
|
assert(!pads[18].hold && physical(18)==1);
|
|
}
|
|
''', caps=1, defines=('SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP=1',
|
|
'CONFIG_IDF_TARGET_ESP32C6=1'))
|
|
|
|
def test_only_terminal_sleep_commits_after_virtual_shutdown_and_wake_setup(self):
|
|
production=(ROOT/'src/helpers/ESP32Board.cpp').read_text()
|
|
sleep=body(production,'void ESP32Board::enterDeepSleep(uint32_t secs)')
|
|
self.compile_run(r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#include <cstdint>
|
|
constexpr int ESP_SLEEP_WAKEUP_ALL=99;
|
|
static bool shut=false,wake_cleared=false,timer_set=false,slept=false;
|
|
void esp_sleep_disable_wakeup_source(int value){assert(shut && !deep_hold && value==99); wake_cleared=true;}
|
|
void esp_sleep_enable_timer_wakeup(uint64_t value){assert(wake_cleared && !deep_hold && value==5000000); timer_set=true;}
|
|
void esp_deep_sleep_start(){assert(shut && timer_set && deep_hold); slept=true;}
|
|
class ESP32Board { public: virtual void shutdownPeripherals(){}; void enterDeepSleep(uint32_t secs); };
|
|
class FEMBoard:public ESP32Board { void shutdownPeripherals() override {
|
|
assert(!deep_hold); mesh::esp32::holdOutputForDeepSleep(43,1);
|
|
mesh::esp32::holdOutputForDeepSleep(42,1); shut=true;
|
|
} };
|
|
@SLEEP@
|
|
int main(){FEMBoard board; board.enterDeepSleep(5); assert(slept);}
|
|
'''.replace('@SLEEP@',sleep),s3=1)
|
|
header=(ROOT/'src/helpers/ESP32Board.h').read_text()
|
|
light='\n'.join(body(header, signature) for signature in (
|
|
'void sleep(uint32_t secs) override',
|
|
'void sleepMilliseconds(uint32_t millisecs) override',
|
|
'void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)'))
|
|
self.assertNotIn('commitTerminalDeepSleepHolds',light)
|
|
for folder in (ROOT/'src',ROOT/'variants'):
|
|
for path in folder.rglob('*'):
|
|
if path.suffix not in ('.cpp','.h') or path in (HEADER,ROOT/'src/helpers/ESP32Board.cpp'):
|
|
continue
|
|
self.assertNotIn('commitTerminalDeepSleepHolds(',path.read_text(),str(path))
|
|
|
|
def test_actual_boot_restores_radio_output_before_runtime_initialization(self):
|
|
header=(ROOT/'src/helpers/ESP32Board.h').read_text()
|
|
boot=body(header,'void begin()')
|
|
prefix=boot[:boot.index('// Arduino\'s early init hook')]
|
|
prefix=prefix[prefix.index('{')+1:]
|
|
self.compile_run(r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#define HIGH 1
|
|
class Board{public:void begin(){ @BOOT@ }};
|
|
int main(){using namespace mesh::esp32;
|
|
holdOutputForDeepSleep(P_LORA_NSS,1); commitTerminalDeepSleepHolds();
|
|
pads[P_LORA_NSS].latch=0; pads[P_LORA_NSS].output=false;
|
|
Board board; board.begin(); assert(!deep_hold && !pads[P_LORA_NSS].hold && physical(P_LORA_NSS)==1);
|
|
}
|
|
'''.replace('@BOOT@',prefix),s3=1,defines=('P_LORA_NSS=43',))
|
|
|
|
def test_g3_fem_real_startup_and_terminal_lna_disable_both_slots(self):
|
|
variant=ROOT/'variants/station_g3_esp32'
|
|
shutdown=body((variant/'StationG3Board.cpp').read_text(),'void StationG3Board::shutdownPeripherals()')
|
|
code=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#include "LoRaFEMControl.h"
|
|
#include "StationG3RadioSlot.h"
|
|
static bool radio_off=false;
|
|
class ESP32Board{public:void shutdownPeripherals(){radio_off=true;mesh::esp32::holdOutputForDeepSleep(P_LORA_NSS,HIGH);}};
|
|
class StationG3Board:public ESP32Board{public:LoRaFEMControl loRaFEMControl;void shutdownPeripherals();};
|
|
@SHUTDOWN@
|
|
int main(){ using namespace mesh::esp32;
|
|
station_g3::slotState().candidate=@SLOT@;
|
|
const int active_lna=station_g3::activePins().lna;
|
|
const int inactive_lna=station_g3::pinsForSlot(@SLOT@==1 ? 2:1).lna;
|
|
StationG3Board board; resetTerminalDeepSleepHolds();
|
|
releaseOutputAfterDeepSleep(P_LORA_NSS,HIGH); board.loRaFEMControl.init();
|
|
assert(physical(P_PA1_EN)==LOW && physical(active_lna)==HIGH && physical(inactive_lna)==HIGH);
|
|
// The selector enables RX only after a physical radio has been found.
|
|
board.loRaFEMControl.setRxModeEnable();
|
|
assert(physical(active_lna)==LOW && physical(inactive_lna)==HIGH);
|
|
board.loRaFEMControl.setPAGainEnable(true); board.loRaFEMControl.setTxModeEnable();
|
|
assert(physical(P_PA1_EN)==HIGH && physical(active_lna)==HIGH && physical(inactive_lna)==HIGH);
|
|
board.loRaFEMControl.setRxModeEnable(); assert(physical(active_lna)==LOW && physical(inactive_lna)==HIGH);
|
|
board.shutdownPeripherals(); assert(radio_off && !deep_hold);
|
|
assert(pads[active_lna].output && pads[active_lna].hold);
|
|
assert(pads[inactive_lna].output && pads[inactive_lna].hold);
|
|
assert(physical(P_PA1_EN)==LOW && physical(active_lna)==HIGH && physical(inactive_lna)==HIGH);
|
|
commitTerminalDeepSleepHolds(); simulate_deep_sleep();
|
|
assert(physical(active_lna)==HIGH && physical(inactive_lna)==HIGH && physical(P_LORA_NSS)==HIGH);
|
|
// Reset has changed backing latches/modes; startup must configure before release.
|
|
pads[P_PA1_EN].output=false; pads[active_lna].output=false; pads[inactive_lna].output=false;
|
|
resetTerminalDeepSleepHolds(); releaseOutputAfterDeepSleep(P_LORA_NSS,HIGH);
|
|
board.loRaFEMControl.init();
|
|
assert(!pads[P_PA1_EN].hold && !pads[active_lna].hold && !pads[inactive_lna].hold);
|
|
assert(physical(P_PA1_EN)==HIGH && physical(active_lna)==HIGH && physical(inactive_lna)==HIGH && !deep_hold);
|
|
board.loRaFEMControl.setRxModeEnable();
|
|
assert(physical(active_lna)==LOW && physical(inactive_lna)==HIGH);
|
|
}
|
|
'''.replace('@SHUTDOWN@',shutdown)
|
|
files={'LoRaFEMControl.h':(variant/'LoRaFEMControl.h').read_text(),
|
|
'StationG3RadioSlot.h':(variant/'StationG3RadioSlot.h').read_text(),
|
|
'Arduino.h':'#pragma once\n#include <driver/rtc_io.h>\ninline void digitalWrite(int pin,int level){ gpio_set_level(pin,level); }\n'}
|
|
# Both historical build-macro configurations must drive either runtime
|
|
# slot, not merely the one which happened to be compiled in.
|
|
for nss,lna in ((11,10),(43,42)):
|
|
for slot in (1,2):
|
|
self.compile_run(code.replace('@SLOT@',str(slot)),s3=1,
|
|
defines=(f'P_LORA_NSS={nss}',f'P_PRIMARY_LNA_EN={lna}',
|
|
'P_PA1_EN=9','P_PA1_EN_ACTIVE=1','P_PRIMARY_LNA_EN_ACTIVE=0','HIGH=1','LOW=0'),
|
|
extra_source=(variant/'LoRaFEMControl.cpp').read_text(),files=files)
|
|
|
|
def test_terminal_gps_uart_release_respects_owner_and_leaves_i2c_alone(self):
|
|
source=(ROOT/'src/helpers/sensors/EnvironmentSensorManager.cpp').read_text()
|
|
hook=body(source,
|
|
'void EnvironmentSensorManager::shutdownGpsTransportForDeepSleep()')
|
|
stop=body(source,'void EnvironmentSensorManager::stopGpsSerialTransport()')
|
|
code=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#define INPUT 0
|
|
static unsigned released_pads=0;
|
|
struct Port { bool installed=false;unsigned ends=0;explicit operator bool()const{return installed;}
|
|
void end(){assert(installed);installed=false;++ends;} } Serial1;
|
|
void pinMode(int pin,int mode){assert(!Serial1.installed && mode==INPUT);++released_pads;pads[pin].output=false;}
|
|
class EnvironmentSensorManager {public:bool gps_serial_transport=false,gps_serial_transport_blocked=false;
|
|
bool gps_serial_open=false;
|
|
void shutdownGpsTransportForDeepSleep();void stopGpsSerialTransport();};
|
|
#if ENV_INCLUDE_GPS
|
|
@STOP@
|
|
#endif
|
|
@HOOK@
|
|
int main(){ EnvironmentSensorManager sensors;
|
|
// I2C/no-owned GPS: an unrelated serial user is untouched.
|
|
Serial1.installed=true;sensors.shutdownGpsTransportForDeepSleep();assert(Serial1.installed && !Serial1.ends && !released_pads);
|
|
sensors.gps_serial_transport=true;sensors.gps_serial_transport_blocked=true;
|
|
sensors.shutdownGpsTransportForDeepSleep();assert(Serial1.installed && !Serial1.ends && !released_pads);
|
|
sensors.gps_serial_transport_blocked=false;sensors.gps_serial_open=true;
|
|
sensors.shutdownGpsTransportForDeepSleep();
|
|
#if EXPECT_RELEASE
|
|
assert(!Serial1.installed && Serial1.ends==1 && released_pads==EXPECT_PADS);
|
|
sensors.shutdownGpsTransportForDeepSleep();assert(Serial1.ends==1 && released_pads==EXPECT_PADS);
|
|
#else
|
|
assert(Serial1.installed && !Serial1.ends && !released_pads);
|
|
#endif
|
|
}
|
|
'''.replace('@HOOK@',hook).replace('@STOP@',stop)
|
|
for definitions in (
|
|
('ESP32_PLATFORM=1','ENV_INCLUDE_GPS=1','PIN_GPS_TX=18','PIN_GPS_RX=17','EXPECT_RELEASE=1','EXPECT_PADS=2'),
|
|
('ESP32_PLATFORM=1','ENV_INCLUDE_GPS=0','PIN_GPS_TX=18','PIN_GPS_RX=17','EXPECT_RELEASE=0'),
|
|
('NRF52_PLATFORM=1','ENV_INCLUDE_GPS=1','PIN_GPS_TX=18','PIN_GPS_RX=17','EXPECT_RELEASE=1','EXPECT_PADS=2'),
|
|
('ESP32_PLATFORM=1','ENV_INCLUDE_GPS=1','PIN_GPS_TX=-1','PIN_GPS_RX=17','EXPECT_RELEASE=1','EXPECT_PADS=1'),
|
|
('ESP32_PLATFORM=1','ENV_INCLUDE_GPS=1','EXPECT_RELEASE=1','EXPECT_PADS=0'),
|
|
('NRF52_PLATFORM=1','ENV_INCLUDE_GPS=0','PIN_GPS_TX=-1','PIN_GPS_RX=-1','EXPECT_RELEASE=0'),
|
|
):
|
|
self.compile_run(code,defines=definitions)
|
|
|
|
def test_w12_fem_rails_restore_known_output_before_releasing_terminal_hold(self):
|
|
source=(ROOT/'variants/meshnology_w12/MeshnologyW12Board.cpp').read_text()
|
|
begin=body(source,'void MeshnologyW12Board::begin()')
|
|
prefix=begin[:begin.index(' prepareRadioFrequency(')]+'}\n'
|
|
code=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#include <initializer_list>
|
|
#define HIGH 1
|
|
#define LOW 0
|
|
#define OUTPUT 1
|
|
#define P_LORA_LF_PA_POWER 4
|
|
#define P_LORA_HF_PA_POWER 3
|
|
#define PIN_ADC_CTRL 2
|
|
void pinMode(int pin,int mode){assert(mode==OUTPUT);pads[pin].output=true;}
|
|
void digitalWrite(int pin,int level){gpio_set_level(pin,level);}
|
|
class ESP32Board {public:void begin(){mesh::esp32::resetTerminalDeepSleepHolds();}};
|
|
class MeshnologyW12Board:public ESP32Board {public:void begin();};
|
|
@BEGIN@
|
|
int main(){ using namespace mesh::esp32;
|
|
for(int active : {-1,3,4}) {
|
|
// Wake safely from terminal shutdown or either band's packet-wake sleep.
|
|
holdOutputForDeepSleep(4,active==4?HIGH:LOW);
|
|
holdOutputForDeepSleep(3,active==3?HIGH:LOW);
|
|
pads[4].output=false;pads[3].output=false;require_configured_release=true;
|
|
MeshnologyW12Board board;board.begin();
|
|
assert(!pads[3].hold && !pads[4].hold && physical(3)==LOW && physical(4)==LOW);
|
|
assert(!deep_hold && !terminalDigitalHoldPending());
|
|
}
|
|
}
|
|
'''
|
|
self.compile_run(code.replace('@BEGIN@',prefix),s3=1)
|
|
# The prior release-before-pinMode sequence must be caught.
|
|
broken=prefix.replace('releaseOutputAfterDeepSleep((gpio_num_t)P_LORA_LF_PA_POWER, LOW)',
|
|
'releaseRtcGpioIfSupported((gpio_num_t)P_LORA_LF_PA_POWER)')
|
|
self.compile_run(code.replace('@BEGIN@',broken),s3=1,helper_mutation=lambda text:text)
|
|
|
|
def test_w12_packet_wake_retains_only_selected_band_and_terminal_shutdown_disables_both(self):
|
|
source=(ROOT/'variants/meshnology_w12/MeshnologyW12Board.cpp').read_text()
|
|
methods='\n'.join(body(source,signature) for signature in (
|
|
'bool MeshnologyW12Board::prepareRadioFrequency(float frequency)',
|
|
'void MeshnologyW12Board::enterDeepSleep(uint32_t secs, int pin_wake_btn)',
|
|
'void MeshnologyW12Board::powerOff()'))
|
|
code=r'''
|
|
#include <helpers/ESP32GpioHold.h>
|
|
#include <helpers/radiolib/LR2021Band.h>
|
|
#include <vector>
|
|
#define HIGH 1
|
|
#define LOW 0
|
|
#define P_LORA_LF_PA_POWER 4
|
|
#define P_LORA_HF_PA_POWER 3
|
|
#define P_LORA_DIO_1 14
|
|
#define P_LORA_NSS 8
|
|
constexpr int ESP_PD_DOMAIN_RTC_PERIPH=1,ESP_PD_OPTION_ON=1;
|
|
constexpr int RTC_GPIO_MODE_INPUT_ONLY=0,ESP_EXT1_WAKEUP_ANY_HIGH=1;
|
|
static bool slept=false,shutdown=false;
|
|
static unsigned settles=0;
|
|
static uint64_t wake_mask=0,timer=0;
|
|
static std::vector<int> writes;
|
|
void digitalWrite(int pin,int level){
|
|
assert(!pads[pin].hold);pads[pin].output=true;gpio_set_level(pin,level);
|
|
writes.push_back(pin*2+level);
|
|
// Never power both FEMs, even transiently, while changing bands.
|
|
assert(physical(3)!=HIGH || physical(4)!=HIGH);
|
|
}
|
|
void delay(unsigned ms){assert(ms==1);++settles;}
|
|
void esp_sleep_pd_config(int domain,int option){assert(domain==1 && option==1);}
|
|
void rtc_gpio_set_direction(int pin,int mode){assert(pin==14 && mode==0);}
|
|
void rtc_gpio_pulldown_en(int pin){assert(pin==14);}
|
|
void esp_sleep_enable_ext1_wakeup(uint64_t mask,int mode){assert(mode==1);wake_mask=mask;}
|
|
void esp_sleep_enable_timer_wakeup(uint64_t value){timer=value;}
|
|
void sleep_pads(){
|
|
// Unretained outputs may float once the CPU domain powers down.
|
|
for(int pin : {3,4}) if(!pads[pin].hold) pads[pin].output=false;
|
|
}
|
|
void esp_deep_sleep_start(){sleep_pads();slept=true;}
|
|
class ESP32Board {public:void powerOff(){
|
|
assert(pads[3].hold && pads[4].hold && physical(3)==LOW && physical(4)==LOW);
|
|
sleep_pads();shutdown=true;
|
|
}};
|
|
class MeshnologyW12Board:public ESP32Board {
|
|
public:
|
|
bool radio_band_initialized=false,radio_high_band=false;
|
|
bool prepareRadioFrequency(float frequency);
|
|
void enterDeepSleep(uint32_t secs,int pin_wake_btn);
|
|
void powerOff();
|
|
};
|
|
@METHODS@
|
|
int main(){using namespace mesh::esp32;
|
|
for(bool high_band : {false,true}) {
|
|
resetTerminalDeepSleepHolds();
|
|
releaseOutputAfterDeepSleep(3,LOW);releaseOutputAfterDeepSleep(4,LOW);
|
|
MeshnologyW12Board board;
|
|
const float frequency=high_band?2440.0f:909.5f;
|
|
const unsigned before=settles;
|
|
assert(board.prepareRadioFrequency(frequency) && settles==before+1);
|
|
assert(writes[writes.size()-3]==4*2 && writes[writes.size()-2]==3*2);
|
|
assert(physical(3)==high_band && physical(4)==!high_band);
|
|
assert(board.prepareRadioFrequency(frequency) && settles==before+1);
|
|
// A real LF/HF crossing disables the previous rail before enabling the next.
|
|
assert(board.prepareRadioFrequency(high_band?909.5f:2440.0f));
|
|
assert(physical(3)==!high_band && physical(4)==high_band);
|
|
assert(board.prepareRadioFrequency(frequency) && settles==before+3);
|
|
for(int button : {-1,0}) {
|
|
slept=false;timer=0;
|
|
board.enterDeepSleep(button<0?5:0,button);
|
|
assert(slept && pads[3].hold && pads[4].hold);
|
|
assert(physical(3)==high_band && physical(4)==!high_band);
|
|
assert(wake_mask==((1ULL<<14)|(button<0?0:(1ULL<<button))));
|
|
assert(timer==(button<0?5000000:0));
|
|
assert(!deep_hold && !terminalDigitalHoldPending());
|
|
// Deep sleep never returns: model begin() releasing the retained rails,
|
|
// then its normal band selection, before the next runtime operation.
|
|
releaseOutputAfterDeepSleep(3,LOW);releaseOutputAfterDeepSleep(4,LOW);
|
|
board.radio_band_initialized=false;
|
|
assert(board.prepareRadioFrequency(frequency));
|
|
}
|
|
// Explicit runtime shutdown must disable whichever band was powered.
|
|
board.powerOff();assert(shutdown && physical(3)==LOW && physical(4)==LOW);
|
|
}
|
|
}
|
|
'''
|
|
self.compile_run(code.replace('@METHODS@',methods),s3=1)
|
|
# Detect either active-rail loss or a floating inactive rail on sleep.
|
|
for rail in ('P_LORA_LF_PA_POWER','P_LORA_HF_PA_POWER'):
|
|
broken=re.sub(r'(?m)^\s*mesh::esp32::holdOutputForDeepSleep\(\(gpio_num_t\)'+rail+
|
|
r', radio_high_band[^;]*;', '', methods)
|
|
self.assertNotEqual(broken,methods)
|
|
self.compile_run(code.replace('@METHODS@',broken),s3=1,helper_mutation=lambda text:text)
|
|
|
|
def test_all_production_hold_release_deinit_sites_use_capability_wrapper(self):
|
|
# No board or FEM may keep using unsupported RTC calls behind the fix.
|
|
for folder in (ROOT/"src",ROOT/"variants"):
|
|
for path in folder.rglob("*"):
|
|
if path.suffix not in (".h",".cpp") or path==HEADER:
|
|
continue
|
|
self.assertNotRegex(path.read_text(),r"\brtc_gpio_(?:hold_en|hold_dis|deinit)\s*\(",str(path))
|
|
pins=(ROOT/"variants/station_g3_esp32/platformio.ini").read_text()
|
|
self.assertRegex(pins,r"P_LORA_NSS=43\b")
|
|
self.assertRegex(pins,r"P_PRIMARY_LNA_EN=42\b")
|
|
self.assertRegex((ROOT/"variants/lilygo_tbeam_SX1262/platformio.ini").read_text(),r"P_LORA_NSS=18\b")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|