arduino-i2c-json

Enviar y recibir por I2C datos en formato Json en Arduino

En esta entrada vamos a continuar viendo con la conexión de dos procesadores como Arduino por bus I2C, viendo cómo enviar y recibir datos en formato Json.

En la entrada anterior vimos cómo conectar dos microprocesadores por I2C, cómo enviar datos desde el Master, y cómo solicitar datos a uno de los Slaves del bus.

Vimos que teníamos dos limitaciones importantes. La primera, que la comunicación era enviando o solicitando una cantidad determinada de bytes. No se puede enviar un mensaje de longitud arbitraría. La segunda es que el tamaño máximo del mensaje en Arduino es de 32 bytes (aunque puede ampliarse hasta 64).

La solución habitual es que los dispositivos implementen una estructura común que empleamos como definición del mensaje, y emplear esta estructura como elemento codificado de la misma forma en ambos procesadores para la comunicación.

Pero esto era un poco un jarro de agua fría, porque uno de los puntos más interesantes de emplear la comunicación I2C era comunicar entre procesadores de tipo distintos, como un Arduino con un ESP8266/32 o incluso una Raspberry Pi. Y, al menos en estos dos últimos, aspiramos a usar formato Json como estándar en el intercambio de datos.

¡Bueno! Pero no tiremos la toalla aún. En realidad, sí es posible y no demasiado complicado, enviar un Json de longitud arbitraria y mayor de 32 bytes por el puerto I2C.

Para ello, es tan sencillo como realizar varias peticiones al Slave, en dos fases.

  • Fase 1) Pedimos la longitud de los datos a transmitir.
  • Fase 2) Recogemos los datos en llamadas de máximo 32 bytes.

Pero lo veremos mucho mejor si hacemos un pequeño ejemplo.

Código del Master

Así, aquí tenemos el código del Master

#include "Wire.h"
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>

const byte I2C_SLAVE_ADDR = 0x20;

String response;
StaticJsonDocument<300> doc;

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

void loop()
{
  askSlave();
  if(response != "") DeserializeResponse();

}

const char ASK_FOR_LENGTH = 'L';
const char ASK_FOR_DATA = 'D';
void askSlave()
{
  response = "";

  unsigned int responseLenght = askForLength();
  if (responseLenght == 0) return;

  askForData(responseLenght);
  delay(2000);
}

unsigned int askForLength()
{
  Wire.beginTransmission(I2C_SLAVE_ADDR);
  Wire.write(ASK_FOR_LENGTH);
  Wire.endTransmission();

  Wire.requestFrom(I2C_SLAVE_ADDR, 1);
  unsigned int responseLenght = Wire.read();
  return responseLenght;
}

void askForData(unsigned int responseLenght)
{
  Wire.beginTransmission(I2C_SLAVE_ADDR);
  Wire.write(ASK_FOR_DATA);
  Wire.endTransmission();

  for (int requestIndex = 0; requestIndex <= (responseLenght / 32); requestIndex++)
  {
    Wire.requestFrom(I2C_SLAVE_ADDR, requestIndex < (responseLenght / 32) ? 32 : responseLenght % 32);
    while (Wire.available())
    {
      response += (char)Wire.read();
    }
  }
}

char*  text;
int id;
bool stat;
float value;
void DeserializeResponse()
{
  DeserializationError error = deserializeJson(doc, response);
    if (error) { return; }
 
    text = doc["text"];
    id = doc["id"];
    stat = doc["status"];
    value = doc["value"];
 
    Serial.println(text);
    Serial.println(id);
    Serial.println(stat);
    Serial.println(value);
}

Como vemos, la llamada al Slave se realiza en dos fases, contenidas en la función ‘askSlave()‘. Esta llama primero a la función ‘askForLength’. Aquí enviamos al Slave identificado por ‘I2C_SLAVE_ADDR’ un identificador ‘ASK_FOR_LENGTH’ que es simplemente el caracter ‘L’.

El Slave responderá con la longitud del mensaje que tiene que enviarnos. Si es mayor que 0 es que tiene algo que decirnos, y pasamos a la fase dos ‘askForData’.

