ESP32, tasks and flow control

Hi,

I have a project where I need a WiFi scan. I plan to start a task for this.
The main loop might enable WiFi and give a signal so the task can start the WiFi scan. The main loop will continue with other functions, the scan shall run over and over again until the main loop gives a signal to stop.
My first plan was using a mutex. So the main loop takes the mutex (because the scan should not be started yet). Then WiFi will be enabled and the scans should start. Some time later the main loop will give the signal to the task to start no new scan, but is allowed to finish the current scan, if it is running.

In short:

Scan Task
loop
  takeMutex // wait forever
    start scan
    process results
    giveMutex

main loop
  do something
    on special event:
      enable WiFi
      giveMutex
  do more stuff
    on other event
      takeMutex
      disable WiFi
  continue working

The project is currently too large for posting.

My problem / question is:
Is the mutex a good way because the control / check should be done in the main loop and neither blocking the main loop nor trying to take the mutex and if it doesn't get it in time try another task some time later. This might result that the main loop needs several tries to stopp the scan.
A normal semaphore also works the way like a mutex.

So I'm thinking about only using global variables:

bool wifiScanStart;
bool wifiScanRunning;

wifiScanTask:
  endless loop
    while wifiScanStart==false
      delay
    wifiScanRunning = true
    do the scan
    check and process the results
    wifiScanRunning = false
    longer delay

main loop
  do something
    on special event:
      enable WiFi
      wifiScanStart=true
  do more stuff
    on other event
      if wifiScanRunning==false
        wifiScanStart=false
        disable WiFi
  continue working

Since the tasks run parallel it might be possible that the check in the main loop
if wifiScanRunning==false
wifiScanStart=false
will be processes while the scan task processes
while wifiScanStart==false
and and error occur.

So what is a good idea to handle this?
I'm looking for a concept where the scan task will use a method to give the mutex back to the main loop, the main loop itself chan check if it is the owner of the mutex. Or use something like taskENTER_CRITICAL and taskEXIT_CRITICAL around the checks.
Is there a simple other solution?

Regards
Nils

Post an MRE. This is the smallest, complete code that compiles and demonstrates the issue at hand.

It would probably be more appropriate to use FreeRTOS Task Notifications.

Does your system need to be that complicated?

When using the ESP32 RTOS each task behaves as an independent program, so the challenge is to communicate in both directions without blocking each other. The RTOS provides thread-safe queues for this specific purpose. Normally each task would check for inbound messages once per loop. There are faster ways of communicating, but WiFi scanning is a slow process so performance in this case is not important.

Hi,
no, it does not need to be complicated.
I just like to have two tasks:
One starts and process Wifi scan if allowed.
The second (usually the main loop) initiates Wifi scan and sends a command to not start the next scan but turn off Wifi not before the last scan have been processed..

I‘ve started a new sketch as an example but not finished yet.
The main part is working but I just want to make sure that a rare case will be handled properly.

Regards
Nils

So conceptually task A tells task B to do a single scan and report back once done, or is there more to it than that?

A tells B to start scans and process them.
Sometime later A tells B to not start and process new scans, but finish the current one (if there is one).
A reads information from B if B is idle and can continue with stopping parts which are used by A.

Hmmmm… maybe it would be an other idea to handle everything directly in B? So A tells B to start and B enables Wifi, scan and so on until A tells B to not start new scans and B will then turn off Wifi after the current scan finished…
Need to think about this… :grinning:

As I told you back in Post #2, that's exactly what Task Notifications are for.

Hi,

now I have a shorter version of the script where the issue can be checked:

/*
 * Using Arduino IDE v2.2.1
 * Settings:
 * Board: ESP32 / Wemos D1 Mini ESP32
 * CPU frequency: 240 MHz (WiFi/BT)
 * Core Debung Level: "verbose"
 * Erase all Flash before sketch upload: disabled
 * Flash Frequency: 80MHz
 * Partition Scheme: Default
 * Upload Speed: 921600
 *
 * Hardware:
 * Wemos D1 Mini ESP32
*/

#include <WiFi.h>
#include <esp_wifi.h>

#define WIFI_SCAN_PERIOD 1000

bool bWiFiScanStart = false;    // modified only in loop
bool bWiFiScanRunning = false;  // modified only in xTaskWiFiScan
bool bEnableWiFi = false;
bool bWiFiEnabled = false;  // WiFi on or off
bool bAskForDisablingWiFi = false;

