ESP32 et Arduino

Servomoteur ESP32 MQTT : piloter un servo à distance avec retour de position

Déplacez un servomoteur par des commandes MQTT avec un ESP32 et la bibliothèque ESP32Servo, amenez-le en douceur à l'angle cible et publiez un retour de position que les tableaux de bord peuvent afficher.

Servomoteur SG90 relié à une carte ESP32 recevant des commandes d'angle dans un montage servomoteur ESP32 MQTT
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Un projet de servomoteur ESP32 MQTT transforme un servo de modélisme bon marché en actionneur pilotable à distance pour des stores, des vannes, des têtes panoramiques de caméra ou des distributeurs de croquettes. Dans ce tutoriel, vous allez déplacer un servo à l'aide de commandes d'angle MQTT, l'amener progressivement à sa position plutôt que brusquement, imposer des limites mécaniques dans le firmware et publier un retour de position lisible par n'importe quel tableau de bord. Vous pouvez tester chaque étape sur broker.connectmq.com sans créer de compte.

Ce que vous allez construire

L'ESP32 écoute les commandes sur un topic et rend compte de sa progression sur un autre. Les commandes peuvent être un simple angle ou un petit objet JSON avec une vitesse :

Topic Sens Retained Exemple de payload
connectmq/demo/<id>/servo/set Application vers ESP32 Non 90 ou {"angle":45,"speed":60}
connectmq/demo/<id>/servo/position ESP32 vers applications Oui {"angle":72,"target":45,"moving":true}
connectmq/demo/<id>/status ESP32 vers applications Oui online / offline

Une limite à annoncer d'emblée, en toute honnêteté : un servo de modélisme standard ne renvoie pas la position réelle de son axe. Le retour publié ici correspond à l'angle commandé par le firmware, mis à jour au fur et à mesure que le servo se déplace vers la cible. Cela suffit pour les tableaux de bord et pour confirmer qu'une commande a été acceptée. Si vous avez besoin de la position réellement mesurée, il vous faut un servo doté d'un fil de retour ou un codeur externe, ce qui est un autre projet.

Matériel et alimentation

  • Une carte de développement ESP32.
  • Un servo de modélisme comme un SG90 (micro, pignons en plastique) ou un MG996R (taille standard, pignons métalliques).
  • Une alimentation 5 V séparée pour tout servo plus gros qu'un micro-servo, dimensionnée au moins pour le courant de blocage du servo.
  • Un condensateur électrolytique de 470 µF à 1000 µF sur l'alimentation du servo, au plus près de celui-ci.
Fil du servo À relier à
Marron / noir (GND) GND de l'alimentation et GND de l'ESP32
Rouge (V+) Alimentation 5 V
Orange / jaune (signal) GPIO 18 de l'ESP32

La masse commune n'est pas facultative : sans elle, le signal n'a pas de référence et le servo tressaute de façon aléatoire. Alimenter un MG996R depuis la broche 5 V de l'ESP32 est la cause la plus fréquente de redémarrages par chute de tension (brownout) dans les projets de servos, car le servo tire un fort pic de courant à chaque démarrage de mouvement. Le signal 3,3 V de l'ESP32 est accepté par la quasi-totalité des servos de modélisme.

Bibliothèques

Installez le paquet de cartes esp32 et les bibliothèques suivantes depuis le gestionnaire de bibliothèques Arduino :

  • PubSubClient de Nick O'Leary, pour MQTT.
  • ESP32Servo de Kevin Harrington et John K. Bennett, qui pilote les servos via le matériel PWM LEDC de l'ESP32.
  • ArduinoJson de Benoit Blanchon (version 7), pour analyser le format de commande JSON.

La bibliothèque Arduino Servo classique ne prend pas en charge l'ESP32 : ESP32Servo est donc le bon choix. Vous découvrez MQTT sur cette carte ? Commencez par le tutoriel IoT ESP32, qui couvre la configuration de la carte et une première publication.

Le sketch servomoteur ESP32 MQTT

#include <WiFi.h>
#include <PubSubClient.h>
#include <ESP32Servo.h>
#include <ArduinoJson.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* DEVICE_ID     = "servo-4c81d2";      // unique pour chaque appareil
const String BASE = String("connectmq/demo/") + DEVICE_ID;

