como-servir-un-api-rest-con-json-desde-esp8266

Cómo emplear ESP32 como servidor de un API Rest con Json

  • 5 min

Un ESP32 puede servir un API REST para recibir acciones y devolver datos en formato JSON.

El ejemplo está orientado a ESP32. En muchos casos también puede adaptarse a ESP8266 cambiando librerías y algunos detalles de pines.

Un API REST se ha convertido en un estándar en la comunicación web. De forma muy resumida, las acciones se transmiten a través de peticiones HTTP y usamos JSON para el intercambio de datos.

Cuando el ESP32 actúa como servidor, es interesante que proporcione su propio API REST como forma de comunicación. Así otros clientes pueden leer datos, enviar comandos o integrarse con el dispositivo de una forma ordenada.

Eso es lo que vamos a hacer aquí: configurar un ESP32 como servidor de un API REST, interpretar peticiones y datos para ejecutar acciones, e intercambiar información empleando la librería ArduinoJson.

Por ejemplo, podríamos devolver los datos de un sensor de temperatura o humedad. O ejecutar acciones como mover un robot, accionar un mecanismo o intercambiar información con otro dispositivo, como una Raspberry Pi u otro ESP32.

Básicamente vamos a emular un API REST sencillo, similar al que podríamos servir desde una aplicación Node.js o desde cualquier otro backend.

El código aquí empieza a ser algo más avanzado. En realidad, vamos a ver que no es tan difícil si mantenemos el proyecto ordenado.

Así que ¡Vamos allá!

Nuestro código del programa principal queda realmente sencillo, como viene siendo habitual

#include <WiFi.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <ArduinoJson.h>

#include "config.h"  // Sustituir con datos de vuestra red
#include "API.hpp"
#include "Server.hpp"
#include "ESP32_Utils.hpp"

void setup() 
{
  Serial.begin(115200);

  ConnectWiFi_STA();

  InitServer();
}

void loop() 
{
}
Copied!

La mayor parte de la ‘magia’ ocurre en el fichero ‘Server.hpp’, donde asociamos los distintos verbos y acciones disponibles en el ejemplo a las funciones correspondientes de nuestro API.

AsyncWebServer server(80);

void homeRequest(AsyncWebServerRequest *request) {
  request->send(200, "text/plain", "Hello, world");
}

void notFound(AsyncWebServerRequest *request) {
  request->send(404, "text/plain", "Not found");
}

void InitServer()
{
  server.on("/", HTTP_GET, homeRequest);
  server.on("/item", HTTP_GET, getRequest);
  server.on("/item", HTTP_POST, [](AsyncWebServerRequest * request){}, NULL, postRequest);
  server.on("/item", HTTP_PUT, [](AsyncWebServerRequest * request){}, NULL, putRequest);
  server.on("/item", HTTP_PATCH, [](AsyncWebServerRequest * request){}, NULL, patchRequest);
  server.on("/item", HTTP_DELETE, deleteRequest);
  
  server.onNotFound(notFound);

  server.begin();
    Serial.println("HTTP server started");
}
Copied!

Estas funciones del API las tenemos definidas en el fichero ‘API.hpp’ (lógico ¿verdad?).

#include "ESP32_Utils_APIREST.hpp"

const char* PARAM_FILTER = "filter";

void getAll(AsyncWebServerRequest *request)
{
  String message = "Get All";
  Serial.println(message);
  request->send(200, "text/plain", message);
}

void getFiltered(AsyncWebServerRequest *request)
{
  String message = "Get filtered by " + request->getParam(PARAM_FILTER)->value();
  Serial.println(message);
  request->send(200, "text/plain", message);
}

void getById(AsyncWebServerRequest *request)
{
  int id = GetIdFromURL(request, "/item/");

  String message = String("Get by Id ") + id;
  Serial.println(message);
  request->send(200, "text/plain", message);
}

