Thursday, 29 December 2011

Arduino Live GPS Tracker - HOWTO

As promised earlier, this post is a complete HOWTO with code on how to use an Antrax GSM/GPRS/GPS Shield with an Arduino for Live Position Tracking.  Note that I am using the Rev 2 of the Antrax GSM/GPRS/GPS shield which has the Telit GE864-QUAD GSM Module controlled via Serial and the Origin ORG1318 GPS Module controlled via SPI interface. Antrax has since then launched a Rev 6 Version which uses the Telit GE865-QUAD GSM module and the Fastrax UP501 GPS module.

Objective
We will try to get the GPS position and send the data to an HTTP Server to store it which can then be used for various purposes one of which is plotting the current position on Google Maps.

SPI Wiring
The SPI port on the Arduino Mega is at pin 50-53 as opposed to 10-13 on the Arduino Uno. Hence we need to do some soldering to connect the corresponding SPI pins from the shield to the right ports. Antrax provides all the necessary tidbits to do this. The rework instructions can be obtained from here. Be careful while soldering. If you do not know how to do it get the help of someone who knows.

The rework documentation shows the pins 10-13 bent outside. However, in my case, it's bent inwards to save space in case I decide to box the final product.

Now mount the shield over the Arduino Mega/Uno making sure all the pins are correctly aligned. Place in the SIM card with the notched edge facing outwards and the logo upwards. The final setup should look like this.
My Kit with the Vodafone Sim inserted. The White cable is the SPI connection.
Software
I used the latest version of Arduino IDE available(1.0). Two changes I noticed with this version are that WProgam.h is replaced by Arduino.h and Serial.print(byte, BYTE) has been replaced with Serial.Write(byte).

Make sure you have selected the right board in Tools>Board. For me it is the "Arduino Mega 2560 or Mega ADK". Also ensure you have selected the right Serial Port in Tools>Serial Port. You can identify your Serial Port in Linux by tailing the /var/log/messages when connecting the Arduino via USB.

Arduino Code
Projects in Arduino are called Sketches. Our Sketch will include 3 files. Arduino IDE does not allow adding separate files from the IDE, so it is advisable to create these outside of the IDE and then open the .ino file with Arduino which will open all the three files. Arduino also requires that the ino file and the directory containing it should have the same basename. So, if your ino file is called GSM.ino the folder which holds this file should be called GSM.

For my example the folder is called GSM_HTTP. Use a text editor for copying the code into the individual files.

Following is the code for the GSM_HTTP.ino file:

/* Filename: GSM_HTTP.ino
 * Author: Sam Albuquerque
 * Description: Sketch to periodically upload GPS data over GPRS
 *              to remote Server via HTTP connection
 * used with Arduino Mega
 */

#include "GM862.h"
#include <SPI.h>
#define GSMPin 7    //Pin 7 is the Power Pin for our shield
#define baud 9600

//Class to handle the GPS part of the shield.
class GPS {
  public:
    GPS(int ledPin);
    char coordinate[23];  //DDMM.SSSSN,DDDMM.SSSSW
    boolean gpsState;  
    void getGPS();
    long time;            //HHMMSS  UTC time
  private:
    int gpsLed;           //PIN Number for GPS LED
};


GPS::GPS(int ledPin){
  gpsLed=ledPin;
  pinMode(ledPin, OUTPUT);
  digitalWrite(ledPin,LOW);
  gpsState = false;
  time=0;
}

