ESP32 et Google Cloud IoT

L’objectif est déployer un ESP32 dans le cloud de Google et de découvrir les différents services pour exploiter les données reccueillies.

Google Cloud Platform (GCP) est une plateforme de cloud computing fournie par Google.

Cloud IoT Core est un service pour gérer des données issues d’appareils répartis partout dans le monde. Associé à d’autres services de la plate-forme Cloud IoT, Cloud IoT Core constitue une solution complète pour collecter, traiter, analyser et visualiser des données IoT en temps réel.

  • Gestionnaire d’appareils : Le gestionnaire d’appareils permet de configurer et gérer les appareils individuellement. Le gestionnaire établit l’identité des appareils et fournit le mécanisme permettant de les authentifier lors de la connexion.

  • Passerelle de protocole : Elle offre une compatibilité native pour les connexions sécurisées via les protocoles standards tels que MQTT et HTTP. Cette passerelle publie dans Cloud Pub/Sub les données de télémétrie de tous les appareils, qui peuvent ensuite être exploitées par les systèmes d’analyse en aval.

  • Sécurité : Cloud IoT Core utilise une authentification par clés asymétriques via le protocole TLS 1.2.

Exemple :

Il est possible d’utiliser gratuitement les produits Google Cloud Platform jusqu’aux limites spécifiées :

L’essai gratuit comprend un crédit de 300 $ valable pendant 12 mois qui permet de payer les ressources utilisées au cours de l’apprentissage de Google Cloud.

L’utilisation des services de Google Cloud Platform peut se faire à partir de la console web ou en mode CLI avec Google SDK Cloud.

Google Cloud SDK

Le SDK Cloud est un ensemble d’outils en ligne de commande conçu pour développer et interagir avec Google Cloud. Ces outils peuvent servir à accéder à Compute Engine, Cloud Storage, BigQuery ainsi qu’à d’autres produits et services Google Cloud, directement à partir de la ligne de commande.

Lien : SDK Cloud

→ Installation du SDK Google Cloud : Debian et Ubuntu

Ici, on pourra utiliser l’image Google Cloud SDK à partir de docker :

$ docker pull google/cloud-sdk:latest

$ docker run -ti  google/cloud-sdk:latest gcloud version
$ docker run -ti -e CLOUDSDK_CONFIG=/config/mygcloud -v /home/tv/mygcloud/config:/config/mygcloud -v /home/tv/mygcloud:/certs  google/cloud-sdk:latest /bin/bash

Il faut tout d’abord s’authentifier :

root@7d58a5a1f233:/# gcloud auth login
...
You are now logged in as [thierry.vaira@gmail.com].

root@7d58a5a1f233:/# gcloud auth list
     Credentialed Accounts
ACTIVE  ACCOUNT
*       thierry.vaira@gmail.com

root@7d58a5a1f233:/# gcloud config list
[core]
account = thierry.vaira@gmail.com
disable_usage_reporting = True

Your active configuration is: [default]

root@7d58a5a1f233:/# gcloud projects list
PROJECT_ID                 NAME                 PROJECT_NUMBER

On peut aussi utiliser Cloud Shell :

L’utilisation de Google Cloud Shell est gratuite.

Google Cloud IoT Core

Documentation : Google Cloud IoT Core

Remarque : Tous les services de Google Cloud Platform sont liés à un projet.

On commence donc par créer un nouveau projet :

root@7d58a5a1f233:/# gcloud projects create test-esp32-iot

root@7d58a5a1f233:/# gcloud config set project test-esp32-iot

root@7d58a5a1f233:/# gcloud projects list
PROJECT_ID                 NAME                 PROJECT_NUMBER
test-esp32-iot             test-esp32-iot       580197265786

On vérifie la création du projet dans la console web :

Puis, on active le service Pub/Sub :

root@7d58a5a1f233:/# gcloud services enable pubsub.googleapis.com

Pour continuer, il faut activer l’API IoT Core et lui associer un compte de facturation :

La tarification est la suivante :

root@7d58a5a1f233:/# gcloud services enable cloudiot.googleapis.com

root@7d58a5a1f233:/# gcloud projects add-iam-policy-binding test-esp32-iot --member=serviceAccount:cloud-iot@system.gserviceaccount.com --role=roles/pubsub.publisher

Pour que les messages sur le Cloud IoT Core soient automatiquement transférés vers Pub/Sub, il faut créer un topic puis un abonnement :

root@7d58a5a1f233:/# gcloud pubsub topics create esp32-iot-capteurs
Created topic [projects/test-esp32-iot/topics/esp32-iot-capteurs].
root@7d58a5a1f233:/# gcloud pubsub subscriptions create esp32-iot-abonnement --topic esp32-iot-capteurs
Created subscription [projects/test-esp32-iot/subscriptions/esp32-iot-abonnement].

On vérifie les créations dans la console web :

Il est déjà possible de tester Pub/Sub en publiant un message sur le topic :

root@7d58a5a1f233:/# gcloud pubsub subscriptions pull --auto-ack esp32-iot-abonnement --limit=1
┌──────┬──────────────────┬────────────┬──────────────────┐
│ DATA │    MESSAGE_ID    │ ATTRIBUTES │ DELIVERY_ATTEMPT │
├──────┼──────────────────┼────────────┼──────────────────┤
│ test │ 1196227930592791 │            │                  │
└──────┴──────────────────┴────────────┴──────────────────┘

Il faut maintenant créer un registre d’appareils. Cela va permettre de regrouper un ensemble d’ appareils et de leur définir des propriétés communes (protocoles comme MQTT, HTTP, l’emplacement de stockage des données et les topics Pub/Sub, …) :

root@7d58a5a1f233:/# gcloud iot registries create mes-esp32-iot --region=europe-west1  --event-notification-config=topic=esp32-iot-capteurs --enable-mqtt-config --enable-http-config
Created registry [mes-esp32-iot].

On obtient :

Il est maintenant possible de créer un périphérique (ici l’ESP32) dans le registre d’appareils.

Les appareils s’authentifieront à partir d’un jeton JWT (Json Web Token). Cloud IoT Core utilise l’authentification par une paire de clés asymétriques et prend en charge les algorithmes RSA et Elliptic Curve : la clé privée est stockée sur l’appareil et la clé publique sur la plateforme Google.

Il faut tout d’abord générer des paires de clés :

root@7d58a5a1f233:/# openssl ecparam -genkey -name prime256v1 -noout -out ec_private.pem
root@7d58a5a1f233:/# openssl ec -in ec_private.pem -pubout -out ec_public.pem
root@7d58a5a1f233:/# ls -l *.pem
-rw------- 1 root root 227 Jun 12 05:37 ec_private.pem
-rw-r--r-- 1 root root 178 Jun 12 05:37 ec_public.pem

On crée un nouvel appareil à l’aide de la clé publique nouvellement générée :

root@7d58a5a1f233:/# gcloud iot devices create esp32-1 --region=europe-west1 --registry=mes-esp32-iot --public-key="path=./ec_public.pem,type=es256"
Created device [esp32-1].

L’appareil esp32-1 est bien créé :

Un JWT comprend trois parties :

  • un en-tête précisant l’algorithme utilisé pour générer la signature du JWT ainsi que le type de jeton utilisé.
  • une charge utile (payload) comprenant l’identifiant du projet (aud), la date d’émission du jeton (iat) et la date d’expiration (exp).
  • une signature sur les deux parties précédentes utilisant la clé privée de l’appareil est calculée à partir de l’algorithme précisé dans l’en-tête.

L’en-tête et la charge utile sont des objets JSON, qui sont sérialisés en octets UTF-8, puis encodés en base64url. Au final, l’en-tête, la charge utile et la signature du JWT sont concaténés avec des points ‘.’. Le résultat constitue le JWT.

Liens : jwt.io et Using JSON Web Tokens

Exemple de création d’un JWT :

root@7d58a5a1f233:/# pip3 install pyjwt
Collecting pyjwt
  Downloading PyJWT-1.7.1-py2.py3-none-any.whl (18 kB)
Installing collected packages: pyjwt
Successfully installed pyjwt-1.7.1

root@7d58a5a1f233:/# pip3 install cryptography

// Si besoin :
root@7d58a5a1f233:/# apt-get update
root@7d58a5a1f233:/# apt install vim

root@7d58a5a1f233:/# vim main.py
#!/usr/bin/env python

import datetime
import jwt

PROJECT_ID = 'test-esp32-iot'
PRIVATE_KEY_FILE = './ec_private.pem'
ALGORITHM = 'ES256'

jeton = {
    'iat': datetime.datetime.utcnow(),
    'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=60),
    'aud': PROJECT_ID
}

with open(PRIVATE_KEY_FILE, 'r') as fichier:
    cle_privee = fichier.read()

print('JWT :')
print(jwt.encode(jeton, cle_privee, algorithm=ALGORITHM))
root@7d58a5a1f233:/# python3 main.py
JWT :
b'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1OTIwMjYxMDgsImV4cCI6MTU5MjAyOTcwOCwiYXVkIjoidGVzdC1lc3AzMi1pb3QifQ.QjGkkWh92iP5FNCdIzmIoflCsOr-XOj3cu16ge7d8HNp4C7CLhwtESG34gcKJ5U402aGnmPveLedF1rZIqvsSw'

Test HTTP : le JWT doit être inclus dans l’en-tête de chaque demande HTTP.

root@7d58a5a1f233:/# curl -X POST -H 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1OTIwMjY1MDYsImV4cCI6MTU5MjAzMDEwNiwiYXVkIjoidGVzdC1lc3AzMi1pb3QifQ.rjxqfhdxFNX402v33AF_U9xt-yEwWFvNiwU13MeTa5iGDJRlPPxDqprsuYfijfDB9yFfdI4OZS-2ztbd9SM6Fw' -H 'content-type: application/json' -H 'cache-control: no-cache' --data '{"binary_data": "aGVsbG8K"}' 'https://cloudiotdevice.googleapis.com/v1/projects/test-esp32-iot/locations/europe-west1/registries/mes-esp32-iot/devices/esp32-1:publishEvent'

root@7d58a5a1f233:/# gcloud pubsub subscriptions pull --auto-ack esp32-iot-abonnement --limit=1
┌───────┬──────────────────┬─────────────────────────────────────┬──────────────────┐
│  DATA │    MESSAGE_ID    │              ATTRIBUTES             │ DELIVERY_ATTEMPT │
├───────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ hello │ 1287053429139632 │ deviceId=esp32-1                    │                  │
│       │                  │ deviceNumId=2903960430253926        │                  │
│       │                  │ deviceRegistryId=mes-esp32-iot      │                  │
│       │                  │ deviceRegistryLocation=europe-west1 │                  │
│       │                  │ projectId=test-esp32-iot            │                  │
│       │                  │ subFolder=                          │                  │
└───────┴──────────────────┴─────────────────────────────────────┴──────────────────┘

Test MQTT : le JWT doit être transmis dans le password lors d’une connexion en MQTT.

root@7d58a5a1f233:/# apt-get install mosquitto-clients

root@7d58a5a1f233:/# mosquitto_pub --host mqtt.googleapis.com --port 8883 --id projects/test-esp32-iot/locations/europe-west1/registries/mes-esp32-iot/devices/esp32-1 --username unused --pw "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJpYXQiOjE1OTIwMjY1MDYsImV4cCI6MTU5MjAzMDEwNiwiYXVkIjoidGVzdC1lc3AzMi1pb3QifQ.rjxqfhdxFNX402v33AF_U9xt-yEwWFvNiwU13MeTa5iGDJRlPPxDqprsuYfijfDB9yFfdI4OZS-2ztbd9SM6Fw" --cafile ./roots.pem --tls-version tlsv1.2 --protocol-version mqttv311 --debug --qos 1 --topic /devices/esp32-1/events --message "Hello MQTT"

