Ship IoT with Kinoma Create and PubNub
For the Love of Coffee
Coffee is amazing stuff, and when brewed just right, tastes incredible! I'm a coffee aficionado, and I'm always pursuing The Perfect Cup™. Preparation technique is critical! The Specialty Coffee Association of America has very rigid standards for how to prepare coffee, designed to ensure consistent and peak quality flavor in the resulting drink. Water temperature is one of the major factors, because as any good chemist knows, various compounds dissolve at different rates in different temperatures of water. The flavor of your cup of coffee is greatly determined by the temperature of water used, and consequently, the varying fractions of coffee compounds thereby extracted.
From the SCAA standard:
Cupping water temperature shall be 200°F ± 2°F (92.2 – 94.4°C) when poured on grounds.
We are engineers. We appreciate the scientific method, and data-driven decisions. The quest for The Perfect Cup must therefore entail data collection for later analysis. This collection should be automated, because life is too short for repetitive manual processes. So let's start out by checking our existing daily brewing process' water temperature, and logging the long-term variance.
We're going to do this with a Kinoma Create, which packs an 800 MHz ARM v5t processor, WiFi, Bluetooth, a color touchscreen, sound, I/O pins, and all kinds of other goodies. It's a comprehensive development kit that lets you code with JavaScript and XML, so it's a great choice, and even more so if JavaScript is one of your competencies. This will make our temperature logging simple, and the data services available through wot.io give us easy insights into our data because the integration is already done and working. Expanding beyond our first-steps of temperature logging will be a snap, as the Kinoma Create has more I/O than we can shake a stick at. Let's get to it!
Getting Started
For this project, I used:
- A Kinoma Create
- Kinoma Studio Eclipse-based IDE
- A Texas Instruments LM35 precision temperature sensor
- One genuine borosilicate Pyrex test tube
- Arctic Silver epoxy thermal adhesive
- Shielded cable, heatshrink tube, etc.
- PubNub endpoints
- wot.io data services
- Delicious coffee! A city-roast bean from the Kivu Butembo region of the Congo
The Probe
First thing I did was make a probe suitable for testing something I was going to drink. It needed to be precise, non-toxic, and tolerant of rapid temperature changes.
The temperature sensor is the LM35 from Texas Instruments, a military-grade precision Centigrade sensor with analog output, accurate to ±0.5ºC. That's well-within the specified ±2ºF spec from the SCAA for brewing water.
I attached the sensor inside a Pyrex borosilicate glass test tube, which will withstand the thermal shock inherent in measuring boiling water. We certainly don't want shattered glass shards or contaminants in our coffee! To ensure good heat transfer, I used some thermal epoxy to affix the sensor at the bottom of the tube.
The cable is Belden 9841, typically used for RS-485 industrial controls and DMX 512 systems. While we don't need precision 120Ω data cable for this, it has 100% foil+braid shield and will keep our analog signals nice and clean. Plus, I had a spool of it on the rack - always an advantage ;)
About that LED... It functions as a power indicator, and makes the probe look good for showing off at World Maker Faire. Normally I wouldn't stick an LED next to a precision temperature sensor. The power dissipated by the LED and current-limiting resistor will cause a slight temperature rise and throw off the measurement. But it only dissipates maybe 10 milliwatts, and coffee is really hot, so I stuck an LED in there! No worries.
Testing the Sensor
Before writing the code, I needed to be sure the sensor output matched what's claimed on the datasheet (always check your assumptions!). A quick setup on a breadboard proved the datasheet to be correct.
The temperature of the sensor itself measured ~24.1ºC with a calibrated FLIR thermal camera (with an assumed emissivity of ε0.90 for the plastic TO-92 case):
...and the output of the device was 245mV, right on target!
Now we know we don't need much correction factor in software, if any.
The Code
I'll lead you through a very brief walkthrough of the code. You can grab the code from the repo on github.
First thing you'll want to do is put your PubNub publish and subscribe keys into the code, and your channel name.
Grab the keys and put them in a the top of main.xml
:
<variable id="PUBNUB_PUBLISH_KEY" value="'YOUR_PUB_KEY_HERE'" />
<variable id="PUBNUB_SUBSCRIBE_KEY" value="'YOUR_SUB_KEY_HERE'" />
<variable id="PUBNUB_CHANNEL" value="'YOUR_CHANNEL_NAME_HERE'" />;
PubNub Library Integration
One of the key bits to using this PubNub library is you need to override the default application behavior. Their example came as straight JS, but I converted it to XML here, so you get to see both methods and learn some new tricks.
At the top, we include the pubnub.js
library file, and then define a behavior that uses the PubNubBehavior
prototype. While I won't claim to be an expert on PubNub's library, I believe we do things this way so that the PubNub library can handle the asynchronous events coming in from the message bus.
We also start into the main startup code, which resides in the onLaunch
method.
<program xmlns="http://www.kinoma.com/kpr/1">
<include path="pubnub.js"/>
<behavior id="ApplicationBehavior" like="PubNubBehavior">
<method id="constructor" params="content,data"><![CDATA[
PubNubBehavior.call(this, content, data);
]]></method>
<method id="onLaunch" params="application"><![CDATA[
...
...and we see the rest down at the bottom, where we instantiate the new ApplicationBehavior
and stick it into our main applicaiton.behavior
thusly:
<script>
<![CDATA[
application.behavior = new ApplicationBehavior(application, {});
application.add( maincontainer = new MainContainer() );
]]>
</script>
onLaunch Initialization
First thing we do is set up the pubnub
object with our publish and subscribe keys. Note that you don't need to use keys from the same exchange - you can write to one, and read from an entirely different one. That's part of the amazing flexibility of message bus architectures like PubNub and wot.io.
After init, we subscribe to the specified channel, and set up callbacks for receiving messages (the message
key) and connection events (connect
). Upon connection we just fire off a quick Hello message so we can tell it's working. For receiving, we stick the message contents into a UI label element, and increment a counter, again doing both so we can tell what's going on for demonstration purposes.
You could certainly parse the incoming messages and do whatever you want with them!
pubnub = PUBNUB.init({
publish_key: PUBNUB_PUBLISH_KEY,
subscribe_key: PUBNUB_SUBSCRIBE_KEY
});
pubnub.subscribe({
channel : PUBNUB_CHANNEL,
message : function(message, env, channel) {
maincontainer.receivedMessage.string = JSON.stringify(message);
maincontainer.receivedLabel.string = "Last received (" + ++receivedCount + "):";
},
connect: function pub() {
/*
We're connected! Send a message.
*/
pubnub.publish({
channel : PUBNUB_CHANNEL,
message : "Hello from wotio kinoma pubnub temperature demo!"
});
}
});
Next we set up our input pins for the temp sensor:
application.invoke( new MessageWithObject( "pins:configure", {
analogSensor: {
require: "analog",
pins: {
analogTemp: { pin: 52 }
}
}
} ) );
This uses Kinoma's BLL files which define the pin layout for hardware modules. I created a simple one for our temp sensor. I did not have the system configure the power and ground pins. At the time I coded this, Kinoma doesn't document an official way to do it (although it does exist if you dig into their codebase).
exports.pins = {
analogTemp: { type: "A2D" }
};
exports.configure = function() {
this.analogTemp.init();
}
exports.read = function() {
return this.analogTemp.read();
}
exports.close = function() {
this.analogTemp.close();
}
Lastly, we set up what is effectively the main loop. This fires off a message that will be processed by the analogSensor
read
method defined in the BLL file. It also sets it up to repeat with an interval of 500 milliseconds. The results are sent via a callback, /gotAnalogResult
:
/* Use the initialized analogSensor object and repeatedly
call its read method with a given interval. */
application.invoke( new MessageWithObject( "pins:/analogSensor/read?" +
serializeQuery( {
repeat: "on",
interval: 500,
callback: "/gotAnalogResult"
} ) ) );
The Results Callback
This is a message handler behavior which processes the analog value results from our periodic sensor read. It converts the reading to degrees Celsius, and fires off the data with an onAnalogValueChanged
and onTempValueChanged
message to whomever is listening. (We'll see who's listening down below...)
The sensor outputs 10 millivolts per degree Celsius, so 22ºC would be 220mV. This goes into our analog pin, which when read, gives a floating-point value from 0 to 1, representing 0V up to whatever the I/O voltage is set to, 3.3V or 5V. We do some conversion to get our temperature back.
You may notice that we only use a small range of the A/D converter's potential for typical temperatures, and this results in lower resolution readings. Ideally we'd pre-scale things using a DC amplifier with a gain of, say, 2 or 4, so the temperature signal uses more of the available input range.
<handler path="/gotAnalogResult">
<behavior>
<method id="onInvoke" params="handler, message"><![CDATA[
var result = message.requestObject;
// Convert voltage result to temperature
// LM35 is 10mV/ªC output; analog input is 0-1 for 0-3.3v (or 5 if set)
// Subtract 1 degree for self-heating
var temp = (result * 3.3 * 100) - 1;
application.distribute( "onTempValueChanged", temp.toFixed(2) );
application.distribute( "onAnalogValueChanged", result );
pubnub.publish({channel:PUBNUB_CHANNEL, message:
{"k1-fd3b584da918": {"meta": "dont care", "tlv": [ {"name": "temperature", "value": temp.toFixed(2), "units": "C"} ] }}
});
]]></method>
</behavior>
</handler>
The UI
Here we define the main container for the user interface. You'll see entries for the various text labels. Some of them have event listeners for the onAnalogValueChanged
and onTempValueChanged
events, and that's how they update the display.
<container id="MainContainer" top="0" left="0" bottom="0" right="0">
<skin color="white"/>
<label left="5" top="0" string="'PubNub Temperature Telemetry Demo'">
<style font="24px" color="red"/>
</label>
<label left="5" top="23" string="'Last Received (0):'" name="receivedLabel">
<style font="20px" color="blue"/>
</label>
<label left="5" top="39" string="'--no message received yet--'" name="receivedMessage">
<style font="14px" color="black"/>
</label>
<label left="0" right="0" top="80" string="'- - -'">
<style font="60px" color="black"/>
<behavior>
<method id="onTempValueChanged" params="content,result"><![CDATA[
content.string = "Temp: " + result + " ºC";
]]></method>
</behavior>
</label>
<label left="0" right="0" top="65" string="'- - -'">
<style font="24px" color="green"/>
<behavior>
<method id="onAnalogValueChanged" params="content,result"><![CDATA[
content.string = result.toFixed(6) + " raw analog pin value";
]]></method>
</behavior>
</label>
<picture url="'./assets/wotio_logo_500x120.png'" top="210" left="10" height="24" width="100" />
</container>
Results
It worked well! After perfecting my water boiling technique (who would have thought that was a thing), I got a great cup with the data to prove it. Dark chocolate, caramel, hints of cherry and vanilla; earthy and full.
The messages flowed to PubNub from the Kinoma Create, and anything published to PubNub from elsewhere would show up nearly instantly on the Kinoma Create's screen. Keep reading to see how we used some data services via wot.io.
World Maker Faire 2015
This setup was demonstrated at World Maker Faire 2015 in the Kinoma booth, where we also had a number of data services connected, scriptr.io, bip.io, and Circonus to start.
These fed into Twitter and Gmail also. You can see the message flow graph created with bip.io, showing the message processing and fan-out:
We've written about creating these graphs before, just look through the other posts on the wot.io labs blog for several examples.
In Closing
Kinoma's Create platform pairs effortlessly with the data services available via wot.io, and the power to leverage existing expertise in JavaScript is a huge advantage when it comes time to develop and ship your product. That power extends further with wot.io partners like scriptr, where you can integrate further cloud-based JavaScript processing into your data service exchange workflow. To get started, grab a Kinoma Create and take a look at shipiot.net today!