const int SERVO_PIN = 18;
const int MIN_ANGLE = 10;      // limites mécaniques de VOTRE tringlerie
const int MAX_ANGLE = 170;
const int MIN_US = 500;        // largeur d'impulsion pour 0 degré
const int MAX_US = 2400;       // largeur d'impulsion pour 180 degrés

Servo servo;
WiFiClient net;
PubSubClient mqtt(net);

float currentAngle = 90;
int targetAngle = 90;
float speedDegPerSec = 90;     // vitesse de rampe par défaut
unsigned long lastStep = 0, lastReport = 0, lastAttempt = 0;
bool wasMoving = false;

void publishPosition(bool force) {
  bool moving = (int)round(currentAngle) != targetAngle;
  if (!force && millis() - lastReport < 250) return;
  lastReport = millis();
  char buf[96];
  snprintf(buf, sizeof(buf), "{\"angle\":%d,\"target\":%d,\"moving\":%s}",
           (int)round(currentAngle), targetAngle, moving ? "true" : "false");
  String topic = BASE + "/servo/position";
  mqtt.publish(topic.c_str(), buf, true);
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  JsonDocument doc;
  int angle;
  if (deserializeJson(doc, payload, length) == DeserializationError::Ok && doc.is<JsonObject>()) {
    if (!doc["angle"].is<int>()) return;
    angle = doc["angle"];
    if (doc["speed"].is<float>()) speedDegPerSec = constrain(doc["speed"].as<float>(), 5.0f, 600.0f);
  } else if (doc.is<int>()) {
    angle = doc.as<int>();                          // nombre seul comme "90"
  } else {
    return;
  }
  targetAngle = constrain(angle, MIN_ANGLE, MAX_ANGLE);
  if (!servo.attached()) servo.attach(SERVO_PIN, MIN_US, MAX_US);
  Serial.printf("Target %d deg at %.0f deg/s\n", targetAngle, speedDegPerSec);
  publishPosition(true);
}

bool connectMqtt() {
  String status = BASE + "/status";
  if (!mqtt.connect(DEVICE_ID, nullptr, nullptr, status.c_str(), 1, true, "offline")) {
    Serial.printf("MQTT failed, state=%d\n", mqtt.state());
    return false;
  }
  mqtt.publish(status.c_str(), "online", true);
  mqtt.subscribe((BASE + "/servo/set").c_str(), 1);
  publishPosition(true);
  return true;
}

void setup() {
  Serial.begin(115200);
  ESP32PWM::allocateTimer(0);
  servo.setPeriodHertz(50);                         // trame servo standard
  servo.attach(SERVO_PIN, MIN_US, MAX_US);
  servo.write((int)currentAngle);

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setSocketTimeout(5);
}

void stepServo() {
  unsigned long now = millis();
  float dt = (now - lastStep) / 1000.0f;
  lastStep = now;
  float diff = targetAngle - currentAngle;
  if (fabs(diff) < 0.5f) {
    currentAngle = targetAngle;
    if (wasMoving) {
      servo.write(targetAngle);
      publishPosition(true);                        // rapport de position final
      wasMoving = false;
    }
    return;
  }
  float stepSize = speedDegPerSec * dt;
  currentAngle += constrain(diff, -stepSize, stepSize);
  servo.write((int)round(currentAngle));
  wasMoving = true;
  publishPosition(false);                           // progression limitée en fréquence
}

void loop() {
  stepServo();
  if (WiFi.status() != WL_CONNECTED) return;
  if (!mqtt.connected()) {
    if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
    return;
  }
  mqtt.loop();
}

Pourquoi une rampe plutôt qu'un saut

Appeler directement servo.write(170) fait claquer le servo vers le nouvel angle à pleine vitesse. Sur une tringlerie qui actionne une vanne ou un couvercle, cela malmène les pignons en plastique et tire un gros pic de courant sur l'alimentation. La fonction stepServo() déplace un peu l'angle commandé à chaque boucle, en fonction du temps écoulé, si bien que la vitesse reste constante quelle que soit la cadence d'exécution de loop(). Une vitesse de 60 degrés par seconde paraît posée ; 300 donne une impression de vivacité.

