Aller au contenu
Mission 4Objectif 4 · Planète Naboucle

Le radar contre-attaque

Maître du PWMSeigneur Sith du PWM ★★★ 📡 Il ne tourne pas sans fin ? Ce n'est pas un bug : c'est une enquête résolue.📡 Un radar qui tourne sans fin sans rien trouver ? C'est un ventilateur très ambitieux.

📡 Transmission de l'Alliance · R2-DuPont

« Cette fois, il ne suffit plus de faire tourner quelque chose. Il faut observer, mesurer, balayer, détecter. Et surtout empêcher ton programme de décider que tourner à l'infini constitue une stratégie parfaitement valide. »

🔊 Écouter le briefing de R2-DuPont

🛰️ Haut commandement · Opération Contre-Circuit

« L'Alliance tentera d'aveugler le système de détection. Ta mission : le remettre en fonctionnement. Balayage, mesure, détection. Et si le capteur tourne continuellement sans rien trouver, ce n'est pas un radar : c'est un ventilateur très ambitieux. »

🔊 Écouter le briefing de R2-DuPont

Objectif : mener l'enquête sur le Geekservo EF10155 (2 kg, 360°) pour découvrir quel genre de servo il est vraiment, puis en faire un radar à balayage piloté à la manette : l'antenne pivote aux gâchettes, balaie toute seule d'un bord à l'autre, et on règle la largeur et la vitesse du balayage.

🧭 Notions de la mission : Le servo · Le PWM · Le temps

Équipement de mission

🧰 Matériel

  • ESP32 sur son support + câble USB de données
  • Geekservo EF10155 (2 kg, 360°)
  • 3 câbles Dupont mâle-mâle
  • Quelques pièces LEGO Technic (une antenne de radar)
  • Manette PS4 chargée
  • En option : un écran OLED, voir l'holo-écran

💻 Logiciels

  • Arduino IDE 2 + cartes « ESP32 + Bluepad32 »
  • Bibliothèque ESP32Servo (mission 3)

🌐 Connexion internet

Non

🔓 Missions à réussir avant

Mission 3 (le premier servo)

🛠️ Outils

Un bout de scotch de couleur (pour repérer l'axe)

⏱️ Durée

1 h

Comment ça marche ?

Les servos existent en deux grandes familles, qui se ressemblent comme deux gouttes d'eau mais ne comprennent pas la même chose :

Servo à position Servo à rotation continue
Ce que veut dire l'impulsion « va à cet angle » « tourne à cette vitesse, dans ce sens »
write(90) va au milieu, et tient la position s'arrête
write(0) ou write(180) va à une extrémité tourne à fond, dans un sens ou dans l'autre
Exemple dans l'atelier le Geekservo 270° de la mission 3, et le EF10155 (verdict de l'enquête) le moteur de la mission 5 joue ce rôle

Le vendeur du EF10155 parle à la fois de « rotation complète », de « vitesse » et de « sens »… mais aussi de « contrôle précis de l'angle ». Les fiches se contredisent : le seul moyen d'en avoir le cœur net, c'est de faire l'expérience. Spoiler : l'atelier l'a menée le 10 octobre 2026, et c'est un servo à position, sur environ un tour complet. Refais quand même l'enquête : c'est comme ça qu'on l'a découvert ! Le détail des deux familles est dans la fiche Le servo.

Étape 1 · Câbler

Exactement comme à la mission 3 : orange sur D18, rouge sur VIN, marron sur GND, USB débranché pendant le câblage.

Étape 2 · L'enquête

  1. Coller un petit bout de scotch sur l'axe du servo, pour bien voir s'il tourne.
  2. Téléverser le programme d'enquête ci-dessous.
  3. Moniteur série à 115200 bauds, en mode « Nouvelle ligne ».
  4. Taper les valeurs du tableau une par une, et noter ce que fait le servo.
Le programme d'enquête · cliquer pour l'afficher
// Mission 4 · L'enquête : tape une valeur de 0 à 180 dans le moniteur série
// et observe ce que fait le servo EF10155.
#include <ESP32Servo.h>

const int PIN_SERVO = 18;
Servo servo;

void setup() {
  Serial.begin(115200);
  servo.setPeriodHertz(50);
  servo.attach(PIN_SERVO, 500, 2500);
  servo.write(90);                                   // on commence au milieu
  Serial.println("Enquête prête. Tape une valeur entre 0 et 180, puis Entrée.");
}

