Enable reversible ESP32 BLE shutdown and bounded radio2 CPU sleep

This commit is contained in:
mikecarper committed 2026-10-21 17:53:57 -07:00
1 parent a3d77a9a76
commit 61eecbd971
38 files changed
+1829 -117

No files matched your search

+5
View File
@@ -699,6 +699,10 @@ jobs:
- name: Verify ESP32 USB reboot sleep grace and G3 button wake
run: |
python3 -B test/test_usb_host_sleep_policy.py
python3 -B test/test_esp32_light_sleep_policy.py -v
python3 -B test/test_esp32_profile_idle_sleep.py -v
python3 -B test/test_profile_idle_esp32.py -v
python3 -B test/test_profile_idle.py -v
python3 -B test/test_esp32_usb_sleep.py
python3 -B test/test_esp32_full_sleep.py
python3 -B test/test_esp32_full_power_lifecycle.py
@@ -997,6 +1001,7 @@ jobs:
python3 -B test/test_voice_prefixes.py -v
MESH_VOICE_TEST_SANITIZERS=1 python3 -B test/test_voice_prefixes.py -v
python3 -B test/test_nimble_companion.py
python3 -B test/test_esp32_ble_power.py -v
python3 -B test/test_bluetooth_pairing_ui.py
python3 -B test/test_companion_pairing_display.py
python3 -B test/test_companion_identity_startup.py
+19 -2
View File
@@ -45,8 +45,10 @@ after off starts a fresh window. Collection defaults off at every boot.
`radio2` means the scanner cannot safely idle right now: for example, a short
remaining visit, calibration, pending reception or an unvalidated platform.
On nRF52, settled long visits can idle the MCU with power saving enabled;
the RF receiver stays on. See [radio profiles](radio_profiles.md#mcu-power-saving-while-scanning).
On nRF52 and ESP32, settled long visits can idle the MCU with power saving
enabled; the RF receiver stays on. ESP32 requires working radio GPIO wake and
physically stopped WiFi/Bluetooth drivers. Short visits and active transports
retain polling. See [radio profiles](radio_profiles.md#mcu-power-saving-while-scanning).
The role reports the first actual short-circuit blocker. It does not query
otherwise skipped hardware just to populate an exhaustive list. Board guards
@@ -71,6 +73,21 @@ than the 32-bit `micros()` wrap (about 71 minutes); the normal wait paths here
are much shorter. Keep gaps between policy observations below the `millis()`
wrap (about 49 days).
## ESP32 Bluetooth off and CPU sleep
Switching Bluetooth off now completes a reversible host/controller shutdown,
including when Bluetooth was saved off at startup. Existing pairings, address,
name and PIN are retained when it is switched back on. A pending disconnect or
failed teardown keeps the CPU awake rather than treating stopped advertising
as a stopped controller. `get bluetooth` reports a pending change until that
transition finishes.
USB host sessions, logging, active WiFi/ESP-NOW, OTA and serial work keep their
existing sleep guards. This change does not sleep an ESP32 through a live
Bluetooth or WiFi connection. Such connections require SDK-managed modem and
automatic sleep; lowering CPU frequency or yielding alone is not proof of
light sleep.
## Noise-floor sampling and sleep
Startup still collects a complete baseline promptly: 64 accepted samples,
+9 -2
View File
@@ -932,8 +932,15 @@ retries, or short visits. All normal USB, logging, bridge, OTA, queue and role
sleep guards still apply. An nRF52 periodic interrupt bounds each wait to
about 0.977 ms; admission reserves another 0.488 ms before the visit deadline
and is rechecked immediately before sleeping. If that wake source is not
available, scanning keeps polling. Other MCU families retain polling until
their timing is validated.
available, scanning keeps polling.
ESP32 also permits MCU idle during settled long visits. It requires radio GPIO
wake and physically stopped WiFi/Bluetooth drivers. Its timer slices are at
most 10 ms, reserve 4 ms before the visit deadline, and require at least 2 ms
usable time. Absolute deadlines are checked again after wake-source setup;
expired visits, pending interrupts or setup failures retain polling. USB,
logging, active wireless services and the other role guards remain blockers.
Other MCU families retain polling until their timing is validated.
The integer admission gate requires at least 49 RTC2 ticks (about 1.495 ms)
remaining after the work checks. A configured visit must also cover the loop
+43 -26
View File
@@ -32,6 +32,8 @@ void companionPairingUiHilProbe();
#include "esp_pm.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include <helpers/esp32/ProfileIdleSleep.h>
#include <helpers/esp32/LightSleepPolicy.h>
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
#include <esp_bt_main.h>
#endif
@@ -3260,6 +3262,9 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
snprintf(reply, reply_size, "bluetooth %s%s",
interface_manager.isBluetoothEnabled() ? "on" : "off",
companion_bluetooth_off_at ? " (off pending)"
#if defined(ESP32_PLATFORM)
: bluetooth_interface.isPowerTransitionPending() ? " (change pending)"
#endif
: interface_manager.isBluetoothInhibited()
? " (USB data connected)" : "");
return true;
@@ -3286,6 +3291,11 @@ bool handleCompanionBluetoothCommand(const char* command, char* reply,
companion_bluetooth_off_at = 0;
snprintf(reply, reply_size,
"OK - Bluetooth on (saved); paused while USB data is connected");
#if defined(ESP32_PLATFORM)
} else if (bluetooth_interface.isPowerTransitionPending()) {
companion_bluetooth_off_at = 0;
snprintf(reply, reply_size, "OK - Bluetooth on (saved); start pending");
#endif
} else if (interface_manager.isBluetoothEnabled()) {
companion_bluetooth_off_at = 0;
snprintf(reply, reply_size, "OK - Bluetooth on (saved)");
@@ -3425,6 +3435,9 @@ public:
if (on) interface_manager.enableBluetooth();
else disableCompanionBluetoothForCli();
if (on && interface_manager.isBluetoothInhibited()) return Result::Done;
#if defined(ESP32_PLATFORM)
if (bluetooth_interface.isPowerTransitionPending()) return Result::Pending;
#endif
return interface_manager.isBluetoothEnabled() == on ? Result::Done : Result::Failed;
}
#endif
@@ -3495,28 +3508,14 @@ static bool companionNativeUsbLightSleepReady() {
enabled_services &= static_cast<uint8_t>(~mesh::wireless::WiFi);
}
}
#endif
#if defined(BLE_PIN_CODE)
if (bluetooth_interface.isPowerTransitionPending()) return false;
#endif
if (enabled_services != 0 || interface_manager.hasPendingIO()
|| board.isOTAUpdateRunning() || board.isRadioTestActive()
|| sensors.gpsUsesSerialUart(1)) return false;
#if defined(BLE_PIN_CODE) && !CONFIG_IDF_TARGET_ESP32C6
// Stopped advertising is not a stopped controller. Manual light sleep
// requires the RF driver to be disabled, including during BLE teardown.
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED)
return false;
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
if (esp_bluedroid_get_status() == ESP_BLUEDROID_STATUS_ENABLED) return false;
#endif
#endif
#if defined(ESP32) && defined(WIFI_SSID)
// Catch an unfinished teardown even if the logical WiFi owner is off.
wifi_mode_t mode = WIFI_MODE_NULL;
const esp_err_t result = esp_wifi_get_mode(&mode);
if (result == ESP_OK) return mode == WIFI_MODE_NULL;
return result == ESP_ERR_WIFI_NOT_INIT || result == ESP_ERR_WIFI_NOT_STARTED;
#else
return true;
#endif
return mesh::esp32::manualLightSleepReady();
}
#endif
@@ -4337,6 +4336,7 @@ void loop() {
if (noise_floor_wait) board.sleepMilliseconds(idle_slice_ms);
else board.sleep(0); // nrf ignores seconds param, sleeps whenever possible
#elif defined(ESP32_PLATFORM)
const uint32_t sleep_budget_started_us = static_cast<uint32_t>(esp_timer_get_time());
#if defined(ENABLE_USB_INTERFACE) \
&& defined(ARDUINO_USB_CDC_ON_BOOT) && ARDUINO_USB_CDC_ON_BOOT \
&& !defined(ETHERNET_ENABLED) && !defined(SERIAL_RX)
@@ -4348,6 +4348,13 @@ void loop() {
if (!companionNativeUsbLightSleepReady()) {
if (mesh::powerdiag::enabled()) mesh::powerdiag::boardVeto(Reason::Board, millis());
sleep_failed = true;
} else if (the_mesh.isDualRadioActive()) {
// The scanner owns a shorter absolute deadline than the normal loop
// cadence. The board also arms a radio GPIO wake and rechecks elapsed
// time after its setup; short/expired visits keep polling.
if (!mesh::esp32::tryProfileIdleSleep(the_mesh, board, sleep_budget_started_us, idle_slice_ms)
&& mesh::powerdiag::enabled())
mesh::powerdiag::decision(mask(Reason::DualRadio), millis());
} else {
esp_err_t error = esp_sleep_enable_timer_wakeup(static_cast<uint64_t>(idle_slice_ms) * 1000ULL);
if (error == ESP_OK) {
@@ -4360,17 +4367,22 @@ void loop() {
sleep_failed = true;
}
}
if (sleep_failed) {
if (sleep_failed && !the_mesh.isDualRadioActive()) {
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
}
#elif COMPANION_IDF_PM_AVAILABLE
// Yield long enough for ESP-IDF automatic light sleep to enter when no
// driver holds a power-management lock.
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
#else
// Connected transports need their own modem sleep and must retain the
// normal FreeRTOS idle behavior.
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
if (the_mesh.isDualRadioActive()) {
// A rounded RTOS delay can outlast a scanner visit. Use the same
// absolute, GPIO-wake deadline as infrastructure, with board USB,
// logging and physical wireless guards. A veto keeps polling.
if (!mesh::esp32::tryProfileIdleSleep(the_mesh, board, sleep_budget_started_us, idle_slice_ms)
&& mesh::powerdiag::enabled())
mesh::powerdiag::decision(mask(Reason::DualRadio), millis());
} else {
// PM-enabled cores may use automatic sleep; other connected transports
// retain their modem sleep and normal FreeRTOS idle behavior.
vTaskDelay(pdMS_TO_TICKS(idle_slice_ms));
}
#endif
#elif defined(RP2040_PLATFORM) || defined(STM32_PLATFORM)
if (noise_floor_wait) board.sleepMilliseconds(idle_slice_ms);
@@ -4537,6 +4549,11 @@ void loop() {
#endif
serviceCompanionBluetoothIdentity();
serviceCompanionBluetoothControl();
#if defined(ESP32_PLATFORM)
// A logical off request must finish physical host/controller teardown even
// while this interface is disabled and no longer polled for frames.
bluetooth_interface.servicePowerState();
#endif
#if defined(NRF52_PLATFORM)
serviceCompanionBluetoothName();
#endif
+11
View File
@@ -24,6 +24,7 @@
#endif
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
#include <helpers/esp32/ProfileIdleSleep.h>
#include <helpers/esp32/BootFileSystem.h>
#endif
#if defined(NRF52_PLATFORM)
@@ -674,6 +675,9 @@ void loop() {
can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button);
#endif
if (can_power_save) {
#if defined(ESP32_PLATFORM)
const uint32_t sleep_budget_started_us = static_cast<uint32_t>(esp_timer_get_time());
#endif
uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000);
uint32_t noise_floor_delay_ms = 0;
const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms);
@@ -695,6 +699,13 @@ void loop() {
#else
if (sleep_ms > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
#if defined(ESP32_PLATFORM)
if (the_mesh.isDualRadioActive()) {
if (!mesh::esp32::tryProfileIdleSleep(the_mesh, board, sleep_budget_started_us, sleep_ms)
&& mesh::powerdiag::enabled())
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::DualRadio), millis());
} else
#endif
board.sleepMilliseconds(sleep_ms); // Wake for scheduled jobs or a LoRa packet.
} else if (sleep_ms > 0 && mesh::powerdiag::enabled()) {
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
+11
View File
@@ -21,6 +21,7 @@
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
#include <helpers/esp32/ProfileIdleSleep.h>
#include <helpers/esp32/BootFileSystem.h>
#endif
#if defined(NRF52_PLATFORM)
@@ -446,6 +447,9 @@ void loop() {
can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button);
#endif
if (can_power_save) {
#if defined(ESP32_PLATFORM)
const uint32_t sleep_budget_started_us = static_cast<uint32_t>(esp_timer_get_time());
#endif
uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000);
uint32_t noise_floor_delay_ms = 0;
const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms);
@@ -467,6 +471,13 @@ void loop() {
#else
if (sleep_ms > 0 && the_mesh.millisHasNowPassed(POWERSAVING_FIRSTSLEEP_SECS * 1000)) { // To check if it is time to sleep
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
#if defined(ESP32_PLATFORM)
if (the_mesh.isDualRadioActive()) {
if (!mesh::esp32::tryProfileIdleSleep(the_mesh, board, sleep_budget_started_us, sleep_ms)
&& mesh::powerdiag::enabled())
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::DualRadio), millis());
} else
#endif
board.sleepMilliseconds(sleep_ms); // Wake for scheduled jobs or a LoRa packet.
} else if (sleep_ms > 0 && mesh::powerdiag::enabled()) {
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
+11
View File
@@ -58,6 +58,7 @@ static bool armTrackerSleepWake(uint32_t seconds) {
#if defined(ESP32_PLATFORM)
#include <helpers/ESP32TrueRandom.h>
#include <helpers/esp32/ProfileIdleSleep.h>
#endif
#ifdef DISPLAY_CLASS
@@ -405,6 +406,9 @@ void loop() {
can_power_save = can_power_save && sleep_guard(!user_btn.needsPolling(), mesh::powerdiag::Reason::Button);
#endif
if (can_power_save) {
#if defined(ESP32_PLATFORM)
const uint32_t sleep_budget_started_us = static_cast<uint32_t>(esp_timer_get_time());
#endif
uint32_t sleep_ms = the_mesh.getPowerSaveSleepMilliseconds(30000);
uint32_t noise_floor_delay_ms = 0;
const bool noise_floor_wait = the_mesh.getNextNoiseFloorWakeDelay(noise_floor_delay_ms);
@@ -444,6 +448,13 @@ void loop() {
&& the_mesh.millisHasNowPassed(
POWERSAVING_FIRST_SLEEP_SECS * 1000UL)) {
if (mesh::powerdiag::enabled()) mesh::powerdiag::decision(0, millis());
#if defined(ESP32_PLATFORM)
if (the_mesh.isDualRadioActive()) {
if (!mesh::esp32::tryProfileIdleSleep(the_mesh, board, sleep_budget_started_us, sleep_ms)
&& mesh::powerdiag::enabled())
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::DualRadio), millis());
} else
#endif
board.sleepMilliseconds(sleep_ms);
} else if (sleep_ms > 0 && mesh::powerdiag::enabled()) {
mesh::powerdiag::decision(mesh::powerdiag::mask(mesh::powerdiag::Reason::Startup), millis());
+12
View File
@@ -663,6 +663,18 @@ public:
return radio_available && _radio->getProfileIdleDelay(delay_millis)
&& delay_millis > 0;
}
// A timed-sleep caller must use the remaining visit budget, not just the
// boolean admission. Single-profile radios retain their existing policy.
// Read the sleep clock before calling, cap the caller's absolute deadline,
// and fail closed if setup/preemption consumes that budget before sleeping.
bool getDualRadioIdleDelay(uint32_t& delay_millis) const {
delay_millis = 0;
uint32_t profile_millis = 0;
if (!isDualRadioActive() || !radio_available
|| !_radio->getProfileIdleDelay(profile_millis) || !profile_millis) return false;
delay_millis = profile_millis;
return true;
}
Packet* obtainNewPacket();
void releasePacket(Packet* packet);
+55 -13
View File
@@ -20,6 +20,7 @@
#include "UsbLogging.h"
#include "UsbHostSleepPolicy.h"
#include "PowerSavingDiagnostics.h"
#include "esp32/LightSleepPolicy.h"
#if defined(STATION_G3_ESP32)
#include <StationG3RadioSlot.h>
#endif
@@ -137,6 +138,18 @@ public:
}
void sleepMicroseconds(uint64_t wake_us, bool require_timer = false) {
sleepMicrosecondsImpl(wake_us, require_timer, false, 0);
}
// Radio-profile visits use the low 32 bits of esp_timer_get_time()/micros().
// An absolute deadline prevents setup work or a task preemption from making
// the sleep slice outlast the visit which the scanner proved safe.
void sleepUntilMicroseconds(uint32_t deadline_us) {
sleepMicrosecondsImpl(0, true, true, deadline_us);
}
void sleepMicrosecondsImpl(uint64_t wake_us, bool require_timer,
bool has_deadline, uint32_t deadline_us) {
using mesh::powerdiag::Reason;
const bool diagnostics = mesh::powerdiag::enabled();
// Native USB loses its connection in light sleep. An enumerated host
@@ -166,9 +179,13 @@ public:
// it and across host disconnects. Compiled-out logging is not a blocker.
keep_awake = keep_awake || hold(mesh::isUsbLoggingEnabled(), Reason::UsbLogging);
#endif
if (!keep_awake) {
const Reason wireless = mesh::esp32::manualLightSleepBlocker();
keep_awake = hold(wireless != Reason::Count, wireless);
}
if (keep_awake) {
if (diagnostics) mesh::powerdiag::boardVeto(veto_reason, millis(), veto_detail);
delay(1); // Keep USB and OTA tasks running.
if (!has_deadline) delay(1); // A bounded profile visit must retain polling.
return;
}
@@ -176,7 +193,7 @@ public:
// Otherwise its real elapsed time would be mislabelled as blocked time.
esp_err_t setup_error = ESP_OK;
// Configure timer wakeup
if (wake_us > 0) {
if (!has_deadline && wake_us > 0) {
const esp_err_t error = esp_sleep_enable_timer_wakeup(wake_us);
// A calibration slice must not inherit an old, longer timer after a
// setup failure. Keep the legacy seconds API's policy unchanged.
@@ -189,6 +206,11 @@ public:
const uint32_t irqGpio = getIRQGpio();
const bool radio_wakeup = irqGpio != static_cast<uint32_t>(-1);
if (has_deadline && !radio_wakeup) {
// A timer-only wake cannot service a newly arrived profile packet.
if (diagnostics) mesh::powerdiag::boardVeto(Reason::NoWake, millis());
return;
}
#if defined(MOMENTARY_BUTTON_WAKE_FROM_SLEEP) \
&& MOMENTARY_BUTTON_WAKE_FROM_SLEEP && defined(PIN_USER_BTN)
const int button_gpio = PIN_USER_BTN;
@@ -225,28 +247,43 @@ public:
if (diagnostics) mesh::powerdiag::boardVeto(
radio_pending ? Reason::Mesh : Reason::Button, millis(),
radio_pending ? static_cast<int32_t>(irqGpio) : button_gpio);
delay(1);
if (!has_deadline) delay(1);
return;
}
// Configure GPIO wakeup
const esp_err_t gpio_error = esp_sleep_enable_gpio_wakeup();
if (diagnostics && setup_error == ESP_OK && gpio_error != ESP_OK) setup_error = gpio_error;
// Configure each level source before enabling global GPIO wake. Setup
// errors must veto sleep even with diagnostics off: a timer alone cannot
// replace the pending radio/button event which this caller must service.
esp_err_t gpio_error = ESP_OK;
if (radio_wakeup) {
const esp_err_t error = gpio_wakeup_enable(wakeupPin, GPIO_INTR_HIGH_LEVEL);
if (diagnostics && setup_error == ESP_OK && error != ESP_OK) setup_error = error;
if (error != ESP_OK) gpio_error = error;
}
if (button_gpio >= 0) {
const esp_err_t error = gpio_wakeup_enable((gpio_num_t)button_gpio,
USER_BTN_PRESSED == LOW ? GPIO_INTR_LOW_LEVEL : GPIO_INTR_HIGH_LEVEL);
if (diagnostics && setup_error == ESP_OK && error != ESP_OK) setup_error = error;
if (gpio_error == ESP_OK && error != ESP_OK) gpio_error = error;
}
if (gpio_error == ESP_OK) gpio_error = esp_sleep_enable_gpio_wakeup();
// MCU enters light sleep
if (diagnostics) mesh::powerdiag::sleepBegin(micros());
const esp_err_t sleep_error = esp_light_sleep_start();
if (diagnostics) {
mesh::powerdiag::sleepEnd(micros());
// The level source also catches an IRQ asserted while configuring wake.
// Avoid entering the SDK at all if its pending state is already visible.
const bool pending_after_setup = (radio_wakeup && gpio_get_level(wakeupPin) == HIGH)
|| (button_gpio >= 0
&& gpio_get_level((gpio_num_t)button_gpio) == USER_BTN_PRESSED);
bool deadline_expired = false;
if (has_deadline && gpio_error == ESP_OK && !pending_after_setup) {
// 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;
else gpio_error = esp_sleep_enable_timer_wakeup(static_cast<uint32_t>(remaining_us));
}
esp_err_t sleep_error = ESP_OK;
if (gpio_error == ESP_OK && !pending_after_setup && !deadline_expired) {
if (diagnostics) mesh::powerdiag::sleepBegin(micros());
sleep_error = esp_light_sleep_start();
if (diagnostics) mesh::powerdiag::sleepEnd(micros());
}
// Avoid ISR flood during wakeup due to HIGH LEVEL interrupt
@@ -264,7 +301,12 @@ public:
portEXIT_CRITICAL(&sleepMux);
if (diagnostics) {
if (setup_error != ESP_OK) mesh::powerdiag::boardError(setup_error, millis());
if (gpio_error != ESP_OK) mesh::powerdiag::boardError(gpio_error, millis());
if (sleep_error != ESP_OK) mesh::powerdiag::boardError(sleep_error, millis());
if (pending_after_setup) mesh::powerdiag::boardVeto(
radio_wakeup && gpio_get_level(wakeupPin) == HIGH ? Reason::Mesh : Reason::Button,
millis());
if (deadline_expired) mesh::powerdiag::boardVeto(Reason::Scheduled, millis());
}
}
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <esp_wifi.h>
#include <helpers/PowerSavingDiagnostics.h>
#if defined(CONFIG_BT_ENABLED) && CONFIG_BT_ENABLED
#include <esp_bt.h>
#if defined(CONFIG_BLUEDROID_ENABLED) && CONFIG_BLUEDROID_ENABLED \
&& !defined(MESH_USE_NIMBLE_ARDUINO)
#include <esp_bt_main.h>
#endif
#endif
namespace mesh {
namespace esp32 {
// Explicit light sleep powers down the WiFi/Bluetooth peripheral, even when
// its connections are idle. Only automatic SDK sleep can preserve an active
// driver. Read the physical drivers: a logical service-off flag can precede
// asynchronous teardown, and ESP-NOW may own WiFi without Arduino's facade.
inline powerdiag::Reason manualLightSleepBlocker() {
#if defined(CONFIG_BT_ENABLED) && CONFIG_BT_ENABLED
const esp_bt_controller_status_t controller = esp_bt_controller_get_status();
if (controller != ESP_BT_CONTROLLER_STATUS_IDLE
&& controller != ESP_BT_CONTROLLER_STATUS_INITED)
return powerdiag::Reason::Wireless;
#if defined(CONFIG_BLUEDROID_ENABLED) && CONFIG_BLUEDROID_ENABLED \
&& !defined(MESH_USE_NIMBLE_ARDUINO)
const esp_bluedroid_status_t host = esp_bluedroid_get_status();
if (host != ESP_BLUEDROID_STATUS_UNINITIALIZED
&& host != ESP_BLUEDROID_STATUS_INITIALIZED)
return powerdiag::Reason::Wireless;
#endif
#endif
wifi_mode_t mode = WIFI_MODE_NULL;
const esp_err_t result = esp_wifi_get_mode(&mode);
if (result == ESP_OK)
return mode == WIFI_MODE_NULL ? powerdiag::Reason::Count
: powerdiag::Reason::Network;
if (result == ESP_ERR_WIFI_NOT_INIT || result == ESP_ERR_WIFI_NOT_STARTED)
return powerdiag::Reason::Count;
return powerdiag::Reason::Network; // An unknown driver state is not off.
}
inline bool manualLightSleepReady() {
return manualLightSleepBlocker() == powerdiag::Reason::Count;
}
} // namespace esp32
} // namespace mesh
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <stdint.h>
#include <esp_timer.h>
namespace mesh {
namespace esp32 {
// Preserve both the role's original wake deadline and the scanner's late
// admission. The board checks this absolute deadline again after configuring
// wake sources, so preemption cannot turn a short visit into a longer sleep.
// True means a board sleep was attempted, not proof that the SDK slept.
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;
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)
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;
const uint32_t profile_us = profile_ms * 1000UL;
const uint32_t remaining_us = static_cast<uint32_t>(role_remaining_us) < profile_us
? static_cast<uint32_t>(role_remaining_us) : profile_us;
board.sleepUntilMicroseconds(observed_us + remaining_us);
return true;
}
} // namespace esp32
} // namespace mesh
+247 -51
View File
@@ -2,12 +2,15 @@
#include "../BluetoothMac.h"
#include "../CompanionFrameQueue.h"
#include "esp_mac.h"
#include "esp_bt.h"
#include <stdlib.h>
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
#include "esp_gap_ble_api.h"
#include "esp_bt_main.h"
#endif
#if MESH_BLE_USES_NIMBLE && !defined(MESH_USE_NIMBLE_ARDUINO)
#include <host/ble_store.h>
#include <nimble/nimble_port.h>
#endif
// Arduino 3.x relies on a library constructor to mark BLE as used before
@@ -29,12 +32,83 @@ extern "C" bool bleInUse(void) {
#define ADVERT_RESTART_DELAY 1000 // millis
#define BLE_BOND_PERSIST_TIMEOUT_MS 15000
static void cleanupFailedBluetoothStart() {
#if defined(MESH_USE_NIMBLE_ARDUINO)
BLEDevice::deinit(true);
#else
BLEDevice::deinit(false);
#define BLE_POWER_RETRY_MS 1000
static bool stopBluetoothController() {
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_ENABLED
&& esp_bt_controller_disable() != ESP_OK) return false;
if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_INITED
&& esp_bt_controller_deinit() != ESP_OK) return false;
return esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
// The pinned 2.0.17/3.1.3 wrappers leave their static server/advertiser alive
// on deinit(false). The newer wrapper exposing getServer() deletes them.
// Keep that ownership distinction independent of the processor model.
template <typename Device>
static auto bluetoothLibraryOwnsServer(int)
-> decltype(Device::getServer(), bool()) { return true; }
template <typename Device>
static bool bluetoothLibraryOwnsServer(...) { return false; }
#endif
void SerialBLEInterface::forgetGattPointers() {
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
pRxCharacteristic = NULL;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxDescriptor = NULL;
#endif
}
bool SerialBLEInterface::stopBluetoothStack() {
_callbacksEnabled.store(false, std::memory_order_release);
#if defined(MESH_USE_NIMBLE_ARDUINO)
// clearAll is OBJECT ownership, not bond storage. Stop first without
// deleting objects: a failed stop must leave callback targets valid.
if (!BLEDevice::deinit(false)) return false;
if (!stopBluetoothController()) return false;
BLEDevice::deinit(true);
forgetGattPointers();
_advertisingConfigured = false;
#else
#if defined(CONFIG_BLUEDROID_ENABLED)
// Stop callback dispatch before the newer wrapper deletes its server.
// Never release controller memory: Bluetooth on must work in this boot.
if (esp_bluedroid_get_status() == ESP_BLUEDROID_STATUS_ENABLED
&& esp_bluedroid_disable() != ESP_OK) return false;
if (esp_bluedroid_get_status() == ESP_BLUEDROID_STATUS_INITIALIZED
&& esp_bluedroid_deinit() != ESP_OK) return false;
#elif defined(CONFIG_NIMBLE_ENABLED)
if (!_nativeHostStopped) {
if (nimble_port_stop() != 0) return false;
_nativeHostStopped = true;
}
#endif
#if defined(CONFIG_BLUEDROID_ENABLED)
if (!stopBluetoothController()) return false;
#endif
const bool owns_server = bluetoothLibraryOwnsServer<BLEDevice>(0);
BLEDevice::deinit(false);
// Neither pinned Arduino wrapper owns our characteristics/service. The
// legacy server is retained until createServer() replaces its static
// pointer; deleting it sooner would leave a dangling callback target.
delete pTxDescriptor;
delete pRxCharacteristic;
delete pTxCharacteristic;
delete pService;
pTxDescriptor = NULL;
pRxCharacteristic = pTxCharacteristic = NULL;
pService = NULL;
if (owns_server) {
pServer = NULL;
_advertisingConfigured = false;
}
if (!stopBluetoothController()) return false;
#endif
return true;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
@@ -276,6 +350,8 @@ bool SerialBLEInterface::configureBondedOnlyAdvertising(
advertising->setAdvertisementData(minimal_advertisement);
_bonded_only = true;
_bondedPeer = peer;
_hasBondedPeer = true;
_stealth_pair_once = false;
_advertisingSuppressed.store(false, std::memory_order_release);
_adv_restart_pending = false;
@@ -310,7 +386,9 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
bool clear_bonds, bool stealth_pair_once,
const mesh::companion::BluetoothPeerIdentity*
bonded_only_peer) {
if (_stackActive || _shutdownPending) return false;
_pin_code = pin_code;
_hasBondedPeer = false;
_successfulConnectionPending.store(false, std::memory_order_release);
_successfulConnectionStarted.store(0, std::memory_order_relaxed);
_bondedOnlyRecoveryPending.store(false, std::memory_order_release);
@@ -344,22 +422,58 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
return false;
}
memcpy(_deviceName, dev_name, dev_name_len + 1);
_hasCustomAddress = custom_address != nullptr;
if (_hasCustomAddress) {
memcpy(_customAddress, custom_address, sizeof(_customAddress));
}
if (bonded_only_peer != nullptr) {
_bondedPeer = *bonded_only_peer;
_hasBondedPeer = true;
_bonded_only = true;
}
_configured = true;
if (startBluetoothStack(clear_bonds)) return true;
servicePowerState();
return false;
}
bool SerialBLEInterface::startBluetoothStack(bool clear_bonds) {
const char* dev_name = _deviceName;
const uint8_t* custom_address = _hasCustomAddress ? _customAddress : nullptr;
const uint32_t pin_code = _pin_code;
// A failed start can have partially initialized the controller. Keep the
// shutdown gate closed until servicePowerState() has cleaned it up.
_stackActive = true;
_nativeHostStopped = false;
_callbacksEnabled.store(false, std::memory_order_release);
// Create the BLE Device
#if MESH_BLE_USES_NIMBLE
if (!BLEDevice::init(dev_name)) {
BLE_DEBUG_PRINTLN("BLEDevice::init failed");
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#else
// Arduino-ESP32 2.x Bluedroid exposes a void init(). Its first observable
// failure is a null server below, which is handled without dereferencing it.
BLEDevice::init(dev_name);
#if defined(CONFIG_BLUEDROID_ENABLED)
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED
|| esp_bluedroid_get_status() != ESP_BLUEDROID_STATUS_ENABLED) {
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#endif
#endif
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (clear_bonds && !BLEDevice::deleteAllBonds()) {
BLE_DEBUG_PRINTLN("Could not clear NimBLE bonds for the new identity");
cleanupFailedBluetoothStart();
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#else
@@ -376,13 +490,15 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
if (!BLEDevice::setOwnAddr(native_address)
|| !BLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM)) {
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC setup failed");
cleanupFailedBluetoothStart();
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
else if (!BLEDevice::setOwnAddrType(BLE_OWN_ADDR_PUBLIC)) {
cleanupFailedBluetoothStart();
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#endif
@@ -424,10 +540,19 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
//BLEDevice::setPower(ESP_PWR_LVL_N8);
// Create the BLE Server
#if !defined(MESH_USE_NIMBLE_ARDUINO)
BLEServer* retired_server = pServer;
#endif
pServer = BLEDevice::createServer();
#if !defined(MESH_USE_NIMBLE_ARDUINO)
// createServer has replaced the legacy library's static callback pointer.
// Its old server can now be freed, without ever exposing a dangling one.
delete retired_server;
#endif
if (pServer == NULL) {
BLE_DEBUG_PRINTLN("BLEDevice::createServer failed");
cleanupFailedBluetoothStart();
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
@@ -454,8 +579,8 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
pService = pServer->createService(SERVICE_UUID);
if (pService == NULL) {
BLE_DEBUG_PRINTLN("BLEServer::createService failed");
cleanupFailedBluetoothStart();
pServer = NULL;
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
@@ -479,9 +604,8 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
CHARACTERISTIC_UUID_TX, tx_properties);
if (pTxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(TX) failed");
cleanupFailedBluetoothStart();
pServer = NULL;
pService = NULL;
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
@@ -500,17 +624,12 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
pTxCharacteristic->addDescriptor(pTxDescriptor);
#endif
BLECharacteristic * pRxCharacteristic = pService->createCharacteristic(
pRxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_RX, rx_properties);
if (pRxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(RX) failed");
cleanupFailedBluetoothStart();
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxDescriptor = NULL;
#endif
_isEnabled = _enableRequested = false;
_shutdownPending = true;
return false;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
@@ -522,10 +641,16 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
pServer->getAdvertising()->setName(dev_name);
pServer->getAdvertising()->enableScanResponse(true);
#endif
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
if (bonded_only_peer != nullptr) {
configureBondedOnlyAdvertising(*bonded_only_peer, true);
if (!_advertisingConfigured) {
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
_advertisingConfigured = true;
}
if (_bonded_only && _hasBondedPeer) {
const bool suppressed = _advertisingSuppressed.load(std::memory_order_acquire);
configureBondedOnlyAdvertising(_bondedPeer, true);
if (suppressed) _advertisingSuppressed.store(true, std::memory_order_release);
}
_callbacksEnabled.store(true, std::memory_order_release);
return true;
}
@@ -533,11 +658,13 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
#if defined(MESH_USE_NIMBLE_ARDUINO)
uint32_t SerialBLEInterface::onPassKeyDisplay() {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return _pin_code;
_pairingRequestPending.store(true, std::memory_order_release);
return _pin_code;
}
void SerialBLEInterface::onAuthenticationComplete(NimBLEConnInfo& info) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
if (pServer == NULL || pServer->getConnectedCount() == 0
|| info.getConnHandle() != last_conn_id) return;
if (info.isEncrypted() && info.isAuthenticated() && info.isBonded()) {
@@ -562,17 +689,20 @@ void SerialBLEInterface::onAuthenticationComplete(NimBLEConnInfo& info) {
}
#else
uint32_t SerialBLEInterface::onPassKeyRequest() {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return _pin_code;
BLE_DEBUG_PRINTLN("onPassKeyRequest()");
_pairingRequestPending.store(true, std::memory_order_release);
return _pin_code;
}
void SerialBLEInterface::onPassKeyNotify(uint32_t pass_key) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
BLE_DEBUG_PRINTLN("onPassKeyNotify(%u)", pass_key);
_pairingRequestPending.store(true, std::memory_order_release);
}
bool SerialBLEInterface::onConfirmPIN(uint32_t pass_key) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return false;
BLE_DEBUG_PRINTLN("onConfirmPIN(%u)", pass_key);
_pairingRequestPending.store(true, std::memory_order_release);
return true;
@@ -580,11 +710,12 @@ bool SerialBLEInterface::onConfirmPIN(uint32_t pass_key) {
bool SerialBLEInterface::onSecurityRequest() {
BLE_DEBUG_PRINTLN("onSecurityRequest()");
return true; // allow
return _callbacksEnabled.load(std::memory_order_acquire);
}
#if MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
const bool success = desc != NULL && desc->sec_state.encrypted &&
desc->sec_state.authenticated
&& desc->sec_state.bonded;
@@ -621,6 +752,7 @@ void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) {
}
#else
void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
if (cmpl.success) {
BLE_DEBUG_PRINTLN(" - SecurityCallback - Authentication Success");
deviceConnected = true;
@@ -662,6 +794,11 @@ void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onConnect(BLEServer* pServer, NimBLEConnInfo& info) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) {
last_conn_id = info.getConnHandle();
pServer->disconnect(last_conn_id);
return;
}
last_conn_id = info.getConnHandle();
deviceConnected = false;
oldDeviceConnected = false;
@@ -682,6 +819,11 @@ void SerialBLEInterface::onConnect(BLEServer* pServer) {
#if MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onConnect(BLEServer* pServer, ble_gap_conn_desc* desc) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) {
last_conn_id = desc->conn_handle;
pServer->disconnect(last_conn_id);
return;
}
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", desc->conn_handle,
pServer->getPeerMTU(desc->conn_handle));
last_conn_id = desc->conn_handle;
@@ -703,6 +845,11 @@ void SerialBLEInterface::onMtuChanged(BLEServer* pServer,
}
#else
void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t *param) {
if (!_callbacksEnabled.load(std::memory_order_acquire)) {
last_conn_id = param->connect.conn_id;
pServer->disconnect(last_conn_id);
return;
}
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", param->connect.conn_id, pServer->getPeerMTU(param->connect.conn_id));
last_conn_id = param->connect.conn_id;
deviceConnected = false; // becomes usable only after authentication completes
@@ -734,6 +881,7 @@ void SerialBLEInterface::onDisconnect(BLEServer* pServer, NimBLEConnInfo& info,
#else
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
#endif
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
BLE_DEBUG_PRINTLN("onDisconnect()");
deviceConnected = false;
notifySucceeded = false;
@@ -763,6 +911,7 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
esp_ble_gatts_cb_param_t* param) {
(void)param;
#endif
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
if (_tx_disconnect_recovery.pending()) {
BLE_DEBUG_PRINTLN("onWrite(): dropping frame while BLE reconnect is pending");
return;
@@ -801,6 +950,7 @@ void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
(void)desc;
#endif
#if MESH_BLE_USES_NIMBLE
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
if (pCharacteristic == pTxCharacteristic) {
// NimBLE uses bit 0 for notification subscriptions.
notificationsEnabled.store((subValue & 0x0001u) != 0,
@@ -812,6 +962,7 @@ void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
#if !defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onStatus(BLECharacteristic* pCharacteristic, Status status, uint32_t code) {
(void)pCharacteristic;
if (!_callbacksEnabled.load(std::memory_order_acquire)) return;
notifySucceeded = status == SUCCESS_NOTIFY;
if (!notifySucceeded) {
BLE_DEBUG_PRINTLN("notify failed, status=%d, code=%u; retaining frame",
@@ -926,46 +1077,89 @@ void SerialBLEInterface::recoverStalledTx(const char* cause) {
serviceTxRecovery((uint32_t)millis());
}
void SerialBLEInterface::enable() {
if (_isEnabled) return;
_isEnabled = true;
void SerialBLEInterface::startServiceAndAdvertising() {
if (pServer == NULL || pService == NULL) return;
clearBuffers();
// Start the service
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (!pServer->start()) {
_isEnabled = false;
_isEnabled = _enableRequested = false;
_shutdownPending = true;
_callbacksEnabled.store(false, std::memory_order_release);
return;
}
#else
pService->start();
#endif
// Start advertising
//pServer->getAdvertising()->setMinInterval(500);
//pServer->getAdvertising()->setMaxInterval(1000);
_isEnabled = true;
if (advertisingAllowed()) {
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (!pServer->getAdvertising()->start()) {
_isEnabled = _enableRequested = false;
_shutdownPending = true;
_callbacksEnabled.store(false, std::memory_order_release);
pServer->getAdvertising()->stop();
}
#else
pServer->getAdvertising()->start();
#endif
}
_adv_restart_pending = false;
}
void SerialBLEInterface::enable() {
if (_isEnabled || !_configured) return;
_enableRequested = true;
if (_shutdownPending) {
// Finish a previously requested disconnect/stop before rebuilding. A
// second toggle must never revive half of a stopped host.
servicePowerState();
return;
}
if (!_stackActive && !startBluetoothStack(false)) {
servicePowerState();
return;
}
startServiceAndAdvertising();
}
void SerialBLEInterface::disable() {
_isEnabled = false;
BLE_DEBUG_PRINTLN("SerialBLEInterface::disable");
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pService->stop();
#endif
oldDeviceConnected = deviceConnected = false;
clearBuffers();
_isEnabled = _enableRequested = false;
_adv_restart_pending = false;
// The caller drains its command reply before this point. Only stop
// advertising/disconnect here; the host is stopped in the loop task after
// its asynchronous disconnect has completed.
servicePowerState();
}
void SerialBLEInterface::servicePowerState() {
const uint32_t now = (uint32_t)millis();
if (!_enableRequested && _stackActive && !_shutdownPending) {
_shutdownPending = true;
_powerAttempted = false;
_callbacksEnabled.store(false, std::memory_order_release);
_adv_restart_pending = false;
if (pServer != NULL) pServer->getAdvertising()->stop();
oldDeviceConnected = deviceConnected = false;
}
if (!_shutdownPending) return;
if (_powerAttempted
&& !mesh::bleElapsedAtLeast(now, _lastPowerAttempt, BLE_POWER_RETRY_MS))
return;
_powerAttempted = true;
_lastPowerAttempt = now;
if (pServer != NULL && pServer->getConnectedCount() != 0) {
pServer->disconnect(last_conn_id);
return;
}
if (!stopBluetoothStack()) return;
_stackActive = false;
_shutdownPending = false;
_powerAttempted = false;
clearBuffers();
_pairingRequestPending.store(false, std::memory_order_release);
_successfulConnectionPending.store(false, std::memory_order_release);
_successfulConnectionStarted.store(0, std::memory_order_relaxed);
if (_enableRequested && startBluetoothStack(false)) startServiceAndAdvertising();
}
size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) {
@@ -1026,6 +1220,7 @@ bool SerialBLEInterface::isWriteBusy() const {
size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
const uint32_t now = (uint32_t)millis();
if (!_isEnabled || !_stackActive || _shutdownPending) return 0;
servicePendingTxReset();
if (_tx_disconnect_recovery.pending()) {
serviceTxRecovery(now);
@@ -1137,5 +1332,6 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
}
bool SerialBLEInterface::isConnected() const {
return !_tx_disconnect_recovery.pending() && deviceConnected;
return _isEnabled && _stackActive && !_shutdownPending
&& !_tx_disconnect_recovery.pending() && deviceConnected;
}
+28
View File
@@ -29,6 +29,7 @@ class SerialBLEInterface : public BaseSerialInterface,
BLEServer *pServer;
BLEService *pService;
BLECharacteristic * pTxCharacteristic;
BLECharacteristic *pRxCharacteristic;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
BLE2902 *pTxDescriptor;
#endif
@@ -37,6 +38,22 @@ class SerialBLEInterface : public BaseSerialInterface,
bool notifySucceeded;
std::atomic<bool> notificationsEnabled{false};
bool _isEnabled;
bool _enableRequested = false;
// This interface outlives each host/controller session. Disable callbacks
// before stopping the host, and only then destroy its GATT objects.
std::atomic<bool> _callbacksEnabled{false};
bool _stackActive = false;
bool _shutdownPending = false;
bool _nativeHostStopped = false;
bool _configured = false;
bool _powerAttempted = false;
uint32_t _lastPowerAttempt = 0;
char _deviceName[48] = {};
uint8_t _customAddress[mesh::companion::BLUETOOTH_MAC_BYTES] = {};
bool _hasCustomAddress = false;
bool _advertisingConfigured = false;
bool _hasBondedPeer = false;
mesh::companion::BluetoothPeerIdentity _bondedPeer;
uint16_t last_conn_id;
uint32_t _pin_code;
uint32_t _last_write;
@@ -67,6 +84,10 @@ class SerialBLEInterface : public BaseSerialInterface,
Frame send_queue[FRAME_QUEUE_SIZE];
void clearBuffers();
bool startBluetoothStack(bool clear_bonds);
bool stopBluetoothStack();
void forgetGattPointers();
void startServiceAndAdvertising();
bool hasRequiredReplyCapacity() const;
void servicePendingTxReset();
void scheduleAdvertisingRestart(uint32_t now);
@@ -131,6 +152,7 @@ public:
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
pRxCharacteristic = NULL;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxDescriptor = NULL;
#endif
@@ -171,6 +193,12 @@ public:
void enable() override;
void disable() override;
bool isEnabled() const override { return _isEnabled; }
// Call from the application task even while the logical interface is off.
// Disconnects are asynchronous; never stop a host from its own callback.
void servicePowerState();
bool isPowerTransitionPending() const {
return _shutdownPending || (_stackActive != _enableRequested);
}
bool isConnected() const override;
+92 -1
View File
@@ -9,6 +9,7 @@
#include <new>
#if defined(ESP32_PLATFORM)
#include <esp_heap_caps.h>
#include <esp_timer.h>
#elif defined(NRF52_PLATFORM)
#include <utility/debug.h>
#include "../NRF52Board.h"
@@ -94,7 +95,12 @@ static uint32_t profileTimestamp(bool dual_profile) {
return profile_clock_ticks;
}
#endif
#if defined(ESP32_PLATFORM)
// Unlike a cycle counter, this domain includes elapsed light-sleep time.
return static_cast<uint32_t>(esp_timer_get_time());
#else
return micros();
#endif
}
static uint32_t profileElapsedUs(uint32_t since, uint32_t now, bool dual_profile) {
@@ -486,7 +492,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::trySetParams(float freq, float bw,
}
}
#if defined(ESP32_PLATFORM)
_esp32_profile_visit_started_us = static_cast<uint64_t>(esp_timer_get_time());
_profile_visit_stamp = static_cast<uint32_t>(_esp32_profile_visit_started_us);
#else
_profile_visit_stamp = profileTimestamp(_profiles.enabled());
#endif
return success ? mesh::RadioParamApplyResult::APPLIED : mesh::RadioParamApplyResult::FAILED;
}
@@ -610,7 +621,12 @@ mesh::RadioParamApplyResult RadioLibWrapper::tuneProfile(uint8_t profile) {
if (!restored || (resume && !isInRecvMode()))
_profile_refresh_required = _profile_repair_required = true;
}
#if defined(ESP32_PLATFORM)
_esp32_profile_visit_started_us = static_cast<uint64_t>(esp_timer_get_time());
_profile_visit_stamp = static_cast<uint32_t>(_esp32_profile_visit_started_us);
#else
_profile_visit_stamp = profileTimestamp(dual_profile);
#endif
// A profile retune re-enters RX on a different tuple. Do not read its
// instantaneous RSSI until the chip family's settle interval has elapsed.
if (applied && _nf_refresh_requested) {
@@ -837,6 +853,9 @@ mesh::RadioParamApplyResult RadioLibWrapper::tryRestoreCodingRate(uint8_t cr) {
void RadioLibWrapper::serviceProfileScan() {
#if defined(NRF52_PLATFORM)
_profile_idle_visit_ticks = 0; // Every deferred/restarted scan fails closed.
#endif
#if defined(ESP32_PLATFORM)
_esp32_profile_idle_visit_us = 0;
#endif
(void)profileTimestamp(_profiles.enabled());
if (_cw_active) return;
@@ -886,7 +905,7 @@ void RadioLibWrapper::serviceProfileScan() {
if (!_profiles.switchTestReady()) return;
}
uint8_t target = _active_profile;
#if defined(NRF52_PLATFORM)
#if defined(NRF52_PLATFORM) || defined(ESP32_PLATFORM)
const uint8_t visited_profile = _active_profile;
#endif
uint32_t visit_us = 0;
@@ -919,13 +938,25 @@ void RadioLibWrapper::serviceProfileScan() {
&& visit_us < NF_CALIB_SETTLE_MS * 1000UL) {
visit_us = NF_CALIB_SETTLE_MS * 1000UL;
}
#if defined(ESP32_PLATFORM)
// Keep long visits and light-sleep elapsed time in the same 64-bit domain.
const uint64_t now_us = static_cast<uint64_t>(esp_timer_get_time());
if (now_us < _esp32_profile_visit_started_us
|| now_us - _esp32_profile_visit_started_us >= visit_us) target ^= 1;
#else
if (profileElapsedUs(_profile_visit_stamp, profileTimestamp(true), true) >= visit_us) target ^= 1;
#endif
}
const auto result = tuneProfile(target);
if (restart_scan && result == mesh::RadioParamApplyResult::APPLIED) {
_profile_scan_generation[0] = _profiles.generation[0];
_profile_scan_generation[1] = _profiles.generation[1];
#if defined(ESP32_PLATFORM)
_esp32_profile_visit_started_us = static_cast<uint64_t>(esp_timer_get_time());
_profile_visit_stamp = static_cast<uint32_t>(_esp32_profile_visit_started_us);
#else
_profile_visit_stamp = profileTimestamp(true);
#endif
}
if (result == mesh::RadioParamApplyResult::FAILED) {
// Retry a rejected or failing profile at a bounded rate; retain primary RX.
@@ -955,6 +986,19 @@ void RadioLibWrapper::serviceProfileScan() {
_profile_idle_preamble = _physical_preamble;
}
#endif
#if defined(ESP32_PLATFORM)
if (_profiles.enabled() && !restart_scan && _active_profile == visited_profile
&& result == mesh::RadioParamApplyResult::APPLIED
&& !isChipBusy() && !isPacketPendingOrReceiving()) {
_esp32_profile_idle_visit_us = visit_us;
_esp32_profile_idle_stamp_us = _esp32_profile_visit_started_us;
_esp32_profile_idle_switch_us = _profiles.switchBudgetUs();
_esp32_profile_idle_preamble = _physical_preamble;
_esp32_profile_idle_profile = _active_profile;
_esp32_profile_idle_generation[0] = _profiles.generation[0];
_esp32_profile_idle_generation[1] = _profiles.generation[1];
}
#endif
}
bool RadioLibWrapper::getProfileIdleDelay(uint32_t& delay_ms) const {
@@ -993,6 +1037,48 @@ bool RadioLibWrapper::getProfileIdleDelay(uint32_t& delay_ms) const {
|| _profile_idle_visit_ticks - elapsed <= 32 + guard_ticks) return false;
delay_ms = uint64_t(_profile_idle_visit_ticks - elapsed - guard_ticks) * 15625 / 512000;
return delay_ms != 0;
#elif defined(ESP32_PLATFORM)
if (!_profiles.enabled() || !_esp32_profile_idle_visit_us
|| !_params_valid || state != STATE_RX || !_profile_rxps_suspended
|| _rx_ps_enabled || _rx_ps_armed || _rx_hold_continuous
|| _rx_retry_pending || _profile_retry_at || _profile_refresh_required
|| _profile_repair_required || _nf_refresh_requested || _nf_calib_active
|| !_noise_floor_valid || !_noise_floor_secondary_valid
|| _wd_observe_until || _cw_active || _morse_owned || hasPendingRadioInterrupt()
|| !_profiles.switchTestReady() || _active_profile > 1
|| _esp32_profile_idle_profile != _active_profile
|| _profile_generation != _profiles.generation[_active_profile]
|| _profile_scan_generation[0] != _profiles.generation[0]
|| _profile_scan_generation[1] != _profiles.generation[1]
|| _esp32_profile_idle_generation[0] != _profiles.generation[0]
|| _esp32_profile_idle_generation[1] != _profiles.generation[1]
|| _esp32_profile_idle_stamp_us != _esp32_profile_visit_started_us
|| _esp32_profile_idle_preamble != _physical_preamble
|| _esp32_profile_idle_switch_us != _profiles.switchBudgetUs()) return false;
#if MESH_ENABLE_TX_BEACONS
if (_beacon_owned) return false;
#endif
#if MESH_ENABLE_MORSE_RX
if (_morse_rx_owned) return false;
#endif
const uint64_t now_us = static_cast<uint64_t>(esp_timer_get_time());
if (now_us < _esp32_profile_idle_stamp_us) return false;
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
|| 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;
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;
return true;
#else
return false; // Other platforms retain polling until their wake path is validated.
#endif
@@ -2070,7 +2156,12 @@ void RadioLibWrapper::startRecv() {
int err = startReceiveMode();
if (err == RADIOLIB_ERR_NONE) {
// A very short frame may complete while startReceiveMode() returns.
#if defined(ESP32_PLATFORM)
_esp32_profile_visit_started_us = static_cast<uint64_t>(esp_timer_get_time());
_profile_visit_stamp = static_cast<uint32_t>(_esp32_profile_visit_started_us);
#else
_profile_visit_stamp = profileTimestamp(_profiles.enabled());
#endif
// Retain that RX interrupt instead of overwriting it with software state.
state = STATE_RX;
// RSSI is unsettled after every RX entry, including CAD/TX completion.
+12
View File
@@ -125,6 +125,18 @@ protected:
uint32_t _profile_idle_switch_us = 0;
uint16_t _profile_idle_preamble = 0;
#endif
#if defined(ESP32_PLATFORM)
// ESP Timer advances by the RTC interval after light sleep. Retain its
// full domain as well as the legacy scanner timestamp: a long RX hold or
// delayed loop must not resurrect a visit after micros() wraps.
uint64_t _esp32_profile_visit_started_us = 0;
uint64_t _esp32_profile_idle_stamp_us = 0;
uint32_t _esp32_profile_idle_visit_us = 0;
uint32_t _esp32_profile_idle_switch_us = 0;
uint32_t _esp32_profile_idle_generation[2] = {};
uint16_t _esp32_profile_idle_preamble = 0;
uint8_t _esp32_profile_idle_profile = 0;
#endif
#if defined(MESH_HIL_TX_FREQ_MHZ)
bool hilPacketTransmitAllowed() const;
#endif
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "BLEDevice.h"
+187
View File
@@ -0,0 +1,187 @@
#pragma once
#include <Arduino.h>
#include <string>
#include <vector>
#include <cstring>
#include <cassert>
using esp_err_t = int;
constexpr esp_err_t ESP_OK = 0;
enum esp_bt_controller_status_t { ESP_BT_CONTROLLER_STATUS_IDLE, ESP_BT_CONTROLLER_STATUS_INITED, ESP_BT_CONTROLLER_STATUS_ENABLED };
enum esp_bluedroid_status_t { ESP_BLUEDROID_STATUS_UNINITIALIZED, ESP_BLUEDROID_STATUS_INITIALIZED, ESP_BLUEDROID_STATUS_ENABLED };
using esp_bd_addr_t = uint8_t[6];
using esp_ble_wl_addr_type_t = int;
using esp_gatt_perm_t = int;
constexpr int BLE_ADDR_TYPE_PUBLIC=0, BLE_ADDR_TYPE_RANDOM=1, BLE_WL_ADDR_TYPE_PUBLIC=0, BLE_WL_ADDR_TYPE_RANDOM=1;
constexpr int ESP_LE_KEY_PID=1, ESP_BLE_ADV_FLAG_BREDR_NOT_SPT=4;
constexpr int ESP_GATT_PERM_READ_ENC_MITM=1, ESP_GATT_PERM_WRITE_ENC_MITM=2;
constexpr int ESP_IO_CAP_OUT=0, ESP_LE_AUTH_REQ_SC_MITM_BOND=15;
struct esp_ble_auth_cmpl_t { bool success=true; int addr_type=0, fail_reason=0; uint8_t bd_addr[6]={1,2,3,4,5,6}; };
struct esp_ble_gatts_cb_param_t { struct { uint16_t conn_id=7; } connect, mtu; };
struct esp_ble_bond_dev_t {
uint8_t bd_addr[6]={1,2,3,4,5,6};
struct { unsigned key_mask=0; struct { uint8_t static_addr[6]={}; int addr_type=0; } pid_key; } bond_key;
};
class BLEServer;
class BLECharacteristic;
namespace fake_bt {
inline esp_bt_controller_status_t controller=ESP_BT_CONTROLLER_STATUS_IDLE;
inline esp_bluedroid_status_t host=ESP_BLUEDROID_STATUS_UNINITIALIZED;
inline std::string failure, name;
inline bool initialized=false, persisted=true;
inline unsigned servers=0, services=0, characteristics=0, descriptors=0;
inline unsigned bond_deletions=0, disconnects=0, stops=0, uuid_adds=0;
inline uint8_t address[6]={};
inline uint32_t pin=0;
inline BLEServer* server=nullptr;
}
inline esp_bt_controller_status_t esp_bt_controller_get_status() { return fake_bt::controller; }
inline esp_bluedroid_status_t esp_bluedroid_get_status() { return fake_bt::host; }
inline int esp_bluedroid_disable() {
++fake_bt::stops;
if(fake_bt::failure=="stop-host") return 1;
fake_bt::host=ESP_BLUEDROID_STATUS_INITIALIZED; return ESP_OK;
}
inline int esp_bluedroid_deinit() {
if(fake_bt::failure=="deinit-host") return 1;
fake_bt::host=ESP_BLUEDROID_STATUS_UNINITIALIZED; return ESP_OK;
}
inline int esp_bt_controller_disable() {
if(fake_bt::failure=="stop-controller") return 1;
fake_bt::controller=ESP_BT_CONTROLLER_STATUS_INITED; return ESP_OK;
}
inline int esp_bt_controller_deinit() {
if(fake_bt::failure=="deinit-controller") return 1;
fake_bt::controller=ESP_BT_CONTROLLER_STATUS_IDLE; return ESP_OK;
}
inline int esp_ble_get_bond_device_num() { return fake_bt::persisted ? 1 : 0; }
inline int esp_ble_get_bond_device_list(int*, esp_ble_bond_dev_t* dest) { *dest=esp_ble_bond_dev_t{}; return ESP_OK; }
inline int esp_ble_remove_bond_device(uint8_t*) { ++fake_bt::bond_deletions; return ESP_OK; }
inline int esp_ble_gap_clear_whitelist() { return ESP_OK; }
inline int esp_ble_gap_update_whitelist(bool, esp_bd_addr_t, esp_ble_wl_addr_type_t) { return ESP_OK; }
class BLESecurityCallbacks {
public:
virtual ~BLESecurityCallbacks()=default;
virtual uint32_t onPassKeyRequest() { return 0; }
virtual void onPassKeyNotify(uint32_t) {}
virtual bool onConfirmPIN(uint32_t) { return false; }
virtual bool onSecurityRequest() { return false; }
virtual void onAuthenticationComplete(esp_ble_auth_cmpl_t) {}
};
class BLEServerCallbacks {
public:
virtual ~BLEServerCallbacks()=default;
virtual void onConnect(BLEServer*) {}
virtual void onConnect(BLEServer*, esp_ble_gatts_cb_param_t*) {}
virtual void onDisconnect(BLEServer*) {}
virtual void onMtuChanged(BLEServer*, esp_ble_gatts_cb_param_t*) {}
};
class BLECharacteristicCallbacks {
public:
enum Status { SUCCESS_NOTIFY, ERROR_GATT };
virtual ~BLECharacteristicCallbacks()=default;
virtual void onWrite(BLECharacteristic*, esp_ble_gatts_cb_param_t*) {}
virtual void onStatus(BLECharacteristic*, Status, uint32_t) {}
};
class BLESecurity {
public:
void setStaticPIN(uint32_t pin) { fake_bt::pin=pin; }
void setCapability(int) {}
void setAuthenticationMode(int) {}
};
class BLE2902 {
bool subscribed=false;
public:
BLE2902() { ++fake_bt::descriptors; }
~BLE2902() { --fake_bt::descriptors; }
void setNotifications(bool v) { subscribed=v; }
bool getNotifications() const { return subscribed; }
void setAccessPermissions(int) {}
};
class BLEAdvertisementData { public: void setFlags(int) {} };
class BLEAdvertising {
public:
bool active=false, filtered=false;
void start() { active=true; }
void stop() { active=false; }
void addServiceUUID(const char*) { ++fake_bt::uuid_adds; }
void setDeviceAddress(esp_bd_addr_t addr, int) { memcpy(fake_bt::address,addr,6); }
void setScanResponse(bool) {}
void setScanFilter(bool a,bool b) { filtered=a&&b; }
void setAdvertisementData(BLEAdvertisementData&) {}
};
class BLECharacteristic {
public:
static constexpr uint32_t PROPERTY_READ=1, PROPERTY_NOTIFY=2, PROPERTY_WRITE=4;
BLECharacteristicCallbacks* callbacks=nullptr;
std::vector<uint8_t> value;
BLECharacteristic() { ++fake_bt::characteristics; }
~BLECharacteristic() { --fake_bt::characteristics; }
void setCallbacks(BLECharacteristicCallbacks* cb) { callbacks=cb; }
void setAccessPermissions(int) {}
void addDescriptor(BLE2902*) {}
void setValue(uint8_t* data,size_t len) { value.assign(data,data+len); }
uint8_t* getData() { return value.data(); }
size_t getLength() { return value.size(); }
void notify() { callbacks->onStatus(this,BLECharacteristicCallbacks::SUCCESS_NOTIFY,0); }
};
class BLEService {
public:
BLEService() { ++fake_bt::services; }
~BLEService() { --fake_bt::services; }
unsigned made=0;
BLECharacteristic* createCharacteristic(const char*,uint32_t) {
const char* phase = made++ ? "rx" : "tx";
return fake_bt::failure==phase ? nullptr : new BLECharacteristic;
}
void start() {}
};
inline BLEAdvertising* fake_advertising=nullptr;
class BLEServer {
public:
bool connected=false;
BLEServerCallbacks* callbacks=nullptr;
BLEServer() { ++fake_bt::servers; }
~BLEServer() {
assert(fake_bt::host!=ESP_BLUEDROID_STATUS_ENABLED || fake_bt::server!=this);
--fake_bt::servers;
}
BLEService* createService(const char*) { return fake_bt::failure=="service" ? nullptr : new BLEService; }
BLEAdvertising* getAdvertising() { if(!fake_advertising) fake_advertising=new BLEAdvertising; return fake_advertising; }
void setCallbacks(BLEServerCallbacks* cb) { callbacks=cb; }
unsigned getConnectedCount() const { return connected; }
void disconnect(uint16_t) { ++fake_bt::disconnects; }
uint16_t getConnId() { return 7; }
uint16_t getPeerMTU(uint16_t) { return 179; }
};
class BLEDevice {
public:
inline static BLESecurityCallbacks* security=nullptr;
static void init(const std::string& name) {
fake_bt::name=name;
if(fake_bt::failure=="init") return;
fake_bt::initialized=true;
fake_bt::host=ESP_BLUEDROID_STATUS_ENABLED;
fake_bt::controller=ESP_BT_CONTROLLER_STATUS_ENABLED;
}
static BLEServer* createServer() {
fake_bt::server=fake_bt::failure=="server" ? nullptr : new BLEServer;
return fake_bt::server;
}
#ifdef TEST_BLE_OWNED_SERVER
static BLEServer* getServer() { return fake_bt::server; }
#endif
static void deinit(bool release) {
assert(!release); // BLE memory release is irreversible until reboot.
if(!fake_bt::initialized) return;
#ifdef TEST_BLE_OWNED_SERVER
assert(fake_bt::host!=ESP_BLUEDROID_STATUS_ENABLED);
delete fake_bt::server; fake_bt::server=nullptr;
delete fake_advertising; fake_advertising=nullptr;
#endif
fake_bt::host=ESP_BLUEDROID_STATUS_UNINITIALIZED;
fake_bt::controller=ESP_BT_CONTROLLER_STATUS_IDLE;
fake_bt::initialized=false;
}
static void setSecurityCallbacks(BLESecurityCallbacks* cb) { security=cb; }
static void setMTU(uint16_t) {}
};
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "BLEDevice.h"
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "BLEDevice.h"
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "BLEDevice.h"
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "BLEDevice.h"
+2
View File
@@ -0,0 +1,2 @@
#pragma once
#include "BLEDevice.h"
+81
View File
@@ -0,0 +1,81 @@
#include <helpers/esp32/SerialBLEInterface.h>
#include <cassert>
using namespace fake_bt;
static const uint8_t custom[]={0xc2,1,2,3,4,5};
static unsigned suites;
static void retire_global() {
delete server; server=nullptr;
delete fake_advertising; fake_advertising=nullptr;
uuid_adds=0;
}
static void stopped(SerialBLEInterface& a) {
a.disable(); test_millis+=1000; a.servicePowerState();
assert(!a.isEnabled() && !a.isPowerTransitionPending());
assert(controller==ESP_BT_CONTROLLER_STATUS_IDLE);
assert(host==ESP_BLUEDROID_STATUS_UNINITIALIZED);
assert(services==0 && characteristics==0 && descriptors==0);
#ifdef TEST_BLE_OWNED_SERVER
assert(servers==0);
#else
assert(servers==1); // Valid static library callback target, retired on next init.
#endif
}
static void good(SerialBLEInterface& a) {
assert(a.isEnabled() && !a.isPowerTransitionPending());
assert(servers==1 && services==1 && characteristics==2 && descriptors==1);
assert(name=="MeshCore-legacy" && pin==246810);
assert(memcmp(address,custom,6)==0 && fake_advertising->active);
}
static void repeated_cycles() {
SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom));
// Test initial saved-off as well as all subsequent live toggles.
stopped(a);
for(unsigned i=0;i<100;++i) {
a.enable(); good(a); assert(bond_deletions==0); stopped(a);
#ifndef TEST_BLE_OWNED_SERVER
assert(uuid_adds==1); // Reused static advertiser must not grow forever.
#endif
}
retire_global(); ++suites;
}
static void partial_stop_failures_and_late_callbacks() {
for(const char* fault : {"stop-host","deinit-host","stop-controller","deinit-controller"}) {
SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); a.enable(); good(a);
auto* original=server; original->connected=true;
esp_ble_gatts_cb_param_t connection;
original->callbacks->onConnect(original,&connection);
BLEDevice::security->onAuthenticationComplete(esp_ble_auth_cmpl_t{});
assert(a.isConnected());
a.disable(); assert(!a.isEnabled() && a.isPowerTransitionPending());
assert(server==original && services==1 && characteristics==2);
original->connected=false;
failure=fault; test_millis+=1000; a.servicePowerState();
assert(a.isPowerTransitionPending() && server==original && services==1);
esp_ble_auth_cmpl_t failed; failed.success=false;
BLEDevice::security->onAuthenticationComplete(failed);
assert(bond_deletions==0);
a.enable(); assert(!a.isEnabled() && a.isPowerTransitionPending());
failure.clear(); test_millis+=1000; a.servicePowerState(); good(a); stopped(a); retire_global();
}
++suites;
}
static void startup_failures() {
for(const char* fault : {"init","server","service","tx","rx"}) {
SerialBLEInterface a; assert(a.begin("MeshCore-","legacy",246810,custom)); a.enable(); stopped(a);
failure=fault; a.enable();
assert(!a.isEnabled() && !a.isConnected()); a.servicePowerState();
assert(!a.isPowerTransitionPending());
assert(services==0 && characteristics==0 && descriptors==0);
failure.clear(); a.enable(); good(a); stopped(a); retire_global();
}
++suites;
}
int main() {
repeated_cycles(); partial_stop_failures_and_late_callbacks(); startup_failures();
// The legacy library deliberately retains its static server/advertiser
// until replacement. Release the final objects after the host has stopped.
delete server; server=nullptr;
delete fake_advertising; fake_advertising=nullptr;
assert(servers==0 && services==0 && characteristics==0 && descriptors==0);
printf("%u lifecycle suites passed\n",suites);
}
+176
View File
@@ -0,0 +1,176 @@
#include <helpers/esp32/SerialBLEInterface.h>
#include <algorithm>
#include <cassert>
#include <cstring>
using namespace fake_nimble;
using mesh::companion::BluetoothPeerIdentity;
static const uint8_t custom[] = {0xc2, 1, 2, 3, 4, 5};
static unsigned suites;
static void reset() {
failure.clear();
NimBLEDevice::deinit(true);
calls.clear(); transmitted.clear(); connection_updates.clear();
stops = deleted_servers = bond_deletions = disconnects = 0;
persisted = accept_notification = true;
test_millis = 100;
}
static void stop(SerialBLEInterface& adapter) {
adapter.disable();
test_millis += 1000;
adapter.servicePowerState();
assert(!adapter.isEnabled() && !adapter.isPowerTransitionPending());
assert(!initialized && !NimBLEDevice::server);
}
static void connect(SerialBLEInterface& adapter, NimBLEConnInfo& info) {
auto* server = NimBLEDevice::server.get();
server->connected = true;
server->callbacks->onConnect(server, info);
server->callbacks->onAuthenticationComplete(info);
assert(adapter.isConnected());
uint8_t rx[MAX_FRAME_SIZE]; adapter.checkRecvFrame(rx);
}
static void verify(SerialBLEInterface& adapter) {
auto* server = NimBLEDevice::server.get();
assert(adapter.isEnabled() && !adapter.isPowerTransitionPending());
assert(initialized_name == "MeshCore-fixed");
assert(server && server->advertising.active);
assert(server->advertising.name == "MeshCore-fixed");
assert(server->advertising.service == "6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
assert(server->displayPairingPasskey() == 246810);
assert(bonding && mitm && sc && mtu == MAX_FRAME_SIZE + 3);
assert(own_type == BLE_OWN_ADDR_RANDOM);
for (unsigned i=0; i<6; ++i) assert(address[i] == custom[5-i]);
assert(server->service->characteristics.size() == 2);
assert(server->service->characteristics[0]->properties & NIMBLE_PROPERTY::READ_AUTHEN);
assert(server->service->characteristics[1]->properties & NIMBLE_PROPERTY::WRITE_AUTHEN);
}
static void initial_saved_off() {
reset(); SerialBLEInterface a;
char prefix[] = "MeshCore-", name[] = "fixed";
uint8_t address_copy[6]; memcpy(address_copy, custom, 6);
assert(a.begin(prefix, name, 246810, address_copy));
assert(!a.isEnabled() && a.isPowerTransitionPending());
a.servicePowerState();
assert(!initialized && !a.isPowerTransitionPending());
memset(prefix, 'x', sizeof(prefix)-1); memset(name, 'z', sizeof(name)-1);
memset(address_copy, 0, sizeof(address_copy));
a.enable(); verify(a); stop(a); ++suites;
}
static void repeated_cycles_preserve_identity_bonds_and_gatt() {
reset(); SerialBLEInterface a;
assert(a.begin("MeshCore-", "fixed", 246810, custom, true));
for (unsigned i=0; i<100; ++i) {
a.enable(); verify(a); stop(a);
assert(bond_deletions == 1 && persisted);
assert(deleted_servers == i+1);
}
++suites;
}
static void deferred_disconnect_and_quick_on() {
reset(); SerialBLEInterface a;
assert(a.begin("MeshCore-", "fixed", 246810, custom)); a.enable();
NimBLEConnInfo info; connect(a, info);
auto* original = NimBLEDevice::server.get();
auto* tx = original->service->characteristics[0].get();
auto* rx = original->service->characteristics[1].get();
tx->callbacks->onSubscribe(tx, info, 1);
const uint8_t reply[] = {29, 'O', 'K'};
assert(a.writeFrame(reply, sizeof(reply)) == sizeof(reply));
test_millis += 61; uint8_t frame[MAX_FRAME_SIZE];
a.checkRecvFrame(frame); // Existing main drains its off-command reply first.
assert(transmitted.size() == 1 && !a.hasPendingIO());
a.disable();
assert(!a.isEnabled() && a.isPowerTransitionPending());
assert(initialized && NimBLEDevice::server.get() == original && stops == 0);
assert(disconnects == 1 && !original->advertising.active);
// Late authentication failure during intentional disconnect must not erase
// a good saved bond. Late writes/subscriptions cannot repopulate the queue.
info.authenticated = false;
original->callbacks->onAuthenticationComplete(info);
rx->setValue(reply, sizeof(reply)); rx->callbacks->onWrite(rx, info);
tx->callbacks->onSubscribe(tx, info, 1);
original->callbacks->onDisconnect(original, info, 0);
assert(bond_deletions == 0 && !a.hasPendingIO());
a.enable();
assert(!a.isEnabled() && a.isPowerTransitionPending());
original->connected = false;
test_millis += 999; a.servicePowerState();
assert(NimBLEDevice::server.get() == original && stops == 0);
++test_millis; a.servicePowerState(); verify(a);
assert(deleted_servers == 1 && bond_deletions == 0);
stop(a); ++suites;
}
static void failed_stop_keeps_callback_objects_alive() {
reset(); SerialBLEInterface a;
assert(a.begin("MeshCore-", "fixed", 246810, custom)); a.enable();
auto* original = NimBLEDevice::server.get();
failure = "stop-host"; a.disable();
assert(!a.isEnabled() && a.isPowerTransitionPending());
assert(initialized && NimBLEDevice::server.get() == original && deleted_servers == 0);
unsigned attempts = stops;
for (unsigned i=0; i<100; ++i) a.servicePowerState();
assert(stops == attempts); // No tight retry loop or premature deletion.
a.enable(); assert(!a.isEnabled() && a.isPowerTransitionPending());
failure.clear(); test_millis += 1000; a.servicePowerState(); verify(a);
assert(bond_deletions == 0); stop(a); ++suites;
}
static void ignored_controller_teardown_failure_is_retried() {
for (const char* fault : {"stop-controller", "deinit-controller"}) {
reset(); SerialBLEInterface a;
assert(a.begin("MeshCore-", "fixed", 246810, custom)); a.enable();
auto* original = NimBLEDevice::server.get();
failure = fault; a.disable();
// Host says it stopped, but its wrapper silently ignored a live
// controller. Objects and the sleep gate must remain intact.
assert(!initialized && controller_state != 0);
assert(!a.isEnabled() && a.isPowerTransitionPending());
assert(NimBLEDevice::server.get() == original && deleted_servers == 0);
a.enable(); assert(!a.isEnabled() && a.isPowerTransitionPending());
failure.clear(); test_millis += 1000; a.servicePowerState(); verify(a);
assert(deleted_servers == 1 && bond_deletions == 0);
stop(a);
}
++suites;
}
static void startup_failures_remain_off_and_recover() {
for (const char* fault : {"init", "address", "address-type", "server", "service", "tx", "rx", "start-service", "advertise"}) {
reset(); SerialBLEInterface a;
assert(a.begin("MeshCore-", "fixed", 246810, custom)); a.enable(); stop(a);
failure = fault; a.enable();
assert(!a.isEnabled() && !a.isConnected());
a.servicePowerState();
assert(!initialized && !a.isPowerTransitionPending());
assert(bond_deletions == 0);
failure.clear(); a.enable(); verify(a); stop(a);
}
++suites;
}
static void stealth_and_recovery_never_reopen_general_advertising() {
reset(); SerialBLEInterface a;
BluetoothPeerIdentity peer; peer.type = mesh::companion::BLUETOOTH_PEER_ADDRESS_PUBLIC;
const uint8_t known[] = {1, 2, 3, 4, 5, 6}; memcpy(peer.address, known, 6);
assert(a.begin("MeshCore-", "fixed", 246810, custom, false, false, &peer));
for (unsigned i=0; i<3; ++i) {
a.enable(); auto& adv = NimBLEDevice::server->advertising;
assert(adv.active && adv.name.empty() && adv.service.empty());
assert(adv.scan_filter && adv.connect_filter && !adv.scan_response);
for (unsigned j=0; j<6; ++j) assert(allowlisted.val[j] == known[5-j]);
stop(a);
}
// A missing stored bond must remain suppressed, including after the
// application consumes the recovery event and toggles Bluetooth again.
persisted = false; a.enable();
assert(a.takeBondedOnlyRecovery());
assert(!NimBLEDevice::server->advertising.active); stop(a);
persisted = true; a.enable();
assert(!NimBLEDevice::server->advertising.active); stop(a); ++suites;
}
int main() {
initial_saved_off(); repeated_cycles_preserve_identity_bonds_and_gatt();
deferred_disconnect_and_quick_on(); failed_stop_keeps_callback_objects_alive();
ignored_controller_teardown_failure_is_retried();
startup_failures_remain_off_and_recover();
stealth_and_recovery_never_reopen_general_advertising();
printf("%u lifecycle suites passed\n", suites);
}
+26 -8
View File
@@ -51,8 +51,10 @@ struct NimBLEAdvertisementData {
bool setFlags(uint8_t value) { flags = value; return true; }
};
namespace fake_nimble {
inline int controller_state = 0;
inline bool initialized = false, persisted = true, accept_notification = true;
inline std::string failure;
inline std::string failure, initialized_name;
inline unsigned stops = 0, deleted_servers = 0, bond_deletions = 0;
inline std::vector<std::string> calls;
inline uint8_t address[6] = {}, own_type = BLE_OWN_ADDR_PUBLIC, io_cap = 255;
inline ble_addr_t allowlisted;
@@ -76,7 +78,8 @@ struct NimBLEAdvertising {
bool active = false, scan_response = false, scan_filter = false, connect_filter = false;
std::string name, service;
bool start(uint32_t = 0, const NimBLEAddress* = nullptr) {
fake_nimble::calls.push_back("advertise"); active = true; return true;
fake_nimble::calls.push_back("advertise");
active = fake_nimble::failure != "advertise"; return active;
}
bool stop() { active = false; return true; }
void enableScanResponse(bool value) { scan_response = value; }
@@ -115,7 +118,11 @@ struct NimBLEServer {
std::unique_ptr<NimBLEService> service;
NimBLEServerCallbacks* callbacks = nullptr;
bool owns_callbacks = true, auto_advertise = true, connected = false;
~NimBLEServer() { if (owns_callbacks) delete callbacks; }
~NimBLEServer() {
++fake_nimble::deleted_servers;
assert(!fake_nimble::initialized); // Stop must precede destruction.
if (owns_callbacks) delete callbacks;
}
void setCallbacks(NimBLEServerCallbacks* cb, bool take_ownership = true) {
callbacks = cb; owns_callbacks = take_ownership;
}
@@ -126,7 +133,7 @@ struct NimBLEServer {
return fake_nimble::pin == 123456
? callbacks->onPassKeyDisplay() : fake_nimble::pin;
}
bool start() { return true; }
bool start() { return fake_nimble::failure != "start-service"; }
uint8_t getConnectedCount() const { return connected; }
uint16_t getPeerMTU(uint16_t) const { return fake_nimble::mtu; }
void updateConnParams(uint16_t handle, uint16_t minimum, uint16_t maximum,
@@ -142,17 +149,28 @@ struct NimBLEServer {
};
struct NimBLEDevice {
inline static std::unique_ptr<NimBLEServer> server;
static bool init(const std::string&) {
static bool init(const std::string& name) {
fake_nimble::initialized_name = name;
fake_nimble::calls.push_back("init");
return fake_nimble::initialized = fake_nimble::failure != "init";
fake_nimble::initialized = fake_nimble::failure != "init";
fake_nimble::controller_state = fake_nimble::initialized ? 2 : 0;
return fake_nimble::initialized;
}
static bool deinit(bool clear = false) {
++fake_nimble::stops;
if (fake_nimble::failure == "stop-host") return false;
if (fake_nimble::initialized) {
// The real NimBLE wrapper ignores its port's controller teardown error.
if (fake_nimble::failure == "stop-controller") fake_nimble::controller_state = 2;
else if (fake_nimble::failure == "deinit-controller") fake_nimble::controller_state = 1;
else fake_nimble::controller_state = 0;
}
fake_nimble::initialized = false;
if (clear) server.reset();
return true;
}
static bool deleteAllBonds() { fake_nimble::calls.push_back("clear-bonds"); return fake_nimble::failure != "clear-bonds"; }
static bool deleteBond(const NimBLEAddress&) { return true; }
static bool deleteAllBonds() { ++fake_nimble::bond_deletions; fake_nimble::calls.push_back("clear-bonds"); return fake_nimble::failure != "clear-bonds"; }
static bool deleteBond(const NimBLEAddress&) { ++fake_nimble::bond_deletions; return true; }
static bool isBonded(const NimBLEAddress&) { return fake_nimble::persisted; }
static bool setOwnAddr(const uint8_t* address) {
fake_nimble::calls.push_back("address");
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include <NimBLEDevice.h>
using esp_err_t = int;
constexpr esp_err_t ESP_OK = 0;
enum esp_bt_controller_status_t {
ESP_BT_CONTROLLER_STATUS_IDLE,
ESP_BT_CONTROLLER_STATUS_INITED,
ESP_BT_CONTROLLER_STATUS_ENABLED
};
inline esp_bt_controller_status_t esp_bt_controller_get_status() {
return static_cast<esp_bt_controller_status_t>(fake_nimble::controller_state);
}
inline esp_err_t esp_bt_controller_disable() {
if (fake_nimble::failure == "stop-controller") return 1;
fake_nimble::controller_state = ESP_BT_CONTROLLER_STATUS_INITED;
return ESP_OK;
}
inline esp_err_t esp_bt_controller_deinit() {
if (fake_nimble::failure == "deinit-controller") return 1;
fake_nimble::controller_state = ESP_BT_CONTROLLER_STATUS_IDLE;
return ESP_OK;
}
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Run the actual ESP32 BLE adapter through reversible stack lifecycles."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class Esp32BlePowerTests(unittest.TestCase):
def test_nimble_reversible_power_faults_identity_bonds_and_callbacks(self):
self.run_adapter("test_nimble_power.cpp", ["-DMESH_USE_NIMBLE_ARDUINO=1"],
ROOT / "test/fixtures/nimble_companion/mocks")
def test_bluedroid_legacy_static_ownership_and_partial_stop_failures(self):
self.run_adapter("test_bluedroid_power.cpp", ["-DCONFIG_BLUEDROID_ENABLED=1"],
ROOT / "test/fixtures/esp32_ble_power/mocks")
def test_bluedroid_new_library_ownership_and_partial_stop_failures(self):
self.run_adapter("test_bluedroid_power.cpp", ["-DCONFIG_BLUEDROID_ENABLED=1",
"-DTEST_BLE_OWNED_SERVER=1"],
ROOT / "test/fixtures/esp32_ble_power/mocks")
def run_adapter(self, fixture, flags, mocks):
with tempfile.TemporaryDirectory(prefix="meshcore-ble-power-") as temp:
binary = Path(temp) / "ble-power"
result = subprocess.run([
"c++", "-std=c++17", "-g", "-fsanitize=address,undefined", *flags,
"-I", str(mocks),
"-I", str(ROOT / "test/fixtures/nimble_companion/mocks"),
"-I", str(ROOT / "src"),
str(ROOT / "src/helpers/esp32/SerialBLEInterface.cpp"),
str(ROOT / "test/fixtures/esp32_ble_power" / fixture),
"-o", str(binary),
], text=True, capture_output=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("lifecycle suites passed", result.stdout)
if __name__ == "__main__":
unittest.main()
+19 -6
View File
@@ -27,6 +27,9 @@ def run_cpp(source, defines=()):
cpp = Path(directory) / "test.cpp"
exe = Path(directory) / "test"
(Path(directory) / "Dispatcher.h").write_text(MORSE_RADIO, encoding="ascii")
(Path(directory) / "esp_timer.h").write_text("#pragma once\n#include <stdint.h>\nint64_t esp_timer_get_time();\n")
for header in ("esp_wifi.h", "esp_bt.h", "esp_bt_main.h"):
(Path(directory) / header).write_text("#pragma once\n")
cpp.write_text(source)
built = subprocess.run(
[os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
@@ -105,18 +108,25 @@ int main() {
static uint32_t now = 1000;
uint32_t millis() { return now; }
uint32_t micros() { return now * 1000U; }
int64_t esp_timer_get_time() { return int64_t(now) * 1000; }
using esp_err_t = int;
enum wifi_mode_t { WIFI_MODE_NULL, WIFI_MODE_STA, WIFI_MODE_AP, WIFI_MODE_APSTA };
constexpr int ESP_OK = 0, ESP_ERR_WIFI_NOT_INIT = 1, ESP_ERR_WIFI_NOT_STARTED = 2;
static wifi_mode_t sdk_mode = WIFI_MODE_NULL;
static int sdk_result = ESP_OK, sleeps = 0, yields = 0;
constexpr int ESP_BT_CONTROLLER_STATUS_ENABLED = 1;
using esp_bt_controller_status_t = int;
constexpr int ESP_BT_CONTROLLER_STATUS_IDLE = 0, ESP_BT_CONTROLLER_STATUS_INITED = 1,
ESP_BT_CONTROLLER_STATUS_ENABLED = 2;
static bool bt_controller_active = false;
int esp_bt_controller_get_status() { return bt_controller_active ? 1 : 0; }
constexpr int ESP_BLUEDROID_STATUS_ENABLED = 1;
int esp_bt_controller_get_status() { return bt_controller_active ? 2 : 0; }
using esp_bluedroid_status_t = int;
constexpr int ESP_BLUEDROID_STATUS_UNINITIALIZED = 0, ESP_BLUEDROID_STATUS_INITIALIZED = 1,
ESP_BLUEDROID_STATUS_ENABLED = 2;
static bool bt_host_active = false;
int esp_bluedroid_get_status() { return bt_host_active ? 1 : 0; }
int esp_bluedroid_get_status() { return bt_host_active ? 2 : 0; }
int esp_wifi_get_mode(wifi_mode_t* mode) { *mode = sdk_mode; return sdk_result; }
#include <helpers/esp32/LightSleepPolicy.h>
#include <helpers/esp32/ProfileIdleSleep.h>
int esp_sleep_enable_timer_wakeup(uint64_t us) { assert(us == 10000); return ESP_OK; }
int esp_light_sleep_start() { ++sleeps; return ESP_OK; }
int pdMS_TO_TICKS(int ms) { return ms; }
@@ -129,6 +139,7 @@ struct Board {
@USB_HELD@
bool isOTAUpdateRunning() const { return ota; }
bool isRadioTestActive() const { return test; }
void sleepUntilMicroseconds(uint32_t) { ++sleeps; }
} board;
struct Prefs { bool powersaving_enabled = true; } prefs;
struct TestRadio : mesh::Radio {
@@ -146,10 +157,12 @@ struct Mesh {
}
bool isDualRadioActive() const { return dual; }
bool canIdleDualRadio() const { return !dual; }
bool getDualRadioIdleDelay(uint32_t& out) const { out = 0; return false; }
bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }
mesh::Radio* getProfileRadio() { return &timing_radio; }
} the_mesh;
struct Interfaces { bool pending = false; bool hasPendingIO() const { return pending; } } interface_manager;
struct Bluetooth { bool pending = false; bool isPowerTransitionPending() const { return pending; } } bluetooth_interface;
struct Sensors { bool gps_uart = false; bool gpsUsesSerialUart(uint8_t uart) const { return uart == 1 && gps_uart; } } sensors;
struct Button { bool polling = false; bool needsPolling() const { return polling; } } user_btn;
struct Wireless { unsigned services = 0; unsigned enabled() const { return services; } } companion_wireless;
@@ -202,7 +215,7 @@ int main() {
blocks(board.ota); blocks(board.test); blocks(the_mesh.pending);
blocks(interface_manager.pending); blocks(user_btn.polling); blocks(mesh::logging);
blocks(bt_controller_active);
blocks(bt_host_active); blocks(sensors.gps_uart);
blocks(bt_host_active); blocks(sensors.gps_uart); blocks(bluetooth_interface.pending);
blocks(mesh::watchdog); blocks(mesh::wireless::controller.transition);
prefs.powersaving_enabled = false; before = sleeps; idle(); assert(sleeps == before);
prefs.powersaving_enabled = true; idle(); assert(sleeps == before + 1);
@@ -263,7 +276,7 @@ int main() {
source = "#include <initializer_list>\n" + source
flags = ("ESP32_PLATFORM", "ESP32", "ENABLE_USB_INTERFACE", "ARDUINO_USB_CDC_ON_BOOT=1",
'WIFI_SSID=""', "BLE_PIN_CODE=123456", "MESH_USB_LOGGING_AVAILABLE=1",
"CONFIG_BLUEDROID_ENABLED=1",
"CONFIG_BLUEDROID_ENABLED=1", "CONFIG_BT_ENABLED=1",
"MESH_ESP32_USB_CONSOLE_COOPERATIVE=1", "MESH_ESP32_USB_HOST_LOSS_SLEEP_GRACE_MS=120000UL",
"MOMENTARY_BUTTON_WAKE_FROM_SLEEP=1", "PIN_USER_BTN=0", "DISPLAY_CLASS=Mock")
for pm in (0, 1):
+3 -1
View File
@@ -301,7 +301,9 @@ int main(){FEMBoard board; board.enterDeepSleep(5); assert(slept);}
light='\n'.join(body(header, signature) for signature in (
'void sleep(uint32_t secs) override',
'void sleepMilliseconds(uint32_t millisecs) override',
'void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)'))
'void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)',
'void sleepUntilMicroseconds(uint32_t deadline_us)',
'void sleepMicrosecondsImpl(uint64_t wake_us, bool require_timer,'))
self.assertNotIn('commitTerminalDeepSleepHolds',light)
for folder in (ROOT/'src',ROOT/'variants'):
for path in folder.rglob('*'):
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""Compile the real manual-sleep driver guard for WiFi/BT SDK configurations."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
WIFI = r'''
#pragma once
using esp_err_t = int;
constexpr int ESP_OK = 0, ESP_ERR_WIFI_NOT_INIT = 1, ESP_ERR_WIFI_NOT_STARTED = 2;
enum wifi_mode_t { WIFI_MODE_NULL, WIFI_MODE_STA, WIFI_MODE_AP, WIFI_MODE_APSTA };
static wifi_mode_t mode = WIFI_MODE_NULL;
static int wifi_error = ESP_OK;
inline int esp_wifi_get_mode(wifi_mode_t* out) { *out = mode; return wifi_error; }
'''
CONTROLLER = r'''
#pragma once
enum esp_bt_controller_status_t { ESP_BT_CONTROLLER_STATUS_IDLE,
ESP_BT_CONTROLLER_STATUS_INITED, ESP_BT_CONTROLLER_STATUS_ENABLED };
static auto controller = ESP_BT_CONTROLLER_STATUS_IDLE;
inline esp_bt_controller_status_t esp_bt_controller_get_status() { return controller; }
'''
HOST = r'''
#pragma once
enum esp_bluedroid_status_t { ESP_BLUEDROID_STATUS_UNINITIALIZED,
ESP_BLUEDROID_STATUS_INITIALIZED, ESP_BLUEDROID_STATUS_ENABLED };
static auto host = ESP_BLUEDROID_STATUS_UNINITIALIZED;
inline esp_bluedroid_status_t esp_bluedroid_get_status() { return host; }
'''
HARNESS = r'''
#include <helpers/esp32/LightSleepPolicy.h>
#include <cassert>
using mesh::powerdiag::Reason;
using mesh::esp32::manualLightSleepBlocker;
using mesh::esp32::manualLightSleepReady;
int main() {
assert(manualLightSleepReady());
for (auto value : {WIFI_MODE_STA, WIFI_MODE_AP, WIFI_MODE_APSTA}) {
mode = value;
assert(manualLightSleepBlocker() == Reason::Network);
}
mode = WIFI_MODE_NULL;
for (int error : {ESP_ERR_WIFI_NOT_INIT, ESP_ERR_WIFI_NOT_STARTED}) {
wifi_error = error;
assert(manualLightSleepReady());
}
wifi_error = -1;
assert(manualLightSleepBlocker() == Reason::Network);
wifi_error = ESP_OK;
#if defined(CONFIG_BT_ENABLED) && CONFIG_BT_ENABLED
controller = ESP_BT_CONTROLLER_STATUS_INITED;
assert(manualLightSleepReady());
controller = ESP_BT_CONTROLLER_STATUS_ENABLED;
assert(manualLightSleepBlocker() == Reason::Wireless);
controller = static_cast<esp_bt_controller_status_t>(99);
assert(manualLightSleepBlocker() == Reason::Wireless);
controller = ESP_BT_CONTROLLER_STATUS_IDLE;
#if defined(CONFIG_BLUEDROID_ENABLED) && CONFIG_BLUEDROID_ENABLED \
&& !defined(MESH_USE_NIMBLE_ARDUINO)
host = ESP_BLUEDROID_STATUS_INITIALIZED;
assert(manualLightSleepReady());
host = ESP_BLUEDROID_STATUS_ENABLED;
assert(manualLightSleepBlocker() == Reason::Wireless);
host = static_cast<esp_bluedroid_status_t>(99);
assert(manualLightSleepBlocker() == Reason::Wireless);
host = ESP_BLUEDROID_STATUS_UNINITIALIZED;
#endif
#endif
assert(manualLightSleepReady());
}
'''
class LightSleepPolicyTest(unittest.TestCase):
def test_real_guard_across_sdk_and_host_modes(self):
for flags in (
(), ("CONFIG_BT_ENABLED=0", "CONFIG_IDF_TARGET_ESP32S2=1"),
("CONFIG_BT_ENABLED=1", "CONFIG_BLUEDROID_ENABLED=1", "CONFIG_IDF_TARGET_ESP32=1"),
("CONFIG_BT_ENABLED=1", "CONFIG_BLUEDROID_ENABLED=1", "CONFIG_IDF_TARGET_ESP32S3=1"),
("CONFIG_BT_ENABLED=1", "CONFIG_BLUEDROID_ENABLED=0", "CONFIG_IDF_TARGET_ESP32C3=1"),
("CONFIG_BT_ENABLED=1", "CONFIG_BLUEDROID_ENABLED=1", "MESH_USE_NIMBLE_ARDUINO=1", "CONFIG_IDF_TARGET_ESP32C6=1"),
):
with self.subTest(flags=flags), tempfile.TemporaryDirectory(prefix="mesh-esp32-sleep-driver-") as directory:
root = Path(directory)
(root / "esp_wifi.h").write_text(WIFI)
bt = "CONFIG_BT_ENABLED=1" in flags
bluedroid = bt and "CONFIG_BLUEDROID_ENABLED=1" in flags and "MESH_USE_NIMBLE_ARDUINO=1" not in flags
(root / "esp_bt.h").write_text(CONTROLLER if bt else '#error "Bluetooth absent on this target"\n')
(root / "esp_bt_main.h").write_text(HOST if bluedroid else '#error "Bluedroid absent from this host build"\n')
cpp, exe = root / "guard.cpp", root / "guard"
cpp.write_text("#include <initializer_list>\n" + HARNESS)
result = subprocess.run([
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
*("-D" + flag for flag in flags), "-I", str(root), "-I", str(ROOT / "src"),
str(cpp), "-o", str(exe)], text=True, capture_output=True, timeout=30)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(exe)], text=True, capture_output=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()
+97
View File
@@ -0,0 +1,97 @@
"""Execute the production profile sleep handoff with elapsed and wrapping clocks."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class Esp32ProfileIdleSleepTest(unittest.TestCase):
def test_absolute_role_and_radio_deadlines_survive_preemption(self):
source = r'''
#include <cassert>
#include <cstdint>
#include <helpers/esp32/ProfileIdleSleep.h>
static uint64_t now_us = 0;
int64_t esp_timer_get_time() { return static_cast<int64_t>(now_us); }
struct Mesh {
bool ready = true;
uint32_t budget_ms = 10, query_cost_us = 0;
unsigned reads = 0;
bool getDualRadioIdleDelay(uint32_t& out) {
++reads;
out = budget_ms;
now_us += query_cost_us;
return ready;
}
} mesh_node;
struct Board {
unsigned calls = 0;
uint32_t deadline = 0;
void sleepUntilMicroseconds(uint32_t value) { ++calls; deadline = value; }
} board;
int main() {
for (uint64_t start : {100000ULL, 0xfffffc00ULL, 0x1fffffc00ULL}) {
now_us = start;
mesh_node = {};
board = {};
assert(mesh::esp32::tryProfileIdleSleep(mesh_node, board, uint32_t(start), 30));
assert(board.calls == 1 && board.deadline == uint32_t(start + 10000));
// A role deadline calculated before other loop work is never restarted.
now_us = start + 4000;
assert(mesh::esp32::tryProfileIdleSleep(mesh_node, board, uint32_t(start), 7));
assert(board.deadline == uint32_t(start + 7000));
now_us = start + 5001;
const unsigned before = board.calls;
assert(!mesh::esp32::tryProfileIdleSleep(mesh_node, board, uint32_t(start), 7));
assert(board.calls == before);
now_us = start + 8000;
assert(!mesh::esp32::tryProfileIdleSleep(mesh_node, board, uint32_t(start), 7));
// An IRQ/task delay in the scanner query reduces the board's remaining
// time instead of moving the absolute deadline forward by that delay.
now_us = start;
mesh_node.query_cost_us = 6000;
assert(mesh::esp32::tryProfileIdleSleep(mesh_node, board, uint32_t(start), 30));
assert(now_us == start + 6000 && board.deadline == uint32_t(start + 10000));
mesh_node.query_cost_us = 0;
mesh_node.budget_ms = 2;
now_us = start;
assert(mesh::esp32::tryProfileIdleSleep(mesh_node, board, uint32_t(start), 30));
assert(board.deadline == uint32_t(start + 2000));
}
now_us = 100000;
for (uint32_t budget : {0U, 1U, 11U, 0xffffffffU}) {
mesh_node = {};
mesh_node.budget_ms = budget;
const unsigned before = board.calls;
assert(!mesh::esp32::tryProfileIdleSleep(mesh_node, board, 100000, 30));
assert(board.calls == before);
}
mesh_node = {};
mesh_node.ready = false;
assert(!mesh::esp32::tryProfileIdleSleep(mesh_node, board, 100000, 30));
mesh_node = {};
const unsigned reads = mesh_node.reads;
assert(!mesh::esp32::tryProfileIdleSleep(mesh_node, board, 100000, 0));
assert(!mesh::esp32::tryProfileIdleSleep(mesh_node, board, 100000, 0xffffffffU));
assert(mesh_node.reads == reads);
}
'''
with tempfile.TemporaryDirectory(prefix="mesh-esp32-profile-handoff-") as directory:
path = Path(directory)
(path / "esp_timer.h").write_text("#pragma once\n#include <stdint.h>\nint64_t esp_timer_get_time();\n")
(path / "test.cpp").write_text("#include <initializer_list>\n" + source)
built = subprocess.run([os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I", str(path), "-I", str(ROOT / "src"), str(path / "test.cpp"),
"-o", str(path / "test")], capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(path / "test")], capture_output=True, text=True)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
if __name__ == "__main__":
unittest.main()
+115 -2
View File
@@ -58,6 +58,10 @@ static void attachHost(bool attached) {
namespace mesh {
static bool logging_enabled = false;
bool isUsbLoggingEnabled() { return logging_enabled; }
namespace esp32 {
static powerdiag::Reason wireless_blocker = powerdiag::Reason::Count;
powerdiag::Reason manualLightSleepBlocker() { return wireless_blocker; }
}
}
using gpio_num_t = int;
@@ -71,6 +75,9 @@ constexpr int GPIO_INTR_DISABLE = 0;
static unsigned sleep_calls = 0;
static uint64_t timer_us = 0;
static int timer_error = ESP_OK;
static int radio_wake_error = ESP_OK, button_wake_error = ESP_OK, gpio_wake_error = ESP_OK;
static unsigned gpio_setup_us = 0;
static bool irq_during_setup = false;
static bool radio_irq_high = false;
static int critical_depth = 0;
static int wake_pin = -1;
@@ -98,11 +105,17 @@ static int esp_light_sleep_start() {
}
return ESP_OK;
}
static int esp_sleep_enable_gpio_wakeup() { return ESP_OK; }
static int esp_sleep_enable_gpio_wakeup() {
mock_millis += gpio_setup_us / 1000U;
if (irq_during_setup) radio_irq_high = true;
return gpio_wake_error;
}
static int gpio_get_level(gpio_num_t pin) {
return pin == 38 ? (button_pressed ? LOW : HIGH) : (radio_irq_high ? HIGH : LOW);
}
static int gpio_wakeup_enable(gpio_num_t pin, int level) {
if (pin == 48 && radio_wake_error != ESP_OK) return radio_wake_error;
if (pin == 38 && button_wake_error != ESP_OK) return button_wake_error;
wake_pin = pin;
wake_levels.at(pin) = level;
return ESP_OK;
@@ -267,7 +280,9 @@ def board_sleep_methods() -> str:
return "\n".join(board_method(signature) for signature in (
"void sleep(uint32_t secs) override",
"void sleepMilliseconds(uint32_t millisecs) override",
"void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)"))
"void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)",
"void sleepUntilMicroseconds(uint32_t deadline_us)",
"void sleepMicrosecondsImpl(uint64_t wake_us, bool require_timer,"))
def v4_profile_config():
@@ -345,6 +360,9 @@ struct SerialPort {
} Serial;
namespace mesh {
bool isUsbLoggingEnabled() { return false; }
namespace esp32 {
powerdiag::Reason manualLightSleepBlocker() { return powerdiag::Reason::Count; }
}
struct Console { void printf(const char*) {} };
static Console console;
Console& usbConsolePort() { return console; }
@@ -502,6 +520,101 @@ int main() {
class Esp32UsbSleepTest(unittest.TestCase):
def test_physical_wireless_vetoes_and_deadline_gpio_failures(self):
methods = board_sleep_methods() + "\n" + "\n".join(board_method(signature) for signature in (
"bool isUsbDataConnected() override", "bool isUsbHostConnected() override",
"bool isUsbSleepHeld()"))
harness = HARNESS.split("int main()", 1)[0] + r'''
int main() {
try {
ESP32Board board;
attachHost(false);
Serial.end();
for (auto reason : {mesh::powerdiag::Reason::Network,
mesh::powerdiag::Reason::Wireless}) {
mesh::esp32::wireless_blocker = reason;
mesh::powerdiag::setEnabled(true, millis());
board.sleepMilliseconds(10);
require(sleep_calls == 0 && timer_us == 0 && critical_depth == 0,
"physical wireless veto configured wake or slept");
require(mesh::powerdiag::snapshot(millis()).current & mesh::powerdiag::mask(reason),
"physical wireless veto lost its actual diagnostic reason");
const unsigned before_yield = yield_ms;
board.sleepUntilMicroseconds(micros() + 10000U);
require(yield_ms == before_yield && sleep_calls == 0,
"blocked profile sleep yielded beyond the scanner's safe visit");
mesh::powerdiag::setEnabled(false, millis());
}
mesh::esp32::wireless_blocker = mesh::powerdiag::Reason::Count;
for (bool diagnostics : {false, true}) {
mesh::powerdiag::setEnabled(diagnostics, millis());
for (int source : {0, 1, 2, 3}) {
radio_wake_error = source == 0 ? 31 : ESP_OK;
button_wake_error = source == 1 ? 32 : ESP_OK;
gpio_wake_error = source == 2 ? 33 : ESP_OK;
timer_error = source == 3 ? 34 : ESP_OK;
const unsigned before = sleep_calls;
const uint64_t before_timer = timer_us;
board.sleepUntilMicroseconds(micros() + 10000U);
require(sleep_calls == before && timer_us == before_timer && critical_depth == 0,
"failed timed wake setup slept or reused an old timer");
require(wake_levels[48] == 0 && wake_levels[38] == 0
&& restored_interrupts[48] == GPIO_INTR_POSEDGE
&& restored_interrupts[38] == GPIO_INTR_DISABLE,
"failed timed wake setup left level IRQ or wake pin armed");
}
radio_wake_error = button_wake_error = gpio_wake_error = timer_error = ESP_OK;
gpio_setup_us = 3000;
const unsigned before = sleep_calls;
board.sleepUntilMicroseconds(micros() + 10000U);
require(sleep_calls == before + 1 && timer_us == 7000U,
"absolute timer restarted the full visit after wake setup work");
gpio_setup_us = 0;
const uint64_t last_timer = timer_us;
for (uint32_t delta : {0U, 1U, 1999U, UINT32_MAX}) {
board.sleepUntilMicroseconds(micros() + delta);
require(sleep_calls == before + 1 && timer_us == last_timer && critical_depth == 0,
"expired or subminimum deadline slept or inherited an old timer");
}
board.sleepUntilMicroseconds(micros() + 2000U);
require(sleep_calls == before + 2 && timer_us == 2000U,
"minimum useful absolute deadline did not sleep");
mock_millis = 4294967U;
board.sleepUntilMicroseconds(micros() + 5000U);
require(sleep_calls == before + 3 && timer_us == 5000U,
"microsecond clock wrap corrupted a valid absolute deadline");
board.irq = UINT32_MAX;
board.sleepUntilMicroseconds(micros() + 10000U);
require(sleep_calls == before + 3 && timer_us == 5000U,
"profile deadline accepted a timer without a radio IRQ wake");
board.irq = 48;
irq_during_setup = true;
board.sleepUntilMicroseconds(micros() + 10000U);
require(sleep_calls == before + 3 && timer_us == 5000U && critical_depth == 0,
"profile wait slept over an IRQ arriving during wake setup");
irq_during_setup = radio_irq_high = false;
mesh::powerdiag::setEnabled(false, millis());
}
} catch (const std::exception& error) {
std::cerr << error.what() << '\n'; return 1;
}
}
'''
with tempfile.TemporaryDirectory(prefix="meshcore-esp32-profile-deadline-") as directory:
cpp = Path(directory) / "sleep.cpp"
exe = Path(directory) / "sleep"
cpp.write_text(harness.replace("@METHODS@", methods), encoding="ascii")
result = subprocess.run([
os.environ.get("CXX", "c++"), "-std=c++17", "-Wall", "-Wextra",
"-DARDUINO_USB_MODE=1", "-DARDUINO_USB_CDC_ON_BOOT=1",
"-DMESH_ESP32_USB_CONSOLE_COOPERATIVE=0", "-DMESH_USB_LOGGING_AVAILABLE=0",
"-DMOMENTARY_BUTTON_WAKE_FROM_SLEEP=1", "-DPIN_USER_BTN=38",
"-I", str(ROOT / "src"), str(cpp), "-o", str(exe)],
capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(exe)], capture_output=True, text=True, timeout=10)
self.assertEqual(result.returncode, 0, result.stderr)
def test_v4_profiles_inherit_button_wake_with_the_actual_gpio_pins(self):
config = v4_profile_config()
# These standalone partition-migration utilities deliberately bypass
+8 -1
View File
@@ -101,8 +101,15 @@ static mesh::Radio radio_driver;
sleep_getter = " unsigned getPowerSaveSleepMilliseconds(unsigned limit) { return limit; }"
assert fixture.count(sleep_getter) == 1, "Missing sleep-deadline peripheral boundary"
fixture = fixture.replace(sleep_getter, sleep_getter + "\n" +
" bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }")
" bool getNextNoiseFloorWakeDelay(uint32_t&) const { return false; }\n" +
# This RF boundary has no settled scanner proof, so a live radio2
# must keep polling. Execute the production deadline helper below.
" bool getDualRadioIdleDelay(uint32_t& ms) const { ms = 0; return false; }")
source += "\n#include <helpers/PowerManagementUtils.h>\n"
profile_helper = body((ROOT / "src/helpers/esp32/ProfileIdleSleep.h").read_text(),
"template <typename Mesh, typename Board>")
source += ("\nstatic int64_t esp_timer_get_time() { return micros(); }\n"
"namespace mesh { namespace esp32 {\n" + profile_helper + "\n} }\n")
return source + morse_radio + (fixture.replace("@SETTER@", setter).replace("@GETTER@", getter)
.replace("@COMMAND_PUMP@", pump).replace("@POWER_LOOP@", power_loop))
+19 -2
View File
@@ -356,6 +356,8 @@ int main() {
"void sleep(uint32_t secs) override",
"void sleepMilliseconds(uint32_t millisecs) override",
"void sleepMicroseconds(uint64_t wake_us, bool require_timer = false)",
"void sleepUntilMicroseconds(uint32_t deadline_us)",
"void sleepMicrosecondsImpl(uint64_t wake_us, bool require_timer,",
))
code = r'''
#include <cassert>
@@ -388,6 +390,9 @@ static esp_err_t esp_light_sleep_start() { ++sdk_calls; now_us += sleep_duration
namespace mesh {
static bool logging = false;
static bool isUsbLoggingEnabled() { return logging; }
namespace esp32 {
static powerdiag::Reason manualLightSleepBlocker() { return powerdiag::Reason::Count; }
}
}
struct MainBoard {
virtual void sleep(uint32_t) = 0;
@@ -412,8 +417,8 @@ int main() {
board.sleepMilliseconds(0);
assert(sdk_calls == 2 && timer_us == 50000ULL);
setEnabled(true, millis()); reset(millis()); decision(0, millis());
timer_error = 29; gpio_error = 31; pin_error = 33;
board.sleep(30); // Diagnostics may observe failures, never repair/change policy.
timer_error = 29;
board.sleep(30); // Keep the legacy seconds API's timer-error policy.
auto value = snapshot(millis());
assert(sdk_calls == 3 && value.calls == 1 && value.returns == 1 && value.errors == 1);
assert(value.detail == 29); // First setup failure, without changing later SDK calls.
@@ -426,6 +431,18 @@ int main() {
value = snapshot(millis());
assert(sdk_calls == before_failed_slice && value.calls == 0);
assert(value.errors == 1 && value.detail == 29 && critical_depth == 0);
const int gpio_errors[] = {31, 33};
for (int error : gpio_errors) {
reset(millis()); decision(0, millis());
timer_error = 0;
gpio_error = error == 31 ? error : 0;
pin_error = error == 33 ? error : 0;
board.sleep(30);
value = snapshot(millis());
assert(sdk_calls == before_failed_slice && value.calls == 0 && value.returns == 0);
assert(value.errors == 1 && value.detail == error && critical_depth == 0);
assert(value.eligible_ms == 0 && value.call_us == 0);
}
reset(millis()); decision(0, millis());
timer_error = gpio_error = pin_error = 0; sleep_error = 41;
board.sleep(30);
+249
View File
@@ -0,0 +1,249 @@
"""Execute ESP32 scanner publication and its late, integer-only idle query."""
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from cpp_source import body
from test_radio_profile_scan import HARNESS as SCAN_HARNESS
ROOT = Path(__file__).resolve().parents[1]
ESP_MEMBERS = r'''
uint64_t _esp32_profile_visit_started_us=0, _esp32_profile_idle_stamp_us=0;
uint32_t _esp32_profile_idle_visit_us=0, _esp32_profile_idle_switch_us=0;
uint32_t _esp32_profile_idle_generation[2]={};
uint16_t _esp32_profile_idle_preamble=0;
uint8_t _esp32_profile_idle_profile=0;
uint32_t _wd_observe_until=0;
bool _rx_retry_pending=false, _morse_owned=false;
bool _beacon_owned=false, _beacon_restoring=false, _morse_rx_owned=false;
bool getProfileIdleDelay(uint32_t&) const;
'''
COMMON = r'''
// Complete both independent baselines after startup without faking the idle
// token: its RX stamp, generation, preamble and visit come from the scanner.
static void ready(RadioLibWrapper& w, uint8_t sf=11, float bw=125) {
w._profiles.primary.sf=w._cur_sf=sf;
w._profiles.primary.bw=w._cur_bw=bw;
w.enable();
assert(w._profiles.switchTestReady());
w._nf_refresh_requested=w._nf_calib_active=false;
w._noise_floor_valid=w._noise_floor_secondary_valid=true;
w.serviceProfileScan();
assert(w._esp32_profile_idle_visit_us>0);
}
static bool idle(RadioLibWrapper& w, uint32_t expected) {
uint32_t delay=999;
const bool allowed=w.getProfileIdleDelay(delay);
assert(delay==expected);
return allowed;
}
'''
ADMISSION = r'''
int main() {
RadioLibWrapper w; ready(w);
const uint64_t start=w._esp32_profile_visit_started_us;
const uint32_t visit=w._esp32_profile_idle_visit_us;
assert(visit>30000);
// Frontend settling keeps polling even when the whole visit is long.
elapsed_us=start+RadioLibWrapper::NoiseFloorSettleMillis*1000-1;
assert(!idle(w,0));
elapsed_us=start+RadioLibWrapper::NoiseFloorSettleMillis*1000;
assert(idle(w,10));
// The original RX stamp survives repeated loop passes on the same tuple.
w.serviceProfileScan();
assert(w._esp32_profile_visit_started_us==start && idle(w,10));
// Visit end is strict: require 4 ms guard plus at least 2 ms of useful idle.
for (uint32_t remaining=0; remaining<=17000; ++remaining) {
elapsed_us=start+visit-remaining;
const auto usable=remaining>=6000 ? (remaining-4000)/1000 : 0;
const auto expected=usable>10 ? 10 : usable;
assert(idle(w,expected)==(expected!=0));
}
elapsed_us=start+visit+1;
assert(!idle(w,0));
// Stale low-32-bit clock equality after a full wrap is never a new visit.
elapsed_us=start+0x100000000ULL+10000;
assert(!idle(w,0));
elapsed_us=start-1;
assert(!idle(w,0));
// A late IRQ, RX restart, owner or changed tuning proof revokes admission.
auto denied=[&](auto mutate) {
RadioLibWrapper fresh; ready(fresh);
elapsed_us=fresh._esp32_profile_visit_started_us+10000;
assert(idle(fresh,10));
mutate(fresh);
assert(!idle(fresh,0));
};
denied([](auto& x){x._profiles.secondary.mode=mesh::RadioProfileMode::Off;});
denied([](auto& x){x._esp32_profile_idle_visit_us=0;});
denied([](auto& x){x._params_valid=false;});
denied([](auto& x){x.state=STATE_TX_WAIT;});
denied([](auto& x){x._profile_rxps_suspended=false;});
denied([](auto& x){x._rx_ps_enabled=true;});
denied([](auto& x){x._rx_ps_armed=true;});
denied([](auto& x){x._rx_hold_continuous=true;});
denied([](auto& x){x._rx_retry_pending=true;});
denied([](auto& x){x._profile_retry_at=1;});
denied([](auto& x){x._profile_refresh_required=true;});
denied([](auto& x){x._profile_repair_required=true;});
denied([](auto& x){x._nf_refresh_requested=true;});
denied([](auto& x){x._nf_calib_active=true;});
denied([](auto& x){x._noise_floor_valid=false;});
denied([](auto& x){x._noise_floor_secondary_valid=false;});
denied([](auto& x){x._wd_observe_until=1;});
denied([](auto& x){x._cw_active=true;});
denied([](auto& x){x._morse_owned=true;});
denied([](auto& x){x._beacon_owned=true;});
denied([](auto& x){x._morse_rx_owned=true;});
denied([](auto& x){x.events.record();});
denied([](auto& x){x._profiles.resetSwitchTest();});
denied([](auto& x){x._active_profile=2;});
denied([](auto& x){x._active_profile^=1;});
denied([](auto& x){++x._profile_generation;});
denied([](auto& x){++x._profiles.generation[0];});
denied([](auto& x){++x._profiles.generation[1];});
denied([](auto& x){++x._profile_scan_generation[0];});
denied([](auto& x){++x._profile_scan_generation[1];});
denied([](auto& x){++x._esp32_profile_idle_generation[0];});
denied([](auto& x){++x._esp32_profile_idle_generation[1];});
denied([](auto& x){++x._esp32_profile_visit_started_us;});
denied([](auto& x){++x._physical_preamble;});
denied([](auto& x){++x._profiles.switch_test_max_us[1];});
// Both scanner generations matter even if a replacement copied the same
// active tuple and its retune sample budget happened to be unchanged.
denied([](auto& x){
++x._profiles.generation[1]; ++x._profile_scan_generation[1];
});
}
'''
SCANNER = r'''
int main() {
RadioLibWrapper w; ready(w);
auto start=w._esp32_profile_visit_started_us;
elapsed_us=start+10000;
assert(idle(w,10));
w._cw_active=true; w.serviceProfileScan();
assert(w._esp32_profile_idle_visit_us==0 && !idle(w,0));
w._cw_active=false; w.serviceProfileScan(); assert(idle(w,10));
w._profile_retry_at=millis()+1000; w.serviceProfileScan();
assert(w._esp32_profile_idle_visit_us==0);
w._profile_retry_at=0; w.serviceProfileScan(); assert(idle(w,10));
w.busy=true; w.serviceProfileScan(); assert(!idle(w,0));
w.busy=false; w.packet=true; w.serviceProfileScan(); assert(!idle(w,0));
w.packet=false; w.events.record(); w.serviceProfileScan(); assert(!idle(w,0));
w.events.claim(); w.serviceProfileScan(); assert(idle(w,10));
// A timer wake advances the same clock the scanner uses. It cannot let a
// due scan spend an extra visit on the old profile or reuse its old budget.
auto previous=w._active_profile;
elapsed_us=start+w._esp32_profile_idle_visit_us;
w.serviceProfileScan();
assert(w._active_profile!=previous && !idle(w,0));
start=w._esp32_profile_visit_started_us;
w.serviceProfileScan();
assert(w._esp32_profile_idle_stamp_us==start && !idle(w,0));
// The fast visit is sub-ms (SF5/BW500) and must keep polling throughout.
RadioLibWrapper fast; ready(fast,5,500);
assert(fast._esp32_profile_idle_visit_us<1000 && !idle(fast,0));
elapsed_us+=fast._esp32_profile_idle_visit_us;
fast.serviceProfileScan();
assert(!idle(fast,0));
// A long hold spanning micros() wrap still causes a real hop on release.
RadioLibWrapper wrap; ready(wrap);
const auto old_profile=wrap._active_profile;
elapsed_us=wrap._esp32_profile_visit_started_us+0x100000000ULL+10000;
wrap.serviceProfileScan();
assert(wrap._active_profile!=old_profile && !idle(wrap,0));
assert(wrap._esp32_profile_visit_started_us==elapsed_us);
assert(wrap._profile_visit_stamp==uint32_t(elapsed_us));
RadioLibWrapper changed; ready(changed);
elapsed_us=changed._esp32_profile_visit_started_us+10000;
assert(idle(changed,10));
auto replacement=changed._profiles.secondary;
replacement.params.freq+=1;
changed._profiles.setSecondary(replacement,false);
assert(!idle(changed,0));
changed.serviceProfileScan();
assert(changed._esp32_profile_idle_visit_us==0 && !idle(changed,0));
assert(changed._nf_refresh_requested && !changed._noise_floor_secondary_valid);
changed._profiles.setSecondary({},false);
changed.serviceProfileScan();
assert(changed._esp32_profile_idle_visit_us==0 && !idle(changed,0));
assert(!changed._profile_rxps_suspended && changed._rx_ps_enabled);
}
'''
class Esp32ProfileIdleTest(unittest.TestCase):
@staticmethod
def fixture():
source = (ROOT / "src/helpers/radiolib/RadioLibWrappers.cpp").read_text()
names = [("uint8_t", "beginReconfigure"), ("void", "endReconfigure"),
("void", "serviceProfileScan")] + [("mesh::RadioParamApplyResult", name) for name in
("tuneProfile", "prepareTransmitProfile", "tryRestoreCodingRate",
"trySetParams", "trySetPrimaryParams")]
methods = "\n".join(body(source, f"{kind} RadioLibWrapper::{name}(") for kind, name in names)
methods += "\n" + body(source,
"mesh::RadioParamApplyResult RadioLibWrapper::prepareTransmitProfile(uint8_t profile, bool")
query = body(source, "bool RadioLibWrapper::getProfileIdleDelay(")
# This final admission must never perform SPI, derive float timing or
# run the scanner. Every such operation precedes the published proof.
for forbidden in ("profilePreamble(", "listenUs(", "profileTimestamp(",
"isChipBusy(", "isPacketPendingOrReceiving(", "tuneProfile("):
assert forbidden not in query, forbidden
methods += "\n" + query
wrapper = (ROOT / "src/helpers/radiolib/CustomSX1262Wrapper.h").read_text()
methods += "\n" + body(wrapper, "void setProfileStandbyWarm(").replace(
"void setProfileStandbyWarm(bool enabled) override",
"void RadioLibWrapper::setProfileStandbyWarm(bool enabled)")
fixture = SCAN_HARNESS.split("int main()", 1)[0]
fixture = fixture.replace(" bool _cw_active=false;", ESP_MEMBERS + "\n bool _cw_active=false;")
fixture = fixture.replace("static uint64_t elapsed_us;", """
#define MESH_ENABLE_TX_BEACONS 1
#define MESH_ENABLE_MORSE_RX 1
static uint64_t elapsed_us;
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
def run_fixture(self, main):
with tempfile.TemporaryDirectory(prefix="meshcore-esp32-profile-idle-") as directory:
folder = Path(directory)
(folder / "Arduino.h").write_text("#pragma once\n#include <cstdint>\n#include <cmath>\n")
(folder / "test.cpp").write_text(self.fixture() + main)
flags = ["-std=c++17", "-Wall", "-Wextra", "-Werror", "-DESP32_PLATFORM=1"]
if os.name != "nt":
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
result = subprocess.run([os.environ.get("CXX", "c++"), *flags,
"-I", directory, "-I", str(ROOT / "src"), str(folder / "test.cpp"),
"-o", str(folder / "test")], capture_output=True, text=True, timeout=60)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
result = subprocess.run([str(folder / "test")], capture_output=True, text=True, timeout=20)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
def test_late_integer_admission_boundaries_and_revocation(self):
self.run_fixture(ADMISSION)
def test_real_scanner_clock_hops_and_failed_publication(self):
self.run_fixture(SCANNER)
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -121,6 +121,7 @@ int main() {
#define ESP32_PLATFORM
uint32_t clockUs=0;
uint32_t micros() { return clockUs; }
int64_t esp_timer_get_time() { return clockUs; }
@CLOCK@
struct BenchWrapper { uint32_t _profile_visit_stamp=0; @GETTER@ };
int main() {
+14 -1
View File
@@ -20,6 +20,8 @@ struct FakeRtc {
} rtc;
#define NRF_RTC2 (&rtc)
uint32_t micros() { return 1234; }
static uint64_t esp_elapsed_us=1234;
int64_t esp_timer_get_time() { return static_cast<int64_t>(esp_elapsed_us); }
@CLOCK@
int main() {
assert(profileTimestamp(false) == 1234);
@@ -61,6 +63,17 @@ int main() {
rtc.COUNTER=(rtc.COUNTER+32768)&0x00ffffffUL;
const auto wrapped=profileTimestamp(true);assert(wrapped==0);
assert(profileElapsedUs(before,wrapped,true)==1000000UL);
#elif defined(ESP32_PLATFORM)
assert(profileTimestamp(true) == 1234);
// The ESP Timer is the light-sleep-adjusted clock; a frozen cycle/micros
// source must not extend a radio visit while the MCU was asleep.
esp_elapsed_us+=10000;
assert(micros()==1234 && profileTimestamp(true)==11234);
assert(profileTimestamp(false)==11234);
esp_elapsed_us=0x100000010ULL;
assert(profileTimestamp(true)==0x10);
assert(profileElapsedUs(0xfffffff0,profileTimestamp(true),true)==32);
assert(rtc.TASKS_START == 0 && rtc.TASKS_STOP == 0);
#else
assert(profileTimestamp(true) == 1234);
assert(profileElapsedUs(12, 42, true) == 30);
@@ -76,7 +89,7 @@ class ProfileClockTest(unittest.TestCase):
cpp = Path(folder) / "test.cpp"
exe = Path(folder) / "test.exe"
cpp.write_text(HARNESS.replace("@CLOCK@", CLOCK))
for defines in ([], ["-DNRF52_PLATFORM=1"]):
for defines in ([], ["-DNRF52_PLATFORM=1"], ["-DESP32_PLATFORM=1"]):
result = subprocess.run(
[os.environ.get("CXX", "g++"), "-std=c++17", "-Wall", "-Wextra",
"-Werror", *defines, str(cpp), "-o", str(exe)],
+9 -1
View File
@@ -76,8 +76,10 @@ int main() {
Manager manager;
Dispatcher legacy{&unknown, &clock, &manager};
assert(legacy.canIdleDualRadio());
assert(!legacy.getDualRadioIdleDelay(unchanged) && unchanged == 0);
unknown.profile.active = true;
assert(!legacy.canIdleDualRadio());
assert(!legacy.getDualRadioIdleDelay(unchanged) && unchanged == 0);
assert(legacy.getNextQueueWakeDelay(unchanged) && unchanged == 0);
for (uint32_t now : {1000U, UINT32_MAX - 15U}) {
@@ -87,20 +89,25 @@ int main() {
Dispatcher node{&radio, &clock, &manager};
uint32_t delay = 99;
assert(node.canIdleDualRadio() && radio.reads == 0);
assert(!node.getDualRadioIdleDelay(delay) && delay == 0 && radio.reads == 0);
assert(!node.getNextQueueWakeDelay(delay) && radio.reads == 0);
radio.profile.active = true;
radio.delay = 37;
assert(!node.canIdleDualRadio()); // Unknown ports keep their polling contract.
assert(!node.getDualRadioIdleDelay(delay) && delay == 0); // Ignore a false writer's output.
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
radio.supported = true;
radio.delay = 0;
assert(!node.canIdleDualRadio()); // A due/too-short visit is not an idle budget.
assert(!node.getDualRadioIdleDelay(delay) && delay == 0);
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
radio.delay = 37;
assert(node.canIdleDualRadio());
assert(node.getDualRadioIdleDelay(delay) && delay == 37);
assert(node.getNextQueueWakeDelay(delay) && delay == 37);
node.radio_available = false;
assert(!node.canIdleDualRadio());
assert(!node.getDualRadioIdleDelay(delay) && delay == 0);
assert(node.getNextQueueWakeDelay(delay) && delay == 0);
node.radio_available = true;
@@ -173,7 +180,8 @@ class RadioProfileIdleDispatcherTest(unittest.TestCase):
header = (ROOT / 'src/Dispatcher.h').read_text(encoding='ascii')
source = (ROOT / 'src/Dispatcher.cpp').read_text(encoding='ascii')
code = HARNESS.replace('@DEFAULT@', method(header, 'virtual bool getProfileIdleDelay('))
code = code.replace('@ADMISSION@', method(header, 'bool canIdleDualRadio() const'))
code = code.replace('@ADMISSION@', method(header, 'bool canIdleDualRadio() const') + '\n'
+ method(header, 'bool getDualRadioIdleDelay('))
code = code.replace('@DEADLINES@', method(source, 'bool Dispatcher::getNextQueueWakeDelay('))
with tempfile.TemporaryDirectory(prefix='meshcore-profile-idle-dispatcher-') as directory:
cpp, binary = Path(directory) / 'test.cpp', Path(directory) / 'test'