home castoo
chapitre electronique
Electronique PAC

Construire un lanceur de balles pour toutous.

mars 2026

Un jeu pour chien : Le lanceur de balle.


Mon chien adore jouer à la balle. Le problème c’est que je ne suis pas toujours disponible pour faire joujou avec lui.
J’ai maintenant un deuxième chien. La demande est donc double et intense !
J’ai donc décidé de leur apprendre à jouer avec un lanceur de balles.

Deux possibilités :

Acheter un lanceur dans le commerce, et je vois que pour un bon modèle, il faut ouvrir grand son porte-monnaie. De plus les balles ne sont pas forcément standard, il faut donc prévoir d’en racheter régulièrement vu que mes deux petits monstres ont un grand besoin de se faire les dents.
Deuxième possibilité, celle que je vais adopter : Construire moi-même un lanceur de balle. Certes, il ne sera pas aussi performant que ceux du commerce, mais mes chiens seront contents de leur nouveau joujou (enfin je l’espère) car j’imagine bien qu’une fois le lanceur construit, il va falloir apprendre aux chiens à déposer la balle dans le capteur. Ceci n’étant pas gagné vu que mes chiens sont jeunes et un peu fou fou. Je vous livre ici le montage de la réalisation si cela peut vous inspirer…


Construction d'un lanceur de balle pour chiens.

Les actions à réaliser

Détecter la présence d’une balle.

J'ai essayé plusieurs systèmes
- détecteur infrarouge

Lanceur de balle detecteur infrarouge. Lanceur de balle detecteur infrarouge solution abandonnée.

Cette solution a été rapidement abandonnée car le soleil déclenchait le mécanisme même sans balle.
Je suis donc revenue à utiliser un simple contact.

Ouvrir une trappe, lorsque les moteurs seront à la bonne vitesse pour laisser tomber la balle dans le lanceur.

Lanceur de balle trape mecanisme de la trape. Lanceur de balle.

Faire tourner les deux moteurs qui actionnent les roues lanceuses.


Lanceur de balle vue générale. Lanceur de balle vue générale moteurs. Lanceur de balle vue générale moteurs 3. Lanceur de balle vue générale moteurs 4.

Attendre qu’une nouvelle balle, soit positionnée dans le lanceur.



En option :

Une page HTML qui permet :
De choisir la vitesse de chaque moteur.
De tester l’ouverture et la fermeture de la trappe.
La mise en route et la vitesse des moteurs.
Un lancement à distance, sans intervention du chien.
Les paramètres vitesses moteurs sont mémorisés en eeprom afin de retrouver la config aux prochain démarrage.
Perso j’alimente le montage avec une ancienne batterie de voiture qui est rechargée par du solaire.

La réalisation :

La structure générale est en bois.
Le capteur de balle, la trappe sont des pièces imprimées en 3D (voir les fichiers STL plus bas).
Les balles sont des balles de tennis et les roues celles d'un ancien jouet.
Lanceur de balle Serveur balles de tennis.
Le processeur est un Wemos ESP8266 mini (3 euros).
Il est alimenté en 3v3 ou 5v à partir du 12v avec un petit convertisseur DC-DC.
Les moteurs sont des DC12 volts modele 755 (18 euros les deux)
Lanceur de balle Serveur moteurs.
Les drivers de moteur (pont en H haute puissance 43A) sont des BTS7960. (6 euros les deux) Lanceur de balle Serveur drivers moteurs.
Le capteur infrarouge (1 euro) ça sa marche que si il n'y a pas de soleil préférer un bon vieux micro-contact. La trappe est actionnée par un servomoteur « Graupner C677 » mais d’autres modèles sont utilisables.
L'alimentation 12volts dans mon cas est assuré par une batterie de récupération rechargée par un panneau solaire.
J'ai récemment ajouter un SR-04 pour vérifier que le chien n'est pas devant au moment du lancement.

Attention dans le code la librairie « servo.h » doit être modifiée car quand la version pour esp8266 est utilisée l’angle du servo est limité à 90 degrés et nous avons besoin des 180 degrés. Info trouvée sur site « reddit » voir ce lien : Modif à réaliser pour angle de 180 degrés.
Lanceur de balle Serveur fixation moteur. Lanceur de balle Serveur fixation moteur 2. Lanceur de balle Serveur fixation moteur 3.

led d'état :
clignote rapidement => pas prêt
clignote lent => prêt pour chargement.

Serveur WIFI du lanceur de balle.