void loop() {
  if (Serial.available()) {
    String ligne = Serial.readStringUntil('\n');
    ligne.trim();
    if (ligne.length() == 0) return;
    int valeur = constrain(ligne.toInt(), 0, 180);
    servo.write(valeur);
    Serial.printf("→ write(%d)\n", valeur);
  }
}
Valeur tapée Ce que j'observe
90 …
60 …
120 …
0 …
180 …
90 de nouveau …

Le verdict :

Chaque valeur l'envoie à une position différente, où il s'immobilise et résiste un peu. C'est un servo à position, sur une grande plage : 0 et 180 l'envoient presque un tour complet l'un de l'autre. Comme write() reçoit 0 à 180 pour environ 360°, chaque unité vaut 2°.

👉 C'est le verdict de l'atelier pour notre EF10155. Continue avec l'étape 3 : le radar à balayage.

Avec 60 et 120, il tourne sans fin, dans un sens puis dans l'autre ; vers 90, il s'arrête. C'est un servo à rotation continue : ce n'est pas le cas de notre EF10155, mais d'autres modèles le sont.

👉 Le programme de cette mission ne lui convient pas. Pour un radar qui tourne sans fin, l'atelier utilise le moteur de la mission 5, comme dans la base de la mission 6.

Notre EF10155 est un servo à : position, sur environ un tour (360°)

Étape 3 · Le programme Naboucle (radar à balayage)

Qui fait quoi

Commande Effet Barre lumineuse
R2 (gâchette) l'antenne pivote vers la droite, d'autant plus vite qu'on appuie verte
L2 (gâchette) l'antenne pivote vers la gauche bleue
△ balayage automatique : aller-retour d'un bord à l'autre (marche / arrêt) verte puis bleue
↑ / ↓ vitesse du balayage (de 15 à 180 degrés par seconde)
← / → largeur du balayage (de ±10° à ±90° autour du centre)
R1 retour au centre, en douceur blanche à l'arrivée

L'antenne ne dépasse jamais 90° de chaque côté du centre : c'est une butée logicielle, comme à la mission 3.

Le code

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

Le programme · cliquer pour l'afficher
// Mission 4 · Naboucle : le radar à balayage
// Verdict de l'enquête : notre EF10155 est un servo À POSITION, sur environ un tour (360°).
// R2 / L2 : tourner le radar à la main, plus ou moins vite selon l'appui.
// △ : balayage automatique (aller-retour entre les deux bords). R1 : retour au centre.
// ↑ ↓ : vitesse du balayage. ← → : largeur du balayage.

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

// ================= Réglages =================
const int PIN_SERVO = 18;
const int US_MIN = 500;            // impulsion pour 0°    (comme dans le programme d'enquête)
const int US_MAX = 2500;           // impulsion pour 360°
const int CENTRE = 180;            // le milieu de la course, en degrés : l'antenne regarde devant
const int ECART_MAX = 90;          // sécurité : jamais plus de 90° de chaque côté du centre
                                   // (impulsions de 1 000 à 2 000 µs), tant qu'on n'a pas réglé

Servo radar;
ControllerPtr manette = nullptr;

float angle = CENTRE;              // où est le radar, en degrés
int demiLargeur = 60;              // le balayage va de CENTRE - demiLargeur à CENTRE + demiLargeur
int vitesseAuto = 45;              // vitesse du balayage, en degrés par seconde
bool automatique = false;          // le radar balaie-t-il tout seul ?
bool retourCentre = false;         // R1 : on rentre au centre
int sensAuto = 1;                  // +1 : vers la droite, -1 : vers la gauche
int sensPrecedent = 99;            // pour ne recolorer la barre que si ça change
unsigned long dernierTemps = 0;

enum { B_TRIANGLE, B_R1, B_HAUT, B_BAS, B_GAUCHE, B_DROITE, 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 a) {
  return US_MIN + (int)(a * (US_MAX - US_MIN) / 360);
}

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  sensPrecedent = 99;
  Serial.println("🎮 Manette connectée : le radar est prêt.");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    automatique = false;
    Serial.println("❌ Manette déconnectée : le radar s'arrête.");
  }
}

