Note: For an updated version with simpler code and important information about hardware issues see this new post: Arduino Sous Vide, Version 2
Darwin Award Disclaimer: Playing with electricity around big containers of water can be dangerous!
After several successful cooks with the sous vide machine and ironing out a few software bugs, I decided it’s good enough to post. Like my other entries, it’s hardly pythonic, but gets the job done and it’s not all that bad for two months of self taught lackadaisical python study.
Photos to follow this post
I will upload some photos when I get a chance in the coming weeks.
You will need the parts listed in Part 1. The machine is assembled relatively easily from here. The immersion heaters plug into the GFCI relay box. The relay should be connected to the arduino with M to F jumper wires as pictured in the first post.
The thermistor should be enclosed in something that will lend additional waterproofing (partially sealing it in a piece of vacuum bag is a good idea or any old ziploc). The thermistor is connected to the arduino as depicted in the image above. Don’t bother moving on unless you know the resistance of your thermistor at 25C and have determined your specific resistor’s Steinhart-Hart equation coefficients as described in this post. When you have these values, you will need to add them to the code below. (My values will work fine for the 4.7k ohm epoxy thermistor linked in Part 1).
The binder clips mentioned in Part 1 are useful for holding the Thermistor / waterproofing in place and keeping food filled vacuum bags in place when needed. The coat hanger / safety wire is used to hold the immersion heaters at the proper place in the container. I used two and have the heaters pinched in between. It’s important to not submerge them completely, but leave some space between the water and the start of the electrical cord. The aquarium pump goes at the bottom along a wall of the container to ensure constant heat redistribution. The container lid is not necessary, but can be placed on top once you’ve started.
Code available for download. Hack it, improve it, MIT license yada yada yada…
# ARDUINO SOUS VIDE # v 0.1 # Simple arduino sous vide program # Controls relay based on thermistor readings import time import os import pyfirmata from math import log status = 'off' def slp(): ''' INSERTS 5 SECOND DELAY ''' time.sleep(5) def cls(): ''' CLEARS CONSOLE BETWEEN READINGS AND USER ENTRIES ''' os.system('clear') # SET COOKING PARAMETERS def setup(): ''' SET UP COOKING PARAMETERS SUCH AS TEMP SCALE, TEMP, LENGTH OF TIME, PRECISION ''' global cook_temp global cook_time global end_time global cook_temp_f global scale # CHOOSE TEMPERATURE SCALE AND COOKING TEMP n = True while n == True: cls() # SELECT TEMPERATURE SCALE scale = raw_input("Select temperature scale\n\n 1) Celsius \n\n 2) Fahrenheit \n\nEnter your selection: ") scale = scale.strip() if scale == '1': scale = [scale, 'Celsius', 1.0] # scale list is temperature scale selection from menu, followed by name of scale, and last value is number of +/- degrees deviation allowed from cook_temp in cook() elif scale == '2': scale = [scale, 'Fahrenheit', 2.66] else: print '\n\nERROR: You must enter 1 for Celsius or 2 for Fahrenheit\n\n' slp() continue cls() # SELECT COOKING TEMPERATURE; DON'T ALLOW NONSENSE ENTRIES SUCH AS < ROOM TEMP OR > BOILING POINT cook_temp = float(raw_input('Enter desired cooking temperature in degrees ' + scale + ': ')) if (scale == '1' and cook_temp > 100) or (scale == '2' and cook_temp > 212): print '\n\nERROR: Temperature may not be higher than boiling point of water (BP: 100 C, 212 F)\n\n' slp() continue elif (scale == '1' and cook_temp < 25) or (scale == '2' and cook_temp < 77): print '\n\nERROR: Temperature may not be lower than room temperature (RT: 25 C, 77 F)\n\n' slp() continue try: # SET ALLOWED TEMPERATURE DEVIATION. DON'T ALLOW DEVIATION BEYOND +/- 5 DEGREES. deviation = float((raw_input('Enter allowed +/- degrees deviation from setpoint. Press enter for default of ' +str(scale) + ' degrees ' + scale +': '))) if deviation > 5: print '\n\nERROR: Deviation of ' +str(deviation) + ' degrees ' + scale + ' is too great. Reduce your value to 5 degrees or lower for greater precision cooking.' print '\n\nExample: cook temp of 70 degrees C with 5 degrees deviation will allow a low temp of 65 C to be achieved before heaters are turned on and a high temp of 75 C before heaters are shut off' slp() continue scale = deviation except: pass # SET COOKING TIME LENGTH IN MINUTES cook_time = raw_input("Enter desired cook time in minutes: ") cook_time = float(cook_time) * 60 start_time = time.time() end_time = start_time + cook_time cls() # SETUP VERIFICATION / START print 'COOKING SETTINGS' print '\n\nSet for ' +str(cook_temp) +' degrees ' + scale , print 'for ' +str(cook_time / 60) +' minutes' print time.strftime('Cooking will complete at %a %I:%M %p', time.localtime(end_time)) #convert end_time seconds to readable day H:m format start = raw_input('\n\nDo you wish to continue with current settings? ') if start.lower() == 'n' or start.lower() == 'no': continue if start.lower() == 'y' or start.lower() == 'yes': n = False def connection(): ''' CONNECT TO ARDUINO ''' global pin0 global pin4 global board n = 0 # first port number to try ie: ttyACM0 PORT = '/dev/ttyACM' + str(n) # device location, yours may very. if unable to connect, try /dev/USB or check dmesg connected = False # ATTEMPT TO CONNECT TO ARDUINO. ITERATE THROUGH 1000 PORT NUMBERS UNTIL CONNECTED OR FAIL ON 1001. while connected == False: try: print 'Trying ' + PORT board = pyfirmata.Arduino(PORT) print 'Port found, connecting...' connected = True print "Connection established" except: n = n+1 if n > 1000: raise Exception("Error connecting to arduino. Check to make sure it's connected and check dmesg for correct location. If other than /dev/ttyACMx, edit line #102 of sousvide.py to reflect your location.") PORT = '/dev/ttyACM' + str(n) pass pin0 = board.get_pin('a:0:i') # edit to reflect your setup pin4 = board.get_pin('d:4:o') it = pyfirmata.util.Iterator(board) it.start() pin4.write(0) # start with pin off pin0.enable_reporting() print 'Waiting on reading...' while pin0.read() is None: # ignore input until pin is active pass def temp_check(): ''' FUNCTION THAT POLLS ANALOG PIN FOR TEMPERATURE, USED EVERY 30 SECONDS ''' global current_temperature current_temperature = '' # reset temperature each time while pin0.read() is None: # ignore input until pin is active print 'waiting on thermistor reading...' pass analog_value = pin0.read() # VOLTAGE DIVIDER CALCULATION analog_value = analog_value * 5 analog_value = 4700.0 * ((5.0/analog_value) -1.0) # make sure you use the correct number here reflecting your thermistor. ie: for 10k ohm resistor, replace 4700 with 10000 # STEINHART-HART EQUATION temp = (1 / (0.001308463361 + 0.0002344771590 * log(analog_value) + 0.0000001041772095 * log(analog_value)**3)) # substitute your thermistor's unique values for A, B, C. # CONVERT TEMPS FROM KELVIN # TEMP IN CELSIUS if scale == '1': current_temperature = temp - 272.15 # TEMP IN FAHRENHEIT if scale == '2': current_temperature= 9/5.0*(temp-272.15) + 32 # TIME / TEMPERATURE LOGGING f = open('/tmp/sousvide.log', 'a') f.write(str(time.time()) + ' ' + str(current_temperature) + '\n') f.close() # STATUS REPORTING TO CONSOLE cls() print 'ARDUINO SOUS VIDE v 0.1' print '-'*80+'\n\n' print ' '*5 + 'Set temperature: ' + str(cook_temp) + '\n\n' print ' '*5 + 'Current temperature: ' + str(current_temperature) + ' ' + scale + '\n\n' print ' '*5 + 'Heating element(s): ' + status + '\n\n' print ' '*5 + 'Time remaining: ' + str((end_time - time.time())/60.0) + ' minutes\n\n\n\n' print '-'*80 slp() def cook(): while True: global status while time.time() < end_time: # continue cook function until time up temp_check() if current_temperature < (cook_temp - scale): # start heating element every time temp drops X degrees below goal temp pin4.write(0) # START HEAT board.pass_time(5) time.sleep(20) status = 'on' elif current_temperature > (cook_temp + 0.4 * scale): # kill heating element every time temp goes 0.5X degrees over goal temp pin4.write(1) # KILL HEAT board.pass_time(5) time.sleep(20) status = 'off' else: # if temperature is within X*2 degrees (-X goal +X) of goal temp, continue with whatever is currently happening board.pass_time(5) time.sleep(20) pass if time.time() > end_time or time.time() == end_time: pin4.write(1) cls() print "all finished, killing heat..." slp() board.exit() exit() setup() connection() cook()
The code offers the following features:
- Fahrenheit and Celsius heating options
- Status updates every 30 seconds
- Control over hysteresis with modifiable temperature range from setpoint (the deviation option)
- Automatic logging of temperature and time to /tmp/sousvide.log
Now to get cooking, there are some excellent resources online. Douglas Baldwin offers some of the most oft quoted resources. He has some good information on his website, including some carefully calculated pasteurization tables that if followed, let you cook food low and slow while still getting all the nasties.
For a more general overview, you can check out his presentations that he did for the American Chemical Society, Sous Vide Cooking and Chemistry and Giving Thanks for the Water Bath: Sous Vide Cooking for the Holidays.
This set up and code is good to go as-is ‘out of the box’. FYI: timer starts immediately after accepting cook settings. You must take into account preheat time (approximately one hour to obtain 60C from room temp water, see ‘future updates’ section below).
A little fine tuning may be helpful to get the most precise and near constant temperature. In order to avoid a constant on/off, a hysteresis curve is simulated in the cook() function. After about 6 cooks, I’ve gotten it calibrated pretty well. It originally was giving some wild peaks due to the fact that the heaters continue to heat after shutting off.
Case in point:
I found that reducing the deviation variable to 40% for the cut off temp and reducing the temperature check interval by 10s greatly improved the outcome. I may go one step further and reduce the polling interval by another 5s to try to get equidistant ‘peaks’ to ‘troughs’ from setpoint temp).
If your sous vide cooking relies on even more precise cooking that deviates less than 1C from setpoint, feel free to fiddle with the cook() function and/or set deviation to something very low (< 0.5 degrees).
I ran out of time in the short run to make any new updates to this. For my own satisfaction, I will likely add a preheat function from the data gleaned in the last post that will preheat the water bath and once preheated, start the timer from there. Should be a quick fix. As an exercise in dealing with formatting Python time strings, I may make the time input a little smarter with options other than minutes.
I’m also considering modifying the code to make use of PyPi and the Raspberry Pi via its GPIO pins.