root@7d58a5a1f233:/# gcloud pubsub subscriptions pull --auto-ack esp32-iot-abonnement --limit=1
┌────────────┬──────────────────┬─────────────────────────────────────┬──────────────────┐
│    DATA    │    MESSAGE_ID    │              ATTRIBUTES             │ DELIVERY_ATTEMPT │
├────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ Hello MQTT │ 1287053647839009 │ deviceId=esp32-1                    │                  │
│            │                  │ deviceNumId=2903960430253926        │                  │
│            │                  │ deviceRegistryId=mes-esp32-iot      │                  │
│            │                  │ deviceRegistryLocation=europe-west1 │                  │
│            │                  │ projectId=test-esp32-iot            │                  │
│            │                  │ subFolder=                          │                  │
└────────────┴──────────────────┴─────────────────────────────────────┴──────────────────┘

ESP32

L’ESP32 doit pouvoir transmettre les données de ces capteurs vers le pont MQTT de Google Cloud IoT Core. Pour cela, l’ESP32 aura besoin d’un client MQTT et de générer un JWT à partir d’une clé privée fournie.

Google Cloud IoT Core fournit le SDK Cloud IoT Device (des bibliothèques écrites en C) pour se connecter, provisionner et gérer des appareils avec Cloud IoT Core.

→ Exemple de projet : ESP-Google-IoT avec le framework ESP-IDF qui s’appuie sur freeRTOS.

Il est aussi possible d’utiliser Mongoose OS un framework IoT.

→ Exemple de projet : ESP32 Feather HUZZAH32 + Google IoT Core

De manière plus simple, il est aussi possible d’utiliser des bibliothèques pour le framework Arduino :

ESP32 : Envoyer des données de télémétrie

Pour les tests, on utilisera un ESP32 avec un Bluedot (BME2820 + TSL2591) :

Si on utilise PlatformIO, on ajouttera les bibliothèques suivantes Google Cloud IoT Core JWT, rBase64 et MQTT dans le fichier platformio.ini :

[env:lolin32]
platform = espressif32
board = lolin32
framework = arduino
lib_deps =
  ESP8266_SSD1306
  Adafruit Unified Sensor
  DHT sensor library
  Adafruit TSL2591 Library
  Adafruit BME280 Library
  Google Cloud IoT Core JWT
  rBase64
  MQTT

Il faut inclure les fichiers d’en-têtes suivants :

#include <Arduino.h>
#include <Client.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <MQTT.h>
#include <CloudIoTCore.h>
#include <CloudIoTCoreMqtt.h>
#include "ca_crt.h"

On va avoir besoin de la clé privée :

root@7d58a5a1f233:/# openssl ec -in ec_private.pem -noout -text

Ensuite, on effectue les déclarations ci-dessous (notamment en recopiant la clé privée dans le tableau private_key_str):

// Wifi
const char *ssid = "SSID";
const char *password = "cle_wifi";

// Cloud IoT
const char *project_id = "test-esp32-iot";
const char *location = "europe-west1";
const char *registry_id = "mes-esp32-iot";
const char *device_id = "esp32-1";

// NTP
const char* ntp_primary = "pool.ntp.org";
const char* ntp_secondary = "time.nist.gov";

const char* private_key_str =
  "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:"
  "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:"
  "xx:xx";

const int jwt_exp_secs = 3600; // Maximum 24H (3600*24)
const int ex_num_topics = 0;
const char* ex_topics[ex_num_topics];

Client *netClient;
CloudIoTCoreDevice *device;
CloudIoTCoreMqtt *mqtt;
MQTTClient *mqttClient;
unsigned long iss = 0;
String jwt;
unsigned long lastMillis = 0;

Il faut aussi le certificat Google que l’on place dans le fichier ca_crt.h :

// root certificate
static const uint8_t ca_crt[] PROGMEM = {
 0x30, 0x82, 0x05, 0x5a, 0x30, 0x82, 0x03, 0x42, 0xa0, 0x03, 0x02, 0x01,
 0x02, 0x02, 0x10, 0x6e, 0x47, 0xa9, 0xc5, 0x4b, 0x47, 0x0c, 0x0d, 0xec,
 ...
 0x98, 0x68, 0x66, 0x5b, 0xf1, 0xc6, 0x63, 0x47, 0x55, 0x1c, 0xba, 0xa5,
 0x08, 0x51, 0x75, 0xa6, 0x48, 0x25
};

size_t ca_crt_len = 1374;

Le code source qui permet l’envoi de données en MQTT vers le cloud :

...

String getJwt();
void setupWifi();
void connectWifi();
void publishTelemetry(String data);
void publishTelemetry(String subfolder, String data);
void connect();
void setupCloudIoT();

void messageReceived(String &topic, String &payload)
{
  Serial.println("messageReceived <- " + topic + " - " + payload);
}

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

  setupCloudIoT();

  ...
}

void loop()
{
  mqttClient->loop();
  delay(10);  // <- fixes some issues with WiFi stability

  if (!mqttClient->connected())
  {
    connect();
  }

  if (millis() - lastMillis > PERIODE_ENVOI)
  {
    lastMillis = millis();

    // réaliser une acquisition des mesures
    ...

    String payload = String("{\"timestamp\":") + time(nullptr) +
                     String(",\"temperature\":") + sonde.getTemperature() +
                     String(",\"humidite\":") + sonde.getHumidite() +
                     String("}");
    publishTelemetry(payload);
    Serial.println("publishTelemetry -> " + payload);
  }
}

String getJwt()
{
  iss = time(nullptr);
  Serial.println("Refreshing JWT");
  jwt = device->createJWT(iss, jwt_exp_secs);
  return jwt;
}

void setupWifi()
{
  WiFi.mode(WIFI_STA);
  // WiFi.setSleep(false); // May help with disconnect? Seems to have been removed from WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(100);
  }

  configTime(0, 0, ntp_primary, ntp_secondary);
  while (time(nullptr) < 1510644967)
  {
    delay(10);
  }
}

void connectWifi()
{
  while (WiFi.status() != WL_CONNECTED)
  {
    //Serial.print(".");
    delay(1000);
  }
}

///////////////////////////////
// Orchestrates various methods from preceeding code.
///////////////////////////////
void publishTelemetry(String data)
{
  mqtt->publishTelemetry(data);
}

void publishTelemetry(String subfolder, String data)
{
  mqtt->publishTelemetry(subfolder, data);
}

void connect()
{
  connectWifi();
  mqtt->mqttConnect();
}

void setupCloudIoT()
{
  device = new CloudIoTCoreDevice(project_id, location, registry_id, device_id, private_key_str);

  setupWifi();
  netClient = new WiFiClientSecure();
  mqttClient = new MQTTClient(512);
  mqttClient->setOptions(180, true, 1000); // keepAlive, cleanSession, timeout
  mqtt = new CloudIoTCoreMqtt(mqttClient, netClient, device);
  mqtt->startMQTT();
}

Remarque : la fonction messageReceived() est une fonction de callback qui sera exécutée à chaque fois qu’un message sera publié sur le topic de l’appareil (nottament pour la mise à jour de la configuration ou la réception de commandes)

Code source : ESP32-Google-IoT-Core.zip

root@7d58a5a1f233:/# gcloud pubsub subscriptions pull --auto-ack esp32-iot-abonnement --limit=100
┌───────────────────┬──────────────────┬─────────────────────────────────────┬──────────────────┐
│        DATA       │    MESSAGE_ID    │              ATTRIBUTES             │ DELIVERY_ATTEMPT │
├───────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ esp32-1-connected │ 1287403475555760 │ deviceId=esp32-1                    │                  │
│                   │                  │ deviceNumId=2903960430253926        │                  │
│                   │                  │ deviceRegistryId=mes-esp32-iot      │                  │
│                   │                  │ deviceRegistryLocation=europe-west1 │                  │
│                   │                  │ projectId=test-esp32-iot            │                  │
│                   │                  │ subFolder=events                    │                  │
└───────────────────┴──────────────────┴─────────────────────────────────────┴──────────────────┘

root@7d58a5a1f233:/# gcloud pubsub subscriptions pull --auto-ack esp32-iot-abonnement --limit=100
┌───────────────────────────────────────────────────────────┬──────────────────┬─────────────────────────────────────┬──────────────────┐
│                            DATA                           │    MESSAGE_ID    │              ATTRIBUTES             │ DELIVERY_ATTEMPT │
├───────────────────────────────────────────────────────────┼──────────────────┼─────────────────────────────────────┼──────────────────┤
│ {"timestamp":1592041880,"temperature":25.1,"humidite":54}1287402243041588 │ deviceId=esp32-1                    │                  │
│                                                           │                  │ deviceNumId=2903960430253926        │                  │
│                                                           │                  │ deviceRegistryId=mes-esp32-iot      │                  │
│                                                           │                  │ deviceRegistryLocation=europe-west1 │                  │
│                                                           │                  │ projectId=test-esp32-iot            │                  │
│                                                           │                  │ subFolder=                          │                  │
└───────────────────────────────────────────────────────────┴──────────────────┴─────────────────────────────────────┴──────────────────┘

Exemple de debug sur le moniteur série :

Connect with mqtt.2030.ltsapis.goog:8883
ClientId: projects/test-esp32-iot/locations/europe-west1/registries/mes-esp32-iot/devices/esp32-1
Waiting 60 seconds, retry will likely fail
Refreshing JWT
connected

Library connected!
publishTelemetry -> {"timestamp":1592897597,"temperature":26.6,"humidite":52}
publishTelemetry -> {"timestamp":1592897657,"temperature":26.7,"humidite":52}
...

ESP32 : Gestion de l’appareil

Chaque appareil peut fournir ou utiliser différents types d’informations :

  • Métadonnées de l’appareil : les métadonnées contiennent des informations sur un appareil. La plupart des métadonnées sont immuables ou changent rarement. L’ identifiant (ID) est une métadonnée qui identifie un appareil de manière unique. L’ID ne doit jamais changer pendant toute la durée de vie d’un appareil déployé.
  • Informations d’état : les informations d’état décrivent l’état actuel de l’appareil et non celui de l’environnement. Ces informations peuvent être accessibles en lecture/écrire. Elles sont mises à jour, mais pas fréquemment en principe.
  • Informations de configuration : ce sont des données de configuration “libres” envoyées à l’appareil. Une fois qu’une configuration a été appliquée à un appareil, celui-ci peut signaler son état. Les mises à jour de configuration sont limitées à 1 mise à jour par seconde, par appareil. La configuration des périphériques fonctionne différemment pour les ponts MQTT et HTTP. En MQTT, l’appareil doit s’abonner au topic /devices/{device-id}/config. En HTTP, l’appareil doit explicitement demander une nouvelle configuration.

Exemple de debug sur le moniteur série :

messageReceived <- /devices/esp32-1/config - your-config-data
  • Télémétrie : les données collectées par l’appareil sont désignées par le terme télémétrie. La télémétrie est une donnée accessible en lecture seule concernant l’environnement, généralement collectée à l’aide de capteurs. Ici la température et l’humidité qui sont envoyées par l’appareil.

  • Commandes : les commandes sont des actions effectuées par un appareil. Exemples de commandes : allumer une led, tourner à 360 degrés vers la droite, exécuter le cycle d’autonettoyage, … En MQTT, l’appareil doit s’abonner au topic /devices/{device-id}/commands pour recevoir des commandes.

Exemple de debug sur le moniteur série :

messageReceived <- /devices/esp32-1/commands - on
  • Informations opérationnelles : les informations opérationnelles sont les données les plus pertinentes pour le fonctionnement de l’appareil. Elles peuvent inclure des éléments tels que la température de fonctionnement du processeur et l’état de la batterie. Les informations opérationnelles peuvent être transmises sous forme de données de télémétrie ou d’état.

Vue de l’appareil dans la console web :

Voir aussi : OTA update flow for IoT devices using Google Cloud Build

Exploitation des données

Les données de l’ESP32 sont maintenant dans le cloud :

Il y a plusieurs moyens de les exploiter au sein du cloud Google avec par exemple : BigQuery, DataFlow, etc …

BigQuery

BigQuery est un système de stockage de données (sans serveur) dans le cloud de Google. Il est possible de créer des tables et d’effectuer des requêtes SQL.

BigQuery est un entrepôt de données (data warehouse). Un entrepôt peut contenir des datasets qui sont équivalents aux bases de données. Les datasets peuvent contenir des tables ou des vues.

Le service est gratuit jusqu’à 1 To de données analysées par mois et 10 Go de données stockées.

Lien : Tarification

