Oriol Closa's profile

💤 Not at the office right now

Oriol Closa

oriolclosa

Katalan i Stockholm och programmerare på 5M. Com que la independència va per llarg, he acabat fotent el camp.

Stockholm, Sverige

5 Monkeys

oriol_closa

oriolclosa

Oriol Closa

oriolclosa

💤 Not at the office right now

Katalan i Stockholm och programmerare på 5M. Com que la independència va per llarg, he acabat fotent el camp.

💡 Crafts

GPS bike tracker

This project uses an Arduino UNO R3 to send the current location to a server in order to track a bike. The idea is to allocate this below the seat which will send data as long as it has power. A 10.000mAh battery will keep the Arduino and its modules supplied for several days, with the LCD screen powered on though this will drop to about 2 days of non-stop operation.

Components
  • Arduino UNO R3.
  • SIM800L GSM module.
    • IPEX antenna.
    • LM2596 voltage regulator.
  • MakerHawk GPS module.
    • IPEX antenna.
  • Battery pack with a 2A output.
  • USB cable and jumpers.
  • SIM card with data subscription.

Software

Arduino

For this small project using Arduino code was enough along with the TinyGPS and LiquidCristal libraries. The data is sent to a personal server that accepts a JSON with the lat and lon attributes and whose time parameter is also optional in case we would like to send data taken from another time.

We start by importing the libraries and defining the serial ports as long as the APN of our SIM provider and our server endpoint where we will POST the data. Note the pins for the serial connections might differ in your case.

#include <SoftwareSerial.h>
#include <TinyGPS.h>
#include <LiquidCrystal.h>

SoftwareSerial ss_sim(3, 2);
SoftwareSerial ss_gps(6, 5);

const String APN = "[GSM_APN]";
const String URL = "[API_ENDPOINT]";

TinyGPS gps;

const int rs = 12, en = 11, d4 = 10, d5 = 9, d6 = 8, d7 = 7;
LiquidCrystal lcd(rs, en, d4, d5, d6, d7);

String LCD_LINE1 = "GPS BIKE TRACKER";
String LCD_LINE2 = "Oriol Closa 2020";

bool CONNECTED = false;
int TRIES = 0;

void(* reset) (void) = 0;

In our setup function we try to connect to the GPRS network by setting the SIM to GPRS mode along with the APN. We also deactivate and activate it as AT commands are sometimes unpredictable with cheap equipment.

void setup(){
    Serial.begin(9600);
    ss_sim.begin(9600);
    ss_gps.begin(9600);

    ss_sim.listen();

    Serial.println("Initializing...");

    lcd.begin(16, 2);
    update_lcd();
    delay(2000);

    LCD_LINE1 = "GPRS:  Waiting";
    LCD_LINE2 = "GPS:  Waiting";

    connect_gprs();
    delay(1000);
}

void connect_gprs(){
    LCD_LINE1 = "GPRS: Connecting";
    update_lcd();

    ss_sim.println("AT+SAPBR=3,1,\"Contype\",\"GPRS\"");
    delay(1000);
    update_serial();

    ss_sim.println("AT+SAPBR=3,1,\"APN\",\"" + APN  +"\"");
    delay(1000);
    update_serial();

    ss_sim.println("AT+SAPBR=1,1");
    delay(1000);
    update_serial();

    ss_sim.println("AT+SAPBR=2,1");
    delay(1000);
    update_serial();
}

Our loop function will check if we have a GPRS connection as it wouldn't make sense to keep getting our location if we can't send it. In case we are not connected we try to put the module in flight mode and try to connect again. Sometimes this procedure doesn't work with the SIM800L and we need to reset the module. As we want to ensure we keep sending data we better just reset the system to its original setup function.

void loop(){
    check_gprs();
    while(!CONNECTED){
        LCD_LINE1 = "GPRS:   Retrying";
        update_lcd();
        ss_sim.println("AT+CFUN = 0");
        update_serial();
        delay(5000);
        ss_sim.println("AT+CFUN = 1");
        update_serial();
        delay(1000);
        connect_gprs();
        if(!CONNECTED){
            reset();
        }
    }
    if(TRIES > 9){
        LCD_LINE2 = "GPS: Locat. (" + String(TRIES) + ")";
    }
    else if(TRIES > 0){
        LCD_LINE2 = "GPS:  Locat. (" + String(TRIES) + ")";
    }
    else{
        LCD_LINE2 = "GPS:   Locating";
    }
    update_lcd();
    String data = get_location();
    if(data != ""){
        Serial.println(data);
        delay(1000);
        LCD_LINE2 = "GPS:    Waiting";
        update_lcd();
        delay(1000);
        send_location(data);
        delay(2000);
        LCD_LINE1 = "GPRS:  Connected";
        update_lcd();
        delay(8000);
    }
    else{
        delay(1000);
    }
}

Checking for the GPRS connection is easy using the AT+CREG? command which returns the status of our connection. We then just need to check if the number this command returned is a expected value.

