The Internet Of Things ("IoT") is a buzzword these days which describes physical devices that are connected to the internet (often wirelessly) which allows these devices to exchange data with the manufacturer or the owner in order to achieve greater value. Examples of IoT devices include refrigerators, washers/dryers, street lights, HVAC thermostats, electric vehicles, camera systems, etc., etc. Just about anything containing a sensor can be connected to the internet and used for monitoring or remote control. Consequently, I decided that an IoT project was in order to build simple IoT device that I could use to monitor temperature and report this to the cloud (ie a remote server). This way, I can remotely check the temperature in my garden from any device with an internet connection (like my iPhone).
In it's own right, this device is useful as it stands for use as a remotely-monitored thermometer, however, with the addition of a few more sensors (e.g. rainfall sensor, wind speed sensor, wind direction sensor, barometer, humidity sensor) it can be easily upgraded into a full-blown remote weather station. Such devices could be fabricated very cheaply and many could be deployed simultaneously in order to monitor weather in various locations simultaneously.
Central to this project will be the use of an Arduino microcontroller to which a temperature sensor and wireless connectivity will be added.
Parts
For this project I will use an Arduino Uno rev3.0 which is my go-to Arduino for anything of an experimental nature. You could pretty much use any Arduino that you might have laying around for this project. It matters not.
TMP36 - Analog Temperature Sensor
The TMP36 is an inexpensive ($1.50) solid-state, low-powered temperature sensor with a wide range (-50°C to +150°C) and a small form factor. The sensor is actually a transistor which, as its temperature increases, experiences a voltage drop between its base and emitter (known as Vbe). This voltage drop is linearly proportional to the Celsius temperature. At typical outdoor temperatures (10°C to +40°C), this sensor is accurate to within 0.5 degrees or less. By using the Arduino to measure the output voltage (we're actually measuring the Vbe) in volts, we can determine the temperature of the sensor in degrees Celsius
as follows:
Temp(°C) = [Vbe (in volts) x 100] - 50
There is no great magic in coming up with this formula. It is found by simply fitting a straight line to the sensor's empirical test data.
The wireless card will be used to provide the Arduino the ability to connect to an existing wireless network. In this way, our thermometer becomes an IoT device can connect to the internet and report sensor data to the cloud.
There are numerous wireless cards available for the Arduino.
For the purpose of this project, I decided to try out Roving Networks' RN-XV WiFly module because it is convenient and simple to configure and can be used as a drop-in replacement for an XBee module as it has the identical footprint. This makes it very easy to convert a XBee wireless device (802.15.4) to WiFi (802.11 b/g) which allows one to leverage existing WiFi hardware (which tends to be ubiquitous) and it has an ultra-low power sleep mode. Additionally, if one is smart about it, one can use the RN-XV module to send sensor data to the cloud without the need for an Arduino at all since the module contains its own processor, analog sensor interface and TCP/IP stack (although we won't demonstrate this feature in this project due to limitations on adding more sensors). It is therefore an extremely useful piece of hardware to know how to use although for the purpose of this project we will be using just its http client functionality.
Temperature Sensing
The TMP36 can handle an input voltage of 2.7V to 5.5V. Since we are going to power the sensor from the Arduino, our options are to power it at either 3.3V or 5.0V. For now, we will power it at 5.0V by wiring pin 1 on the sensor to the 5.0V pin on the Arduino and pin 3 on the sensor is wired to ground. Measurement is taken by reading the voltage on pin 2 by connecting it to any one of the Arduino's analog input pins. In this case, I arbitrarily chose to connect it to analog pin A0. Reading the the analog pins will return a value from 0 to 1023. Regardless of what input voltage is used, this range of values is mapped to the voltage range of 0V (ground) to 5V (or to 3.3V if using that power supply instead). This means that the output voltage can be determined from reading pin A0 as follows:
Voltage at Pin A0 (in volts) = (Reading from Pin A0 / 1024) x 5V
NOTE: Regardless of what power supply is used, the Vbe reading will range from 0V to approximately 1.75V
The Code
The following sketch was uploaded to the Arduino:
int sensorPin = 0;
void setup()
{
Serial.begin(9600);
}
void loop()
{
// read the temperature sensor
int reading = analogRead(sensorPin);
// convert reading to voltage (for 3.3v Arduino multiply 3.3 instead)
float voltage = reading / 1024.0 * 5.0;
Serial.print(voltage); Serial.println(" volts");
float temperatureC = (voltage - 0.5) * 100 ; //converting voltage to °C
Serial.print(temperatureC); Serial.println(" degrees C");
float temperatureF = (temperatureC * 9.0 / 5.0) + 32.0;
Serial.print(temperatureF); Serial.println(" degrees F");
Serial.println();
delay(1000);
}
This code is very straight-forward and should require very little explanation. After setting the pin number and starting the serial port, the code run a continuous loop that reads the value of the pin, converts this to a voltage and a temperature (in both Celsius and Fahrenheit) and prints these values to the serial port. After pausing for one second (1000 milliseconds), the loop repeats.
The resulting output, which can be viewed by using the serial monitor (which is available in the Arduino IDE under the 'Tools' menu) should look something like this:
Wireless Communication
The next piece to tackle is the wireless communication aspect. By making the device wireless and powering it by battery, we are given a great deal of flexibility in placing it in a desirable location - just a long as it is within range of a wireless access point. REMEMBER: air temperature is always measured in the shade. In my case, I will place the device somewhere on my patio (which is shaded).
This WiFi module can operate in various different modes depending on the exact requirements of the given application. For this particular, we would like for it to behave as a wireless client which is to say that it should be able to connect to an existing wireless network and post data to a web server. The user's manual for the RN-XV described a few different ways to configure the module, I find it easiest to first boot the module in Ad-Hoc mode, then connect to it wirelessly from my Mac and configure it using a terminal emulator to send it configuration instructions and then reboot.
For this simple mode of operation, we need connect only 4 of the RN-XV's pins to the Arduino, viz. power, ground, transmit (Tx) and receive (Rx).
NOTE: The module does not have a voltage onboard voltage regulator and therefore MUST be powered from a regulated 3.3V source. Fortunately, the Arduino provides this. As mentioned previously, the RN-XV uses the identical pin layout to that used by the Xbee modules. Pins 1 (+3.3V) and 10 (Gnd) are connected to the Arduino's 3.3V and Ground pins respectively. Pins 2 (Data Out) and 3 (Data In) are connected to the Arduino's Tx (transmit) and Rx (receive) pins respectively. Take care to ensure that the wiring is correct BEFORE powering up the Arduino. I have mistakenly cross-wired these more times than I care to admit.
The following steps were used to configure the RN-XV module to enable it to connect to the WiFi in my home:
- Connected the Arduino to the USB port on my Mac. I launched the Arduino IDE and selected the Serial Monitor from the Tools menu (make sure the Port is correctly set to USB).
- The Serial Monitor must be set to 9600 baud and select "No Line Ending" from the pulldown menu in the window.
- Type $$$ and click Send puts the module into Command mode and it responds with a CMD.
- In order to configure the module for accessing a WiFi network, we need to set the SSID and password for the network. This is achieved by using typing each of the following commands and hitting RETURN after each:
set wlan ssid <SSID>
set wlan phrase <passphrase>
set wlan join 1
save
reboot
where <SSID> and <passphrase> are substituted for the actual values for your WiFi. After each of the above command you should get the response 'AOK'. This assumes that the WiFi has been set up for DHCP. If not, then the IP information for the RN-XV needs to be set up manually (see the user's manual for commands to configure the IP information). These should be added above before the save and reboot commands. The set wlan join 1 command ensures that the RN-XV' will try to auto-connect to the network upon reboot.
- If you do not have the IP address that the RN-XV is using, here are a couple of easy ways to find this. In my case, I used my Mac to connect to my router and looked at the various devices that were connected to the router and their IP addresses. This further confirmed that the device was, in fact, connected to my WiFI router. AN alternative method is to follow steps 2 and 3 above to put the module back into Command mode and then issue the command
get ip
which will return the current IP settings. When done, issue the reboot command as above.
- As a quick test I used the Terminal app to ping the IP address of the RN-XV module to which it responded...showing that it is online.
Setting up a Remote Server
The other end of the IoT equation is setting up a remote host that will receive the sensor data, process it and likely store it to a database. To this end, there are a tons of different options. A product like
Temboo makes it very easy to integrate your project with just about any cloud-based product you can imagine. Perhaps you would like to have the sensor data posted into a Google spreadsheet with each update or add a file into Dropbox or whatever. In this case, I am going to leave Temboo for another project. For this project, I will just set up a simple server on my Mac (using Python) in order to demonstrate the principle. The server code will run a simple service which it will make available at an endpoint to allow the Arduino to post sensor data to it. Upon receipt of the data, the server will display it on the screen - nothing fancy, but demonstrates the proof-of-concept. Once the server has the data, there are a gazillion different things that can be done with it...including storing it in a database.
The Code
I have become a big fan of the Python programming language over the past few years. I find it incredibly powerful yet easy to use and deploy for simple tasks. It is perfect for this project though you could have done the same thing a bunch of different web application technologies, including using .net, Php, Ruby, etc. I don't believe that any of these make this setup any easier that this method.
The purpose of the code it to achieve the following goals:
- It must provide an address/endpoint to which the Arduino can post data.
- It must demonstrate that it has received the data and can parse it into a useable format.
For the goal of creating a light-weight server using Python, I will use Flask. Flask is a microframework based on Werkzeug and Jinja 2 and is great for building web-based applications.
from flask import Flask, request
app = Flask(__name__)
@app.route('/data', methods=['POST'])
def data():
temp = float(request.json['temperature'])
print 'Temperature is: ', temp
return ''
app.run(host='0.0.0.0', port=5000)
That's it! Just 8 lines of code and very readable.
The first two lines of code
from flask import Flask, request
app = Flask(__name__)
imports the
Flask &
request modules from the flask library and instantiates a new
Flask application object which it assigns to the variable named
app. Next, we need to define a route for the object. The line
@app.route('/data', methods=['POST'])
defines the route (endpoint) to where the object is located. This route is defined relative to the host's address. In this case, the endpoint will be located at
<hostname>/data and will be expecting data to be sent using the html
POST method.
The following lines of code define a function that gets called each time a request is
POSTed to the
def data():
temp = float(request.json['temperature'])
print 'Temperature is: ', temp
return ''
endpoint. In this case, I have (arbitrarily) named the function
data. This function expects the incoming data to be posted in JSON format (which consists of key/value pairs). This makes the protocol readily extensible and can accommodate whatever additional data elements one might wish to add at a later time. In this case, the temperature value must be accompanied by a key named
'temperature'. Flask will create a 'request' object for each incoming request. Using this object's
json method allows one to pass the name of a key and returns that corresponding value. In this case, we parse the temperature data, convert it to a float (since we will be posting it as a string) and then assign it to a variable named temp which, in the following line, is printed to the screen.
Flask will return a response object after each request consequently we need to return something at the end of the function (simply entering return makes Flask angry) so for this case we just return the empty string ''.
Finally, the last line of code
app.run(host='0.0.0.0', port=5000)
is needed to run the application - everything up to this point in the code only defines the functionality. This instructs the application to run and to listen on port 5000 and at any available IP address that is currently configured on the server.
Testing the Server Code
In order to test the server code, here is a quick test application I threw together will will post data to
import requests, json
url = 'http://192.168.1.86:5000'
payload = {'temperature': '69.5', 'humidity': '56.3', 'windspeed': '10.1'}
r = requests.post(url, data=json.dumps(payload), headers={'content-type': 'application/json'})
print r.status_code
the server in correct format. The code imports the modules we need and then defines two variables, viz.
url and
payload a. The url is just the address of the server. For the purpose of this project, I have made the server available on the same private network that the Arduino will connect to. In general, it could be made available publicly, but it is not needed for this demonstration. Consequently, the url is just the private IP address of the server PLUS the port number since by default, http requests will be made over port 80 but our server is listening on port 5000. Note also that the
payload is just a Python dictionary (which is just an unordered list), so order is unimportant as is the number of key/value pairs. I have included additional data in the
payload in order to demonstrate how this protocol can be easily extended to include additional data. Since the current server code is looking for the key 'temperature', that is the only one that
must be included. For good measure, the test code will output the response status code (200 means everything is OK; anything else indicates an error).
So, first, I run the server code in Terminal which should print the following output to the console to
* Running on http://0.0.0.0:5000/
show that its online. Then I run the above test code in Terminal and I get the following printed to the server's console which is exactly the result I was expecting. So all appears to be working from on the server side. The server code has correctly received the data and parsed it into a useable format.
Temperature is: 69.5
192.168.0.250 - - [23/Apr/2015 12:49:40] "POST /data HTTP/1.1" 200 -
In addition, the server has displayed the time, date and ip address where the posted data was received and includes the 200 status code showing that there were no errors.
Putting It All Together
There are several different WiFly libraries out there and I don't pretend to be an expert in the differences. For my purposes, I settled on
Harlequin's WiFly library which I found very easy to use. This allows me to control the RN-XV programmatically from Arduino sketch. Additionally, since we will would like to be able to print to the serial port for debug purposes (ie printing to the console), we will include the SoftSerial library so that we can communicate with the RN-XV using ports other than 0 and 1 (in this case will use 8 and 9).
The first point of order is to setup the Arduino, RN-XV and TMP36 as shown in the figure.
Remember that the TMP36 will be using the 5V power source while the RN-XV will be running off of 3.3V. Both will share a common ground. The only other difference here is that the RN-XV's Pins 2 (Data Out) and 3 (Data In) are connected to the Arduino's pins 8 and 9 respectively.
As a starting point I used code from the
examples in the library for a basic http client which provides a good initial frame work. I then edited the sketch to configure the WiFly for access to my wireless network and added two methods (functions) to allow me separately to read the temperature sensor and to post temperature data to the remote server. Seems pretty easy, we then need just the following steps:
- Setup the WiFly connection.
- Read the temperature sensor.
- Post data to the sever.
I abstracted the WiFly setup into a setup method which is just called from the setup portion of the Arduino sketch. The read and post methods are called in a continuous fashion by calling them from the loop() portion of the sketch. Additionally, there is one additional method called terminal that is provided by the example code which simply connects the serial ports and soft serial ports together (ie it wires the output of one to the input of the other and vice versa).
The Code
The final form of the Arduino sketch is as follows. I have separated the main body of the code from the methods for the sake of clarity.
#include <WiFlyHQ.h>
#include <SoftwareSerial.h>
SoftwareSerial wifiSerial(8,9);
WiFly wifly;
/* Change these to match your WiFi network */
const char mySSID[] = "<your_SSID_name>";
const char myPassword[] = "<your_password>";
const char site[] = "192.168.1.86";
float temp;
int sensorPin = 0; // (This is analog pin A0)
void terminal();
void wifi_setup();
void post(float temp);
float getTemp();
void setup()
{
wifi_setup();
}
void loop()
{
temp = getTemp();
post(temp);
delay(1000);
}
We begin by including both the WiFlyHQ and SoftwareSerial libraries and assigning the Arduino's pins 8 and 9 to be used for this purpose; these pins will be used to communicate with the RN-XV (Tx and Rx) . Doing this frees up pins 0 and 1 which means that the Arduino's actual serial port may be used for printing debug information to the console whereas pins 8 and 9 will be used for transmitting to and receiving data from the RN_XV.
The
terminal() method was lifted exactly from the example code. Its function is to run an endless loop that sends all output from the WiFly to the serial port and sends all output from the serial port to the RN_XV, so it allows one to send instructions to the:
/* Connect the WiFly (soft) serial to the serial monitor. */
void terminal(){
while (1) {
if (wifly.available() > 0) {Serial.write(wifly.read());}
if (Serial.available() > 0) {wifly.write(Serial.read());}
}
}
The post method take the temperature as a parameter (and can be easily extended to include other sensor data). It constructs a string consisting of the same JSON payload as used to test the remote server earlier - only this time the temperature value is a variable which is passed to the method when calling it. A connection to the remote server is initiated and instructions are sent to the RN-XV (and in turn to the remote host) by simply using the
println() command.
void post(float temp){
Serial.println("*** TRYING ***");
if (wifly.open(site, 5000)) {
Serial.print("Connected to ");
Serial.println(site);
String PostData3 = "{\"temperature\":\"";
String PostData2 = PostData3 + temp;
String PostData = PostData2 +"\",\"humidity\":\"56.3\",\"windspeed\":\"10.1\"}";
wifly.println("POST /data HTTP/1.1");
wifly.println("Host: 192.168.1.86:5000");
wifly.println("User-Agent: Arduino/1.0");
wifly.println("Content-Type: application/json");
wifly.print("Content-Length: ");
wifly.println(PostData.length());
wifly.println("Connection: Close");
wifly.println();
wifly.println(PostData);
} else {
Serial.println("*** NO GOOD - Sorry ***");
Serial.println("connection failed");
}
}
The
getTemp() method is just an abstraction of the code used for testing or the temperature sensor earlier in this article. It has just been wrapped up in a method to keep things neat and easily readable.
float getTemp(){
//getting the voltage reading from the temperature sensor
int reading = analogRead(sensorPin);
// converting that reading to voltage, (for 3.3v arduino use 3.3)
float voltage = reading * 5.0;
voltage /= 1024.0;
float temperatureC = (voltage - 0.5) * 100 ;
float temperatureF = (temperatureC * 9.0 / 5.0) + 32.0;
return temperatureF;
}
The code contained in the
wifi_setup() method is pretty much taken from the
example code contained in the WiFlyHQ library. It is pretty self-explanatory consisting mostly of configuring the RN-XV to connect to my network and printing to the console the various RN_XV's settings.
void wifi_setup(){ // this sets up the WiFly
char buf[32];
Serial.begin(115200);
Serial.println("Starting");
Serial.print("Free memory: ");
Serial.println(wifly.getFreeMemory(),DEC);
wifiSerial.begin(9600);
if (!wifly.begin(&wifiSerial, &Serial)) {
Serial.println("Failed to start wifly");
terminal();
}
/* Join wifi network if not already associated */
if (!wifly.isAssociated()) {
/* Setup the WiFly to connect to a wifi network */
Serial.println("Joining network");
wifly.setSSID(mySSID);
wifly.setPassphrase(myPassword);
wifly.enableDHCP();
if (wifly.join()) {
Serial.println("Joined wifi network");
} else {
Serial.println("Failed to join wifi network");
terminal();
}
} else {
Serial.println("Already joined network");
}
//terminal();
Serial.print("MAC: ");
Serial.println(wifly.getMAC(buf, sizeof(buf)));
Serial.print("IP: ");
Serial.println(wifly.getIP(buf, sizeof(buf)));
Serial.print("Netmask: ");
Serial.println(wifly.getNetmask(buf, sizeof(buf)));
Serial.print("Gateway: ");
Serial.println(wifly.getGateway(buf, sizeof(buf)));
wifly.setDeviceID("Wifly-WebClient");
Serial.print("DeviceID: ");
Serial.println(wifly.getDeviceID(buf, sizeof(buf)));
if (wifly.isConnected()) {
Serial.println("Old connection active. Closing");
wifly.close();
}
}
This method achieves programmatically what was demonstrated earlier in the article by connecting to the WiFly using the serial monitor and sending command line instructions. Note some of the methods used here like
begin(),
isAssociated(),
isConnected() and
join() which, among other things, allows the Arduino to detect various states of the RN-XV's connection to the network.
Testing the Device
For the purpose of powering the device, a battery (7V to 12V) is connected to the Vin and Ground pins of the Arduino. By connecting to Vin, we use the Arduino's built-in voltage regulator which will deliver 5V and 3.3V via the appropriate pins to the temperature sensor and wireless chip.