Ya te enseñé cómo enviar un mensaje simple desde la NodeMCU ESP8266 a Telegram y cómo enviar las mediciones de temperatura y humedad usando un Bot de Telegram. En este post vamos a ver cómo controlar una ESP8266 desde Telegram, encendiendo y apagando un LED con los comandos enviados.
De este modo vas a poder controlar tu tarjeta de desarrollo NodeMCU ESP8266 desde Telegram enviándole comandos para hacer cosas como girar un motor, encender un LED, un relevador, mostrar un mensaje en la LCD, etcétera; aunque en este ejemplo solo trabajaremos con el LED integrado.
Explicación del algoritmo
Básicamente vamos a alojar un Bot de Telegram en la ESP8266 consumiendo la API de Telegram en la ubicación getUpdates
, parseando el JSON resultante y revisando el comando. Recomiendo ver el siguiente vídeo para ver cómo instalar los drivers:
Como la API de Telegram se maneja con peticiones HTTP te recomiendo ver cómo hacer peticiones usando un cliente HTTP:
En resumen vamos a ver si hay actualizaciones para el Bot cada cierto tiempo y cuando encontremos una actualización vamos a revisar el comando recibido. A partir de los datos recibidos vas a poder verificar el id de usuario y el contenido del mensaje.
Recibir comandos desde Bot de Telegram
Comenzamos haciendo la petición a la URL para obtener los mensajes que nuestro Bot haya recibido:
String url = "https://api.telegram.org/bot" + tokenTelegram + "/getUpdates?limit=" + String(limite);
if (ultimoIdDeActualizacion != 0)
{
url += "&offset=" + String(ultimoIdDeActualizacion + 1);
}
if (!clienteHttp.begin(*clienteWifi, url))
{
Serial.println("Error con clienteHttp.begin");
}
Serial.println("Haciendo petición a " + url);
clienteHttp.addHeader("Content-Type", "application/json", false, true);
int httpCode = clienteHttp.GET();
Al hacer la petición vamos a obtener un JSON en el cuerpo de la respuesta; mismo que debemos decodificar. Aquí he establecido el límite de memoria en 2048; hay que ajustar este parámetro teniendo en cuenta la cantidad de actualizaciones en el límite que enviamos a la API.
Yo prefiero recibir una actualización por petición y si hay más actualizaciones las reviso en el siguiente paso del loop.
String respuestaDelServidor = clienteHttp.getString();
if (httpCode == HTTP_CODE_OK)
{
Serial.println("Petición OK");
DynamicJsonDocument documentoJson(2048);
DeserializationError errorAlDecodificarJson = deserializeJson(documentoJson, respuestaDelServidor);
if (errorAlDecodificarJson)
{
Serial.print(F("Error al parsear JSON: "));
Serial.println(errorAlDecodificarJson.c_str());
return;
}
Ahora que el JSON está decodificado ya tenemos un arreglo que podemos recorrer usando C++. Extraemos las propiedades verificando que el mensaje recibido sea uno de texto y actuamos en consecuencia:
JsonArray actualizaciones = documentoJson["result"].as<JsonArray>();
for (JsonObject actualizacion : actualizaciones)
{
int idUsuarioRemitente = actualizacion["message"]["from"]["id"];
int idActualizacion = actualizacion["update_id"];
if (actualizacion["message"].containsKey("text"))
{
const char *contenidoDelMensajeRecibido = actualizacion["message"]["text"];
// Imprime los valores en el serial
Serial.print("Id remitente: ");
Serial.println(idUsuarioRemitente);
Serial.print("Contenido del mensaje: ");
Serial.println(contenidoDelMensajeRecibido);
if (strcmp(contenidoDelMensajeRecibido, "/encenderLed") == 0)
{
encenderLed();
}
else if (strcmp(contenidoDelMensajeRecibido, "/apagarLed") == 0)
{
apagarLed();
}
// Aquí puedes manejar más comandos
}
ultimoIdDeActualizacion = idActualizacion;
}
El mensaje enviado por los usuarios al Bot está en contenidoDelMensajeRecibido
, podemos usar strcmp para comparar la cadena. En mi ejemplo, si el contenido es /encenderLed
invoco a la función encenderLed
y lo mismo para apagarlo. Puedes evaluar el contenido del mensaje para actuar según sea necesario.
Por cierto, es necesario comprobar que la propiedad message
tiene a su vez la propiedad text
con containsKey
; ya que si los usuarios envían una imagen o algo que no sea texto entonces esta propiedad no estará definida.
Funcionamiento de getUpdates
Al invocar a la siguiente URL con una petición GET:
https://api.telegram.org/botTU_TOKEN/getUpdates?limit=1&offset=123456789
Vamos a obtener un JSON parecido al siguiente, mismo que es un objeto con la propiedad ok
en true
si la petición fue exitosa y también la propiedad result
que es un arreglo de tipo Update. Cada propiedad de tipo Update tiene propiedades como message
y update_id
.
{
"ok": true,
"result": [
{
"update_id": 123456789,
"message": {
"message_id": 1234,
"from": {
"id": 123455678,
"is_bot": false,
"first_name": "Luis",
"username": "parzibyte",
"language_code": "es"
},
"chat": {
"id": 12345656,
"first_name": "Luis",
"username": "parzibyte",
"type": "private"
},
"date": 123213123,
"text": "/apagarLed",
"entities": [
{
"offset": 0,
"length": 10,
"type": "bot_command"
}
]
}
}
]
}
Entonces solo es cuestión de recorrer el result
y revisar el message.text
para el comando recibido desde Telegram y message.from.id
para el id de usuario. La propiedad update_id
de cada actualización es importante, pues debemos enviar la última update_id
en la siguiente petición (en el parámetro offset
) para decirle a Telegram que necesitamos actualizaciones más recientes que ese id.
Otro punto importante es el parámetro limit
. Con él le decimos a Telegram que solo queremos recibir una actualización por petición, ya que la memoria de la ESP8266 es limitada, por lo que es mejor parsear el JSON de una actualización a la vez. Obviamente puedes cambiar este parámetro y hacer las pruebas necesarias combinándola con la memoria indicada al crear el DynamicJsonDocument.
Poniendo todo junto
Para decodificar el JSON devuelto por la API de Bot de Telegram vas a necesitar la librería ArduinoJson. Yo utilizo PlatformIO y mi platformio.ini queda así:
[env:nodemcuv2]
platform = espressif8266
board = nodemcuv2
framework = arduino
lib_deps =
bblanchon/ArduinoJson@^6.18.5
El código completo para controlar una NodeMCU ESP8266 desde un Bot de Telegram queda como se ve a continuación, solo asegúrate de colocar correctamente las credenciales para la red Wi-Fi y tu Token de Bot de Telegram.
#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecureBearSSL.h>
#include <string.h>
// Obtenido con el @Botfather
const String tokenTelegram = "";
// Red WiFi para conectarse
const char *NOMBRE_RED_WIFI = "";
const char *CLAVE_RED_WIFI = "";
int ultimoIdDeActualizacion = 0;
int limite = 1;
void encenderLed()
{
// Sí, para encenderlo le mandas un LOW
digitalWrite(LED_BUILTIN, LOW);
}
void apagarLed()
{
// Sí, para apagarlo le mandas un HIGH
digitalWrite(LED_BUILTIN, HIGH);
}
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
Serial.begin(9600);
WiFi.mode(WIFI_STA);
WiFi.begin(NOMBRE_RED_WIFI, CLAVE_RED_WIFI);
while (WiFi.status() != WL_CONNECTED)
{
delay(1000);
Serial.printf(".");
}
}
void loop()
{
if ((WiFi.status() != WL_CONNECTED))
{
Serial.println("No hay WiFi");
return;
}
std::unique_ptr<BearSSL::WiFiClientSecure> clienteWifi(new BearSSL::WiFiClientSecure);
clienteWifi->setInsecure();
HTTPClient clienteHttp;
String url = "https://api.telegram.org/bot" + tokenTelegram + "/getUpdates?limit=" + String(limite);
if (ultimoIdDeActualizacion != 0)
{
url += "&offset=" + String(ultimoIdDeActualizacion + 1);
}
if (!clienteHttp.begin(*clienteWifi, url))
{
Serial.println("Error con clienteHttp.begin");
}
Serial.println("Haciendo petición a " + url);
clienteHttp.addHeader("Content-Type", "application/json", false, true);
int httpCode = clienteHttp.GET();
if (httpCode > 0)
{
String respuestaDelServidor = clienteHttp.getString();
if (httpCode == HTTP_CODE_OK)
{
Serial.println("Petición OK");
DynamicJsonDocument documentoJson(2048);
DeserializationError errorAlDecodificarJson = deserializeJson(documentoJson, respuestaDelServidor);
if (errorAlDecodificarJson)
{
Serial.print(F("Error al parsear JSON: "));
Serial.println(errorAlDecodificarJson.c_str());
return;
}
JsonArray actualizaciones = documentoJson["result"].as<JsonArray>();
for (JsonObject actualizacion : actualizaciones)
{
int idUsuarioRemitente = actualizacion["message"]["from"]["id"];
int idActualizacion = actualizacion["update_id"];
if (actualizacion["message"].containsKey("text"))
{
const char *contenidoDelMensajeRecibido = actualizacion["message"]["text"];
// Imprime los valores en el serial
Serial.print("Id remitente: ");
Serial.println(idUsuarioRemitente);
Serial.print("Contenido del mensaje: ");
Serial.println(contenidoDelMensajeRecibido);
if (strcmp(contenidoDelMensajeRecibido, "/encenderLed") == 0)
{
encenderLed();
}
else if (strcmp(contenidoDelMensajeRecibido, "/apagarLed") == 0)
{
apagarLed();
}
// Aquí puedes manejar más comandos
}
ultimoIdDeActualizacion = idActualizacion;
}
}
else
{
Serial.printf(
"Petición realizada pero hubo un error en el servidor. Código HTTP: %d. Respuesta: %s\n",
httpCode,
respuestaDelServidor.c_str());
}
}
else
{
// Petición Ok pero código no es 200
Serial.printf("Error haciendo petición: %s\n", clienteHttp.errorToString(httpCode).c_str());
}
clienteHttp.end();
// Esperar 10 segundos antes de enviar el siguiente mensaje
delay(10000);
}
Por cierto, me parece que ya existe una librería completa para ejecutar Bots de Telegram en Arduino y la ESP8266: https://github.com/witnessmenow/Universal-Arduino-Telegram-Bot. Yo preferí hacerlo manualmente y de manera simple para entender el proceso del funcionamiento de getUpdates
.