Task asincrono o molto lungo

Ciao,
il modo migliore per creare ed eseguire un task in maniera asincrona (lo eseguo, la chiamata ritorna subito e, al termine del task, viene eseguita una callback) o comunque sia una operazione MOLTO lunga (30-40-50 MINUTI) senza che ovviamente si blocchi l'intero arduino o scatti il watchdog ?

Pensavo di usare il TaskScheduler, non è esattamente la stessa cosa ma dovrebbe "sganciare" l'esecuzione del task dal flusso corrente.

Idee migliori ?

Direi che dipende anche dal task
Cosa vuoi fare?

Al momento sto cercando di ritornare l'elenco delle reti wifi raggiungibili, tramite websocket. Funziona tutto, tranne il fatto che se impiega più del dovuto, parte il watchdog.

Sono riuscito a bypassare il problema usando appunto un task con esecuzione immediata e mettendo il codice dentro una Lamba, nonostante la presenza di un delay(300) il resto del programma resta operativo.

MA, come al solito, io mi schianto contro la definizione degli oggetti, è veramente veramente difficile lavorare in questo modo per me, dopo anni ed anni abituato a programmare in altri linguaggi dove questo problema non esiste.

nel file .ino:

#ifndef Scheduler
#include <TaskScheduler.h>
extern Scheduler runner;
#endif

nel file incluso (nel cpp, non nell'header):

#ifndef Scheduler
#include <TaskScheduler.h>
extern Scheduler runner;
#endif

si schianta dicendo che c'è qualcosa di già definito

.pio/build/esp32/src/webserver.cpp.o: In function `Task::isEnabled()':
/home/gandalf/.platformio/packages/framework-arduinoespressif32@3.10006.210326/libraries/FS/src/FS.h:47: multiple definition of `Task::isEnabled()'
.pio/build/esp32/src/xxxxxx.ino.cpp.o:/home/gandalf/.platformio/packages/framework-arduinoespressif32@3.10006.210326/libraries/FS/src/FS.h:47: first defined here

Visto che stai usando un esp32 perché non fai semplicemente partire un task FreeRTOS in parallelo?

Non vedo proprio per quale motivo dovresti reimplementare un meccanismo che già esiste ed è stra-collaudato.

2 Likes

Come ti ha indicato qua sopra cotestatnt, il funzionamenti di ESP32 è basato su FreeRTOS™ per cui tu poi semplicemente sfruttare tale RTOS per creare tuoi task aggiuntivi ed evitare di usare strane configurazioni e librerie.

Prova a leggere QUI.

Ovviamnete occorre una discreta conoscienza di FreeRTOS™ ...
... ma la documentazione relativa a tale RTOS non manca :wink:

Guglielmo

P.S.: ... dai retta, evita di creare task sul Core0 ma limitati a Core1 (che comunque è più che sufficiente con FreeRTOS™)... Core0 è usato dal sistema del ESP32 e qualsiasi minima anomalia fa scattare il WatchDog o può creare problemi.

1 Like

Perchè non ne ero a conoscenza, ora vado a vedere...

Evitare problemi è ciò che vorrei fare, non devo eseguire chissà quali operazioni pesanti o con migliaia di utenti contemporanei, tutt'altro. Sono solo operazioni lunghe. Banalmente, una di queste operazioni lunghe è la scansione delle reti wifi, che in questo momento mi sta dando problemi.

L'uso del TaskScheduler l'avevo previsto perchè dovrei poter pianificare l'esecuzione di un task (a memoria, solo 1) ad orari prestabiliti, ad esempio ogni 2 ore, ogni 6 ore, ogni 10 minuti ecc ecc, a scelta dall'utente (ma il task è sempre quello).

Ci sono QUASI riuscito, ma non c'è un overload per poter usare una lambda con capture list come callback del task, quindi non saprei come fare:

Task t_wifiScanNetworks(TASK_IMMEDIATE, 1, [result](){
      int n = WiFi.scanNetworks();
      JsonObject wifiNet = result.createNestedObject();
      }
    });