Le lanceur est soit autonome soit il est possible d'effectuer des réglages et faire des tests depuis un point d'accès WiFi créé par le lanceur dès qu'il est alimenté.


Connexion au Wi-Fi Point d’accès du lanceur :
SSID qui va apparaitre dans les réseaux WiFi accessibles = "Lanceur_Balle"
Mot de passe pour entrer sur le réseau du lanceur = "chien1234";
ensuite dans votre browser préféré vous tapez l'adresse ip : 192.168.4.1

On arrive sur page du lanceur (désolé la photo est ancienne, depuis les caractères français sont affichés normalement)

Lanceur de balle Serveur WiFi de paramètrage.
réglage vitesse moteur haut.
réglage vitesse moteur bas.
mémorisation dans l'eeprom de l'ESP8266 des valeurs de rotation des moteurs (on les retrouve donc au redémarrage du lanceur après une coupure).
test servo-moteur ouverture de trappe.
test vitesse des moteurs.

Le code de base qui doit vous permettre de démarrer
pour votre propre projet...


// Lanceur de balle
// castoo.fr
// code libre mais un lien vers https://castoo.fr doit être lisiblement mentionné.
//
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <EEPROM.h>
#include <Servo.h>

// === Wi-Fi Point d’accès ===
const char* ssid_ap = "Lanceur_Balle";
const char* password_ap = "chien1234";

// === Serveur Web ===
ESP8266WebServer server(80);

// === Broches ===
const int motorLeftPWM = D5;
const int motorRightPWM = D7;
const int motorEnable = D8;
const int ballSensorPin = D2;  // Actif LOW (balle présente)
const int servoPin = D6;
const int ledStatus = D4;      // LED d’état (active LOW)

// === Servo ===
Servo servoBall;
const int servoFerme = 0;
const int servoOuvert = 180;

// === Variables ===
int speedLeft = 0;
int speedRight = 0;
bool tirEnCours = false;
bool balleDetectee = false;
bool tirVerrouille = false;

unsigned long tirEndTime = 0;

bool capteur_optique = false; // a true si un capteur optique est utilisé sinon a false si capteur manuel avec rebond

// === Anti-rebond ===
unsigned long dernierChangementCapteur = 0;
const unsigned long delaiAntiRebond = 100; // ms
bool etatStableCapteur = HIGH; // par défaut pas de balle

// === Durées (ms) ===
const unsigned long delaiVerrou = 8000;
const unsigned long clignoInterval = 500;

// === EEPROM ===
const int addrLeft = 0;
const int addrRight = 4;

// === LED clignotement ===
unsigned long lastBlink = 0;
bool ledState = false;

// === Gestion LED ===
void gererLED() {
  unsigned long now = millis();
  if (tirEnCours) {
    digitalWrite(ledStatus, LOW); // allumée fixe
  }
  else if (tirVerrouille) {
    if (now - lastBlink >= 200) { // rapide = verrou
      lastBlink = now;
      ledState = !ledState;
      digitalWrite(ledStatus, ledState ? LOW : HIGH);
    }
  }
  else {
    if (now - lastBlink >= clignoInterval) { // lent = prêt
      lastBlink = now;
      ledState = !ledState;
      digitalWrite(ledStatus, ledState ? LOW : HIGH);
    }
  }
}

// === Rampe de démarrage progressive ===
void demarrageProgressif() {
  Serial.println("Démarrage progressif moteurs...");
  int maxSpeed = max(speedLeft, speedRight);
  for (int p = 0; p <= maxSpeed; p += 100) {
      analogWrite(motorLeftPWM, constrain(p, 0, speedLeft));
      delay(200);
      analogWrite(motorRightPWM, constrain(p, 0, speedRight));
      delay(200);
  }
}

// === Lancer tir ===
void lancerPreparationTir() {
  if (speedLeft == 0 && speedRight == 0) {
      Serial.println("Vitesse nulle — tir annulé.");
      return;
  }
  Serial.println("Balle détectée → Démarrage moteurs...");
  tirEnCours = true;
  tirVerrouille = true;
  servoBall.write(servoFerme);
  delay(200);
  demarrageProgressif(); // montée douce en vitesse
}

// === Gestion tir ===
void gererTir() {
  unsigned long now = millis();
  if (tirEnCours) {
    servoBall.write(servoOuvert);
    delay(3000);
    analogWrite(motorLeftPWM, 0);
    delay(500);
    analogWrite(motorRightPWM, 0);
    delay(500);
    servoBall.write(servoFerme);
    delay(1000);
    tirEnCours = false;
    tirEndTime = now;
    Serial.println("Tir terminé — moteurs arrêtés, servo refermé.");
  }
  if (tirVerrouille && (now - tirEndTime >= delaiVerrou) && !tirEnCours) {
      tirVerrouille = false;
      Serial.println("Sécurité levée — prêt à tirer.");
    }
}

