Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Répondre
Avatar du membre
France ramasoft
Chef es bogies
Chef es bogies
Messages : 5833
Enregistré le : 29 juil. 2024, 19:06
Prénom : Patrick
LOCALISATION : Indre - Levroux
Centres d'intérêts : Modélisme ferroviaire
Programmation C++, VB6 et assembleur
Électronique
Echelle pratiquée : H0
Âge : 68
Pays :
France
France
sept. 2026 12 16:19

Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France ramasoft

:hellod:

Je commence une série de panneaux d’affichages publicitaires (ou autre) avec des petits écrans LCD TFT pilotés par un ESP32 et munis d’un lecteur SD pour stocker les images qu’on veut afficher.
IMG_20260912_144357.jpg

L’écran est un 1,14 pouces en résolution 135x240 pixels logé dans le côté d’un abri de quai mais qui peut aussi faire abribus. Il est bien adapté à l’affichage vertical des panneaux publicitaires dits « sucettes »

https://fr.aliexpress.com/item/1005008555269972.html

L’affichage change régulièrement avec un délai réglable selon l’ordre alphabétique ou avec un mode aléatoire. La luminosité de l’écran est aussi réglable le tout grâce à un fichier texte de configuration qui est aussi sur la carte avec les images.
Le nombre d’images est quelconque.

Pour simplifier le code, seuls les jpg sont traités et il faut les mettre à la résolution exacte de l’écran car je n’ai pas fait de routine de mise à l’échelle. Inutile de se compliquer les choses.

Le lecteur de carte micro SD existe sous deux formes :
https://fr.aliexpress.com/item/1005009224403161.html
- avec un régulateur de tension permettant de l’alimenter en 5V
- en format réduit sans régulateur et alimenté en 3,3V uniquement


L’écran et le lecteur sont reliés à l’ESP par une liaison SPI, ils utilisent les mêmes broches :
C’est une liaison de type série bidirectionnelle synchrone
MOSI (Master Out Slave IN) pour la liaison de l’ESP vers le périphérique
MISO (Master IN Slave Out) pour le retour du périphérique vers l’ESP
CLK ou SDA : c’est l’horloge qui cadence la transmission : un octet est échangé à chaque cycle d’horloge dans les deux sens simultanément.
CS (ou SS) : (Chip select ou Slave select) c’est la ligne permettant à l’ESP de choisir le périphérique avec lequel il veut dialoguer. La ligne est mise à l’état bas pour sélectionner le périphérique.

Certaines broches comme MOSI, MISO, CLK ou SDA sont imposées par la configuration de l'ESP32 qui dispose de deux bus SPI indépendants.
Diapositive5.JPG
Et sur les écrans on trouve aussi les broches
RS : Register Select
A l’état haut elle sert à transmettre la couleur d’un pixel
A l’état bas elle sert à effacer l’écran, modifier la luminosité, définir la zone de dessin, éteindre/allumer l'affichage.

Comme le protocole de communication se contente d'envoyer une suite de bits bruts sans contexte, l'écran a besoin de savoir comment interpréter ces octets. Le microcontrôleur bascule l'état logique de cette broche juste avant d'envoyer l'information
Dans la norme SPI classique, il n'existe aucun moyen de séparer logiciellement les commandes des données. Pour éviter de devoir envoyer un octet de configuration supplémentaire à chaque transfert (ce qui ralentirait l'affichage), les fabricants ajoutent cette ligne matérielle dédiée DC = Data Command.
Cela permet de maximiser la vitesse de rafraîchissement de l'écran

Il y a aussi une broche notée BLK (Backlight) qui permet de contrôler le rétroéclairage :
- connectée sur le +3,3V le rétroéclairage est à fond
- connectée sur une broche de l’ESP et pilotée en PWM, elle permet de varier le rétroéclairage et c’est ce que j’ai utilisé.
- On peut aussi la piloter en mode On/Off en tout ou rien depuis une broche de l’ESP.

Dans le cas où on la connecte directement au +3,3V il est conseillé d’ajouter une résistance de 100 ohms pour diminuer sa consommation sur l’ESP.

Là, avec le rétroéclairage à fond tout le montage consomme environ 80mA et frôle les 100mA pendant la lecture de la carte SD.



Comme d’habitude, je monte l’ESP sur un shield fait avec deux connecteurs plats. Un sticker permet de visualiser les broches avec les couleurs de fils à connecter. Même chose sur le connecteur du lecteur SD.
Ainsi il n’y a aucun risque d’erreur de branchement et tout est repéré si on doit réintervenir plus tard.
J’utilise ce shield dès la mise au point plutôt que de m’embêter avec une breadboard et des fils volants dont les couleurs ne correspondent jamais et qui sont juste bons à faire des faux contacts ou à inverser des branchements…
Travailler sur un shield avec des fils soudés n’est pas une perte de temps, bien au contraire et il ressert dans le montage final.
IMG_20260911_144816.jpg
IMG_20260911_171758.jpg
IMG_20260911_174032.jpg

L’écran est enfiché sur un connecteur à l’arrière de l’abri. Un panneau qui supporte un banc vient cacher ça.

Tout est assemblé par encastrement sans colle. Il y a juste le banc qui est collé.
Et tout est branché sur des connecteurs pour pouvoir être démonté rapidement.

IMG_20260911_172926.jpg
IMG_20260911_172958.jpg
IMG_20260911_173003.jpg
IMG_20260912_144323.jpg
Les fils venant de l'écran sont montés sur des broches par groupe de (3 - 3 - 2) comme sur cette photo du panneau de gare pour pouvoir passer par un trou de 8mm dans le plateau
IMG_20260730_175905_1.jpg
IMG_20260912_144422.jpg
IMG_20260912_144442.jpg
IMG_20260912_144450.jpg
IMG_20260912_144526.jpg
Les STL:
Abri quai.stl
Banc.stl
Fond.stl
Panneau Écran.stl
Support ESP.stl
Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.
Modifié en dernier par France ramasoft le 12 sept. 2026, 16:45, modifié 1 fois.
Avatar du membre
France ramasoft
Chef es bogies
Chef es bogies
Messages : 5833
Enregistré le : 29 juil. 2024, 19:06
Prénom : Patrick
LOCALISATION : Indre - Levroux
Centres d'intérêts : Modélisme ferroviaire
Programmation C++, VB6 et assembleur
Électronique
Echelle pratiquée : H0
Âge : 68
Pays :
France
France
sept. 2026 12 16:23

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France ramasoft

Le code est simple et sans fioritures pour être facilement compréhensible et modifiable.
Le décodage et l'affichage des jpg est fait par la bibliothèque TJpg_Decoder (Bodmer)
Le déroulement du code peut être suivi sur le moniteur série, en particulier s'il y avait un problème d'affichage causé par un nom de fichier mal interprété.
La carte SD doit être < 32Go et IMPÉRATIVEMENT formatée en FAT32 et surtout pas en ex-Fat32 qui ne sera pas lue correctement.
Panneau_Publicitaire_V2.ino

Code : Tout sélectionner

/*
 ===================================================================
  Panneau publicitaire HO - affichage JPEG depuis carte SD en boucle
  Version 2.00 © Ramasoft Septembre 2026
===================================================================
  Utilisable avec tout écran géré par la bibliothèque TFT_eSPI
  Ecran ST7735 1.8" (128x160) + lecteur SD SPI sur ESP32 mode paysage
  Ecran ST7789 1.14" (135x240) + lecteur SD SPI sur ESP32 mode portrait
  Bibliothèque requise (à installer via le gestionnaire de bibliothèques Arduino) : TJpg_Decoder (par Bodmer)
  La configuration de l'écran et des broches se fait par le User_Setup.h qui est dans le répertoire de la bibliothèque
  Fichiers : n'importe quel nombre de .jpg a la racine de la carte SD,
  scanné automatiquement au demarrage et affichage par ordre alphabetique.
  Redimensionner les images à la taille exacte pour l'écran avec n'importe quel logiciel (comme Paint de Windows)
  Ils sont affichés par ordre alphabétique
  Attention: certaines cartes acceptent des noms de fichiers jusqu'à 255 caractères mais ... pas toutes ...
  En cas de problème, il vaut mieux rester avec un format 8.3 MS-DOS Ex: PUB0042.JPG
  - 8 caractères maximum pour le nom et 3 pour l'extension.
  - Pas d'accents (é, è, à...) qui posent problème sur FAT quelle que soit la version
  - Pas d'espaces ni de caractères spéciaux (# % & { } \ < > * ? / $ ! ' " : @ + | =`)
  - Majuscules de préférence (plus sûr, même si généralement pas obligatoire).
  Vous pouvez vérifier la bonne lecture des noms de fichiers avec le sketch : Test_lecteur_sd.ino
  
  Fichier Config.txt aussi à la racine de la carte
  Adapter l'orientation à la ligne 75:
  tft.setRotation(x); //  0 ou 2 pour Portrait et 1 ou 3 pour Paysage
*/