TaskHandle_t hWiFiScanHandle;
TaskHandle_t hSwitchScanHandle;

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("\n\n\n");
  xTaskCreatePinnedToCore(xTaskWiFiScan, "xTaskWiFiScan", 10000, NULL, 0, &hWiFiScanHandle, 1);
  xTaskCreatePinnedToCore(xTaskSwitchScan, "xTaskSwitchScan", 10000, NULL, 0, &hSwitchScanHandle, 1);
}

void enableWiFi() {
  Serial.println("C: WiFi.setSleep(false)=" + String(WiFi.setSleep(false)));
  Serial.println("C: WiFi.mode(WIFI_STA)=" + String(WiFi.mode(WIFI_STA)));
  bWiFiEnabled = true;  // WiFi is enabled
  bEnableWiFi = false;  // do not enable again
  Serial.println("C: WiFi turned on");
}
void disableWiFi() {
  //Serial.println("D: WiFi.mode(WIFI_OFF)=" + String(WiFi.mode(WIFI_OFF)));
  // WiFi.mode(WIFI_OFF) should be the same as esp_wifi_stop()?
  esp_err_t wifiStop = esp_wifi_stop();
  Serial.print("D: esp_wifi_stop()=");
  if (wifiStop == ESP_OK)
    Serial.println("ok");
  else if (wifiStop == ESP_ERR_WIFI_NOT_INIT)
    Serial.println("ESP_ERR_WIFI_NOT_INIT");
  else
    Serial.println("not ok");

  bool bWiFiSleep = WiFi.setSleep(true);
  Serial.print("D: WiFi.setSleep(true)=");
  if (bWiFiSleep)
    Serial.println("ok");
  else
    Serial.println("not ok");

  esp_err_t powerSave = esp_wifi_set_ps(WIFI_PS_MAX_MODEM);
  Serial.print("D: esp_wifi_set_ps(WIFI_PS_MAX_MODEM)=");
  if (powerSave == ESP_OK)
    Serial.println("ok");
  else
    Serial.println("not ok");

  esp_err_t wifiDeInit = esp_wifi_deinit();
  Serial.print("D: esp_wifi_deinit()=");
  if (wifiDeInit == ESP_OK)
    Serial.println("ok");
  else
    Serial.println("not ok");

  bWiFiEnabled = false;          // WiFi is disabled now
  bAskForDisablingWiFi = false;  // WiFi is disabled, do not disable again
  Serial.println("D: WiFi turned off");
}

void loop() {
  // do some other stuff
  if (!bWiFiEnabled)  // WiFi off?
  {
    if (bEnableWiFi)  // should be turned on?
    {
      enableWiFi();
      bWiFiScanStart = true;  // Notification: start scans
    }
  } else {
    if (bAskForDisablingWiFi)  // should be turned off?
    {
      bWiFiScanStart = false;  // Notification: start no new scan
      if (!bWiFiScanRunning) {
        // no scan currently running
        disableWiFi();
      } else {
        Serial.print("#");
      }
    }
  }
  // continue with other stuff
  delay(20);
}

void xTaskWiFiScan(void* pvParameters) {
  Serial.println("B: xTaskWiFiScan started.");
  int16_t n;
  while (1) {
    Serial.println("B: Scan allowed?");
    while (!bWiFiScanStart)
      vTaskDelay(100 / portTICK_PERIOD_MS);
    // This is the (more or less) critical part. The variable bWiFiScanStart is
    // checked. If it becomes true and the task is processing it, the variable
    // bWiFiScanRunning will be set to true. But in rare cases the FreeRTOS
    // scheduler may interrupt the task here and continue with the main loop.
    // The variable could be changed to false there and because the varaible
    // bWiFiScanRunning is still false, WiFi would be disabled. But then this
    // task will continue and the scan can not be started.
    // THe next vTaskDelay is just a demonstration that this case may happen.
    Serial.println("B: vTaskDelay start.");
    vTaskDelay(20000 / portTICK_PERIOD_MS);
    Serial.println("B: vTaskDelay finished.");
    bWiFiScanRunning = true;
    Serial.println("B: WiFi status: " + String(WiFi.status()));
    Serial.println("B: Start WiFi scan.");
    WiFi.scanNetworks(true);

    n = WiFi.scanComplete();
    while (n < 0) {
      Serial.print(".");
      vTaskDelay(500 / portTICK_PERIOD_MS);
      n = WiFi.scanComplete();
    }
    Serial.println(".");
    Serial.println("B: " + String(n) + " network(s) found.");
    WiFi.scanDelete();
    bWiFiScanRunning = false;
    Serial.println("B: Finished WiFi scan.");
    vTaskDelay(WIFI_SCAN_PERIOD / portTICK_PERIOD_MS);
  }
}

