Arduino Beer/Fermentation Helper

Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum

Help Support Homebrew Talk - Beer, Wine, Mead, & Cider Brewing Discussion Forum:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.

camronnie

New Member
Joined
Jan 13, 2016
Messages
1
Reaction score
0
I have posted this in the Arduino forum, but I figure I might as well post this in a specific home brew forum as well.

I have been working on an Arduino program to help monitor my beer, for data collection, for better control and repeatability when making the same beer again. I have not truly stepped into the world of automation, yet, I am just taking things bit by bit for now.

Hardware:
Arduino Uno
Sainsmart 1602 LCD/button shield
Sainsmart 2 channel relay board
Sainsmart MQ135 air quality sensor
Beer heater
Adafruit DS1307 RTC
Adafruit 10k thermistor


Basic overview:
For each stage in the beer process the RTC will create a timestamp of when the stage started, and then the stage duration is calculated by reading the current RTC time.

The thermistor measures the temperature of the brew vessel, and will display different temp average values for primary and secondary fermentation. Based on the setpoint (user adjustable while in program) and an operating window of +/- 2*, the relay will either turn the heater on or off.

During primary fermentation, after 12 hours, the air sensor will start looking for fermentation activity. The current airlock interval can be displayed, along with the maximum interval seen.

Still to come:
Wireless communication to another arduino to check on the information without having to physically be in the brew room.
Datalogging! Hopefully being able to send "cached" data from when my desktop was off, and then once that has been sent, real time information to visualize conditions and trends. This would most likely replace the need of using another arduino to check on the brew, unless I wanted that arduino to always be on and monitoring for a condition to alert me to move the beer to the next stage.

I hope this is useful to someone else! If you enjoy the code, and have any suggestions/corrections/modifications, please repost to this thread.



/*
* Beer helper, V4
*
* Uses a CO2 sensor to monitor airlock activity, to monitor fermentation activity
* Uses a thermistor to monitor temperature
* Uses a 2 channel relay to control a heater, no set point control, set point will be set in code, specific for an ale
* Uses a real time clock to time stamp start/stops of beer stages, to track stage duration
*
* Button layout, analogRead(A0)
* No button - 1023
* Select - 740, 741
* Left - 503, 504
* Up - 141, 142
* Down - 327, 328
* Right - 0, 1
*
* What's new:
* Hardware change!
* Using a thermistor, attached to the fermenting vessel, to record temperature
* Using a relay to control a heating pad for ale fermentation during winter months
* Using a real time clock chip for time stamps, insteald of using millis() to increment counters to record time
*
* Created by Cameron Knights
* Revised on 1/13/2016
*/

// Libraries used
#include <LiquidCrystal.h>
#include <Wire.h>
#include <RTClib.h>

// values used for time duration calculations
// place holders of how many seconds are in a larger duration of time
#define sWeek 604800
#define sDay 86400
#define sHour 3600
#define sMin 60

// used to convert *C into *F
#define CToF(x) (x) * 1.8 + 32

// temp calculation definitions
const int temppin = A1; // sensor wire from voltage divider between known resistor and thermistor
int thermistornominal = 10000; // nominal resistance of thermistor at known temp
int temperaturenominal = 25; // known temp of nominal resistance
int bcoefficient = 3950; // thermistor coefficient
int seriesresistor = 9960; // resistance of known resistor in voltage divider
float steinhart; // temperature value using the steinhart calculation
const boolean enableheat = true; // variable to turn on/off in code, if heat control is desired or not
int setpoint = 72; // initial temperature setpoint
const int SPrange = 2; // range of setpoint to maintain temp
const int numsamples = 5; // number of thermistor readings to take and average at once
int samples[5]; // matrix to store thermistor readings

// pins to activate relay, relay activates on LOW
const int relay1 = 2;
const int relay2 = 3;

// Defines which pins the LCD uses, sainsmart 1602 16x2 lcd with buttons
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

// Srings for recipe and stage
String stage = "Primary";
String recipe = "Imperial Stout";

// Pages for information displayed
int LCDpage = 1; // page to control main information, up/down changes pages
int bubble_page = 1; // used to toggle between sensor information, only on page 1, left/right changes information

// Used to determine if the button is pressed or not
// keeps from buttons running away
boolean button_press = true;

// Used to control an explanation with page change
boolean firstprint = true;

// Used to toggle main pages, outside of a menu page
boolean menu = 0;

// Controls buttons, which are on A0
// buttons are called using a specific voltage value
const int button = A0;

// CO2 sensor information
boolean enable_bubble = false; // controls if checking airlock should happen or not
const int bubble_sensor = A2; // analog output of CO2 sensor
int last_sense = 1000; // larger initial value than the sensor value, so logic doesn't trigger before it should
const int threshold = 5; // value that current sensor value - last sensor value needs to exceed to determine fermentation activity
const int bubble_check = 100; // interval at which to check the bubble sensor

// State controls
boolean primary_state = true;
boolean secondary_state = false;
boolean bottle_state = false;
boolean finish_state = true;

// Timestamps
boolean primary_enable = true; // used to record the start of a beer state, to calculate state duration
unsigned long primary_start; // timestamp of when the state was started

boolean secondary_enable = true;
unsigned long secondary_start;

boolean bottle_enable = true;
unsigned long bottle_start;

// Timer values
unsigned long lcd_millis;
unsigned long bubble_millis;
unsigned long temp_millis;
unsigned long RTC_millis;

unsigned long bubble_timer;
unsigned long last_bubble;
unsigned long last_bubble_time;
unsigned long last_bubble_time_max;

// Values for calculating duration
unsigned int primary_weeks;
unsigned int primary_days;
unsigned int primary_hours;
unsigned int primary_mins;

unsigned int secondary_weeks;
unsigned int secondary_days;
unsigned int secondary_hours;
unsigned int secondary_mins;

unsigned int bottle_weeks;
unsigned int bottle_days;
unsigned int bottle_hours;
unsigned int bottle_mins;

unsigned int total_weeks;
unsigned int total_days;
unsigned int total_hours;
unsigned int total_mins;

// Temperature variables
unsigned long primary_temp; // current temp
float primary_temp_avg; // average temp

