Usando Azure IoT hubs con ESP8266

Después de probar durante bastante tiempo puedo finalmente empezar con una serie de artículos sobre cómo usar un Azure IoT Hub desde Arduino! Este proyecto resultó un poco más complicado de lo que pensaba, estaba acostumbrado a trabajar con Raspberry PI que cuenta con un sistema operativo completo y es capaz de ejecutar código basado en .Net, pero Arduino es más simple y tiene un par de detalles interesantes que vale la pena compartir.

SDK para Arduino

Lo primero que debemos ubicar es el SDK para Arduino, Azure provee kits de desarrollo para múltiples lenguajes de programación, del enlace anterior podemos revisar la información para el SDK de C, estos enlaces nos darán suficiente información para empezar a trabajar con los IoT Hubs pero no son específicos a Arduino y más importante no nos indica cómo podemos obtener las librerías para Arduino!

Finalmente, al revisar la referencia podemos encontrar un enlace que discute cómo utilizar los IoT hubs con dispositivos físicos, este enlace contiene 3 dispositivos basados en Arduino de los cuales el basado en el Sparkfun ESP8266 Thing Dev creo que es el más fácil de seguir.

Ejemplo de Código

El ejemplo de código es el mismo para todos los dispositivos basados en Arduino, sin embargo, al ser genérico lleva muchas condicionales y puede ser complicado de entender, mi objetivo era tener un ejemplo mucho más simple y es aquí donde empecé a tener algunas sorpresas.

Epoch Time

Hay un detalle que no encontré en la documentación y es que los IoT Hubs requieren que los mensajes sean enviados con la hora del evento como uno de los campos obligatorios, en Raspberry PI esto no es un problema porque asume que el sistema operativo tiene la hora correcta y utiliza esta para llenar esta información... pero Arduino no cuenta con esta funcionalidad! Por esta razón los ejemplos tienen una función para obtener el tiempo Epoch, internamente la librería utiliza este valor cada vez que se envía un mensaje. Es muy importante inicializar este valor, si no se hace, el mensaje parece que fuera entregado correctamente pero nunca ingresa al hub, este detalle me dio un par de problemas, ya que aunque sabía que era necesario borré la llamada sin percatarme y tarde mucho en darme cuenta de esto.

iotHubClientHandle

Otra de las sorpresas al experimentar con el código es que el objeto iotHubClientHandle de tipo IOTHUB_CLIENT_LL_HANDLE es creado dentro del método loop en los ejemplos, a pesar que no parece óptimo no pensé que sería mucho problema, lo que descubrí es que la creación de este objeto dentro de ese método tarde o temprano causa un excepción dentro del ESP8266 y luego un reinicio; este es otro problema que me mantuvo investigando un tiempo, sin embargo, al sacar la definición y luego inicializarlo dentro del método setup todo funcionó sin problema.

Low Level

La librería cuenta con dos tipos de funciones, las "normales" y las de "bajo nivel" las cuales se pueden identificar fácilmente gracias a que contienen _LL_ dentro del nombre; fuera del nombre, la diferencia principal de entre los dos tipos de funciones es el manejo del buffer que contiene los mensajes, al enviar un mensaje, este no es depositado directamente en Azure, es primero almacenado en un buffer local que será utilizado para luego enviar la información a Azure. Si deciden utilizar las funciones de bajo nivel, cómo se ha hecho en el ejemplo simplificado, es importante recordar que debe llamarse la función IoTHubClient_LL_DoWork para procesar el buffer y envíar la información a la nube.

Librerías

Este es uno de los puntos que menos me gustaba los ejemplos en GitHub, con el objetivo de ser genéricos habían muchas secciones que no eran realmente necesarias, yo quería un ejemplo fácil de leer y comprender por lo que era necesario incluir sólo las librerías necesarias y eliminar todo el código condicional, para un ejemplo simple como el mostrado en este artículo sólo se requieren 3 librerías:

  1. AzureIoTHub (para interactuar con Azure)
  2. AzureIoTProtocol_HTTP (esta dependerá de nuestra elección de protocolo)
  3. ESP8266WiFi (para conectarnos a la red inalámbrica)

Serialización

Aun cuando es posible utilizar los IoT hubs sin esta funcionalidad, el código terminará siendo mucho más legible, la declaración de modelos es una forma fácil de ahorrarnos la complejidad de manejar cadenas de texto para enviar nuestra información. El único punto clave es que deben recordar incluir la función IoTHubMessage, esta puede ser copiada directamente de los ejemplos de GitHub y no necesitan realizar ningún cambio en ella; aun cuando la función no es llamada directamente en el ejemplo, la librería la utilizará para enviar la información a la nube de forma correcta.

Finalmente... El código simplificado!

El siguiente listado de código es un ejemplo simplificado para usar con un ESP8266 genérico, con el objetivo de mantener su simplicidad no se obtiene información de ningún sensor, si se requiere un ejemplo más real basta con llenar la propiedad data con la lectura de un pin o sensor.

 #include <AzureIoTHub.h>
