Aller au contenu
Mission 2Objectif 2 · Planète Mustaflash

Ce ne sont pas les LEDs que tu cherches

Chevalier du câble DupontApprenti Sith du câble Dupont ★★ 😎 Luminosité à 40 : on ne veut pas aveugler l'Empire.😎 Luminosité à 40 : une balise visible, pas observable depuis une galaxie voisine.

📡 Transmission de l'Alliance · R2-DuPont

« Ce ne sont pas les LEDs que tu cherches. Enfin… si, techniquement, ce sont exactement les LEDs que tu cherches. Tu devras contrôler leur lumière sans transformer l'atelier en phare intergalactique. »

🔊 Écouter le briefing de R2-DuPont

🛰️ Haut commandement · Opération Contre-Circuit

« Les rebelles ciblent maintenant le réseau lumineux. Rétablis les LEDs. Contrôle leur intensité. Le haut commandement rappelle qu'une balise doit être visible… pas observable depuis une galaxie voisine. »

🔊 Écouter le briefing de R2-DuPont

Objectif : passer de la petite LED intégrée à 10 LEDs de couleur du ruban, et les piloter à la manette : un scénario lumineux par bouton, la luminosité et la vitesse aux flèches, des réacteurs qui s'emballent avec R2, et une couleur qui suit le joystick.

🧭 Notions de la mission : La tension · L'intensité · La résistance · La LED · Les circuits · Tableaux, boucles, fonctions · Le temps

Équipement de mission

🧰 Matériel

💻 Logiciels

  • Arduino IDE 2 + cartes « ESP32 + Bluepad32 » (mission 1)
  • Bibliothèque FastLED, version 3.10.3

🌐 Connexion internet

Oui : pour installer FastLED

🔓 Missions à réussir avant

Mission 1 (la manette)

🛠️ Outils

Ciseaux (si on coupe un tronçon), petit tournevis plat

⏱️ Durée

1 h à 1 h 30

Comment ça marche ?

Une LED classique a besoin de sa propre broche : pour 10 LEDs, il en faudrait 10, et pour 300… impossible. Le ruban WS2812B triche : chaque LED contient une minuscule puce, et toutes les LEDs se partagent un seul fil de données. L'ESP32 y envoie une longue liste de couleurs ; la première LED garde la sienne et passe le reste à la suivante, et ainsi de suite. Tout le détail est sur la page du ruban de LEDs.

Côté programme, la bibliothèque FastLED fait le travail difficile : on range les couleurs dans un tableau leds[], une case par LED, puis FastLED.show() envoie tout au ruban.

Étape 1 · Préparer 10 LEDs

On teste 10 LEDs : c'est assez pour de beaux effets, et peu gourmand en courant.

Pourquoi pas tout le ruban ?

