Add ESP32 clock selection and bounded power test controls
This commit is contained in:
1 parent
11dbca377c
commit
6e599d1709
33 files changed
+4028
-2430
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+2396
-2370
File diff suppressed because it is too large.
Load diff
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('):
|
||||
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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; }
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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"],
|
||||
|
||||
Reference in new issue
Block a user