#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>

TFT_eSPI tft = TFT_eSPI();

// --- Broches ---  
#define TFT_MOSI_PIN 23
#define TFT_SCLK_PIN 18
#define SD_MISO_PIN  19  // Attention si Miso est à -1 dans le UserSetup le lecteur SD ne s'initialise pas
#define SD_CS_PIN    33
#define PIN_BLK      15 // rétroéclairage géré dans le code pour la luminosité. Laisser à -1 dans le User_Setup.h

// --- Reglage de la luminosite : 0 = eteint, 255 = luminosite maximale ---
uint8_t LUMINOSITE = 255; // Valeur par défaut

// --- Liste des images a afficher en boucle ---
// MAX_FICHIERS_ABSOLU fixe la taille du tableau en memoire (ne peut pas
// changer sans reflasher). limiteFichiers est la limite reellement
// appliquee, ajustable via config.txt (bornee par MAX_FICHIERS_ABSOLU).
#define MAX_FICHIERS_ABSOLU 50
String images[MAX_FICHIERS_ABSOLU];
uint8_t nbImages = 0;
uint8_t indexImage = 0;
uint8_t limiteFichiers = 30;   // valeur par defaut, modifiable via config.txt

// --- Temporisation entre deux images ---
unsigned long intervalleAffichage = 5000; // valeur par defaut (ms), modifiable via config.txt
unsigned long dernierChangement = 0;
bool modeAleatoire = false; // valeur par defaut, modifiable via config.txt (ALEATOIRE=1)

// Callback appele par TJpg_Decoder pour chaque bloc decode (16x16 px typiquement)
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  tft.pushImage(x, y, w, h, bitmap);
  return true; // continuer le decodage
}

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

  ledcAttach(PIN_BLK, 5000, 8); // broche, frequence PWM (5 kHz), resolution (8 bits = 0-255)
  ledcWrite(PIN_BLK, LUMINOSITE); // allume l'écran même si la carte SD n'est pas lue
  randomSeed(esp_random()); // generateur aleatoire materiel de l'ESP32
  SPI.begin(TFT_SCLK_PIN, SD_MISO_PIN, TFT_MOSI_PIN, SD_CS_PIN);

  tft.init();
  tft.setRotation(2); // 0 ou 2 pour Portrait et 1 ou 3 pour Paysage
  tft.fillScreen(TFT_BLACK);

  Serial.print("Initialisation carte SD... ");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("ECHEC.");
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setCursor(5, 5);
    tft.println("Erreur carte SD");
    while (true) delay(1000);
  }
  Serial.println("OK.");

  lireConfiguration();
  ledcWrite(PIN_BLK, LUMINOSITE);


  scannerImagesJpg();
  if (nbImages == 0) {
    Serial.println("Aucun fichier .jpg trouve sur la carte SD.");
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setCursor(5, 5);
    tft.println("Aucun .jpg trouve");
    while (true) delay(1000);
  }

  // Configuration du decodeur JPEG
  TJpgDec.setJpgScale(1);       // pas de reduction d'echelle (image deja a la bonne taille)
  TJpgDec.setSwapBytes(true);   // ordre des octets attendu par TFT_eSPI
  TJpgDec.setCallback(tft_output);

  if (modeAleatoire && nbImages > 1) {   // première image affichée
  indexImage = random(nbImages);
}
afficherImage(images[indexImage].c_str());
dernierChangement = millis();
}

// Lit /config.txt s'il existe (format cle=valeur, une par ligne, lignes
// commencant par # ignorees). Sinon, conserve les valeurs par defaut.
void lireConfiguration() {
  File cfg = SD.open("/config.txt");
  if (!cfg) {
    Serial.println("Pas de config.txt, valeurs par defaut utilisees.");
    return;
  }

  while (cfg.available()) {
    String ligne = cfg.readStringUntil('\n');
    ligne.trim();

    if (ligne.length() == 0 || ligne.startsWith("#")) continue;

    int pos = ligne.indexOf('=');
    if (pos == -1) continue;

    String cle = ligne.substring(0, pos);
    String valeur = ligne.substring(pos + 1);
    cle.trim();
    valeur.trim();

    if (cle == "INTERVALLE_AFFICHAGE") {
      long v = valeur.toInt();
      if (v >= 500) { // borne basse de securite
        intervalleAffichage = (unsigned long)v;
      } else {
        Serial.println("INTERVALLE_AFFICHAGE ignore (valeur trop faible ou invalide).");
      }
    } else if (cle == "MAX_FICHIERS") {
      int v = valeur.toInt();
      if (v > 0 && v <= MAX_FICHIERS_ABSOLU) {
        limiteFichiers = (uint8_t)v;
      } else {
        Serial.print("MAX_FICHIERS ignore (doit etre entre 1 et ");
        Serial.print(MAX_FICHIERS_ABSOLU);
        Serial.println(").");
      }
    } else if (cle == "ALEATOIRE") {
      modeAleatoire = (valeur.toInt() != 0);
    } else if (cle == "LUMINOSITE") {
      LUMINOSITE = valeur.toInt() ;
    }
  }
  cfg.close();

  Serial.println("config.txt charge :");
  Serial.print("  INTERVALLE_AFFICHAGE = ");
  Serial.println(intervalleAffichage);
  Serial.print("  MAX_FICHIERS = ");
  Serial.println(limiteFichiers);
  Serial.print("  ALEATOIRE = ");
  Serial.println(modeAleatoire ? "oui" : "non");
  Serial.print("  Retro-eclairage regle a : ");
  Serial.print(LUMINOSITE);
  Serial.println(" / 255");

}

// Parcourt la racine de la carte SD et retient tous les fichiers se terminant par .jpg (insensible a la casse)
void scannerImagesJpg() {
  nbImages = 0;
  File dossier = SD.open("/");
  File f = dossier.openNextFile();

  while (f) {
    if (!f.isDirectory()) {
      String nom = String(f.name());
      String nomMinuscule = nom;
      nomMinuscule.toLowerCase();

      if (nomMinuscule.endsWith(".jpg") || nomMinuscule.endsWith(".jpeg")) {
        if (nbImages < limiteFichiers) {
          // f.name() ne contient pas toujours le "/" initial selon les versions de la bibliotheque
          images[nbImages] = nom.startsWith("/") ? nom : "/" + nom;
          nbImages++;
        } else {
          Serial.println("Limite MAX_FICHIERS atteinte, fichiers suivants ignores.");
        }
      }
    }
    f = dossier.openNextFile();
  }
  dossier.close();

  // Tri alphabetique simple, pour un ordre d'affichage previsible et stable
  for (uint8_t i = 0; i < nbImages; i++) {
    for (uint8_t j = i + 1; j < nbImages; j++) {
      if (images[j] < images[i]) {
        String tmp = images[i];
        images[i] = images[j];
        images[j] = tmp;
      }
    }
  }

  Serial.print(nbImages);
  Serial.println(" fichier(s) .jpg trouve(s) :");
  for (uint8_t i = 0; i < nbImages; i++) {
    Serial.print("  ");
    Serial.println(images[i]);
  }
}

void loop() {
  if (millis() - dernierChangement >= intervalleAffichage) {
    indexImage = prochainIndex();
    afficherImage(images[indexImage].c_str());
    dernierChangement = millis();
  }
}

// Determine le prochain index a afficher, selon le mode (sequentiel ou aleatoire)
uint8_t prochainIndex() {
  if (!modeAleatoire || nbImages <= 1) {
    return (indexImage + 1) % nbImages;
  }

  // Tirage aleatoire en evitant de retomber sur l'image actuelle
  uint8_t nouvelIndex;
  do {
    nouvelIndex = random(nbImages);
  } while (nouvelIndex == indexImage);
  return nouvelIndex;
}

void afficherImage(const char* chemin) {
  uint16_t w = 0, h = 0;
  TJpgDec.getSdJpgSize(&w, &h, chemin);
  Serial.print("Image : ");
  Serial.print(chemin);
  Serial.print(" (");
  Serial.print(w);
  Serial.print("x");
  Serial.print(h);
  Serial.println(")");

  JRESULT resultat = TJpgDec.drawSdJpg(0, 0, chemin);
  if (resultat != JDR_OK) {
    Serial.print("Erreur de decodage JPEG, code : ");
    Serial.println(resultat);
  }
}
Il permet d’utiliser n’importe quel écran supporté par la bibliothèque TFT_eSPI (Bodmer)
Les paramètres sont passés dans un fichier externe : User_Setup.h qui se trouve dans le répertoire d’installation de la bibliothèque.
Chaque type d’écran a donc son propre fichier de configuration dans lequel on retrouve l’assignation des broches, le nom du driver à utiliser, ici c’est le ST7735
Ainsi que la définition de l’écran en pixel.
Donc pour utiliser un écran, il faut recompiler le code après avoir mis le bon User_Setup.h dans le répertoire de TFT_eSPI.

