Aller au contenu
Mission 3Objectif 3 · Planète Dagoservo

Je suis ton servo

Chevalier du câble DupontApprenti Sith du câble Dupont ★★ 🗯️ « Fais-le, ou ne le fais pas. Mais ne force pas l'axe à la main. »🗯️ Pas de manipulation manuelle : nous avons des protocoles. Et des engrenages.

📡 Transmission de l'Alliance · R2-DuPont

« Là-bas, tu apprendras une règle ancienne : un servo ne se force pas. On lui donne un angle, on l'observe, et lorsqu'il commence à faire grrrrr, on coupe l'alimentation et on réfléchit à ses choix. »

🔊 Écouter le briefing de R2-DuPont

🛰️ Haut commandement · Opération Contre-Circuit

« Le mécanisme d'orientation est la prochaine cible. Reprends le contrôle du servo. Angle correct. Signal correct. Pas de manipulation manuelle. Nous avons des protocoles. Et accessoirement, des engrenages. »

🔊 Écouter le briefing de R2-DuPont

Objectif : faire bouger le servo Geekservo EF09085 (270°) en douceur, comme une porte de hangar : l'ouvrir et la fermer à la manette, la piloter au joystick, régler sa vitesse, et ne jamais la laisser forcer en bout de course.

🧭 Notions de la mission : Le servo · Le PWM · L'intensité · Anatomie d'un programme · Le temps

Équipement de mission

🧰 Matériel

  • ESP32 sur son support + câble USB de données
  • Geekservo EF09085 (gris, 270°)
  • 3 câbles Dupont mâle-mâle
  • Pièces LEGO Technic pour la porte
  • Manette PS4 chargée
  • En option : un écran OLED, voir l'holo-écran

💻 Logiciels

  • Arduino IDE 2 + cartes « ESP32 + Bluepad32 »
  • Bibliothèque ESP32Servo

🌐 Connexion internet

Oui : pour installer ESP32Servo

🔓 Missions à réussir avant

Mission 1 (la manette)

🛠️ Outils

Petit tournevis plat (bornier du support)

⏱️ Durée

1 h 30, montage LEGO compris

Comment ça marche ?

Un servo, c'est un petit moteur qui sait où il est. À l'intérieur : un moteur, des engrenages qui le ralentissent et le rendent costaud, un capteur qui mesure la position de l'axe, et une petite électronique qui compare sans arrêt « où je suis » et « où on me demande d'aller ». On ne lui dit donc pas « tourne », mais « va à tel angle », et il y va tout seul, puis tient la position.

L'ESP32 lui donne la consigne par une impulsion répétée 50 fois par seconde : plus elle est longue, plus l'angle est grand. Tout est expliqué dans la fiche Le servo, et le Geekservo est présenté dans l'Arsenal.

Étape 1 · Installer ESP32Servo

  1. Croquis → Inclure une bibliothèque → Gérer les bibliothèques…
  2. Chercher ESP32Servo.
  3. Installer « ESP32Servo » par Kevin Harrington (la version 3 ou plus récente, faite pour notre carte).

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

Le Geekservo a un câble à 3 fils terminé par un petit connecteur noir. Des câbles Dupont mâle-mâle s'enfoncent d'un côté dans ce connecteur, de l'autre dans les connecteurs extérieurs du support.

Fil du servo Son rôle ESP32
Orange le signal (les impulsions) D18
Rouge le + : l'alimentation 5 V VIN
Marron le − : la masse GND
ESP32 (support)D18VINGNDconnecteurGeekservoorange : le signalrouge : le + (5 V)marron : le − (masse)
Trois fils : le signal (orange) sur D18, le + (rouge) sur VIN, le − (marron) sur GND.

Vérifie les couleurs de ton servo

Orange, rouge, marron est le code le plus courant pour ces servos. Si les tiens ont d'autres couleurs, le fil du milieu est presque toujours le +, et le plus foncé (marron ou noir) le −.

