Introduction: Internet of Things
Measure real world things, turn a knob and move a servo on the other side of the world. This has been possible with a PC but now it is possible using inexpensive boards and low or battery power. This project uses pre-made Arduino boards and no soldering is required.
This project was inspired by this Instructable https://www.instructables.com/id/ARDUINO-ENERGY-METER/
Step 1: The Internet of Things - Smaller, Cheaper, Less Power
A standard Arduino board is now under $10. A W5100 Ethernet shield is also under $10. A LCD keypad display shield is under $6. Debugging Arduino can be done via the serial port, but for out in the field there are some advantages to having a display. Adding a display can be done in many ways, and for this project we are looking to use premade boards. There are two small catches - some pins clash with the Ethernet shield and the LCD shield, and also the display shield shorts out a few pins on the metal of the Ethernet plug, so it needs to be raised up higher.
To get a cheap board, search Arduino on ebay and sort on price+postage and “buy it now”. Then for the Ethernet board, search on Arduino Ethernet. For the display board, search Arduino LCD Keypad.
Step 2: A Small Hack - Snap Off a Pin
The Ethernet shield uses pins 10,11,12,13. The standard Arduino display (as per code on the Arduino site) uses pins 2,3,4,5,11,12. The pre- made display shield uses 4,5,6,7,8,9,10. That still clashes with the Ethernet shield, but fortunately pin 10 is not really needed, as it is used to turn off the backlight. There is a transistor controlling this light, and the simple hack is to break off pin 10 under the LCD board. The backlight now is always on, and there are no conflicts with the Ethernet shield. To remove this pin, grab it with some pliers and bend it back and forth until it breaks off. As another option, it may be possible to trace the track on the PCB and cut this.
The next issue is the height of the board. Search on ebay for Arduino Stackable Header. This project needs some 6s and some 8s, and these often come in kits. Get a few more – they are very cheap and will come in handy for other projects.
The hardware is almost done – just plug it all together.
Step 3: Wifi or Ethernet
There are 5 analog inputs on an Arduino and the LCD board uses analog 0 to read all the keypresses, so that leaves 4 analog channels and some digital ones too. The buttons could be disabled by removing analog pin 0 under the board.
It might at this point be worth mentioning wireless vs wifi. There are wifi Arduino boards available, but they are expensive – a quick check on ebay is around $57. A slightly cheaper option is to get a wifi router and configure it as a wifi repeater and use a short Ethernet patch cable. I used a TP Link router (search ebay for TP Link Repeater) which was $38. Add the $10 for the Ethernet board and it is cheaper. It is also a bit more flexible as you can add an Ethernet switch and have multiple Ethernet sockets and hence many Arduino boards. Configuring the router as a repeater is very simple – follow the instructions, log into the router, let it search for your wifi, add the password and save. The only small catch – the router has a dedicated IP address, and if it is then set up as a repeater, it asks the main router to allocate IP addresses. This means if you want to repeat a different main router it is hard to log back into the repeater! Do a factory reset if this happens.
Step 4: Talk to the Cloud
For software, there are many ways to configure things. Arduino has code to create a small webpage server, and other code to read this, so it can be done locally. Or, as in this example, we can upload to the cloud, and download at any location there is internet or wifi. This example uses Xively and their site will display the data in a graph format. This code reads 5 analog values, uploads these, then reads them back and extracts the actual values from the Xively text stream. Xively is free, and you need an account. Log in and click on Develop. Click on Add Device, and there are two numbers. The first is the device feed number which is around 9 numbers. Then there is the API key which is a longer number with numbers and letters. Copy and paste these into the code. Then add some channels – this project uses five channels and I called them Sensor1, Sensor2 etc.
Step 5: Program the Board
There are seveal important things to change in the code below. The first is the IP address. Each board has to have a unique IP address otherwise the router will get very confused. I started at IPAddress ip(192,168,1,178); and then added one to the last number. Some routers have different numbers eg 192.168.2.x and a quick check on a PC running IPCONFIG in a DOS shell will give the correct first 3 numbers.
The other number to change is the MAC address range. The range byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE}; is a default number – maybe change one to a different hex number on each board, eg the last one, count backwards in hex 0xED, 0xEC.
The final thing to change is whether the board is an uploader or downloader. This code does both and about half way down is this
if(!client.connected() && (millis() - lastConnectionTime > postingInterval)) {
sendData(Analog0,Analog1,Analog2,Analog3,Analog4);
// comment out either send or get data
//getData();
which is configured to send data. To fetch back that data, comment out senddata, and uncomment getdata.
There is some leftover code commented out for things like dumping out the entire text string from Xively which is handy for debugging to work out how to cut up the string and extract the individual sensor readings.
Xively can do other things such as send an SMS or other message when certain conditions are met.
Have Fun!
/*
Pachube sensor client
This sketch connects an analog sensor to Pachube (http://www.pachube.com)
using a Wiznet Ethernet shield. You can use the Arduino Ethernet shield, or
the Adafruit Ethernet shield, either one will work, as long as it's got
a Wiznet Ethernet module on board.
This example has been updated to use version 2.0 of the Pachube.com API.
To make it work, create a feed with a datastream, and give it the ID
sensor1. Or change the code below to match your feed.
Circuit:
* Analog sensor attached to analog in 0 for pushbuttons
* Ethernet shield attached to pins 10, 11, 12, 13
The LCD circuit: standard is 12,11,5,4,3,2 change to 8,9,4,5,6,7
* LCD RS pin to digital pin 12
* LCD Enable pin to digital pin 11
* LCD D4 pin to digital pin 5
* LCD D5 pin to digital pin 4
* LCD D6 pin to digital pin 3
* LCD D7 pin to digital pin 2
and cut the header pin to D10 going to the LCD display (ethernet board needs this, and on the LCD display only used to turn backlight off
Serial debug commented out now and send to LCD instead
Change IP address to a different number for each board
created 15 March 2010
modified 9 Apr 2012
by Tom Igoe with input from Usman Haque and Joe Saavedra
http://arduino.cc/en/Tutorial/PachubeClient
This code is in the public domain.
*/
#include <SPI.h>
#include <Ethernet.h>
#include <LiquidCrystal.h>
#define APIKEY "shsCNFtxuGELLZx8ehqglXAgDo9lkyBam5Zj22p3g3urH2FM" // replace your pachube api key here
#define FEEDID 970253233 // replace your feed ID
#define USERAGENT "Arduino1" // user agent is the project name
// assign a MAC address for the ethernet controller.
// Newer Ethernet shields have a MAC address printed on a sticker on the shield
// fill in your address here:
byte mac[] = {
0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xEE};
// fill in an available IP address on your network here,
// for manual configuration:
IPAddress ip(192,168,1,178);
// initialize the library instance:
EthernetClient client;
// initialize the lcd library with the numbers of the interface pins
LiquidCrystal lcd(8,9,4,5,6,7);
// if you don't want to use DNS (and reduce your sketch size)
// use the numeric IP instead of the name for the server:
//IPAddress server(216,52,233,122); // numeric IP for api.pachube.com
char server[] = "api.xively.com"; // name address for xively API
unsigned long lastConnectionTime = 0; // last time you connected to the server, in milliseconds
boolean lastConnected = false; // state of the connection last time through the main loop
const unsigned long postingInterval = 10*1000; //delay between updates to Pachube.com
int counter;
String stringOne,stringTwo; // built string when data comes back
boolean stringData = false; // reset when a new block of data comes back
void setup() {
//delay(1000); // in case the serial port causes a latchup
// Open serial communications and wait for port to open:
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for Leonardo only
}
Serial.println("api.xively.com"); // if go more than ?32s then integer calc doesn't work
lcd.begin(16, 2);
Cls(); // clear the screen
delay(1000);
lcd.setCursor(0,1); // x,y top left corner is 0,0
lcd.print("api.xively.com");
lcd.setCursor(0,0);
lcd.print("Start Ethernet");
////Serial.println("Start Ethernet");
delay(1000);
// start the Ethernet connection:
if (Ethernet.begin(mac) == 0) {
//Serial.println("Failed to configure Ethernet using DHCP");
// DHCP failed, so use a fixed IP address:
//lcd.setCursor(0,1);
//lcd.print("Failed to configure");
Ethernet.begin(mac, ip);
}
Serial.println("Wait 10s");
lcd.setCursor(0,1);
lcd.print("Wait 10s ");
}
void loop() {
// read the analog sensor:
int Analog0 = analogRead(A0); // with a LCD display analog0 is all the buttons
int Analog1 = analogRead(A1);
int Analog2 = analogRead(A2);
int Analog3 = analogRead(A3);
int Analog4 = analogRead(A4);
// int sensorReading = analogRead(A2);
// if there's incoming data from the net connection.
// send it out the serial port. This is for debugging
// purposes only:
if (client.available()) {
char c = client.read();
Serial.print(c);
if (stringData == false)
{
stringData = true; // some data has come in
}
if (stringData == true)
{
stringOne += c; // build the string
}
if ((c>32) and (c<127))
{
lcd.print(c);
counter +=1;
}
if (counter==16)
{
lcd.setCursor(0,1);
//lcd.print(" ");
//lcd.setCursor(0,1);
counter = 0;
//delay(100);
}
}
// if there's no net connection, but there was one last time
// through the loop, then stop the client:
if (!client.connected() && lastConnected) {
//Serial.println();
//Serial.println("Disconnect");
client.stop();
lcd.setCursor(0,0);
lcd.print("Disconnect ");
lcd.setCursor(0,1);
counter = 0;
if (stringData == true)
{
PrintResults(); // extract the values and print out
stringData = false; // reset the flag
stringOne = ""; // clear the string
}
}
// if you're not connected, and ten seconds have passed since
// your last connection, then connect again and send data:
if(!client.connected() && (millis() - lastConnectionTime > postingInterval)) {
//sendData(Analog0,Analog1,Analog2,Analog3,Analog4);
// comment out either send or get data
getData();
}
// store the state of the connection for next time through
// the loop:
lastConnected = client.connected();
}
void PrintResults() // print results of the GET from xively
{
int n = 292; // start at the sensor data
int i;
char lf = 10;
int v;
Cls();
lcd.setCursor(0,0);
stringOne += lf; // add an end of line character
for(i=0;i<5;i++)
{
while (stringOne.charAt(n) != 44) // find first comma
{
n +=1;
}
n += 1;
while (stringOne.charAt(n) != 44) // find second comma
{
n+=1 ;
}
n+=1;
stringTwo = "";
while (stringOne.charAt(n) != 10) // find the end of the line which is a line feed ascii 10
{
//lcd.print(stringOne.charAt(n));
stringTwo+=stringOne.charAt(n);
n+=1;
}
v=stringTwo.toInt();
lcd.print(v);
lcd.print(" "); // space at end
if (i==1)
{
lcd.setCursor(0,1);
}
}
}
void Cls() // clear LCD screen
{
lcd.setCursor(0,0);
lcd.print(" "); // clear lcd screen
lcd.setCursor(0,1);
lcd.print(" ");
}
void PrintValues(int n0,int n1,int n2, int n3, int n4)
{
//Serial.print(n0);
//Serial.print(" ");
//Serial.print(n1);
//Serial.print(" ");
//Serial.print(n2);
//Serial.print(" ");
//Serial.print(n3);
//Serial.print(" ");
//Serial.println(n4);
Cls();
lcd.setCursor(0,0);
lcd.print(n0);
lcd.print(" ");
lcd.print(n1);
lcd.setCursor(0,1);
lcd.print(n2);
lcd.print(" ");
lcd.print(n3);
lcd.print(" ");
lcd.print(n4);
delay(2000);
}
// this method makes a HTTP connection to the server:
void sendData(int data0,int data1,int data2,int data3, int data4) {
PrintValues(data0,data1,data2,data3,data4);
//Serial.println("Connecting...");
lcd.setCursor(0,0);
lcd.print("Connecting... ");
lcd.setCursor(0,1);
lcd.print("No reply "); // if there is a reply this will very quickly get overwritten
lcd.setCursor(0,1);
counter = 0;
// if there's a successful connection:
if (client.connect(server, 80)) {
client.print("PUT /v2/feeds/");
client.print(FEEDID);
client.println(".csv HTTP/1.1");
client.println("Host: api.pachube.com");
client.print("X-PachubeApiKey: ");
client.println(APIKEY);
client.print("User-Agent: ");
client.println(USERAGENT);
client.print("Content-Length: ");
// calculate the length of the sensor reading in bytes:
// 8 bytes for "sensor1," + number of digits of the data:
//int thisLength = 8 + getLength(thisData);
//client.println(thisLength);
// 8 is length of sensor1 and 2 more for crlf
int stringLength = 8 + getLength(data0) + 10 + getLength(data1) + 10 + getLength(data2) + 10 + getLength(data3) + 10 + getLength(data4);
client.println(stringLength);
// last pieces of the HTTP PUT request:
client.println("Content-Type: text/csv");
client.println("Connection: close");
client.println();
// here's the actual content of the PUT request:
client.print("sensor1,");
client.println(data0);
client.print("sensor2,");
client.println(data1);
client.print("sensor3,");
client.println(data2);
client.print("sensor4,");
client.println(data3);
client.print("sensor5,");
client.println(data4);
//Serial.println("Wait for reply"); // xively responds with some text, if nothing then there is an error
lcd.setCursor(0,1);
lcd.print("Wait for reply ");
}
else {
// if you couldn't make a connection:
//Serial.println("connection failed");
//Serial.println();
//Serial.println("so disconnecting.");
client.stop();
//lcd.setCursor(0,1);
//lcd.print("Connect Fail");
}
// note the time that the connection was made or attempted:
lastConnectionTime = millis();
}
// this method makes a HTTP connection to the server:
void getData() {
// if there's a successful connection:
if (client.connect(server, 80)) {
//Serial.println("connecting to request data...");
lcd.setCursor(0,0);
lcd.print("Connect ");
client.print("GET /v2/feeds/");
client.print(FEEDID);
client.println(".csv HTTP/1.1");
client.println("Host: api.pachube.com");
client.print("X-PachubeApiKey: ");
client.println(APIKEY);
client.print("User-Agent: ");
client.println(USERAGENT);
client.println("Content-Type: text/csv");
client.println("Connection: close");
client.println();
//Serial.println("Finished requesting, wait for response.");
lcd.setCursor(0,1);
lcd.print("Finish request");
}
else {
// if you couldn't make a connection:
//Serial.println("connection failed");
//Serial.println();
//Serial.println("so disconnecting.");
client.stop();
}
// note the time that the connection was made or attempted:
lastConnectionTime = millis();
}
// This method calculates the number of digits in the
// sensor reading. Since each digit of the ASCII decimal
// representation is a byte, the number of digits equals
// the number of bytes:
int getLength(int someValue) {
// there's at least one byte:
int digits = 1;
// continually divide the value by ten,
// adding one to the digit count for each
// time you divide, until you're at 0:
int dividend = someValue /10;
while (dividend > 0) {
dividend = dividend /10;
digits++;
}
// return the number of digits:
return digits;
}
Step 6: VB Net and Xively
After testing this out for several months I have come across some problems with the ethernet shield reliability. This is mainly if there are multiple router/repeater hops and might be due to timeout delays. There are problems with coping with semi-reliable connections which of course includes radio links that may be subject to interference. There may also be bugs in the standard Arduino ethernet shield code - there do seem to be a number of fixes on the internet but I am not sure which ones work. It is not the easiest to debug as the whole system will run for several days and then hang.
A hardware hack is to have one Arduino controlling a relay and turning on the power to a second Arduino that has an ethernet shield. Then the whole system can be powered down and then powered back up again.
Another option might be to look at wifi modules - this year (2014) they have come down to as low as $5, and these might eventually come with code that hopefully fails more gracefully, or perhaps can be reset with software.
Another fix is to use a computer as the internet interface. A small netbook will do. The following code is vb.net and listens to the arduino on a com port and then uploads the data to xively.
Imports System
Imports System.IO Imports System.Net Imports System.Text ' create a form. From the toolbox add button1, textbox1, textbox2, timer1, serialport1 ' change the timer1 ticks to 4000. Change timer1 enabled to True ' in the opencomport routine, change the com port number ' add checkbox1, name it upload continuously
' Arduino test code '// sends an increasing number every 5 secs 'int n; 'void setup() '{ ' Serial.begin(9600); // also talk at a slow 1200 baud - easier debugging if all baud rates the same ' while (!Serial) {} ; //wait to connect '} 'void loop() // run over and over '{ ' Serial.println(n); ' n += 1; ' delay(5000); '}
Public Class Form1 Public InPacket(0 To 2000) As Byte
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load OpenComPort() end sub Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click xivelyFeedUpdate("shsCNFtxuGELLZx8ehqglXAgDo9lkyBam5Zj22p3g3urH2FM", "970253233", "sensor1", "14") End Sub
Sub xivelyFeedUpdate(ByVal ApiKey As String, ByVal feedId As String, ByVal channel As String, ByVal value As String) Dim request As WebRequest = WebRequest.Create("http://api.xively.com/v2/feeds/" + feedId + ".csv") Dim postData As String postData = channel + "," + value ' eg sensor1,5 ' build string to send Dim byteArray As Byte() = Encoding.UTF8.GetBytes(postData) request.Method = "PUT" ' PUT or GET request.ContentLength = byteArray.Length ' the length of channel and value request.ContentType = "text/csv" ' text and comma separated data request.Headers.Add("X-ApiKey", ApiKey) ' send the header request.Timeout = 5000 Try Dim dataStream As Stream = request.GetRequestStream() ' Get the request stream. dataStream.Write(byteArray, 0, byteArray.Length) ' Write the data to the request stream. dataStream.Close() ' Close the Stream object. Dim response As WebResponse = request.GetResponse() ' Get the response - usually just Ok ' need to add a try/catch error routine here in case the internet connection goes down TextBox1.Text += CType(response, HttpWebResponse).StatusDescription ' Display the status. dataStream = response.GetResponseStream() ' Get the stream containing content returned by the server. Dim reader As New StreamReader(dataStream) ' Open the stream using a StreamReader for easy access. Dim responseFromServer As String = reader.ReadToEnd() ' Read the content. TextBox1.Text += responseFromServer ' Display the content. reader.Close() ' close the streams dataStream.Close() response.Close() Catch ex As Exception TextBox1.Text = "No connection" End Try End Sub Sub OpenComPort() Try SerialPort1.PortName = "COM9" ' windows key, "control panel", device manager, serial ports to find the number SerialPort1.BaudRate = "9600" SerialPort1.Parity = IO.Ports.Parity.None ' no parity SerialPort1.DataBits = 8 ' 8 bits SerialPort1.StopBits = IO.Ports.StopBits.One ' one stop bit 'SerialPort1.ReadTimeout = 1000 ' milliseconds so times out in 1 second if no response SerialPort1.Open() ' open the port SerialPort1.DiscardInBuffer() ' clear the input buffer 'SerialPort1.Handshake = System.IO.Ports.Handshake.RequestToSend 'handshaking on (or .None to turn off) Catch ex As Exception MsgBox("Error opening serial port - is another program using the selected COM port?") End Try End Sub Private Sub Timer1_Tick_1(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Timer1.Tick Dim BytesToRead As Integer Dim i As Integer Dim Character As String ' collect bytes from the serial port Timer1.Enabled = False TextBox2.Clear() ' clear the text box If SerialPort1.IsOpen = True Then Do If SerialPort1.BytesToRead = 0 Then Exit Do ' no more bytes BytesToRead = SerialPort1.BytesToRead If BytesToRead > 2000 Then BytesToRead = 2000 SerialPort1.Read(InPacket, 0, BytesToRead) ' read in a packet For i = 1 To BytesToRead Character = Strings.Chr(InPacket(i - 1)) TextBox2.Text += Character ' add to the text box Next Loop If CheckBox1.Checked = True Then TextBox1.Clear() xivelyFeedUpdate("shsCNFtxuGELLZx8ehqglXAgDo9lkyBam5Zj22p3g3urH2FM", "970253233", "sensor1", Str(Val(TextBox2.Text))) End If End If Timer1.Enabled = True End Sub End Class