Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Manufacturing Technology >> Производственный процесс

Умная система обнаружения утечек воды и управления клапаном

Компоненты и материалы

Алюминиевый корпус × 1
Arduino UNO × 1
Arduino Ethernet Shield 2 × 1
Блок питания × 1
Корпус для переменного тока × 1
Набор светодиодов с резисторами × 1
Реле (1 двухканальное 5В и 1 двухканальное 12В) × 1
Моторизованный клапан × 1
Разъемы JST × 1
Датчик воды × 1

О проекте

Обзор

Во время разговора с другом я понял, что утечки воды — серьезная проблема. Мой друг был вынужден заменить всю мебель в подвале, потому что труба лопнула, пока он был на работе.

Этот проект похож на тот, что я публиковал для конкурса Microsoft IoT, но он основан на Arduino, а не на Raspberry Pi. Мой подход: вместо централизации множества задач на мощной платформе (Raspberry Pi, DragonBoard или ПК) я предпочитаю делегировать простые задачи простым устройствам (Arduino и другим). Они выполняют свою работу и при необходимости подключаются по сети к другим устройствам для предоставления расширенных сервисов. В случае сбоя сети они продолжают выполнять свои функции.

По сути, система мониторит утечки воды и запускает действия, такие как перекрытие главного водопровода, в зависимости от обнаруженной неисправности.

Она также публикует сообщения в MQTT-брокере. Идея в том, что устройство управляет водоснабжением локально, но также участвует в более крупной системе домашней автоматизации.

Внешний вид системы

Умная система обнаружения утечек воды и управления клапаном

Главный водопровод находится внизу. Первое устройство установлено городскими водоканалами для контроля давления. Я установил свою часть сверху. Моторизованный клапан (синий) установлен параллельно ручному. На фото ручной клапан открыт, а моторизованный обойден — это полезно при отключении электричества. В нормальном режиме ручной клапан должен быть закрыт.

Внутри клапана находится двигатель постоянного тока (12В), вращающийся по или против часовой стрелки в зависимости от полярности. Есть обратная связь, показывающая, открыт или закрыт клапан. Для открытия нужно подать плюсовое напряжение на верхний левый контакт.

Контроллер

Умная система обнаружения утечек воды и управления клапаном

Слева направо: сетевая вилка, кнопка сброса, светодиоды статуса, разъемы (к датчикам, к двигателю), Ethernet и USB интерфейсы.

Умная система обнаружения утечек воды и управления клапаном

Датчик установлен в месте потенциальной утечки.

Умная система обнаружения утечек воды и управления клапаном

⚠ ВАЖНОЕ ПРЕДУПРЕЖДЕНИЕ!

Используемый мною AC/DC блок питания имеет два выхода: первый — 12В DC для питания моторизованного клапана (управляется двумя реле, переключающими полярность), второй — ровно 5В DC для питания Arduino. Поэтому я запитываю Arduino напрямую на 5В цепи, а не через Vin (который требует минимум 6В). После подключения AC/DC блока НИКОГДА не подключайте ни DC-джек Arduino, ни USB-кабель. Если нужна отладка по USB, сделайте кабель без линий питания, оставив только линии данных. Кстати, связь между AC-корпусом и блоком питания — 110В. Не касайтесь!

Программный код

Обнаружение утечек и управление моторизованным клапаном (Arduino)

#include <PubSubClient.h>
#include <Network.h>
#include <Timer.h>
#include <SPI.h>
#include <Ethernet.h>
#include <Dns.h>
#include <avr/wdt.h>

/* Принцип работы оборудования:
Три подсистемы:
- Основной блок:
  - Arduino Uno с Ethernet Shield
  - Красный LED: постоянно — вода локально, мигает — вода удаленно, выкл. — нет утечки
  - Желтый LED: постоянно — клапан неисправен, мигает — MQTT брокер недоступен, выкл. — ОК
  - Синий LED: постоянно — клапан открыт, мониторинг активен; мигает — клапан закрыт; выкл. — система внизу
  - Кнопка: при нажатии запускает самотестирование
  - Двухканальное реле для управления удаленным моторизованным клапаном
  - Другое двухканальное реле для опроса концевых выключателей на клапане
  - Набор датчиков воды (все параллельно) (все 3 разъема на панели соединены параллельно)

Моторизованный клапан имеет соединения:
- Желтый и Синий: DC для питания двигателя
- Черный: вход концевых выключателей (подключаем к GND)
- Красный: становится GND при достижении полностью закрытого положения (примечание: из-за конструкции концевого выключателя нет гарантии сохранения контакта после отключения питания)
- Зеленый: становится GND при достижении полностью открытого положения (аналогично)
*/

