Serial Interrupt for HVAC thermostat

Greetings everyone,

Brand-noob here trying to get his first "real" Arduino project off the ground. I'm working on building an HVAC thermostat that can function on its own as well as accept (serial) input from an external PC/server.

The problem I'm running into is that I can manually set a global variable and make the thing decide whether to turn on the AC or heat based on the setpoint vs. inside temperature, but I haven't figured out how to change that setpoint variable over serial because the sketch is constantly looping, and unless the serial data is received at exactly the precise moment in the program, the data is ignored. The concept of a serial interrupt seems like the answer, but the few days of searching has led me to things that appear to be over my head (at least at this point).

Here's my code so far. I still have quite a bit to do (add hysteresis where appropriate, add hardware interrupts to accept manual input, etc.) but I have it in my head that those things should be relatively easy compared to what I'm up against at the moment.

Thanks in advance for any help/suggestions/reality-checks!

/*
code to read serial port from command-line:
while head -c 1 /dev/ttyUSB0; do : ;done
*/

#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 7, 6, 5, 4);

int tempInside; // create a variable for the inside temp
int tempOutside; // create a variable for the outside temp
int insidePin = 5; // connect inside temp sensor to pin 5
int outsidePin = 4; // connect outside temp sensor to pin 4
int fan = 8; // fan control on pin 8
int cool = 9; // AC control on pin 9
int heat = 10; // heat control on pin 10
int setpoint; // create a variable for the desired temp. we'll set this later

int hum = 23; // if we had a humidity sensor, it would likely read much higher than this around here!
int hvacPin = 3; // this pin would read the state of the fan. Probably not going to be used for this in the end design
int hvacState = 0; // create a variable for whether or not the hvacPin is high or low

void setup() 
{
  lcd.begin(16, 2);
  Serial.begin(9600);
  pinMode(fan, OUTPUT);
  pinMode(cool, OUTPUT);
  pinMode(heat, OUTPUT);
  
}

void loop() 
{
    tempInside = analogRead(insidePin);
    tempOutside = analogRead(outsidePin);
    lcd.print("Inside | Outside");
    lcd.setCursor(1, 4);
    lcd.print(tempInside);
    lcd.print(char(223));
    lcd.print("      ");
    lcd.print(tempOutside);
    lcd.print(char(223));
    Serial.print(tempInside);
    Serial.print(", ");
    Serial.println(tempOutside);
    delay(3000);
      for (int positionCounter = 0; positionCounter < 16; positionCounter++) {
      lcd.scrollDisplayLeft();
      delay(50);
      }
    lcd.clear();
    lcd.print("Humidity | HVAC");
    lcd.setCursor(1, 4);
    lcd.print(hum);
    lcd.print("%");
    lcd.print("       ");
      if (tempInside > setpoint)
      { 
        lcd.print("Cool");
        digitalWrite(heat, LOW);
        digitalWrite(fan, HIGH);
        digitalWrite(cool, HIGH);
      } else {
        lcd.print("Heat");
        digitalWrite(cool, LOW);
        digitalWrite(fan, HIGH);
        digitalWrite(heat, HIGH);
      } 
    delay(3000);
      for (int positionCounter = 0; positionCounter < 16; positionCounter++) {
    lcd.scrollDisplayLeft();
    delay(50);
  }
    lcd.clear();
    }

but I haven't figured out how to change that setpoint variable over serial because the sketch is constantly looping, and unless the serial data is received at exactly the precise moment in the program, the data is ignored.

Well serial receive data is already interrupt driven and stored in a 32 character buffer. As long as you check using the Serial.avalible() command fequently enough in your main loop, you should not have a problem checking for and then reading in the characters representing a new setpoint value.

It doesn't help that you have a couple of long delay(3000) commands in your sketch ( 3,000 characters can come down in that time and overflow the 32 character buffer, while your sketch is stuck in that delay and not checking if there are any characters ready to be read. Check the blink without delay example sketch to see a method to keep track of time and act when desired time has elapsed.

What you want to do is very possible, you just need to learn better program structure and how the avalible library commands work and can be best used.

Lefty

Since the serial data is buffered, as Retrolefty mentioned, it should not be time critical that you read it as it arrives. Can you post the code you were using to read serial that didn't work? The most common issue people have with serial is trying to read more data than has been delivered. Search the forum for serial.available to see more examples of this than you can stand.

Thanks for the quick replies guys!

The entire sketch I'm using is above, and I agree - it's not at all time-sensitive based on the intended application. As for what's sending the data to the arduino, it's merely an 'echo 77 > /dev/ttyUSB0' from a Linux Terminal session (to set the desired setpoint in degrees). A little troubleshooting there shows me that it seems to be read in hex, but that shouldn't be a big deal to work out. I'm basically just looking to send two bytes of data to set a numeric value (hey arduino, set the setpoint variable to 77*). The part that receives this data doesn't seem to be in the version that I posted late last night (HUGE apologies for that!). Basically what I was doing is something along the lines of:

void loop() {
	if (Serial.available() > 0) {
		setpoint = Serial.read();
	}
...

What really seems to make a ton of sense to me is the suggestion that Lefty made about taking out the long (three second) delays and using the "blink without delay" examples he pointed me to. I didn't know about that option until he pointed it out, and it really looks like it's the right way to do this as opposed to the hack I have now.

Oh one last thing Lefty - you're absolutely correct about the part where you said I need to learn better programming! I know my code-structure is a mess, but my thought-process was to get it functional first (so as to understand how the different code components work together) and then learn better structuring. I figured I'd get it working and then work on cleanup - asking for advice where appropriate.

Again, thanks to everyone who has helped me so far, especially you two for replying. I know I've got a long way to go to truly learn to do things the right way, and I appreciate the helpful nature of this forum. That's why I chose an Arduino!

Now I can't wait to get home from work and try this out. I'll post results, and working code if I achieve it so others can benefit.

I'm basically just looking to send two bytes of data to set a numeric value (hey arduino, set the setpoint variable to 77*).

The thing to keep in mind with Arduino serial communications is that the basic hardware and Serial software library is a pretty simple, and dumb, one 8 bit character at a time process. The fact that your setpoint value is a two byte value puts the burden on you to figure out how to go about receiving single characters and converting them into a two byte value. If just reading in a series of single characters how do you know if a recieved byte is the second byte of the prior value or the first of a new value? One is required to define and use their own simple 'serial protocol' where the source computer might be made to send a special character say a '#' to specify that the next two bytes are will be the two byte values and the first byte is the MSB or the LSB, etc.

Again Serial input is very basic, more of a, is there at least a single character ready to read Y/N?, If so I can read it off the buffer and do something with it, and keep checking for more characters avalible, do I have enough read in to do something useful? It's very low level, you can't just say "Serial, recieve a 16 bit signed integer ready to use in my sketch as a setpoint variable".

Lefty

Well serial receive data is already interrupt driven and stored in a 32 character buffer

And probably four times that.

Okay, after a brief hiatus, following the advice above and some tinkering, I've come up with the following code that seems to do most of what I want it to do. The only thing I haven't worked out yet is how to send serial data from Linux to update the setpoint (currently, I have to send an ASCII character to set the temperature). What I'm doing right now is echo M > /dev/ttyUSB0

#include <OneWire.h>
#include <DallasTemperature.h>
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 7, 6, 5, 4);
#define ONE_WIRE_BUS 2
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);
DeviceAddress insideThermometer = { 0x28, 0x84, 0xEB, 0x75, 0x03, 0x00, 0x00, 0x4F };
DeviceAddress outsideThermometer = { 0x28, 0xBD, 0xEC, 0x75, 0x03, 0x00, 0x00, 0x65 };

int tempInside = 77; // create a variable for the inside temp
int tempOutside = 104; // create a variable for the outside temp
int fan = 8; // fan control on pin 8
int cool = 9; // AC control on pin 9
int heat = 10; // heat control on pin 10
int setpoint = 78; // create a variable for the desired temp. we'll set this later

int season;
int mode;

int hvacPin = 3; // this pin would read the state of the fan. Probably not going to be used for this in the end design
int hvacState = 0; // create a variable for whether or not the hvacPin is high or low

const int ledPin =  13;      // the number of the LED pin
int ledState = LOW;             // ledState used to set the LED
long previousMillis = 0;        // will store last time LED was updated
long interval = 3000;           // interval at which to swap screens (3 seconds)

int fanState = 1;
long previousFanState = 0;        // will store last time the fan state changed
long faninterval = 600;           // interval at which to hold the fan on before heat or AC start (one minute)

int tempState = 1;
long previoustempState = 0;        // will store last time we acted on a temperature 
long tempinterval = 3000;           // interval at which to wait before changing the heat or AC state (five minutes)

void setup() {
  pinMode(ledPin, OUTPUT);      
  lcd.begin(16, 2);
  Serial.begin(9600);
  pinMode(fan, OUTPUT);
  pinMode(cool, OUTPUT);
  pinMode(heat, OUTPUT);
  sensors.begin();
  sensors.setResolution(insideThermometer, 10);
  sensors.setResolution(outsideThermometer, 10);
 // sensors.setResolution(dogHouseThermometer, 10);
}

void loop()
{
  outsideTemperature(outsideThermometer);
  insideTemperature(insideThermometer);
  loopDelay();
}

