14 de julio de 2019

NodeMCU: comunicación con Firebase. Escribe los datos de Firebase en Google Spreadsheet (VII-C).

Una vez que hemos encontrado la forma de comunicar los dos dispositivos NodeMCU a través de la base de datos Realtime de Firebase cabe preguntrase si es posible mantener un registro de los datos. ¿Qué tal si aprovechamos la hoja de cálculo de la entrada "NodeMCU: proyecto "botón del pánico" (VI-A): la hoja de cálculo y su script." para registrar los datos?

Lo primero que se me puede ocurrir es comunicar Firebase con Google Spreadsheet. Un poco de lectura y averiguo que se usa la clase UrlFetchApp para conseguirlo.

El script en google spreadsheet


function obtenerDatos() {
  // Dirección de la base de datos RealTime de Firebase
  var bd = "[midireccionenfirebase].firebaseio.com" ;
  // Secreto de la base de datos Firebase
  var secreto = 'ffsfhjrf9sadfnsdfynas7fya78ynf78n7sc';
  // Ruta de la base de datos RealTime
  var ruta = "Estaciones";
 
  var options = { method:'get', contentType: 'application/json' }; //opciones de la llamada a fetch
  var firebaseURL = 'https://'+bd+'/'+ruta+'.json?auth='+secreto; // construyo la ruta
 
  var resultado = UrlFetchApp.fetch(firebaseURL, options);    
 
  // Hoja de cálculo activa
  var sheet = SpreadsheetApp.getActiveSheet();
  var ultimalinea = sheet.getLastRow(); // obtiene la última línea
 
  // Fechas y horas actuales.
  var fecha=Utilities.formatDate(new Date, "GMT+1", "dd/MM/yyyy");
  var d = new Date();
  var hora = d.toLocaleTimeString();
 
  // Objeto JSON según el resultado
 
  var e = JSON.parse(resultado);
 
  for (clave in e) {
   var columna = clave.charCodeAt(0)-62; // columna de cada valor
   var valorActual = sheet.getRange(2,columna).getValue();
   if (valorActual!=e[clave]) { // Si los valores son distintos
      // 1) Cambio fecha y hora en la fila dos
         sheet.getRange(2,1).setValue(fecha);
         sheet.getRange(2,2).setValue(hora);
      // 2) Cambio valor actual en la fila dos
         sheet.getRange(2,columna).setValue(parseInt(e[clave]));
      // 3) Pongo fecha y hora en la última línea
         sheet.getRange(ultimalinea+1,1).setValue(fecha);
         sheet.getRange(ultimalinea+1,2).setValue(hora);
      // 4) Escribo en la última línea un registro de lo que acontece
         segunActivo(clave,e[clave],ultimalinea+1,columna,sheet);
   }  
 
   // Logging de los datos...
   Logger.log(clave);
   Logger.log(e[clave]);
 
  }
   
}

// funcion que escribe en el registro según si está activo o no
function segunActivo(estacion,dato,fila,columna,sheet) {
  if (dato==1) {
      sheet.getRange(fila,columna).setValue("Estación "estacion" se activa");
      sheet.getRange(fila,columna).setFontColor("darkgreen");
      sheet.getRange(fila,columna).setBackground("#AAFFAA");
  } else if (dato==0) {
      sheet.getRange(fila,columna).setValue("Estación "estacion" se desactiva");
      sheet.getRange(fila,columna).setFontColor("darkred");
      sheet.getRange(fila,columna).setBackground("#FFAAAA");
  }
}

  1. Defino las variables de mi programa: direccion a Firebase, su token o secreto y la ruta a los datos. Si no ponemos la ruta, devuelve un JSON más complicado.
  2. La variable options recoge las opciones de la llamada que vamos a hacer después con la clase UrlFetchApp y el método fetch. La variable firebaseURL recoge la ruta a los datos. El resultado de aplicar este método se guarda como un string en resultado.
  3. Obtengo el objeto hoja de cálculo, su última línea, y la fecha y hora actuales. 
  4. Consigo el objeto e como un objeto JSON de la cadena resultado. Método parse.
  5. Por cada clave dentro del objeto JSON e, 
  6. Calculo la columna que corresponde a la estación A, B,C...
  7. Obtengo el valor actual de la celda,
  8. Y si son distintos quiere decir que se ha desactivado o activado la estación luego...
  9. Cambio fecha y hora en la fila dos, y también escribo el valor en la celda correspondiente a cada estación en la fila dos.
  10.  En la siguiente línea vacía del registro escribo la fecha y la hora, y escribo, llamando a la función segunActivo el registro de la estación. 
