🚉 Tutoriel : Créez votre propre tableau d'affichage de départ connecté avec un ESP32 et un écran TFT
- Thomas Nicodeme
- il y a 5 jours
- 4 min de lecture
Vous en avez marre de rater votre train ?Avec ce projet, nous allons fabriquer un tableau d'affichage de départ temps réel (ou tout autre réseau compatible OpenData.ch), directement sur un petit écran couleur connecté en Wi-Fi.
Ce tutoriel vous guidera étape par étape, du matériel à la programmation, jusqu’à l’affichage final des trains en direct.
🔧 Matériel nécessaire
Élément | Description | Lien indicatif |
🧠 ESP32 Dev Board | Idéalement une IDEASpark ESP32 (16 MB) ou équivalent (Wi-Fi + BLE intégrés) | |
🖥️ Écran TFT 1.9” ST7789 | Résolution 170×320, compatible avec les bibliothèques Adafruit_ST7789 et Adafruit_GFX | |
🔌 Câbles Dupont M/F | Pour connecter l’écran au microcontrôleur | – |
⚡ Alimentation USB 5 V | Pour alimenter l’ESP32 | – |
🧭 Schéma de câblage
Écran TFT ST7789 | ESP32 |
VCC | 3.3 V |
GND | GND |
SCL (CLK) | GPIO 18 |
SDA (MOSI) | GPIO 23 |
DC | GPIO 2 |
CS | GPIO 15 |
RST | GPIO 4 |
BLK | GPIO 32 |
🧩 Bibliothèques à installer dans l’IDE Arduino
Avant de téléverser le code, assurez-vous d’avoir installé ces bibliothèques via Gestionnaire de bibliothèques (Ctrl+Shift+I) :
WiFiManager (pour configurer le Wi-Fi sans coder les identifiants)
Preferences
HTTPClient
ArduinoJson
Adafruit_GFX
Adafruit_ST7789
🌐 Fonctionnement général
Au démarrage, l’ESP32 lance WiFiManager pour que vous puissiez choisir votre réseau Wi-Fi depuis votre téléphone.
Une fois connecté, il ouvre un serveur web local (accessible via l’adresse IP affichée dans le moniteur série).
Depuis cette interface, vous pouvez saisir le nom d’un arrêt CFF (avec autocomplétion grâce à l’API OpenData Transport).
L’écran TFT affiche ensuite la liste des prochains départs en direct : ligne, heure, destination et retard éventuel.
🧠 Le code complet
Voici le code à copier dans un nouveau fichier Afficheur_CFF_ESP32.ino dans votre IDE Arduino :
// === Librairies ===
#include <WiFiManager.h>
#include <Preferences.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
#include <WebServer.h>
// === Définition des broches écran ===
#define LCD_MOSI 23
#define LCD_SCLK 18
#define LCD_CS 15
#define LCD_DC 2
#define LCD_RST 4
#define LCD_BLK 32
Adafruit_ST7789 tft = Adafruit_ST7789(LCD_CS, LCD_DC, LCD_RST);
Preferences preferences;
WebServer server(80);
String stop_name = "Lausanne";
String apiUrl = "https://transport.opendata.ch/v1/stationboard?limit=7&station=";
// Couleurs personnalisées
uint16_t bleuCFF_Royal125 = tft.color565(3, 38, 104);
uint16_t bleuCFF_Royal = tft.color565(6, 52, 139);
uint16_t rougeCFF = tft.color565(235, 0, 0);
void setupScreen() {
pinMode(LCD_BLK, OUTPUT);
digitalWrite(LCD_BLK, HIGH);
tft.init(170, 320);
tft.setRotation(1);
tft.fillScreen(bleuCFF_Royal125);
tft.setFont();
tft.setTextSize(1);
}
void displayDepartures(JsonArray departures) {
tft.fillScreen(bleuCFF_Royal125);
tft.fillRect(0, 0, 320, 30, bleuCFF_Royal);
tft.setTextColor(ST77XX_WHITE);
tft.setTextSize(2);
tft.setCursor(10, 5);
tft.print("CFF - " + stop_name);
tft.setTextSize(1);
tft.setCursor(10, 40); tft.print("Ligne");
tft.setCursor(70, 40); tft.print("Heure");
tft.setCursor(130, 40); tft.print("Destination");
tft.setCursor(230, 40); tft.print("Statut");
int y_start = 60;
int line_height = 20;
for (int i = 0; i < departures.size(); i++) {
JsonObject d = departures[i];
String category = d["category"].as<String>();
String number = d["number"].as<String>();
String time = d["stop"]["departure"].as<String>().substring(11, 16);
String to = d["to"].as<String>();
bool delayed = d["stop"].containsKey("delay");
int delay = delayed ? d["stop"]["delay"].as<int>() : 0;
int y = y_start + i * line_height;
tft.setTextColor(ST77XX_WHITE);
tft.setCursor(10, y); tft.print(category + number);
tft.setCursor(70, y); tft.print(time);
if (to.length() > 15) to = to.substring(0, 15);
tft.setCursor(130, y); tft.print(to);
if (delayed && delay > 1) {
tft.setTextColor(rougeCFF);
tft.setCursor(230, y);
tft.printf("+%d min", delay);
} else {
tft.setTextColor(0x07E0);
tft.setCursor(230, y);
tft.print("À l'heure");
}
}
}
void fetchAndDisplay() {
HTTPClient http;
String url = apiUrl + stop_name;
http.begin(url);
int code = http.GET();
if (code == 200) {
String payload = http.getString();
DynamicJsonDocument doc(8192);
deserializeJson(doc, payload);
JsonArray departures = doc["stationboard"].as<JsonArray>();
displayDepartures(departures);
}
http.end();
}
void savePrefs() {
preferences.begin("cfg", false);
preferences.putString("stop_name", stop_name);
preferences.end();
}
void loadPrefs() {
preferences.begin("cfg", true);
stop_name = preferences.getString("stop_name", stop_name);
preferences.end();
}
void handleSearchStops() {
if (!server.hasArg("q")) { server.send(400, "application/json", "[]"); return; }
String query = server.arg("q");
if (query.length() < 3) { server.send(200, "application/json", "[]"); return; }
HTTPClient http;
String url = "https://transport.opendata.ch/v1/locations?query=" + query;
http.begin(url);
int code = http.GET();
if (code != 200) { server.send(500, "application/json", "[]"); http.end(); return; }
String payload = http.getString();
http.end();
DynamicJsonDocument doc(8192);
deserializeJson(doc, payload);
JsonArray stations = doc["stations"].as<JsonArray>();
String json = "[";
for (int i = 0; i < stations.size(); i++) {
if (i > 0) json += ",";
json += "\"" + stations[i]["name"].as<String>() + "\"";
}
json += "]";
server.send(200, "application/json", json);
}
void handleRoot() {
String html = R"rawliteral(
<!DOCTYPE html><html lang='fr'>
<head>
<meta charset='UTF-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<title>Configuration CFF</title>
<style>
body { font-family: sans-serif; background: #f5f5f5; padding: 20px; }
.container { background: white; padding: 20px; border-radius: 8px; max-width: 400px; margin: auto; }
h1 { color: #2d327d; }
label { display: block; margin-top: 10px; }
input[type=text] { width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ccc; }
button { background: #2d327d; color: white; padding: 10px; border: none; border-radius: 4px; margin-top: 20px; }
</style>
<script>
async function searchStops(){
let q = document.getElementById('stop_name').value;
if(q.length < 3){
document.getElementById('suggestions').innerHTML='';
return;
}
let res = await fetch('/search?q='+encodeURIComponent(q));
let arr = await res.json();
let html = '';
arr.forEach(name=>{
html += '<div onclick="selectStop(\\''+name.replace(/'/g,"\\'")+'\\')">'+name+'</div>';
});
document.getElementById('suggestions').innerHTML = html;
}
function selectStop(name){
document.getElementById('stop_name').value = name;
document.getElementById('suggestions').innerHTML = '';
}
</script>
</head>
<body>
<div class='container'>
<h1>Configuration</h1>
<form method='POST' action='/save'>
<label>Nom de l'arrêt</label>
<input type='text' id='stop_name' name='stop_name' placeholder='Lausanne' onkeyup='searchStops()' required>
<div id='suggestions' style='border:1px solid #ccc;'></div>
<button type='submit'>Sauvegarder</button>
</form>
</div>
</body></html>
)rawliteral";
server.send(200, "text/html", html);
}
void handleSave() {
if (server.hasArg("stop_name")) {
stop_name = server.arg("stop_name");
savePrefs();
fetchAndDisplay();
server.send(200, "text/plain", "Sauvegarde ok. Écran mis à jour.");
} else {
server.send(400, "text/plain", "Paramètres manquants");
}
}
void setup() {
Serial.begin(115200);
setupScreen();
WiFiManager wm;
wm.autoConnect("Transport_Display_App");
loadPrefs();
server.on("/", handleRoot);
server.on("/save", HTTP_POST, handleSave);
server.on("/search", handleSearchStops);
server.begin();
fetchAndDisplay();
}
void loop() {
server.handleClient();
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate > 60000) {
fetchAndDisplay();
lastUpdate = millis();
}
}
🌍 Améliorations possibles
Ajouter une carte OpenStreetMap dans l’interface web pour visualiser la localisation de l’arrêt.
Afficher une animation de transition à chaque rafraîchissement sur l’écran TFT.
Permettre la sélection de plusieurs arrêts à faire défiler.
Intégrer la météo du lieu affiché (via OpenWeatherMap).
🎯 Conclusion
Félicitations 🎉 !Vous venez de créer un tableau d'affichage de départ intelligent, totalement autonome et configurable via Wi-Fi.
Ce petit écran est parfait pour poser dans l’entrée, sur un bureau, ou même dans un hall d’entreprise.


































Commentaires