Pourquoi les limites sont dans le firmware

MIN_ANGLE et MAX_ANGLE doivent correspondre à la course physique de ce que le servo entraîne. Si une application envoie 0 et que votre tringlerie bute à 8 degrés, le servo cale, chauffe et tire son courant de blocage jusqu'à ce que quelque chose cède. Borner les valeurs sur l'appareil garantit qu'une faute de frappe dans un tableau de bord ne pourra jamais endommager le matériel. Traitez chaque commande MQTT comme une entrée non fiable.

Envoyer des commandes et observer le retour

Abonnez-vous au préfixe de l'appareil :

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4c81d2 -v -t "connectmq/demo/servo-4c81d2/#"

Puis déplacez le servo :

mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-4c81d2 -t "connectmq/demo/servo-4c81d2/servo/set" -m 150
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-4c81d2 -t "connectmq/demo/servo-4c81d2/servo/set" -m '{"angle":30,"speed":40}'

Vous verrez défiler la position pendant le mouvement du servo, jusqu'à "moving":false :

connectmq/demo/servo-4c81d2/servo/position {"angle":90,"target":150,"moving":true}
connectmq/demo/servo-4c81d2/servo/position {"angle":113,"target":150,"moving":true}
connectmq/demo/servo-4c81d2/servo/position {"angle":150,"target":150,"moving":false}

Les rapports de progression sont limités à quatre par seconde. Cela vous maintient largement sous la limite du serveur MQTT public gratuit de 10 messages par seconde par client, et c'est amplement suffisant pour une jauge fluide sur un tableau de bord. N'oubliez pas que le serveur MQTT public gratuit est lisible par tous : c'est un banc d'essai, pas l'endroit pour un servo qui ouvre votre porte d'entrée.

Étalonner le servo avant de lui faire confiance

Les servos de fabricants différents, et même de lots différents d'un même modèle, font correspondre largeur d'impulsion et angle de façon légèrement différente. Consacrez cinq minutes à l'étalonnage avant de construire un support autour d'un servo :

  1. Retirez toute tringlerie pour que le palonnier tourne librement.
  2. Réglez temporairement MIN_ANGLE à 0 et MAX_ANGLE à 180, puis envoyez 0 et écoutez. Si le servo bourdonne ou continue de forcer en butée, augmentez MIN_US de 50 et réessayez jusqu'à ce qu'il repose silencieusement.
  3. Répétez à 180, en diminuant MAX_US jusqu'à ce que la position extrême soit silencieuse.
  4. Envoyez 90 et vérifiez que le palonnier pointe là où vous attendez le « centre ». Sinon, repositionnez le palonnier sur les cannelures plutôt que de compenser par logiciel.
  5. Fixez la tringlerie, approchez lentement chaque extrémité avec une vitesse faible comme {"angle":20,"speed":15}, et notez où elle commence à forcer. Réglez MIN_ANGLE et MAX_ANGLE quelques degrés en deçà de ces points.

Notez les valeurs finales dans un commentaire à côté des constantes. Quand vous remplacerez un servo cassé dans un an, vous saurez quels chiffres revérifier. L'étalonnage explique aussi pourquoi le topic de retour indique honnêtement l'angle commandé au lieu de prétendre le mesurer : ce nombre signifie « là où le firmware a dit au servo d'aller », et c'est l'étalonnage qui le fait correspondre à la réalité.

