Prioritize ESP32 Auto CPU speed during dual-profile radio scanning
This commit is contained in:
1 parent
663312c820
commit
1412ee4954
16 files changed
+399
-28
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
Reference in new issue
Block a user