// ================= Lecture de la manette =================
void lireManette() {
  if (vientDAppuyer(B_TRIANGLE, manette->y())) {
    automatique = !automatique;
    retourCentre = false;
    Serial.println(automatique ? "△ Balayage automatique : marche" : "△ Balayage automatique : arrêt");
  }
  if (vientDAppuyer(B_R1, manette->r1())) {
    automatique = false;
    retourCentre = true;
    Serial.println("R1 : retour au centre");
  }

  uint8_t f = manette->dpad();
  if (vientDAppuyer(B_HAUT, f & DPAD_UP))      { vitesseAuto = min(vitesseAuto + 15, 180); Serial.printf("Vitesse : %d °/s\n", vitesseAuto); }
  if (vientDAppuyer(B_BAS, f & DPAD_DOWN))     { vitesseAuto = max(vitesseAuto - 15, 15);  Serial.printf("Vitesse : %d °/s\n", vitesseAuto); }
  if (vientDAppuyer(B_DROITE, f & DPAD_RIGHT)) { demiLargeur = min(demiLargeur + 10, ECART_MAX); Serial.printf("Largeur : ±%d°\n", demiLargeur); }
  if (vientDAppuyer(B_GAUCHE, f & DPAD_LEFT))  { demiLargeur = max(demiLargeur - 10, 10);        Serial.printf("Largeur : ±%d°\n", demiLargeur); }
}

// ================= Le mouvement =================
// Renvoie le déplacement fait pendant ce tour (en degrés), pour la couleur de la barre
float bouger() {
  unsigned long maintenant = millis();
  float dt = (maintenant - dernierTemps) / 1000.0;          // secondes écoulées depuis le dernier tour
  dernierTemps = maintenant;
  float avantAngle = angle;

  int gachettes = manette ? manette->throttle() - manette->brake() : 0;   // R2 moins L2 : -1020 à 1020
  if (abs(gachettes) > 40) {                                 // les gâchettes ont la priorité
    automatique = false;
    retourCentre = false;
    angle += gachettes / 1020.0 * 120 * dt;                  // jusqu'à 120 °/s à fond
  } else if (automatique) {
    angle += sensAuto * vitesseAuto * dt;
    if (angle >= CENTRE + demiLargeur) { angle = CENTRE + demiLargeur; sensAuto = -1; }   // bord droit : demi-tour
    if (angle <= CENTRE - demiLargeur) { angle = CENTRE - demiLargeur; sensAuto = 1; }    // bord gauche : demi-tour
  } else if (retourCentre) {
    float pas = vitesseAuto * dt;
    if (abs(angle - CENTRE) <= pas) { angle = CENTRE; retourCentre = false; }
    else angle += (angle < CENTRE) ? pas : -pas;
  }

  angle = constrain(angle, CENTRE - ECART_MAX, CENTRE + ECART_MAX);   // butées logicielles
  radar.writeMicroseconds(versMicrosecondes(angle));
  return angle - avantAngle;
}

// ================= Ce que voit le pilote =================
void signaler(float deplacement) {
  int sens = (deplacement > 0.01) - (deplacement < -0.01);   // 1, -1 ou 0
  if (sens == sensPrecedent || !manette) return;
  sensPrecedent = sens;
  if (sens > 0)      manette->setColorLED(0, 255, 0);        // vert : vers la droite
  else if (sens < 0) manette->setColorLED(0, 80, 255);       // bleu : vers la gauche
  else               manette->setColorLED(255, 255, 255);    // blanc : immobile
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  radar.setPeriodHertz(50);
  radar.attach(PIN_SERVO, US_MIN, US_MAX);
  radar.writeMicroseconds(versMicrosecondes(angle));
  dernierTemps = millis();
  BP32.setup(&onConnectedController, &onDisconnectedController);
  BP32.forgetBluetoothKeys();   // à mettre en commentaire une fois que tout marche
  Serial.println("Naboucle prêt. Maintiens SHARE + PS pour coupler la manette.");
}

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

Comment il est construit

Bloc Son travail
angle la position de l'antenne, en degrés (un nombre à virgule), de 0 à 360
versMicrosecondes transforme l'angle en durée d'impulsion : 0° → 500 µs, 360° → 2 500 µs
CENTRE = 180 le milieu de la course : l'antenne regarde « droit devant »
bouger à chaque tour : les gâchettes d'abord, sinon le balayage, sinon le retour au centre. Le pas dépend du temps écoulé (dt) : la vitesse reste la même, quelle que soit la durée d'un tour de loop()
sensAuto +1 ou −1 : il change de signe quand l'antenne touche un bord, c'est le demi-tour
constrain(angle, …) les butées logicielles : jamais plus de ECART_MAX degrés de chaque côté
signaler colore la barre de la manette selon le sens du mouvement, seulement quand il change