void check_gprs(){
    ss_sim.println("AT+CREG?");
    delay(2000);
    String registration = update_serial_with_return();
    registration = registration.substring(registration.indexOf(": ") + 2).charAt(2);
    CONNECTED = false;
    if(registration == "0" || registration == "2"){
        registration = "        Failed";
    }
    else if(registration == "3"){
        registration = "        Denied";
    }
    else if(registration == "1" || registration == "6" || registration == "9" || registration == "5" || registration == "7" || registration == "10"){
        registration = " Connected";
        CONNECTED = true;
    }
    else if(registration == "8"){
        registration = " Emergency";
    }
    else{
        registration = "     Unknown";
    }

    LCD_LINE1 = "GPRS: " + registration;
    update_lcd();
}

To get our current location we need to make use of the GPS module and because this is defined with another serializer we need to switch and listen to that one. We then check for valid values coming from the module as it's always sending raw data. In case the data the module sends is valid (that is, can be encoded) we just get our current position and switch over to listening to the GMS module.

String get_location(){
    ss_gps.listen();

    bool new_data = false;
    unsigned long start = millis();
    while (millis() - start < 5000){
        if(ss_gps.available()) {
            char c = ss_gps.read();
            Serial.print(c);
            if (gps.encode(c)) {
                new_data = true;
            }
        }
    }

    if(new_data){
        float flat, flon;
        unsigned long age;
        gps.f_get_position(&flat, &flon, &age);
        ss_sim.listen();
        TRIES = 0;
        Serial.println(String(flat, 6) + " " + String(flon, 6));
        LCD_LINE2 = "GPS:    Located";
        update_lcd();
        return "{\"lat\": " + String(flat, 6) + ", \"lon\": " + String(flon, 6) + "}";
    }
    else{
        TRIES += 1;
        if(TRIES > 99){
            TRIES = 0;
        }
    }

    ss_sim.listen();
    return "";
}

Finally we send the data to our server using some AT commands. Take into account if your sever uses the HTTPS protocol you will need to run AT+HTTPSSL=1 as shown below. We then need to let the module know the endpoint we will send the data as well as the longitude of the JSON string we are sending. Finally we just POST the data.

void send_location(String data){
    LCD_LINE1 = "GPRS:      Sending";
    update_lcd();

    ss_sim.println("AT+HTTPINIT");
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPSSL=1");
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPPARA=\"CID\",1");
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPPARA=\"URL\",\""+ URL + "\"");
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPPARA=\"CONTENT\",\"application/json\"");
    delay(1000);
    update_serial();


    ss_sim.println("AT+HTTPDATA=" + String(data.length()) + ",100000");
    delay(1000);
    update_serial();

    ss_sim.println(data);
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPACTION=1");
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPREAD");
    delay(1000);
    update_serial();

    ss_sim.println("AT+HTTPTERM");
    delay(1000);
    update_serial();
}

Here are also some util functions we use on the code above.

void update_lcd(){
    lcd.clear();
    lcd.print(LCD_LINE1);
    lcd.setCursor(0, 1);
    lcd.print(LCD_LINE2);
    lcd.display();
}

String update_serial_with_return(){
    delay(500);
    while (Serial.available()){
        ss_sim.write(Serial.read());
    }
    String ss_read = "";
    while(ss_sim.available()){
        ss_read += (char) ss_sim.read();
    }
    return ss_read;
}

void update_serial(){
    delay(500);
    while (Serial.available()){
        ss_sim.write(Serial.read());
    }
    String ss_read = "";
    while(ss_sim.available()){
        ss_read += (char) ss_sim.read();
    }
    if(ss_read.indexOf("HTTPACTION:") > 0){
        if(ss_read.indexOf("200") > 0){
            LCD_LINE1 = "GPRS:  Data sent";
        }
        else{
            LCD_LINE1 = "GPRS:  Send fail";
        }
        update_lcd();
    }
    Serial.print(ss_read);
}
Server

In order to receive the data I use a Django project with a very basic view that takes care of the data in the JSON the GSM module sends.

import datetime
import json

from django.db import IntegrityError
from django.http import HttpResponseNotFound, HttpResponse, HttpResponseServerError
from django.views.decorators.csrf import csrf_exempt

from travel.api.gps import add_gps_location


@csrf_exempt
def location(request, code):
    if request.method == "POST":
        data = json.loads(request.body)
        time = data.get("time")
        lat = data.get("lat")
        lon = data.get("lon")
        if lat and lon:
            if time:
                time = datetime.datetime.strptime(time, "%Y-%m-%dT%H:%M:%S")
            else:
                time = datetime.datetime.now()
            try:
                add_gps_location(code=code, time=time, lat=lat, lon=lon)
                return HttpResponse()
            except IntegrityError:
                return HttpResponseServerError()
    return HttpResponseNotFound()

Extensions

Possible extensions for this basic project would be to store the data in a SD card in case there's no connectivity. Data could also be sent in bursts instead of just one at a time, which would ease the previous suggestion. The LCD is also not used in daily operation as it consumes a lot of power.

Some more data could also be sent using a speedometer to also power down the modules in case the bike is not moving for example.