Ejercicio 03 — Enviar una lectura de sensor a un endpoint HTTP
Dificultad: amarillo · Módulo 10 (ESP32 IoT (MQTT / API))
Enunciado
Enviar una lectura de sensor a un endpoint HTTP (POST con JSON) usando HTTPClient. Puente conceptual a una API propia (por ejemplo, un FastAPI tuyo).
Montaje (ESP32 DevKit)
Ninguno (sensor simulado; opcional: potenciómetro/LDR en GPIO34)
Cómo se resuelve
El reto del módulo, y el que cierra el círculo: la misma idea de los dos anteriores (sacar un dato de la placa) pero por la otra vía, HTTP, y con el formato que espera un backend de verdad. Al terminar este ejercicio tienes el puente completo entre un sensor y una API.
Aquí no hace falta PubSubClient: HTTPClient.h viene con el core del ESP32, igual que WiFi.h. El WiFi es el de siempre, Wokwi-GUEST sin contraseña, red simulada pero con salida real a internet, que es lo que permite que la petición llegue de verdad a un servidor público desde la simulación.
El destino es una constante:
const char* API_URL = "https://httpbin.org/post";httpbin.org/post es un servicio de pruebas que hace una cosa muy útil: te devuelve en la respuesta exactamente lo que le has mandado. No guarda nada ni sirve para nada real, pero es el mejor espejo posible para comprobar que tu petición sale bien formada. En cuanto lo compruebes, esa línea es lo único que cambia para apuntar a tu propio servidor: algo como http://TU-SERVIDOR:8000/lecturas. Como es un endpoint público de pruebas, vale la misma advertencia que con el broker: lo que mandes ahí lo puede ver el que administre ese servicio, así que nada de datos reales.
El sensor es opcional. El pin es el GPIO34, uno de los pines solo entrada del ADC1 del ESP32 (junto con el 35, el 36 y el 39): no tienen salida ni pull-up interno, solo sirven para leer, y por eso son el sitio natural para un potenciómetro o una LDR. Si no conectas nada, el pin queda al aire y analogRead devuelve ruido, lo cual para este ejercicio da igual: lo que se está practicando es el envío, no la medida.
La conversión es la del módulo 04:
int crudo = analogRead(PIN_SENSOR);
float valor = crudo * 100.0 / 4095.0;El ADC del ESP32 tiene 12 bits, así que devuelve de 0 a 4095 para un rango de 0 a 3.3 V. Esa regla de tres lo pasa a un porcentaje de 0 a 100. Fíjate en el 100.0 y el 4095.0 con decimal: si escribieras crudo * 100 / 4095 con enteros, la división entera te daría saltos de 1 en 1 y perderías toda la resolución. El .0 obliga a hacer la cuenta en coma flotante.
El envío está encapsulado en enviarLectura(float valor), y empieza con una guarda:
if (WiFi.status() != WL_CONNECTED) return;Sin red no hay nada que intentar. Salir pronto evita quedarse esperando timeouts que bloquearían el loop().
Luego viene la secuencia HTTP, que es siempre la misma cuatro pasos:
HTTPClient http;
http.begin(API_URL);
http.addHeader("Content-Type", "application/json");
...
int code = http.POST(body);
...
http.end();begin prepara la conexión hacia esa URL (no manda nada todavía). addHeader es la línea que más gente olvida y la que más tiempo hace perder: la cabecera Content-Type: application/json es lo que le dice al servidor cómo tiene que interpretar el cuerpo. Sin ella, FastAPI recibe un montón de texto plano, no intenta parsearlo como JSON y te responde con un error de validación mientras tú miras el cuerpo y juras que el JSON está bien escrito. Y end libera el socket: si no lo llamas, a base de peticiones te quedas sin recursos.
El cuerpo se arma a mano, sin librerías de JSON:
char body[96];
snprintf(body, sizeof(body),
"{\"sensor\":\"esp32-demo\",\"valor\":%.2f}", valor);Lo que sale por el cable es literalmente {"sensor":"esp32-demo","valor":37.15}. Las barras invertidas son de C, no del JSON: en C, para meter unas comillas dentro de una cadena delimitada por comillas hay que escaparlas. El %.2f formatea el float con dos decimales, que es de sobra para un porcentaje y evita mandar quince cifras que nadie va a usar. Y el buffer de 96 bytes con sizeof en el snprintf es lo que garantiza que, pase lo que pase con el valor, no se escriba fuera del array. Para un JSON de dos campos, montarlo así es perfectamente razonable y no arrastra ninguna dependencia; cuando el cuerpo crezca o tenga campos anidados, entonces sí compensa una librería como ArduinoJson.
Ese JSON no está elegido al azar: encaja campo por campo con un modelo Pydantic de FastAPI, un sensor: str y un valor: float, que es lo que valida un endpoint POST /lecturas antes de guardarlo. Esa es toda la distancia que hay entre este ejercicio y un backend real.
La respuesta merece atención:
int code = http.POST(body);
Serial.print("POST -> HTTP ");
Serial.println(code);
if (code > 0) {
Serial.println(http.getString());
}POST devuelve el código HTTP cuando ha habido conversación: 200 o 201 significan que el servidor aceptó, un 4xx que la petición estaba mal (URL equivocada, cuerpo que no valida) y un 5xx que el problema es suyo. Pero si el número es negativo no hubo conversación en absoluto: no se resolvió el DNS, no se pudo abrir el socket, falló el TLS. Esa distinción es media depuración: un número negativo te manda a mirar la red y la URL, un 4xx te manda a mirar el cuerpo y las cabeceras. Por eso el http.getString() está dentro del if (code > 0): solo tiene sentido leer la respuesta si la hay, y con httpbin esa respuesta te devuelve tu propio JSON, así que ahí ves con tus ojos si lo que mandaste era lo que creías.
El loop() repite el ciclo cada 5000 ms con el patrón millis() de siempre, más espaciado que el ej01 porque una petición HTTP completa cuesta bastante más que publicar en MQTT: hay que abrir el socket, negociar TLS (httpbin es HTTPS, y el cifrado cuesta CPU y tiempo en un microcontrolador), mandar cabeceras, esperar la respuesta y cerrar. Cinco segundos entre lecturas también da tiempo a leer con calma lo que aparece en el monitor serie a 115200 baudios.
Y ahí está la comparación que da sentido al módulo. Este ejercicio hace lo mismo que el ej01 (sacar un número de la placa cada pocos segundos) pero abriendo y cerrando una conexión entera cada vez, mandando cabeceras que pesan más que el dato y hablando con un servidor concreto cuya dirección tiene que conocer. A cambio, se integra con cualquier API REST del mundo sin que nadie tenga que montar un broker. Multiplica esto por cincuenta sensores y verás por qué existe MQTT; déjalo en un dispositivo que reporta cada cinco minutos a un backend que ya existe y verás por qué HTTP sigue siendo la respuesta correcta la mayoría de las veces.
Para practicar — cópialo y complétalo
Pega este esqueleto y completa los TODO. Es la mejor forma de aprender: inténtalo antes de mirar la solución.
/*
* Curso de Arduino — Módulo 10: ESP32 IoT (MQTT / API)
* Ejercicio 03 — PRÁCTICA (rellena los TODO)
* Enunciado: Enviar una lectura de sensor a un endpoint HTTP (POST con JSON)
* usando HTTPClient. Puente conceptual a una API propia (por ejemplo, un FastAPI tuyo).
* Dificultad: amarillo
* Placa: ESP32 DevKit Montaje: ninguno (sensor simulado; opcional: potenciómetro/LDR en GPIO34)
* Correr: Arduino IDE / Wokwi (wokwi/ej03) / arduino-cli (ver README)
* NOTA: en Wokwi la red es SSID "Wokwi-GUEST" SIN contraseña.
* Para probar el POST puedes usar API_URL = "https://httpbin.org/post".
*/
#include <WiFi.h>
// TODO: incluye el cliente HTTP -> #include <HTTPClient.h>
const char* SSID = "Wokwi-GUEST";
const char* PASSWORD = "";
const char* API_URL = "https://httpbin.org/post"; // cambia por la URL de tu propia API
const int PIN_SENSOR = 34;
unsigned long ultimo = 0;
void conectarWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
Serial.print(" IP="); Serial.println(WiFi.localIP());
}
void enviarLectura(float valor) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
// TODO: abre la conexión -> http.begin(API_URL)
// TODO: añade la cabecera -> http.addHeader("Content-Type", "application/json")
char body[96];
snprintf(body, sizeof(body),
"{\"sensor\":\"esp32-demo\",\"valor\":%.2f}", valor);
// TODO: haz el POST y guarda el código -> int code = http.POST(body);
// TODO: imprime code (y opcional http.getString())
// TODO: cierra -> http.end()
}
void setup() {
Serial.begin(115200);
delay(100);
conectarWiFi();
}
void loop() {
if (millis() - ultimo >= 5000) {
ultimo = millis();
int crudo = analogRead(PIN_SENSOR);
float valor = crudo * 100.0 / 4095.0; // ADC 0..4095 -> 0..100 %
Serial.print("Lectura="); Serial.println(valor);
enviarLectura(valor);
}
}Solución — cópiala y ejecútala
/*
* Curso de Arduino — Módulo 10: ESP32 IoT (MQTT / API)
* Ejercicio 03 — MODELO (resuelto)
* Enunciado: Enviar una lectura de sensor a un endpoint HTTP (POST con JSON)
* usando HTTPClient. Puente conceptual a una API propia (por ejemplo, un FastAPI tuyo).
* Dificultad: amarillo
* Placa: ESP32 DevKit Montaje: ninguno (sensor simulado; opcional: potenciómetro/LDR en GPIO34)
* Correr: Arduino IDE / Wokwi (wokwi/ej03) / arduino-cli (ver README)
* NOTA: en Wokwi la red es SSID "Wokwi-GUEST" SIN contraseña.
* API_URL es de ejemplo: apunta a tu propia API. httpbin.org/post
* sirve para PROBAR que el POST sale bien (te devuelve lo que enviaste).
* El JSON se arma a mano para no depender de librerías extra.
*/
#include <WiFi.h>
#include <HTTPClient.h> // cliente HTTP del core ESP32
const char* SSID = "Wokwi-GUEST";
const char* PASSWORD = "";
// Endpoint destino. En producción sería algo como
// "http://TU-SERVIDOR:8000/lecturas" (tu propia API).
const char* API_URL = "https://httpbin.org/post";
const int PIN_SENSOR = 34; // ADC1: pin de solo entrada del ESP32 (opcional)
unsigned long ultimo = 0;
void conectarWiFi() {
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
Serial.print("Conectando WiFi");
while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
Serial.print(" OK IP=");
Serial.println(WiFi.localIP());
}
void enviarLectura(float valor) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
http.begin(API_URL); // abre la conexión al endpoint
http.addHeader("Content-Type", "application/json"); // avisamos: el cuerpo es JSON
// Cuerpo JSON armado a mano. FastAPI lo parsea a un modelo Pydantic.
char body[96];
snprintf(body, sizeof(body),
"{\"sensor\":\"esp32-demo\",\"valor\":%.2f}", valor);
int code = http.POST(body); // hace el POST y devuelve el código HTTP
Serial.print("POST -> HTTP ");
Serial.println(code); // 200/201 = ok ; <0 = error de conexión
if (code > 0) {
Serial.println(http.getString()); // respuesta del servidor (para depurar)
}
http.end(); // libera el socket
}
void setup() {
Serial.begin(115200);
delay(100);
conectarWiFi();
}
void loop() {
if (millis() - ultimo >= 5000) { // cada 5 s
ultimo = millis();
// Lectura simulada: ADC 0..4095 -> 0..100 (%). Sin sensor da ruido, sirve igual.
int crudo = analogRead(PIN_SENSOR);
float valor = crudo * 100.0 / 4095.0;
Serial.print("Lectura=");
Serial.println(valor);
enviarLectura(valor);
}
}Cómo ejecutar la simulación
Sin instalar nada, en el navegador
- Abre wokwi.com y crea un proyecto nuevo de la placa que pide el ejercicio.
- Pega la solución (o tu intento) en la pestaña
sketch.ino. - Abre la pestaña
diagram.json, borra lo que haya y pega el circuito de aquí abajo. - Dale al play. Verás el circuito funcionando: LEDs, pantallas y monitor serie.
Circuito del ejercicio (diagram.json)
{
"version": 1,
"author": "ESIBot",
"editor": "wokwi",
"parts": [
{ "type": "board-esp32-devkit-c-v4", "id": "esp", "top": 0, "left": 0, "attrs": {} }
],
"connections": [],
"dependencies": {}
}En VS Code, con el repo del curso
El paquete de simulación de este ejercicio está en practica/10-esp32-iot-mqtt/wokwi/ej03/. Con la extensión Wokwi instalada y la licencia activada (lo cuenta el onboarding del curso):
make sim EJ=10-esp32-iot-mqtt/ej03y después, en VS Code, Ctrl+Shift+P → Wokwi: Start Simulator. La extensión no compila: simula un firmware ya compilado, y de compilarlo se encarga ese make.
Cómo usarlo
Con placa: abre el .ino en el Arduino IDE, elige placa y puerto y pulsa Subir. Sin placa: simulalo en Wokwi con cualquiera de las dos vias de arriba. Antes de mirar la solucion, intenta completar tu el esqueleto: es la mejor forma de aprender.