Quelques fonctionnalités :

  • La gestion des données : créer et supprimer des tables sur la base d’un schéma JSON, importer des données au format CSV ou JSON à partir de l’espace de stockage Google.
  • Requête : les requêtes sont exprimées dans la norme du langage SQL et les résultats sont retournés en JSON avec une réponse de taille maximum de 128 MO
  • Intégration : BigQuery peut être utilisé à partir de Google Apps Script, les feuilles de calcul Google, ou n’importe quel langage qui peut travailler avec son API REST ou les bibliothèques clientes.

Création d’un dataset :

thierry_vaira@cloudshell:~ (test-esp32-iot)$ bq --location=EU mk capteurs_esp32_iot
Dataset 'test-esp32-iot:capteurs_esp32_iot' successfully created.

thierry_vaira@cloudshell:~ (test-esp32-iot)$ bq ls
      datasetId
 --------------------
  capteurs_esp32_iot

thierry_vaira@cloudshell:~ (test-esp32-iot)$ bq mk --table capteurs_esp32_iot.mesures timestamp:TIMESTAMP,temperature:FLOAT,humidite:INT64
Table 'test-esp32-iot:capteurs_esp32_iot.mesures' successfully created.

thierry_vaira@cloudshell:~ (test-esp32-iot)$ bq show test-esp32-iot:capteurs_esp32_iot.mesures
Table test-esp32-iot:capteurs_esp32_iot.mesures
   Last modified            Schema            Total Rows   Total Bytes   Expiration   Time Partitioning   Clustered Fields   Labels
 ----------------- ------------------------- ------------ ------------- ------------ ------------------- ------------------ --------
  14 Jun 09:54:45   |- timestamp: timestamp   0            0
                    |- temperature: float
                    |- humidite: integer

{
  "creationTime": "1592121285015",
  "etag": "iI7ycnk9k8NrypcEu+1qkQ==",
  "id": "test-esp32-iot:capteurs_esp32_iot.mesures",
  "kind": "bigquery#table",
  "lastModifiedTime": "1592121285066",
  "location": "EU",
  "numBytes": "0",
  "numLongTermBytes": "0",
  "numRows": "0",
  "schema": {
    "fields": [
      {
        "name": "timestamp",
        "type": "TIMESTAMP"
      },
      {
        "name": "temperature",
        "type": "FLOAT"
      },
      {
        "name": "humidite",
        "type": "INTEGER"
      }
    ]
  },
  "selfLink": "https://bigquery.googleapis.com/bigquery/v2/projects/test-esp32-iot/datasets/capteurs_esp32_iot/tables/mesures",
  "tableReference": {
    "datasetId": "capteurs_esp32_iot",
    "projectId": "test-esp32-iot",
    "tableId": "mesures"
  },
  "type": "TABLE"
}

thierry_vaira@cloudshell:~ (test-esp32-iot)$ bq query 'SELECT * FROM capteurs_esp32_iot.mesures'
Waiting on bqjob_r25a4a5d7a470355c_00000172b1d87f10_1 ... (0s) Current status: DONE

On obtient :

Remarque : pour supprimer l’ensemble des données, il faudra utiliser la commande bq rm -r capteurs_esp32_iot.

Il est possible de tester l’accès avec un script Python par exemple :

thierry_vaira@cloudshell:~ (test-esp32-iot)$ vim test-bigquery.py
from google.cloud import bigquery

def query_stackoverflow():
    client = bigquery.Client()
    query_job = client.query("""SELECT * FROM `capteurs_esp32_iot.mesures` ORDER BY timestamp DESC LIMIT 10""")
    print("Test SQL")

    results = query_job.result()  # Waits for job to complete.

    for row in results:
        print("{} : {} - {}".format(row.timestamp, row.temperature, row.humidite))

if __name__ == '__main__':
    query_stackoverflow()
thierry_vaira@cloudshell:~ (test-esp32-iot)$ python3 test-bigquery.py
Test SQL
2020-06-14 13:00:59+00:00 : 25.1 - 49
2020-06-14 12:59:59+00:00 : 25.1 - 49
2020-06-14 12:58:58+00:00 : 24.9 - 50
2020-06-14 12:57:58+00:00 : 24.8 - 50
2020-06-14 12:56:58+00:00 : 24.8 - 50
2020-06-14 12:55:58+00:00 : 24.8 - 50
2020-06-14 12:54:58+00:00 : 24.8 - 50
2020-06-14 12:53:58+00:00 : 24.8 - 50
2020-06-14 12:52:58+00:00 : 24.8 - 50
2020-06-14 12:51:58+00:00 : 24.8 - 50

Dataflow

Dataflow est une service de traitement des données par flux et par lot sans serveur. Il est possible par exemple de créer une tâche Dataflow pour récupérer les messages du topic Pub/sub et de les envoyer dans une table BigQuery ou un ficher de Cloud Storage.

Dans Pub/Sub, il faut sélectionner le topic puis créer une tâche Dataflow pour exporter les données vers une table BigQuery :

Ce modèle préproduit un pipeline de streaming qui lit les messages au format JSON d’un topic Pub/Sub, les transforme à l’aide d’une fonction JavaScript et les écrit dans une table BigQuery existante. Les messages Pub/Sub doivent être au format JSON. Par exemple, le message au format {"timestamp":1592041880,"temperature":25.1,"humidite":54} sera inséré dans la table BigQuery avec les colonnes nommées “timestamp”, “temperature” et “humidite”. Un emplacement de sortie temporaire doit exister dans Cloud Storage pour y écrire les fichiers avant l’exécution du pipeline.

Attention : une tarification horaire s’applique pour l’exécution d’une tâche Dataflow.

Cloud Functions

Cloud Functions permet d’exécuter du code dans le cloud Google sans serveur.

La tarification est basée sur le temps d’exécution de la fonction. Le service est gratuit jusqu’à 2 millions d’appels par mois (avec 400 000 Go-seconde et de 200 000 GHz-seconde de temps de calcul, ainsi que de 5 Go de trafic Internet par mois).

Lien : Tarification

La fonction peut être écrite en Go, Node.js, Java ou Python. Elle doit être associée à un déclencheur (trigger) ici Cloud Pub/Sub. Elle va permettre de créer un lien vers un autre service du cloud Google.

Il faut activer le service :

gcloud services enable cloudfunctions.googleapis.com

Ensuite, on va créer une fonction codée en Python :

thierry_vaira@cloudshell:~ (test-esp32-iot)$ mkdir esp32_pubsub_to_bigquery
thierry_vaira@cloudshell:~ (test-esp32-iot)$ vim main.py
import base64
import json
from datetime import datetime

def esp32_pubsub_to_bigquery(event, context):
    """Triggered from a message on a Cloud Pub/Sub topic.
    Args:
         event (dict): Event payload.
         context (google.cloud.functions.Context): Metadata for the event.
    """
    print("""This Function was triggered by messageId {} published at {}""".format(context.event_id, context.timestamp))

    if 'attributes' in event:
        device_id = event['attributes']['deviceId']
        print('Device Id : {}'.format(device_id))

    if 'data' in event:
        pubsub_message = base64.b64decode(event['data']).decode('utf-8')
        iot_data = json.loads(pubsub_message)
        horodatage = datetime.utcfromtimestamp(iot_data['timestamp']).strftime('%Y-%m-%dT%H:%M:%SZ')
        print('Horodatage : {}'.format(horodatage))
        print('Temperature : {}'.format(iot_data['temperature']))
        print('Humidite : {}'.format(iot_data['humidite']))
        print(pubsub_message)

Déploiement de la fonction dans le cloud :

thierry_vaira@cloudshell:~ (test-esp32-iot)$ gcloud functions deploy esp32_pubsub_to_bigquery --runtime python37 --trigger-topic esp32-iot-capteurs --region europe-west1
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: esp32_pubsub_to_bigquery
eventTrigger:
  eventType: google.pubsub.topic.publish
  failurePolicy: {}
  resource: projects/test-esp32-iot/topics/esp32-iot-capteurs
  service: pubsub.googleapis.com
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/test-esp32-iot/locations/europe-west1/functions/esp32_pubsub_to_bigquery
runtime: python37
serviceAccountEmail: test-esp32-iot@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-europe-west1-ce429b91-2e9d-414f-b699-dbdeabba8d79/4c3f1a10-9061-4c3d-876a-4b73c6a8c9e4.zip?GoogleAccessId=service-580197265786@gcf-admin-robot.iam.gserviceaccount.com&Expires=1592126704&Signature=...
status: ACTIVE
timeout: 60s
updateTime: '2020-06-14T08:55:46.043Z'
versionId: '3'

thierry_vaira@cloudshell:~ (test-esp32-iot)$ gcloud functions list
NAME                      STATUS  TRIGGER        REGION
esp32_pubsub_to_bigquery  ACTIVE  Event Trigger  europe-west1

On peut vérifier la création de la fonction :

Test : on peut envoyer un message sur le topic Pub/Sub

thierry_vaira@cloudshell:~ (test-esp32-iot)$ gcloud pubsub topics publish esp32-iot-capteurs --message "{\"timestamp\":$(date +%s),\"temperature\":25.6,\"humidite\":40}" --attribute "deviceId=test"

thierry_vaira@cloudshell:~ (test-esp32-iot)$ gcloud functions logs read --limit 50 --region europe-west1

Test ESP32 :

Cloud Functions et BigQuery

On va maintenant utiliser la Cloud Function esp32_pubsub_to_bigquery pour insérer les données du topic Pub/Sub dans la table BigQuery :

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ vim requirements.txt
google-cloud-bigquery==1.25.0

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ vim main.py
import base64
import json
from datetime import datetime
from google.cloud import bigquery

def esp32_pubsub_to_bigquery(event, context):
    """Triggered from a message on a Cloud Pub/Sub topic.
    Args:
         event (dict): Event payload.
         context (google.cloud.functions.Context): Metadata for the event.
    """

    if 'data' in event:
        pubsub_message = base64.b64decode(event['data']).decode('utf-8')
        print(pubsub_message)
        iot_data = json.loads(pubsub_message)
        client = bigquery.Client()
        table_id = "test-esp32-iot.capteurs_esp32_iot.mesures"
        table = client.get_table(table_id)
        rows_to_insert = [(iot_data['timestamp'],iot_data['temperature'],iot_data['humidite'])]
        errors = client.insert_rows(table, rows_to_insert)
        if errors == []:
            print("Donnees ajoutees")

On déploie :

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ gcloud functions deploy esp32_pubsub_to_bigquery --runtime python37 --trigger-topic esp32-iot-capteurs --region europe-west1

Liens : Google BigQuery API client library ou Python Client for Google BigQuery

Test :

thierry_vaira@cloudshell:~/esp32_pubsub_to_bigquery (test-esp32-iot)$ bq query 'SELECT * FROM capteurs_esp32_iot.mesures'
Waiting on bqjob_r3f82b4506c59d8cd_00000172b25ff491_1 ... (0s) Current status: DONE
+---------------------+-------------+----------+
|      timestamp      | temperature | humidite |
+---------------------+-------------+----------+
| 2020-06-14 10:26:42 |        24.7 |       52 |
| 2020-06-14 10:28:42 |        24.7 |       51 |
| 2020-06-14 10:25:42 |        24.7 |       53 |
| 2020-06-14 10:27:42 |        24.7 |       51 |
+---------------------+-------------+----------+

BigQuery et Google Data Studio

Google Data Studio transforme des données en tableaux de bord et rapports informatifs (à l’aide de graphiques, de filtres, des liens et des images cliquables, de textes, …).

Depuis 2017, Google Data Studio est gratuit pour tous.

Lien : 6 raisons d’utiliser Google Data Studio

Le connecteur BigQuery de Google Data Studio permet d’accéder aux données des tables BigQuery dans Google Data Studio.

Il faut accepter les conditions d’utilisation puis Ajouter des données au rapport (on sélectionne BigQuery) :

Il faut ensuite sélectionner la table :

Ensuite, on crée et paramètre son rapport …

Il est ausii possible d’ajouter d’autres sources de données :

Et de créer une requête personnalisée :

Exemple de rapport dynamique :

InfluxDB et Grafana

