Picture from ESP32S3-Cam displayed on ESP32S3-8048s070 : http issues

Hi,

I’ve been trying (for like 8 months now) to build a RC Rover quite similar to Perseverance.

Part of the project aims at displaying the picture taken by a camera on the rover (ESP32S3-Cam N16R8), on the 7-inch screen of an ESP32S3-8048s070c (big CYD)
Picture size : 480*320, approx 20-30 kb each, jpg.

The touchscreen sends a few commands :

  • laser = briefly activates a linear laser to indication the “forward” direction on the picture
  • flash = LED flash during capture
  • rotation = servo rotates the whole “head” of the rover, and with it the camera

Here is my problem :

→ the rover behaves correctly when i’m issuing commands manually through a computer browser. I therefore tend to think that the code on my ESPCam is correct

→ however, the 7-inch ESP32S3 can’t keep a stable connection to the ESPCAM. Sometimes I manage to take a whole series of pictures, sometimes the ESP32 returns a “connection refused” error.

I’ve asked for help on 4 different IA agents (chatGPT, Gemini, Claude, Mistral), but they are quite useless.

QUESTION 1 : has anyone an idea about what could cause such an unreliable behavior with the 7inch CYD ?

QUESTION 2 : Are there other projects similar to this one (remotely capturing and displaying a picture from an ESPCAM)

Here are both codes (ESPCAM and CYD)

ESP32-S3 CAM

// ------------------------------------------------------------------------
// Camera embarquée ESP32CAMS3
// Code par Florent Kieffer
// 2 fonctions : piloter le servo par pwm manuel puis capturer ponctuellement l'image afin de l'afficher sur la tablette de contrôle
// ------------------------------------------------------------------------

#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

// --- Configuration Pins ESP32-S3 CAM (Freenove / AI-Thinker S3) ---
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 15
#define SIOD_GPIO_NUM 4
#define SIOC_GPIO_NUM 5
#define Y9_GPIO_NUM 16
#define Y8_GPIO_NUM 17
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 12
#define Y5_GPIO_NUM 10
#define Y4_GPIO_NUM 8
#define Y3_GPIO_NUM 9
#define Y2_GPIO_NUM 11
#define VSYNC_GPIO_NUM 6
#define HREF_GPIO_NUM 7
#define PCLK_GPIO_NUM 13

const byte laserPin = 46;
const byte flashPin = 42;
const byte pinServo = 3;

// --- Variables d'état ---
int targetAngle = 90;
int currentAngle = 90;
const char* ssid_AP = "NEM6NaviCam";
const char* password_AP = "pw*navicam3";

WebServer server(80);

// --- Handlers HTTP ---

// 1. Route pour la rotation : /rotation?angle=90
void handleRotation() {
  Serial.println("=== Nouvelle requête HTTP HANDLEROTATION ===");
  Serial.print("IP client : ");
  Serial.println(server.client().remoteIP());
  Serial.print("Nombre de connections");
  Serial.println(WiFi.softAPgetStationNum());


  if (server.hasArg("angle")) {
    int val = server.arg("angle").toInt();
    targetAngle = constrain(val, 50, 140);
    targetAngle = 180 - targetAngle;  // engrenage : inversion du sens de rotation
    Serial.printf("Rotation demandée : %d°\n", targetAngle);
    server.send(200, "text/plain", "OK");
    server.client().stop();  // ← fermeture explicite

  } else {
    server.send(400, "text/plain", "Angle manquant");
    server.client().stop();  // ← fermeture explicite
  }
}

