Aller au contenu
Mission 1 bisObjectif secondaire · Planète Kaminoled

L'holo-écran s'allume

Chevalier du câble DupontApprenti Sith du câble Dupont ★★ 📟 « Aidez-moi, Nathan Kenobi. Vous êtes mon seul écran. »📟 Escale facultative. Prédiction : tu vas vouloir regarder.

📡 Transmission de l'Alliance · R2-DuPont

« Une escale qui ne figure pas exactement dans le plan de vol : un holo-écran, des données à afficher. Kaminoled n'est pas obligatoire pour poursuivre la mission, mais un vrai explorateur ne refuse jamais une planète bonus. »

🔊 Écouter le briefing de R2-DuPont

🛰️ Haut commandement · Opération Contre-Circuit

« Un signal secondaire vient d'apparaître. Cette planète ne figure pas parmi les sept objectifs principaux, mais elle abrite un holo-écran susceptible de fournir des informations supplémentaires. Statut : escale facultative. Prédiction : tu peux continuer sans t'y rendre… mais tu sais très bien que tu vas vouloir regarder. »

🔊 Écouter le briefing de R2-DuPont

Objectif : brancher un petit écran OLED à l'ESP32, comprendre comment il communique (le bus I2C), y afficher du texte et des dessins, puis en faire le tableau de bord de la manette : joysticks, gâchettes, boutons et batterie en direct, un radar animé, et une page d'infos.

🧭 Notions de la mission : Le bus I2C · Anatomie d'un programme · Tableaux, boucles, fonctions · Le temps

Équipement de mission

🧰 Matériel

  • ESP32 sur son support + câble USB de données
  • Écran OLED 0,96″ (le 2ᵉ pour le défi bonus)
  • Plaque d'essai + 4 câbles Dupont
  • Manette PS4 chargée

💻 Logiciels

  • Arduino IDE 2 + cartes « ESP32 + Bluepad32 »
  • Bibliothèque Adafruit SSD1306 (et ses compagnes)

🌐 Connexion internet

Oui : pour installer les bibliothèques

🔓 Missions à réussir avant

Mission 1 (la manette)

🛠️ Outils

Aucun

⏱️ Durée

1 h

Comment ça marche ?

L'écran fait 128 pixels de large et 64 de haut : 8 192 petits points, chacun allumé ou éteint. Il est de type OLED : chaque pixel produit sa propre lumière, sans éclairage derrière, d'où son noir parfait et son contraste éclatant (tout est expliqué sur la page de l'écran OLED).

Pour lui parler, l'ESP32 utilise le bus I2C : seulement deux fils de communication, SDA (les données) et SCL (l'horloge), sur lesquels plusieurs appareils peuvent se brancher, chacun avec son adresse. Le nôtre répond en général à l'adresse 0x3C. Le fonctionnement est détaillé dans la fiche Le bus I2C.

Côté programme, on ne dessine pas directement sur l'écran : on dessine dans une image en mémoire, puis display() l'envoie d'un coup. Comme au cinéma : on prépare chaque image, puis on la projette.