Code : Tout sélectionner

// Configuration TFT_eSPI pour écran ST7789 1.14" (135x240) sur ESP32 DevKit
// A copier/remplacer dans : Documents/Arduino/libraries/TFT_eSPI/User_Setup.h

#define ST7789_DRIVER
#define TFT_WIDTH  135
#define TFT_HEIGHT 240

// Décalage nécessaire sur beaucoup de modules ST7789 1.14" (bord non actif du panneau)
#define TFT_RGB_ORDER TFT_BGR

#define TFT_MISO 19   
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS   5
#define TFT_DC   2
#define TFT_RST  4
#define TFT_BL  -1   // optionnel, laisser -1 si BLK câblé directement en 3.3V

#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#define SMOOTH_FONT

#define SPI_FREQUENCY  27000000


Un mot sur le fichier de configuration : Config.txt
C’est un fichier texte modifiable avec le bloc notes de Windows.
Le format est nom_du_paramètre=valeur
Il accepte les paramètres suivants :
INTERVALLE_AFFICHAGE en microsecondes (minimum 500)
MAX_FICHIERS nombre maxi de fichier pouvant se trouver sur la carte : il est modifiable et quelconque mais sa valeur maxi doit être indiquée ici pour pouvoir réserver la mémoire nécessaire
ALEATOIRE 0 = lecture par ordre alphabétique et 1= lecture aléatoire
LUMINOSITE = valeur du rétroéclairage de 0 (éteint) à 255 (éclairage plein pot comme branché directement sur le 3,3V)

Code : Tout sélectionner

# Fichier de configuration - panneau publicitaire HO
# Une ligne par reglage, format : CLE=VALEUR
# Les lignes commencant par # sont ignorees (commentaires)
# Si ce fichier est absent, des valeurs par defaut raisonnables sont utilisees.

# Duree d'affichage de chaque image, en millisecondes (minimum 500)
INTERVALLE_AFFICHAGE=10000

# Nombre maximum d'images chargees parmi celles trouvees sur la carte (1 a 50)
MAX_FICHIERS=40

# Ordre d'affichage : 0 = sequentiel (ordre alphabetique), 1 = aleatoire
ALEATOIRE=1

# Luminosité 0 à 255 
LUMINOSITE=200
Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.
Modifié en dernier par France ramasoft le 12 sept. 2026, 16:33, modifié 2 fois.
Avatar du membre
France C2Vues-BricoTrain
2000 posts et +
2000 posts et +
Messages : 9563
Enregistré le : 08 mars 2024, 11:45
Prénom : Fernando
LOCALISATION : Clamart
Centres d'intérêts : Modélisme Ferroviaire - Échelles N et HO
Images IA
Echelle pratiquée : N
Âge : 74
Pays :
France
France
sept. 2026 12 16:30

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France C2Vues-BricoTrain

:bienv: :bienv: :bienv: :bienv: :bienv: :bienv:

Patrick, c'est magnifique ce que tu as fait, et en plus ça s'intègre bien dans un abri bus. :cooooool:

L'animation doit être chouette, j'ai hâte de voir ça dans une petite vidéo.

:PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg: :PetitPoucejpg:
Pour me retrouver sur ma chaîne :
https://youtube.com/@c2vues-bricotrain? ... VpM6qhmP2u

Quand trop gentil, tu seras,
Dans le côté sombre, tu l'auras !

Yoda
Maître Jedi dans Star Wars
Avatar du membre
France ramasoft
Chef es bogies
Chef es bogies
Messages : 5833
Enregistré le : 29 juil. 2024, 19:06
Prénom : Patrick
LOCALISATION : Indre - Levroux
Centres d'intérêts : Modélisme ferroviaire
Programmation C++, VB6 et assembleur
Électronique
Echelle pratiquée : H0
Âge : 68
Pays :
France
France
sept. 2026 12 16:35

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France ramasoft

Oui je vais mettre une petite vidéo plus tard.
J'ai aussi un autre panneau plus grand au format paysage en 3m x 4m

J'avais aussi testé des fondus divers entre deux images mais sur ces petits écran, ça ne donne pas grand chose et ça rallonge énormément le code, si bien que j'ai enlevé tout ça.

:a_suivre:
Avatar du membre
France likiki
Site Admin
Site Admin
Messages : 17018
Enregistré le : 27 févr. 2021, 16:11
Prénom : Christian
LOCALISATION : Ile de France
Centres d'intérêts : Le train miniature (H0 3R), l'impression 3D
Echelle pratiquée : H0
Âge : 59
Pays :
France
France
sept. 2026 12 16:45

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France likiki

C'est magnifique, cela ressemble vraiment au panneaux publicitaire vidéo que l'on trouve partout comme dans les grandes surfaces.


:bienv: :bienv: :bienv: :bienv: :bienv: :bienv: :bienv:
Si le Goëland se gratte le gland, il fera mauvais temps,
Si il se gratte le cul, il ne fera pas beau non plus.

Proverbe de là bas.

ImageImageImage

:tirechap:

.
Si vous souhaitez faire un don pour le forum , c'est ici.
Avatar du membre
France bunny94
2000 posts et +
2000 posts et +
Messages : 15463
Enregistré le : 22 sept. 2021, 08:35
Prénom : Alain
LOCALISATION : Noiseau
Centres d'intérêts : Photo-Vidéo-Modélisme ferroviaire-Trains réels-Impression 3D-Miniature automobile 1/43ème
Echelle pratiquée : H0
Âge : 75
Pays :
France
France
sept. 2026 12 17:12

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France bunny94

Oui c'est super chouette Patrick. :bienv: :bienv: :bienv:
Vivement la vidéo.
Pour me retrouver sur ma chaîne YT: https://www.youtube.com/@abunny94/videos
Avatar du membre
France Amigorl
Posteur irrécupérable
Posteur irrécupérable
Messages : 989
Enregistré le : 02 févr. 2025, 20:15
Prénom : Rémy
LOCALISATION : Pau
Centres d'intérêts : Digitalisation matériel ancien, construction d'un réseau HO. Arduino, Dccex-Conmmand-Station, Rocrail, Digitalisation pont tournant Jouef, Decodeur accessoires DIY. 3D PLA et débutant SLA.
Echelle pratiquée : H0
Âge : 64
Pays :
France
France
sept. 2026 12 18:23

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France Amigorl

:bienv: , magnifique projet et résultats obtenus.

Encore une fois c'est un montage qui pourra servir de référence pour l'utilisation des esp32, écrans et cartes sd.
Une mine d'informations ! :merciiii:
Avatar du membre
France Bud_fr
Posteur fou
Posteur fou
Messages : 261
Enregistré le : 07 févr. 2026, 09:55
Prénom : Didier
LOCALISATION : Six Fours les Plages (83)
Centres d'intérêts : Les nouvelles technologie: La 3D, l'Arduino, l'IA, etc .... tout cela au service du modélisme, ferroviaire actuellement, après être passé par l'aéromodélisme, et le bateau.
Echelle pratiquée : H0
Pays :
France
France
sept. 2026 12 19:26

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France Bud_fr

:bienv: :bienv: :bienv:

Je trouve que c'est original et bien pensé, comme ça dans un abris.

A+
A la maison, il n'y a que la télévision qui me parle, et que mon ordinateur qui me répond. :super:
Avatar du membre
France C2Vues-BricoTrain
2000 posts et +
2000 posts et +
Messages : 9563
Enregistré le : 08 mars 2024, 11:45
Prénom : Fernando
LOCALISATION : Clamart
Centres d'intérêts : Modélisme Ferroviaire - Échelles N et HO
Images IA
Echelle pratiquée : N
Âge : 74
Pays :
France
France
sept. 2026 12 19:59

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France C2Vues-BricoTrain

ramasoft a écrit : 12 sept. 2026, 16:35 J'avais aussi testé des fondus divers entre deux images mais sur ces petits écran, ça ne donne pas grand chose et ça rallonge énormément le code, si bien que j'ai enlevé tout ça.
Patrick,

Pourquoi ça rallonge le code, ce n'est pas un petit fichier vidéo qui est chargé et qui fait tout le boulot des fondus...?
(Fichier fait avec un logiciel de montage en .mp4 ou en .gif s'il n'y a pas de son...)
Pour me retrouver sur ma chaîne :
https://youtube.com/@c2vues-bricotrain? ... VpM6qhmP2u

Quand trop gentil, tu seras,
Dans le côté sombre, tu l'auras !

