Echtzeit-IoT-Drucküberwachung mit MKR GSM, Arduino Cloud & Google Sheets
Komponenten und Materialien
| Arduino UNO | × | 1 |
| Arduino MKR GSM 1400 | × | 1 |
| Druckwandler (0-150 psi) | × | 1 |
| Adafruit DHT22 Temperatur- & Feuchtigkeitssensor | × | 1 |
| DS3231 RTC Modul | × | 1 |
| Adafruit Logic Level Converter (BSS138, 4-Kanal I2C) | × | 1 |
| Adafruit 3,7V LiPo Akku (2000 mAh+) | × | 2 |
| Step-Up Spannungswandler | × | 1 |
| Starterkit (LEDs, Widerstände, Kabel) | × | 1 |
| Breadboard | × | 1 |
Apps und Online-Dienste
- Google Sheets
- Arduino IoT Cloud
- Arduino Web Editor
- Arduino IDE
Projektziel
Entwicklung eines kostengünstigen Prototyps zur Drucküberwachung an Industrieanlagen an abgelegenen Standorten mittels Mobilfunkdaten.
Projektdurchlauf
Schritt 1: Druckwandler → Arduino Uno
Der Druckwandler wandelt Druck in ein analoges elektrisches Signal um. Der Arduino Uno wandelt dieses Signal in Druckwerte (psi) um.
Schritt 2: Uno → MKR GSM 1400 via Serial
Serielle Kommunikation zwischen zwei Arduinos:
- RX am Gerät #1 ↔ TX am Gerät #2
- TX am Gerät #1 ↔ RX am Gerät #2
- Gemeinsame Masse (GND)
Datenübertragungs-Häufigkeit (Uno → MKR GSM 1400):
- NORMAL: Alle 30 Minuten (transmitFrequency) sendet der Uno Daten an den MKR GSM 1400, der sie in die Cloud überträgt.
- HIGH/LOW TRIGGER: Wenn Druck > 40 psi (highTrigger) oder < 20 psi (lowTrigger) für > 2 Minuten (dtLastTriggerLimit) anliegt, erfolgt sofortige Übertragung.
- DEMAND POLL: Wenn Pin A1 am Uno high geschaltet wird, sendet er sofort Daten. Zwei Wege: (1) Taster am Breadboard, (2) Pin A3 am MKR GSM 1400 geht high → schaltet Pin A1 am Uno high. Pin A3 wird über Arduino Cloud gesteuert, ermöglicht ferngesteuerte Abfrage ohne Wartezeit.
Hinweise: Der Uno-Code könnte für mehrere Eingänge (Temperatur, Feuchtigkeit, Batteriespannung) mit eigenen Schwellwerten erweitert werden. Die Umwandlung des Analogsignals in psi basiert auf diesem YouTube-Video. Der Arduino Forum Post "Serial Input Basics" half bei der seriellen Kommunikation.
Uno-Code: "InstrumentReader" (mit Kommentaren im Anhang)
Schritt 3: MKR GSM 1400 → Arduino Cloud via Mobilfunk
Der MKR GSM 1400 verarbeitet Seraldaten vom Uno und sendet sie via Mobilfunk an die Arduino Cloud.
Wichtig: Im MKR-Code wird Serial1.read statt Serial.read verwendet, da MKR-Boards TX/RX über Serial1 ansprechen. Siehe Arduino Reference.
Arduino Cloud Variablen:
- dataStringCloud: Datenpaket mit allen Gerätedaten (vereinfacht Parsing in Google Sheets gegenüber Einzelvariablen).
- pinCloud: Steuert MKR GSM 1400 aus der Cloud. Switch-Funktion im Sketch: pinCloud=1 → Pin A1 high (LED an), pinCloud=2 → Pin A3 high → Uno sendet aktuelle Daten.
MKR-Code: "CommunicationsDevice" (mit Kommentaren im Anhang)
Schritt 4: Arduino Cloud → Google Sheets via Webhook
Datenübertragung per Webhook. Kern ist die doPost-Funktion im Google Sheets Script.
Webhook-Einrichtung (Start in Google Sheets!):
- Neue Google Sheets Datei erstellen
- Tools → Scripteditor
- Code mit doPost-Funktion schreiben (siehe
GoogleSheetsScript.js) - Veröffentlichen → Als Web-App bereitstellen
- Dialog: (1) Projektversion: "Neu" wählen, (2) Ausführen als: "Ich", (3) Zugriff: "Jeder, auch anonym"
- Deploy klicken → "Current web app URL" kopieren → in Arduino Cloud Webhook-Tab einfügen (URL bleibt bei Updates gleich)
Großer Teil des JavaScript-Codes basiert auf dem Projekt Arduino IoT Cloud Google Sheets Integration.
Schritt 5: Google Sheets – Daten parsen
Einzelwerte aus dataStringCloud extrahieren und eindeutige Datensätze anzeigen. Test-Sheet mit Farbcodierung: Google Sheets Beispiel
Schritt 6: Google Sheets – Benachrichtigungen senden
Zwei Funktionen in GoogleSheetsScript.js:
doPost: Empfängt Daten vom Arduino Cloud Webhook (läuft bei neuen Cloud-Daten)sendEmail: Sendet E-Mails basierend auf Werten im Sheet "Data" (läuft minütlich per Trigger)
Trigger für sendEmail einrichten:
- Google Sheets → Tools → Scripteditor
- Bearbeiten → Aktuelle Projekt-Trigger
- Trigger hinzufügen →
sendEmailauswählen - Zeitgesteuert (z. B. jede Minute) → ermöglicht Benachrichtigung bei Geräteausfall
Batterielaufzeit
~24 Stunden. Optimierbar durch Abschalten der Displays oder Entfernen nicht-essentieller Sensoren (DHT22, DS3231).
Datenverbrauch
~0,7 MB/Tag. Optimierbar durch Reduktion von Datenmenge (nur Druck statt aller Werte) oder Frequenz (stündlich statt 30-minütlich).
Projektkosten
Gesamt: ~241 $
- Arduino MKR GSM 1400 (70 $)
- Arduino Uno (22 $)
- 2× 3,7V LiPo Akkus (30 $)
- 2× LED-Displays (29 $)
- Wetterfestes Kunststoffgehäuse (22 $)
- Drucksensor (19 $)
- DHT22 (10 $)
- DS3231 RTC (5 $)
- Step-Up Wandler (5 $)
- Logic Level Converter (4 $)
- Sonstiges – LEDs, Widerstände, Kabel (25 $)
Hardware / Werkzeuge
Bezugsquellen: Arduino Online Store, Adafruit, Amazon, Harbor Freight, Home Depot.
Fazit
Danke fürs Review! Fragen, Feedback, Kommentare, Vorschläge sind willkommen.
Code
InstrumentReader – Sketch für Arduino Uno
// Sketch 1 of 2 // Arduino Uno
// Datenerfassung & Übertragung an MKR 1400 via Serial
// DHT22 benötigt 2 Libraries (nur eine wird im Code aufgerufen):
// (1) DHT Sensor Library: https://github.com/adafruit/DHT-sensor-library
// (2) Adafruit Unified Sensor: https://github.com/adafruit/Adafruit_Sensor
#include <SPI.h>
#include <Wire.h>
#include <DS3231.h> // RTC
#include <DHT.h> // DHT22
#include "U8glib.h" // Velleman 128x64 OLED SPI Display
// Hinweis: Standard U8glib funktionierte nicht; Hersteller-Library nötig:
// https://www.velleman.eu/support/downloads/?code=VMA437
// Syntax: https://github.com/olikraus/u8glib/wiki/userreference
RTClib RTC;
#define DHTPIN 11
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
U8GLIB_SH1106_128X64 u8g(3, 4, 6, 7); // CLK:3, MOSI:4, CS:6, DC:7
int y_pos = 0;
int i = 0; // Messzähler
const int ledPin = 9; // Sendeanzeige
const int ledPin2 = 8; // Manuelle Sendeanzeige
const int buttonPin = A1;
int buttonState = 0;
int transmitFrequency = 30; // Minuten
String pTransmitDateTime = "";
int transmitCounter = 0;
int pTransmitMinute = 0;
int ptriggerTransmitAlertIndicator;
float cRuntimeAtTriggerStart = 0.0;
float dtLastTrigger = 0.0;
int triggerCounter = 0;
int triggerTransmitAlertCounter = 0;
float lowTrigger = 20.0;
float highTrigger = 40.0;
float dtLastTriggerLimit = 2.0;
void setup() {
Serial.begin(9600);
Wire.begin();
dht.begin();
pinMode(ledPin, OUTPUT);
pinMode(ledPin2, OUTPUT);
pinMode(buttonPin, INPUT);
u8g.setRot180(); // Display drehen falls nötig
}
void loop() {
delay(5000);
DateTime now = RTC.now();
float cRuntime = millis() / 60000;
float p = getPressure();
buttonState = digitalRead(buttonPin);
if (buttonState == 1) {
digitalWrite(ledPin2, HIGH);
delay(30000); // Warten bis MKR1400 bereit
} else {
digitalWrite(ledPin2, LOW);
}
float h = dht.readHumidity();
float t = dht.readTemperature(true); // true = Fahrenheit
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;
// Datenstring erstellen
String 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) + ">";
String cDateTime = String(now.month()) + "/" + String(now.day()) + "/" + String(now.year()) +
" " + String(now.hour()) + ":" + String(now.minute()) + ":" + String(now.second());
String cHumTemp = "H:" + String(h) + "% T:" + String(t) + "degF";
String 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);
// Display aktualisieren
u8g.firstPage();
do {
draw(cDateTime, cHumTemp, cP, i, transmitCounter, now.minute(), transmitFrequency, pTransmitMinute);
} while (u8g.nextPage());
delay(1000);
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;
// Bei 0 psi: Analog Input = 100 → Voltage = 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);
}
}
GoogleSheetsScript.js
// 2 Funktionen:
// doPost - Empfängt Daten vom Arduino Cloud Webhook (läuft bei neuen Cloud-Daten)
// sendEmail - Sendet E-Mails basierend auf Sheet "Data" (läuft minütlich per Trigger)
// Basiert auf: https://create.arduino.cc/projecthub/Arduino_Genuino/arduino-iot-cloud-google-sheets-integration-71b6bc
// GitHub: 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; // Max Datenzeilen (12h bei 30s Intervall)
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;
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; // 120 Sekunden Toleranz
if (now.getTime() - date.getTime() > COMM_TIME * 1000) {
return; // Veraltete Nachricht verwerfen
}
}
// Header schreiben
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;
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);
MailApp.sendEmail(emailAddress, "Status Change Alert - Outside Setpoints", message);
}
if (triggerMonitor == 1 && triggerAlertCount <= 4) {
sd.getRange("L3").setValue(lastUpdate);
sd.getRange("L5").setValue(1);
sd.getRange("L6").setValue(triggerAlertCount + 1);
MailApp.sendEmail(emailAddress, "Status Change Alert - Normal", message);
}
if (emailSentNoUpdate == 0 && dtLastDeviceUpdate1 > 60 && dtLastDeviceUpdate2 > 60) {
sd.getRange("T3").setValue(1);
sd.getRange("U3").setValue(0);
sd.getRange("T4").setValue(new Date().getTime());
MailApp.sendEmail(emailAddress, "Alert - Over 60 minutes Since Last Device Update", message);
}
if (emailSentNoUpdate == 1 && dtLastDeviceUpdate1 < 60) {
sd.getRange("T3").setValue(0);
sd.getRange("U3").setValue(1);
sd.getRange("U4").setValue(new Date().getTime());
MailApp.sendEmail(emailAddress, "Alert - Device Started Updating", message);
}
}
CommunicationsDevice – Sketch für MKR GSM 1400
Code im Anhang des Originalprojekts verfügbar.
Schaltplan
Herstellungsprozess
- ADLINK & Google Cloud kooperieren für nahtlose IoT-Lösungen
- Raspberry Pi & HDC2010: Kompletter I2C Temperatur- & Feuchtigkeitssensor Setup-Guide
- BMP180 I2C Barometer-Sensor – Raspberry Pi Integrationsanleitung
- Python/MicroPython Sensor Logger: Sensordaten nahtlos in Google Sheets speichern
- Adafruit 10DOF IMU Daten von Raspberry Pi 2 an Azure Event Hubs senden (Windows 10 IoT Core)
- Präzise Temperatur- & Feuchtigkeitsüberwachung mit SHT15 unter Windows 10 IoT Core
- UnifiedWater v1 – Komplettes Arduino Wasserqualitäts-Sensorkit
- Smart IoT-Glas bauen: Füllstand überwachen mit ESP8266, Arduino & Ultraschall
- Sensorturm mit Arduino Cloud: Temperatur, Bewegung & Gase überwachen
- DIY Arduino Smartwatch: Eigene Apple-Watch-Alternative bauen