Water control system

Hi
The device is designed for remote access to the readings of water consumption (cold / hot) and leakage control (wired or wireless probes). In the case of water leak detection is interrupted by means of electrically controlled valves. The device uses esp8266 + Blynk. Also NRF24L01 receiver to read the leakage from the wireless sensor. Wireless traffic is encrypted using AES128. Wireless sensor operates without using Arduino and Blynk, assembled on STM32 + nrf24l01, managing software for which is written in pure “C” in the other IDE.

digital dashboard

the device (with the cover open, top) and UPS (bottom)

water meters and electric valves

wired sensor

wireless sensor

Part 1:

Part 2:

Sketch (some parts of the code altered or deleted):

#define BLYNK_PRINT Serial    // Comment this out to disable prints and save space
    #include <ESP8266WiFi.h>
    #include <BlynkSimpleEsp8266.h>
    #include <SimpleTimer.h>
    #include "RF24.h"
    #include "AES.h"
    #include <EEPROM.h>
    #include <WiFiUdp.h>
    #include <TimeLib.h>
    #include <Ticker.h>

    Ticker counterReaderTicker;

    // NTP Servers:
    IPAddress timeServer(132, 163, 4, 101); // time-a.timefreq.bldrdoc.gov
    // IPAddress timeServer(132, 163, 4, 102); // time-b.timefreq.bldrdoc.gov
    // IPAddress timeServer(132, 163, 4, 103); // time-c.timefreq.bldrdoc.gov
    const int timeZone = 3;
    WiFiUDP Udp;
    unsigned int localPort = 8888;  // local port to listen for UDP packets

    const char auth[] = "xxxxxxxxxxxxxxxxxxxxxxxxx";


    RF24 radio(2, 15);
    uint8_t AES_key[176] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};

    SimpleTimer timer;

    WidgetLED led_leak_detected(V1);
    #define ACCIDENT_BUTTON_VPIN V2
    #define CWM_VPIN V3
    #define HWM_VPIN V4
    WidgetLCD lcd(V5);
    #define VALVES_BUTTOM_VPIN V6
    #define SELECT_MODE_VPIN V7
    #define PLUS_BUTTON_VPIN V8
    #define MINUS_BUTTON_VPIN V9
    #define TIME_VALUE_VPIN V10
    WidgetLED led_radio_error(V11);
    #define CWM_SENSOR_PIN 0
    #define HWM_SENSOR_PIN 3
    #define VALVE_OPENING_PIN 4
    #define VALVE_CLOSEING_PIN 5
    #define INFOLED_PIN 16

    void adjustButtonRun(bool inLoop);

    class CountSensor
    {
        int lastValue = 0;
        unsigned long lastStateChenges = 0;

      public:
        bool RegisterCurValue(int curValue)
        {
          bool needUpdate = false;
          if (curValue != lastValue) {
            if (millis() - lastStateChenges < 500)return false;
            if (curValue == 0) {
              lastStateChenges = millis();
              needUpdate = true;
            }
          }
          lastValue = curValue;
          return needUpdate;
        }
    };


    class StopWatch
    {
        uint32_t start_time = 0;

      public:

        void begin()
        {
          start_time = millis();
        }

        int millisPassed()
        {
          return millis() - start_time;
        }

        bool goneSeconds(uint32_t sec)
        {
          return millis() - start_time > sec * 1000;
        }

        bool goneMinutes(uint32_t min)
        {
          return millis() - start_time > min * 1000 * 60;
        }

        uint32_t secondsPassed()
        {
          return millisPassed() / 1000;
        }

        int minutesPassed()
        {
          return secondsPassed() / 60;
        }

    };

    class ValveControlManager
    {
      public:
        enum States {CLOSED, OPENED, UNKNOWN};

      private:
        StopWatch motor_workStopWatch;
        States cur_state = UNKNOWN;

      public:
        States getState()
        {
          return cur_state;
        }

        void open()
        {
          BLYNK_LOG("Close valves");

          motor_workStopWatch.begin();
          digitalWrite(VALVE_OPENING_PIN, HIGH);
          digitalWrite(VALVE_CLOSEING_PIN, LOW);
          cur_state = States::OPENED;
          Blynk.virtualWrite(VALVES_BUTTOM_VPIN, HIGH);

          lcd.clear();
          lcd.print(0, 0, "water supply");
          lcd.print(0, 1, "is turned on");
        }

        void close()
        {
          BLYNK_LOG("Close valves");

          motor_workStopWatch.begin();
          digitalWrite(VALVE_CLOSEING_PIN, HIGH);
          digitalWrite(VALVE_OPENING_PIN, LOW);
          cur_state = States::CLOSED;
          Blynk.virtualWrite(VALVES_BUTTOM_VPIN, LOW);

          lcd.clear();
          lcd.print(0, 0, "water supply");
          lcd.print(0, 1, "is turned off");
        }

        void run()
        {
          if (motor_workStopWatch.goneSeconds(5)) {
            digitalWrite(VALVE_CLOSEING_PIN, LOW);
            digitalWrite(VALVE_OPENING_PIN, LOW);
          }
        }

    };
    ValveControlManager valve_control_manager;

    StopWatch lastRadioReportStopWatch;

    void radioSetup()
    {
      radio.begin();
      radio.setChannel(99);
      radio.setPALevel(RF24_PA_HIGH);
      radio.openWritingPipe(0xd7d7d7d727LL);
      radio.openReadingPipe(1, 0xe7e7e7e728LL);
      radio.setCRCLength(RF24_CRC_DISABLED);
      radio.enableDynamicPayloads();
      radio.startListening();
      //radio.printDetails();
    }


    class StorageOfCurrentState
    {
      public:
        struct StoredValues
        {
          uint32_t cwm;
          uint32_t hwm;
          uint8_t accident_mode;
          uint8_t radio_error;
          uint8_t leak_detected;
        };

      private:
        struct StoredValues stored_values;

        bool current_state_storage_changed = false;

        StopWatch storageUpdateStopWatch;

      public:

        bool getRegularMode()
        {
          return !stored_values.accident_mode;
        }

        void activateAccidentMode(bool val) {
          current_state_storage_changed = true;
          stored_values.accident_mode = val;
          if (val) {
            valve_control_manager.close();
            Blynk.virtualWrite(ACCIDENT_BUTTON_VPIN, HIGH);
          } else {
            stored_values.radio_error = false;
            stored_values.leak_detected = false;
            led_leak_detected.off();
            led_radio_error.off();
          }
        }

        void leakDetected()
        {
          current_state_storage_changed = true;
          stored_values.accident_mode = true;
          stored_values.leak_detected = true;
          Blynk.virtualWrite(ACCIDENT_BUTTON_VPIN, HIGH);
          led_leak_detected.on();
        }

        uint8_t getLeakDetected()
        {
          return stored_values.leak_detected;
        }

        void radioErrorDetected()
        {
          current_state_storage_changed = true;
          stored_values.radio_error = true;
          led_radio_error.on();
        }

        void radioErrorReset()
        {
          current_state_storage_changed = true;
          stored_values.radio_error = false;
          led_radio_error.off();
        }

        bool getRadioErrorMode()
        {
          return stored_values.radio_error;
        }

        uint32_t getCWM()
        {
          return stored_values.cwm;
        }

        void setCWM(uint32_t val)
        {
          current_state_storage_changed = true;
          stored_values.cwm = val;
        }

        uint32_t getHWM()
        {
          return stored_values.hwm;
        }

        void setHWM(uint32_t val)
        {
          current_state_storage_changed = true;
          stored_values.hwm = val;
        }

        void loadPropertiesFromEEPROM()
        {
          uint8_t * buff = (uint8_t*)&stored_values;
          for (int i = 0; i < sizeof(StoredValues); i++)
          {
            buff[i] = EEPROM.read(i);
          }
        }

        void savePropertiesToEEPROM()
        {
          if ((!current_state_storage_changed) || (storageUpdateStopWatch.secondsPassed() < 10))return;

          BLYNK_LOG("Properties stored in eeprom %d", storageUpdateStopWatch.secondsPassed());

          uint8_t * buff = (uint8_t*)&stored_values;
          for (int i = 0; i < sizeof(StoredValues); i++) {
            EEPROM.write(i, buff[i]);
          }
          EEPROM.commit();
          current_state_storage_changed = false;
          storageUpdateStopWatch.begin();
        }
    };

    StorageOfCurrentState current_state_storage;

    CountSensor cwm_sensor;
    CountSensor hwm_sensor;

    bool wifi_connected()
    {
      return WiFi.status() == WL_CONNECTED;
    }

    int testWifi(void)
    {
      int c = 0;
      Serial.println("Waiting for Wifi to connect");
      while ( c < 20 ) {
        if (wifi_connected()) {
          Serial.println("WiFi connected.");
          return (1);
        }
        delay(500);
        Serial.print(WiFi.status());
        c++;
      }
      return (0);
    }

    void digitalClockDisplay(time_t time);

    enum LeakTypes {ANALOG_PROBE, WIRELESS_PROBE};

    void leakDetected(LeakTypes leakType)
    {
      if (current_state_storage.getRegularMode()) {
        valve_control_manager.close();

        current_state_storage.leakDetected();

        switch (leakType) {
          case ANALOG_PROBE:
            Blynk.notify("Accident mode! (analog sensor)");
            break;

          case WIRELESS_PROBE:
            Blynk.notify("Accident mode! (wireless sensor)");
            break;
        }

        BLYNK_LOG("Accident mode!");
      }
    }


    void counterReaderProc() //100ms
    {
      pinMode(CWM_SENSOR_PIN, INPUT_PULLUP);
      if (cwm_sensor.RegisterCurValue(digitalRead(CWM_SENSOR_PIN))) {
        current_state_storage.setCWM(current_state_storage.getCWM() + 1);
        BLYNK_LOG("CWM flow sensor");
      }

      pinMode(HWM_SENSOR_PIN, INPUT_PULLUP);
      if (hwm_sensor.RegisterCurValue(digitalRead(HWM_SENSOR_PIN))) {
        current_state_storage.setHWM(current_state_storage.getHWM() + 1);
        BLYNK_LOG("HWM flow sensor");
      }

      valve_control_manager.run();
    }

    void interrupt250() //interrupt 250ms
    {
      digitalWrite(INFOLED_PIN, !digitalRead(INFOLED_PIN));

      adjustButtonRun(true);


      int sensorValue = analogRead(A0);
      if (sensorValue < 1000) {
        leakDetected(LeakTypes::ANALOG_PROBE);
      }

      if (radio.available()) {
        byte radio_msg[16];
        byte radio_tmp[16];
        radio.read(radio_msg, 16);
        AES_decrypt_128(radio_msg, AES_key, radio_tmp);
        int messageId = radio_msg[0] + radio_msg[1] * 256 + radio_msg[2] * 65536 + radio_msg[3] * 65536 * 256;

        lastRadioReportStopWatch.begin();

        digitalClockDisplay(now());
        Serial.print(messageId);
        Serial.print(" ");
        int val = radio_msg[4] + radio_msg[5] * 256;
        Serial.println(val);

        if (val < 4095) {
          leakDetected(LeakTypes::WIRELESS_PROBE);
        }

        if (current_state_storage.getRadioErrorMode()) {
          current_state_storage.radioErrorReset();
        }

      }

      if (lastRadioReportStopWatch.minutesPassed() >= 5 && !current_state_storage.getRadioErrorMode()) {
        current_state_storage.radioErrorDetected();
        Blynk.notify("No data from the radio probe!");
      }

      current_state_storage.savePropertiesToEEPROM();
    }

    void interrupt1000()
    {
      Blynk.virtualWrite(CWM_VPIN, current_state_storage.getCWM() / 1000 );
      Blynk.virtualWrite(HWM_VPIN, current_state_storage.getHWM() / 1000 );

      time_t time = now();
      String time_str = String(hour(time)) + ":";
      if (minute(time) < 10) {
        time_str += String("0");
      }
      time_str += String(minute(time));
      Blynk.virtualWrite(TIME_VALUE_VPIN, time_str);
    }

    /*-------- NTP code ----------*/

    const int NTP_PACKET_SIZE = 48; // NTP time is in the first 48 bytes of message
    byte packetBuffer[NTP_PACKET_SIZE]; //buffer to hold incoming & outgoing packets

    // send an NTP request to the time server at the given address
    void sendNTPpacket(IPAddress &address)
    {
      // set all bytes in the buffer to 0
      memset(packetBuffer, 0, NTP_PACKET_SIZE);
      // Initialize values needed to form NTP request
      // (see URL above for details on the packets)
      packetBuffer[0] = 0b11100011;   // LI, Version, Mode
      packetBuffer[1] = 0;     // Stratum, or type of clock
      packetBuffer[2] = 6;     // Polling Interval
      packetBuffer[3] = 0xEC;  // Peer Clock Precision
      // 8 bytes of zero for Root Delay & Root Dispersion
      packetBuffer[12]  = 49;
      packetBuffer[13]  = 0x4E;
      packetBuffer[14]  = 49;
      packetBuffer[15]  = 52;
      // all NTP fields have been given values, now
      // you can send a packet requesting a timestamp:
      Udp.beginPacket(address, 123); //NTP requests are to port 123
      Udp.write(packetBuffer, NTP_PACKET_SIZE);
      Udp.endPacket();
    }

    time_t getNtpTime()
    {
      while (Udp.parsePacket() > 0) ; // discard any previously received packets
      Serial.println("Transmit NTP Request");
      sendNTPpacket(timeServer);
      uint32_t beginWait = millis();
      while (millis() - beginWait < 1500) {
        int size = Udp.parsePacket();
        if (size >= NTP_PACKET_SIZE) {
          Serial.println("Receive NTP Response");
          Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
          unsigned long secsSince1900;
          // convert four bytes starting at location 40 to a long integer
          secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
          secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
          secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
          secsSince1900 |= (unsigned long)packetBuffer[43];
          return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
        }
      }
      Serial.println("No NTP Response :-(");
      return 0; // return 0 if unable to get the time
    }



    int currentMode = 0;

    StopWatch LCDRefreshStopWatch;

    void refreshLCD(bool over)
    {
      if ((!over) && (LCDRefreshStopWatch.millisPassed() < 100)) {
        return;
      }

      LCDRefreshStopWatch.begin();

      BLYNK_LOG("Refresh LCD");

      lcd.clear();

      switch (currentMode) {
        case 0:
          lcd.print(0, 0, "uptime");
          lcd.print(0, 1, String(millis() / 1000 / 60 / 60) + "h");
          break;

        case 1:
          lcd.print(0, 0, "cold water meter");
          lcd.print(0, 1, String(current_state_storage.getCWM()));
          break;

        case 2:
          lcd.print(0, 0, "hot water meter");
          lcd.print(0, 1, String(current_state_storage.getHWM()));
          break;
      }
    }

    StopWatch adjustButtonStopWatch;

    BLYNK_WRITE(SELECT_MODE_VPIN)
    {
      adjustButtonStopWatch.begin();

      int i = param.asInt();
      if (i == 0) {
        return;
      }

      currentMode++;
      if (currentMode == 3)currentMode = 0;

      refreshLCD(true);
    }

    bool adjustButtonPressed = false;

    int adjust_direction = 1;

    void adjustSetUp(int button)
    {
      if (button == 0) {
        adjustButtonPressed = false;
        refreshLCD(true);
        return;
      }
      if (!adjustButtonPressed) {
        adjustButtonPressed = true;
      }
      adjustButtonRun(false);
    }

    BLYNK_WRITE(PLUS_BUTTON_VPIN)
    {
      adjustButtonStopWatch.begin();
      adjust_direction = 1;
      adjustSetUp(param.asInt());
    }

    BLYNK_WRITE(MINUS_BUTTON_VPIN)
    {
      adjustButtonStopWatch.begin();
      adjust_direction = -1;
      adjustSetUp(param.asInt());
    }

    void adjustButtonRun(bool inLoop)
    {
      if (adjustButtonStopWatch.secondsPassed() > 20 && currentMode != 0 && !adjustButtonPressed) {
        currentMode = 0;
        refreshLCD(true);
      }

      if (!adjustButtonPressed)return;

      int currentStep = 1;
      uint32_t delta = adjustButtonStopWatch.secondsPassed();
      if (inLoop && (delta < 1))return;

      if (delta >= 15) {
        currentStep = 10000;
      } else if (delta >= 10) {
        currentStep = 1000;
      } else if (delta >= 5) {
        currentStep = 100;
      } else if (delta >= 1) {
        currentStep = 10;
      }

      currentStep *= adjust_direction;

      switch (currentMode) {
        case 1:
          current_state_storage.setCWM(current_state_storage.getCWM() + currentStep);
          break;
        case 2:
          current_state_storage.setHWM(current_state_storage.getHWM() + currentStep);
          break;
      }

      refreshLCD(false);
    }

    BLYNK_WRITE(ACCIDENT_BUTTON_VPIN)
    {
      BLYNK_LOG("Accident button %d", param.asInt());


      switch (param.asInt()) {
        case 0:
          if (!current_state_storage.getRegularMode()) {
            current_state_storage.activateAccidentMode(false);
            lcd.clear();
            lcd.print(0, 0, "accident mode is");
            lcd.print(0, 1, "disabled");
          }
          break;
        case 1:
          if (current_state_storage.getRegularMode()) {
            current_state_storage.activateAccidentMode(true);
            lcd.clear();
            lcd.print(0, 0, "accident mode is");
            lcd.print(0, 1, "enabled");
          }
          break;
      }
    }

    bool possibleToOpenValves()
    {
      return (valve_control_manager.getState() != ValveControlManager::States::OPENED) && (current_state_storage.getRegularMode());
    }

    BLYNK_WRITE(VALVES_BUTTOM_VPIN)
    {
      BLYNK_LOG("Valves button %d", param.asInt());
      switch (param.asInt()) {
        case 0:
          valve_control_manager.close();
          break;

        case 1:
          if (possibleToOpenValves()) {
            valve_control_manager.open();
          } else {
            Blynk.virtualWrite(VALVES_BUTTOM_VPIN, LOW);
          }
          break;
      }
    }

    time_t getNtpTime();

    void printDigits(int digits)
    {
      Serial.print(":");
      if (digits < 10)
        Serial.print('0');
      Serial.print(digits);
    }

    void digitalClockDisplay(time_t time)
    {
      // digital clock display of the time
      Serial.print(hour(time));
      printDigits(minute(time));
      printDigits(second(time));
      Serial.print(" ");
      Serial.print(day(time));
      Serial.print(".");
      Serial.print(month(time));
      Serial.print(".");
      Serial.print(year(time));
      Serial.print(" - ");
      //Serial.println();
    }

    void updateVirtualPinValues()
    {
      BLYNK_LOG("Restore vpins values from EEPROM");
      if (current_state_storage.getLeakDetected()) {
        led_leak_detected.on();
      } else {
        led_leak_detected.off();
      }

      if (current_state_storage.getRadioErrorMode()) {
        led_radio_error.on();
      } else {
        led_radio_error.off();
      }

      Blynk.virtualWrite(VALVES_BUTTOM_VPIN, valve_control_manager.getState() == ValveControlManager::States::OPENED ? HIGH : LOW );
      Blynk.virtualWrite(ACCIDENT_BUTTON_VPIN, !current_state_storage.getRegularMode() ? HIGH : LOW);
    }

    BLYNK_CONNECTED() {
      updateVirtualPinValues();
    }

    IPAddress blynkServer(192, 168, 1, 40);

    void setup()
    {
      Serial.begin(115200);

      radioSetup();

      EEPROM.begin(sizeof(StorageOfCurrentState::StoredValues));
      current_state_storage.loadPropertiesFromEEPROM();

      WiFi.begin("xxxxxxxxxx", "xxxxxxxxxxxx");
      Blynk.config(auth, blynkServer);

      pinMode(VALVE_OPENING_PIN, OUTPUT);
      pinMode(VALVE_CLOSEING_PIN, OUTPUT);

      pinMode(INFOLED_PIN, OUTPUT);

      timer.setInterval(250L, interrupt250);

      timer.setInterval(1000L, interrupt1000);

      Serial.print("IP number assigned by DHCP is ");
      Serial.println(WiFi.localIP());
      Serial.println("Starting UDP");
      Udp.begin(localPort);
      Serial.print("Local port: ");
      Serial.println(Udp.localPort());
      Serial.println("waiting for sync");
      setSyncProvider(getNtpTime);


      counterReaderTicker.attach(0.1, counterReaderProc);
    }

    void loop()
    {
      Blynk.run();
      timer.run();
    }