// Сетевые настройки
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xCF, 0xFC, 0xEE }; // MAC Arduino
IPAddress ip(192, 168, 12, 215); // IP Arduino
IPAddress server(192, 168, 12, 130); // IP MQTT брокера
EthernetClient ethClient;
PubSubClient client(ethClient);

#define mqttClientPrefix "GLX"
#define mqttClientLocation "BASEMENT"
#define mqttClientUID "001"
#define mqttClientStatusTopic "Status"
#define mqttClientFaultTopic "Fault"
const int mqttInterval = 20; // интервал отчетов в MQTT (в циклах основного цикла)
int mqttIntervalCnt = 0;
int isConnectedToBroker = -1; // 1 — подключено, -1 — неизвестно, 0 — не удалось подключиться

// Распиновка
const int SystemLedPin = A0; // Синий LED
const int FaultLedPin = A1;  // Желтый LED
const int AlarmLedPin = A2;  // Красный LED
const int WaterDetectorPin = 2; // LOW при обнаружении воды, иначе подтянут к VCC
const int ToggleButtonPin = 3; // LOW при нажатии, иначе подтянут к GND
const int SdCardPin = 4; // SD карта на Ethernet Shield, не используется
const int ValveClosedPin = 5; // LOW при срабатывании концевого выключателя "закрыто", иначе подтянут к HIGH
const int ValveOpenedPin = 6; // LOW при срабатывании концевого выключателя "открыто", иначе подтянут к HIGH
const int ValveControl1 = 8; // управление первым реле клапана
const int ValveControl2 = 7; // управление вторым реле клапана
// D10-D13 зарезервированы для Ethernet Shield

// Состояния
int isWaterDetected = 0; // локальная утечка
int isWaterDetectedRemotely = 0; // удаленная утечка
int isValveClosed = -1; // статус клапана (-1 неизвестно, 0 открыт, 1 закрыт)
const int valveTimeOut = 15; // сек, макс. время открытия/закрытия
int isConnectedToValve = -1; // 0 — не можем управлять клапаном, 1 — можно, -1 — неизвестно
volatile boolean isResetRequested = 0; // флаг запроса сброса (из прерывания)
const int mainLoopDelay = 500; // задержка основного цикла, мс
void(* resetFunc) (void) = 0; // функция программного сброса

void setup() {
  wdt_disable();
  Serial.begin(9600);
  Serial.println(F("Begin of setup"));

  // Настройка пинов
  pinMode(SystemLedPin, OUTPUT);
  pinMode(FaultLedPin, OUTPUT);
  pinMode(AlarmLedPin, OUTPUT);
  pinMode(WaterDetectorPin, INPUT);
  pinMode(ToggleButtonPin, INPUT);
  pinMode(ValveOpenedPin, INPUT); // реле 12В по умолчанию неактивно, пин подключен к NO контакту с подтяжкой -> HIGH
  pinMode(ValveClosedPin, INPUT); // аналогично
  pinMode(ValveControl1, OUTPUT); digitalWrite(ValveControl1, HIGH); // реле 5В 1 неактивно -> двигатель на GND
  pinMode(ValveControl2, OUTPUT); digitalWrite(ValveControl2, HIGH); // реле 5В 2 неактивно -> двигатель на GND
  pinMode(SdCardPin, OUTPUT); digitalWrite(SdCardPin, HIGH); // отключаем SD карту

  // Самотест LED
  testLeds();

  // Сеть и MQTT
  client.setServer(server, 1883);
  client.setCallback(MQTTBrokerCallback);
  Ethernet.begin(mac, ip);
  Serial.print(F("Current IP is : "));
  Serial.print(Ethernet.localIP());
  Serial.print(F(" - MQTT broker IP is : "));
  Serial.println(server);

  // Изначально статус клапана неизвестен, концевые выключатели ненадежны.
  // Открываем клапан и ждем завершения. Худший случай — уже открыт, кратковременно сработает концевой выключатель.
  if (openValve() == 0) {
    Serial.println(F("Valve is open and system is now monitoring"));
    subscribeToRemoteWaterSensors();
  } else {
    Serial.println(F("Unable to open valve, system is out of order. Please use plumbing bypass"));
  }
  enableInterruptOnResetButton();
  delay(1500);
  Serial.println(F("End of setup"));
}

