Tutorial: più potenzialità nella tua progettazione con Elasticsearch e Arduino

Arduino
Elastic è la piattaforma leader a livello mondiale per le soluzioni basate sulla ricerca. Di recente, Arduino ha collaborato con Elastic su un progetto di ricerca e sviluppo basato su Portenta H7 per fornire una semplice libreria client Elasticsearch (scritta in C++) che funziona su moduli Arduino, permettendo così di comunicare con un server Elasticsearch direttamente da una scheda Arduino.

Per testare questa nuova opportunità nata dalla collaborazione tra Arduino ed Elastic, il team Arduino ha provato a sviluppare un dispositivo IoT che invia i dati di temperatura acquisiti dai sensori ogni cinque minuti ad Elastic Cloud. Questo, combinato con le funzionalità geografiche di Elasticsearch, potrebbe essere il primo passo nella costruzione di una soluzione che fornisca la temperatura media attuale da tutti i sensori a 5 km di distanza su richiesta.

Applicazione: feedback della temperatura da più dispositivi IoT

Il team Arduino ha progettato un caso d'uso per un'azienda che aveva bisogno di gestire più dispositivi IoT situati in Italia. Ogni dispositivo invia i dati provenienti dai sensori, ad esempio di temperatura, ad Elastic Cloud. Utilizzando Elastic Cloud l'azienda può gestire qualsiasi scala di dispositivi IoT, senza la necessità di gestire al contempo un'infrastruttura dedicata. Inoltre, l'azienda ha bisogno di regolare alcuni parametri interni di ciascun dispositivo dalla temperatura media dei dispositivi vicini, in un raggio di 100 km.

Questo è uno scenario tipico nelle applicazioni di ingegneria di controllo

Utilizzando Elasticsearch possiamo fornire feedback multipli attraverso funzionalità di ricerca, come filtraggio, aggregazione, multi-match, geospaziale, ricerca vettoriale (kNN), ricerca semantica e apprendimento automatico. In questo caso d'uso, viene utilizzata l'aggregazione media e la distanza geografica per recuperare tutti i dispositivi tra 100 km. Mediante l'impiego di Kibana, l'interfaccia utente disponibile in Elastic Cloud, possiamo facilmente creare una dashboard per monitorare i dati provenienti da tutti i dispositivi. Poiché abbiamo anche dati geografici, possiamo rappresentare queste informazioni su una mappa di calore creata con colori diversi che rappresentano temperature diverse.

Arduino

Configurazione di Elastic Cloud

Il primo passo è avere un account per Elastic Cloud. Se non ne hai uno, puoi registrarti sulla piattaforma per una prova. Una volta effettuato l'accesso, è possibile creare una nuova distribuzione, scegliendo la dimensione delle istanze di Elasticsearch che si desidera utilizzare. Dopo aver creato una distribuzione, è necessario recuperare l'URL dell'endpoint e generare una chiave API di Elasticsearch. È possibile leggere queste linee guida per ricevere assistenza su come ottenere queste informazioni.

Preparazione dell'indice Elasticsearch

Abbiamo bisogno di creare un indice per memorizzare i dati provenienti dalle schede Arduino. Vogliamo memorizzare i valori di temperatura, la posizione del dispositivo utilizzando la geolocalizzazione (latitudine e longitudine), un nome identificativo del dispositivo e un timestamp.

Possiamo creare un indice "temperatura" con la seguente richiesta HTTP a Elasticsearch:

PUT /temperature
{
  "mappings": {
    "properties": {
      "temperature": { "type": "float" }, 
      "timestamp":   { "type": "date" }, 
      "location":    { "type": "geo_point" },
      "device-id":   { "type": "keyword" }
    }
  }
}

Per inviare questa richiesta HTTP è possibile utilizzare gli strumenti di sviluppo di Kibana in Elastic Cloud. Vogliamo ora memorizzare il timestamp dell'operazione ogni volta che un dispositivo invia dati. Questa operazione può essere eseguita utilizzando la funzionalità della pipeline di inserimento di Elasticsearch. Una pipeline di inserimento è un'azione eseguita da Elasticsearch prima dell'indicizzazione (archiviazione) di un documento. Ad esempio, una pipeline può assegnare il valore di un campo documento specifico, in base a un calcolo. Nel nostro caso, dobbiamo solo memorizzare il timestamp e possiamo creare una pipeline "set-timestamp":

PUT _ingest/pipeline/set-timestamp
{
  "description": "sets the timestamp",
  "processors": [
    {
      "set": {
        "field": "timestamp",
        "value": "{{{_ingest.timestamp}}}"
      }
    }
  ]
}

Utilizzando questa pipeline possiamo quindi inviare i dati a Elasticsearch come segue:

POST /temperature/_doc?pipeline=set-timestamp
{
  "temperature": 21.45,
  "device-id": "H7-001",
  "location": {
    "type": "Point",
    "coordinates": [12.4923, 41.8903]
  }
}