Concluyendo: este script, si lo hago funcionar periódicamente (por ejemplo cada minuto), con esa cadencia comprobará si las estaciones están o no activas. Esta funcionalidad no me parece completa, ya que puede pasar que en ese lapso de tiempo la estación se active y se desactive, no registrando nada, o registrando con cierto retraso los sucesos.

La secuencia más correcta sería: cuando se active/desactive una estación, registrar el dato en Firebase y desde el NodeMCU también registrar el dato en la hoja de cálculo. Esto último podríamos hacerlo directamente, como dos sucesos independientes, o bien hacer que se ejecute un script desde el NodeMCU que lea la base de datos Firebase. 

= = = = = = = = = = =

Enlaces:

Lee datos desde firebase
https://stackoverflow.com/questions/25529863/reading-data-from-firebase-with-google-apps-script


Escribe datos en Firebase
https://www.sohamkamani.com/blog/2017/03/09/sync-data-between-google-sheets-and-firebase/

13 de julio de 2019

NodeMCU: comunicación con Firebase. La central lee datos (VII-B).

Montaje


Pues exactamente igual a la central de antes



El programa


/*
 * Created by Aurelio Gallardo Rodríguez
 * Based on: K. Suwatchai (Mobizt)
 *
 * Email: aurelio@seritium.es
 *
 * CENTRAL. Lectura
 *
 * Julio - 2019
 *
*/



//FirebaseESP8266.h must be included before ESP8266WiFi.h
#include "FirebaseESP8266.h"
#include <ESP8266WiFi.h>

#define FIREBASE_HOST "[elquesea].firebaseio.com"
#define FIREBASE_AUTH "sdfsgdlkm3485j3dfu992349322njfewrtet" //inventado
#define WIFI_SSID "miSSID"
#define WIFI_PASSWORD "miCONTRASEÑA"

//Define FirebaseESP8266 data object
FirebaseData bd;

String path = "/Estaciones"; // path a FireBase
char *estaciones[]= {"A","B","C","D","E"}; // Estaciones que voy a controlar

int i=1; // contador general

int activo=0; // Alarma

#define LED 5 // D1(gpio5)

void setup()
{

  Serial.begin(115200);
  pinMode(LED, OUTPUT);
 
  // conectando a la WIFI
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Conectando a la WiFi");
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.print(".");
    delay(300);
  }
  Serial.println();
  Serial.print("Conectado con IP: ");
  Serial.println(WiFi.localIP());
  Serial.println();

  // Conectando a la bd real Time Firebase
  Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
  Firebase.reconnectWiFi(true);

  //Set database read timeout to 1 minute (max 15 minutes)
  Firebase.setReadTimeout(bd, 1000 * 60);
  //tiny, small, medium, large and unlimited.
  //Size and its write timeout e.g. tiny (1s), small (10s), medium (30s) and large (60s).
  Firebase.setwriteSizeLimit(bd, "unlimited");
   
}

// Bucle principal
void loop() {

  activo=0; // reinicializo la variable
 
  for(i=0;i<=4;i++) {
    delay(1);
    activo=activo+rFB("/"+(String) estaciones[i]);  
  }
  

   digitalWrite(LED,(activo>=1)); // activo el LED si es mayor o igual a 1.
 
}



// Función de lectura
int rFB(String estacion){// lee el dato de la entrada correspondiente

  int valor=0;

  if (Firebase.getInt(bd, pathestacion)) {
    if (bd.dataType() == "int") {
      Serial.println("Dato leído: "+ path+estacion+ " --> " +(String) bd.intData());
      valor = bd.intData(); // retorna el valor      
    }
  } else {
    // Si no existe el dato de la estación, salta el error
    // Pero el error se muestra en pantalla (bd.errorReason()) Y REINICIALIZA LA UNIDAD, lo cual no quiero.
    Serial.println("Estoy dando un error... ojo");
    // Serial.println(bd.errorReason());  
  }

  return valor;
 
}

