Internet de les coses amb ESP32 i ESP8266

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

Canviem el color d'un NeoPixel fent servir un script

En un exemple anterior hem fet servir un full de càlcul de Google Drive i un script però creant el full de càlcul des d'un formulari. Si fem servir un script no ens cal crear cap formulari sinó que podem crear directament el full de càlcul que ens interessi.

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 Vermell i Verd i Blau a les altres dues. Serà convenient escriure uns valors a la segona filera del full per poder fer proves. 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 fora 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 = "1OfvIrw5E1P4kvIYbo_QfqRWMiOSViD5PVjtWiOBxqhQ"; // 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
var numCols = 3;  // Nombre de columnes de treball que tenim al full de càlcul
var range;
// Script per interactuar amb el full de càlcul
// Funció que s'executa quan hi ha una ordre get
// La nostra funció tindrà un paràmetre:
      // accio    l'acció que s'ha de realitzar (darrer)
function doGet(e) {
  // Assignem els paràmetres a variables
  var Accio = e.parameter.accio; 
  // Obrim el full de càlcul
  var sh = SpreadsheetApp.openById(IdFull);
  var sheet = sh.getSheets();
  // Llegim les dades del full seleccionat
  var full = sheet[numFull].getDataRange().getValues();
  range = sheet[numFull].getLastRow();  // Índex de la darrera filera (+1 pels títols)
  if(Accio=="darrer"){
    if (range > 1){    // Si és 1 només hi ha els títols
      var filera = full[range -1];    // Agafem la darrera filera
      var resultat = '';
      for(j = 0;j < numCols;j++){ 
        if (j > 0){
          resultat = resultat + ',';
        }
        resultat = resultat + filera[j];
      }
    }
  } else {
      var resultat = 'Acció incorrecta';
  }
  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 abans. 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

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 les dues variables que ens permeten 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ç)
numCols 3 Nombre de columnes del full que fem servir
(si posem 3 treballarem amb les columnes que van de la 0 a la 2)

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é un paràmetre anomenat accio que ha de ser darrer. Tal com s'ha fet, però, es podrien afegir més opcions i, fins i tot, més paràmetres en el futur. La URL hauria de quedar així:

https://script.google.com/macros/s/AKfycbysb5cirzf08AzksvdF-tQ2wc3YMK3qGQJIgfiomwcMHqI3mvE/exec?accio=darrer

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 del NeoPixel. 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.

Quan la resposta ha estat el que esperàvem hem de tractar els valors rebuts però hem de tenir present que el nostre script envia només els valors de les tres columnes i no la marca de temps i, per tant, ens simplifica una mica el tractament. Finalment, fem una nova connexió passats els deu segons excepte (i això també és nou) en cas que hi hagi hagut redireccionament, que ho farem de manera immediata.

// Aquest programa està parcialment basat en els exemples de la pàgina
// https://www.arduino.cc/en/Tutorial/LibraryExamples#wifi1010
#include <SPI.h>    // Carreguem la biblioteca SPI
#include <WiFiNINA.h>    // Carreguem la biblioteca WiFiNINA
#include <Adafruit_NeoPixel.h>    // biblioteca per als NeoPixels
#define LED 1    // pota on hem connectat el LED
#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 char server0[] = "script.google.com";
const char pagina0[] = "/macros/s/AKfycbx2VGLHQK_if-Yw9ySzbE1CZ5VPk9DYlhOQh7Z2BBumHChSs1Y/exec?accio=darrer";
char server[server_len];
char pagina[pag_len];
unsigned long darreraConnexio = 0;
const unsigned long periodeConnexio = 10000UL;
bool pendent, completa, redir;
bool ara = false;
String R, G, B;    // Aquí guardarem els colors
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;
Adafruit_NeoPixel cadena = Adafruit_NeoPixel(1, LED, NEO_GRB + NEO_KHZ800);
WiFiSSLClient client;
void setup() {    // Inicialització
    Serial.begin(9600);    // Monitor sèrie
    cadena.begin();  // Inicialitza els NeoPixels        
    cadena.show();
    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(); 
    Serial.println("Anem a connectar al servidor");
    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, server_len);
                pagina1.toCharArray(pagina, pag_len);
                ara = true;
            }
            completa = false;
        }
    }
    // Hi ha una resposta per processar
    // En aquest cas, les dades estan a la darrera línia rebuda
    if (pendent) {
        pendent = false;
        if (peticio == ""){    // Si està buit
            peticio = peticioAux;    // Agafem l'anterior
        }
        // Ara només s'envien els tres colors, no hi ha la marca de temps
        R = peticio.substring(0, peticio.indexOf(","));
        peticio = peticio.substring(peticio.indexOf(",") +1);
        G = peticio.substring(0, peticio.indexOf(","));
        B = peticio.substring(peticio.indexOf(",") +1);
        cadena.setPixelColor(0, R.toInt(), G.toInt(), B.toInt());
        cadena.show();
        Serial.print("(");
        Serial.print(R);
        Serial.print(", ");
        Serial.print(G);
        Serial.print(", ");
        Serial.print(B);
        Serial.println(")");
    }
    // Quan toca, tornem a fer una petició
    if (ara || ((millis() - darreraConnexio > periodeConnexio))) {
        if (redir){
            redir = false;
        } else {
            for (int j = 0; j < server_len; j++){
                server[j] = server0[j];
            }
            for (int j = 0; j < pag_len; j++){
                pagina[j] = pagina0[j];
            }
        }
        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();
        } 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.

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.