void GPS::getGPS(){
  char gps_data_buffer[7];
  char command_result;
  byte high_Byte;
  int i;
  byte GPGGA;

  SPI.begin();
  SPI.setDataMode((0<<CPOL) | (1 << CPHA) |(1<<SPI2X));

  delay(1000);

  GPGGA=0;
  i=0;
  do {
    digitalWrite(SS, LOW);
   
    if(high_Byte == 1){
      command_result=SPI.transfer(0xA7);
      high_Byte = 0;
    } else {
      command_result=SPI.transfer(0xB4);
      high_Byte = 1;
    }
    for(int index=0; index<6; index++){
      gps_data_buffer[index] = gps_data_buffer[index + 1];
    }
    gps_data_buffer[6] = command_result;

    if((gps_data_buffer[0] == '$') && (gps_data_buffer[1] == 'G') && (gps_data_buffer[2] == 'P') && (gps_data_buffer[3] == 'G') && (gps_data_buffer[4] =='G') && (gps_data_buffer[5] == 'A')) {
      GPGGA = 1;         //We found a $GPGGA Sentence
    }

    if((GPGGA == 1) && (i < 80 ))
    {

      if(gps_data_buffer[0] == 0x0D) { //Sentence ends with 0x0D(Return) Character
        i = 80;
        GPGGA = 0;
      } else {
        if(i>=7 && i<13){   //Characters 7-12 are UTC Time String
          time = ((time*10) + (int(gps_data_buffer[0]) - 48))%1000000;
        }
        if(i>=18 && i<27){  //Characters 18-26 are Latitude
         coordinate[i-18]=gps_data_buffer[0];
        }
        if(i==28) coordinate[9]=gps_data_buffer[0];  //Character 28 is either N or S Hemisphere
        if(i==29) coordinate[10]=',';       
        if(i>=30 && i<40){  //Characters 30 to 39 are Longitude
         coordinate[i-19]=gps_data_buffer[0];
        }        
        if(i==41) coordinate[21]=gps_data_buffer[0];  

        i++;                //Character 41 is either E or W Hemisphere
        }
    }

    digitalWrite(SS, HIGH);
  } while (i<80);
  if((coordinate[0]) == ','){
      gpsState=false;          //GPS Position Not Found
  } else {
    gpsState=true;            //GPS Position Found
    coordinate[22]='\0';      //Valid String ends with Null Character.
  }
  digitalWrite(gpsLed, gpsState);
  return;
}

GPS gps(9);                          //Pin 9 is used to control the GPS LED.

char PIN[5] = "0000";                // replace this with your SIM PIN
GM862 modem(&Serial, GSMPin, PIN);   // Our shield communicates over default Serial
char cmd;                            // command read from terminal
long lastTime=0;

void setup() {          
  Serial.begin(9600);
  Serial.println("GM862 monitor");
  modem.switchOn();                   // switch the modem on
  delay(4000);                        // wait for the modem to boot
  modem.init();                       // initialize the GSM part of Module
  modem.version();                    // request modem version info
  while (!modem.isRegistered()) {
    delay(1000);
    modem.checkNetwork();             // check the network availability
  }

}


void requestHTTP() {
  char buf[100];
  byte i = 0;
  modem.initGPRS();                   // setup of GPRS context
  modem.enableGPRS();                 // switch GPRS on
  modem.openHTTP("IP.AD.RE.SS");    //  Replace IP.AD.RE.SS with your target's IP

                                    //   or domain name
  modem.send("GET /location.php?time=");  //Connects to location.php on Server
  Serial.print(gps.time);                 //Passes UTC Time
  modem.send("&loc=");                    // and
  modem.send(gps.coordinate);             //GPS coordinates
  modem.send(" HTTP/1.1\r\n");          //Completes HTTP GET Request
  modem.send("HOST: IP.AD.RE.SS \r\n");   //write on the socket
  modem.send("\r\n");                     //Completes the HTTP Connection
  delay(1000);
  while (i++ < 10) {                  // try to read for 10s
    modem.receive(buf);               // read from the socket, timeout 1s
    if (strlen(buf) > 0) {            // we received something
      i--;                            // reset the timeout
    }
  }
  modem.disableGPRS();                // switch GPRS off
}


void loop() {
    gps.getGPS();
     if(gps.gpsState==true){
       if(gps.time-lastTime > 100){       //if posted more than a minute ago
          requestHTTP();                  // do a HTTP request
          lastTime=gps.time;              //Store the last update time
          delay(5000);
       }
     } else {
       Serial.println("0");              //Prints 0 if GPS fix not received.
     }
}