= = = = = = = = =  

Explico simplemente qué cambia respecto al programa de la entrada anterior.
  1. Defino un array tipo string con las cinco estaciones que puede leer la central: char *estaciones[]= {"A","B","C","D","E"}. Evidentemente esto es ampliable y modificable.
  2. Defino la variable activo. Será cero si no se activa ninguna estación, y uno o más de uno si se activa una o más estaciones.
  3. Solamente defino la salida del led de activación #define LED 5 y pinMode(LED, OUTPUT).
  4. SETUP --> Las instrucciones de conexión son las mismas, tanto para la conexión WiFi como para la de Firebase.
  5. rFB(String estacion) --> La función de lectura es idéntica a la de la entrada anterior. Solo cambia que devuelve cero si no es capaz de encontrar un dato, evitando ejecutar .errorReason(), y puedo pasar como variable la estacion que quiero leer. 
  6. LOOP --> El programa es sencillo. Empieza siempre con activo=0 y lee desde un bucle todas las estaciones (que deben tener valores 0 o 1). Suma sus valores, de forma que si una está activa, activo valdrá uno y 3 si hay tres activas. Actuará sobre el led encendiéndolo si activo es un número mayor o igual que uno.

NodeMCU: comunicación con Firebase. La estación escribe datos (VII-A).

Montaje



No cambiamos el Hardware de la estación ni de la central. Exactamente lo mismo.

El programa



/*
 * Created by Aurelio Gallardo Rodríguez
 * Based on: K. Suwatchai (Mobizt)
 *
 * Email: aurelio@seritium.es
 *
 * ESTACION. Escritura
 *
 * Julio - 2019
 *
*/


//FirebaseESP8266.h must be included before ESP8266WiFi.h
#include "FirebaseESP8266.h"
#include <ESP8266WiFi.h>

#define FIREBASE_HOST "[loquesea].firebaseio.com"
#define FIREBASE_AUTH "klf4fdgHjkdfgk4lldfglk4pmvnsflksdfg"
#define WIFI_SSID "miSSID"
#define WIFI_PASSWORD "miCONTRASEÑA"

//Define FirebaseESP8266 data object
FirebaseData bd;

String path = "/Estaciones"; // path a FireBase
String estacion = "/A"; // Estacion que voy a controlar

int i=1; // contador general

#define LED 5 // D1(gpio5)
#define BUTTON 4 //D2(gpio4)

int estado=0;
int estadoAnterior=0;
int estadoLuz=0;

void setup()
{

  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);

  // conectando a la WIFI
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("Conectando a la WiFi");
  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.print(".");
    delay(300);
  }
  Serial.println();
  Serial.print("Conectado con IP: ");
  Serial.println(WiFi.localIP());
  Serial.println();

  // Conectando a la bd real Time Firebase
  Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
  Firebase.reconnectWiFi(true);

  //Set database read timeout to 1 minute (max 15 minutes)
  Firebase.setReadTimeout(bd, 1000 * 60);
  //tiny, small, medium, large and unlimited.
  //Size and its write timeout e.g. tiny (1s), small (10s), medium (30s) and large (60s).
  Firebase.setwriteSizeLimit(bd, "unlimited");

  wFB(0); // Escribe un cero en la estación al arrancar
   
}

// Bucle principal
void loop() {

  estado=digitalRead(BUTTON);
  if (estado!=estadoAnterior){
    // el estadoAnterior ahora es el estado
    estadoAnterior=estado;
    // si estado es alto, se pasa de bajo a alto
    if (estado) {
       estadoLuz=!estadoLuz; // El estado de la luz cambia
       // sendDataToGoogleSheets(estacion,estadoLuz); // envío a googleSheet esa información
       wFB(estadoLuz); // en vez de enviarlo a Google Spreadsheet, se lo envío a Real Time BD Firebase
       Serial.println("Estado del LED: " + String(estadoLuz)); //Manda al monitor serie ese estado.
       delay(1);
    }
  }

  digitalWrite(LED,estadoLuz);
  // digitalWrite(LED,rFB()); // Escribe el estado en un LED, PERO leyéndolo de la BD.
}