6 Likes

How exactly you detect water leakage.

Using a sensor:

“A0” connected to pin “ADC” ESP8266.

 int sensorValue = analogRead(A0);
 if (sensorValue < 1000) {
    leakDetected(LeakTypes::ANALOG_PROBE);
  }

Wireless unit not used such a sensor. Just measure the voltage between the two pins (+ and ADC of STM32).

This type of captor measure relative humidity of the air. If the leakage is slow or laminar you will not detect it, if your are in a place where their aren’t movement of air too…
Detect humidity is an complex question, it depends of a lot of parameter.
Your wifi connection action on valve can work but detect humidity is like find a ghost. Much time running after wind and own mind than detect something.

Initially, the sensor is designed to determine the moisture content of the soil (not in the air!). But I use it to determine the presence of water directly on the electrodes. Sensor electrodes are located directly near the floor in areas where leakage is possible (a plurality of electrodes connected in parallel). The electrodes are immersed in water, in case of leakage. Believe me :slight_smile:, the electronic circuit is working properly. Simple tests show it. Earlier, the same scheme worked well. My project, but without the use Blynk - (ru) https://geektimes.ru/post/259086/ sensors used are the same.

Are you using ST HAL or STD LIB on stm32?

Only HAL, STM32CubeMX to generate the initial image of the project works fine.
The source code of the main controller ( for STM32F103CBT6 + HAL, was to ESP8266 + Blyn) and wireless sensor (STM32F103CBT6 + HAL, actually) - https://github.com/ravendmaster/water-control-system/
Written in C

nice. I like this combo a lot, simplifies everything and it’s also helpful in hardware design. BTW use DMA for SPI it’s much quicker, also you can add RTOS it will allow your MCU to go to sleep when it’s unused. It’s great when you are running on battery.