Yoda
Maître Jedi dans Star Wars
Avatar du membre
France ramasoft
Chef es bogies
Chef es bogies
Messages : 5833
Enregistré le : 29 juil. 2024, 19:06
Prénom : Patrick
LOCALISATION : Indre - Levroux
Centres d'intérêts : Modélisme ferroviaire
Programmation C++, VB6 et assembleur
Électronique
Echelle pratiquée : H0
Âge : 68
Pays :
France
France
sept. 2026 12 20:37

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France ramasoft

Ce n'est pas aussi simple que cela ...
Il n'y a rien de prévu en natif dans la bibliothèque TFT_eSPI et il faut tout gérer dans le code. Mais l'ESP32 a suffisamment de ressources pour le faire sans problème d'autant que la résolution des images est faible, mais ça oblige à charger les deux images en mémoire.
Dans la version actuelle, TJpg_Decoder envoie chaque bloc décodé directement à l'écran (tft.pushImage() dans le callback), sans jamais garder l'image complète en mémoire. Pour faire un fondu, il faut au contraire garder les deux images en RAM (l'actuelle et la suivante) pour pouvoir les mélanger progressivement avant de les envoyer à l'écran.

Les écrans sont petits (135×240 ou 128×160 pixels), donc une image complète en RGB565 ne pèse que 40 à 65 Ko. Garder deux images en mémoire simultanément (environ 130 Ko max) ne pose aucun problème sur l'ESP32 (~320 Ko de RAM disponible, et on n'utilise pas le Wi-Fi). C'est tout à fait jouable.

Le principe du fondu :
1- Décoder la prochaine image JPEG non pas directement vers l'écran, mais vers un buffer mémoire.
2 - Pendant une quinzaine d'étapes, calculer un mélange progressif pixel par pixel entre l'ancienne et la nouvelle image (en interpolant les composantes Rouge/Vert/Bleu de chaque pixel), et envoyer chaque étape intermédiaire à l'écran. (on peut faire d'autres combinaisons pour avoir des fondus différents mais je ne me suis pas trop embêté)
3- À la fin, la nouvelle image est pleinement affichée et devient "l'ancienne" pour la transition suivante.

Je le mettrai dans le prochain panneau qui sera un plus grand en 1,8 ou 2 pouces (je suis en train de tester ça)

Si ça vous intéresse, je mets le code avec le fondu 1ère version qui fonctionne mais demande à être amélioré:

Code : Tout sélectionner

/*
 ===================================================================
  Panneau publicitaire HO - affichage JPEG depuis carte SD en boucle
  Version 3.00 © Ramasoft Septembre 2026
  Ajout du fondu entre deux images
===================================================================
  Utilisable avec tout écran géré par la bibliothèque TFT_eSPI
  Ecran ST7735 1.8" (128x160) + lecteur SD SPI sur ESP32 mode paysage
  Ecran ST7789 1.14" (135x240) + lecteur SD SPI sur ESP32 mode portrait
  Bibliothèque requise (à installer via le gestionnaire de bibliothèques Arduino) : TJpg_Decoder (par Bodmer)
  La configuration de l'écran et des broches se fait par le User_Setup.h qui est dans le répertoire de la bibliothèque
  Fichiers : n'importe quel nombre de .jpg a la racine de la carte SD,
  scanné automatiquement au demarrage et affichage par ordre alphabetique.
  Redimensionner les images à la taille exacte pour l'écran avec n'importe quel logiciel (comme Paint de Windows)
  Ils sont affichés par ordre alphabétique
  Attention: certaines cartes acceptent des noms de fichiers jusqu'à 255 caractères mais ... pas toutes ...
  En cas de problème, il vaut mieux rester avec un format 8.3 MS-DOS Ex: PUB0042.JPG
  - 8 caractères maximum pour le nom et 3 pour l'extension.
  - Pas d'accents (é, è, à...) qui posent problème sur FAT quelle que soit la version
  - Pas d'espaces ni de caractères spéciaux (# % & { } \ < > * ? / $ ! ' " : @ + | =`)
  - Majuscules de préférence (plus sûr, même si généralement pas obligatoire).
  Vous pouvez vérifier la bonne lecture des noms de fichiers avec le sketch : Test_lecteur_sd.ino
  
  Fichier Config.txt aussi à la racine de la carte
  Adapter l'orientation à la ligne 75:
  tft.setRotation(x); //  0 ou 2 pour Portrait et 1 ou 3 pour Paysage

  Nouveaux réglages ajoutés dans config.txt :
  FONDU=1              (0 pour désactiver le fondu et revenir à un changement direct)
  FONDU_ETAPES=15       (2 à 60 : plus élevé = fondu plus doux mais plus long)
*/

#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>

TFT_eSPI tft = TFT_eSPI();

// --- Broches ---  
#define TFT_MOSI_PIN 23
#define TFT_SCLK_PIN 18
#define SD_MISO_PIN  19  // Attention si Miso est à -1 dans le UserSetup le lecteur SD ne s'initialise pas
#define SD_CS_PIN    33
#define PIN_BLK      15 // rétroéclairage géré dans le code pour la luminosité. Laisser à -1 dans le User_Setup.h

// --- Reglage de la luminosite : 0 = eteint, 255 = luminosite maximale ---
uint8_t LUMINOSITE = 255; // Valeur par défaut

// --- Liste des images a afficher en boucle ---
// MAX_FICHIERS_ABSOLU fixe la taille du tableau en memoire (ne peut pas
// changer sans reflasher). limiteFichiers est la limite reellement
// appliquee, ajustable via config.txt (bornee par MAX_FICHIERS_ABSOLU).
#define MAX_FICHIERS_ABSOLU 50
String images[MAX_FICHIERS_ABSOLU];
uint8_t nbImages = 0;
uint8_t indexImage = 0;
uint8_t limiteFichiers = 30;   // valeur par defaut, modifiable via config.txt

// --- Temporisation entre deux images ---
unsigned long intervalleAffichage = 5000; // valeur par defaut (ms), modifiable via config.txt
unsigned long dernierChangement = 0;
bool modeAleatoire = false; // valeur par defaut, modifiable via config.txt (ALEATOIRE=1)

// Callback appele par TJpg_Decoder pour chaque bloc decode (16x16 px typiquement)
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  tft.pushImage(x, y, w, h, bitmap);
  return true; // continuer le decodage
}

// ===================================================================
// Fondu enchaine entre deux images
// ===================================================================
bool fonduActif = true;              // modifiable via config.txt (FONDU=0 pour desactiver)
uint8_t fonduEtapes = 15;            // modifiable via config.txt (FONDU_ETAPES=...)

uint16_t *bufferActuel = nullptr;
uint16_t *bufferSuivant = nullptr;
uint16_t *bufferFondu = nullptr;
uint32_t nbPixelsEcran = 0;

// A appeler dans setup(), juste apres tft.init() + tft.setRotation()
void allouerBuffersFondu() {
  nbPixelsEcran = (uint32_t)tft.width() * tft.height();
  bufferActuel  = (uint16_t*)malloc(nbPixelsEcran * sizeof(uint16_t));
  bufferSuivant = (uint16_t*)malloc(nbPixelsEcran * sizeof(uint16_t));
  bufferFondu   = (uint16_t*)malloc(nbPixelsEcran * sizeof(uint16_t));

  if (!bufferActuel || !bufferSuivant || !bufferFondu) {
    Serial.println("Erreur : memoire insuffisante pour les buffers de fondu, fondu desactive.");
    fonduActif = false;
    return;
  }
  memset(bufferActuel, 0, nbPixelsEcran * sizeof(uint16_t));
}

// Callback special : au lieu d'afficher le bloc decode, on le copie
// dans bufferSuivant (utilise uniquement pendant le decodage JPEG en mode fondu)
bool jpeg_vers_buffer(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  int16_t largeurEcran = tft.width();
  int16_t hauteurEcran = tft.height();

  for (int16_t j = 0; j < h; j++) {
    int16_t py = y + j;
    if (py < 0 || py >= hauteurEcran) continue;
    for (int16_t i = 0; i < w; i++) {
      int16_t px = x + i;
      if (px < 0 || px >= largeurEcran) continue;
      bufferSuivant[(uint32_t)py * largeurEcran + px] = bitmap[j * w + i];
    }
  }
  return true;
}

