On a la liste des trains qui doivent partir après l'heure affichée. Avec 6 lignes
Le dernier train de la journée étant à 20h25, le 6e est le premier du matin suivant à 7h15.
IMG_20260725_163155.jpg
Pour le montage je n'aime pas les breadboard et les Dupont et Durand, je préfère souder

Donc j'ai fait un shield qui s'enfiche sur l'ESP et un sticker qui rappelle les couleurs des fils que je vais utiliser, et ainsi aucun risque d'erreur de branchement ni de faux contacts.
Sticker ESP32.docx
Et le shield servira dans le montage final que je ferai sur un support en impression 3D qui regroupera tous les éléments.
IMG_20260725_135216.jpg
Là, c'est le module RTC qui donne l'heure et qui est relié en I2C
https://fr.aliexpress.com/item/1005010120637681.html
IMG_20260725_135220.jpg
L’ESP est un 32D CH9102 38 pins Dev Modele avec interface USB
https://fr.aliexpress.com/item/1005006984793820.html
L’écran est un OLED IPS 1,14 pouces 135x240
https://fr.aliexpress.com/item/1005010444597930.html
Il y a 8 fils à connecter.
Attention il s'alimente en 3,3V et ses broches fonctionnent avec des niveaux logiques eux aussi en 3,3V donc si vous voulez le brancher sur un Arduino, qui envoie du 5V sur ses broches, il faudra utiliser un adaptateur de niveaux bidirectionnel comme celui-ci
https://fr.aliexpress.com/item/1005009377821005.html
VCC : sur le 3.3V de l'ESP32 (ne pas alimenter en 5V).
GND : sur GND (masse).
SCL / SCLK / SCK : brancher sur le GPIO d'horloge SPI matériel, par exemple GPIO 18 (VSPI SCK).
SDA / MOSI / SDI : brancher sur le GPIO de données SPI matériel, par exemple GPIO 23 (VSPI MOSI).
RES / RST : brancher sur un GPIO de votre choix, par exemple GPIO 04.
DC / AO : brancher sur un GPIO de votre choix, par exemple GPIO 02 (Data/Command).
CS : brancher sur un GPIO aussi quelconque GPIO 5
BLK / BL / LED rétroéclairage: peut être relié soit directement au 3.3V et on aura un rétroéclairage fixe , soit sur une broche pour le piloter en PWM par la bibliothèque.
J'ai choisi les broches pour qu'elles soient toutes du même côté.
La bibliothèque que j’ai prise est TFT_eSPI (Bodmer). Il faut l’installer dans l’IDE et il faut aussi reconfigurer son fichier
User_Setup car les affectations de broches ne se font pas dans le code comme avec un Arduino mais dans ce fichier qui se situe dans le répertoire d’installation de la bibliothèque.
Il faut remplacer le fichier d’origine par celui-ci :
Code : Tout sélectionner
// ============================================================
// User_Setup.h pour écran ST7789 135x240 (1,14") - ESP32
// ============================================================
// A COPIER dans : <dossier_libraries>/TFT_eSPI/User_Setup.h
// (remplacer le contenu existant par un copier/coller)
// ============================================================
#define ST7789_DRIVER
#define TFT_WIDTH 135
#define TFT_HEIGHT 240
// --- Câblage SPI matériel ESP32 (VSPI) ---
#define TFT_MOSI 23 // SDA de l'écran
#define TFT_SCLK 18 // SCL de l'écran
#define TFT_CS 5 // CS
#define TFT_DC 2 // DC (Data/Command)
#define TFT_RST 4 // RES
#define TFT_BL 15 // BLK (rétroéclairage) - optionnel si câblé direct au 3.3V
// Décalage nécessaire sur beaucoup de modules 135x240
// (souvent basés sur un contrôleur pensé pour 240x240,
// avec un offset de 40 px en Y et 52 px en X)
#define TFT_RGB_ORDER TFT_BGR // à tester : TFT_RGB si couleurs inversées
#define TFT_BACKLIGHT_ON HIGH // allume le rétroéclairage
#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
#define SPI_READ_FREQUENCY 20000000
Attention la définition des broches est celle que j’ai utilisé mais si vous voulez prendre d’autres broches (on peut mettre ce qu’on veut… ou presque) il faudra modifier la section
// --- Câblage SPI matériel ESP32 (VSPI) ---
Les broches 18 et 23 ne sont pas modifiables (enfin pas directement)
J’ai mis la fréquence au maxi = 27MHz mais on peut la descendre en cas d’anomalie d’affichage liée à un câble trop long. (50 cm environ, au-delà les problèmes peuvent commencer)
Petite vidéo d'essai qui montre le changement de l'affichage quand l'heure de départ du premier train est passée :
On voit que l'écran est très réactif et l'affichage est instantané.
Essai_Ecran_1.mp4
Au niveau du code, c’est une version 1, donc minimaliste et je vais certainement rajouter des fonctionnalités au fur et à mesure.
J'avais ajouté la gestion du rétroéclairage car ça ne marchait pas au début, mais c'est simplement que j'avais oublié la ligne "#define TFT_BACKLIGHT_ON HIGH " dans le User_Setup.h. (ce que j'avais pris pour un bug dans la bibliothèque et qui n'était qu'une erreur de configuration de ma part ...)
Il y a 4 colonnes, un affichage de l’heure et la liste des trains est mise à jour en fonction de l’heure.
On a 19 caractères pour les destinations : attention ce ne sont pas des polices proportionnelles donc les majuscules tiennent plus de place que les minuscules.
Le rafraichissement ne se fait que sur l’horloge pour éviter les clignotements.
Il n’y a rien d’extraordinaire et si vous voulez une explication sur une partie précise du code il suffit de demander.
Le code de cette toute première version qui peut servir de base pour essayer:
Panneau_Gare_V1_11.ino
Code : Tout sélectionner
/*
============================================================
Panneau d'affichage de gare HO
Version 1.11 © Ramasoft Juillet 2026
- Affichage 4 colonnes : ARRIV / DEP / nom / V, horloge centrée
- Bandeau d'en-tête et espacement des lignes calculés dynamiquement
- Ligne séparatrice retracée à chaque rafraîchissement de l'horloge
ESP32 + écran ST7789 135x240 (SPI) + RTC DS3231 (I2C)
============================================================
Bibliothèques nécessaires :
- TFT_eSPI (Bodmer) -> configurer User_Setup.h (fichier fourni à part)
- RTClib (Adafruit)
- Wire (incluse avec l'ESP32)
Câblage :
Écran ST7789 ESP32 couleur fil
------------------------------------------
VCC -> 3.3V rouge
GND -> GND noir
SCL -> GPIO18 (SCK) jaune
SDA -> GPIO23 (MOSI) blanc
RES -> GPIO04 bleu
DC -> GPIO02 gris
CS -> GPIO05 vert
BLK -> GPIO15 orange
RTC DS3231 ESP32
---------------------------
VCC -> 3.3V rouge (ou 5V selon module, la plupart acceptent les deux)
GND -> GND noir
SDA -> GPIO21 blanc
SCL -> GPIO22 jaune
============================================================
*/
#include <SPI.h>
#include <TFT_eSPI.h>
#include <Wire.h>
#include <RTClib.h>
TFT_eSPI tft = TFT_eSPI();
RTC_DS3231 rtc;
// Broches I2C explicites (RTC DS3231)
// Ce sont déjà les valeurs par défaut de l'ESP32, mais les préciser
// ici rend le câblage lisible directement dans le sketch.
#define RTC_SDA 21
#define RTC_SCL 22
// Broche de rétroéclairage, pilotée explicitement
#define PIN_BACKLIGHT 15
// ---------- Structure d'un train ----------
struct Train {
uint8_t heure; // heure de départ de Châteauroux (0-23)
uint8_t minute; // minute de départ (0-59)
const char* nom; // destination affichée en colonne DEPART
const char* arrivee; // heure d'arrivée affichée en colonne ARRIVEE (texte libre, ex "7h12")
const char* quai; // voie affichée en colonne V
};
// ---------- Table d'horaires ----------
// A classer par heure croissante, ça simplifie le filtrage ...
Train horaires[] = {
{ 7, 15, "PARIS Austerlitz", "7h12", "1" },
{ 7, 42, "TOULOUSE Matabiau", "7h39", "3" },
{ 8, 5, "LIMOGES Benedictins", "8h02", "3" },
{ 8, 30, "PARIS Capitole", "8h27", "2" },
{ 9, 20, "TOULOUSE Capitole", "9h17", "1" },
{ 10, 10, "BRIVE Omnibus", "10h07", "5" },
{ 10, 45, "LIMOGES Benedictins", "10h42", "4" },
{ 11, 15, "VAL de L'INDRE", "*****", "6" },
{ 12, 0, "ORLEANS", "11h57", "2" },
{ 13, 30, "PARIS Austerlitz", "13h27", "1" },
{ 14, 10, "MONTAUBAN Omnibus", "14h07", "4" },
{ 14, 35, "VIERZON (NANTES)", "14h32", "3" },
{ 16, 20, "LIMOGES Benedictins", "16h17", "5" },
{ 17, 45, "TOULOUSE Matabiau", "17h42", "3" },
{ 18, 30, "PARIS Austerlitz", "18h27", "1" },
{ 19, 0, "ORLEANS", "18h27", "2" },
{ 20, 15, "PARIS Capitole", "20h12", "1" },
{ 20, 25, "TOULOUSE Capitole", "20h22", "3" },
};
const int NB_TRAINS = sizeof(horaires) / sizeof(horaires[0]);
// Nombre de lignes de trains affichées simultanément à l'écran
const int LIGNES_AFFICHEES = 6;
// Nombre max de caractères affichés pour le nom de destination
// (au-delà, le nom est tronqué - à ajuster selon le rendu réel à l'écran)
const int NOM_MAX_CAR = 19;
// ---------- Couleurs (RGB565) ----------
#define COL_FOND TFT_BLACK
#define COL_ENTETE_BG TFT_NAVY
#define COL_ENTETE_TXT TFT_YELLOW
#define COL_TXT TFT_WHITE
#define COL_DEPART TFT_GREENYELLOW // colonne DEPART (heure de départ locale)
#define COL_ARRIVEE TFT_ORANGE // colonne ARRIVEE dans une couleur distincte
#define COL_QUAI TFT_CYAN
#define COL_HORLOGE TFT_YELLOW
// ---------- Emplacements des colonnes (à ajuster si ça déborde chez toi) ----------
const int X_ARRIVEE = 1; // colonne ARRIVEE (heure d'arrivée)
const int X_NOM = 46; // colonne destination (juste après l'heure de départ)
const int X_DEPART = 180; // colonne DEPART (heure de départ, ex "7h15")
const int X_VOIE = 236; // colonne V (voie/quai), alignée à droite
const int HAUTEUR_MARGE_BASSE = 4; // marge sous le texte de l'horloge avant la ligne séparatrice
// Ces 3 valeurs ne sont plus des constantes figées : elles sont calculées
// dans setup(), une fois que tft.fontHeight() est utilisable, pour que le
// bandeau d'en-tête et l'espacement des lignes s'adaptent automatiquement
// à la vraie hauteur de la police de l'horloge (plus de débordement possible).
int HAUTEUR_ENTETE; // hauteur de la bande d'en-tête
int Y_DEBUT_LISTE; // position Y de la première ligne de train
int HAUTEUR_LIGNE; // hauteur allouée à chaque ligne de train
const int Y_LABELS = 10; // position verticale des libellés d'en-tête
const int Y_HORLOGE_CENTRE = 3; // position verticale de l'horloge (police plus grande)
unsigned long derniereMAJ = 0;
const unsigned long INTERVALLE_MAJ = 1000; // vérifier/rafraîchir l'horloge 1x/seconde
int dernierIndexTrain = -1; // mémorise le 1er train affiché, pour ne redessiner
// la liste que si elle change réellement
void setup() {
Serial.begin(115200);
// --- Rétroéclairage piloté explicitement ---
pinMode(PIN_BACKLIGHT, OUTPUT);
digitalWrite(PIN_BACKLIGHT, HIGH);
// --- Init écran ---
tft.init();
tft.setRotation(1); // 1 = paysage (240x135). Essayer 3 si à l'envers => 270°
tft.fillScreen(COL_FOND);
// --- Calcul dynamique des hauteurs, à partir de la vraie hauteur de police ---
int hPoliceHorloge = tft.fontHeight(4);
HAUTEUR_ENTETE = Y_HORLOGE_CENTRE + hPoliceHorloge + HAUTEUR_MARGE_BASSE;
Y_DEBUT_LISTE = HAUTEUR_ENTETE + 3;
HAUTEUR_LIGNE = (tft.height() - Y_DEBUT_LISTE) / LIGNES_AFFICHEES;
// --- Init RTC ---
Wire.begin(RTC_SDA, RTC_SCL); // explicite : SDA=GPIO21, SCL=GPIO22
if (!rtc.begin()) {
tft.setTextColor(TFT_RED, COL_FOND);
tft.setTextSize(1);
tft.drawString("Erreur RTC DS3231 !", 5, 5, 2);
Serial.println("RTC DS3231 non détecté");
while (1) delay(1000);
}
if (rtc.lostPower()) {
Serial.println("RTC réinitialisée à l'heure de compilation");
// Décommenter la ligne suivante UNE FOIS pour régler l'heure
// (à l'heure de compilation du sketch), puis recommenter
// l'heure est sauvegardée par la pile
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
// --- Dessin initial : tout l'écran une seule fois ---
dessinerEnTeteStatique();
DateTime maintenant = rtc.now();
dessinerHorloge(maintenant);
dernierIndexTrain = trouverPremierTrainFutur(maintenant);
dessinerListeTrains(dernierIndexTrain);
}
void loop() {
if (millis() - derniereMAJ >= INTERVALLE_MAJ) {
derniereMAJ = millis();
DateTime maintenant = rtc.now();
// --- L'horloge est la seule chose repeinte chaque seconde ---
dessinerHorloge(maintenant);
// --- La liste des trains n'est repeinte QUE si elle change réellement ---
int idx = trouverPremierTrainFutur(maintenant);
if (idx != dernierIndexTrain) {
dernierIndexTrain = idx;
dessinerListeTrains(idx);
}
}
}
// ============================================================
// En-tête STATIQUE : libellés de colonnes + ligne séparatrice
// Dessiné une seule fois dans setup(), jamais réécrit ensuite
// ============================================================
void dessinerEnTeteStatique() {
tft.fillRect(0, 0, tft.width(), HAUTEUR_ENTETE, COL_ENTETE_BG);
tft.setTextColor(COL_ENTETE_TXT, COL_ENTETE_BG);
tft.drawString("Arrivee", X_ARRIVEE, Y_LABELS, 2);
tft.drawString("Depart", X_DEPART, Y_LABELS, 2);
tft.drawRightString("V", X_VOIE, Y_LABELS, 2);
// Ligne séparatrice horizontale, dessinée une seule fois
tft.drawFastHLine(0, HAUTEUR_ENTETE, tft.width(), TFT_DARKGREY);
}
// ============================================================
// Horloge : SEULE zone repeinte chaque seconde (pas tout l'écran)
// HH:MM en grand (police 4), secondes en petit à côté (police 2,
// même taille que les colonnes DEPART/ARRIV)
// ============================================================
void dessinerHorloge(DateTime &maintenant) {
char bufHeureMin[6];
sprintf(bufHeureMin, "%02dh%02d", maintenant.hour(), maintenant.minute());
char bufSec[4];
sprintf(bufSec, "%02d", maintenant.second());
// Zone élargie maintenant que ARRIV a été déplacé à x=180
const int xZoneDebut = 56;
const int xZoneFin = 176;
tft.fillRect(xZoneDebut, 0, xZoneFin - xZoneDebut, HAUTEUR_ENTETE, COL_ENTETE_BG);
tft.setTextColor(COL_HORLOGE, COL_ENTETE_BG);
// Largeurs réelles des deux textes, pour bien centrer l'ensemble
int largeurGrand = tft.textWidth(bufHeureMin, 4);
int largeurPetit = tft.textWidth(bufSec, 2);
const int espace = 5;
int largeurTotale = largeurGrand + espace + largeurPetit;
int xDebut = (xZoneDebut + xZoneFin) / 2 - largeurTotale / 2;
// Heure:minute en grand
tft.drawString(bufHeureMin, xDebut, Y_HORLOGE_CENTRE, 4);
// Secondes en petit, alignées vers le bas du grand texte (effet "exposant")
tft.drawString(bufSec, xDebut + largeurGrand + espace, Y_HORLOGE_CENTRE + 5, 2);
// On retrace systématiquement la ligne séparatrice : par sécurité, au cas
// où le texte de l'horloge déborderait légèrement de la zone repeinte
tft.drawFastHLine(0, HAUTEUR_ENTETE, tft.width(), TFT_DARKGREY);
}
// ============================================================
// Liste des prochains trains - dessinée seulement quand elle change
// ============================================================
void dessinerListeTrains(int indexDepart) {
// Efface uniquement la zone de la liste (pas l'en-tête)
tft.fillRect(0, Y_DEBUT_LISTE, tft.width(), tft.height() - Y_DEBUT_LISTE, COL_FOND);
for (int i = 0; i < LIGNES_AFFICHEES; i++) {
int idx = (indexDepart + i) % NB_TRAINS;
int y = Y_DEBUT_LISTE + i * HAUTEUR_LIGNE;
Train &t = horaires[idx];
// --- Colonne DEPART (heure de départ locale, ex "7h15") ---
char bufDepart[6];
sprintf(bufDepart, "%dh%02d", t.heure, t.minute);
tft.setTextColor(COL_DEPART, COL_FOND);
tft.drawString(bufDepart, X_DEPART, y, 2);
// --- Colonne nom de destination (tronqué si trop long) ---
tft.setTextColor(COL_TXT, COL_FOND);
afficherTronque(t.nom, X_NOM, y, NOM_MAX_CAR, 2);
// --- Colonne ARRIVEE (couleur distincte) ---
tft.setTextColor(COL_ARRIVEE, COL_FOND);
tft.drawString(t.arrivee, X_ARRIVEE, y, 2);
// --- Colonne V (voie/quai) ---
tft.setTextColor(COL_QUAI, COL_FOND);
tft.drawRightString(t.quai, X_VOIE, y, 2);
}
}
// ---------- Affiche une chaîne tronquée à maxLen caractères ----------
void afficherTronque(const char* texte, int x, int y, int maxLen, uint8_t font) {
char buf[32];
int len = strlen(texte);
if (len > maxLen) len = maxLen;
memcpy(buf, texte, len);
buf[len] = '\0';
tft.drawString(buf, x, y, font);
}
// ============================================================
// Trouve l'index du premier train dont l'heure de départ
// est >= à l'heure actuelle. Si aucun (fin de journée),
// reboucle sur le premier train du lendemain.
// ============================================================
int trouverPremierTrainFutur(DateTime &maintenant) {
int minutesActuelles = maintenant.hour() * 60 + maintenant.minute();
for (int i = 0; i < NB_TRAINS; i++) {
int minutesTrain = horaires[i].heure * 60 + horaires[i].minute;
if (minutesTrain >= minutesActuelles) {
return i;
}
}
// Tous les trains du jour sont passés -> on reboucle sur le premier
return 0;
}
Reste à faire les supports en impression 3D.

Vous n’avez pas les permissions nécessaires pour voir les fichiers joints à ce message.