L'alliance rebelle (le grand final)
Grand Maître de la manetteSeigneur noir de la manette ★★★★ 🔵🔴 Bleu d'un côté, rouge de l'autre : choisis ton camp.🔴 Nom de l'opération ennemie : Alliance rebelle. Objectif : empêcher l'assemblage final.
📡 Transmission de l'Alliance · R2-DuPont
« Tout ce que tu auras appris devra fonctionner ensemble : manette, affichage, LEDs, servo, radar, moteur. Plusieurs systèmes, une seule création… et probablement beaucoup trop de câbles Dupont. Si tu réussis, la création prendra vie. Pas parce qu'une prophétie l'avait décidé : parce que tu auras compris comment elle fonctionne. »
🛰️ Haut commandement · Opération Contre-Circuit
« Dernière planète. Nom de l'opération ennemie : Alliance rebelle. Objectif : empêcher l'assemblage final. Ta mission est simple à décrire, beaucoup moins simple à câbler : tous les systèmes devront fonctionner ensemble. Manette, affichage, LEDs, servo, radar, moteur. À partir de ce point : aucune planète suivante. Seulement le résultat final. »
Objectif : réunir tout ce qu'on a appris dans une vraie création pilotée à la manette : une base rebelleimpériale avec sa porte de hangar (servo), son radar qui tourne (moteur et driver) et ses réacteurs lumineux (ruban de LEDs), le tout sur une alimentation externe.
🧭 Notions de la mission : L'alimentation · Les circuits · L'intensité · Le servo · Le moteur et son driver · Le temps
Équipement de mission¶
🧰 Matériel
- Tout le matériel des missions 2, 3 et 5
- Une alimentation 5 V, 2 A minimum (3 A conseillé), à prévoir
- Un adaptateur jack femelle → bornier si l'alimentation a une prise jack
- Plaque d'essai, câbles Dupont
- Les pièces LEGO de la base
- En option : un écran OLED, voir l'holo-écran
💻 Logiciels
- Arduino IDE 2 + cartes « ESP32 + Bluepad32 »
- Bibliothèques ESP32Servo et FastLED (version 3.10.3, voir la mission 2)
🌐 Connexion internet
Non
🛠️ Outils
Petit tournevis plat, multimètre
⏱️ Durée
Un après-midi (et plus pour la construction LEGO !)
Le plan de bataille¶
| Commande | Ce qui se passe |
|---|---|
| ✕ | la porte du hangar s'ouvre en douceur (servo, D18) |
| □ | la porte se ferme |
| R2 / L2 | le radar tourne dans un sens ou dans l'autre (moteur via le driver, D25 / D26) |
| △ | radar automatique, marche / arrêt |
| (automatique) | les réacteurs pulsent en bleu et s'intensifient avec la vitesse du radar (ruban, D5) |
| ○ | alerte rouge : les réacteurs clignotent en rouge, la manette vibre |
| R1 | tout s'arrête |
Étape 1 · L'alimentation de la base¶
Le montage de la mission 5 a déjà séparé le cerveau et les muscles pour le moteur : on continue sur la même lancée. Servo + ruban + moteur, c'est trop pour l'USB : jusqu'à 2 A dans le pire des cas (le calcul est dans la fiche L'alimentation). On sépare donc les rôles :
- l'ESP32 reste sur l'USB de l'ordinateur : il réfléchit, on peut le reprogrammer et lire le moniteur série ;
- les muscles et les lumières (servo, driver, ruban) prennent leur énergie sur l'alimentation 5 V externe.
Les deux règles absolues
- Tous les GND reliés : ESP32, alimentation externe, servo, driver, ruban.
- Le +5 V externe ne va jamais sur VIN de l'ESP32 tant que l'USB est branché.
Étape 2 · Câbler, un sous-système à la fois¶
La règle n°1 du code de l'atelier s'applique plus que jamais : un seul actionneur à la fois. On branche, on teste avec le programme de sa mission, puis on passe au suivant.
| Composant | Signal (depuis l'ESP32) | + (alimentation) | − |
|---|---|---|---|
| Servo de la porte | fil orange sur D18 | fil rouge sur le +5 V externe | fil marron sur la masse commune |
| Ruban (réacteurs) | D5 → 330 Ω → DIN | 5V du ruban sur le +5 V externe | GND du ruban sur la masse commune |
| Driver DRV8833 | D25 → IN1, D26 → IN2, 3V3 → EEP | VCC sur le +5 V externe | GND sur la masse commune |
| ESP32 | (son USB) | un GND de l'ESP32 sur la masse commune |
- Tout débrancher : USB et alimentation externe.
- Sur la plaque d'essai, une ligne + pour le +5 V externe et une ligne − pour la masse commune. Relier un GND de l'ESP32 à cette ligne −.
- Brancher le servo, rebrancher, tester avec le programme de la mission 3. Débrancher.
- Ajouter le ruban, tester avec le programme de la mission 2. Débrancher.
- Ajouter le driver et le moteur, tester avec la mission 5. Débrancher.
- Tout fonctionne séparément ? Place au programme complet.
Le multimètre avant tout
Avant de brancher le premier composant, mesure la tension entre les lignes + et − de la plaque (alimentation externe seule) : environ 5 V, dans le bon sens. Ça évite bien des surprises.
Étape 3 · Construire la base¶
Quelques idées, à adapter avec tes pièces :
- Le hangar : la porte de la mission 3, sur une façade en briques. Deux vantaux avec un seul servo ? Voir les engrenages du manuel du mécano.
- Le radar : une antenne en pièces Technic sur l'axe du moteur, avec un engrenage pour la ralentir (8 dents vers 24 dents : 3 fois plus lent, voir le moteur).
- Les réacteurs : le tronçon de ruban à l'arrière, sous des pièces transparentes bleues pour diffuser la lumière.
Étape 4 · Le programme Coruscâble¶
Le programme réunit les morceaux des missions précédentes : le mouvement en douceur de la porte (mission 3), la commande du moteur (mission 5) et les animations du ruban (mission 2).
- Nouveau croquis, coller le programme ou télécharger le fichier.
- Remplacer
FERMEetOUVERTpar tes angles de la mission 3. - Enregistrer : mission6_coruscable, téléverser.
Le programme · cliquer pour l'afficher
// Mission 6 · Coruscâble : la base rebelle complète
// Porte du hangar (servo), radar (moteur via le driver), réacteurs (ruban de LEDs).
// ✕ ouvrir la porte, □ la fermer, R2 / L2 le radar, △ radar automatique,
// ○ alerte rouge, R1 tout arrêter. Les réacteurs suivent la vitesse du radar.
#include <Bluepad32.h>
#include <ESP32Servo.h>
#include <FastLED.h>
// ================= Branchements =================
const int PIN_SERVO = 18;
#define PIN_RUBAN 5
#define NB_LEDS 10
const int M_IN1 = 25, M_IN2 = 26; // le moteur du radar, via le DRV8833
// ================= Réglages =================
const int FERME = 30, OUVERT = 150; // ← tes angles de la mission 3
const int US_MIN = 500, US_MAX = 2500;
const int VITESSE_PORTE = 60; // degrés par seconde
const int RADAR_AUTO = 120; // vitesse du radar automatique (sur 255)
const int LUM_MAX = 120;
Servo porte;
CRGB leds[NB_LEDS];
ControllerPtr manette = nullptr;
float position = FERME, cible = FERME;
int vitesseRadar = 0; // de -255 à 255
bool radarAuto = false;
bool alerte = false;
unsigned long dernierTemps = 0;
enum { B_CROIX, B_CARRE, B_ROND, B_TRIANGLE, B_R1, NB_BOUTONS };
bool avant[NB_BOUTONS];
bool vientDAppuyer(int numero, bool appuye) {
bool nouveau = appuye && !avant[numero];
avant[numero] = appuye;
return nouveau;
}
// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
if (manette != nullptr) return;
manette = ctl;
ctl->setColorLED(0, 0, 255);
Serial.println("🎮 Manette connectée : la base est à toi, commandant.");
}
void onDisconnectedController(ControllerPtr ctl) {
if (manette == ctl) {
manette = nullptr;
radarAuto = false;
alerte = false;
cible = position;
Serial.println("❌ Manette déconnectée : tout s'arrête.");
}
}
// ================= Les trois sous-systèmes =================
void moteurRadar(int v) {
v = constrain(v, -255, 255);
if (v > 0) { analogWrite(M_IN1, v); analogWrite(M_IN2, 0); }
else if (v < 0) { analogWrite(M_IN1, 0); analogWrite(M_IN2, -v); }
else { analogWrite(M_IN1, 0); analogWrite(M_IN2, 0); }
}
void bougerPorte() {
unsigned long maintenant = millis();
float pas = VITESSE_PORTE * (maintenant - dernierTemps) / 1000.0;
dernierTemps = maintenant;
if (abs(cible - position) <= pas) position = cible;
else position += (cible > position) ? pas : -pas;
porte.writeMicroseconds(US_MIN + (int)(position * (US_MAX - US_MIN) / 270));
}
void reacteurs() {
if (alerte) { // alerte : tout clignote en rouge
bool allume = (millis() / 250) % 2;
fill_solid(leds, NB_LEDS, allume ? CRGB::Red : CRGB::Black);
} else {
int puissance = abs(vitesseRadar); // 0 à 255
uint8_t bleu = 40 + scale8(sin8(millis() / 4), 60) + puissance * 155 / 255;
uint8_t blanc = puissance / 3; // à pleine vitesse, le bleu blanchit
fill_solid(leds, NB_LEDS, CRGB(blanc, blanc, bleu));
}
FastLED.show();
}
// ================= Lecture de la manette =================
void lireManette() {
if (vientDAppuyer(B_CROIX, manette->a())) { cible = OUVERT; Serial.println("✕ Ouverture du hangar"); }
if (vientDAppuyer(B_CARRE, manette->x())) { cible = FERME; Serial.println("□ Fermeture du hangar"); }
if (vientDAppuyer(B_TRIANGLE, manette->y())) { radarAuto = !radarAuto; Serial.println(radarAuto ? "△ Radar automatique" : "△ Radar manuel"); }
if (vientDAppuyer(B_ROND, manette->b())) {
alerte = !alerte;
Serial.println(alerte ? "○ ALERTE ROUGE !" : "○ Fin de l'alerte");
if (alerte) { manette->setColorLED(255, 0, 0); manette->playDualRumble(0, 400, 255, 255); }
else manette->setColorLED(0, 0, 255);
}
if (vientDAppuyer(B_R1, manette->r1())) {
radarAuto = false; alerte = false; cible = position;
manette->setColorLED(0, 0, 255);
Serial.println("R1 : tout s'arrête");
}
int gachettes = manette->throttle() - manette->brake(); // -1020 à 1020
if (abs(gachettes) > 60) {
radarAuto = false;
vitesseRadar = map(gachettes, -1020, 1020, -255, 255);
} else {
vitesseRadar = radarAuto ? RADAR_AUTO : 0;
}
}
// ================= Programme principal =================
void setup() {
Serial.begin(115200);
porte.setPeriodHertz(50);
porte.attach(PIN_SERVO, US_MIN, US_MAX);
porte.writeMicroseconds(US_MIN + (int)(position * (US_MAX - US_MIN) / 270));
pinMode(M_IN1, OUTPUT);
pinMode(M_IN2, OUTPUT);
moteurRadar(0);
FastLED.addLeds<WS2812B, PIN_RUBAN, GRB>(leds, NB_LEDS);
FastLED.setBrightness(LUM_MAX);
FastLED.setMaxPowerInVoltsAndMilliamps(5, 800); // l'alimentation externe en fournit davantage
dernierTemps = millis();
BP32.setup(&onConnectedController, &onDisconnectedController);
BP32.forgetBluetoothKeys(); // à mettre en commentaire une fois que tout marche
Serial.println("Coruscâble prêt. Maintiens SHARE + PS pour coupler la manette.");
}
void loop() {
BP32.update();
if (manette && manette->isConnected()) lireManette();
else vitesseRadar = 0;
bougerPorte();
moteurRadar(vitesseRadar);
reacteurs();
delay(10);
}
Comment il est construit¶
| Bloc | Son travail | Vient de |
|---|---|---|
bougerPorte |
rapproche la porte de sa cible, à vitesse constante | mission 3 |
moteurRadar |
PWM sur IN1 ou IN2 selon le signe de la vitesse | mission 5 |
reacteurs |
pulsation bleue qui s'intensifie avec le radar, ou alerte rouge | mission 2 |
lireManette |
boutons, gâchettes et modes | missions 1 à 5 |
loop |
chaque tour : lire, bouger la porte, piloter le moteur, dessiner les réacteurs |
Ce qu'on apprend ici
- Assembler des briques : chaque sous-système a sa fonction, testée dans sa mission. Le programme final les fait travailler ensemble.
- Le temps partagé : aucun
delay()long, donc la porte bouge, le radar tourne et les réacteurs pulsent en même temps. - L'énergie séparée de l'information : l'ESP32 commande, l'alimentation externe nourrit.
Si le servo tremble quand le moteur tourne
Le servo, le moteur et le ruban utilisent tous les trois des circuits de minuterie de l'ESP32, qui en a un nombre limité. Les bibliothèques se les partagent automatiquement, et ça fonctionne en général très bien. Si on observe un tremblement du servo uniquement quand le moteur tourne, on cherchera ensemble la cause : souvent la masse commune ou un fil de signal trop proche des fils du moteur, parfois un conflit de minuteries.
Option · L'holo-écran¶
La base rebelleimpériale mérite un poste de commandement. Avec l'holo-écran de la mission 1 bis, Coruscâble affiche l'état du hangar, du radar et des réacteurs, et tout l'écran clignote pendant l'alerte rouge.
🔴 Côté obscur ? Dans le programme, remplace dessinerTitre("BASE REBELLE") par dessinerTitre("BASE IMPERIALE"). Sans accent : l'écran les afficherait mal.


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. L'écran se branche sur le 3V3 de l'ESP32, comme d'habitude, jamais sur l'alimentation externe. Son GND rejoint la masse commune de la base.
Le 3V3 sert déjà à EEP ? On le partage
Le 3V3 de l'ESP32 alimente déjà la broche EEP du driver. Pour brancher aussi l'écran : un fil du 3V3 vers une colonne libre de la plaque d'essai (ses 5 trous sont reliés entre eux), puis un fil de cette colonne vers EEP, et un autre vers le VCC de l'écran. Ne jamais utiliser pour ça la ligne + qui reçoit le 5 V de l'alimentation externe : 3,3 V et 5 V ne se mélangent pas.
Ce qu'il affiche¶
| Zone | Ce qu'on y voit |
|---|---|
| En haut | BASE REBELLE, et la batterie de la manette |
| Hangar | une jauge de 0 (fermé) à 100 % (ouvert), et FERME, OUVERT ou « ... » pendant le mouvement |
| Radar | une jauge centrée : à droite un sens, à gauche l'autre ; AUTO en mode automatique |
| Reacteurs | la puissance des réacteurs, en % |
| En bas | « Commandant a bord » quand la manette est connectée |
| Alerte rouge | tout l'écran clignote, au même rythme que le ruban |
Le code¶
Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission6_coruscable_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 6 · Coruscâble, version holo-écran
// Le même programme que Coruscâble, avec en plus l'écran OLED : le poste de commandement.
// Il affiche l'état du hangar, du radar et des réacteurs, et clignote en cas d'alerte rouge.
// Sans écran branché, tout marche quand même.
#include <Bluepad32.h>
#include <ESP32Servo.h>
#include <FastLED.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
// ================= Branchements =================
const int PIN_SERVO = 18;
#define PIN_RUBAN 5
#define NB_LEDS 10
const int M_IN1 = 25, M_IN2 = 26; // le moteur du radar, via le DRV8833
// ================= Réglages =================
const int FERME = 30, OUVERT = 150; // ← tes angles de la mission 3
const int US_MIN = 500, US_MAX = 2500;
const int VITESSE_PORTE = 60; // degrés par seconde
const int RADAR_AUTO = 120; // vitesse du radar automatique (sur 255)
const int LUM_MAX = 120;
Servo porte;
CRGB leds[NB_LEDS];
ControllerPtr manette = nullptr;
float position = FERME, cible = FERME;
int vitesseRadar = 0; // de -255 à 255
bool radarAuto = false;
bool alerte = false;
unsigned long dernierTemps = 0;
int puissanceReacteurs = 0; // de 0 à 255, pour l'écran
enum { B_CROIX, B_CARRE, B_ROND, B_TRIANGLE, B_R1, NB_BOUTONS };
bool avant[NB_BOUTONS];
bool vientDAppuyer(int numero, bool appuye) {
bool nouveau = appuye && !avant[numero];
avant[numero] = appuye;
return nouveau;
}
// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
if (manette != nullptr) return;
manette = ctl;
ctl->setColorLED(0, 0, 255);
Serial.println("🎮 Manette connectée : la base est à toi, commandant.");
}
void onDisconnectedController(ControllerPtr ctl) {
if (manette == ctl) {
manette = nullptr;
radarAuto = false;
alerte = false;
cible = position;
Serial.println("❌ Manette déconnectée : tout s'arrête.");
}
}
// ================= Les trois sous-systèmes =================
void moteurRadar(int v) {
v = constrain(v, -255, 255);
if (v > 0) { analogWrite(M_IN1, v); analogWrite(M_IN2, 0); }
else if (v < 0) { analogWrite(M_IN1, 0); analogWrite(M_IN2, -v); }
else { analogWrite(M_IN1, 0); analogWrite(M_IN2, 0); }
}
void bougerPorte() {
unsigned long maintenant = millis();
float pas = VITESSE_PORTE * (maintenant - dernierTemps) / 1000.0;
dernierTemps = maintenant;
if (abs(cible - position) <= pas) position = cible;
else position += (cible > position) ? pas : -pas;
porte.writeMicroseconds(US_MIN + (int)(position * (US_MAX - US_MIN) / 270));
}
void reacteurs() {
if (alerte) { // alerte : tout clignote en rouge
bool allume = (millis() / 250) % 2;
fill_solid(leds, NB_LEDS, allume ? CRGB::Red : CRGB::Black);
} else {
int puissance = abs(vitesseRadar); // 0 à 255
puissanceReacteurs = puissance;
uint8_t bleu = 40 + scale8(sin8(millis() / 4), 60) + puissance * 155 / 255;
uint8_t blanc = puissance / 3; // à pleine vitesse, le bleu blanchit
fill_solid(leds, NB_LEDS, CRGB(blanc, blanc, bleu));
}
FastLED.show();
}
// ================= Lecture de la manette =================
void lireManette() {
if (vientDAppuyer(B_CROIX, manette->a())) { cible = OUVERT; Serial.println("✕ Ouverture du hangar"); }
if (vientDAppuyer(B_CARRE, manette->x())) { cible = FERME; Serial.println("□ Fermeture du hangar"); }
if (vientDAppuyer(B_TRIANGLE, manette->y())) { radarAuto = !radarAuto; Serial.println(radarAuto ? "△ Radar automatique" : "△ Radar manuel"); }
if (vientDAppuyer(B_ROND, manette->b())) {
alerte = !alerte;
Serial.println(alerte ? "○ ALERTE ROUGE !" : "○ Fin de l'alerte");
if (alerte) { manette->setColorLED(255, 0, 0); manette->playDualRumble(0, 400, 255, 255); }
else manette->setColorLED(0, 0, 255);
}
if (vientDAppuyer(B_R1, manette->r1())) {
radarAuto = false; alerte = false; cible = position;
manette->setColorLED(0, 0, 255);
Serial.println("R1 : tout s'arrête");
}
int gachettes = manette->throttle() - manette->brake(); // -1020 à 1020
if (abs(gachettes) > 60) {
radarAuto = false;
vitesseRadar = map(gachettes, -1020, 1020, -255, 255);
} else {
vitesseRadar = radarAuto ? RADAR_AUTO : 0;
}
}
// ================= 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
}
// Une jauge centrée sur zéro : se remplit vers la droite (positif) ou vers la gauche (négatif)
void dessinerJaugeCentree(int x, int y, int largeur, int valeur, int maxi) {
int milieu = x + largeur / 2;
ecran.drawRect(x, y, largeur, 7, SSD1306_WHITE);
ecran.drawFastVLine(milieu, y - 2, 11, SSD1306_WHITE); // le repère du zéro
int longueur = (largeur / 2 - 1) * constrain(abs(valeur), 0, maxi) / maxi;
if (valeur > 0) ecran.fillRect(milieu, y + 1, longueur, 5, SSD1306_WHITE);
else ecran.fillRect(milieu - longueur, y + 1, longueur, 5, SSD1306_WHITE);
}
void mettreAJourEcran() {
if (!ecranPret || millis() - derniereImage < PERIODE_ECRAN) return;
derniereImage = millis();
ecran.clearDisplay();
if (alerte) { // alerte : tout l'écran clignote
bool allume = (millis() / 250) % 2;
if (allume) ecran.fillRect(0, 0, 128, 64, SSD1306_WHITE);
ecran.setTextColor(allume ? SSD1306_BLACK : SSD1306_WHITE);
ecran.setTextSize(2);
ecran.setCursor(28, 20);
ecran.print("ALERTE");
ecran.setCursor(34, 40);
ecran.print("ROUGE");
ecran.setTextColor(SSD1306_WHITE);
ecran.display();
return;
}
dessinerTitre("BASE REBELLE");
ecran.setTextSize(1);
// Le hangar : son état, et une jauge de 0 (fermé) à 100 (ouvert)
ecran.setCursor(0, 17);
ecran.print("Hangar");
int ouverture = (position - FERME) * 100 / (OUVERT - FERME);
dessinerJauge(46, 17, 40, ouverture, 100);
ecran.setCursor(91, 17);
if (abs(position - FERME) < 1) ecran.print("FERME");
else if (abs(position - OUVERT) < 1) ecran.print("OUVERT");
else ecran.print("...");
// Le radar : une jauge centrée (un sens ou l'autre), et AUTO s'il tourne tout seul
ecran.setCursor(0, 29);
ecran.print("Radar");
dessinerJaugeCentree(46, 29, 40, vitesseRadar, 255);
ecran.setCursor(91, 29);
ecran.print(radarAuto ? "AUTO" : "");
// Les réacteurs : leur puissance en %
ecran.setCursor(0, 41);
ecran.print("Reacteurs");
dessinerJauge(60, 41, 40, puissanceReacteurs, 255);
ecran.setCursor(104, 41);
ecran.printf("%d%%", puissanceReacteurs * 100 / 255);
// En bas : la manette
ecran.drawFastHLine(0, 52, 128, SSD1306_WHITE);
ecran.setCursor(0, 55);
ecran.print(manette ? "Commandant a bord" : "En attente manette");
ecran.display();
}
// ================= Programme principal =================
void setup() {
Serial.begin(115200);
porte.setPeriodHertz(50);
porte.attach(PIN_SERVO, US_MIN, US_MAX);
porte.writeMicroseconds(US_MIN + (int)(position * (US_MAX - US_MIN) / 270));
pinMode(M_IN1, OUTPUT);
pinMode(M_IN2, OUTPUT);
moteurRadar(0);
FastLED.addLeds<WS2812B, PIN_RUBAN, GRB>(leds, NB_LEDS);
FastLED.setBrightness(LUM_MAX);
FastLED.setMaxPowerInVoltsAndMilliamps(5, 800); // l'alimentation externe en fournit davantage
dernierTemps = millis();
demarrerEcran();
BP32.setup(&onConnectedController, &onDisconnectedController);
BP32.forgetBluetoothKeys(); // à mettre en commentaire une fois que tout marche
Serial.println("Coruscâble prêt. Maintiens SHARE + PS pour coupler la manette.");
}
void loop() {
BP32.update();
if (manette && manette->isConnected()) lireManette();
else vitesseRadar = 0;
bougerPorte();
moteurRadar(vitesseRadar);
reacteurs();
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 |
puissanceReacteurs |
retient la puissance calculée dans reacteurs(), pour l'afficher |
dessinerJaugeCentree() |
une jauge dont le zéro est au milieu, pour le sens du radar |
if (alerte) { … return; } |
pendant l'alerte, l'écran affiche seulement ALERTE ROUGE, en clignotant |
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, la porte, le radar et les réacteurs restent réactifs. Pour des chiffres, 10 images par seconde, c'est largement assez.
À essayer · Le décollage en images
Le défi « séquence de décollage » ci-dessous gagne à s'afficher aussi : un compte à rebours en grands chiffres, comme le défi 3 de la mission 1 bis. À toi de mélanger les deux !
🎓 Académie : Le bus I2C · Le temps · L'alimentation · Tableaux, boucles, fonctions
Si ça coince¶
| Symptôme | Cause probable | Solution |
|---|---|---|
| L'ESP32 redémarre au démarrage du moteur | les muscles sont encore sur l'USB | vérifier que le servo, le driver et le ruban prennent bien le +5 V externe |
| Le servo tremble, le ruban fait n'importe quoi | masse commune manquante | relier un GND de l'ESP32 à la ligne − |
| Rien ne bouge, mais l'ESP32 fonctionne | alimentation externe éteinte ou mal branchée | mesurer 5 V entre + et − de la plaque |
| Un seul sous-système ne marche pas | son câblage | le tester seul, avec le programme de sa mission |
| L'alimentation externe se coupe | trop de courant demandé | alimentation plus puissante, démarrages plus doux |
dspm_add.h: No such file or directory à la compilation |
FastLED trop récente pour la carte Bluepad32 | FastLED en version 3.10.3 (mission 2, étape 2) |
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 · La séquence de décollage¶
OPTIONS lance une séquence automatique : le hangar s'ouvre, puis le radar et les réacteurs montent en puissance pendant 3 secondes, puis 2 secondes à fond avec la manette qui vibre.
Indice
Une variable « étape » et un switch : chaque étape fait son travail, puis passe à la suivante quand c'est le moment (porte arrivée, ou temps écoulé). Surtout pas de delay() : la manette doit rester écoutée.
Défi 2 · Le mode veille¶
Si personne ne touche à la manette pendant 30 secondes, les réacteurs s'éteignent doucement. La moindre commande les rallume.
Indice
Un chrono remis à zéro à chaque commande (bouton, flèche, joystick ou gâchette). Pour l'extinction douce, fadeToBlackBy avec une valeur qui grandit avec le temps passé en veille.
Validation¶
- L'alimentation externe donne bien 5 V, et toutes les masses sont reliées
- Chaque sous-système a été testé seul (porte, réacteurs, radar)
- Le programme complet : la porte, le radar et les réacteurs obéissent à la manette
- L'alerte rouge fonctionne, R1 arrête tout
- Bonus : au moins un défi réussi
🎖️ Grand Maître de la manette
Si tu as coché toutes les cases de toutes les missions, tu as parcouru toute la galaxie de l'atelier : électricité, programmation, LEDs, servos, moteurs et alimentation. Que la Force soit avec toi, Nathan, pour tes prochaines créations !