🎓 Académie : Le servo · La tension · L'intensité

Étape 3 · Premier test : le balayage

Ce premier programme fait aller et venir le servo lentement, sans manette. Il reste prudemment dans la zone de 1 à 2 millisecondes, que tous les servos acceptent.

  1. Nouveau croquis, coller le programme, enregistrer sous mission3_balayage, téléverser.
  2. Le bras du servo (la pièce jaune) doit tourner doucement dans un sens, puis dans l'autre.
Le programme · cliquer pour l'afficher
// Mission 3 · Test du servo : un lent balayage, sans manette
#include <ESP32Servo.h>

const int PIN_SERVO = 18;
const int US_MIN = 500;          // impulsion supposée pour 0°   (en microsecondes)
const int US_MAX = 2500;         // impulsion supposée pour 270°

Servo servo;

void setup() {
  Serial.begin(115200);
  servo.setPeriodHertz(50);                  // 50 impulsions par seconde
  servo.attach(PIN_SERVO, US_MIN, US_MAX);
}

void loop() {
  for (int us = 1000; us <= 2000; us += 10) {   // de 1 ms à 2 ms : la zone sûre
    servo.writeMicroseconds(us);
    delay(20);
  }
  Serial.println("aller terminé");
  for (int us = 2000; us >= 1000; us -= 10) {
    servo.writeMicroseconds(us);
    delay(20);
  }
  Serial.println("retour terminé");
}

writeMicroseconds, c'est quoi ?

servo.writeMicroseconds(1500) envoie une impulsion de 1 500 microsecondes (1,5 ms). C'est le langage « brut » du servo. Le reste de la mission traduit des angles en microsecondes, pour que ce soit plus parlant.

Étape 4 · Calibrer : trouver les bons angles

Chaque montage est différent : il faut trouver l'angle « porte fermée » et l'angle « porte ouverte » de ta porte. Ce programme fait aller le servo à l'angle que tu tapes au clavier.

  1. Téléverser le programme de calibrage ci-dessous.
  2. Ouvrir le moniteur série, 115200 bauds, et choisir « Nouvelle ligne » dans la liste à côté de la vitesse.
  3. Le servo se place au milieu (135°). C'est le moment de monter le bras LEGO sur l'axe, porte à moitié ouverte : ainsi, il reste de la marge des deux côtés.
  4. Taper un angle (par exemple 100), puis Entrée : le servo y va. Recommencer jusqu'à trouver les positions de ta porte.
  5. Noter les valeurs, en gardant 5° de marge avant les butées.
Le programme de calibrage · cliquer pour l'afficher
// Mission 3 · Calibrage : tape un angle dans le moniteur série, le servo y va
#include <ESP32Servo.h>

const int PIN_SERVO = 18;
const int US_MIN = 500;          // impulsion supposée pour 0°
const int US_MAX = 2500;         // impulsion supposée pour 270°

Servo servo;

int versMicrosecondes(int angle) {           // 0..270° → 500..2500 µs
  return map(angle, 0, 270, US_MIN, US_MAX);
}

void setup() {
  Serial.begin(115200);
  servo.setPeriodHertz(50);
  servo.attach(PIN_SERVO, US_MIN, US_MAX);
  servo.writeMicroseconds(versMicrosecondes(135));   // on démarre au milieu
  Serial.println("Calibrage prêt. Tape un angle entre 0 et 270, puis Entrée.");
}

void loop() {
  if (Serial.available()) {
    String ligne = Serial.readStringUntil('\n');      // lit ce qui a été tapé
    ligne.trim();                                     // enlève espaces et retours
    if (ligne.length() == 0) return;                  // ligne vide : on ignore
    int angle = constrain(ligne.toInt(), 0, 270);
    int us = versMicrosecondes(angle);
    servo.writeMicroseconds(us);
    Serial.printf("→ %d° demandés : impulsion de %d µs\n", angle, us);
  }
}
Position Angle noté
Porte fermée (FERME) …
Porte ouverte (OUVERT) …

Le servo bourdonne et force ?

Il est en butée : la porte cogne contre quelque chose, ou on a demandé un angle hors de sa portée. Il consomme alors beaucoup de courant et chauffe. Redemande vite un angle plus proche du milieu, et garde ta marge de 5°.

Pour les curieux : 0° et 270° existent-ils vraiment ?

La plage d'impulsions exacte de notre Geekservo n'est pas publiée par son fabricant. Le programme suppose 500 µs pour 0° et 2 500 µs pour 270°, la convention la plus courante. Si en tapant 0 ou 270 ton servo s'arrête avant, bourdonne, ou ne bouge plus entre deux valeurs proches, c'est la limite réelle : note-la, on adaptera US_MIN et US_MAX.

Étape 5 · Monter la porte

Le manuel du mécano explique le montage : le Geekservo se fixe au châssis par ses trous ronds avec des pins, et l'essieu passe dans un trou en croix du bras de la porte.

Monter au milieu

Si le bras a été monté pendant l'étape 4, servo au milieu, la porte a de la marge dans les deux sens. Si tu le remontes plus tard, relance le calibrage pour le replacer à 135° d'abord.

Étape 6 · Le programme Dagoservo

Qui fait quoi

Commande Effet Barre lumineuse
✕ ouvrir, en douceur orange pendant le mouvement, puis verte
○ fermer, en douceur orange, puis rouge
△ aller à mi-chemin orange
Joystick gauche ← → la « manivelle » : la porte avance tant qu'on pousse
↑ / ↓ vitesse du mouvement (20 à 240 degrés par seconde)
R1 arrêt d'urgence : la porte s'immobilise où elle est
(automatique) petite vibration quand la porte arrive au bout

Le code

  1. Nouveau croquis, coller le programme, ou télécharger le fichier.
  2. Remplacer FERME et OUVERT par tes angles notés à l'étape 4.
  3. Enregistrer : mission3_dagoservo.
Le programme · cliquer pour l'afficher
// Mission 3 · Dagoservo : la porte du hangar, pilotée à la manette
// ✕ ouvre, ○ ferme, △ position du milieu, joystick gauche = manivelle,
// ↑ ↓ vitesse, R1 = arrêt d'urgence. Barre lumineuse : rouge fermé, vert ouvert.

#include <Bluepad32.h>
#include <ESP32Servo.h>

// ================= Réglages (à adapter après le calibrage) =================
const int PIN_SERVO = 18;
const int US_MIN = 500;          // impulsion pour 0°
const int US_MAX = 2500;         // impulsion pour 270°
const int FERME = 30;            // ← ton angle « porte fermée », noté au calibrage
const int OUVERT = 150;          // ← ton angle « porte ouverte »

Servo porte;
ControllerPtr manette = nullptr;

float position = FERME;          // où est la porte maintenant (en degrés)
float cible = FERME;             // où elle doit aller
int vitesse = 60;                // en degrés par seconde
unsigned long dernierTemps = 0;
int etatPrecedent = -1;          // pour ne signaler un changement qu'une fois

// ================= Petits outils =================
enum { B_CROIX, B_ROND, B_TRIANGLE, B_R1, B_HAUT, B_BAS, NB_BOUTONS };
bool avant[NB_BOUTONS];

bool vientDAppuyer(int numero, bool appuye) {
  bool nouveau = appuye && !avant[numero];
  avant[numero] = appuye;
  return nouveau;
}

int versMicrosecondes(float angle) {
  return US_MIN + (int)(angle * (US_MAX - US_MIN) / 270);
}

int angleMin() { return min(FERME, OUVERT); }    // marche même si FERME > OUVERT
int angleMax() { return max(FERME, OUVERT); }

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  etatPrecedent = -1;                            // pour recolorer la barre
  Serial.println("🎮 Manette connectée : la porte est à tes ordres.");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    cible = position;                            // par sécurité, la porte s'arrête
    Serial.println("❌ Manette déconnectée : la porte s'arrête.");
  }
}