The above code is self explanatory and highly commented, but still let me give a gist. The setup() and loop() are the 2 main functions of an Arduino sketch. The setup is used to initialise all the hardware and the loop is run repetitively. The only thing we do in the setup is initialize the GSM module so that it can be used for our communications.

The loop function gets the GPS data and updates it to a remote Server only if a GPS position has been detected. When you execute this, you will notice that the blue GPS Led will get lit once we get the position fix.

The requestHTTP() function starts the GPRS and sends a HTTP GET Request to a remote server. In this example I have replaced the IP Address of my target machine with IP.AD.RE.SS. You can replace it with either the IP Address of your target or the domain name. As I was using a machine setup in my home network, I had to use my home IP which I obtained from http://goo.gl/RYtrC

You will notice the GPS Class which is a heavily modified version of the original GPS class provided by Antrax designed to save some memory and bit of extra functionality. The GPS::getGPS() constantly reads the data from the GPS module via SPI interface and monitors it for a GPGGA Sentence which looks like the following:

$GPGGA,092750.000,5321.6802,N,00630.3372,W,1,8,1.03,61.7,M,55.2,M,,*76


This provides us with loads of information regarding the GPS data including the time, Position Altitude and  Number of satellites used, etc. What we care for in our function are just the time and the location data. Note that the above sentence is not for my location.

You will notice that I included the GSM862 library that provides the GSM functions and the SPI library that is a part of the Arduino IDE. The original GSM862 Library can be obtained from here. The code that I am providing is the stripped down version of the library that removes the Telit GPS functions as my module has no use of them and removes a lot of debug messages as they interfered with the GSM communication.

Code for GSM862.h:

/*
 * GM862.h
 * http://tinkerlog.com
 */

#ifndef GM862_h
#define GM862_h

#include "Arduino.h"

#define STATE_NONE 0
#define STATE_ON 1
#define STATE_INITIALIZED 2
#define STATE_REGISTERED 4

class GM862 {

 public:
  GM862(HardwareSerial *modemPort, byte onOffPin, char *pin);

  void init();
  void checkNetwork();

  boolean isOn();
  boolean isInitialized();
  boolean isRegistered();

  void switchOn();
  void switchOff();

  void sendSMS(char *number, char *message);
  void version();

  void initGPRS();
  void enableGPRS();
  void disableGPRS();
  boolean openHTTP(char *domain);
  void send(char *buf);
  char *receive(char *buf);

 private:
  void switchModem();
  byte requestModem(const char *command, uint16_t timeout, boolean check, char *buf);
  byte getsTimeout(char *buf, uint16_t timeout);

  char *readToken(char *str, char *buf, char delimiter);
  char *skip(char *str, char match);

  char pin[5];
  byte state;
  byte onOffPin;
  HardwareSerial *modem;

};

#endif


Code for GSM862.cpp:

/*
 * GM862.cpp
 * http://tinkerlog.com
 */

#include "GM862.h"
#include <string.h>
#define BUF_LENGTH 100

GM862::GM862(HardwareSerial *modemPort, byte onOffPin, char *pin) {
  state = STATE_NONE;
  modem = modemPort;
  modem->begin(9600);  //baud rate for our shield is 9600
  this->onOffPin = onOffPin;
  pinMode(onOffPin, OUTPUT);
  strcpy(this->pin, pin);
}


boolean GM862::isOn() {
  return (state & STATE_ON);
}


boolean GM862::isInitialized() {
  return (state & STATE_INITIALIZED);
}


boolean GM862::isRegistered() {
  return (state & STATE_REGISTERED);
}


void GM862::switchOn() {
  Serial.println("switching on");
  if (!isOn()) {
    switchModem();
    state |= STATE_ON;
  }
  Serial.println("done");
}


void GM862::switchOff() {
  Serial.println("switching off");
  if (isOn()) {
    switchModem();
    state &= ~STATE_ON;
  }
  Serial.println("done");
}


void GM862::switchModem() {
  digitalWrite(onOffPin, HIGH);
  delay(2000);
}


