Friday, January 2, 2015

Solar pool water heater controller

Arduino

For some time I was interested in creating a project using Arduino, the "open-source electronics platform based on easy-to-use hardware and software", sometimes called Open Hardware Platform, created by the italian Massimo Banzi and his friends. Watch the video about the project at TED.

Unexpected incident


Unfortunately I never had the time to work with Arduino, but recently a lightning strike changed this.



My solar power pool heater original controller was blown and I had to make a choice: to buy another one or to create a brand-new from scratch. Of course I was motivated to create an entirely new one using Arduino.

Previous controller design


Basically a system like this has 2 thermometers and a relay to control the pump motor. When the sun is shining, the heater collector gets hot and (if the pool water is cold) the water pump is turned on, so the water passes through the collector and the water is heat up. Some systems like these are designed so there is a dedicated pump to filter the water and another one to push the water through the collector. My installation uses an alternative design where the same pump is used both to filter the water and to make it flow through the solar collector.

Previous controller sensors placement
After a few years using this system I started to realize many deficiencies it had. The first thing I realized is that, in cold days, when the water was very cold, as soon as this cold water flowed through the collector in the roof, it cooled both the solar collector and the roof temperature sensor in a way that it switched off the pump in a short time, as if there was no more sun. The cold water itself was confusing the roof temperature sensor. Another thing is that when clouds passed through the sky, this cooled the sensor momentarily and the pump motor was switched off and on again several times in short periods of time. This certainly was not a good thing for the health of the water pump motor. In many other ways I felt the algorithm was very poor to handle the variety of weather situations.

The new design


I started designing the same basic system but with two additional temperature sensors: one in the water inlet of the heater collector and another one in the outlet of the collector. This way I could measure the real temperature gain when the water passed through the collector. Maybe it wouldn't be very useful for the final control algorithm but at least it would be useful for the analysis of the performance of the system and algorithm tuning. As a final tweak, the original roof sensor was detached from the solar collector, to avoid interference from the cold water.

New controller sensors placement

Beyond the basic features of the heater system, I had also a few personal requirements.
  • It had to be operated with the same basic user interface of the previous system, because my employee that cleans the pool (among many other tasks) would have a lot of difficulty to learn a different or more elaborated system.
  • In cloudy or cold days when there is no sun for the whole day and the pump is not switched on to heat the water, it should be forcibly switched on for at least an hour, at the end of the day, to make sure the pool water is filtered at least a bit.
  • It should feature a programmable timer that switches on the pump at any time of day so if I had to process chemicals that were dropped in the pool water at night time. It should return to automatic mode after the time configured.
  • If somebody switches the system to manual operation, after a certain time (12 hours) probably this means it was forgotten like this and it should switch off automatically to avoid energy waste.
  • A way to view and analyze temperature history, relay state and internal state remotely, so I could improve the algorithm.

Wish List (for next version)


  • Web Interface for temperature view, configuration and operation (not enough memory to do this with Arduino Uno)
  • Dedicated pumps for filtering and heating
  • Better switches responsiveness (see explanation below)

Implementation


Everything worked well and I was happy with the result. The main choice I regret was that I should've used a bigger Arduino version. I used all digital and analog input/outputs available and because of this I was unable to use hardware interrupts to handle the switches. The consequence of this is that the responsiveness of the command switches is poor. But considering it is supposed to work automatically without any user interaction, the switches are rarely used, so I think it is acceptable.
There is also a service that runs within the controller that sends an UDP packet with all sensors and internal state info to an UDP server. Soon I will publish another post with details of the UDP Server and some analysis that can be done with the resulting data.

Device housing


Prototype

Wood support for the Arduino board and the display

Ethernet Shield on top of Arduino board

Display, custom board and wiring

Ready for installation

Installed in place. Still needs some finishing touches

Electronics


I like to deal with hardware, but I have to admit I am a software guy, so I don't have a lot to say about the hardware design. I based my project on the hints I found at Arduino forums and blogs like this.

Bill of materials:

1x Arduino Uno R3
1x Arduino Ethernet Shield (W5100 compatible)
1x Display LCD SCM1602A (16 Cols, 2 Rows)
4x DS18B20 digital temperature sensor (water proof housing)
1x 5V Relay
1x Transistor 2N2222
1x Diode 1N4004
4x LEDs 5mm
1x Trimpot 1K
1x Trimpot 10K
2x Push button switch
1x Switch (Auto/Off/Manual)
7x 1K resistor (1/4w)
4x 330R resistor  (1/8w)
1x 4k7 resistor (1/4w)
1x Case

Digital Input/Output allocation:

D0, D1: reserved for serial communication (USB)
D2: Display DB7
D3: Display DB6
D4: Display DB5
D5: Display DB4
D6: One Wire (temperature sensors)
D7: Relay output
D8: Display enable
D9: Display RS
D10: Ethernet shield SS
D11: Ethernet shield MOSI
D12: Ethernet shield MISO
D13: Ethernet shield SCK

