/* ---------------------------------------------------------------------------------------- * Arduino Thermostat for Millivolt Gas and Low-Voltage Electric Baseboard Heat * Copyright (c) Graham McMicken <graham@mcmicken.ca> * ---------------------------------------------------------------------------------------- */ #include <glcd.h> #include <IRremote.h> #include <EEPROM.h> #include <OneWire.h> #include <DallasTemperature.h> #include <Wire.h> #include <RealTimeClockDS1307.h> #include "fonts/allFonts.h" #include "bitmaps/allBitmaps.h" #define TIMER_TEMP 30000 // 30 Seconds #define TIMER_SLOPE 300000 // 5 Minutes #define TIMER_NOINPUT 30000 // 30 Seconds #define TIMER_SETTINGS 10000 // 10 Seconds #define TOTAL_SCHEDULES 8 // 7 for days of the week + one ad-hoc schedule, this is necessary to blank the EEPROM, uncomment in setup(). #define SCHEDULE_ZONES 2 #define MIN_TEMP 12 #define MAX_TEMP 30 #define TEMP_ADJUST 6.0 #define BUFFER_UBOUND 0.35 #define BUFFER_LBOUND 0.40 #define ROWS 0 #define COLS 1 #define WIDTH 2 #define HEIGHT 3 #define LENGTH 4 #define PADDING 5 #define GRAPH_X 6 #define GRAPH_Y 60 #define GRAPH_LENGTH 55 #define GRAPH_HEIGHT 27 #define HISTORY_LENGTH ( ( 24 * 60 ) / (TIMER_SLOPE / 1000 / 60) ) // 24 Hours #define IR_LEFT 0x2FD1AE5 #define IR_RIGHT 0x2FD38C7 #define IR_UP 0x2FDF00F #define IR_DOWN 0x2FD9867 #define IR_EXIT 0x2FD7887 #define IR_MENU 0x2FD58A7 #define IR_MODE 0x2FDE817 #define IR_SAVE 0x2FDB847 #define IR_VIEWTMP 0x2FDB847 #define IR_ELEC 0x2FD30CF #define IR_EN_SAVE 0x2FDEA15 #define IR_GAS 0x2FDC639 #define IR_GASLCK 0x2FD2AD5 #define IR_NUM0 0x2FD00FF #define IR_NUM1 0x2FD807F #define IR_NUM2 0x2FD40BF #define IR_NUM3 0x2FDC03F #define IR_NUM4 0x2FD20DF #define IR_NUM5 0x2FDA05F #define IR_NUM6 0x2FD609F #define IR_NUM7 0x2FDE01F #define IR_NUM8 0x2FD10EF #define IR_NUM9 0x2FD906F //Pin Assignments int pinLDR = A1; // Light Dependent Resistor int pinIR = 2; // IR Receiver int pinRelayGas = 3; // Gas Switch int pinRelayElec = 4; // Electric Switch int pinTemp = 12; // Temp Sensor int pinBacklightPWM = 13; // LCD Backlight Output //Display Initialization int iCursorPos = 0; boolean bCursorMovement = true; int iDefaultX = 0; int iDefaultY = 0; char formatted[] = "00-00-00 00:00:00x"; char* rDaysOfTheWeek[] = {"Ad-hoc Schedule", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}; //Operating Variables boolean bHeatCycle = false; boolean bElec = true; boolean bElecSave = true; boolean bGas = true; boolean bGasLock = false; boolean bAdjustedOff = false; boolean bTempReqRec = false; boolean bSettingChanged = false; int iMode = 0; int iTempRise = 0; float fTempSlope = 0.0; float fCurrentTemp = 0.0; float fPrevTemp = 0.0; int iSetTemp = 0; long iGraphOffset = 0; int iPwmVal = 0; byte rTempHistory[HISTORY_LENGTH]; //IR & Temp Setup IRrecv irrecv(pinIR); decode_results results; OneWire ds(pinTemp); DallasTemperature dsTemp(&ds); //Timer Setup static unsigned long lWaitSlope = 0; static unsigned long lWaitTemp = 0; static unsigned long lOffTimer = 0; static unsigned long lWaitSetting = 0; void setup() { //Un-comment this once to blank EEPROM values (default 255) for the programmable schedule. //for (int i = 0; i < (TOTAL_SCHEDULES*SCHEDULE_ZONES*5); i++) EEPROM.write(i, 0); GLCD.Init(); //Serial.begin(9600); pinMode(pinRelayElec, OUTPUT); digitalWrite(pinRelayElec, HIGH); pinMode(pinRelayGas, OUTPUT); digitalWrite(pinRelayGas, HIGH); irrecv.enableIRIn(); //Start the IR receiver buffer dsTemp.begin();; //Start the temp sensor library Wire.begin(); //Start the I2C RTC /* Fill the history array with the current temperature * so that slope calculations begin immediately after startup. */ dsTemp.requestTemperatures(); delay(1000); pollTemp(); fPrevTemp = fCurrentTemp; for (int i = 0; i < HISTORY_LENGTH; i++) rTempHistory[i] = (int)floor(fCurrentTemp + 0.5); uiMain(); //Here we go! } /* ---------------------------------------------------------------------------------------- * Main Loop * * Optimized to run quickly to feel responsive, calling temperature polling * and display updates at intervals defined at the top of the script. * ---------------------------------------------------------------------------------------- */ void loop() { //Set the backlight brightness dependent on the LDR value iPwmVal = analogRead(pinLDR); analogWrite(pinBacklightPWM, map(constrain(iPwmVal, 150, 600), 600, 150, 100, 255)); //Serial.println(iPwmVal); //Poll temperature and adjust backlight as defined by TIMER_TEMP (plus one second for poll time) if( (long)( millis() - lWaitTemp ) >= 0 ){ if (bTempReqRec) { pollTemp(); lWaitTemp = millis() + TIMER_TEMP; }else{ dsTemp.requestTemperatures(); lWaitTemp = millis() + 1000; //Read time on DS18B20 is ~700ms } bTempReqRec = !bTempReqRec; } //Graph the temp every 5 minutes, determine rising or falling indicator, and calculate temp slope. if( (long)( millis() - lWaitSlope ) >= 0 ){ calculateSlope(); determineHeatState(); lWaitSlope = millis() + TIMER_SLOPE; } //Apply settings changes after a brief timeout to prevent relay abuse. if( (long)( millis() - lWaitSetting ) >= 0 && bSettingChanged == true ){ determineHeatState(); bSettingChanged = false; } if( (long)( millis() - lOffTimer ) >= 0 && bAdjustedOff == true ){ bAdjustedOff = false; controlHeat(false); lWaitSlope = millis() + TIMER_SLOPE; } //Send IR commands to the inputDirector() function. if (irrecv.decode(&results)) { inputDirector(results.value); irrecv.resume(); } } /* ---------------------------------------------------------------------------------------- * IR Input Handling: * * inputDirector() - Handles IR input from the main screen to call various * toggle functions and launch auxilary screens. * ---------------------------------------------------------------------------------------- */ void inputDirector(unsigned long irKey){ //Serial.println(irKey, HEX); switch (irKey) { case IR_ELEC: bElec = !bElec; uiMainDrawElec(); break; case IR_EN_SAVE: if (bElec) { bElecSave = !bElecSave; uiMainDrawElec(); } break; case IR_GAS: if (!bGasLock) { bGas = !bGas; uiMainDrawGas(); } break; case IR_GASLCK: bGasLock = !bGasLock; uiMainDrawGas(); controlHeat(bHeatCycle); return; break; case IR_MODE: iMode++; uiMainDrawMode(); if (iMode == 3) iSetTemp = 18; checkScheduleTime(); break; case IR_UP: case IR_DOWN: if (iMode == 3) { uiMainSetTemp(); uiMain(); } break; case IR_LEFT: iGraphOffset = iGraphOffset + 10; if (iGraphOffset > HISTORY_LENGTH - GRAPH_LENGTH) iGraphOffset = HISTORY_LENGTH - GRAPH_LENGTH; uiMainDrawGraph(); return; break; case IR_RIGHT: iGraphOffset = iGraphOffset - 10; if (iGraphOffset < 0) iGraphOffset = 0; uiMainDrawGraph(); return; break; case IR_VIEWTMP: uiMainViewTemp(); uiMain(); break; case IR_MENU: while(1) { if (!uiSetCal(0)) break; //Ad-hoc schedule first for ease of access. if (!uiSetCal(1)) break; if (!uiSetCal(2)) break; if (!uiSetCal(3)) break; if (!uiSetCal(4)) break; if (!uiSetCal(5)) break; if (!uiSetCal(6)) break; if (!uiSetCal(7)) break; if (!uiSetTime()) break; } uiMain(); break; } //end switch lWaitSetting = millis() + TIMER_SETTINGS; bSettingChanged = true; } /* ---------------------------------------------------------------------------------------- * Heater Control * * determineHeatState() * controlHeat() * ---------------------------------------------------------------------------------------- */ void determineHeatState(){ if (!checkScheduleTime()){ controlHeat(false); return; } float fLowTempDiff = iSetTemp - BUFFER_LBOUND - fCurrentTemp; float fHighTempDiff = iSetTemp + BUFFER_UBOUND - fCurrentTemp; if (fLowTempDiff >= 0 && bHeatCycle == false) { //Turn the heat on controlHeat(true); }else if (fHighTempDiff <= 0 && bHeatCycle == true) { //Turn the heat off controlHeat(false); }else if (fHighTempDiff > 0 && fTempSlope > 0.05 && bHeatCycle == true) { //Adjust the off time to prevent over-shoot adjustOffTime(fHighTempDiff); }else{ //Re-apply our current heat state. (In the case of a heat source change etc) controlHeat(bHeatCycle); } } void controlHeat(boolean bOnOff){ if (bOnOff == true){ bHeatCycle = true; if ((iSetTemp - fCurrentTemp) < 3 && bElecSave == true && bGas == true) { digitalWrite(pinRelayGas, LOW); digitalWrite(pinRelayElec, HIGH); }else{ if (bElec) digitalWrite(pinRelayElec, LOW); else digitalWrite(pinRelayElec, HIGH); if (bGas) digitalWrite(pinRelayGas, LOW); else if (!bGasLock) digitalWrite(pinRelayGas, HIGH); } }else if (bOnOff == false) { bHeatCycle = false; bAdjustedOff = false; digitalWrite(pinRelayElec, HIGH); if (!bGasLock) digitalWrite(pinRelayGas, HIGH); else digitalWrite(pinRelayGas, LOW); } } /* ---------------------------------------------------------------------------------------- * Temperature Polling and Calculations: * * checkScheduleTime() - Check to see if we're inside a scheduled block and set iSetTemp appropriately. * pollTemp() - To poll and store the current temperature, and to call the display function if necessary. * calculateSlope() - To calculate the rate of change and call the graphing function. * adjustOffTime() - A timer set to expire as we reach our set temperature, to combat overshooting. * ---------------------------------------------------------------------------------------- */ boolean checkScheduleTime(){ if (iMode == 3) { uiMainDrawSetTemp(); return true; } if (iMode == 0) { iSetTemp = 0; uiMainDrawSetTemp(); return false; } RTC.readClock(); /* RTC.getFormatted(formatted); //Serial.println(formatted); if (RTC.isStopped()) //Serial.println("Clock Stopped"); else //Serial.println("Clock Running"); */ iSetTemp = 0; int iScheduleStart, iScheduleEnd, iScheduleTemp, iOffset; int iCurrDay = RTC.getDayOfWeek(); int iHours = RTC.getHours(); //Serial.print("Current Day: "); //Serial.print(iCurrDay); //Serial.print(", Current Hour: "); //Serial.println(iHours); if (iMode == 1) { int iPrevDay; if (iCurrDay == 1) iPrevDay = 7; else iPrevDay = iCurrDay - 1; for (int i = 0; i < SCHEDULE_ZONES; i++) { iOffset = (iPrevDay * SCHEDULE_ZONES * 5) + (i * 5); iScheduleStart = convert12to24(EEPROM.read(iOffset + 0), EEPROM.read(iOffset + 1)); iScheduleEnd = convert12to24(EEPROM.read(iOffset + 2), EEPROM.read(iOffset + 3)); iScheduleTemp = EEPROM.read(iOffset + 4); //Serial.print("Start: "); //Serial.print(iScheduleStart); //Serial.print(", End: "); //Serial.print(iScheduleEnd); //Serial.print(", Current: "); //Serial.println(iHours); if (iScheduleEnd < iScheduleStart && iHours < iScheduleEnd && iScheduleTemp >= MIN_TEMP) { iSetTemp = EEPROM.read(iOffset + 4); break; //There should not be more than one occurence of an end time into the next day so stop when we find one. } } for (int i = 0; i < SCHEDULE_ZONES; i++) { iOffset = (iCurrDay * SCHEDULE_ZONES * 5) + (i * 5); iScheduleStart = convert12to24(EEPROM.read(iOffset + 0), EEPROM.read(iOffset + 1)); iScheduleEnd = convert12to24(EEPROM.read(iOffset + 2), EEPROM.read(iOffset + 3)); iScheduleTemp = EEPROM.read(iOffset + 4); //Serial.print("Start: "); //Serial.print(iScheduleStart); //Serial.print(", End: "); //Serial.print(iScheduleEnd); //Serial.print(", Current: "); //Serial.println(iHours); if (iScheduleEnd < iScheduleStart) iScheduleEnd = 24; if (iHours >= iScheduleStart && iHours < iScheduleEnd && iScheduleTemp >= MIN_TEMP) { iSetTemp = EEPROM.read(iOffset + 4); //Overwrite value to give lower zones priority. } } } if (iMode == 2) { for (int i = 0; i < SCHEDULE_ZONES; i++) { iOffset = i * 5; iScheduleStart = convert12to24(EEPROM.read(iOffset + 0), EEPROM.read(iOffset + 1)); iScheduleEnd = convert12to24(EEPROM.read(iOffset + 2), EEPROM.read(iOffset + 3)); iScheduleTemp = EEPROM.read(iOffset + 4); if (((iHours >= iScheduleStart && iHours < iScheduleEnd) || (iScheduleEnd < iScheduleStart && (iHours < iScheduleEnd || iHours >= iScheduleStart))) && iScheduleTemp >= MIN_TEMP) { iSetTemp = EEPROM.read(iOffset + 4); //Overwrite value to give lower zones priority. } } } uiMainDrawSetTemp(); if (iSetTemp > 0) return true; else return false; } void pollTemp(){ float fOldTemp = 0.0; float fNewTemp = 0.0; fOldTemp = fCurrentTemp; fNewTemp = dsTemp.getTempCByIndex(0) - TEMP_ADJUST; fCurrentTemp = fNewTemp; if (floor(fNewTemp + 0.5) != floor(fOldTemp + 0.5)) uiMainDrawTemp(); } void calculateSlope(){ //Get our temp rise over 5 min fTempSlope = (float)(fCurrentTemp - fPrevTemp); fPrevTemp = fCurrentTemp; //Increment and add on to our graph data. for (int i = 0; i < HISTORY_LENGTH - 1; i++) { rTempHistory[i] = rTempHistory[i + 1]; } rTempHistory[HISTORY_LENGTH - 1] = (int)floor(fCurrentTemp + 0.5); //Determine if the temp is rising or falling over 15 minutes iTempRise = rTempHistory[HISTORY_LENGTH - 1] - rTempHistory[HISTORY_LENGTH - 4]; //Redraw the main screen uiMainDrawTrend(); uiMainDrawGraph(); } void adjustOffTime(float fTempDiff){ bAdjustedOff = true; lOffTimer = millis() + ((fTempDiff/fTempSlope) * TIMER_SLOPE); lOffTimer = lOffTimer - (fTempSlope/0.5*90*1000); //Allow for room equilization, 90 seconds for .5 degree/5 min rise. if (bElec) lOffTimer = lOffTimer - 45000; //Baseboard heat has an internal relay delay of 30-45 seconds. } /* ---------------------------------------------------------------------------------------- * Main UI: * * uiMain() --> * uiMainSetTemp(); * uiMainDrawTemp(); * uiMainDrawGraph(); * uiMainDrawTemp(); * uiMainDrawTrend(); * uiMainDrawSetTemp(); * uiMainDrawGraph(); * uiMainDrawMode(); * uiMainDrawGas(); * uiMainDrawElec(); * ---------------------------------------------------------------------------------------- */ boolean uiMain(){ GLCD.ClearScreen(); GLCD.DrawLine(6, 30, 61, 30); //Seperator //GLCD.DrawLine(GRAPH_X, GRAPH_Y - GRAPH_HEIGHT, GRAPH_X, GRAPH_Y); //Graph Y //GLCD.DrawLine(GRAPH_X, GRAPH_Y, GRAPH_X + GRAPH_LENGTH, GRAPH_Y); //Graph X uiMainDrawTemp(); uiMainDrawTrend(); uiMainDrawSetTemp(); uiMainDrawGraph(); uiMainDrawMode(); uiMainDrawGas(); uiMainDrawElec(); } void uiMainSetTemp() { GLCD.ClearScreen(); GLCD.SelectFont(lucida_Fixed21x42); unsigned long lTimeout = millis() + 2000; unsigned long irKey; int iNewTemp; while ((long)( millis() - lTimeout ) < 0){ iNewTemp = iSetTemp; if (irrecv.decode(&results)) { lTimeout = millis() + 2000; irKey = results.value; delay(100); irrecv.resume(); switch (irKey) { case IR_UP: if (iNewTemp < MAX_TEMP) iNewTemp++; break; case IR_DOWN: if (iNewTemp > MIN_TEMP) iNewTemp--; break; } } if (iNewTemp != iSetTemp) { GLCD.FillRect(42, 12, 42, 42, WHITE); GLCD.CursorToXY(42, 12); GLCD.print(iNewTemp); iSetTemp = iNewTemp; } } //end while } void uiMainViewTemp() { GLCD.ClearScreen(); GLCD.SelectFont(lucida_Fixed21x42); GLCD.FillRect(15, 12, 94, 42, WHITE); GLCD.CursorToXY(15, 12); GLCD.print(fCurrentTemp); unsigned long lTimeout = millis() + 2000; while ((long)( millis() - lTimeout ) < 0){ delay(100); } } void uiMainDrawTemp(){ GLCD.FillRect(6, 4, 38, 24, WHITE); GLCD.SelectFont(Verdana24); GLCD.CursorToXY(6, 4); GLCD.print((int)floor(fCurrentTemp + 0.5)); } void uiMainDrawTrend(){ if (iTempRise >= 1) GLCD.DrawBitmap(arrow_up, 46, 4); else if (iTempRise <= -1) GLCD.DrawBitmap(arrow_down, 46, 4); else GLCD.FillRect(46, 4, 16, 8, WHITE); } void uiMainDrawSetTemp(){ GLCD.FillRect(46, 14, 16, 15, WHITE); GLCD.SelectFont(fixednums7x15); GLCD.CursorToXY(46, 14); if (iSetTemp) GLCD.print(iSetTemp); } void uiMainDrawGraph(){ GLCD.FillRect(GRAPH_X, GRAPH_Y - GRAPH_HEIGHT, GRAPH_LENGTH, GRAPH_HEIGHT, WHITE); int iCurrPosX = 0; int iCurrPosY = 0; int iLastPosX = GRAPH_X; int iLastPosY = 0; for (int i = (HISTORY_LENGTH - GRAPH_LENGTH - iGraphOffset); i < (HISTORY_LENGTH - iGraphOffset); i++){ iCurrPosX = GRAPH_X + (i - (HISTORY_LENGTH - GRAPH_LENGTH - iGraphOffset)); iCurrPosY = GRAPH_Y - (int)floor(rTempHistory[i] + 0.5) + 5; if (iCurrPosY >= GRAPH_Y) iCurrPosY = GRAPH_Y - 1; else if (iCurrPosY < GRAPH_Y - GRAPH_HEIGHT) iCurrPosY = GRAPH_Y - GRAPH_HEIGHT; if (iLastPosY == 0) iLastPosY = iCurrPosY; GLCD.DrawLine(iLastPosX, iLastPosY, iCurrPosX, iCurrPosY); GLCD.DrawLine(iLastPosX, iLastPosY + 1, iCurrPosX, iCurrPosY + 1); iLastPosX = iCurrPosX; iLastPosY = iCurrPosY; } } void uiMainDrawMode(){ GLCD.SelectFont(Arial_bold_14); GLCD.FillRect(72, 10, 48, 14, WHITE); if (iMode == 4 || iMode == 0) { iMode = 0; GLCD.CursorToXY(82, 10); GLCD.print("Off"); }else if (iMode == 1) { iMode = 1; GLCD.CursorToXY(78, 10); GLCD.print("Auto"); }else if (iMode == 2) { GLCD.CursorToXY(72, 10); GLCD.print("Ad-hoc"); }else if (iMode == 3) { GLCD.CursorToXY(72, 10); GLCD.print("Manual"); } } void uiMainDrawGas(){ if (bGasLock == true) GLCD.DrawBitmap(fire_locked,62,31); else if (bGas == true) GLCD.DrawBitmap(fire,62,31); else GLCD.FillRect(62, 31, 32, 32, WHITE); } void uiMainDrawElec(){ if (bElecSave == true && bElec == true) GLCD.DrawBitmap(electricity_save,94,31); else if (bElec == true) GLCD.DrawBitmap(electricity,94,31); else GLCD.FillRect(94, 31, 32, 32, WHITE); } /* ---------------------------------------------------------------------------------------- * Settings Screens: * * uiSetTime() * uiSetCal(int iDay) -> Where iDay is the day of the week, 1 = Monday and so forth. * ---------------------------------------------------------------------------------------- */ boolean uiSetTime(){ //Input Grid // rows cols width height length padding int rGrid[] = { 2, 5, 16, 14, 10, 2 }; iDefaultX = 4; iDefaultY = 18; iCursorPos = 0; //Setup RTC.readClock(); unsigned long irKey; bCursorMovement = true; String s2Digit = ""; String sExclude = "1368"; int rTime[10] = {RTC.getHours(), -1, RTC.getMinutes(), -1, RTC.getSeconds(), RTC.getMonth(), -1, RTC.getDate(), -1, RTC.getYear()}; GLCD.ClearScreen(); drawTitle("Date/Time Setup"); for (int i = 0; i < rGrid[LENGTH]; i++) if (rTime[i] == -1 && i < 5) drawString(":", fixednums8x16, i, rGrid, BLACK); else if (rTime[i] == -1 && i > 5) drawString("/", fixednums8x16, i, rGrid, BLACK); for (int i = 0; i < rGrid[LENGTH]; i++) if (rTime[i] > -1) drawString(rTime[i], fixednums8x16, i, rGrid, BLACK); while(1) { //Await user interaction irKey = awaitUserInput(rGrid, sExclude); //Exit to homescreen or to next option screen. if(irKey == IR_EXIT){ return false; }else if(irKey == IR_MENU){ return true; } //Digit input if (irKey >= 0 && irKey <= 9 && sExclude.indexOf(String(iCursorPos)) == -1) { if (bCursorMovement) { s2Digit = String(irKey); bCursorMovement = false; }else s2Digit = String(s2Digit + irKey); drawString(s2Digit, fixednums8x16, iCursorPos, rGrid, BLACK); rTime[iCursorPos] = stringToInt(s2Digit); if (s2Digit.length() == 2) { bCursorMovement = true; if ( (stringToInt(s2Digit) > 59 && (iCursorPos == 2 || iCursorPos == 4) ) || (stringToInt(s2Digit) > 23 && (iCursorPos == 0) ) || (stringToInt(s2Digit) > 12 && (iCursorPos == 5) ) || (stringToInt(s2Digit) > 31 && (iCursorPos == 7) ) ) { delay(500); drawString("", fixednums8x16, iCursorPos, rGrid, BLACK); rTime[iCursorPos] = 0; }else moveCursor(1, 0, rGrid, sExclude); s2Digit = ""; } } if (irKey == IR_SAVE) { RTC.switchTo24h(); RTC.start(); //Serial.print("Hours: "); //Serial.print(rTime[0]); RTC.setHours(rTime[0]); //Serial.print(", Minutes: "); //Serial.print(rTime[2]); RTC.setMinutes(rTime[2]); //Serial.print(", Seconds: "); //Serial.print(rTime[4]); RTC.setSeconds(rTime[4]); //Serial.print(", Month: "); //Serial.print(rTime[5]); RTC.setMonth(rTime[5]); //Serial.print(", Day: "); //Serial.print(rTime[7]); RTC.setDate(rTime[7]); //Serial.print(", Year: "); //Serial.print(rTime[9]); RTC.setYear(rTime[9]); //Serial.print(", DoW: "); //Serial.print(getDayOfWeek(rTime[7], rTime[5], rTime[9])); RTC.setDayOfWeek(getDayOfWeek(rTime[7], rTime[5], rTime[9])); RTC.setClock(); //Serial.println("."); drawSaved(); return false; } } } boolean uiSetCal(int iDay) { //Input Grid // rows cols width height length padding int rGrid[] = { 2, 5, 16, 14, 10, 2 }; iDefaultX = 4; iDefaultY = 18; iCursorPos = 0; //Setup GLCD.ClearScreen(); drawTitle(rDaysOfTheWeek[iDay]); unsigned long irKey; bCursorMovement = true; String s2Digit = ""; String sExclude = ""; int rSchedule[10]; int iOffset = 0; for (int i = iDay * SCHEDULE_ZONES * 5; i < (iDay + 1) * SCHEDULE_ZONES * 5; i++) { iOffset = i - (iDay * SCHEDULE_ZONES * 5); rSchedule[iOffset] = EEPROM.read(i); if (iOffset == 1 || iOffset == 3 || iOffset == 6 || iOffset == 8 ){ drawString(getAMorPM(rSchedule[iOffset]), SystemFont5x7, iOffset, rGrid, BLACK); }else{ drawString(rSchedule[iOffset], fixednums8x16, iOffset, rGrid, BLACK); } } while(1) { //Await user interaction irKey = awaitUserInput(rGrid, sExclude); //Exit to homescreen or to next option screen. if(irKey == IR_EXIT){ return false; }else if(irKey == IR_MENU){ return true; } if (iCursorPos == 1 || iCursorPos == 3 || iCursorPos == 6 || iCursorPos == 8){ if (irKey == 1){ drawString("am", SystemFont5x7, iCursorPos, rGrid, BLACK); rSchedule[iCursorPos] = 0; moveCursor(1, 0, rGrid, sExclude); } if (irKey == 2){ drawString("pm", SystemFont5x7, iCursorPos, rGrid, BLACK); rSchedule[iCursorPos] = 12; moveCursor(1, 0, rGrid, sExclude); } bCursorMovement = true; }else if (irKey >= 0 && irKey <= 9) { //if (bCursorMovement) //Serial.println("Cursor Movement"); else //Serial.println("No Cursor Movement"); if (bCursorMovement) { s2Digit = String(irKey); bCursorMovement = false; }else s2Digit = String(s2Digit + irKey); drawString(s2Digit, fixednums8x16, iCursorPos, rGrid, BLACK); rSchedule[iCursorPos] = stringToInt(s2Digit); if (s2Digit.length() == 2) { bCursorMovement = true; if ( ((stringToInt(s2Digit) > 12 || stringToInt(s2Digit) < 1) && (iCursorPos == 0 || iCursorPos == 2 || iCursorPos == 5 || iCursorPos == 7) ) || (((stringToInt(s2Digit) > MAX_TEMP || stringToInt(s2Digit) < MIN_TEMP) && stringToInt(s2Digit) != 0) && (iCursorPos == 4 || iCursorPos == 9) ) ) { delay(500); drawString("", fixednums8x16, iCursorPos, rGrid, BLACK); rSchedule[iCursorPos] = 0; }else moveCursor(1, 0, rGrid, sExclude); s2Digit = ""; } } if (irKey == IR_SAVE) { //Serial.print("Schedule for: "); //Serial.print(iDay); //Serial.print(", Start 1: "); //Serial.print(rSchedule[0]); //Serial.print(getAMorPM(rSchedule[1])); //Serial.print(", Stop 1:: "); //Serial.print(rSchedule[2]); //Serial.print(getAMorPM(rSchedule[3])); //Serial.print(", Temp 1: "); //Serial.print(rSchedule[4]); //Serial.print(", Start 2: "); //Serial.print(rSchedule[5]); //Serial.print(getAMorPM(rSchedule[6])); //Serial.print(", Stop 2: "); //Serial.print(rSchedule[7]); //Serial.print(getAMorPM(rSchedule[8])); //Serial.print(", Temp 2: "); //Serial.print(rSchedule[9]); //Serial.println("."); for (int i = iDay * SCHEDULE_ZONES * 5; i < (iDay + 1) * SCHEDULE_ZONES * 5; i++) EEPROM.write(i, rSchedule[i - (iDay * SCHEDULE_ZONES * 5)]); drawSaved(); return false; } } } /* ---------------------------------------------------------------------------------------- * awaitUserInput() for Settings Screens * * Responsible for updating the cursor position. When a non directional key is pressed, * the function returns the action. On timeout, the fuction returns the exit key. * ---------------------------------------------------------------------------------------- */ unsigned long awaitUserInput(int rGrid[], String sExclude){ unsigned long irKey; unsigned long lTimeout = millis() + TIMER_NOINPUT; irrecv.resume(); while ((long)( millis() - lTimeout ) < 0){ if (irrecv.decode(&results)) { lTimeout += TIMER_NOINPUT; irKey = results.value; irrecv.resume(); //Serial.println(irKey, HEX); switch (irKey) { case IR_LEFT: moveCursor(-1, 0, rGrid, sExclude); break; case IR_RIGHT: moveCursor(1, 0, rGrid, sExclude); break; case IR_UP: moveCursor(0, -1, rGrid, sExclude); break; case IR_DOWN: moveCursor(0, 1, rGrid, sExclude); break; case IR_NUM0: return 0; case IR_NUM1: return 1; case IR_NUM2: return 2; case IR_NUM3: return 3; case IR_NUM4: return 4; case IR_NUM5: return 5; case IR_NUM6: return 6; case IR_NUM7: return 7; case IR_NUM8: return 8; case IR_NUM9: return 9; default: return irKey; //Any other key is returned for handling. } } } return IR_EXIT; //Timeout the same as Exit Key } /* ---------------------------------------------------------------------------------------- * Cursor and text drawing functions for the settings screens: * * drawCursor() * drawString() * moveCursor() * drawSaved() * ---------------------------------------------------------------------------------------- */ void drawCursor(int rGrid[], uint8_t bColour){ int iFloor = floor(iCursorPos / rGrid[COLS]); int iCursorPosX = iCursorPos - (iFloor * rGrid[COLS]); int iCursorPosY = iFloor; int iTotalWidth = rGrid[WIDTH] + (rGrid[PADDING] * 2) + 4; int iTotalHeight = rGrid[HEIGHT] + (rGrid[PADDING] * 2) + 4; // (x, y, width, height, radius, color) GLCD.DrawRoundRect((iCursorPosX * iTotalWidth) + rGrid[PADDING] + iDefaultX, (iCursorPosY * iTotalHeight) + rGrid[PADDING] + iDefaultY, iTotalWidth - (rGrid[PADDING] * 2), iTotalHeight - (rGrid[PADDING] * 2) - 1, 2, bColour); } void drawString(String sString, uint8_t* iFont, int iPos, int rGrid[], uint8_t bColour){ int iFloor = floor(iPos / rGrid[COLS]); int iCursorPosX = iPos - (iFloor * rGrid[COLS]); int iCursorPosY = iFloor; int iTotalWidth = rGrid[WIDTH] + (rGrid[PADDING] * 2) + 4; int iTotalHeight = rGrid[HEIGHT] + (rGrid[PADDING] * 2) + 4; GLCD.FillRect((iCursorPosX * iTotalWidth) + rGrid[PADDING] + 2 + iDefaultX, (iCursorPosY * iTotalHeight) + rGrid[PADDING] + 2 + iDefaultY, rGrid[WIDTH], rGrid[HEIGHT], WHITE); GLCD.CursorToXY((iCursorPosX * iTotalWidth) + rGrid[PADDING] + 2 + iDefaultX, (iCursorPosY * iTotalHeight) + rGrid[PADDING] + 2 + iDefaultY); GLCD.SelectFont(iFont); GLCD.print(sString); drawCursor(rGrid, BLACK); } void moveCursor(int x, int y, int rGrid[], String sExclude){ if (!bCursorMovement) return; //Erase where we were - Note for this to work the cursor cannot overlap any pixels already drawn on the screen. drawCursor(rGrid, WHITE); iCursorPos += x + (y * rGrid[COLS]); if (iCursorPos < 0) { if (x < 0){ iCursorPos = rGrid[LENGTH]; }else{ iCursorPos = (rGrid[ROWS] * rGrid[COLS]) + iCursorPos; } } if (iCursorPos >= rGrid[LENGTH]) { if (x > 0){ iCursorPos = 0; }else if (y < 0){ iCursorPos = iCursorPos - rGrid[COLS]; }else{ iCursorPos = iCursorPos - (floor(iCursorPos / rGrid[COLS]) * rGrid[COLS]); } } //Skip excluded locations by calling moveCursor recursively and adding 1 each time if (sExclude.indexOf(String(iCursorPos)) == -1){ //Draw where we are. drawCursor(rGrid, BLACK); }else{ moveCursor(x, y, rGrid, sExclude); } } void drawTitle(char sTitle[]){ GLCD.SelectFont(Arial_bold_14); GLCD.CursorToXY(6, 3); GLCD.print(sTitle); GLCD.DrawLine(6, 16, 121, 16); GLCD.DrawLine(6, 17, 121, 17); } void drawSaved(){ GLCD.FillRect(43, 24, 44, 15, WHITE); GLCD.SelectFont(Arial_bold_14); GLCD.CursorToXY(46, 26); GLCD.print("Saved"); GLCD.DrawRoundRect(43, 24, 44, 15, 5); delay(2000); } /* ---------------------------------------------------------------------------------------- * Utility Functions * ---------------------------------------------------------------------------------------- */ int stringToInt(String input){ char convert[input.length() + 1]; input.toCharArray(convert, sizeof(convert)); int output = atoi(convert); return output; } int convert12to24(int i12t, int iAmPm){ if (i12t == 12) return iAmPm; if (iAmPm == 12) return i12t+12; else return i12t; } String getAMorPM(int iAmPm){ if (iAmPm == 0) return "am"; else return "pm"; } int getDayOfWeek(int day, int mth, int yr) { int val; const int table[12] = {6,2,2,5,0,3,5,1,4,6,2,4}; val = yr + yr / 4; //leap year adj good 2007 to 2099 val = val + table[mth - 1]; //table contains modulo 7 adjustments for mths val = val + day; if ((yr % 4 == 0) && (mth < 3)) val = val - 1; // adjust jan and feb down one for leap year val = val % 7; if (val == 0) val = 7; // val is now the day of week 1=Mon... 7=Sun return(val); } void irDump(decode_results *results) { int count = results->rawlen; if (results->decode_type == UNKNOWN) { Serial.print("Unknown encoding: "); } else if (results->decode_type == NEC) { Serial.print("Decoded NEC: "); } else if (results->decode_type == SONY) { Serial.print("Decoded SONY: "); } else if (results->decode_type == RC5) { Serial.print("Decoded RC5: "); } else if (results->decode_type == RC6) { Serial.print("Decoded RC6: "); } Serial.print(results->value, HEX); Serial.print(" ("); Serial.print(results->bits, DEC); Serial.println(" bits)"); Serial.print("Raw ("); Serial.print(count, DEC); Serial.print("): "); for (int i = 0; i < count; i++) { if ((i % 2) == 1) { Serial.print(results->rawbuf[i]*USECPERTICK, DEC); } else { Serial.print(-(int)results->rawbuf[i]*USECPERTICK, DEC); } Serial.print(" "); } Serial.println(""); }