unsigned long secondary_temp;
float secondary_temp_avg;

RTC_DS1307 RTC; // real time clock call

void setup()
{
Wire.begin();
RTC.begin();
lcd.begin(16,2);

pinMode(button, INPUT);
pinMode(bubble_sensor, INPUT);

pinMode(relay1, OUTPUT);
digitalWrite(relay1, HIGH); // set output to HIGH right away, so the relay does not activate. relay will activate on LOW

pinMode(relay2, OUTPUT);
digitalWrite(relay2, HIGH);

// displays which version is running
lcd.setCursor(0,0);
lcd.print("Beer Helper V4");
lcd.setCursor(0,1);
lcd.print("Loading...");

delay(2000);

// displays which beer the arduino is monitoring
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Recipe");
lcd.setCursor(0,1);
lcd.print(recipe);

delay(2000);

lcd.clear();

// waits for the user to hit a button to start the process
wait();

lcd.clear();
}

void loop()
{
//Used to reset timers, if millis overflows
if(millis() <= 50)
{
lcd_millis = 0;
temp_millis = 0;
RTC_millis = 0;
}

// Updates LCD
if(millis() - lcd_millis >= 250)
{
lcdprint();
lcd_millis = millis();
}

// Senses a button press, determines what to do
if(analogRead(button) <= 1000 && button_press == false)
{
buttonfunction();
}

// Button is no longer pressed, resets button sense
if(analogRead(button) >= 1000 && button_press == true)
{
button_press = false;
}

// waits 12 hours before checking for fermentation
// if fermentation is not steady, the max interval may become too great
if(primary_hours >= 12 && primary_state == true)
{
enable_bubble = true;
}

// Bubble check, only for primary fermentation
// Waits 1 day before checking for activity
if(enable_bubble == true && primary_state == true)
{
if(millis() - bubble_millis >= bubble_check) // current time is at or greater than the interval, check CO2 sensor
{
if(analogRead(bubble_sensor) - last_sense >= threshold) // current reading is above a threshold, activity has occured
{
last_bubble = millis(); // marks the time of the airlock activity for a time value
}

last_sense = analogRead(bubble_sensor); // reads the sensor again to save a last sense value for comparison

bubble_millis = millis();

last_bubble_time = (millis() - last_bubble)/1000; //time passed since last airlock activity

if(last_bubble_time > last_bubble_time_max) //records the longest interval of airlock activity
{
last_bubble_time_max = last_bubble_time; // stores in seconds, converts to min in the LCD loop
}
}
}
else if(primary_state == false) // Only record airlock activity for primary fermentation
{
last_bubble_time = 0;
}

// Temp read, every 5sec
if(millis() - temp_millis >= 5000)
{
temp_read();
temp_millis = millis();
}

// RTC update, every 1sec
if(millis() - RTC_millis >= 1000)
{
timer();
RTC_millis = millis();
}
}

void timer()
{
// initiates a starting timestamp for each state
if(primary_state == true && primary_enable == true)
{
DateTime now = RTC.now();
primary_start = now.unixtime();

primary_enable = false;
}
else if(secondary_state == true && secondary_enable == true)
{
DateTime now = RTC.now();
secondary_start = now.unixtime();

secondary_enable = false;
}
else if(bottle_state == true && bottle_enable == true)
{
DateTime now = RTC.now();
bottle_start = now.unixtime();

bottle_enable = false;
}

// gets the current time
DateTime now = RTC.now();

// converts unixtime into week, day, hour, min time
if(primary_state == true) // primary stage time values
{
float elapsed = now.unixtime() - primary_start;
primary_weeks = elapsed / sWeek;
primary_days = (elapsed - primary_weeks * sWeek) / sDay;
primary_hours = (elapsed - primary_weeks * sWeek - primary_days * sDay) / sHour;
primary_mins = (elapsed - primary_weeks * sWeek - primary_days * sDay - primary_hours * sHour) / sMin;
}
else if(secondary_state == true) // secondary stage time values, primary stage values are not changed, and are remembered
{
float elapsed = now.unixtime() - secondary_start;
secondary_weeks = elapsed / sWeek;
secondary_days = (elapsed - secondary_weeks * sWeek) / sDay;
secondary_hours = (elapsed - secondary_weeks * sWeek - secondary_days * sDay) / sHour;
secondary_mins = (elapsed - secondary_weeks * sWeek - secondary_days * sDay - secondary_hours * sHour) / sMin;
}
if(bottle_state == true)
{
float elapsed = now.unixtime() - bottle_start;
bottle_weeks = elapsed / sWeek;
bottle_days = (elapsed - bottle_weeks * sWeek) / sDay;
bottle_hours = (elapsed - bottle_weeks * sWeek - bottle_days * sDay) / sHour;
bottle_mins = (elapsed - bottle_weeks * sWeek - bottle_days * sDay - bottle_hours * sHour) / sMin;
}

if(finish_state == true)
{
float elapsed = now.unixtime() - primary_start;
total_weeks = elapsed / sWeek;
total_days = (elapsed - total_weeks * sWeek) / sDay;
total_hours = (elapsed - total_weeks * sWeek - total_days * sDay) / sHour;
total_mins = (elapsed - total_weeks * sWeek - total_days * sDay - total_hours * sHour) / sMin;
}
}