Google Kubernetes Engine est un environnement prêt à l’emploi pour le déploiement d’applications conteneurisées. Les applications Kubernetes sont des applications préemballées qui peuvent être déployées sur Google Kubernetes Engine en quelques clics. Ces application Kubernetes sont disponibles sur le Google Marketplace.

Il est possible d’installer les outils open source Telegraph, InfluxDB et Grafana à partir du Google Marketplace :

  • Telegraf : pour la collecte de métriques,
  • InfluxDB : pour le stockage métrique et,
  • Grafana : pour la visualisation.

Présentation de la pile TICK (Telegraf, InfluxDB, Chronograf, Kapacitor) :

  • Telegraf est un agent serveur piloté par plug-in pour collecter et envoyer des mesures et des événements à partir de bases de données, de systèmes et de capteurs IoT.

  • InfluxDB est un système de gestion de base de données orientée séries chronologiques ou temporelles hautes performances et qui fournit un langage de requête de type SQL appelé InfluxQL pour interagir avec les données.

  • Chronograf est l’interface utilisateur et le composant administratif de la plate-forme InfluxDB. Il permet de créer des tableaux de bord avec des visualisations en temps réel.

  • Kapacitor est un moteur de traitement de données natif pour InfluxDB. Il peut traiter à la fois les données de flux et les données par lots à partir d’InfluxDB, en agissant sur ces données en temps réel via son langage de programmation TICKscript. En complément d’un tableau de bord, Kapacitor permet de déclencher des actions à partir d’alertes sur un modèle de publication-abonnement (pub/sub).

Grafana est un logiciel libre qui permet la visualisation de données. Il permet de réaliser des tableaux de bord et des graphiques depuis plusieurs sources dont des bases de données temporelles comme InfluxDB. L’outil offre aussi la possibilité de mettre en place des alertes.

Finalement, on va déployer seulement InfluxDB et Grafana manuellement.

On commence par créer un cluster :

Puis, on déploie InfluxDB et Grafana en utilisant les scripts fournis à cette adresse https://github.com/ThinkBigEg/influxDB-grafana-gke :

thierry_vaira@cloudshell:~ (test-esp32-iot)$ gcloud container clusters get-credentials cluster-1 --zone europe-west1-c --project test-esp32-iot
Fetching cluster endpoint and auth data.
kubeconfig entry generated for cluster-1.

thierry_vaira@cloudshell:~ (test-esp32-iot)$ git clone "https://github.com/ThinkBigEg/influxDB-grafana-gke"
Cloning into 'influxDB-grafana-gke'...
remote: Enumerating objects: 78, done.
remote: Total 78 (delta 0), reused 0 (delta 0), pack-reused 78
Unpacking objects: 100% (78/78), done.
thierry_vaira@cloudshell:~ (test-esp32-iot)$ cd influxDB-grafana-gke/configs/
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl create -f pv-claim.yaml
persistentvolumeclaim/pv-claim created
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl create -f influxdb.yaml
deployment.apps/influxdb created
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl create -f grafana.yaml
deployment.apps/grafana created
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl create -f influxdb-internal-service.yaml
service/influxdb-internal-service created
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl create -f influxdb-external-service.yaml
service/influxdb-external-service created
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ ^C
thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl create -f grafana-service.yaml
service/grafana-external-service created

thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl get pods
NAME                        READY   STATUS    RESTARTS   AGE
grafana-77556c8c97-8nbx2    1/1     Running   0          3m18s
influxdb-596764bb57-8qz2t   1/1     Running   0          3m26s

thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl get services influxdb-external-service
NAME                        TYPE           CLUSTER-IP   EXTERNAL-IP    PORT(S)                         AGE
influxdb-external-service   LoadBalancer   10.0.4.129   35.195.198.8   8086:32524/TCP,8083:30520/TCP   6m18s

On vérifie dans la console web :

Création de la base de données InfluxDB :

Lien : Get started with InfluxDB

thierry_vaira@cloudshell:~/influxDB-grafana-gke/configs (test-esp32-iot)$ kubectl exec -it influxdb-596764bb57-8qz2t influx
kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl kubectl exec [POD] -- [COMMAND] instead.
Connected to http://localhost:8086 version 1.8.0
InfluxDB shell version: 1.8.0
> create database iot_esp32_db
> exit

Création de la fonction Python qui va insérer les données dans InfluxDB :

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ pip install influxdb

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ pip freeze | grep influxdb > requirements.txt

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ vim main.py
import base64
import json
from datetime import datetime
from influxdb import InfluxDBClient

def esp32_pubsub_to_influxdb(event, context):
    """Triggered from a message on a Cloud Pub/Sub topic.
    Args:
         event (dict): Event payload.
         context (google.cloud.functions.Context): Metadata for the event.
    """
    print("""This Function was triggered by messageId {} published at {}""".format(context.event_id, context.timestamp))

    if 'attributes' in event:
        device_id = event['attributes']['deviceId']
        print('Device Id : {}'.format(device_id))

    if 'data' in event:
        pubsub_message = base64.b64decode(event['data']).decode('utf-8')
        iot_data = json.loads(pubsub_message)
        device_id = event['attributes']['deviceId']
        horodatage = datetime.utcfromtimestamp(iot_data['timestamp']).strftime('%Y-%m-%dT%H:%M:%SZ')
        print('Horodatage : {}'.format(horodatage))
        print('Temperature : {}'.format(iot_data['temperature']))
        print('Humidite : {}'.format(iot_data['humidite']))
        print(pubsub_message)
        host = '35.195.198.8'
        port = 8086
        username = ''
        password = ''
        database = 'iot_esp32_db'
        client = InfluxDBClient(host, port, username, password, database)
        json_data = [
            {
                "measurement": "temperature",
                "time": horodatage,
                "tags": {
                    'device': device_id
                },
                "fields": {
                    "value": iot_data['temperature']
                }
            }
        ]
        result = client.write_points(json_data)
        print("Result temperature : ", result)
        json_data = [
            {
                "measurement": "humidite",
                "time": horodatage,
                "tags": {
                    'device': device_id
                },
                "fields": {
                    "value": iot_data['humidite']
                }
            }
        ]
        result = client.write_points(json_data)
        print("Result humidite : ", result)

Déploiement de la Cloud Function :

thierry_vaira@cloudshell:~/ (test-esp32-iot)$ gcloud functions deploy esp32_pubsub_to_influxdb --runtime python37 --trigger-topic esp32-iot-capteurs --region europe-west1

Vérification :

kubectl exec -it influxdb-596764bb57-8qz2t influx
Connected to http://localhost:8086 version 1.8.0
InfluxDB shell version: 1.8.0
> show DATABASES
name: databases
name
----
_internal
iot_esp32_db
> use iot_esp32_db
Using database iot_esp32_db
> select * from temperature
name: temperature
time                device  value
----                ------  -----
1592301848000000000 esp32-1 25
1592301908000000000 esp32-1 24.9
1592301968000000000 esp32-1 25
1592302028000000000 esp32-1 24.9
> select * from humidite
name: humidite
time                device  value
----                ------  -----
1592301848000000000 esp32-1 50
1592301908000000000 esp32-1 50
1592301968000000000 esp32-1 50
1592302028000000000 esp32-1 50

Dans Grafana, on se connecte en administrateur (admin/admin) :

On ajoute une source de données InfluxDB :

On paramètre en indiquant l’URL et la Database :

Et on crée un tableau de bord :

Résultat :

Le tableau de bord au format JSON (à importer) : grafana.json

InfluxDB et Qt

Il faut utiliser un objet de la classe QNetworkAccessManager pour émettre une requête GET ou POST avec un objet QNetworkRequest. L’url sera stocké dans un objet de type QUrl. On obtiendra la réponse à la requête par le signal finished(QNetworkReply*) qui passera l’adresse d’un objet QNetworkReply. On accédera au contenu de la réponse en appelant la méthode readAll().

Documentation : InfluxDB API reference

InfluxDB : [protocol]://[username:password@]host:port[/?db=database]

Test :

$ curl -G 'http://35.195.198.8:8086/query?pretty=true' --data-urlencode "db=iot_esp32_db" --data-urlencode "q=SELECT \"value\" FROM \"temperature\""

Exemple :

#include <QtWidgets>
#include <QNetworkAccessManager>
#include <QNetworkReply>

class IHM : public QWidget
{
    Q_OBJECT

public:
    IHM( QWidget *parent = 0 );
    ~IHM();

private:
  QNetworkAccessManager *manager;
  QNetworkReply         *reply;
  ...

public slots:
    void                   envoyerRequete();
    void                   replyFinished(QNetworkReply *reply);
};

...

void IHM::envoyerRequete()
{
    QString URL("http://35.195.198.8:8086/query");
    QUrl url(URL);
    QNetworkRequest request;

    request.setUrl(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");

    QUrlQuery postData;
    //postData.addQueryItem("pretty", "true");
    postData.addQueryItem("db", "iot_esp32_db");
    postData.addQueryItem("q", "SELECT value FROM temperature");

    qDebug() << Q_FUNC_INFO << request.url();
    qDebug() << Q_FUNC_INFO << postData.toString();

    manager->post(request, postData.toString(QUrl::FullyEncoded).toUtf8());
}

void IHM::replyFinished(QNetworkReply *reply)
{
    QByteArray datas = reply->readAll();
    qDebug() << Q_FUNC_INFO << datas;
    ...
}

Code source : test-qt-database-influxdb.zip

BigQuery et Apps Script

Il est possible d’effectuer des requêtes vers BigQuery à partir d’un script Google.

Par exemple dans Google Sheets :

Il faut activer l’API BigQuery :

Le script (voir bigquery.gs):

function onOpen() {
  var spreadsheet = SpreadsheetApp.getActive();
  var menuItems = [
    {name: 'Températures', functionName: 'runQueryTemperature'},
    {name: 'Humidités', functionName: 'runQueryHumidite'}
  ];
  spreadsheet.addMenu('BigQuery', menuItems);
}

function runQueryTemperature() {
  var projectId = 'test-esp32-iot';
  var request = {
    query: 'SELECT timestamp, temperature FROM capteurs_esp32_iot.mesures ORDER BY timestamp DESC LIMIT 10;'
  };
  var queryResults = BigQuery.Jobs.query(request, projectId);
  var jobId = queryResults.jobReference.jobId;

  var sleepTimeMs = 500;
  while (!queryResults.jobComplete) {
    Utilities.sleep(sleepTimeMs);
    sleepTimeMs *= 2;
    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);
  }
  
  var rows = queryResults.rows;
  while (queryResults.pageToken) {
    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {
      pageToken: queryResults.pageToken
    });
    rows = rows.concat(queryResults.rows);
  }

  if (rows) {
    var spreadsheet = SpreadsheetApp.getActive();
    var sheet = spreadsheet.getSheetByName('Températures');
    var timezone = spreadsheet.getSpreadsheetTimeZone();

    // headers
    var headers = queryResults.schema.fields.map(function(field) {
      return field.name;
    });
    sheet.appendRow(headers);

    // results
    var data = new Array(rows.length);
    for (var i = 0; i < rows.length; i++) {
      var cols = rows[i].f;
      data[i] = new Array(cols.length);
      for (var j = 0; j < cols.length; j++) {
        if(j == 0) { // timestamp
          var d = new Date();
          d.setTime(cols[j].v*1000);
          var formattedDate = Utilities.formatDate(d, timezone, "dd/MM/yyyy HH:mm:ss");
          data[i][j] = formattedDate;
        }
        else {
          data[i][j] = cols[j].v;
        }
      }
    }
    sheet.getRange(2, 1, rows.length, headers.length).setValues(data);

    Logger.log('Ok');
  } else {
    Logger.log('Aucun résultat');
  }
}