// Dummy task, just for enabling and disabling WiFi
void xTaskSwitchScan(void* pvParameters) {
  Serial.println("A: xTaskSwitchScan started.");
  vTaskDelay(5000 / portTICK_PERIOD_MS);
  bEnableWiFi = true;
  Serial.println("A: Enable WiFi scan.");
  vTaskDelay(60000 / portTICK_PERIOD_MS);
  if (bWiFiEnabled) {
    bAskForDisablingWiFi = true;
    Serial.println("A: Start no more WiFi scan.");
  }
  while (bWiFiScanRunning) {
    Serial.println("A: Check for running WiFi scan.");
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
  Serial.println("A: WiFi scan finished, WiFi can be turned off.");
  while (1) {
    vTaskDelay(20000 / portTICK_PERIOD_MS);
  }
}

just comment the "vTaskDelay(20000 / portTICK_PERIOD_MS);" in the function "xTaskWiFiScan()" and the script may work correctly most times. For my tests this delay causes the error.
Using other kind of notifications might not solve the conceptual issue. This does not mean I will not switch to "xTaskNotification"... :wink:

Regards
Nils

I cannot make sense of that program.
Draw a timing diagram showing how each of your key variables is is supposed to change over time.
Then add a high priority task that continuously that monitors the key variables and each time a change occurs print a timestamp, the variable name and its value.
Then compare the two.

Hi mikb55,

this part of program is just a demonstration. If I start this on an ESP32 I get this output for example:

B: xTaskWiFiScan started.
B: Scan allowed?
B: a) bWiFiScanStart=0, bWiFiScanRunning=0
A: xTaskSwitchScan started.
A: Enable WiFi scan.
D: esp_wifi_init(cfg)=ok
D: esp_wifi_set_ps(WIFI_PS_NONE)=ok
C: WiFi.setSleep(false)=1
C: WiFi.disconnect()=0
C: WiFi.mode(WIFI_STA)=1
C: WiFi turned on
B: b) bWiFiScanStart=1, bWiFiScanRunning=0
B: vTaskDelay start.
B: vTaskDelay finished.
B: c) bWiFiScanStart=1, bWiFiScanRunning=1
B: WiFi status: 255
B: Start WiFi scan.
..............
B: 12 network(s) found.
B: Finished WiFi scan.
B: Scan allowed?
B: a) bWiFiScanStart=1, bWiFiScanRunning=0
B: b) bWiFiScanStart=1, bWiFiScanRunning=0
B: vTaskDelay start.
B: vTaskDelay finished.
B: c) bWiFiScanStart=1, bWiFiScanRunning=1
B: WiFi status: 255
B: Start WiFi scan.
..............
B: 12 network(s) found.
B: Finished WiFi scan.
B: Scan allowed?
B: a) bWiFiScanStart=1, bWiFiScanRunning=0
B: b) bWiFiScanStart=1, bWiFiScanRunning=0
B: vTaskDelay start.
A: Start no more WiFi scan.
A: WiFi scan finished, WiFi can be turned off.
D: esp_wifi_stop()=ok
D: WiFi.setSleep(true)=ok
D: esp_wifi_set_ps(WIFI_PS_MAX_MODEM)=ok
D: esp_wifi_deinit()=ok
D: WiFi turned off
B: vTaskDelay finished.
B: c) bWiFiScanStart=0, bWiFiScanRunning=1
B: WiFi status: 255
B: Start WiFi scan.
[ 82222][E][WiFiGeneric.cpp:1272] mode(): Could not set mode! 12289
........................................................

I marked the section bold which may occur and can cause the issue I'm talking about. The vTaskDelay start and end message is just to force the issue. Of course this snippet is not complete, but I'm asking for help on a conceptual problem, not having the issue with the code itself. The last wifi error message is just of the issue above.

I'm looking for a solution for my specific problem. An other example would be a car racing. All cars are waiting for the traffic light to get green (here: "A: Enable WiFi scan"). Then the cars start racing. When the first car get close to the finish mark in the last round, the black-white flag will be raised ("A: Start no more WiFi scan."). But this will not cause all cars to stop, they finish their round. And after all cars left the racing course it can be cleaned up.

If I would like to force a break I could disable WiFi and ignore the error. Or use vTaskDelete to kill the task. Or something like this. But this is not my question.

