Enable reversible ESP32 BLE shutdown and bounded radio2 CPU sleep
This commit is contained in:
1 parent
a3d77a9a76
commit
61eecbd971
38 files changed
+1829
-117
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "BLEDevice.h"
|
||||
+187
@@ -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) {}
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "BLEDevice.h"
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "BLEDevice.h"
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "BLEDevice.h"
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "BLEDevice.h"
|
||||
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
#include "BLEDevice.h"
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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('*'):
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)],
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user