Étape 4 · Régler la largeur et le centre

  1. Monte une antenne LEGO sur l'axe, puis appuie sur R1 : l'antenne va au centre. Si elle ne regarde pas « droit devant », retire-la et remets-la un cran plus loin sur l'axe.
  2. Lance le balayage avec △, puis règle la largeur avec ← → et la vitesse avec ↑ ↓, en regardant ce qui est le plus joli… et ce que permet la construction autour.
  3. Le moniteur série affiche les valeurs : « Largeur : ±60° », « Vitesse : 45 °/s ». Note celles qui te plaisent et écris-les dans int demiLargeur = …; et int vitesseAuto = …;.

Avant d'élargir au-delà de ±90°

Les impulsions restent pour l'instant entre 1 000 et 2 000 µs, la zone que tous les servos acceptent. Pour aller plus loin, augmente ECART_MAX de 10 en 10, en regardant les bords : si le servo grogne ou tremble en bout de course, il force contre sa butée. Reviens alors de 10.

Ce qu'on apprend ici

  • Expérimenter avant de coder : quand les documents se contredisent, une petite expérience bien menée tranche. Et le programme s'adapte au verdict.
  • Raisonner en degrés, pas en impulsions : une petite fonction fait la conversion, le reste du programme parle le langage du radar.
  • Une vitesse indépendante du rythme : avancer de vitesse × temps écoulé donne un mouvement régulier.
  • Le demi-tour : un simple changement de signe (sensAuto) fait repartir l'antenne dans l'autre sens.

🎓 Académie : Le servo · Le temps · Le PWM

Option · L'holo-écran

Avec l'holo-écran de la mission 1 bis, Naboucle affiche le radar vu de dessus : la vraie position de l'antenne, les deux bords du balayage, et les chiffres en direct (angle, largeur, vitesse, impulsion). Très pratique pour régler la largeur et le centre.

L'écran pendant le balayage automatique
Balayage automatique : l'antenne à +39° du centre, entre les deux bords à ±60°.
L'écran avec la gâchette L2 enfoncée
L2 enfoncée : l'antenne pivote vers la gauche, en mode manuel.

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 RADAR, et la batterie de la manette
Le cercle le radar vu de dessus : l'antenne en trait plein, les deux bords du balayage en pointillés, « droit devant » en haut
Le mode BALAYAGE, MANUEL ou RETOUR (vers le centre)
Angle la position de l'antenne par rapport au centre : 0 = droit devant, + à droite, − à gauche
Larg, Vit la largeur (±) et la vitesse du balayage, réglées avec ← → et ↑ ↓
us la durée de l'impulsion envoyée au servo, en microsecondes

Le code

Nouveau croquis, coller le programme ou télécharger le fichier, enregistrer sous mission4_naboucle_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 4 · Naboucle, version holo-écran
// Le même programme que Naboucle, avec en plus l'écran OLED qui affiche :
// le radar vu de dessus avec la vraie position de l'antenne et les bords du balayage,
// le mode, l'angle, la largeur, la vitesse et 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 =================
const int PIN_SERVO = 18;
const int US_MIN = 500;            // impulsion pour 0°    (comme dans le programme d'enquête)
const int US_MAX = 2500;           // impulsion pour 360°
const int CENTRE = 180;            // le milieu de la course, en degrés : l'antenne regarde devant
const int ECART_MAX = 90;          // sécurité : jamais plus de 90° de chaque côté du centre
                                   // (impulsions de 1 000 à 2 000 µs), tant qu'on n'a pas réglé

Servo radar;
ControllerPtr manette = nullptr;

float angle = CENTRE;              // où est le radar, en degrés
int demiLargeur = 60;              // le balayage va de CENTRE - demiLargeur à CENTRE + demiLargeur
int vitesseAuto = 45;              // vitesse du balayage, en degrés par seconde
bool automatique = false;          // le radar balaie-t-il tout seul ?
bool retourCentre = false;         // R1 : on rentre au centre
int sensAuto = 1;                  // +1 : vers la droite, -1 : vers la gauche
int sensPrecedent = 99;            // pour ne recolorer la barre que si ça change
unsigned long dernierTemps = 0;

enum { B_TRIANGLE, B_R1, B_HAUT, B_BAS, B_GAUCHE, B_DROITE, 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 a) {
  return US_MIN + (int)(a * (US_MAX - US_MIN) / 360);
}

// ================= Connexion =================
void onConnectedController(ControllerPtr ctl) {
  if (manette != nullptr) return;
  manette = ctl;
  sensPrecedent = 99;
  Serial.println("🎮 Manette connectée : le radar est prêt.");
}

