En aquest exemple farem 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.
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:
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'App Inventor 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 Extensions i triarem l'opció Apps Script. Se'ns obrirà una finestra similar a la següent:
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); }
També podríem seleccionar el full pel seu nom, d'aquesta manera no ens afectaria si es canvien d'ordre.
var sh = SpreadsheetApp.openById(IdFull).getSheetByName("Llistat"); // Agafem el full var dades = sh.appendRow(camps); // Afegeix una fila amb la llista de dades en format matriu
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.
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ó.
Picarem el botó Implementar.
En el desplegable triarem Nueva implementación i s'obrirà una finestra similar a la següent:
El primer cop que ho fem, haurem de picar en el botó que es mostra a continuació.
I triar l'opció Aplicación web. La finestra ens preguntarà en nom de qui volem que s'executi l'aplicació (li direm Yo) i qui hi té accés (li direm que qualsevol usuari). Finalment, picarem el botó Implementar.
Se'ns mostrarà una pantalla en la que se'ns indicarà l'adreça URL de l'aplicació, que haurem de copiar.
URL script | https://script.google.com/macros/s/^^fycbxqrJpVA-KT1sUd8HIta643R3bH4ixpDahttayGSGjkpHUBjPQ/exec |
Un cop estiguem, podem picar el botó Listo.
Atenció: Hem de recordar que cal guardar el programa (botó del disquet) abans d'implementar, si no ho fem ens implementarà la darrera versió guardada que no serà l'actual.
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 h, 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 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 a exemples anteriors. 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 <SPI.h> // Carreguem la biblioteca SPI #include <WiFiNINA.h> // 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))) { if(!ara) { hum = dht.readHumidity(); temp = dht.readTemperature(); data = "?"; 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); } 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"); } ara = false; } }
El programa anterior dona per suposat que les dades que envia l'script arriben en una de les dues darreres línies rebudes. Però podria passar que no fos així i, de fet, sovint Google modifica el funcionament de les seves aplicacions per introduir elements de seguretat. Podríem fer que l'script enviés les dades amb un marcador, de manera que la línia on hi ha les dades quedés perfectament identificada. Podeu trobar una mostra de com fer-ho en aquest exemple.
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); }
Podríem fer una aplicació d'App Inventor que ens permeti llegir el darrer valor guardat. Per fer-ho, caldria modificar l'script per afegir la possibilitat de rebre una comanda per a la lectura i que es retorni la corresponent resposta. A continuació es mostra una nova versió del primer script (el que afegeix valors un sota l'altre) que implementa aquesta opció. L'ordre per llegir el darrer valor guardat seria:
https://script.google.com/macros/s/AKfycbysb5cirzf08AzksvdF-tQ2wc3YMK3qGQJIgfiomwcMHqI3mvE/exec?acc=read
que ens retornaria la temperatura i la humitat separades per una barra. Hem marcat en groc les línies que hem afegit.
// 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 var numCols = 2; // Nombre de columnes que hem de llegir // 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 Accio = e.parameter.acc; var Temp = e.parameter.t; var Hum = e.parameter.h; if (Accio == undefined){ 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'; } } if (Accio == "read"){ 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 (range > 1){ // Si és 1 només hi ha els títols var filera = full[range -1]; // Agafem la darrera filera var resultat = ''; for(var j = 0;j < numCols;j++){ if (j > 0){ resultat = resultat + '/'; // La barra separarà els valors } resultat = resultat + filera[j]; } } } return ContentService.createTextOutput(resultat); }
Un cop modificat l'script, ja podem fer la nostra aplicació. L'aspecte de l'aplicació serà el següent:
Atès que en aquest web el nostre objectiu no és crear aplicacions, ens hem preocupat només del funcionament i la comprensió però no del disseny. Si ho desitgem, l'App Inventor ens permet personalitzar la disposició, els colors i altres propietats dels elements de la pantalla segons els nostres gustos.
Les següents taules mostren les propietats de cada un dels elements:
Propietat | Valor | Comentaris |
Nom | Label_T | Aquest nom l'hem de posar en el requadre Components |
FontSize | 50 | |
Width | Fill parent | |
Text | T °C | |
TextAlignment | center |
Propietat | Valor | Comentaris |
Nom | Label_H | Aquest nom l'hem de posar en el requadre Components |
FontSize | 50 | |
Width | Fill parent | |
Text | H % | |
TextAlignment | center |
Propietat | Valor | Comentaris |
Nom | Button_consulta | Aquest nom l'hem de posar en el requadre Components |
Text | Nova lectura |
Propietat | Valor | Comentaris |
Nom | Web_connecta | Aquest nom l'hem de posar en el requadre Components |
Atès que volem que els valors es llegeixin tant quan premem el botó com en inicialitzar la pantalla, hem creat la funció llegir i la cridem en els dos casos. El programa serà el següent:
En aquest altre espai web pots trobar informació sobre els scripts.
Aquesta obra d'Oriol Boix està llicenciada sota una llicència no importada Reconeixement-NoComercial-SenseObraDerivada 3.0.