Thank you
Nils

I highly recommend that you spend some time reading the attached guide. You can skip Chapters 1 & 2.

FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf (4.4 MB)

A cautionary note, that particular document doesn't mention the watchdog at all. Presumably because it's talking about the generic FreeRTOS rather than the specific implementation used in the ESP32. As such some of the examples would not make sense on an ESP32 as the watchdog would likely trigger.

As I understand it, on the ESP32 you need to make sure that high priority tasks briefly enter the blocked state every few milliseconds so that the low priority task that feeds the watchdog has a chance to run. (You also have the less elegant option of manually feeding the watchdog using esp_task_wdt_feed().)

Therefore as a matter of routine when writing a new task, I add a delay(1) to the last line of the infinite loop. This is translated into a vTaskDelay which briefly puts the task into a blocked state. Later on adding other code may make that delay() redundant, but at the beginning of the writing process it helps avoid triggering watchdog reboots.

Which ones? Almost all the examples I saw in that reference had the task putting themselves in the Blocked state for one reason or another .... intentional delay, pending on a notification, waiting for data to enter a queue, etc. Doing so is, of course, fundamental to programming in the multi-tasking OS paradigm and represents something radically new to newbies coming from Arduino land.

Yes and No, it appears to depend on which core the task is running on. Here's what I've found through experimentation:

  • On Core 1 you can have a high-priority task hog all the CPU time without blocking and not trigger the watchdog .... as long as interrupts remain enabled. That tells me that the Core 1 watchdog is reset in the FreeRTOS Tick Interrupt. Of course it means that the (Priority 0) Idle Task will never run. But, that's probably OK given that there's only one task (the user code) running.

  • The situation is different on Core 0. There, if any task with a Priority higher than the WiFi task hogs the CPU, the watchdog will trigger .... even if interrupts remain enabled.

On the ESP32 there are multiple watchdogs. If your configuration enables the Task Watchdog Timer (TWDT) then you have to let the idle task run otherwise it'll trigger and reboot (if configured).

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/wdts.html

I guess the assumption is that in a 'properly functioning' system you would expect all the tasks to be given CPU time. For example, if a high priority task monopolizes the CPU and the low priority tasks aren't allowed to run then why do these low priority tasks even exist?

I've been using whatever watchdog IWDT and TWDT defaults are set in the PlatformIO ESP32 plugin. I have no idea what settings the Arduino IDE uses when creating ESP32 projects.

In your system it looks like core 0 is using the TWDT, but core 1 isn't. Perhaps that was done intentionally to make it easy for people to port code from other platforms.

That may be true. But out of the box it behaves as I described. Try running the below code. Recompile and run it with all 4 combinations of the 'disableInterrupts' and 'core' variables and note which ones cause constant reboots and which don't.

const bool disableInterrupts = false;
const uint8_t core = 1;
void hungTask(void *pvParameters);

void setup() {
  Serial.begin(115200);
  delay(3000);
  Serial.println("Starting");

  BaseType_t returnCode = xTaskCreatePinnedToCore(hungTask, "Hung Task", 2000, NULL, 8, NULL, core);
  assert(returnCode != pdFAIL);
}

void hungTask(void *pvParameters) {
  volatile uint32_t var = 0;
  Serial.println("Entered Hung Task");
  vTaskDelay(100);

  if (disableInterrupts) {
    noInterrupts();
  }

  for (;;) {
    var++;
  }
}

void loop() {
}

You can predict the behavior by looking at the specific project, or default project settings file defined by skdconfig.
For example, AppData\Local\Arduino15\packages\esp32\hardware\esp32\2.0.3\tools\sdk\esp32\sdkconfig

As I speculated, on the Arduino IDE someone has disabled the task watchdog for core 1.

CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y
# CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1 is not set

This inconsistent behavior means that someone who pastes some example non-yielding code that uses xTaskCreate will likely see behavior that randomly alternates between working fine and and rebooting depending on which core it gets assigned to. Perhaps this is the origin of the delay(1) I saw and copied into my code which magically made the problem go away.

If you reserve core 0 for WiFi and pin all user code to core 1 then these problems never appear.

Agreed. As a guess, I'd say it was done that way (at least for the Arduino ESP32 Core) so that "normal" Arduino code could be ported the the ESP32 and run on Core 1 with fewer problems.

The above notwithstanding, my original point is still valid. If you want to learn how to write code for FreeRTOS on an ESP32, then start by learning how to write code for Vanilla FreeRTOS. After that, learn the exceptions specific to ESP32.