// Melange deux couleurs RGB565 selon un ratio (0 = entierement c1, 255 = entierement c2)
uint16_t melangerCouleur(uint16_t c1, uint16_t c2, uint8_t ratio) {
  uint8_t r1 = (c1 >> 11) & 0x1F, g1 = (c1 >> 5) & 0x3F, b1 = c1 & 0x1F;
  uint8_t r2 = (c2 >> 11) & 0x1F, g2 = (c2 >> 5) & 0x3F, b2 = c2 & 0x1F;

  uint8_t r = r1 + ((int16_t)(r2 - r1) * ratio) / 255;
  uint8_t g = g1 + ((int16_t)(g2 - g1) * ratio) / 255;
  uint8_t b = b1 + ((int16_t)(b2 - b1) * ratio) / 255;

  return (r << 11) | (g << 5) | b;
}

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

  ledcAttach(PIN_BLK, 5000, 8); // broche, frequence PWM (5 kHz), resolution (8 bits = 0-255)
  ledcWrite(PIN_BLK, LUMINOSITE); // allume l'écran même si la carte SD n'est pas lue
  randomSeed(esp_random()); // generateur aleatoire materiel de l'ESP32
  SPI.begin(TFT_SCLK_PIN, SD_MISO_PIN, TFT_MOSI_PIN, SD_CS_PIN);

  tft.init();
  tft.setRotation(2); // 0 ou 2 pour Portrait et 1 ou 3 pour Paysage
  tft.fillScreen(TFT_BLACK);

  allouerBuffersFondu();

  Serial.print("Initialisation carte SD... ");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("ECHEC.");
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setCursor(5, 5);
    tft.println("Erreur carte SD");
    while (true) delay(1000);
  }
  Serial.println("OK.");

  lireConfiguration();
  ledcWrite(PIN_BLK, LUMINOSITE);


  scannerImagesJpg();
  if (nbImages == 0) {
    Serial.println("Aucun fichier .jpg trouve sur la carte SD.");
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setCursor(5, 5);
    tft.println("Aucun .jpg trouve");
    while (true) delay(1000);
  }

  // Configuration du decodeur JPEG
  TJpgDec.setJpgScale(1);       // pas de reduction d'echelle (image deja a la bonne taille)
  TJpgDec.setSwapBytes(true);   // ordre des octets attendu par TFT_eSPI
  TJpgDec.setCallback(tft_output);

  if (modeAleatoire && nbImages > 1) {   // première image affichée
    indexImage = random(nbImages);
  }
  afficherImageAvecFondu(images[indexImage].c_str());
  dernierChangement = millis();
}

// Lit /config.txt s'il existe (format cle=valeur, une par ligne, lignes
// commencant par # ignorees). Sinon, conserve les valeurs par defaut.
void lireConfiguration() {
  File cfg = SD.open("/config.txt");
  if (!cfg) {
    Serial.println("Pas de config.txt, valeurs par defaut utilisees.");
    return;
  }

  while (cfg.available()) {
    String ligne = cfg.readStringUntil('\n');
    ligne.trim();

    if (ligne.length() == 0 || ligne.startsWith("#")) continue;

    int pos = ligne.indexOf('=');
    if (pos == -1) continue;

    String cle = ligne.substring(0, pos);
    String valeur = ligne.substring(pos + 1);
    cle.trim();
    valeur.trim();

    if (cle == "INTERVALLE_AFFICHAGE") {
      long v = valeur.toInt();
      if (v >= 500) { // borne basse de securite
        intervalleAffichage = (unsigned long)v;
      } else {
        Serial.println("INTERVALLE_AFFICHAGE ignore (valeur trop faible ou invalide).");
      }
    } else if (cle == "MAX_FICHIERS") {
      int v = valeur.toInt();
      if (v > 0 && v <= MAX_FICHIERS_ABSOLU) {
        limiteFichiers = (uint8_t)v;
      } else {
        Serial.print("MAX_FICHIERS ignore (doit etre entre 1 et ");
        Serial.print(MAX_FICHIERS_ABSOLU);
        Serial.println(").");
      }
    } else if (cle == "ALEATOIRE") {
      modeAleatoire = (valeur.toInt() != 0);
    } else if (cle == "LUMINOSITE") {
      LUMINOSITE = valeur.toInt() ;
    } else if (cle == "FONDU") {
      fonduActif = (valeur.toInt() != 0);
    } else if (cle == "FONDU_ETAPES") {
      int v = valeur.toInt();
      if (v >= 2 && v <= 60) {
        fonduEtapes = (uint8_t)v;
      } else {
        Serial.println("FONDU_ETAPES ignore (doit etre entre 2 et 60).");
      }
    }
  }
  cfg.close();

  Serial.println("config.txt charge :");
  Serial.print("  INTERVALLE_AFFICHAGE = ");
  Serial.println(intervalleAffichage);
  Serial.print("  MAX_FICHIERS = ");
  Serial.println(limiteFichiers);
  Serial.print("  ALEATOIRE = ");
  Serial.println(modeAleatoire ? "oui" : "non");
  Serial.print("  Retro-eclairage regle a : ");
  Serial.print(LUMINOSITE);
  Serial.println(" / 255");
  Serial.print("  FONDU = ");
  Serial.println(fonduActif ? "oui" : "non");
  Serial.print("  FONDU_ETAPES = ");
  Serial.println(fonduEtapes);
}

// Parcourt la racine de la carte SD et retient tous les fichiers se terminant par .jpg (insensible a la casse)
void scannerImagesJpg() {
  nbImages = 0;
  File dossier = SD.open("/");
  File f = dossier.openNextFile();

  while (f) {
    if (!f.isDirectory()) {
      String nom = String(f.name());
      String nomMinuscule = nom;
      nomMinuscule.toLowerCase();

      if (nomMinuscule.endsWith(".jpg") || nomMinuscule.endsWith(".jpeg")) {
        if (nbImages < limiteFichiers) {
          // f.name() ne contient pas toujours le "/" initial selon les versions de la bibliotheque
          images[nbImages] = nom.startsWith("/") ? nom : "/" + nom;
          nbImages++;
        } else {
          Serial.println("Limite MAX_FICHIERS atteinte, fichiers suivants ignores.");
        }
      }
    }
    f = dossier.openNextFile();
  }
  dossier.close();

  // Tri alphabetique simple, pour un ordre d'affichage previsible et stable
  for (uint8_t i = 0; i < nbImages; i++) {
    for (uint8_t j = i + 1; j < nbImages; j++) {
      if (images[j] < images[i]) {
        String tmp = images[i];
        images[i] = images[j];
        images[j] = tmp;
      }
    }
  }

  Serial.print(nbImages);
  Serial.println(" fichier(s) .jpg trouve(s) :");
  for (uint8_t i = 0; i < nbImages; i++) {
    Serial.print("  ");
    Serial.println(images[i]);
  }
}

void loop() {
  if (millis() - dernierChangement >= intervalleAffichage) {
    indexImage = prochainIndex();
    afficherImageAvecFondu(images[indexImage].c_str());
    dernierChangement = millis();
  }
}

// Determine le prochain index a afficher, selon le mode (sequentiel ou aleatoire)
uint8_t prochainIndex() {
  if (!modeAleatoire || nbImages <= 1) {
    return (indexImage + 1) % nbImages;
  }

  // Tirage aleatoire en evitant de retomber sur l'image actuelle
  uint8_t nouvelIndex;
  do {
    nouvelIndex = random(nbImages);
  } while (nouvelIndex == indexImage);
  return nouvelIndex;
}

void afficherImage(const char* chemin) {
  uint16_t w = 0, h = 0;
  TJpgDec.getSdJpgSize(&w, &h, chemin);
  Serial.print("Image : ");
  Serial.print(chemin);
  Serial.print(" (");
  Serial.print(w);
  Serial.print("x");
  Serial.print(h);
  Serial.println(")");

  JRESULT resultat = TJpgDec.drawSdJpg(0, 0, chemin);
  if (resultat != JDR_OK) {
    Serial.print("Erreur de decodage JPEG, code : ");
    Serial.println(resultat);
  }
}

// Affiche une image avec un fondu enchaine depuis l'image actuellement affichee
// (bascule automatiquement sur l'affichage direct si le fondu est desactive
// ou si les buffers n'ont pas pu etre alloues)
void afficherImageAvecFondu(const char* chemin) {
  if (!fonduActif || !bufferActuel || !bufferSuivant || !bufferFondu) {
    afficherImage(chemin);
    return;
  }

  Serial.print("Image (fondu) : ");
  Serial.println(chemin);

  TJpgDec.setCallback(jpeg_vers_buffer);
  JRESULT resultat = TJpgDec.drawSdJpg(0, 0, chemin);
  TJpgDec.setCallback(tft_output);

  if (resultat != JDR_OK) {
    Serial.print("Erreur de decodage JPEG, code : ");
    Serial.println(resultat);
    return;
  }

  for (uint8_t etape = 1; etape <= fonduEtapes; etape++) {
    uint8_t ratio = (uint16_t)etape * 255 / fonduEtapes;
    for (uint32_t p = 0; p < nbPixelsEcran; p++) {
      bufferFondu[p] = melangerCouleur(bufferActuel[p], bufferSuivant[p], ratio);
    }
    tft.pushImage(0, 0, tft.width(), tft.height(), bufferFondu);
  }

  memcpy(bufferActuel, bufferSuivant, nbPixelsEcran * sizeof(uint16_t));
}
Panneau_Publicitaire_V3.ino
Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.
Avatar du membre
France patrice_b
2000 posts et +
2000 posts et +
Messages : 3891
Enregistré le : 31 janv. 2025, 14:45
Prénom : Patrice
LOCALISATION : Beziers Herault
Centres d'intérêts : Modelisme HO et HOM ou HOe
Echelle pratiquée : H0
Âge : 65
Pays :
France
France
sept. 2026 14 12:58

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France patrice_b