function runQueryHumidite() {
  var projectId = 'test-esp32-iot';
  var request = {
    query: 'SELECT timestamp, humidite FROM capteurs_esp32_iot.mesures ORDER BY timestamp DESC LIMIT 10;'
  };
  var queryResults = BigQuery.Jobs.query(request, projectId);
  var jobId = queryResults.jobReference.jobId;

  var sleepTimeMs = 500;
  while (!queryResults.jobComplete) {
    Utilities.sleep(sleepTimeMs);
    sleepTimeMs *= 2;
    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId);
  }
  
  var rows = queryResults.rows;
  while (queryResults.pageToken) {
    queryResults = BigQuery.Jobs.getQueryResults(projectId, jobId, {
      pageToken: queryResults.pageToken
    });
    rows = rows.concat(queryResults.rows);
  }

  if (rows) {
    var spreadsheet = SpreadsheetApp.getActive();
    var sheet = spreadsheet.getSheetByName('Humidités');
    var timezone = spreadsheet.getSpreadsheetTimeZone();

    // Append the headers.
    var headers = queryResults.schema.fields.map(function(field) {
      return field.name;
    });
    sheet.appendRow(headers);

    // Append the results.
    var data = new Array(rows.length);
    for (var i = 0; i < rows.length; i++) {
      var cols = rows[i].f;
      data[i] = new Array(cols.length);
      for (var j = 0; j < cols.length; j++) {
        if(j == 0) { // timestamp
          var d = new Date();
          d.setTime(cols[j].v*1000);
          var formattedDate = Utilities.formatDate(d, timezone, "dd/MM/yyyy HH:mm:ss");
          data[i][j] = formattedDate;
        }
        else {
          data[i][j] = cols[j].v;
        }
      }
    }
    sheet.getRange(2, 1, rows.length, headers.length).setValues(data);

    Logger.log('Ok');
  } else {
    Logger.log('Aucun résultat');
  }
}

Node-Red

Liens : Node-RED comes to GCP et Node-RED on Google Cloud Platform

Node-Red est utilisable sous forme de conteneur. Pour cela, il faut créer une instance pour le déployer :

Il faut ensuite créer une régle Pare-Feu pour autoriser le trafic entrant sur le port 1880. Après on se connecte sur http://<adresse_ip>:1880/.

Dans Manage Palette, il faut installer node-red-contrib-google-cloud :

On dispose maintenant de noeuds spécifiques à Google Cloud Platform :

Firebase

Firebase est une plateforme de développement pour les applications web et mobile. Il fournit des APIs rassemblés dans un SDK unique.

Firebase propose un ensemble d’outils hébergés : Firebase Realtime Database (base de données NoSQL et en temps réel), Firebase Storage (upload et download de fichiers) et Cloud Firestore (du contenu), Firebase Authentication (authentification et gestion des utilisateurs), Firebase Remote Config (configuration à la volée de paramètres) et Firebase Cloud Messaging (des notifications et messages), ou encore Firebase Hosting (hébergement et déploiement d’applications) …

Le service a été racheté par Google en octobre 2014.

Lien : Documentation

On va ajouter une Cloud Function pour publier les données des capteurs vers une base de données Firebase Realtime Database et ensuite les exploiter à partir d’une application web en utilisant Firebase Hosting :

On va commencer par créer un projet dans Firebase en le liant au projet de Google Cloud Platform :

On valide les étapes suivantes :

La suite (création, gestion et déploiement du projet) va être réalisé en ligne de commande avec CLI Firebase.

Lien : Firebase CLI reference

Il faut tout d’abord l’installer :

$ npm install -g firebase-tools
...
+ firebase-tools@8.4.3
added 532 packages from 354 contributors in 17.866s

Et on s’authentifie :

$ firebase login
i  Firebase optionally collects CLI usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.

? Allow Firebase to collect CLI usage and error reporting information? Yes
i  To change your data collection preference at any time, run `firebase logout` and log in again.

Visit this URL on this device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=563584335869-fgrhgmd47bqnekij5i8b5pr03ho849e6.apps.googleusercontent.com&scope=email%20openid%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloudplatformprojects.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Ffirebase%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcloud-platform&response_type=code&state=468677606&redirect_uri=http%3A%2F%2Flocalhost%3A9005

Waiting for authentication...

✔  Success! Logged in as thierry.vaira@gmail.com

On vérifie la connexion en listant les projets :

$ firebase projects:list
✔ Preparing the list of your Firebase projects
┌──────────────────────┬───────────────────────────┬────────────────┬──────────────────────┐
│ Project Display Name │ Project ID                │ Project Number │ Resource Location ID │
├──────────────────────┼───────────────────────────┼────────────────┼──────────────────────┤
│ myapplicationcamera  │ myapplicationcamera-91f51 │ 521442060522   │ [Not specified]      │
├──────────────────────┼───────────────────────────┼────────────────┼──────────────────────┤
│ test-esp32-iot       │ test-esp32-iot            │ 580197265786   │ [Not specified]      │
└──────────────────────┴───────────────────────────┴────────────────┴──────────────────────┘

2 project(s) total.

Les nombreuses tâches que l’on va effectuer à l’aide de la CLI Firebase (comme le déploiement) nécessitent un répertoire de projet :

$ mkdir test-esp32-iot
$ cd test-esp32-iot/

Pour initialiser un nouveau projet Firebase, il faut exécuter la commande suivante à partir du répertoire de projet :

$ firebase init

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /home/tv/Téléchargements/test-esp32-iot

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. Database: Deploy Firebase Realtime D
atabase Rules, Functions: Configure and deploy Cloud Functions, Hosting: Configure and deploy Firebase Hosting sites

=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: test-esp32-iot (test-esp32-iot)
i  Using project test-esp32-iot (test-esp32-iot)

=== Database Setup

Firebase Realtime Database Rules allow you to define how your data should be
structured and when your data can be read from and written to.

? What file should be used for Database Rules? database.rules.json
✔  Database Rules for test-esp32-iot have been downloaded to database.rules.json.
Future modifications to database.rules.json will update Database Rules when you run
firebase deploy.

=== Functions Setup

A functions directory will be created in your project with a Node.js
package pre-configured. Functions can be deployed with firebase deploy.

? What language would you like to use to write Cloud Functions? JavaScript
? Do you want to use ESLint to catch probable bugs and enforce style? No
✔  Wrote functions/package.json
✔  Wrote functions/index.js
✔  Wrote functions/.gitignore
? Do you want to install dependencies with npm now? Yes

> protobufjs@6.9.0 postinstall /home/tv/Téléchargements/test-esp32-iot/functions/node_modules/protobufjs
> node scripts/postinstall

npm notice created a lockfile as package-lock.json. You should commit this file.
added 252 packages from 206 contributors and audited 252 packages in 4.22s

29 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? public
? Configure as a single-page app (rewrite all urls to /index.html)? No
✔  Wrote public/404.html
✔  Wrote public/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

On va commencer par implémenter la Cloud Function.

Liens : Get started et Firebase SDK for Cloud Functions Quickstart

$ vim functions/index.js
const functions = require('firebase-functions');

// https://firebase.google.com/docs/functions/write-firebase-functions

// The Firebase Admin SDK to access the Firebase Realtime Database:
const admin = require('firebase-admin');
admin.initializeApp();

exports.esp32_pubsub_to_firebase = functions.pubsub.topic('esp32-iot-capteurs').onPublish(
    (message, context) => {
        const deviceId = message.attributes.deviceId;
        const timestamp = context.timestamp
        const temperature = message.json.temperature;
        const humidite = message.json.humidite;
        console.log(`Device=${deviceId}, Timestamp=${timestamp} - Temperature=${temperature}°C - Humidite=${humidite}%`);
        return admin.database().ref(`capteurs_esp32_iot/${deviceId}`).push({
            timestamp: timestamp,
            temperature: temperature,
            humidite: humidite
        })
});

On déploie la fonction :

$ firebase deploy --only functions

=== Deploying to 'test-esp32-iot'...

i  deploying functions
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
✔  functions: required API cloudfunctions.googleapis.com is enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (27.46 KB) for uploadingfunctions: functions folder uploaded successfully
i  functions: creating Node.js 8 function esp32_pubsub_to_firebase(us-central1)...functions[esp32_pubsub_to_firebase(us-central1)]: Successful create operation. 

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/test-esp32-iot/overview

On vérifie son fonctionnement en consultant les logs :

On déploie maintenant la base de données :

$ firebase deploy --only database

=== Deploying to 'test-esp32-iot'...

i  deploying database
i  database: checking rules syntax...
✔  database: rules syntax for database test-esp32-iot is valid
i  database: releasing rules...
✔  database: rules for database test-esp32-iot released successfully

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/test-esp32-iot/overview

Si on souhaite gérer plusieurs ESP32, il est possible d’y ajouter leurs identifiants :

Remarque : Il est possible de définir les droits d’accès à la base de données dans le fichier database.rules.json, ici on va autoriser l’accès en lecture :

{
  /* Visit https://firebase.google.com/docs/database/security to learn more about security rules. */
  "rules": {
    "devices_id": {
      ".read": true,
      ".write": false
    },
    "capteurs_esp32_iot": {
      ".read": true,
      ".write": false
    }
  }
}

On va maintenant développer l’application web dans le répertoire public. L’application s’appuie sur les ressources suivantes :

  • Bootstrap Material Design : une bibliothèque Open Source pour le développement HTML, CSS et Javascript basée sur jQuery
  • Plotly.js : une bibliothèque Javascript Open Source pour l’affichage de graphiques