Un ruban de 5 m contient des centaines de LEDs, et chaque puce consomme un peu de courant même éteinte (de l'ordre de 1 mA). Tout le ruban branché sur l'USB, c'est déjà plusieurs centaines de mA avant d'allumer quoi que ce soit : l'USB est à la limite. Le calcul est sur la page du ruban.

Deux possibilités :

  1. Repérer, en partant du bout DIN (celui d'où part la flèche imprimée), la 10ᵉ LED.
  2. Juste après elle, couper aux ciseaux sur le repère de découpe (un trait entre deux pastilles cuivrées, souvent marqué d'un petit ciseau).
  3. Ce tronçon de 10 LEDs devient le ruban de test de l'atelier. Le reste du ruban n'est pas perdu : il marche toujours, à partir de son propre bout DIN.

Possible pour un essai court, avec le programme de cette mission qui n'allume que les 10 premières LEDs. Mais tout le ruban reste alimenté : surveille que l'USB ne décroche pas, et ne laisse pas l'ensemble branché longtemps.

Étape 2 · Installer FastLED

  1. Dans l'Arduino IDE : Croquis → Inclure une bibliothèque → Gérer les bibliothèques… (ou l'icône de livres dans la barre de gauche).
  2. Chercher FastLED.
  3. Dans le menu déroulant des versions, choisir 3.10.3, puis installer « FastLED » par Daniel Garcia.

Pourquoi la version 3.10.3, et pas la dernière ?

Depuis la version 3.10.4, FastLED embarque des fonctions audio qui cherchent une bibliothèque de calcul d'Espressif (« esp-dsp »). La carte Bluepad32 n'en fournit qu'une partie, et la compilation échoue avec fatal error: dspm_add.h: No such file or directory. La 3.10.3 n'a pas ce problème, et elle contient tout ce dont l'atelier a besoin.

Si FastLED est déjà installée en version plus récente : même fenêtre, choisir 3.10.3 dans le menu des versions, puis Installer (elle remplace l'autre). Et ne pas accepter la mise à jour quand l'IDE la propose.

Toujours la carte Bluepad32

Comme à la mission 1, la carte à choisir est ESP32 + Bluepad32 Arduino → ESP32 Dev Module.

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

Le bout DIN du ruban a trois contacts, imprimés sur le ruban : 5V, DIN (les données) et GND. Beaucoup de rubans ont à ce bout un petit connecteur à 3 fils : les broches mâles des câbles Dupont s'enfoncent en général dedans. Sinon, on utilise le bornier à vis du support et la plaque d'essai.

Ruban Passe par ESP32 (connecteurs extérieurs du support)
DIN (données) la résistance de 330 Ω, sur la plaque d'essai D5
5V VIN
GND GND
ESP32 (support)D5VINGND330 Ω5VDINGNDle ruban, côté DIN (la flèche part d'ici)données5 Vmasse
Trois fils : les données (D5, à travers la résistance de 330 Ω), le 5 V (VIN) et la masse (GND). Le côté du ruban à brancher est celui d'où part la flèche.
  1. Débrancher l'USB.
  2. Planter la résistance de 330 Ω sur la plaque d'essai, chaque patte dans une colonne différente.
  3. Un câble Dupont de D5 vers la première patte de la résistance.
  4. Un câble de la deuxième patte de la résistance vers le contact DIN du ruban.
  5. Un câble de VIN vers le 5V du ruban.
  6. Un câble de GND vers le GND du ruban.
  7. Relire les trois fils avant de rebrancher l'USB : un 5V sur DIN ou une inversion 5V/GND peut abîmer la première LED.

La résistance : près du ruban

Elle adoucit le signal et protège l'entrée de la première LED. Elle travaille mieux placée du côté du ruban que du côté de la carte.

🎓 Académie : La résistance · Les circuits · La tension

Étape 4 · Premier test, sans manette

Avant d'ajouter la manette, on vérifie le câblage avec un petit programme qui ne fait que ça.

  1. Fichier → Nouveau croquis, coller le programme ci-dessous, enregistrer sous mission2_test_ruban.
  2. Téléverser.
Le programme · cliquer pour l'afficher
// Mission 2 · Test du ruban, sans manette
// Les 3 premières LEDs s'allument en rouge, vert, bleu pendant 3 secondes,
// puis une LED bleue parcourt les 10 LEDs, encore et encore.

#include <FastLED.h>

#define PIN_RUBAN 5
#define NB_LEDS   10
CRGB leds[NB_LEDS];

void setup() {
  FastLED.addLeds<WS2812B, PIN_RUBAN, GRB>(leds, NB_LEDS);
  FastLED.setBrightness(40);              // doucement : 40 sur 255

  leds[0] = CRGB::Red;                    // test des couleurs
  leds[1] = CRGB::Green;
  leds[2] = CRGB::Blue;
  FastLED.show();
  delay(3000);
}

void loop() {
  for (int i = 0; i < NB_LEDS; i++) {
    fill_solid(leds, NB_LEDS, CRGB::Black);   // tout éteint…
    leds[i] = CRGB::Blue;                     // … sauf la LED n° i
    FastLED.show();
    delay(150);
  }
}

Ce que tu dois voir :

  • pendant 3 secondes, la 1ʳᵉ LED en rouge, la 2ᵉ en vert, la 3ᵉ en bleu ;
  • ensuite, un point bleu qui parcourt les 10 LEDs, encore et encore.

Rouge et vert inversés ?

Certains rubans rangent les couleurs dans un autre ordre. Remplace GRB par RGB dans addLeds, téléverse, et vérifie de nouveau.

Étape 5 · Le programme Mustaflash

Qui fait quoi

Commande Effet sur le ruban Barre lumineuse
✕ réacteur : pulsation bleue bleue
○ alerte rouge : une moitié, puis l'autre rouge
△ hyperespace : un point blanc qui file, avec sa traînée blanche
□ arc-en-ciel qui défile verte
R1 extinction presque éteinte
↑ / ↓ luminosité (de 5 à 120)
← / → vitesse des animations (de 25 % à 400 %)
R2 (gâchette) en mode réacteur : plus on appuie, plus il pulse vite et blanchit
Joystick droit tant qu'il est poussé, tout le ruban prend la couleur de sa direction

Le code

  1. Fichier → Nouveau croquis, effacer tout ce qui est écrit.
  2. Déplier le programme ci-dessous et le copier, ou télécharger le fichier.
  3. Enregistrer : mission2_mustaflash.
Le programme · cliquer pour l'afficher
// Mission 2 · Mustaflash : le ruban de LEDs aux commandes de la manette
// Chaque bouton lance un scénario lumineux sur 10 LEDs du ruban WS2812B.
// Flèches : luminosité et vitesse. R2 : les réacteurs s'emballent.
// Joystick droit : la couleur suit sa direction.

#include <Bluepad32.h>
#include <FastLED.h>

// ================= Réglages =================
#define PIN_RUBAN  5             // le fil de données (DIN), via la résistance de 330 Ω
#define NB_LEDS    10            // on ne commande que les 10 premières LEDs
const int LUM_MAX = 120;         // plafond de luminosité (sur 255) : l'USB ne suivrait pas plus
const int MILLIAMPERES_MAX = 400;// FastLED baisse tout seul la luminosité au-delà de ce courant

CRGB leds[NB_LEDS];              // une case par LED : sa couleur
ControllerPtr manette = nullptr;

// ================= Les scénarios =================
// 0 éteint, 1 réacteur, 2 alerte, 3 hyperespace, 4 arc-en-ciel
const int NB_SCENARIOS = 5;
const char* NOMS[NB_SCENARIOS] = {"éteint", "réacteur", "alerte rouge", "hyperespace", "arc-en-ciel"};
int scenario = 0;
int luminosite = 40;             // de 5 à LUM_MAX
int vitesse = 100;               // en % : 100 = normal
unsigned long horloge = 0;       // horloge des animations (avance selon la vitesse)
unsigned long dernierTemps = 0;

// ================= Mémoire des boutons =================
enum { B_CROIX, B_ROND, B_TRIANGLE, B_CARRE, B_R1, B_HAUT, B_BAS, B_GAUCHE, B_DROITE, NB_BOUTONS };
bool avant[NB_BOUTONS];

bool vientDAppuyer(int numero, bool appuye) {   // vrai seulement au moment de l'appui
  bool nouveau = appuye && !avant[numero];
  avant[numero] = appuye;
  return nouveau;
}

// ================= Connexion de la manette =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;               // une seule manette à la fois
  manette = ctl;
  ctl->setColorLED(0, 0, 255);
  Serial.println("🎮 Manette connectée : à toi de jouer !");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    scenario = 0;                               // par prudence, on éteint le ruban
    Serial.println("❌ Manette déconnectée : ruban éteint");
  }
}

void choisir(int s, uint8_t r, uint8_t g, uint8_t b) {
  scenario = s;
  manette->setColorLED(r, g, b);                // la barre lumineuse prend la couleur du scénario
  Serial.printf("Scénario : %s\n", NOMS[s]);
}

// ================= Lecture de la manette =================
void lireManette() {
  if (vientDAppuyer(B_CROIX,    manette->a()))  choisir(1, 0, 80, 255);    // réacteur bleu
  if (vientDAppuyer(B_ROND,     manette->b()))  choisir(2, 255, 0, 0);     // alerte rouge
  if (vientDAppuyer(B_TRIANGLE, manette->y()))  choisir(3, 255, 255, 255); // hyperespace
  if (vientDAppuyer(B_CARRE,    manette->x()))  choisir(4, 0, 255, 0);     // arc-en-ciel
  if (vientDAppuyer(B_R1,       manette->r1())) choisir(0, 20, 20, 20);    // extinction

  uint8_t f = manette->dpad();
  if (vientDAppuyer(B_HAUT, f & DPAD_UP)) {
    luminosite = min(luminosite + 10, LUM_MAX);
    Serial.printf("Luminosité : %d / %d\n", luminosite, LUM_MAX);
  }
  if (vientDAppuyer(B_BAS, f & DPAD_DOWN)) {
    luminosite = max(luminosite - 10, 5);
    Serial.printf("Luminosité : %d / %d\n", luminosite, LUM_MAX);
  }
  if (vientDAppuyer(B_DROITE, f & DPAD_RIGHT)) {
    vitesse = min(vitesse + 25, 400);
    Serial.printf("Vitesse : %d %%\n", vitesse);
  }
  if (vientDAppuyer(B_GAUCHE, f & DPAD_LEFT)) {
    vitesse = max(vitesse - 25, 25);
    Serial.printf("Vitesse : %d %%\n", vitesse);
  }
}

// ================= Les animations =================
void reacteur(unsigned long t) {
  int boost = manette ? manette->throttle() / 4 : 0;          // R2 : 0 à 255
  unsigned long bpm = 30 + boost / 4;                         // battements par minute : R2 accélère
  uint8_t phase = t * bpm * 256UL / 60000UL;                  // où on en est dans le battement
  uint8_t base = 40 + scale8(sin8(phase), 215);               // pulsation douce entre 40 et 255
  fill_solid(leds, NB_LEDS, CRGB(boost / 3, boost / 3, base)); // bleu, qui blanchit avec R2
}

void alerte(unsigned long t) {
  bool phase = (t / 300) % 2;                                 // change toutes les 300 ms
  for (int i = 0; i < NB_LEDS; i++) {
    bool gauche = i < NB_LEDS / 2;
    leds[i] = (gauche == phase) ? CRGB::Red : CRGB::Black;    // une moitié, puis l'autre
  }
}

void hyperespace(unsigned long t) {
  fadeToBlackBy(leds, NB_LEDS, 60);                           // la traînée s'efface doucement
  int pos = (t / 60) % NB_LEDS;                               // le point avance d'une LED / 60 ms
  leds[pos] = CRGB::White;
}

void arcEnCiel(unsigned long t) {
  fill_rainbow(leds, NB_LEDS, t / 10, 255 / NB_LEDS);         // tout l'arc-en-ciel, qui défile
}

void couleurJoystick(int rx, int ry) {
  int angle = atan2(-ry, rx) * 180 / PI;                      // direction du joystick droit
  uint8_t teinte = (angle + 360) % 360 * 255 / 360;           // de 0 à 255 pour FastLED
  fill_solid(leds, NB_LEDS, CHSV(teinte, 255, 255));
}

void afficher() {
  unsigned long maintenant = millis();
  horloge += (maintenant - dernierTemps) * vitesse / 100;
  dernierTemps = maintenant;

  int rx = manette ? manette->axisRX() : 0;
  int ry = manette ? manette->axisRY() : 0;
  if (abs(rx) > 200 || abs(ry) > 200) {
    couleurJoystick(rx, ry);                                  // le joystick droit a la priorité
  } else {
    switch (scenario) {
      case 0: fill_solid(leds, NB_LEDS, CRGB::Black); break;
      case 1: reacteur(horloge); break;
      case 2: alerte(horloge); break;
      case 3: hyperespace(horloge); break;
      case 4: arcEnCiel(horloge); break;
    }
  }
  FastLED.setBrightness(luminosite);
  FastLED.show();                                             // on envoie tout au ruban
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  FastLED.addLeds<WS2812B, PIN_RUBAN, GRB>(leds, NB_LEDS);
  FastLED.setMaxPowerInVoltsAndMilliamps(5, MILLIAMPERES_MAX);
  FastLED.setBrightness(luminosite);
  fill_solid(leds, NB_LEDS, CRGB::Black);
  FastLED.show();

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

void loop() {
  BP32.update();
  if (manette && manette->isConnected()) lireManette();
  afficher();
  delay(10);
}

Comment il est construit

Bloc Son travail
CRGB leds[NB_LEDS] le tableau des couleurs : une case par LED
FastLED.addLeds<WS2812B, PIN_RUBAN, GRB> dit à FastLED quel ruban est branché, sur quelle broche, dans quel ordre de couleurs
FastLED.setMaxPowerInVoltsAndMilliamps(5, 400) garde-fou : si les couleurs demandées dépassent 400 mA, FastLED baisse la luminosité tout seul
lireManette boutons et flèches, avec vientDAppuyer comme à la mission 1
reacteur, alerte, hyperespace, arcEnCiel une fonction par animation : chacune remplit le tableau leds[]
afficher choisit l'animation (switch), règle la luminosité et envoie tout avec FastLED.show()

Ce qu'on apprend ici

  • Un tableau pour piloter le monde réel : chaque case de leds[] est une vraie LED.
  • Les couleurs en chiffres : CRGB(rouge, vert, bleu) de 0 à 255, ou CHSV(teinte, saturation, luminosité) pour tourner autour de la roue des couleurs.
  • Animer sans bloquer : chaque animation calcule son image à partir de l'heure (horloge), puis rend la main. La manette reste lue en permanence.
  • Penser au courant : le plafond LUM_MAX et le garde-fou de FastLED protègent le port USB.

🎓 Académie : Tableaux, boucles, fonctions · Le temps · L'intensité · La LED

Étape 6 · Téléverser et jouer

  1. Téléverser, ouvrir le moniteur série à 115200 bauds.
  2. Coupler la manette : SHARE + PS jusqu'au clignotement rapide.
  3. Le moniteur annonce « Manette connectée » : ✕, ○, △, □… à toi de jouer !
Mustaflash prêt. Maintiens SHARE + PS pour coupler la manette.
🎮 Manette connectée : à toi de jouer !
Scénario : réacteur
Luminosité : 50 / 120
Vitesse : 125 %
Scénario : arc-en-ciel

Option · L'holo-écran

Le ruban, c'est joli… mais combien consomme-t-il ? Avec l'holo-écran de la mission 1 bis, Mustaflash affiche en direct le scénario en cours, un miroir des 10 LEDs, les réglages, et le courant estimé du ruban.

L'écran pendant le scénario réacteur
Le réacteur : 10 LEDs allumées, environ 26 mA.
L'écran pendant l'hyperespace
L'hyperespace : le point blanc et sa traînée, dans le miroir.

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.

Brancher l'écran

Les quatre fils de la mission 1 bis, USB débranché : VCC → 3V3, GND → GND, SCL → D22, SDA → D21. Le ruban garde ses trois fils (D5, VIN, GND) : les deux montages se partagent la ligne − de la plaque d'essai, c'est-à-dire la masse.

ESP32 (support)D21D223V3GNDNATHAN0,96″ · 128 × 64GNDVCCSCLSDAVCC → 3V3 · GND → GNDSCL → D22 · SDA → D21l'ordre des broches changeselon les modèles : lis les noms !
Les mêmes quatre fils qu'à la mission 1 bis, en plus du montage de la mission : aucune de ses broches n'est utilisée par l'écran.

Ce qu'il affiche

Zone Ce qu'on y voit
En haut MUSTAFLASH, et la batterie de la manette (ou « ... » qui clignote tant qu'elle n'est pas connectée)
Au milieu le scénario en grand ; JOYSTICK quand le joystick droit commande la couleur
Les 10 barres le miroir du ruban : une barre par LED, d'autant plus haute que la LED est vive
LUM et VIT la luminosité (jauge, de 5 à 120) et la vitesse des animations
Ruban ~ mA le courant estimé du ruban

Le code

Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission2_mustaflash_ecran. C'est le programme de la mission, avec l'écran en plus : les commandes ne changent pas.

Le programme holo-écran · cliquer pour l'afficher
// Mission 2 · Mustaflash, version holo-écran
// Le même programme que Mustaflash, avec en plus l'écran OLED qui affiche :
// le scénario en cours, un « miroir » des 10 LEDs, la luminosité, la vitesse
// et le courant estimé du ruban. Sans écran branché, tout marche quand même.

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

// ================= Réglages =================
#define PIN_RUBAN  5             // le fil de données (DIN), via la résistance de 330 Ω
#define NB_LEDS    10            // on ne commande que les 10 premières LEDs
const int LUM_MAX = 120;         // plafond de luminosité (sur 255) : l'USB ne suivrait pas plus
const int MILLIAMPERES_MAX = 400;// FastLED baisse tout seul la luminosité au-delà de ce courant

CRGB leds[NB_LEDS];              // une case par LED : sa couleur
ControllerPtr manette = nullptr;

// ================= Les scénarios =================
// 0 éteint, 1 réacteur, 2 alerte, 3 hyperespace, 4 arc-en-ciel
const int NB_SCENARIOS = 5;
const char* NOMS[NB_SCENARIOS] = {"éteint", "réacteur", "alerte rouge", "hyperespace", "arc-en-ciel"};
const char* NOMS_ECRAN[NB_SCENARIOS] = {"ETEINT", "REACTEUR", "ALERTE !", "HYPERESPACE", "ARC-EN-CIEL"};  // sans accents
bool joystickActif = false;      // vrai quand le joystick droit commande la couleur
int scenario = 0;
int luminosite = 40;             // de 5 à LUM_MAX
int vitesse = 100;               // en % : 100 = normal
unsigned long horloge = 0;       // horloge des animations (avance selon la vitesse)
unsigned long dernierTemps = 0;

// ================= Mémoire des boutons =================
enum { B_CROIX, B_ROND, B_TRIANGLE, B_CARRE, B_R1, B_HAUT, B_BAS, B_GAUCHE, B_DROITE, NB_BOUTONS };
bool avant[NB_BOUTONS];

bool vientDAppuyer(int numero, bool appuye) {   // vrai seulement au moment de l'appui
  bool nouveau = appuye && !avant[numero];
  avant[numero] = appuye;
  return nouveau;
}

// ================= Connexion de la manette =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;               // une seule manette à la fois
  manette = ctl;
  ctl->setColorLED(0, 0, 255);
  Serial.println("🎮 Manette connectée : à toi de jouer !");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    scenario = 0;                               // par prudence, on éteint le ruban
    Serial.println("❌ Manette déconnectée : ruban éteint");
  }
}

void choisir(int s, uint8_t r, uint8_t g, uint8_t b) {
  scenario = s;
  manette->setColorLED(r, g, b);                // la barre lumineuse prend la couleur du scénario
  Serial.printf("Scénario : %s\n", NOMS[s]);
}

// ================= Lecture de la manette =================
void lireManette() {
  if (vientDAppuyer(B_CROIX,    manette->a()))  choisir(1, 0, 80, 255);    // réacteur bleu
  if (vientDAppuyer(B_ROND,     manette->b()))  choisir(2, 255, 0, 0);     // alerte rouge
  if (vientDAppuyer(B_TRIANGLE, manette->y()))  choisir(3, 255, 255, 255); // hyperespace
  if (vientDAppuyer(B_CARRE,    manette->x()))  choisir(4, 0, 255, 0);     // arc-en-ciel
  if (vientDAppuyer(B_R1,       manette->r1())) choisir(0, 20, 20, 20);    // extinction

  uint8_t f = manette->dpad();
  if (vientDAppuyer(B_HAUT, f & DPAD_UP)) {
    luminosite = min(luminosite + 10, LUM_MAX);
    Serial.printf("Luminosité : %d / %d\n", luminosite, LUM_MAX);
  }
  if (vientDAppuyer(B_BAS, f & DPAD_DOWN)) {
    luminosite = max(luminosite - 10, 5);
    Serial.printf("Luminosité : %d / %d\n", luminosite, LUM_MAX);
  }
  if (vientDAppuyer(B_DROITE, f & DPAD_RIGHT)) {
    vitesse = min(vitesse + 25, 400);
    Serial.printf("Vitesse : %d %%\n", vitesse);
  }
  if (vientDAppuyer(B_GAUCHE, f & DPAD_LEFT)) {
    vitesse = max(vitesse - 25, 25);
    Serial.printf("Vitesse : %d %%\n", vitesse);
  }
}

// ================= Les animations =================
void reacteur(unsigned long t) {
  int boost = manette ? manette->throttle() / 4 : 0;          // R2 : 0 à 255
  unsigned long bpm = 30 + boost / 4;                         // battements par minute : R2 accélère
  uint8_t phase = t * bpm * 256UL / 60000UL;                  // où on en est dans le battement
  uint8_t base = 40 + scale8(sin8(phase), 215);               // pulsation douce entre 40 et 255
  fill_solid(leds, NB_LEDS, CRGB(boost / 3, boost / 3, base)); // bleu, qui blanchit avec R2
}

void alerte(unsigned long t) {
  bool phase = (t / 300) % 2;                                 // change toutes les 300 ms
  for (int i = 0; i < NB_LEDS; i++) {
    bool gauche = i < NB_LEDS / 2;
    leds[i] = (gauche == phase) ? CRGB::Red : CRGB::Black;    // une moitié, puis l'autre
  }
}

void hyperespace(unsigned long t) {
  fadeToBlackBy(leds, NB_LEDS, 60);                           // la traînée s'efface doucement
  int pos = (t / 60) % NB_LEDS;                               // le point avance d'une LED / 60 ms
  leds[pos] = CRGB::White;
}

void arcEnCiel(unsigned long t) {
  fill_rainbow(leds, NB_LEDS, t / 10, 255 / NB_LEDS);         // tout l'arc-en-ciel, qui défile
}

void couleurJoystick(int rx, int ry) {
  int angle = atan2(-ry, rx) * 180 / PI;                      // direction du joystick droit
  uint8_t teinte = (angle + 360) % 360 * 255 / 360;           // de 0 à 255 pour FastLED
  fill_solid(leds, NB_LEDS, CHSV(teinte, 255, 255));
}

void afficher() {
  unsigned long maintenant = millis();
  horloge += (maintenant - dernierTemps) * vitesse / 100;
  dernierTemps = maintenant;

  int rx = manette ? manette->axisRX() : 0;
  int ry = manette ? manette->axisRY() : 0;
  joystickActif = abs(rx) > 200 || abs(ry) > 200;
  if (joystickActif) {
    couleurJoystick(rx, ry);                                  // le joystick droit a la priorité
  } else {
    switch (scenario) {
      case 0: fill_solid(leds, NB_LEDS, CRGB::Black); break;
      case 1: reacteur(horloge); break;
      case 2: alerte(horloge); break;
      case 3: hyperespace(horloge); break;
      case 4: arcEnCiel(horloge); break;
    }
  }
  FastLED.setBrightness(luminosite);
  FastLED.show();                                             // on envoie tout au ruban
}

// ================= L'holo-écran =================
Adafruit_SSD1306 ecran(128, 64, &Wire, -1);
bool ecranPret = false;            // faux si l'écran n'est pas branché : le reste marche quand même
unsigned long derniereImage = 0;
const int PERIODE_ECRAN = 100;     // une image toutes les 100 ms : 10 images par seconde

void demarrerEcran() {
  Wire.begin(21, 22);              // SDA sur D21, SCL sur D22
  ecranPret = ecran.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  if (!ecranPret) {
    Serial.println("📟 Pas d'écran trouvé : on continue sans lui.");
    return;
  }
  ecran.setTextColor(SSD1306_WHITE);
  ecran.setTextWrap(false);
}

// 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);
}

