Add ESP32 clock selection and bounded power test controls

This commit is contained in:
mikecarper committed 2026-10-21 21:06:42 -07:00
1 parent 11dbca377c
commit 6e599d1709
33 files changed
+4028 -2430

No files matched your search

+4
View File
@@ -447,6 +447,9 @@ jobs:
python3 -B test/test_power_saving_diagnostics.py -v
python3 -B test/test_power_saving_role_diagnostics.py -v
python3 -B test/test_companion_power_diagnostics.py -v
python3 -B test/test_esp32_cpu_clock.py -v
python3 -B test/test_esp32_cpu_clock_board.py -v
python3 -B test/test_esp32_power_test_controls.py -v
python3 -B test/test_companion_rxps_getters.py -v
python3 -B test/test_nrf52_sleep_cycle.py -v
python3 -B test/test_nrf52_sleep_events.py -v
@@ -1053,6 +1056,7 @@ jobs:
python3 -B test/test_sensor_webconfig_capabilities.py -v
python3 -B test/test_webconfig_ui_runtime.py
python3 -B test/test_webconfig_lost_reply.py -v
python3 -B test/test_webconfig_cpu_frequency.py -v
MESHCORE_REQUIRE_ROOM_BROWSER=1 python3 -B test/test_room_web_ui.py -v
MESHCORE_REQUIRE_ROOM_BROWSER=1 python3 -B test/test_room_mail_web_ui.py -v
+43
View File
@@ -92,6 +92,49 @@ The [ESP32 Bluetooth and radio2 qualification report](test-results/esp32_ble_rad
records the tested firmware, saved-pairing checks, SDK wait measurements and
packet-test failures. SDK wait time is not a current measurement.
## ESP32 processor speed and temporary test controls
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.
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.
Local USB/Bluetooth console commands provide temporary controls for controlled
radio2 experiments. They are held in RAM and reset at reboot:
| Command | Values | Default |
| --- | --- | --- |
| `set powersaving.test sleep VALUE` | `on`, `off` | `on` |
| `set powersaving.test slice_ms VALUE` | 2–20 ms | 10 ms |
| `set powersaving.test guard_ms VALUE` | 0–20 ms | 4 ms |
| `set powersaving.test min_ms VALUE` | 1–10 ms | 2 ms |
| `set powersaving.test settle_ms VALUE` | `auto`, 1–50 ms | `auto` |
| `set powersaving.test uart_quiet VALUE` | 5–300 seconds, `off` | inactive |
`get powersaving.test` shows the current controls; `set powersaving.test reset`
restores their defaults. Sleep on permits existing power-saving decisions; the
saved device power-saving setting and every normal transport, interrupt and
radio deadline guard still apply. Settle changes only when profile sleep is
allowed, leaving radio calibration timing unchanged.
UART quiet is available only for an ordinary hardware UART console, such as
the V3's external USB bridge. After draining its acknowledgement, it stops the
UART driver and automatically restores the console at 115200 baud after the
chosen interval. USB logging and OTA must be off. Native USB boards reject
this command. A reboot also restores the console.
To separate clock speed from CPU sleep, hold the clock at 80 or 160 MHz and
compare sleep off/on at the same RF settings. Use matching transmitter and
receiver preambles, unique packets without retries, and diagnostic counters
that prove SDK sleep occurred. Keep every failed packet in the result. These
controls are experimental tools; a reported SDK wait is not a current reading
or proof of a loss-free radio link.
## Noise-floor sampling and sleep
Startup still collects a complete baseline promptly: 64 accepted samples,
+34 -1
View File
@@ -4,6 +4,9 @@
#include <new>
#include "DataStore.h"
#include "PrefsStorageLayout.h"
#if defined(ESP32_PLATFORM)
#include <helpers/esp32/CpuClockPolicy.h>
#endif
#if defined(RAK_4631)
#include <helpers/PeripheralModePrefs.h>
#endif
@@ -1253,6 +1256,14 @@ bool DataStore::loadPrefsInt(const char *filename,
+ mesh::companion::timezone::FooterSize;
const uint32_t prefs_size = file.size();
bool known_size = prefs_size == TIMEZONE_PREFS_SIZE;
#if defined(ESP32_PLATFORM)
const bool has_cpu_footer = prefs_size == TIMEZONE_PREFS_SIZE
+ mesh::esp32::cpuclock::FooterSize
|| prefs_size == TIMEZONE_PREFS_SIZE - mesh::companion::timezone::FooterSize
+ mesh::esp32::cpuclock::FooterSize;
known_size = known_size || has_cpu_footer;
loaded_prefs.cpu_freq_mhz = 0;
#endif
for (size_t i = 0;
i < sizeof(KNOWN_PREFS_SIZES) / sizeof(KNOWN_PREFS_SIZES[0]); i++) {
if (prefs_size == KNOWN_PREFS_SIZES[i]) {
@@ -1437,7 +1448,11 @@ bool DataStore::loadPrefsInt(const char *filename,
if (prefs_size >= TIMEZONE_PREFS_SIZE - mesh::companion::timezone::FooterSize) {
uint8_t footers[mesh::companion::traffic::FooterSize
+ mesh::companion::timezone::FooterSize];
const bool fractional = prefs_size == TIMEZONE_PREFS_SIZE;
const bool fractional = prefs_size == TIMEZONE_PREFS_SIZE
#if defined(ESP32_PLATFORM)
|| prefs_size == TIMEZONE_PREFS_SIZE + mesh::esp32::cpuclock::FooterSize
#endif
;
const size_t length = fractional ? sizeof(footers)
: mesh::companion::traffic::FooterSize;
success = success && file.read(footers, length) == length
@@ -1448,6 +1463,15 @@ bool DataStore::loadPrefsInt(const char *filename,
footers + mesh::companion::traffic::FooterSize,
mesh::companion::timezone::FooterSize, loaded_prefs.tz_offset));
}
#if defined(ESP32_PLATFORM)
if (has_cpu_footer) {
uint8_t cpu_footer[mesh::esp32::cpuclock::FooterSize] = {};
const size_t length = file.read(cpu_footer, sizeof(cpu_footer));
// A bad CPU selection must not discard valid identity/radio preferences.
// Missing or corrupt CPU metadata alone falls back to Auto.
mesh::esp32::cpuclock::decodeFooter(cpu_footer, length, loaded_prefs.cpu_freq_mhz);
}
#endif
// Any bytes left over form only part of a historically appended field.
// Preserve the file and defaults rather than treating that tail as EOF.
@@ -1692,6 +1716,15 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
#else
success = success && file.write(footers, footer_length) == footer_length;
#endif
#if defined(ESP32_PLATFORM)
// Auto is represented by the legacy image without a CPU footer. Saving
// Auto removes an earlier explicit choice without changing old offsets.
if (_prefs.cpu_freq_mhz && mesh::esp32::cpuclock::validSelection(_prefs.cpu_freq_mhz)) {
uint8_t cpu_footer[mesh::esp32::cpuclock::FooterSize];
mesh::esp32::cpuclock::encodeFooter(cpu_footer, _prefs.cpu_freq_mhz);
success = success && file.write(cpu_footer, sizeof(cpu_footer)) == sizeof(cpu_footer);
}
#endif
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) || defined(ESP32_PLATFORM) || defined(RP2040_PLATFORM)
success = file.commit(success);
if (!success) MESH_DEBUG_PRINTLN("DataStore: atomic preferences write failed");
+28
View File
@@ -1,4 +1,8 @@
#include <helpers/RegulatorSettings.h>
#if defined(ESP32_PLATFORM)
#include <helpers/esp32/CpuClockPolicy.h>
#include <helpers/esp32/PowerTestControls.h>
#endif
#include <helpers/LedSettings.h>
#include <helpers/ui/DisplayPowerSettings.h>
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS)
@@ -3177,6 +3181,11 @@ void MyMesh::begin(bool has_display, bool radio_available,
_prefs.radio_fem_txgain = constrain(_prefs.radio_fem_txgain, 0, 1);
_prefs.rx_powersaving_enabled = constrain(_prefs.rx_powersaving_enabled, 0, 1);
_prefs.powersaving_enabled = constrain(_prefs.powersaving_enabled, 0, 1);
#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;
#endif
_prefs.wifi_enabled = constrain(_prefs.wifi_enabled, 0, 1);
_prefs.usb_logging_enabled = constrain(_prefs.usb_logging_enabled, 0, 2);
_prefs.usb_debug_enabled = _prefs.usb_debug_enabled == 1 ? 1 : 0;
@@ -3869,6 +3878,12 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
}
#endif
if (mesh::powerdiag::handleCommand(command, reply, reply_size, millis())) return true;
#if defined(ESP32_PLATFORM)
if (mesh::esp32::cpuclock::handleCommand(board, _prefs.cpu_freq_mhz,
_prefs.powersaving_enabled != 0, command, reply, reply_size,
[this]() { return savePrefs(); })) return true;
if (mesh::esp32::handlePowerTestCommand(command, reply, reply_size, true)) return true;
#endif
#if defined(OTA_RAK15002_SD_STORE) && defined(ETHERNET_USE_RAK13800)
if (handleRak4631PeripheralCommand(command, reply, reply_size)) return true;
#endif
@@ -5144,6 +5159,13 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.power_saving = _prefs.powersaving_enabled;
#if defined(ESP32_PLATFORM)
s.cpu_freq_mhz = _prefs.cpu_freq_mhz;
s.cpu_actual_mhz = board.getCpuFrequencyMHz();
s.cpu_supported_mask = (board.supportsCpuFrequencyMHz(80) ? 1 : 0)
| (board.supportsCpuFrequencyMHz(160) ? 2 : 0)
| (board.supportsCpuFrequencyMHz(240) ? 4 : 0);
#endif
#if MESH_ENABLE_LOST_REPLY
s.lost_reply = _prefs.lost_reply;
#endif
@@ -10772,6 +10794,12 @@ bool MyMesh::handleCommand(const char* command, uint32_t sender_timestamp,
while (*command == ' ' || *command == '\t') command++;
}
#if defined(ESP32_PLATFORM)
// Catch remote requests before the direct-transport router below. That
// router is also used by USB/BLE/browser commands without a timestamp.
if (mesh::esp32::handlePowerTestCommand(command, reply, reply_capacity,
sender_timestamp == 0)) return true;
#endif
if (mesh::power::handleRegulatorCommand(command, reply, reply_capacity)) return true;
#if defined(WIO_E5_MINI_HEADLESS_COMPANION_UI)
if (handleCompactLedCommand(command, reply, reply_capacity)) return true;
+6
View File
@@ -12,6 +12,9 @@
#include <helpers/DynamicConfigSerializer.h>
#include <helpers/CompanionTrafficPrefs.h>
#include <helpers/CompanionTimezonePrefs.h>
#if defined(ESP32_PLATFORM)
#include <helpers/esp32/CpuClockPolicy.h>
#endif
#include "BluetoothName.h"
@@ -130,6 +133,9 @@ public:
uint8_t peripherals_shared = 0; // applied at boot, not on a live CLI save
#endif
// Checked footer after all previously published fields/board footers.
#if defined(ESP32_PLATFORM)
uint16_t cpu_freq_mhz = 0; // 0=Auto; ESP32-only CPU footer, no legacy offset moves
#endif
uint8_t tx_media = mesh::RADIO_TX_AUTO;
uint8_t tx_ssh = mesh::RADIO_TX_AUTO;
+30 -46
View File
@@ -539,18 +539,10 @@ static void serviceCompanionRadioRecovery() {
#ifdef ESP32_PLATFORM
static int8_t applied_power_saving = -1;
static int8_t attempted_power_saving = -1;
static uint16_t applied_cpu_frequency = UINT16_MAX;
static uint16_t attempted_cpu_frequency = UINT16_MAX;
static unsigned long power_saving_retry_at = 0;
static uint32_t companionNominalCpuMhz() {
#ifdef ESP32_POST_BOOT_CPU_FREQ
return ESP32_POST_BOOT_CPU_FREQ;
#elif defined(ESP32_CPU_FREQ)
return ESP32_CPU_FREQ;
#else
return F_CPU / 1000000UL;
#endif
}
#if COMPANION_BT_MODEM_SLEEP_AVAILABLE
static void applyCompanionBluetoothSleep(bool enabled,
bool allow_uninitialized) {
@@ -568,11 +560,17 @@ static void applyCompanionBluetoothSleep(bool enabled,
#endif
static bool applyCompanionPowerSaving(bool enabled) {
const uint32_t nominal_mhz = companionNominalCpuMhz();
const uint32_t max_mhz = enabled && nominal_mhz > 80 ? 80 : nominal_mhz;
const uint16_t selected = the_mesh.getNodePrefs()->cpu_freq_mhz;
uint32_t max_mhz = mesh::esp32::cpuclock::resolveMHz(selected, enabled, true);
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 = enabled && max_mhz > 40 ? 40 : max_mhz;
const uint32_t min_mhz = !selected && enabled && max_mhz > 40 ? 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.
@@ -581,35 +579,6 @@ static bool applyCompanionPowerSaving(bool enabled) {
#else
const bool automatic_light_sleep = enabled;
#endif
#if CONFIG_IDF_TARGET_ESP32C3
esp_pm_config_esp32c3_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32S3
esp_pm_config_esp32s3_t pm_config;
#elif CONFIG_IDF_TARGET_ESP32
esp_pm_config_esp32_t pm_config;
#else
esp_pm_config_t pm_config;
#endif
pm_config.max_freq_mhz = max_mhz;
pm_config.min_freq_mhz = min_mhz;
pm_config.light_sleep_enable = automatic_light_sleep;
esp_err_t pm_result = esp_pm_configure(&pm_config);
if (pm_result != ESP_OK) {
mesh::usbLoggingPort().printf("Device power saving failed: %s\r\n",
esp_err_to_name(pm_result));
return false;
}
#else
// Arduino's prebuilt ESP-IDF normally has CONFIG_PM_ENABLE disabled, in
// which case esp_pm_configure() is a stub returning ESP_ERR_NOT_SUPPORTED.
// Keep frequency throttling functional instead of retrying that stub.
if (!setCpuFrequencyMhz(max_mhz)) {
mesh::usbLoggingPort().printf(
"Device power saving failed: CPU %lu MHz is unsupported\r\n",
(unsigned long)max_mhz);
return false;
}
#endif
#if COMPANION_BT_MODEM_SLEEP_AVAILABLE
@@ -636,18 +605,21 @@ static bool applyCompanionPowerSaving(bool enabled) {
static void serviceCompanionPowerSaving(bool force = false) {
const int8_t requested = the_mesh.getNodePrefs()->powersaving_enabled ? 1 : 0;
if (!force && applied_power_saving == requested) return;
const uint16_t selected = the_mesh.getNodePrefs()->cpu_freq_mhz;
if (!force && applied_power_saving == requested && applied_cpu_frequency == selected) return;
const unsigned long now = millis();
if (!force && attempted_power_saving == requested
if (!force && attempted_power_saving == requested && attempted_cpu_frequency == selected
&& power_saving_retry_at != 0
&& (int32_t)(now - power_saving_retry_at) < 0) {
return;
}
attempted_power_saving = requested;
attempted_cpu_frequency = selected;
if (applyCompanionPowerSaving(requested != 0)) {
applied_power_saving = requested;
applied_cpu_frequency = selected;
power_saving_retry_at = 0;
} else {
power_saving_retry_at = now + 5000;
@@ -3571,6 +3543,9 @@ void setup() {
mesh::serialLogBegin();
#endif
mesh::beginUsbLoggingPort();
#if defined(ESP32_PLATFORM)
board.setCompanionCpuClockPolicy(true);
#endif
board.begin();
#if defined(ESP32) && defined(WIFI_SSID)
@@ -4278,6 +4253,7 @@ void loop() {
#endif
#ifdef ESP32_PLATFORM
board.servicePowerTestControls(!interface_manager.hasPendingIO());
serviceCompanionPowerSaving();
#endif
#if defined(NRF52_PLATFORM)
@@ -4293,6 +4269,9 @@ void loop() {
// actually vetoed sleep, without polling otherwise skipped peripherals.
mesh::powerdiag::Mask sleep_reason = [&]() -> mesh::powerdiag::Mask {
if (!the_mesh.getNodePrefs()->powersaving_enabled) return mask(Reason::PolicyOff);
#if defined(ESP32_PLATFORM)
if (!mesh::esp32::manualCpuSleepEnabled()) return mask(Reason::PolicyOff);
#endif
if (morse_timing) return mask(Reason::Morse);
const auto pending = the_mesh.pendingWorkReason();
if (pending) return pending;
@@ -4330,8 +4309,13 @@ void loop() {
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(sleep_reason, millis());
const bool can_sleep = sleep_reason == 0;
if (can_sleep && the_mesh.canIdleDualRadio()) {
const uint32_t idle_slice_ms = noise_floor_wait && noise_floor_delay_ms < 10
? noise_floor_delay_ms : 10;
#if defined(ESP32_PLATFORM)
const uint32_t maximum_slice_ms = mesh::esp32::profileSleepSliceMillis();
#else
const uint32_t maximum_slice_ms = 10;
#endif
const uint32_t idle_slice_ms = noise_floor_wait && noise_floor_delay_ms < maximum_slice_ms
? noise_floor_delay_ms : maximum_slice_ms;
#if defined(NRF52_PLATFORM)
if (noise_floor_wait) board.sleepMilliseconds(idle_slice_ms);
else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
+7
View File
@@ -11410,6 +11410,13 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.power_saving = _prefs.powersaving_enabled;
#if defined(ESP32_PLATFORM)
s.cpu_freq_mhz = _prefs.cpu_freq_mhz;
s.cpu_actual_mhz = board.getCpuFrequencyMHz();
s.cpu_supported_mask = (board.supportsCpuFrequencyMHz(80) ? 1U : 0U)
| (board.supportsCpuFrequencyMHz(160) ? 2U : 0U)
| (board.supportsCpuFrequencyMHz(240) ? 4U : 0U);
#endif
s.mqtt_enabled = _prefs.bridge_enabled != 0;
s.repeat = !_prefs.disable_fwd;
// Editable web configuration must retain the saved intervals during tempradio.
+5
View File
@@ -655,6 +655,11 @@ void loop() {
#endif
// Report the guard that really stopped this iteration, without probing
// later guards or changing their short-circuit hardware access.
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis());
#endif
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
if (!allowed && mesh::powerdiag::enabled())
mesh::powerdiag::decision(mesh::powerdiag::mask(reason), millis());
+7
View File
@@ -3194,6 +3194,13 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.rx_ps_rx_us = _prefs.rx_ps_rx_us;
s.rx_ps_sleep_us = _prefs.rx_ps_sleep_us;
s.power_saving = _prefs.powersaving_enabled;
#if defined(ESP32_PLATFORM)
s.cpu_freq_mhz = _prefs.cpu_freq_mhz;
s.cpu_actual_mhz = board.getCpuFrequencyMHz();
s.cpu_supported_mask = (board.supportsCpuFrequencyMHz(80) ? 1U : 0U)
| (board.supportsCpuFrequencyMHz(160) ? 2U : 0U)
| (board.supportsCpuFrequencyMHz(240) ? 4U : 0U);
#endif
s.mqtt_enabled = _prefs.bridge_enabled != 0;
s.repeat = !_prefs.disable_fwd;
s.advert_interval = _prefs.advert_interval * 2;
+5
View File
@@ -429,6 +429,11 @@ void loop() {
#if defined(NRF52_PLATFORM)
if (the_mesh.getNodePrefs()->powersaving_enabled && the_mesh.isDualRadioActive()
&& !morse_timing) board.prepareProfileIdleCycle();
#endif
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis());
#endif
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
if (!allowed && mesh::powerdiag::enabled())
+5
View File
@@ -390,6 +390,11 @@ void loop() {
&& the_mesh.isDualRadioActive() && !morse_timing) board.prepareProfileIdleCycle();
#endif
#if defined(ESP32_PLATFORM)
board.servicePowerTestControls();
mesh::esp32::serviceCpuFrequencyPolicy(board, the_mesh.getNodePrefs()->cpu_freq_mhz,
the_mesh.getNodePrefs()->powersaving_enabled != 0, millis());
#endif
const auto sleep_guard = [](bool allowed, mesh::powerdiag::Reason reason) {
if (!allowed && mesh::powerdiag::enabled())
mesh::powerdiag::decision(mesh::powerdiag::mask(reason), millis());
+7
View File
@@ -148,6 +148,13 @@ public:
virtual void onAfterTransmit() { }
// Update only indicator outputs; transmit hooks may also control RF hardware.
virtual void applyLedPolicy() { }
#if defined(ESP32_PLATFORM)
// CPU clock selection is ESP32-only; other platforms retain their current
// 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 bool applyCpuFrequencyMHz(uint16_t, bool) { return false; }
#endif
virtual void reboot() = 0;
MESH_REGULATOR_VIRTUAL uint8_t mcuRegulatorModes() const { return 0; }
MESH_REGULATOR_VIRTUAL power::RegulatorMode defaultMcuRegulatorMode() const {
+38
View File
@@ -9,6 +9,10 @@
#include "UsbLoggingWatchdog.h"
#include "PowerSavingDiagnostics.h"
#include "HilStartupTrace.h"
#if defined(ESP32_PLATFORM)
#include <helpers/esp32/CpuClockPolicy.h>
#include <helpers/esp32/PowerTestControls.h>
#endif
#include <helpers/ui/DisplayPowerSettings.h>
#if defined(OLED_AUTO_DETECT) && OLED_AUTO_DETECT && defined(DISPLAY_CLASS)
#include <helpers/ui/OLEDControllerSettings.h>
@@ -1100,6 +1104,9 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->gps_sync_interval_hours = 0;
_prefs->ota_channel = 0;
_prefs->rs232_bridge_enabled = 0;
#if defined(ESP32_PLATFORM)
_prefs->cpu_freq_mhz = 0;
#endif
#if defined(RAK_4631)
// Failure to read an existing policy cannot authorize a shared-rail cut.
_prefs->peripherals_shared = 1;
@@ -1803,6 +1810,11 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
// Read the footer independently of optional legacy UART tails/profile.
#if defined(RAK_4631)
_prefs->peripherals_shared = mesh::peripherals::readCommonMode(file);
#endif
#if defined(ESP32_PLATFORM)
_prefs->cpu_freq_mhz = mesh::esp32::cpuclock::readFooter(file);
if (_prefs->cpu_freq_mhz && !_board->supportsCpuFrequencyMHz(_prefs->cpu_freq_mhz))
_prefs->cpu_freq_mhz = 0;
#endif
file.close();
}
@@ -1974,6 +1986,13 @@ static bool writeCommonPrefsImage(Writer& writer, NodePrefs* prefs) {
mesh::peripherals::encodeFooter(peripheral_footer, prefs->peripherals_shared);
WRITE_COMMON_PREFS_BYTES(peripheral_footer, sizeof(peripheral_footer));
#endif
#if defined(ESP32_PLATFORM)
if (prefs->cpu_freq_mhz && mesh::esp32::cpuclock::validSelection(prefs->cpu_freq_mhz)) {
uint8_t cpu_footer[mesh::esp32::cpuclock::FooterSize];
mesh::esp32::cpuclock::encodeFooter(cpu_footer, prefs->cpu_freq_mhz);
WRITE_COMMON_PREFS_BYTES(cpu_footer, sizeof(cpu_footer));
}
#endif
#undef WRITE_COMMON_PREFS_BYTES
#undef WRITE_COMMON_PREFS
@@ -2057,6 +2076,11 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
_common_save_succeeded = false;
_prefs->usb_debug_enabled = _prefs->usb_debug_enabled == 1 ? 1 : 0;
_prefs->trace_when_repeat_off = _prefs->trace_when_repeat_off == 1 ? 1 : 0;
#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;
#endif
#ifdef WITH_RS232_BRIDGE
mesh::bridge::rs232power::sanitize(_prefs->rs232_heartbeat,
_prefs->rs232_autosleep, _prefs->rs232_idle_seconds);
@@ -2213,6 +2237,13 @@ void CommonCLI::savePrefs(FILESYSTEM* fs, PrefsSaveRouting::Scope scope) {
mesh::peripherals::encodeFooter(peripheral_footer, _prefs->peripherals_shared);
file.write(peripheral_footer, sizeof(peripheral_footer));
#endif
#if defined(ESP32_PLATFORM)
if (_prefs->cpu_freq_mhz && mesh::esp32::cpuclock::validSelection(_prefs->cpu_freq_mhz)) {
uint8_t cpu_footer[mesh::esp32::cpuclock::FooterSize];
mesh::esp32::cpuclock::encodeFooter(cpu_footer, _prefs->cpu_freq_mhz);
file.write(cpu_footer, sizeof(cpu_footer));
}
#endif
_common_save_succeeded = file.commit();
if (!_common_save_succeeded) {
@@ -3033,6 +3064,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
if (mesh::leds::handleCommand(command, reply, 157)) return;
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
if (mesh::powerdiag::handleCommand(command, reply, 157, millis())) return;
#if defined(ESP32_PLATFORM)
if (mesh::esp32::cpuclock::handleCommand(*_board, _prefs->cpu_freq_mhz,
_prefs->powersaving_enabled != 0, command, reply, 157,
[this]() { return saveCommonPrefs(); })) return;
if (mesh::esp32::handlePowerTestCommand(command, reply, 157,
sender_timestamp == 0)) return;
#endif
if (handleManagementCommand(command, reply)) return;
#if MESH_ENABLE_TX_BEACONS
if (handleBeaconCommand(command, reply, 157)) return;
+6
View File
@@ -162,6 +162,9 @@ public:
char bridge_secret[16] = {}; // for XOR encryption of bridge packets (ESP-NOW only)
// Power setting
uint8_t powersaving_enabled = DEFAULT_POWERSAVING_ENABLED ? 1 : 0; // boolean
#if defined(ESP32_PLATFORM)
uint16_t cpu_freq_mhz = 0; // 0=Auto; checked ESP32-only append-only footer
#endif
uint8_t reboot_interval = 0; // hours, 0-255 (default 0=disable)
// Gps settings
uint8_t gps_enabled = 0;
@@ -398,6 +401,9 @@ private:
void structure() override {
def("adc_mult", _parent->adc_multiplier);
def("pwr_sav_en", _parent->powersaving_enabled);
#if defined(ESP32_PLATFORM)
def("cpu_freq", _parent->cpu_freq_mhz);
#endif
}
public:
+169 -2
View File
@@ -21,6 +21,12 @@
#include "UsbHostSleepPolicy.h"
#include "PowerSavingDiagnostics.h"
#include "esp32/LightSleepPolicy.h"
#include "esp32/PowerTestControls.h"
#include "esp32/CpuClockPolicy.h"
#include <esp_pm.h>
#if CONFIG_IDF_TARGET_ESP32
#include <soc/efuse_reg.h>
#endif
#if defined(STATION_G3_ESP32)
#include <StationG3RadioSlot.h>
#endif
@@ -47,6 +53,10 @@ protected:
bool inhibit_sleep = false;
bool ota_started_ap = false;
bool ota_started_radio = false;
bool companion_cpu_clock_policy = false;
uint32_t uart_quiet_start_at = 0;
uint32_t uart_quiet_restore_at = 0;
uint16_t uart_quiet_seconds = 0;
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::UsbHostSleepPolicy usb_host_sleep_policy;
#endif
@@ -76,6 +86,12 @@ public:
// for future use, sub-classes SHOULD call this from their begin()
startup_reason = BD_STARTUP_NORMAL;
mesh::esp32::registerUartQuietControl(this,
[](void* owner, mesh::esp32::UartQuietAction action, uint16_t seconds,
char* reply, size_t capacity) {
return static_cast<ESP32Board*>(owner)->handleUartQuiet(action, seconds, reply, capacity);
});
#ifdef ESP32_CPU_FREQ
setCpuFrequencyMhz(ESP32_CPU_FREQ);
#endif
@@ -104,6 +120,137 @@ public:
void attachDynamicPrefs(KeyValueStore* prefs) { (void)prefs; } // no-op
void setCompanionCpuClockPolicy(bool enabled) { companion_cpu_clock_policy = enabled; }
uint16_t getCpuFrequencyMHz() const override { return ::getCpuFrequencyMhz(); }
bool supportsCpuFrequencyMHz(uint16_t mhz) const override {
#if CONFIG_IDF_TARGET_ESP32H2
(void)mhz;
return false;
#else
if (mhz == 80) return true;
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3 \
|| CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6
if (mhz == 160) return true;
#endif
#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
if (mhz == 240) {
#if CONFIG_IDF_TARGET_ESP32
if (REG_GET_BIT(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_CPU_FREQ_RATED)
&& REG_GET_BIT(EFUSE_BLK0_RDATA3_REG, EFUSE_RD_CHIP_CPU_FREQ_LOW)) return false;
#endif
return true;
}
#endif
return false;
#endif
}
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;
#if defined(CONFIG_PM_ENABLE) && CONFIG_PM_ENABLE
const uint16_t minimum = !selected && companion_cpu_clock_policy && powersaving && maximum > 40
? 40 : maximum;
#if CONFIG_IDF_TARGET_ESP32C3
esp_pm_config_esp32c3_t config;
#elif CONFIG_IDF_TARGET_ESP32S3
esp_pm_config_esp32s3_t config;
#elif CONFIG_IDF_TARGET_ESP32
esp_pm_config_esp32_t config;
#else
esp_pm_config_t config;
#endif
config.max_freq_mhz = maximum;
config.min_freq_mhz = minimum;
#if (defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT) || defined(ENABLE_USB_INTERFACE)
config.light_sleep_enable = false;
#else
config.light_sleep_enable = companion_cpu_clock_policy && powersaving;
#endif
if (esp_pm_configure(&config) != ESP_OK) return false;
#else
if (!setCpuFrequencyMhz(maximum)) return false;
#endif
return !selected || getCpuFrequencyMHz() == selected;
}
bool handleUartQuiet(mesh::esp32::UartQuietAction action, uint16_t seconds,
char* reply, size_t capacity) {
#if (defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT) \
|| defined(SERIAL_RX) || defined(SERIAL_TX)
(void)seconds;
const bool harmless = action != mesh::esp32::UartQuietAction::Schedule;
snprintf(reply, capacity, harmless ? "uart=unsupported"
: "Error: UART quiet requires a default hardware UART console");
return harmless;
#else
if (action == mesh::esp32::UartQuietAction::Cancel) {
uart_quiet_start_at = 0;
if (uart_quiet_restore_at) {
Serial.begin(115200); // all four roles start this same default console
if (!Serial) {
uart_quiet_restore_at = millis() + 1000;
if (!uart_quiet_restore_at) uart_quiet_restore_at = 1;
snprintf(reply, capacity, "Error: UART restore failed; retrying automatically");
return false;
}
uart_quiet_restore_at = 0;
}
uart_quiet_seconds = 0;
} else if (action == mesh::esp32::UartQuietAction::Schedule) {
if (seconds < 5 || seconds > 300 || uart_quiet_start_at || uart_quiet_restore_at
|| !Serial || inhibit_sleep || isOTAUpdateRunning() || isRadioTestActive()
|| mesh::isUsbLoggingEnabled()) {
snprintf(reply, capacity, "Error: UART quiet needs an idle console, logging/OTA off, and 5..300 seconds");
return false;
}
uart_quiet_seconds = seconds;
uart_quiet_start_at = millis() + 100;
if (!uart_quiet_start_at) uart_quiet_start_at = 1;
snprintf(reply, capacity, "OK - UART quiet in 100ms after replies drain, for %us; automatic restore", unsigned(seconds));
return true;
}
const int32_t remaining = static_cast<int32_t>(uart_quiet_restore_at - millis());
snprintf(reply, capacity, "uart=%s remaining_s=%lu", uart_quiet_start_at ? "armed"
: uart_quiet_restore_at ? "quiet" : "active", static_cast<unsigned long>(
uart_quiet_start_at ? uart_quiet_seconds : uart_quiet_restore_at && remaining > 0
? (uint32_t(remaining) + 999) / 1000 : 0));
return true;
#endif
}
// Call after the role's reply pump, and regardless of its sleep decision.
// A quiet console is always restored by a timer; no permanent guard bypass.
void servicePowerTestControls(bool replies_drained = true) {
#if (!defined(ARDUINO_USB_CDC_ON_BOOT) || !ARDUINO_USB_CDC_ON_BOOT) \
&& !defined(SERIAL_RX) && !defined(SERIAL_TX)
char ignored[96];
const uint32_t now = millis();
if (uart_quiet_restore_at && (static_cast<int32_t>(now - uart_quiet_restore_at) >= 0
|| inhibit_sleep || isOTAUpdateRunning() || isRadioTestActive() || mesh::isUsbLoggingEnabled())) {
handleUartQuiet(mesh::esp32::UartQuietAction::Cancel, 0, ignored, sizeof(ignored));
}
if (!uart_quiet_start_at || static_cast<int32_t>(now - uart_quiet_start_at) < 0) return;
if (static_cast<int32_t>(now - uart_quiet_start_at) >= 1900
|| inhibit_sleep || isOTAUpdateRunning() || isRadioTestActive() || mesh::isUsbLoggingEnabled()) {
uart_quiet_start_at = 0; uart_quiet_seconds = 0;
return;
}
if (!replies_drained || mesh::hasPendingUsbTerminalOutput()) return;
Serial.flush(); // drain the hardware FIFO containing the acknowledgement
Serial.end();
uart_quiet_restore_at = millis() + uint32_t(uart_quiet_seconds) * 1000;
if (!uart_quiet_restore_at) uart_quiet_restore_at = 1;
uart_quiet_start_at = 0;
#else
(void)replies_drained;
#endif
}
// Temperature from ESP32 MCU
float getMCUTemperature() override {
uint32_t raw = 0;
@@ -152,12 +299,32 @@ public:
bool has_deadline, uint32_t deadline_us) {
using mesh::powerdiag::Reason;
const bool diagnostics = mesh::powerdiag::enabled();
// Preserve native-host signal history even while a test disables sleep.
const bool usb_sleep_held = isUsbSleepHeld();
if (!mesh::esp32::manualCpuSleepEnabled()) {
if (diagnostics) mesh::powerdiag::boardVeto(Reason::PolicyOff, millis());
return;
}
// A temporary UART quiet interval must not sleep past console recovery.
// This only shortens the request and leaves every normal wake guard live.
if (uart_quiet_restore_at) {
const int32_t remaining_ms = static_cast<int32_t>(uart_quiet_restore_at - millis());
if (remaining_ms <= 0) return;
const uint32_t quiet_us = uint32_t(remaining_ms) * 1000UL;
if (has_deadline) {
const uint32_t now_us = micros();
if (static_cast<int32_t>(deadline_us - now_us) > static_cast<int32_t>(quiet_us))
deadline_us = now_us + quiet_us;
} else {
if (!wake_us || wake_us > quiet_us) wake_us = quiet_us;
require_timer = true;
}
}
// Native USB loses its connection in light sleep. An enumerated host
// still needs USB serviced when no terminal asserts CDC DTR (for example,
// after a Pi closes its serial port). Guard every caller here.
// Sample even when another blocker is active, so an open console/OTA does
// not leave the last positive native USB host signal stale.
const bool usb_sleep_held = isUsbSleepHeld();
Reason veto_reason = Reason::Board;
int32_t veto_detail = 1; // Explicit inhibit_sleep.
const auto hold = [&veto_reason, &veto_detail](bool active, Reason reason, int32_t detail = 0) {
@@ -276,7 +443,7 @@ public:
// Keep this immediately before the SDK wait with local interrupts held.
// A short/rejected slice polls instead; it must not reuse an older timer.
const int32_t remaining_us = static_cast<int32_t>(deadline_us - micros());
if (remaining_us < 2000) deadline_expired = true;
if (remaining_us < static_cast<int32_t>(mesh::esp32::profileSleepMinimumMicros())) deadline_expired = true;
else gpio_error = esp_sleep_enable_timer_wakeup(static_cast<uint32_t>(remaining_us));
}
esp_err_t sleep_error = ESP_OK;
+1 -1
View File
@@ -20,7 +20,7 @@ static const char* const WC_ALLOWED_SET_KEYS[] = {
// NodePrefs (radio / node)
"name", "bluetooth.name", "bluetooth.mac", "bluetooth.stealth", "lost.reply",
"lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain", "radio.rxps", "powersaving",
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain", "radio.rxps", "powersaving", "cpu.freq",
"mcu.regulator", "radio.regulator",
"repeat",
"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout",
+156
View File
@@ -0,0 +1,156 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
namespace mesh {
namespace esp32 {
namespace cpuclock {
// Zero retains each role's established automatic clock policy. Explicit
// selections are deliberately limited to PLL clocks supported by this UI;
// the board must additionally validate the actual chip/SDK capability.
inline bool validSelection(uint16_t mhz) {
return mhz == 0 || mhz == 80 || mhz == 160 || mhz == 240;
}
inline uint16_t nominalMHz() {
#if defined(ESP32_POST_BOOT_CPU_FREQ)
return ESP32_POST_BOOT_CPU_FREQ;
#elif defined(ESP32_CPU_FREQ)
return ESP32_CPU_FREQ;
#elif defined(F_CPU)
return F_CPU / 1000000UL;
#else
return 80;
#endif
}
inline uint16_t resolveMHz(uint16_t selected, bool powersaving, bool companion) {
if (selected && validSelection(selected)) return selected;
const uint16_t nominal = nominalMHz();
return companion && powersaving && nominal > 80 ? 80 : nominal;
}
static constexpr size_t FooterSize = 8;
inline uint16_t footerChecksum(const uint8_t* bytes, size_t length) {
uint16_t crc = 0xffff;
for (size_t i = 0; i < length; ++i) {
crc ^= static_cast<uint16_t>(bytes[i]) << 8;
for (unsigned bit = 0; bit < 8; ++bit)
crc = static_cast<uint16_t>((crc & 0x8000) ? (crc << 1) ^ 0x1021 : crc << 1);
}
return crc;
}
inline void encodeFooter(uint8_t* out, uint16_t selected) {
if (!validSelection(selected)) selected = 0;
out[0] = 'C'; out[1] = 'P'; out[2] = 'U'; out[3] = 1;
out[4] = static_cast<uint8_t>(selected >> 8);
out[5] = static_cast<uint8_t>(selected);
const uint16_t crc = footerChecksum(out, 6);
out[6] = static_cast<uint8_t>(crc >> 8);
out[7] = static_cast<uint8_t>(crc);
}
// A missing, truncated, foreign or corrupt footer always selects Auto, even
// when loading into an object which previously held an explicit selection.
inline bool decodeFooter(const uint8_t* in, size_t length, uint16_t& selected) {
selected = 0;
if (!in || length != FooterSize || in[0] != 'C' || in[1] != 'P'
|| in[2] != 'U' || in[3] != 1) return false;
const uint16_t value = static_cast<uint16_t>((in[4] << 8) | in[5]);
const uint16_t crc = static_cast<uint16_t>((in[6] << 8) | in[7]);
if (!validSelection(value) || footerChecksum(in, 6) != crc) return false;
selected = value;
return true;
}
template <typename File>
uint16_t readFooter(File& file) {
uint16_t selected = 0;
uint8_t bytes[FooterSize] = {};
if (file.size() >= FooterSize && file.seek(file.size() - FooterSize))
decodeFooter(bytes, file.read(bytes, sizeof(bytes)), selected);
return selected;
}
inline bool parseSelection(const char* value, uint16_t& selected) {
if (!value) return false;
while (*value == ' ' || *value == '\t') ++value;
const char* end = value + strlen(value);
while (end > value && (end[-1] == ' ' || end[-1] == '\t')) --end;
const size_t length = static_cast<size_t>(end - value);
if (length == 4 && memcmp(value, "auto", 4) == 0) selected = 0;
else if (length == 2 && memcmp(value, "80", 2) == 0) selected = 80;
else if (length == 3 && memcmp(value, "160", 3) == 0) selected = 160;
else if (length == 3 && memcmp(value, "240", 3) == 0) selected = 240;
else return false;
return true;
}
template <typename Board>
void formatStatus(Board& board, uint16_t selected, char* reply, size_t capacity,
const char* prefix = ">") {
char selection[8] = "auto";
if (selected) snprintf(selection, sizeof(selection), "%u", unsigned(selected));
char supported[24] = "auto";
const uint16_t choices[] = {80, 160, 240};
for (uint16_t mhz : choices) {
if (board.supportsCpuFrequencyMHz(mhz)) {
const size_t used = strlen(supported);
snprintf(supported + used, sizeof(supported) - used, ",%u", unsigned(mhz));
}
}
snprintf(reply, capacity, "%s selected=%s actual=%uMHz supported=%s", prefix,
selection, unsigned(board.getCpuFrequencyMHz()), supported);
}
template <typename Board, typename Save>
bool handleCommand(Board& board, uint16_t& selected, bool powersaving,
const char* command, char* reply, size_t capacity, Save save) {
if (!command || !reply || !capacity) return false;
const bool get = strncmp(command, "get cpu.freq", 12) == 0;
const bool set = strncmp(command, "set cpu.freq", 12) == 0;
if ((!get && !set) || (command[12] && command[12] != ' ' && command[12] != '\t'))
return false;
const char* value = command + 12;
while (*value == ' ' || *value == '\t') ++value;
if (get) {
if (*value) snprintf(reply, capacity, "Error: usage get cpu.freq");
else formatStatus(board, selected, reply, capacity);
return true;
}
uint16_t wanted = 0;
if (!parseSelection(value, wanted)) {
snprintf(reply, capacity, "Error: use set cpu.freq auto|80|160|240");
return true;
}
if (wanted && !board.supportsCpuFrequencyMHz(wanted)) {
snprintf(reply, capacity, "Error: unsupported CPU clock; use get cpu.freq");
return true;
}
const uint16_t previous = selected;
selected = wanted;
if (!board.applyCpuFrequencyMHz(wanted, powersaving)) {
selected = previous;
const bool restored = board.applyCpuFrequencyMHz(previous, powersaving);
snprintf(reply, capacity, restored ? "Error: CPU clock apply failed; unchanged"
: "Error: CPU clock apply and restore failed; check get cpu.freq");
} else if (!save()) {
selected = previous;
const bool restored = board.applyCpuFrequencyMHz(previous, powersaving);
snprintf(reply, capacity, restored ? "Error: CPU clock save failed; restored"
: "Error: CPU clock save and restore failed; check get cpu.freq");
} else {
formatStatus(board, selected, reply, capacity, "OK - saved");
}
return true;
}
} // namespace cpuclock
} // namespace esp32
} // namespace mesh
+146
View File
@@ -0,0 +1,146 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
namespace mesh {
namespace esp32 {
// Local experiments only. Nothing here is persisted; reboot restores the
// production policy. RF parameters and calibration settling are unchanged.
struct PowerTestControls {
bool sleep_enabled = true;
uint8_t slice_ms = 10;
uint8_t guard_ms = 4;
uint8_t minimum_ms = 2;
uint8_t settle_ms = 0; // zero selects the radio family's normal idle settle
};
inline PowerTestControls& powerTestControls() {
static PowerTestControls value;
return value;
}
enum class UartQuietAction : uint8_t { Status, Schedule, Cancel };
using UartQuietHandler = bool (*)(void*, UartQuietAction, uint16_t, char*, size_t);
struct UartQuietControl {
void* owner = nullptr;
UartQuietHandler handle = nullptr;
};
inline UartQuietControl& uartQuietControl() {
static UartQuietControl value;
return value;
}
inline void registerUartQuietControl(void* owner, UartQuietHandler handle) {
uartQuietControl().owner = owner;
uartQuietControl().handle = handle;
}
inline bool manualCpuSleepEnabled() { return powerTestControls().sleep_enabled; }
inline uint32_t profileSleepSliceMillis() { return powerTestControls().slice_ms; }
inline uint32_t profileSleepGuardMicros() { return uint32_t(powerTestControls().guard_ms) * 1000; }
inline uint32_t profileSleepMinimumMicros() { return uint32_t(powerTestControls().minimum_ms) * 1000; }
inline uint32_t profileSleepSettleMillis(uint32_t normal) {
return powerTestControls().settle_ms ? powerTestControls().settle_ms : normal;
}
// Infrastructure has no Bluetooth modem policy, but must honor a saved CPU
// 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) {
struct State {
uint16_t selected = UINT16_MAX;
bool powersaving = false;
uint32_t retry_at = 0;
};
static State state;
if (!force && state.selected == selected && state.powersaving == powersaving) 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;
if (!state.retry_at) state.retry_at = 1;
return false;
}
state.selected = selected;
state.powersaving = powersaving;
state.retry_at = 0;
return true;
}
inline bool powerTestNumber(const char* text, uint16_t minimum, uint16_t maximum, uint16_t& output) {
if (!text || !*text) return false;
uint32_t value = 0;
for (const char* p = text; *p; ++p) {
if (*p < '0' || *p > '9') return false;
value = value * 10 + unsigned(*p - '0');
if (value > maximum) return false;
}
if (value < minimum) return false;
output = uint16_t(value);
return true;
}
inline void formatPowerTestControls(char* reply, size_t capacity) {
const auto& value = powerTestControls();
char settle[8], uart[48] = "uart=unsupported";
if (value.settle_ms) snprintf(settle, sizeof(settle), "%u", unsigned(value.settle_ms));
else strcpy(settle, "auto");
const auto& quiet = uartQuietControl();
if (quiet.handle) quiet.handle(quiet.owner, UartQuietAction::Status, 0, uart, sizeof(uart));
snprintf(reply, capacity, "sleep=%s slice_ms=%u guard_ms=%u min_ms=%u settle_ms=%s %s; RAM only",
value.sleep_enabled ? "on" : "off", unsigned(value.slice_ms), unsigned(value.guard_ms),
unsigned(value.minimum_ms), settle, uart);
}
inline bool handlePowerTestCommand(const char* command, char* reply, size_t capacity, bool local) {
const bool get = strcmp(command, "get powersaving.test") == 0;
const char* arguments = strncmp(command, "set powersaving.test ", 21) == 0 ? command + 21 : nullptr;
if (!get && !arguments && strcmp(command, "set powersaving.test") != 0) return false;
if (!local) {
snprintf(reply, capacity, "Error: powersaving.test is local only");
return true;
}
if (get) { formatPowerTestControls(reply, capacity); return true; }
auto& value = powerTestControls();
if (arguments && strcmp(arguments, "reset") == 0) {
const auto& quiet = uartQuietControl();
if (quiet.handle && !quiet.handle(quiet.owner, UartQuietAction::Cancel, 0, reply, capacity)) return true;
value = PowerTestControls();
} else if (arguments && (strcmp(arguments, "sleep on") == 0 || strcmp(arguments, "sleep off") == 0)) {
value.sleep_enabled = strcmp(arguments, "sleep on") == 0;
} else if (arguments && strncmp(arguments, "uart_quiet ", 11) == 0) {
const auto& quiet = uartQuietControl();
uint16_t seconds = 0;
const bool cancel = strcmp(arguments + 11, "off") == 0;
if (!cancel && !powerTestNumber(arguments + 11, 5, 300, seconds)) {
snprintf(reply, capacity, "Error: uart_quiet requires 5..300 seconds or off"); return true;
}
if (!quiet.handle) snprintf(reply, capacity, "Error: UART quiet is unsupported");
else quiet.handle(quiet.owner, cancel ? UartQuietAction::Cancel : UartQuietAction::Schedule,
seconds, reply, capacity);
return true;
} else {
uint16_t number = 0;
if (arguments && strncmp(arguments, "slice_ms ", 9) == 0
&& powerTestNumber(arguments + 9, 2, 20, number)) value.slice_ms = uint8_t(number);
else if (arguments && strncmp(arguments, "guard_ms ", 9) == 0
&& powerTestNumber(arguments + 9, 0, 20, number)) value.guard_ms = uint8_t(number);
else if (arguments && strncmp(arguments, "min_ms ", 7) == 0
&& powerTestNumber(arguments + 7, 1, 10, number)) value.minimum_ms = uint8_t(number);
else if (arguments && strcmp(arguments, "settle_ms auto") == 0) value.settle_ms = 0;
else if (arguments && strncmp(arguments, "settle_ms ", 10) == 0
&& powerTestNumber(arguments + 10, 1, 50, number)) value.settle_ms = uint8_t(number);
else {
snprintf(reply, capacity, "Error: use sleep on|off, slice_ms 2..20, guard_ms 0..20, min_ms 1..10, settle_ms auto|1..50, uart_quiet 5..300|off, reset");
return true;
}
}
formatPowerTestControls(reply, capacity);
return true;
}
} // namespace esp32
} // namespace mesh
+6 -3
View File
@@ -2,6 +2,7 @@
#include <stdint.h>
#include <esp_timer.h>
#include "PowerTestControls.h"
namespace mesh {
namespace esp32 {
@@ -13,16 +14,18 @@ namespace esp32 {
template <typename Mesh, typename Board>
bool tryProfileIdleSleep(Mesh& mesh, Board& board,
uint32_t budget_started_us, uint32_t budget_ms) {
if (budget_ms == 0 || budget_ms > 0x7fffffffUL / 1000UL) return false;
if (!manualCpuSleepEnabled() || budget_ms == 0 || budget_ms > 0x7fffffffUL / 1000UL) return false;
const uint32_t observed_us = static_cast<uint32_t>(esp_timer_get_time());
uint32_t profile_ms = 0;
if (!mesh.getDualRadioIdleDelay(profile_ms) || profile_ms < 2 || profile_ms > 10)
const uint32_t minimum_us = profileSleepMinimumMicros();
if (!mesh.getDualRadioIdleDelay(profile_ms) || profile_ms * 1000UL < minimum_us
|| profile_ms > profileSleepSliceMillis())
return false;
const uint32_t role_deadline = budget_started_us + budget_ms * 1000UL;
const int32_t role_remaining_us = static_cast<int32_t>(role_deadline - observed_us);
if (role_remaining_us < 2000) return false;
if (role_remaining_us < static_cast<int32_t>(minimum_us)) return false;
const uint32_t profile_us = profile_ms * 1000UL;
const uint32_t remaining_us = static_cast<uint32_t>(role_remaining_us) < profile_us
File diff suppressed because it is too large. Load diff
+10
View File
@@ -2374,6 +2374,16 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
radio["rxps_rx_us"] = node.rx_ps_rx_us;
radio["rxps_sleep_us"] = node.rx_ps_sleep_us;
radio["powersaving"] = (bool)node.power_saving;
if (node.cpu_supported_mask & 7U) {
JsonObject cpu = doc.createNestedObject("cpu");
cpu["selected_mhz"] = node.cpu_freq_mhz;
cpu["actual_mhz"] = node.cpu_actual_mhz;
JsonArray supported = cpu.createNestedArray("supported_mhz");
static const uint16_t frequencies[] = {80, 160, 240};
for (unsigned i = 0; i < 3; ++i) {
if (node.cpu_supported_mask & (1U << i)) supported.add(frequencies[i]);
}
}
#ifdef MESHCORE_HAS_REAL_DISPLAY
JsonObject screen = doc["display"].to<JsonObject>();
const auto& display_prefs = mesh::ui::displayPowerPrefs();
+3
View File
@@ -130,6 +130,9 @@ public:
uint32_t rx_ps_rx_us;
uint32_t rx_ps_sleep_us;
uint8_t power_saving;
uint16_t cpu_freq_mhz; // 0 = automatic device policy
uint16_t cpu_actual_mhz;
uint8_t cpu_supported_mask; // bits 0/1/2 = 80/160/240 MHz
bool display_rotation_supported;
uint16_t display_rotation_degrees;
bool mqtt_enabled;
+6 -4
View File
@@ -11,6 +11,7 @@
#include <esp_heap_caps.h>
#include <esp_timer.h>
#include <driver/gpio.h>
#include "../esp32/PowerTestControls.h"
#elif defined(NRF52_PLATFORM)
#include <utility/debug.h>
#include "../NRF52Board.h"
@@ -1089,18 +1090,19 @@ bool RadioLibWrapper::getProfileIdleDelay(uint32_t& delay_ms) const {
const uint64_t elapsed_us = now_us - _esp32_profile_idle_stamp_us;
// Keep frontend settling and short visits on their polling path. Only the
// MCU idles; the RF receiver stays in continuous RX on the current profile.
if (elapsed_us < NoiseFloorSettleMillis * 1000UL
if (elapsed_us < mesh::esp32::profileSleepSettleMillis(NoiseFloorSettleMillis) * 1000UL
|| elapsed_us >= _esp32_profile_idle_visit_us) return false;
const uint32_t remaining_us = _esp32_profile_idle_visit_us - static_cast<uint32_t>(elapsed_us);
// Reserve return-to-loop/wake latency and require a useful timer interval.
// These bounds are a conservative policy, not a measured latency guarantee.
// The caller anchors this budget to an absolute ESP Timer deadline before
// querying it and checks that deadline again immediately before sleeping.
constexpr uint32_t wake_guard_us = 4000;
constexpr uint32_t minimum_idle_us = 2000;
const uint32_t wake_guard_us = mesh::esp32::profileSleepGuardMicros();
const uint32_t minimum_idle_us = mesh::esp32::profileSleepMinimumMicros();
if (remaining_us < wake_guard_us + minimum_idle_us) return false;
delay_ms = (remaining_us - wake_guard_us) / 1000;
if (delay_ms > 10) delay_ms = 10;
if (delay_ms > mesh::esp32::profileSleepSliceMillis())
delay_ms = mesh::esp32::profileSleepSliceMillis();
return true;
#else
return false; // Other platforms retain polling until their wake path is validated.
+12 -2
View File
@@ -238,6 +238,11 @@ int main() {
accepted_sizes.push_back(historical_size + mesh::peripherals::FooterSize);
#endif
accepted_sizes.push_back(before.size());
#if defined(ESP32_PLATFORM)
// A complete CPU footer, including a corrupt one, preserves the checked
// prefix and falls back to Auto. A partial append remains an invalid image.
accepted_sizes.push_back(before.size() + mesh::esp32::cpuclock::FooterSize);
#endif
for (size_t length = 0; length <= before.size() + 256; ++length) {
DataStore reboot;
auto candidate = before;
@@ -256,8 +261,13 @@ int main() {
assert(live.buzzer_quiet == (length >= 85 ? 0 : 1));
assert(live.vibe_quiet == (length >= 143 ? 1 : 0));
assert(live.lost_reply == (length >= historical_size ? 2 : 0));
assert(live.tx_media == (length == before.size() ? mesh::RADIO_TX_SECONDARY : mesh::RADIO_TX_AUTO));
assert(live.tx_ssh == (length == before.size() ? mesh::RADIO_TX_OFF : mesh::RADIO_TX_AUTO));
const bool has_traffic = length == before.size()
#if defined(ESP32_PLATFORM)
|| length == before.size() + mesh::esp32::cpuclock::FooterSize
#endif
;
assert(live.tx_media == (has_traffic ? mesh::RADIO_TX_SECONDARY : mesh::RADIO_TX_AUTO));
assert(live.tx_ssh == (has_traffic ? mesh::RADIO_TX_OFF : mesh::RADIO_TX_AUTO));
#if defined(RAK_4631)
assert(live.peripherals_shared == (length >= 244 ? 1 : 0));
#endif
+5
View File
@@ -243,12 +243,17 @@ class CompanionPrefsValuesTests(unittest.TestCase):
'wifi_pwd': 'defined(RP2040_PLATFORM) && defined(ENABLE_WIFI_INTERFACE)',
'peripherals_shared': 'RAK_4631',
'leds_quiet': 'WIO_E5_MINI_HEADLESS_COMPANION_UI',
'cpu_freq_mhz': 'ESP32_PLATFORM',
}
def fields(template):
rows = []
for name in declarations:
row = template.format(name=name)
# Historical byte/EOF comparisons use Auto; explicit CPU
# footer round trips have their own actual-codec fault suite.
if name == 'cpu_freq_mhz' and template.startswith('fill('):
row = 'prefs.cpu_freq_mhz = 0;'
if name in optional:
condition = optional[name]
if not condition.startswith('defined('):
+288
View File
@@ -0,0 +1,288 @@
"""Exercise CPU selection transactions and the actual Companion preference codec."""
from pathlib import Path
import os
import re
import subprocess
import tempfile
import unittest
from test_companion_preferences_transaction import HARNESS, esp_recovery_helpers
from test_radio_receive_contract import method
ROOT = Path(__file__).resolve().parents[1]
POLICY = r'''
#include <cassert>
#include <deque>
#include <string>
#include <vector>
#include <helpers/esp32/CpuClockPolicy.h>
using namespace mesh::esp32::cpuclock;
struct Board {
uint16_t actual=80;
bool supports240=true, companion=true;
unsigned applies=0;
std::deque<bool> outcomes;
bool supportsCpuFrequencyMHz(uint16_t value) const {
return value==80 || value==160 || (value==240 && supports240);
}
uint16_t getCpuFrequencyMHz() const { return actual; }
bool applyCpuFrequencyMHz(uint16_t selected,bool ps) {
++applies;
actual=resolveMHz(selected,ps,companion); // partial mutation can precede failure
if(outcomes.empty()) return true;
bool ok=outcomes.front();outcomes.pop_front();return ok;
}
};
int main() {
assert(nominalMHz()==160);
assert(resolveMHz(0,true,true)==80 && resolveMHz(0,false,true)==160);
assert(resolveMHz(0,true,false)==160 && resolveMHz(0,false,false)==160);
for(uint16_t selected : {80,160,240})
for(bool ps : {false,true}) for(bool companion : {false,true})
assert(resolveMHz(selected,ps,companion)==selected);
uint16_t selected=160,durable=160;Board board;board.actual=160;
bool storage=true;unsigned saves=0;char reply[160]={};
auto command=[&](const char* text,bool ps=true) {
return handleCommand(board,selected,ps,text,reply,sizeof(reply),[&] {
++saves;if(!storage)return false;durable=selected;return true;
});
};
assert(command("get cpu.freq"));
assert(std::string(reply)=="> selected=160 actual=160MHz supported=auto,80,160,240");
assert(!command("get cpu.frequency"));
for(const char* invalid : {"", "0", "40", "200", "080", "160MHz", "160.0",
"-80", "+80", "AUTO", "160 240", "99999999999999999999999"}) {
const std::string text=std::string("set cpu.freq ")+invalid;
unsigned old=board.applies,old_saves=saves;
assert(command(text.c_str()));assert(std::string(reply).find("Error:")==0);
assert(selected==160&&durable==160&&board.actual==160);
assert(board.applies==old&&saves==old_saves);
}
board.supports240=false;assert(command("set cpu.freq 240"));
assert(selected==160&&durable==160&&board.actual==160&&board.applies==0&&saves==0);
assert(command("get cpu.freq"));
assert(std::string(reply)=="> selected=160 actual=160MHz supported=auto,80,160");
board.supports240=true;
assert(command("set cpu.freq 240"));assert(selected==240&&durable==240&&board.actual==240);
for(bool ps : {false,true}) {
assert(board.applyCpuFrequencyMHz(selected,ps));assert(board.actual==240);
}
storage=false;assert(command("set cpu.freq 80"));
assert(selected==240&&durable==240&&board.actual==240);
assert(std::string(reply)=="Error: CPU clock save failed; restored");
storage=true;board.outcomes={false,true};unsigned old_saves=saves;
assert(command("set cpu.freq 160"));assert(saves==old_saves);
assert(selected==240&&durable==240&&board.actual==240);
board.outcomes={true,false};storage=false;
assert(command("set cpu.freq 80"));
assert(std::string(reply).find("save and restore failed")!=std::string::npos);
assert(selected==240&&durable==240);
storage=true;board.outcomes.clear();
assert(command("set cpu.freq auto"));assert(selected==0&&durable==0&&board.actual==80);
assert(board.applyCpuFrequencyMHz(selected,false));assert(board.actual==160);
assert(command("set cpu.freq \t160\t "));
assert(selected==160&&durable==160&&board.actual==160);
for(uint16_t value : {0,80,160,240}) {
uint8_t footer[FooterSize];encodeFooter(footer,value);
uint16_t decoded=999;assert(decodeFooter(footer,sizeof(footer),decoded)&&decoded==value);
for(size_t n=0;n<FooterSize;++n) {
decoded=240;assert(!decodeFooter(footer,n,decoded)&&decoded==0);
for(unsigned bit=0;bit<8;++bit) {
footer[n]^=uint8_t(1u<<bit);decoded=240;
assert(!decodeFooter(footer,sizeof(footer),decoded)&&decoded==0);
footer[n]^=uint8_t(1u<<bit);
}
}
footer[4]=0;footer[5]=200;uint16_t crc=footerChecksum(footer,6);
footer[6]=uint8_t(crc>>8);footer[7]=uint8_t(crc);
decoded=240;assert(!decodeFooter(footer,sizeof(footer),decoded)&&decoded==0);
}
}
'''
PERSISTENCE = r'''
int main() {
using namespace mesh::esp32::cpuclock;
for(float timezone : {-7.0f,-7.5f}) {
DataStore store;CompanionNodePrefs prefs;
prefs.freq=910.525f;strcpy(prefs.node_name,"preserved");prefs.ble_pin=123456;
prefs.tz_offset=timezone;prefs.tx_media=mesh::RADIO_TX_SECONDARY;
assert(store.savePrefs(prefs,47.125,-122.25));
const auto legacy=store.fs.files["/new_prefs"];
assert(legacy.size()==236+mesh::companion::traffic::FooterSize
+(timezone==-7.5f?mesh::companion::timezone::FooterSize:0));
for(uint16_t value : {80,160,240}) {
prefs.cpu_freq_mhz=value;assert(store.savePrefs(prefs,47.125,-122.25));
const auto image=store.fs.files["/new_prefs"];
assert(image.size()==legacy.size()+FooterSize);
assert(std::equal(legacy.begin(),legacy.end(),image.begin()));
CompanionNodePrefs loaded;double lat=0,lon=0;
assert(store.loadPrefsInt("/new_prefs",loaded,lat,lon));
assert(loaded.cpu_freq_mhz==value&&loaded.freq==prefs.freq&&loaded.ble_pin==123456);
assert(loaded.tz_offset==timezone&&loaded.tx_media==mesh::RADIO_TX_SECONDARY);
assert(lat==47.125&&lon==-122.25);
CompanionNodePrefs snapshot;
assert(snapshot.copyPersistedValuesFrom(loaded)&&snapshot.cpu_freq_mhz==value);
// Every CPU-footer bit fault falls back to Auto without discarding the
// independently checked radio, timezone, traffic, identity and location.
for(size_t n=0;n<FooterSize;++n) for(unsigned bit=0;bit<8;++bit) {
DataStore bad;auto corrupt=image;corrupt[legacy.size()+n]^=uint8_t(1u<<bit);
bad.fs.files["/new_prefs"]=corrupt;loaded.cpu_freq_mhz=240;
assert(bad.loadPrefsInt("/new_prefs",loaded,lat,lon));
assert(loaded.cpu_freq_mhz==0&&loaded.freq==prefs.freq&&loaded.tz_offset==timezone);
assert(loaded.ble_pin==123456&&loaded.tx_media==mesh::RADIO_TX_SECONDARY);
}
for(size_t quota=0;quota<image.size();++quota) {
DataStore failed;failed.fs.files["/new_prefs"]=legacy;
failed.fs.fail_write_after=int(quota);
assert(!failed.savePrefs(prefs,47.125,-122.25));
assert(failed.fs.files["/new_prefs"]==legacy);
}
// Old images explicitly clear a prior in-memory fixed selection.
DataStore old;old.fs.files["/new_prefs"]=legacy;loaded.cpu_freq_mhz=value;
assert(old.loadPrefsInt("/new_prefs",loaded,lat,lon));assert(loaded.cpu_freq_mhz==0);
// Returning to Auto removes only the appended override.
prefs.cpu_freq_mhz=0;assert(store.savePrefs(prefs,47.125,-122.25));
assert(store.fs.files["/new_prefs"]==legacy);
}
}
}
'''
class CpuClockTests(unittest.TestCase):
def compile_run(self, code, *, persistence=False):
with tempfile.TemporaryDirectory(prefix="meshcore-cpu-clock-") as directory:
work = Path(directory)
(work / "Utils.h").write_text('''#pragma once
#include <Arduino.h>
#include <cassert>
namespace mesh { struct Utils {
static void printHex(Stream&,const uint8_t*,size_t){assert(false);}
static void fromHex(uint8_t*,size_t,const char*){assert(false);}
}; }
''')
(work / "shim.h").write_text('''#include <cstdlib>
#include <cstdio>
inline char* utoa(unsigned value,char* out,int base){
if(base!=10)abort();sprintf(out,"%u",value);return out;
}
''')
transaction = (ROOT / "src/helpers/ContactFileTransaction.h").read_text()
(work / "ContactFileTransaction.h").write_text(transaction.replace(
'#include "IdentityStore.h"', '#include <helpers/IdentityStore.h>'))
cpp, binary = work / "test.cpp", work / "test"
cpp.write_text(code)
command = [os.environ.get("CXX", "g++"), "-std=c++17", "-DESP32_PLATFORM=1",
"-DESP32_CPU_FREQ=160", "-fsanitize=address,undefined",
"-fno-sanitize-recover=all", "-fno-pie", "-no-pie",
"-include", str(work / "shim.h"), "-I", str(work),
"-I", str(ROOT / "test/fixtures/radio_profiles/mocks"),
"-I", str(ROOT / "test/mocks"), "-I", str(ROOT / "src"),
"-I", str(ROOT / "src/helpers"), "-I", str(ROOT), str(cpp)]
if persistence:
command += [str(ROOT / "src/helpers" / name) for name in (
"ConfigSerializer.cpp", "DynamicConfigSerializer.cpp",
"CommonRadioPrefs.cpp", "TxtDataHelpers.cpp")]
built = subprocess.run(command + ["-o", str(binary)], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(binary)], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
def test_policy_cli_transactions_and_checked_footer(self):
self.compile_run(POLICY)
def test_actual_companion_codec_roundtrip_legacy_and_write_faults(self):
source = (ROOT / "examples/companion_radio/DataStore.cpp").read_text()
methods = esp_recovery_helpers(source) + "\n".join(
method(source, signature) for signature in (
"bool DataStore::loadPrefs(", "bool DataStore::loadPrefsInt(",
"bool DataStore::savePrefs("))
prefix = HARNESS.split("int main(")[0].replace("@METHODS@", methods)
self.compile_run(prefix + PERSISTENCE, persistence=True)
def test_actual_common_writers_match_and_footer_is_append_only(self):
source = (ROOT / "src/helpers/CommonCLI.cpp").read_text()
atomic = "template <typename Writer>\n" + method(source, "static bool writeCommonPrefsImage(")
retry = "template <typename Writer>\n" + method(source, "static void writeCommonRetryPrefs(")
save = method(source, "void CommonCLI::savePrefs(FILESYSTEM*")
scalar = save[save.index(" uint8_t pad[8];"):
save.index(" _common_save_succeeded = file.commit();")]
fields_used = set(re.findall(r"(?:_prefs|prefs)->(\w+)", atomic + retry + scalar))
header = (ROOT / "src/helpers/CommonCLI.h").read_text()
prefix = header.split("class NodePrefs :", 1)[1].split("private:", 1)[0]
fields = []
for kind, declaration in re.findall(
r"^\s*(float|double|char|u?int(?:8|16|32)_t)\s+([^;\n]+);", prefix, re.M):
declaration = re.sub(r"\s*=\s*[^,;]+", "", declaration)
names = re.findall(r"\b[A-Za-z_]\w*\b", declaration)
if fields_used.intersection(names):
fields.append(kind + " " + declaration + "{};")
# Field-array dimensions are the existing shipping limits. Both real
# emitters must retain every historical byte and append only CPUv1.
constants = "\n".join(re.findall(
r"^\s*#define FLOOD_RETRY_(?:PREFIX_SLOTS|PREFIX_LEN|BRIDGE_BUCKETS|BUCKET_PREFIXES|IGNORE_PREFIXES)\s+\d+",
header, re.M))
constants += "\n" + "\n".join(re.findall(
r"^#define DIRECT_RETRY_PREFS_MAGIC_[01]\s+0x[0-9A-Fa-f]+", header, re.M))
marker = method(source, "static void markDirectRetryPrefsValid(")
harness = r'''
#include <cassert>
#include <algorithm>
#include <cstring>
#include <vector>
#include <helpers/esp32/CpuClockPolicy.h>
#include <helpers/bridges/RS232PowerSettings.h>
@CONSTANTS@
struct NodePrefs { @FIELDS@ };
@MARKER@
struct File {
std::vector<uint8_t> bytes;size_t cursor=0;bool short_write=false;
size_t write(const uint8_t* data,size_t size){
if(short_write&&size)--size;
bytes.insert(bytes.end(),data,data+size);return size;
}
size_t size() const{return bytes.size();}
bool seek(size_t value){if(value>size())return false;cursor=value;return true;}
size_t read(uint8_t* out,size_t count){
count=std::min(count,size()-cursor);memcpy(out,bytes.data()+cursor,count);cursor+=count;return count;
}
};
@ATOMIC@
@RETRY@
void non_mqtt(File& file,NodePrefs* _prefs){ @SCALAR@ }
int main(){
using namespace mesh::esp32::cpuclock;
NodePrefs prefs{};prefs.freq=910.525f;prefs.bw=62.5f;prefs.sf=7;prefs.cr=5;
prefs.powersaving_enabled=1;strcpy(prefs.node_name,"unchanged prefix");
File legacy;assert(writeCommonPrefsImage(legacy,&prefs));
File nonlegacy;non_mqtt(nonlegacy,&prefs);assert(nonlegacy.bytes==legacy.bytes);
assert(readFooter(legacy)==0);
for(uint16_t mhz : {80,160,240}){
prefs.cpu_freq_mhz=mhz;File a,b;
assert(writeCommonPrefsImage(a,&prefs));non_mqtt(b,&prefs);
assert(a.bytes==b.bytes&&a.size()==legacy.size()+FooterSize);
assert(std::equal(legacy.bytes.begin(),legacy.bytes.end(),a.bytes.begin()));
assert(readFooter(a)==mhz);
for(size_t n=0;n<FooterSize;++n){
a.bytes[legacy.size()+n]^=0x80;assert(readFooter(a)==0);
a.bytes[legacy.size()+n]^=0x80;
}
File short_write;short_write.short_write=true;
assert(!writeCommonPrefsImage(short_write,&prefs));
}
prefs.cpu_freq_mhz=0;File restored;non_mqtt(restored,&prefs);
assert(restored.bytes==legacy.bytes&&readFooter(restored)==0);
}
'''
for marker, value in {"CONSTANTS": constants, "FIELDS": "\n".join(fields),
"ATOMIC": atomic, "RETRY": retry, "SCALAR": scalar,
"MARKER": marker}.items():
harness = harness.replace("@" + marker + "@", value)
self.compile_run(harness)
self.compile_run("#define WITH_RS232_BRIDGE 1\n" + harness)
if __name__ == "__main__":
unittest.main()
+185
View File
@@ -0,0 +1,185 @@
"""Execute the production board clock API and actual role policy handoffs."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_esp32_usb_sleep import board_method
from cpp_source import body
ROOT=Path(__file__).resolve().parents[1]
PRELUDE=r'''
#include <cassert>
#include <cstdint>
#include <helpers/esp32/CpuClockPolicy.h>
#include <helpers/esp32/PowerTestControls.h>
static uint16_t actual_mhz=240;
static unsigned clock_writes=0;
static bool fail_clock=false,wrong_readback=false;
static uint32_t now_ms=100;
uint32_t millis(){return now_ms;}
uint32_t getCpuFrequencyMhz(){return actual_mhz;}
bool setCpuFrequencyMhz(uint32_t value){
++clock_writes;if(fail_clock)return false;actual_mhz=wrong_readback?80:value;return true;
}
#define EFUSE_BLK0_RDATA3_REG 0
#define EFUSE_RD_CHIP_CPU_FREQ_RATED 1
#define EFUSE_RD_CHIP_CPU_FREQ_LOW 2
static unsigned fuse_bits=0;
#define REG_GET_BIT(reg,mask) (fuse_bits&(mask))
using esp_err_t=int;constexpr int ESP_OK=0;
struct esp_pm_config_t {int max_freq_mhz,min_freq_mhz;bool light_sleep_enable;};
using esp_pm_config_esp32_t=esp_pm_config_t;
using esp_pm_config_esp32s3_t=esp_pm_config_t;
using esp_pm_config_esp32c3_t=esp_pm_config_t;
static esp_pm_config_t applied_pm{};
int esp_pm_configure(esp_pm_config_t* config){
++clock_writes;if(fail_clock)return 9;applied_pm=*config;
actual_mhz=wrong_readback?80:config->min_freq_mhz;return ESP_OK;
}
struct MainBoard {
virtual uint16_t getCpuFrequencyMHz()const=0;
virtual bool supportsCpuFrequencyMHz(uint16_t)const=0;
virtual bool applyCpuFrequencyMHz(uint16_t,bool)=0;
};
struct ESP32Board:MainBoard {
bool companion_cpu_clock_policy=false;
unsigned quiet_services=0;
void servicePowerTestControls(){++quiet_services;}
@METHODS@
} board;
namespace mesh {
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;
'''
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("))
return PRELUDE.replace("@METHODS@",methods)
def run_fixture(self,main,defines=()):
with tempfile.TemporaryDirectory(prefix="mesh-esp32-clock-board-") as directory:
folder=Path(directory);path=folder/"test.cpp";path.write_text(self.fixture()+main)
result=subprocess.run([os.environ.get("CXX","c++"),"-std=c++17","-Wall","-Wextra",
"-fsanitize=address,undefined","-fno-sanitize-recover=all","-fno-pie","-no-pie",
"-DESP32_CPU_FREQ=240","-I",str(ROOT/"src"),*defines,str(path),"-o",str(folder/"test")],
capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stderr)
result=subprocess.run([str(folder/"test")],capture_output=True,text=True)
self.assertEqual(result.returncode,0,result.stdout+result.stderr)
def test_supported_silicon_fixed_clocks_pm_and_auto_role_policy(self):
source=r'''
int main(){
const unsigned nominal=board.supportsCpuFrequencyMHz(240)?240:160;
assert(board.supportsCpuFrequencyMHz(80)&&board.supportsCpuFrequencyMHz(160));
#if CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6
assert(!board.supportsCpuFrequencyMHz(240));
#else
assert(board.supportsCpuFrequencyMHz(240));
#endif
assert(!board.supportsCpuFrequencyMHz(120)&&!board.supportsCpuFrequencyMHz(0));
unsigned before=clock_writes;assert(!board.applyCpuFrequencyMHz(120,true));assert(clock_writes==before);
for(uint16_t selected:{uint16_t(80),uint16_t(160)}){
for(bool ps:{false,true}){
assert(board.applyCpuFrequencyMHz(selected,ps));assert(board.getCpuFrequencyMHz()==selected);
#if CONFIG_PM_ENABLE
assert(applied_pm.min_freq_mhz==selected&&applied_pm.max_freq_mhz==selected);
#endif
}
}
board.setCompanionCpuClockPolicy(true);
assert(board.applyCpuFrequencyMHz(0,true));
#if CONFIG_PM_ENABLE
assert(applied_pm.max_freq_mhz==80&&applied_pm.min_freq_mhz==40);
#if ENABLE_USB_INTERFACE || ARDUINO_USB_CDC_ON_BOOT
assert(!applied_pm.light_sleep_enable);
#else
assert(applied_pm.light_sleep_enable);
#endif
#else
assert(actual_mhz==80);
#endif
assert(board.applyCpuFrequencyMHz(0,false));assert(actual_mhz==nominal);
board.setCompanionCpuClockPolicy(false);
assert(board.applyCpuFrequencyMHz(0,true));assert(actual_mhz==nominal);
#if CONFIG_PM_ENABLE
assert(unsigned(applied_pm.max_freq_mhz)==nominal&&unsigned(applied_pm.min_freq_mhz)==nominal&&!applied_pm.light_sleep_enable);
#endif
fail_clock=true;assert(!board.applyCpuFrequencyMHz(160,true));fail_clock=false;
wrong_readback=true;assert(!board.applyCpuFrequencyMHz(160,true));wrong_readback=false;
#if CONFIG_IDF_TARGET_ESP32
// Rated+low is the classic160MHz-only eFuse combination. Reject explicit
//240 without letting Arduino silently replace it with a different clock.
for(unsigned bits:{0U,1U,2U,3U}){
fuse_bits=bits;assert(board.supportsCpuFrequencyMHz(240)==(bits!=3));
}
before=clock_writes;assert(!board.applyCpuFrequencyMHz(240,false));assert(clock_writes==before);
assert(board.applyCpuFrequencyMHz(0,false)&&actual_mhz==160);
#endif
}
'''
source="#include <initializer_list>\n"+source
for target in ("ESP32","ESP32S3","ESP32C3","ESP32C6"):
for pm in (0,1):
with self.subTest(target=target,pm=pm):
self.run_fixture(source,[f"-DCONFIG_IDF_TARGET_{target}=1",f"-DCONFIG_PM_ENABLE={pm}","-DENABLE_USB_INTERFACE=1"])
self.run_fixture(source,["-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=1"])
def test_actual_companion_service_tracks_clock_choice_independently_of_ps(self):
main=(ROOT/"examples/companion_radio/main.cpp").read_text()
methods="\n".join(body(main,signature) for signature in (
"static bool applyCompanionPowerSaving(","static void serviceCompanionPowerSaving("))
prefix=r'''
static int8_t applied_power_saving=-1,attempted_power_saving=-1;
static uint16_t applied_cpu_frequency=UINT16_MAX,attempted_cpu_frequency=UINT16_MAX;
static unsigned long power_saving_retry_at=0;
'''
tests=r'''
int main(){
board.setCompanionCpuClockPolicy(true);serviceCompanionPowerSaving(true);
assert(actual_mhz==80);unsigned writes=clock_writes;
serviceCompanionPowerSaving();assert(clock_writes==writes);
prefs.cpu_freq_mhz=160;serviceCompanionPowerSaving();assert(actual_mhz==160);
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);
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;
serviceCompanionPowerSaving();assert(clock_writes==writes);
now_ms+=5000;fail_clock=false;serviceCompanionPowerSaving();assert(actual_mhz==160);
}
'''
self.run_fixture(prefix+methods+tests,["-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=0",
"-DCOMPANION_IDF_PM_AVAILABLE=0","-DCOMPANION_BT_MODEM_SLEEP_AVAILABLE=0"])
def test_actual_infrastructure_loop_services_saved_choice_and_quiet_deadline(self):
for role in ("simple_repeater","simple_room_server","simple_sensor"):
source=(ROOT/"examples"/role/"main.cpp").read_text()
start=source.index(" board.servicePowerTestControls();",source.index("void loop()"))
end=source.index("#endif",start)
service=source[start:end]
tests="void role_service(){\n"+service+"}\n"+r'''
int main(){
prefs.cpu_freq_mhz=160;role_service();assert(actual_mhz==160&&board.quiet_services==1);
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==240);
}
'''
with self.subTest(role=role):
self.run_fixture(tests,["-DCONFIG_IDF_TARGET_ESP32S3=1","-DCONFIG_PM_ENABLE=0"])
if __name__=="__main__": unittest.main(verbosity=2)
+185
View File
@@ -0,0 +1,185 @@
"""Execute RAM-only controls, cross-TU scanner handoff and UART recovery."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from test_esp32_usb_sleep import HARNESS, board_method, board_sleep_methods
import test_profile_idle_esp32 as profile_tests
ROOT = Path(__file__).resolve().parents[1]
def compile_run(sources, defines=()):
with tempfile.TemporaryDirectory(prefix="mesh-esp32-power-controls-") as directory:
folder = Path(directory)
(folder / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
(folder / "esp_timer.h").write_text("#pragma once\n#include <stdint.h>\nint64_t esp_timer_get_time();\n")
files=[]
for index, source in enumerate(sources):
path=folder/f"source{index}.cpp"; path.write_text(source); files.append(str(path))
command=[os.environ.get("CXX","c++"),"-std=c++17","-Wall","-Wextra",
"-fsanitize=address,undefined","-fno-sanitize-recover=all","-fno-pie","-no-pie",
"-I",str(folder),"-I",str(ROOT/"src"),*defines,*files,"-o",str(folder/"test")]
built=subprocess.run(command,capture_output=True,text=True)
if built.returncode: raise AssertionError(built.stderr)
ran=subprocess.run([str(folder/"test")],capture_output=True,text=True)
if ran.returncode: raise AssertionError(ran.stdout+ran.stderr)
class PowerTestControlsTest(unittest.TestCase):
def test_bounds_local_authority_summary_and_reset(self):
compile_run([r'''
#include <cassert>
#include <cstring>
#include <helpers/esp32/PowerTestControls.h>
using namespace mesh::esp32;
static char reply[160];
static bool command(const char* text,bool local=true) {
memset(reply,0x5a,sizeof(reply));
bool handled=handlePowerTestCommand(text,reply,157,local);
assert(reply[157]==0x5a&&reply[159]==0x5a);
if(handled) assert(memchr(reply,0,157));
return handled;
}
int main(){
assert(command("get powersaving.test"));
assert(strstr(reply,"sleep=on slice_ms=10 guard_ms=4 min_ms=2 settle_ms=auto"));
assert(strstr(reply,"RAM only"));
assert(!command("get powersaving.other"));
assert(command("set powersaving.test sleep off",false)&&strstr(reply,"local only"));
assert(manualCpuSleepEnabled());
assert(command("get powersaving.test",false)&&strstr(reply,"local only"));
for(const char* invalid:{"slice_ms 1","slice_ms 21","guard_ms 21","guard_ms -1",
"min_ms 0","min_ms 11","settle_ms 0","settle_ms 51","min_ms 2x",
"slice_ms 99999999999999999","uart_quiet 4","uart_quiet 301","sleep maybe"}){
char input[96];snprintf(input,sizeof(input),"set powersaving.test %s",invalid);
assert(command(input)&&strstr(reply,"Error:"));
}
assert(command("set powersaving.test slice_ms 20"));assert(profileSleepSliceMillis()==20);
assert(command("set powersaving.test guard_ms 0"));assert(profileSleepGuardMicros()==0);
assert(command("set powersaving.test min_ms 1"));assert(profileSleepMinimumMicros()==1000);
assert(command("set powersaving.test settle_ms 50"));assert(profileSleepSettleMillis(7)==50);
assert(command("set powersaving.test sleep off"));assert(!manualCpuSleepEnabled());
assert(command("set powersaving.test reset"));
assert(manualCpuSleepEnabled()&&profileSleepSliceMillis()==10&&profileSleepGuardMicros()==4000);
assert(profileSleepMinimumMicros()==2000&&profileSleepSettleMillis(7)==7);
assert(command("set powersaving.test uart_quiet 5")&&strstr(reply,"unsupported"));
}
'''.replace("#include <cassert>","#include <cassert>\n#include <initializer_list>")])
def test_cli_in_another_translation_unit_changes_real_scanner_and_deadline(self):
setter=r'''
#include <helpers/esp32/PowerTestControls.h>
extern "C" bool change(const char* command){
char reply[160]={};return mesh::esp32::handlePowerTestCommand(command,reply,sizeof(reply),true)
&& strncmp(reply,"Error:",6)!=0;
}
'''
scanner=profile_tests.Esp32ProfileIdleTest.fixture()+r'''
#include <helpers/esp32/ProfileIdleSleep.h>
extern "C" bool change(const char*);
struct Mesh {
RadioLibWrapper& radio;
bool getDualRadioIdleDelay(uint32_t& out){return radio.getProfileIdleDelay(out);}
};
struct Board {unsigned calls=0;uint32_t deadline=0;void sleepUntilMicroseconds(uint32_t at){++calls;deadline=at;}};
int main(){
RadioLibWrapper w;ready(w);Mesh m{w};Board b;
auto start=w._esp32_profile_visit_started_us;auto visit=w._esp32_profile_idle_visit_us;
elapsed_us=start+visit-14000;assert(idle(w,10));
assert(change("set powersaving.test guard_ms 9"));assert(idle(w,5));
assert(change("set powersaving.test slice_ms 3"));assert(idle(w,3));
assert(mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30));
assert(b.calls==1&&b.deadline==uint32_t(elapsed_us+3000));
assert(change("set powersaving.test min_ms 6"));assert(!idle(w,0));
assert(!mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30)&&b.calls==1);
assert(change("set powersaving.test reset"));assert(idle(w,10));
assert(change("set powersaving.test settle_ms 50"));
elapsed_us=start+10000;assert(!idle(w,0));
assert(change("set powersaving.test settle_ms auto"));assert(idle(w,10));
assert(change("set powersaving.test sleep off"));
assert(!mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30)&&b.calls==1);
assert(change("set powersaving.test reset"));
assert(mesh::esp32::tryProfileIdleSleep(m,b,uint32_t(elapsed_us),30)&&b.calls==2);
}
'''
compile_run([setter,scanner],["-DESP32_PLATFORM=1"])
def quiet_harness(self):
methods=board_sleep_methods()+"\n"+"\n".join(board_method(signature) for signature in (
"bool isUsbDataConnected() override","bool isUsbHostConnected() override","bool isUsbSleepHeld()",
"bool handleUartQuiet(","void servicePowerTestControls("))
code=HARNESS.split("int main()",1)[0]
code=code.replace(" void end() { driver_initialized = false; }",r'''
unsigned begins=0,ends=0,flushes=0;
void begin(unsigned baud){assert(baud==115200);++begins;driver_initialized=true;}
void flush(){++flushes;}
void end(){++ends;driver_initialized=false;}
''')
code=code.replace("static bool logging_enabled = false;","static bool logging_enabled = false;\nstatic bool text_pending=false;\nbool hasPendingUsbTerminalOutput(){return text_pending;}")
code=code.replace(" bool inhibit_sleep = false;"," bool inhibit_sleep = false;\n bool ota=false;bool isOTAUpdateRunning(){return ota;}\n uint32_t uart_quiet_start_at=0;uint16_t uart_quiet_seconds=0;")
return "#include <cassert>\n"+code.replace("@METHODS@",methods)
def test_actual_uart_ack_drain_bounded_quiet_and_sleep_deadline_restore(self):
compile_run([self.quiet_harness()+r'''
int main(){
ESP32Board board;char reply[160]={};using mesh::esp32::UartQuietAction;
mesh::logging_enabled=true;
assert(!board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mesh::logging_enabled=false;board.ota=true;
assert(!board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
board.ota=false;
assert(board.handleUartQuiet(UartQuietAction::Schedule,10,reply,sizeof(reply)));
assert(strstr(reply,"100ms"));mock_millis=1099;board.servicePowerTestControls();assert(Serial.ends==0);
mock_millis=1100;board.servicePowerTestControls(false);assert(Serial.ends==0);
mock_millis=1150;mesh::text_pending=true;board.servicePowerTestControls();assert(Serial.ends==0);
mock_millis=1200;mesh::text_pending=false;board.servicePowerTestControls();
assert(Serial.ends==1&&Serial.flushes==1&&!Serial&&board.uart_quiet_restore_at==11200);
board.sleepMilliseconds(30000);assert(sleep_calls==1&&timer_us==10000000);
mock_millis=11199;board.sleepUntilMicroseconds(micros()+10000);
assert(sleep_calls==1); // recovery is due before the minimum useful sleep
mock_millis=11200;board.servicePowerTestControls();
assert(Serial&&Serial.begins==1&&!board.uart_quiet_restore_at);
board.sleepMilliseconds(10);assert(sleep_calls==1); // normal UART guard restored
// An acknowledgement which never drains leaves the console available.
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=2000;board.servicePowerTestControls(false);
assert(Serial&&!board.uart_quiet_start_at&&Serial.ends==1);
// Reboot is not required to recover: local reset/off restores immediately.
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
assert(board.handleUartQuiet(UartQuietAction::Cancel,0,reply,sizeof(reply)));assert(Serial);
// Active logging cancels a quiet interval rather than losing its output.
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
mesh::logging_enabled=true;board.servicePowerTestControls();assert(Serial);
// Wrap-safe recovery, even with manual CPU sleep disabled.
mesh::logging_enabled=false;mock_millis=UINT32_MAX-50;
assert(board.handleUartQuiet(UartQuietAction::Schedule,5,reply,sizeof(reply)));
mock_millis+=100;board.servicePowerTestControls();assert(!Serial);
mesh::esp32::powerTestControls().sleep_enabled=false;
mock_millis+=5000;board.servicePowerTestControls();assert(Serial);
}
'''],["-DESP32_PLATFORM=1","-DARDUINO_USB_CDC_ON_BOOT=0","-DMESH_USB_LOGGING_AVAILABLE=1"])
def test_native_usb_quiet_is_unsupported_and_never_ends_driver(self):
compile_run([self.quiet_harness()+r'''
int main(){
ESP32Board board;char reply[160]={};
assert(!board.handleUartQuiet(mesh::esp32::UartQuietAction::Schedule,5,reply,sizeof(reply)));
assert(strstr(reply,"Error:")&&Serial.ends==0);
board.servicePowerTestControls();assert(Serial.ends==0);
mesh::esp32::registerUartQuietControl(&board,
[](void* ptr,mesh::esp32::UartQuietAction action,uint16_t seconds,char* out,size_t size){
return static_cast<ESP32Board*>(ptr)->handleUartQuiet(action,seconds,out,size);
});
mesh::esp32::powerTestControls().guard_ms=15;
assert(mesh::esp32::handlePowerTestCommand("set powersaving.test reset",reply,sizeof(reply),true));
assert(mesh::esp32::profileSleepGuardMicros()==4000&&strstr(reply,"uart=unsupported"));
}
'''],["-DESP32_PLATFORM=1","-DARDUINO_USB_CDC_ON_BOOT=1","-DMESH_USB_LOGGING_AVAILABLE=1"])
if __name__ == "__main__": unittest.main(verbosity=2)
+4
View File
@@ -21,6 +21,7 @@ HARNESS = r'''
#include <array>
#include <helpers/UsbHostSleepPolicy.h>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/esp32/PowerTestControls.h>
#ifndef MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS
#define MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS 120000UL
@@ -140,6 +141,7 @@ struct MainBoard {
};
struct ESP32Board : MainBoard {
bool inhibit_sleep = false;
uint32_t uart_quiet_restore_at = 0;
#if MESH_ESP32_USB_CONSOLE_COOPERATIVE
mesh::UsbHostSleepPolicy usb_host_sleep_policy;
#endif
@@ -342,6 +344,7 @@ V4_BUTTON_HARNESS = r'''
#include <Arduino.h>
#include <helpers/UsbHostSleepPolicy.h>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/esp32/PowerTestControls.h>
#include <helpers/ui/MomentaryButton.h>
#include <helpers/ui/DisplayPowerPolicy.h>
#include <helpers/MorseLoopPolicy.h>
@@ -408,6 +411,7 @@ struct MainBoard {
};
struct ESP32Board : MainBoard {
bool inhibit_sleep = false;
uint32_t uart_quiet_restore_at = 0;
uint32_t getIRQGpio() { return P_LORA_DIO_1; }
bool isExternalPowered() const { return false; }
@METHODS@
@@ -362,6 +362,7 @@ int main() {
code = r'''
#include <cassert>
#include <helpers/PowerSavingDiagnostics.h>
#include <helpers/esp32/PowerTestControls.h>
#define ARDUINO_USB_CDC_ON_BOOT 1
#define MESH_USB_LOGGING_AVAILABLE 1
using esp_err_t = int;
@@ -400,6 +401,7 @@ struct MainBoard {
};
struct ESP32Board : MainBoard {
bool inhibit_sleep = false, usb = false, test = false;
uint32_t uart_quiet_restore_at = 0;
uint32_t irq = 14;
bool isUsbSleepHeld() { return usb; }
bool isRadioTestActive() const { return test; }
+1 -1
View File
@@ -221,7 +221,7 @@ int64_t esp_timer_get_time() { return static_cast<int64_t>(elapsed_us); }
fixture = fixture.replace("state=STATE_RX; _profile_visit_stamp=profileTimestamp(_profiles.enabled());",
"state=STATE_RX; _esp32_profile_visit_started_us=elapsed_us; "
"_profile_visit_stamp=profileTimestamp(_profiles.enabled());")
return fixture.replace("@METHODS@", methods) + COMMON
return "#include <helpers/esp32/PowerTestControls.h>\n" + fixture.replace("@METHODS@", methods) + COMMON
def run_fixture(self, main):
with tempfile.TemporaryDirectory(prefix="meshcore-esp32-profile-idle-") as directory:
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Exercise CPU-speed snapshots, canonical config writes and the shipped form."""
import json
from pathlib import Path
import unittest
import test_webconfig_lost_reply as web_fixture
ROOT = Path(__file__).resolve().parents[1]
class CpuFrequencyWebConfigTest(unittest.TestCase):
compile_run = web_fixture.LostReplyWebConfigTest.compile_run
setup_values = web_fixture.LostReplyWebConfigTest.setup_values
run_page = web_fixture.LostReplyWebConfigTest.run_page
def test_actual_json_snapshot_and_canonical_cli_batch(self):
# Reuse the real authenticated GET/POST handlers, not a second JSON
# implementation. Only their peripheral/HTTP boundaries are faked.
source = web_fixture.LostReplyWebConfigTest.fixture(self)
source = source.split("int main() {", 1)[0] + r'''
#include <helpers/esp32/CpuClockPolicy.h>
struct CpuBoard {
uint16_t actual = 160;
int applies = 0;
bool supportsCpuFrequencyMHz(uint16_t mhz) const { return mhz == 80 || mhz == 160; }
uint16_t getCpuFrequencyMHz() const { return actual; }
bool applyCpuFrequencyMHz(uint16_t selected, bool) {
++applies; actual = selected ? selected : 80; return true;
}
};
String cpuWriteBody(const char* value, const char* key = "cpu.freq") {
return String("{\"reqid\":\"0123456789abcdef\",\"set\":{\"") + key + "\":" + value + "}}";
}
int main() {
assert(mesh::esp32::managementPassword::publish("password"));
for (unsigned mask = 0; mask < 16; ++mask) {
WebConfigServer server;
server.callbacks.node.cpu_freq_mhz = 80;
server.callbacks.node.cpu_actual_mhz = 160;
server.callbacks.node.cpu_supported_mask = mask;
AsyncWebServerRequest get;
server.handleConfigGet(&get);
JsonDocument snapshot;
assert(get.status == 200 && deserializeJson(snapshot, get.output) == DeserializationError::Ok);
if (!(mask & 7)) {
assert(snapshot["cpu"].isNull());
continue;
}
assert(snapshot["cpu"]["selected_mhz"].as<unsigned>() == 80);
assert(snapshot["cpu"]["actual_mhz"].as<unsigned>() == 160);
JsonArray options = snapshot["cpu"]["supported_mhz"];
const unsigned frequencies[] = {80, 160, 240};
unsigned count = 0;
for (unsigned bit = 0; bit < 3; ++bit) {
if (mask & (1U << bit)) assert(options[count++].as<unsigned>() == frequencies[bit]);
}
assert(options.size() == count);
}
for (const char* text : {"auto", "80", "160", "240", "81", "80;reboot", ""}) {
WebConfigServer server;
const String value = String("\"") + text + "\"";
AsyncWebServerRequest save(cpuWriteBody(value.c_str()));
server.handleConfigPost(&save);
assert(save.status == 202 && server._batch_count == 1 && !server._batch_reboot);
assert(strcmp(server._batch[0].key, "cpu.freq") == 0);
assert(String(server._batch[0].cmd) == String("set cpu.freq ") + text);
CpuBoard board;
uint16_t selected = 160;
unsigned saves = 0;
char reply[160] = {};
assert(mesh::esp32::cpuclock::handleCommand(board, selected, true,
server._batch[0].cmd, reply, sizeof(reply), [&]() { ++saves; return true; }));
const bool valid = strcmp(text, "auto") == 0 || strcmp(text, "80") == 0 || strcmp(text, "160") == 0;
if (valid) {
assert(strncmp(reply, "OK - saved", 10) == 0 && saves == 1 && board.applies == 1);
assert(selected == (strcmp(text, "auto") == 0 ? 0 : atoi(text)));
} else {
assert(strncmp(reply, "Error:", 6) == 0 && !saves && !board.applies);
assert(selected == 160 && board.actual == 160);
}
}
for (const char* invalid : {"null", "true", "80"}) {
WebConfigServer server;
AsyncWebServerRequest save(cpuWriteBody(invalid));
server.handleConfigPost(&save);
assert(save.status == 400 && server._batch_count == 0);
}
WebConfigServer server;
AsyncWebServerRequest extra(cpuWriteBody("\"80\"", "cpu.freq.extra"));
server.handleConfigPost(&extra);
assert(extra.status == 400 && server._batch_count == 0);
server.clearSessionLocked();
AsyncWebServerRequest unauthenticated(cpuWriteBody("\"80\""));
server.handleConfigPost(&unauthenticated);
assert(unauthenticated.status == 401 && server._batch_count == 0);
assert(wcIsAllowedSetKey("cpu.freq") && !wcSetKeyRequiresReboot("cpu.freq"));
}
'''
self.compile_run(source)
def test_shipped_form_uses_supported_options_and_saves_through_normal_batch(self):
status, config = self.setup_values()
status.update(mode="lan", capabilities=8192)
config["cpu"] = {"selected_mhz": 0, "actual_mhz": 80, "supported_mhz": [80, 160]}
prelude = r'''<script>
(function(){
var status=%s,config=%s,submitted=null,saves=[];
function reply(value,code){return Promise.resolve({ok:true,status:code||200,json:function(){return Promise.resolve(value)}})}
window.fetch=function(path,options){
if(path==="/api/status")return reply(status);
if(path==="/api/config"&&(!options||options.method!=="POST"))return reply(config);
if(path==="/api/config"){
submitted=JSON.parse(options.body);saves.push(submitted);
if(saves.length!==3){
config.cpu.selected_mhz=submitted.set["cpu.freq"]==="auto"?0:Number(submitted.set["cpu.freq"]);
config.cpu.actual_mhz=config.cpu.selected_mhz||80;
}
return reply({state:"pending",reqid:submitted.reqid,count:1},202);
}
if(path.indexOf("/api/config/result?")===0)return reply({state:"done",reqid:submitted.reqid,results:[{key:"cpu.freq",reply:saves.length===3?"Error: CPU clock apply failed; unchanged":"OK - saved"}]});
return reply({});
};
function mark(key,value){document.body.setAttribute("data-test-cpu-"+key,String(value))}
function waitFor(check,next,attempt){
if(check()){next();return}
if((attempt||0)>100){document.body.setAttribute("data-test-error","CPU save did not complete");return}
setTimeout(function(){waitFor(check,next,(attempt||0)+1)},20);
}
window.addEventListener("load",function(){
setTimeout(function(){enterApp()},50);
setTimeout(function(){waitFor(function(){return st.cfg&&st.cfg.cpu},function(){
var select=document.querySelector('[data-k="cpu.freq"]');
mark("visible",!document.getElementById("cpu-frequency-setting").classList.contains("hide"));
mark("enabled",!select.disabled);mark("initial",select.value);
mark("options",Array.from(select.options).map(function(o){return o.value}).join(","));
mark("actual",document.getElementById("cpu-frequency-status").textContent);
mark("clean",Object.keys(st.dirty).length===0);
select.value="160";select.dispatchEvent(new Event("input",{bubbles:true}));
mark("dirty",st.dirty["cpu.freq"]);appSave();
waitFor(function(){return saves.length===1&&st.orig["cpu.freq"]==="160"&&!st.dirty["cpu.freq"]},function(){
mark("saved",saves[0].set["cpu.freq"]);mark("only-key",Object.keys(saves[0].set).join(","));
mark("no-reboot",!saves[0].reboot);
mark("after-actual",document.getElementById("cpu-frequency-status").textContent);
select.value="auto";select.dispatchEvent(new Event("input",{bubbles:true}));appSave();
waitFor(function(){return saves.length===2&&st.orig["cpu.freq"]==="auto"&&!st.dirty["cpu.freq"]},function(){
mark("auto-saved",saves[1].set["cpu.freq"]);
select.value="160";select.dispatchEvent(new Event("input",{bubbles:true}));appSave();
waitFor(function(){return saves.length===3&&!document.getElementById("save-btn").disabled&&select.closest(".f").querySelector(".chip.err")},function(){
mark("rejected-dirty",st.dirty["cpu.freq"]);mark("rejected-saved",st.orig["cpu.freq"]);
mark("rejected-field",select.value);mark("rejected-actual",document.getElementById("cpu-frequency-status").textContent);
setTimeout(function(){mark("requests",saves.length)},200);
});
});
});
});},200);
});
})();
</script>''' % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude, virtual_time=4000)
for key, value in (("visible", "true"), ("enabled", "true"), ("initial", "auto"),
("options", "auto,80,160"), ("actual", "Running at 80 MHz"),
("clean", "true"), ("dirty", "160"), ("saved", "160"),
("only-key", "cpu.freq"), ("no-reboot", "true"),
("after-actual", "Running at 160 MHz"), ("auto-saved", "auto"),
("rejected-dirty", "160"), ("rejected-saved", "auto"),
("rejected-field", "160"), ("rejected-actual", "Running at 80 MHz"),
("requests", "3")):
self.assertIn('data-test-cpu-%s="%s"' % (key, value), dom)
def test_shipped_form_hides_unavailable_clock_and_can_load_240(self):
for cpu, values, visible, selected in (
(None, "auto", False, "auto"),
({"selected_mhz": 240, "actual_mhz": 240,
"supported_mhz": [240, 80, 160, 240, 20]}, "auto,80,160,240", True, "240")):
with self.subTest(cpu=cpu):
status, config = self.setup_values()
status.update(mode="lan")
if cpu is not None:
config["cpu"] = cpu
else:
config.pop("cpu", None)
prelude = r'''<script>
window.fetch=function(path){var value=path==="/api/status"?%s:%s;return Promise.resolve({ok:true,status:200,json:function(){return Promise.resolve(value)}})};
window.addEventListener("load",function(){
setTimeout(function(){enterApp()},50);
setTimeout(function(){
var select=document.querySelector('[data-k="cpu.freq"]'),body=document.body;
body.setAttribute("data-test-cpu-visible",!document.getElementById("cpu-frequency-setting").classList.contains("hide"));
body.setAttribute("data-test-cpu-disabled",select.disabled);
body.setAttribute("data-test-cpu-options",Array.from(select.options).map(function(o){return o.value}).join(","));
body.setAttribute("data-test-cpu-selected",select.value);
body.setAttribute("data-test-cpu-dirty",Object.keys(st.dirty).length);
},650);
});
</script>''' % (json.dumps(status), json.dumps(config))
dom = self.run_page(prelude)
for key, value in (("visible", str(visible).lower()),
("disabled", str(not visible).lower()),
("options", values), ("selected", selected), ("dirty", "0")):
self.assertIn('data-test-cpu-%s="%s"' % (key, value), dom)
if __name__ == "__main__":
unittest.main()
+21
View File
@@ -429,6 +429,10 @@ body.tab-cli{padding-bottom:0}
<span class="tgl"><input type="checkbox" data-k="radio.fem.rxgain"><u></u></span></div>
<div class="sw" data-cap="8192"><span><b>Device power saving</b><i>Reduce device and GPS idle power</i></span>
<span class="tgl"><input type="checkbox" data-k="powersaving"><u></u></span></div>
<div class="f hide" id="cpu-frequency-setting"><label>Processor speed</label>
<select data-k="cpu.freq" disabled><option value="auto">Auto (device policy)</option></select>
<div class="hint" id="cpu-frequency-status"></div>
<div class="hint">Power saving can still pause the processor while idle.</div></div>
<div class="hide" id="regulator-settings">
<h2>Power regulators</h2>
<div class="note">Auto follows the board default. Changes are saved and restart the node. Selecting a mode clears that regulator's automatic fallback.</div>
@@ -923,6 +927,7 @@ function cfgVal(k){ // map a `set` key to its current value string, from st.cfg
case"radio.fem.rxgain":return r.fem_rxgain?"on":"off";
case"radio.rxps":return rxpsCfgValue(r);
case"powersaving":return r.powersaving?"on":"off";
case"cpu.freq":return c.cpu&&c.cpu.selected_mhz?String(c.cpu.selected_mhz):"auto";
case"mcu.regulator":return ((c.regulators||{}).mcu||{}).requested||"auto";
case"radio.regulator":return ((c.regulators||{}).radio||{}).requested||"auto";
case"display.mode":return (st.cfg.display||{}).mode||"button";
@@ -1086,6 +1091,16 @@ function resetRegulatorFallback(domain){
// through the normal authenticated save/result/reboot flow.
st.dirty[key]=select.value;select.classList.add("dirty");updateSaveBar();
}
function syncCpuFrequency(c){
var cpu=c.cpu||{},select=$('[data-k="cpu.freq"]'),supported=cpu.supported_mhz;
var values=Array.isArray(supported)?[80,160,240].filter(function(mhz){return supported.indexOf(mhz)>=0}):[];
select.textContent="";
var automatic=document.createElement("option");automatic.value="auto";automatic.textContent="Auto (device policy)";select.appendChild(automatic);
values.forEach(function(mhz){var option=document.createElement("option");option.value=String(mhz);option.textContent=mhz+" MHz";select.appendChild(option)});
var available=values.length>0;
$("#cpu-frequency-setting").classList.toggle("hide",!available);select.disabled=!available;
$("#cpu-frequency-status").textContent=available&&cpu.actual_mhz>0?"Running at "+cpu.actual_mhz+" MHz":"";
}
function loadConfig(loadCapture){
var epoch=++st.configEpoch;
if(loadCapture)loadCapture.loadEpoch=epoch;
@@ -1096,6 +1111,7 @@ function loadConfig(loadCapture){
st.cfg=c;st.orig={};st.dirty={};st.skipRadio=false;st.skipMqtt=false;
st.wifiPasswordAutoCleared=false;
syncRegulators(c);
syncCpuFrequency(c);
if(st.hasMqtt){buildSlots("#app-slots");buildSlots("#wz-slots")}
$$("[data-k]").forEach(function(el){
var k=el.dataset.k,v=cfgVal(k);
@@ -1498,6 +1514,7 @@ function loadConfigSoft(){ // re-sync origin values without clobbering result ch
api("/api/config").then(function(c){
st.cfg=c;
syncRegulators(c);
syncCpuFrequency(c);
$$("[data-k]").forEach(function(el){
var k=el.dataset.k,stored=cfgVal(k);
st.orig[k]=stored;
@@ -2038,6 +2055,7 @@ var CLI_VERBS=[
["gps advert share","Advertise the live GPS position"],
["gps advert prefs","Advertise the stored lat/lon"],
["get powersaving","Show the power-saving mode"],
["get cpu.freq","Show selected, running and supported processor speeds"],
["set powersaving on","Enable power saving"],
["set powersaving off","Disable power saving"],
["get bluetooth.mac","Show the Bluetooth address mode"],
@@ -2116,6 +2134,7 @@ var CLI_KEYS=[
["webui","WiFi settings website",0,"on|off"],
["usb.logging","USB logging",0,"on|off"],
["powersaving","Device power saving",0,"on|off"],
["cpu.freq","Processor speed (get cpu.freq shows supported values)",0,"auto|80|160|240"],
["mcu.regulator","Processor regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["radio.regulator","LoRa regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["gps","GPS power",0,"on|off"],
@@ -2249,6 +2268,7 @@ var CLI_COMPANION_VERBS=[
["board","Board and hardware info"],
["memory","Heap and PSRAM free/min/largest block (Full Companion)"],
["get powersaving","Show device power saving"],
["get cpu.freq","Show selected, running and supported processor speeds"],
["set powersaving on","Enable device power saving"],
["set powersaving off","Disable device power saving"],
["tempradio","Show the temporary radio window (Full Companion)"],
@@ -2278,6 +2298,7 @@ var CLI_COMPANION_KEYS=[
["radio.fem.txgain","Front-end module TX gain",0,"on|off"],
["radio.rxps","Configured RX power saving",0,"on|off"],
["powersaving","Device power saving",0,"on|off"],
["cpu.freq","Processor speed (get cpu.freq shows supported values)",0,"auto|80|160|240"],
["mcu.regulator","Processor regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["radio.regulator","LoRa regulator (supported boards; reboot required)",0,"auto|dcdc|ldo"],
["usb.logging","USB packet logging (logging-enabled builds)",0,"on|off"],