Idea:
The main idea behind this project is to open the window to cool or heat your room when the outside temperature is what you want your room temperature to be. For example, in the summer, if it is a super nice day out and your room is very hot, you could enter a temperature into the system and it would automatically open the window to let outside air in to adjust the room temperature. This also has a dual purpose in the winter for if your house is too hot and the system could open the window to let a cool breeze in.
The main components of this system are two thermistors for measuring outside and inside temperature, a motor with an encoder to actuate the window, a motor driver to control the speed of the motor, and a p5 script animating the information from the Arduino in a helpful way. The Arduino sends the data over serial communication to the p5 script which then displays this information to the user and animates the virtual window corresponding to the current motor position.
As shown in the demo video below, the system works very well detecting a need for opening the window and acting upon it. In addition, the p5 script very accurately shows the degree of openness of the window. While these component work very well together, the controller is still fairly simple. Currently, there is just one setting for open and closed meaning the window doesn’t open a certain degree based off the temperature difference between the room and the goal temp. With more time this feature could be implemented, but for demonstration purposes of this concept, the current controller works well.
Images:
Here are images of the final system concept. The first image is an overall image showing both the physical build and the virtual animation. The second image is a close up of the virtual animation screen showing the displayed outside and inside temperatures as well as the window itself. Finally, the third image shows a top view of the physical wiring for the system.

