Un proyecto ESP32 servo MQTT convierte un servo de modelismo barato en un actuador controlado a distancia para persianas, válvulas, cabezales de cámara o comederos para mascotas. En este tutorial moverás un servo con comandos de ángulo MQTT, lo llevarás con suavidad en lugar de a golpes, aplicarás límites mecánicos en el firmware y publicarás una realimentación de posición que cualquier dashboard puede leer. Puedes probar cada paso contra broker.connectmq.com sin necesidad de cuenta.
Qué vas a construir
El ESP32 escucha comandos en un topic e informa del progreso en otro. Los comandos pueden ser un ángulo sin más o un pequeño objeto JSON con una velocidad:
| Topic | Dirección | Retenido | Payload de ejemplo |
|---|---|---|---|
connectmq/demo/<id>/servo/set |
De la app al ESP32 | No | 90 o {"angle":45,"speed":60} |
connectmq/demo/<id>/servo/position |
Del ESP32 a las apps | Sí | {"angle":72,"target":45,"moving":true} |
connectmq/demo/<id>/status |
Del ESP32 a las apps | Sí | online / offline |
Una limitación que conviene dejar clara desde el principio: un servo de modelismo estándar no informa de la posición real de su eje. La realimentación que se publica aquí es el ángulo que el firmware ha ordenado, actualizado a medida que el servo avanza hacia el objetivo. Basta para los dashboards y para confirmar que se aceptó un comando. Si necesitas la posición medida de verdad, necesitas un servo con cable de realimentación o un encoder externo, que es otro proyecto distinto.
Hardware y alimentación
- Placa de desarrollo ESP32.
- Un servo de modelismo como un SG90 (micro, engranajes de plástico) o un MG996R (tamaño estándar, engranajes metálicos).
- Una fuente de 5 V independiente para cualquier servo mayor que un micro, capaz de suministrar al menos la corriente de bloqueo del servo.
- Un condensador electrolítico de 470 µF a 1000 µF en la alimentación del servo, cerca de este.
| Cable del servo | Conectar a |
|---|---|
| Marrón / negro (GND) | GND de la fuente y GND del ESP32 |
| Rojo (V+) | Fuente de 5 V |
| Naranja / amarillo (señal) | GPIO 18 del ESP32 |
La masa común no es opcional: sin ella la señal no tiene referencia y el servo se agita de forma aleatoria. Alimentar un MG996R desde el pin de 5 V del ESP32 es la causa más habitual de reinicios por caída de tensión (brownout) en los proyectos con servos, porque el servo genera un gran pico de corriente cada vez que empieza a moverse. Casi todos los servos de modelismo aceptan la señal de 3,3 V del ESP32.
Librerías
Instala el paquete de placas esp32 y estas librerías desde el Gestor de librerías de Arduino:
- PubSubClient de Nick O'Leary, para MQTT.
- ESP32Servo de Kevin Harrington y John K. Bennett, que controla los servos a través del hardware PWM LEDC del ESP32.
- ArduinoJson de Benoit Blanchon (versión 7), para analizar el formato de comandos JSON.
La librería Servo habitual de Arduino no es compatible con el ESP32, así que ESP32Servo es la elección correcta. ¿Eres nuevo en MQTT con esta placa? Empieza por el tutorial de IoT con ESP32, que cubre la configuración de la placa y una primera publicación.
El sketch ESP32 servo 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"; // único por dispositivo
const String BASE = String("connectmq/demo/") + DEVICE_ID;
const int SERVO_PIN = 18;
const int MIN_ANGLE = 10; // límites mecánicos de TU mecanismo
const int MAX_ANGLE = 170;
const int MIN_US = 500; // ancho de pulso para 0 grados
const int MAX_US = 2400; // ancho de pulso para 180 grados
Servo servo;
WiFiClient net;
PubSubClient mqtt(net);
float currentAngle = 90;
int targetAngle = 90;
float speedDegPerSec = 90; // velocidad de rampa por defecto
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>(); // número sin más, como "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); // trama estándar de servo
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); // informe de posición final
wasMoving = false;
}
return;
}
float stepSize = speedDegPerSec * dt;
currentAngle += constrain(diff, -stepSize, stepSize);
servo.write((int)round(currentAngle));
wasMoving = true;
publishPosition(false); // progreso limitado en frecuencia
}
void loop() {
stepServo();
if (WiFi.status() != WL_CONNECTED) return;
if (!mqtt.connected()) {
if (millis() - lastAttempt > 5000) { lastAttempt = millis(); connectMqtt(); }
return;
}
mqtt.loop();
}
Por qué usar una rampa en lugar de saltar
Llamar directamente a servo.write(170) hace que el servo se lance al nuevo ángulo a toda velocidad. En un mecanismo que mueve una válvula o una tapa, eso castiga los engranajes de plástico y provoca un gran pico de corriente en la fuente. La función stepServo() mueve un poco el ángulo ordenado en cada vuelta del bucle, en función del tiempo transcurrido, de modo que la velocidad se mantiene constante con independencia de lo rápido que se ejecute loop(). Una velocidad de 60 grados por segundo parece deliberada; 300 resulta enérgica.
Por qué los límites viven en el firmware
MIN_ANGLE y MAX_ANGLE deben coincidir con el recorrido físico de lo que mueva el servo. Si una app envía 0 y tu mecanismo se atasca a 8 grados, el servo se bloquea, se calienta y consume su corriente de bloqueo hasta que algo cede. Limitar en el dispositivo garantiza que una errata en un dashboard nunca pueda dañar el hardware. Trata cada comando MQTT como una entrada no fiable.
Envía comandos y observa la realimentación
Suscríbete al prefijo del dispositivo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-4c81d2 -v -t "connectmq/demo/servo-4c81d2/#"
Después mueve el 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}'
Verás el flujo de posiciones mientras el servo se mueve, terminando con "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}
Los informes de progreso están limitados a cuatro por segundo. Eso te mantiene holgadamente dentro del límite del broker público de 10 mensajes por segundo por cliente, y es más que suficiente para un indicador fluido en un dashboard. Recuerda que el servidor MQTT público gratuito lo puede leer cualquiera, así que es un banco de pruebas y prototipos, no el lugar para un servo que abre la puerta de tu casa.
Calibra el servo antes de fiarte de él
Los servos de distintos fabricantes, e incluso de lotes diferentes del mismo modelo, traducen el ancho de pulso en ángulo de forma ligeramente distinta. Dedica cinco minutos a calibrarlo antes de construir un soporte a su alrededor:
- Quita cualquier mecanismo para que el brazo gire libremente.
- Pon temporalmente
MIN_ANGLEa 0 yMAX_ANGLEa 180, envía0y escucha. Si el servo zumba o sigue empujando en el extremo, subeMIN_USen 50 y vuelve a probar hasta que repose en silencio. - Repite con
180, bajandoMAX_UShasta que la posición final sea silenciosa. - Envía
90y comprueba que el brazo apunta donde esperas que esté el "centro". Si no, vuelve a colocar el brazo en el estriado en lugar de compensarlo por software. - Monta el mecanismo, avanza despacio hacia cada extremo con una velocidad baja, como
{"angle":20,"speed":15}, y anota dónde empieza a atascarse. FijaMIN_ANGLEyMAX_ANGLEunos grados por dentro de esos puntos.
Escribe los valores finales en un comentario junto a las constantes. Cuando sustituyas un servo roto un año después, sabrás qué números revisar. La calibración es también la razón de que el topic de realimentación informe con honestidad del ángulo ordenado en lugar de fingir que lo mide: el número significa "adonde el firmware le dijo al servo que fuera", y la calibración es lo que hace que ese número coincida con la realidad.
Solución de problemas
- El servo tiembla en reposo. Normalmente es ruido en la alimentación. Añade el condensador de desacoplo, acorta los cables del servo y aleja el cable de señal de los cables del motor. Si el servo no soporta carga, puedes llamar a
servo.detach()cuando alcance el objetivo para dejar de enviar pulsos; el sketch vuelve a conectarlo con el siguiente comando. - El ESP32 se reinicia cuando se mueve el servo. Caída de tensión. Usa una fuente independiente para el servo con masa común.
- El recorrido es de solo unos 90 grados. El servo espera un rango de pulsos más amplio. Ajusta
MIN_USyMAX_USen pasos pequeños; muchos servos usan aproximadamente de 500 a 2500 microsegundos, pero sobrepasar los topes reales del servo hace que zumbe y se fuerce. - El servo se mueve pero nunca llega la realimentación. Comprueba que las publicaciones no superen el buffer por defecto de PubSubClient y que
mqtt.loop()se ejecute con suficiente frecuencia. Undelay()bloqueante en cualquier parte deloop()rompe tanto la rampa como MQTT. - Se ignoran los comandos. El JSON debe contener un
angleentero. Las cadenas como"angle":"90"se rechazan a propósito. - Dos servos se interfieren. Cada uno necesita su propio pin;
ESP32Servoasigna los canales PWM automáticamente, pero llama aallocateTimerpara los timers que pienses usar.
De la demo a un despliegue real
Cuando el servo funcione, sácalo del área pública. Crea un broker en el portal de ConnectMQ, copia el usuario, la contraseña y el client ID generados y conéctate por TLS en el puerto 8883 con WiFiClientSecure. La guía de ESP32 MQTT con SSL muestra paso a paso la configuración del certificado.
En todo lo que se mueve, la disponibilidad importa. Un broker MQTT gestionado como ConnectMQ da a cada dispositivo sus propias credenciales y te permite observar servo/position en el explorador de topics en vivo del portal mientras ajustas las velocidades. El mismo patrón de comando y estado se traslada directamente al control de relés con ESP32 y MQTT y a los motores paso a paso controlados por MQTT, el siguiente paso cuando necesites más par o rotación continua con posicionamiento preciso. En robótica y celdas de automatización, el mismo topic servo/position puede animar un gemelo digital 3D del mecanismo en el navegador.
Empieza con ConnectMQ
Regístrate en app.connectmq.com y crea un broker Serverless gratuito, sin tarjeta de crédito. Obtienes credenciales TLS al instante, un cliente MQTT en el navegador para enviar comandos al servo y un dashboard para ver la realimentación de posición en vivo.
Preguntas frecuentes
¿Informa un servo de modelismo de su posición real por MQTT?
No. Un servo estándar no tiene salida de posición, así que el ESP32 informa del ángulo que ordenó. Para la posición medida, usa un servo con realimentación o añade un encoder o un potenciómetro leído por el ADC del ESP32.
¿Qué pines del ESP32 pueden controlar un servo?
Sirve cualquier GPIO con capacidad de salida, porque el PWM procede del periférico LEDC. Evita los pines solo de entrada 34 a 39 y los pines de arranque como 0, 2, 12 y 15.
¿Cuántos servos puede controlar un ESP32 por MQTT?
El ESP32 original tiene 16 canales LEDC (las variantes más recientes, como el S3, tienen menos), así que en principio admite muchos servos; el límite real lo ponen la alimentación y el cableado. Usa un topic por servo, por ejemplo servo/1/set y servo/2/set.
¿Deben usar QoS 1 los comandos del servo?
Sí, QoS 1 es una opción por defecto sensata. Un comando de ángulo absoluto es idempotente, así que una entrega duplicada simplemente vuelve a fijar el mismo objetivo.



