When it comes to smart home thermostat - there is no shortage of them on the market. But if you build an MySensors-based smart home, it's more natural to use the same technology for controlling heating and cooling of the house. And also, I found all the thermostats on the market although rich with the UI, but quite limited when it comes to the fine tuning or using multiple sensors. With open code and integration with open source home automation software (I use OpenHAB) your capabilities are virtually unlimited!
So, MySensors-based thermostat:
Typical HVAC system has rather simplistic wiring. Common wire, 24V power, Call-for-AC wire, and Call-for-Heat wire. When you connect Call-for-AC wire to 24V, HVAC will be cooling; and when you connect Call-for-Heat wire 24V, HVAC will be heating. What can be simpler? You need 2 relays to control this: the first connects 24V to Cooling wire and the second switches it from Cooling to Heating wire. To independently control the status of the relays by Arduino the voltage measurement circuit was added (voltage divider to analog Arduino pins); and also color LEDs were added to it to visually indicate the status independently from the display. To minimize the footprint of the device, bare bone relays were used instead of relay modules. This tutorial explains the circuit: https://create.arduino.cc/projecthub/tarantula3/driving-a-relay-with-an-arduino-722c24
As the UI for the thermostat the Adafruit 2.8" TFT with capacitive touch screen was used. Adafruit has it also with the resistive touch screen if you prefer press the screen instead of touch :) and then you can choose between 2.8" and 3.5", but I really like how easy it is to control UI with the capacitive screen.
The annoying part is that due to some bug (or feature) in CH340G chip, if you connect Mega to PC through on-board USB port, the TFT display would not initialize. That's why a serial interface to Arduino was wired in the circuit to connect to PC via FT232RL serial adapter (like this one: https://www.amazon.com/gp/product/B01HXT8DZ4)
Overall schematic looks like this:
The PCB made with SMD components to keep the footprint small:
You would need to solder SMD components first: (TIP: check the device operation while you are soldering. Faulty components might be difficult to replace after the screen is soldered)
After all components are soldered, you can solder the screen:
For the case I chose transparent white PETG:
And, this is assembled device:
The sketch is in attached files.
// Timer3 and Timer5 are only available on Arduino Mega boards. These 2 timers are all 16-bit timers.
#define USE_TIMER_3 true
#define USE_TIMER_5 true
#include <TimerInterrupt.hpp>
#include <TimerInterrupt.h>
#include <ISR_Timer.hpp>
#include <ISR_Timer.h>
#include <DHT_U.h>
#include <DHT.h>
#define FT6206_DEBUG
#include <Adafruit_FT6206.h>
#include <SPI.h>
#include <gfxfont.h>
#include <Adafruit_SPITFT_Macros.h>
#include <Adafruit_SPITFT.h>
#include <Adafruit_GrayOLED.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>
//mysensors settings
#define MY_NODE_ID 48
#define MY_DEBUG
#define MY_RADIO_RF24
#define MY_REPEATER_FEATURE
#define MY_PARENT_NODE_ID 0
#define MY_SOFTSPI
#define MY_RF24_CE_PIN 22 //D22
#define MY_RF24_CS_PIN 24 //D24
#define MY_SOFT_SPI_SCK_PIN 26 //D26
#define MY_SOFT_SPI_MISO_PIN 30 //D30
#define MY_SOFT_SPI_MOSI_PIN 28 //D28
#include <MySensors.h>
// For the Adafruit shield, these are the default.
//#define TFT_DC 9
//#define TFT_CS 10
// These are for Mega PRO
#define TFT_DC 49
#define TFT_CS 53
// Use hardware SPI (on Uno, #13, #12, #11) and the above for CS/DC
Adafruit_ILI9341 _tft = Adafruit_ILI9341(TFT_CS, TFT_DC);
Adafruit_FT6206 _ts = Adafruit_FT6206();
// When adjusting to new screen size it's usefull to draw lines separating different regions
// define VISUAL_DIVIDERS to draw these lines
//#define VISUAL_DIVIDERS
#define SCREEN_WIDTH 320
#define SCREEN_HEIGHT 240
//My definitions
#define BACKGROUND_COLOR 0x4398
#define FONT_COLOR 0xDEDB
// TODO: implement clock (not implemented for now, shows just hardcoded time)
// Options:
// 1) get time from controller periodically
// 2) add RTC device
char _timeOfDay[9] = "01:28 am"; // 9th char is the '\0' (end of string)
byte _targetTemperature = 69;
char _targetTemperatureStr[] = "69";
float _indoorTemperature = 68.5;
char _indoorTemperatureBuf[6];
float _indoorHumidity = 100;
char _indoorHumidityBuf[6];
float _outdoorTemperatureC = 20;
float _outdoorTemperatureF = 20;
char _outdoorTemperatureCBuf[6];
char _outdoorTemperatureFBuf[6];
float _outdoorHumidity = 100;
char _outdoorHumidityBuf[6];
float _voltageAC = 0;
float _voltageHeat = 0;
#define DHTPIN 46 // Digital pin connected to the DHT sensor
#define DHTTYPE DHT22 // DHT 22 (AM2302), AM2321
DHT dht(DHTPIN, DHTTYPE);
// Current mode of Thermostat:
// AUTO - can call for cool or heat depending on the current temperature and target temperature
// AC or HEAT - can only call for cool or heat. For example, if mode is AC and temperature is below target it will switch off, it will not call for heat.
enum HvacMode
{
HVAC_OFF = 0,
HVAC_AUTO = 1,
HVAC_AC = 2,
HVAC_HEAT = 3
};
HvacMode _mode = HvacMode::HVAC_OFF;
// Current state of the device. Really, it aither off or calling for cool or for heat. ON is a dummy value, thermostat never gets to this mode.
enum HvacState
{
OFF = 0,
ON = 1,
COOLING = 2,
HEATING = 3
};
HvacState _state = HvacState::OFF;
char* _HvacStateStr[] = { "OFF", "ON", "COOLING", "HEATING"};
volatile bool bReadDTH = false;
void TimerHandler_readDTH()
{
bReadDTH = true;
Serial.println("DTH Timer, millis() = " + String(millis()));
}
#define RELAY_ONOFF_PIN 31
#define RELAY_HOTCOLD_PIN 29
#define VOLTAGE_AC_PIN A14
#define VOLTAGE_HEAT_PIN A12
#define LCD_LIGHT_PIN 17
//Specify the ID of every sensor. This will help you identifying the MQTT messages.
#define CHILD_ID_TEMP 0 // ID of the temperature sensor
#define CHILD_ID_HUM 1 // ID of the Humidity Sensor
#define CHILD_ID_TARGET_TEMP 2 // ID of the variable holding Set Temperature
#define CHILD_ID_BTN_ONOFF 3 // ID of the ON/OFF Button (0 - OFF, 1 - ON)
#define CHILD_ID_BTN_AUTO 4 // ID of the AUTO Button (0 - Auto is OFF, 1 - Auto is ON)
#define CHILD_ID_BTN_AC 5 // ID of the AC Button (0 - AC is OFF, 1 - AC is ON)
#define CHILD_ID_BTN_HEAT 6 // ID of the HEAT Button (0 - Heat is OFF, 1 - Heat is ON)
#define CHILD_ID_VOLTAGE_AC 7 // ID of the AC line voltage sensor
#define CHILD_ID_VOLTAGE_HEAT 8 // ID of the Heat line voltage sensor
#define CHILD_ID_HVAC_STATE 9 // ID of the current state of the equipment (HvacState enum)
#define CHILD_ID_TEMP_OUTDOOR 10 // ID of the receiver of outdoor temperature (in C)
#define CHILD_ID_HUM_OUTDOOR 11 // ID of the receiver of outdoor humidity
// node-id child-sensor-id command ack type payload
// /48 /x /1 /0 /x
void presentation()
{
present(CHILD_ID_TEMP, S_TEMP); // /48/0/1/0/0
present(CHILD_ID_HUM, S_HUM); // /48/1/1/0/1
present(CHILD_ID_TARGET_TEMP, S_TEMP); // /48/2/1/0/0
present(CHILD_ID_BTN_ONOFF, S_BINARY); // /48/3/1/0/2
present(CHILD_ID_BTN_AUTO, S_BINARY); // /48/4/1/0/2
present(CHILD_ID_BTN_AC, S_BINARY); // /48/5/1/0/2
present(CHILD_ID_BTN_HEAT, S_BINARY); // /48/6/1/0/2
present(CHILD_ID_VOLTAGE_AC, S_MULTIMETER); // /48/7/1/0/38
present(CHILD_ID_VOLTAGE_HEAT, S_MULTIMETER); // /48/8/1/0/38
present(CHILD_ID_HVAC_STATE, S_CUSTOM); // /48/9/1/0/48
present(CHILD_ID_TEMP_OUTDOOR, S_TEMP); // /48/10/1/0/0
present(CHILD_ID_HUM_OUTDOOR, S_HUM); // /48/11/1/0/1
}
MyMessage msgTemp(CHILD_ID_TEMP, V_TEMP); // V_TEMP = 0
MyMessage msgHum(CHILD_ID_HUM, V_HUM); // V_HUM = 1
MyMessage msgTargetTemp(CHILD_ID_TARGET_TEMP, V_TEMP);
MyMessage msgBtnOnOff(CHILD_ID_BTN_ONOFF, V_STATUS); // (0 - OFF, 1 - ON)
MyMessage msgBtnAuto(CHILD_ID_BTN_AUTO, V_STATUS); // (0 - Auto is OFF, 1 - Auto is ON)
MyMessage msgBtnAC(CHILD_ID_BTN_AC, V_STATUS); // (0 - AC is OFF, 1 - AC is ON)
MyMessage msgBtnHeat(CHILD_ID_BTN_HEAT, V_STATUS); // (0 - Heat is OFF, 1 - Heat is ON)
MyMessage msgVoltageAC(CHILD_ID_VOLTAGE_AC, V_VOLTAGE);
MyMessage msgVoltageHeat(CHILD_ID_VOLTAGE_HEAT, V_VOLTAGE);
MyMessage msgHvacStatus(CHILD_ID_HVAC_STATE, V_CUSTOM); // HvacStatus enum
byte sendDelay = 25; // delay a few milliseconds between messages to improve reliablility of the radio.
void sendStatusToController()
{
send(msgTemp.set(_indoorTemperature, 1));
wait(sendDelay);
send(msgHum.set(_indoorHumidity, 1));
wait(sendDelay);
send(msgTargetTemp.set(_targetTemperature, 0));
wait(sendDelay);
send(msgBtnOnOff.set(_mode == HvacMode::HVAC_OFF ? 0 : 1));
wait(sendDelay);
send(msgBtnAuto.set(_mode == HvacMode::HVAC_AUTO ? 1 : 0));
wait(sendDelay);
send(msgBtnAC.set(_mode == HvacMode::HVAC_AC ? 1 : 0));
wait(sendDelay);
send(msgBtnHeat.set(_mode == HvacMode::HVAC_HEAT ? 1 : 0));
wait(sendDelay);
send(msgVoltageAC.set(_voltageAC, 2));
wait(sendDelay);
send(msgVoltageHeat.set(_voltageHeat, 2));
wait(sendDelay);
send(msgHvacStatus.set(_HvacStateStr[(int)_state]));
}
// There is no much sense in sending status to the controller in every main loop
// How often do you want to send then? Well... it makes sense to do it in every HVAC cycle, because this is when we read the DHT and update status of the device
// HVAC cycle is called from timer interrupt, we can't send messages within interrupt, it's too time-consuming operation
// So, we just set _sendStatusInLoop to true inside the interrupt and then in the main loop check if it is true to degtermine should we send info to controller or not
bool _sendStatusInLoop = false;
// and this function is called from main loop
void sendStatusToController_fromHVACCycle()
{
if (_sendStatusInLoop)
{
sendStatusToController();
_sendStatusInLoop = false;
}
}
void receive(const MyMessage& message)
{
int id = message.getSensor();
bool status;
switch (id)
{
case CHILD_ID_TARGET_TEMP:
_targetTemperature = message.getFloat();
drawSetTemperature();
break;
case CHILD_ID_BTN_ONOFF:
status = message.getBool(); // (0 - OFF, 1 - ON)
if (!status) // received the command to turn OFF
{
_mode = HvacMode::HVAC_OFF;
}
else // received the command to turn ON
{
// This is a tricky part. Thermostat can be already ON, and in this case you don't want to reset the mode. You just need to do nothing in this case
if (_mode == HvacMode::HVAC_OFF)
_mode = HvacMode::HVAC_AUTO;
}
drawButtons();
break;
case CHILD_ID_BTN_AUTO:
status = message.getBool(); // (0 - Auto is OFF, 1 - Auto is ON)
if (status) // received the command to switch to Auto mode
{
_mode = HvacMode::HVAC_AUTO;
}
else // received the command to switrch to manual select mode
{
if (_state == HvacState::COOLING)
_mode = HvacMode::HVAC_AC;
else if (_state == HvacState::HEATING)
_mode = HvacMode::HVAC_HEAT;
}
drawButtons();
break;
case CHILD_ID_BTN_AC:
status = message.getBool(); // (0 - AC is OFF, 1 - AC is ON)
if (status) // received the command to turn AC ON
{
_mode = HvacMode::HVAC_AC;
}
else
{
// This is the tricky part.
// If the equipment was running AC, then just turn it off
// If the equipment was running Heat, then this message is most probably a mistake. Do nothing
if (_state == HvacState::COOLING)
_mode = HvacMode::HVAC_OFF;
else
; // do nothing
}
drawButtons();
break;
case CHILD_ID_BTN_HEAT:
status = message.getBool(); // (0 - Heat is OFF, 1 - Heat is ON)
if (status) // received the command to turn Heat ON
{
_mode = HvacMode::HVAC_HEAT;
}
else
{
// This is the tricky part.
// If the equipment was running Heat, then just turn it off
// If the equipment was running AC, then this message is most probably a mistake. Do nothing
if (_state == HvacState::HEATING)
_mode = HvacMode::HVAC_OFF;
else
; // do nothing
}
drawButtons();
break;
case CHILD_ID_TEMP_OUTDOOR:
_outdoorTemperatureC = message.getFloat();
Serial.print("Outdoor temperature received: "); Serial.println(_outdoorTemperatureC);
break;
case CHILD_ID_HUM_OUTDOOR:
_outdoorHumidity = message.getFloat();
Serial.print("Outdoor humidity received: "); Serial.println(_outdoorHumidity);
break;
default:
break;
}
}
void setup()
{
Serial.begin(115200);
Serial.println(F("Thermostat with capacitive touch display! #2"));
_tft.begin();
if (!_ts.begin(0))
Serial.println("Can't initialize Touch Screen");
else
Serial.println("Touch Screen has been initialized");
_tft.setRotation(1);
_tft.setTextColor(FONT_COLOR, BACKGROUND_COLOR);
_tft.setTextSize(1);
drawScreen();
dht.begin();
// Temperature read timer
bReadDTH = false;
ITimer3.init();
// Interval in unsigned long millisecs
if (ITimer3.attachInterruptInterval(10000, TimerHandler_readDTH))
Serial.println("Starting ITimer3 OK, millis() = " + String(millis()));
else
Serial.println("Can't set ITimer3. Select another freq. or timer");
// HVAC control timer
ITimer5.init();
// Interval in unsigned long millisecs
if (ITimer5.attachInterruptInterval(5000, hvac_Cycle))
Serial.println("Starting ITimer5 OK, millis() = " + String(millis()));
else
Serial.println("Can't set ITimer5. Select another freq. or timer");
pinMode(RELAY_ONOFF_PIN, OUTPUT);
pinMode(RELAY_HOTCOLD_PIN, OUTPUT);
pinMode(VOLTAGE_AC_PIN, INPUT);
pinMode(VOLTAGE_HEAT_PIN, INPUT);
digitalWrite(LCD_LIGHT_PIN,HIGH);
hvac_PowerOFF();
}
///////////////////////////////////
// HVAC hardware control methods //
///////////////////////////////////
void hvac_PowerON()
{
_state = HvacState::ON;
digitalWrite(RELAY_ONOFF_PIN, HIGH); // HIGH - engage relay; LOW - disengage relay
Serial.println("hvac_PowerON");
}
void hvac_PowerOFF()
{
_state = HvacState::OFF;
digitalWrite(RELAY_ONOFF_PIN, LOW); // HIGH - engage relay; LOW - disengage relay
Serial.println("hvac_PowerOFF");
}
long _acLastTime = -1; // mills from last time did AC call
long _heatLastTime = -1; // mills from last time did Heat call
long _minSwitchInterval = 5ul*60ul*1000ul; // minimum time between switching from heat to cold
void hvac_CallForAC()
{
if (_heatLastTime > 0 && millis() - _heatLastTime < _minSwitchInterval)
{
// before switching to AC from Heating, need to wait for system to calm down
hvac_PowerOFF();
Serial.println("hvac_CallForAC - powering off to calm down");
}
else if (_heatLastTime == -1 || millis() - _heatLastTime >= _minSwitchInterval) // long enough time passed sinse last time heating
{
_state = HvacState::COOLING;
digitalWrite(RELAY_HOTCOLD_PIN, LOW); // HIGH - engage relay; LOW - disengage relay
_acLastTime = millis(); // record when switched to cooling
Serial.println("hvac_CallForAC");
}
else
Serial.println("hvac_CallForAC - why are we even here???");
}
// see CallForAC for explanation
void hvac_CallForHeat()
{
if (_acLastTime > 0 && millis() - _acLastTime < _minSwitchInterval)
{
hvac_PowerOFF();
Serial.println("hvac_CallForHeat - powering off to calm down");
}
else if (_heatLastTime == -1 || millis() - _acLastTime >= _minSwitchInterval)
{
_state = HvacState::HEATING;
digitalWrite(RELAY_HOTCOLD_PIN, HIGH); // HIGH - engage relay; LOW - disengage relay
_heatLastTime = millis();
Serial.println("hvac_CallForHeat");
}
else
Serial.println("hvac_CallForHeat - why are we even here???");
}
/////////////////////////////////////////////////////////
// Main HVAC cycle where we determine how to switch it //
/////////////////////////////////////////////////////////
void hvac_Cycle()
{
if (_mode == HvacMode::HVAC_OFF)
{
hvac_PowerOFF();
}
else if (_indoorTemperature < _targetTemperature) // action only if need to heat
{
if (abs(_indoorTemperature - _targetTemperature) >= 1)
{
if (_mode == HvacMode::HVAC_AUTO || _mode == HvacMode::HVAC_HEAT)
{
hvac_PowerON();
hvac_CallForHeat();
}
else
{
hvac_PowerOFF();
}
}
}
else if (_indoorTemperature > _targetTemperature) // action only if need to cool
{
if (abs(_indoorTemperature - _targetTemperature) >= 1)
{
if (_mode == HvacMode::HVAC_AUTO || _mode == HvacMode::HVAC_AC)
{
hvac_PowerON();
hvac_CallForAC();
}
else
{
hvac_PowerOFF();
}
}
}
// and now tell main loop to send the status to controller with the results of the HVAC cycle
_sendStatusInLoop = true;
}
////////////////////////////////////////////////////////////////////
// UI drawing functions //
// Mostly hardcoded positions of the controls. //
// Not flexible and will need adjustments for another screen size //
////////////////////////////////////////////////////////////////////
void drawScreen()
{
_tft.fillScreen(ILI9341_BLACK);
_tft.fillRoundRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 25, BACKGROUND_COLOR);
drawTime();// TODO - this method is not implemented
drawIndoor();
drawOutdoor();
drawTemperatureControl();
drawButtons();
}
char _txtAuto[] = "Auto";
char _txtON[] = "On";
char _txtAC[] = "AC";
char _txtHeat[] = "Heat";
char _txtTemp[] = "Temp";
char _txtPerm[] = "Perm";
char _txtCancelHold[] = "Cancel Hold";
char _txtDone[] = "Done";
char _txtFollow[] = "Follow";
byte _Margin = 5;
uint16_t _rcAUTO[] = { 5,183,62,23 };
uint16_t _rcON[] = { 73,183,35,23 };
uint16_t _rcTEMP[] = { 115,183,60,23 };
uint16_t _rcPERM[] = { 181,183,61,23 };
uint16_t _rcFOLLW[] = { 248,183,70,23 };
uint16_t _rcAC[] = { 5,213,36,23 };
uint16_t _rcHEAT[] = { 47,213,58,23 };
uint16_t _rcCANCEL_HOLD[] = { 111,213,141,23 };
uint16_t _rcDONE[] = { 258,213,60,23 };
uint16_t _rcUp[] = { 240,0,80,60 };
uint16_t _rcDown[] = { 240,120,80,60 };
uint16_t _rcSetTemperature[] = { 240,80,80,40 };
uint16_t _rcIndoorTemp[] = { 117,46,110,78 };
uint16_t _rcIndoorHum[] = { 153,124,65,27 };
uint16_t _rcOutdoorTemp[] = { 0,93,104,23 };
uint16_t _rcOutdoorHum[] = { 20,123,60,23 };
void drawButton(char *text, uint16_t* rc, bool bInverse = false)
{
_tft.setFont(&FreeSansBold9pt7b);
_tft.setCursor(rc[0] + _Margin, rc[1] + _Margin + 12);
if (!bInverse)
{
_tft.fillRect(rc[0], rc[1], rc[2], rc[3], BACKGROUND_COLOR);
_tft.drawRect(rc[0], rc[1], rc[2], rc[3], FONT_COLOR);
_tft.print(text);
}
else
{
_tft.fillRect(rc[0], rc[1], rc[2], rc[3], FONT_COLOR);
_tft.setTextColor(BACKGROUND_COLOR, FONT_COLOR);
_tft.print(text);
_tft.setTextColor(FONT_COLOR, BACKGROUND_COLOR);
}
}
bool isInRect(uint16_t* rc, uint16_t x, uint16_t y)
{
bool result = false;
if ((x > rc[0]) && (x < rc[0] + rc[2]) && (y > rc[1]) && (y < rc[1] + rc[3]))
result = true;
return result;
}
void drawButtons()
{
drawButton(_txtAuto, _rcAUTO, _mode == HvacMode::HVAC_AUTO ? true : false);
drawButton(_txtON, _rcON, _mode == HvacMode::HVAC_OFF ? false : true);
drawButton(_txtTemp, _rcTEMP);
drawButton(_txtPerm, _rcPERM);
drawButton(_txtFollow, _rcFOLLW);
drawButton(_txtAC, _rcAC,_mode == HvacMode::HVAC_AC ? true : false);
drawButton(_txtHeat, _rcHEAT, _mode == HvacMode::HVAC_HEAT ? true : false);
drawButton(_txtCancelHold, _rcCANCEL_HOLD);
drawButton(_txtDone, _rcDONE);
}
void drawTime()
{
// TODO Implement me
int16_t x, y;
#ifdef VISUAL_DIVIDERS
int16_t w = SCREEN_WIDTH / 4;
int16_t h = SCREEN_HEIGHT / 4;
_tft.drawRect(0, 0, w, h, ILI9341_BLACK);
#endif // VISUAL
x = 5;
y = 40;
_tft.setFont(&FreeSansBold9pt7b);
_tft.setCursor(x, y);
_tft.print(_timeOfDay);
}
void drawOutdoor()
{
int16_t x;
int16_t y;
#ifdef VISUAL_DIVIDERS
x = 0;
y = SCREEN_HEIGHT / 4;
int16_t w = SCREEN_WIDTH / 4;
int16_t h = 2 * SCREEN_HEIGHT / 4;
_tft.drawRect(x, y, w, h, ILI9341_BLACK);
#endif // VISUAL_DIVIDERS
x = 5;
y = SCREEN_HEIGHT / 4 + 20;
_tft.setCursor(x, y);
_tft.setFont(&FreeSansBold9pt7b);
_tft.print(F("Outdoor"));
}
void drawIndoor()
{
int16_t x;
int16_t y;
#ifdef VISUAL_DIVIDERS
x = SCREEN_WIDTH / 4;
y = 0;
int16_t w = 2 * SCREEN_WIDTH / 4;
int16_t h = 3 * SCREEN_HEIGHT / 4;
_tft.drawRect(x, y, w, h, ILI9341_BLACK);
#endif // VISUAL_DIVIDERS
x = SCREEN_WIDTH / 4 + 40;
y = SCREEN_HEIGHT / 8;
_tft.setCursor(x, y);
_tft.setFont(&FreeSansBold12pt7b);
_tft.print(F("INDOOR"));
x += 0;
y += 85;
}
void printIndoorClimate()
{
dtostrf(_indoorTemperature, 2, 0, _indoorTemperatureBuf);
dtostrf(_indoorHumidity, 3, 0, _indoorHumidityBuf);
// print temperature
_tft.fillRect(_rcIndoorTemp[0], _rcIndoorTemp[1], _rcIndoorTemp[2], _rcIndoorTemp[3], BACKGROUND_COLOR);
int16_t x = 120, y = 115;
_tft.setCursor(x, y);
_tft.setFont(&FreeSansBold24pt7b);
_tft.setTextSize(2);
_tft.print(_indoorTemperatureBuf);
// print humidity
_tft.fillRect(_rcIndoorHum[0], _rcIndoorHum[1], _rcIndoorHum[2], _rcIndoorHum[3], BACKGROUND_COLOR);
_tft.setTextSize(1);
y += 30; x += 35;
_tft.setCursor(x, y);
_tft.setFont(&FreeSansBold12pt7b);
_tft.print(_indoorHumidityBuf);
x = 150;
_tft.print("%");
}
void printOutdoorClimate()
{
char buf[50];
dtostrf(_outdoorTemperatureC, 2, 0, _outdoorTemperatureCBuf);
_outdoorTemperatureF = ((_outdoorTemperatureC*9)+3)/5+32;
dtostrf(_outdoorTemperatureF, 2, 0, _outdoorTemperatureFBuf);
dtostrf(_outdoorHumidity, 3, 0, _outdoorHumidityBuf);
_tft.setFont(&FreeSansBold9pt7b);
_tft.setTextSize(1);
// print temperature
_tft.fillRect(_rcOutdoorTemp[0], _rcOutdoorTemp[1], _rcOutdoorTemp[2], _rcOutdoorTemp[3], BACKGROUND_COLOR);
int16_t x = _rcOutdoorTemp[0]+ _Margin, y = _rcOutdoorTemp[1]+ _rcOutdoorTemp[3]- _Margin;
_tft.setCursor(x, y);
sprintf(buf,"%s C %s F",_outdoorTemperatureCBuf,_outdoorTemperatureFBuf);
_tft.print(buf);
// print humidity
_tft.fillRect(_rcOutdoorHum[0], _rcOutdoorHum[1], _rcOutdoorHum[2], _rcOutdoorHum[3], BACKGROUND_COLOR);
x = _rcOutdoorHum[0]+ _Margin; y = _rcOutdoorHum[1]+ _rcOutdoorHum[3]-_Margin;
_tft.setCursor(x, y);
sprintf(buf,"%s %%",_outdoorHumidityBuf);
_tft.print(buf);
}
void drawSetTemperature()
{
uint16_t x = 260;
uint16_t y = 115;
_tft.setFont(&FreeSansBold24pt7b);
_tft.setCursor(x, y);
itoa(_targetTemperature, _targetTemperatureStr, 10);
_tft.fillRect(_rcSetTemperature[0], _rcSetTemperature[1], _rcSetTemperature[2], _rcSetTemperature[3], BACKGROUND_COLOR);
_tft.print(_targetTemperatureStr);
}
void drawTemperatureControl()
{
int16_t x = 3 * SCREEN_WIDTH / 4 + 5;
int16_t y = 0;
#ifdef VISUAL_DIVIDERS
int16_t w = SCREEN_WIDTH / 4;
int16_t h = SCREEN_HEIGHT / 4;
_tft.drawRect(x, y, w, h, ILI9341_BLACK);
#endif // VISUAL_DIVIDERS
int16_t dx = 25;// SCREEN_WIDTH / 8;
int16_t dy = 50;// SCREEN_HEIGHT / 4;
int16_t cx = x + SCREEN_WIDTH / 8;
for (int i = -1; i < 4; i++)
{
_tft.drawLine(cx - i, y + 15, cx - dx / 2 - i, y + dy - 15, FONT_COLOR);
_tft.drawLine(cx + i, y + 15, cx + dx / 2 + i, y + dy - 15, FONT_COLOR);
}
x = 3 * SCREEN_WIDTH / 4 + 10;
y = y + dy + 15;
_tft.setCursor(x, y);
_tft.setFont(&FreeSansBold9pt7b);
_tft.print(F("SET TO"));
y += 50;
x += 10;
drawSetTemperature();
//y += 15;
#ifdef VISUAL_DIVIDERS
_tft.drawRect(x, y, w, h, ILI9341_BLACK);
#endif // VISUAL_DIVIDERS
for (int i = -1; i < 4; i++)
{
_tft.drawLine(cx - i, y + dy - 15, cx - dx / 2 - i, y + 15, FONT_COLOR);
_tft.drawLine(cx + i, y + dy - 15, cx + dx / 2 + i, y + 15, FONT_COLOR);
}
}
void map(TS_Point& pt, uint16_t& x, uint16_t& y)
{
y = pt.x;
x = SCREEN_WIDTH - pt.y;
}
// Add the main program code into the continuous loop() function
void loop()
{
processTouch();
sendStatusToController_fromHVACCycle();
readDHT();
}
void processTouch()
{
if (_ts.touched())
{
TS_Point pt = _ts.getPoint();
uint16_t x, y;
map(pt, x, y);
Serial.print("x = "); Serial.print(x); Serial.print(" y = "); Serial.println(y);
if (isInRect(_rcUp, x, y))
{
_targetTemperature++;
drawSetTemperature();
}
else if (isInRect(_rcDown, x, y))
{
_targetTemperature--;
drawSetTemperature();
}
else if (isInRect(_rcON, x, y))
{
onBtn_OnFF();
}
else if (isInRect(_rcAUTO,x,y))
{
onBtn_Auto();
}
else if (isInRect(_rcAC, x, y))
{
onBtn_AC();
}
else if (isInRect(_rcHEAT, x, y))
{
onBtn_Heat();
}
// Also send status to the controller after any of the buttons is touched (or screen is just touched)
sendStatusToController();
// you need this delay to "debounce" topuch screen buttons
// Howevedr, you don't need this delay if you are sending status to controller (above line of code) - it adds enough delay on it's own
//delay(250);
}
}
void onBtn_OnFF()
{
if (_mode == HvacMode::HVAC_OFF)
{
_mode = HvacMode::HVAC_AUTO;
}
else
{
_mode = HvacMode::HVAC_OFF;
}
drawButtons();
}
void onBtn_AC()
{
if (_mode != HvacMode::HVAC_AC)
{
_mode = HvacMode::HVAC_AC;
}
else
{
_mode = HvacMode::HVAC_OFF;
}
drawButtons();
}
void onBtn_Heat()
{
if (_mode != HvacMode::HVAC_HEAT)
{
_mode = HvacMode::HVAC_HEAT;
}
else
{
_mode = HvacMode::HVAC_OFF;
}
drawButtons();
}
void onBtn_Auto()
{
if (_mode != HvacMode::HVAC_AUTO)
{
_mode = HvacMode::HVAC_AUTO;
}
else
{
if (_state == HvacState::COOLING)
_mode = HvacMode::HVAC_AC;
else if (_state == HvacState::HEATING)
_mode = HvacMode::HVAC_HEAT;
}
drawButtons();
}
void readDHT()
{
if (bReadDTH == true)
{
bReadDTH = false;
Serial.println(F("readDHT"));
// Wait a few seconds between measurements.
// delay is not needed because we controll this call from timer
//delay(2000);
// Reading temperature or humidity takes about 250 milliseconds!
// Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
float h = dht.readHumidity();
// Read temperature as Celsius (the default)
float t = dht.readTemperature();
// Read temperature as Fahrenheit (isFahrenheit = true)
float f = dht.readTemperature(true);
_indoorTemperature = f;
_indoorHumidity = h;
printIndoorClimate();
printOutdoorClimate();
// Check if any reads failed and exit early (to try again).
if (isnan(h) || isnan(t) || isnan(f)) {
Serial.println(F("Failed to read from DHT sensor!"));
return;
}
// Compute heat index in Fahrenheit (the default)
float hif = dht.computeHeatIndex(f, h);
// Compute heat index in Celsius (isFahreheit = false)
float hic = dht.computeHeatIndex(t, h, false);
Serial.print(F("Humidity: "));
Serial.print(h);
Serial.print(F("% Temperature: "));
Serial.print(t);
Serial.print(F(" C "));
Serial.print(f);
Serial.print(F(" F Heat index: "));
Serial.print(hic);
Serial.print(F(" C "));
Serial.print(hif);
Serial.println(F(" F"));
int chAC = analogRead(VOLTAGE_AC_PIN); _voltageAC = (5.0 * chAC / 1024.0)* (2.2 + 47 - 15.75) / 2.2;
int chHeat = analogRead(VOLTAGE_HEAT_PIN); _voltageHeat = (5.0 * chHeat / 1024.0) * (2.2 + 47 - 15.75) / 2.2;
Serial.print("Heat Voltage = "); Serial.print(_voltageHeat); Serial.print("; AC Voltage = "); Serial.println(_voltageAC);
// Also update status in the controller with the same interval as reading temperature
sendStatusToController();
}
}
Name | Size | # Downloads |
---|---|---|
Thermostat_Mega-B_Cu.gbr | 24.18 kB | 118 |
Thermostat_Mega-B_Mask.gbr | 5.64 kB | 121 |
Thermostat_Mega-B_Paste.gbr | 739 B | 101 |
Thermostat_Mega-B_Silkscreen.gbr | 6.72 kB | 106 |
Thermostat_Mega-Edge_Cuts.gbr | 1.55 kB | 103 |
Thermostat_Mega-F_Cu.gbr | 31.73 kB | 98 |
Thermostat_Mega-F_Mask.gbr | 7.68 kB | 106 |
Thermostat_Mega-F_Paste.gbr | 2.77 kB | 125 |
Thermostat_Mega-F_Silkscreen.gbr | 97 kB | 111 |
Thermostat_Mega-NPTH.drl | 285 B | 1389 |
Thermostat_Mega-PTH.drl | 3.57 kB | 109 |
Thermostat_Mega-job.gbrjob | 2.63 kB | 134 |
Thermostat_Case_Back-2.stl | 257.31 kB | 122 |
Thermostat_Case_Front-2.stl | 6.08 MB | 129 |
Name | Size | # Downloads |
---|---|---|
Thermostat.ino | 27.69 kB | 185 |