:PetitPoucejpg: :bienv: :bienv:

Encore un sacré travail Patrick .
Cordialement,

Patrice
Avatar du membre
France ramasoft
Chef es bogies
Chef es bogies
Messages : 5833
Enregistré le : 29 juil. 2024, 19:06
Prénom : Patrick
LOCALISATION : Indre - Levroux
Centres d'intérêts : Modélisme ferroviaire
Programmation C++, VB6 et assembleur
Électronique
Echelle pratiquée : H0
Âge : 68
Pays :
France
France
sept. 2026 15 19:34

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France ramasoft

Suite avec deux panneaux sur pied.


Le premier utilise un écran de 1,8 pouce en 128 x 160 pixel ce qui semble peu mais donne quand même une image correcte.
Le gros défaut de ces écrans pas chers c’est leur angle de vision qui est faible : dès qu’on se décale un peu sur le côté l’image se dégrade.
DSC01049.JPG

IMG_20260914_173427.jpg
IMG_20260914_173606.jpg
Pour diminuer la largeur du panneau, j’ai coupé la partie du circuit imprimé que ne sert à rien.
Les fils passent dans le pied et en les groupant par deux on arrive à passer les connecteurs ce qui permet d’avoir tout démontable et aucune soudure à faire sur le réseau, ou plutôt en dessous…
IMG_20260913_174144.jpg


Le deuxième écran est un 2 pouces de 240 x 320 pixels, donc avec une bonne résolution et une très belle image.
Il a la taille d’un panneau d’affichage publicitaire classique.
DSC01056.JPG
IMG_20260915_155533.jpg

L’ESP et le lecteur SD sont montés sur des connecteurs et des stickers repèrent la branchements à faire avec la couleur des fils. Il n’y a aucun risque d’erreur et ça se monte réellement en 5 minutes. Ne pas oublier la coupure des pistes entre les deux côtés de l’ESP.

IMG_20260913_155239.jpg
IMG_20260911_174032.jpg

Au niveau du logiciel, j’ai essayé plusieurs modes de fondus entre images mais aucun ne m’a vraiment donné satisfaction. J’ai mis Claude sur l’affaire mais il ne m’a rien proposé de vraiment convaincant, si bien que j’ai fini par faire tout bêtement un recouvrement plus ou moins lent de l’ancienne image par la nouvelle en commençant par le haut.

Pour faire une transition il faut que les deux images soient chargées en mémoire en même temps et si ça passe avec les petites résolutions, le problème se pose avec l’écran en 240x320 avec des images trop lourdes et l’allocation mémoire échoue. J’avais prévu une sécurité dans ce cas qui désactive simplement la transition.
Les nouveaux réglages se font toujours dans le fichier config.txt

On peut suivre le déroulement du logiciel sur le moniteur série qui affiche chaque action en cours et les éventuelles erreurs rencontrées.

Donc si ça vous intéresse, c’est très facile à faire.

Les écrans utilisés :

Écran 2 pouces 240-320: https://fr.aliexpress.com/item/1005008828553824.html
Écran 1,8 pouces 128 x 160 : https://fr.aliexpress.com/item/1005006139989470.html
Lecteur carte micro SD: https://fr.aliexpress.com/item/1005009224403161.html

Les fichiers STL des deux panneaux :
Panneau 2 pouces.stl
Fond 2 pouces.stl
Fond 2 pouces inversé.stl
Socle Panneau 2 pouces.stl
Panneau 1_8p.stl
Fond 1_8p.stl
Socle 1_8p.stl

Le code de la version 3 :
Panneau_Publicitaire_V3.ino

Code : Tout sélectionner

/*
 ===================================================================
  Panneau publicitaire HO - affichage JPEG depuis carte SD en boucle
  Version 3.00 © Ramasoft Septembre 2026
  Transition entre images
===================================================================
  Utilisable avec tout écran géré par la bibliothèque TFT_eSPI
  Ecran ST7735 1.8" (128x160) + lecteur SD SPI sur ESP32 mode paysage
  Ecran ST7789 1.14" (135x240) + lecteur SD SPI sur ESP32 mode portrait
  Bibliothèque requise (à installer via le gestionnaire de bibliothèques Arduino) : TJpg_Decoder (par Bodmer)
  La configuration de l'écran et des broches se fait par le User_Setup.h qui est dans le répertoire de la bibliothèque
  Fichiers : n'importe quel nombre de .jpg a la racine de la carte SD,
  scanné automatiquement au demarrage et affichage par ordre alphabetique.
  Redimensionner les images à la taille exacte pour l'écran avec n'importe quel logiciel (comme Paint de Windows)
  Ils sont affichés par ordre alphabétique
  Attention: certaines cartes acceptent des noms de fichiers jusqu'à 255 caractères mais ... pas toutes ...
  En cas de problème, il vaut mieux rester avec un format 8.3 MS-DOS Ex: PUB0042.JPG
  - 8 caractères maximum pour le nom et 3 pour l'extension.
  - Pas d'accents (é, è, à...) qui posent problème sur FAT quelle que soit la version
  - Pas d'espaces ni de caractères spéciaux (# % & { } \ < > * ? / $ ! ' " : @ + | =`)
  - Majuscules de préférence (plus sûr, même si généralement pas obligatoire).
  Vous pouvez vérifier la bonne lecture des noms de fichiers avec le sketch : Test_lecteur_sd.ino
  
  Fichier Config.txt aussi à la racine de la carte
  Adapter l'orientation à la ligne 75:
  tft.setRotation(x); //  0 ou 2 pour Portrait et 1 ou 3 pour Paysage

  Nouveaux réglages dans config.txt :
  TRANSITION=1          (0 pour désactiver et revenir à un changement direct)
  TRANSITION_PAS=4       (1 à 40 : nombre de lignes révélées à chaque étape)
  TRANSITION_DELAI=12    (0 à 200 ms : pause entre chaque étape du balayage)
*/

#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>

TFT_eSPI tft = TFT_eSPI();

// --- Broches ---  
#define TFT_MOSI_PIN 23
#define TFT_SCLK_PIN 18
#define SD_MISO_PIN  19  // Attention si Miso est à -1 dans le UserSetup le lecteur SD ne s'initialise pas
#define SD_CS_PIN    33
#define PIN_BLK      15 // rétroéclairage géré dans le code pour la luminosité. Laisser à -1 dans le User_Setup.h

// --- Reglage de la luminosite : 0 = eteint, 255 = luminosite maximale ---
uint8_t LUMINOSITE = 255; // Valeur par défaut

// A ajuster si l'image apparaît décalée ou coupée sur les bords
// (décale le dessin vers le bas/la droite ; laisse une marge noire au lieu
// de la bande blanche sur les bords opposés)
#define OFFSET_X 0  //4  
#define OFFSET_Y 0  //3  

// --- Liste des images a afficher en boucle ---
// MAX_FICHIERS_ABSOLU fixe la taille du tableau en memoire (ne peut pas
// changer sans reflasher). limiteFichiers est la limite réellement
// appliquée, ajustable via config.txt (bornée par MAX_FICHIERS_ABSOLU).
#define MAX_FICHIERS_ABSOLU 50
String images[MAX_FICHIERS_ABSOLU];
uint8_t nbImages = 0;
uint8_t indexImage = 0;
uint8_t limiteFichiers = 30;   // valeur par defaut, modifiable via config.txt

// --- Temporisation entre deux images ---
unsigned long intervalleAffichage = 5000; // valeur par defaut (ms), modifiable via config.txt
unsigned long dernierChangement = 0;
bool modeAleatoire = false; // valeur par défaut, modifiable via config.txt (ALEATOIRE=1)

// Callback appelé par TJpg_Decoder pour chaque bloc décodé (16x16 px typiquement)
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  tft.pushImage(x + OFFSET_X, y + OFFSET_Y, w, h, bitmap);
  return true; // continuer le décodage
}

// ===================================================================
// Transition en balayage : révèle la nouvelle image ligne par ligne
// depuis le haut de l'écran, par-dessus l'ancienne
// ===================================================================
bool transitionActive = true;        // modifiable via config.txt (TRANSITION=0 pour desactiver)
uint8_t lignesParEtape = 4;          // modifiable via config.txt (TRANSITION_PAS=...)
uint8_t delaiEtape = 12;             // modifiable via config.txt (TRANSITION_DELAI=... en ms)