ESP32l'image en mémoire (1 024 octets)display()par le bus I2C≈ 23 msl'écrandrawLine(), print()… dessinent ici(rien ne change encore à l'écran)
Les fonctions de dessin travaillent dans la mémoire de l'ESP32. L'écran ne change qu'au moment de display().

Étape 1 · Installer les bibliothèques

  1. Croquis → Inclure une bibliothèque → Gérer les bibliothèques…
  2. Chercher Adafruit SSD1306.
  3. Installer « Adafruit SSD1306 » par Adafruit. L'IDE propose d'installer aussi ses compagnes (Adafruit GFX Library, Adafruit BusIO) : répondre « Tout installer ».

Étape 2 · Câbler (USB débranché !)

Le module a 4 broches, dont les noms sont imprimés au-dessus : GND, VCC, SCL et SDA. Leur ordre varie d'un modèle à l'autre : lis bien les noms avant de brancher.

Broche de l'écran ESP32 (connecteurs du support) Rôle
VCC 3V3 l'alimentation, en 3,3 V
GND GND la masse
SCL D22 l'horloge du bus I2C
SDA D21 les données du bus I2C
ESP32 (support)D21D223V3GNDNATHAN0,96″ · 128 × 64GNDVCCSCLSDAVCC → 3V3 · GND → GNDSCL → D22 · SDA → D21l'ordre des broches changeselon les modèles : lis les noms !
Quatre fils seulement : deux pour l'alimentation (3V3 et GND), deux pour le bus I2C (SCL sur D22, SDA sur D21).
  1. Débrancher l'USB.
  2. Planter l'écran sur la plaque d'essai, une broche par colonne.
  3. Relier chaque broche au support avec un câble Dupont, d'après le tableau.
  4. Vérifier deux fois VCC et GND : inversés, ils peuvent abîmer l'écran.

D21 et D22 sont maintenant pris

Le défi « piste d'atterrissage » de la mission 0 utilisait aussi D21 et D22 pour deux de ses LEDs. Pour avoir la piste et l'écran en même temps, déplace ces deux LEDs sur D23 et D4, deux broches libres (et change le tableau LEDS[] du programme en conséquence).

🎓 Académie : Le bus I2C · La tension

Étape 3 · Le scanner I2C : qui est là ?

Avant d'afficher quoi que ce soit, on vérifie que l'ESP32 « voit » l'écran. Ce petit programme appelle toutes les adresses possibles, une par une, et note celles qui répondent. C'est comme frapper à toutes les portes d'un couloir.

  1. Nouveau croquis, coller le programme, enregistrer sous mission1bis_scanner, téléverser.
  2. Ouvrir le moniteur série à 115200 bauds.
Le programme · cliquer pour l'afficher
// Mission 1 bis · Le scanner I2C : qui est branché sur le bus ?
#include <Wire.h>

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);                       // SDA sur D21, SCL sur D22
  delay(500);
  Serial.println("Recherche des appareils I2C...");

  int trouves = 0;
  for (int adresse = 1; adresse < 127; adresse++) {
    Wire.beginTransmission(adresse);        // « il y a quelqu'un à cette adresse ? »
    if (Wire.endTransmission() == 0) {      // 0 = quelqu'un a répondu
      Serial.printf("  ✔ appareil trouvé à l'adresse 0x%02X (%d)\n", adresse, adresse);
      trouves++;
    }
  }
  Serial.printf("%d appareil(s) trouvé(s).\n", trouves);
}

void loop() {}

Ce que tu dois lire :

Recherche des appareils I2C...
  ✔ appareil trouvé à l'adresse 0x3C (60)
1 appareil(s) trouvé(s).

Une autre adresse ?

Si le scanner trouve 0x3D, note-la : il faudra remplacer 0x3C par 0x3D dans les programmes suivants. S'il ne trouve rien, vérifie le câblage : SDA et SCL inversés, c'est l'erreur la plus fréquente. Appuie sur le bouton EN de la carte pour relancer le scan.

Étape 4 · Premier affichage

  1. Téléverser le programme ci-dessous (enregistré sous mission1bis_bonjour).
  2. L'écran affiche « Bonjour » en jaune, et « Nathan ! » en bleu dans un cadre, avec une petite planète.
Bonjour en jaune, Nathan en bleu
Ce que l'écran doit afficher (aperçu calculé à partir du programme).
Le programme · cliquer pour l'afficher
// Mission 1 bis · Premier affichage sur l'holo-écran
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 ecran(128, 64, &Wire, -1);   // 128 × 64 pixels, sur le bus I2C principal

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  if (!ecran.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {   // 0x3C : l'adresse trouvée par le scanner
    Serial.println("Écran introuvable : vérifie le câblage et l'adresse.");
    while (true) delay(100);                         // on s'arrête là
  }

  ecran.clearDisplay();                     // 1. on efface la mémoire de l'image
  ecran.setTextColor(SSD1306_WHITE);
  ecran.setTextSize(2);                     // texte deux fois plus gros : 16 pixels de haut
  ecran.setCursor(22, 0);                   // en haut : pile dans la bande jaune (lignes 0 à 15)
  ecran.println("Bonjour");
  ecran.setCursor(8, 24);                   // en dessous : dans la partie bleue
  ecran.println("Nathan !");
  ecran.drawRect(0, 16, 128, 48, SSD1306_WHITE);     // un cadre autour de la partie bleue
  ecran.fillCircle(110, 50, 8, SSD1306_WHITE);       // une petite planète
  ecran.display();                          // 2. on envoie l'image à l'écran
}

