Fix W12 TX prearming without standby PA drain
This commit is contained in:
1 parent
f1d281a766
commit
8874cf62ca
9 files changed
+747
-10
No files matched your search
@@ -65,6 +65,10 @@ jobs:
|
||||
- name: Compile shared-slot Station G3 and legacy alias with buffered KISS
|
||||
# One PlatformIO process: these environments share the checkout build tree.
|
||||
run: pio run -e Station_G3_ESP32_repeater -e Station_G3_ESP32_r2_repeater -e Station_G3_ESP32_kiss_modem
|
||||
- name: Compile W12 low-power FEM TX preparation in repeater and Full Companion
|
||||
run: |
|
||||
pio run -e meshnology_w12_repeater
|
||||
bash build.sh build-firmware meshnology_w12_companion_radio_full --firmware-version v1.17.1.10-halo-keymind-cascade-dev --radio-preset usa-cascade-fixed --profile cascade --require-ota
|
||||
|
||||
esp32-ethernet-companion-smoke:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -228,6 +232,7 @@ jobs:
|
||||
python3 -B test/test_wio_tracker_l2_gps_power.py -v
|
||||
python3 -B test/test_lr1121_power_merge.py -v
|
||||
python3 -B test/test_lr2021_power.py -v
|
||||
python3 -B test/test_lr2021_tx_prearm.py -v
|
||||
python3 -B test/test_meshtracker_x1_battery.py -v
|
||||
python3 -B test/test_drv2605_power.py -v
|
||||
python3 -B test/test_radio_interrupt_recovery.py -v
|
||||
|
||||
@@ -153,3 +153,22 @@ Nano G2 Ultra, ThinkNode M1/M8 and Keepteen LT1 retain their plain `NRF52Board`
|
||||
initializers for the same reason. The MeshTracker X1's LR2021 regulator likewise
|
||||
remains unchanged. Do not turn on a switching regulator without its required
|
||||
inductor/capacitor network. The MCU and LoRa regulator choices are independent.
|
||||
|
||||
## Meshnology W12 transmit preparation
|
||||
|
||||
The W12 adopts the GC1109 TX preparation from
|
||||
[upstream PR #3579](https://github.com/meshcore-dev/MeshCore/pull/3579), but keeps
|
||||
the PA shut down in ordinary standby rather than permanently selecting TX mode.
|
||||
Before sub-GHz packet or Morse CW transmission, it temporarily enables the LF
|
||||
PA in standby, waits 1 ms, starts TX, then restores the normal standby mask.
|
||||
The TX mask stays unchanged during that restoration. Failure paths restore all
|
||||
affected DIOs and request radio recovery if the PA state cannot be confirmed.
|
||||
The 2.4 GHz path and other LR2021 boards do not use this LF prearm policy.
|
||||
|
||||
LF/HF FEM rail changes now settle for 1 ms instead of 100 ms. Packet-wake deep
|
||||
sleep retains only the selected FEM rail; terminal shutdown retains both rails
|
||||
off. This is separate from the W12's existing circuit-verified DC/DC setting.
|
||||
|
||||
Native fault-injection tests cover the mask transitions and power retention;
|
||||
RF envelope and battery-current measurements on a physical W12 are still needed
|
||||
to validate the TX improvement and quantify power use.
|
||||
@@ -70,8 +70,10 @@ inline esp_err_t configureHeldOutput(gpio_num_t pin, uint32_t level) {
|
||||
return gpio_config(&config);
|
||||
}
|
||||
|
||||
// Terminal shutdown only: explicitly select a known output before latching
|
||||
// it. The IDF GPIO API selects the correct RTC/digital hold implementation.
|
||||
// Explicitly select a known output before latching it. RTC pads can also be
|
||||
// retained for packet-wake sleep (e.g. W12 FEM rails). Non-RTC/digital pads
|
||||
// require the terminal-only commit below; never globally freeze live pads.
|
||||
// The IDF GPIO API selects the correct RTC/digital hold implementation.
|
||||
inline esp_err_t holdOutputForDeepSleep(gpio_num_t pin, uint32_t level) {
|
||||
esp_err_t result = configureHeldOutput(pin, level);
|
||||
if (result != ESP_OK) return result;
|
||||
|
||||
@@ -15,6 +15,11 @@
|
||||
#define LR2021_USE_DCDC 0
|
||||
#endif
|
||||
|
||||
// Board-specific FEM warm-up. Do not leave a PA enabled in ordinary standby.
|
||||
#ifndef LR2021_TX_PREARM_MS
|
||||
#define LR2021_TX_PREARM_MS 0
|
||||
#endif
|
||||
|
||||
class CustomLR2021 : public LR2021 {
|
||||
uint32_t _preambleMillis = 66;
|
||||
uint32_t _maxPayloadMillis = 3934;
|
||||
@@ -24,6 +29,70 @@ class CustomLR2021 : public LR2021 {
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS] = {};
|
||||
const Module::RfSwitchMode_t* _rf_switch_table = nullptr;
|
||||
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
bool _tx_standby_armed = false;
|
||||
|
||||
int16_t setTxStandbyMask(bool arm) {
|
||||
if (!_rf_switch_table || (!arm && !_tx_standby_armed)) return RADIOLIB_ERR_NONE;
|
||||
// Set the flag before writing: a failed SPI response may still have changed
|
||||
// a DIO. Cleanup must attempt every affected pin, not stop at the first error.
|
||||
_tx_standby_armed = true;
|
||||
int16_t result = RADIOLIB_ERR_NONE;
|
||||
for (size_t i = 0; i < Module::RFSWITCH_MAX_PINS; ++i) {
|
||||
const uint32_t pin = _rf_switch_pins[i];
|
||||
if (pin == RADIOLIB_NC || !(pin & RFSWITCH_PIN_FLAG)) continue;
|
||||
const uint8_t dio = RADIOLIB_LRXXXX_DIOx_VAL(pin);
|
||||
if (dio >= 7) {
|
||||
if (result == RADIOLIB_ERR_NONE) result = RADIOLIB_ERR_UNKNOWN;
|
||||
continue;
|
||||
}
|
||||
uint8_t config = 0;
|
||||
for (size_t j = 0; j < 8 && _rf_switch_table[j].mode != LR2021::MODE_END_OF_TABLE; ++j) {
|
||||
const uint8_t mode = _rf_switch_table[j].mode;
|
||||
if (mode > 0 && mode <= 8
|
||||
&& _rf_switch_table[j].values[i] == this->mod->hal->GpioLevelHigh)
|
||||
config |= 1UL << (mode - 1);
|
||||
}
|
||||
const uint8_t standby_bit = 1U << (LR2021::MODE_STBY - 1);
|
||||
const uint8_t tx_bit = 1U << (LR2021::MODE_TX - 1);
|
||||
if (!(config & tx_bit) || (config & standby_bit)) continue;
|
||||
const int16_t status = setDioRfSwitchConfig(dio + 5, arm ? config | standby_bit : config);
|
||||
if (result == RADIOLIB_ERR_NONE) result = status;
|
||||
}
|
||||
if (!arm && result == RADIOLIB_ERR_NONE) _tx_standby_armed = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
int16_t prepareTxRfSwitch() {
|
||||
// stageMode(TX) does not enter standby. Stop RX before temporarily enabling
|
||||
// the LF PA there; HF routing and boards without this opt-in are unchanged.
|
||||
int16_t status = standby();
|
||||
if (status == RADIOLIB_ERR_NONE && !mesh::lr2021::isHighBand(freqMHz)) {
|
||||
status = setTxStandbyMask(true);
|
||||
// Reissue the mode after programming its mask: do not rely on a mask
|
||||
// write changing an already-selected mode's outputs immediately. Bypass
|
||||
// our cleanup override here so this one intentional warm-up stays armed.
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR2021::standby();
|
||||
if (status == RADIOLIB_ERR_NONE) this->mod->hal->delay(LR2021_TX_PREARM_MS);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
int16_t finishTxRfSwitch(int16_t status) {
|
||||
// Only change masks while transmitting. SetDioFunction (used by the full
|
||||
// reset-restoration helper) is restricted to Standby RC by the LR2021.
|
||||
// TX bits remain HIGH: disarming standby does not interrupt the packet/CW.
|
||||
const int16_t restored = setTxStandbyMask(false);
|
||||
if (status != RADIOLIB_ERR_NONE || restored != RADIOLIB_ERR_NONE) {
|
||||
const int16_t stopped = standby();
|
||||
// Request the wrapper's hard-reset recovery if the PA state is uncertain.
|
||||
if (restored != RADIOLIB_ERR_NONE || stopped != RADIOLIB_ERR_NONE)
|
||||
return RADIOLIB_ERR_SPI_CMD_TIMEOUT;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
#endif
|
||||
|
||||
int16_t restoreRfSwitchTable() {
|
||||
if (!_rf_switch_table) return RADIOLIB_ERR_NONE;
|
||||
for (size_t i = 0; i < Module::RFSWITCH_MAX_PINS; ++i) {
|
||||
@@ -44,6 +113,9 @@ class CustomLR2021 : public LR2021 {
|
||||
if (status == RADIOLIB_ERR_NONE) status = setDioRfSwitchConfig(dio + 5, config);
|
||||
if (status != RADIOLIB_ERR_NONE) return status;
|
||||
}
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
_tx_standby_armed = false;
|
||||
#endif
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
@@ -58,6 +130,31 @@ class CustomLR2021 : public LR2021 {
|
||||
public:
|
||||
CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; }
|
||||
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
using LR2021::standby;
|
||||
int16_t standby() override {
|
||||
const bool was_armed = _tx_standby_armed;
|
||||
const int16_t status = LR2021::standby();
|
||||
const int16_t restored = setTxStandbyMask(false);
|
||||
if (restored != RADIOLIB_ERR_NONE) return RADIOLIB_ERR_SPI_CMD_TIMEOUT;
|
||||
// Also latch the normal mask after a dirty/prearmed-state cleanup.
|
||||
if (was_armed && (status != RADIOLIB_ERR_NONE || LR2021::standby() != RADIOLIB_ERR_NONE)) {
|
||||
// A mask write alone does not prove the PA turned off. Keep retrying
|
||||
// cleanup (or hard reset) until a normal standby is confirmed.
|
||||
_tx_standby_armed = true;
|
||||
return RADIOLIB_ERR_SPI_CMD_TIMEOUT;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
using LR2021::sleep;
|
||||
int16_t sleep() override {
|
||||
const int16_t status = standby();
|
||||
if (status != RADIOLIB_ERR_NONE) return status;
|
||||
return LR2021::sleep();
|
||||
}
|
||||
#endif
|
||||
|
||||
int16_t setProfileProtocol(uint8_t syncWord) {
|
||||
int16_t status = LR2021::setSyncWord(syncWord);
|
||||
if (status == RADIOLIB_ERR_NONE) status = LR2021::setCRC(2);
|
||||
@@ -174,8 +271,17 @@ class CustomLR2021 : public LR2021 {
|
||||
int16_t transmitMorseCarrier() {
|
||||
// RadioLib's generic direct-TX path always selects MODE_TX. Use the
|
||||
// band-aware routing also used by our normal packet transmit path.
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
const int16_t prepared = prepareTxRfSwitch();
|
||||
if (prepared != RADIOLIB_ERR_NONE) return finishTxRfSwitch(prepared);
|
||||
#endif
|
||||
this->mod->setRfSwitchState(rfSwitchTxMode());
|
||||
return this->setTxTestMode(RADIOLIB_LR2021_TX_TEST_MODE_CW);
|
||||
const int16_t status = this->setTxTestMode(RADIOLIB_LR2021_TX_TEST_MODE_CW);
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
return finishTxRfSwitch(status);
|
||||
#else
|
||||
return status;
|
||||
#endif
|
||||
}
|
||||
|
||||
int16_t setRxBoostedGainMode(uint8_t level) {
|
||||
@@ -191,9 +297,23 @@ class CustomLR2021 : public LR2021 {
|
||||
const bool transmitting = this->stagedMode == RADIOLIB_RADIO_MODE_TX;
|
||||
int16_t state;
|
||||
if (this->stagedMode == RADIOLIB_RADIO_MODE_RX) {
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
state = setTxStandbyMask(false);
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
this->stagedMode = RADIOLIB_RADIO_MODE_NONE;
|
||||
return RADIOLIB_ERR_SPI_CMD_TIMEOUT;
|
||||
}
|
||||
#endif
|
||||
this->mod->setRfSwitchState(rfSwitchRxMode());
|
||||
state = this->setRx(this->rxTimeout);
|
||||
} else if (transmitting) {
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
state = prepareTxRfSwitch();
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
this->stagedMode = RADIOLIB_RADIO_MODE_NONE;
|
||||
return finishTxRfSwitch(state);
|
||||
}
|
||||
#endif
|
||||
this->mod->setRfSwitchState(rfSwitchTxMode());
|
||||
state = this->setTx(RADIOLIB_LR2021_TX_TIMEOUT_NONE);
|
||||
} else {
|
||||
@@ -202,6 +322,9 @@ class CustomLR2021 : public LR2021 {
|
||||
|
||||
if (state != RADIOLIB_ERR_NONE) {
|
||||
this->stagedMode = RADIOLIB_RADIO_MODE_NONE;
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
if (transmitting) return finishTxRfSwitch(state);
|
||||
#endif
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -211,12 +334,19 @@ class CustomLR2021 : public LR2021 {
|
||||
this->mod->hal->yield();
|
||||
if (this->mod->hal->millis() - started >= LR2021_TX_BUSY_TIMEOUT_MS) {
|
||||
this->stagedMode = RADIOLIB_RADIO_MODE_NONE;
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
return finishTxRfSwitch(RADIOLIB_ERR_SPI_CMD_TIMEOUT);
|
||||
#else
|
||||
return RADIOLIB_ERR_SPI_CMD_TIMEOUT;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->stagedMode = RADIOLIB_RADIO_MODE_NONE;
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
if (transmitting) return finishTxRfSwitch(RADIOLIB_ERR_NONE);
|
||||
#endif
|
||||
return RADIOLIB_ERR_NONE;
|
||||
}
|
||||
|
||||
|
||||
@@ -398,6 +398,7 @@ int main(){ EnvironmentSensorManager sensors;
|
||||
prefix=begin[:begin.index(' prepareRadioFrequency(')]+'}\n'
|
||||
code=r'''
|
||||
#include <helpers/ESP32GpioHold.h>
|
||||
#include <initializer_list>
|
||||
#define HIGH 1
|
||||
#define LOW 0
|
||||
#define OUTPUT 1
|
||||
@@ -410,11 +411,15 @@ class ESP32Board {public:void begin(){mesh::esp32::resetTerminalDeepSleepHolds()
|
||||
class MeshnologyW12Board:public ESP32Board {public:void begin();};
|
||||
@BEGIN@
|
||||
int main(){ using namespace mesh::esp32;
|
||||
holdOutputForDeepSleep(4,LOW);holdOutputForDeepSleep(3,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());
|
||||
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)
|
||||
@@ -423,6 +428,99 @@ int main(){ using namespace mesh::esp32;
|
||||
'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"):
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
"""Exercise the W12 TX warm-up using real driver methods and board routing.
|
||||
|
||||
The native radio mock models the distinction between host RF-switch GPIOs and
|
||||
LR2021 DIO masks. No PlatformIO or RF hardware is needed. Failed SPI responses
|
||||
may still apply a mask, so cleanup must not assume an error left a pin unchanged.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def method(source, signature):
|
||||
start = source.index(signature)
|
||||
end = source.index("{", start) + 1
|
||||
depth = 1
|
||||
while depth:
|
||||
depth += (source[end] == "{") - (source[end] == "}")
|
||||
end += 1
|
||||
return source[start:end].replace(" override", "")
|
||||
|
||||
|
||||
HARNESS = r'''
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <helpers/radiolib/LR2021Band.h>
|
||||
@POLICY@
|
||||
#define LR2021_TX_BUSY_TIMEOUT_MS 4UL
|
||||
#define LR2021_USE_DCDC 0
|
||||
#define RADIOLIB_ERR_NONE 0
|
||||
#define RADIOLIB_ERR_UNKNOWN -16
|
||||
#define RADIOLIB_ERR_UNSUPPORTED -99
|
||||
#define RADIOLIB_ERR_SPI_CMD_TIMEOUT -705
|
||||
#define RADIOLIB_NC 0xffffffffU
|
||||
#define RFSWITCH_PIN_FLAG (1UL << 31)
|
||||
#define RADIOLIB_LRXXXX_DIOx_VAL(pin) ((pin) & ~RFSWITCH_PIN_FLAG)
|
||||
#define RADIOLIB_LR2021_DIO5 (RFSWITCH_PIN_FLAG | 0)
|
||||
#define RADIOLIB_LR2021_DIO6 (RFSWITCH_PIN_FLAG | 1)
|
||||
#define RADIOLIB_LR2021_DIO9 (RFSWITCH_PIN_FLAG | 4)
|
||||
#define RADIOLIB_LR2021_DIO10 (RFSWITCH_PIN_FLAG | 5)
|
||||
#define RADIOLIB_LR2021_DIO11 (RFSWITCH_PIN_FLAG | 6)
|
||||
#define RADIOLIB_LR2021_DIO_SLEEP_PULL_UP 2
|
||||
#define RADIOLIB_LR2021_DIO_SLEEP_PULL_AUTO 3
|
||||
#define RADIOLIB_LR2021_DIO_FUNCTION_RF_SWITCH 0x20
|
||||
#define RADIOLIB_LR2021_TX_TIMEOUT_NONE 0
|
||||
#define RADIOLIB_LR2021_TX_TEST_MODE_CW 0
|
||||
#define RADIOLIB_LR2021_LORA_SYNC_WORD_PRIVATE 0x12
|
||||
#define RADIOLIB_LR2021_RX_TIMEOUT_INF 0xffffff
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_FLAGS 0
|
||||
#define RADIOLIB_IRQ_RX_DEFAULT_MASK 0
|
||||
#define RADIOLIB_IRQ_PREAMBLE_DETECTED 5
|
||||
#define RADIOLIB_RADIO_MODE_NONE 0
|
||||
#define RADIOLIB_RADIO_MODE_RX 1
|
||||
#define RADIOLIB_RADIO_MODE_TX 2
|
||||
#define LOW 0
|
||||
#define HIGH 1
|
||||
#define END_OF_MODE_TABLE {0, {0, 0, 0, 0, 0}}
|
||||
#define LORA_FREQ 909.5f
|
||||
#define LORA_BW 500.0f
|
||||
#define LORA_SF 5
|
||||
#define LORA_CR 5
|
||||
#define LORA_TX_POWER 2
|
||||
using RadioLibTime_t = uint32_t;
|
||||
struct SPIClass {};
|
||||
struct Logger { void print(const char*) {} void println(int) {} };
|
||||
namespace mesh { Logger& usbLoggingPort() { static Logger logger;return logger; } }
|
||||
struct Hal {
|
||||
uint32_t GpioLevelHigh=HIGH;
|
||||
uint32_t now=0;
|
||||
bool busy=false;
|
||||
int delays=0;
|
||||
uint32_t millis() { return now; }
|
||||
int digitalRead(uint32_t) { return busy; }
|
||||
void yield() { ++now; }
|
||||
void delay(uint32_t ms) { assert(ms==LR2021_TX_PREARM_MS);now+=ms;++delays; }
|
||||
};
|
||||
struct Module {
|
||||
static constexpr size_t RFSWITCH_MAX_PINS=5;
|
||||
// These are RadioLib's actual Module::OpMode_t values, not synthetic modes.
|
||||
enum OpMode_t { MODE_END_OF_TABLE=0, MODE_IDLE, MODE_RX, MODE_TX };
|
||||
struct RfSwitchMode_t { uint8_t mode;uint32_t values[RFSWITCH_MAX_PINS]; };
|
||||
Hal storage;Hal* hal=&storage;
|
||||
uint8_t host_switch=0;
|
||||
uint32_t getGpio() { return 13; }
|
||||
// DIO-controlled RF switches are NOT host GPIOs. The chip mode selects them.
|
||||
void setRfSwitchState(uint8_t mode) { host_switch=mode; }
|
||||
};
|
||||
class LR2021 {
|
||||
public:
|
||||
// Exact RadioLib LR2021::OpMode_t declaration (aliases plus HF extensions).
|
||||
enum OpMode_t {
|
||||
MODE_END_OF_TABLE=Module::MODE_END_OF_TABLE,
|
||||
MODE_STBY=Module::MODE_IDLE,
|
||||
MODE_RX=Module::MODE_RX,
|
||||
MODE_TX=Module::MODE_TX,
|
||||
MODE_RX_HF,
|
||||
MODE_TX_HF,
|
||||
};
|
||||
Module* mod;
|
||||
float freqMHz=909.5f;
|
||||
uint8_t stagedMode=RADIOLIB_RADIO_MODE_NONE,chip_mode=MODE_STBY;
|
||||
uint32_t rxTimeout=0xffffff;
|
||||
uint8_t masks[7]={};
|
||||
bool levels[7]={},latch_masks_on_mode=false;
|
||||
int fail_dio=0,fail_count=0,fail_standby_at=0;
|
||||
bool fail_arm=false,fail_disarm=false,fail_tx=false,fail_standby=false;
|
||||
bool busy_after_tx=false,failed_standby_not_applied=false;
|
||||
int tx_calls=0,rx_calls=0,sleep_calls=0,standby_calls=0,function_calls=0;
|
||||
int previous_tx_delays=0;
|
||||
std::vector<int> writes,clear_attempts;
|
||||
explicit LR2021(Module* module):mod(module) {}
|
||||
virtual ~LR2021()=default;
|
||||
virtual int16_t launchMode() { return -99; }
|
||||
virtual int16_t startReceive() { return -99; }
|
||||
bool level(uint8_t dio) const { return levels[dio-5]; }
|
||||
void applyMode(uint8_t mode) {
|
||||
chip_mode=mode;
|
||||
for(size_t i=0;i<7;++i) levels[i]=(masks[i] & (1U << (mode-1)))!=0;
|
||||
}
|
||||
bool offInStandby() const {
|
||||
for(uint8_t dio:{9,10,11}) if(masks[dio-5]&1) return false;
|
||||
return true;
|
||||
}
|
||||
virtual int16_t standby() {
|
||||
++standby_calls;
|
||||
const bool failed=fail_standby || standby_calls==fail_standby_at;
|
||||
if(!failed || !failed_standby_not_applied) applyMode(MODE_STBY);
|
||||
return failed ? -71 : 0;
|
||||
}
|
||||
virtual int16_t sleep() { ++sleep_calls;applyMode(MODE_STBY);return 0; }
|
||||
int16_t setTx(uint32_t timeout) {
|
||||
assert(timeout==0);++tx_calls;
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
assert(chip_mode==MODE_STBY);
|
||||
if(!mesh::lr2021::isHighBand(freqMHz)) {
|
||||
assert(level(9) && level(10) && level(11));
|
||||
assert(mod->hal->delays==previous_tx_delays+1);
|
||||
assert(mod->hal->now>=LR2021_TX_PREARM_MS);
|
||||
}
|
||||
#endif
|
||||
previous_tx_delays=mod->hal->delays;
|
||||
// A failed result does not prove that the TX command was not applied.
|
||||
applyMode(mesh::lr2021::isHighBand(freqMHz) ? MODE_TX_HF : MODE_TX);
|
||||
mod->hal->busy=busy_after_tx;
|
||||
return fail_tx ? -72 : 0;
|
||||
}
|
||||
int16_t setTxTestMode(uint8_t mode) { assert(mode==0);return setTx(0); }
|
||||
int16_t setRx(uint32_t timeout) {
|
||||
assert(timeout==rxTimeout);++rx_calls;
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
assert(offInStandby());
|
||||
#endif
|
||||
applyMode(mesh::lr2021::isHighBand(freqMHz) ? MODE_RX_HF : MODE_RX);
|
||||
return 0;
|
||||
}
|
||||
int16_t setDioRfSwitchConfig(uint8_t dio,uint8_t mask) {
|
||||
assert(dio>=5 && dio<=11);writes.push_back(dio);
|
||||
const bool arming=(mask&1)!=0;
|
||||
if(!arming && (dio>=9)) clear_attempts.push_back(dio);
|
||||
masks[dio-5]=mask;
|
||||
if(!latch_masks_on_mode) levels[dio-5]=(mask & (1U << (chip_mode-1)))!=0;
|
||||
// Restoring STBY bits must never momentarily deassert an active TX FEM.
|
||||
if(chip_mode==MODE_TX && dio>=9) assert(level(dio));
|
||||
const bool match=(arming && fail_arm) || (!arming && fail_disarm);
|
||||
if(dio==fail_dio && match && fail_count) {
|
||||
if(fail_count>0) --fail_count;
|
||||
return -73;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int16_t setDioFunction(uint8_t dio,uint8_t function,uint8_t pull) {
|
||||
assert(chip_mode==MODE_STBY); // Semtech permits this in Standby RC only.
|
||||
assert(dio>=5 && dio<=11 && function==0x20 && (pull==2 || pull==3));
|
||||
++function_calls;return 0;
|
||||
}
|
||||
void setRfSwitchTable(const uint32_t (&pins)[5],const Module::RfSwitchMode_t* table) {
|
||||
for(size_t i=0;i<5;++i) {
|
||||
uint8_t mask=0;
|
||||
for(size_t j=0;table[j].mode;++j)
|
||||
if(table[j].values[i]==HIGH) mask|=1U<<(table[j].mode-1);
|
||||
masks[RADIOLIB_LRXXXX_DIOx_VAL(pins[i])]=mask;
|
||||
}
|
||||
applyMode(chip_mode);
|
||||
}
|
||||
int16_t begin(float,float,uint8_t,uint8_t,uint8_t,int8_t,uint16_t,float) {
|
||||
std::fill(masks,masks+7,0);applyMode(MODE_STBY);return 0;
|
||||
}
|
||||
int16_t setCRC(uint8_t) { return 0; }
|
||||
int16_t explicitHeader() { return 0; }
|
||||
int16_t clearRxFifo() { return 0; }
|
||||
int16_t startReceive(uint32_t timeout,uint32_t,uint32_t,int) {
|
||||
rxTimeout=timeout;stagedMode=RADIOLIB_RADIO_MODE_RX;return launchMode();
|
||||
}
|
||||
};
|
||||
class CustomLR2021:public LR2021 {
|
||||
public:
|
||||
using LR2021::LR2021;
|
||||
uint32_t _rf_switch_pins[Module::RFSWITCH_MAX_PINS]={};
|
||||
const Module::RfSwitchMode_t* _rf_switch_table=nullptr;
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
bool _tx_standby_armed=false;
|
||||
@MASK@
|
||||
@PREPARE@
|
||||
@FINISH@
|
||||
@STANDBY@
|
||||
@SLEEP@
|
||||
#endif
|
||||
@RESTORE@
|
||||
@TABLE@
|
||||
@INIT@
|
||||
@RX_MODE@
|
||||
@TX_MODE@
|
||||
@LAUNCH@
|
||||
@MORSE@
|
||||
@RECEIVE@
|
||||
};
|
||||
@BOARD_PINS@
|
||||
@BOARD_TABLE@
|
||||
static void configure(CustomLR2021& radio) {
|
||||
radio.setRfSwitchTable(rfswitch_dios,rfswitch_table);
|
||||
assert(radio.offInStandby());
|
||||
radio.writes.clear();radio.clear_attempts.clear();
|
||||
}
|
||||
static void noBias(CustomLR2021& radio) {
|
||||
assert(radio.offInStandby());
|
||||
if(radio.chip_mode==LR2021::MODE_STBY)
|
||||
assert(!radio.level(9) && !radio.level(10) && !radio.level(11));
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
assert(!radio._tx_standby_armed);
|
||||
#endif
|
||||
}
|
||||
static int packet(CustomLR2021& radio) {
|
||||
radio.stagedMode=RADIOLIB_RADIO_MODE_TX;
|
||||
const int result=radio.launchMode();
|
||||
assert(radio.stagedMode==RADIOLIB_RADIO_MODE_NONE);
|
||||
return result;
|
||||
}
|
||||
int main(int argc,char** argv) {
|
||||
assert(argc==2);std::string test=argv[1];
|
||||
Module module;CustomLR2021 radio(&module);configure(radio);
|
||||
if(test.rfind("latched_",0)==0) {
|
||||
radio.latch_masks_on_mode=true;test=test.substr(8);
|
||||
}
|
||||
if(test=="packet" || test=="cw") {
|
||||
radio.chip_mode=LR2021::MODE_RX;
|
||||
assert((test=="packet" ? packet(radio) : radio.transmitMorseCarrier())==0);
|
||||
assert(radio.tx_calls==1 && radio.chip_mode==LR2021::MODE_TX);
|
||||
noBias(radio);
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
assert(radio.writes==std::vector<int>({9,10,11,9,10,11}));
|
||||
assert(module.hal->delays==1 && radio.standby_calls==2);
|
||||
#else
|
||||
assert(radio.writes.empty() && module.hal->delays==0 && radio.standby_calls==0);
|
||||
#endif
|
||||
assert(radio.function_calls==0);
|
||||
assert(radio.standby()==0);noBias(radio);
|
||||
assert(radio.startReceive()==0 && radio.chip_mode==LR2021::MODE_RX);
|
||||
assert(!radio.level(9) && radio.level(11));
|
||||
} else if(test=="hf") {
|
||||
radio.freqMHz=2440;
|
||||
assert(packet(radio)==0 && radio.chip_mode==LR2021::MODE_TX_HF);
|
||||
assert(radio.writes.empty() && module.hal->delays==0);noBias(radio);
|
||||
assert(radio.mod->host_switch==LR2021::MODE_TX_HF);
|
||||
assert(radio.level(5) && !radio.level(6) && !radio.level(9) && !radio.level(11));
|
||||
radio.stagedMode=RADIOLIB_RADIO_MODE_RX;
|
||||
assert(radio.launchMode()==0 && radio.mod->host_switch==LR2021::MODE_RX_HF);
|
||||
assert(!radio.level(5) && radio.level(6) && !radio.level(11));
|
||||
}
|
||||
#if LR2021_TX_PREARM_MS > 0
|
||||
else if(test=="tx_fail" || test=="cw_fail" || test=="busy") {
|
||||
radio.fail_tx=(test!="busy");radio.busy_after_tx=(test=="busy");
|
||||
assert((test=="cw_fail" ? radio.transmitMorseCarrier() : packet(radio))==
|
||||
(test=="busy" ? RADIOLIB_ERR_SPI_CMD_TIMEOUT : -72));
|
||||
assert(radio.tx_calls==1 && radio.chip_mode==LR2021::MODE_STBY);
|
||||
assert(radio.clear_attempts==std::vector<int>({9,10,11}));noBias(radio);
|
||||
} else if(test=="standby_fail" || test=="second_standby_fail") {
|
||||
if(test=="standby_fail") radio.fail_standby=true;
|
||||
else radio.fail_standby_at=2;
|
||||
assert(packet(radio)==(radio.fail_standby ? RADIOLIB_ERR_SPI_CMD_TIMEOUT : -71));
|
||||
assert(radio.tx_calls==0 && module.hal->delays==0);noBias(radio);
|
||||
if(radio.fail_standby) assert(radio.writes.empty());
|
||||
else assert(radio.clear_attempts==std::vector<int>({9,10,11}));
|
||||
} else if(test.rfind("arm_fail",0)==0 || test.rfind("disarm_fail",0)==0) {
|
||||
const bool arm=test.rfind("arm_fail",0)==0;
|
||||
radio.fail_dio=std::stoi(test.substr(arm?8:11));
|
||||
radio.fail_arm=arm;radio.fail_disarm=!arm;radio.fail_count=1;
|
||||
assert(packet(radio)==(arm ? -73 : RADIOLIB_ERR_SPI_CMD_TIMEOUT));
|
||||
assert(radio.tx_calls==(arm?0:1));noBias(radio);
|
||||
assert(radio.writes.size()>=(arm?6U:9U));
|
||||
// A failed response still requires every later pin to be restored.
|
||||
assert(radio.clear_attempts[0]==9 && radio.clear_attempts[1]==10 && radio.clear_attempts[2]==11);
|
||||
assert(radio.chip_mode==LR2021::MODE_STBY);
|
||||
} else if(test=="persistent_cleanup") {
|
||||
radio.fail_dio=10;radio.fail_disarm=true;radio.fail_count=-1;
|
||||
assert(packet(radio)==RADIOLIB_ERR_SPI_CMD_TIMEOUT);
|
||||
assert(radio._tx_standby_armed && radio.clear_attempts.size()==6);
|
||||
assert(radio.clear_attempts==std::vector<int>({9,10,11,9,10,11}));
|
||||
radio.stagedMode=RADIOLIB_RADIO_MODE_RX;
|
||||
assert(radio.launchMode()==RADIOLIB_ERR_SPI_CMD_TIMEOUT && radio.rx_calls==0);
|
||||
assert(radio.stagedMode==RADIOLIB_RADIO_MODE_NONE && radio._tx_standby_armed);
|
||||
assert(radio.sleep()==RADIOLIB_ERR_SPI_CMD_TIMEOUT && radio.sleep_calls==0);
|
||||
radio.fail_count=0;assert(radio.sleep()==0 && radio.sleep_calls==1);noBias(radio);
|
||||
} else if(test=="standby" || test=="sleep" || test=="receive" || test=="reset") {
|
||||
assert(radio.prepareTxRfSwitch()==0 && radio._tx_standby_armed);
|
||||
assert(!radio.offInStandby());
|
||||
if(test=="standby") assert(radio.standby()==0);
|
||||
if(test=="sleep") assert(radio.sleep()==0 && radio.sleep_calls==1);
|
||||
if(test=="receive") {
|
||||
assert(radio.startReceive()==0 && radio.rx_calls==1);
|
||||
assert(radio.chip_mode==LR2021::MODE_RX && !radio.level(9));
|
||||
}
|
||||
if(test=="reset") assert(radio.std_init(nullptr));
|
||||
noBias(radio);
|
||||
assert(radio.clear_attempts.size()>=3);
|
||||
} else if(test=="dirty_first_standby_fail" || test=="dirty_final_standby_fail") {
|
||||
// A mask can be written successfully while the current DIO output is
|
||||
// unchanged until a mode command. A failed mode command may not execute.
|
||||
radio.latch_masks_on_mode=true;
|
||||
assert(radio.prepareTxRfSwitch()==0 && radio._tx_standby_armed);
|
||||
assert(radio.standby_calls==2 && radio.level(9) && radio.level(10) && radio.level(11));
|
||||
radio.failed_standby_not_applied=true;
|
||||
radio.fail_standby_at=(test=="dirty_first_standby_fail" ? 3 : 4);
|
||||
assert(radio.standby()==RADIOLIB_ERR_SPI_CMD_TIMEOUT);
|
||||
assert(radio._tx_standby_armed && radio.offInStandby());
|
||||
assert(radio.level(9) && radio.level(10) && radio.level(11));
|
||||
assert(radio.clear_attempts==std::vector<int>({9,10,11}));
|
||||
assert(radio.tx_calls==0 && radio.rx_calls==0 && radio.sleep_calls==0);
|
||||
radio.fail_standby_at=0;
|
||||
assert(radio.standby()==0);noBias(radio);
|
||||
assert(radio.clear_attempts==std::vector<int>({9,10,11,9,10,11}));
|
||||
} else if(test=="repeat") {
|
||||
for(float frequency:{909.5f,2440.0f,909.5f,2440.0f,909.5f}) {
|
||||
radio.freqMHz=frequency;
|
||||
for(bool cw:{false,true}) {
|
||||
assert((cw ? radio.transmitMorseCarrier() : packet(radio))==0);
|
||||
noBias(radio);
|
||||
assert(radio.standby()==0);noBias(radio);
|
||||
assert(radio.startReceive()==0);noBias(radio);
|
||||
assert(radio.chip_mode==(frequency>1500 ? LR2021::MODE_RX_HF : LR2021::MODE_RX));
|
||||
}
|
||||
}
|
||||
assert(radio.tx_calls==10 && module.hal->delays==6);
|
||||
assert(radio.function_calls==0);
|
||||
}
|
||||
#endif
|
||||
else assert(false);
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
class LR2021TxPrearmTest(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
compiler = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
||||
if not compiler:
|
||||
raise AssertionError("a host C++17 compiler is required")
|
||||
cls.temp = tempfile.TemporaryDirectory(prefix="meshcore-lr2021-prearm-")
|
||||
cls.addClassCleanup(cls.temp.cleanup)
|
||||
cls.binaries = {}
|
||||
driver = (ROOT / "src/helpers/radiolib/CustomLR2021.h").read_text()
|
||||
board = (ROOT / "variants/meshnology_w12/target.cpp").read_text()
|
||||
source = HARNESS
|
||||
for marker, signature in {
|
||||
"@MASK@": "int16_t setTxStandbyMask(",
|
||||
"@PREPARE@": "int16_t prepareTxRfSwitch(",
|
||||
"@FINISH@": "int16_t finishTxRfSwitch(",
|
||||
"@STANDBY@": "int16_t standby()",
|
||||
"@SLEEP@": "int16_t sleep()",
|
||||
"@RESTORE@": "int16_t restoreRfSwitchTable(",
|
||||
"@TABLE@": "void setRfSwitchTable(",
|
||||
"@INIT@": "bool std_init(",
|
||||
"@RX_MODE@": "uint8_t rfSwitchRxMode(",
|
||||
"@TX_MODE@": "uint8_t rfSwitchTxMode(",
|
||||
"@LAUNCH@": "int16_t launchMode(",
|
||||
"@MORSE@": "int16_t transmitMorseCarrier(",
|
||||
"@RECEIVE@": "int16_t startReceive()",
|
||||
}.items():
|
||||
source = source.replace(marker, method(driver, signature))
|
||||
source = source.replace("@POLICY@", re.search(
|
||||
r"#ifndef LR2021_TX_PREARM_MS\b.*?#endif", driver, re.S).group(0))
|
||||
for marker, pattern in {
|
||||
"@BOARD_PINS@": r"const uint32_t rfswitch_dios\[\].*?\n};",
|
||||
"@BOARD_TABLE@": r"static const Module::RfSwitchMode_t rfswitch_table\[\].*?\n};",
|
||||
}.items():
|
||||
source = source.replace(marker, re.search(pattern, board, re.S).group(0))
|
||||
for name, flags in {"w12": ["-DLR2021_TX_PREARM_MS=1"], "default": [],
|
||||
"disabled": ["-DLR2021_TX_PREARM_MS=0"]}.items():
|
||||
cpp = Path(cls.temp.name) / (name + ".cpp")
|
||||
binary = Path(cls.temp.name) / (name + (".exe" if os.name == "nt" else ""))
|
||||
cpp.write_text(source, encoding="utf-8")
|
||||
sanitizers = [] if os.name == "nt" else ["-fsanitize=address,undefined",
|
||||
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
||||
built = subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
|
||||
"-Wno-unused-parameter", *sanitizers, *flags, "-I", str(ROOT / "src"),
|
||||
str(cpp), "-o", str(binary)], capture_output=True, text=True, timeout=40)
|
||||
if built.returncode:
|
||||
raise AssertionError(built.stdout + built.stderr)
|
||||
cls.binaries[name] = binary
|
||||
|
||||
def run_case(self, case, build="w12"):
|
||||
ran = subprocess.run([str(self.binaries[build]), case], capture_output=True,
|
||||
text=True, timeout=10)
|
||||
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
|
||||
|
||||
def test_packet_settles_pa_without_retaining_standby_bias(self):
|
||||
self.run_case("packet")
|
||||
|
||||
def test_morse_cw_uses_same_preparation_and_cleanup(self):
|
||||
self.run_case("cw")
|
||||
|
||||
def test_high_band_does_not_arm_low_frequency_pa(self):
|
||||
self.run_case("hf")
|
||||
|
||||
def test_failed_packet_cw_and_busy_timeout_abort_and_disarm(self):
|
||||
for case in ["tx_fail", "cw_fail", "busy", "standby_fail", "second_standby_fail"]:
|
||||
with self.subTest(case=case):
|
||||
self.run_case(case)
|
||||
|
||||
def test_every_arm_and_cleanup_pin_failure_still_cleans_later_pins(self):
|
||||
for phase in ["arm_fail", "disarm_fail"]:
|
||||
for dio in [9, 10, 11]:
|
||||
with self.subTest(phase=phase, dio=dio):
|
||||
self.run_case(phase + str(dio))
|
||||
|
||||
def test_persistent_cleanup_error_blocks_receive_and_sleep_until_recovery(self):
|
||||
self.run_case("persistent_cleanup")
|
||||
|
||||
def test_standby_sleep_receive_and_actual_reset_restore_normal_table(self):
|
||||
for case in ["standby", "sleep", "receive", "reset"]:
|
||||
with self.subTest(case=case):
|
||||
self.run_case(case)
|
||||
|
||||
def test_default_and_explicit_optout_keep_existing_tx_behavior(self):
|
||||
for build in ["default", "disabled"]:
|
||||
for case in ["packet", "cw", "hf"]:
|
||||
with self.subTest(build=build, case=case):
|
||||
self.run_case(case, build)
|
||||
|
||||
def test_prearm_and_cleanup_with_mode_latched_dio_outputs(self):
|
||||
for case in ["packet", "cw", "tx_fail", "cw_fail", "busy", "second_standby_fail",
|
||||
"persistent_cleanup", "standby", "sleep", "receive", "reset"]:
|
||||
with self.subTest(case=case):
|
||||
self.run_case("latched_" + case)
|
||||
|
||||
def test_repeated_packet_and_cw_across_lf_hf_switches_on_same_instance(self):
|
||||
for case in ["repeat", "latched_repeat"]:
|
||||
with self.subTest(case=case):
|
||||
self.run_case(case)
|
||||
|
||||
def test_dirty_cleanup_keeps_retry_flag_until_physical_standby_is_confirmed(self):
|
||||
for case in ["dirty_first_standby_fail", "dirty_final_standby_fail"]:
|
||||
with self.subTest(case=case):
|
||||
self.run_case(case)
|
||||
|
||||
def test_only_w12_enables_prearm_and_normal_standby_remains_off(self):
|
||||
opted_in = []
|
||||
for recipe in sorted((ROOT / "variants").glob("*/platformio.ini")):
|
||||
if re.search(r"(?m)^\s*-D\s+LR2021_TX_PREARM_MS=[1-9]\d*\b", recipe.read_text()):
|
||||
opted_in.append(recipe.parent.name)
|
||||
self.assertEqual(opted_in, ["meshnology_w12"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -47,7 +47,9 @@ void MeshnologyW12Board::begin() {
|
||||
digitalWrite(P_LORA_LF_PA_POWER, LOW);
|
||||
digitalWrite(P_LORA_HF_PA_POWER, LOW);
|
||||
digitalWrite(high_band ? P_LORA_HF_PA_POWER : P_LORA_LF_PA_POWER, HIGH);
|
||||
delay(100);
|
||||
// The FEM rail needs only 1 ms to settle. TX control-line prearming is
|
||||
// handled separately immediately before transmit, not by idling the PA on.
|
||||
delay(1);
|
||||
|
||||
radio_high_band = high_band;
|
||||
radio_band_initialized = true;
|
||||
@@ -63,6 +65,12 @@ void MeshnologyW12Board::begin() {
|
||||
|
||||
mesh::esp32::holdRtcGpioIfSupported((gpio_num_t)P_LORA_NSS);
|
||||
|
||||
// Packet-wake sleep leaves the radio receiving. Retain the selected FEM
|
||||
// rail and the inactive rail's LOW level; both PA enable pins are RTC GPIOs
|
||||
// on this ESP32-S3. Terminal powerOff() deliberately retains both LOW.
|
||||
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_LF_PA_POWER, radio_high_band ? LOW : HIGH);
|
||||
mesh::esp32::holdOutputForDeepSleep((gpio_num_t)P_LORA_HF_PA_POWER, radio_high_band ? HIGH : LOW);
|
||||
|
||||
if (pin_wake_btn < 0) {
|
||||
esp_sleep_enable_ext1_wakeup( (1L << P_LORA_DIO_1), ESP_EXT1_WAKEUP_ANY_HIGH); // wake up on: recv LoRa packet
|
||||
} else {
|
||||
|
||||
@@ -11,6 +11,7 @@ build_flags =
|
||||
-D WRAPPER_CLASS=CustomLR2021Wrapper
|
||||
-D USE_LR2021
|
||||
-D LR2021_USE_DCDC=1 ; verified W12 schematic: L5 2.2uH across LXA/LXB
|
||||
-D LR2021_TX_PREARM_MS=1 ; settle GC1109 before LF TX, without an always-on standby PA
|
||||
-D NEOPIXEL_LED=46
|
||||
-D NEOPIXEL_BRIGHTNESS=64
|
||||
-D P_LORA_DIO_1=14
|
||||
|
||||
@@ -39,7 +39,7 @@ const uint32_t rfswitch_dios[] = {
|
||||
static const Module::RfSwitchMode_t rfswitch_table[] = {
|
||||
// DIO5 DIO6 DIO9 DIO10 DIO11
|
||||
{ LR2021::MODE_STBY, { LOW, LOW, LOW, LOW, LOW } }, // everything off
|
||||
{ LR2021::MODE_RX, { LOW, LOW, LOW, LOW, HIGH } }, // rx_lf: GC1109 LNA
|
||||
{ LR2021::MODE_RX, { LOW, LOW, LOW, HIGH, HIGH } }, // rx_lf: GC1109 LNA (CPS is don't-care in RX)
|
||||
{ LR2021::MODE_TX, { LOW, LOW, HIGH, HIGH, HIGH } }, // tx_lf: GC1109 full PA
|
||||
{ LR2021::MODE_RX_HF, { LOW, HIGH, LOW, LOW, LOW } }, // rx_hf: 2G4 LNA, GC1109 off
|
||||
{ LR2021::MODE_TX_HF, { HIGH, LOW, LOW, LOW, LOW } }, // tx_hf: 2G4 PA, GC1109 off
|
||||
|
||||
Reference in new issue
Block a user