Internet de les coses amb ESP32 i ESP8266

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

Consultem les lectures de temperatura i humitat des de Telegram

Aquest exemple és una modificació d'un exemple anterior en el que en lloc de llegir l'estat del LED escriurem al full de càlcul els dos valors corresponents a les lectures de temperatura i humitat. Per un altre costat, podrem llegir els darrers valors guardats des de Telegram.

En el full de càlcul posarem dues columnes, una per a la temperatura i una per a la humitat.

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.

Aquest és l'script que farem servir:

// Funció per interaccionar amb el full de càlcul des de Telegram
// Oriol Boix, 2020   Basat en un exemple de Ferran Mas
// 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
var token = "^^34368844:AAFIpk-e7j3UZtMQYQaTduf4hEhnDqIcNXI"; // API Token de Telegram 
var telegramUrl = "https://api.telegram.org/bot" + token;  // Url que comunica el nostre xatbot amb Telegram 
// Adreça de l'script (ens la donen en el moment de publicar-lo)
var webAppUrl = "https://script.google.com/macros/s/AKfycbzXSTtXkzqyJ_sABbGZanVX2EuRAIUB5vct53WXp6-MbLscpLHO/exec";
// Id del full de càlcul (l'obtenim quan el compartim)
var IdFull = "15CWPEcIVpS512vGw3ziCZHEuMev838GAaQ6yJKnS2Pg";  
// Funció que envia un text a Telegram
function sendText(chatId,text_env){  
  var cont = {
    method: "post",
    payload: {
      method: "sendMessage",
      chat_id: String(chatId),
      text: text_env,
      parse_mode: "HTML"
    }
  }
  UrlFetchApp.fetch(telegramUrl + '/', cont);
}
var sh = SpreadsheetApp.openById(IdFull);  // Obrim el full de càlcul
var sheet = sh.getSheets();  // Agafem els fulls
// 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;
    sheet[0].appendRow(camps);   // Afegeix una fila amb la llista de dades en format matriu 
    resultat = 'Valors guardats';
  }
  return ContentService.createTextOutput(resultat); 
}
// Funció que s'executa quan l'script rep una ordre post
function doPost(e){ // Funció que rep les dades enviades per Telegram 
  var temp = 200;
  var humi = 200;
  var full = sheet[0].getDataRange().getValues();
  range = sheet[0].getLastRow();  // Índex de la darrera filera
  if (range > 1){    // Si és 1 només hi ha els títols
    var filera = full[range -1];    // Agafem la darrera filera
    temp = filera[0];
    humi = filera[1];
  }
  var fet = 0;
  var resposta = '';
  var dades = JSON.parse(e.postData.contents);  // Tracta les dades rebudes com a JSON i les guarda a dades
  // Extraiem les dades
  var text = dades.message.text;  // Recupera el text del missatge
  var id = dades.message.chat.id;  // Recupera l'identificador de la finestra des de la que s'ha enviat
  var id_usuari = dades.message.from.id; // Recupera l'identificador de l'usuari que ha escrit el missatge
  var id_missatge = dades.message.message_id; // Recupera l'identificador del missatge
  var lang = dades.message.from.language_code ;  // Recupera la llengua que l'usuari té configurada a Telegram 
  var nom_usuari = dades.message.from.first_name ;  // Recupera el nom de l'usuari que ha enviat el missatge
  var posicio = dades.message.location;  // Recupera la localizació de l'usuari, si és possible
  // La comanda pot tenir paràmetres, separats amb @
  // Descomposem el text rebut a partir de les @
  var comanda = text.split("@");  // Separa per les @
  var cmd = comanda[0];  // Ho passem a minúscules
  if (cmd == '/llistat'){
    var llista = "/llistat - Llista de les comandes disponibles\n";  // Text que es retornarà a Telegram
    llista = llista + "/llegir - Consulta les dades de temperatura i humitat\n";
    llista = llista + "/temp - Consulta les dades de temperatura\n";
    llista = llista + "/humi - Consulta les dades d'humitat\n";
    sendText(id,llista);
    fet = 1;
  }
  if ((cmd == '/llegir') || (cmd == '/temp') || (cmd == '/humi')){
    if ((temp < 200) && (humi < 200)){    // Si és 200 no hi ha valors
      if (cmd == '/llegir'){
        resposta = temp + " °C\n" + humi + " %\n";
      }
      if (cmd == '/temp'){
        resposta = temp + " °C\n";
      }
      if (cmd == '/humi'){
        resposta = humi + " %\n";
      }
    } else {
      resposta = "No hi ha cap dada guardada";
    }
    sendText(id,resposta);
    fet = 1;
  }
  // Si no hem entrat a cap if vol dir que la comanda no s'ha reconegut
  if (fet == 0){
    resposta = "Comanda incorrecta";
    sendText(id,resposta);
  }
}

En el nostre script afegim una línia al full de càlcul cada cop que arriben dades. Podríem fer que sempre s'escrivissin a la mateixa filera si canviem la línia marcada en color per les següents:

    var valu = new Array(1);
    valu[0] = camps;  // Converteix el vector en una matriu d'una filera i una columnes
    var range = sheet[0].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[0].getRange(range, 1, 1, 2);  // Selecciona una casella
      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[0].appendRow(camps);
    }

I aquest serà el programa del microcontrolador:

#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";
const String pagina_base = "/macros/s/AKfycbwzJfJIZd9-yS6prAbLRDNHUTFT4Ua122Kv2yb-jo3jd73Gl9x0/exec";
String pagina0 = "";
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 = "?";
        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");
        }
    }
}

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.

En aquest altre espai web pots trobar informació sobre els scripts.

 

 

 

 

 

 

 

 

 

 

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