void loop() {}

Les coordonnées de l'écran

Chaque pixel a une adresse : sa colonne x (de 0 à 127, de gauche à droite) et sa ligne y (de 0 à 63, de haut en bas). Le point (0, 0) est en haut à gauche ; y augmente quand on descend, comme les lignes d'un livre.

lignes 0 à 15 : jaunexy(0, 0)(127, 0)(0, 63)(127, 63)drawRect(10, 22, 30, 20)drawCircle(96, 32, 14)un pixel (0, 0) est en haut à gauche ; y augmente vers le bas
128 colonnes, 64 lignes. On donne d'abord x (vers la droite), puis y (vers le bas). Le quadrillage montre des pas de 16 pixels.

Notre écran est bicolore

Les lignes 0 à 15 s'allument en jaune, les lignes 16 à 63 en bleu. Le programme ne choisit pas la couleur : elle dépend seulement de la ligne. Astuce : le titre en haut, dans le jaune, et les dessins à partir de y = 16, sinon ils sont coupés en deux couleurs.

Fonction Ce qu'elle dessine
drawPixel(x, y, couleur) un seul point
drawLine(x1, y1, x2, y2, couleur) un trait entre deux points
drawRect(x, y, largeur, hauteur, couleur) un rectangle vide (fillRect : plein)
drawCircle(x, y, rayon, couleur) un cercle de centre (x, y) (fillCircle : plein)
drawTriangle(x1, y1, x2, y2, x3, y3, couleur) un triangle (fillTriangle : plein)
setCursor(x, y) puis print("…") du texte, à partir du point (x, y)
setTextSize(n) texte n fois plus grand : en taille 1, une lettre fait 6 × 8 pixels
clearDisplay() efface tout (dans la mémoire)
display() envoie l'image à l'écran : sans lui, rien ne change !

Pas d'accents sur l'écran

La police de l'écran ne connaît que les lettres sans accent : « Pret » s'affiche bien, « Prêt » donne un drôle de symbole. Le moniteur série, lui, accepte les accents.

Étape 5 · Le programme Holo-écran

Qui fait quoi

Page Ce qu'elle affiche
Attente (manette pas connectée) « En attente de la manette… » avec des points animés
1 · Cockpit les deux joysticks (un point dans un cercle), les 4 boutons (formes pleines quand on appuie), L2 et R2 en jauges, la batterie en haut à droite
2 · Radar un balayage qui tourne, et trois cibles qui s'allument à son passage
3 · Infos le modèle de manette, la batterie en %, le temps depuis le démarrage

OPTIONS passe à la page suivante.

La page d'attente
En attente de la manette.
La page cockpit
Le cockpit : △ et ○ appuyés, R2 presque à fond.
La page radar
Le radar et sa cible.
La page infos
Les infos : batterie et temps de marche.

Aperçus calculés sur ordinateur, à partir du vrai programme et de la vraie bibliothèque graphique d'Adafruit : c'est ce que l'écran devrait afficher, avec sa bande jaune en haut et le reste en bleu.

Le code

Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission1bis_holoecran.

Le programme · cliquer pour l'afficher
// Mission 1 bis · Kaminoled : l'holo-écran de bord
// Trois pages, qu'on fait défiler avec OPTIONS :
//   1. le cockpit : joysticks, gâchettes, boutons et batterie de la manette, en direct
//   2. le radar : un balayage avec des cibles qui s'allument
//   3. les infos : modèle de manette, batterie, temps écoulé
// Rappel : la police de l'écran ne connaît pas les accents.

#include <Bluepad32.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

