Arduino Sous Vide, Version 2.0

I previously built a sous vide machine based off Seattle Food Geek’s plans but with an arduino + relay in place of the PID. It ran off a shoddy script I wrote in Python to be used with Pyfirmata. The recommended Norpro heating devices in the Seattle Food Geek blog failed within 10 uses and the Pyfirmata script required a computer to be plugged into the arduino. It worked but was hardly optimal. This new sketch is written in C and doesn’t require Pyfirmata or an additional computer to be plugged into the arduino.

Very simple, works great!

The Sketch

// A simple arduino sous vide machine sketch

void setup() {

Serial.begin(9600);
}
void loop() {

// set pins and mode
// set relay pin and relay status to off
const int relay_pin = 2; 
int relay_status = LOW;
pinMode(relay_pin, OUTPUT);

int analogValue = analogRead(A0); // read the raw value for thermistor via analog pin0

// assign variables
float temperature; // the current temperature
float upper;       // the upper limit at which the relay & heat source shut off
float setpoint;    // the desired setpoint cooking temperature
float lower;       // opposite of upper
float steinhartA;  // steinhart-hart equation coefficients
float steinhartB;
float steinhartC;
float rt;          // thermistor resistance

setpoint = 60.0;   // desired cooking temperature in degrees C 
lower = setpoint - 1.0; // can manually edit cutoff values to affect hysteresis curve
upper = setpoint + 1.0;
steinhartA = 1.137699953e-3; // replace with your thermistor's unique steinhart-hart coefficients
steinhartB = 2.325339915e-4;
steinhartC = 0.9547893220e-7;

rt = 9910*(1023/float(analogValue)-1.0);
temperature = steinhartA + steinhartB*log(rt) + steinhartC*(log(rt)*log(rt)*log(rt));
temperature = (1.0/temperature)-273.15; // convert from K

Serial.println(temperature); // debug, print temp to serial

if (temperature >= upper)
{
relay_status = LOW; 
digitalWrite(relay_pin, relay_status);
Serial.println("RELAY OFF"); // debug, print status to serial
}

else if (temperature <= lower)
{
relay_status = HIGH;
digitalWrite(relay_pin, relay_status);
Serial.println("RELAY ON");
}

delay(30000);        //  30s delay between temperature reads
}

How to use

Use it with a thermistor, relay, and a heating device of your choice. Edit the setpoint temperature to your desired temp and be sure to edit the Steinhart-Hart values to reflect those of your thermistor. If you wish to change the temperature, you will need to upload an updated sketch (for now….).

My current setup

A few things have changed since my previous posts. Since my immersion heaters have failed, I had to find a new heating source. A commenter recommended a turkey roaster which sounds like a fantastic idea (and they look very similar to lab water baths which is a plus). I was averse to spending any more money on this project though so I scavenged around the house and am currently using a hot plate with a large stockpot on top. The stockpot holds an excess of water which helps maintain a more constant temperature and the pot + large volume of water retains heat better than the thin sterilite container used previously. Its greater size allows me to fit larger cuts of meat in it also which is a big plus. Getting heat up to the setpoint temperature no longer takes a century with the hotplate which is also very nice.

I am using a 10k ohm thermistor that I had laying around as I misplaced the old 4.7k ohm thermistor. I only mention it in case someone looks at any old posts and is left scratching their head over the sudden change in equation values.

I am using the same relay setup as described previously.

Future

I am waiting on an Amazon shipment to come with an LCD screen. Once that arrives (hopefully within another 3 weeks… I live in a remote location!), I will feature creep this sketch to include a live temperature readout and the ability to configure setpoint temperature without needing to modify the sketch and reupload.

Further resources:

My other posts on arduino sous vide

How to use a relay with an arduino

Seattle Food Geek sous vide machine

Everything you would ever want to know about using a thermistor with an arduino

Thermistor calculator

 

Barrow, Alaska Food Prices

I have recently relocated to Barrow, Alaska: America’s northern most city located several hundred miles inside the Arctic Circle. All food is flown in and as such I’ve been enjoying the cringe worthy and laugh out loud insanity of the food prices. I’ll often chuckle, laugh, or indignantly snort out loud at the outrageous prices when shopping. No one has looked at me strangely for doing so in the grocery store, so it’s likely a normal reaction for the recently relocated.