// Función que escribe un dato en Firebase.
// Además, actualiza el dato si tiene la misma ruta, no genera un error.
void wFB(int dato) { //Write Data in FB

  if (Firebase.setInt(bd,path+estacion,dato)) {
      Serial.println("PASSED");
      Serial.println("PATH: " bd.dataPath());
      Serial.println("TYPE: " bd.dataType());
      Serial.println("ETag: " bd.ETag());
      Serial.print("VALUE: ");
      if (bd.dataType() == "int")
        Serial.println(bd.intData());
      else if (bd.dataType() == "float")
        Serial.println(bd.floatData(), 5);
      else if (bd.dataType() == "double")
        printf("%.9lf\n", bd.doubleData());
      else if (bd.dataType() == "boolean")
        Serial.println(bd.boolData() == 1 ? "true" : "false");
      else if (bd.dataType() == "string")
        Serial.println(bd.stringData());
      else if (bd.dataType() == "json")
        Serial.println(bd.jsonData());
      Serial.println("------------------------------------");
      Serial.println();
  } else {
      Serial.println("FAILED");
      Serial.println("REASON: " bd.errorReason());
      Serial.println("------------------------------------");
      Serial.println();
  }
}

// Función de lectura
int rFB(){// lee el dato de la entrada correspondiente

  if (Firebase.getInt(bd, pathestacion)) {

    if (bd.dataType() == "int") {
      Serial.println("Dato leído: " + (String) bd.intData());
      return bd.intData();      
    }

  } else {
    Serial.println(bd.errorReason());  
  }
}

= = = = = = = = = =
  1. El código está desarrollado a partir del ejemplo básico de la biblioteca FirebaseESP8266. Además cargamos la biblioteca ESP8266WiFi en la cabecera del programa. Para programas más elaborados quizás es interesante la lectura detallada del README de la biblioteca FirebaseESP8266.
  2. Defino cuatro parámetros: FIREBASE_HOST, FIREBASE_AUTH, WIFI_SSID y WIFI_PASSWORD. En el post anterior expliqué dónde conseguir los dos primeros, los que me permiten conectarme a FIREBASE y los otros dos ya los hemos visto con anterioridad.
  3. Creamos un objeto de base de datos de la clase FirebaseData: FirebaseData bd.
  4. Defino después varias variables:  path, ruta dentro de Firebase a los campos de la base de datos, el nombre de la estación que estoy manejando "A", un contador general, las definiciones de los pines y de las distintas variables de estado, que ya vimos en entradas anteriores.
  5. SETUP --> inicializamos la conexión monitor serie, definimos los pines como entradas y salidas e iniciamos el proceso de conexión a la red Wifi, muy similar a programas anteriores. Posteriormente conectamos con la base de datos Firebase: Firebase.begin conecta a la base de datos con su token de autentificación, Firebase.reconnectWiFi es una función que viene en el ejemplo, opcional según README, y que supongo que comprobará que estamos conectados a la WiFi. Las siguientes dos instrucciones:  Firebase.setReadTimeout(bd, 1000 * 60) y Firebase.setwriteSizeLimit(bd, "unlimited") no vienen explicadas en el README  de la biblioteca FirebaseESP8266. Me imagino que la primera es el tiempo de espera límite cuando se realiza una petición de lectura y la otra función tiene que ver con las peticiones de escritura y también el tiempo de espera, según los comentarios en el ejemplo. El setup acaba llamando a la función de escritura en Firebase con un cero. Lógico, si reinicio la estación, esta debe tener como valor inicial cero.
  6. LOOP --> las instrucciones son casi análogas a la de la entrada anterior. Reconocen el cambio de estado del botón (flanco de subida) y cambian el estado del LED. Este estado se envía ahora, no a una hoja de cálculo de Google, sino a la función wFB(estadoLuz), que escribirá el dato en la base de datos Firebase. También ponen el estado en el LED, y, comentado, aparece la forma de hacerlo leyendo el dato desde la base de datos: rFB().
  7. wFB(int dato) --> esta función escribe un entero en la base de datos. La función que lo hace es Firebase.setInt(bd,path+estacion,dato) devuelve true si consigue hacerlo. El parámetro bd es la base de datos, path+estacion es la ruta al campo donde se guarda el dato "/Estacion/A" y dato es el dato que hemos pasado a la función. Si efectivamente graba el dato, se imprime información complementaria en el monitor serie: tipo, ruta, Etag, y el valor. Si no es capaz de grabar el dato, lanza un mensaje de error con el método .errorReason(). 
  8. rFB() --> esta función, no imprescindible, pero que se añade al programa, lee un dato en la ruta path+estacion (o sea, "/Estacion/A"). Simplemente se comprueba si se produce la lectura con Firebase.getInt(bd, path+estacion), y, si existe, se comprueba si es un entero y se devuelve con return bd.intData(). Asimismo si no existiera, se lanza el método .errorReason(). 