uint16_t *bufferSuivant = nullptr;
uint32_t nbPixelsEcran = 0;

// A appeler dans setup(), juste après tft.init() + tft.setRotation()
void allouerBufferTransition() {
  nbPixelsEcran = (uint32_t)tft.width() * tft.height();
  bufferSuivant = (uint16_t*)malloc(nbPixelsEcran * sizeof(uint16_t));

  if (!bufferSuivant) {
    Serial.println("Erreur : memoire insuffisante pour le buffer de transition, balayage desactive.");
    transitionActive = false;
  }
}

// Callback spécial : au lieu d'afficher le bloc décodé, on le copie
// dans bufferSuivant (utilisé uniquement pendant le decodage JPEG en mode balayage)
bool jpeg_vers_buffer(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  int16_t largeurEcran = tft.width();
  int16_t hauteurEcran = tft.height();

  for (int16_t j = 0; j < h; j++) {
    int16_t py = y + j;
    if (py < 0 || py >= hauteurEcran) continue;
    for (int16_t i = 0; i < w; i++) {
      int16_t px = x + i;
      if (px < 0 || px >= largeurEcran) continue;
      bufferSuivant[(uint32_t)py * largeurEcran + px] = bitmap[j * w + i];
    }
  }
  return true;
}

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

  ledcAttach(PIN_BLK, 5000, 8); // broche, fréquence PWM (5 kHz), résolution (8 bits = 0-255)
  ledcWrite(PIN_BLK, LUMINOSITE); // allume l'écran même si la carte SD n'est pas lue
  randomSeed(esp_random()); // generateur aléatoire matériel de l'ESP32
  SPI.begin(TFT_SCLK_PIN, SD_MISO_PIN, TFT_MOSI_PIN, SD_CS_PIN);

  tft.init();
  tft.setRotation(3); // 0 ou 2 pour Portrait et 1 ou 3 pour Paysage
  tft.fillScreen(TFT_BLACK);

  allouerBufferTransition();

  Serial.print("Initialisation carte SD... ");
  if (!SD.begin(SD_CS_PIN)) {
    Serial.println("ECHEC.");
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setCursor(5, 5);
    tft.println("Erreur carte SD");
    while (true) delay(1000);
  }
  Serial.println("OK.");

  lireConfiguration();
  ledcWrite(PIN_BLK, LUMINOSITE);


  scannerImagesJpg();
  if (nbImages == 0) {
    Serial.println("Aucun fichier .jpg trouve sur la carte SD.");
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.setCursor(5, 5);
    tft.println("Aucun .jpg trouve");
    while (true) delay(1000);
  }

  // Configuration du decodeur JPEG
  TJpgDec.setJpgScale(1);       // pas de réduction d'échelle (image déjà à la bonne taille)
  TJpgDec.setSwapBytes(true);   // ordre des octets attendu par TFT_eSPI
  TJpgDec.setCallback(tft_output);

  if (modeAleatoire && nbImages > 1) {   // première image affichée
    indexImage = random(nbImages);
  }
  afficherImageAvecBalayage(images[indexImage].c_str());
  dernierChangement = millis();
}

// Lit /config.txt s'il existe (format clé=valeur, une par ligne, lignes
// commencant par # ignorees). Sinon, conserve les valeurs par défaut.
void lireConfiguration() {
  File cfg = SD.open("/config.txt");
  if (!cfg) {
    Serial.println("Pas de config.txt, valeurs par defaut utilisees.");
    return;
  }

  while (cfg.available()) {
    String ligne = cfg.readStringUntil('\n');
    ligne.trim();

    if (ligne.length() == 0 || ligne.startsWith("#")) continue;

    int pos = ligne.indexOf('=');
    if (pos == -1) continue;

    String cle = ligne.substring(0, pos);
    String valeur = ligne.substring(pos + 1);
    cle.trim();
    valeur.trim();

    if (cle == "INTERVALLE_AFFICHAGE") {
      long v = valeur.toInt();
      if (v >= 500) { // borne basse de securité
        intervalleAffichage = (unsigned long)v;
      } else {
        Serial.println("INTERVALLE_AFFICHAGE ignore (valeur trop faible ou invalide).");
      }
    } else if (cle == "MAX_FICHIERS") {
      int v = valeur.toInt();
      if (v > 0 && v <= MAX_FICHIERS_ABSOLU) {
        limiteFichiers = (uint8_t)v;
      } else {
        Serial.print("MAX_FICHIERS ignore (doit etre entre 1 et ");
        Serial.print(MAX_FICHIERS_ABSOLU);
        Serial.println(").");
      }
    } else if (cle == "ALEATOIRE") {
      modeAleatoire = (valeur.toInt() != 0);
    } else if (cle == "LUMINOSITE") {
      LUMINOSITE = valeur.toInt() ;
    } else if (cle == "TRANSITION") {
      transitionActive = (valeur.toInt() != 0);
    } else if (cle == "TRANSITION_PAS") {
      int v = valeur.toInt();
      if (v >= 1 && v <= 40) {
        lignesParEtape = (uint8_t)v;
      } else {
        Serial.println("TRANSITION_PAS ignore (doit etre entre 1 et 40).");
      }
    } else if (cle == "TRANSITION_DELAI") {
      int v = valeur.toInt();
      if (v >= 0 && v <= 200) {
        delaiEtape = (uint8_t)v;
      } else {
        Serial.println("TRANSITION_DELAI ignore (doit etre entre 0 et 200).");
      }
    }
  }
  cfg.close();

  Serial.println("config.txt charge :");
  Serial.print("  INTERVALLE_AFFICHAGE = ");
  Serial.println(intervalleAffichage);
  Serial.print("  MAX_FICHIERS = ");
  Serial.println(limiteFichiers);
  Serial.print("  ALEATOIRE = ");
  Serial.println(modeAleatoire ? "oui" : "non");
  Serial.print("  Retro-eclairage regle a : ");
  Serial.print(LUMINOSITE);
  Serial.println(" / 255");
  Serial.print("  TRANSITION = ");
  Serial.println(transitionActive ? "oui" : "non");
  Serial.print("  TRANSITION_PAS = ");
  Serial.println(lignesParEtape);
  Serial.print("  TRANSITION_DELAI = ");
  Serial.println(delaiEtape);
}

// Parcourt la racine de la carte SD et retient tous les fichiers se terminant par .jpg (insensible à la casse)
void scannerImagesJpg() {
  nbImages = 0;
  File dossier = SD.open("/");
  File f = dossier.openNextFile();

  while (f) {
    if (!f.isDirectory()) {
      String nom = String(f.name());
      String nomMinuscule = nom;
      nomMinuscule.toLowerCase();

      if (nomMinuscule.endsWith(".jpg") || nomMinuscule.endsWith(".jpeg")) {
        if (nbImages < limiteFichiers) {
          // f.name() ne contient pas toujours le "/" initial selon les versions de la bibliothèque
          images[nbImages] = nom.startsWith("/") ? nom : "/" + nom;
          nbImages++;
        } else {
          Serial.println("Limite MAX_FICHIERS atteinte, fichiers suivants ignores.");
        }
      }
    }
    f = dossier.openNextFile();
  }
  dossier.close();

  // Tri alphabétique simple, pour un ordre d'affichage prévisible et stable
  for (uint8_t i = 0; i < nbImages; i++) {
    for (uint8_t j = i + 1; j < nbImages; j++) {
      if (images[j] < images[i]) {
        String tmp = images[i];
        images[i] = images[j];
        images[j] = tmp;
      }
    }
  }

  Serial.print(nbImages);
  Serial.println(" fichier(s) .jpg trouve(s) :");
  for (uint8_t i = 0; i < nbImages; i++) {
    Serial.print("  ");
    Serial.println(images[i]);
  }
}

void loop() {
  if (millis() - dernierChangement >= intervalleAffichage) {
    indexImage = prochainIndex();
    afficherImageAvecBalayage(images[indexImage].c_str());
    dernierChangement = millis();
  }
}

// Détermine le prochain index à afficher, selon le mode (séquentiel ou aléatoire)
uint8_t prochainIndex() {
  if (!modeAleatoire || nbImages <= 1) {
    return (indexImage + 1) % nbImages;
  }

  // Tirage aléatoire en evitant de retomber sur l'image actuelle
  uint8_t nouvelIndex;
  do {
    nouvelIndex = random(nbImages);
  } while (nouvelIndex == indexImage);
  return nouvelIndex;
}

void afficherImage(const char* chemin) {
  uint16_t w = 0, h = 0;
  TJpgDec.getSdJpgSize(&w, &h, chemin);
  Serial.print("Image : ");
  Serial.print(chemin);
  Serial.print(" (");
  Serial.print(w);
  Serial.print("x");
  Serial.print(h);
  Serial.println(")");

  JRESULT resultat = TJpgDec.drawSdJpg(0, 0, chemin);
  if (resultat != JDR_OK) {
    Serial.print("Erreur de decodage JPEG, code : ");
    Serial.println(resultat);
  }
}