Adafruit_SSD1306 ecran(128, 64, &Wire, -1);
ControllerPtr manette = nullptr;

const int NB_PAGES = 3;
int page = 0;
bool avantOptions = false;
unsigned long derniereImage = 0;

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  ctl->setColorLED(0, 120, 255);
  Serial.println("🎮 Manette connectée");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    Serial.println("❌ Manette déconnectée");
  }
}

// ================= Petits outils de dessin =================
// Un joystick : un cercle, et un point qui montre sa position
void dessinerJoystick(int cx, int cy, int x, int y) {
  ecran.drawCircle(cx, cy, 13, SSD1306_WHITE);
  int px = cx + x * 10 / 512;               // -508..512 → -10..10 pixels
  int py = cy + y * 10 / 512;
  ecran.fillCircle(px, py, 3, SSD1306_WHITE);
}

// Une jauge horizontale : un cadre, rempli selon la valeur (0 à maxi)
void dessinerJauge(int x, int y, int largeur, int valeur, int maxi) {
  ecran.drawRect(x, y, largeur, 7, SSD1306_WHITE);
  int plein = (largeur - 2) * constrain(valeur, 0, maxi) / maxi;
  ecran.fillRect(x + 1, y + 1, plein, 5, SSD1306_WHITE);
}

// Les 4 boutons de face : la forme est pleine quand le bouton est appuyé
void dessinerBoutons(int cx, int cy) {
  bool triangle = manette->y(), rond = manette->b(), croix = manette->a(), carre = manette->x();
  // △ en haut
  if (triangle) ecran.fillTriangle(cx, cy - 13, cx - 4, cy - 6, cx + 4, cy - 6, SSD1306_WHITE);
  else          ecran.drawTriangle(cx, cy - 13, cx - 4, cy - 6, cx + 4, cy - 6, SSD1306_WHITE);
  // ○ à droite
  if (rond) ecran.fillCircle(cx + 10, cy, 4, SSD1306_WHITE);
  else      ecran.drawCircle(cx + 10, cy, 4, SSD1306_WHITE);
  // □ à gauche
  if (carre) ecran.fillRect(cx - 14, cy - 4, 8, 8, SSD1306_WHITE);
  else       ecran.drawRect(cx - 14, cy - 4, 8, 8, SSD1306_WHITE);
  // ✕ en bas
  ecran.drawLine(cx - 4, cy + 6, cx + 4, cy + 14, SSD1306_WHITE);
  ecran.drawLine(cx + 4, cy + 6, cx - 4, cy + 14, SSD1306_WHITE);
  if (croix) ecran.drawCircle(cx, cy + 10, 7, SSD1306_WHITE);   // un cercle autour quand appuyé
}

// ================= Les pages =================
void pageAttente() {
  ecran.setTextSize(1);
  ecran.setCursor(0, 3);
  ecran.print("HOLO-ECRAN");                         // le titre, dans la bande jaune
  ecran.drawFastHLine(0, 13, 128, SSD1306_WHITE);
  ecran.setCursor(10, 20);
  ecran.print("En attente de la");
  ecran.setCursor(10, 32);
  ecran.print("manette");
  int points = (millis() / 400) % 4;                 // des points qui s'animent
  for (int i = 0; i < points; i++) ecran.print(".");
  ecran.setCursor(10, 50);
  ecran.print("SHARE + PS");
}

void pageCockpit() {
  ecran.setTextSize(1);
  ecran.setCursor(0, 3);
  ecran.print("COCKPIT");
  int batterie = manette->battery();                 // 0 = inconnue, sinon 1 à 255
  if (batterie > 0) dessinerJauge(88, 3, 40, batterie, 255);
  ecran.drawFastHLine(0, 13, 128, SSD1306_WHITE);   // fin de la bande jaune

  dessinerJoystick(16, 36, manette->axisX(), manette->axisY());
  dessinerJoystick(111, 36, manette->axisRX(), manette->axisRY());
  dessinerBoutons(64, 34);

  ecran.setCursor(36, 56);  ecran.print("L2");
  dessinerJauge(50, 56, 22, manette->brake(), 1020);
  ecran.setCursor(76, 56);  ecran.print("R2");
  dessinerJauge(90, 56, 22, manette->throttle(), 1020);
}