Analog Input (all used as digital) allocation:

A0: Auto switch (input)
A1: Manual switch (input)
A2: Timer + switch (input)
A3: Timer - switch (input)
A4: Timer LED (output)
A5: Summer Time switch (input)


Schematics


The schematics are separated in 4 blocks (relay, sensors, switches, display) to make it easier to understand the connections.

Relay connections


Sensors connections


Switches and LEDs connections


LCD Display Connections



Arduino software


The final version of the software is almost 1000 lines long. I will dedicate another blog post to explain it per logic block, so it could be helpful to others. It features:

  • DHCP client (Dynamic IP address)
  • NTP client (wall time synchronization)
  • Temperature sensor reading, averaging and error handling (One Wire)
  • Controller logic (state machine)
  • Relay control
  • Timer service
  • Key debouncing (for push buttons and switches)
  • LCD display handling
  • UDP client (temperature and state report)
  • Manual summer time selection (dip switch)

Finally, the code:

#include <Time.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
#include <SPI.h>
#include <avr/pgmspace.h> 

#define TRACE_ENABLED 1

#if TRACE_ENABLED
    #define TRACE( s )  Serial.println( s )
#else
    #define TRACE( s )
#endif

// ***** Constants *****

// Serial USB needs Digital 0,1
// Ethernet Card needs Digital 10,11,12,13
// SD Card needs Digital 4

#define POOL_VERSION    "1.4"

// ***** Hardware Configuration *****

#define RELAY_OUTPUT    7
#define TIMER_LED       A4

#define AUTO_SWITCH         A0
#define MANUAL_SWITCH       A1
#define TIMER_PLUS_SWITCH   A2
#define TIMER_MINUS_SWITCH  A3
#define SUMMER_TIME_SWITCH  A5

#define LCD_RS      9
#define LCD_ENABLE  8
#define LCD_D0      5
#define LCD_D1      4
#define LCD_D2      3
#define LCD_D3      2

#define LCD_ROWS    2
#define LCD_COLS    16

#define ONE_WIRE_BUS                6
#define SENSOR_RESOLUTION_BITS      12
#define TEMPERATURE_HISTORY_SIZE    15

// ***** Operation set points *****

const float POOL_WARM_HI_THRESHOLD    = 32.0;
const float ROOF_WARM_LO_THRESHOLD    = 35.0;
const float ROOF_WARM_DELTA_THRESHOLD = 10.0;
const float HEATER_GAIN_LO_THRESHOLD  = 0.15;
const float MAX_TEMP_JUMP_PERCENT     = 0.20;   // 20%

const int DEBOUNCE_DELAY_MILLIS    = 10;
const int DEBOUNCE_READ_COUNT      = 5;
const int UNKNOWN_STATE            = -1;

const time_t TIMER_STEP_SECS         = 30L * 60L;
const time_t MAX_TIMER_PERIOD_SECS   = 6L * 60L * 60L;
const time_t MAX_MANUAL_PERIOD_SECS  = 12L * 60L * 60L;

const time_t WARM_WAIT_SECS          = 1L * 60L * 60L;
const time_t STEADY_WAIT_SECS        = 5L * 60L;
const time_t INEFFICIENT_WAIT_SECS   = 5L * 60L;

const time_t MIN_UPTIME              = 8L * 60L * 60L;
const time_t MIN_FILTER_TIME         = 45L * 60L;
const time_t TARGET_MIN_FILTER_TIME  = 60L * 60L;
const time_t MIN_EXTRA_FILTER_TIME   = 30L * 60L;

const int    WAKEUP_HOUR             = 8;
const int    SLEEP_HOUR              = 18;
const int    MIN_FILTER_HOUR         = 17;

// ***** Sensors identifications *****

DeviceAddress roofThermometer   = { 0x28, 0xDD, 0xB2, 0x08, 0x06, 0x00, 0x00, 0x91 };
DeviceAddress inletThermometer  = { 0x28, 0xD9, 0xDE, 0x08, 0x06, 0x00, 0x00, 0x7D };
DeviceAddress outletThermometer = { 0x28, 0xE6, 0x9B, 0x0A, 0x06, 0x00, 0x00, 0x88 };
DeviceAddress poolThermometer   = { 0x28, 0x15, 0x64, 0x40, 0x05, 0x00, 0x00, 0x75 };

float roofTempHistory[ TEMPERATURE_HISTORY_SIZE ];
float inletTempHistory[ TEMPERATURE_HISTORY_SIZE ];
float outletTempHistory[ TEMPERATURE_HISTORY_SIZE ];
float poolTempHistory[ TEMPERATURE_HISTORY_SIZE ];
int tempHistoryIndex = 0;

float lastRoofTemp      = 0;
float roofTempAverage   = 0;
float lastInletTemp     = 0;
float inletTempAverage  = 0;
float lastOutletTemp    = 0;
float outletTempAverage = 0;
float lastPoolTemp      = 0;
float poolTempAverage   = 0;

