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. »
🛰️ 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. »
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¶
- Coller un petit bout de scotch sur l'axe du servo, pour bien voir s'il tourne.
- Téléverser le programme d'enquête ci-dessous.
- Moniteur série à 115200 bauds, en mode « Nouvelle ligne ».
- 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¶
- 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.
- 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.
- Le moniteur série affiche les valeurs : « Largeur : ±60° », « Vitesse : 45 °/s ». Note celles qui te plaisent et écris-les dans
int demiLargeur = …;etint 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.


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.
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