// 2. Route pour la capture : /capture?flash=1&laser=0
// ============ code CLAUDE IA
void handleCapture() {
  Serial.println("=== Nouvelle requête HTTP HANDLECAPTURE ===");
  Serial.print("IP client : ");
  Serial.println(server.client().remoteIP());
  Serial.print("Nombre de connections");
  Serial.println(WiFi.softAPgetStationNum());

  server.client().setNoDelay(true);  // désactive Nagle, envoi immédiat

  bool useFlash = (server.hasArg("flash") && server.arg("flash") == "1");
  bool useLaser = (server.hasArg("laser") && server.arg("laser") == "1");

  digitalWrite(flashPin, useFlash ? HIGH : LOW);
  digitalWrite(laserPin, useLaser ? HIGH : LOW);
  if (useFlash || useLaser) delay(200);

  // Vider le buffer caméra pour avoir une image fraîche
  camera_fb_t* fb = esp_camera_fb_get();
  if (fb) esp_camera_fb_return(fb);  // on jette la première (peut être ancienne)
  fb = esp_camera_fb_get();          // on prend la suivante, vraiment fraîche

  if (!fb) {
    digitalWrite(flashPin, LOW);
    digitalWrite(laserPin, LOW);
    server.send(500, "text/plain", "Erreur Camera");
    return;
  }

  // Envoi via le client directement référencé, sans copie
  WiFiClient& client = server.client();  // référence, pas copie !

  // Construction manuelle de la réponse HTTP
  String header = "HTTP/1.1 200 OK\r\n";
  header += "Content-Type: image/jpeg\r\n";
  header += "Content-Length: " + String(fb->len) + "\r\n";
  header += "Connection: close\r\n\r\n";
  client.print(header);

  // Envoi par chunks pour ne pas saturer le buffer TCP
  const size_t chunkSize = 1024;
  size_t sent = 0;
  while (sent < fb->len) {
    size_t toSend = min(chunkSize, fb->len - sent);
    client.write(fb->buf + sent, toSend);
    sent += toSend;
  }
  client.flush();  // s'assure que tout est bien parti
  client.stop();   // ← ajout après le flush
  esp_camera_fb_return(fb);
  digitalWrite(flashPin, LOW);
  digitalWrite(laserPin, LOW);

  Serial.printf("Image envoyée : %d octets\n", fb->len);
}

void webServerTask(void* arg) {
  for (;;) {
    server.handleClient();
    vTaskDelay(pdMS_TO_TICKS(2));  // 2ms suffit, libère le CPU entre deux clients
  }
}

// --- Tâche Servo (Core 1) ---
void servoTask(void* arg) {
  pinMode(pinServo, OUTPUT);
  while (true) {
    // Déplacement progressif (Smooth)
    if (currentAngle < targetAngle) currentAngle++;
    else if (currentAngle > targetAngle) currentAngle--;

    // Génération du signal PWM manuel (plus stable sur S3 que ledc pour les servos)
    int pulse = map(currentAngle, 0, 180, 500, 2500);
    digitalWrite(pinServo, HIGH);
    delayMicroseconds(pulse);
    digitalWrite(pinServo, LOW);

    // Fréquence ~50Hz (20ms) moins le temps du pulse
    vTaskDelay(pdMS_TO_TICKS(20));
  }
}


void setup() {
  Serial.begin(115200);
  pinMode(laserPin, OUTPUT);
  pinMode(flashPin, OUTPUT);
  pinMode(pinServo, OUTPUT);
  pinMode(2, OUTPUT);
  // Configuration Caméra S3
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_HVGA;  // 480x320
  config.pixel_format = PIXFORMAT_JPEG;
  config.grab_mode = CAMERA_GRAB_LATEST;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 10;
  config.fb_count = 2;

  if (esp_camera_init(&config) != ESP_OK) {
    Serial.println("Erreur Camera");
  }
  sensor_t* s = esp_camera_sensor_get();
  s->set_vflip(s, 1);
  //s->set_hmirror(s, 1);
  s->set_quality(s, 10);

  // WiFi & Serveur
  WiFi.softAP(ssid_AP, password_AP);

  IPAddress IP = WiFi.softAPIP();

  Serial.print("Point d'acces cree. SSID: ");
  Serial.println(ssid_AP);
  Serial.print("Adresse IP: http://");
  Serial.println(IP);

  server.on("/rotation", HTTP_GET, handleRotation);
  server.on("/capture", HTTP_GET, handleCapture);
  server.begin();

  // Task Servo sur Core 1
  xTaskCreatePinnedToCore(servoTask, "Servo", 2048, NULL, 1, NULL, 1);

  // Task Server sur Core 0
  xTaskCreatePinnedToCore(webServerTask, "WebSrv", 4096, NULL, 2, NULL, 0);


  digitalWrite(laserPin, 1);
  delay(300);
  digitalWrite(laserPin, 0);
  digitalWrite(flashPin, 1);
  delay(300);
  digitalWrite(flashPin, 0);
  digitalWrite(pinServo, 1);
  delay(300);
  digitalWrite(pinServo, 0);
}
unsigned long ltimer;