NOTA: si no existe el campo, la función de escritura lo crea. Si volvemos a escribir el mismo campo con otro valor, se actualiza. Sin embargo, si intentamos leer un campo que no existe, se lanza un error.  El método .errorReason() muestra las causas de no haberlo conseguido, pero también reinicia el módulo NodeMCU.


NodeMCU: comunicación con Firebase. Configuración (VII)

Siguiendo con el proyecto anterior, del botón del pánico, modifico los programas para poder comunicarlo con la base de datos Real Time de Firebase.

Firebase es una plataforma con diversas funcionalidades orientada al desarrollo de aplicaciones web y aplicaciones móviles. Al crear un proyecto de Firebase, en principio para ilustrar la comunicación entre dos NodeMCU, me centraré exclusivamente en el desarrollo de una pequeña base de datos.



Recuerdo, son necesarios los siguientes datos para los programas de los NodeMCU:
  1. Identificación de la base de datos RealTime, por ejemplo: https://proprueba-5e0d7.firebaseio.com/
  2. Un token o "secreto", que podemos encontrar en Configuración del proyecto >> Cuentas de servicio >> Secretos de la base de datos y copiar el dato del tipo "W6BogjZIO37kV2yAOSvQtyo23noureOKtEljKj71" que aparece en la lista.

10 de julio de 2019

Listado de las entradas de NodeMCU


NodeMCU


NodeMCU: proyecto "botón del pánico" (VI-C): el NodeMCU como central.

El esquema.

Simplemente voy a ilustrar el funcionamiento. Con un simple LED conectado al pin D1 será suficiente.


Modificando el script de Google


Pensé en crear un nuevo script para la central. Pero no lo he visto necesario. Una modificación como la que sigue hace el trabajo.

 } else if (estacion.toUpperCase()==="CENTRAL") { // caso del requerimiento de la central
   
    var respuesta = {};
    var est="";
    var valor = 0;
    var suma = 0;
   
    for (var i=3;i<=7;i++) {
        est = sheet.getRange(1,i).getValue();
        respuesta[est]=sheet.getRange(2,i).getValue();
        suma = suma respuesta[est];
    }
   
    respuesta["alarma"]=suma; // si es mayor o igual a 1, se activa la alarma
   
    var output = HtmlService.createHtmlOutput('EMPEZAR'JSON.stringify(respuesta)'TERMINAR');
    return output;
   
    // Logger.log(JSON.stringify(respuesta)); 


  } 

De forma simple, lee el contenido de las columnas desde la C a la G en la fila 2 y las asigna a un arreglo por clave - valor. Además crea un valor alarma que contendrá la suma de todos los valores. Y de la misma forma que antes, se envía ese arreglo como salida html.

El programa para el NodeMCU central.

 


/*
  Envio de datos para Google Sheets
  Hardware Utilizado: Nodemcu v1.0,
  Autor:Aurelio Gallardo Rodríguez
*/


// -- Bibliotecas auxiliares --

#include <ESP8266WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>

// -- Hardware --


// -- Variables y constantes --
const char* ssid = "miSSID"; // Rellena con el nombre de tu red WiFi
const char* password = "miCONTRASEÑA"; // Rellena con la contraseña de tu red WiFi

