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