si arrabbia non poco, perchè la lambda con [result] non si può usare e se tolgo result, poi si arrabbia perchè result non è catturato.

Il link di esempio che ti ha fornito Guglielmo fa partire dei task che continuano a ciclare in parallelo, nel tuo caso non è necessario perché devi solatnto eseguire una funzione specifica una tantum.

Il tutto si potrebbe tradurre in qualcosa del genere. Se vuoi sapere quando il task è stato completato potresti usare una varbiabile globale booleana che metti a false prima di avviare il task e a true subito prima di uscire.

// Questa è la funzione che sarà eseguita in un task parallelo
void scanWiFi(void * args) {
  int n = WiFi.scanNetworks();
  if (n == 0) {
    Serial.println("No networks found");
  }
  else {
    Serial.println(n);
    Serial.println(" networks found:");

    for (int i = 0; i < n; ++i) {
        String ssid = WiFi.SSID(i);
        int rssi = WiFi.RSSI(i);
    #if defined(ESP8266)
        String security = WiFi.encryptionType(i) == AUTH_OPEN ? "none" : "enabled";
    #elif defined(ESP32)
        String security = WiFi.encryptionType(i) == WIFI_AUTH_OPEN ? "none" : "enabled";
    #endif
       Serial.printf("SSID: %s - %ddb - Encrypt: %s\n", ssid.c_str(), rssi, security.c_str());
   }
   vTaskDelete(NULL);
}


void loop() {
  if (doScan) {
    doScan = false;
    // Avvio del task in parallelo
    xTaskCreate(
      scanWiFi,    // Function to implement the task
      "scanWiFi",  // Name of the task
      8192,        // Stack size in words
      NULL,        // Task input parameter
      1,           // Priority of the task
      NULL         // Task handle.
    );
  }
}
1 Like

Allora, il task RTOS potrebbe essere una buona soluzione, ma persiste il problema della Lamba con capture (io ho necessità di eseguire il task facendo riferimento alle variabili a monte e sopratutto poter richiamare i metodi a monte al termine del task stesso). In pratica, sto cercando di creare una callback per operazioni lunghe.

altro problema, l'errore che si presenta è:
FreeRTOS: FreeRTOS Task "Task1" should not return, Aborting now!

i miei task ritornano sempre qualcosa, non si tratta solo di esecuzione di operazioni

manca un pezzo o sbaglio ? Alla fine della funzione cancelli il task, ma dove l'hai creato ?

I task FreeRTOS prevedono un puntatore a void come argomento che puoi successivamente "castare" a quello che preferisci.

Nel loop, quando la variabile doScan diventa true, ovviamente è solo un esempio

Vedi perché ho scritto ...

... perché NON si può pensare di utilizzare un Sistema Operativo Real Time senza studiarselo almeno un po' e capendo i concetti fondamentali di come funziona.

Guglielmo

Si scusa, non avevo visto sotto :pensive:

per questo avrei preferito restare su un classico arduino.
Non credo di essere l'unico a dover eseguire una operazione lunga in maniera non bloccante ed intercettarne il risultato.

Chiunque faccia uso di una comunicazione via websocket/mqtt/webserver prima o poi dovrà scontrarsi con una richiesta ad un comando lungo e rispondere al client. Io lo faccio in differita (o è ciò che sto cercando di fare). Ricevo la richiesta, faccio partire il task, rispondo. Ma per farlo devo poter passare dei riferimenti al task tra cui l'istanza del server ed è proprio qui che sto avendo difficoltà.

Non esiste una libreria ?

Ma guarda che ad un task FreeRTOS puoi passare quello che vuoi proprio perché prevede come parametro in ingresso un genericissimo void *
Io di solito per comodità uso delle struct dove raccolgo tutto quello che serve nel task, ma ad esempio in passato mi è capitato di usarlo anche in un metodo di una classe dove passavo il riferimento all'istanza della classe stessa cosi da poter usare gli altri metodi della classe all'interno del task.

Prova a mettere il codice completo in questione e vediamo come si può procedere.

1 Like

ah, fosse facile. Ci provo, ma metto pezzi.

// webserver.cpp

websocket.onEvent(WebServer::wsOnEvent);

void WebServer::wsOnEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
    DynamicJsonDocument doc(1024);
    DeserializationError error;

    if(type == WS_EVT_DATA){
        AwsFrameInfo * info = (AwsFrameInfo*)arg;
        if(info->final && info->index == 0 && info->len == len){
          if(info->opcode == WS_TEXT){
            data[len] = 0;
            Serial.printf("%s\n", (char*)data);
          } else {
            for(size_t i=0; i < info->len; i++){
              Serial.printf("%02x ", data[i]);
            }
            Serial.printf("\n");
          }
          
          client->text( WebServer::processRPC((char*)data) );
        }
    }
}