void loop() {
  configureLedsWithInitialStates();

  // Реакция на запрос сброса
  if (isResetRequested == 1) {
    Serial.println(F("Someone pushed on the button to reset this device"));
    publishStatus();
    wdt_enable(WDTO_1S); // включить watchdog, сработает через 1 сек
    delay(5000);
    Serial.println(F("this message should never appear"));
  }

  // Проверка локального датчика воды
  readLocalWaterSensor();
  if (isWaterDetected == 1 || isWaterDetectedRemotely == 1) {
    if (isValveClosed == 0) closeValve();
  }

  // Публикация в MQTT
  if (mqttIntervalCnt == 0) {
    if (isWaterDetected == 1) publishFault();
    publishStatus();
    mqttIntervalCnt = mqttInterval;
  } else {
    if (isConnectedToValve == 0) {
      Serial.println(F("System is out of order - unable to control motorized valve. No monitoring in place"));
    } else {
      Serial.print(F("."));
    }
    mqttIntervalCnt--;
  }

  delay(mainLoopDelay / 2);
  client.loop();
  configureLedsWithFinalStates();
  delay(mainLoopDelay / 2);
}

// Чтение локального датчика воды с дебаунсом
void readLocalWaterSensor() {
  isWaterDetected = !getDebouncedValue(WaterDetectorPin, 100, 10);
  Serial.print(isWaterDetected);
}

// Управление кнопкой сброса (прерывание)
void enableInterruptOnResetButton() {
  isResetRequested = 0;
  attachInterrupt(1, onResetRequested, CHANGE);
}
void onResetRequested() {
  detachInterrupt(1);
  isResetRequested = 1;
}

// Открытие клапана
int openValve() {
  Serial.print(F("Opening valve..."));
  // Сначала подтверждаем, что клапан был закрыт, кратковременно заезжая на концевой "закрыто"
  setupRelays(1);
  if (waitForEndOfCycle(ValveClosedPin) == 0) {
    // Теперь пытаемся открыть
    setupRelays(2);
    if (waitForEndOfCycle(ValveOpenedPin) == 0) {
      isConnectedToValve = 1;
      isValveClosed = 0;
      setupRelays(0); // реле выкл.
      Serial.println(F(""));
      return 0;
    }
  }
  setupRelays(0);
  isConnectedToValve = 0;
  return -1;
}

// Закрытие клапана
int closeValve() {
  Serial.print(F("Closing valve..."));
  // Сначала подтверждаем, что клапан был открыт
  setupRelays(2);
  if (waitForEndOfCycle(ValveOpenedPin) == 0) {
    // Пытаемся закрыть
    setupRelays(1);
    if (waitForEndOfCycle(ValveClosedPin) == 0) {
      isConnectedToValve = 1;
      isValveClosed = 1;
      setupRelays(0);
      Serial.println(F("Valve has been turned off. Please inspect carefully all rooms and cleanup detectors"));
      return 0;
    }
  }
  setupRelays(0);
  isConnectedToValve = 0;
  return -1;
}

// Настройка реле для нужной полярности
void setupRelays(int scenario) {
  switch (scenario) {
    case 0: // все выкл.
      digitalWrite(ValveControl1, HIGH);
      digitalWrite(ValveControl2, HIGH);
      break;
    case 1: // цикл закрытия
      digitalWrite(ValveControl1, HIGH);
      digitalWrite(ValveControl2, LOW);
      break;
    case 2: // цикл открытия
      digitalWrite(ValveControl1, LOW);
      digitalWrite(ValveControl2, HIGH);
      break;
    default:
      Serial.print(F("Unexpected relay scenario: "));
      Serial.println(scenario);
      digitalWrite(ValveControl1, HIGH);
      digitalWrite(ValveControl2, HIGH);
  }
}