void temp_read()
{
static int count = 0; // used to determine if an average temp calculation needs to occur
float average; // averages thermistor value

// take numsamples samples in a row, with a slight delay
for (int i=0; i< numsamples; i++) {
samples = analogRead(temppin);
delay(10);
}

// average all the samples out
average = 0;
for (int i=0; i< numsamples; i++) {
average += samples;
}
average /= numsamples; // average read value

// convert the value to resistance
average = 1023 / average - 1;
average = seriesresistor / average;

steinhart = average / thermistornominal; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= bcoefficient; // 1/B * ln(R/Ro)
steinhart += 1.0 / (temperaturenominal + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert to C
steinhart = (CToF(steinhart));

if(enableheat == true) // boolean to control if the heater should be used or not, needs to be changed by the user before compiling
{
if(steinhart >= setpoint + SPrange) // temperature is above the setpoint + range, turn off the heater
{
digitalWrite(relay1, HIGH); // HIGH turns off the relay
}
else if(steinhart <= setpoint - SPrange) // temperature is below the setpoint - range, turn on the heater
{
digitalWrite(relay1, LOW); // LOW turns on the relay
}
}

count++; // keeps track of how many times the temp has been polled

if(count >= 12) // 12 is 1min based on 5sec polling, calculate average
{
if(primary_state == true)
{
static unsigned long samples = 1; // keeps track of how many temp samples have been read
primary_temp += steinhart; // sum of all temp values read, for averaging
primary_temp_avg = primary_temp / samples; // averages temp sum by samples

samples++;

count = 0; // resets counter
}

else if(secondary_state == true)
{
static unsigned long samples = 1;
secondary_temp += steinhart;
secondary_temp_avg = secondary_temp / samples;

samples++;

count = 0;
}
}
}

void lcdprint()
{
// used for clearing LCD between page changes
static int oldpage;
static int oldbubblepage;

// Clears page between page changes
if(oldpage != LCDpage)
{
lcd.clear();
firstprint = true; // primes first print, to explain what information will be shown on the page
}

// recipe name page
if(LCDpage == 0)
{
oldpage = LCDpage;

lcd.setCursor(0,0);
lcd.print("Recipe");
lcd.setCursor(0,1);
lcd.print(recipe);

lcd.setCursor(15,0);
lcd.print(LCDpage);
}

// Main page, displays sensor information
if(LCDpage == 1)
{
if(analogRead(button) < 1000)
{
delay(5);

if(analogRead(button) <= 5) // right button is hit, move to next bubble page
{
bubble_page = 2;
}
else if(analogRead(button) >= 500 && analogRead(button) <= 510) // left button is hit, move to first bubble page
{
bubble_page = 1;
}
}

if(oldbubblepage != bubble_page)
{
lcd.clear();
}

if(firstprint == true)
{
lcd.clear();

lcd.home();
lcd.print("Sensor Info");
delay(1000);

lcd.clear();

firstprint = false;

oldpage = LCDpage;
}

if(primary_state == true) // primary state sensor information
{
if(bubble_page == 1)
{
oldbubblepage = bubble_page;

lcd.setCursor(0,0);
lcd.print("Temp: ");
lcd.print((int)steinhart);
lcd.print(", ");
lcd.print((int)primary_temp_avg);
lcd.setCursor(0,1);
}
else if(bubble_page == 2)
{
oldbubblepage = bubble_page;

lcd.home();
lcd.print("Set point: ");
lcd.print(setpoint);
lcd.setCursor(0,1);
}
}
else if(secondary_state == true) // secondary state sensor information
{
if(bubble_page == 1)
{
oldbubblepage = bubble_page;

lcd.setCursor(0,0);
lcd.print("Temp: ");
lcd.print((int)steinhart);
lcd.print(", ");
lcd.print((int)secondary_temp_avg);
lcd.setCursor(0,1);
}
else if(bubble_page == 2)
{
oldbubblepage = bubble_page;

lcd.home();
lcd.print("Set point: ");
lcd.print(setpoint);
lcd.setCursor(0,1);
}
}
else if(bottle_state == true) // nothing to display in bottle stage
{
lcd.home();
lcd.print("Move along...");
lcd.setCursor(0,1);
lcd.print("No info to show");
}

if(bubble_page == 1 && primary_state == true || secondary_state == true)
{
lcd.print("Bubble: ");
lcd.print(last_bubble_time); // time since the last airlock activity
lcd.print(" sec");
lcd.print(" ");
}
else if(bubble_page == 2 && primary_state == true || secondary_state == true)
{
lcd.print("Max: ");
lcd.print((int)last_bubble_time_max/60); // longest interval between airlock activities, in min
lcd.print(" min");
lcd.print(" ");
}

lcd.setCursor(15,0);
lcd.print(LCDpage);
}

// Displays primary fermentation time
else if(LCDpage == 2)
{
if(firstprint == true)
{
lcd.clear();

lcd.home();
lcd.print("Primary");
lcd.setCursor(0,1);
lcd.print("Fermentation");
delay(1000);

lcd.clear();

firstprint = false;

oldpage = LCDpage;
}

lcd.setCursor(0,0);
lcd.print("Min:");
lcd.print(primary_mins);
lcd.print(" ");
lcd.setCursor(7,0);
lcd.print("Hour:");
lcd.print(primary_hours);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Day:");
lcd.print(primary_days);
lcd.print(" ");
lcd.setCursor(7,1);
lcd.print("Week:");
lcd.print(primary_weeks);
lcd.print(" ");

lcd.setCursor(15,0);
lcd.print(LCDpage);
}

// Displays secondary fermentation time
else if(LCDpage == 3)
{
if(firstprint == true)
{
lcd.clear();

lcd.home();
lcd.print("Secondary");
lcd.setCursor(0,1);
lcd.print("Fermentation");
delay(1000);

lcd.clear();

firstprint = false;

oldpage = LCDpage;
}

lcd.setCursor(0,0);
lcd.print("Min:");
lcd.print(secondary_mins);
lcd.print(" ");
lcd.setCursor(7,0);
lcd.print("Hour:");
lcd.print(secondary_hours);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Day:");
lcd.print(secondary_days);
lcd.print(" ");
lcd.setCursor(7,1);
lcd.print("Week:");
lcd.print(secondary_weeks);
lcd.print(" ");

lcd.setCursor(15,0);
lcd.print(LCDpage);

}

// Displays bottle time
else if(LCDpage == 4)
{
if(firstprint == true)
{
lcd.clear();

lcd.home();
lcd.print("Bottle time");
delay(1000);

lcd.clear();

firstprint = false;

oldpage = LCDpage;
}

lcd.setCursor(0,0);
lcd.print("Min:");
lcd.print(bottle_mins);
lcd.print(" ");
lcd.setCursor(7,0);
lcd.print("Hour:");
lcd.print(bottle_hours);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Day:");
lcd.print(bottle_days);
lcd.print(" ");
lcd.setCursor(7,1);
lcd.print("Weeks:");
lcd.print(bottle_weeks);
lcd.print(" ");

lcd.setCursor(15,0);
lcd.print(LCDpage);
}

// Displays overall time
if(LCDpage == 5)
{
if(firstprint == true)
{
lcd.clear();

lcd.home();
lcd.print("Overall time");

delay(1000);

lcd.clear();

firstprint = false;

oldpage = LCDpage;
}

lcd.setCursor(0,0);
lcd.print("Min:");
lcd.print(total_mins);
lcd.print(" ");
lcd.setCursor(7,0);
lcd.print("Hour:");
lcd.print(total_hours);
lcd.print(" ");
lcd.setCursor(0,1);
lcd.print("Day:");
lcd.print(total_days);
lcd.print(" ");
lcd.setCursor(7,1);
lcd.print("Weeks:");
lcd.print(total_weeks);
lcd.print(" ");

lcd.setCursor(15,0);
lcd.print(LCDpage);
}

// prompts user of changing beer stage, select to cancel, right to accept
if(LCDpage == 11)
{
lcd.setCursor(0,0);
lcd.print("Start secondary?");
lcd.setCursor(0,1);
lcd.print("No");
lcd.setCursor(4,1);
lcd.print("Yes");

lcd.setCursor(14,1);
lcd.print(LCDpage);

oldpage = LCDpage;
firstprint = false;
}

else if(LCDpage == 12)
{
lcd.setCursor(0,0);
lcd.print("Start bottle?");
lcd.setCursor(0,1);
lcd.print("No");
lcd.setCursor(4,1);
lcd.print("Yes");

lcd.setCursor(14,1);
lcd.print(LCDpage);

oldpage = LCDpage;
firstprint = false;
}

else if(LCDpage == 13)
{
lcd.setCursor(0,0);
lcd.print("Finished?");
lcd.setCursor(0,1);
lcd.print("No");
lcd.setCursor(4,1);
lcd.print("Yes");

lcd.setCursor(14,1);
lcd.print(LCDpage);

oldpage = LCDpage;
firstprint = false;
}

else if(LCDpage == 14)
{
lcd.home();
lcd.print("Change setpoint?");
lcd.setCursor(0,1);
lcd.print("No");
lcd.setCursor(4,1);
lcd.print("Yes");

lcd.setCursor(14,1);
lcd.print(LCDpage);

oldpage = LCDpage;
firstprint = false;
}
}

void buttonfunction()
{
// Boolean used for keeping buttons from running away
if(button_press == false)
{
// Up button pressed, increment page value
if(menu == 0 && analogRead(button) >= 140 && analogRead(button) <= 145)
{
if(LCDpage < 5)
{
++LCDpage;
}
}

// Down button pressed, decrement page
else if(menu == 0 && analogRead(button) >= 325 && analogRead(button) <= 330)
{
if(LCDpage > 0)
{
--LCDpage;
}
}

// Select button pressed, enter menu
if(menu == 0 && analogRead(button) >= 738 && analogRead(button) <= 743)
{
menu = 1;

if(LCDpage == 1)
{
LCDpage = 14;
}
if(LCDpage == 2)
{
LCDpage = 11;
}
else if(LCDpage == 3)
{
LCDpage = 12;
}
else if(LCDpage == 4)
{
LCDpage = 13;
}
}
else if(menu == 1 && analogRead(button) >= 738 && analogRead(button) <= 743)
{
menu = 0;
LCDpage = 1;
}

// Up button pressed, increment page value
if(menu == 1 && analogRead(button) >= 140 && analogRead(button) <= 145)
{
if(LCDpage < 13)
{
++LCDpage;
}
}

// Down button pressed, decrement page
else if(menu == 1 && analogRead(button) >= 325 && analogRead(button) <= 330)
{
if(LCDpage > 10)
{
--LCDpage;
}
}

// Right button pressd
if(menu == 1 && analogRead(button) <= 5)
{
if(LCDpage == 11)
{
secondary_state = true;
primary_state = false;

menu = 0;
LCDpage = 3;
}

else if(LCDpage == 12)
{
bottle_state = true;
secondary_state = false;

menu = 0;
LCDpage = 4;
}

else if(LCDpage == 13)
{
bottle_state = false;
finish_state = false;

menu = 0;
LCDpage = 5;
}
else if(LCDpage == 14)
{
delay(250);
setpoint = changesetpoint(setpoint);

menu = 0;
LCDpage = 1;
}
}
}

// Turns off button_press, so this loop
// is not called again, without another button press
button_press = true;
}

int changesetpoint(int value)
{
// controls when the change is complete
boolean complete = false;

// loops until the user is done changing the setpoint
while(complete == false)
{
// used to clear the LCD once
static boolean cleared = false;

if(cleared == false)
{
lcd.clear();

cleared = true;
}

// displays setpoint and exit info
lcd.home();
lcd.print("Setpoint: ");
lcd.print(value);
lcd.setCursor(0,1);
lcd.print("Right to save");

if(analogRead(button) < 1000)
{
delay(5);

// up button is pressed, increment setpoint
if(analogRead(button) >= 140 && analogRead(button) <= 145)
{
value++;
delay(250);
}

// down button is pressed, decrement setpoint
else if(analogRead(button) >= 325 && analogRead(button) <= 330)
{
value--;
delay(250);
}

// right button is pressed, save setpoint and exit
if(analogRead(button) <= 5)
{
cleared = false;

return value;

complete = true;
}
}
}
}

// Wait loop
void wait()
{
while(analogRead(button) > 1000)
{
lcd.setCursor(0,0);
lcd.print("Press a button");
lcd.setCursor(0,1);
lcd.print("when ready");
}
}
 
Just a quick FYI you can use the "code" html tags to pull your code into a better format for reading (it is also under the "#" in the post controls.
Code:
/*
 * Beer helper, V4
 * 
 * Uses a CO2 sensor to monitor airlock activity, to monitor fermentation activity
 * Uses a thermistor to monitor temperature
 * Uses a 2 channel relay to control a heater, no set point control, set point will be set in code, specific for an ale
 * Uses a real time clock to time stamp start/stops of beer stages, to track stage duration
 * 
 * Button layout, analogRead(A0)
 * No button - 1023
 * Select - 740, 741
 * Left - 503, 504
 * Up - 141, 142
 * Down - 327, 328
 * Right - 0, 1
 * 
 * What's new:
 * Hardware change!
 * Using a thermistor, attached to the fermenting vessel, to record temperature
 * Using a relay to control a heating pad for ale fermentation during winter months
 * Using a real time clock chip for time stamps, insteald of using millis() to increment counters to record time
 * 
 * Created by Cameron Knights
 * Revised on 1/13/2016
 */

// Libraries used
#include <LiquidCrystal.h>
#include <Wire.h>
#include <RTClib.h>

//  values used for time duration calculations
//  place holders of how many seconds are in a larger duration of time
#define sWeek 604800
#define sDay 86400
#define sHour 3600
#define sMin 60

//  used to convert *C into *F
#define CToF(x) (x) * 1.8 + 32

//  temp calculation definitions
const int temppin = A1; //  sensor wire from voltage divider between known resistor and thermistor
int thermistornominal = 10000;  //  nominal resistance of thermistor at known temp
int temperaturenominal = 25;  //  known temp of nominal resistance
int bcoefficient = 3950;  //  thermistor coefficient
int seriesresistor = 9960;  //  resistance of known resistor in voltage divider
float steinhart;  //  temperature value using the steinhart calculation
const boolean enableheat = true; //  variable to turn on/off in code, if heat control is desired or not
int setpoint = 72;  //  initial temperature setpoint
const int SPrange = 2;  //  range of setpoint to maintain temp
const int numsamples = 5; //  number of thermistor readings to take and average at once
int samples[5]; //  matrix to store thermistor readings

//  pins to activate relay, relay activates on LOW
const int relay1 = 2;
const int relay2 = 3;

// Defines which pins the LCD uses, sainsmart 1602 16x2 lcd with buttons
LiquidCrystal lcd(8, 9, 4, 5, 6, 7);

// Srings for recipe and stage
String stage = "Primary";
String recipe = "Imperial Stout";

// Pages for information displayed
int LCDpage = 1;  //  page to control main information, up/down changes pages
int bubble_page = 1;  //  used to toggle between sensor information, only on page 1, left/right changes information

// Used to determine if the button is pressed or not
// keeps from buttons running away
boolean button_press = true;

// Used to control an explanation with page change
boolean firstprint = true;

// Used to toggle main pages, outside of a menu page
boolean menu = 0;

// Controls buttons, which are on A0
// buttons are called using a specific voltage value
const int button = A0;

//  CO2 sensor information
boolean enable_bubble = false;  //  controls if checking airlock should happen or not
const int bubble_sensor = A2; //  analog output of CO2 sensor
int last_sense = 1000;  //  larger initial value than the sensor value, so logic doesn't trigger before it should
const int threshold = 5; //  value that current sensor value - last sensor value needs to exceed to determine fermentation activity
const int bubble_check = 100; //  interval at which to check the bubble sensor

//  State controls
boolean primary_state = true;
boolean secondary_state = false;
boolean bottle_state = false;
boolean finish_state = true;

//  Timestamps
boolean primary_enable = true;  //  used to record the start of a beer state, to calculate state duration
unsigned long primary_start;  //  timestamp of when the state was started

boolean secondary_enable = true;
unsigned long secondary_start;

boolean bottle_enable = true;
unsigned long bottle_start;

//  Timer values
unsigned long lcd_millis;
unsigned long bubble_millis;
unsigned long temp_millis;
unsigned long RTC_millis;

unsigned long bubble_timer;
unsigned long last_bubble;
unsigned long last_bubble_time;
unsigned long last_bubble_time_max;

//  Values for calculating duration
unsigned int primary_weeks;
unsigned int primary_days;
unsigned int primary_hours;
unsigned int primary_mins;

unsigned int secondary_weeks;
unsigned int secondary_days;
unsigned int secondary_hours;
unsigned int secondary_mins;

unsigned int bottle_weeks;
unsigned int bottle_days;
unsigned int bottle_hours;
unsigned int bottle_mins;

unsigned int total_weeks;
unsigned int total_days;
unsigned int total_hours;
unsigned int total_mins;

//  Temperature variables
unsigned long primary_temp; //  current temp
float primary_temp_avg; //  average temp

unsigned long secondary_temp;
float secondary_temp_avg;

RTC_DS1307 RTC; //  real time clock call

void setup()
{
  Wire.begin();
  RTC.begin();
  lcd.begin(16,2);

  pinMode(button, INPUT);
  pinMode(bubble_sensor, INPUT);

  pinMode(relay1, OUTPUT);
  digitalWrite(relay1, HIGH); //  set output to HIGH right away, so the relay does not activate. relay will activate on LOW

  pinMode(relay2, OUTPUT);
  digitalWrite(relay2, HIGH);

  //  displays which version is running
  lcd.setCursor(0,0);
  lcd.print("Beer Helper V4");
  lcd.setCursor(0,1);
  lcd.print("Loading...");

  delay(2000);

  //  displays which beer the arduino is monitoring
  lcd.clear();
  lcd.setCursor(0,0);
  lcd.print("Recipe");
  lcd.setCursor(0,1);
  lcd.print(recipe);

  delay(2000);

  lcd.clear();

  //  waits for the user to hit a button to start the process
  wait();
  
  lcd.clear();
}

void loop()
{
  //Used to reset timers, if millis overflows
  if(millis() <= 50)
  {
    lcd_millis = 0;
    temp_millis = 0;
    RTC_millis = 0;
  }

  // Updates LCD
  if(millis() - lcd_millis >= 250)
  {
    lcdprint();
    lcd_millis = millis();
  }

  // Senses a button press, determines what to do
  if(analogRead(button) <= 1000 && button_press == false)
  {
    buttonfunction();
  }

  // Button is no longer pressed, resets button sense
  if(analogRead(button) >= 1000 && button_press == true)
  {
    button_press = false;
  }

  //  waits 12 hours before checking for fermentation
  //  if fermentation is not steady, the max interval may become too great
  if(primary_hours >= 12 && primary_state == true)
  {
    enable_bubble = true;
  }
  
  // Bubble check, only for primary fermentation
  // Waits 1 day before checking for activity
  if(enable_bubble == true && primary_state == true)
  {
    if(millis() - bubble_millis >= bubble_check)  //  current time is at or greater than the interval, check CO2 sensor
    {
      if(analogRead(bubble_sensor) - last_sense >= threshold) //  current reading is above a threshold, activity has occured
      {
        last_bubble = millis(); //  marks the time of the airlock activity for a time value
      }

      last_sense = analogRead(bubble_sensor); //  reads the sensor again to save a last sense value for comparison

      bubble_millis = millis();
      
      last_bubble_time = (millis() - last_bubble)/1000; //time passed since last airlock activity

      if(last_bubble_time > last_bubble_time_max)  //records the longest interval of airlock activity
      {
        last_bubble_time_max = last_bubble_time;  //  stores in seconds, converts to min in the LCD loop
      }
    }
  }
  else if(primary_state == false) //  Only record airlock activity for primary fermentation
  {
    last_bubble_time = 0;
  }
    
  // Temp read, every 5sec
  if(millis() - temp_millis >= 5000)
  {
    temp_read();
    temp_millis = millis();
  }

  //  RTC update, every 1sec
  if(millis() - RTC_millis >= 1000)
  {
    timer();
    RTC_millis = millis();
  }
}

void timer()
{
  //  initiates a starting timestamp for each state
  if(primary_state == true && primary_enable == true)
  {
    DateTime now = RTC.now();
    primary_start = now.unixtime();

    primary_enable = false;
  }
  else if(secondary_state == true && secondary_enable == true)
  {
    DateTime now = RTC.now();
    secondary_start = now.unixtime();

    secondary_enable = false;
  }
  else if(bottle_state == true && bottle_enable == true)
  {
    DateTime now = RTC.now();
    bottle_start = now.unixtime();

    bottle_enable = false;
  }

  //  gets the current time
  DateTime now = RTC.now();

  //  converts unixtime into week, day, hour, min time
  if(primary_state == true) //  primary stage time values
  {
    float elapsed = now.unixtime() - primary_start;
    primary_weeks = elapsed / sWeek;
    primary_days = (elapsed - primary_weeks * sWeek) / sDay;
    primary_hours = (elapsed - primary_weeks * sWeek - primary_days * sDay) / sHour;
    primary_mins = (elapsed - primary_weeks * sWeek - primary_days * sDay - primary_hours * sHour) / sMin;
  }
  else if(secondary_state == true)  //  secondary stage time values, primary stage values are not changed, and are remembered
  {
    float elapsed = now.unixtime() - secondary_start;
    secondary_weeks = elapsed / sWeek;
    secondary_days = (elapsed - secondary_weeks * sWeek) / sDay;
    secondary_hours = (elapsed - secondary_weeks * sWeek - secondary_days * sDay) / sHour;
    secondary_mins = (elapsed - secondary_weeks * sWeek - secondary_days * sDay - secondary_hours * sHour) / sMin;
  }
  if(bottle_state == true)
  {
    float elapsed = now.unixtime() - bottle_start;
    bottle_weeks = elapsed / sWeek;
    bottle_days = (elapsed - bottle_weeks * sWeek) / sDay;
    bottle_hours = (elapsed - bottle_weeks * sWeek - bottle_days * sDay) / sHour;
    bottle_mins = (elapsed - bottle_weeks * sWeek - bottle_days * sDay - bottle_hours * sHour) / sMin;
  }
  
  if(finish_state == true)
  {
    float elapsed = now.unixtime() - primary_start;
    total_weeks = elapsed / sWeek;
    total_days = (elapsed - total_weeks * sWeek) / sDay;
    total_hours = (elapsed - total_weeks * sWeek - total_days * sDay) / sHour;
    total_mins = (elapsed - total_weeks * sWeek - total_days * sDay - total_hours * sHour) / sMin;
  }
}

void temp_read()
{
  static int count = 0; //  used to determine if an average temp calculation needs to occur
  float average;  //  averages thermistor value
 
  // take numsamples samples in a row, with a slight delay
  for (int i=0; i< numsamples; i++) {
   samples[i] = analogRead(temppin);
   delay(10);
  }
 
  // average all the samples out
  average = 0;
  for (int i=0; i< numsamples; i++) {
     average += samples[i];
  }
  average /= numsamples;  //  average read value
 
  // convert the value to resistance
  average = 1023 / average - 1;
  average = seriesresistor / average;
 
  steinhart = average / thermistornominal;     // (R/Ro)
  steinhart = log(steinhart);                  // ln(R/Ro)
  steinhart /= bcoefficient;                   // 1/B * ln(R/Ro)
  steinhart += 1.0 / (temperaturenominal + 273.15); // + (1/To)
  steinhart = 1.0 / steinhart;                 // Invert
  steinhart -= 273.15;                         // convert to C
  steinhart = (CToF(steinhart));

  if(enableheat == true)  //  boolean to control if the heater should be used or not, needs to be changed by the user before compiling
  {
    if(steinhart >= setpoint + SPrange) //  temperature is above the setpoint + range, turn off the heater
    {
      digitalWrite(relay1, HIGH); //  HIGH turns off the relay
    }
    else if(steinhart <= setpoint - SPrange)  //  temperature is below the setpoint - range, turn on the heater
    {
      digitalWrite(relay1, LOW);  //  LOW turns on the relay
    }
  }

  count++;  //  keeps track of how many times the temp has been polled

  if(count >= 12)  //  12 is 1min based on 5sec polling, calculate average
  {
    if(primary_state == true)
    {
      static unsigned long samples = 1; //  keeps track of how many temp samples have been read
      primary_temp += steinhart;  //  sum of all temp values read, for averaging
      primary_temp_avg = primary_temp / samples;  //  averages temp sum by samples

      samples++;

      count = 0;  //  resets counter
    }

    else if(secondary_state == true)
    {
      static unsigned long samples = 1;
      secondary_temp += steinhart;
      secondary_temp_avg = secondary_temp / samples;

      samples++;

      count = 0;
    }
  }
}

void lcdprint()
{
  //  used for clearing LCD between page changes
  static int oldpage;
  static int oldbubblepage;
  
  // Clears page between page changes
  if(oldpage != LCDpage)
  {
    lcd.clear();
    firstprint = true;  //  primes first print, to explain what information will be shown on the page
  }

  //  recipe name page
  if(LCDpage == 0)
  {
    oldpage = LCDpage;
    
    lcd.setCursor(0,0);
    lcd.print("Recipe");
    lcd.setCursor(0,1);
    lcd.print(recipe);

    lcd.setCursor(15,0);
    lcd.print(LCDpage);
  }
  
  // Main page, displays sensor information
  if(LCDpage == 1)
  {
    if(analogRead(button) < 1000)
    {
      delay(5);

      if(analogRead(button) <= 5) //  right button is hit, move to next bubble page
      {
        bubble_page = 2;
      }
      else if(analogRead(button) >= 500 && analogRead(button) <= 510) //  left button is hit, move to first bubble page
      {
        bubble_page = 1;
      }
    }

    if(oldbubblepage != bubble_page)
    {
      lcd.clear();
    }
    
    if(firstprint == true)
    {
      lcd.clear();
      
      lcd.home();
      lcd.print("Sensor Info");
      delay(1000);

      lcd.clear();

      firstprint = false;

      oldpage = LCDpage;
    }

    if(primary_state == true) //  primary state sensor information
    {
      if(bubble_page == 1)
      {
        oldbubblepage = bubble_page;
        
        lcd.setCursor(0,0);
        lcd.print("Temp: ");
        lcd.print((int)steinhart);
        lcd.print(", ");
        lcd.print((int)primary_temp_avg);
        lcd.setCursor(0,1);
      }
      else if(bubble_page == 2)
      {
        oldbubblepage = bubble_page;
        
        lcd.home();
        lcd.print("Set point: ");
        lcd.print(setpoint);
        lcd.setCursor(0,1);
      }
    }
    else if(secondary_state == true)  //  secondary state sensor information
    {
      if(bubble_page == 1)
      {
        oldbubblepage = bubble_page;
        
        lcd.setCursor(0,0);
        lcd.print("Temp: ");
        lcd.print((int)steinhart);
        lcd.print(", ");
        lcd.print((int)secondary_temp_avg);
        lcd.setCursor(0,1);
      }
      else if(bubble_page == 2)
      {
        oldbubblepage = bubble_page;
        
        lcd.home();
        lcd.print("Set point: ");
        lcd.print(setpoint);
        lcd.setCursor(0,1);
      }
    }
    else if(bottle_state == true) //  nothing to display in bottle stage
    {
      lcd.home();
      lcd.print("Move along...");
      lcd.setCursor(0,1);
      lcd.print("No info to show");
    }

    if(bubble_page == 1 && primary_state == true || secondary_state == true)
    {
      lcd.print("Bubble: ");
      lcd.print(last_bubble_time);  //  time since the last airlock activity
      lcd.print(" sec");
      lcd.print("  ");
    }
    else if(bubble_page == 2 && primary_state == true || secondary_state == true)
    {
      lcd.print("Max: ");
      lcd.print((int)last_bubble_time_max/60);  //  longest interval between airlock activities, in min
      lcd.print(" min");
      lcd.print("  ");
    }

    lcd.setCursor(15,0);
    lcd.print(LCDpage);
  }
  
  // Displays primary fermentation time
  else if(LCDpage == 2)
  {
    if(firstprint == true)
    {
      lcd.clear();

      lcd.home();
      lcd.print("Primary");
      lcd.setCursor(0,1);
      lcd.print("Fermentation");
      delay(1000);

      lcd.clear();

      firstprint = false;

      oldpage = LCDpage;
    }
    
    lcd.setCursor(0,0);
    lcd.print("Min:");
    lcd.print(primary_mins);
    lcd.print(" ");
    lcd.setCursor(7,0);
    lcd.print("Hour:");
    lcd.print(primary_hours);
    lcd.print(" ");
    lcd.setCursor(0,1);
    lcd.print("Day:");
    lcd.print(primary_days);
    lcd.print(" ");
    lcd.setCursor(7,1);
    lcd.print("Week:");
    lcd.print(primary_weeks);
    lcd.print(" ");

    lcd.setCursor(15,0);
    lcd.print(LCDpage);
  }

  // Displays secondary fermentation time
  else if(LCDpage == 3)
  {
    if(firstprint == true)
    {
      lcd.clear();

      lcd.home();
      lcd.print("Secondary");
      lcd.setCursor(0,1);
      lcd.print("Fermentation");
      delay(1000);

      lcd.clear();

      firstprint = false;

      oldpage = LCDpage;
    }

    lcd.setCursor(0,0);
    lcd.print("Min:");
    lcd.print(secondary_mins);
    lcd.print(" ");
    lcd.setCursor(7,0);
    lcd.print("Hour:");
    lcd.print(secondary_hours);
    lcd.print(" ");
    lcd.setCursor(0,1);
    lcd.print("Day:");
    lcd.print(secondary_days);
    lcd.print(" ");
    lcd.setCursor(7,1);
    lcd.print("Week:");
    lcd.print(secondary_weeks);
    lcd.print(" ");

    lcd.setCursor(15,0);
    lcd.print(LCDpage);
    
  }

  // Displays bottle time
  else if(LCDpage == 4)
  {
    if(firstprint == true)
    {
      lcd.clear();

      lcd.home();
      lcd.print("Bottle time");
      delay(1000);

      lcd.clear();

      firstprint = false;

      oldpage = LCDpage;
    }

    lcd.setCursor(0,0);
    lcd.print("Min:");
    lcd.print(bottle_mins);
    lcd.print(" ");
    lcd.setCursor(7,0);
    lcd.print("Hour:");
    lcd.print(bottle_hours);
    lcd.print(" ");
    lcd.setCursor(0,1);
    lcd.print("Day:");
    lcd.print(bottle_days);
    lcd.print(" ");
    lcd.setCursor(7,1);
    lcd.print("Weeks:");
    lcd.print(bottle_weeks);
    lcd.print(" ");

    lcd.setCursor(15,0);
    lcd.print(LCDpage);
  }

  // Displays overall time
  if(LCDpage == 5)
  {
    if(firstprint == true)
    {
      lcd.clear();

      lcd.home();
      lcd.print("Overall time");

      delay(1000);

      lcd.clear();

      firstprint = false;

      oldpage = LCDpage;
    }

    lcd.setCursor(0,0);
    lcd.print("Min:");
    lcd.print(total_mins);
    lcd.print(" ");
    lcd.setCursor(7,0);
    lcd.print("Hour:");
    lcd.print(total_hours);
    lcd.print(" ");
    lcd.setCursor(0,1);
    lcd.print("Day:");
    lcd.print(total_days);
    lcd.print(" ");
    lcd.setCursor(7,1);
    lcd.print("Weeks:");
    lcd.print(total_weeks);
    lcd.print(" ");

    lcd.setCursor(15,0);
    lcd.print(LCDpage);
  }

  //  prompts user of changing beer stage, select to cancel, right to accept
  if(LCDpage == 11)
  {
    lcd.setCursor(0,0);
    lcd.print("Start secondary?");
    lcd.setCursor(0,1);
    lcd.print("No");
    lcd.setCursor(4,1);
    lcd.print("Yes");

    lcd.setCursor(14,1);
    lcd.print(LCDpage);

    oldpage = LCDpage;
    firstprint = false;
  }

  else if(LCDpage == 12)
  {
    lcd.setCursor(0,0);
    lcd.print("Start bottle?");
    lcd.setCursor(0,1);
    lcd.print("No");
    lcd.setCursor(4,1);
    lcd.print("Yes");

    lcd.setCursor(14,1);
    lcd.print(LCDpage);

    oldpage = LCDpage;
    firstprint = false;
  }

  else if(LCDpage == 13)
  {
    lcd.setCursor(0,0);
    lcd.print("Finished?");
    lcd.setCursor(0,1);
    lcd.print("No");
    lcd.setCursor(4,1);
    lcd.print("Yes");

    lcd.setCursor(14,1);
    lcd.print(LCDpage);

    oldpage = LCDpage;
    firstprint = false;
  }

  else if(LCDpage == 14)
  {
    lcd.home();
    lcd.print("Change setpoint?");
    lcd.setCursor(0,1);
    lcd.print("No");
    lcd.setCursor(4,1);
    lcd.print("Yes");

    lcd.setCursor(14,1);
    lcd.print(LCDpage);

    oldpage = LCDpage;
    firstprint = false;
  }
}

void buttonfunction()
{
  // Boolean used for keeping buttons from running away
  if(button_press == false)
  {
    // Up button pressed, increment page value
    if(menu == 0 && analogRead(button) >= 140 && analogRead(button) <= 145)
    {
      if(LCDpage < 5)
      {
        ++LCDpage;
      }
    }

    // Down button pressed, decrement page
    else if(menu == 0 && analogRead(button) >= 325 && analogRead(button) <= 330)
    {
      if(LCDpage > 0)
      {
      --LCDpage;
      }
    }

    // Select button pressed, enter menu
    if(menu == 0 && analogRead(button) >= 738 && analogRead(button) <= 743)
    {
      menu = 1;
      
      if(LCDpage == 1)
      {
        LCDpage = 14;
      }
      if(LCDpage == 2)
      {
        LCDpage = 11;
      }
      else if(LCDpage == 3)
      {
        LCDpage = 12;
      }
      else if(LCDpage == 4)
      {
        LCDpage = 13;
      }
    }
    else if(menu == 1 && analogRead(button) >= 738 && analogRead(button) <= 743)
    {
      menu = 0;
      LCDpage = 1;
    }

    // Up button pressed, increment page value
    if(menu == 1 && analogRead(button) >= 140 && analogRead(button) <= 145)
    {
      if(LCDpage < 13)
      {
        ++LCDpage;
      }
    }

    // Down button pressed, decrement page
    else if(menu == 1 && analogRead(button) >= 325 && analogRead(button) <= 330)
    {
      if(LCDpage > 10)
      {
      --LCDpage;
      }
    }

    // Right button pressd
    if(menu == 1 && analogRead(button) <= 5)
    {
      if(LCDpage == 11)
      {
        secondary_state = true;
        primary_state = false;

        menu = 0;
        LCDpage = 3;
      }

      else if(LCDpage == 12)
      {
        bottle_state = true;
        secondary_state = false;
        
        menu = 0;
        LCDpage = 4;
      }

      else if(LCDpage == 13)
      {
        bottle_state = false;
        finish_state = false;
        
        menu = 0;
        LCDpage = 5;
      }
      else if(LCDpage == 14)
      {
        delay(250);
        setpoint = changesetpoint(setpoint);

        menu = 0;
        LCDpage = 1;
      }
    }
  }

  // Turns off button_press, so this loop
  // is not called again, without another button press
  button_press = true;
}

int changesetpoint(int value)
{
  //  controls when the change is complete
  boolean complete = false;

  //  loops until the user is done changing the setpoint
  while(complete == false)
  {
    //  used to clear the LCD once
    static boolean cleared = false;

    if(cleared == false)
    {
      lcd.clear();

      cleared = true;
    }

    //  displays setpoint and exit info
    lcd.home();
    lcd.print("Setpoint: ");
    lcd.print(value);
    lcd.setCursor(0,1);
    lcd.print("Right to save");
  
    if(analogRead(button) < 1000)
    {
      delay(5);

      //  up button is pressed, increment setpoint
      if(analogRead(button) >= 140 && analogRead(button) <= 145)
      {
        value++;
        delay(250);
      }
      
      //  down button is pressed, decrement setpoint
      else if(analogRead(button) >= 325 && analogRead(button) <= 330)
      {
        value--;
        delay(250);
      }

      //  right button is pressed, save setpoint and exit
      if(analogRead(button) <= 5)
      {
        cleared = false;
      
        return value;

        complete = true;
      }
    }
  }
}

// Wait loop
void wait()
{
  while(analogRead(button) > 1000)
  {
    lcd.setCursor(0,0);
    lcd.print("Press a button");
    lcd.setCursor(0,1);
    lcd.print("when ready");
  }
}
 
Back
Top