Internet de les coses amb ESP32 i ESP8266

Exemples Referència Plaques   Recursos CITCEA
Projectes Programació Perifèrics   Inici

Guardem les lectures de temperatura i humitat a Google Drive fent servir un script

En un exemple anterior hem fet servir un full de càlcul de Google Drive on guardarem les lectures del sensor de temperatura i humitat. Podem crear un codi executable (anomenat script) que interactuï amb el nostre full de càlcul. Aquest codi rebrà l'ordre d'escriptura que li envia el microcontrolador i afegirà una línia al full de càlcul amb els valors rebuts. Una manera senzilla d'executar aquest codi des del microcontrolador és creant una funció que s'executi quan rep una ordre HTTP de tipus get. El nostre script tindrà, doncs, una adreça URL que ens permetrà executar-lo.

Podríem crear el full de càlcul a partir d'un formulari, com hem fet a l'exemple anterior però és més senzill i ràpid crear-lo directament. Les diferències, però, serien poques,bàsicament que la taula tindria una columna més (Marca de temps) i hauríem de tenir en compte a l'script que les dades es guarden a les columnes 1 i 2 en lloc de fer-ho a la 0 i la 1.

En aquest exemple no pretenem entendre com està fet aquest codi ja que el nostre objectiu és un altre. Així, doncs, el que farem serà explicar com adjuntar l'script al nostre full de càlcul i després comentarem com fer-lo servir.

Obrim la nostra pàgina de Google Drive i creem un full de càlcul nou. A la primera filera posem els títols de les columnes. A la primera, per exemple, hi podem posar Temperatura i Humitat a la segona. Un cop introduït, es veuria d'aquesta manera:

Columnes de la taula

Les pàgines i els serveis de Google canvien d'aspecte amb certa freqüència. Les imatges que trobarem en aquest apartat, per tant, poden no correspondre exactament amb les reals. Aquí l'important són els passos, no l'aspecte.

De moment, el nostre full de càlcul és només accessible per a ús personal. Si hi volem accedir des de l'AppInventor ho haurem de canviar. Piquem sobre el botó Comparteix.... Se'ns obrirà una finestra en la que picarem el botó Opcions avançades. On s'indica Qui hi té accés hi deu dir Privat i ho hem de canviar per Activat: tothom que tingui l'enllaç i en el desplegable on diu Pot visualitzar haurem de triar Pot editar (en aquest cas ho podríem deixar a Pot visualitzar però llavors no podríem modificar el formulari des d'altres aplicacions). En aquesta finestra hi surt un enllaç que copiarem i ens guardarem en algun lloc, per exemple un document del bloc de notes. L'enllaç obtingut serà similar a aquest:

https://docs.google.com/spreadsheets/d/1TThsoSjkeMSfwEKy4mn_4QEYH96sxv3VURqE3WHCTswDA/edit?usp=sharing

D'aquesta adreça ens interessa el codi de la taula, que és una seqüència llarga de caràcters situada entre barres. Aquí l'hem marcada en verd perquè sigui fàcil d'identificar. Piquem al botó Fet. Ara anem a la pestanya Fitxer i piquem sobre l'opció Publica al web.... Se'ns obrirà una finestra en la que picarem el botó Publica, acceptarem la confirmació i tancarem la finestra.

En el nostre full de càlcul, anirem a la pestanya Eines i triarem l'opció Editor de scripts. Se'ns obrirà una finestra similar a la següent:

Vista del programa

Hem d'esborrar la funció buida myFunction i deixar l'espai en blanc. Aquest és l'script que farem servir:

// Funció per interaccionar amb el full de càlcul des del microcontrolador
// Oriol Boix, 2020
// Sota llicència Creative Commons BY-NC-ND
// https://creativecommons.org/licenses/by-nc-nd/3.0/deed.es_ES
//
// Les variables següents ens permeten personalitzar l'script al nostre projecte
// En principi, no hauríem de tocar la resta de l'script 
var IdFull = "14eaPdj5ge66EYiFmto1LL37kxdTkmzLZTxrib1rH9CI"; // Identificador del full de càlcul 
              // S'aconsegueix picant a Comparteix i triant Opcions avançades
var numFull = 0;  // Número del full amb el que hem de treballar
// Script per interactuar amb el full de càlcul
// Funció que s'executa quan hi ha una ordre get
// La nostra funció tindrà dos paràmetres:
      // t    Valor de la temperatura
      // h    Valor de la humitat
function doGet(e) {
  var resultat = '';
  var camps = new Array(2);  // Valors per guardar a la taula
  // Assignem els paràmetres a variables
  var Temp = e.parameter.t; 
  var Hum = e.parameter.h; 
  if ((Temp == undefined) || (Hum == undefined)){
    resultat = 'Falten paràmetres';
  } else {
    camps[0] = Temp;
    camps[1] = Hum;
    var sh = SpreadsheetApp.openById(IdFull);
    var sheet = sh.getSheets();
    sheet[numFull].appendRow(camps);   // Afegeix una fila amb la llista de dades en format matriu 
    resultat = 'Valors guardats';
  }
  return ContentService.createTextOutput(resultat); 
}

Copiem tot el text i l'enganxem a la finestra de l'editor d'scripts. Un cop enganxat, haurem de fer-hi un canvi. En la imatge següent s'han requadrat aquelles línies pensades per a que es pugui personalitzar el programa. En el nostre exemple haurem de modificar la línia requadrada en vermell canviant el text entre cometes pel codi de la taula que hem trobat a l'exemple anterior. Els elements requadrats en blau no s'han de tocar per a l'exemple que farem però sí pot ser necessari tocar-los si aprofitem el programa per a altres aplicacions.

Vista del programa

Per a altres exemples caldria adaptar la dimensió del vector camps al nombre de paràmetres, les variables on guardem els paràmetres i les accions que es fan sobre aquestes variables.

Un cop personalitzat el programa l'hem de guardar, picant el botó que es mostra a continuació.

Botó guardar

En el menú, anirem a la pestanya Publica i triarem l'opció Implementa com a aplicació web... i s'obrirà una finestra similar a la següent:

Finestra

En aquesta pantalla hem d'anar al desplegable on diu Qui té accés a l'aplicació i hem de triar l'opció Qualsevol usuari, fins i tot els anònims. També hem de cercar, a la part superior, l'adreça URL corresponent al nostre script i copiar-la.

URL script https://script.google.com/macros/s/AKfycbysb5cirzf08AzksvdF-tQ2wc3YMK3qGQJIgfiomwcMHqI3mvEGY/exec

Un cop estiguem, podem picar el botó Actualitza.

Atenció: Cada cop que modifiquem el codi del nostre script l'hem de salvar i tornar-lo a publicar. En el desplegable Versió del projecte: cal triar cada vegada l'opció Nou. Si no ho fem així s'executarà la versió anterior del codi, sense tenir en compte les modificacions.

Ara que ja tenim el nostre full de càlcul preparat, anem a comentar les possibilitats que ens ofereix aquest script, per després poder-les emprar des del microcontrolador. Primer comencem per la variable que ens permet personalitzar el codi. A la taula següent hi ha el seu nom i la descripció corresponent.

Variable Valor
exemple
Descripció
numFull 0 Número de full que fem servir, el primer és el 0
(poden haver-hi diversos fulls amb el mateix enllaç)

Els scripts es poden provar amb el navegador web. Si posem l'adreça URL a la barra d'adreces del navegador obtindrem la resposta a la pantalla. La nostra funció té dos paràmetres anomenats t i p, però es podrien afegir més paràmetres en el futur. La URL, amb un parell de possibles valors, hauria de quedar així:

https://script.google.com/macros/s/AKfycbysb5cirzf08AzksvdF-tQ2wc3YMK3qGQJIgfiomwcMHqI3mvE/exec?t=23.5&h=46.8

en aquest cas, però, cal tenir en compte un problema afegit que ens obligarà a complicar el programa. Si fem aquesta petició des del microcontrolador, el servidor de Google ens enviarà una resposta com la següent:

HTTP/1.1 302 Moved Temporarily
Content-Type: text/html; charset=UTF-8
Access-Control-Allow-Origin: *
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: Mon, 01 Jan 1990 00:00:00 GMT
Date: Tue, 23 Apr 2019 12:27:56 GMT
Location: https://script.googleusercontent.com/macros/echo?user_content_key=oenhEvjO3yPbc5l3PWi6ufvR8cirQxMahB_E8SK3bq6ntEINk5x5P5nifS8kHstSQscGgtkc5KFCdEpp6Y6gAR8RseYwh1icm5_BxDlH2jW0nuo2oDemN9CCS2h10ox_1xSncGQajx_ryfhECjZEnFqIf1F7oW1FfCOAcLCKwzzmi2u-p9u7-sO1n73TUZfMkGAo9dguy94CzIWlTYkUuAOv8ef6LK8V0WMzD7_o4OAjoDVIujcsbA&lib=Mshmn-khZ4-y7RLovKvN0gQ677b5FxXsc
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
Server: GSE
Alt-Svc: quic=":443"; ma=2592000; v="46,44,43,39"
Accept-Ranges: none
Vary: Accept-Encoding
Connection: close

<HTML>
<HEAD>
<TITLE>Moved Temporarily</TITLE>
</HEAD>
<BODY BGCOLOR="#FFFFFF" TEXT="#000000">
<H1>Moved Temporarily</H1>
The document has moved <A HREF="https://script.googleusercontent.com/macros/echo?user_content_key=oenhEvjO3yPbc5l3PWi6ufvR8cirQxMahB_E8SK3bq6ntEINk5x5P5nifS8kHstSQscGgtkc5KFCdEpp6Y6gAR8RseYwh1icm5_BxDlH2jW0nuo2oDemN9CCS2h10ox_1xSncGQajx_ryfhECjZEnFqIf1F7oW1FfCOAcLCKwzzmi2u-p9u7-sO1n73TUZfMkGAo9dguy94CzIWlTYkUuAOv8ef6LK8V0WMzD7_o4OAjoDVIujcsbA&lib=Mshmn-khZ4-y7RLovKvN0gQ677b5FxXsc">here</A>.
</BODY>
</HTML>

En aquesta resposta el més important són les línies marcades en groc. El que ens diu aquesta resposta és un missatge que ens indica que el servidor ha canviat temporalment d'adreça. Aquests redireccionaments no són estranys quan es fan tasques de manteniment d'un servidor però en aquest cas no es tracta d'això. Aquesta és una de les proteccions que posa Google per evitar accions no destijades. Quan nosaltres fem una petició a l'adreça "correcta" ens envien una adreça temporal que dura molt poc temps i que ens permet fer l'acció desitjada. Només si el nostre dispositiu és capaç d'enviar immediatament la petició a la nova adreça es farà l'acció.

Aixi, doncs, hem hagut de complicar el programa respecte al de l'exemple anterior per preveure aquest canvi d'adreça i actuar en conseqüència. Malgrat tot, en algunes de les proves que hem fet el programa (o més ben dit la xarxa Wi-Fi) no ha estat prou ràpida en servir la nova connexió i en enviar la segona adreça hem rebut un nou avís de canvi, de manera que hem hagut de fer fins a tres o quatre connexions per obtenir els valors desitjats.

Ara anem a comentar el programa que hem de posar al microcontrolador. Només comentarem aquelles coses que han canviat respecte al de l'anterior exemple. A la part de definició de variables tenim les variables server i pagina que fins ara havíem definit amb la mida que correspongués per al contingut que hi escrivíem i, en canvi, ara les definim molt més grosses perquè després necessitarem guardar-hi l'adreça que ens retorna el servidor. Com hem vist, el servidor ens retorna adreces molt llargues i, per tant, ens hem curat en salut posant valors relativament grans.

Al començament del loop és quan rebem els caràcters que envia el servidor. Hem afegit unes instruccions per detectar quan ens arriba un salt de línia (\n) i així ens assegurem que només analitzarem línies senceres. En cada connexió cerquem una línia amb un codi de resposta 200 (correcte) o 302 (redirecció temporal) i ens guardem quina hem trobat. Si era la segona, busquem també la línia que comença amb Location per agafar-ne l'adreça que hem de partir en dos trossos (just després de les dues barres) per guardar-la com a nom del servidor i com a adreça de la pàgina.

La resposta rebuda la ignorem. Podríem fer que la mirés per saber si les dades s'han enviat correctament.

Quan fem la petició afegim els paràmetres (variable data) a l'adreça de la pàgina.

// Aquest programa està parcialment basat en els exemples de la pàgina
// https://www.arduino.cc/en/Tutorial/LibraryExamples#wifi1010
#include     // Carreguem la biblioteca SPI
#include     // Carreguem la biblioteca WiFiNINA
#include "DHT.h"
#define DHTPIN 2
#define DHTTYPE DHT22
#define server_len 50
#define pag_len 400
const char idXarxa[] = "xarxa-wifi";    // Nom del punt d'accés 
const char contrasenya[] = "contrasenya-wifi";    // Contrasenya de connexió 
const String server0 = "script.google.com";
String pagina0 = "";
const String pagina_base = "/macros/s/AKfycbwzJfJIZd9-yS6prAbLRDNHUTFT4Ua122Kv2yb-jo3jd73Gl9x0/exec?";
char server[server_len];
char pagina[pag_len];
unsigned long darreraConnexio = 0;
const unsigned long periodeConnexio = 10000UL;
bool pendent, completa, redir;
bool ara = false;
float hum, temp;
String data;
String peticio = "";    // Aquí guardarem una línia de la petició del client
String peticioAux = "";    // i la petició anterior (també ho farem servir de reserva)
int status = WL_IDLE_STATUS;
WiFiSSLClient client;
DHT dht(DHTPIN, DHTTYPE);
void setup() {    // Inicialització
    Serial.begin(9600);    // Monitor sèrie
    dht.begin();
    while (!Serial) {
        ;    // Esperem que l'usuari obri el monitor sèrie
    }
    if (WiFi.status() == WL_NO_MODULE) {
        Serial.println("No s'ha trobat el dispositiu Wi-Fi");
        while (true);    // Bloquegem el programa
    }
    String versio = WiFi.firmwareVersion();
    if (versio < "1.0.0") {
        Serial.println("Convindria actualitzar el firmware");
    }
    while (status != WL_CONNECTED) {
        Serial.print("Connectant a la xarxa ");
        Serial.println(idXarxa);
        status = WiFi.begin(idXarxa, contrasenya);
        delay(10000);    // Ho tornarem a intentar passats 10 s
    }
    Serial.print("Connectat a "); 
    Serial.println(WiFi.SSID());
    Serial.print("Estat de la connexió: ");
    Serial.println(WiFi.status()); 
    Serial.print("Adreça IP del dispositiu: ");
    Serial.println(WiFi.localIP()); 
    Serial.print("Intensitat del senyal: ");
    Serial.print(WiFi.RSSI()); 
    Serial.println(" dBm");
    Serial.println(); 
    redir = false;
}
void loop() {    // Programa que es repeteix indefinidament
    // El bucle principal té tres parts: 
    //     1. Gestió dels caràcters que arriben
    //     2. Tractament de les dades rebudes
    //     3. Nova petició quan ha passat el temps
    while (client.available()) {
        // Gestió dels caràcters que arriben
        // Aquest bucle va guardant els caràcters rebuts
        // i espera al moment en que arriba un salt de línia
        char c = client.read();    // Rebem caràcters del servidor
        if (c == '\n') {    // Mirem si és un salt de línia
            peticioAux = peticio;    // Guardem la petició anterior
            peticio = "";    // Ens preparem per a la línia següent
            completa = true;    // Preparat per escriure-ho
        } else {
            peticio += c;    // Afegim el caràcter rebut
        }
        // Quan ha arribat un salt de línia, hem de mirar què ha arribat
        if (completa){  // Ha arribat una línia completa
            if (peticioAux.startsWith("HTTP/1.1 200")){    // Resposta bona
                pendent = true;
            }
            if (peticioAux.startsWith("HTTP/1.1 302")){    // Redireccionament
                redir = true;
            }
            if (redir && (peticioAux.startsWith("Location:"))){
                // Si hi ha redireccionament, hem de buscar l'adreça
                // i extreure'n el servidor i la pàgina
                String adre = peticioAux.substring(peticioAux.indexOf("//") +2);
                String server1 = adre.substring(0, adre.indexOf(".com") +4);
                String pagina1 = adre.substring(adre.indexOf(".com") +4);
                server1.toCharArray(server, 50);
                pagina1.toCharArray(pagina, 400);
                ara = true;
            }
            completa = false;
        }
    }
    // Hi ha una resposta per processar
    // Però ignorarem les dades que ens envia l'script
    if (pendent) {
        pendent = false;
    }
    // Quan toca, tornem a fer una petició
    if (ara || ((millis() - darreraConnexio > periodeConnexio))) {
        hum = dht.readHumidity();
        temp = dht.readTemperature();
        data = "t=";
        data += temp;
        data += "&h=";
        data += hum;
        if (redir){
            redir = false;
        } else {
            pagina0 = pagina_base + data;
            server0.toCharArray(server, server_len);
            pagina0.toCharArray(pagina, pag_len);
        }
        ara = false;
        client.stop();
        if (client.connect(server, 443)) {
            Serial.println("S'ha fet la connexió al servidor");
            client.print("GET ");
            client.print(pagina);
            client.println(" HTTP/1.1");
            client.print("Host: ");
            client.println(server);
            client.println("Connection: close");
            client.println();
            // Guardem quan hem fet la connexió
            darreraConnexio = millis();
            Serial.print("Enviat: ");
            Serial.println(data);
        } else {
            Serial.println("connection failed");
        }
    }
}

En aquest programa hi ha la condició que el monitor sèrie estigui obert per a que el programa comenci a funcionar. Això ens permet seguir tot el procés encara que triguem a obrir el monitor sèrie. Quan el microcontrolador hagi de funcionar de manera independent de l'ordinador caldrà eliminar les línies següents per evitar aquest bloqueig.

    while (!Serial) {
        ;    // Esperem que l'usuari obri el monitor sèrie
    }

El nostre programa de prova escriu unes quantes coses al monitor sèrie (instruccions Serial.print i Serial.println). Quan ja tinguem clar que el programa funciona, les podem eliminar quasi totes i probablement anirà una mica més ràpid.

Ens pot interessar que el nostre script guardi els valors sempre a la mateixa filera en lloc d'anar afegint una filera cada cop. En aquest cas, podem fer servir aquest altre script:

// Funció per interaccionar amb el full de càlcul des del microcontrolador
// Oriol Boix, 2020
// Sota llicència Creative Commons BY-NC-ND
// https://creativecommons.org/licenses/by-nc-nd/3.0/deed.es_ES
//
// Les variables següents ens permeten personalitzar l'script al nostre projecte
// En principi, no hauríem de tocar la resta de l'script 
var IdFull = "14eaPdj5ge66EYiFmto1LL37kxdTkmzLZTxrib1rH9CI"; // Identificador del full de càlcul 
              // S'aconsegueix picant a Comparteix i triant Opcions avançades
var numFull = 0;  // Número del full amb el que hem de treballar
// Script per interactuar amb el full de càlcul
// Funció que s'executa quan hi ha una ordre get
// La nostra funció tindrà dos paràmetres:
      // t    Valor de la temperatura
      // h    Valor de la humitat
function doGet(e) {
  var resultat = '';
  var camps = new Array(2);  // Valors per guardar a la taula
  // Assignem els paràmetres a variables
  var Temp = e.parameter.t; 
  var Hum = e.parameter.h; 
  if ((Temp == undefined) || (Hum == undefined)){
    resultat = 'Falten paràmetres';
  } else {
    camps[0] = Temp;
    camps[1] = Hum;
    // Ja tenim les dues caselles que hem d'escriure a la filera
    // Les nostres dades han d'escriure en un espai, que seleccionarem més endavant, 
    //    de dues columnes en una filera
    var valu = new Array(1);
    valu[0] = camps;  // Converteix el vector en una matriu d'una filera i dues columnes
    var sh = SpreadsheetApp.openById(IdFull);
    var sheet = sh.getSheets();
    var range = sheet[numFull].getLastRow();
    if (range > 1){  // Mira si ja hi ha un valor
      // La funció getRange ens permet seleccionar el grup de caselles sobre les que anem a treballar,
      //    en aquest cas a escriure
      // El primer paràmetre és la filera on comença el grup, en el nostre cas la darrera escrita
      // El segon paràmetre és la columna on comença el grup, en el nostre exemple la primera
      // El tercer paràmetre és el nombre de fileres que tindrà el grup, per a nosaltres una
      // El quart paràmetre és el nombre de columnes que ocupa el grup, en el nostre cas dues
      var rangeVal = sheet[numFull].getRange(range, 1, 1, 2);  // Selecciona dues caselles
      rangeVal.setValues(valu);  // Guarda els valors a les caselles, substituïnt els anteriors
    } else {
      // Si no n'hi ha cap, afegeix una fila amb la llista de dades en format matriu
      sheet[numFull].appendRow(camps);
    }
    resultat = 'Valors guardats';
  }
  return ContentService.createTextOutput(resultat); 
}

En aquest cas no ens serviria l'aplicació d'App Inventor que hem fet servir en l'altre exemple perquè ja no hi ha cap formulari. Però també és possible fer una aplicació per a aquest cas ja que podríem afegir una opció a l'script que, amb els paràmetres adequats, ens servís per escriure a la taula. Així, l'aplicació enviaria dades a l'script i podria afegir nous valors. Aquest exemple ens pot servir de guia.

 

 

 

 

 

 

 

 

 

 

Llicència de Creative Commons
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.