Prioritize ESP32 Auto CPU speed during dual-profile radio scanning

This commit is contained in:
mikecarper committed 2026-10-23 02:34:50 -07:00
1 parent 663312c820
commit 1412ee4954
16 files changed
+399 -28

No files matched your search

+1 -1
View File
@@ -3024,7 +3024,7 @@ The G3 button also wakes the device for at least two minutes. See the
for verification steps and the distinction between the fix and the published
binaries.
Companion firmware defaults this setting to `on`. Full Companion accepts the command from its local USB terminal and exposes the same setting in WebConfig. On ESP32, it lowers the CPU clock to 80 MHz, enables idle yielding, and enables the configured GPS duty cycle. USB and each active wireless transport remain available; SenseCAP Indicator Full keeps only its selected BLE or infrastructure-WiFi secondary transport active. `set powersaving off` restores the board's normal CPU clock and disables the GPS duty cycle. This device setting is separate from LoRa RXPS (`radio.rxps`) and WiFi modem power save (`wifi.powersave`). Infrastructure uses the same commands; its hardware and active-service sleep guards determine when the node can sleep.
Companion firmware defaults this setting to `on`. Full Companion accepts the command from its local USB terminal and exposes the same setting in WebConfig. On ESP32 with processor speed set to Auto, it normally selects 80 MHz, enables idle yielding, and enables the configured GPS duty cycle. While a second radio profile is active, Auto holds the highest supported CPU speed for faster switching; SenseCAP Indicator retains its existing policy. Explicit processor speeds remain selected. USB and each active wireless transport remain available; SenseCAP Indicator Full keeps only its selected BLE or infrastructure-WiFi secondary transport active. `set powersaving off` restores the board's normal CPU clock and disables the GPS duty cycle. This device setting is separate from LoRa RXPS (`radio.rxps`) and WiFi modem power save (`wifi.powersave`). Infrastructure uses the same commands; its hardware and active-service sleep guards determine when the node can sleep.
#### Diagnose what prevents power saving
+5 -3
View File
@@ -496,9 +496,11 @@ turns it on once when upgrading an older Companion preference file, including
one written by the short-lived default-off regression. After that one-time
migration, an explicit `set powersaving off` selection remains persistent.
On ESP32, enabling it lowers the CPU clock to 80 MHz, enables idle yielding,
and enables the configured GPS duty cycle. Disabling it restores the normal CPU
clock and keeps GPS awake. Active Full Companion transports remain available in
On ESP32, enabling it normally selects 80 MHz when processor speed is Auto,
enables idle yielding, and enables the configured GPS duty cycle. Auto holds
the highest supported CPU speed while a second radio profile is active, except
on SenseCAP Indicator. An explicit processor speed stays selected. Disabling
device power saving restores the normal Auto CPU policy and keeps GPS awake. Active Full Companion transports remain available in
both states; on the SenseCAP Indicator this means USB plus the selected BLE or
infrastructure-WiFi secondary transport. WiFi modem sleep stays enabled when
BLE and the ESP32 WiFi radio are active together because coexistence requires
+6 -3
View File
@@ -115,9 +115,12 @@ packet-test failures. SDK wait time is not a current measurement.
The WebConfig **Processor speed** selector and `set cpu.freq auto|80|160|240`
save the processor clock choice. `get cpu.freq` reports the choice, actual MHz
and supported values. The board rejects unsupported clocks. Auto retains the
existing policy: Companion normally selects 80 MHz with device power saving
on and its configured nominal speed with it off. Other roles retain their
nominal speed in Auto. A fixed speed stays fixed when power saving changes.
normal policy: Companion selects 80 MHz with device power saving on and its
configured nominal speed with it off. Other roles retain their nominal speed.
While radio2 or a temporary second profile is active, Auto instead holds the
highest supported CPU speed (240 or 160 MHz) to prioritize switching speed.
SenseCAP Indicator retains its existing policy. A fixed speed stays fixed when
power saving or radio2 changes.
The selection applies immediately; a failed apply or save reports an error and
attempts to restore the previous selection. Returning to Auto clears the saved
override. Existing preferences load as Auto.
+1
View File
@@ -3196,6 +3196,7 @@ void MyMesh::begin(bool has_display, bool radio_available,
if (!mesh::esp32::cpuclock::validSelection(_prefs.cpu_freq_mhz)
|| (_prefs.cpu_freq_mhz && !board.supportsCpuFrequencyMHz(_prefs.cpu_freq_mhz)))
_prefs.cpu_freq_mhz = 0;
board.setCpuFrequencyRequest(_prefs.cpu_freq_mhz, _prefs.powersaving_enabled != 0);
#endif
_prefs.wifi_enabled = constrain(_prefs.wifi_enabled, 0, 1);
_prefs.usb_logging_enabled = constrain(_prefs.usb_logging_enabled, 0, 2);
+8 -7
View File
@@ -561,16 +561,16 @@ static void applyCompanionBluetoothSleep(bool enabled,
static bool applyCompanionPowerSaving(bool enabled) {
const uint16_t selected = the_mesh.getNodePrefs()->cpu_freq_mhz;
uint32_t max_mhz = mesh::esp32::cpuclock::resolveMHz(selected, enabled, true);
board.setRadioProfileCpuClockPolicy(the_mesh.isDualRadioActive());
const uint32_t max_mhz = board.resolveCpuFrequencyMHz(selected, enabled);
if (!board.applyCpuFrequencyMHz(selected, enabled)) {
mesh::usbLoggingPort().printf("Device power saving failed: CPU clock policy rejected\r\n");
return false;
}
if (!selected && max_mhz == 240 && !board.supportsCpuFrequencyMHz(240)
&& board.supportsCpuFrequencyMHz(160)) max_mhz = 160;
#if COMPANION_IDF_PM_AVAILABLE
const uint32_t min_mhz = !selected && enabled && max_mhz > 40 ? 40 : max_mhz;
const uint32_t min_mhz = !selected && enabled && max_mhz > 40
&& !mesh::esp32::cpuclock::profilePerformanceEnabled(the_mesh.isDualRadioActive())
? 40 : max_mhz;
#if (defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT) \
|| defined(ENABLE_USB_INTERFACE)
// Automatic light sleep interrupts native USB CDC and USB companions.
@@ -604,7 +604,8 @@ static bool applyCompanionPowerSaving(bool enabled) {
}
static void serviceCompanionPowerSaving(bool force = false) {
const int8_t requested = the_mesh.getNodePrefs()->powersaving_enabled ? 1 : 0;
const int8_t requested = (the_mesh.getNodePrefs()->powersaving_enabled ? 1 : 0)
| (mesh::esp32::cpuclock::profilePerformanceEnabled(the_mesh.isDualRadioActive()) ? 2 : 0);
const uint16_t selected = the_mesh.getNodePrefs()->cpu_freq_mhz;
if (!force && applied_power_saving == requested && applied_cpu_frequency == selected) return;
@@ -617,7 +618,7 @@ static void serviceCompanionPowerSaving(bool force = false) {
attempted_power_saving = requested;
attempted_cpu_frequency = selected;
if (applyCompanionPowerSaving(requested != 0)) {
if (applyCompanionPowerSaving((requested & 1) != 0)) {
applied_power_saving = requested;
applied_cpu_frequency = selected;
power_saving_retry_at = 0;
+1 -1
View File
@@ -658,7 +658,7 @@ void loop() {
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis());
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis(), false, the_mesh.isDualRadioActive());
#endif
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
if (!allowed && mesh::powerdiag::enabled())
+1 -1
View File
@@ -433,7 +433,7 @@ void loop() {
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis());
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis(), false, the_mesh.isDualRadioActive());
#endif
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
if (!allowed && mesh::powerdiag::enabled())
+1 -1
View File
@@ -393,7 +393,7 @@ void loop() {
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis());
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis(), false, the_mesh.isDualRadioActive());
#endif
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
if (!allowed && mesh::powerdiag::enabled())
+5
View File
@@ -170,7 +170,12 @@ public:
// preference layout and board vtable. Zero selects the role's Auto policy.
virtual uint16_t getCpuFrequencyMHz() const { return 0; }
virtual bool supportsCpuFrequencyMHz(uint16_t) const { return false; }
virtual void setCpuFrequencyRequest(uint16_t, bool) { }
virtual bool applyCpuFrequencyMHz(uint16_t, bool) { return false; }
virtual bool prepareRadioProfileCpuClock(bool, bool& changed) {
changed = false;
return true;
}
#endif
virtual void reboot() = 0;
MESH_REGULATOR_VIRTUAL uint8_t mcuRegulatorModes() const { return 0; }
+6
View File
@@ -1070,6 +1070,12 @@ void CommonCLI::loadPrefs(FILESYSTEM* fs) {
if (loaded) syncOtaConfigFromPrefs(); // persisted OTA policy/keys -> OtaContext (else keep safe defaults)
#endif
mesh::hilStartupTrace("prefs_apply_begin");
#if defined(ESP32_PLATFORM)
if (!mesh::esp32::cpuclock::validSelection(_prefs->cpu_freq_mhz)
|| (_prefs->cpu_freq_mhz && !_board->supportsCpuFrequencyMHz(_prefs->cpu_freq_mhz)))
_prefs->cpu_freq_mhz = 0;
_board->setCpuFrequencyRequest(_prefs->cpu_freq_mhz, _prefs->powersaving_enabled != 0);
#endif
#if defined(NRF52_POWER_MANAGEMENT)
// The startup voltage policy owns the relative calibration on these boards.
// Its checked record must not be replaced by the legacy infrastructure field.
+76 -6
View File
@@ -54,6 +54,13 @@ protected:
bool ota_started_ap = false;
bool ota_started_radio = false;
bool companion_cpu_clock_policy = false;
bool radio_profile_cpu_clock_policy = false;
bool applied_profile_cpu_clock_policy = false;
bool applied_cpu_clock_valid = false;
uint16_t requested_cpu_clock_selection = 0;
bool requested_cpu_clock_powersaving = false;
uint16_t sampled_profile_cpu_mhz = 0;
uint32_t profile_cpu_clock_retry_at = 0;
uint32_t uart_quiet_start_at = 0;
uint32_t uart_quiet_restore_at = 0;
uint16_t uart_quiet_seconds = 0;
@@ -121,6 +128,9 @@ public:
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
void setCompanionCpuClockPolicy(bool enabled) { companion_cpu_clock_policy = enabled; }
void setRadioProfileCpuClockPolicy(bool enabled) {
radio_profile_cpu_clock_policy = mesh::esp32::cpuclock::profilePerformanceEnabled(enabled);
}
uint16_t getCpuFrequencyMHz() const override { return ::getCpuFrequencyMhz(); }
bool supportsCpuFrequencyMHz(uint16_t mhz) const override {
#if CONFIG_IDF_TARGET_ESP32H2
@@ -145,15 +155,40 @@ public:
#endif
}
uint16_t resolveCpuFrequencyMHz(uint16_t selected, bool powersaving) const {
uint16_t maximum = mesh::esp32::cpuclock::resolveMHz(
selected, powersaving, companion_cpu_clock_policy);
if (!selected && radio_profile_cpu_clock_policy) {
const uint16_t candidates[] = {240, 160, 80};
for (uint16_t candidate : candidates) {
if (supportsCpuFrequencyMHz(candidate)) return candidate;
}
}
if (!selected && maximum == 240 && !supportsCpuFrequencyMHz(240)
&& supportsCpuFrequencyMHz(160)) maximum = 160;
return maximum;
}
// Seed the loaded choice before saved profiles can retune the radio. Keep
// this request even when the SDK cannot apply it on the first attempt.
void setCpuFrequencyRequest(uint16_t selected, bool powersaving) override {
if (requested_cpu_clock_selection != selected || requested_cpu_clock_powersaving != powersaving) {
requested_cpu_clock_selection = selected;
requested_cpu_clock_powersaving = powersaving;
applied_cpu_clock_valid = false;
profile_cpu_clock_retry_at = 0;
}
}
bool applyCpuFrequencyMHz(uint16_t selected, bool powersaving) override {
if (!mesh::esp32::cpuclock::validSelection(selected)
|| (selected && !supportsCpuFrequencyMHz(selected))) return false;
uint16_t maximum = mesh::esp32::cpuclock::resolveMHz(
selected, powersaving, companion_cpu_clock_policy);
if (!selected && maximum == 240 && !supportsCpuFrequencyMHz(240)
&& supportsCpuFrequencyMHz(160)) maximum = 160;
setCpuFrequencyRequest(selected, powersaving);
const uint16_t maximum = resolveCpuFrequencyMHz(selected, powersaving);
applied_cpu_clock_valid = false;
#if defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE
const uint16_t minimum = !selected && companion_cpu_clock_policy && powersaving && maximum > 40
const uint16_t minimum = !selected && !radio_profile_cpu_clock_policy
&& companion_cpu_clock_policy && powersaving && maximum > 40
? 40 : maximum;
#if CONFIG_IDF_TARGET_ESP32C3
esp_pm_config_esp32c3_t config;
@@ -175,7 +210,42 @@ public:
#else
if (!setCpuFrequencyMhz(maximum)) return false;
#endif
return !selected || getCpuFrequencyMHz() == selected;
if ((selected || radio_profile_cpu_clock_policy) && getCpuFrequencyMHz() != maximum) return false;
applied_profile_cpu_clock_policy = radio_profile_cpu_clock_policy;
applied_cpu_clock_valid = true;
profile_cpu_clock_retry_at = 0;
return true;
}
bool prepareRadioProfileCpuClock(bool enabled, bool& changed) override {
changed = false;
setRadioProfileCpuClockPolicy(enabled);
uint16_t previous = 0;
const bool fixed_clock = requested_cpu_clock_selection || radio_profile_cpu_clock_policy;
const bool clock_drifted = fixed_clock && getCpuFrequencyMHz()
!= resolveCpuFrequencyMHz(requested_cpu_clock_selection, requested_cpu_clock_powersaving);
if (!applied_cpu_clock_valid || applied_profile_cpu_clock_policy != radio_profile_cpu_clock_policy
|| clock_drifted) {
const uint32_t now = millis();
if (profile_cpu_clock_retry_at && static_cast<int32_t>(now - profile_cpu_clock_retry_at) < 0)
return false;
previous = getCpuFrequencyMHz();
if (!applyCpuFrequencyMHz(requested_cpu_clock_selection, requested_cpu_clock_powersaving)) {
profile_cpu_clock_retry_at = now + 5000;
if (!profile_cpu_clock_retry_at) profile_cpu_clock_retry_at = 1;
return false;
}
profile_cpu_clock_retry_at = 0;
}
// Observe explicit clock changes too. Reset only the pair's timing proof,
// once per effective transition; hardware failure counters stay intact.
if (radio_profile_cpu_clock_policy) {
const uint16_t current = getCpuFrequencyMHz();
changed = (previous && previous != current)
|| (sampled_profile_cpu_mhz && sampled_profile_cpu_mhz != current);
sampled_profile_cpu_mhz = current;
} else sampled_profile_cpu_mhz = 0;
return true;
}
bool handleUartQuiet(mesh::esp32::UartQuietAction action, uint16_t seconds,
+9
View File
@@ -34,6 +34,15 @@ inline uint16_t resolveMHz(uint16_t selected, bool powersaving, bool companion)
return companion && powersaving && nominal > 80 ? 80 : nominal;
}
inline bool profilePerformanceEnabled(bool profiles_enabled) {
#if defined(SENSECAP_INDICATOR_LORA)
(void)profiles_enabled;
return false;
#else
return profiles_enabled;
#endif
}
static constexpr size_t FooterSize = 8;
inline uint16_t footerChecksum(const uint8_t* bytes, size_t length) {
+8 -2
View File
@@ -4,6 +4,7 @@
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "CpuClockPolicy.h"
namespace mesh {
namespace esp32 {
@@ -72,14 +73,18 @@ inline uint32_t profileSleepSettleMillis(uint32_t normal) {
// choice at boot and after preference changes without reconfiguring each loop.
template <typename Board>
bool serviceCpuFrequencyPolicy(Board& board, uint16_t selected, bool powersaving,
uint32_t now_ms, bool force = false) {
uint32_t now_ms, bool force = false, bool profiles_enabled = false) {
struct State {
uint16_t selected = UINT16_MAX;
bool powersaving = false;
bool profiles_enabled = false;
uint32_t retry_at = 0;
};
static State state;
if (!force && state.selected == selected && state.powersaving == powersaving) return true;
profiles_enabled = cpuclock::profilePerformanceEnabled(profiles_enabled);
board.setRadioProfileCpuClockPolicy(profiles_enabled);
if (!force && state.selected == selected && state.powersaving == powersaving
&& state.profiles_enabled == profiles_enabled) return true;
if (!force && state.retry_at && static_cast<int32_t>(now_ms - state.retry_at) < 0) return false;
if (!board.applyCpuFrequencyMHz(selected, powersaving)) {
state.retry_at = now_ms + 5000;
@@ -88,6 +93,7 @@ bool serviceCpuFrequencyPolicy(Board& board, uint16_t selected, bool powersaving
}
state.selected = selected;
state.powersaving = powersaving;
state.profiles_enabled = profiles_enabled;
state.retry_at = 0;
return true;
}
@@ -626,6 +626,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
#endif
if (_cw_active) return mesh::RadioParamApplyResult::BUSY;
if (profile > 1 || (profile == 1 && !_profiles.enabled())) return mesh::RadioParamApplyResult::FAILED;
#if defined(ESP32_PLATFORM) && !defined(SENSECAP_INDICATOR_LORA)
bool clock_changed = false;
if (!_board->prepareRadioProfileCpuClock(_profiles.enabled(), clock_changed))
return mesh::RadioParamApplyResult::BUSY;
if (clock_changed) _profiles.resetSwitchTest();
#endif
const auto& p = _profiles.params(profile);
const uint16_t preamble = profilePreamble(profile);
if (!_profile_refresh_required && !_profile_repair_required && _active_profile == profile && _profile_generation == _profiles.generation[profile]
+257 -3
View File
@@ -42,10 +42,16 @@ int esp_pm_configure(esp_pm_config_t* config){
struct MainBoard {
virtual uint16_t getCpuFrequencyMHz()const=0;
virtual bool supportsCpuFrequencyMHz(uint16_t)const=0;
virtual void setCpuFrequencyRequest(uint16_t,bool){}
virtual bool applyCpuFrequencyMHz(uint16_t,bool)=0;
virtual bool prepareRadioProfileCpuClock(bool,bool& changed){changed=false;return true;}
};
struct ESP32Board:MainBoard {
bool companion_cpu_clock_policy=false;
bool radio_profile_cpu_clock_policy=false,applied_profile_cpu_clock_policy=false,applied_cpu_clock_valid=false;
uint16_t requested_cpu_clock_selection=0,sampled_profile_cpu_mhz=0;
bool requested_cpu_clock_powersaving=false;
uint32_t profile_cpu_clock_retry_at=0;
unsigned quiet_services=0;
void servicePowerTestControls(){++quiet_services;}
@METHODS@
@@ -55,15 +61,17 @@ struct Console {void printf(const char*,...){}};
Console& usbLoggingPort(){static Console value;return value;}
}
struct Prefs {uint16_t cpu_freq_mhz=0;bool powersaving_enabled=true;} prefs;
struct Mesh {Prefs* getNodePrefs(){return &prefs;}} the_mesh;
struct Mesh {bool dual=false;Prefs* getNodePrefs(){return &prefs;}bool isDualRadioActive(){return dual;}} the_mesh;
'''
class CpuClockBoardTest(unittest.TestCase):
def fixture(self):
methods="\n".join(board_method(signature) for signature in (
"void setCompanionCpuClockPolicy(","uint16_t getCpuFrequencyMHz() const override",
"bool supportsCpuFrequencyMHz(","bool applyCpuFrequencyMHz("))
"void setCompanionCpuClockPolicy(","void setRadioProfileCpuClockPolicy(",
"uint16_t getCpuFrequencyMHz() const override", "bool supportsCpuFrequencyMHz(",
"uint16_t resolveCpuFrequencyMHz(", "void setCpuFrequencyRequest(", "bool applyCpuFrequencyMHz(",
"bool prepareRadioProfileCpuClock("))
return PRELUDE.replace("@METHODS@",methods)
def run_fixture(self,main,defines=()):
@@ -153,6 +161,14 @@ int main(){
prefs.powersaving_enabled=false;serviceCompanionPowerSaving();assert(actual_mhz==160);
prefs.powersaving_enabled=true;serviceCompanionPowerSaving();assert(actual_mhz==160);
prefs.cpu_freq_mhz=0;serviceCompanionPowerSaving();assert(actual_mhz==80);
the_mesh.dual=true;serviceCompanionPowerSaving();assert(actual_mhz==240);
writes=clock_writes;serviceCompanionPowerSaving();assert(clock_writes==writes);
// Repeated Auto commands use the same board policy, rather than dropping
// back to80 behind the role's unchanged request cache.
assert(board.applyCpuFrequencyMHz(0,true)&&actual_mhz==240);
prefs.cpu_freq_mhz=80;serviceCompanionPowerSaving();assert(actual_mhz==80);
prefs.cpu_freq_mhz=0;serviceCompanionPowerSaving();assert(actual_mhz==240);
the_mesh.dual=false;serviceCompanionPowerSaving();assert(actual_mhz==80);
prefs.powersaving_enabled=false;serviceCompanionPowerSaving();assert(actual_mhz==240);
// Failures retain the request and retry after the original bounded delay.
prefs.cpu_freq_mhz=160;fail_clock=true;serviceCompanionPowerSaving();writes=clock_writes;
@@ -175,11 +191,249 @@ int main(){
unsigned writes=clock_writes;role_service();assert(clock_writes==writes&&board.quiet_services==2);
prefs.powersaving_enabled=false;role_service();assert(actual_mhz==160);
prefs.cpu_freq_mhz=80;role_service();assert(actual_mhz==80);
prefs.cpu_freq_mhz=0;role_service();assert(actual_mhz==mesh::esp32::cpuclock::nominalMHz());
// Active profiles prefer the chip maximum over a low nominal or Auto+PS.
prefs.powersaving_enabled=true;the_mesh.dual=true;role_service();assert(actual_mhz==240);
prefs.cpu_freq_mhz=80;role_service();assert(actual_mhz==80);
prefs.cpu_freq_mhz=0;role_service();assert(actual_mhz==240);
the_mesh.dual=false;role_service();assert(actual_mhz==mesh::esp32::cpuclock::nominalMHz());
}
'''
with self.subTest(role=role):
self.run_fixture(tests,["-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=0"])
self.run_fixture(tests,["-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=0",
"-DESP32_CPU_FREQ=80"])
def test_profile_auto_clock_limits_dials_timing_changes_and_failed_boost_retry(self):
tests=r'''
int main(){
board.setCompanionCpuClockPolicy(true);
assert(board.applyCpuFrequencyMHz(0,true));
bool changed=true;
assert(board.prepareRadioProfileCpuClock(true,changed));
const unsigned fast=board.supportsCpuFrequencyMHz(240)?240:160;
assert(actual_mhz==fast);
#if CONFIG_PM_ENABLE
assert(unsigned(applied_pm.min_freq_mhz)==fast&&unsigned(applied_pm.max_freq_mhz)==fast);
// Fixing the clock does not disable the existing CPU sleep permission.
assert(applied_pm.light_sleep_enable);
#endif
unsigned writes=clock_writes;
assert(board.prepareRadioProfileCpuClock(true,changed)&&!changed&&clock_writes==writes);
for(uint16_t dial : {uint16_t(80),uint16_t(160)}){
assert(board.applyCpuFrequencyMHz(dial,true)&&actual_mhz==dial);
assert(board.prepareRadioProfileCpuClock(true,changed));
assert(changed);
assert(actual_mhz==dial);
assert(board.prepareRadioProfileCpuClock(true,changed)&&!changed);
}
assert(board.applyCpuFrequencyMHz(0,true)&&actual_mhz==fast);
assert(board.prepareRadioProfileCpuClock(true,changed));
assert(changed==(fast!=160));
assert(board.prepareRadioProfileCpuClock(false,changed)&&!changed);
#if CONFIG_PM_ENABLE
assert(actual_mhz==40&&applied_pm.max_freq_mhz==80);
#else
assert(actual_mhz==80);
#endif
fail_clock=true;writes=clock_writes;
assert(!board.prepareRadioProfileCpuClock(true,changed)&&!changed);
assert(clock_writes==writes+1);
writes=clock_writes;
assert(!board.prepareRadioProfileCpuClock(true,changed)&&clock_writes==writes);
now_ms+=5000;fail_clock=false;
assert(board.prepareRadioProfileCpuClock(true,changed)&&actual_mhz==fast);
assert(board.prepareRadioProfileCpuClock(true,changed)&&!changed);
}
'''
for target in ("ESP32S3","ESP32C3","ESP32C6"):
for pm in (0,1):
with self.subTest(target=target,pm=pm):
self.run_fixture("#include <initializer_list>\n"+tests,[
f"-DCONFIG_IDF_TARGET_{target}=1",f"-DCONFIG_PM_ENABLE={pm}"])
def test_indicator_keeps_existing_auto_clock_policy_with_active_profiles(self):
tests=r'''
int main(){
board.setCompanionCpuClockPolicy(true);
board.setRadioProfileCpuClockPolicy(true);
assert(board.applyCpuFrequencyMHz(0,true));
assert(!board.radio_profile_cpu_clock_policy);
#if CONFIG_PM_ENABLE
assert(applied_pm.min_freq_mhz==40&&applied_pm.max_freq_mhz==80);
#else
assert(actual_mhz==80);
#endif
bool changed=true;
assert(board.prepareRadioProfileCpuClock(true,changed)&&!changed);
assert(board.applyCpuFrequencyMHz(160,true)&&actual_mhz==160);
}
'''
for pm in (0,1):
self.run_fixture(tests,["-DSENSECAP_INDICATOR_LORA=1",
"-DCONFIG_IDF_TARGET_ESP32S3=1",f"-DCONFIG_PM_ENABLE={pm}"])
def test_retune_entry_boosts_before_preamble_and_resets_only_pair_timing(self):
source=(ROOT/"src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
entry=body(source,"mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(")
# Execute the production admission prefix through tuple validation.
# Stop before the measured hardware transaction; its SPI lifecycle has
# separate coverage. Clock application must precede even preamble math.
entry=entry[:entry.index(" const bool dual_profile =")]
entry=entry.replace("RadioLibWrapper::tuneProfile", "tuneProfile")
code=r'''
#include <RadioProfiles.h>
namespace mesh {enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};}
struct RadioLibWrapper {
ESP32Board* _board=&board;
mesh::RadioProfiles _profiles;
bool _cw_active=false,_profile_refresh_required=true,_profile_repair_required=false;
uint8_t _active_profile=0;
uint32_t _profile_generation=0;
uint16_t _physical_preamble=32;
unsigned preamble_reads=0,retunes=0;
uint16_t expected=240;
uint16_t profilePreamble(uint8_t){++preamble_reads;assert(actual_mhz==expected);return 32;}
bool validateProfile(const mesh::RadioProfileParams&)const{return true;}
@ENTRY@
++retunes;return mesh::RadioParamApplyResult::APPLIED;
}
};
int main(){
board.setCompanionCpuClockPolicy(true);
assert(board.applyCpuFrequencyMHz(0,true)&&actual_mhz==80);
RadioLibWrapper radio;
mesh::RadioProfileConfig config;config.mode=mesh::RadioProfileMode::RxTx;
radio._profiles.setSecondary(config,false);
radio._profiles.switch_test_max_us[0]=99999;
radio._profiles.switch_test_samples[0]=4;
radio._profiles.switches=19;radio._profiles.switch_failures=3;
assert(radio.tuneProfile(1)==mesh::RadioParamApplyResult::APPLIED&&actual_mhz==240);
assert(!radio._profiles.switch_test_max_us[0]&&!radio._profiles.switch_test_samples[0]);
assert(radio._profiles.switches==19&&radio._profiles.switch_failures==3);
radio._profiles.switch_test_max_us[0]=1200;radio._profiles.switch_test_samples[0]=1;
unsigned writes=clock_writes;
assert(radio.tuneProfile(1)==mesh::RadioParamApplyResult::APPLIED&&clock_writes==writes);
assert(radio._profiles.switch_test_max_us[0]==1200&&radio._profiles.switch_test_samples[0]==1);
assert(board.applyCpuFrequencyMHz(160,true));radio.expected=160;
assert(radio.tuneProfile(1)==mesh::RadioParamApplyResult::APPLIED);
assert(!radio._profiles.switch_test_max_us[0]&&!radio._profiles.switch_test_samples[0]);
radio._profiles.switch_test_max_us[0]=1700;
assert(radio.tuneProfile(1)==mesh::RadioParamApplyResult::APPLIED);
assert(radio._profiles.switch_test_max_us[0]==1700);
// A failed boost defers before preamble math or hardware retuning.
bool changed=false;assert(board.prepareRadioProfileCpuClock(false,changed));
assert(board.applyCpuFrequencyMHz(0,true));fail_clock=true;
const unsigned previous=radio.retunes,reads=radio.preamble_reads;
assert(radio.tuneProfile(1)==mesh::RadioParamApplyResult::BUSY);
assert(radio.retunes==previous&&radio.preamble_reads==reads);
}
'''.replace("@ENTRY@",entry)
self.run_fixture(code,["-DESP32_PLATFORM=1","-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=0"])
def test_loaded_clock_choice_is_seeded_before_saved_radio_and_survives_apply_failure(self):
common=(ROOT/"src/helpers/CommonCLI.cpp").read_text()
load=body(common,"void CommonCLI::loadPrefs(")
seed=load[load.index("#if defined(ESP32_PLATFORM)",load.index('mesh::hilStartupTrace("prefs_apply_begin")')):]
seed=seed[:seed.index("#endif")]
seed=seed[seed.index("\n")+1:]
seed=seed.replace("_board->","board.").replace("_prefs->","prefs.")
companion=(ROOT/"examples/companion_radio/MyMesh.cpp").read_text()
begin=body(companion,"void MyMesh::begin(bool has_display,")
companion_seed=begin[begin.index(" if (!mesh::esp32::cpuclock::validSelection("):]
companion_seed=companion_seed[:companion_seed.index("#endif")]
companion_seed=companion_seed.replace("_prefs.","prefs.")
self.assertLess(begin.index("_store->loadPrefs("),begin.index("board.setCpuFrequencyRequest("))
self.assertLess(begin.index("board.setCpuFrequencyRequest("),begin.index("configureRadioFromPrefs();"))
self.assertLess(load.index("loadPrefsInt("),load.index("_board->setCpuFrequencyRequest("))
self.assertLess(load.index("_board->setCpuFrequencyRequest("),load.index("_radio_profiles.stagePrimary("))
for path,signature in (("simple_repeater/MyMesh.cpp","void MyMesh::begin(FILESYSTEM"),
("simple_room_server/MyMesh.cpp","void MyMesh::begin(FILESYSTEM"),
("simple_sensor/SensorMesh.cpp","void SensorMesh::begin(FILESYSTEM")):
role=body((ROOT/"examples"/path).read_text(),signature)
self.assertLess(role.index("_cli.loadPrefs("),role.index("applySavedRadioParams()"))
tests=r'''
int main(){
board.setCompanionCpuClockPolicy(true);
prefs.cpu_freq_mhz=80;prefs.powersaving_enabled=true;
seed_request();
assert(clock_writes==0); // Seeding must not use unconfigured peripherals/SDK.
bool changed=false;
assert(board.prepareRadioProfileCpuClock(true,changed)&&actual_mhz==80);
assert(board.requested_cpu_clock_selection==80&&board.requested_cpu_clock_powersaving);
prefs.cpu_freq_mhz=160;prefs.powersaving_enabled=false;
seed_request();
fail_clock=true;const unsigned writes=clock_writes;
assert(!board.prepareRadioProfileCpuClock(true,changed));
assert(clock_writes==writes+1);
assert(board.requested_cpu_clock_selection==160&&!board.requested_cpu_clock_powersaving);
assert(!board.prepareRadioProfileCpuClock(true,changed)&&clock_writes==writes+1);
now_ms+=5000;fail_clock=false;
assert(board.prepareRadioProfileCpuClock(true,changed)&&actual_mhz==160);
// A direct CLI/role apply which fails also retains its requested clock;
// the retune gate must not retry the last successful160MHz selection.
fail_clock=true;assert(!board.applyCpuFrequencyMHz(80,true));fail_clock=false;
assert(board.prepareRadioProfileCpuClock(true,changed)&&actual_mhz==80);
assert(board.requested_cpu_clock_selection==80&&board.requested_cpu_clock_powersaving);
}
'''
for role,seed_code in (("infrastructure",seed),("companion",companion_seed)):
for pm in (0,1):
with self.subTest(role=role,pm=pm):
self.run_fixture("void seed_request(){\n"+seed_code+"\n}\n"+tests,[
"-DESP32_PLATFORM=1","-DCONFIG_IDF_TARGET_ESP32S3=1",f"-DCONFIG_PM_ENABLE={pm}"])
def test_rc32_post_boot_override_is_repaired_before_retune_with_cached_role_policy(self):
source=(ROOT/"src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
entry=body(source,"mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(")
entry=entry[:entry.index(" const bool dual_profile =")]
entry=entry.replace("RadioLibWrapper::tuneProfile", "tuneProfile")
for role in ("simple_repeater","simple_room_server","simple_sensor"):
main=(ROOT/"examples"/role/"main.cpp").read_text()
setup=body(main,"void setup()")
# The role must not cache the loaded request before board-specific
# onBootComplete can select its post-startup clock (RC32:160MHz).
self.assertNotIn("serviceCpuFrequencyPolicy(",setup[:setup.index("board.onBootComplete();")])
start=main.index(" board.servicePowerTestControls();",main.index("void loop()"))
service=main[start:main.index("#endif",start)]
code=r'''
#include <RadioProfiles.h>
namespace mesh {enum class RadioParamApplyResult {APPLIED,BUSY,FAILED};}
void role_service(){@SERVICE@}
struct RadioLibWrapper {
ESP32Board* _board=&board;
mesh::RadioProfiles _profiles;
bool _cw_active=false,_profile_refresh_required=true,_profile_repair_required=false;
uint8_t _active_profile=0;uint32_t _profile_generation=0;
uint16_t _physical_preamble=32,expected=240;
uint16_t profilePreamble(uint8_t){assert(actual_mhz==expected);return 32;}
bool validateProfile(const mesh::RadioProfileParams&)const{return true;}
@ENTRY@
return mesh::RadioParamApplyResult::APPLIED;
}
};
int main(){
RadioLibWrapper radio;mesh::RadioProfileConfig config;config.mode=mesh::RadioProfileMode::RxTx;
radio._profiles.setSecondary(config,false);the_mesh.dual=true;
// Loaded profiles can apply before onBootComplete. A fixed requested clock
// must be respected both there and after the board's post-boot override.
for(uint16_t selected : {uint16_t(0),uint16_t(80),uint16_t(160),uint16_t(240)}){
prefs.cpu_freq_mhz=selected;prefs.powersaving_enabled=true;
board.setCpuFrequencyRequest(selected,true);
radio.expected=selected?selected:240;
assert(radio.tuneProfile(0)==mesh::RadioParamApplyResult::APPLIED);
role_service();assert(actual_mhz==radio.expected);
actual_mhz=160; // RC32::onBootComplete hardware change, outside role cache.
const unsigned writes=clock_writes;
role_service();assert(clock_writes==writes); // Same cached role request.
assert(radio.tuneProfile(1)==mesh::RadioParamApplyResult::APPLIED);
assert(actual_mhz==radio.expected);
assert(clock_writes==writes+(radio.expected!=160));
}
}
'''.replace("@SERVICE@",service).replace("@ENTRY@",entry)
self.run_fixture("#include <initializer_list>\n"+code,["-DESP32_PLATFORM=1",
"-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=0","-DESP32_POST_BOOT_CPU_FREQ=160"])
def test_rc32_post_boot_nominal_survives_shared_policy_and_explicit_selection(self):
main=(ROOT/"examples/companion_radio/main.cpp").read_text()
+8
View File
@@ -31,7 +31,15 @@ uint32_t profileElapsedUs(uint32_t since, uint32_t now, bool) { return now - sin
uint32_t millis() { return (uint32_t)(elapsed_us / 1000); }
struct Chip { bool standbyXOSC=false; int standby() { return 0; } };
using CustomSX1262 = Chip;
#if defined(ESP32_PLATFORM)
struct Board {
bool prepareRadioProfileCpuClock(bool, bool& changed) { changed=false;return true; }
};
#endif
struct RadioLibWrapper {
#if defined(ESP32_PLATFORM)
Board board; Board* _board=&board;
#endif
uint8_t state = STATE_RX;
RadioInterruptEvents events;
bool hasPendingRadioInterrupt() const { return events.pending(); }