// === Tir manuel ===
void handleTir() {
  if (tirEnCours) {
      server.send(200, "text/plain", "Tir déjà en cours !");
  } else if (tirVerrouille) {
      server.send(200, "text/plain", "Sécurité active — attendre 10 secondes.");
  } else {
      lancerPreparationTir();
      server.send(200, "text/plain", "Tir manuel lancé !");
  }
}

// === Test Servo ===
void handleTestServo() {
  Serial.println("Test servo...");
  servoBall.write(servoOuvert);
  delay(2000);
  servoBall.write(servoFerme);
  delay(100);
  server.send(200, "text/plain", "Test Servo effectué !");
}

// === Test Moteurs ===
void handleTestMoteurs() {
  Serial.println("Test moteurs 2s...");
  analogWrite(motorLeftPWM, speedLeft / 2);
  delay(1000);
  analogWrite(motorRightPWM, speedRight / 2);
  delay(1000);
  delay(4000); // On fait tourner pendant 4 secondes.
  analogWrite(motorLeftPWM, 0);
  delay(500);
  analogWrite(motorRightPWM, 0);
  delay(500);
  server.send(200, "text/plain", "Test Moteurs effectué !");
}

// === Page Web ===
void handleRoot() {
  String html = R"rawliteral(
  <!DOCTYPE html><html><head>
  <title>Lanceur de balle</title>
  <style>
  body { font-family: Arial; text-align:center; background:#f7f7f7; }
  .slider { width: 300px; }
  button { padding:10px 20px; margin:10px; font-size:16px; }
  </style></head><body>
  <h2>Controle Lanceur de Balle</h2>
  <p>Wi-Fi : <b>Lanceur_Balle</b> (mdp: chien1234)</p>
  <p>Balle : <span id="balle">?</span></p>
  <p>Vitesse gauche : <span id="Lval"></span></p>
  <input type="range" min="0" max="1023" value="%L%" class="slider" id="L">
  <p>Vitesse droite : <span id="Rval"></span></p>
  <input type="range" min="0" max="1023" value="%R%" class="slider" id="R">
  <br>
  <button onclick="tir()">TIR Manuel</button>
  <button onclick="testServo()">Test Servo</button>
  <button onclick="testMoteurs()">Test Moteurs</button>
  <script>
  let L=document.getElementById("L");
  let R=document.getElementById("R");
  let Lval=document.getElementById("Lval");
  let Rval=document.getElementById("Rval");
  Lval.innerHTML=L.value; Rval.innerHTML=R.value;
  L.oninput=()=>{Lval.innerHTML=L.value;send()};
  R.oninput=()=>{Rval.innerHTML=R.value;send()};
  function send(){fetch(`/set?L=${L.value}&R=${R.value}`);}
  function tir(){fetch(`/tir`).then(r=>r.text()).then(alert);}
  function testServo(){fetch(`/testServo`).then(r=>r.text()).then(alert);}
  function testMoteurs(){fetch(`/testMoteurs`).then(r=>r.text()).then(alert);}
  setInterval(()=>{
  fetch(`/ball`).then(r=>r.text()).then(t=>{
  document.getElementById("balle").innerHTML=t;
  });
  },500);
  </script></body></html>
  )rawliteral";
  html.replace("%L%", String(speedLeft));
  html.replace("%R%", String(speedRight));
  server.send(200, "text/html", html);
}

// === Réglage vitesses ===
void handleSetSpeed() {
  if (server.hasArg("L")) speedLeft = server.arg("L").toInt();
  if (server.hasArg("R")) speedRight = server.arg("R").toInt();
  EEPROM.put(addrLeft, speedLeft);
  EEPROM.put(addrRight, speedRight);
  EEPROM.commit();
  Serial.printf("Sauvegarde vitesses L=%d R=%d\n", speedLeft, speedRight);
  server.send(200, "text/plain", "OK");
}

// === Capteur balle ===
void handleBall() {
  bool balle = (etatStableCapteur == LOW);
  server.send(200, "text/plain", balle ? "Présente " : "Absente ");
}


