Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C

Arduino Semi-intelligent Air Dryer Control

4.83/5 (14 votes)
25 Mar 2014CPOL4 min read 33.4K   311  
How to control an air-dryer in your bathroom

Introduction

I am always aware of the relative humidity in our bathroom. Recently we bought an air dryer, but I was very dissatisfied by the on-off cycles of this stupid device. Although I set it to 70 or 80% RH, it switches on for very short periods and (at times) I did not see this high humidity. I am already driving a network of wireless sensors within the 433MHz band and fill a database with the readings of three sensors. I added one more using one Arduino Uno R3 with a DHT22 sensor and a 433MHz transmitter so I could track the temperature and humidity values of the bath too.

In general, controlling humidity cannot be done simply by watching for the current RH% and then switch on or off a dryer or fan. RH% will vary with temperature changes and depends on the outside humidity. For example, you will not reach 50% relative humidity (RH) in a room if the outside has 75% RH and the room is not sealed.

I decided to control the air dryer from the Arduino with some better logic than just going on/off at a specific humidity level.

Image 1

Background

When showering starts in the bathroom, the humidity rapidly grows. I would like the Arduino to recognize this peak change and to switch the dryer on for the maximum time.

Image 2

Image 3

As the Arduino was already equipped with the 433MHz transmitter and there are power switches that are also controlled by 433MHz and I would like to avoid any contact with high voltage, I decided to control the power from the same transmitter.

So we have the Arduino, a DHT22 (the DHT11 was too bad), and a 433 MHz receiver. For some visual feedback I added an LCD, but it is not needed for the function.

Image 4

Using the Code

The code uses two buffers to store the recent humidity measurements. Only the last values are kept. Every time a new value is pushed, the oldest value is removed. I use one 'short-time' buffer, which is updated every minute. The second buffer is the long-time buffer (memory), which is filled every time when the buffer capacity of the first buffer is reached. So let's say I store five values in the first buffer, then the second buffer is updated on every fifth update.

If the newest value is above a threshold of the last one and the values are increasing, I set a state flag which represents the powering of the dryer. The second buffer is currently not used but would be useful to switch the dryer off when the humidity is below the normal long-time values.

I have implemented a base class (using code of circular buffer) and init two of these buffer classes in my new class.

ByteBuffer.h (a 'stack' like buffer):

C++
#include <stdlib.h>

#ifndef BYTEBUFFER_H
#define BYTEBUFFER_H

#define byte unsigned char

class ByteBuffer
{
public:
    ByteBuffer();

    // This method initializes the datastore of the buffer to a certain sizem the buffer should NOT be used before this call is made
    void init(unsigned int buf_size);

    // This method resets the buffer into an original state (with no data)
    void clear();

    // This releases resources for this buffer, after this has been called the buffer should NOT be used
    void deAllocate();

    // Returns how much space is left in the buffer for more data
    int getSize();

    // Returns the maximum capacity of the buffer
    int getCapacity();

    // This method returns the byte that is located at index in the buffer but doesn't modify the buffer like the get methods (doesn't remove the retured byte from the buffer)
    byte peek(unsigned int index);

    //
    // Put methods, either a regular put in back or put in front
    //
    int putInFront(byte in);
    int put(byte in);

    int push(byte in);    //HGO
    byte getMedian();    //HGO
    byte getMax();    //HGO
    byte getMin();    //HGO
    byte getState();    //HGO
    void setTreshold(byte t);    //HGO
    byte getTreshold();    //HGO
    int getDirection();
    int getStateDuration();

    int putIntInFront(int in);
    int putInt(int in);

    int putLongInFront(long in);
    int putLong(long in);

    int putFloatInFront(float in);
    int putFloat(float in);

    //
    // Get methods, either a regular get from front or from back
    //
    byte get();
    byte getFromBack();

    int getInt();
    int getIntFromBack();

    long getLong();
    long getLongFromBack();

    float getFloat();
    float getFloatFromBack();

    void setMaxDuration(int);

protected:
    byte* data;

    unsigned int capacity;
    unsigned int position;
    unsigned int length;
    byte state;
    byte treshold;

    int stateDuration;  //start if state=1 for
    int maxDuration;    //max duration (ie 5)
    int direction;
};

#endif // BYTEBUFFER_H 

This class is used two times for a long and short time storage in NewBuffer:

C++
#ifndef NEWBUFFER_H
#define NEWBUFFER_H

#include "ByteBuffer.h"


class newBuffer
{
    public:
        newBuffer();
        ~newBuffer();
        void init(int s);
        ByteBuffer bytebuffer1;
        ByteBuffer bytebuffer2;

        byte peek1(unsigned int index);
        byte peek2(unsigned int index);
        int getSize1();
        int getSize2();
           byte getMedian1();    //HGO
        byte getMax1();    //HGO
        byte getMin1();    //HGO
        byte getState1();    //HGO
        byte getTreshold1();    //HGO

           byte getMedian2();    //HGO
        byte getMax2();    //HGO
        byte getMin2();    //HGO
        byte getState2();    //HGO
        void setTreshold(byte t);    //HGO
        byte getTreshold2();    //HGO

        int getDirection1();
        int getStateDuration1();
        int getDirection2();
        int getStateDuration2();
        void setMaxDuration1(int);
        void setMaxDuration2(int);

        int put(byte in);
        int push(byte);

    protected:
        int iCount;
        byte treshold;
        int capacity;

        int maxDuration;    //max duration (ie 5)
    private:
};

#endif // NEWBUFFER_H

You see there are getters to get values from the short and the long time buffer.

The main work is done when a new value is pushed to the stack:

C++
int newBuffer::push(byte in){
    //static byte lastByte;
    iCount++;
    bytebuffer1.push(in);
    if(iCount==capacity){
        bytebuffer2.push(in);
        iCount=0;
    }
    return 0;
}  