$ vim public/index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>ESP32 IoT</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons">
    <link rel="stylesheet" href="https://unpkg.com/bootstrap-material-design@4.1.1/dist/css/bootstrap-material-design.min.css" integrity="sha384-wXznGJNEXNG1NFsbm0ugrLFMQPWswR3lds2VeinahP8N0zJw9VWSopbjv2x7WCvX" crossorigin="anonymous">
    <!-- update the version number as needed -->
    <script defer src="/__/firebase/7.15.1/firebase-app.js"></script>
    <!-- include only the Firebase features as you need -->
    <script defer src="/__/firebase/7.15.1/firebase-auth.js"></script>
    <script defer src="/__/firebase/7.15.1/firebase-database.js"></script>
    <script defer src="/__/firebase/7.15.1/firebase-messaging.js"></script>
    <script defer src="/__/firebase/7.15.1/firebase-storage.js"></script>
    <!-- initialize the SDK after all desired features are loaded -->
    <script defer src="/__/firebase/init.js"></script>
    <!-- Include Plotly.js -->
    <script defer src="https://cdn.plot.ly/plotly-latest.min.js"></script>
    <!-- Include the moment.js library -->
    <script defer src="https://momentjs.com/downloads/moment.js"></script>
    <script defer src="script.js"></script>
    <style media="screen">
      body { background: #ECEFF1; color: rgba(0,0,0,0.87); font-family: Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
      @media (max-width: 600px) {
        body, #message { margin-top: 0; background: white; box-shadow: none; }
        body { border-top: 16px solid #ffa100; }
      }
      .card { margin: 0 auto; float: none; margin-bottom: 10px; margin-top: 50px }
    </style>
  </head>
  <body>
    <div class="row">
        <div class="col-sm-6">
            <div class="card text-white bg-warning mb-3" style="max-width: 18rem;">
                <div style="font-size: 20px;" class="card-header">Température</div>
                <div class="card-body">
                    <h1 id="temperatureValue" style="font-size: 75px;" class="card-title">°C</h1>
                    <p id="temperatureHorodatage" class="card-text"></p>
                </div>
            </div>
        </div>
        <div class="col-sm-6">
            <div class="card text-white bg-primary mb-3" style="max-width: 18rem;">
                <div style="font-size: 20px;" class="card-header">Humidité</div>
                <div class="card-body">
                    <h1 id="humiditeValue" style="font-size: 75px;" class="card-title"><span>%</span></h1>
                    <p id="humiditeHorodatage" class="card-text"></p>
                </div>
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-sm-6">
            <div class="card text-white bg-warning mb-3" style="max-width: 45vw;">
                    <div id="temperaturePlot" style="max-height:55vh"></div>
            </div>
        </div>
        <div class="col-sm-6">
            <div class="card text-white bg-primary mb-3" style="max-width: 45vw;">
                    <div id="humiditePlot" style="max-height:55vh"></div>
            </div>
        </div>
    </div>
    <p style="text-align: center; margin: 0">
        <a target="_blank" href="http://tvaira.free.fr/esp32/esp32-google-iot.html">GCP Cloud IoT et ESP32</a>
    </p>
    <!-- Needed for Bootstrap Material Design -->
    <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/popper.js@1.12.6/dist/umd/popper.js" integrity="sha384-fA23ZRQ3G/J53mElWqVJEGJzU0sTs+SvzG8fXVWP+kJQ1lwFAOkcUOysnlKJC33U" crossorigin="anonymous"></script>
    <script src="https://unpkg.com/bootstrap-material-design@4.1.1/dist/js/bootstrap-material-design.js" integrity="sha384-CauSuKpEqAFajSpkdjv3z9t8E7RlpJ1UP0lKM/+NdtSarroVKu069AlsRPKkFBz9" crossorigin="anonymous"></script>
  </body>
</html>
$ vim public/script.js
// A web app that lively plots data from Firebase Realtime Database nodes, thanks to plotly.js
// cf. https://github.com/olivierlourme/iot-store-display

// Number of last records to display:
const nbOfElts = 60*24;

// Declaration of an array of devices ids
let devicesIds = [];
// Declaration of an array of devices aliases
let devicesAliases = [];

// Get references to the DOM node that welcomes the plots drawn by Plotly.js
const temperaturePlotDiv = document.getElementById('temperaturePlot');
const humiditePlotDiv = document.getElementById('humiditePlot');

const temperatureElement = document.getElementById('temperatureValue');
const humiditeElement = document.getElementById('humiditeValue');
const temperatureHorodatage = document.getElementById('temperatureHorodatage');
const humiditeHorodatage = document.getElementById('humiditeHorodatage');

// Get a reference to Firebase Realime Database :
const db = firebase.database();

// Display last value
db.ref(`capteurs_esp32_iot/esp32-1`).limitToLast(1).on('value', ts_measures => {
    ts_measures.forEach(ts_measure => {
        temperatureElement.innerText = ts_measure.val().temperature + '°C';
        temperatureHorodatage.innerText = moment(ts_measure.val().timestamp).format('DD/MM/YYYY à HH:mm');
    });
});

db.ref(`capteurs_esp32_iot/esp32-1`).limitToLast(1).on('value', ts_measures => {
    ts_measures.forEach(ts_measure => {
        humiditeElement.innerText = ts_measure.val().humidite + '%';
        humiditeHorodatage.innerText = moment(ts_measure.val().timestamp).format('DD/MM/YYYY à HH:mm');
    });
});

// Plot
let timestamps;
let temperatures;
let humidites;

// For temperature and humidity, the common plotly.js layout
const commonLayout = {
    autosize: true,
    titlefont: {
        //family: 'Roboto',
        size: 16,
        color: '#000'
    },
    xaxis: {
        linecolor: 'black',
        linewidth: 2
    },
    yaxis: {
        titlefont: {
            //family: 'Courier New, monospace',
            size: 14,
            color: '#000'
        },
        linecolor: 'black',
        linewidth: 2,
    },
    margin: {
        r: 50,
        b: 150,
        pad: 0
    }
};
// Specific layout aspects for temperature chart
let temperatureLayout = JSON.parse(JSON.stringify(commonLayout));
temperatureLayout.title = '<b>Température</b>';
temperatureLayout.yaxis.title = '<b>°C</b>';
temperatureLayout.yaxis.range = [0, 50];
// Specific layout aspects for humidity chart
let humidityLayout = JSON.parse(JSON.stringify(commonLayout));
humidityLayout.title = '<b>Humidité</b>';
humidityLayout.yaxis.title = '<b>%</b>';
humidityLayout.yaxis.range = [0, 100];

// Make ONCE an array of devices ids and devices aliases
db.ref('devices_id').once('value', (snapshot) => {
    snapshot.forEach(childSnapshot => {
        const childKey = childSnapshot.key;
        devicesIds.push(childKey);
        const childData = childSnapshot.val();
        let deviceAlias;
        if(childData == true) {
            deviceAlias = childKey; // alias is 'esp32-1' for instance
        } else {
            deviceAlias = childData; // alias is 'salle 1' for instance
        }
        devicesAliases.push(deviceAlias);
    });
    //console.log(devicesAliases);
    if (devicesIds.length != 0) {
        // objects 1st property (an array) initialization...
        timestamps = { [devicesIds[0]]: [] };
        temperatures = { [devicesIds[0]]: [] };
        humidites = { [devicesIds[0]]: [] };
        // ...and the rest of properties (somme arrays) initialization
        for (let i = 1; i < devicesIds.length; i++) {
            timestamps[devicesIds[i]] = [];
            temperatures[devicesIds[i]] = [];
            humidites[devicesIds[i]] = [];
        }
    } else console.log('No device id was found.')
})
.then(() => { // We start building database nodes listeners only when we have devices ids.
    for (let i = 0; i < devicesIds.length; i++) {
        db.ref(`capteurs_esp32_iot/${devicesIds[i]}`).limitToLast(nbOfElts).on('value', ts_measures => {
            //console.log(ts_measures.val());
            // We reinitialize the arrays to welcome timestamps, temperatures and humidites values:
            timestamps[devicesIds[i]] = [];
            temperatures[devicesIds[i]] = [];
            humidites[devicesIds[i]] = [];

            ts_measures.forEach(ts_measure => {
                timestamps[devicesIds[i]].push(moment(ts_measure.val().timestamp).format('DD/MM/YYYY HH:mm'));
                temperatures[devicesIds[i]].push(ts_measure.val().temperature);
                humidites[devicesIds[i]].push(ts_measure.val().humidite);
            });

            // plotly.js: See https://plot.ly/javascript/getting-started/
            // Temperatures
            let temperatureTraces = []; // array of plotly temperature traces (n devices => n traces) 
            for (let i = 0; i < devicesIds.length; i++) {
                temperatureTraces[i] = {
                    x: timestamps[devicesIds[i]],
                    y: temperatures[devicesIds[i]],
                    //mode: 'lines+markers',
                    line: {shape: 'spline'},
                    connectgaps: true,
                    name: devicesAliases[i]
                }
            }
            let temperatureData = []; // last plotly object to build
            for (let i = 0; i < devicesIds.length; i++) {
                temperatureData.push(temperatureTraces[i]);
            }
            Plotly.newPlot(temperaturePlotDiv, temperatureData, temperatureLayout, { responsive: true });

            // humidites
            let humidityTraces = []; // array of plotly humidity traces (n devices => n traces) 
            for (let i = 0; i < devicesIds.length; i++) {
                humidityTraces[i] = {
                    x: timestamps[devicesIds[i]],
                    y: humidites[devicesIds[i]],
                    line: {shape: 'spline'},
                    connectgaps: true,
                    name: devicesAliases[i]
                }
            }
            let humidityData = []; // last plotly object to build
            for (let i = 0; i < devicesIds.length; i++) {
                humidityData.push(humidityTraces[i]);
            }
            Plotly.newPlot(humiditePlotDiv, humidityData, humidityLayout, { responsive: true });
        });
    }
})
.catch(err => {
    console.err('An error occured:', err);
});

Il est préférable de tester d’abord en local :

$ firebase serve --only hosting
i  hosting: Serving hosting files from: public
✔  hosting: Local server: http://localhost:5000

On peut ensuite déployer :

$ firebase deploy --only hosting

=== Deploying to 'test-esp32-iot'...

i  deploying hosting
i  hosting[test-esp32-iot]: beginning deploy...
i  hosting[test-esp32-iot]: found 3 files in public
✔  hosting[test-esp32-iot]: file upload complete
i  hosting[test-esp32-iot]: finalizing version...
✔  hosting[test-esp32-iot]: version finalized
i  hosting[test-esp32-iot]: releasing new version...
✔  hosting[test-esp32-iot]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/test-esp32-iot/overview
Hosting URL: https://test-esp32-iot.web.app

On obtient :

Pour un coût :

Firebase et Qt

Firebase dispose de deux API pouvant être utilisées avec des projets Qt, C ++ et QML :

On va utiliser l’API REST Firebase pour Qt, un wrapper autour de l’API REST Firebase.

Il suffit de copier les fichiers firebase.h et firebase.cpp dans son projet Qt. Il faut aussi ajouter le module network dans le fichier de projhet .pro :

TEMPLATE = app
TARGET = test-qt-firebase

QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

HEADERS += ihm.h firebase.h

SOURCES += main.cpp ihm.cpp firebase.cpp

Il suffit d’instancier un objet Firebase et de lui passer en arguments l’URL de la base de données et le point de terminaison REST dans la base de données :

Firebase *firebase = new Firebase("https://test-esp32-iot.firebaseio.com/", "capteurs_esp32_iot/esp32-1");

Ensuite, on appelle getValue() pour récupérer les valeurs ou listenEvents() pour obtenir les données lors d’un changement. La classe Firebase fournit leux signaux eventResponseReady() et eventDataChanged() associés aux appels précédents et qu’il suffit de connecter à des slots.

Exemple :

#include "ihm.h"
#include <firebase.h>

/*
    Voir aussi :
        https://github.com/Sriep/Qt_Firebase_REST_API
        http://piersshepperson.co.uk/programming/2017/06/26/firebase-database-rest-api-qt/
 */

IHM::IHM( QWidget *parent ) : QWidget( parent )
{
    // les widgets
    labelHost = new QLabel(this);
    labelHost->setText(QString::fromUtf8("Host : "));
    leHost = new QLineEdit(this);
    leHost->setFixedWidth(350);
    leHost->setText("https://test-esp32-iot.firebaseio.com/");
    labelRequete = new QLabel(this);
    labelRequete->setText(QString::fromUtf8("Requête : "));
    leRequete = new QLineEdit(this);
    leRequete->setText(QString::fromUtf8("capteurs_esp32_iot/esp32-1"));
    leRequete->setFixedWidth(250);
    bGet  = new QPushButton(QString::fromUtf8("GET"), this);
    bGet->setEnabled(false);
    bListen  = new QPushButton(QString::fromUtf8("LISTEN"), this);
    bListen->setEnabled(false);
    bClear  = new QPushButton(QString::fromUtf8("EFFACER"), this);
    bClear->setEnabled(false);
    journal = new QTextEdit(this);
    journal->setReadOnly(true);

    // mise en place des layout
    QHBoxLayout *hLayout0 = new QHBoxLayout;
    QHBoxLayout *hLayout1 = new QHBoxLayout;
    QHBoxLayout *hLayout2 = new QHBoxLayout;
    QVBoxLayout *mainLayout = new QVBoxLayout;
    hLayout0->addWidget(labelHost);
    hLayout0->addWidget(leHost);
    hLayout0->addWidget(labelRequete);
    hLayout0->addWidget(leRequete);
    hLayout0->addStretch(1);
    hLayout1->addWidget(bGet);
    hLayout1->addWidget(bListen);
    hLayout1->addWidget(bClear);
    hLayout1->addStretch();
    hLayout2->addWidget(journal);
    mainLayout->addLayout(hLayout0);
    mainLayout->addLayout(hLayout1);
    mainLayout->addLayout(hLayout2);
    setLayout(mainLayout);

    // connexion signal/slot
    connect(leHost, SIGNAL(editingFinished()), this, SLOT(majHost()));
    connect(leRequete, SIGNAL(editingFinished()), this, SLOT(majRequete()));
    connect(bGet, SIGNAL(clicked()), this, SLOT(envoyerRequete()));
    connect(bListen, SIGNAL(clicked()), this, SLOT(ecouterChangement()));
    connect(bClear, SIGNAL(clicked()), this, SLOT(effacer()));

    // init
    majHost();
    majRequete();
}

IHM::~IHM()
{
}

void IHM::majHost()
{
  ...
}

void IHM::majRequete()
{
  ...
}

void IHM::envoyerRequete()
{
    Firebase *firebase = new Firebase(host, requete);
    qDebug() << Q_FUNC_INFO << "URL : " << firebase->getPath();
    firebase->getValue();

    connect(firebase, SIGNAL(eventResponseReady(QByteArray)), this, SLOT(onResponseReady(QByteArray)));
}

void IHM::ecouterChangement()
{
    Firebase *firebase = new Firebase(host, requete);
    firebase->listenEvents();
    connect(firebase, SIGNAL(eventDataChanged(QString)), this, SLOT(onDataChanged(QString)));
}

void IHM::effacer()
{
    journal->clear();
}

void IHM::onResponseReady(QByteArray datas)
{
    qDebug() << Q_FUNC_INFO << datas;
    QString infos(datas);
    journal->append(infos);
    bClear->setEnabled(true);
}

void IHM::onDataChanged(QString datas)
{
    qDebug() << Q_FUNC_INFO << datas;
    QString infos(datas);
    journal->append(infos);
    bClear->setEnabled(true);
}

Code source : test-qt-firebase.zip

Firebase et Android

Lien : Add Firebase to your Android project.

On commence par ajouter l’application Android au projet :

On poursuit le processus étape par étape :

Ensuite, on télécharge le fichier google-services.json que l’on copie dans le dossier app du projet AndroidStudio :

On suit les consignes pour modifier les fichiers du projet :

Le fichier build.gradle à la racine du projet :

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.6.1'
        classpath 'com.google.gms:google-services:4.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url 'https://www.jitpack.io'
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

Le fichier build.gradle du dossier app du projet :

apply plugin: 'com.android.application'
apply plugin: 'com.google.gms.google-services'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.3"

    defaultConfig {
        applicationId "com.example.myapplicationfirebase"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

    implementation 'com.google.firebase:firebase-database:19.3.0'

    implementation 'pl.pawelkleczkowski.customgauge:CustomGauge:1.0.4'
    implementation 'com.github.Pygmalion69:Gauge:1.5.2'
}

Remarque : L’API firebase-analytics n’est pas utile ici pour utiliser firebase-database.

Il est possible d’exécuter l’application pour vérifier l’installation :

Le layout de l’exemple :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#424242"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textTemperature"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="monospace"
        android:text="Température"
        android:textAlignment="center"
        android:textSize="28sp"
        android:textColor="#ffffff"
        app:layout_constraintBottom_toTopOf="@+id/temperature"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_bias="0.0" />

    <!-- #738ffe -> -9203714 et #424242 -> -12434878 -->
    <de.nitri.gauge.Gauge
        android:id="@+id/gauge"
        android:layout_width="250dp"
        android:layout_height="250dp"
        android:layout_centerHorizontal="true"
        android:layout_below="@+id/textTemperature"
        app:initialValue="0"
        app:maxValue="50"
        app:minValue="0"
        app:totalNicks="60"
        app:valuePerNick="1"
        app:needleColor="-1"
        app:scaleColor="-9203714"
        app:faceColor="-12434878"
        app:lowerTextSize="38"
        app:layout_constraintBottom_toTopOf="@+id/textHumidite"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textTemperature" />

    <TextView
        android:id="@+id/temperature"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fontFamily="monospace"
        android:textAlignment="center"
        android:textColor="#738ffe"
        android:textSize="38sp"
        app:layout_constraintBottom_toTopOf="@+id/textHumidite"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textTemperature" />

    <TextView
        android:id="@+id/textHumidite"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="monospace"
        android:text="Humidité"
        android:textAlignment="center"
        android:textSize="28sp"
        android:textColor="#ffffff"
        app:layout_constraintBottom_toTopOf="@+id/humidite"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/temperature"
        app:layout_constraintVertical_bias="0.0" />

    <pl.pawelkleczkowski.customgauge.CustomGauge
        android:id="@+id/gaugeHumidite"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerHorizontal="true"
        android:layout_below="@+id/textHumidite"
        app:gaugePointStartColor="#d4e157"
        app:gaugePointEndColor="#d4e157"
        app:gaugeStartAngle="135"
        app:gaugeStrokeCap="BUTT"
        app:gaugeStrokeColor="#757575"
        app:gaugeStrokeWidth="10dp"
        app:gaugeStartValue="0"
        app:gaugeEndValue="100"
        app:gaugeSweepAngle="270"
        app:gaugeDividerSize="1"
        app:gaugeDividerColor="#ffffff"
        app:gaugeDividerStep="10"
        app:gaugeDividerDrawFirst="true"
        app:gaugeDividerDrawLast="true"
        app:layout_constraintBottom_toTopOf="@+id/horodatage"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textHumidite" />

    <TextView
        android:id="@+id/humidite"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fontFamily="monospace"
        android:textAlignment="center"
        android:textColor="#d4e157"
        android:textSize="40sp"
        app:layout_constraintBottom_toTopOf="@+id/horodatage"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textHumidite" />

    <TextView
        android:id="@+id/horodatage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:fontFamily="monospace"
        android:text=""
        android:textAlignment="center"
        android:textSize="28sp"
        android:textColor="#ffffff"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/humidite" />

</androidx.constraintlayout.widget.ConstraintLayout>

Ressources : CustomGauge et Gauge

On crée une classe Mesure :

package com.example.myapplicationfirebase;

public class Mesure
{
    public String timestamp;
    public Double temperature;
    public Double humidite;

    public Mesure()
    {
        // Default constructor required for calls to DataSnapshot.getValue(Mesure.class)
    }

    public Mesure(String timestamp, Double temperature, Double humidite)
    {
        this.timestamp = timestamp;
        this.temperature = temperature;
        this.humidite = humidite;
    }

    public String toString()
    {
        return "Timestamp : " + timestamp + " Température : " + temperature + " Humidité : " + humidite;
    }
}

Lien : Read and Write Data on Android

Le code source de l’activité principale :

package com.example.myapplicationfirebase;

import androidx.appcompat.app.AppCompatActivity;
import de.nitri.gauge.Gauge;
import pl.pawelkleczkowski.customgauge.CustomGauge;

import android.graphics.Color;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.google.firebase.database.ChildEventListener;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.Query;
import com.google.firebase.database.ValueEventListener;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

public class MainActivity extends AppCompatActivity
{
    private static final String TAG = "MainActivity";
    private FirebaseDatabase database = null;
    private DatabaseReference databaseRef;
    private boolean chargement = false;
    private TextView temperature;
    private TextView humidite;
    private TextView horodatage;
    private CustomGauge gaugeTemperature;
    private Gauge gauge;
    private CustomGauge gaugeHumidite;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        temperature = (TextView) findViewById(R.id.temperature);
        humidite = (TextView) findViewById(R.id.humidite);
        horodatage = (TextView) findViewById(R.id.horodatage);
        //gaugeTemperature = (CustomGauge) findViewById(R.id.gaugeTemperature);
        gauge = (Gauge) findViewById(R.id.gauge);
        gaugeHumidite = (CustomGauge) findViewById(R.id.gaugeHumidite);

        database = FirebaseDatabase.getInstance();
        //databaseRef = database.getReference("capteurs_esp32_iot/esp32-1");
        databaseRef = database.getReference().child("capteurs_esp32_iot").child("esp32-1");

        Query query = databaseRef.limitToLast(1);
        query.addChildEventListener(new ChildEventListener()
        {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
                Log.d(TAG, "dataSnapshot : " + dataSnapshot.toString());
                Mesure mesure = dataSnapshot.getValue(Mesure.class);
                Log.d(TAG, "Mesure : " + mesure.toString());
                //temperature.setText(mesure.temperature.toString() + "°C");
                humidite.setText(mesure.humidite.intValue() + " %");
                horodatage.setText(getHorodatageFormate(mesure.timestamp));
                Double temperature = mesure.temperature*10.;
                //gaugeTemperature.setValue(temperature.intValue());
                gauge.moveToValue(mesure.temperature.floatValue());
                gauge.setLowerText(mesure.temperature.toString() + "°C");
                gaugeHumidite.setValue(mesure.humidite.intValue());
            }
            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {}
            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {}
            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}
            @Override
            public void onCancelled(DatabaseError databaseError) {}
        });

        /*databaseRef.addValueEventListener(new ValueEventListener()
        {
            @Override
            public void onDataChange(DataSnapshot dataSnapshot)
            {
                if(!chargement)
                {
                    Log.d(TAG, "Nb mesures : " + dataSnapshot.getChildrenCount());
                    Log.d(TAG, "dataSnapshot : " + dataSnapshot.toString());
                    for (DataSnapshot postSnapshot: dataSnapshot.getChildren())
                    {
                        Mesure mesure = postSnapshot.getValue(Mesure.class);
                        //Log.d(TAG, "Mesure : " + mesure.toString());
                        //temperature.setText(mesure.temperature.toString() + "°C");
                        humidite.setText(mesure.humidite.intValue() + " %");
                        horodatage.setText(getHorodatageFormate(mesure.timestamp));
                        //gaugeTemperature.setValue(mesure.temperature.intValue());
                        gauge.moveToValue(mesure.temperature.floatValue());
                        gauge.setLowerText(mesure.temperature.toString() + "°C");
                        gaugeHumidite.setValue(mesure.humidite.intValue());
                    }
                    chargement = true;
                }
            }

            @Override
            public void onCancelled(DatabaseError error)
            {
                Log.w(TAG, "Erreur !", error.toException());
            }
        });
        databaseRef.addChildEventListener(new ChildEventListener()
        {
            @Override
            public void onChildAdded(DataSnapshot dataSnapshot, String prevChildKey) {
                //Log.w(TAG, "onChildAdded");
                if(chargement)
                {
                    Log.d(TAG, "Nb mesures : " + dataSnapshot.getChildrenCount());
                    Log.d(TAG, "dataSnapshot : " + dataSnapshot.toString());
                    Mesure mesure = dataSnapshot.getValue(Mesure.class);
                    Log.d(TAG, "Mesure : " + mesure.toString());
                    //temperature.setText(mesure.temperature.toString() + "°C");
                    humidite.setText(mesure.humidite.intValue() + " %");
                    horodatage.setText(getHorodatageFormate(mesure.timestamp));
                    Double temperature = mesure.temperature*10.;
                    //gaugeTemperature.setValue(temperature.intValue());
                    gauge.moveToValue(mesure.temperature.floatValue());
                    gauge.setLowerText(mesure.temperature.toString() + "°C");
                    gaugeHumidite.setValue(mesure.humidite.intValue());
                }
            }

            @Override
            public void onChildChanged(DataSnapshot dataSnapshot, String prevChildKey) {}
            @Override
            public void onChildRemoved(DataSnapshot dataSnapshot) {}
            @Override
            public void onChildMoved(DataSnapshot dataSnapshot, String prevChildKey) {}
            @Override
            public void onCancelled(DatabaseError databaseError) {}
        });*/
    }

    private String getHorodatageFormate(String horodatage)
    {
        horodatage = horodatage.substring(0, 10) + " " + horodatage.substring(11, 19);
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.FRENCH);
        df.setTimeZone(TimeZone.getTimeZone("UTC"));
        Date date = null;
        String horodatageFormate = horodatage;
        try
        {
            date = df.parse(horodatage);
            df.setTimeZone(TimeZone.getDefault());
            horodatageFormate = df.format(date);
            Log.d(TAG, "horodatageFormate : " + horodatageFormate);
        }
        catch (ParseException e)
        {
            e.printStackTrace();
        }
        return horodatageFormate;
    }
}