// ================= Lecture de la manette =================
void lireManette() {
  if (vientDAppuyer(B_CROIX, manette->a()))    { cible = OUVERT; Serial.println("✕ Ouverture"); }
  if (vientDAppuyer(B_ROND, manette->b()))     { cible = FERME;  Serial.println("○ Fermeture"); }
  if (vientDAppuyer(B_TRIANGLE, manette->y())) { cible = (FERME + OUVERT) / 2; Serial.println("△ Position du milieu"); }
  if (vientDAppuyer(B_R1, manette->r1()))      { cible = position; Serial.println("R1 : arrêt d'urgence"); }

  uint8_t f = manette->dpad();
  if (vientDAppuyer(B_HAUT, f & DPAD_UP)) {
    vitesse = min(vitesse + 20, 240);
    Serial.printf("Vitesse : %d °/s\n", vitesse);
  }
  if (vientDAppuyer(B_BAS, f & DPAD_DOWN)) {
    vitesse = max(vitesse - 20, 20);
    Serial.printf("Vitesse : %d °/s\n", vitesse);
  }

  // Le joystick gauche, comme une manivelle : plus on pousse, plus la cible avance vite
  int x = manette->axisX();                      // de -508 à 512
  if (abs(x) > 80) {
    cible += x / 512.0 * 2.0;                    // jusqu'à 2° par tour de loop()
  }
  cible = constrain(cible, angleMin(), angleMax());   // butées logicielles
}

// ================= Le mouvement en douceur =================
void bouger() {
  unsigned long maintenant = millis();
  float pas = vitesse * (maintenant - dernierTemps) / 1000.0;   // degrés autorisés depuis le dernier tour
  dernierTemps = maintenant;

  if (abs(cible - position) <= pas) position = cible;           // arrivé !
  else if (cible > position) position += pas;
  else position -= pas;

  porte.writeMicroseconds(versMicrosecondes(position));
}

