Monitorización IoT de Presión en Tiempo Real con MKR GSM, Arduino Cloud y Google Sheets
Componentes y materiales
| Componente | Cantidad |
|---|---|
| Arduino UNO | 1 |
| Arduino MKR GSM 1400 | 1 |
| Transductor de presión (0-150 psi) | 1 |
| Sensor temperatura/humedad Adafruit DHT22 | 1 |
| Módulo RTC DS3231 | 1 |
| Convertidor de nivel lógico Adafruit BSS138 4-canales I2C | 1 |
| Batería LiPo 3.7V 2000 mAh (o mayor) | 2 |
| Convertidor step-up de voltaje | 1 |
| Kit básico con LEDs y resistencias surtidas | 1 |
| Protoboard (genérica) | 1 |
Aplicaciones y servicios online
- Google Sheets
- Arduino IoT Cloud
- Arduino Web Editor
- Arduino IDE
Descripción del proyecto
Objetivo
Crear un prototipo de dispositivo de bajo coste para monitorizar presión en equipos industriales en ubicaciones remotas usando datos móviles.
Recorrido del proyecto
Paso a paso siguiendo el flujo de datos desde el transductor de presión hasta la notificación por email generada por el script en Google Sheets.
Paso 1: Transductor de presión a Arduino Uno
El transductor convierte la presión en señal eléctrica analógica. El Arduino Uno convierte esa señal analógica en presión (psi).
Paso 2: Arduino Uno a MKR GSM 1400 por Serial
Comunicación entre dos Arduinos por Serial:
- Conectar RX del Dispositivo #1 a TX del Dispositivo #2
- Conectar TX del Dispositivo #1 a RX del Dispositivo #2
- Ambos dispositivos deben compartir masa (GND común)
Frecuencia de transmisión de datos (Uno → MKR GSM 1400):
- NORMAL: Cada 30 minutos (
transmitFrequency) el Uno envía datos por serial al MKR GSM 1400, que los transmite a la nube. - DISPARADOR ALTO/BAJO: Si la presión supera 40 psi (
highTrigger) o baja de 20 psi (lowTrigger) y se mantiene así más de 2 minutos (dtLastTriggerLimit), el Uno envía datos inmediatamente. - SONDEO BAJO DEMANDA: Si el pin A1 del Uno se pone en HIGH, envía datos al MKR GSM 1400. Dos formas de poner A1 en HIGH: (1) pulsador en protoboard; (2) si el pin A3 del MKR GSM 1400 está en HIGH, empuja A1 a HIGH. Como A3 se controla desde Arduino Cloud, se puede obtener la presión actual remotamente en cualquier momento sin esperar transmisión programada.
Notas:
- El sketch del Uno podría modificarse para monitorizar múltiples entradas (temperatura, humedad, voltaje batería, etc.) con puntos de ajuste alto/bajo además de presión.
- El código de conversión de señal analógica a presión (psi) se basa en este video de YouTube: https://www.youtube.com/watch?v=AB7zgnfkEi4
- Este post del foro Arduino sobre "Serial Input Basics" fue muy útil para la comunicación serial entre dispositivos: https://forum.arduino.cc/index.php?topic=288234.0
El código para Arduino Uno está adjunto con comentarios explicativos. Nombre de archivo: "InstrumentReader".
Paso 3: MKR GSM 1400 a Arduino Cloud por celular
El MKR GSM 1400 procesa los datos seriales del Arduino Uno y los transmite a Arduino Cloud usando datos móviles.
En el código del MKR GSM 1400 verás Serial1.read en vez de Serial.read. La referencia de Arduino explica bien la diferencia: en placas MKR, los pines TX/RX se acceden vía Serial1. Ver documentación oficial.
Arduino Cloud
El proyecto usa 2 variables en Arduino Cloud. La imagen muestra cómo se muestran en el dashboard:
- dataStringCloud: paquete con todos los datos del dispositivo. Este enfoque simplifica el procesamiento en Google Sheets vs. variables individuales (difícil distinguir valor sin cambiar de valor no actualizado). Los datos se parsean en Google Sheets.
- pinCloud: controla el MKR GSM 1400 desde Arduino Cloud. Un
switchen el sketch actúa según el valor:pinCloud = 1pone pin A1 en HIGH (enciende LED en placa);pinCloud = 2pone pin A3 en HIGH → Arduino Uno envía datos actuales (como se describió).
Código para MKR GSM 1400 adjunto con comentarios. Nombre: "CommunicationsDevice".
Paso 4: Arduino Cloud a Google Sheets vía Webhook
Los datos se transfieren de Arduino Cloud a Google Sheets usando un webhook. El núcleo es la función doPost escrita en el script de Google Sheets.
Resumen rápido de configuración del webhook. Nota: el proceso empieza en Google Sheets. No llegas a Arduino Cloud hasta el final. Para ir de A a B, empieza en B.
- Crear nuevo archivo Google Sheets
- Menú "Herramientas" → "Editor de secuencias de comandos"
- Escribir código con función doPost (ver
GoogleSheetsScript.jsadjunto) - Menú "Publicar" → "Desplegar como aplicación web..."
- En el diálogo: (1) Versión del proyecto: siempre seleccionar "Nuevo" en el desplegable; (2) Ejecutar la app como: "yo (tuemail@gmail.com)"; (3) Quién tiene acceso: "Cualquiera, incluso anónimo"
- Pulsar "Desplegar"
- Aparece segunda ventana con "URL actual de la aplicación web". Esta URL se copia y pega en la pestaña webhook de Arduino Cloud. Nota: la URL no cambia con versiones.
- Pulsar "Aceptar", ¡listo!
Gran parte del código JavaScript se basa en el proyecto "Arduino IoT Cloud Google Sheets Integration": enlace al proyecto
Paso 5: Usar Google Sheets para parsear datos
Google Sheets parsea los valores individuales de dataStringCloud y muestra los registros únicos transferidos desde Arduino Cloud.
Enlace a Google Sheets de prueba reciente (celdas coloreadas según leyenda en cada hoja): hoja de cálculo de prueba
Paso 6: Usar Google Sheets para enviar notificaciones
En el archivo JavaScript (GoogleSheetsScript.js) hay dos funciones:
doPost: recibe datos del webhook de Arduino Cloud. Se ejecuta al llegar nuevos datos.sendEmail: envía emails basándose en valores extraídos de la hoja "Data". Se ejecuta cada minuto según trigger.
Pasos para configurar trigger de sendEmail:
- Abrir Google Sheets
- "Herramientas" → "Editor de secuencias de comandos"
- En editor: "Editar" → "Activadores del proyecto actual"
- En G Suite Developer Hub: "Añadir activador" (esquina inferior derecha)
- Configurar para ejecutar
sendEmailen base temporal (Time-Driven). Esto permite generar alertas por email cuando el dispositivo deja de actualizar.
Duración de batería
~24 horas. Optimizable apagando/quitando displays o eliminando sensores no esenciales (DHT22, DS3231).
Consumo de datos
~0.7 MB/día. Optimizable reduciendo tamaño o frecuencia de transmisiones (ej. solo presión vs. presión+temperatura+humedad+tiempo; cada hora vs. cada 30 min).
Coste del proyecto
Total = $241
- Arduino MKR GSM 1400 ($70)
- Arduino Uno ($22)
- 2× Baterías LiPo 3.7V ($30)
- 2× Displays LED ($29)
- Caja plástica resistente a intemperie ($22)
- Sensor de presión ($19)
- Sensor temp/humedad DHT22 ($10)
- Módulo RTC DS3231 ($5)
- Convertidor step-up ($5)
- Convertidor nivel lógico ($4)
- Varios - LEDs, resistencias, cableado, etc. ($25)
Hardware / Herramientas
Todo comprado en:
- Arduino Online Store (https://store.arduino.cc/)
- Adafruit (https://www.adafruit.com/)
- Amazon (https://www.amazon.com/)
- Harbor Freight
- Home Depot
Conclusión
Gracias por revisar este proyecto. Cualquier pregunta, feedback, comentario o sugerencia es bienvenido y apreciado.
Código
InstrumentReader - Sketch para Arduino Uno
// Sketch 1 de 2 // Arduino Uno
// Los datos los recoge este dispositivo y transmite al MKR 1400 por serial
// El sensor DHT22 requiere 2 librerías, pero solo una se llama en el código.
// (1): DHT Sensor Library: https://github.com/adafruit/DHT-sensor-library
// (2): Adafruit Unified Sensor Lib: https://github.com/adafruit/Adafruit_Sensor
#include <SPI.h>
#include <Wire.h>
#include <DS3231.h> // Módulo RTC
#include <DHT.h> // DHT22
#include "U8glib.h" // Display OLED SPI Velleman 128x64
// Nota: Otra librería U8glib estándar no funcionó para este display. Funcionó con la del fabricante.
// Librería: https://www.velleman.eu/support/downloads/?code=VMA437
// Sintaxis: https://github.com/olikraus/u8glib/wiki/userreference & https://github.com/olikraus/u8glib/wiki/thelloworld
//#include <SD.h> // Opción: guardar en tarjeta SD en Ethernet Shield para Uno
RTClib RTC;
#define DHTPIN 11
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
U8GLIB_SH1106_128X64 u8g(3, 4, 6, 7); // (CLK/SCK: 3, MOSI: 4, CS: 6, DC(A0): 7)
int y_pos = 0;
//const int chipSelect = 10; // Opción SD
//float fileSizeSD = 0.0; // Opción SD
int i = 0; // Contador de lecturas del Uno
const int ledPin = 9; // Indicador transmisión (parpadea al transmitir)
const int ledPin2 = 8; // Indicador transmisión forzada (pulsador o MKR 1400 desde cloud)
const int buttonPin = A1;
int buttonState = 0;
int transmitFrequency = 30; // Frecuencia envío datos (minutos)
String pTransmitDateTime = "";
int transmitCounter = 0;
int pTransmitMinute = 0;
int ptriggerTransmitAlertIndicator;
float cRuntimeAtTriggerStart = 0.0;
float dtLastTrigger = 0.0;
int triggerCounter = 0.0;
int triggerTransmitAlertCounter = 0;
// Variables de control de disparador
float lowTrigger = 20.0;
float highTrigger = 40.0;
float dtLastTriggerLimit = 2.0; // Si condición se cumple este tiempo, genera alerta
void setup(void) {
Serial.begin(9600);
Wire.begin();
dht.begin();
pinMode(ledPin, OUTPUT);
pinMode(ledPin2, OUTPUT);
pinMode(buttonPin, INPUT);
u8g.setRot180(); // voltear pantalla si hace falta
// Opción SD
// Serial.print("Inicializando tarjeta SD...");
// if (!SD.begin(chipSelect)) { Serial.println("Fallo tarjeta"); while (1); }
// Serial.println("tarjeta inicializada.");
}
void loop(void) {
delay(5000);
DateTime now = RTC.now();
float cRuntime = millis()/60000;
float p = getPressure();
buttonState = digitalRead(buttonPin);
if(buttonState == 1) {
digitalWrite(ledPin2, HIGH);
delay(30000); // delay para que MKR1400 esté listo si Uno:buttonPin puesto HIGH por MKR1400:pingPin
} else {
digitalWrite(ledPin2, LOW);
}
float h = dht.readHumidity();
float t = dht.readTemperature(true); // true = °F, false = °C
int transmitIndicator = 0;
if(now.minute() % transmitFrequency == 0 && now.minute() != pTransmitMinute) {
transmitIndicator = 1;
pTransmitMinute = now.minute();
pTransmitDateTime = String(now.hour())+":"+String(now.minute())+":"+String(now.second());
}
int triggerStatus = 0;
if(p <= lowTrigger || p >= highTrigger) {
triggerStatus = 1;
triggerCounter++;
} else {
triggerCounter = 0;
}
if(triggerStatus == 1 && triggerCounter == 1) {
cRuntimeAtTriggerStart = cRuntime;
}
dtLastTrigger = cRuntime - cRuntimeAtTriggerStart;
int triggerTransmitAlertIndicator = 0;
if((dtLastTrigger > dtLastTriggerLimit) && triggerStatus == 1) {
triggerTransmitAlertIndicator = 1;
triggerTransmitAlertCounter++;
} else {
triggerTransmitAlertCounter = 0;
}
if(triggerTransmitAlertCounter > 0 && triggerTransmitAlertCounter % 10 == 0) {
flashLED(2,500);
}
int triggerPushTransmitAlertIndicator = 0;
if((triggerTransmitAlertIndicator == 1 && triggerTransmitAlertCounter == 1) ||
ptriggerTransmitAlertIndicator != triggerTransmitAlertIndicator) {
triggerPushTransmitAlertIndicator = 1;
flashLED(5,500);
delay(5000);
}
ptriggerTransmitAlertIndicator = triggerTransmitAlertIndicator;
// Crear strings
String dataString = "";
String cDateTime = "";
String cHumTemp = "";
String cP = "";
dataString += "<"+String(i)+","+String(triggerTransmitAlertIndicator)+","+String(dtLastTrigger,0)+","+String(buttonState)+", "+String(now.month())+","+String(now.day())+","+String(now.year())+", "+String(now.hour())+","+String(now.minute())+","+String(now.second())+", "+String(h)+","+String(t)+","+String(p)+">";
cDateTime += String(now.month())+"/"+String(now.day())+"/"+String(now.year())+" "+String(now.hour())+":"+String(now.minute())+":"+String(now.second());
cHumTemp += "H:"+String(h)+"% T:"+String(t)+"degF";
cP += "P:"+String(p)+"psi";
if(transmitIndicator == 1 || triggerPushTransmitAlertIndicator == 1 || buttonState == 1) {
char dataArray[100];
dataString.toCharArray(dataArray, 100);
Serial.println(dataArray);
flashLED(10,500);
transmitCounter++;
}
delay(100);
// Bucle display
u8g.firstPage();
do {
draw(cDateTime,cHumTemp, cP,i,transmitCounter,now.minute(),transmitFrequency,pTransmitMinute);
} while(u8g.nextPage());
delay(1000);
// writeToSD(dataString); // Opción SD
i++;
}
void draw(String DcDateTime,String DcHumTemp, String DcP, int Di, int DtransmitCounter,int DnowMinute,int DtransmitFrequency, int DpTransmitMinute) {
u8g.begin();
u8g.setFont(u8g_font_5x7);
u8g.setFontPosTop();
u8g.setPrintPos(0,0); u8g.print(DcDateTime);
u8g.setPrintPos(0,8); u8g.print(2);
u8g.setPrintPos(10,8); u8g.print(DcHumTemp);
u8g.setPrintPos(0,16); u8g.print("3 #:"); u8g.setPrintPos(30,16); u8g.print(Di);
u8g.setPrintPos(50,16); u8g.print(DcP);
u8g.setPrintPos(0,24); u8g.print("4 #t:"); u8g.setPrintPos(30,24); u8g.print(DtransmitCounter);
u8g.setPrintPos(50,24); u8g.print("tFreq: "); u8g.setPrintPos(83,24); u8g.print(DtransmitFrequency);
u8g.setPrintPos(0,32); u8g.print(5); u8g.setPrintPos(10,32); u8g.print("nowMinute:"); u8g.setPrintPos(70,32); u8g.print(DnowMinute);
u8g.setPrintPos(0,40); u8g.print(6); u8g.setPrintPos(10,40); u8g.print("pTransmitMinute:"); u8g.setPrintPos(95,40); u8g.print(DpTransmitMinute);
u8g.setPrintPos(0,48); u8g.print(7); u8g.setPrintPos(10,48); u8g.print("Remainder:"); u8g.setPrintPos(70,48); u8g.print(DnowMinute % DtransmitFrequency);
}
float getPressure() {
int sensorVal = analogRead(A2);
float voltage = (sensorVal * 5.0) / 1023.0;
// Cuando Presión = 0, Entrada Analógica = 100
// Conversión: Entrada Analógica = 100 -> Voltaje = 100*(5/1023) = 0.4889
float m = ((150-0)/(4.5-0.4889));
float b = 150 - (m*4.5);
float pressure_psi = ((m*voltage) + b);
return pressure_psi;
}
void flashLED(int num, int t) {
for (int z = 1; z <= num; z++) {
digitalWrite(ledPin, HIGH); delay(t);
digitalWrite(ledPin, LOW); delay(t);
}
}
// Opción SD
//void writeToSD(String dataToWrite) {
// File dataFile = SD.open("datalog4.txt", FILE_WRITE);
// fileSizeSD = dataFile.size(); fileSizeSD = fileSizeSD / 1000000;
// if (dataFile) { dataFile.println(dataToWrite); dataFile.close(); Serial.println(dataToWrite); }
// else { Serial.println("error opening datalog1.txt"); }
//}
GoogleSheetsScript.js (JavaScript)
// 2 funciones en este código:
// doPost - Transmite datos del webhook Arduino Cloud. Se ejecuta al llegar nuevos datos.
// sendEmail - Envía emails basándose en valores de hoja "Data". Se ejecuta cada minuto según trigger.
// La mayor parte (salvo sendEmail) se basa en proyecto Arduino Project Hub:
// https://create.arduino.cc/projecthub/Arduino_Genuino/arduino-iot-cloud-google-sheets-integration-71b6bc?f=1
// Repositorio GitHub con script Google usado en ese proyecto:
// https://github.com/arduino/arduino-iot-google-sheet-script/blob/master/Code.gs
var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName("RawData");
var sd = ss.getSheetByName("Data");
var sref = ss.getSheetByName("References");
var MAX_ROWS = 1440; // máx filas de datos a mostrar
var HEADER_ROW = 1;
var TIMESTAMP_COL = 1;
function doPost(e) {
var cloudData = JSON.parse(e.postData.contents);
console.log(cloudData);
var values = cloudData.values;
console.log(values);
var incLength = values.length;
var incNames = [];
var incValues = [];
for (var i = 0; i < incLength; i++) {
incNames[i] = values[i].name;
incValues[i] = values[i].value;
}
console.log(incNames); console.log(incValues);
var timestamp = values[0].updated_at; // formato: yyyy-MM-ddTHH:mm:ss.mmmZ
var date = new Date(Date.parse(timestamp));
if (date.getYear() > 2018) {
if (sheet.getRange(HEADER_ROW+1, 1).getValue() != '') {
var now = new Date();
var COMM_TIME = 120; // seg, cambiado de 5 para permitir más mensajes
if (now.getTime() - date.getTime() > COMM_TIME * 1000) {
return; // descartar datos antiguos
}
}
// Escribir nombres de columnas en fila de cabecera
sheet.getRange(HEADER_ROW, 1).setValue('timestamp');
for (var i = 0; i < incLength; i++) {
var lastCol = sheet.getLastColumn();
if (lastCol == 1) {
sheet.getRange(HEADER_ROW, lastCol + 1).setValue(incNames[i]);
} else {
var found = 0;
for (var col = 2; col <= lastCol; col++) {
if (sheet.getRange(HEADER_ROW, col).getValue() == incNames[i]) {
found = 1; break;
}
}
if (found == 0) {
sheet.getRange(HEADER_ROW, lastCol+1).setValue(incNames[i]);
}
}
}
var lastCol = sheet.getLastColumn();
var lastRow = sheet.getLastRow();
if (lastRow > MAX_ROWS + HEADER_ROW - 1) {
sheet.deleteRow(lastRow);
}
sheet.insertRowAfter(HEADER_ROW);
var range = sheet.getRange('A2:Z2');
range.setFontColor('#000000');
range.setFontSize(10);
range.setFontWeight('normal');
sheet.getRange(HEADER_ROW+1, TIMESTAMP_COL).setValue(date).setNumberFormat("yyyy-MM-dd HH:mm:ss");
for (var col = 1+TIMESTAMP_COL; col <= lastCol; col++) {
for (var i = 0; i < incLength; i++) {
var currentName = sheet.getRange(HEADER_ROW, col).getValue();
if (currentName == incNames[i]) {
if (incValues[i] == true) incValues[i] = 1;
else if (incValues[i] == false) incValues[i] = 0;
sheet.getRange(HEADER_ROW+1, col).setValue(incValues[i]);
}
}
if(sheet.getRange(HEADER_ROW+1, col).getValue() == ''){
sheet.getRange(HEADER_ROW+1, col).setValue("-");
}
}
}
}
function sendEmail() {
var emailAddress = sd.getRange("V3").getValue();
var lastPressure = sd.getRange("K3").getValue();
var lastUpdate = sd.getRange("A3").getValue();
var ssLink = "https://docs.google.com/spreadsheets/d/1XwCir2Llw8RvGPGgZI3Yk6U5a3LeIfUACNuO1Gr_LFQ/edit#gid=1123486497";
var triggerAlertCount = sd.getRange("L6").getValue();
var triggerMonitor = sd.getRange("M3").getValue();
var dtLastStatusChange = sd.getRange("P3").getValue();
var dtLastDeviceUpdate1 = sd.getRange("S3").getValue();
var dtLastDeviceUpdate2 = sd.getRange("S4").getValue();
var emailSentNoUpdate = sd.getRange("T3").getValue();
var emailSentStartedReading = sd.getRange("U3").getValue();
var message = "Last Device Update: " + "\n" + lastUpdate + "\n\n" +
" Last Pressure: " + "\n\t" + lastPressure.toFixed(2) + " psi" + "\n\n" +
" Link to Spreadsheet: " + "\n\t" + ssLink;
var now = new Date();
if(triggerMonitor == 0){
sd.getRange("L5").setValue(0); sd.getRange("L6").setValue(0);
}
if(triggerMonitor == -1 && triggerAlertCount <= 4){
sd.getRange("L3").setValue(lastUpdate);
sd.getRange("L5").setValue(-1);
sd.getRange("L6").setValue(triggerAlertCount + 1);
var subject = "Status Change Alert - Outside Setpoints";
MailApp.sendEmail(emailAddress, subject, message);
}
if(triggerMonitor == 1 && triggerAlertCount <= 4){
sd.getRange("L3").setValue(lastUpdate);
sd.getRange("L5").setValue(1);
sd.getRange("L6").setValue(triggerAlertCount + 1);
var subject = "Status Change Alert - Normal";
MailApp.sendEmail(emailAddress, subject, message);
}
if(emailSentNoUpdate == 0 && dtLastDeviceUpdate1 > 60 && dtLastDeviceUpdate2 > 60){
sd.getRange("T3").setValue(1);
sd.getRange("U3").setValue(0);
sd.getRange("T4").setValue(now.getTime());
var subject = "Alert - Over 60 minutes Since Last Device Update";
MailApp.sendEmail(emailAddress, subject, message);
}
if(emailSentNoUpdate == 1 && dtLastDeviceUpdate1 < 60){
sd.getRange("T3").setValue(0);
sd.getRange("U3").setValue(1);
sd.getRange("U4").setValue(now.getTime());
var subject = "Alert - Device Started Updating";
MailApp.sendEmail(emailAddress, subject, message);
}
}
CommunicationsDevice - Sketch para MKR 1400
(Código disponible en el repositorio del proyecto)
Esquemáticos
Proceso de manufactura
- Raspberry Pi y HDC2010: Guía completa de configuración I2C para sensor de temperatura y humedad
- Sensor Barométrico BMP180 I2C – Guía de Integración con Raspberry Pi
- Logger de Sensores Python/MicroPython: Almacena Datos de Sensores en Google Sheets Fácilmente
- Envío de Datos IMU Adafruit 10DOF desde Raspberry Pi 2 a Azure Event Hubs con Windows 10 IoT Core
- Monitorización Precisa de Temperatura y Humedad con SHT15 en Windows 10 IoT Core
- UnifiedWater v1 – Kit completo de sensor de calidad de agua con Arduino
- Construye un tarro inteligente IoT con ESP8266, Arduino y sensor ultrasónico: Monitorización en tiempo real
- Construye una Torre de Sensores con Arduino Cloud: Temperatura, Movimiento y Gas
- Smartwatch Arduino DIY: Construye tu Propio Reloj Estilo Apple Watch
- Controla tu TV con Alexa mediante Arduino IoT Cloud: Guía Paso a Paso
- ADLINK y Google Cloud Colaboran para Entregar Soluciones IoT Sin Fisuras