void loop() {
  if (millis() - ltimer > 2000) {
    analogWrite(2, 40);
  }
  if (millis() - ltimer > 2200) {
    analogWrite(2, 0);
    ltimer = millis();
  }
  vTaskDelay(5);  // Laisse l'IDLE task respirer
}

7 INCH CYD : ESP32-S3 8048 S070c


String version = "0.7";

#include <LovyanGFX.hpp>
#include <lgfx_user/LGFX_Sunton_ESP32-8048S070.h>
#include <WiFi.h>
#include <esp_wifi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include <esp_heap_caps.h>
#include "GUI.h"

static LGFX lcd;
LGFX_Sprite spriteTete(&lcd);

// ATTENTION DEFINITION DES COULEURS : utiliser lcd.


#define bleu1 0x7c98
#define bleu2 0x5d19
#define bleunuit 0x2946
#define rougefonce 0x2021
#define bggrey 0x10a2  //0x18a3

#define ORANGE lcd.color565(255, 165, 0)
#define YELLOW lcd.color565(255, 255, 0)

#define MINIMAL_FRAME_INTERVAL 10000
/* ========================================================= */

bool lastClic = 0;
unsigned long lastClicTime = 0;
unsigned long lastFrameTime = 0;

const char* ssid = "NEM6NaviCam";
const char* password = "pw*navicam3";
volatile bool wifiOK;
String camUrl = "http://192.168.4.1/capture?flash=";
String rotUrl = "http://192.168.4.1/rotation?angle=";
bool rotationRequest = 0;
unsigned long rotationSentTime = 0;  // utile pour le délai entre les requetes rotation et capture
unsigned long lastWifiCheck;         // verification du WiFi pour affichage sur écran

volatile unsigned long imageReceivedTime = 0;
volatile bool imageFailed = false;




uint8_t* jpgBuf = nullptr;
size_t jpgLen = 0;

SemaphoreHandle_t jpgMutex;
volatile bool newFrame = false;
volatile bool fetchNewImage = false;
unsigned long redrawTimeout;


enum graphicsRequestENUM {
  REDRAW_NONE,
  REDRAW_FLASH,
  REDRAW_LASER,
  REDRAW_PRE_JPG,
  REDRAW_WAIT_ROT,
  REDRAW_JPG,
};

volatile graphicsRequestENUM graphReq = REDRAW_NONE;
bool flash = 0;
bool laser = 0;
int rotation_step = 0;

/* ======================= Fonctions d'affichage sur core 1 ================= */

void trace_ecran_accueil() {
  Serial0.println("-----Tracé de l'écran d'accueil -------- ");
  lcd.clear(bggrey);
  lcd.setFont(0);
  lcd.setTextDatum(BR_DATUM);
  lcd.setTextColor(TFT_WHITE);
  lcd.drawString(version, 799, 479);
  // cadres partie droite
  lcd.fillRect(315, 60, 482, 322, TFT_BLACK);
  lcd.drawRect(315, 60, 482, 322, TFT_RED);
  lcd.fillRect(340, 390, 400, 26, TFT_BLACK);
  // tracé de textes
  traceTitreCadre("NEM_6 NAVICAM", 320, 40, TFT_ORANGE, 0);
  traceTitreCadre("Eclairage", 5, 90, bleu1, 1);
  traceTitreCadre("Orientation", 5, 290, bleu1, 1);
  traceTitreCadre(">> CAPTURE <<", 460, 460, TFT_YELLOW, 0);
  lcd.setTextColor(TFT_WHITE);
  lcd.setFont(&Roboto_Thin_24);
  lcd.drawString("FLASH", 15, 150);
  lcd.drawString("LASER", 15, 210);

  // Rover - tracé
  lcd.fillRect(5, 400, 50, 81, TFT_BLACK);  //roue
  lcd.drawRect(5, 400, 50, 81, TFT_GREEN);  //roue
  for (int i = 408; i <= 480; i += 12) {    // dents
    lcd.drawFastHLine(10, i, 40, TFT_GREEN);
  }
  lcd.fillRect(75, 360, 15, 65, 0xdedb);     //bras1
  lcd.drawRect(75, 360, 15, 65, TFT_GREEN);  //bras1
  lcd.fillRect(92, 360, 8, 35, 0xdedb);      // bras2
  lcd.drawRect(92, 360, 8, 35, TFT_GREEN);   // bras2
  lcd.drawRect(90, 362, 2, 4, TFT_GREEN);    // pivot

  lcd.fillRect(65, 426, 160, 73, 0xdedb);
  lcd.drawRect(65, 426, 160, 73, TFT_GREEN);
  lcd.fillRect(235, 400, 50, 81, TFT_BLACK);  //roueD
  lcd.drawRect(235, 400, 50, 81, TFT_GREEN);  //roueD
  for (int i = 408; i <= 480; i += 12) {      // dentsD
    lcd.drawFastHLine(240, i, 40, TFT_GREEN);
  }

  drawBtnOnOff(0, flash);
  drawBtnOnOff(1, laser);

  spriteTete.pushSprite(130, 350, 0x1000);  // à réorienter selon 'rotation' — transparence sur 0x1000

  lcd.pushImage(10, 320, 60, 60, turnL);
  lcd.pushImage(240, 320, 60, 60, turnR);
}