Dépannage

  1. Le servo tremble au repos. C'est généralement du bruit d'alimentation. Ajoutez le condensateur de découplage, raccourcissez les fils du servo et éloignez le fil de signal des câbles moteur. Si le servo ne supporte aucune charge, vous pouvez appeler servo.detach() une fois la cible atteinte pour arrêter complètement les impulsions ; le sketch le rattache à la commande suivante.
  2. L'ESP32 redémarre quand le servo bouge. Chute de tension. Utilisez une alimentation séparée pour le servo avec une masse commune.
  3. La course n'est que d'environ 90 degrés. Le servo attend une plage d'impulsions plus large. Ajustez MIN_US et MAX_US par petits pas ; beaucoup de servos utilisent environ 500 à 2500 microsecondes, mais dépasser les vraies butées du servo le fait bourdonner et forcer.
  4. Le servo bouge mais le retour n'arrive jamais. Vérifiez que les publications ne dépassent pas le buffer par défaut de PubSubClient et que mqtt.loop() s'exécute assez souvent. Un delay() bloquant n'importe où dans loop() casse à la fois la rampe et MQTT.
  5. Commandes ignorées. Le JSON doit contenir un angle entier. Les chaînes comme "angle":"90" sont volontairement rejetées.
  6. Deux servos interfèrent. Chacun a besoin de sa propre broche ; ESP32Servo attribue automatiquement les canaux PWM, mais appelez allocateTimer pour les timers que vous comptez utiliser.

De la démo à un vrai déploiement

Une fois le servo fonctionnel, sortez-le de la zone publique. Créez un broker dans le portail ConnectMQ, copiez le nom d'utilisateur, le mot de passe et le client ID générés, puis connectez-vous en TLS sur le port 8883 avec WiFiClientSecure. Le guide ESP32 MQTT SSL détaille pas à pas la configuration des certificats.

Pour tout ce qui bouge, la disponibilité compte. Un broker MQTT managé comme ConnectMQ donne à chaque appareil ses propres identifiants et vous permet de suivre servo/position dans l'explorateur de topics en direct du portail pendant que vous ajustez les vitesses. Le même schéma commande/état se transpose directement au pilotage de relais avec ESP32 et MQTT et aux moteurs pas à pas pilotés par MQTT, l'étape suivante lorsque vous avez besoin de plus de couple ou d'une rotation continue avec un positionnement précis.

Démarrer avec ConnectMQ

Inscrivez-vous sur app.connectmq.com et créez un broker Serverless gratuit, sans carte bancaire. Vous obtenez immédiatement des identifiants TLS, un client MQTT dans le navigateur pour envoyer des commandes au servo et un tableau de bord pour suivre le retour de position en direct.

Questions fréquentes

Un servo de modélisme renvoie-t-il sa position réelle via MQTT ?

Non. Un servo standard n'a pas de sortie de position : l'ESP32 renvoie donc l'angle qu'il a commandé. Pour une position mesurée, utilisez un servo à retour ou ajoutez un codeur ou un potentiomètre lu par l'ADC de l'ESP32.

Quelles broches de l'ESP32 peuvent piloter un servo ?

N'importe quel GPIO capable de fonctionner en sortie convient, car le PWM provient du périphérique LEDC. Évitez les broches 34 à 39, uniquement en entrée, et les broches de configuration au démarrage comme 0, 2, 12 et 15.

Combien de servos un ESP32 peut-il piloter via MQTT ?

L'ESP32 d'origine dispose de 16 canaux LEDC (les variantes plus récentes comme le S3 en ont moins) : de nombreux servos sont donc possibles en principe ; la vraie limite, ce sont l'alimentation et le câblage. Utilisez un topic par servo, par exemple servo/1/set et servo/2/set.

Les commandes de servo doivent-elles utiliser le QoS 1 ?

Oui, le QoS 1 est un choix par défaut judicieux. Une commande d'angle absolu est idempotente : une livraison en double redéfinit simplement la même cible.

Articles liés

Exemple MQTT ESP-IDF : construire un client esp-mqtt à partir de zéro

Construisez un projet ESP-IDF à partir d'un dossier vide avec le composant esp-mqtt : configuration du Wi-Fi en mode station, gestionnaire d'événements MQTT, publication et abonnement, propriétés MQTT 5 et TLS avec le bundle de certificats.

9 min de lecture

Horodatage NTP ESP32 MQTT : ajouter une heure précise à chaque message

Synchronisez l'horloge de l'ESP32 avec NTP, formatez des horodatages ISO 8601 et epoch en millisecondes, et joignez-les aux payloads MQTT pour que les mesures conservent leur véritable heure de capture même lorsqu'elles sont publiées en retard.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.