Introduction: Imp Chef: Internet-Connected BBQ Thermometer

About: Developer at Electric Imp

When I was a kid, we'd cook our holiday turkey or roast with one of those little pop-up thermometers that you just stick into the bird, that pops up (presumably) when the bird is done, or at least safe to eat. We had a lot of dry turkeys and roasts.

These days, you can buy a thermometer that tells you the actual temperature, which is nice. Better still, you can get a wireless one! But at that point, things get a little disappointing. Most of the wireless sensors have a pretty low range - a hundred feet or so. No good if you're out on the deck or in the back yard, and the part you have to carry or bring with you is a big heavy thing that clips to your belt and makes you look silly. And the worst part: no graphs.

Everything should make graphs. Obviously.

I've already built some internet-connected thermometers with Electric Imp, so why not do the same thing with a meat thermometer probe and send the data off to a web site I can view on my phone? As it turns out, it's easy. Build one of these and impress your dinner guests.

Step 1: Parts and Tools

I started this project with an AW129 wireless BBQ sensor from Oregon Scientific, and wound up replacing everything except for the probe. Picking up this whole sensor package sets you back about $25 on Amazon. It looks like you can pick up just the probe for about $10, but you may need to adjust a resistor value in order to get your calibration to a point where you're satisfied with it. For the rest of this tutorial, I'm going to assume that you've begged, bought, or stolen an authentic AW129 temperature probe.

Here's the other things you'll need, as far as parts:

  1. Electric Imp and Breakout Board, available from Digikey, Adafruit, or Sparkfun for about $35 to $45 total. Make sure you get both the card and the breakout board!
  2. A pushbutton. I'm using this one from Digikey, which cost $0.23.
  3. A sub-mini audio jack, stereo or mono. I'm using the TR2A from Digikey, which cost $4.00.
  4. A 9V battery, battery clip and battery connector. About $4.00 all together.
  5. A 47kΩ resistor, about $0.10.
  6. A bit of wire.
  7. A bit of foam double-sided tape.

Depending on how you come by your electric imp parts and your temperature probe, you should be able to gather the parts you'll need for under $70.

You'll need a few tools as well:

  1. A soldering iron and some solder.
  2. Wire cutters.
  3. Wire strippers.
  4. A computer for programming your electric imp.
  5. A smart phone for configuring your device to connect to your WiFi network. You'll only need to use it to get started, so you can always just borrow one. An iPod touch or iPad works fine, too. Any iOS and most Android devices will do.


Ok! Let's get started wiring this thing together.

Step 2: Wire It Up

