top of page

🚉 Tutoriel : Créez votre propre tableau d'affichage de départ connecté avec un ESP32 et un écran TFT

  • Photo du rédacteur: Thomas Nicodeme
    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

  1. Au démarrage, l’ESP32 lance WiFiManager pour que vous puissiez choisir votre réseau Wi-Fi depuis votre téléphone.

  2. Une fois connecté, il ouvre un serveur web local (accessible via l’adresse IP affichée dans le moniteur série).

  3. Depuis cette interface, vous pouvez saisir le nom d’un arrêt CFF (avec autocomplétion grâce à l’API OpenData Transport).

  4. 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


Posts à l'affiche
Posts Récents
Archives
Rechercher par Tags
Retrouvez-nous
  • Facebook Basic Square
  • Twitter Basic Square
  • Google+ Basic Square
  • Facebook - Black Circle
  • LinkedIn - Black Circle
  • Twitter - Black Circle
  • YouTube - Black Circle
  • Instagram - Black Circle

Inscrivez-vous à notre liste de diffusion

Ne manquez aucune actualité

bottom of page