// ================= Ce que voit et sent le pilote =================
void signaler() {
  int etat;                                       // 0 fermée, 1 ouverte, 2 entre les deux
  if (abs(position - FERME) < 1) etat = 0;
  else if (abs(position - OUVERT) < 1) etat = 1;
  else etat = 2;
  if (etat == etatPrecedent || !manette) return;  // rien de nouveau
  etatPrecedent = etat;

  if (etat == 0) {
    manette->setColorLED(255, 0, 0);
    manette->playDualRumble(0, 150, 0, 200);      // « clac » : porte fermée
    Serial.println("Porte fermée");
  } else if (etat == 1) {
    manette->setColorLED(0, 255, 0);
    manette->playDualRumble(0, 150, 0, 200);
    Serial.println("Porte ouverte");
  } else {
    manette->setColorLED(255, 120, 0);            // orange : en mouvement
  }
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  porte.setPeriodHertz(50);
  porte.attach(PIN_SERVO, US_MIN, US_MAX);
  porte.writeMicroseconds(versMicrosecondes(position));
  dernierTemps = millis();

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

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

Comment il est construit

Bloc Son travail
position et cible deux nombres à virgule (float) : où est la porte, et où elle doit aller
lireManette les boutons changent la cible ; le joystick la fait avancer petit à petit
constrain(cible, …) les butées logicielles : la cible ne sort jamais de la plage de la porte
bouger rapproche position de cible, d'un petit pas à chaque tour, selon la vitesse et le temps écoulé
versMicrosecondes traduit un angle (0 à 270°) en impulsion (500 à 2 500 µs)
signaler couleur de la barre lumineuse et vibration, seulement quand l'état change

Ce qu'on apprend ici

  • Séparer l'ordre et le mouvement : les boutons fixent une cible, une autre fonction s'occupe d'y aller en douceur. Le servo n'est jamais brusqué.
  • Le temps écoulé : le pas de chaque tour est calculé avec millis(), donc la vitesse en degrés par seconde reste juste, même si loop() ralentit.
  • Les butées logicielles : le programme protège la mécanique, même si le pilote insiste.

🎓 Académie : Le servo · Le temps · Anatomie d'un programme

Étape 7 · Téléverser et jouer

Dagoservo prêt. Maintiens SHARE + PS pour coupler la manette.
🎮 Manette connectée : la porte est à tes ordres.
Porte fermée
✕ Ouverture
Porte ouverte
Vitesse : 80 °/s
○ Fermeture
Porte fermée

Option · L'holo-écran

Avec l'holo-écran de la mission 1 bis, la porte du hangar a son tableau de bord : son état, un petit dessin de la porte qui bouge en même temps qu'elle, son angle, la cible, la vitesse et la durée de l'impulsion envoyée au servo.

L'écran pendant l'ouverture de la porte
En route vers 150° : le petit rond montre où va la porte.
L'écran porte ouverte
Porte ouverte : 150°, soit une impulsion de 1 611 µs.

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 servo garde ses trois fils (D18, VIN, GND). L'écran se plante sur la plaque d'essai, comme à la mission 1 bis.

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 HANGAR, et la batterie de la manette
En grand FERMEE, OUVERTE ou BOUGE
Angle, Cible où est la porte, et où elle va, en degrés
deg/s, us la vitesse réglée avec ↑ ↓, et la durée de l'impulsion envoyée au servo, en microsecondes (µs)
Le dessin la porte vue de côté : couchée quand elle est fermée, debout quand elle est ouverte ; un petit rond montre sa cible

Le code

Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission3_dagoservo_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 3 · Dagoservo, version holo-écran
// Le même programme que Dagoservo, avec en plus l'écran OLED qui affiche :
// l'état de la porte, un dessin de la porte qui bouge, son angle, la cible,
// la vitesse et la durée de l'impulsion envoyée au servo.
// Sans écran branché, tout marche quand même.

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

// ================= Réglages (à adapter après le calibrage) =================
const int PIN_SERVO = 18;
const int US_MIN = 500;          // impulsion pour 0°
const int US_MAX = 2500;         // impulsion pour 270°
const int FERME = 30;            // ← ton angle « porte fermée », noté au calibrage
const int OUVERT = 150;          // ← ton angle « porte ouverte »

Servo porte;
ControllerPtr manette = nullptr;

float position = FERME;          // où est la porte maintenant (en degrés)
float cible = FERME;             // où elle doit aller
int vitesse = 60;                // en degrés par seconde
unsigned long dernierTemps = 0;
int etatPrecedent = -1;          // pour ne signaler un changement qu'une fois

// ================= Petits outils =================
enum { B_CROIX, B_ROND, B_TRIANGLE, B_R1, B_HAUT, B_BAS, NB_BOUTONS };
bool avant[NB_BOUTONS];

bool vientDAppuyer(int numero, bool appuye) {
  bool nouveau = appuye && !avant[numero];
  avant[numero] = appuye;
  return nouveau;
}

int versMicrosecondes(float angle) {
  return US_MIN + (int)(angle * (US_MAX - US_MIN) / 270);
}

int angleMin() { return min(FERME, OUVERT); }    // marche même si FERME > OUVERT
int angleMax() { return max(FERME, OUVERT); }

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  etatPrecedent = -1;                            // pour recolorer la barre
  Serial.println("🎮 Manette connectée : la porte est à tes ordres.");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    cible = position;                            // par sécurité, la porte s'arrête
    Serial.println("❌ Manette déconnectée : la porte s'arrête.");
  }
}

