Implementazione echo server no-block FreeRTOS LWIP FreeRTOS

In questo articolo spiego come implementare la ricezione non bloccante sul ESP8266. Il progetto è su github

I file in breve:

Sorgenti:

  • main.c: Main di FreeRTOS fa alcune inizializzazioni e avvia il Task principale
  • private_ssid_config.h: SSID, e Password Wi-Fi.
  • reciver.c: Implementa la funzione che effettua la ricezione e trasmissione, dei dati.
  • reciver.h: Header del Task

File complementari:

  • .clang_complete: Autocompletamente di Clang
  • Makefile: Makefile del progetto

Main del programma:

Buona parte del codice che troviamo in questa sezione è l'implementazione standard di FreeRTOS.

//<< main.c >>\\
void user_init(void) {
  uart_set_baud(0, 115200);
  printf("SDK version:%s\n", sdk_system_get_sdk_version());

  struct sdk_station_config config = {
    .ssid     = WIFI_SSID,
    .password = WIFI_PASS,
  };

  /* required to call wifi_set_opmode before station_set_config */
  sdk_wifi_set_opmode(STATION_MODE);
  sdk_wifi_station_set_config(&config);
  

  // Creo il task per il Server TCP
  xTaskCreate(&Task_ServerTCP, "Echo_Server", 256, NULL, s 2, NULL);
}

Implementazione del echo server:

L'implementazione del echo server, è per buona parte identica al esempio fornito da LWIP. Utilizza una libreria di alto livello, non sembra nemmeno C. Analizziamo il sorgente nel dettaglio.

void Task_ServerTCP(void *pvParameters) {
  struct netconn *conn, *newconn;
  err_t err;
  
  // Rimuovo un warning inutile, questo task non utlizza parametri
  LWIP_UNUSED_ARG(pvParameters);

  /* Alloco un identificatore a una connessione di tipo IP */
  conn = netconn_new(NETCONN_TCP);

  /* 
   * Effettuo il bind della porta, su tutte le interfacce di rete del ESP8266
   * Sulla porta definita dalla macro PORT, definita nel file reciver.h  
   */
  netconn_bind(conn, NULL, PORT);
  printf("Binded Port:%d", PORT);

  /* Mi metto in ascolto sulla porta in attesa di nuove connessioni  */
  netconn_listen(conn);

  while (true) {
    /* Il programma si blocca su questa syscall, in attessa di una connessione.
     * Nel frattempo, FreeRTOS, esegue altri task che sono pronti per essere schedulati.
     * Questo echo server è implementato per poter servire una sola connessione alla volta.
     * Fino a quando non si chiude la precedente connessione sarà impossibile aprirne una nuova.
     * 
     * conn, è una connession in ascolto.
     * Ogni qualvolta, arriva una nuova conessione, viene instanziata una nuova netconn.
     * da qui useremo newconn per tutte le operazioni di lettura e scrittura.
     */
    err = netconn_accept(conn, &newconn);
   
    /* Impostiamo la nostra newconn per non essere bloccante */
    netconn_set_recvtimeout(newconn, TIMEOUT_SELECT);// Timeout della syscall
    netconn_set_nonblocking(newconn, true);// SetNonBlock

    printf("Accepted new connection\n");

    /* Controllo che la connessione sia OK, non ci siano errori */
    if (err == ERR_OK) {
      struct netbuf *buf;
      void  *data;
      u16_t  len;
      size_t Writed;
      
      /* 
       * Questo è il cuore del programma.
       * Controllo se ho ricevuto qualcosa, con netconn_recv.
       * Metto il valore di ritorno di questa funzione dentro err.
       * Tutti i dati ricevuti andranno dentro buff, apposta struttura dati di LWIP.
       * Rimango in questo ciclo fino a quando err e uguale a ERR_OK oppure ERR_TIMEOUT
       */
      while ((err = netconn_recv(newconn, &buf)),
             (err == ERR_OK || err == ERR_TIMEOUT)) {
        /*printf("Recved\n");*/
        switch (err) {
        case ERR_OK: {
          /*
           * ERR_OK, recv ha ricevuto dei dati, si trovano dentro buff.
           * Sposta i dati da buff, a data, imposta il valore di len con il numero di byte letti
           */
          do {
            netbuf_data(buf, &data, &len);
            Writed = 0;

            do { // Flush Buffer
              /*
               * Uso write partly per scrivere i dati ricevuti come risposta.
               * Ripeto questa operazione fino a quando, ho scritto tutti i dati.
               */
              netconn_write_partly(newconn, data, len, NETCONN_COPY, &Writed);
            } while (Writed != len);

            // err = netconn_write(newconn, data, len, NETCONN_COPY);
          } while (netbuf_next(buf) >= 0);// Proseguo fino a quando ho ricevuto tutto
          netbuf_delete(buf);// Cancello il buffer instanziato da recv
        } break;

        case ERR_TIMEOUT: {
          // printf("Time out\r\n");
          /* 
           * In caso recv non riceve alcun dato, ogni TIMEOUT si sblocca.
           * Ritorna come valore ERR_TIMEOUT.
           * In questo modo possono essere gestite agevolemente tutte le operazioni a tempo.
           * Per esempio muovere dei motori Passo Passo
           * Generamente mediante delle macchine a stati finiti
           */
          break;
        }

        default: continue;// Il comportamento di default e ripetere il ciclo.
        }
      }
      // Se il client chiude la connessione, new conn viene deallocato
      netconn_close(newconn);
      netconn_delete(newconn);
    }
  }
}

Possibili potenziamenti

Risposta puntuale ad IO

Il server mantiene la stessa struttura che ha ora, ad ogni trasmissione in arrivo corrisponde un'operazione di ristrasmissione, di una risposta, di un dato elaborato.

Vantaggi:

  • Semplicità di implementazione

Svantaggi:

  • Scarsa flessibilità

Utilizzerei questo design solo nel caso in cui ad ogni ricezione, l'ESP8266 risponde con un messaggio, oppure non risponde proprio ad alcuni messaggi.

Non utilizzerei questo design nel caso in cui, L'ESP8266 deve in alcune situazioni trasmettere di sua iniziativa dei messaggi.

In questo caso, è più opportuno ulitizzare dei buffer, e gestire tutte le operazioni, di trasmissione e ricezione in un unico punto.

Ricezione e trasmissione mediante Buffer:

Ricezione: I dati andrebbero quindi copiati in un buffer di ricezione/queue che possa contenere un numero sufficiente di pacchetti.

Trasmissione: La trasmissione andrebbe verificata e effettuata ad ogni iterazione, che sia Ricezione o TimeOut.

Tempi di esecuzione:

E' opportuno impostare i tempi di timeout in modo da essere coerente con i tempi di esecuzione della funzione di timeout. La funzione di timeout dovrebbe essere quasi atomica, puo contenere dei cicli, è meglio che siano piccoli e senza troppe iterazioni.

In caso di tempi di elaborazione molto lunghi, potrebbe essere necessario gestire l'elaborazione, con degli stati interni, spezzando la stessa in piu fasi, e verificando così ad ogni passaggio di non avere altri dati da ricevere/ operazioni di IO da fare.

In caso di elaborazioni veramente impegnative a livello computazionale, sarebbe opportuno avviare un apposito task (generici algoritmi che lavorano molto sui dati senza fare altro tipo di IO).