byte GM862::requestModem(const char *command, uint16_t timeout, boolean check, char *buf) {
           
  byte count = 0;

  *buf = 0;

  modem->flush();
  modem->println(command);
  count = getsTimeout(buf, timeout);
  return count;
}


byte GM862::getsTimeout(char *buf, uint16_t timeout) {
  byte count = 0;
  long timeIsOut = 0;
  char c;
  *buf = 0;
  timeIsOut = millis() + timeout;
  while (timeIsOut > millis() && count < (BUF_LENGTH - 1)) { 
    if (modem->available()) {
      count++;
      c = modem->read();
      *buf++ = c;
      timeIsOut = millis() + timeout;
    }
  }
  if (count != 0) {
    *buf = 0;
    count++;
  }
  return count;
}


void GM862::init() {
  char buf[BUF_LENGTH];
  char cmdbuf[30] = "AT+CPIN=";
  strcat(cmdbuf, pin);
  requestModem("AT", 1000, true, buf);
  requestModem("AT+IPR=9600", 1000, true, buf);
  requestModem("AT#SIMDET=1", 1000, true, buf);
  requestModem("AT+CMEE=2", 1000, true, buf);
  requestModem(cmdbuf, 1000, true, buf);
  state |= STATE_INITIALIZED;
}


void GM862::version() {
  char buf[BUF_LENGTH];
  requestModem("AT+GMI", 1000, false, buf);
  requestModem("AT+GMM", 1000, false, buf);
  requestModem("AT+GMR", 1000, false, buf);
  requestModem("AT+CSQ", 1000, false, buf);
}


void GM862::sendSMS(char *number, char *message) {
  char buf[BUF_LENGTH];
  char cmdbuf[30] = "AT+CMGS=\"";
  Serial.println("sending SMS ...");
  requestModem("AT+CMGF=1", 1000, true, buf);             // send text sms
  strcat(cmdbuf, number);
  strcat(cmdbuf, "\"");
  requestModem(cmdbuf, 1000, true, buf);
  modem->print(message);
  modem->write(0x1a);
  getsTimeout(buf, 2000);
  Serial.println(buf);
  Serial.println("done");
}


void GM862::checkNetwork() {
  char buf[BUF_LENGTH];
  char result;
  requestModem("AT+CREG?", 1000, true, buf);
  result = buf[21];

  if (result == '1') {
    state |= STATE_REGISTERED;
  }
  else {
    state &= ~STATE_REGISTERED;
  }
}


/*
 * Vodafone UK Contract
 *   internet, <>, <>
 * TATA DOCOMO India
 *   TATA.DOCOMO.INTERNET, <>, <>
 */
void GM862::initGPRS() {
  char buf[BUF_LENGTH];
  requestModem("AT+CGDCONT=1,\"IP\",\"internet\",\"0.0.0.0\",0,0", 1000, false, buf);
  requestModem("AT#USERID=\"\"", 1000, false, buf);
  requestModem("AT#PASSW=\"\"", 1000, false, buf);
}   


void GM862::enableGPRS() {
  char buf[BUF_LENGTH];
  requestModem("AT#GPRS=1", 1000, false, buf);
  Serial.println("done");
}


void GM862::disableGPRS() {
  char buf[BUF_LENGTH];
  requestModem("AT#GPRS=0", 1000, false, buf);
  Serial.println("done");
}


boolean GM862::openHTTP(char *domain) {
  char buf[BUF_LENGTH];
  char cmdbuf[50] = "AT#SKTD=0,80,\"";
  byte i = 0;
  boolean connect = false;
  strcat(cmdbuf, domain);
  strcat(cmdbuf, "\",0,0\r");
  requestModem(cmdbuf, 1000, false, buf);
  do {
    getsTimeout(buf, 1000);
    if (strstr(buf, "CONNECT")) {;
      connect = true;
      break;
    }
  } while (i++ < 10);
  if (!connect) {
  //  Serial.println("failed");
  }
  return (connect);
}


void GM862::send(char *buf) {
  modem->print(buf);
}