int roofErrorCount   = 0;
int inletErrorCount  = 0;
int outletErrorCount = 0;
int poolErrorCount   = 0;

// ***** Display behavior *****

const int DISPLAY_CYCLE_START = 0;
const int DISPLAY_CYCLE_END = 5;
const unsigned long LOOP_CYCLE_MILLIS = 1000;

// PROGMEM used just for exercise. It wasn't really needed for this use case

#define STATE_BUFFER_SIZE   16

char state0[] PROGMEM = "DETECT";
char state1[] PROGMEM = "MANUAL";
char state2[] PROGMEM = "TIMER";
char state3[] PROGMEM = "STARTING";
char state4[] PROGMEM = "SLEEPING";
char state5[] PROGMEM = "WARM";
char state6[] PROGMEM = "COLD";
char state7[] PROGMEM = "STEADY";
char state8[] PROGMEM = "HEATING";
char state9[] PROGMEM = "INEFFICIENT";
char state10[] PROGMEM = "OFF";
PGM_P stateNames_P[] PROGMEM = { state0, state1, state2, state3, state4, state5, state6, state7, state8, state9, state10 };

#define MESSAGE_BUFFER_SIZE   16

char message0[] PROGMEM = "Setup...    ";
char message1[] PROGMEM = "Starting... ";
char message2[] PROGMEM = "Sensors...  ";
char message3[] PROGMEM = "Ethernet... ";
char message4[] PROGMEM = "DHCP Failure";
char message5[] PROGMEM = "IP Address  ";
char message6[] PROGMEM = "Time set... ";
char message7[] PROGMEM = "NTP Sync    ";
char message8[] PROGMEM = "NTP Failure ";
char message9[] PROGMEM = "Setup ok... ";
PGM_P messages_P[] PROGMEM = { message0, message1, message2, message3, message4, message5, message6, message7, message8, message9 };

#define MSG_SETUP           0
#define MSG_STARTING        1
#define MSG_SENSORS         2
#define MSG_ETHERNET        3
#define MSG_DHCP_FAILURE    4
#define MSG_IP_ADDRESS      5
#define MSG_TIME_SET        6
#define MSG_NTP_SYNC        7
#define MSG_NTP_FAILURE     8
#define MSG_SETUP_OK        9

// State machine constants and attributes
#define STATE_DETECT        0
#define STATE_MANUAL        1
#define STATE_TIMER         2
#define STATE_STARTING      3
#define STATE_SLEEPING      4
#define STATE_WARM          5
#define STATE_COLD          6
#define STATE_STEADY        7
#define STATE_HEATING       8
#define STATE_INEFFICIENT   9
#define STATE_OFF           10

int currState = STATE_STARTING;
time_t stateTime;

// Running time counters
time_t upTime = 0;
time_t startFilterTime = 0;
time_t accumFilterTime = 0;
bool minFilterTimeChecked = false;

// Timer requested period
time_t timerPeriod = 0;