Hello,

yes, as far as I have understood yet (but still reading the linked document, read a book about FreeRTOS before), the watch dogs are there, but they aren't my problem.
Since I try to cut down a sketch into a part just for asking on support for a logical question or in other words an algorithm, my sketch seems to not make sense anyway.

The task "xTaskSwitchScan" can be understood as a user pressing a button to start the WiFi scans, also another press of the button will stop them. But as written before, I do not like to just disable them if the user presses the button the second time, the loop should be finish its work. So telling a task to start somehow (using a variable like I've done here or using task notification or explicitly a semaphore does not solve my issue: between the detection for start the loop again and setting the variable, that a scan is now in progress it is possible that the FreeRTOS scheduler just cut the process here and switches to the main loop. The main loop may just then tell the scan task to not start a new scan, but detects that the bool variable (or another notification like a semaphore) has not been changed. This has nothing to do with the understanding of multi tasking.
I have two ideas:
a) change the check in the scan loop from "while(x){...}" to a "do{...portENTER_CRITICAL(&mux);...portEXIT_CRITICAL(&mux);...}while(x)"
b) chaning the task priority for the scan task in the main loop while the status is checked - but this only works if the main loop and the scan task run on the same core

For idea a) the scan task woo be changed to:

void xTaskWiFiScan(void* pvParameters) {
  Serial.println("xTaskWiFiScan(): Executing on core " + String(xPortGetCoreID()));
  Serial.println("B: xTaskWiFiScan started.");
  int16_t n;
  while (1) {
    Serial.println("B: Scan allowed?");
    do {
      vTaskDelay(100 / portTICK_PERIOD_MS);
      Serial.println("B: a) bWiFiScanStart=" + String(bWiFiScanStart) + ", bWiFiScanRunning=" + String(bWiFiScanRunning));
      Serial.println("B: portENTER_CRITICAL.");
      Serial.flush();
      portENTER_CRITICAL(&mux);
      if (bWiFiScanStart == true) {
        Serial.println("B: vTaskDelay start.");
        delay(5000);
        Serial.println("B: vTaskDelay finished.");
        bWiFiScanRunning = true;
      }
      portEXIT_CRITICAL(&mux);
      Serial.println("B: portEXIT_CRITICAL.");
      Serial.println("B: b) bWiFiScanStart=" + String(bWiFiScanStart) + ", bWiFiScanRunning=" + String(bWiFiScanRunning));
      vTaskDelay(20000 / portTICK_PERIOD_MS);
      Serial.println("B: after vTaskDelay");
      Serial.println("B: c) bWiFiScanStart=" + String(bWiFiScanStart) + ", bWiFiScanRunning=" + String(bWiFiScanRunning));
    } while (!bWiFiScanRunning);
    Serial.println("B: WiFi status: " + String(WiFi.status()));
    Serial.println("B: Start WiFi scan.");
    WiFi.scanNetworks(true);

    n = WiFi.scanComplete();
    while (n < 0) {
      Serial.print(".");
      vTaskDelay(500 / portTICK_PERIOD_MS);
      n = WiFi.scanComplete();
    }
    Serial.println(".");
    Serial.println("B: " + String(n) + " network(s) found.");
    WiFi.scanDelete();
    bWiFiScanRunning = false;
    Serial.println("B: Finished WiFi scan.");
    vTaskDelay(WIFI_SCAN_PERIOD / portTICK_PERIOD_MS);
  }
  vTaskDelete(NULL);
}

I think this solution for just making sure that bWiFiScanStart is checked to be true and then bWiFiScanRunning is set to true seems to be a good solution. However, using a notification to wait for execution would be the next step.
Additional, the while loop can be simplified to

    do {
      vTaskDelay(100 / portTICK_PERIOD_MS);
      portENTER_CRITICAL(&mux);
      bWiFiScanRunning = bWiFiScanStart;
      portEXIT_CRITICAL(&mux);
    } while (!bWiFiScanRunning);

(and removed all testing code). I'm not sure if the critical commands are needed here because I don't know how the setting of the variable is done internally.

So with these changes and also using vTaskSuspend/vTaskResume the scan task can be controlled.
This also allows to skip the usage of notifications since the benefit for blocking a task without cpu consumption can be ignored.

The complete short sketch:

/*
 * Using Arduino IDE v2.2.1
 * Settings:
 * Board: ESP32 / Wemos D1 Mini ESP32
 * CPU frequency: 240 MHz (WiFi/BT)
 * Core Debung Level: "verbose"
 * Erase all Flash before sketch upload: disabled
 * Flash Frequency: 80MHz
 * Partition Scheme: Default
 * Upload Speed: 921600
 *
 * Hardware:
 * Wemos D1 Mini ESP32
*/

#include <WiFi.h>
#include <esp_wifi.h>

#define WIFI_SCAN_PERIOD 1000

portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;

bool bWiFiScanStart = false;    // modified only in loop
bool bWiFiScanRunning = false;  // modified only in xTaskWiFiScan
bool bEnableWiFi = false;
bool bWiFiEnabled = false;  // WiFi on or off
bool bAskForDisablingWiFi = false;

TaskHandle_t hWiFiScanHandle;
TaskHandle_t hSwitchScanHandle;

void setup() {
  Serial.begin(115200);
  delay(2000);
  Serial.println("\n\n\n");
  xTaskCreatePinnedToCore(xTaskWiFiScan, "xTaskWiFiScan", 10000, NULL, 0, &hWiFiScanHandle, 0);
  vTaskSuspend(hWiFiScanHandle);
  xTaskCreatePinnedToCore(xTaskSwitchScan, "xTaskSwitchScan", 10000, NULL, 0, &hSwitchScanHandle, 1);
}

void enableWiFi() {
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  if (esp_wifi_init(&cfg) == ESP_OK)
    Serial.println("ok");
  else
    Serial.println("not ok");

  Serial.print("D: esp_wifi_set_ps(WIFI_PS_NONE)=");
  if (esp_wifi_set_ps(WIFI_PS_NONE) == ESP_OK)
    Serial.println("ok");
  else
    Serial.println("not ok");

  Serial.println("C: WiFi.setSleep(false)=" + String(WiFi.setSleep(false)));
  esp_wifi_start();
  Serial.println("C: WiFi.disconnect()=" + String(WiFi.disconnect()));
  Serial.println("C: WiFi.mode(WIFI_STA)=" + String(WiFi.mode(WIFI_STA)));
  bWiFiEnabled = true;  // WiFi is enabled
  bEnableWiFi = false;  // do not enable again
  Serial.println("C: WiFi turned on");
}
void disableWiFi() {
  esp_err_t wifiStop = esp_wifi_stop();
  Serial.print("D: esp_wifi_stop()=");
  if (wifiStop == ESP_OK)
    Serial.println("ok");
  else if (wifiStop == ESP_ERR_WIFI_NOT_INIT)
    Serial.println("ESP_ERR_WIFI_NOT_INIT");
  else
    Serial.println("not ok");

  Serial.print("D: WiFi.setSleep(true)=");
  if (WiFi.setSleep(true))
    Serial.println("ok");
  else
    Serial.println("not ok");

  Serial.print("D: esp_wifi_set_ps(WIFI_PS_MAX_MODEM)=");
  if (esp_wifi_set_ps(WIFI_PS_MAX_MODEM) == ESP_OK)
    Serial.println("ok");
  else
    Serial.println("not ok");

  Serial.print("D: esp_wifi_deinit()=");
  if (esp_wifi_deinit() == ESP_OK)
    Serial.println("ok");
  else
    Serial.println("not ok");

  bWiFiEnabled = false;          // WiFi is disabled now
  bAskForDisablingWiFi = false;  // WiFi is disabled, do not disable again
  Serial.println("D: WiFi turned off");
}

void loop() {
  // do some other stuff
  if (!bWiFiEnabled)  // WiFi off?
  {
    if (bEnableWiFi)  // should be turned on?
    {
      enableWiFi();
      bWiFiScanStart = true;  // Notification: start scans
      vTaskResume(hWiFiScanHandle);
    }
  } else {
    if (bAskForDisablingWiFi)  // should be turned off?
    {
      bWiFiScanStart = false;  // Notification: start no new scan
      if (!bWiFiScanRunning) {
        // no scan currently running
        vTaskSuspend(hWiFiScanHandle);
        disableWiFi();
      } else {
        Serial.print("#");
      }
    }
  }
  // continue with other stuff
  delay(20);
}