En esta segunda fase transmitimos el identificador ‘ASK_FOR_DATA’, que es este código es el caracter ‘D’. El Slave se preparará para iniciar la fase 2 de transmisión de datos. Finalmente, realizamos la petición de datos al Slave en bloques de 32 bytes como máximo, empleando la longitud que nos ha enviado en la petición anterior.

Finalmente, en el bucle principal, comprobamos si tenemos una respuesta pendiente por procesar y, de ser así, mostramos los datos por puerto serie. En un proyecto real, por supuesto, haríamos lo que tuviéramos que hacer con estos datos.

Código del Slave

Por su parte, el código del Slave es el siguiente,

#include "Wire.h"
#include <ArduinoJson.hpp>
#include <ArduinoJson.h>
 
String json;
StaticJsonDocument<300> doc;
void SerializeObject()
{
    doc["text"] = "myText";
    doc["id"] = 10;
    doc["status"] = true;
    doc["value"] = 3.14;
 
    serializeJson(doc, json);
}

const byte I2C_SLAVE_ADDR = 0x20;

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

  SerializeObject();

  Wire.begin(I2C_SLAVE_ADDR);
  Wire.onRequest(requestEvent);
  Wire.onReceive(receiveEvent);
}

const char ASK_FOR_LENGTH = 'L';
const char ASK_FOR_DATA = 'D';

char request = ' ';
char requestIndex = 0;

void receiveEvent(int bytes)
{
  while (Wire.available())
  {
    request = (char)Wire.read();
  }
}

void requestEvent()
{
  if(request == ASK_FOR_LENGTH)
  {
    Wire.write(json.length());
    char requestIndex = 0;
  }
  if(request == ASK_FOR_DATA)
  {
    if(requestIndex < (json.length() / 32)) 
    {
      Wire.write(json.c_str() + requestIndex * 32, 32);
      requestIndex ++;
    }
    else
    {
      Wire.write(json.c_str() + requestIndex * 32, (json.length() % 32));
      requestIndex = 0;
    }
  }

}

void loop() 
{
}

El código del Slave es incluso más sencillo. En primer lugar, hemos generado la cadena de texto que queremos enviar en la variable ‘Json’ durante el principio del programa. En un proyecto real, por supuesto, regeneraríamos esta variable cuando hiciera falta y, probablemente, activaríamos un flag para saber que tenemos datos pendientes que enviar, por ejemplo.

La parte que nos importa, la comunicación. Por un lado, tenemos la función de callback ‘receiveEvent’, que se encarga de recibir los caracteres que indican la fase de la recepción ASK_FOR_LENGTH y ASK_FOR_DATA, y almacenarlo en la variable ‘request’.

Por otro lado, en el callback ‘requestEvent’ consultamos la fase actual que guardamos en la variable ‘request’. Si estamos en la fase 1, enviamos la longitud actual del fichero Json. Si es la fase 2, enviamos el fichero Json en bloques de máximo 32 bytes.

Conclusión

Hemos visto como enviar un mensaje de longitud indeterminada entre dos microprocesadores como Arduino realizando la petición desde el Master al Slave en dos fases, y dividiendo el envío en bloques de máximo 32 bytes.

Al trabajar en formato Json tendremos la ventaja de despreocuparnos por el tamaño de las variables al trabajar en distintos microprocesaores (aquí un int son 2 bytes, allí son 4, etc etc).

Por contra, como sabemos, un Arduino ‘normal’ de 8bits (Uno, Nano…) va justitos de memoria dinámica, y quizás no sean las máquinas más adecuadas para trabajar con formato Json. Otros procesadores como el ESP8266/32, o los Arduino Mega o van mucho más holgados en esta tarea.

Esto no significa que no debamos hacerlo, sólo que prestar especial cuidado al trabajar de memoria dinámica. Algo que, por otro lado, deberíamos hacer siempre en todo caso. Por ejemplo, no generéis una variable String para contener el Json enviado o recibido cada vez, si no reutilizad el mismo (como hemos hecho en el ejemplo) o acabaréis fragmentación y el programa se acabará volviendo inestable.

Si tenemos en cuenta estas consideraciones en cuanto a uso de memoria dinámica, lo cierto es que el intercambio en formato Json a través de I2C es una gran herramienta para compartir datos entre microprocesadores, y una forma estupenda de conectar un Arduino con un ESP8266/32 o una Raspberry Pi.