Résultat :

Code source : MyApplicationFirebase.zip

Google Cloud API

Google Cloud fournit une API pour :

Exemple : Google Cloud IoT Core Python

On va tout d’abord tester à partir de Cloud Shell :

  • la configuration :
$ vim requirements.txt
google-cloud-iot==1.0.0

$ pip install -r requirements.txt

$ vim sendConfig.py
from google.cloud import iot_v1

project_id = 'test-esp32-iot'
cloud_region = 'europe-west1'
registry_id = 'mes-esp32-iot'
device_id = 'esp32-1'
version = 0
config= 'your-config-data'
print('Set device configuration')
client = iot_v1.DeviceManagerClient()
device_path = client.device_path(project_id, cloud_region, registry_id, device_id)

data = config.encode('utf-8')

client.modify_cloud_to_device_config(device_path, data, version)

Test :

$ python sendConfig.py

On obtient sur le moniteur série :

messageReceived <- /devices/esp32-1/config - your-config-data
  • l’envoi de commandes :
$ vim sendCommand.py
from google.cloud import iot_v1

project_id = 'test-esp32-iot'
cloud_region = 'europe-west1'
registry_id = 'mes-esp32-iot'
device_id = 'esp32-1'

print('Sending command to device')
client = iot_v1.DeviceManagerClient()
device_path = client.device_path(project_id, cloud_region, registry_id, device_id)
command = 'Hello esp32-1'
data = command.encode('utf-8')