void pageRadar() {
  const int cx = 64, cy = 40, r = 22;               // sous la bande jaune (lignes 16 à 63)
  ecran.setTextSize(1);
  ecran.setCursor(0, 3);
  ecran.print("RADAR");
  ecran.drawCircle(cx, cy, r, SSD1306_WHITE);
  ecran.drawCircle(cx, cy, r / 2, SSD1306_WHITE);
  ecran.drawPixel(cx, cy, SSD1306_WHITE);

  float angle = (millis() % 3000) / 3000.0 * 2 * PI;   // un tour toutes les 3 secondes
  ecran.drawLine(cx, cy, cx + r * cos(angle), cy + r * sin(angle), SSD1306_WHITE);

  // Trois cibles : elles s'allument juste après le passage du balayage
  const float CIBLES[3][2] = {{0.8, 16}, {2.6, 9}, {4.4, 19}};   // {angle, distance}
  for (int i = 0; i < 3; i++) {
    float ecart = angle - CIBLES[i][0];
    if (ecart < 0) ecart += 2 * PI;
    if (ecart < 1.2) {                                // pendant un petit moment après le passage
      int x = cx + CIBLES[i][1] * cos(CIBLES[i][0]);
      int y = cy + CIBLES[i][1] * sin(CIBLES[i][0]);
      ecran.fillCircle(x, y, 2, SSD1306_WHITE);
    }
  }
}

void pageInfos() {
  ecran.setTextSize(1);
  ecran.setCursor(0, 3);
  ecran.print("INFOS");
  ecran.drawFastHLine(0, 13, 128, SSD1306_WHITE);
  ecran.setCursor(0, 18);
  ecran.print("Manette : ");
  ecran.print(manette->getModelName());
  ecran.setCursor(0, 29);
  ecran.print("Batterie : ");
  int b = manette->battery();
  if (b > 0) { ecran.print(b * 100 / 255); ecran.print(" %"); }
  else ecran.print("?");
  unsigned long s = millis() / 1000;
  ecran.setCursor(0, 40);
  ecran.printf("En marche : %02lu:%02lu", s / 60, s % 60);
  ecran.setCursor(0, 53);
  ecran.print("OPTIONS : page +1");
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);                        // SDA sur D21, SCL sur D22
  if (!ecran.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println("Écran introuvable : vérifie le câblage et l'adresse.");
  }
  ecran.clearDisplay();
  ecran.setTextColor(SSD1306_WHITE);
  ecran.display();

  BP32.setup(&onConnectedController, &onDisconnectedController);
  BP32.forgetBluetoothKeys();   // à mettre en commentaire une fois que tout marche
  Serial.println("Holo-écran prêt. Maintiens SHARE + PS pour coupler la manette.");
}

void loop() {
  BP32.update();

  bool connectee = manette && manette->isConnected();
  if (connectee) {
    bool options = manette->miscStart();
    if (options && !avantOptions) {          // OPTIONS vient d'être appuyé
      page = (page + 1) % NB_PAGES;
      Serial.printf("Page %d\n", page + 1);
    }
    avantOptions = options;
  }

  if (millis() - derniereImage >= 40) {      // environ 25 images par seconde
    derniereImage = millis();
    ecran.clearDisplay();
    if (!connectee)     pageAttente();
    else if (page == 0) pageCockpit();
    else if (page == 1) pageRadar();
    else                pageInfos();
    ecran.display();
  }
  delay(5);
}

Comment il est construit

Bloc Son travail
dessinerJoystick, dessinerJauge, dessinerBoutons de petites fonctions de dessin, réutilisées partout
pageAttente, pageCockpit, pageRadar, pageInfos une fonction par page : chacune dessine son image
if (millis() - derniereImage >= 40) une nouvelle image toutes les 40 ms, soit 25 images par seconde, sans bloquer la manette
clearDisplay() … display() chaque image est entièrement redessinée, puis envoyée
page = (page + 1) % NB_PAGES OPTIONS fait tourner les pages en boucle : 0, 1, 2, 0, 1…

