/* ----------------------------------------------------------------------------------------
* 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"
/* ----------------------------------------------------------------------------------------
* Constants
* ----------------------------------------------------------------------------------------
*/
#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 7 // 7 Days of the week. Note: This is also necessary to blank the EEPROM, uncomment in setup().
#define SCHEDULE_ZONES 2 // 2 Schedules/Zones in each day.
#define MIN_TEMP 12
#define MAX_TEMP 30
#define TEMP_ADJUST -4.64 // The Dallas DS18B20 picks up some heat from the enclosure. Faux calibration.
#define BUFFER_UBOUND 0.35 // Allow temperature to rise .35 degrees above the set temperature.
#define BUFFER_LBOUND 0.40 // Allow temperature to fall .40 defress below the set temperature.
#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 of temperature history.
#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 // Re-use of IR_SAVE
#define IR_PAUSE 0x2FD00FF // Re-use of IR_NUM0
#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
/* ----------------------------------------------------------------------------------------
* Global Variables
* ----------------------------------------------------------------------------------------
*/
//Pin Assignments
int pinLDR = A1; // Light Dependent Resistor (Analog)
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";
//Operating Variables
boolean bDebug = false;
boolean bHeatCycle = false;
boolean bElec = true;
boolean bElecSave = true;
boolean bGas = true;
boolean bGasLock = false;
boolean bAdjustedOff = false;
boolean bTempReqRec = false;
boolean bSettingChanged = false;
boolean bPaused = false;
int iMode = 0;
int iTempRise = 0;
float fTempSlope = 0.0;
float fCurrentTemp = 0.0;
float fPrevTemp = 0.0;
int iSetTemp = 0;
int iCurrentOffset = 0;
int iOverrideOffset = -1;
long iGraphOffset = 0;
int iPwmVal = 0;
byte rTempHistory[HISTORY_LENGTH];
byte rPauseTime[2];
//Timer Setup
static unsigned long lWaitSlope = 0;
static unsigned long lWaitTemp = 0;
static unsigned long lOffTimer = 0;
static unsigned long lWaitSetting = 0;
/* ----------------------------------------------------------------------------------------
* Library Setup
* ----------------------------------------------------------------------------------------
*/
IRrecv irrecv(pinIR);
decode_results results;
OneWire ds(pinTemp);
DallasTemperature dsTemp(&ds);
/* ----------------------------------------------------------------------------------------
* Board Setup - setup()
* ----------------------------------------------------------------------------------------
*/
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();
if (bDebug) Serial.begin(9600);
pinMode(pinRelayElec, OUTPUT);
digitalWrite(pinRelayElec, HIGH); //Pin LOW engages (closes) relay (Normally Open). HIGH = OFF.
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 and be responsive, calling temperature polling
* and display updates at intervals defined above.
* ----------------------------------------------------------------------------------------
*/
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));
if (bDebug) 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(bHeatCycle = 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){
if (bDebug) 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++;
iOverrideOffset = -1;
bPaused = false;
uiMainDrawMode();
if (iMode == 2) iSetTemp = 18;
checkScheduleTime();
break;
case IR_UP:
case IR_DOWN: if (checkScheduleTime()) {
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();
return;
break;
case IR_PAUSE: if (iMode > 0) {
if (uiModalPauseUntil()) bPaused = true; else bPaused = false;
uiMain();
checkScheduleTime();
} break;
case IR_MENU:
while(1) {
if (!uiSetCal(0)) break;
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 (!uiSetTime()) break;
}
uiMain();
break;
} //end switch
lWaitSetting = millis() + TIMER_SETTINGS;
bSettingChanged = true;
}
/* ----------------------------------------------------------------------------------------
* Heater Control
*
* determineHeatState()
* controlHeat()
* ----------------------------------------------------------------------------------------
*/
void determineHeatState(){
if (!checkScheduleTime()){
controlHeat(bHeatCycle = false);
return;
}
float fLowTempDiff = iSetTemp - BUFFER_LBOUND - fCurrentTemp;
float fHighTempDiff = iSetTemp + BUFFER_UBOUND - fCurrentTemp;
if (fLowTempDiff >= 0 && bHeatCycle == false) {
//Turn the heat on
bHeatCycle = true;
}else if (fHighTempDiff <= 0 && bHeatCycle == true) {
//Turn the heat off
bHeatCycle = false;
}else if (fHighTempDiff > 0 && fTempSlope > 0.05 && bHeatCycle == true) {
//Adjust the off time to prevent over-shoot
adjustOffTime(fHighTempDiff);
}
controlHeat(bHeatCycle);
}
void controlHeat(boolean bOnOff){
if (bOnOff == 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) {
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(){
RTC.readClock();
int iScheduleStart, iScheduleEnd, iScheduleTemp, iOffset;
int iCurrDay = RTC.getDayOfWeek() - 1;
int iHours = RTC.getHours();
if (bDebug) RTC.getFormatted(formatted);
if (bDebug) Serial.println(formatted);
if (bDebug) if (RTC.isStopped()) Serial.println("Clock Stopped"); else Serial.println("Clock Running");
if (bDebug) Serial.print("Current Day: ");
if (bDebug) Serial.print(iCurrDay);
if (bDebug) Serial.print(", Current Hour: ");
if (bDebug) Serial.println(iHours);
if (bPaused == true) {
if (iHours < rPauseTime[1] || (rPauseTime[1] < rPauseTime[0] && (iHours < rPauseTime[1] || iHours >= rPauseTime[0]))) {
uiMainDrawSetTemp();
return false;
}else{
bPaused = false;
uiMainDrawMode();
}
}
if (iMode == 0) {
iSetTemp = 0;
}
if (iMode == 1) {
int iNewSetTemp = 0;
int iPrevDay;
if (iCurrDay == 0) iPrevDay = 6; else iPrevDay = iCurrDay - 1;
iCurrentOffset = 0;
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);
if (bDebug) Serial.print("Start: ");
if (bDebug) Serial.print(iScheduleStart);
if (bDebug) Serial.print(", End: ");
if (bDebug) Serial.print(iScheduleEnd);
if (bDebug) Serial.print(", Current: ");
if (bDebug) Serial.println(iHours);
if (iScheduleEnd < iScheduleStart && iHours < iScheduleEnd && iScheduleTemp >= MIN_TEMP) {
iCurrentOffset = iOffset + 4;
iNewSetTemp = EEPROM.read(iCurrentOffset);
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);
if (bDebug) Serial.print("Start: ");
if (bDebug) Serial.print(iScheduleStart);
if (bDebug) Serial.print(", End: ");
if (bDebug) Serial.print(iScheduleEnd);
if (bDebug) Serial.print(", Current: ");
if (bDebug) Serial.println(iHours);
if (iScheduleEnd < iScheduleStart) iScheduleEnd = 24;
if (iHours >= iScheduleStart && iHours < iScheduleEnd && iScheduleTemp >= MIN_TEMP) {
iCurrentOffset = iOffset + 4;
iNewSetTemp = EEPROM.read(iCurrentOffset);
//Overwrite value to give lower zones priority.
}
}
if (iCurrentOffset != iOverrideOffset) {
iSetTemp = iNewSetTemp;
iOverrideOffset = -1;
}
}
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
if (fTempSlope > 0.25) iTempRise = 1; else if (fTempSlope < -0.20) iTempRise = -1; else iTempRise = 0;
//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
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;
if (iMode == 1) iOverrideOffset = iCurrentOffset;
}
} //end while
}
void uiMainViewTemp() {
GLCD.ClearScreen();
GLCD.SelectFont(lucida_Fixed21x42);
GLCD.FillRect(15, 12, 94, 42, WHITE);
GLCD.CursorToXY(15, 12);
GLCD.print(fCurrentTemp);
delay(2000);
}
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 && !bPaused) 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 (bPaused) {
GLCD.CursorToXY(72, 10);
GLCD.print("Paused");
}else if (iMode == 3 || 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("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:
*
* uiModalPauseUntil()
* uiSetTime()
* uiSetCal(int iDay) -> Where iDay is the day of the week, 1 = Monday and so forth.
* ----------------------------------------------------------------------------------------
*/
boolean uiModalPauseUntil() {
drawModal(90);
iDefaultX = DISPLAY_WIDTH/2-90/2 + 6;
iDefaultY = DISPLAY_HEIGHT/2-ceil(90/2/2) + 4;
GLCD.CursorToXY(iDefaultX, iDefaultY);
GLCD.SelectFont(Arial_bold_14);
GLCD.print("Pause Until");
iDefaultY = iDefaultY + 16;
//Input Grid
// rows cols width height length padding
int rGrid[] = { 1, 2, 16, 14, 2, 2 };
iCursorPos = 0;
unsigned long irKey;
String s2Digit = "";
int iEnd = 0;
byte iAmPm = 0;
RTC.readClock();
if (bPaused) {
iEnd = rPauseTime[1];
}else{
iEnd = RTC.getHours() + 2;
if (iEnd >= 24) iEnd = iEnd - 24;
}
if (iEnd >= 12) {
iAmPm = 12;
if (iEnd > 12) iEnd = iEnd - 12;
drawString("pm", SystemFont5x7, 1, rGrid, BLACK);
}else{
iAmPm = 0;
if (iEnd == 0) iEnd = 12;
drawString("am", SystemFont5x7, 1, rGrid, BLACK);
}
drawString(iEnd, fixednums8x16, 0, rGrid, BLACK);
while (1){
irKey = awaitUserInput(rGrid, "");
if (iCursorPos == 1){
if (irKey == 1){
drawString("am", SystemFont5x7, iCursorPos, rGrid, BLACK);
iAmPm = 0;
moveCursor(1, 0, rGrid, "");
}
if (irKey == 2){
drawString("pm", SystemFont5x7, iCursorPos, rGrid, BLACK);
iAmPm = 12;
moveCursor(1, 0, rGrid, "");
}
bCursorMovement = true;
}else if (irKey >= 0 && irKey <= 9) {
if (bCursorMovement) {
s2Digit = String(irKey);
bCursorMovement = false;
}else s2Digit = String(s2Digit + irKey);
drawString(s2Digit, fixednums8x16, iCursorPos, rGrid, BLACK);
iEnd = stringToInt(s2Digit);
if (s2Digit.length() == 2) {
bCursorMovement = true;
if ( (stringToInt(s2Digit) > 12 || stringToInt(s2Digit) < 1) && iCursorPos == 0 ) {
delay(500);
drawString("", fixednums8x16, iCursorPos, rGrid, BLACK);
iEnd = 0;
}else moveCursor(1, 0, rGrid, "");
s2Digit = "";
}
}
if (irKey == IR_SAVE) {
rPauseTime[0] = RTC.getHours();
rPauseTime[1] = convert12to24(iEnd, iAmPm);
return true;
}
if (irKey == IR_EXIT) {
return false;
}
}
}
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();
if (bDebug) Serial.print("Hours: ");
if (bDebug) Serial.print(rTime[0]);
RTC.setHours(rTime[0]);
if (bDebug) Serial.print(", Minutes: ");
if (bDebug) Serial.print(rTime[2]);
RTC.setMinutes(rTime[2]);
if (bDebug) Serial.print(", Seconds: ");
if (bDebug) Serial.print(rTime[4]);
RTC.setSeconds(rTime[4]);
if (bDebug) Serial.print(", Month: ");
if (bDebug) Serial.print(rTime[5]);
RTC.setMonth(rTime[5]);
if (bDebug) Serial.print(", Day: ");
if (bDebug) Serial.print(rTime[7]);
RTC.setDate(rTime[7]);
if (bDebug) Serial.print(", Year: ");
if (bDebug) Serial.print(rTime[9]);
RTC.setYear(rTime[9]);
if (bDebug) Serial.print(", DoW: ");
if (bDebug) Serial.print(getDayOfWeek(rTime[7], rTime[5], rTime[9]));
RTC.setDayOfWeek(getDayOfWeek(rTime[7], rTime[5], rTime[9]));
RTC.setClock();
if (bDebug) Serial.println(".");
drawMessage("Saved");
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(getDayOfWeek(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 (bDebug) 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) {
if (bDebug) Serial.print("Schedule for: ");
if (bDebug) Serial.print(getDayOfWeek(iDay));
if (bDebug) Serial.print(", Start 1: ");
if (bDebug) Serial.print(rSchedule[0]);
if (bDebug) Serial.print(getAMorPM(rSchedule[1]));
if (bDebug) Serial.print(", Stop 1:: ");
if (bDebug) Serial.print(rSchedule[2]);
if (bDebug) Serial.print(getAMorPM(rSchedule[3]));
if (bDebug) Serial.print(", Temp 1: ");
if (bDebug) Serial.print(rSchedule[4]);
if (bDebug) Serial.print(", Start 2: ");
if (bDebug) Serial.print(rSchedule[5]);
if (bDebug) Serial.print(getAMorPM(rSchedule[6]));
if (bDebug) Serial.print(", Stop 2: ");
if (bDebug) Serial.print(rSchedule[7]);
if (bDebug) Serial.print(getAMorPM(rSchedule[8]));
if (bDebug) Serial.print(", Temp 2: ");
if (bDebug) Serial.print(rSchedule[9]);
if (bDebug) 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)]);
drawMessage("Saved");
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();
if (bDebug) 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()
* drawMessage()
* drawModal()
* ----------------------------------------------------------------------------------------
*/
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(String 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 drawMessage(String sMessage){
byte iWidth = sMessage.length() * 8 + 4;
drawModal(iWidth);
GLCD.CursorToXY(DISPLAY_WIDTH/2-iWidth/2 + 3, DISPLAY_HEIGHT/2-ceil(iWidth/2/2) + 5);
GLCD.SelectFont(Arial_bold_14);
GLCD.print(sMessage);
delay(2000);
}
void drawModal(int iWidth){
int i = iWidth;
GLCD.DrawRect(DISPLAY_WIDTH/2-i/2, DISPLAY_HEIGHT/2-ceil(i/2/2), i, i/2);
GLCD.FillRect((DISPLAY_WIDTH/2-i/2) + 1, (DISPLAY_HEIGHT/2-ceil(i/2/2)) + 1, i - 2, (i/2) - 2, WHITE);
}
/* ----------------------------------------------------------------------------------------
* Utility Functions
* ----------------------------------------------------------------------------------------
*/
String getDayOfWeek(int iDay){
switch (iDay) {
case 0: return "Monday";
case 1: return "Tuesday";
case 2: return "Wednesday";
case 3: return "Thursday";
case 4: return "Friday";
case 5: return "Saturday";
case 6: return "Sunday";
}
}
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("");
}