void onDisconnectedController(ControllerPtr ctl) {
  if (manette == ctl) {
    manette = nullptr;
    automatique = false;
    Serial.println("❌ Manette déconnectée : le radar s'arrête.");
  }
}

// ================= Lecture de la manette =================
void lireManette() {
  if (vientDAppuyer(B_TRIANGLE, manette->y())) {
    automatique = !automatique;
    retourCentre = false;
    Serial.println(automatique ? "△ Balayage automatique : marche" : "△ Balayage automatique : arrêt");
  }
  if (vientDAppuyer(B_R1, manette->r1())) {
    automatique = false;
    retourCentre = true;
    Serial.println("R1 : retour au centre");
  }

  uint8_t f = manette->dpad();
  if (vientDAppuyer(B_HAUT, f & DPAD_UP))      { vitesseAuto = min(vitesseAuto + 15, 180); Serial.printf("Vitesse : %d °/s\n", vitesseAuto); }
  if (vientDAppuyer(B_BAS, f & DPAD_DOWN))     { vitesseAuto = max(vitesseAuto - 15, 15);  Serial.printf("Vitesse : %d °/s\n", vitesseAuto); }
  if (vientDAppuyer(B_DROITE, f & DPAD_RIGHT)) { demiLargeur = min(demiLargeur + 10, ECART_MAX); Serial.printf("Largeur : ±%d°\n", demiLargeur); }
  if (vientDAppuyer(B_GAUCHE, f & DPAD_LEFT))  { demiLargeur = max(demiLargeur - 10, 10);        Serial.printf("Largeur : ±%d°\n", demiLargeur); }
}

// ================= Le mouvement =================
// Renvoie le déplacement fait pendant ce tour (en degrés), pour la couleur de la barre
float bouger() {
  unsigned long maintenant = millis();
  float dt = (maintenant - dernierTemps) / 1000.0;          // secondes écoulées depuis le dernier tour
  dernierTemps = maintenant;
  float avantAngle = angle;

  int gachettes = manette ? manette->throttle() - manette->brake() : 0;   // R2 moins L2 : -1020 à 1020
  if (abs(gachettes) > 40) {                                 // les gâchettes ont la priorité
    automatique = false;
    retourCentre = false;
    angle += gachettes / 1020.0 * 120 * dt;                  // jusqu'à 120 °/s à fond
  } else if (automatique) {
    angle += sensAuto * vitesseAuto * dt;
    if (angle >= CENTRE + demiLargeur) { angle = CENTRE + demiLargeur; sensAuto = -1; }   // bord droit : demi-tour
    if (angle <= CENTRE - demiLargeur) { angle = CENTRE - demiLargeur; sensAuto = 1; }    // bord gauche : demi-tour
  } else if (retourCentre) {
    float pas = vitesseAuto * dt;
    if (abs(angle - CENTRE) <= pas) { angle = CENTRE; retourCentre = false; }
    else angle += (angle < CENTRE) ? pas : -pas;
  }

  angle = constrain(angle, CENTRE - ECART_MAX, CENTRE + ECART_MAX);   // butées logicielles
  radar.writeMicroseconds(versMicrosecondes(angle));
  return angle - avantAngle;
}

// ================= Ce que voit le pilote =================
void signaler(float deplacement) {
  int sens = (deplacement > 0.01) - (deplacement < -0.01);   // 1, -1 ou 0
  if (sens == sensPrecedent || !manette) return;
  sensPrecedent = sens;
  if (sens > 0)      manette->setColorLED(0, 255, 0);        // vert : vers la droite
  else if (sens < 0) manette->setColorLED(0, 80, 255);       // bleu : vers la gauche
  else               manette->setColorLED(255, 255, 255);    // blanc : immobile
}

// ================= 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
}