// Ethernet and UDP constants
byte macAddress[] = {  0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
IPAddress timeServer( 192, 168, 0, 1 );         // Local network NTP server
IPAddress udpLogServer( 192, 168, 0, 126 );     // Local network UDP server (for temperature history storage)
const uint16_t udpLogServerPort = 8126;         // Local network UDP server port

EthernetUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets

// Time zones
const int BRASIL_EAST        = -3;
const int BRASIL_EAST_SUMMER = -2;

int lastAutoSwitchState       = LOW;
int lastManualSwitchState     = LOW;
int lastTimerPlusSwitchState  = LOW;
int lastTimerMinusSwitchState = LOW;
int lastSummerTimeSwitchState = LOW;
int relayState                = LOW;

unsigned long startCycle = 0;
int startingCount = 0;
int displayCycle = DISPLAY_CYCLE_START;

// Setup a oneWire instance to communicate with any OneWire devices 
OneWire oneWire( ONE_WIRE_BUS );

// Pass our oneWire reference to Dallas Temperature.
DallasTemperature sensors( &oneWire );

// initialize the library with the numbers of the interface pins
LiquidCrystal lcd( LCD_RS, LCD_ENABLE, LCD_D0, LCD_D1, LCD_D2, LCD_D3 );

void setup() 
{
    char msgBuffer[ MESSAGE_BUFFER_SIZE ];
    
    Serial.begin( 9600 );
    TRACE( getMessageStr( msgBuffer, MSG_SETUP ) );

    lcd.begin( LCD_COLS, LCD_ROWS );
    print1( getMessageStr( msgBuffer, MSG_SETUP ) );

    // Setup digital outputs
    pinMode( TIMER_LED,          OUTPUT );
    pinMode( RELAY_OUTPUT,       OUTPUT );
    pinMode( AUTO_SWITCH,        INPUT );
    pinMode( MANUAL_SWITCH,      INPUT );
    pinMode( TIMER_PLUS_SWITCH,  INPUT );
    pinMode( TIMER_MINUS_SWITCH, INPUT );
    pinMode( SUMMER_TIME_SWITCH, INPUT );

    // Set initial state for digital outputs
    digitalWrite( TIMER_LED,    LOW );
    digitalWrite( RELAY_OUTPUT, LOW );

    resetSwitchStates();

    // Setup thermometers library
    TRACE( getMessageStr( msgBuffer, MSG_SENSORS ) );
    print1( getMessageStr( msgBuffer, MSG_SENSORS ) );
    sensors.begin();
    sensors.setResolution( roofThermometer,   SENSOR_RESOLUTION_BITS );
    sensors.setResolution( inletThermometer,  SENSOR_RESOLUTION_BITS );
    sensors.setResolution( outletThermometer, SENSOR_RESOLUTION_BITS );
    sensors.setResolution( poolThermometer,   SENSOR_RESOLUTION_BITS );
    resetThermometers();

    // Setup Ethernet card
    TRACE( getMessageStr( msgBuffer, MSG_ETHERNET ) );
    print1( getMessageStr( msgBuffer, MSG_ETHERNET ) );
    while ( Ethernet.begin( macAddress ) == 0 )
    {
        // no point in carrying on, so do nothing forevermore:
        print2( getMessageStr( msgBuffer, MSG_DHCP_FAILURE ) );
        delay( 10000 );
    }

    TRACE( getMessageStr( msgBuffer, MSG_IP_ADDRESS ) );
    TRACE( Ethernet.localIP() );
    
    print1( getMessageStr( msgBuffer, MSG_IP_ADDRESS ) );
    lcd.setCursor( 0, 1 );
    lcd.print( Ethernet.localIP() );

    // Setup time from NTP
    TRACE( getMessageStr( msgBuffer, MSG_NTP_SYNC ) );
    print1( getMessageStr( msgBuffer, MSG_NTP_SYNC ) );
    Udp.begin( localPort );
    setSyncProvider( getNtpTime );
    if ( timeStatus() == timeNotSet )
    {
        print1( getMessageStr( msgBuffer, MSG_NTP_FAILURE ) );
        TRACE( getMessageStr( msgBuffer, MSG_NTP_FAILURE ) );
        delay( 1000 );
    }
    else
        TRACE( getMessageStr( msgBuffer, MSG_TIME_SET ) );

    // Reset State Machine
    upTime = now();
    setState( STATE_STARTING );
    TRACE( getMessageStr( msgBuffer, MSG_SETUP_OK ) );
}

void loop()
{
    unsigned long startCycle = millis();

    if ( currState != STATE_STARTING )
        readSwitches();

    readThermometers();

    switch( currState )
    {
        case STATE_DETECT:
            if ( isSleepTime() )
                setState( STATE_SLEEPING );
            else
            {
                resetSwitchStates();
                if ( lastManualSwitchState == HIGH )
                    setState( STATE_MANUAL );
                else if ( lastAutoSwitchState == HIGH )
                {
                    if ( isPoolWarm() )
                        setState( STATE_WARM );
                    else
                    {
                        if ( isRoofWarm() )
                            setState( STATE_HEATING );
                        else
                            setState( STATE_COLD );
                    }
                }
                else
                    setState( STATE_OFF );
            }
            break;
        case STATE_STARTING:
            if ( ++startingCount > TEMPERATURE_HISTORY_SIZE )
            {
                startingCount = 0;
                setState( STATE_DETECT );
            }
            break;
        case STATE_MANUAL:
            setHeatingOn();
            if ( hasElapsedAtState( MAX_MANUAL_PERIOD_SECS ) )
                setState( STATE_OFF );
            break;
        case STATE_TIMER:
            if ( ( now() - stateTime ) >= timerPeriod )
            {
                setState( STATE_DETECT );
                digitalWrite( TIMER_LED, LOW );
                timerPeriod = 0;
            }
            break;
        case STATE_SLEEPING:
            setHeatingOff();
            dailyReset();
            if ( timeStatus() != timeNotSet )
            {
                if ( !isSleepTime() )
                    setState( STATE_DETECT );
            }
            break;
        case STATE_WARM:
            if ( isSleepTime() )
                setState( STATE_SLEEPING );
            else if ( hasElapsedAtState( WARM_WAIT_SECS ) )
                setState( STATE_DETECT );
            else if ( hasElapsedAtState( STEADY_WAIT_SECS ) )
            {
                setHeatingOff();
                checkMinFilterTime();
            }
            else
            {
                if ( !isPoolWarm() )
                    setState( STATE_HEATING );
            }
            break;
        case STATE_COLD:
            if ( isSleepTime() )
                setState( STATE_SLEEPING );
            else if ( isRoofWarm() )
                setState( STATE_STEADY );
            else
                setHeatingOff();
            checkMinFilterTime();
            break;
        case STATE_STEADY:
            if ( !isRoofWarm() )
                setState( STATE_COLD );
            else if ( hasElapsedAtState( STEADY_WAIT_SECS ) )
                setState( STATE_HEATING );
            break;
        case STATE_HEATING:
            if ( isSleepTime() )
                setState( STATE_SLEEPING );
            else if ( isPoolWarm() )
                setState( STATE_WARM );
            else
            {
                setHeatingOn();
                if ( !isHeaterEfficient() )
                    setState( STATE_INEFFICIENT );
            }
            break;
        case STATE_INEFFICIENT:
            if ( isHeaterEfficient() )
                setState( STATE_HEATING );
            else if ( hasElapsedAtState( INEFFICIENT_WAIT_SECS ) )
                setState( STATE_COLD );
            break;
        case STATE_OFF:
            setHeatingOff();
            break;
        default:
            setState( STATE_DETECT );
            break;
    }

    updateDisplay();
    logCurrentState();

    // Wait complement of time cycle 
    unsigned long elapsed = millis() - startCycle;
    if ( elapsed < LOOP_CYCLE_MILLIS )
        delay( LOOP_CYCLE_MILLIS - elapsed );
}

const char *getStateStr( char *buffer, int state )
{
    strcpy_P( buffer, ( PGM_P )pgm_read_word( &( stateNames_P[ state ] ) ) );
    return( buffer );
}

const char *getMessageStr( char *buffer, int messageIndex )
{
    strcpy_P( buffer, ( PGM_P )pgm_read_word( &( messages_P[ messageIndex ] ) ) );
    return( buffer );
}

void setState( int newState )
{
#if TRACE_ENABLED
    char buffer[ 64 ];
    char state1Buffer[ STATE_BUFFER_SIZE ];
    char state2Buffer[ STATE_BUFFER_SIZE ];
    sprintf( buffer, "From %s to %s", getStateStr( state1Buffer, currState ), getStateStr( state2Buffer, newState ) );
    TRACE( buffer );
    TRACE( getDateAndTimeString( buffer ) );
#endif
    currState = newState;
    stateTime = now();
}

void setHeatingOff( void )
{
    if ( relayState == HIGH )
    {
        relayState = LOW;
        digitalWrite( RELAY_OUTPUT, LOW );

        // Compute running time for filter/heater
        if ( startFilterTime > 0 )
        {
            time_t elapsedFilterTime = now() - startFilterTime;
            accumFilterTime += elapsedFilterTime;
            startFilterTime = 0;
#if TRACE_ENABLED
            char buffer[ 64 ];
            char timeBuffer[ 16 ];
            sprintf( buffer, "Filter time: ", getDurationHMS( accumFilterTime, timeBuffer ) );
            TRACE( buffer );
#endif
        }
    }
}

void setHeatingOn( void )
{
    if ( relayState == LOW )
    {
        relayState = HIGH;
        digitalWrite( RELAY_OUTPUT, HIGH );
        startFilterTime = now();
    }
}

void dailyReset( void )
{
    if ( accumFilterTime > 0 );
        accumFilterTime = 0;
    minFilterTimeChecked = false;
}

time_t getAccumulatedFilterTime( void )
{
    // Account previously accumulated plus currently accumulated
    time_t totalAccumFilterTime = accumFilterTime;
    if ( startFilterTime > 0 )
    {
        time_t elapsedFilterTime = now() - startFilterTime;
        totalAccumFilterTime += elapsedFilterTime;
    }
    return( totalAccumFilterTime );
}

void checkMinFilterTime( void )
{
    // Check only once a day
    if ( minFilterTimeChecked )
        return;

    // Is it time to check?
    if ( hour() >= MIN_FILTER_HOUR )
    {
        minFilterTimeChecked = true;

        // Not enough filter time today ?
        time_t totalAccumFilterTime = getAccumulatedFilterTime();
        if ( totalAccumFilterTime < MIN_FILTER_TIME )
        {
            // Do we have sufficient up time to reach this conclusion?
            time_t sysRunningTime = now() - upTime;
            if ( sysRunningTime > MIN_UPTIME )
            {
                // Simulate timer Action
                if ( currState != STATE_TIMER )
                {
                    // Complement filter time, attempting to reach target filter time, but respecting minimum
                    time_t additionalFilterTime = TARGET_MIN_FILTER_TIME - totalAccumFilterTime;
                    if ( additionalFilterTime < MIN_EXTRA_FILTER_TIME )
                        additionalFilterTime = MIN_EXTRA_FILTER_TIME;
                    timerPeriod = additionalFilterTime;
                    setState( STATE_TIMER );
                    digitalWrite( TIMER_LED, HIGH );
                    setHeatingOn();
                }
            }
        }
    }
}

time_t elapsedAtState( void )
{
    return( now() - stateTime );
}

bool hasElapsedAtState( time_t seconds )
{
    return( elapsedAtState() >= seconds );
}

time_t getRemainingAtState( time_t totalTime )
{
    time_t elapsed = elapsedAtState();
    if ( elapsed >= totalTime )
        return( 0 );
    else
        return( totalTime - elapsed );
}

void resetSwitchStates( void )
{
    lastAutoSwitchState       = getButtonState( AUTO_SWITCH,        UNKNOWN_STATE );
    lastManualSwitchState     = getButtonState( MANUAL_SWITCH,      UNKNOWN_STATE );
    lastTimerPlusSwitchState  = getButtonState( TIMER_PLUS_SWITCH,  UNKNOWN_STATE );
    lastTimerMinusSwitchState = getButtonState( TIMER_MINUS_SWITCH, UNKNOWN_STATE );
    lastSummerTimeSwitchState = getButtonState( SUMMER_TIME_SWITCH, UNKNOWN_STATE );
}

void readSwitches( void )
{
    int autoSwitchState = getButtonState( AUTO_SWITCH, lastAutoSwitchState );
    if ( autoSwitchState != lastAutoSwitchState )
    {
        lastAutoSwitchState = autoSwitchState;
        setState( STATE_DETECT );
    }

    int manualSwitchState = getButtonState( MANUAL_SWITCH, lastManualSwitchState );
    if ( manualSwitchState != lastManualSwitchState )
    {
        lastManualSwitchState = manualSwitchState;
        if ( manualSwitchState == HIGH )
            setState( STATE_MANUAL );
        else
            setState( STATE_DETECT );
    }

    // Do not accept timer requests when OFF
    if ( ( currState == STATE_OFF ) || ( currState == STATE_MANUAL ) )
        return;

    int timerPlusSwitchState = getButtonState( TIMER_PLUS_SWITCH, lastTimerPlusSwitchState );
    if ( timerPlusSwitchState != lastTimerPlusSwitchState )
    {
        lastTimerPlusSwitchState = timerPlusSwitchState;
        if ( timerPlusSwitchState == HIGH )
        {
            timerPeriod += TIMER_STEP_SECS;
            if ( timerPeriod > MAX_TIMER_PERIOD_SECS )
                timerPeriod = MAX_TIMER_PERIOD_SECS;
            if ( currState != STATE_TIMER )
            {
                setState( STATE_TIMER );
                digitalWrite( TIMER_LED, HIGH );
                setHeatingOn();
            }
        }
    }

    int timerMinusSwitchState = getButtonState( TIMER_MINUS_SWITCH, lastTimerMinusSwitchState );
    if ( timerMinusSwitchState != lastTimerMinusSwitchState )
    {
        lastTimerMinusSwitchState = timerMinusSwitchState;
        if ( timerMinusSwitchState == HIGH )
        {   
            if ( currState == STATE_TIMER )
            {
                if ( timerPeriod > TIMER_STEP_SECS )
                {
                    timerPeriod -= TIMER_STEP_SECS;
                }
                else
                {
                    timerPeriod = 0;
                    digitalWrite( TIMER_LED, LOW );
                    setState( STATE_DETECT );
                }
            }
        }
    }
}

int getButtonState( int button, int previousState )
{
    int newState = digitalRead( button );
    if ( newState != previousState )
    {
        for( int i = 0; i < DEBOUNCE_READ_COUNT; i++ )
        {
            delay( DEBOUNCE_DELAY_MILLIS );
            int checkState = digitalRead( button );
            if ( checkState != newState )
            {
                if ( previousState == UNKNOWN_STATE )
                {
                    // Try again with new state
                    i = 0;
                    newState = checkState;
                }
                else
                    return( previousState );
            }
        }
    }
    return( newState );
}

// -------- DISPLAY code ----------

void updateDisplay( void )
{
    char lcdBuffer[ LCD_COLS + 1 ];
    if ( currState == STATE_STARTING )
    {
        int remaining = TEMPERATURE_HISTORY_SIZE - startingCount;
        sprintf( lcdBuffer, "Starting v%s %02d", POOL_VERSION, remaining );
        print1( lcdBuffer );
    }
    else
    {
        char timeBuffer[ LCD_COLS + 1 ];
        char floatBuffer1[ 9 ];
        char floatBuffer2[ 9 ];
        sprintf( lcdBuffer, "P%s  C%s", getFloatStr( floatBuffer1, poolTempAverage ), getFloatStr( floatBuffer2, roofTempAverage ) );
        print1( lcdBuffer );
        if ( ++displayCycle == DISPLAY_CYCLE_END )
        {
            displayCycle = DISPLAY_CYCLE_START;
            if ( timeStatus() == timeNotSet )
                print2( "Time not set    " );
            else
            {
                getCompactDateTimeString( lcdBuffer );
                print2( lcdBuffer );
            }
        }
        else
        {
            switch( currState )
            {
                case STATE_MANUAL:
                    sprintf( lcdBuffer, "Manual  %s", getDurationHMS( getRemainingAtState( MAX_MANUAL_PERIOD_SECS ), timeBuffer ) );
                    print2( lcdBuffer );
                    break;
                case STATE_TIMER:
                    sprintf( lcdBuffer, "Timer   %s", getDurationHMS( getRemainingAtState( timerPeriod ), timeBuffer ) );
                    print2( lcdBuffer );
                    break;
                case STATE_SLEEPING:
                    sprintf( lcdBuffer, "Sleep   %s", getTimeString( timeBuffer ) );
                    print2( lcdBuffer );
                    break;
                case STATE_WARM:
                    sprintf( lcdBuffer, "Warm    %s", getDurationHMS( getRemainingAtState( WARM_WAIT_SECS ), timeBuffer ) );
                    print2( lcdBuffer );
                    break;
                case STATE_COLD:
                    sprintf( lcdBuffer, "Auto    %s", getTimeString( timeBuffer ) );
                    print2( lcdBuffer );
                    break;
                case STATE_STEADY:
                    sprintf( lcdBuffer, "Waiting %s", getDurationHMS( getRemainingAtState( STEADY_WAIT_SECS ), timeBuffer ) );
                    print2( lcdBuffer );
                    break;
                case STATE_HEATING:
                case STATE_INEFFICIENT:
                    sprintf( lcdBuffer, "Heating  G%s", getFloatStr( floatBuffer1, getHeaterGain() )  );
                    print2( lcdBuffer );
                    break;
                case STATE_OFF:
                    sprintf( lcdBuffer, "Off     %s", getTimeString( timeBuffer ) );
                    print2( lcdBuffer );
                    break;
            }
        }
    }
}

void print1( const char *text )
{
    lcd.home();
    lcd.print( text );
}

void print2( const char *text )
{
    lcd.setCursor( 0, 1 );
    lcd.print( text );
}

char *getFloatStr( char *buffer, float value )
{
    bool negative = ( value < 0 );
    if ( negative )
        value = -value;
    sprintf( buffer, "%c%02d.%02d", ( negative ? '-' : ' ' ), ( int )value, ( ( int )( value * 100.0 ) % 100 ) );
    return( buffer );
}

// -------- Thermometers code ----------

void resetThermometers( void )
{
    memset( roofTempHistory,   0, sizeof( roofTempHistory ) );
    memset( inletTempHistory,  0, sizeof( inletTempHistory ) );
    memset( outletTempHistory, 0, sizeof( outletTempHistory ) );
    memset( poolTempHistory,   0, sizeof( poolTempHistory ) );
}

void readThermometers( void )
{
    sensors.requestTemperatures();

    readTemp( roofThermometer,   lastRoofTemp,   roofTempAverage,   roofErrorCount,   roofTempHistory,   "Roof" );
    readTemp( inletThermometer,  lastInletTemp,  inletTempAverage,  inletErrorCount,  inletTempHistory,  "Inlet" );
    readTemp( outletThermometer, lastOutletTemp, outletTempAverage, outletErrorCount, outletTempHistory, "Outlet" );
    readTemp( poolThermometer,   lastPoolTemp,   poolTempAverage,   poolErrorCount,   poolTempHistory,   "Pool" );

    if ( ++tempHistoryIndex >= TEMPERATURE_HISTORY_SIZE )
        tempHistoryIndex = 0;
}

void readTemp( DeviceAddress thermometer, float &lastTemp, float &tempAverage, int &errorCount, float temperatureHistory[], const char *sensorName )
{
    float currTemp = sensors.getTempC( thermometer );
    if ( currTemp == DEVICE_DISCONNECTED )
    {
        errorCount++;
        reportSensorError( sensorName );
    }
    else
    {
        float jumpPercent = ( lastTemp == 0 ? 0 : ( abs( currTemp - lastTemp ) / lastTemp ) );
        if ( jumpPercent > MAX_TEMP_JUMP_PERCENT )
            reportJumpError( sensorName, lastTemp, currTemp, jumpPercent );
        else
        {
            lastTemp = currTemp;
            temperatureHistory[ tempHistoryIndex ] = currTemp;
            tempAverage = getAverageTemperature1( temperatureHistory );
        }
    }
}

bool isPoolWarm( void )
{
    return(  poolTempAverage >= POOL_WARM_HI_THRESHOLD );
}

bool isRoofWarm( void )
{
    return( ( roofTempAverage >= ROOF_WARM_LO_THRESHOLD ) ||
            ( roofTempAverage > ( poolTempAverage + ROOF_WARM_DELTA_THRESHOLD ) ) );
}

bool isHeaterEfficient( void )
{
    return( getHeaterGain() >= HEATER_GAIN_LO_THRESHOLD );
}

float getAverageTemperature1( float temperatureHistory[] )
{
    float tempSum = 0;
    int count = 0;
    for( int i = 0; i < TEMPERATURE_HISTORY_SIZE; i++ )
    {
        if ( temperatureHistory[ i ] != 0 )
        {
            tempSum += temperatureHistory[ i ];
            count++;
        }
    }
    if ( count > 0 )
        return( tempSum / ( float )count );
    else
        return( 0 );
}

float getHeaterGain( void )
{
    return( outletTempAverage - inletTempAverage );
}

// -------- TIME code ----------

bool isSleepTime( void )
{
    if ( ( timeStatus() == timeNotSet ) )
        return( false );
    else
    {
        int hourNow = hour();
        return( ( hourNow < WAKEUP_HOUR ) || ( hourNow >= SLEEP_HOUR ) );
    }
}

char *getDateAndTimeString( char *buffer )
{
    sprintf( buffer, "%04d-%02d-%02d,%02d:%02d:%02d", year(), month(), day(), hour(), minute(), second() );
    return( buffer );
}

char *getCompactDateTimeString( char *buffer )
{
    sprintf( buffer, "%02d/%s  %02d:%02d:%02d", day(), monthShortStr( month() ), hour(), minute(), second() );
    return( buffer );
}

char *getTimeString( char *buffer )
{
    sprintf( buffer, "%02d:%02d:%02d", hour(), minute(), second() );
    return( buffer );    
}

char *getDurationHMS( time_t duration, char *buffer )
{
    int seconds = ( duration % 60 );
    int minutes = ( duration / 60 ) % 60;
    int hours   = ( duration / 3600 );
    sprintf( buffer, "%02d:%02d:%02d", hours, minutes, seconds );
    return( buffer );    
}

// -------- 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

const int NTP_TIMEOUT = 2500;

int getTimeZone( void )
{
    if ( lastSummerTimeSwitchState == HIGH )
        return( BRASIL_EAST_SUMMER );
    else
        return( BRASIL_EAST );
}

time_t getNtpTime()
{
    // discard any previously received packets
    while ( Udp.parsePacket() > 0 ); 
    Serial.println( "Transmit NTP Request" );
    sendNTPpacket( timeServer );
    uint32_t beginWait = millis();
    while ( millis() - beginWait < NTP_TIMEOUT ) 
    {
        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 + getTimeZone() * SECS_PER_HOUR );
        }
    }
    Serial.println( "No NTP Response :-(" );
    return 0; // return 0 if unable to get the time
}