void getRequest(AsyncWebServerRequest *request) {
  
  if (request->hasParam(PARAM_FILTER)) {
    getFiltered(request);
  }
  else if(request->url().indexOf("/item/") != -1)
  {
    getById(request);
  }
  else {
    getAll(request);
  }
}
Copied!

Finalmente, siguiendo la filosofía de la serie de realizar componentes reutilizables para mejorar la limpieza de nuestros proyectos, tenemos el fichero ‘ESP32_Utils_APIREST.hpp’ que ya usamos en la entrada anterior, con funciones útiles para nuestros API Rest.

Este fichero contiene funciones reutilizables que podemos emplear en nuestro proyecto, y que son empleadas por el fichero ‘API.hpp’ para realizar el trabajo del API Rest.

void postRequest(AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total)
{ 
  String bodyContent = GetBodyContent(data, len);
  
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, bodyContent);
  if (error) { request->send(400); return;}

  String string_data = doc["data"];
  String message = "Create " + string_data;
  Serial.println(message);
  request->send(200, "text/plain", message);
}

void patchRequest(AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total)
{
  int id = GetIdFromURL(request, "/item/");
  String bodyContent = GetBodyContent(data, len);
  
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, bodyContent);
  if (error) { request->send(400); return;}

  String string_data = doc["data"];
  String message = String("Update ") + id + " with " + string_data;
  Serial.println(message);
  request->send(200, "text/plain", message);
}

void putRequest(AsyncWebServerRequest * request, uint8_t *data, size_t len, size_t index, size_t total)
{
  int id = GetIdFromURL(request, "/item/");
  String bodyContent = GetBodyContent(data, len);
   
  StaticJsonDocument<200> doc;
  DeserializationError error = deserializeJson(doc, bodyContent);
  if (error) { request->send(400); return;}

  String string_data = doc["data"];
  String message = String("Replace ") + id + " with " + string_data;
  Serial.println(message);
  request->send(200, "text/plain", message);
}

void deleteRequest(AsyncWebServerRequest *request) {
  int id = GetIdFromURL(request, "/item/");

  String message = String("Delete ") + id;
  Serial.println(message);
  request->send(200, "text/plain", message);
}

int GetIdFromURL(AsyncWebServerRequest *request, String root)
{
  String string_id = request->url();
  string_id.replace(root, "");
  int id = string_id.toInt();
  return id;
}

String GetBodyContent(uint8_t *data, size_t len)
{
  String content = "";
  for (size_t i = 0; i < len; i++) {
    content .concat((char)data[i]);
  }
  return content;
}
Copied!

Resultado

Si cargamos este código en el ESP8266, y lanzamos las peticiones correspondientes a nuestro API Rest desde el Postman, veremos que obtenemos las respuestas correctamente.

esp8266-servir-api-rest-resultado

También podemos verificar que todo funciona correctamente desde la consola de puerto serie de Arduino.

esp8266-servir-api-rest-serial-port

Con esto ya sabemos configurar y servir un API REST correctamente estructurado desde un ESP32 usando JSON para el intercambio de datos. Podéis usar la base de este ejemplo en vuestros proyectos para proporcionar una forma normalizada de comunicación.

¿Entonces, ya hemos terminado? ¡Para nada! Consumir y, especialmente, servir API Rest es un importante hito en el camino, pero no es un final. Es más bien un buen comienzo. Aún tenemos que ver cómo emplearla en nuestros proyectos, por ejemplo, para controlar un robot o hacer un datalogger.

Esta misma base nos servirá para interactuar con páginas web servidas al cliente, paneles de control o cualquier aplicación que pueda realizar peticiones HTTP.

Descarga el código

Todo el código de esta entrada está disponible para su descarga en Github.

github-full

Versión para el ESP8266: https://github.com/luisllamasbinaburo/ESP8266-Examples

Versión para el ESP32: https://github.com/luisllamasbinaburo/ESP32-Examples