Arduino Sous Vide Machine: Part 2

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!

sous vide_fritz
The wiring scheme

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.

The Assembly

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.

sousvideconsole2
This console output will refresh every 30 seconds with an updated temperature, time remaining, and whether the heater is on or off.

The Code

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[1] + ': '))
    if (scale[0] == '1' and cook_temp > 100) or (scale[0] == '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[0] == '1' and cook_temp < 25) or (scale[0] == '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[2]) + ' degrees ' + scale[1] +': ')))       if deviation > 5:
	print '\n\nERROR: Deviation of ' +str(deviation) + ' degrees ' + scale[1] + ' 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[2] = 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[1] ,
    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[0] == '1':
    current_temperature = temp - 272.15

  # TEMP IN FAHRENHEIT
  if scale[0] == '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[1] + '\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[2]): 	# 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[2]): # 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()

Features

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

What’s Next

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.

Limitations

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:

sous vide premodification

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).

I cooked my chicken for more than 1 hour. Scale abbreviated to better match above graph.
I cooked my chicken for more than 1 hour. Scale abbreviated to better match above graph.

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).

Future Updates

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.

Leave a Reply

Your email address will not be published. Required fields are marked *