ESP32 multi-tasking?

Hi!

these days I have been working with new boards to include its support in the client libraries (for the upcoming 2.2.0 release), like the Arduino Nano RP2040 connect. This board includes mbed RTOS which allows, for example, creating multiple tasks. I have adapted the client library so Thinger client starts on its own Thread, leaving the loop completely free for the user. For example, a working sketch for this device is the following:

#define THINGER_SERIAL_DEBUG

#include <ThingerMbed.h>
#include <ThingerMbedOTA.h>
#include "arduino_secrets.h"

ThingerMbed thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL);
ThingerMbedOTA ota(thing);

void setup() {
  // configure LED_BUILTIN for output
  pinMode(LED_BUILTIN, OUTPUT);

  // open serial for debugging
  Serial.begin(115200);

  // configure wifi network
  thing.add_wifi(SSID, SSID_PASSWORD);

  // pin control example (i.e. turning on/off a light, a relay, etc)
  thing["led"] << digitalPin(LED_BUILTIN);

  // resource output example (i.e. reading a sensor value, a variable, etc)
  thing["millis"] >> outputValue(millis());

  // start thinger.io on its own thread
  thing.start();
}

void loop() {
  Serial.println("user can work freely here");
  delay(1000);
}

It even allows to start/stop thinger on demand. Since ESP32 on arduino runs using FreeRTOS, do you think it is worth to include the same approach shown here!?

Very interesting. Visually, it is simpler for the user to understand.

Also, are there any expectations to update the library so that it is possible to configure ESP32 with “WebConfig”/WiFiManager? And that the OTA update works with “WebConfig/WiFiManager”?
Apparently, WiFiManager version 2.0.4-Beta already works with ESP32.

Hi,

This multitasking is just in one core or involves both? I ask this because it could be dangerous at the moment that two processes (running simultaneously, at both cores) access at the very same moment the very same memory, for example thinger’s task uploading (or writing) variables and another task writing them, at the same time, yes maybe the probabilities are soooo low, but it is something to keep in mind.

Hi @George_Santiago, I will take a look to WifiManager for ESP32.

And yes, @ega, with mulltitasking there is a chance of multiple task using the same memory. Thinger client library already had semaphores for ESP32 (started whit the ESP32Core project) and now included Mutex for Mbed platform, so, at least, the use of the thinger client instance is ok with multiple task, i.e., the loop calling endpoints, writing buckets, etc., and the background task handling incoming messages, streams, etc.

Moreover, if the loop updates or uses variables that are used someway inside a resource definition, it is possible to acquire the lock before using them. Here it is a working example for the upcoming library version.

#define THINGER_SERIAL_DEBUG

#include <ThingerESP32.h>
#include "arduino_secrets.h"

ThingerESP32 thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL);
int threshold = 0;

void setup() {
  Serial.begin(115200);

  pinMode(16, OUTPUT);

  thing.add_wifi(SSID, SSID_PASSWORD);

  thing["threshold"] << inputValue(threshold);
  
  thing.start();
}

void loop() {
  delay(1000);
  thing.lock();
  Serial.printf("Threshold value: %d\n", threshold);
  thing.unlock();
}

We have a new library with RTOS support for ESP32:

Example for creating a background task for Thinger.io (notice the THINGER_FREE_RTOS define to enable RTOS functionality).

#define THINGER_SERIAL_DEBUG
#define THINGER_FREE_RTOS

#include <ThingerESP32.h>
#include "arduino_secrets.h"

ThingerESP32 thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL);

void setup() {
  // open serial for debugging
  Serial.begin(115200);

  pinMode(16, OUTPUT);

  thing.add_wifi(SSID, SSID_PASSWORD);

  // digital pin control example (i.e. turning on/off a light, a relay, configuring a parameter, etc)
  thing["GPIO_16"] << digitalPin(16);

  // resource output example (i.e. reading a sensor value)
  thing["millis"] >> outputValue(millis());

  // start thinger in its own task
  thing.start();

  // more details at http://docs.thinger.io/arduino/
}