String WebServer::processRPC(String jsonRPC) {
  const size_t capacity = 10240;
  DynamicJsonDocument doc(capacity);
  deserializeJson(doc, jsonRPC);

  String method  = doc["method"];

   if (method.equals("wifi.getNetworks")) {
      // QUI DENTRO DEVO FARE L'OPERAZIONE LUNGA, ATTENDERE IL RISULTATO E RITORNARE LA RISPOSTA AL SOCKET
     // risultato = molto_lungo();

     JsonArray result = docResponse.createNestedArray("result");
     serializeJson(docResponse, responseJSON);
     return responseJSON;
    
   }

}

perché pezzi e non intero?

Perchè sono una discreta quantità di file, alcuni non sviluppati da me ma da colleghi e che non posso pubblicare. E' un progetto a tempo perso che stiamo facendo in azienda, ad uso interno, nessuna vendita, ma non ci è permesso di pubblicare sorgenti di alcun tipo (indipendentemente dal linguaggio usato o da finalità commerciali o meno). Un conto è pubblicare singole funzioni o spezzoni, un altro è pubblicare il progetto.

Ieri sera ho controllato meglio come funziona il websocket creato con ESPAsyncWebServer.
In realtà, come lo sto usando ora, non va bene, visto che cerco di ritornare la risposta del task lungo direttamente nell'handler degli eventi wsOnEvent

Quindi sto facendo un po di refactor, adesso l'handler mi esegue il parser dei comandi processRPC che però ritorna void, quindi lo esegue e basta.

Ho modificato l'intestazione per portarmi dietro sia il puntatore a server sia quello a client (a regime penso mi basti avere solo l'intero ritornato da client->id() dato che il server teoricamente è già globalmente accessibile. Di conseguenza, la processRPC instanzia un task avente tra i parametri sia client che server, fa quello che deve fare e risponde direttamente tramite websocket con client->text().

Ma c'è un problema, sarebbe tutto molto bello se solo riuscissi a passare 2 (o più) parametri alla xTaskCreatePinnedToCore. Riesco a passarne solo 1 e vorrei evitare di creare una struct a monte con dentro i vari oggetti che mi servono, dato che la struct potrebbe essere diversa a seconda del comando da eseguire. In sostanza, servirebbe dinamica, ma dovendola definire fuori dalla classe e dal metodo, non saprei come fare senza staticizzare varie combinazioni.

qualche idea ?

Potresti modificare la libreria AsyncWebServer aggiungendo il puntatore all'istanza di AsyncWebSocket cosi con il solo puntatore ad AsyncWebServer ti porti dietro tutto quello che ti serve.

Oppure se non vuoi modificare i sorgenti delle librerie esterne al progetto, crei una tua classe figlia di AsyncWebServer a cui aggiungi tutto quello che ti manca.

class myWebServer : public AsyncWebServer
{
  public:
   AsyncWebSocket * myWebSocket;
};
1 Like