void traceTitreCadre(String titre, int xpos, int ypos, uint16_t color, bool font) {  // tracé facilité de texte encadré

  if (!font) lcd.setFont(&fonts::Orbitron_Light_24);
  else lcd.setFont(&FreeSansBold18pt7b);

  lcd.setTextDatum(BL_DATUM);
  int wtext = lcd.textWidth(titre);
  int htext = lcd.fontHeight();
  Serial0.printf("X1 : %d, Y1 : %d, X2 : %d, Y2 : %d\n", xpos - 5, ypos - htext - 3, wtext + 10, htext + 6);
  lcd.fillRect(xpos - 5, ypos - htext - 3, wtext + 10, htext + 6, TFT_BLACK);
  lcd.drawRect(xpos - 5, ypos - htext - 3, wtext + 10, htext + 6, color);
  lcd.setTextColor(color);
  lcd.drawString(titre, xpos, ypos);
}

void dynamicCaptureCallout(int x, int y, String callout) {
  lcd.setTextFont(0);
  lcd.fillRect(340, 390, 400, 26, TFT_BLACK);
  lcd.setTextColor(TFT_GREEN);
  if (callout == "ERREUR IMAGE") {
    lcd.setTextColor(TFT_RED);
    lcd.fillRect(315, 60, 482, 322, TFT_BLACK);
  }
  lcd.setCursor(x, y);
  lcd.print(callout);
}

void drawBtnOnOff(bool select, bool onOff) {  // select vaut 0 -> on affiche flash, select vaut 1-> on affiche laser
  if (onOff) lcd.pushImage(120, 117 + select * 60, 80, 35, btnOn);
  else lcd.pushImage(120, 117 + select * 60, 80, 35, btnOff);
}

void action_flash() {
  flash = !flash;
  graphReq = REDRAW_FLASH;
}

void action_laser() {
  laser = !laser;
  graphReq = REDRAW_LASER;
}

void action_turn(int vari) {
  // rotation_step varie de -4 à +4
  rotation_step += vari;
  rotation_step = constrain(rotation_step, -4, 4);
  lcd.fillRect(90, 320, 210, 159, bggrey);
  lcd.pushImage(240, 320, 60, 60, turnR);
  lcd.fillRect(90, 426, 135, 73, 0xdedb);
  //lcd.fillRect(75, 360, 15, 65, 0xdedb);     //bras1
  //lcd.drawRect(75, 360, 15, 65, TFT_GREEN);  //bras1
  lcd.fillRect(92, 360, 8, 35, 0xdedb);     // bras2
  lcd.drawRect(92, 360, 8, 35, TFT_GREEN);  // bras2
  lcd.drawRect(90, 362, 2, 4, TFT_GREEN);   // pivot
  lcd.drawRect(65, 426, 160, 73, TFT_GREEN);
  lcd.fillRect(235, 400, 50, 81, TFT_BLACK);  //roueD
  lcd.drawRect(235, 400, 50, 81, TFT_GREEN);  //roueD
  for (int i = 408; i <= 480; i += 12) {      // dentsD
    lcd.drawFastHLine(240, i, 40, TFT_GREEN);
  }
  int angle = rotation_step * 10;
  Serial0.printf("Vari : %d, Rotation_step : %d, Angle : %d\n", vari, rotation_step, angle);
  spriteTete.pushRotateZoom(180, 414, angle, 1.0, 1.0, 0x1000);
}

void action_captureWasClicked() {
  if (millis() - lastFrameTime > MINIMAL_FRAME_INTERVAL) {
    Serial0.println("Bouton pressé : Capture ");
    graphReq = REDRAW_PRE_JPG;
    lastFrameTime = millis();
  } else {
    Serial0.println("Trop tôt !!");
  }
}
/* ======================= Taches FreeRTOS ================= */

void taskFetchCam(void* pv) {
  for (;;) {
    // Guard : ne rien faire si pas connecté
    if (WiFi.status() != WL_CONNECTED) {
      Serial0.println("WiFi perdu, attente...");
      wifiOK = false;
      vTaskDelay(pdMS_TO_TICKS(1000));
      continue;
    } else {
      wifiOK = true;
    }

    if (rotationRequest) {
      HTTPClient http;
      WiFiClient client;
      Serial0.print("rotation_request at time : ");
      Serial0.println(millis());
      String URL = rotUrl + String(90 + rotation_step * 15);
      Serial0.println(URL);
      //http.begin(URL);
      http.begin(client, URL);
      http.setTimeout(12000);
      int httpCode = http.GET();  // C'est CETTE ligne qui envoie l'URL à l'ESPCAM
      if (httpCode > 0) {
        Serial0.printf("[HTTP] GET... code: %d\n", httpCode);
      } else {
        Serial0.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
      }
      http.end();
      rotationRequest = 0;
      Serial0.print("rotation_request ENDED at time : ");
      Serial0.println(millis());
    }

    if (fetchNewImage) {
      HTTPClient http;
      WiFiClient client;
      Serial0.println("Commande de prise de vue");
      String URL = camUrl + String(flash) + "&laser=" + String(laser);
      Serial0.println(URL);
      http.begin(client, URL);
      http.setTimeout(12000);
      int code = http.GET();
      Serial0.printf("GETCODE : %d\n", code);

      if (code == HTTP_CODE_OK) {
        int len = http.getSize();
        Serial0.printf("GETSIZE : %d\n", len);
        if (len > 0) {
          uint8_t* tmp = (uint8_t*)ps_malloc(len);
          if (tmp) {
            WiFiClient* stream = http.getStreamPtr();
            int pos = 0;
            unsigned long tStart = millis();
            while (pos < len && millis() - tStart < 10000) {
              int avail = stream->available();
              if (avail > 0) {
                pos += stream->read(tmp + pos, min(avail, len - pos));
              }
              vTaskDelay(1);
            }

            if (pos < len) {
              Serial0.printf("Stream interrompu : %d/%d\n", pos, len);
              free(tmp);
              imageFailed = true;  // ← stream coupé en cours de route
            } else {
              xSemaphoreTake(jpgMutex, portMAX_DELAY);
              if (jpgBuf) free(jpgBuf);
              jpgBuf = tmp;
              jpgLen = len;
              newFrame = true;
              xSemaphoreGive(jpgMutex);
              Serial0.println("Nouvelle image Recuperee");
            }

          } else {
            Serial0.println("ps_malloc échoué");
            imageFailed = true;  // ← pas de mémoire disponible
          }
        } else {
          Serial0.println("Taille image nulle");
          imageFailed = true;  // ← taille = 0
        }

      } else {
        Serial0.printf("HTTP erreur : %d\n", code);
        imageFailed = true;  // ← code HTTP != 200
      }
      http.end();
      fetchNewImage = 0;
    }
    vTaskDelay(100);
  }
}