// 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();
}

// UDP Log service

void logCurrentState( void )
{
    char message[ 128 ];
    char timeBuffer[ 32 ];
    char durationBuffer[ 16 ];
    char stateBuffer[ STATE_BUFFER_SIZE ];
    char floatBuffer1[ 9 ];
    char floatBuffer2[ 9 ];
    char floatBuffer3[ 9 ];
    char floatBuffer4[ 9 ];

    sprintf( message, "%s,%s,%s,%s,%s,%s,%s,%s",
             getDateAndTimeString( timeBuffer ),
             getFloatStr( floatBuffer1, lastRoofTemp ),
             getFloatStr( floatBuffer2, lastInletTemp ),
             getFloatStr( floatBuffer3, lastOutletTemp ),
             getFloatStr( floatBuffer4, lastPoolTemp ),
             getDurationHMS( getAccumulatedFilterTime(), durationBuffer ),
             getStateStr( stateBuffer, currState ),
             ( relayState == HIGH ? "ON" : "OFF" ) );
    TRACE( message );
    sendUDPMessage( message );
}

void reportSensorError( const char *sensorName )
{
    char message[ 128 ];
    char timeBuffer[ 32 ];
    sprintf( message, "%s,ERROR,%s sensor,%d,%d,%d,%d",
             getDateAndTimeString( timeBuffer ), sensorName,
             roofErrorCount, inletErrorCount, outletErrorCount, poolErrorCount );
    sendUDPMessage( message );
}