// === Détection balle avec anti-rebond ===
void gererDetectionBalle() {
  bool lecture = digitalRead(ballSensorPin); // LOW = balle présente
  if(capteur_optique){
        if (lecture == LOW && !balleDetectee && !tirEnCours && !tirVerrouille) {
            balleDetectee = true;
            lancerPreparationTir();
        } else if (etatStableCapteur == HIGH) {
            balleDetectee = false;
        }
  }else{ 
    // capteur manuel (on evite les rebonds)
    unsigned long now = millis();
    if (lecture != etatStableCapteur && now - dernierChangementCapteur > delaiAntiRebond) {
        dernierChangementCapteur = now;
        etatStableCapteur = lecture;
        if (etatStableCapteur == LOW && !balleDetectee && !tirEnCours && !tirVerrouille) {
            balleDetectee = true;
            lancerPreparationTir();
        } else if (etatStableCapteur == HIGH) {
            balleDetectee = false;
        }
    }
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(motorLeftPWM, OUTPUT);
  pinMode(motorRightPWM, OUTPUT);
  pinMode(motorEnable, OUTPUT);
  pinMode(ballSensorPin, INPUT);
  pinMode(ledStatus, OUTPUT);
  digitalWrite(motorEnable, HIGH);
  digitalWrite(ledStatus, HIGH);
  servoBall.write(servoFerme);
  servoBall.attach(servoPin, 500, 2400);
  servoBall.write(servoFerme);
  EEPROM.begin(512);
  EEPROM.get(addrLeft, speedLeft);
  EEPROM.get(addrRight, speedRight);
  if (speedLeft < 0 || speedLeft > 1023) speedLeft = 300;
  if (speedRight < 0 || speedRight > 1023) speedRight = 300;
  WiFi.mode(WIFI_AP);
  WiFi.softAP(ssid_ap, password_ap);
  Serial.println("\nPoint d'accès : Lanceur_Balle");
  Serial.print("IP locale : ");
  Serial.println(WiFi.softAPIP());
  server.on("/", handleRoot);
  server.on("/set", handleSetSpeed);
  server.on("/ball", handleBall);
  server.on("/tir", handleTir);
  server.on("/testServo", handleTestServo);
  server.on("/testMoteurs", handleTestMoteurs);
  server.begin();
  Serial.println("Serveur web prêt !");
}

void loop() {
  server.handleClient();
  gererDetectionBalle();
  gererTir();
  gererLED();
}
    

Fichier STL Imp 3D

Lanceur de balle STL Corps.
Le fichier STL : lanceur_stl_balles-Corps.stl

Lanceur de balle STL trape haute.
Le fichier STL : lanceur_stl_balles-trape_balle.stl

Lanceur de balle STL trape basse.
Le fichier STL : lanceur_stl_balles-plaque_mobile.stl

Lanceur de balle STL bear.
Le fichier STL : lanceur_stl_balles-bear.stl

Lanceur de balle STL trape ronde.
Le fichier STL : lanceur_stl_trape2FCStd-Corps.stl

Lanceur de balle STL pince.
Le fichier STL : lanceur_stl_balles_2-pince.stl

Lanceur de balle STL pince.
Le fichier STL : lanceur_stl_trape_haut2FCStd-Corps001.stl


accueil electronique

Bricolage Robotique Informatique Peinture Voyage
Téléc. portail Le robot "mécano" Astuces informatique Henri Bertrou Auvergne
Bat. Iphone 6S Le robot "solaire" Réseau couche app. Jean-Michel Castille Floride
Robot piscine Servo et IR" Réseau Les couches New York
Xiaomi M365 Le robot "thymio" Réseaux Outils L'Ouest américain
Mac Mini Le robot "Rovio" Unités grandeur inf. L'Ile Maurice
Putty SSH Windows L'Italie / Venise
Bases Raspberry Tunisie
Termius IPhone/IPad Grece
Le vieux ZX 81
...
Navigation La Rochelle CNC / Imp3D Electronique Programmation
Rencontre dauphins Les Minimes Construction CNC Alim. TPL 5110 Doc. programme
Analyse NMEA 0183 Le Vieux port CNC du commerce Carte ESP8266 Indent programme
graph. NMEA 0183 L'Ile de Ré Martyr CNC ESP8266 1 relai Prog. objet
Analyse trames AIS A visiter Réa. imp. 3D ESP8266 Alarme Prog. procédurale
Analyse AIS TCP-IP Cura impression 3D ESP8266 MQTT
Sortie en ketch Plateau CR10 ESP8266 Temp.
Echange GPS C80 Anémomètre.
HP Sun-Odyssey CNC / 3D en vrac MCP9808 Librairie
LCD yanmar Saisie Oled
Testeur nmea esp1 i2c