void taskTouchAndDisplay(void* pv) {
  trace_ecran_accueil();

  for (;;) {
    // ======================================= Check Wifi ============================================
    if (millis() > lastWifiCheck + 2000) {
      wifiOK = (WiFi.status() == WL_CONNECTED);
      lcd.setTextFont(0);
      if (wifiOK) {
        lcd.setTextColor(TFT_GREEN);
      } else {
        lcd.setTextColor(TFT_RED);
      }
      lcd.drawString("Wifi", 780, 10);
      lastWifiCheck = millis();
    }

    //======================================== GESTION DU CLIC ========================================
    int clic_x, clic_y;
    if (lcd.getTouch(&clic_x, &clic_y) && !lastClic && (millis() - lastClicTime > 300)) {
      //Serial0.printf("clic_x : %d -- clic_y : %d\n", clic_x, clic_y);
      if (clic_x > 120 && clic_x < 200 && clic_y > 117 && clic_y < 152) {
        action_flash();
      } else if (clic_x > 120 && clic_x < 200 && clic_y > 177 && clic_y < 212) {
        action_laser();
      } else if (clic_x < 70 && clic_y > 320 && clic_y < 380) {
        action_turn(-1);
      } else if (clic_x > 240 && clic_x < 320 && clic_y > 320 && clic_y < 380) {
        action_turn(1);
      } else if (clic_x > 470 && clic_x < 630 && clic_y > 420 && clic_y < 465) {
        action_captureWasClicked();
      };
      lastClic = 1;
      lastClicTime = millis();
    } else if (!lcd.getTouch(&clic_x, &clic_y)) {
      lastClic = 0;
    }

    //======================================== GESTION DES ACTIONS DECLENCHEES ========================================
    if (graphReq != REDRAW_NONE) {
      switch (graphReq) {
        case REDRAW_FLASH:
          drawBtnOnOff(0, flash);
          graphReq = REDRAW_NONE;
          break;
        case REDRAW_LASER:
          drawBtnOnOff(1, laser);
          graphReq = REDRAW_NONE;
          break;

        case REDRAW_PRE_JPG:
          dynamicCaptureCallout(345, 400, "Positionnement camera...");
          rotationRequest = 1;
          rotationSentTime = millis();
          graphReq = REDRAW_WAIT_ROT;  // nouvel état
          break;

        case REDRAW_WAIT_ROT:
          if (millis() - rotationSentTime > 2000) {
            dynamicCaptureCallout(345, 400, "Acquisition image...");
            fetchNewImage = 1;
            redrawTimeout = millis() + 10000;
            graphReq = REDRAW_JPG;
          }
          break;

          // case REDRAW_JPG:
          //   if (millis() > redrawTimeout) {
          //     Serial0.println("TimeOut");
          //     graphReq = REDRAW_NONE;
          //     dynamicCaptureCallout(345, 400, "ERREUR IMAGE");
          //   }
          //   if (newFrame) {  // On ne dessine QUE si l'image est prête
          //     xSemaphoreTake(jpgMutex, portMAX_DELAY);
          //     if (jpgBuf != nullptr && jpgLen > 0) {
          //       lcd.drawJpg(jpgBuf, jpgLen, 316, 61, 480, 320);  // Précisez taille max si besoin
          //     }
          //     newFrame = false;
          //     xSemaphoreGive(jpgMutex);
          //     dynamicCaptureCallout(345, 400, "Image en affichage");
          //     graphReq = REDRAW_NONE;  // On a fini le cycle
          //   }
          //   break;


        case REDRAW_JPG:
          // Timeout basé sur l'absence de signal, pas sur un délai fixe
          if (imageFailed) {
            imageFailed = false;
            dynamicCaptureCallout(345, 400, "ERREUR IMAGE");
            graphReq = REDRAW_NONE;
            break;
          }
          if (newFrame) {
            xSemaphoreTake(jpgMutex, portMAX_DELAY);
            if (jpgBuf != nullptr && jpgLen > 0) {
              lcd.drawJpg(jpgBuf, jpgLen, 316, 61, 480, 320);
            }
            newFrame = false;
            xSemaphoreGive(jpgMutex);
            dynamicCaptureCallout(345, 400, "Image affichée");
            graphReq = REDRAW_NONE;
          }
          break;
      }
    }
    vTaskDelay(50);
  };
}

void setup() {
  Serial0.begin(115200);
  lcd.init();
  lcd.setRotation(0);
  lcd.setBrightness(255);
  lcd.setColorDepth(16);
  lcd.setSwapBytes(true);  // images générées par image2cpp : couleurs aberrantes sinon !

  spriteTete.createSprite(100, 128);
  spriteTete.fillScreen(0x1000);
  spriteTete.setSwapBytes(true);
  spriteTete.pushImage(0, 0, 100, 100, tete_camera);

  WiFi.begin(ssid, password);
  WiFi.setSleep(false);

  jpgMutex = xSemaphoreCreateMutex();

  xTaskCreatePinnedToCore(taskFetchCam, "cam", 16384, NULL, 1, NULL, 0);
  xTaskCreatePinnedToCore(taskTouchAndDisplay, "UI", 16384, NULL, 2, NULL, 1);
}