char *GM862::receive(char *buf) {
  getsTimeout(buf, 1000);
  return buf;
}



/*
 * Skips the string until the given char is found.
 */
char *GM862::skip(char *str, char match) {
  uint8_t c = 0;
  while (true) {
    c = *str++;
    if ((c == match) || (c == '\0')) {
      break;
    }
  }
  return str;
}


/*
 * Reads a token from the given string. Token is seperated by the
 * given delimiter.
 */
char *GM862::readToken(char *str, char *buf, char delimiter) {
  uint8_t c = 0;
  while (true) {
    c = *str++;
    if ((c == delimiter) || (c == '\0')) {
      break;
    }
    else if (c != ' ') {
      *buf++ = c;
    }
  }
  *buf = '\0';
  return str;
}


The only thing that you will need to change in the above code is the APN Settings in the GSM862::initGPRS() function. I am using a Vodafone UK Contract Sim and the function is using those settings. I have also provided the APN details for the TATA Docomo Network in India as that is what my final target is.

Now open the GSM_HTTP.ino file in Aduino and you will notice that it has opened all the three files in separate tabs. You can now select File>Upload to compile the Sketch and upload it to the Arduino. If you see an error, it will point out the lines where the errors are. If the upload went successfully, the Arduino execution starts immediately. You should be able to monitor the Serial Console via Tools>Serial Monitor. The GPS Led on the Sheild will glow blue when the GPS position has been determined.


Server Setup
For my Server side setup, I had my Linux machine configured with Apache and PHP. The machine was configured as a DMZ on the Router so that all requests from the outside gets redirected to the local machine. The instructions for this will depend on your Router. The instruction for setting up Apache and PHP can be found elsewhere on the net.

I created a Simple PHP script that would take the inputs and dump them in a file. It also converts the data into coordinates that you can use on Google Maps.

Following is the code for location.php stored in the webserver root.

<?php

function Latitude($lat){ //Converts data to Google Maps acceptable format
  $length=strlen($lat);
   $degree=(integer) substr($lat,0,2);         //Picks up the degrees
  $minutes=(float) substr($lat,2,($length-3)); //Picks up the minutes
  $hemisphere=substr($lat,-1);                 //Picks up the Hemisphere
  $return=$degree + ($minutes/60);             //Converts to Decimal Degree format
  if ( $hemisphere == "S" ) {            // South is negative North is Positive
        $return = $return * -1;
    }
  return $return;
}

function Longitude($long){
//Converts data to Google Maps acceptable format
  $length=strlen($long);
   $degree=(integer) substr($long,0,3);        
//Picks up the degrees
  $minutes=(float) substr($long,3,($length-4)); //Picks up the minutes
  $hemisphere=substr($long,-1);                 //Picks up the Hemisphere
  $return=$degree + ($minutes/60);              //Converts to Decimal Degree format
  if ( $hemisphere == "W" ) {            // West is negative East is Positive
        $return = $return * -1;
    }
  return $return;
}

$time=$_GET[time];
$loc=$_GET[loc];
list($gLatitude,$gLongitude)=explode(",", $location);

$latitude=Latitude($gLatitude);
$longitude=Longitude($gLongitude);

$fh=fopen("filename.txt",'a');
fwrite($fh,"$time : $loc : $latitude,$longitude \n");
echo "done";  //Sends a reply so that the Arduino knows it was a success
fclose($fh);
?>


Now if you tail filename.txt you will notice that a new line is entered every minute with the Time, Coordinates as Obtained from the GPGGA Sentence and Coordinates converted to Google Maps useable format.

You can modify this further to enter the data to a mysql data that can be then used by a HTML file using Google Map API to plot the last known location or location at a given time.

The above example GPGGA sentence would be entered in the file as:
92750 : 5321.6802N,00630.3372W : 53.3613366667,-6.50562

Following link will show this plotted on Google Maps. http://g.co/maps/gcjfc

The GPGGA Sentence was taken from http://en.wikipedia.org/wiki/NMEA_0183 for some location in Ireland.