void loop() {
  // use loop as in normal Arduino Sketch
  // use thing.lock() thing.unlock() if using variables exposed on thinger resources
}

Hi @alvarolb

When trying to compile <ThingerTinyGSM.h> with the FreeRTOS option ( #define THINGER_FREE_RTOS ) an error is generated:

error: 'class ThingerTinyGSM' has no member named 'start'
 thing.start();

Everything works fine when trying to compile <ThingerESP32WebConfig.h>, but with <ThingerTinyGSM.h> an error occurs.

Is it a bug? If so, is it easy to fix? Would it be possible to make the fix available before Friday (8/4/2023)?
I need to make a prototype before Friday. I tried to fix the problem in the <ThingerTinyGSM.h> code, but I couldn’t.

See the code I tried:

#include <Arduino.h>

#define THINGER_SERIAL_DEBUG
#define THINGER_FREE_RTOS

#define THINGER_SERVER "xxxxxxxxxx"
#define USERNAME "xxxxxxxxx"
#define DEVICE_ID "xxxxxxxx"
#define DEVICE_CREDENTIAL "xxxxxxxxxx"

const int PIN_ESP_TX_GSM_RX = 2;
const int PIN_ESP_RX_GSM_TX = 4;
const int PIN_GSM_RESET     = 32;

// -------------------------------------------------------------------------
// #define __WIFI_DEVICE_TESTE__
// #include <WiFiManager.h>
// #include <ThingerESP32WebConfig.h>
// ThingerESP32WebConfig thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL);

// -------------------------------------------------------------------------

// #include <ThingerESP32.h>
// ThingerESP32 thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL);

// -------------------------------------------------------------------------
#define __GSM_DEVICE_TESTE__

#define TINY_GSM_MODEM_SIM800

#include <TinyGsmClient.h>
#include <ThingerTinyGSM.h>

HardwareSerial serialSIM800(1);

ThingerTinyGSM thing(USERNAME, DEVICE_ID, DEVICE_CREDENTIAL, serialSIM800);
TinyGsm modem(serialSIM800);

// -------------------------------------------------------------------------

#include <ThingerESP32OTA.h>
#include <ThingerConsole.h>
ThingerESP32OTA ota(thing);
ThingerConsole console(thing);

int threshold = 0;

void setup() {
  Serial.begin(115200);

  pinMode(16, OUTPUT);

#ifdef  __GSM_DEVICE_TESTE__
  
  serialSIM800.begin(115200, SERIAL_8N1, PIN_ESP_RX_GSM_TX, PIN_ESP_TX_GSM_RX);
  //serialSIM800.begin(115200, SERIAL_8N1, PIN_ESP_RX_GSM_TX, PIN_ESP_TX_GSM_RX, false);


const char* apn_name = "xxxxxxxx";
const char* apn_user = "xxxxxxxx";
const char* apn_pswd = "xxxxxxxx";

thing.setAPN(apn_name, apn_user, apn_pswd);
#endif  //__GSM_DEVICE_TESTE__

#ifdef __WIFI_DEVICE_TESTE__
  thing.add_wifi("xxxxxxxxx", "xxxxxxxxxx");
#endif  //!__WIFI_DEVICE_TESTE__

  thing["threshold"] << inputValue(threshold);
  
  thing.start();
}

void loop() {
  delay(1000);
  thing.lock();
  Serial.printf("Threshold value: %d\n", threshold);
  thing.unlock();
}

Hello, @alvarolb

Tried to perform OTA with FreeRTOS via WiFi and GSM.

Via WiFi, the first attempt gave an error (the serial port was enabled on Termite). So I closed the serial port and tried again. I had success with OTA via WiFi for the next three attempts.
Image of the Error generated on the first attempt:
image

However, all OTA attempts via GSM gave an error. nem starts streaming the data in the VS Code notification bar. The <ThingerTinyGSM.h> library needs fixing in the FreeRTOS implementation. I made some adaptations and managed to make the <ThingerTinyGSM.h> library work with FreeRTOS, but it may need new corrections to work with OTA.
In the serial monitor, the following messages were captured:

I tried to perform the OTA via GSM, but I couldn’t yet. All attempts failed.
See the information below:

image

RAM:   [=         ]   7.6% (used 25052 bytes from 327680 bytes)
Flash: [===       ]  27.8% (used 364741 bytes from 1310720 bytes)


16:28:50: [THINGER] Available bytes: 24
[THINGER] Writing bytes: 83 [OK]
16:28:51: [THINGER] Available bytes: 117
[OTA] Received OTA request...
[OTA] Firmware: esp32
[OTA] Firmware Size: 365104
[OTA] Chunk Size: 32768
[OTA] Initializing update...
[OTA] Set MD5 verification:8baf228966c041c0bc66167967d518a7
[OTA] Init OK: 1
[THINGER] Writing bytes: 20 [OK]
[OTA] Stopping streams...
[OTA] Waiting for firmware...
........
16:28:57: [THINGER] Available bytes: 2920
........
16:29:34: [OTA] Received OTA part (bytes): 32768
[OTA] Wrote OK: 1
[OTA] Elapsed time ms: 122
[THINGER] Writing bytes: 20 [OK]
[THINGER] Writing bytes: 2 [OK]
........
16:29:35: [THINGER] Available bytes: 2
........
16:30:04: [OTA] OTA update timed out after (ms): 30000
[OTA] OTA update failed... Restarting device
[THINGER] Client was requested to stop.
Thinger State Listener: THINGER_STOP_REQUEST
[_SOCKET] Is now closed!
Thinger State Listener: SOCKET_DISCONNECTED
........
16:30:06: [THINGER] Rebooting device...

Hi @alvarolb

I have some questions about how to use thing.lock(); and thing.unlock();

1) If there is a shared resource in the thing object ( eg: ‘Serial.println’ in thing[“print_alert”], executed in CORE_0 ) and a function (CORE_1), we must also use thing.lock(); and thing.unlock(); in the thing[“print_alert”] object? See the example below:

int threshold = 0;

void setup() {
   Serial.begin(115200);

   thing["print_alert"] = [](){
        thing.lock();
        threshold++;
        Serial.println(threshold);
        thing.unlock();
      };

   thing.start();
}

void loop() {
   delay(1000);
   thing.lock();
   threshold++;
   Serial.printf("Threshold value: %d\n", threshold);
   thing.unlock();
}

2) If there are multiple shared resources, the recommended thing would be to create a mutex semaphore for each resource, correct? But with thing.lock(); and thing.unlock(); we just use one mutex semaphary for all resources, correct? See the fictional example below:

int threshold = 0;

void setup() {
   Serial.begin(115200);

   thing["print_alert"] = [](){

  thing.lock("mutex_1");
  threshold++;
  thing.unlock("mutex_1");

  thing.lock("mutex_2");
  Serial.println(threshold);
  thing.unlock("mutex_2");
      };

   thing.start();
}

void loop() {
   delay(1000);
   thing.lock("mutex_1");
   threshold++;
   thing.unlock("mutex_1");

   thing.lock("mutex_2");
   Serial.printf("Threshold value: %d\n", threshold);
   thing.unlock("mutex_2");
}

3) The thing object does not execute xSemaphoreTake(semaphore_, portMAX_DELAY);, nor xSemaphoreGive(semaphore_); when it is running in CORE_0, correct? So, what happens if thing (CORE_0) executes the same feature that is in a function in CORE_1 (which uses thing.lock(); and thing.unlock(); ) ? Does the thing object (which has no internal mutex) take the function’s resource? Or is it blocked, waiting for the function (CORE_1) to release the resource?

4) If thing(CORE_0) is waiting for the WiFi or GSM connection, can it block/lock a function (CORE_1) that shares the same resource, example: Serial.print?