Qui, il device-id H7-001 è il nome della scheda Arduino e location è il punto geografico espresso con 12.4923 (longitudine) e 41.8903 (latitudine), ovvero la posizione del Colosseo a Roma (Italia). Si noti che non è stato specificato il valore timestamp perché questo viene generato automaticamente utilizzando la pipeline "set-timestamp" (specificata nell'URL come stringa di query).

Query sulla distanza geografica

Per recuperare la temperatura media dei dispositivi a distanza fino a 100 km possiamo utilizzare la seguente query di Elasticsearch:


GET /temperature/_search
{
  "query": {
    "bool": {
      "must": {
        "match_all": {}
      },
      "filter": {
        "geo_distance": {
          "distance": "100km",
          "location": [12.4923, 41.8903]
        }
      }
    }
  },
  "aggs": {
    "avg_temp": { "avg": { "field": "temperature" } }
  }
}

Questa query restituirà un campo di aggregazione "avg_temp" contenente la temperatura media di tutti i dispositivi entro un raggio di 100 km.

Utilizzo del client Elasticsearch per Arduino

E' arrivato il momento di mostrare un pò di codice Arduino. Di seguito, è riportato un semplice sketch che invia un valore di temperatura a Elastic Cloud, ottiene la temperatura media eseguendo una query di geodistanza e attende 30 secondi. Il codice qui riportato è disponibile online nella cartella examples del repository github elastic/elasticsearch-arduino. Lo sketch utilizza un file elasticsearch_config.h come indicato di seguito:

#define WIFI_SECRET_SSID ""
#define WIFI_SECRET_PASS ""
#define ELASTIC_ENDPOINT ""
#define ELASTIC_PORT 443
#define ELASTIC_CLOUD_API_KEY ""
#define DEVICE_GEO_LON 12.4923
#define DEVICE_GEO_LAT 41.8903
#define DEVICE_ID "x"
#define DEVICE_GEO_DISTANCE "50km"

Nel nostro esempio, abbiamo utilizzato il Wi-Fi per connettere la scheda Arduino a Internet. Il WIFI_SECRET_SSID e il WIFI_SECRET_PASS sono il nome della rete SSID da utilizzare e la password Wi-Fi. L'ELASTIC_ENDPOINT è l'URL dell'endpoint Elastic Cloud, l'ELASTIC_PORT è 443 poiché Elastic Cloud utilizza TLS (https). L'ELASTIC_CLOUD_API_KEY è la chiave API da generare nell'interfaccia di amministrazione di Elastic Cloud. Questo file contiene anche altre informazioni relative al dispositivo Arduino. Abbiamo la longitudine (DEVICE_GEO_LON) e la latitudine (DEVICE_GEO_LAT), l'ID (DEVICE_ID) e la distanza (DEVICE_GEO_DISTANCE) per la geo-query. Dopo aver compilato tutte le informazioni precedenti, possiamo dare un'occhiata allo sketch, così riportato:

#include <ArduinoJson.h>
#include <WiFi.h>
#include <WiFiSSLClient.h>
#include "ESClient.h" 
#include "elasticsearch_config.h"

// WiFi settings
char ssid[] = WIFI_SECRET_SSID;
char pass[] = WIFI_SECRET_PASS;

// Elastic settings
char serverAddress[] = ELASTIC_ENDPOINT;
int serverPort = ELASTIC_PORT;

WiFiSSLClient wifi;

ESClient client = ESClient(wifi, serverAddress, serverPort); 
int status = WL_IDLE_STATUS;

void setup() {
  Serial.begin(9600);
  Serial.println("Started");

  while (status != WL_CONNECTED) {
    Serial.print("Attempting to connect to Network named: ");
    Serial.println(ssid);
  
    // Connect to WPA/WPA2 network:
    status = WiFi.begin(ssid, pass);
  }

  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);
  
  client.setElasticCloudApiKey(ELASTIC_CLOUD_API_KEY);
}

void loop() {
 
  float temperature;
  // Set the temperature from a sensor (removing the randomness)
  temperature = random(10,30) + random(0,100)/100.00;
  
  // Prepare the JSON with temperature and geopoint for Elasticsearch
  StaticJsonDocument<200> doc;
  doc["temperature"] = temperature;
  doc["device-id"] = DEVICE_ID;
  doc["location"]["type"] = "Point";
  doc["location"]["coordinates"][0] = DEVICE_GEO_LON;
  doc["location"]["coordinates"][1] = DEVICE_GEO_LAT;

  String temp;
  serializeJson(doc, temp);

  Serial.println("Sending to Elasticsearch:");
  Serial.println(temp);
  ESResponse indexResult;
  // Send the temperature to Elastic Cloud
  indexResult = client.index("temperature", temp, "pipeline=set-timestamp");
  
  DynamicJsonDocument result(1024);
  deserializeJson(result, indexResult.body);

  if (result["result"] == "created") {
    Serial.println("Created with _id: " + result["_id"].as<String>());
  } else {
    Serial.println("Error sending data: " + indexResult.body);

  }
  
  StaticJsonDocument<512> query;
  query["query"]["bool"]["filter"]["geo_distance"]["distance"] = DEVICE_GEO_DISTANCE;
  query["query"]["bool"]["filter"]["geo_distance"]["location"][0] = DEVICE_GEO_LON;
  query["query"]["bool"]["filter"]["geo_distance"]["location"][1] = DEVICE_GEO_LAT;
  query["aggs"]["avg_temp"]["avg"]["field"] = "temperature";
  query["size"] = 0;

  String search;
  serializeJson(query, search);
  Serial.println("Geo-location query:");
  Serial.println(search);

  ESResponse searchResult;
  // Send the temperature to Elastic Cloud
  searchResult = client.search("temperature", search);

  DynamicJsonDocument avg(512);
  deserializeJson(avg, searchResult.body);
  float avgTemp = avg["aggregations"]["avg_temp"]["value"];
  int numDevices = avg["hits"]["total"]["value"];
  Serial.println("Average temperature of " + String(numDevices) + " devices in " + DEVICE_GEO_DISTANCE + ": " + String(avgTemp));

  Serial.println("Wait 30 seconds");
  delay(30000);
}