Here's how we'll wire this device up:

  1. Connect the button. This button is going to serve as an on/off button for your device. The firmware included automatically sends the device off to sleep after a period of inactivity. This button will be used to wake the device up, or to force it to sleep immediately to save battery. The button will set the imp's Pin1 high when pressed, which can be used to wake the imp from deep sleep.

    1. Line the button up so the little feet are pointing toward the shorter ends of your breakout board, and line the bottom-left foot up with the Pin1 pad on the imp breakout board. This will line another of the button's feet up with Pin5 on the imp breakout board Tape the button in place, then heat the pad and the button's foot with your soldering iron and apply a little solder. Solder the other foot to Pin5 the same way. Pin5 isn't used for anything in this device, so this is just a good way to make your button more mechanically sturdy.
    2. Cut a short piece of wire, strip both ends, and point-to-point solder the other end to one of the two feet on the far side of the button. Tape the wire in place so it is touching the foot of the button, then heat both the wire and the foot with the iron. Flow a little solder onto the heated parts.
    3. Solder the other end of the wire to the 3V3 pad on the breakout board. You may want to wait to apply the solder to this terminal, because we need to wire another wire to this pad.
  2. Solder leads to your sub-mini audio jack. This jack is where you'll plug your temperature probe in. Because the temperature probe doesn't have any polarity, it doesn't matter which way around the jack is connected.
    1. If you're using a switched jack, like I did, there will be a third terminal; this is a switch terminal, and is used to detect when there is a plug plugged into the jack. You don't want to use this, so make sure you're using only the terminals that are supposed to connect to the tip and the shield of the plug. If you're not sure which you've got, you can use a multimeter to check for continuity. If you do this, make sure you install a plug in the jack before testing, because otherwise the tip contact and switch contact will be electrically connected.
    2. Cut and strip two medium (3") pieces of wire. Run one end through each of the terminals you wish to connect. It's very helpful to tape the jack and the wire down to your work surface at this point to hold them steady.
    3. Heat the terminal and wire with the soldering iron, then flow some solder onto the parts. Make sure you heat the wire as well as the terminal, or the solder joint will break.
  3. Connect the jack between 3V3 and Pin9 on the breakout board. Don't solder the Pin9 pad yet; we still need to attach a resistor to that pad.
    1. Run the free end of the wire from the jack through the appropriate terminal on your breakout board, then heat the wire and pad with the iron and apply some solder.
    2. Make sure both the wire from the jack and the wire from the button are through the 3V3 pad before soldering.
  4. Connect the 47kΩ resistor between Pin9 and Pin8.
    1. The Pin9 pad on the breakout should still be unsoldered. Make sure both the jack and one side of the resistor are through the pad, then solder both in place.
    2. The other side of the resistor should be soldered to the Pin8 pad on the breakout. You can go ahead and solder it right away; we don't need to connect anything else to this pin.
  5. Connect the battery holder. Apply a small square of double-sided foam tape to the bottom of your breakout board and remove the backing on the other side. Apply the battery clip here, so that the battery will sit nicely along the bottom of the breakout board. Hold the battery clip and breakout board together for 10s.
  6. Connect the battery contact. If the ends of the wires on your battery contact did not come pre-stripped, strip the ends. The positive side (red) goes to the "P+" pad on the breakout board, and the negative side (black) goes to "P-". Note that the P+ and P- pads are surface-mount pads, along the side of the breakout board, not through-hole pads along the end of the board like the other contacts. For each wire:
    1. Tape the wire in place.
    2. Heat the wire and pad together with the soldering iron.
    3. Flow a bead of solder over the wire and pad together.

You are now done building your internet-connected BBQ thermometer! Or at least, done putting the hardware together. Let's get started programming the device.

THE REST OF THIS STEP IS OPERATING THEORY! If that's not what you're here for, head to the next step.

The basic operating principle behind your internet-connected meat thermometer is very simple. The temperature probe is really just a big thermistor: a resistor that changes resistance based on its temperature. This particular type is called an "NTC" thermistor; negative temperature coefficient. This means that as temperature goes up, resistance goes down.

Take a look at the breadboard diagram to see how your device is going to be wired up. Here's how it works:

We'll form a resistive divider with the temperature probe and a fixed resistor, and then measure the amount of current that flows through the divider by measuring the voltage across the fixed resistor. The whole thing comes down to Ohm's Law:

Voltage = Current * Resistance

The resistance of the whole divider is the sum of the two resistors, because they're wired up in series.

Rtotal = Rfixed + Rprobe

The voltage across the whole thing is fixed at 3.3V, because we've wired it up that way. Therefore:

3.3 Volts = Current * Rtotal

Current = 3.3 Volts / Rtotal.

We can measure the current through the circuit because the current through each resistor is the same (because they're in series. Every electron moving through the circuit has to go through both of the resistors to get around the circuit). So:

Vfixed = Current * Rfixed

Current = Vfixed / Rfixed

Now we can figure out the resistance of the probe:

Current = 3.3 V / (Rprobe + Rfixed) = Vfixed / Rfixed

Vfixed = (3.3 * Rfixed) / (Rprobe + Rfixed)

3.3 * Rfixed = (Rprobe + Rfixed) / Vfixed

Rprobe + Rfixed = (3.3 * Rfixed) / Vfixed

Rprobe = [(3.3 * Rfixed) / Vfixed] - Rfixed.

Once you've got the resistance of the probe, you can calculate the temperature from an equation in the probe's data sheet. If you haven't got that, you can gather the data experimentally and curve fit it, which is what we've done here.

Step 3: Program Your Thermometer

To get started programming your device, you need to get it connected to the internet through WiFi. The imp supports just about every type of WiFi encryption, so all you need is your SSID and Password, an iOS or Android device, and an Electric Imp account.

If you don't have an Electric Imp account, it's time to register for one. It's free. Head to ide.electricimp.com and sign up there.

To get the imp connected to WiFi, you'll use the free Electric Imp app to send something called "BlinkUp". BlinkUp is just an optical signal, sent by flashing the screen of the iOS or Android device. The imp has a tiny light sensor built in, and it decodes the blinking pattern to get the SSID and password of your WiFi network. Once it has the credentials, the imp will connect to the internet the same way your phone or computer would, and will check in with the Electric Imp cloud. The new device will show up in your IDE, which you can work with in your browser, and you'll be able to program and monitor your device from anywhere in the world with an internet connection.

Once you've signed up for an Electric Imp account, download the free Electric Imp app and sign in with the same user name and password you used to sign up for your account. In the app, add a new network: this is where you enter your SSID and password for the WiFi network.

Power on your device by connecting the battery to the battery contact. Make sure the jumper near the bottom edge of the breakout board is set to "BAT" to select battery power. Insert the imp card into the socket, and it should begin to blink.

Press "Send BlinkUp" in the app, and hold the screen of your phone against the end of the imp with the blinking light. The screen of your phone will flash for about 30s, and then the imp will begin blinking different colors as it goes through the process of connecting to WiFi. You can see what the codes mean here.

When the imp finishes connecting to the Imp Cloud, it will appear at ide.electricimp.com in the left-hand navigation pane under "new devices". You may need to refresh the page to get the device to appear. Click "new devices" to expand the list of new devices, and you'll see a long, random-looking string of characters. This is the default name of your new device, it's "impee ID". Click on the name to open up the device options. Here, you can give the device a new name and assign it a "model". A model is simply a group of firmware; many devices can be members of the same model, and will run the same code. To create a new model, type a new model name in the model name box and hit "save changes".

You'll notice now that there are not one, but two code windows for your device, marked "Agent Code" and "Device Code". Your device code runs on the electric imp, inside a VM, so that if errors occur the device will not become unreachable. An agent is a second VM which runs inside the Imp Cloud. Every device has an agent as a partner. The agent handles things like defining an HTTP interface and doing data-intensive work like file processing. The agent and device can send data back and forth easily.

There's code already written up and ready for you for this project, so you can go ahead and pick it up from https://github.com/electricimp/examples/tree/master/turkeyprobe. Note that there are two files: "turkeyprobe.agent.nut" is the firmware that runs on the agent, and "turkeyprobe.device.nut" is the firmware that runs on the device. Paste each file into the appropriate window. The IDE automatically saves whenever you make changes, and you can also save the code in the model by building and running the code.

You can run the code now, but expect some errors: we're not quite done yet. The imp will log temperature data to a time-series data store called Xively, and you'll need to set up a feed there to receive the data. Let's do that next.

Step 4: Configure a Xively Feed

To get started with Xively, head to xively.com and register for a free account. Once you're registered and logged in, you'll be taken to your dashboard, where you can see a list of devices. Somewhat strangely, you need to "add a device" to create a feed. Click the giant plus button to do this. Name your device whatever you like, and choose whether the data will be private or public. Either way will work fine.

Once your device is created, you'll be taken to a page where you'll be able to gather two important pieces of information: the feed ID and the API key for your feed. The feed ID is in the top-right corner of the dashboard, and the API key is below it, on the right side of the page (the API key is long). Both need to be copied into your agent code, on lines 38 and 39:

const XIVELY_API_KEY = "YOUR KEY HERE";
const XIVELY_FEED_ID = "YOUR FEED ID HERE";

Once you've added these credentials to your agent code, you should be able to build and run the code without error. The device will begin taking readings, and you'll see them appear in the log in the IDE. Back in the Xively dashboard, you should see two new channels automatically created, one starting with "lowbatt" and one starting with "temperature". Both channel names append the device ID of your new thermometer, so you can use the same feed over and over again with many thermometers and each device will know to only update and use its own data.

Step 5: Using Your Thermometer

Here's the completely non-technical user's manual for your new internet-connected BBQ thermometer:

To turn the BBQ thermometer on, press and hold the button for three seconds. You'll see the imp wake up, begin blinking, and connect to the internet just like you did when you first connected it earlier.

The thermometer will automatically turn off after a period of inactivity. The device automatically detects if it is being used for actual cooking purposes by looking at the current temperature and the rate of change in the temperature, and stays awake when in use. Once you're done using it, it will automatically turn itself off. You can also turn it off manually by pressing and holding the button for 3 seconds.

To view the current temperature and temperature graphs, point a browser at the agent URL. The agent URL is visible at the top of the agent window in the IDE. You can select the length of data you wish to show in the graph and toggle between ºC and ºF for the current temperature displayed at the top of the page. To use the page as an app, open the page in safari and select "save to home screen" from the share menu.

To change the WiFi credentials, either cycle power or send the device to sleep and wake it up again. The device listens for a new BlinkUp packet for 1 minute after it boots up. During this period, you can send it new network credentials the same way you did to configure it the first time.

Step 6: A Closer Look at How It Works: Device Code

In case you're curious: let's take a closer look at what makes the BBQ thermometer work!

Let's start with the device code. It's short and simple. It has two jobs: collect temperature readings, and respond to button presses.

Take a look at the function definition for getTemp().

function getTemp() { imp.wakeup(INTERVAL, getTemp);
    vtherm_en_l.write(0);
    imp.sleep(0.01);
    local rawval = vtherm.read();
    local temp = (TEMP_COEFF_2*(math.pow(rawval,2)))+(TEMP_COEFF_1*rawval)+TEMP_OFFSET; 
    temp = (temp * 1.8) + 32.0;
    vtherm_en_l.write(1);
    agent.send("temp",{"temp":temp,"vbat":hardware.voltage()});
}

This function collects temperature readings at a regular interval. The interval is defined by a constant, INTERVAL, at the top of the file. The first thing getTemp does is schedule itself to run again in INTERVAL seconds, by calling imp.wakeup(INTERVAL, getTemp). This schedules a callback; the imp can carry on doing other things, and in INTERVAL seconds, the operating system will call back and ask us to run getTemp again.

After scheduling itself to run again, getTemp reads the voltage in the middle of the thermistor divider, then uses a simple 2nd-order curve fit to estimate the temperature. The coefficients for the curve fit are stored as constants at the top of the file (if you look, you'll notice we're actually just using a linear curve fit! But the 2nd-order fit is there if you want to try it out).

Next, we have a function that tells the imp what to do when it's time to go to sleep:

function goToSleep() {wake.configure(DIGITAL_IN_WAKEUP);
    // go to sleepfor max sleep time (1 day minus 5 seconds)
    server.sleepfor(MAXSLEEP);
}

This function does two things: first, it configures Pin1 as a wakeup pin, so that if the button is pressed it will wake the imp from deep sleep. Second, it tells the imp to go to deep sleep for as long as it is allowed (MAXSLEEP is defined as 86396 seconds at the top of the file; this is 1 day minus 4 seconds. The call to server.sleepfor() alerts the server that the device will be going to sleep, so the agent won't have to wait for the imp to go missing before it realizes what has happened.

Below that, there's a handler function for button press events:

function btnPressed() {
    // wait to see if this is a long press, and go to sleep if it is
    local start = hardware.millis();
    while ((hardware.millis() - start) < LONGPRESS_TIME*1000) {
        if (!hardware.pin1.read()) {return;}
    }
    goToSleep();
}

This is an interesting function. As it turns out, when you press the button at all, the imp calls this function right away. The imp doesn't wait several seconds to go to sleep, as you might have thought from how the thermometer works. Instead, the imp calls this function immediately, and waits to see if the user holds the button down for three seconds. If you do, it goes to sleep. If you don't, it leaves the function and goes back to doing what it was doing before.

Next, we see some callbacks being registered for agent events:

agent.on("sleep", function(val) {
    imp.onidle(function() {
        goToSleep();
    }); 
});<br>
agent.on("needDeviceId", function(val) {
    agent.send("deviceId",hardware.getdeviceid());
});
The first handler here allows the agent to tell the device to go to sleep. While the device is running, the agent keeps an eye on how much temperature change it is seeing and adjusts the amount of time it will let the device stay awake before calling this event and telling the device to go to sleep to save battery.

The second handler is only used in special occasions. The agent doesn't automatically know the device's device ID, but it needs it in order to set the channel names to send data to Xively. Ordinarily, the agent and device boot up together when the agent boots up for the first time, after which the agent stays on. However, sometimes the agent will restart by itself, such as if you push new code to it. In this case, the agent needs a way to ask the device what its device ID is - this function gives it a way to do that.

After that, we're done with definitions, and we reach the point where actual run-time operation will start when the device boots. The first thing the imp does when it boots is figure out why it booted. If it was because of a Pin1 wakeup, the imp does the same thing it does if you hold the button to send it to sleep - it stays right here and waits to see if you hold the button. If you let go before the 3 second wait time, the imp will go directly back to sleep before it even connects to the internet.

// check wakereason and make this a shallow wake if necessary
if ((hardware.wakereason() == WAKEREASON_PIN1) || (hardware.wakereason() == WAKEREASON_TIMER)) {
    local start = hardware.millis();
    while ((hardware.millis() - start) < LONGPRESS_TIME*1000) {
        if (!hardware.pin1.read()) {goToSleep();}
    }
    // if we made it here, somebody's just long-pressed the power button to wake the imp
    // go ahead and boot right up.
}
// not a shallow wake; fire up the radio and let's cook a turkey
imp.setpowersave(true); // save juice, as this application is not latency-critical

Lastly, we instantiate the objects we need to do our job and check in with the agent, then start reading the temperature to get started.

agent.send("justwokeup",hardware.getdeviceid());
getTemp()


Let's take a look at the agent code in the next step, if you're interested.

Step 7: A Closer Look at How It Works: Agent Code

The agent firmware is a lot longer, but as you'll see in a second, that's because it contains two large chunks of code that do more general jobs.

The first thing the agent does when it boots up is to check and see if it's just rebooted and already knows the device ID. Whenever the agent has to go and get the device ID from the device, it saves it in the imp cloud with server.save() as soon as it gets the update. This way, if the agent ever restarts, it can grab the ID right away without even having to check in with the device by calling server.load():

// Device ID used to create new channels in this feed for each new turkey probe<br>config <- server.load();
if (!("myDeviceId" in config)) {
    // grab the pre-saved device ID from the server if it's there
    // if it isn't, we've never seen this device before (or the server forgot - unlikely!)
    // we will request a device ID from the device if we make it past class declarations without
    // the device doing an "I just woke up" check-in.
    config.myDeviceId <- null;
}<br>

Just below this, we run into a giant function with a very big multi-line string in it. This function is called prepWebpage, and all it does is concatenate a few strings together. These strings just so happen to be a web site. This web site is the web UI for the BBQ thermometer, and it's what you see when you request the agentURL in a browser. Because the agent has the ability to set up its own HTTP handler, it can respond to certain requests by serving up this very long string - basically, the agent acts like a tiny web server. The web site even includes some simple javascript that runs on the client machine.

After the website, the agent has a function that keeps track of activity on the device and uses a timer and some simple heuristics to figure out if the device should go to sleep to save battery.

function checkSleepTimer() {imp.wakeup(TIMER_DEC_INTERVAL, checkSleepTimer);
    sleepTimer -= TIMER_DEC_INTERVAL;
    if (sleepTimer < 0) {sleepTimer = 0};
    //server.log("Sleep Timer = "+sleepTimer);
    if ((sleepTimer == 0) && device.isconnected()) {
        if (lastTemp < MAX_AUTOSLEEP_TEMP) {
            // TODO: if app is open, don't sleep
            device.send("sleep",0);
        }        
    }
}

This works much like pouring sand into the top of an hourglass. If the temperature is changing quickly, the agent adds more sand to the top of the hourglass, giving the device more time to work. If the rate of temperature change slows, the agent stops adding sand, the hourglass eventually runs out, and the agent tells the device to go to sleep. If the temperature is over a certain threshold, the agent assumes the device is still involved in cooking something, and waits for the temperature to drop again before sending the sleep order.

After this, there's a big chunk of code dedicated to working with Xively. This is a generic class, and you can learn more about it, as well as other classes for working with other web services, by taking a look at electric imp's webservices github page.

Next, we see the agent registering some event handlers for events from the device, just like we saw in the device code earlier. The most interesting one here is the "temp" event handler, which does everything it needs to do to post the new temperature data to Xively and update the sleep timer:

device.on("temp", function(data) {local delta = math.abs(data.temp - lastTemp);
    lastTemp = data.temp;
    if (delta > MIN_CHANGE) {
        // only add time to the timer if we have activity
        if (delta > 30) {
            sleepTimer += 60;
        } else {
            sleepTimer += delta * 2;
        }
    }
    // don't let the sleep timer exceed the preset max.
    if (sleepTimer > MAX_SLEEP_TIMER) {sleepTimer = MAX_SLEEP_TIMER};
    
    local tempStr = format("%.1f",data.temp);
    server.log("Temp: "+tempStr+" F");
    
    // post the datapoint to the Xively feed
    postToXively(tempStr, "temperature");
    
    // check for low-battery issues
    server.log("Battery: "+data.vbat+" V");
    if (!lowBattAlarm && (data.vbat < LOW_BATT_THRESH)) {
        // set the low batt alarm and post it to xively
        server.log("Low battery alert!");
        lowBattAlarm = 1;
        postToXively(lowBattAlarm, "lowbatt")
    } else if (lowBattAlarm && (data.vbat > LOW_BATT_THRESH)) {
        // clear the low batt alarm and post it to xively
        lowBattAlarm = 0;
        postToXively(lowBattAlarm, "lowbatt")
        server.log("Low battery alert cleared.");
    }
});

Because of how this device is wired up, it actually won't ever trigger the low battery alarm; this was included for a similar device that was powered off of a pair of AA lithium batteries without a regulator between the batteries and the imp, so the imp could look at the battery voltage directly. The code was left in just in case anyone gets intrepid and builds one inside the original housing!

Near the bottom of the agent firmware, we see one of the most important parts of the agent: the HTTP request handler. This handler parses incoming HTTP requests and defines how the agent should respond.

http.onrequest(function(request, res) {<br>    server.log("Agent got new HTTP Request");
    // we need to set headers and respond to empty requests as they are usually preflight checks
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers","Origin, X-Requested-With, Content-Type, Accept");
    res.header("Access-Control-Allow-Methods", "POST, GET, OPTIONS");

    if (request.path == "/sleep" || request.path == "/sleep/") {
        device.send("sleep",0);
        res.send(200, "Going to Sleep");
    } else {
        server.log("Agent got unknown request");
        res.send(200, WEBPAGE);
    }
});

Most of the requests to the agent are going to be requests just for the web page, so requests without additional parameters just get the web page as a response. There's also a "hook" here for external services to tell the imp to go to sleep, which the web page doesn't use.

Lastly, the agent instantiates a Xively Client object which it will use during runtime to post data to the Xively stream, requests the device ID if necessary, and starts running the sleep timer:

server.log("Turkey Probe Agent Started.");
// instantiate our Xively client
xivelyClient <- Xively.Client(XIVELY_API_KEY);

// in case we've just restarted the agent, but not the device, call the device for 
// the device ID in 1 second if it doesn't ping us with an "I just booted" message
imp.wakeup(1, function() {
    if (config.myDeviceId == null) { device.send("needDeviceId",0); } else { prepWebpage(); };
});

// start running the auto-sleep watchdog timer
checkSleepTimer();

And that's all there is to it!

Good luck and bon appetit :)