const char* host = "script.google.com"; // Este es el host de los scripts de google.
const int httpsPort = 443;

// Huella digital del script de Google:
// D4:9E:40:F4:53:7A:04:93:38:F7:6B:4B:DC:70:02:A9:03:98:C2:DE
const char* fingerprint = "D4 9E 40 F4 53 7A 04 93 38 F7 6B 4B DC 70 02 A9 03 98 C2 DE";
// const char* fingerprint = "46 B2 C3 44 9C 59 09 8B 01 B6 F8 BD 4C FB 00 74 91 2F EF F6";

String googleSheetsID = "miCODIGO"; // El que me da al implementar una aplicación web en el script.


WiFiClientSecure cliente;
DynamicJsonDocument doc(1024);

int alarma = 0; // detecta si se ha activado la alarma

int tiempoActivacionMinimo = 3000; // Tiempo en ms de activación mínimo

#define LED 5 // D1(gpio5)

// -- Setup --

void setup() {
  Serial.begin(115200);
  pinMode(LED, OUTPUT);
  connectToWiFi();
}

// -- LOOP: lectura del sensor y envío de datos según el intervalo--

void loop() {

  sendDataToGoogleSheets("CENTRAL");  // se conecta a Google Spreadsheet

  if (alarma>=1) {
    digitalWrite(LED,true); // simplemente cuando cambie el valor de "alarma" se ilumina
    delay(tiempoActivacionMinimo); // dura encendida un tiempo
  } else {
    digitalWrite(LED,false);  
  }
 
}

// -- Funciones auxiliares --

// -- Conectando a la red Wifi. Muestra la IP recibida --