// Le titre dans la bande jaune (lignes 0 à 15), avec la batterie de la manette à droite (ou « ... » qui clignote sans manette)
void dessinerTitre(const char* titre) {
  ecran.setTextSize(1);
  ecran.setCursor(0, 3);
  ecran.print(titre);
  if (manette && manette->battery() > 0) dessinerJauge(100, 3, 28, manette->battery(), 255);
  else if (!manette && (millis() / 500) % 2) { ecran.setCursor(110, 3); ecran.print("..."); }
  ecran.drawFastHLine(0, 13, 128, SSD1306_WHITE);  // la bande jaune s'arrête à la ligne 15
}

void mettreAJourEcran() {
  if (!ecranPret || millis() - derniereImage < PERIODE_ECRAN) return;
  derniereImage = millis();
  ecran.clearDisplay();
  dessinerTitre("MUSTAFLASH");

  // Le nom du scénario, en grand s'il tient sur la largeur (10 lettres de 12 pixels)
  const char* nom = joystickActif ? "JOYSTICK" : NOMS_ECRAN[scenario];
  int taille = strlen(nom) <= 10 ? 2 : 1;
  ecran.setTextSize(taille);
  ecran.setCursor((128 - strlen(nom) * 6 * taille) / 2, taille == 2 ? 16 : 20);   // centré
  ecran.print(nom);

  // Le miroir du ruban : une barre par LED, d'autant plus haute que la LED est vive
  for (int i = 0; i < NB_LEDS; i++) {
    uint8_t vive = max(leds[i].r, max(leds[i].g, leds[i].b));   // sa couleur la plus forte
    int hauteur = 1 + vive * 10 / 255;
    ecran.fillRect(5 + i * 12, 45 - hauteur, 9, hauteur, SSD1306_WHITE);
  }

  // Luminosité et vitesse
  ecran.setTextSize(1);
  ecran.setCursor(0, 47);
  ecran.print("LUM");
  dessinerJauge(20, 47, 44, luminosite, LUM_MAX);
  ecran.setCursor(72, 47);
  ecran.printf("VIT %d%%", vitesse);

  // Le courant estimé : FastLED connaît la consommation de chaque couleur
  uint32_t milliwatts = calculate_unscaled_power_mW(leds, NB_LEDS) * luminosite / 255;
  ecran.setCursor(0, 56);
  ecran.printf("Ruban ~%d mA", (int)(milliwatts / 5));   // P = U x I, donc I = P / 5 V
  ecran.display();
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  FastLED.addLeds<WS2812B, PIN_RUBAN, GRB>(leds, NB_LEDS);
  FastLED.setMaxPowerInVoltsAndMilliamps(5, MILLIAMPERES_MAX);
  FastLED.setBrightness(luminosite);
  fill_solid(leds, NB_LEDS, CRGB::Black);
  FastLED.show();

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

void loop() {
  BP32.update();
  if (manette && manette->isConnected()) lireManette();
  afficher();
  mettreAJourEcran();
  delay(10);
}

Ce qui change par rapport au programme de base

Ajout Son travail
#include <Wire.h>, <Adafruit_GFX.h>, <Adafruit_SSD1306.h> les bibliothèques de l'écran, déjà installées à la mission 1 bis
demarrerEcran(), dans setup() démarre le bus I2C et l'écran ; s'il n'est pas branché, ecranPret reste faux et le programme continue sans lui
mettreAJourEcran(), dans loop() redessine l'écran, mais toutes les 100 ms seulement (10 images par seconde)
dessinerTitre(), dessinerJauge() les petites fonctions de dessin, les mêmes dans toutes les missions
NOMS_ECRAN[] les noms des scénarios sans accents, pour la police de l'écran
joystickActif retient si le joystick droit commande la couleur, pour l'afficher
calculate_unscaled_power_mW(leds, NB_LEDS) demande à FastLED la puissance des couleurs du tableau leds[], en milliwatts ; le programme la règle selon la luminosité, puis la divise par 5 V pour obtenir des milliampères

Pourquoi seulement 10 images par seconde ?

Envoyer une image à l'écran prend environ 23 ms (le calcul est dans la fiche I2C). Pendant ce temps, le programme ne fait rien d'autre. En redessinant toutes les 100 ms, l'écran ne prend qu'un quart du temps, et la manette et les animations restent réactifs. Pour des chiffres, 10 images par seconde, c'est largement assez.

À essayer · Le courant en direct

  • Compare le courant du réacteur, de l'hyperespace et de l'arc-en-ciel : lequel consomme le plus ? Pourquoi ?
  • Monte la luminosité avec ↑ : le courant suit-il ?
  • FastLED compte environ 16 mA pour le rouge, 11 pour le vert et 15 pour le bleu, par LED à fond : c'est moins que notre règle prudente des 20 mA par couleur (fiche L'intensité). Ce n'est qu'une estimation : le multimètre a le dernier mot.