void reportJumpError( const char *sensorName, float lastTemp, float currTemp, float jumpPercent )
{
    char message[ 128 ];
    char timeBuffer[ 32 ];
    char floatBuffer1[ 9 ];
    char floatBuffer2[ 9 ];
    char floatBuffer3[ 9 ];
    sprintf( message, "%s,ERROR,%s sensor,%s->%s(%s%%)",
             getDateAndTimeString( timeBuffer ), sensorName,
             getFloatStr( floatBuffer1, lastTemp ),
             getFloatStr( floatBuffer2, currTemp ),
             getFloatStr( floatBuffer3, jumpPercent * 100 ) );
    sendUDPMessage( message );
}

void sendUDPMessage( const char *message )
{
    Udp.beginPacket( udpLogServer, udpLogServerPort );
    Udp.write( ( const uint8_t * )message, strlen( message ) );
    Udp.endPacket();
}

Tuesday, December 16, 2014

Rainwater filter

A few years ago I made a simple rainwater collector system with a rain gutter, pipes and an underground tank. As water filtration was not a completely solved matter, I only used it to an automated garden irrigation system and other basic uses in the outdoor area.

Recently I decided to make some improvements to expand the usefulness of this water, and decided to build a new rainwater filter.

I followed some tips from this great site (very grateful to the author):

http://www.sempresustentavel.com.br/hidrica/minicisterna/filtro-de-agua-de-chuva.htm



Filter beside of the original pipe that was replaced



I added some decoration to engage my daughter in the project



Installed in the rain gutter


The next step now is to make a second filter to discard the first rainwater drop, which contains a lot of dirt. Coming soon.