// Ожидание срабатывания концевого выключателя
int waitForEndOfCycle(int limitSwitchPin) {
  int cnt = valveTimeOut;
  while (cnt > 0) {
    if (getDebouncedValue(limitSwitchPin, 10, 10) == LOW) return 0;
    cnt--;
    Serial.print(F("."));
    delay(1000);
  }
  Serial.println(F(" - timeout reached while closing valve. Check whether valve is well powered up and cables are connected."));
  return -1;
}

// Дебаунс цифрового входа
int getDebouncedValue(int inputPin, int intervalInMs, int requiredConfirmations) {
  int confirmations = 1;
  int currentValue = digitalRead(inputPin);
  while (confirmations <= requiredConfirmations) {
    delay(intervalInMs);
    if (currentValue == digitalRead(inputPin)) {
      confirmations++;
    } else {
      confirmations = 1;
      currentValue = digitalRead(inputPin);
    }
  }
  return currentValue;
}

// Управление LED
void configureLedsWithInitialStates() {
  clearLeds();
  if (isWaterDetectedRemotely == 1 || isWaterDetected == 1) digitalWrite(AlarmLedPin, HIGH);
  if (isConnectedToValve == 0 || isConnectedToBroker == 0) digitalWrite(FaultLedPin, HIGH);
  digitalWrite(SystemLedPin, HIGH);
}
void configureLedsWithFinalStates() {
  if (isWaterDetectedRemotely == 1) digitalWrite(AlarmLedPin, LOW);
  if (isConnectedToBroker == 0) digitalWrite(FaultLedPin, LOW);
  if (isValveClosed == 1) digitalWrite(SystemLedPin, LOW);
}
void clearLeds() {
  digitalWrite(AlarmLedPin, LOW);
  digitalWrite(FaultLedPin, LOW);
  digitalWrite(SystemLedPin, LOW);
}
void testLeds() {
  clearLeds();
  digitalWrite(AlarmLedPin, HIGH); delay(500);
  digitalWrite(FaultLedPin, HIGH); delay(500);
  digitalWrite(SystemLedPin, HIGH); delay(500);
  clearLeds();
}

// MQTT функции
void MQTTBrokerCallback(char* subscribedTopic, byte* payload, unsigned int length) {
  Serial.print(F("New message received from MQTT broker. Topic="));
  Serial.print(subscribedTopic);
  String payloadAsString = (char*)payload;
  String realPayload = payloadAsString.substring(0, length);
  Serial.print(F(", content="));
  Serial.print(realPayload);
  if (realPayload.indexOf("WaterDetected") > 0 && realPayload.indexOf(mqttClientLocation) == -1) {
    isWaterDetectedRemotely = 1;
  }
  Serial.println();
}

String buildClientIdentifier() {
  String data = mqttClientPrefix; data += "_"; data += mqttClientLocation; data += "_"; data += mqttClientUID;
  return data;
}
String buildDeviceStatusTopic() {
  String data = mqttClientPrefix; data += "/"; data += mqttClientLocation; data += "/"; data += mqttClientUID; data += "/"; data += mqttClientStatusTopic;
  return data;
}
String buildFaultTopic() {
  String data = mqttClientPrefix; data += "/"; data += mqttClientLocation; data += "/"; data += mqttClientUID; data += "/"; data += mqttClientFaultTopic;
  return data;
}
String buildAnyFaultTopic() {
  String data = mqttClientPrefix; data += "/"; data += "+"; data += "/"; data += "+"; data += "/"; data += mqttClientFaultTopic;
  return data;
}

String buildDeviceStatusJson() {
  String data = "{"; data += "\n";
  data += "\"ResetByOperator\": "; data += (int)isResetRequested; data += ","; data += "\n";
  data += "\"WaterDetected\": "; data += (int)isWaterDetected; data += ","; data += "\n";
  data += "\"ValveClosed\": "; data += (int)isValveClosed; data += ","; data += "\n";
  data += "\"ValveDetected\": "; data += (int)isConnectedToValve; data += "\n";
  data += "}";
  return data;
}

String buildFaultJson() {
  String data = "{"; data += "\n";
  data += "\"WaterDetected\": "; data += (int)isWaterDetected; data += ","; data += "\n";
  data += "\"Location\": "; data += mqttClientLocation; data += "\n";
  data += "}";
  return data;
}