I aim to update this table with new Barrow food price entries as I buy or come across new products to help those who are thinking of accepting a job here get a better idea of their food expenses. I don’t care enough or have nearly enough time to continually go back and check old prices, so it will mostly be updated with the addition of new things.  One last note, I’m listing the prices at the regular price. Some of the items I have bought were at a sale price with a few cents off the listed price. There is usually one or two buy one get one items at the AC as well, those special prices have not been reflected in this table as they are the exception and not the rule.  Unless otherwise noted, all prices are from the AC Value Store.

Barrow Food Prices as of September to December 2014 at the AC Value Store

Item (Details) Brand Size Price
Bread (factory made) Orowheat 1.5 lbs $7.93
Laundry detergent Tide 1.3 gallons $49.99
Bread (French, ‘fresh made’ rebaked) AC store bakery $3.99
Frozen vegetables (Steamable) Birdseye 10.8 oz $4.15
Eggs 1 dozen $5.25
Bananas 1 lb $2.99
Candy bar Caramello $1.29
Meat, sliced (Processed meat, turkey) Oscar Meyer 12 oz $6.69
Vitamins (400 IU Vitamin D) Natures Bounty 100 tablets $5.69
Bottled water (500 mL bottles) Super Chill 24 bottles $29.95
Bottled water (500 mL bottles) Dasani 1 bottle $2.00
Laundry detergent Tide 50 oz $12.49
Cheese (colby jack) Crescent Valley 8 oz $6.39
Cheese (provolone) Crescent Valley 8 oz $6.39
Vegetable shortening Crisco 1 lb $5.99
Juice, Orange (frozen concentrated) Minute Maid 16 oz $6.73
Tomatoes 1 lb $4.79
Steak Sauce A1 5 oz $4.52
Milk (2%, lactose free) Darigold ½ gallon $8.85
Lettuce (Romaine, bagged prewashed) Fresh Express 9 oz $5.69
Salt, Kosher Morton’s 1 lb $4.52
Eggs 1.5 dozen $7.35
Watermelon (small, “personal watermelon” varies $19.99
Meat, sliced (Processed meat, turkey and pork) Oscar Meyer 1 lb $7.99
Sweet Potatoes 1 lb $3.29
Kale varies, one bunch $3.59
Strudels (frozen) Pillsbury 1 box $5.65
Onions, yellow 1 lb $2.99
Soy milk (unrefrigerated carton) Pacific 32 oz $5.89
Yogurt, Greek fruit on the bottom Chobani 1 cup $2.65
Baking powder Clabber girl 8.3 oz $4.39
Tomatoes, plum 1 lb $4.29
Pasta sauce, marinara Prego 23 oz $6.85
Beef, steak (NY Strip) 1 lb $15.99
Beef, ground (80% lean) 1 lb $5.68
Pasta, dry whole wheat (penne) Wild Harvest 12.1 oz $3.53
Soy milk (unrefrigerated carton) Kirkland Signature 32 oz $4.97
Potatoes (Russet) 10 lb $24.00
Cigarettes American Spirit (Blue) 1 box $11.65
Oranges, canned (Mandarin) Dole 15 oz $3.99
Mayonnaise Essential everyday 15 oz $5.49
Tuna, canned (chunk light in water) StarKist 5 oz $1.20
Raisins Sunmaid 6 oz $2.89
Kidney beans, canned Essential everyday 15 oz $2.99
Tilapia filets, frozen 1 lb $8.99
Chicken breast, canned (white) Swanson 4.5 oz $3.65
Bottled water (500 mL bottles) Nestle Pure Life 35 bottles $40.79
Marinade, bottled Lawry’s 12 oz $6.53
Apple cider (sparkling, nonalcoholic) Martinelli’s 20 oz $7.99
Cranberry sauce (canned) Oceanspray 14 oz $3.45
Cheese, shredded (Mexican mix) Kraft 8 oz $7.65
Cereal (generic) Malt-o-Meal 2 lbs 6 oz $11.99
Macaroni and Cheese (boxed) Kraft 5.5 oz $3.45
Soup, condensed (canned) Campbell’s 1 can $3.99
Butter (unsalted) Darigold 1 lb $7.49
Lettuce (Romaine) 1 bunch $4.99
Potato chips, kettle cooked (Jalapeno) Lay’s 8 oz $9.49
Potato chips, cheese puffs (Crunchy) Cheetos 9 oz $8.79
Potato crisps (BBQ) Pringles 1 tube $3.99
Rubbing alcohol (isopropyl) 16 oz $6.99
Ketchup Heinz 2 lbs $8.33
Mustard, yellow French’s 8 oz $3.19
Hot sauce Frank’s 12 oz $5.89
Bread (hot dog buns) Ball Park 8 ct $4.00
Fruit, mixed dried Sunmaid 7 oz $5.19
Hotdogs (Turkey) 3 lbs $13.99
Soda Varies 1 can $1.39
Tortillas (fajita) Mission 20 ct $4.99
Pizza sauce Ragu 14 oz $3.99
Barrow food prices
“LOW PRICE” Bottled Water

 

It may be cheaper to just buy new pair of underwear everyday like George Costanza
It may be cheaper to just buy new pair of underwear everyday like George Costanza

 A note on fresh fruits & vegetables:

The AC has a fantastic supply of fresh fruit and vegetables when viewed in the context of the challenging logistics of getting perishable produce on the shelf in the Arctic Circle. With that said, a lot of Barrow locals look to Full Circle Farms for their supply of fruits and veggies. Full Circle is a service that delivers fresh organic produce on a weekly basis. It’s one of those  internet services that sends a box of goodies that you can modify and that you receive weekly. It’s based out of Washington state and the boxes spend a minimum amount of time in transit. I started using it and have been happy with the service thus far. I’m not convinced that the price is really cheaper than buying in Barrow, but the variety is excellent and I suppose that it is a reasonable price for organic produce flown into the nether regions of Alaska. See their website for more information on their various packages and tiers.

Pygal US Map fork progress

I’m happy to report some progress in adding an option in pygal for the generation of SVG maps of the United States. There’s a few snags that I need to work out, but it’s 90% there.

I’m going to modify the borders of the states so that there is greater contrast, fix the mouseover effects, and see about improving the contrast between the states with creating a darker static border, fixing the mouseover effects, and [be] hopefully expanding the range of the color gradient.

Here’s the original France map from pygal that demonstrates the additional mouseover effects and has higher contrast borders.

Edit 9/25: All’s working well now, just don’t have the grouping functionality originally present and the stroke width seems to be too fat for the map compared to France. That’s an aesthetic issue that can wait, though.

Edit 11/9: No progress made in the last two months. Haven’t had internet since the last the update due to relocation to Barrow, Alaska for a work assignment. I’ll upload what I have to github and post a link here accordingly.

Edit 3/15: Just kidding. I’m never updating this. Lost interest 6 months ago and lost the files I was working on about 2 months ago.

 

Aspect ratio explained

I noticed a number of people having a difficult time using the Wolverine crush generator due to using images with incorrect aspect ratio.

Here’s a quick explanation of what aspect ratio is and why it’s important.

In words:

aspect ratio – The ratio of width to height of a pixel, image, or display screen*

In pictures:

aspectratio

Why it’s important

Hopefully this is obvious by now. The image frame has a certain aspect ratio that must be observed or else there will be distortion or an incorrect fit. This ratio is approximately 1:1.14.

Observing the proper aspect ratio yields expected results (right), while ignoring it yields distortion (left)

 

*The Free On-line Dictionary of Computing.

You’re welcome, Internet

Introducing the “Wolverine Crush” meme image generator

Because it had to be made at some point or other.

Generate your own wolverine crush image

Try it out

You will need your desired image to have an approximate aspect ratio of 1:1.14 to fit properly in the Wolverine photo frame. You can crop to a proper aspect ratio / proportions here (I have already set the parameters, but that site is screwy… double check that it is set at 1:1.14 and not 1:1).

The actual size of the image is not important (for best results, use an image over 300px one each side though) as it will be resized correctly as long as the aspect ratio of 1:1.14 is observed. Incorrect proportions will not fit properly and will result in a skewed image or whitespace in Wolverine picture frame.

Image:

The code

The Python code behind making it was surprisingly straight forward. I had been avoiding doing anything that involved the PIL (Python Imaging Library) for some time. I always assumed it would be a nightmare to manipulate images via scripts, but this was actually far easier than I thought it  would be (admittedly, this is a simple manipulation) and is an inspiration to look into making simple custom GIMP scripts or learning a little more PIL at some point down the road.

Most of what I needed to know for this meme generator project came from just two Stack Exchange questions [1][2]

# 'WOLVERINE CRUSH' meme generator
# JS Kouri
# 8/2014
# No restrictions on use

import Image
import hashlib
import time

def file_name():
  # Generate unique file name (variable 'name') based on hash of time
  global name
  t = time.time()
  h = hashlib.md5()
  h.update(str(t))
  name = str(h.hexdigest())
  name = name[0:5] + ".png"

''' Note: I hate sequential file names and I keep forgetting the syntax of the hashlib library, hence this odd naming scheme method. It's personally useful to me for memorizing syntax with repeat exposure.  '''
 
  
file_name()

# user selected image to go into pic frame
fn = "imagetogointopictureframe.png" 
location = "/dir/to/img" + fn

# open base image and image to be superimposed:
wolverine = Image.open("../wp-content/uploads/2014/08/wolverine_generator.png")
user_img = Image.open(location)


# rotate user image to match angle of picture frame, preserve quality with bicubic interpolation
user_img.thumbnail((280,320))
rot = user_img.rotate(6, resample=Image.BICUBIC, expand=1)


# create a new empty image with alpha, set to base (wolverine) image size
new_im = Image.new('RGBA', (480,700))


# paste rotated user image first, paste base image with alpha in picture frame second
new_im.paste(rot, (120,350) )
new_im.paste(wolverine, (0,0), mask=wolverine)


# save
location = "../desired/save/dir/" + name
new_im.save(location)


# DEBUG 'print' image, useful when running on local machine for debugging
#new_im.show()

 

Sous Vide Update, 5/2014

I was about to get started on the code overhaul for the Arduino sous vide machine when I discovered that my immersion heaters no longer work. A word to the wise, you shouldn’t store them in freezing temperatures in a humid garage. The heaters I chose had mediocre reviews on Amazon, but were still the best reviewed and most affordable. I’m going to look into a few alternatives to find something heartier, but equally affordable.

State Gas Prices Project

EDIT: Thanks to not having a computer up and running consistently, I only gathered about 2 months worth of data. Project cancelled to pursue more interesting things. Check out gasbuddy.com for a great source of data!

 

How much would gas be in your state without state and/or federal taxes? What is the current average price of gasoline in your state? Compared to other states? Historically? What would gas cost without the required ethanol component? What would be the average change in MPG from removal of ethanol? These are the questions to be addressed by the upcoming State Gas Price Project.

Will include the following:

  • code for a web scraper program to harvest pricing data and store to local and/or remote database
  • image mapping script to dynamically adjust state colors based on price range and map pricing labels to them (like so)
  • option to export image to svg and png
  • depending on time this week, an alternative HTML5 interactive svg map with mouseover effects. (oooh fancy!)

 

 

The (sort of) Return of Good Eats

The Situation

The host and creator of the Food Network series Good Eats, Alton Brown, has been busy these last few years. From continuing his job as host of Iron Chef America and mentor on The Next Food Network Star, publishing periodical podcasts  and interviews with notable chefs and authors (Alton Browncasts), starting a new series Cutthroat Kitchen, continuing to bemuse his fans with his ridiculous post it note twitter account, and going on a nation wide food & music tour. You can say he’s been very busy and this is all well and good, but none of it compares to the pure bliss of Good Eats. It was a Bill Nye meets Julia Child mashup where the foundational science of cooking had an equal footing and airtime with the process and techniques involved in producing the episode’s dish. On a channel starved of any programs that actually taught cooking or offered any intellectual stimulation, Good Eats was king.

It was something unlike any other cooking show on the Food Network or elsewhere on television. It had no reality show theatrics and it taught you more than how to replicate a recipe, it taught you about why that recipe works and how to think about food. As someone who can assuredly say they learned how to cook nearly everything from the show, I really miss Good Eats.

Brown with his (in)famous yeast puppets
Brown with his (in)famous yeast puppets

The Rebirth of “Good Eats”

You can imagine my excitement when I stumbled across two new videos under the moniker ‘Cook Smart’ on the Alton Brown YouTube page. Looks like this is the Good Eats-esque project Brown has been hinting about for the last year. So far there are only two short web clips, one on cooking eggs in the oven and another on refrigerator organization.

Worth keeping an eye on and periodically checking for updates if you’re a ‘food fan’. Who knows, maybe even the yeast puppets will return for a clip.

 

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.

meandering adventures in the world of science and technology