Videos:
Video demonstration of the motor position feedback controlling the amount the virtual window is open.
Here is a video of the full system demo. As shown, when the outside temperature rises above the inside temperature, the window opens. When the outside temperature falls below the inside temperature, the window closes.
Code:
P5.js
let latestData = "waiting for data";
var minWidth = 600; //set min width and height for canvas
var width, height; // actual width and height for the sketch
var OuterWindowHeight = 800;
var OuterWindowLength = 600;
var InnerWindowHeight = OuterWindowHeight/2;
var InnerWindowLength = OuterWindowLength;
var roomColor = '#f7c07c';
var skyColor = '#c2f9ff';
// set the canvas to match the window size
if (window.innerWidth > minWidth){
width = window.innerWidth;
if (window.innerHeight > minHeight) {
height = window.innerHeight;
bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2;
createCanvas(width, height);
serial = new p5.SerialPort();
serial.open('/dev/tty.usbmodem144101');
serial.on('connected', serverConnected);
serial.on('list', gotList);
serial.on('data', gotData);
serial.on('error', gotError);
serial.on('open', gotOpen);
serial.on('close', gotClose);
function serverConnected() {
print("Connected to Server");
function gotList(thelist) {
print("List of Serial Ports:");
for (let i = 0; i < thelist.length; i++) {
print(i + " " + thelist[i]);
print("Serial Port is Open");
print("Serial Port is Closed");
latestData = "Serial Port is Closed";
function gotError(theerror) {
let currentString = serial.readLine();
if (!currentString) return;
console.log(currentString);
// latestData = currentString;
// currentString = Number(currentString) - 10000;
if ((Number(currentString) - 10000) < 10000) {
insideTemp = (Number(currentString) - 10000);
else if ((Number(currentString) - 20000) < 10000) {
outsideTemp = (Number(currentString) - 20000);
else if ((Number(currentString) - 30000) < 10000) {
y = (Number(currentString) - 30000)/1000;
var outerWindowBottomY = (height - OuterWindowHeight)/2 + 50;
var topInnerWindowY = outerWindowBottomY + InnerWindowHeight/2;
//Update bottom window position
bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2 - (y/3.1415)*(OuterWindowHeight - InnerWindowHeight);
rect((width - RoomLength)/2, 0, RoomLength, height);
text("Window Opener", width/2, 80);
text("Outside Temp: " + outsideTemp + "\t\t\t\t\t\t\t\t\t\t Inside Temp: " + insideTemp, width/2, 125); // displaying the input
rect((width - OuterWindowLength)/2, outerWindowBottomY, OuterWindowLength, OuterWindowHeight);
rect((width - InnerWindowLength)/2, topInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
line((width - InnerWindowLength)/2, topInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, topInnerWindowY);//y line
line(width/2, outerWindowBottomY, width/2, outerWindowBottomY + InnerWindowHeight);//x line
//Draw bottom inner window
rect((width - InnerWindowLength)/2, bottomInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
line((width - InnerWindowLength)/2, bottomInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, bottomInnerWindowY);//y line
line(width/2, bottomInnerWindowY - InnerWindowHeight/2, width/2, bottomInnerWindowY + InnerWindowHeight/2);//x line
function mousePressed() {
function mouseDragged() {
let serial;
let latestData = "waiting for data";
var outData;
var minWidth = 600; //set min width and height for canvas
var minHeight = 400;
var width, height; // actual width and height for the sketch
var squareWidth = 100;
var OuterWindowHeight = 800;
var OuterWindowLength = 600;
var InnerWindowHeight = OuterWindowHeight/2;
var InnerWindowLength = OuterWindowLength;
var panelThickness = 10;
var RoomLength = 800;
var bottomInnerWindowY;
var y = 0;
var roomColor = '#f7c07c';
var skyColor = '#c2f9ff';
var outsideTemp = 100;
var insideTemp = 100;
var prevString;
function setup() {
// set the canvas to match the window size
if (window.innerWidth > minWidth){
width = window.innerWidth;
} else {
width = minWidth;
}
if (window.innerHeight > minHeight) {
height = window.innerHeight;
} else {
height = minHeight;
}
bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2;
//set up canvas
createCanvas(width, height);
noStroke();
serial = new p5.SerialPort();
serial.list();
serial.open('/dev/tty.usbmodem144101');
serial.on('connected', serverConnected);
serial.on('list', gotList);
serial.on('data', gotData);
serial.on('error', gotError);
serial.on('open', gotOpen);
serial.on('close', gotClose);
}
function serverConnected() {
print("Connected to Server");
}
function gotList(thelist) {
print("List of Serial Ports:");
for (let i = 0; i < thelist.length; i++) {
print(i + " " + thelist[i]);
}
}
function gotOpen() {
print("Serial Port is Open");
}
function gotClose(){
print("Serial Port is Closed");
latestData = "Serial Port is Closed";
}
function gotError(theerror) {
print(theerror);
}
function gotData() {
let currentString = serial.readLine();
trim(currentString);
if (!currentString) return;
console.log(currentString);
// latestData = currentString;
// currentString = Number(currentString) - 10000;
if ((Number(currentString) - 10000) < 10000) {
insideTemp = (Number(currentString) - 10000);
return;
}
else if ((Number(currentString) - 20000) < 10000) {
outsideTemp = (Number(currentString) - 20000);
return;
}
else if ((Number(currentString) - 30000) < 10000) {
y = (Number(currentString) - 30000)/1000;
return;
}
}
function draw() {
var outerWindowBottomY = (height - OuterWindowHeight)/2 + 50;
var topInnerWindowY = outerWindowBottomY + InnerWindowHeight/2;
//Update bottom window position
bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2 - (y/3.1415)*(OuterWindowHeight - InnerWindowHeight);
background(0);
//Draw canvas
strokeWeight(4);
stroke('white');
fill('black');
noStroke();
// //Draw Room
strokeWeight(4);
stroke('black');
fill(roomColor);
rect((width - RoomLength)/2, 0, RoomLength, height);
fill('black');
stroke(roomColor);
textSize(30);
textAlign(CENTER);
text("Window Opener", width/2, 80);
textSize(16);
text("Outside Temp: " + outsideTemp + "\t\t\t\t\t\t\t\t\t\t Inside Temp: " + insideTemp, width/2, 125); // displaying the input
//Draw outer window
strokeWeight(4);
stroke('black');
fill(skyColor);
rect((width - OuterWindowLength)/2, outerWindowBottomY, OuterWindowLength, OuterWindowHeight);
//Draw top inner window
strokeWeight(4);
stroke('black');
strokeWeight(10);
noFill();
rect((width - InnerWindowLength)/2, topInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
strokeWeight(10);
line((width - InnerWindowLength)/2, topInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, topInnerWindowY);//y line
line(width/2, outerWindowBottomY, width/2, outerWindowBottomY + InnerWindowHeight);//x line
//Draw bottom inner window
strokeWeight(4);
stroke('black');
strokeWeight(10);
noFill();
rect((width - InnerWindowLength)/2, bottomInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
strokeWeight(10);
line((width - InnerWindowLength)/2, bottomInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, bottomInnerWindowY);//y line
line(width/2, bottomInnerWindowY - InnerWindowHeight/2, width/2, bottomInnerWindowY + InnerWindowHeight/2);//x line
}
function mousePressed() {
}
function mouseDragged() {
}
let serial;
let latestData = "waiting for data";
var outData;
var minWidth = 600; //set min width and height for canvas
var minHeight = 400;
var width, height; // actual width and height for the sketch
var squareWidth = 100;
var OuterWindowHeight = 800;
var OuterWindowLength = 600;
var InnerWindowHeight = OuterWindowHeight/2;
var InnerWindowLength = OuterWindowLength;
var panelThickness = 10;
var RoomLength = 800;
var bottomInnerWindowY;
var y = 0;
var roomColor = '#f7c07c';
var skyColor = '#c2f9ff';
var outsideTemp = 100;
var insideTemp = 100;
var prevString;
function setup() {
// set the canvas to match the window size
if (window.innerWidth > minWidth){
width = window.innerWidth;
} else {
width = minWidth;
}
if (window.innerHeight > minHeight) {
height = window.innerHeight;
} else {
height = minHeight;
}
bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2;
//set up canvas
createCanvas(width, height);
noStroke();
serial = new p5.SerialPort();
serial.list();
serial.open('/dev/tty.usbmodem144101');
serial.on('connected', serverConnected);
serial.on('list', gotList);
serial.on('data', gotData);
serial.on('error', gotError);
serial.on('open', gotOpen);
serial.on('close', gotClose);
}
function serverConnected() {
print("Connected to Server");
}
function gotList(thelist) {
print("List of Serial Ports:");
for (let i = 0; i < thelist.length; i++) {
print(i + " " + thelist[i]);
}
}
function gotOpen() {
print("Serial Port is Open");
}
function gotClose(){
print("Serial Port is Closed");
latestData = "Serial Port is Closed";
}
function gotError(theerror) {
print(theerror);
}
function gotData() {
let currentString = serial.readLine();
trim(currentString);
if (!currentString) return;
console.log(currentString);
// latestData = currentString;
// currentString = Number(currentString) - 10000;
if ((Number(currentString) - 10000) < 10000) {
insideTemp = (Number(currentString) - 10000);
return;
}
else if ((Number(currentString) - 20000) < 10000) {
outsideTemp = (Number(currentString) - 20000);
return;
}
else if ((Number(currentString) - 30000) < 10000) {
y = (Number(currentString) - 30000)/1000;
return;
}
}
function draw() {
var outerWindowBottomY = (height - OuterWindowHeight)/2 + 50;
var topInnerWindowY = outerWindowBottomY + InnerWindowHeight/2;
//Update bottom window position
bottomInnerWindowY = (height - OuterWindowHeight)/2 + 50 + OuterWindowHeight - InnerWindowHeight/2 - (y/3.1415)*(OuterWindowHeight - InnerWindowHeight);
background(0);
//Draw canvas
strokeWeight(4);
stroke('white');
fill('black');
noStroke();
// //Draw Room
strokeWeight(4);
stroke('black');
fill(roomColor);
rect((width - RoomLength)/2, 0, RoomLength, height);
fill('black');
stroke(roomColor);
textSize(30);
textAlign(CENTER);
text("Window Opener", width/2, 80);
textSize(16);
text("Outside Temp: " + outsideTemp + "\t\t\t\t\t\t\t\t\t\t Inside Temp: " + insideTemp, width/2, 125); // displaying the input
//Draw outer window
strokeWeight(4);
stroke('black');
fill(skyColor);
rect((width - OuterWindowLength)/2, outerWindowBottomY, OuterWindowLength, OuterWindowHeight);
//Draw top inner window
strokeWeight(4);
stroke('black');
strokeWeight(10);
noFill();
rect((width - InnerWindowLength)/2, topInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
strokeWeight(10);
line((width - InnerWindowLength)/2, topInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, topInnerWindowY);//y line
line(width/2, outerWindowBottomY, width/2, outerWindowBottomY + InnerWindowHeight);//x line
//Draw bottom inner window
strokeWeight(4);
stroke('black');
strokeWeight(10);
noFill();
rect((width - InnerWindowLength)/2, bottomInnerWindowY - InnerWindowHeight/2, InnerWindowLength, InnerWindowHeight);
strokeWeight(10);
line((width - InnerWindowLength)/2, bottomInnerWindowY, (width - InnerWindowLength)/2 + InnerWindowLength, bottomInnerWindowY);//y line
line(width/2, bottomInnerWindowY - InnerWindowHeight/2, width/2, bottomInnerWindowY + InnerWindowHeight/2);//x line
}
function mousePressed() {
}
function mouseDragged() {
}
Arduino
* Window Climate Controller
* - Used thermistor code from this site: https://www.circuitbasics.com/arduino-thermistor-temperature-sensor-tutorial/
#define THERMISTOR1_PIN A0
#define THERMISTOR2_PIN A1
int encoderCountRev = 48; //48 encoder counts per revolution
// https://www.pololu.com/product/4824
float gearRatio = 2*(22.0*20.0*22.0*22.0*23.0)/(12.0*12.0*10.0*10.0*10.0);
bool motorSpinning = false;
// Motor distance measuring variables
volatile long currentEncoder_pos_f = 0, prevEncoder_pos = 0, revs = 0;
float ep = 0; //Proportional error
float ei = 0; //Integral error
float ed = 0; //Derivative error
float motorGoalPos = P_I; // desired angle [radians]
float goalTemp = 480; //Degrees F
float insideOutsideDiff = 0;
float insideGoalDiff = 0;
float windowOpeningLength = P_I; //radians at the moment
//Initialize motor control pins
pinMode(motorPin1, OUTPUT);
pinMode(motorPin2, OUTPUT);
pinMode(motorPWM, OUTPUT);
attachInterrupt(digitalPinToInterrupt(encoderPinA), encoderA, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPinB), encoderB, CHANGE);
//Update motor current position
motorCurPos = (2*P_I*currentEncoder_pos_f) / (gearRatio*encoderCountRev);
temp1 = readTemp('v', THERMISTOR1_PIN);
temp2 = readTemp('v', THERMISTOR2_PIN);
insideOutsideDiff = temp1 - temp2; //Add in conditionals for if temperature difference is negative
insideGoalDiff = temp1 - goalTemp; //Add in conditionals for if temperature difference is negative
//Update motor goal position based off of difference in temperature
if (insideGoalDiff < 0) { //Goal temp is higher than inside temp
if (insideOutsideDiff < 0) { //Outside temp higher than inside temp
// int openDistance = abs(insideGoalDiff/maxDiff)*windowOpeningLength;
else { //Ouside temp lower than or equal to inside temp
else if (insideGoalDiff > 0) { //Goal temp lower than inside temp
if (insideOutsideDiff > 0) { //Outside temp lower than inside temp
// int openDistance = (insideGoalDiff/maxDiff)*windowOpeningLength;
else { //Ouside temp higher than or equal to inside temp
updateMotorCmd(updatePID(motorGoalPos, motorCurPos));
//Send data to p5 script for visualization
float readTemp(char degreeType, int ThermistorPin) {
float c1 = 1.009249522e-03, c2 = 2.378405444e-04, c3 = 2.019202697e-07;
int Vo = analogRead(ThermistorPin);
R2 = R1 * (1023.0 / (float)Vo - 1.0);
T = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2));
T = (T * 9.0)/ 5.0 + 32.0;
int updatePID(float goalPos, float curPos) {
ep = (goalPos - curPos); // error in position (p)
ei = ei + ep; // integral error in position (i)
ed = (ep - prev_ep) / (curTime - prevTime); // derivative error in position (d)
pwmCmd = (ep*kp + ei*ki + ed*kd);
int temp1Send = 10000 + temp1;
Serial.println(temp1Send); //Inside temp
int temp2Send = 20000 + temp2;
Serial.println(temp2Send); //Outside temp
int sendData = 30000 + (int) (motorCurPos*1000);
Serial.println(sendData); //Wnidow Opening
void updateMotorCmd(int pwmCmd) {
// switch directions if pass set point
digitalWrite(motorPin1, LOW);
digitalWrite(motorPin2, HIGH);
digitalWrite(motorPin1, HIGH);
digitalWrite(motorPin2, LOW);
analogWrite(motorPWM, pwmCmd);
// look for a low-to-high on channel A
if (digitalRead(encoderPinA) == HIGH) {
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinB) == LOW) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
else // must be a high-to-low edge on channel A
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinB) == HIGH) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
// look for a low-to-high on channel B
if (digitalRead(encoderPinB) == HIGH) {
// check channel A to see which way encoder is turning
if (digitalRead(encoderPinA) == HIGH) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
// Look for a high-to-low on channel B
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinA) == LOW) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
/*
* Window Climate Controller
* Judson Kyle
* judsonk
*
* Collaboration:
* - Used thermistor code from this site: https://www.circuitbasics.com/arduino-thermistor-temperature-sensor-tutorial/
*
*/
#define THERMISTOR1_PIN A0
#define THERMISTOR2_PIN A1
#define encoderPinA 18
#define encoderPinB 19
#define motorPin1 10
#define motorPin2 11
#define motorPWM 9
#define P_I 3.14159
int encoderCountRev = 48; //48 encoder counts per revolution
int incomingByte = 0;
// https://www.pololu.com/product/4824
float gearRatio = 2*(22.0*20.0*22.0*22.0*23.0)/(12.0*12.0*10.0*10.0*10.0);
bool motorSpinning = false;
// Motor distance measuring variables
volatile long currentEncoder_pos_f = 0, prevEncoder_pos = 0, revs = 0;
float motorCurPos = 0;
//PID Variables
float ep = 0; //Proportional error
float ei = 0; //Integral error
float ed = 0; //Derivative error
float prev_ep = 0;
double curTime = 0;
double prevTime = 0;
int pwmCommand = 0;
float kp = 50; // P gain
float ki = 0; // I gain
float kd = 0; // D gain
float motorGoalPos = P_I; // desired angle [radians]
float temp1, temp2;
float goalTemp = 480; //Degrees F
float insideOutsideDiff = 0;
float insideGoalDiff = 0;
float windowOpeningLength = P_I; //radians at the moment
float maxDiff = 100;
void setup() {
//Initialize motor control pins
pinMode(motorPin1, OUTPUT);
pinMode(motorPin2, OUTPUT);
pinMode(motorPWM, OUTPUT);
//Initialize interrupts
attachInterrupt(digitalPinToInterrupt(encoderPinA), encoderA, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPinB), encoderB, CHANGE);
Serial.begin(9600);
}
void loop() {
//Update motor current position
motorCurPos = (2*P_I*currentEncoder_pos_f) / (gearRatio*encoderCountRev);
temp1 = readTemp('v', THERMISTOR1_PIN);
temp2 = readTemp('v', THERMISTOR2_PIN);
insideOutsideDiff = temp1 - temp2; //Add in conditionals for if temperature difference is negative
insideGoalDiff = temp1 - goalTemp; //Add in conditionals for if temperature difference is negative
//Update motor goal position based off of difference in temperature
if (insideGoalDiff < 0) { //Goal temp is higher than inside temp
if (insideOutsideDiff < 0) { //Outside temp higher than inside temp
// int openDistance = abs(insideGoalDiff/maxDiff)*windowOpeningLength;
motorGoalPos = P_I;
}
else { //Ouside temp lower than or equal to inside temp
motorGoalPos = 0;
}
}
else if (insideGoalDiff > 0) { //Goal temp lower than inside temp
if (insideOutsideDiff > 0) { //Outside temp lower than inside temp
// int openDistance = (insideGoalDiff/maxDiff)*windowOpeningLength;
motorGoalPos = P_I;
}
else { //Ouside temp higher than or equal to inside temp
motorGoalPos = 0;
}
}
updateMotorCmd(updatePID(motorGoalPos, motorCurPos));
//Send data to p5 script for visualization
sendData();
}
float readTemp(char degreeType, int ThermistorPin) {
float logR2, R2, T;
float c1 = 1.009249522e-03, c2 = 2.378405444e-04, c3 = 2.019202697e-07;
int R1 = 1000;
int Vo = analogRead(ThermistorPin);
R2 = R1 * (1023.0 / (float)Vo - 1.0);
logR2 = log(R2);
T = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2));
switch (degreeType){
case 'f':
case 'F':
T = T - 273.15;
T = (T * 9.0)/ 5.0 + 32.0;
break;
case 'c':
case 'C':
T = T - 273.15;
break;
default:
return Vo;
break;
}
return T;
}
int updatePID(float goalPos, float curPos) {
int pwmCmd;
// PID Controller
curTime = millis();
ep = (goalPos - curPos); // error in position (p)
ei = ei + ep; // integral error in position (i)
ed = (ep - prev_ep) / (curTime - prevTime); // derivative error in position (d)
prev_ep = ep;
prevTime = curTime;
pwmCmd = (ep*kp + ei*ki + ed*kd);
return pwmCmd;
}
void sendData() {
int temp1Send = 10000 + temp1;
Serial.println(temp1Send); //Inside temp
int temp2Send = 20000 + temp2;
Serial.println(temp2Send); //Outside temp
int sendData = 30000 + (int) (motorCurPos*1000);
Serial.println(sendData); //Wnidow Opening
}
void updateMotorCmd(int pwmCmd) {
// switch directions if pass set point
if(ep < 0) {
// Turn on motor A & B
digitalWrite(motorPin1, LOW);
digitalWrite(motorPin2, HIGH);
}
else {
// Turn on motor A & B
digitalWrite(motorPin1, HIGH);
digitalWrite(motorPin2, LOW);
}
pwmCmd = abs(pwmCmd);
if (pwmCmd > 255) {
pwmCmd = 255;
}
analogWrite(motorPWM, pwmCmd);
}
void encoderA(){
// look for a low-to-high on channel A
if (digitalRead(encoderPinA) == HIGH) {
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinB) == LOW) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
else // must be a high-to-low edge on channel A
{
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinB) == HIGH) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
}
void encoderB(){
// look for a low-to-high on channel B
if (digitalRead(encoderPinB) == HIGH) {
// check channel A to see which way encoder is turning
if (digitalRead(encoderPinA) == HIGH) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
// Look for a high-to-low on channel B
else {
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinA) == LOW) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
}
/*
* Window Climate Controller
* Judson Kyle
* judsonk
*
* Collaboration:
* - Used thermistor code from this site: https://www.circuitbasics.com/arduino-thermistor-temperature-sensor-tutorial/
*
*/
#define THERMISTOR1_PIN A0
#define THERMISTOR2_PIN A1
#define encoderPinA 18
#define encoderPinB 19
#define motorPin1 10
#define motorPin2 11
#define motorPWM 9
#define P_I 3.14159
int encoderCountRev = 48; //48 encoder counts per revolution
int incomingByte = 0;
// https://www.pololu.com/product/4824
float gearRatio = 2*(22.0*20.0*22.0*22.0*23.0)/(12.0*12.0*10.0*10.0*10.0);
bool motorSpinning = false;
// Motor distance measuring variables
volatile long currentEncoder_pos_f = 0, prevEncoder_pos = 0, revs = 0;
float motorCurPos = 0;
//PID Variables
float ep = 0; //Proportional error
float ei = 0; //Integral error
float ed = 0; //Derivative error
float prev_ep = 0;
double curTime = 0;
double prevTime = 0;
int pwmCommand = 0;
float kp = 50; // P gain
float ki = 0; // I gain
float kd = 0; // D gain
float motorGoalPos = P_I; // desired angle [radians]
float temp1, temp2;
float goalTemp = 480; //Degrees F
float insideOutsideDiff = 0;
float insideGoalDiff = 0;
float windowOpeningLength = P_I; //radians at the moment
float maxDiff = 100;
void setup() {
//Initialize motor control pins
pinMode(motorPin1, OUTPUT);
pinMode(motorPin2, OUTPUT);
pinMode(motorPWM, OUTPUT);
//Initialize interrupts
attachInterrupt(digitalPinToInterrupt(encoderPinA), encoderA, CHANGE);
attachInterrupt(digitalPinToInterrupt(encoderPinB), encoderB, CHANGE);
Serial.begin(9600);
}
void loop() {
//Update motor current position
motorCurPos = (2*P_I*currentEncoder_pos_f) / (gearRatio*encoderCountRev);
temp1 = readTemp('v', THERMISTOR1_PIN);
temp2 = readTemp('v', THERMISTOR2_PIN);
insideOutsideDiff = temp1 - temp2; //Add in conditionals for if temperature difference is negative
insideGoalDiff = temp1 - goalTemp; //Add in conditionals for if temperature difference is negative
//Update motor goal position based off of difference in temperature
if (insideGoalDiff < 0) { //Goal temp is higher than inside temp
if (insideOutsideDiff < 0) { //Outside temp higher than inside temp
// int openDistance = abs(insideGoalDiff/maxDiff)*windowOpeningLength;
motorGoalPos = P_I;
}
else { //Ouside temp lower than or equal to inside temp
motorGoalPos = 0;
}
}
else if (insideGoalDiff > 0) { //Goal temp lower than inside temp
if (insideOutsideDiff > 0) { //Outside temp lower than inside temp
// int openDistance = (insideGoalDiff/maxDiff)*windowOpeningLength;
motorGoalPos = P_I;
}
else { //Ouside temp higher than or equal to inside temp
motorGoalPos = 0;
}
}
updateMotorCmd(updatePID(motorGoalPos, motorCurPos));
//Send data to p5 script for visualization
sendData();
}
float readTemp(char degreeType, int ThermistorPin) {
float logR2, R2, T;
float c1 = 1.009249522e-03, c2 = 2.378405444e-04, c3 = 2.019202697e-07;
int R1 = 1000;
int Vo = analogRead(ThermistorPin);
R2 = R1 * (1023.0 / (float)Vo - 1.0);
logR2 = log(R2);
T = (1.0 / (c1 + c2*logR2 + c3*logR2*logR2*logR2));
switch (degreeType){
case 'f':
case 'F':
T = T - 273.15;
T = (T * 9.0)/ 5.0 + 32.0;
break;
case 'c':
case 'C':
T = T - 273.15;
break;
default:
return Vo;
break;
}
return T;
}
int updatePID(float goalPos, float curPos) {
int pwmCmd;
// PID Controller
curTime = millis();
ep = (goalPos - curPos); // error in position (p)
ei = ei + ep; // integral error in position (i)
ed = (ep - prev_ep) / (curTime - prevTime); // derivative error in position (d)
prev_ep = ep;
prevTime = curTime;
pwmCmd = (ep*kp + ei*ki + ed*kd);
return pwmCmd;
}
void sendData() {
int temp1Send = 10000 + temp1;
Serial.println(temp1Send); //Inside temp
int temp2Send = 20000 + temp2;
Serial.println(temp2Send); //Outside temp
int sendData = 30000 + (int) (motorCurPos*1000);
Serial.println(sendData); //Wnidow Opening
}
void updateMotorCmd(int pwmCmd) {
// switch directions if pass set point
if(ep < 0) {
// Turn on motor A & B
digitalWrite(motorPin1, LOW);
digitalWrite(motorPin2, HIGH);
}
else {
// Turn on motor A & B
digitalWrite(motorPin1, HIGH);
digitalWrite(motorPin2, LOW);
}
pwmCmd = abs(pwmCmd);
if (pwmCmd > 255) {
pwmCmd = 255;
}
analogWrite(motorPWM, pwmCmd);
}
void encoderA(){
// look for a low-to-high on channel A
if (digitalRead(encoderPinA) == HIGH) {
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinB) == LOW) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
else // must be a high-to-low edge on channel A
{
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinB) == HIGH) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
}
void encoderB(){
// look for a low-to-high on channel B
if (digitalRead(encoderPinB) == HIGH) {
// check channel A to see which way encoder is turning
if (digitalRead(encoderPinA) == HIGH) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
// Look for a high-to-low on channel B
else {
// check channel B to see which way encoder is turning
if (digitalRead(encoderPinA) == LOW) {
currentEncoder_pos_f = currentEncoder_pos_f + 1; // CW
}
else {
currentEncoder_pos_f = currentEncoder_pos_f - 1; // CCW
}
}
}
Electrical Schematic:
