Meatmon Wirelessly smoking meat with firmata

~ 5 months ago ~

Inspired by a recent trip to the Adirondacks during which a good friend blew my mind with some awesome food and great company, I wanted to give hot smoking a beef brisket a go. Despite a few successes with cold smoking bacon and salmon, I'd never actually hot smoked anything. I'm sure people get a feel for it as time passes and they get to know their smoker better, but I wanted to take a more scientific approach.

I bought a piece of brisket from local artisan butchers Flock & Herd and after a short consultation with Huey (owner of the awesome deli at the end of the road and generally opinionated foodie), covered it in molasses and a home-made rub (liberal amounts of garlic salt, celery salt, paprika, pepper and onion powder) and left it in the fridge for a couple of days.

Sitting in the rub

Temperature control

The Internet tells me that I should keep the meat at 225°F for 75 minutes per pound, which I unscientifically converted into new money as about 105°C for 35 minutes per kilogram. When the brisket reaches 75°C, wrap it in foil until it makes it to 85°C and you are more or less done.

The ambient temperature inside the smoker could get into the hundreds of degrees quite easily so I needed something a little more robust than the TMP36 that came with my Arduino kit. Instead I looked into Type-K thermocouples as they are the sort of temperature sensor you'll find in your oven and they can easily cope with these sorts of temperatures. They need an amplifier to work which sounded complicated until I found the AD8495 breakout board (Adafruit to the rescue again) - you plug the thermocouple in one end, 5v and ground into the other and you get pin you can take a analogue reading from to get the temperature. Lovely.

I bought two thermocouples - a glass braid one for measuring the ambient smoker temperature and a stainless steel one for measuring the internal temperature of the meat.

Wait, I need to keep this thing tethered to USB?

I figured to use an Arduino with the rather excellent johnny-five library. There's an important caveat in all this - since the JavaScript code that drives the hardware runs on a computer, the Arduino has to remain tethered to the computer via a USB umbilical cord.

That was clearly not going to fly - I didn't really want to leave my laptop out next to the smoker all day so I needed a better solution.

A while ago I picked up a bunch of nRF24L01+ based 2.4GHz wireless transceiver modules on eBay, little low-energy cards that talk an RF protocol similar to Bluetooth with data rates of up to 2Mbps. They interact with the Arduino via it's SPI pins.

As luck would have it, someone's already provided some example code of how to get a nRF24L01 working with an Arduino, so the hard work had more or less been done for me.

Under the hood, johnny-five speaks the Firmata protocol. Anyone who's attended a NodeBots meetup will remember firing up the Arudino IDE and loading the StandardFirmata sketch onto their board.

StandardFirmata is only one of several options, another is ConfigurableFirmata. This is a version of Firmata that you can use to enable and disable functionality as required and seems to be where some of the more obscure bits of the protocol are implemented.

One such bit is Network Firmata - e.g. receiving firmata messages using an ethernet shield. This works because some bright spark abstracted the serial port operations into a class named Stream. When we enable network firmata, the following code gets executed:

#ifdef NETWORK_FIRMATA
//...
#include <utility/EthernetClientStream.h>
//...
#ifdef local_ip
  EthernetClientStream stream(client,local_ip,remote_ip,NULL,remote_port);
#else
  //...
#endif

So we set up an EthernetClientStream object if NETWORK_FIRMATA is defined. Later on, this happens:

#ifdef NETWORK_FIRMATA
  //...
  Firmata.begin(stream);
#else
  Firmata.begin(57600);
#endif

Inside Firmata.cpp, those begin methods look like this:

void FirmataClass::begin(long speed)
{
  Serial.begin(speed);
  FirmataStream = &Serial;
  //...
}

void FirmataClass::begin(Stream &s)
{
  FirmataStream = &s;
  //...
}

If NETWORK_FIRMATA is defined, the stream that we created earlier gets passed to the Firmata class and it'll use that instead of the SerialPort class.

This is great because all we need to do is implement the nRF24L01 driver as an Arduino stream, which is in the repo as WirelessClientStream.cpp.

Now that we can receive serial port data over wireless, we need to send it too. I attached another Arduino to my laptop, running the following sketch:

// spi pins
#define CE_PIN 8
#define CSN_PIN 7

// the wireless channel to use
#define CHANNEL 0

// our node name on the network
#define RX_ADDR "node2"

// where we will send traffic
#define TX_ADDR "node1"

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

void setup() {
  Serial.begin(57600);

  Mirf.cePin = CE_PIN;
  Mirf.csnPin = CSN_PIN;
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();

  Mirf.setRADDR((byte *)RX_ADDR);

  Mirf.channel = CHANNEL;
  Mirf.payload = sizeof(int);
  Mirf.config();
}

void loop() {
  int incoming;

  // read any incoming data
  while(Mirf.dataReady()) {
    Mirf.getData((byte *)&incoming);

    Serial.write(incoming);
  }

  // write any outgoing data
  while(Serial.available() > 0) {
    incoming = Serial.read();

    Mirf.setTADDR((byte *)TX_ADDR);
    Mirf.send((byte *)&incoming);

    while(Mirf.isSending()) {
      // do nothing while we send
    }
  }
}

During the loop, if there's any data available from the wireless card, it writes it into the serial port for the computer to pick up, then if there's any data available from the serial port, it sends it over the wireless card to the remote Arduino. The idea is to be completely transparent.

When wired up, the Arduino connected to the computer looks like this:

Wireless client connected to laptop

The johnny-five code that runs on the laptop is (as ever) almost embarrassingly simple:

var five = require('johnny-five'),
  temperatureSensor = require('./lib/temperatureSensor')

var board = new five.Board({port: '/dev/tty.usbserial-A603HIUN'})
board.on('ready', function() {
  // setup internal temperature monitor
  var internalTemperatureSensor = temperatureSensor(board, 3, -36)
  internalTemperatureSensor.on('temperature', function(temperature) {
    console.info('Received internal temperature %d°C', temperature);
  })

  // setup external temperature monitor
  var externalTemperatureSensor = temperatureSensor(board, 4, -36)
  externalTemperatureSensor.on('temperature', function(temperature) {
    console.info('Received external temperature %d°C', temperature);
  })
})

The temperatureSensor module is below. It reads the temperature from the thermocouple breakout board, converts it into centigrade and emits an event so listeners can do something useful with the recorded temperature.

var EventEmitter = require('events').EventEmitter,
  util = require('util')

var TemperatureSensor = function(board, pin, fudgeFactor, interval) {
  EventEmitter.call(this)

  // how often to report the temperature
  interval = interval || 60000

  // turn on reporting for that pin
  board.io.reportAnalogPin(pin, 1)

  var measurements = 0
  var temporary = 0
  this._temperature = 0

  // read the temperature occasionally
  board.analogRead(pin, function(value) {
    var voltage = value * (5.0 / 1023.0);
    var temp = (voltage - 1.25) / 0.005;

    temporary += temp
    measurements++

    if(measurements == 10) {
      measurements = 0
      this._temperature = parseInt(temporary/10, 10)
      this._temperature += fudgeFactor
      temporary = 0
    }
  }.bind(this))

  setInterval(function() {
    this._emit('temperature', this._temperature)
  }.bind(this), interval)
}
util.inherits(TemperatureSensor, EventEmitter)

module.exports = function(board, pin, fudgeFactor, interval) {
  return new TemperatureSensor(board, pin, fudgeFactor, interval)
}

The astute reader will notice that I am explicitly turning on pin reporting in the temperatureSensor module. This is because for a reason I cannot comprehend firmata.js turns pin monitoring for all analogue pins by default. This is fine for high-speed, high bandwidth devices/connections (like the Uno over USB) but swamps the RF connection with unnecessary traffic. There's a pull request open around this, hopefully it'll get merged soon. In the interim you have to comment out this for loop in firmata.js to get it to work.

Also, analogRead calls the passed callback loads of times instead of just once which was also a bit confusing.

The really astute reader will notice a variable named fudgeFactor. That's a, uh, 'calibration'. It's hardware, right?

You can see the whole setup at about halfway through the process here:

Arduino connected to temperature sensors

The stainless steel sensor is inserted into the meat and the braid one is lying next to the contingency plan - an actual meat thermometer.

From the external view:

Lid closed, sensors in place

So much data

What do you do with the data? Graph it, of course.

I ran a small webapp on my laptop that gave me a real-time(ish) readout of the internal and external temperatures, as well as a rough estimation of when the meat would be ready based on internal temperature and assuming a constant increase over time (I said it was rough).

It stored the database in a local instance of CouchDB and had a Hapi driven REST API that the johnny-five process would post info to. It used wantsit for IOC, nano-repository to make database access a bit more bearable and columbo for REST resource discovery.

Meatmon

What's nice is you can see the temperature being way too hot to start with, me freaking out and repeatedly opening the lid to remove coals, and when I opened the lid towards the end and forgot to put the external sensor back in.

The final result

I managed to get a quick snap in before the wolves descended.

The finished article

Not too shabby.

Source

The changes I made to ConfigurableFirmata are in the add-wireless branch of achingbrain/firmata

You can see the johnny-five code and stat collector webapp in the achingbrain/meatmon repository.

Other solutions are available

Using an XBee probably would have been a lot easier, but they are 5x the price of the nRF24L01+ and I don't own any.