void connectToWiFi() {
  Serial.println("Conectando a la red: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  WiFi.mode(WIFI_STA);
  Serial.println("");
  Serial.println("Conectado!");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
  delay(1000);
}


/* Función de conexión. Importante la instrucción  cliente.setInsecure(); para conectar de forma anónima
 */


void sendDataToGoogleSheets(String nombreEstacion) {

  String cadena="";
 
  Serial.print("Conectando a: ");
  Serial.println(host);

  cliente.setInsecure();
 
  if (!cliente.connect(host, httpsPort)) {
    Serial.println("Falla la conexión a Google Sheets -->" String(host) ": " String(httpsPort) );
    return;
  }
 
  if (cliente.verify(fingerprint, host)) {
    Serial.println("Certificado OK");
  }
  else {
    Serial.println("Debes comprobar certificado");
  }

  String url = "/macros/s/" googleSheetsID "/exec?estacion=" nombreEstacion;
 
  Serial.print("Petición  URL ");
  Serial.println(url);

  cliente.print(String("GET ") url " HTTP/1.1\r\n"
               "Host: " host "\r\n"
               "User-Agent: BuildFailureDetectorESP8266\r\n"
               "Connection: close\r\n\r\n");

  // Es necesario leer las cabeceras  
  Serial.println("Request enviada");
  while (cliente.connected()) {
    String line = cliente.readStringUntil('\n');
    if (line == "\r") {
      Serial.println("Cabeceras Recibidas");
      Serial.println(line);
      break;
    }
  }

  // lee el contenido de la respuesta y lo almacena en la variable cadena
  while (cliente.available()) {
      char c = cliente.read();    
      cadena = cadena (String) c;
  }
  Serial.println("Respuesta directa del servidor");
  Serial.println(cadena);
  Serial.println("==============================");
  Serial.println();
 
  // PROBLEMA: la respuesta, por motivos de seguridad, no es jamás el texto, o el html, sino que está codificada.
  // He resuelto este problema encerrando la respuesta (JSON) entre dos palabras, EMPEZAR y TERMINAR.
  // Una vez localizada la información, la extraigo con la función midString.
  // Los caracteres, en codificación unicode, los transformo en sus caracteres con varias líneas de código...
  // Y voalá... ¡obtiene la respuesta en formato JSON de texto!
  String respuesta =midString(cadena,"EMPEZAR","TERMINAR");
  char comillas = 34;
  respuesta.replace("x7b","{");
  respuesta.replace("x7d","}");
  respuesta.replace("x22",String(comillas));
  respuesta.replace("\\",""); // doble slash para que lo reconozca ??
  // Serial.println(respuesta);

  Serial.println("Respuesta filtrada desde el servidor");
  Serial.println(respuesta);
  Serial.println("==============================");
  Serial.println();

  // Usar bibliteca JSON
  deserializeJson(doc,respuesta);
  JsonObject obj = doc.as<JsonObject>();

  alarma = (int) obj[String("alarma")]; // extraigo la variable alarma y la pongo en la variable del mismo valor.
   
  Serial.println("Respuesta extraída en variables deserializando la cadena en un objeto JSON");
  Serial.println("Estado de la alarma: "(String) alarma);
  Serial.println("==============================");
  Serial.println();

  delay(1000); // Entre conexión y conexión 1 segundo.
 
  cliente.stop(); //cierra la conexión

  // ===========================
  /* Obteniendo la respuesta  */
  // ===========================


 
  /*
  String line = cliente.readStringUntil('\n');
  Serial.println(line);
  if (line.startsWith("{\"state\":\"success\"")) {
    Serial.println("Éxito");
  } else {
    Serial.println("El envío falló!");
  }
 
  Serial.println("Respuesta:");
  Serial.println("==========");
  Serial.println(line);
  Serial.println("==========");
  Serial.println("¡Cerrando la conexión!"); */

 
}


/* función texto intermedio */
String midString(String str, String start, String finish){
  int locStart = str.indexOf(start);
  if (locStart==-1) return "";
  locStart += start.length();
  int locFinish = str.indexOf(finish, locStart);
  if (locFinish==-1) return "";
  return str.substring(locStart, locFinish);
}

El programa es muy parecido al de la estación. Inclusive más simple. Lee el valor de alarma, y si es igual o mayor que uno, activa el LED.

1.- Defino simplemente el LED como salida en D1 (GPIO 5).
2.- Defino la variable alarma (inicialmente a cero) y tiempoActivacionMinimo (inicialmente a 3000ms). La primera recogerá el estado de la alarma (si es uno o mayor se activará) y la otra variable recogerá el mínimo tiempo que permanecerá activada (independientemente si una estación desactiva la alarma seguirá activa durante ese tiempo). 
3.- LOOP --> Se conecta a sendDataToGoogleSheets pasando el valor clave CENTRAL. La función recogerá y modificará el valor de la variable global alarma. Luego si alarma es uno o mayor o igual a uno, activará el LED y en caso contrario lo apagará.
4.- sendDataToGoogleSheets --> Las modificaciones realizadas permiten la activación del script de Google pasando la clave CENTRAL, la cual devuelve como respuesta el estado de cada estación y la suma de sus valores, y asigna esta última a la variable global alarma. alarma = (int) obj[String("alarma")]. 

Conclusiones.

 

Experimentalmente, el método funciona pero no parece muy efectivo. Recordad que en el post anterior expliqué que a veces la respuesta del servidor no es la que se espera y no puede leerse la cadena de datos desde el script de Google. Sin embargo, como cada segundo se repite la petición de la central, antes o después se activa. Si el valor tiempoActivacionMinimo es pequeño, incluso pueden producirse parpadeos. No sé por qué unas veces sí y otras no. 

Aumentar el delay (y ponerlo en el loop y no al final de la función  sendDataToGoogleSheets) puede mejorar el comportamiento. Quizás no dé respuesta correcta si se realizan peticiones muy seguidas, pero la verdad es que no sé por qué. 

Tengo que investigar más. Saber si hay alguna manera de obtener una respuesta directa DESDE una hoja de cálculo de Google. Conocer otras posibles formas de intercomunicar los NodeMCU. Todo un mundo por investigar.
 

Posibles mejoras. 

 

  1. Evidentemente mejorar el hardware y la funcionalidad. Por ejemplo, que los leds de las estaciones y de la alarma indiquen, mediante parpadeos rápidos, si están puestos en estación, funcionando. O la central ampliarla con un twitter o un altavoz para producir la señal de alarma, etc.
  2. Las mencionadas respecto a la comunicación entre los aparatos. 
  3. Alimentación de las estaciones. Un botón del pánico se encuentra bajo una mesa, luego debería activarse con una batería.