IoT with a Tessel


So I thought writing a post about how incredibly simple it is today to make an IoT app.

The tools I will use:

  • A Tessel with a climate and ambient sensor
  • The Meteor framework for JS

Actually, I've only completed the tutorial for the Meteor framework, but really like it so far. The aim here is also to dive a little deeper into how the Meteor framework works.

It's Saturday night and the clock is 22:30.

Step 1: Familiarization with Tessel

22:30
There's a pretty simple tutorial to get started with the Tessel http://start.tessel.io/install. This was the first step. Basically I just installed the tools, made it flash some LEDs, connected the ambient and climate sensor and played a bit around with those.

The demo code that they have for each module is very self-explanatory: Basically it's just about reading the data from the sensor and pushing it to the console. Very simple to use and understand. E.g. to read the light level from the ambient sensor you just go:

ambient.getLightLevel( function(err, ldata) {}

And bam! You have the light level. Pretty easy. The only other thing we need to do is to set the sampling rate. This is done using Node.js native event-loop API where you can just do something along the lines of:

setInterval(function(){
	ambient.getLightLevel(function(...
,1000}

In this case the sampling rate is once every second.

The next step is then to connect the Tessel to the wifi. Following the tutorial it's also quite easy. Here (all the way at the bottom) they have some code which basically connects the Tessel to your wifi (if you fill in the blanks).

However, when playing around with it later I noticed that the wifi doesn't seem to be very stable. Every once in a while when rebooting the Tessel it wouldn't connect to the wifi. Just rebooting it again and it seemed to be able to connect. Not sure where the problem lies here.

In this part of the Tessel tutorial they also show how to use the Nodejs http npm to access websites. It worked very nice but I needed to use a quite different approach to make it talk to my webserver. More on that later.

Using the Tessel turned out to be very very easy. Just connect the modules and do as your told.

Step 2: Setup API to handle data

22:50
In another Nodejs project I'm working on right now I'm using the Sailsjs framework. But as mentioned earlier for this app I wanted to experiment with Meteor. And the first step is to figure out how to make a simple API that supports CRUD operations, which the Tessel can talk to (actually, I only need the 'C').

A bit of looking around in the Meteor documentation yielded the CollectionAPI. I'm telling you - this thing is the bomb. It's even easier to make a CRUD API with this baby than with Sails.

All you do is install it

meteor add mrt:collection-api 

and then in the server part of the Meteor code, this:

collectionApi = new CollectionAPI({
    apiPath: 'data'
  }
);
collectionApi.addCollection(Ambient, 'ambient');
collectionApi.addCollection(Climate, 'climate');
collectionApi.start();

.. and voila we have a CRUD API connected to two collections: Ambient and Climate. Now I can do stuff like:

curl http://localhost:3000/data/climate

.. to get all records in my Climate collection and this

curl -d "{\"time\": \"2014-12-01 02:03\", \"temp\": \"15\", \"humid\": \"0.6\"}" http://localhost:3000/data/climate

.. to send data to the Climate collection.

Piece of cake. No, it's easier than eating a piece of cake. It's "glass of water" easy.

Step 3: Back to Tessel

23:07
So I needed to look a little deeper into how the Tessel samples data and the ambient sensor allows you to read a buffer instead of just a single point. I guess to enhance the signal quality, so I'm reading the buffer instead for both light and sound and then do an average on the buffers.

I also needed to think a bit about how often I want to sample data. So I'm not interested in very fine-grained readings so to begin with (and to be able to test faster) I just set the sample rate every 10seconds.

I also restructured the code such that after reading and averaging the data I would send an http request.

Step 4: Server

23:35
Ok, so now we need to put the meteor site somewhere that is not just localhost so our Tessel can communicate with it. After googling a bit I found a nice guide that explained how to set up Meteor on an Amazon EC2.

So I sat up a simple EC2 T2.micro instance running Ubuntu as this guide explains. I quickly did the steps contained in the guide and ran into a couple of minor issues.

I also added a CNAME record to my reibel.io pointing the subdomain data.reibel.io to my new Amazon EC2 instance.

The final step about building the meteor app on the server didn't really work. This is what I did:

  1. Enter the dir of where I cloned the app to.

  2. meteor build .

  3. tar -zxvf datareibel.tar.gz

  4. Now when I tried to run the app with

    sudo PORT=80 MONGO_URL=mongodb://localhost:27017/meteor ROOT_URL=http://data.reibel.io node bundle/main.js

it gave a bunch of error messages with almost no hint about what was wrong. The solution here was to go in to bundle/programs/server and run sudo npm install
5. Now I was able to start my app using

sudo PORT=80 MONGO\_URL=mongodb://localhost:27017/meteor ROOT\_URL=http://data.reibel.io node bundle/main.js

(Later on I found out that I could just have deployed to meteor.com using meteor deploy and just pointed the CNAME there with everything setup and working - oh well).

Step 5: Calling the API

23:47
So now we have the Tessel that is reading the sensor data and we have a simple app running on Amazon EC2 instance. The next step is to make the Tessel submit the data to the app using the API we made using CollectionAPI.

This posed the first "major" challenge. In a regular NodeJS app I could do something along the lines of

var body = {role: req.param("role"), type: "user", value: req.param("useremail")};
var args = {
	"method": "POST",
	"headers": { 
	"Authorization": "Bearer " + JSON.parse(guser.tokens.access_token),
	"content-type": "application/json",
},
"url": "https://www.googleapis.com/drive/v2/files/" + req.param("fileid") + "/permissions",
"body": JSON.stringify(body)

};
request(args, function(err, resp, body){...

.. using the npm request for instance. But this wasn't working. I also tried to get the Tessel to do the same using the http npm which was used during the tutorial.

I finally managed to get it to work using a straightforward call with the needle npm. So now I could do this

var URL = 'http://data.reibel.io/data/ambient';
var data = {
      light: light.toFixed(10),
      sound: sound.toFixed(10),
      time: new Date().toISOString().replace(/T/, '').replace(/\..+/, '')
};
needle.request('POST',URL,JSON.stringify(data), function(err,res){
	if (err) console.log("ERR",err);
	else console.log("RES",res.body);
});

.. this worked flawlessly and straight out of the box.

The final thing I needed to do was to figure out how to get a Date (very funny, I actually have a girlfriend). I used the moment npm earlier and liked it so I started with that. However, this proved to be a bad idea, when it came to running it on the Tessel.

When the Tessel runs your code it also tells you how much memory the code occupies and before adding moment it was occupying 0.5KB roughly. But moment put it at 3 MB. And I found that when generating the timestamp as seen above done with Date, moment was simply taking too long time in executing it (like several seconds). So I dumped moment and just went with the Nodejs native Date.

Step 6: Graphing

00:15
So we need an easy-to-use graph framework. I used D3 earlier, but I thought it would be overkill. So I went for Morrisjs. Morris already had a wrapper for Meteor so it seemed simple. Simply do meteor add chhib:morris

With Morris installed I spent some time figuring out how to get the data into Morris also such that I could dynamically update it. This turned out to be the biggest challenge of this small evening project. I thought I was very close to making that happen, but it turned out that I needed several steps.

Step 7: Getting data to Morris

00:40
The problem with getting data to Morris was that I had not understood how Meteor loads the app.

First of all I needed to get rid of the autopublish module which is included in Meteor meteor remove autopublish

Then I needed to make the server publish and the client subscribe to the two collections. Like so:

// Server part
Meteor.publish("ambient", function () {
  return Ambient.find({});
});

Meteor.publish("climate",function(){
	return Climate.find({});
});

// client part
Meteor.subscribe("ambient");
Meteor.subscribe("climate");

However, just doing the publishing and subscription is not enough as I need to do something once all the data has been downloaded in the subscribe call, so I have to add a callback that is called once all data has been downloaded to the client.

And it is HERE inside the subscribe callback that we create the graph on the div that I have made in the HTML. Here we also know that the client has loaded the DOM so we can extract the ambientChart div which should contain our graph.

Meteor.subscribe("ambient", function(){
var ymer = document.getElementById("ambientChart");
var ambdata = Ambient.find({}).fetch();
var graph = Morris.Line({
  element: ymer,
  // the chart.
  data: ambdata,
  xkey: 'time',
  ykeys: ['light','sound'],
  labels: ['Light','Sound'],
  lineColors: ['#1dc8ea','#7b0303'],
  grid: false
});

Now I get the data from the fetch call immediately, because the subscribe call make sure that it has already been downloaded to the client.

The final part is making sure I can handle when data get's updated. This is also done in the same part of the code just by adding:

Ambient.find({}).observeChanges({
  added: function(id,fields){
    ambdata.push(fields);
    graph.setData(ambdata);
  }
});

What this one does is that it queries the collection and then observes if there are any changes. If there is I simply add the data to the graph. You can watch for more stuff than this, but because I know that the Tessel should never change or delete data already there I can do with the added function.

Obviously I need to do the same thing for the climate data graph.

Step 8: Basic security

1:40
Time to add some basic security. Not really that concerned, but what I'm going to do is simply to add an accesstoken to be used when accessing the CollectionAPI CRUD methods. Fortunately, with CollectionAPI this is dead simple:

collectionApi = new CollectionAPI({ 
    authToken: '97f0ad9e24ca5e0408a269748d7fe0a0',
    apiPath: 'data'
  }
);

(For the record, the above is not my real accesstoken.) Now I can only talk wit the data/ambient and data/climate API if I add a header containing the field X-Auth-Token with the value of my token. If this was a commercial application obviously we would need way more security, but it's not really and it's getting late.

Step 8: Final testing

1:43
Ok now the time has come to do the final testing. Goto data.reibel.io and see for yourself! The Tessel should update the data once every 10 minutes.
IoT dashboard

You can play with the code yourself here: https://github.com/RedDeathAt614/IoTTessel1

Some thoughts on addons:

  • Bi-directionality: i.e. press a button on the web interface and order the Tessel to take a new measurement.
  • Sampling: Right now the Tessel just samples once every 10 minutes. Maybe sample more often and do an average to get a more precise measurement. This is probably not going to impact the temperature, humidity and light alot but probably the sound should be impacted.

Update

So it seems like the wifi chip on Tessel is extremely unstable. The longest I could run it without loosing connection was a couple of hours. There has been several blog posts about this as well, such as this. It seems like the problem is especially tough if the router is doing simultaneously 2.4GHz and 5GHz and mine is...
A little disappointed in the Tessel I must admit. For this reason if you check the demo the Tessel is most likely switched off.

Michael Reibel Boesen

comments powered by Disqus