void publishMsg(char (&topic)[200], char (&payload)[200]) {
  if (connectToBroker()) {
    if (client.publish(topic, payload)) {
      isConnectedToBroker = 1;
      Serial.print(F("Message sent to MQTT broker using the following topic "));
      Serial.println(topic);
    } else {
      Serial.print(F("Message NOT sent to MQTT broker using the following topic "));
      Serial.println(topic);
      isConnectedToBroker = 0;
    }
    client.loop();
  }
}

void publishFault() {
  char topicBuffer[200]; buildFaultTopic().toCharArray(topicBuffer, 200);
  char payloadBuffer[200]; buildFaultJson().toCharArray(payloadBuffer, 200);
  publishMsg(topicBuffer, payloadBuffer);
}

void publishStatus() {
  char topicBuffer[200]; buildDeviceStatusTopic().toCharArray(topicBuffer, 200);
  char payloadBuffer[200]; buildDeviceStatusJson().toCharArray(payloadBuffer, 200);
  publishMsg(topicBuffer, payloadBuffer);
}

void subscribeToRemoteWaterSensors() {
  if (connectToBroker()) {
    char tempBuffer[200]; buildAnyFaultTopic().toCharArray(tempBuffer, 200);
    client.subscribe(tempBuffer);
    if (client.subscribe(tempBuffer)) {
      isConnectedToBroker = 1;
      Serial.print(F("Registred to MQTT broker as a subscriber for the following topic: "));
      Serial.println(tempBuffer);
    } else {
      Serial.println(F("Not registred to MQTT broker as a subscriber"));
      isConnectedToBroker = 0;
    }
    client.loop();
  } else {
    isConnectedToBroker = 0;
    Serial.println(F("Cannot subscribe to any topic since connection to MQTT broker is not established"));
  }
}

int connectToBroker() {
  Serial.println(F(""));
  Serial.print(F("Connecting to network and to MQTT Broker... "));
  char tempBuffer[200]; buildClientIdentifier().toCharArray(tempBuffer, 200);
  if (client.connect(tempBuffer)) {
    Serial.print(F("connected as "));
    Serial.println(tempBuffer);
  } else {
    switch (client.state()) {
      case -4: Serial.println(F("MQTT_CONNECTION_TIMEOUT")); break;
      case -3: Serial.println(F("MQTT_CONNECTION_LOST")); break;
      case -2: Serial.println(F("MQTT_CONNECT_FAILED")); break;
      case -1: Serial.println(F("MQTT_DISCONNECTED")); break;
      case 0: break;
      case 1: Serial.println(F("MQTT_CONNECT_BAD_PROTOCOL")); break;
      case 2: Serial.println(F("MQTT_CONNECT_BAD_CLIENT_ID")); break;
      case 3: Serial.println(F("MQTT_CONNECT_UNAVAILABLE")); break;
      case 4: Serial.println(F("MQTT_CONNECT_BAD_CREDENTIALS")); break;
      case 5: Serial.println(F("MQTT_CONNECT_UNAUTHORIZED")); break;
      default: Serial.print("failed, rc="); Serial.println(client.state());
    }
  }
  return client.connected();
}

MQTT библиотека (C/C++)

На случай необходимости — библиотека доступна для скачивания.

Схемы

Умная система обнаружения утечек воды и управления клапаном

Производственный процесс

  1. Зажимные клапаны и перистальтические насосы: эталон для абразивных систем молочника
  2. Мастерство управления датчиками и исполнительными механизмами на Raspberry Pi: акселерометр, сервопривод и потоко…
  3. Автоматизированная система наполнения и мониторинга бассейна
  4. Отладка двухъядерной системы Arduino Portenta H7 с Segger J-Link
  5. Универсальный Android-пульт на Arduino, 1Sheeld и ИК-управлении
  6. Создание анимации и игр на LCD 16×2 с Arduino UNO
  7. Умная копилка: управление монетоприёмником на Arduino Nano
  8. Arduino Power Control Center: N-FET, P-FET, реле и RTC
  9. Детектор аудиочастот на Arduino: измеряем пик громкости с высокочувствительным модулем
  10. Управление светодиодом по Bluetooth с Arduino: простой DIY-гид