// Affiche une image avec un balayage progressif depuis le haut de l'écran
// (bascule automatiquement sur l'affichage direct si désactivé ou si le
// buffer n'a pas pu être alloué)
void afficherImageAvecBalayage(const char* chemin) {
  if (!transitionActive || !bufferSuivant) {
    afficherImage(chemin);
    return;
  }

  Serial.print("Image (balayage) : ");
  Serial.println(chemin);

  uint16_t w = 0, h = 0;
  TJpgDec.getSdJpgSize(&w, &h, chemin);
  if (w != tft.width() || h != tft.height()) {
    Serial.print("  ATTENTION : dimensions de l'image (");
    Serial.print(w); Serial.print("x"); Serial.print(h);
    Serial.print(") differentes de l'ecran (");
    Serial.print(tft.width()); Serial.print("x"); Serial.print(tft.height());
    Serial.println(").");
  }

  memset(bufferSuivant, 0, nbPixelsEcran * sizeof(uint16_t)); // évite les résidus de l'image précédente

  TJpgDec.setCallback(jpeg_vers_buffer);
  JRESULT resultat = TJpgDec.drawSdJpg(0, 0, chemin);
  TJpgDec.setCallback(tft_output);

  if (resultat != JDR_OK) {
    Serial.print("Erreur de decodage JPEG, code : ");
    Serial.println(resultat);
    return;
  }

  int16_t largeurEcran = tft.width();
  int16_t hauteurEcran = tft.height();

  // Révèle bufferSuivant ligne par ligne depuis le haut, par-dessus
  // l'ancienne image encore affichée à l'écran. Décale de OFFSET_X/OFFSET_Y :
  // les pixels au-delà des bords de l'écran sont automatiquement ignorés par
  // TFT_eSPI, et la marge haut/gauche reste noire (fond initial de l'écran).
  for (int16_t y = 0; y < hauteurEcran; y += lignesParEtape) {
    int16_t h = min((int16_t)lignesParEtape, (int16_t)(hauteurEcran - y));
    tft.pushImage(OFFSET_X, y + OFFSET_Y, largeurEcran, h, &bufferSuivant[(uint32_t)y * largeurEcran]);
    if (delaiEtape > 0) delay(delaiEtape);
  }
}
Le fichier User_Setup pour le panneau 1,8 pouces ST7735 128x160

Code : Tout sélectionner

// Configuration TFT_eSPI pour ecran ST7735 1,8" (128x160) sur ESP32 DevKit
// A copier/remplacer dans : Documents/Arduino/libraries/TFT_eSPI/User_Setup.h

#define ST7735_DRIVER

// IMPORTANT : il existe plusieurs variantes du ST7735 selon le fabricant
// (couleur de l'etiquette collee sur la nappe du panneau, "tab color").
// Une seule ligne doit etre active parmi celles-ci - decommente au choix,
// en testant si besoin (voir explications ci-dessous) :
#define ST7735_BLACKTAB
// #define ST7735_GREENTAB
// #define ST7735_GREENTAB2
// #define ST7735_GREENTAB3
// #define ST7735_GREENTAB128   // pour certains modules 128x128
// #define ST7735_REDTAB
// #define ST7735_INITB

// Dimensions natives du panneau (portrait) - NE PAS mettre 160x128 ici,
// le mode paysage se regle par tft.setRotation() dans le sketch, pas ici
#define TFT_WIDTH  128
#define TFT_HEIGHT 160

#define TFT_RGB_ORDER TFT_RGB   // a inverser en TFT_BGR si les couleurs sont fausses

#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS   5
#define TFT_DC   2
#define TFT_RST  4
#define TFT_BL   15   // optionnel, laisser -1 si BLK cablé directement en 3.3V

#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#define SMOOTH_FONT

#define SPI_FREQUENCY  20000000
Le fichier User_Setup pour le pannneau 2 pouces ST7789 240x320

Code : Tout sélectionner

// Configuration TFT_eSPI pour écran ST7789 2" (240x320) sur ESP32 DevKit
// A copier/remplacer dans : Documents/Arduino/libraries/TFT_eSPI/User_Setup.h

#define ST7789_DRIVER
#define TFT_WIDTH  320
#define TFT_HEIGHT 240

// Décalage nécessaire sur beaucoup de modules ST7789 1.14" (bord non actif du panneau)
#define TFT_RGB_ORDER TFT_BGR

#define TFT_MISO 19   
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS   5
#define TFT_DC   2
#define TFT_RST  4
#define TFT_BL  15   // optionnel, laisser -1 si BLK câblé directement en 3.3V

#define LOAD_GLCD
#define LOAD_FONT2
#define LOAD_FONT4
#define LOAD_FONT6
#define LOAD_FONT7
#define LOAD_FONT8
#define LOAD_GFXFF
#define SMOOTH_FONT

#define SPI_FREQUENCY  27000000
Ils sont à mettre dans le répertoire de la bibliothèque TFT_eSPI avant de compiler le code correspondant au panneau.



Et Le fichier config.txt:

Code : Tout sélectionner

# Fichier de configuration - panneau publicitaire HO
# Une ligne par reglage, format : CLE=VALEUR
# Les lignes commencant par # sont ignorees (commentaires)
# Si ce fichier est absent, des valeurs par defaut raisonnables sont utilisees.

# Duree d'affichage de chaque image, en millisecondes (minimum 500)
INTERVALLE_AFFICHAGE=10000

# Nombre maximum d'images chargees parmi celles trouvees sur la carte (1 a 50)
MAX_FICHIERS=40

# Ordre d'affichage : 0 = sequentiel (ordre alphabetique), 1 = aleatoire
ALEATOIRE=0

# Luminosité 0 à 255 
LUMINOSITE=255

# Transition entre images
TRANSITION=1 
TRANSITION_PAS=4
TRANSITION_DELAI=24

Voilà, c'est très facile à réaliser et on oeut y afficher ce qu'on veut ensuite: j'ai mis des publicités des années 80 mais on peut mettre autre chose.
Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.
Avatar du membre
France patrice_b
2000 posts et +
2000 posts et +
Messages : 3891
Enregistré le : 31 janv. 2025, 14:45
Prénom : Patrice
LOCALISATION : Beziers Herault
Centres d'intérêts : Modelisme HO et HOM ou HOe
Echelle pratiquée : H0
Âge : 65
Pays :
France
France
sept. 2026 15 19:58

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France patrice_b

:bienv: :bienv: :bienv:

Merci Patrick, pour toutes ces explications et le partage !

:merciiii: :merciiii:
Cordialement,

Patrice
Avatar du membre
France Amigorl
Posteur irrécupérable
Posteur irrécupérable
Messages : 989
Enregistré le : 02 févr. 2025, 20:15
Prénom : Rémy
LOCALISATION : Pau
Centres d'intérêts : Digitalisation matériel ancien, construction d'un réseau HO. Arduino, Dccex-Conmmand-Station, Rocrail, Digitalisation pont tournant Jouef, Decodeur accessoires DIY. 3D PLA et débutant SLA.
Echelle pratiquée : H0
Âge : 64
Pays :
France
France
sept. 2026 15 20:04

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France Amigorl

:bienv:

Code Esp32 très enrichissant !

Attention, si le 4 septembre elle enlève le bas, l'écran va se casser la geule ! :mdr3:

Au delà de la plaisanterie, quels souvenirs nostalgiques de mes jeunes années.

Attention.... La publicité va tenir ses promesses... Re :mdr3:
Avatar du membre
France likiki
Site Admin
Site Admin
Messages : 17018
Enregistré le : 27 févr. 2021, 16:11
Prénom : Christian
LOCALISATION : Ile de France
Centres d'intérêts : Le train miniature (H0 3R), l'impression 3D
Echelle pratiquée : H0
Âge : 59
Pays :
France
France
sept. 2026 15 21:16

Re: Abri de quai ou bus avec panneau publicitaire écran TFT – lecteur carte SD et ESP32

Message par France likiki

Cela rend super bien. Je sent que je vais m'en faire un cet hivers.

J'ai les écrans, reste a faire le reste.


:bienv: :bienv: :bienv: :bienv: :bienv: :bienv: :bienv:
Si le Goëland se gratte le gland, il fera mauvais temps,
Si il se gratte le cul, il ne fera pas beau non plus.

Proverbe de là bas.

ImageImageImage

:tirechap:

.
Si vous souhaitez faire un don pour le forum , c'est ici.
Répondre

Retourner vers « Arduino et le train miniature »

Qui est en ligne

Utilisateurs parcourant ce forum : Aucun utilisateur enregistré et 1 invité