You see that the second (long time) buffer gets a push only every x times.

The Arduino sketch:

C++
...
void setup(){
  Serial.begin(SERIAL_BAUD);
  Serial.println("ThermoHygroTransmitter version 4 (power save 2)");
  sensor.setup(DHT_DATA_PIN);
    
  sleepTime=10000; //10 seconds 
  
  pinMode(lcd_backlight, OUTPUT);

  lcd.begin(16, 2);
  // Print a message to the LCD.
  lcd.print("Temp/Feuchte"); //12 chars
//  lcdOFF();  
  lcdON();
  // Initialize the send buffer that we will use to send data
  //ByteBuffer send_buffer;
  send_buffer.init(5);
  send_buffer.setTreshold(5);
  send_buffer.setMaxDuration1(10);

  //init RCswitch instance
  mySwitch = RCSwitch();
  // Transmitter is connected to Arduino Pin #3  
  // shared with SensorTransmitter code 
  mySwitch.enableTransmit(TX_DATA_PIN);
  // Optional set protocol (default is 1, will work for most outlets)
  mySwitch.setProtocol(1);
  
  // Optional set number of transmission repetitions.
  //mySwitch.setRepeatTransmit(2); //stops working!
  
  //initialy switch power outlet OFF
  switchOnOff(false);
}

void loop(){
  //do stuff
    animateLCD();
    Serial.println("...waked up");
    float humidity = sensor.getHumidity();
    float temperature = sensor.getTemperature();
    if(strcmp(sensor.getStatusString(),"OK")==0){
      bValidData=true;
      lastTemp=(int)temperature;
      lastHumi=(int)humidity;     
      showLCD(lastTemp, lastHumi, send_buffer.getState1());
    }
    else{
      bValidData=false;
    }
    Serial.print("sendCount="); Serial.println(sendCount);
    if(sendCount >= sendInterval){
      sendCount=0;
     // Displays temperatures from -10 degrees Celsius to +20,
     // and humidity from 10% REL to 40% REL, with increments of 2
     if(bValidData) {

        send_buffer.push((byte)lastHumi);
        state=send_buffer.getState1();
        //send switch command
        if(state==0)
          switchOnOff(false);
        else
          switchOnOff(true);
          
        int x=0;
        Serial.println("============================");
        for(x=0; x<send_buffer.getSize1(); x++){
          Serial.print("["); 
          Serial.print(send_buffer.peek1(x));
          Serial.print("]"); 
        }
        Serial.println("\n============================");

       // Temperatures are passed at 10 times the real value,
       // to avoid using floating point math.
       ///transmitter.sendTempHumi(i * 10, i + 20);
       //HACK: send temp with ending in 5 (which is .5 degree clesius if state==1
       // if state==0 send temp with ending in 0
       if(state==1)
         transmitter.sendTempHumi(lastTemp * 10 + 5, lastHumi);
       else
         transmitter.sendTempHumi(lastTemp * 10, lastHumi);
       
       Serial.println("Transmit done");
       blinkLEDfast();
       // Wait two seconds before sending next.
       bValidData=false;
       showLCD(lastTemp, lastHumi, state);
       
     }//validData
   }//send_loop
   //delay(1000);
  sendCount++;
  blinkLED();

  Serial.println("About to sleep...");
  delay(200);
  sleep.pwrDownMode(); //set sleep mode
  sleep.sleepDelay(sleepTime); //sleep for: sleepTime
}

Here is the push of the new measured RH value and the check of the state property to switch an outlet on/off:

C++
send_buffer.push((byte)lastHumi);
state=send_buffer.getState1();
//send switch command
if(state==0)
  switchOnOff(false);
else
  switchOnOff(true);

When a new value is pushed, we do some calculations and start/continue the powering of a dryer fan using the state var:

C++
int ByteBuffer::push(byte in){
    if(length < capacity){
        // save data byte at end of buffer
        data[(position+length) % capacity] = in;
        // increment the length
        length++;
//        return 1;
    }
    else{
        //move all one down
        unsigned int i=0;
        for(i=0; i<length-1; i++)
            data[i]=data[i+1];
        data[length-1]=in;

    }
    //check if to switch FAN on and keep for nexxt 5 iteratrions
    if( ((getMax()-getMin())>treshold) && (getDirection()>0) ){
        //set state=1 only if stateDuration is less then maxDuration
        if(stateDuration<maxDuration){
            state=1;
        }
    }
    if(state==1 || stateDuration>0)
        stateDuration++;    //increment stateDuration
    if(stateDuration>maxDuration){
        if(state==1){
            state=0;
        }
        stateDuration=0;
    }
     return 0;
}

The above code first pushes the new value onto the stack. If the stack is full in regards of the number of maximum values, the oldest value is removed.

Then we check if max and min of all values is greater than a threshold and if the value shows an increase. If so, we start to set the state var. The state var is used to control the powering of a dryer fan and will remain set for maxDuration push cycles. So we do not switch on/off the fan directly pending to the values but implement a follow-up time. As we push a new value every minute and maxDusration is 9, we have a follow-up time of 9 minutes.

As I log (send) all values to a php/mysql web server, I can control what the sketch does:

Image 5

As you see, the state powered the outlet three times, every time the threshold of the humidity increase was hit. We are three people showering at the morning. ;-)

Currently I am using a netio board (Pollin.de) to receive temperature and humidty of wireless sensors and forward the data to a web server that can produce above graphics.

Image 6

This netio board is connected via ethernet and receives the wireless sensors with a simple 433MHz receiver:

Image 7

Acknowledements

ToDo

  • Cleanup unused code from ByteBuffer and newBuffer

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)