Ce qu'on apprend ici

  • Un écran, c'est une grille : tout dessin est une question de coordonnées.
  • Le dessin hors écran : on prépare l'image en mémoire, on l'envoie d'un coup, et rien ne clignote.
  • Des fonctions qui dessinent : dessinerJauge(x, y, largeur, valeur, maxi) sert pour la batterie et pour les deux gâchettes.
  • Le cosinus et le sinus : dans pageRadar, ils transforment un angle en position sur le cercle.

🎓 Académie : Le bus I2C · Tableaux, boucles, fonctions · Le temps

Étape 6 · Téléverser et jouer

  1. Téléverser, puis coupler la manette (SHARE + PS).
  2. L'écran passe de « En attente » au cockpit : bouge les joysticks, appuie sur les gâchettes et les boutons.
  3. OPTIONS : le radar, puis les infos, puis retour au cockpit.

Si ça coince

Symptôme Cause probable Solution
Le scanner ne trouve rien SDA et SCL inversés, ou un fil mal enfoncé SDA → D21, SCL → D22
Le scanner ne trouve rien, écran froid VCC ou GND pas branché vérifier l'alimentation : VCC sur 3V3
« Écran introuvable » mauvaise adresse mettre celle du scanner (0x3C ou 0x3D)
L'écran reste noir, aucune erreur display() oublié toujours finir par ecran.display()
Image décalée, colonne de « neige » à droite l'écran a une puce SH1106, pas SSD1306 bibliothèque Adafruit SH110X : on adaptera le programme ensemble
Des symboles bizarres dans le texte des accents écrire sans accents sur l'écran
Adafruit_SSD1306.h: No such file or directory bibliothèque non installée étape 1

Préserver l'écran

Comme tous les OLED, une image fixe affichée pendant des heures peut finir par laisser une trace. Pour une création qui reste allumée longtemps, on prévoit un mode veille ou des images qui bougent.

Défis bonus

Les solutions sont rangées dans des holocrons scellés : essaie d'abord, l'indice est là si tu bloques, et le code d'accès du maître de l'atelier ouvre la solution.

Défi 1 · Ton nom qui défile

Une 4ᵉ page : NATHAN en grandes lettres, qui défile de droite à gauche comme sur une enseigne.

Indice

Dessine le texte à une position x qui diminue avec le temps (millis()). Quand il est complètement sorti à gauche, il doit revenir à droite : le reste de la division (%) sait faire ça. Et setTextWrap(false) l'empêche de passer à la ligne au bord de l'écran.

Défi 2 · Le deuxième écran

Avec le 2ᵉ écran : le premier garde le cockpit, le second affiche le radar en permanence.

Indice

Les deux écrans ont la même adresse (0x3C) : sur le même bus, ils se mélangeraient. Mais l'ESP32 a deux bus I2C ! Le second (Wire1) peut utiliser les broches RX2 (GPIO 16) pour SDA et TX2 (GPIO 17) pour SCL.

Défi 3 · Le compte à rebours de décollage

△ lance un compte à rebours de 10 secondes en grands chiffres, avec une barre de progression, puis « DECOLLAGE! » qui clignote.

Indice

Note l'heure de l'appui sur △. Le chiffre à afficher, c'est 10 moins le nombre de secondes écoulées. Pour le clignotement final, regarde la fonction invertDisplay(true / false).

L'holo-écran dans les autres missions

Une fois l'écran apprivoisé, il reste branché ! Chaque mission a sa version « holo-écran » de son programme : le ruban et son courant · la porte du hangar · le radar à balayage · les moteurs et leurs volts · le poste de commandement de la base.

Validation

  • Le scanner trouve l'écran, et je connais son adresse
  • « Bonjour Nathan ! » s'affiche
  • Le cockpit montre les joysticks, les boutons, les gâchettes et la batterie en direct
  • OPTIONS fait défiler les trois pages
  • Bonus : au moins un défi réussi