🎓 Académie : Le bus I2C · Le temps · L'intensité · La puissance

Des points ou une ligne ?

Les LEDs du ruban sont espacées d'environ 2 cm : de près, on voit des points. Pour une ligne continue (un sabre, un néon de cockpit), il faut un diffuseur placé à bonne distance : papier calque, pièces LEGO transparentes… Tout est expliqué sur la page du ruban de LEDs.

Si ça coince

Symptôme Cause probable Solution
Rien ne s'allume mauvais bout du ruban (DOUT au lieu de DIN) brancher le bout d'où part la flèche
Rien ne s'allume un fil mal enfoncé, ou pas de GND commun suivre les trois fils un par un ; les circuits
Le test affiche vert-rouge-bleu ordre des couleurs différent GRB → RGB dans addLeds
Couleurs au hasard, scintillement signal de données trop faible ou parasité fil de données court, résistance près du ruban, luminosité plus basse (voir l'encadré ci-dessous)
La première LED fait n'importe quoi, les autres suivent la première LED reçoit mal le signal même solution ; en dernier recours, sacrifier la première LED (voir l'encadré)
La carte redémarre quand le ruban s'allume trop de courant pour l'USB baisser la luminosité, garder 10 LEDs, ne pas tout mettre en blanc
FastLED.h: No such file or directory bibliothèque non installée étape 2
dspm_add.h: No such file or directory FastLED trop récente (3.10.4 ou plus) pour la carte Bluepad32 installer la version 3.10.3 (étape 2)
Bluepad32.h: No such file or directory mauvaise carte choisie ESP32 + Bluepad32 Arduino → ESP32 Dev Module
Pour les curieux : 3,3 V pour parler à un ruban en 5 V

