update: 11/25/2018
Beeruino is an Arduino Mega 2560 based “Data Logger” / “Controller” for the fermentation process of brewing/making (beer, mead, wine, cider, etc…).
- F.A.Q.
What does it mean to be a “Data Logger” ?
The Beeruino logs temperature data of the fermentation, also external temperature is logged so you have a baseline to compare against.
What can you do with the logged data ?
The data is stored on the memory card, after fermentation you can remove it and then process the data however you want, meaning using whatever you want/know. It can be anything as simple as Excel or something more involved like R, Python, or online tools like Plotly, pretty much any software that can use to process the data file and help you to analyze and plot to show what happened during the fermentation. The data file is just a simple text file with columns and row data.
What does it mean to be a “Controller” ?
When I first started out this project, I just wanted to log some temperature data to generate some plots and see how different yeast behave. It will tell you a lot, you should be able to identify each stage of the fermentation and much more…
Later on I realized that I also needed a way to control fermentation temperature, so I added a relay and a heating element into the project, updated the code and added up/down control buttons, so that you can set the Goal temperature without updating the code. The default goal temperature is set to 68F, but you can change that.
What is the current software release ?
The current program sketch is up to Version 3 / meaning that the UP/DOWN buttons for temperature Goal change and DATA RESET work. DATA RESET allows you to delete the log data without removing the memory card.
Next for Version 4 – develop a way to transfer the Log data over the USB cable, so that the memory card doesn’t have to be removed at all.
How do I develop ?
I usually add new options/update software when the weather is bad and ugly, meaning late Fall/Winter/Early Spring – for the rest of the year I don’t do anything usually… If there is an update, you can expect it to be during this time frame, if you want to join the development effort, hit me up – this project is open.source.
what is needed:
- schematics for all the pins, ground, power, connections…
- if you are a C++ “professional” / it would help to look over my code, offer guidance, etc… – I am an amateur C++ programmer
Is there an overview Youtube video ?
YouTube Video of Version 3 is here: https://www.youtube.com/watch?v=2A-R9e-u-qY
How come you removed the project from GitHub ?
I have removed the project from Github since it was taken over by Microsoft and I am not too keen about that idea. Frankly, the Project is small enough that it doesn’t need that anyway at this stage and I don’t have time to mess around with git commands, if this was a larger project, it would be a different story.
I would like to get started but I have no experience with Arduino, electronics or programming in general, do I offer consulting services ?
This website and this project is a hobby, I don’t sell anything or make any money, however having said that if you can’t figure out how to get going on your own and if you “really” want to, you can hire me for some consulting, use the Contact form or hit my up on Twitter @kodiak_seattle
Do I have any tips of how to approach this project from scratch and make it work ?
Yes, first get all the dependency libraries installed if you choose to replicate all the features from this project or the ones that you want. Once your code compiles without any dependency errors, then you can start to add all the hardware one by one:
- LCD Screen
- Buttons
- Relay
- Temperature Sensors
- RTC – Real Time Clock
Adding one by one and confirming it work is probably the best way.
I used a Arduino Mega, an Ethernet shield (for the memory card) and a prototyping shield (in that order) as far as the hardware goes.
Where is the code ?
The latest code is below, it compiled and verified fine before it was posted, again you will have to satisfy any hardware dependencies…
I have commented a lot of lines, probably more than I should.
// kodiakbrewing.com // Version 1.003 Last Update 10/5 - 2018 // temperature is in Fahrenheit, Celsius support will come in the future... // DECLARE all files/drivers needed for all the hardware: // 1 - Realtime clock, 2 - Dallas 1-wire temperature sensors, 3 - LCD over I2C, 4 - Relays, 5 - SD Card for Logger // 1 Real Time Clock #include <RTClib.h> // RTC (real-time-clock) RTC_DS3231 rtc; // 2 - Dallas 1-wire temperature sensors // The resolution of the temperature sensor is user-configurable to 9, 10, 11 or 12 bits // correspoding to increments of 0.5 C, 0.25 C, 0.125 C and 0.0625 C respectively. // The default resolution is 12-bit, read if you want to understand more or to change the default: // http://www.homautomation.org/2015/11/17/ds18b20-how-to-change-resolution-9101112-bits/ #include <Wire.h> #include <OneWire.h> #include <DallasTemperature.h> // Data wire is plugged into pin 2 on the Arduino #define ONE_WIRE_BUS 2 // Setup a oneWire instance to communicate with any OneWire devices // (not just Maxim/Dallas temperature ICs) OneWire oneWire(ONE_WIRE_BUS); // Pass our oneWire reference to Dallas Temperature. DallasTemperature sensors(&oneWire); // 3 - LCD Liquid Crystal Display 4x20 lines over I2C Bus #include <LiquidCrystal_I2C.h> LiquidCrystal_I2C lcd(0x27,20,4); // LCD address 0x27, 20 characters by 4 line display // 4 - Relays, Setup digital pin 7 to control relay 2 int in1 = 7; int relay1status; // 5 - SD Card for Logger #include <SPI.h> #include <SD.h> File myFile; File dataFile; char file_name[] = "FILE01.TXT"; const int chipSelect = 4; // used by the SD card int counter = 0; // VARIABLES for everything else unsigned long currentMillis = millis(); // stores the value of millis() in each iteration of loop() // timing variables for the SD Card, record value every 10 seconds to the SD Card unsigned long previousSDMillis = 0; const int SDinterval = 20000; // 1000 = 1 second. // timing variables for the Buttons, check button state every 1 second as a good start unsigned long previousBUTTONSMillis = 0; const int BUTTONSinterval = 500; // Interval is set to half a second, so wait that long between pressing a different up/down button // BUTTON Variables on the front of the case // currently there are two buttons: up and down to set and control goal temperature of Relay #2 (Heat Control) // in the future I will test on Relay #1 (Cooling Control or Other) and add a control select button to switch between Relay 1 and 2 int up_button = 6; // up button on PWM pin 6 int down_button = 8; // down button on PWM pin 8 int select_button = 9; // select or "ok" button on pin 9 int up; // variable for reading the pin status int down; int selectbutton; // Relay #2 (heating) temperature variable int internal_temp_goal = 68; // Minimum Goal temperature on internal temperature sensor // you can change this to whatever you want // #include <MemoryFree.h> // this helps us to debug when we get weird behavior, to see if we run out of Memory (dynamic memory) #define F(string_literal) (reinterpret_cast<const __FlashStringHelper *>(PSTR(string_literal))) void setup(void) { lcd.init(); // initialize the lcd Serial.begin(9600); // start serial port for use with monitor, after testing, comment out, since we use LCD screen, free up a bunch of ram delay(3000); // wait for console opening // Start up the library sensors.begin(); lcd.backlight(); lcd.setCursor(0,0); lcd.print(F("BEERUINO Ver. 1.003")); lcd.setCursor(0,1); lcd.print(F("********************")); lcd.setCursor(0,2); lcd.print(F(" kodiakbrewing.com")); if (!SD.begin(4)) { lcd.setCursor(0,3); lcd.print(F("SD Card init failed!")); } else if (SD.begin(4)) { lcd.setCursor(0,3); lcd.print(F("SD Card init. 'ok'. ")); } delay(5000); lcd.clear(); // this clears the screen so that everything displays properly in the Void. // setup the switch pins for the buttons pinMode(up_button, INPUT); pinMode(down_button, INPUT); pinMode(select_button, INPUT); // setup the relay 2 pinMode(in1, OUTPUT); digitalWrite(in1, HIGH); // set the RELAY to OFF by default } void loop(void) { currentMillis = millis(); // capture the latest value of millis() DateTime now = rtc.now(); sensors.requestTemperatures(); // Setup the Variables for Date and Time components char buffer[21]=""; // this is the buffer space used to print using the sprintf int YEAR = now.year(); int MONTH = now.month(); int DAY = now.day(); int HOUR = now.hour(); int MINUTE = now.minute(); int SECOND = now.second(); // do main things to display on LCD screen lcd.backlight(); lcd.setCursor(0,0); lcd.print("Lg:" + String(counter)); // Lg = Log lcd.setCursor(0,1); sprintf(buffer, "%04d/%02d/%02d %02d:%02d:%02d ", YEAR, MONTH, DAY, HOUR, MINUTE, SECOND ); lcd.print(buffer); // consolidated both External and Internal Temps to one line in the Display, to free up one line lcd.setCursor(0,2); lcd.print("IT " + String(sensors.getTempCByIndex(1) * 1.8 + 32.0) + " ET " + String(sensors.getTempCByIndex(0) * 1.8 + 32.0)); lcd.setCursor(0,3); lcd.print("file size: " + String(dataFile.size(), DEC) ); // make a string for assembling the data to log: String external_temp = ""; String internal_temp = ""; // Create an indicator for HEATER ON/OFF and add it to the data.set file // ON = Y OFF = N char HTR_IND = ' '; if ( relay1status == 1 ) { HTR_IND = 'N'; } else if ( relay1status == 0 ) { HTR_IND = 'Y'; } // read the two sensors and append to the string: // we are using index(0) to be external and index(1) to be internal to sensor reference external_temp = String(sensors.getTempCByIndex(0) * 1.8 + 32.0); internal_temp = String(sensors.getTempCByIndex(1) * 1.8 + 32.0); // if the file is available, write to it: if ((unsigned long) (currentMillis - previousSDMillis) >= SDinterval) { File dataFile = SD.open(file_name, FILE_WRITE); if (dataFile) { lcd.setCursor(0,3); lcd.print("file size: " + String(dataFile.size(), DEC) ); dataFile.println(String(counter) + "," + HTR_IND + "," + external_temp + "," + internal_temp + "," + buffer); dataFile.close(); previousSDMillis = currentMillis; counter++; } else { lcd.setCursor(0,3); lcd.print(F("No SD access...2.. ")); } } // The nice thing about having Processes split out into Indepedent Functions // Is that is you want to comment something out for testing purposes // all you have to do is comment out the 1 line calling the Function like: // BUTTONS(); // call the buttons Function and use millis() to control BUTTONS(); // if you want Internet of Things, call the IOT() Function IOT(); // call the Relays Function where all Logic for Relay #1 and #2 happens (setup for only 1 relay currently) RELAYS(); } void RELAYS() { // logic for the relay on/off - Currently setup as a Heating Relay, but you can reverse the check condition to >= for Cooling... // or have dual-Relay module, with each dedicated to Heating or Cooling // It is recommended that the Relay module is setup to receive its own dedicated power/ground, and "not" from the Arduino // We are using a +/- 0.50 F Windows to stop oscillation, to balance the temperature - reduces relay use and increases it's life // hysteresis (referred to as a window above) is the simple thermostat technique. // It is used in both mechanical and low cost thermometers. It is simple, reliable, and requires no training unlike PID. // The digitalRead() function returns an integer – either 1 or 0. This value is then assigned to the variable relay1status. // If it is 1, the voltage at the relay is HIGH or OFF // If if is 0, the voltage at the relay is LOW or ON // It works like this: // In the first iteration, as soon as the condition == 1 and temp <= VMin it turns on the Relay // and of course the relay1status is no longer 1, but 0, so that is executed only once. // // Once the temp raises above the VMax and it is already 0, the Relay Turns Off // // The window is 1 degree F, if you want this lower, change the code. relay1status = digitalRead(in1); float internal_temp = (sensors.getTempCByIndex(1) * 1.8 + 32.0); float VMax = (internal_temp_goal + 0.5); float VMin = (internal_temp_goal - 0.5); // when a sensor is un-plugged, the error message from the sensor is converted to read: -196.60 // so the first IF accounts for this possibility and for good measure we added (internal_temp > 0) under else // this way the Relay is safely turned off and the fermentor beer doesn't boil over // if ( internal_temp = -196.60 ) { digitalWrite(in1, HIGH); } if ( (relay1status == 1) && (internal_temp > 0 && internal_temp <= VMin) ) { digitalWrite(in1, LOW); // turn of the Relay ON } else if ( (relay1status == 0) && (internal_temp >= VMax) ) { digitalWrite(in1, HIGH); // turn the Relay OFF } // To deal with displaying the RELAY STATUS since BUTTONS() would clear RELAY Display inside the CONDITION IF-ELSE (if it was there) if ( relay1status == 1 ) { lcd.setCursor(9,0); lcd.print("RLY OFF " + String(internal_temp_goal) ); } else if ( relay1status == 0 ) { lcd.setCursor(9,0); lcd.print("RLY ON " + String(internal_temp_goal) ); } //Serial.print("below_IF" + String(relay1status));Serial.print("\n"); } void BUTTONS() { // logic-code for the up/down button within the loop up = digitalRead(up_button); // read the state of the button Value and store it into a variable (val) down = digitalRead(down_button); selectbutton = digitalRead(select_button); if (currentMillis - previousBUTTONSMillis > BUTTONSinterval) { if (up == HIGH) { // if value on pin 2 is high or 5 volts, // Serial.print( "UP " + String(up) ); // useful for hardware debug, otherwire comment out internal_temp_goal++; TEMP_SUB_DISPLAY(); previousBUTTONSMillis = currentMillis; } // Process for the DOWN Button if (down == HIGH) { // Serial.print( "DOWN " + String(down) ); // useful for hardware debug, otherwire comment out internal_temp_goal--; TEMP_SUB_DISPLAY(); previousBUTTONSMillis = currentMillis; } // Process for the SELECT or "OK" button, this button is not used for temperature adjustments, so that line of code is gone if (selectbutton == HIGH) { //Serial.print( "SELECT " + String(selectbutton) ); // useful for hardware debug, otherwire comment out RESET_SUB_DISPLAY(); } } } void IOT() { // LOT is nice to have, extra feature, but it is "not" part of the original project requirement, which is off grid capabilities // and internet independency, that's why we use a real-time clock for the date/time and an SD card to record all the data. // It is nice to learn - be prepared to spend some time here if you are new to all of this. // Optional #IoT (internet of things) to send live data over the internet to create live plots on plot.ly // data is sent to the Raspberry Pi over the USB cable.... // read more here - http://adilmoujahid.com/posts/2015/07/practical-introduction-iot-arduino-nodejs-plotly/ // value of this setup is that it uses very little code (and ram) on the Arduino side, and everything complex happens on the Pi side // un-comment below and setup per instructions if you want this... and adjust your temp. sensor int_ or ext_.. //double ext_temp = sensors.getTempCByIndex(0) * 1.8 + 32.0; // this will be plotted on the y axis //Serial.println(ext_temp); } void TEMP_SUB_DISPLAY() { // this code is executed if the UP/DOWN BUTTONS are pressed physically for temperature adjustments String whichbutton = ""; if (up == HIGH) { whichbutton="UP"; } else if (down == HIGH) { whichbutton="DOWN"; } lcd.clear(); lcd.setCursor(0,0); lcd.print( (whichbutton) + " Button Pressed:"); lcd.setCursor(0,2); lcd.print("SET TEMP IS: " + String(internal_temp_goal)); previousBUTTONSMillis = currentMillis; delay(1000); lcd.clear(); } void RESET_SUB_DISPLAY() { // this code is executed when the SELECT OR RESET BUTTON is pressed / its purpose is to help with Calibration, so once you want to start recording clean data ( example, after pitching yeast into the beer ) // it will clear "all" the data from the file.txt on the memory card // Normal function without Reset is that everytime the sytem powers ON, it will Append new Data - so if that is useful to you, don't RESET (you have both Worlds available to you) lcd.clear(); lcd.setCursor(0,0); lcd.print("ENTERING RESET MENU."); lcd.setCursor(0,1); lcd.print("One Moment..."); delay(3000); lcd.clear(); int start = 0; // setup a binary loop condition while ( start < 1 ) { lcd.setCursor(0,0); lcd.print(F("press CLEAR again")); lcd.setCursor(0,1); lcd.print(F("TO wipe DATA... or")); lcd.setCursor(0,2); lcd.print(F("DOWN/UP at same time")); lcd.setCursor(0,3); lcd.print(F("to Exit with nothing.")); selectbutton = digitalRead(select_button); down = digitalRead(down_button); up = digitalRead(up_button); if ( selectbutton == HIGH ) { lcd.clear(); lcd.setCursor(0,0); lcd.print("CLEARED DATA FILE..."); dataFile.close(); SD.remove(file_name); dataFile= SD.open(file_name,FILE_WRITE); delay(3000); start = 1; previousBUTTONSMillis = currentMillis; } if ( down == HIGH && up == HIGH ) { lcd.clear(); lcd.setCursor(0,1); lcd.print("Done nothing----"); lcd.setCursor(0,3); lcd.print("Exiting....."); delay(4000); start = 1; previousBUTTONSMillis = currentMillis; } } lcd.clear(); }