Questo sketch richiede Wi-Fi, WiFiSSLClient (per la connessione tramite TLS) per la connessione a Internet, EsClient per la connessione a Elasticsearch e la libreria ArduinoJson per la serializzazione e la deserializzazione della struttura dati Json. Nella funzione setup() avviamo la connessione Wi-Fi e impostiamo la chiave API di Elastic Cloud utilizzando la chiamata di funzione client.setElasticCloudApiKey(ELASTIC_CLOUD_API_KEY). L'oggetto client viene inizializzato nell'area principale passando l'oggetto Wi-Fi, l'indirizzo del server (endpoint) e la porta HTTP. Nella funzione loop() abbiamo il codice che invia la temperatura ad Elastic Cloud. La temperatura qui è solo un numero float casuale compreso tra 10 e 30, in genere proveniente da un sensore collegato alla scheda Arduino. Per preparare il documento da inviare a Elasticsearch, abbiamo utilizzato la libreria ArduinoJson.

Per creare un oggetto "doc" è stato utilizzato il codice seguente:

StaticJsonDocument<200> doc;
doc["temperature"] = temperature;
doc["device-id"] = DEVICE_ID;
doc["location"]["type"] = "Point";
doc["location"]["coordinates"][0] = DEVICE_GEO_LON;
doc["location"]["coordinates"][1] = DEVICE_GEO_LAT;

Questo oggetto viene serializzato in una stringa JSON come indicato di seguito:

String temp;
serializeJson(doc, temp);

Finally, the document, stored in the “temp” variable, can be sent to Elasticsearch using the index API, as follows:

ESResponse indexResult;
indexResult = client.index("temperature", temp, "pipeline=set-timestamp");

This API adds the “temp” document in the index “temperature” using the “set-timestamp” pipeline. The result is stored in the “indexResult” variable that is a struct type as follows:

struct ESResponse {
    int statusCode;
    String body;
};

"statusCode" è il codice di stato HTTP della risposta e "body" è il corpo della risposta. L'operazione sull'indice ha esito positivo se la risposta contiene un campo "result" con valore "created". Per ottenere la temperatura media dei dispositivi in un raggio di 100 km, abbiamo utilizzato la seguente query di geo-distanza, espressa utilizzando ArduinoJson.

StaticJsonDocument<512> query;
query["query"]["bool"]["filter"]["geo_distance"]["distance"] = DEVICE_GEO_DISTANCE;
query["query"]["bool"]["filter"]["geo_distance"]["location"][0] = DEVICE_GEO_LON;
query["query"]["bool"]["filter"]["geo_distance"]["location"][1] = DEVICE_GEO_LAT;
query["aggs"]["avg_temp"]["avg"]["field"] = "temperature";
query["size"] = 0;

String search;
serializeJson(query, search);

ESResponse searchResult;
searchResult = client.search("temperature", search);

DynamicJsonDocument avg(512);
deserializeJson(avg, searchResult.body);
float avgTemp = avg["aggregations"]["avg_temp"]["value"];
int numDevices = avg["hits"]["total"]["value"];

La risposta della ricerca contiene la temperatura media, come valore di aggregazione. Inoltre, possiamo recuperare il numero di dispositivi recuperati dalla query utilizzando il campo ['hits']['total']['value'] nella risposta JSON di Elasticsearch.

Conclusione

Grazie alla collaborazione con Elastic, il team Arduino ha sviluppato una libreria molto semplice che permette l'utilizzo di Elasticsearch direttamente da una scheda Arduino. In poche righe di codice, possiamo inviare dati a Elasticsearch ed eseguire elaborazioni complesse, utilizzando la geolocalizzazione e altre funzionalità.

Riferimenti

Elasticsearch Platform - Find real-time answers at scale | Elastic

Elastic Cloud: Hosted Elasticsearch, Hosted Search | Elastic

Create your account - Elastic Cloud

GitHub - elastic/elasticsearch-arduino: Experimental library for connecting Arduino boards to Elasticsearch and Elastic Cloud

elasticsearch-arduino/examples at main · elastic/elasticsearch-arduino · GitHub

Scarica subito una copia gratis

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend