Мониторинг давления в реальном времени с MKR GSM, Arduino Cloud и Google Sheets
Компоненты и материалы
| Arduino UNO | × | 1 |
| Arduino MKR GSM 1400 | × | 1 |
| Датчик давления (0-150 psi) | × | 1 |
| Adafruit DHT22 Температура и влажность | × | 1 |
| Модуль RTC DS3231 | × | 1 |
| Adafruit Logic Level Converter BSS138 (4-канальный I2C) | × | 1 |
| Adafruit 3.7В LiPo аккумулятор (2000 мАч и более) | × | 2 |
| Повышающий DC-DC преобразователь | × | 1 |
| Набор для начинающих (LED, резисторы) | × | 1 |
| Макетная плата | × | 1 |
Приложения и онлайн-сервисы
| Google Sheets |
| Arduino IoT Cloud |
| Arduino Web Editor |
| Arduino IDE |
О проекте
Цель
Создать прототип недорогого устройства для мониторинга давления на промышленном оборудовании в удаленных локациях с использованием сотовой связи.





Пошаговое описание потока данных
Шаг 1: Датчик давления → Arduino Uno
Датчик преобразует давление в аналоговый электрический сигнал. Arduino Uno конвертирует аналоговый сигнал в давление (psi).
Шаг 2: Arduino Uno → MKR GSM 1400 по Serial
При последовательном соединении двух Arduino:
- RX устройства #1 → TX устройства #2
- TX устройства #1 → RX устройства #2
- Общий GND обязателен
Частота передачи данных (Uno → MKR GSM 1400):
- НОРМАЛЬНЫЙ: каждые 30 минут (
transmitFrequency) Uno отправляет данные по Serial на MKR GSM 1400, который передает их в облако. - ПОРОГОВЫЙ (HIGH/LOW TRIGGER): если давление поднимается выше 40 psi (
highTrigger) или падает ниже 20 psi (lowTrigger) и остается там более 2 минут (dtLastTriggerLimit), Uno отправляет данные на MKR GSM 1400 для передачи в облако. - ПО ТРЕБОВАНИЮ (DEMAND POLL): если на Uno пин A1 (
buttonPin) подтянут к HIGH, Uno отправляет данные. Два способа поднять A1: (1) кнопка на макетной плате; (2) если на MKR GSM 1400 пин A3 (pingPin) установлен в HIGH. Пин A3 управляется из Arduino Cloud, что позволяет запросить текущие данные удаленно в любой момент.
Скетч для Arduino Uno приложен с комментариями. Имя файла: "InstrumentReader".
Конвертация аналогового сигнала в psi основана на видео: YouTube. Полезен пост на форуме Arduino «Serial Input Basics»: forum.arduino.cc.
Шаг 3: MKR GSM 1400 → Arduino Cloud через сотовую сеть
MKR GSM 1400 обрабатывает Serial-данные от Uno и отправляет их в Arduino Cloud по сотовой связи.
В коде для MKR GSM 1400 используется Serial1.read вместо Serial.read. На MKR-платах пины TX/RX доступны через Serial1 (см. справку Arduino).

Arduino Cloud
Проект настроен с 2 переменными в Arduino Cloud. На дашборде отображаются:

dataStringCloud— пакет всех данных устройства. Так сделано для упрощения парсинга в Google Sheets: при отдельных переменных было сложно отличать неизменное значение от не обновившегося.pinCloud— управление MKR GSM 1400 из облака. В скетче switch-case: приpinCloud = 1пин A1 поднимается в HIGH (светит LED на плате); приpinCloud = 2пин A3 поднимается в HIGH → Uno шлет текущие данные (как описано в шаге 2).
Скетч для MKR GSM 1400 приложен. Имя файла: "CommunicationsDevice".
Шаг 4: Arduino Cloud → Google Sheets через Webhook
Данные передаются из Arduino Cloud в Google Sheets с помощью вебхука. Ядро — функция doPost в скрипте Google Sheets.
Краткая инструкция настройки вебхука (процесс начинается в Google Sheets, а не в Arduino Cloud):
- Создать новый файл Google Sheets.
- Инструменты → Редактор скриптов.
- Написать код с функцией doPost (см. прикрепленный
GoogleSheetsScript.js). - Опубликовать → Развернуть как веб-приложение.
- В диалоге: (1) Версия проекта — всегда выбирать «Новый»; (2) Выполнять от моего имени; (3) Доступ — «Любой, даже анонимный». Нажать «Развернуть».
- Появится «Current web app URL» — его копируем во вкладку вебхука в Arduino Cloud. URL остается неизменным при обновлениях версий.
Часть JavaScript кода основана на проекте «Arduino IoT Cloud Google Sheets Integration»: ссылка.
Шаг 5: Парсинг данных в Google Sheets
Google Sheets парсит отдельные значения из dataStringCloud и отображает уникальные записи.
Пример таблицы из недавнего теста (ячейки раскрашены по легенде на каждом листе): Google Sheets.

Шаг 6: Отправка уведомлений из Google Sheets
В JavaScript файле (GoogleSheetsScript.js) две функции:
doPost— получает данные от вебхука Arduino Cloud. Запускается при новых данных в облаке.sendEmail— отправляет email на основе значений из листа «Data». Запускается каждую минуту по триггеру.
Настройка триггера для sendEmail:
- Открыть Google Sheets → Инструменты → Редактор скриптов.
- Правка → Триггеры текущего проекта.
- В G Suite Developer Hub: Добавить триггер (правый нижний угол).
- Выбрать функцию
sendEmail, тип — Time-Driven (например, каждую минуту). Это позволяет слать алерты, когда устройство перестает обновляться.

Время работы от батареи
~24 часа. Можно оптимизировать: отключить/убрать дисплеи, убрать необязательные датчики (DHT22, DS3231).
Потребление данных
~0.7 МБ/день. Оптимизация: уменьшить размер или частоту передач (только давление вместо набора параметров, обновления раз в час вместо 30 минут).
Стоимость проекта
Итого = $241
- Arduino MKR GSM 1400 ($70)
- Arduino Uno ($22)
- 2 × 3.7В LiPo ($30)
- 2 × LED дисплея ($29)
- Влагостойкий пластиковый корпус ($22)
- Датчик давления ($19)
- DHT22 ($10)
- DS3231 ($5)
- Повышающий преобразователь ($5)
- Logic Level Converter ($4)
- Мелочи (LED, резисторы, провода) ($25)
Инструменты и покупки
Все приобретено в:
- Arduino Online Store (store.arduino.cc)
- Adafruit (adafruit.com)
- Amazon (amazon.com)
- Harbor Freight
- Home Depot

Заключение
Спасибо за внимание к проекту. Любые вопросы, отзывы, комментарии, предложения приветствуются и ценится.
Код
InstrumentReader — Скетч для Arduino Uno
// Sketch 1 of 2 // Arduino Uno
// Данные собираются этим устройством и передаются на MKR 1400 по Serial
// DHT22 требует 2 библиотеки, но в коде вызывается одна:
// (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> // RTC модуль
#include <DHT.h> // DHT22
#include "U8glib.h" // Velleman 128x64 OLED SPI дисплей
// Примечание: стандартная U8glib не работала с этим дисплеем. Работает рекомендованная производителем.
// Библиотека: https://www.velleman.eu/support/downloads/?code=VMA437
// Синтаксис: https://github.com/olikraus/u8glib/wiki/userreference & https://github.com/olikraus/u8glib/wiki/thelloworld
//#include <SD.h> // Опция сохранения на SD карту в Ethernet Shield для 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; // SD card option
//float fileSizeSD = 0.0; // SD card option
int i = 0; // счетчик измерений
const int ledPin = 9; // индикатор передачи (мигает при отправке)
const int ledPin2 = 8; // индикатор принудительной отправки (кнопка на плате или сигнал от MKR 1400 из облака)
const int buttonPin = A1;
int buttonState = 0;
int transmitFrequency = 30; // частота передачи данных (минуты)
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(void) {
Serial.begin(9600);
Wire.begin();
dht.begin();
pinMode(ledPin, OUTPUT);
pinMode(ledPin2, OUTPUT);
pinMode(buttonPin, INPUT);
u8g.setRot180(); // переворот экрана при необходимости
// SD card init option commented out
}
void loop(void) {
delay(5000);
DateTime now = RTC.now();
float cRuntime = millis() / 60000.0;
float p = getPressure();
buttonState = digitalRead(buttonPin);
if (buttonState == 1) {
digitalWrite(ledPin2, HIGH);
delay(30000); // задержка для готовности MKR1400 к приему
} else {
digitalWrite(ledPin2, LOW);
}
float h = dht.readHumidity();
float t = dht.readTemperature(true); // true = Фаренгейт, false = Цельсий
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;
// Формирование строки данных
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);
// Отрисовка на OLED
u8g.firstPage();
do {
draw(cDateTime, cHumTemp, cP, i, transmitCounter, now.minute(), transmitFrequency, pTransmitMinute);
} while (u8g.nextPage());
delay(1000);
// writeToSD(dataString); // SD card option
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;
// При давлении 0: 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);
}
}
// SD card option commented out
GoogleSheetsScript.js (JavaScript)
// Две функции:
// doPost — принимает данные от вебхука Arduino Cloud. Запускается при новых данных.
// sendEmail — шлет email на основе значений из листа "Data". Запускается каждую минуту по триггеру.
// Большая часть кода (кроме sendEmail) основана на проекте Arduino IoT Cloud Google Sheets Integration
// https://create.arduino.cc/projecthub/Arduino_Genuino/arduino-iot-cloud-google-sheets-integration-71b6bc?f=1
// GitHub репозиторий с Google Script: 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; // макс. строк данных для отображения
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; // формат: 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; // секунд, допуск на задержку доставки
if (now.getTime() - date.getTime() > COMM_TIME * 1000) {
return; // отбрасываем старые дубликаты
}
}
// Запись заголовков
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);
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(now.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(now.getTime());
MailApp.sendEmail(emailAddress, "Alert - Device Started Updating", message);
}
}
CommunicationsDevice — Скетч для MKR 1400
(Код не показан в превью, доступен для скачивания)
Схемы

Производственный процесс
- ADLINK и Google Cloud сотрудничают для поставки бесшовных IoT-решений
- Raspberry Pi и HDC2010: полное руководство по настройке I2C датчика температуры и влажности
- Интеграция барометрического датчика BMP180 с Raspberry Pi по I2C
- Python/MicroPython логгер датчиков: бесшовное сохранение данных в Google Sheets
- Передача данных Adafruit 10DOF IMU с Raspberry Pi 2 в Azure Event Hubs на Windows 10 IoT Core
- Точный мониторинг температуры и влажности с SHT15 на Windows 10 IoT Core
- UnifiedWater v1: полный набор датчиков качества воды на Arduino MKR GSM
- Умная банка на ESP8266 + Arduino: мониторинг наполненности в реальном времени
- Сенсорная башня на Arduino Cloud: Мониторинг температуры, движения и газа
- DIY Arduino Smartwatch: самодельные умные часы в стиле Apple Watch