// ================= Lecture de la manette =================
void lireManette() {
  if (vientDAppuyer(B_CROIX, manette->a()))    { cible = OUVERT; Serial.println("✕ Ouverture"); }
  if (vientDAppuyer(B_ROND, manette->b()))     { cible = FERME;  Serial.println("○ Fermeture"); }
  if (vientDAppuyer(B_TRIANGLE, manette->y())) { cible = (FERME + OUVERT) / 2; Serial.println("△ Position du milieu"); }
  if (vientDAppuyer(B_R1, manette->r1()))      { cible = position; Serial.println("R1 : arrêt d'urgence"); }

  uint8_t f = manette->dpad();
  if (vientDAppuyer(B_HAUT, f & DPAD_UP)) {
    vitesse = min(vitesse + 20, 240);
    Serial.printf("Vitesse : %d °/s\n", vitesse);
  }
  if (vientDAppuyer(B_BAS, f & DPAD_DOWN)) {
    vitesse = max(vitesse - 20, 20);
    Serial.printf("Vitesse : %d °/s\n", vitesse);
  }

  // Le joystick gauche, comme une manivelle : plus on pousse, plus la cible avance vite
  int x = manette->axisX();                      // de -508 à 512
  if (abs(x) > 80) {
    cible += x / 512.0 * 2.0;                    // jusqu'à 2° par tour de loop()
  }
  cible = constrain(cible, angleMin(), angleMax());   // butées logicielles
}

// ================= Le mouvement en douceur =================
void bouger() {
  unsigned long maintenant = millis();
  float pas = vitesse * (maintenant - dernierTemps) / 1000.0;   // degrés autorisés depuis le dernier tour
  dernierTemps = maintenant;

  if (abs(cible - position) <= pas) position = cible;           // arrivé !
  else if (cible > position) position += pas;
  else position -= pas;

  porte.writeMicroseconds(versMicrosecondes(position));
}

// ================= Ce que voit et sent le pilote =================
void signaler() {
  int etat;                                       // 0 fermée, 1 ouverte, 2 entre les deux
  if (abs(position - FERME) < 1) etat = 0;
  else if (abs(position - OUVERT) < 1) etat = 1;
  else etat = 2;
  if (etat == etatPrecedent || !manette) return;  // rien de nouveau
  etatPrecedent = etat;

  if (etat == 0) {
    manette->setColorLED(255, 0, 0);
    manette->playDualRumble(0, 150, 0, 200);      // « clac » : porte fermée
    Serial.println("Porte fermée");
  } else if (etat == 1) {
    manette->setColorLED(0, 255, 0);
    manette->playDualRumble(0, 150, 0, 200);
    Serial.println("Porte ouverte");
  } else {
    manette->setColorLED(255, 120, 0);            // orange : en mouvement
  }
}

// ================= 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("HANGAR");

  // L'état de la porte, en grand
  ecran.setTextSize(2);
  ecran.setCursor(0, 16);
  if (abs(position - FERME) < 1)       ecran.print("FERMEE");
  else if (abs(position - OUVERT) < 1) ecran.print("OUVERTE");
  else                                 ecran.print("BOUGE");

  // Les chiffres
  ecran.setTextSize(1);
  ecran.setCursor(0, 34);
  ecran.printf("Angle %d deg", (int)position);
  ecran.setCursor(0, 44);
  ecran.printf("Cible %d deg", (int)cible);
  ecran.setCursor(0, 54);
  ecran.printf("%d deg/s %d us", vitesse, versMicrosecondes(position));

  // La porte vue de côté : fermée = couchée, ouverte = debout
  const int px = 122, py = 44, longueur = 24;     // le pivot et la longueur de la porte
  float fraction = (position - FERME) / (float)(OUVERT - FERME);   // 0 = fermée, 1 = ouverte
  float angle = fraction * PI / 2;                                  // de 0 à 90° à l'écran
  ecran.drawLine(px, py, px - longueur * cos(angle), py - longueur * sin(angle), SSD1306_WHITE);
  ecran.drawLine(px + 1, py, px + 1 - longueur * cos(angle), py - longueur * sin(angle), SSD1306_WHITE);
  ecran.fillCircle(px, py, 2, SSD1306_WHITE);
  float fc = (cible - FERME) / (float)(OUVERT - FERME) * PI / 2;    // un petit rond : là où elle va
  ecran.drawCircle(px - longueur * cos(fc), py - longueur * sin(fc), 2, SSD1306_WHITE);
  ecran.drawFastHLine(90, py + 3, 38, SSD1306_WHITE);              // le sol
  ecran.display();
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  porte.setPeriodHertz(50);
  porte.attach(PIN_SERVO, US_MIN, US_MAX);
  porte.writeMicroseconds(versMicrosecondes(position));
  dernierTemps = millis();

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