client.send_command_to_device(device_path, data)

Test :

$ python sendCommand.py

On obtient sur le moniteur série :

messageReceived <- /devices/esp32-1/commands - Hello esp32-1

Cloud Function HTTP

On va maintenant créer une Cloud Function avec un déclenchement par une requête HTTP pour envoyer des commandes à l’ESP32 :

$ mkdir sendCommand
$ cd sendCommand

$ vim requirements.txt
google-cloud-iot==1.0.0
Flask==1.1.2

$ vim main.py
from google.cloud import iot_v1
import flask

def sendCommand(request):
    request_args = request.args
    if request_args and "deviceID" and "command" in request_args:
        device_id = request_args["deviceID"]
        command = request_args["command"]
        print('Device : {}'.format(device_id))
        print('Command : {}'.format(command))
        data = command.encode('utf-8')
        project_id = 'test-esp32-iot'
        cloud_region = 'europe-west1'
        registry_id = 'mes-esp32-iot'
        client = iot_v1.DeviceManagerClient()
        device_path = client.device_path(project_id, cloud_region, registry_id, device_id)
        client.send_command_to_device(device_path, data)
        return "Send command to {}".format(flask.escape(device_id))
    else:
        print('Args : {}'.format(request_args))
        return "Error !"

On déploie :

$ gcloud functions deploy sendCommand --runtime python37 --trigger-http --allow-unauthenticated

Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: sendCommand
httpsTrigger:
  url: https://us-central1-test-esp32-iot.cloudfunctions.net/sendCommand
ingressSettings: ALLOW_ALL
labels:
  deployment-tool: cli-gcloud
name: projects/test-esp32-iot/locations/us-central1/functions/sendCommand
runtime: python37
serviceAccountEmail: test-esp32-iot@appspot.gserviceaccount.com
sourceUploadUrl: https://storage.googleapis.com/gcf-upload-us-central1-5e43917f-c19b-40fd-b40c-5e03dd87a41d/4845af3d-848b-449f-86a8-649aea2e03a2.zip?GoogleAccessId=service-580197265786@gcf-admin-robot.iam.gserviceaccount.com&Expires=1592844829&Signature=...
status: ACTIVE
timeout: 60s
updateTime: '2020-06-22T16:25:17.370Z'
versionId: '1'

Remarque : pour les tests, on oublier la partie Authentification (--allow-unauthenticated) !

On teste :

$ curl https://us-central1-test-esp32-iot.cloudfunctions.net/sendCommand
Error !

$ curl 'https://us-central1-test-esp32-iot.cloudfunctions.net/sendCommand?deviceID=esp32-1&command=on'
Send command to esp32-1

On obtient :

messageReceived <- /devices/esp32-1/commands - on

IFTTT et Google Assitant

Le moyen le plus simple d’interagir avec l’ESP32 via Google Assistant est d’utiliser IFTTT.

IFTTT est un service web gratuit permettant de créer des chaînes d’instruction simples appelées applets.

On va par exemple créer un applet avec un déclencheur sur le service Google Assitant (This) avec une action Webhooks vers la Cloud Function sendCommand (That) :

Il faut choisir le service Google Assistant :

On sélectionne “Say a simple phrase” :

On complète :

On va ajouter le That :

On sélectionne Webhooks :

On sélectionne “Make a web request” :

On entre l’URL de la Cloud Function :

On termine le processus de création :

On vérifie l’état connected :

Test :

Google Assitant : Actions on Google et Dialogflow

Il est possible de développer et de déployer des applications compatibles avec Google assistant grâce à la plateforme Action On Google. Cette dernière permet d’intégrer des applications grâce à Dialogflow de Google ou développées en Node.Js.

Dialogflow : Tarification et Quotas

Cette plateforme permettra de :

  • Créer, paramétrer et déployer l’application
  • Récupérer des statistiques d’usages
  • Tester l’application avec le simulateur Google Assistant
  • Interconnecter l’application avec différentes fonctionnalités de Firebase

Un agent Dialogflow est un agent virtuel qui gère les conversations avec les utilisateurs. Il s’agit d’un module de compréhension du langage naturel qui saisit les nuances du langage humain. Dialogflow traduit les contenus texte ou audio produits par l’utilisateur au cours d’une conversation en données structurées assimilables par les applications et les services.

En savoir plus : Agents

Les agents servent également de conteneur pour :

  • Les paramètres (options de langue, etc …)
  • Les intents servent à classer les intentions de l’utilisateur final à chaque tour de conversation.
  • Les entités servent à identifier et extraire des données spécifiques des expressions de l’utilisateur final.
  • Les intégrations servent aux applications qui s’exécutent sur des appareils ou au sein de services gérant directement, pour vous, les interactions avec l’utilisateur final (par exemple Google Assistant).
  • Le fulfillment permet la connexion à votre service lorsque vous utilisez des intégrations.

On démarre en se connectant à la console Actions :

Il faut ensuite accepter les conditions de service :

Puis associer le projet :

On définit les premiers paramètres :

On crée une action :

Dans Dialogflow, on crée un agent :

Ensuite à partir du menu, on va pouvoir créer des intents (éventuellement des entities), définir le fulfillment et intégrer Google Assistant :

Exemple de création d’un intent :

Exemple de création d’une entity :

Il est possible de personnaliser l’accueil :

Et de faire l’intégration avec Google Assistant via Actions :

La partie fulfillment sera renseignée par la suite car il faut maintenant développer l’application via une Cloud Function Firebase. On possède déjà la fonction esp32_pubsub_to_firebase qui enregistre la télémétrie dans Realtime Database :

On va réaliser les deux actions suivantes :

  • modifier esp32_pubsub_to_firebase pour enregistrer les valeures courantes de la température et d’humidité dans un chemin spécifique (ce qui permettra de les récupérer plus facilement) :
const functions = require('firebase-functions');

// https://firebase.google.com/docs/functions/write-firebase-functions

// The Firebase Admin SDK to access the Firebase Realtime Database:
const admin = require('firebase-admin');
admin.initializeApp();

exports.esp32_pubsub_to_firebase = functions.pubsub.topic('esp32-iot-capteurs').onPublish(
    (message, context) => {
        const deviceId = message.attributes.deviceId;
        const timestamp = context.timestamp
        const temperature = message.json.temperature;
        const humidite = message.json.humidite;
        console.log(`Device=${deviceId}, Timestamp=${timestamp} - Temperature=${temperature}°C - Humidite=${humidite}%`);
        // Début de l'ajout
        admin.database().ref(`capteurs_esp32_iot/esp32_iot/temperature/value`).set(temperature);
        admin.database().ref(`capteurs_esp32_iot/esp32_iot/humidite/value`).set(humidite);
        // fin de l'ajout
        return admin.database().ref(`capteurs_esp32_iot/${deviceId}`).push({
            timestamp: timestamp,
            temperature: temperature,
            humidite: humidite
        })
    });

On obtiendra ceci :

  • ajouter une fonction (le fullfilment) qui s’occupera de traiter les requêtes faites par Google Assistant et de générer les réponses adaptées :
$ cd test-esp32-iot/functions
$ ls
index.js  node_modules  package.json  package-lock.json

$ npm install --save actions-on-google firebase-admin

$ vim index.js
const functions = require('firebase-functions');

// https://firebase.google.com/docs/functions/write-firebase-functions

// The Firebase Admin SDK to access the Firebase Realtime Database:
const admin = require('firebase-admin');
admin.initializeApp();

...

const { dialogflow } = require('actions-on-google')
//const app = dialogflow({debug: true})
const app = dialogflow()

function getTemperature() {
    return admin.database().ref(`capteurs_esp32_iot/esp32_iot/temperature`).once("value").then(function(snapshot) {
        var data = snapshot.val().value;
        return data;
    });
}

function getHumidite() {
    return admin.database().ref(`capteurs_esp32_iot/esp32_iot/humidite`).once("value").then(function(snapshot) {
        var data = snapshot.val().value;
        return data;
    });
}

app.intent('temperature', (conv) => {
  return getTemperature().then(function(data) {
      console.log("Temperature : " + data);
      conv.ask(`La température est de ${data} degrés. Vous pouvez demander aussi quelle est l'humidité ?`);
    });
});

app.intent('humidite', (conv) => {
  return getHumidite().then(function(data) {
      console.log("Humidite : " + data);
      conv.ask(`L'humidité est de ${data} pourcent. Vous pouvez demander aussi quelle est la température ?`);
    });
});

app.intent('aideTemperature', (conv) => {
  conv.ask(`Voulez-vous connaître la température de Mon ESP ? Dites : quelle est la température ?`);
});

app.intent('aideHumidite', (conv) => {
  conv.ask(`Voulez-vous connaître l'humidité de Mon ESP ? Dites : quelle est l'humidité ?`);
});

app.intent('aide', (conv) => {
  conv.ask(`Vous pouvez demander à Mon ESP : quelle est la température ? ou quelle est l'humidité ?`);
});

exports.monesp = functions.https.onRequest(app)

On doit commencer par instancier un objet dialogflow :

const { dialogflow } = require('actions-on-google')
//const app = dialogflow({debug: true})
const app = dialogflow()

Il faut pouvoir récupérer un enregistrement (contenant la température ou l’humidité) de Firebase en précisant le chemin dans l’appel ref() puis en demandant sa valeur (value).

On utilisera la méthode once() pour obtenir la valeur une seule fois (la méthode on() récupère aussi la valeur une fois mais elle définit également un rappel à appeler à chaque mise à jour).

Les fonction getTemperature() et getHumidite() doivent être des fonctions asynchrones. Pour cela, il faut renvoyer une promesse (cf. Promise en JavaScript). Cette promesse retournera la valeur récupérée lorsque la fonction asynchrone se terminera. La bibliothèque Firebase peut renvoyer une promesse dans le cadre de la clause .then() de la réponse.

function getTemperature() {
    return admin.database().ref(`capteurs_esp32_iot/esp32_iot/temperature`).once("value").then(function(snapshot) {
        var data = snapshot.val().value;
        return data;
    });
}

Les gestionnaires d’intention app.intent() doivent aussi utiliser une promesse si un appel asynchrone est réalisé (car le gestionnaire d’intention doit savoir attendre la fin d’un appel asynchrone). Il faut donc définir la réponse dans la partie .then() puis appeler conv.ask() pour énoncer la réponse :

app.intent('temperature', (conv) => {
  return getTemperature().then(function(data) {
      console.log("Temperature : " + data);
      conv.ask(`La température est de ${data} degrés. Voulez-vous autre chose ?`);
    });
});

On termine par définir la Cloud Function nommée ici monesp :

exports.monesp = functions.https.onRequest(app)

On déploie :

$ firebase deploy --only functions

=== Deploying to 'test-esp32-iot'...

i  deploying functions
i  functions: ensuring required API cloudfunctions.googleapis.com is enabled...
i  functions: ensuring required API cloudbuild.googleapis.com is enabled...
✔  functions: required API cloudbuild.googleapis.com is enabled
✔  functions: required API cloudfunctions.googleapis.com is enabled
i  functions: preparing functions directory for uploading...
i  functions: packaged functions (34.4 KB) for uploadingfunctions: functions folder uploaded successfully
i  functions: updating Node.js 10 function esp32_pubsub_to_firebase(us-central1)...
i  functions: updating Node.js 10 function monesp(us-central1)...functions[monesp(us-central1)]: Successful update operation. 
✔  functions[esp32_pubsub_to_firebase(us-central1)]: Successful update operation. 

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/test-esp32-iot/overview

La nouvelle fonction monesp est disponible :

On peut maintenant renseigner l’URL du fullfilment :

C’est parti pour les tests ! Il faut ouvrir le simulateur :

“Parler avec Mon ESP” :

Et on peut tester les différents intents :

Actuellement, c’est la version de test qui est déployée. On peut déployer une release en renseignant les différents paramètres suivants :

Vidéos

Autres plateformes IoT

Liens : 10 Best IoT Platforms To Watch Out In 2020 et 12 IoT Platforms for Building IoT Projects

Voir aussi

AWS IoT et Alexa avec l’ESP32