void loopDelay() {
     unsigned long currentMillis = millis(); 
    if(currentMillis - previousMillis > interval) { 
      previousMillis = currentMillis;  
      if (ledState == LOW) {
      screen1();
      } else {
      screen2();
      }
      digitalWrite(ledPin, ledState);
    }
}

void screen1() {

    lcd.clear();
    ledState = HIGH;
    lcd.print("Inside  Outside");
    lcd.setCursor(1, 4);
    lcd.print(tempInside);
    lcd.print(char(223));
    lcd.print("     ");
    lcd.print(tempOutside);
    lcd.print(char(223));
    Serial.print("in:");
    Serial.print(tempInside);
    Serial.print(" out:");
    Serial.print(tempOutside);
    Serial.print(" ");
}

void screen2() {
sensors.requestTemperatures();
  if (Serial.available()) {
      delay(100);
      while (Serial.available() > 0){
      setpoint = Serial.read();
        }
      }
    Serial.print("set:");
    Serial.print(setpoint);
    Serial.print(" ");
    lcd.clear();
    ledState = LOW;
    lcd.print("Hold  Mode  Fan");
    lcd.setCursor(1, 1);
    lcd.print(setpoint);
    lcd.print(char(223));
    lcd.print("  ");
control();
Serial.println();
}

void control() {
  
if (setpoint > tempInside) { // possibly heat, but check the outside temp first
	if (setpoint > tempOutside) { // yep, it's cold outside - let's get some heat goin
		digitalWrite(fan, HIGH);
		digitalWrite(heat, HIGH);
		digitalWrite(cool, LOW);
		lcd.print("Heat  On");
		Serial.print("mode:heat fan:on");
		} else {
		digitalWrite(fan, LOW);
		digitalWrite(heat, LOW);
		digitalWrite(cool, LOW);
		lcd.print("Heat  Off");
		Serial.print("mode:heat fan:off");
		} // it's cool in here, but it's warm outside - don't turn anything on
	} else {
if (setpoint < tempInside) { // we'd like it cooler, but let's see what's outside
	if (setpoint > tempOutside) { // it's cooler outside - don't turn anything on
		digitalWrite(fan, LOW);
		digitalWrite(heat, LOW);
		lcd.print("Cool  Off");
		Serial.print("mode:cool fan:off");
		} else {
		digitalWrite(fan, HIGH);
		digitalWrite(cool, HIGH);
		digitalWrite(heat, LOW);
		lcd.print("Cool  On");
		Serial.print("mode:cool fan:on");
		}
  }
}
}

void insideTemperature(DeviceAddress deviceAddress)
{
  float insidetempC = sensors.getTempC(deviceAddress);
  if (insidetempC == -127.00) {
    Serial.print("Error getting temperature");
  } else {
    tempInside = (DallasTemperature::toFahrenheit(insidetempC));
  }
}

void outsideTemperature(DeviceAddress deviceAddress)
{
  float tempC = sensors.getTempC(deviceAddress);
  if (tempC == -127.00) {
    Serial.print("Error getting temperature");
  } else {
    tempOutside = (DallasTemperature::toFahrenheit(tempC));
  }
}

It sounds like the problem you really have with the value "77" is that it's two bytes, and if you happen to just read one of them in your loop, then you won't "know" that the second is coming.
You should check that the data you want is really there first -- thus, only read data if there are at least two bytes! This means that low values ( < 10) will have to be written with a leading 0, and high values ( > 99) cannot be represented -- or, more accurately, need non-numerical characters to be represented :slight_smile:

int maybeReadSerialValue() {
    if (Serial.available() < 2) { return -1; }
    int a = Serial.read();
    int b = Serial.read();
    return (a - '0') * 10 + (b - '0');
}

This function will hopefully read a two-digit serial value between 00 and 99, and return that value as an integer in the range 0-99, or it will return -1 if there wasn't yet enough data on the serial port.

The easiest way to accept a number through the serial port is to add delimiters. A simple carriage return and/or new line will work. Even easier is an * or <> or something like that. If you send an * in between every number, like 77767777* then you look for an *, then you add all the digits to a string until the next *, then you use atoi() to convert the string to an int. Don't forget to null terminate the string.

Very important! You need to add some code that prevents the furnace and especially the AC compressor from turning on and off too quickly. If it goes on, it needs to stay on for at least about a minute. If it goes off, it needs to stay off for at least about a minute. If these things go on and/or off too quickly they will be destroyed.

Also, you need to have a range. If you want the temp to be 72 deg F, you should consider not turning the heat on until the temp drops below about 70.5, and off until it hits 73.5.

All this is designed to prevent the heat or AC from going on and off too quickly and too often. This will extend the life of the appliances and let them rune more efficiently, saving some energy costs.