void loop() {
  vTaskDelay(pdMS_TO_TICKS(1000));  // allègement CPU
}

In the future, it helps if you do an auto format before posting your code, it is hard to read as is.
Flaky wifi can be caused by many things. I live in an apartment where I can 'see' more than a dozen networks, so it often takes me literally hours to get a new Alexa plug to connect. Other than patienc,e I have no more insight.
There may be code deficiencies or wifi techniques, maybe have a peek at the API (after a successful compile rt click goto definition)

the code you posted is incomplete but I can make an educated guess: the vtask code is not reentrant.

taskFetchCam and taskTouchAndDisplay use global [non]volatile variables without protecting them with mutexes. (e.g.: rotation_step)

Thank you both for your answers !

I’ve modified my first message - hadn’t noticed the text wasn’t formatted properly, it should be better now.

I’ll try and add mutex to all the variables you mentionned. When I started this project I didn’t know anything about Wifi connections and multi threading… let’s say I still have a lot to learn ;)

But in general, what do you think about the general idea in this code, is this “the right way to do it ?”. By “it” I mean requesting a still image and displaying it on the 7 inch CYD.

Are there other/better ways to do it ?

First, a minor point: on ESP32, delay is already defined as

void delay(uint32_t ms) {
  vTaskDelay(ms / portTICK_PERIOD_MS);
}

So for example, just use delay(2) instead of vTaskDelay(pdMS_TO_TICKS(2))

As to your code

  • Did you specifically find a need to set the stack size to 16K for each task?
  • How did you choose those priority values? They are different from each other, but each task is pinned to run on different cores
  • How did you decide on which core to pin? In particular, core 0 is the "protocol" core, which runs the WiFi stack among other things; while core 1, being the other core, is therefore the default core under the Tools menu Arduino Runs On: "Core 1", which in turn is available to the sketch as the macro ARDUINO_RUNNING_CORE. It's where setup/loop runs.

As to your the specific problem, you could utilize the fact that HTTP 1.1 supports reusing connections to the same host, and HTTPClient supports that by default, by rearranging the scope of your objects. Here is a sketch with a polling task

#include <WiFi.h>
#include <WiFiClient.h>
#include <HTTPClient.h>
#include <StreamString.h>
#include "arduino_secrets.h"

void httpTask(void *pvParameters) {
  // "permanent" (but not global) variables for task
  WiFiClient wifi;
  HTTPClient http;
  http.setReuse(true);  // defaults to true anyway
  const char *headerKeys[]{ "Content-Type", "Content-Length", "Location" };
  http.collectHeaders(headerKeys, std::extent<decltype(headerKeys)>::value);
  StreamString ss;
  uint32_t pause = 500;

  for (;;) {
    Serial.print("pausing: ");
    Serial.println(pause);
    delay(pause);
    pause += 750;  // slower and slower, in case you forget and leave it running

    http.begin(wifi, "http://google.com");
    int status = http.GET();
    if (status > 0) {
      Serial.print("HTTP ");
      Serial.println(status);
      for (int i = 0; i < http.headers(); ++i) {
        Serial.print(http.headerName(i));
        Serial.print(": ");
        Serial.println(http.header(i));
      }
      ss.remove(0);
      http.writeToStream(&ss);
      constexpr int headLen = 140, tailLen = 80;
      int head = ss.length();
      int tail = head - tailLen;
      if (tail <= headLen) {
        tail = 0;  // skip it
      } else {
        head = headLen;
      }
      Serial.println(ss.substring(0, head));
      if (tail) {
        Serial.println("...");
        Serial.println(ss.substring(tail));
      }
    } else {
      Serial.println(http.errorToString(status));
    }
    http.end();
  }
}