Le ruban est alimenté en 5 V, et sa fiche technique demande un signal d'au moins 0,7 × 5 V = 3,5 V pour être sûr de comprendre un « 1 ». L'ESP32 n'envoie que 3,3 V : c'est un peu juste en théorie, mais ça fonctionne avec la grande majorité des rubans, surtout avec un fil court.

Si ça ne marche pas chez toi, deux solutions classiques :

  • la LED sacrifiée : alimenter la première LED du ruban un peu plus bas (à travers une diode), pour qu'elle comprenne le 3,3 V et retransmette ensuite un signal propre en 5 V aux suivantes ;
  • un adaptateur de niveau (par exemple un circuit 74AHCT125) entre l'ESP32 et le ruban, qui transforme le 3,3 V en 5 V.

On en parlera ensemble si besoin : chez nous, essayons d'abord simplement.

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 · Le sabre laser

Sur L1 : la lame s'allume LED par LED, puis grésille légèrement, comme un vrai sabre.

Indice

Note l'heure (horloge) au moment de l'appui sur L1. Ensuite, le nombre de LEDs allumées = temps écoulé ÷ 40 ms, sans dépasser NB_LEDS. Pour le grésillement : leds[i].nscale8(random8(200, 255)) baisse un peu, au hasard, la luminosité d'une LED.

Défi 2 · Le scanner du droïde

Sur L2 : un point rouge qui fait l'aller-retour d'un bout à l'autre du ruban, avec une traînée.

Indice

Compte les pas d'un aller-retour complet : sur 10 LEDs, 10 à l'aller et 8 au retour. Avec le reste de la division (%), un compteur peut tourner en boucle de 0 à 17. Il ne reste plus qu'à transformer ce compteur en position.

Défi 3 · Les rivières de lave de Mustafar

Sur SHARE : des flammes orange qui vacillent.

Indice

random8(100, 255) donne un nombre au hasard entre 100 et 255. Une couleur avec beaucoup de rouge et un peu de vert, c'est de l'orange. Pour que ça ne clignote pas trop vite, ne change les flammes que toutes les 80 ms.

Validation

  • Le test du ruban : rouge, vert, bleu, puis le point bleu qui parcourt les 10 LEDs
  • La manette se connecte au programme Mustaflash
  • Les 4 scénarios fonctionnent (✕ ○ △ □), et R1 éteint
  • Les flèches règlent la luminosité et la vitesse
  • R2 fait s'emballer le réacteur, le joystick droit change la couleur
  • Bonus : au moins un défi réussi