#include <AzureIoTProtocol_HTTP.h>
#include <ESP8266WiFi.h>

BEGIN_NAMESPACE(SampleNamespace);
DECLARE_MODEL(SampleModel,
WITH_DATA(ascii_char_ptr, DeviceId),
WITH_DATA(int, data)
);
END_NAMESPACE(SampleNamespace);
const char* cn = "[Device ConnectionString]";
IOTHUB_CLIENT_LL_HANDLE iotHubClientHandle;
void setup()
{
        Serial.begin(9600);
 WiFi.begin("WirelessNetwork", "Password");
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
 }
   initTime();
 iotHubClientHandle = IoTHubClient_LL_CreateFromConnectionString(cn, HTTP_Protocol);
}
SampleModel* model;
void loop()
{
    delay(15000);
   serializer_init(NULL);
  model = CREATE_MODEL_INSTANCE(SampleNamespace, SampleModel);
    unsigned char* destination;
   size_t destinationSize;
 model->DeviceId = "device";
  model->data = 1;
 if (SERIALIZE(&destination, &destinationSize, model->DeviceId, model->data) != CODEFIRST_OK)
   {
       Serial.println("Failed to serialize\r\n");
  }
   else
   {
       IOTHUB_MESSAGE_HANDLE messageHandle = IoTHubMessage_CreateFromByteArray(destination, destinationSize);
      if (messageHandle == NULL)
     {
           printf("unable to create a new IoTHubMessage\r\n");
     }
       else
       {
           if (IoTHubClient_LL_SendEventAsync(iotHubClientHandle, messageHandle, sendCallback, (void*)1) != IOTHUB_CLIENT_OK)
          {
               Serial.println("failed to hand over the message to IoTHubClient");
          }
           else
           {
               Serial.println("IoTHubClient accepted the message for delivery\r\n");
           }
           IoTHubClient_LL_DoWork(iotHubClientHandle);
         IoTHubMessage_Destroy(messageHandle);
       }
       free((void*)destination);
  }
}
void sendCallback(IOTHUB_CLIENT_CONFIRMATION_RESULT result, void* userContextCallback)
{
 
}
static IOTHUBMESSAGE_DISPOSITION_RESULT IoTHubMessage(IOTHUB_MESSAGE_HANDLE message, void* userContextCallback)
{
 IOTHUBMESSAGE_DISPOSITION_RESULT result;
    const unsigned char* buffer;
 size_t size;
    if (IoTHubMessage_GetByteArray(message, &buffer, &size) != IOTHUB_MESSAGE_OK)
   {
       Serial.println("unable to IoTHubMessage_GetByteArray\r\n");
     result = IOTHUBMESSAGE_ABANDONED;
 }
   else
   {
       char* temp = (char*)malloc(size + 1);
     if (temp == NULL)
      {
           Serial.println("failed to malloc\r\n");
         result = IOTHUBMESSAGE_ABANDONED;
     }
       else
       {
           EXECUTE_COMMAND_RESULT executeCommandResult;
 
           (void)memcpy(temp, buffer, size);
          temp[size] = '\0';
          executeCommandResult = EXECUTE_COMMAND(userContextCallback, temp);
         result =
                (executeCommandResult == EXECUTE_COMMAND_ERROR) ? IOTHUBMESSAGE_ABANDONED :
             (executeCommandResult == EXECUTE_COMMAND_SUCCESS) ? IOTHUBMESSAGE_ACCEPTED :
                IOTHUBMESSAGE_REJECTED;
           free(temp);
     }
   }
   return result;
}
void initTime() {
  time_t epochTime;
 
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");
 
 while (true) {
     epochTime = time(NULL);
 
        if (epochTime == 0) {
          Serial.println("Fetching NTP epoch time failed! Waiting 2 seconds to retry.");
          delay(2000);
        }
       else {
         Serial.print("Fetched NTP epoch time is: ");
            Serial.println(epochTime);
          break;
     }
   }
}

Aun cuando el listado parece complejo, es mucho más simple que los ejemplos oficiales. Eso es todo por ahora, espero que les sea de utilidad, hasta la próxima!
–Rp

Comments

  • Anonymous
    November 24, 2017
    Hola, estoy tratando de compilar tu código con el IDE de Arduino (ya tengo instaladas las bibliotecas necesarias), pero me salen dos errores: error: 'SampleModel' does not name a type DECLARE_MODEL(SampleModel,error: ISO C++ forbids declaration of 'value' with no type [-fpermissive] DECLARE_MODEL(SampleModel,¿Alguna idea de cómo hacer que compile?Saludos
    • Anonymous
      January 21, 2018
      El error del tipo de datos es normal pues se trata de la definición, normalmente se queja el entorno de desarrollo pero debería compilar sin problemas, podrías decirme qué herramienta estás usando para compilar?--Rp