void loop() {
  BP32.update();
  if (manette && manette->isConnected()) lireManette();
  bouger();
  signaler();
  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
versMicrosecondes(position) la fonction de la mission, réutilisée pour afficher l'impulsion
cos() et sin() dessinent la porte inclinée : on transforme un angle en un point au bout de la porte

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 le mouvement en douceur restent réactifs. Pour des chiffres, 10 images par seconde, c'est largement assez.

À essayer · L'angle et l'impulsion

  • Tourne la manivelle (joystick gauche) et regarde les microsecondes changer : vérifie un ou deux chiffres avec la formule 500 + angle × 2 000 ÷ 270 (fiche Le servo).
  • Baisse la vitesse avec ↓ et regarde l'angle grimper doucement vers la cible.

🎓 Académie : Le bus I2C · Le temps · Le servo

Si ça coince

Symptôme Cause probable Solution
Le servo ne bouge pas du tout fil orange pas sur D18, ou GND manquant vérifier les 3 fils ; le marron doit aller au GND
La carte redémarre quand le servo démarre le servo demande trop de courant d'un coup baisser la vitesse ; un seul servo sur l'USB ; plus tard, une alimentation externe
Le servo tremble à l'arrêt alimentation faible ou signal parasité câble USB court et de bonne qualité ; fils bien enfoncés
La porte s'ouvre quand on demande de fermer FERME et OUVERT inversés échanger les deux valeurs
Le servo bourdonne en fin de course butée mécanique réduire OUVERT ou augmenter FERME de 5°
ESP32Servo.h: No such file or directory bibliothèque non installée étape 1

La règle n°3 du code de l'atelier

Ne jamais forcer l'axe à la main quand le servo est alimenté. Il résiste de toutes ses forces pour tenir sa position : ses petits engrenages en plastique peuvent casser.

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 position mémorisée

SHARE enregistre la position actuelle de la porte ; OPTIONS l'y ramène.

Indice

Une variable de plus suffit, comme un marque-page. Pour y retourner, pas besoin de toucher au servo : change seulement la cible.

Défi 2 · La porte qui se referme toute seule

Une fois grande ouverte, la porte attend 5 secondes, prévient en faisant clignoter la barre lumineuse en orange pendant la dernière seconde, puis se referme.

Indice

Il faut un chrono qui démarre au moment où la porte arrive en position ouverte (un événement !). Ensuite, compare le temps écoulé avec 4 000 et 5 000 ms.

Défi 3 · La tourelle qui balaie

L2 met en marche (ou arrête) un balayage automatique entre les deux extrémités, comme une tourelle de garde.

Indice

Une variable vrai/faux qui s'inverse à chaque appui. Quand le balayage est en marche et que la porte est arrivée à sa cible, donne-lui l'autre extrémité comme nouvelle cible.

Validation

  • Le balayage de test fonctionne
  • Les angles « fermé » et « ouvert » de ma porte sont notés
  • ✕ ouvre et ○ ferme, en douceur, avec la bonne couleur
  • Le joystick fait avancer la porte sans dépasser les butées
  • R1 arrête la porte, ↑ ↓ changent la vitesse
  • Bonus : au moins un défi réussi