void setup() {
  Serial.begin(115200);
  WiFi.begin(SECRET_SSID, SECRET_PASS);
  for (int i = 0; WiFi.status() != WL_CONNECTED; ++i) {
    Serial.print(i % 10 ? "." : "\n.");
    delay(100);
  }
  Serial.print("\nConnected as: ");
  Serial.println(WiFi.localIP());

  auto priority = uxTaskPriorityGet(nullptr);
  xTaskCreateUniversal(httpTask, "http poll",
                       CONFIG_ARDUINO_LOOP_STACK_SIZE, nullptr,
                       priority, nullptr, ARDUINO_RUNNING_CORE);
  Serial.printf("started http poll at priority %d\n", priority);
}

void loop() {
  delay(200);
}

Before Uploading, choose under the Tools menu Core Debug Level: "Debug", which will print messages like this

[  2835][D][HTTPClient.cpp:293] beginInternal(): protocol: http, host: google.com port: 80 url: /
[  2844][D][HTTPClient.cpp:574] sendRequest(): request type: 'GET' redirCount: 0

[  2851][D][NetworkManager.cpp:83] hostByName(): Clearing DNS cache
[  2894][D][NetworkManager.cpp:123] hostByName(): DNS found IPv4 142.250.115.138
[  2998][D][HTTPClient.cpp:1112] connect():  connected to google.com:80
[  3112][D][HTTPClient.cpp:1257] handleHeaderResponse(): code: 301
[  3118][D][HTTPClient.cpp:1260] handleHeaderResponse(): size: 219
[  3124][D][HTTPClient.cpp:618] sendRequest(): sendRequest code=301

HTTP 301
Content-Type: text/html; charset=UTF-8
Content-Length: 219
Location: http://www.google.com/
[  3131][D][HTTPClient.cpp:378] disconnect(): tcp keep open for reuse
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>

[  3155][D][HTTPClient.cpp:378] disconnect(): tcp keep open for reuse
pausing: 1250
[  4420][D][HTTPClient.cpp:293] beginInternal(): protocol: http, host: google.com port: 80 url: /
[  4429][D][HTTPClient.cpp:574] sendRequest(): request type: 'GET' redirCount: 0

[  4436][D][HTTPClient.cpp:1072] connect(): already connected, reusing connection
[  4540][D][HTTPClient.cpp:1257] handleHeaderResponse(): code: 301
[  4546][D][HTTPClient.cpp:1260] handleHeaderResponse(): size: 219
[  4552][D][HTTPClient.cpp:618] sendRequest(): sendRequest code=301

Reusing the connection should be more reliable and save a little time.

Note that running multiple tasks on the same core with the same priority means that they will be time-sliced, like on a real computer. You might also want to do actual work on the loop task; it's running anyway. A task's priority can be modified after it has started. Take note of other tasks that are already running.

Thanks kenb4, any help is always appreciated :)

In fact I got most of my code from ChatGPT/Gemini/Claude - my previous project only involved ESPNow and I wanted quick results for this one (too bad for me, I’m stuck since January :( )

I’ve been “blindly” following the IA’s advice, about stack size, priority values, etc… I thought it was relevant to pin my taskFetchCam to core 0 because it’s the only one that manipulates WiFi, and everything about UI and touchscreen would run on the other core.

Do you mean that it would be better to leave “taskTouchAndDisplay” in the main loop?

Well, as with many topics regarding multi-tasking... maybe? Suppose the hardware/protocol task of grabbing bytes as they fly through the air is a high-priority task. What happens if that task is blocked? Is there data loss?

The setup for every out-of-the-box example is to run the app/sketch on the other core. There is no way for that to (directly) block the WiFi. Protocol does its job grabbing bytes, while the sketch does something with those bytes. A little deeper in the stack, one core fills a buffer, while the other core reads from the buffer, mediated with some multi-tasking primitive.

If the WiFi app is running on the same core, it might repeatedly be pre-empted by the WiFi stack doing its high-priority job. Given that the web stuff is your primary complaint, to give that the best chance to work, run it on core 1.

Then yes, try running your UI on core 1 as well, with the usual setup/loop. Then the decision is whether to set the priority of the HTTP task the same as the main, so that they time-slice; or higher, or lower. (Although the main loop is likely, but not guaranteed, to be priority level 1. You don't want to be priority zero, which already is occupied by the idle task on every core.) The demo sketch has the uxTaskPriorityGet call.