// Un point sur le cercle du radar : l'antenne au CENTRE regarde vers le haut de l'écran
int pointX(int cx, float a, int r) { return cx + r * sin((a - CENTRE) * PI / 180); }
int pointY(int cy, float a, int r) { return cy - r * cos((a - CENTRE) * PI / 180); }

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

  // Le radar vu de dessus, à gauche : les bords du balayage en pointillés, l'antenne en trait plein
  const int cx = 26, cy = 40, r = 22;
  ecran.drawCircle(cx, cy, r, SSD1306_WHITE);
  ecran.fillCircle(cx, cy, 2, SSD1306_WHITE);
  for (int k = 4; k <= r; k += 4) {                          // les deux bords, en pointillés
    ecran.drawPixel(pointX(cx, CENTRE - demiLargeur, k), pointY(cy, CENTRE - demiLargeur, k), SSD1306_WHITE);
    ecran.drawPixel(pointX(cx, CENTRE + demiLargeur, k), pointY(cy, CENTRE + demiLargeur, k), SSD1306_WHITE);
  }
  ecran.drawLine(cx, cy, pointX(cx, angle, r), pointY(cy, angle, r), SSD1306_WHITE);   // l'antenne

  // Les chiffres, à droite
  ecran.setTextSize(1);
  ecran.setCursor(58, 18);
  ecran.print(automatique ? "BALAYAGE" : retourCentre ? "RETOUR" : "MANUEL");
  ecran.setCursor(58, 28);
  ecran.printf("Angle %+d", (int)angle - CENTRE);            // 0 = droit devant
  ecran.setCursor(58, 38);
  ecran.printf("Larg  +-%d", demiLargeur);
  ecran.setCursor(58, 48);
  ecran.printf("Vit %d d/s", vitesseAuto);
  ecran.setCursor(58, 57);
  ecran.printf("%d us", versMicrosecondes(angle));
  ecran.display();
}

// ================= Programme principal =================
void setup() {
  Serial.begin(115200);
  radar.setPeriodHertz(50);
  radar.attach(PIN_SERVO, US_MIN, US_MAX);
  radar.writeMicroseconds(versMicrosecondes(angle));
  dernierTemps = millis();
  demarrerEcran();
  BP32.setup(&onConnectedController, &onDisconnectedController);
  BP32.forgetBluetoothKeys();   // à mettre en commentaire une fois que tout marche
  Serial.println("Naboucle prêt. Maintiens SHARE + PS pour coupler la manette.");
}

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

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
pointX(), pointY() placent un point sur le cercle du radar à partir d'un angle, avec sin() et cos()
les pointillés un pixel tous les 4 pixels le long de chaque bord du balayage

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

À essayer · Le radar en chiffres

  • Pendant le balayage, regarde les microsecondes : à +60°, l'antenne est à 180 + 60 = 240°, soit 500 + 240 × 2 000 ÷ 360 ≈ 1 833 µs. Vérifie sur l'écran !
  • Élargis avec → et observe les pointillés s'écarter, puis accélère avec ↑ : le balayage reste-t-il régulier ?

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

Si ça coince

Symptôme Cause probable Solution
Le servo ne réagit à rien câblage (orange sur D18 ?) même câblage qu'à la mission 3
L'antenne ne regarde pas « droit devant » au centre elle est montée de travers sur l'axe R1, puis la remonter un cran plus loin (étape 4)
Il grogne ou tremble en bout de course il force contre sa butée intérieure baisser ECART_MAX ou la largeur
Le balayage va « à l'envers » c'est une question de point de vue ! échanger throttle() et brake() dans bouger()
La carte redémarre quand le servo démarre trop de courant pour l'USB moins de vitesse ; un seul servo branché
Il tourne sans fin et ne s'arrête jamais ce servo est à rotation continue (un autre modèle) voir le verdict de l'étape 2

Ne jamais bloquer ou forcer l'axe à la main quand il est alimenté.

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 ralenti aux bords

Comme une vraie antenne lourde, le radar ralentit en arrivant au bord du balayage, fait demi-tour en douceur, puis réaccélère.

Indice

Calcule la distance entre l'antenne et le bord le plus proche (min() des deux distances). À moins de 20°, multiplie la vitesse par distance ÷ 20, sans descendre sous 25 % (sinon l'antenne n'arriverait jamais au bord !). constrain() sait garder un nombre entre deux limites.

Défi 2 · La patrouille

OPTIONS lance une patrouille : l'antenne va surveiller à gauche, au centre, à droite, au centre… en s'arrêtant 1 seconde à chaque poste, et recommence sans fin.

Indice

Range les postes dans un tableau : {-1, 0, 1, 0}, en nombre de demi-largeurs. Une variable retient le poste visé. Quand l'antenne y est arrivée, note l'heure (millis()) ; une seconde plus tard, passe au poste suivant avec le reste de la division (%) pour boucler.

Validation

  • L'enquête est menée et le verdict est noté
  • R2 et L2 font pivoter l'antenne dans les deux sens, plus ou moins vite
  • △ lance et arrête le balayage automatique
  • ← → et ↑ ↓ règlent la largeur et la vitesse, R1 ramène au centre
  • L'antenne LEGO regarde « droit devant » au centre
  • Bonus : au moins un défi réussi