void xTaskWiFiScan(void* pvParameters) {
  Serial.println("B: xTaskWiFiScan started.");
  int16_t n;
  while (1) {
    Serial.println("B: Scan allowed?");
    do {
      vTaskDelay(100 / portTICK_PERIOD_MS);
      portENTER_CRITICAL(&mux);
      bWiFiScanRunning = bWiFiScanStart;
      portEXIT_CRITICAL(&mux);
    } while (!bWiFiScanRunning);
    Serial.println("B: WiFi status: " + String(WiFi.status()));
    Serial.println("B: Start WiFi scan.");
    WiFi.scanNetworks(true);

    n = WiFi.scanComplete();
    while (n < 0) {
      Serial.print(".");
      vTaskDelay(500 / portTICK_PERIOD_MS);
      n = WiFi.scanComplete();
    }
    Serial.println(".");
    Serial.println("B: " + String(n) + " network(s) found.");
    WiFi.scanDelete();
    bWiFiScanRunning = false;
    Serial.println("B: Finished WiFi scan.");
    vTaskDelay(WIFI_SCAN_PERIOD / portTICK_PERIOD_MS);
  }
  vTaskDelete(NULL);
}


// Dummy task, just for enabling and disabling WiFi
void xTaskSwitchScan(void* pvParameters) {
  Serial.println("A: xTaskSwitchScan started.");
  vTaskDelay(5000 / portTICK_PERIOD_MS);
  bEnableWiFi = true;
  Serial.println("A: Enable WiFi scan.");
  vTaskDelay(60000 / portTICK_PERIOD_MS);
  if (bWiFiEnabled) {
    bAskForDisablingWiFi = true;
    Serial.println("A: Start no more WiFi scan.");
  }
  while (bWiFiScanRunning) {
    Serial.println("A: Check for running WiFi scan.");
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
  Serial.println("A: WiFi scan finished, WiFi can be turned off.");

  vTaskDelay(30000 / portTICK_PERIOD_MS);
  bEnableWiFi = true;
  Serial.println("A: Enable WiFi scan.");
  vTaskDelay(60000 / portTICK_PERIOD_MS);
  if (bWiFiEnabled) {
    bAskForDisablingWiFi = true;
    Serial.println("A: Start no more WiFi scan.");
  }
  while (bWiFiScanRunning) {
    Serial.println("A: Check for running WiFi scan.");
    vTaskDelay(500 / portTICK_PERIOD_MS);
  }
  Serial.println("A: WiFi scan finished, WiFi can be turned off.");

  while (1) {
    vTaskDelay(20000 / portTICK_PERIOD_MS);
  }

  vTaskDelete(NULL);
}

What do you think about??

It actually does, because I don't think you do.

It will work if you correctly write the code to do it that way.

That sentence doesn't make an sense.

I've already given my advise and it was rejected. But, trying one more time. It turns out that using an event group will be even simpler than using notifications. Take a look at the code below. It doesn't actually include the WiFi stuff, etc. It just uses delay where it would go so you can watch things happen on the screen at a reasonable pace. The main control task uses random delays to set the times for starting and stopping the WiFi scan task.

#include "Arduino.h"

EventGroupHandle_t scanControl;
const EventBits_t startWiFiScan = 1UL << 0;
const EventBits_t stopWiFiScan = 1UL << 1;

void controlTask(void *pvParameters);
void scanTask(void *pvParameters);

void setup() {
  Serial.begin(115200);
  delay(3000);
  Serial.println("Starting");

  scanControl = xEventGroupCreate();
  assert(scanControl != NULL);

  BaseType_t returnCode = xTaskCreatePinnedToCore(scanTask, "Scan Task", 2500, NULL, 4, NULL, CONFIG_ARDUINO_RUNNING_CORE);
  assert(returnCode != pdFAIL);

  returnCode = xTaskCreatePinnedToCore(controlTask, "Control Task", 2500, NULL, 6, NULL, CONFIG_ARDUINO_RUNNING_CORE);
  assert(returnCode != pdFAIL);
}

void controlTask(void *pvParameters) {
  uint32_t waitTime;
  for (;;) {
    Serial.println("Control Task: Starting WiFi Scan");
    xEventGroupSetBits(scanControl, startWiFiScan);
    waitTime = random(500, 30000);  // Let the scan run for 5 to 30 seconds
    vTaskDelay(waitTime);  // In the real code actual work would be done instead of just a delay
    Serial.println("Control Task: Stopping WiFi Scan");
    xEventGroupSetBits(scanControl, stopWiFiScan);
    waitTime = random(5000, 30000);  // Start the next scan in 5 to 30 seconds
    vTaskDelay(waitTime); // Again, the real code would do work here
  }
}

void scanTask(void *pvParameters) {
  uint32_t scanCount = 0;

  for (;;) {
    xEventGroupWaitBits(scanControl, startWiFiScan, pdTRUE, pdFALSE, portMAX_DELAY);
    Serial.println("Scan Task: Instructed to start scanning");
    while (true) {
      EventBits_t checkStop = xEventGroupWaitBits(scanControl, stopWiFiScan, pdTRUE, pdFALSE, 0);
      if ((checkStop & stopWiFiScan) > 0) {
        Serial.println("Scan Task: Instructed to stop scanning");
        break;
      }
      Serial.printf("Scan Task: Starting Scan # %d\n", scanCount);
      uint32_t scanDuration = random(2000, 6000);  // Let's say each scan takes from 2 to 6 seconds
      vTaskDelay(scanDuration);  // In the real code, the scan algoritm would go here
      Serial.printf("Scan Task: Completed Scan # %d\n", scanCount++);
      vTaskDelay(3000); // Wait 3 seconds before starting next scan
    }
  }
}

void loop() {
}

It will work if you correctly write the code to do it that way.
That's true.

However, the code does currently still not meet my needs.
In the "control task" ther is no recognition if a scan is running after a stop request was sent.

Additionally I've modified the scan task:

void scanTask(void *pvParameters) {
  uint32_t scanCount = 0;

  for (;;) {
    xEventGroupWaitBits(scanControl, startWiFiScan, pdTRUE, pdFALSE, portMAX_DELAY);
    Serial.println("Scan Task: Instructed to start scanning");
    while (true) {
      Serial.println("Scan Task: xEventGroupWaitBits()."); // Added print
      EventBits_t checkStop = xEventGroupWaitBits(scanControl, stopWiFiScan, pdTRUE, pdFALSE, 0);
      Serial.println("Scan Task: xEventGroupWaitBits() done, next step if-check"); // Added another print so setting the checkStop variable is logged
      vTaskDelay(3000); // Added this delay for simulating that the scheduler might cut in here, switching the currently running task
      if ((checkStop & stopWiFiScan) > 0) {
        Serial.println("Scan Task: Instructed to stop scanning");
        break;
      }
      Serial.printf("Scan Task: Starting Scan # %d\n", scanCount);
      uint32_t scanDuration = random(2000, 6000);  // Let's say each scan takes from 2 to 6 seconds
      vTaskDelay(scanDuration);  // In the real code, the scan algoritm would go here
      Serial.printf("Scan Task: Completed Scan # %d\n", scanCount++);
      vTaskDelay(300); // Wait 3 seconds before starting next scan
    }
  }
}

I also changed the randomized timing in the control task to get faster this output:
Starting
Control Task: Starting WiFi Scan
Scan Task: Instructed to start scanning
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Starting Scan # 0
Control Task: Stopping WiFi Scan
Scan Task: Completed Scan # 0
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Instructed to stop scanning
Control Task: Starting WiFi Scan
Scan Task: Instructed to start scanning
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Starting Scan # 1
Control Task: Stopping WiFi Scan
Scan Task: Completed Scan # 1
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Instructed to stop scanning
Control Task: Starting WiFi Scan
Scan Task: Instructed to start scanning
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Starting Scan # 2
Scan Task: Completed Scan # 2
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Starting Scan # 3
Control Task: Stopping WiFi Scan
Scan Task: Completed Scan # 3
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Instructed to stop scanning
Control Task: Starting WiFi Scan
Scan Task: Instructed to start scanning
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Scan Task: Starting Scan # 4
Scan Task: Completed Scan # 4
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Control Task: Stopping WiFi Scan
Scan Task: Starting Scan # 5
Scan Task: Completed Scan # 5
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check
Control Task: Starting WiFi Scan
Scan Task: Instructed to stop scanning
Scan Task: Instructed to start scanning
Scan Task: xEventGroupWaitBits().
Scan Task: xEventGroupWaitBits() done, next step if-check

For scan #5 the problem still exists: the event group bits were received for checking to stop (and the stop bit was not set) , but before the if statement has been reached the scheduler switched to the control task where the bit is set. After the scheduler switched back to the scan task the if statement is evaltuated as false and a new scan will be started.

So it is also not really safe to think at the control task a stop request is enough.
Fortunatelly this is currently not a big and also an absolute rare case which might not occur in real life. And if, for my sketch, it is not a big deal.

However, the issue that the detection for another loop run and the notification from the scan task to the control task that another WiFi scan (or what ever) will be started is a point where the scheduler can interrupt.

If additional steps will be implemented, i.e. do no more scan but connect to one of the detected WiFis or unmount an SD card because it is assumed that all data have been written, may fail.

But thank you for your help so far.