Skip to main content

ESP32 - GPS Tracker

ESP32 is a series of low-cost and low-power microcontrollers with integrated Wi-Fi and dual-mode Bluetooth. In this integration we create a GSP tracker with onboard data caching and synchronization via Wi-Fi. This provides a low cost alternative to using a 4G network for streaming real-time data. As soon as the ESP is powered, for example by starting a vehicle, the location, speed and heading data is cached on the SD card. When the vehicle is within reach of the configured Wi-Fi access point the data is synchronized with the server.

Hardware

The ESP32 series employs either a Tensilica Xtensa LX6 microprocessor in both dual-core and single-core variations. In this example we use a ESP32-WROOM-32 dual-core development board which is available for around 5 EUR. The pin layout is displayed below.

GPSMicroSD

The Grove GPS sends signals over NMEA. We use the D2 and D2 ports instead of the ESP RX and TX ports so that the standard RX and TX ports remain available for flashing the software.

ESP32-WROOM-32Grove GPS
D2TX
D4RX
VCCVCC
GNDGND

Make sure you connect the Micro SD VCC to the 5V because when connecting to 3.3V it is unstable.

ESP32-WROOM-32MicroSD
23MOSI
18SCK
19MISO
5CS
VCCVCC
GNDGND

For testing purposes you can use a breadbord with jumpers. An example is displayed below.

Software

The software is written in C++ using the platform.io extension in Visual Studio code. After installing the platform.io extension create a new project and add the ArduinoJson and TinyGPSPlus libraries. Make sure you set insert the following line in the platformio.ini file:

monitor_speed = 115200

Setup

Open the file src/main.cpp. Include the libraties and complete the setup function to intialize the devices:

#include <Arduino.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <HardwareSerial.h>
#include <Preferences.h>
#include <SD.h>
#include <TinyGPSPlus.h>
#include <WiFi.h>
#include <time.h>

const char* ACCESSPOINT_SSID = "3D-T Logger";
const char* ACCESSPOINT_PASSWORD = "23571113";
WiFiServer server(80);
String token = "";
TinyGPSPlus gps;
HardwareSerial SerialGPS(2);
Preferences preferences;
int counter = 0;

void setup() {
Serial.begin(115200); // Monitor
SerialGPS.begin(9600, SERIAL_8N1, 2, 4); //GPS
SD.begin();
delay(2000);
if (!SD.begin()) {
Serial.println("Initialization SD card failed!");
while (1)
;
}
Serial.println("SD card initialized.");
SD.remove("/cache.txt");
WiFi.mode(Wi-Fi_AP_STA);
delay(100);
WiFi.softAP(ACCESSPOINT_SSID, ACCESSPOINT_PASSWORD);
IPAddress IP = WiFi.softAPIP();
Serial.print("Wifi accesspoint ");
Serial.print(ACCESSPOINT_SSID);
Serial.print(" ");
Serial.println(IP);
server.begin(); // Start local wifi access point
preferences.begin("tracker");
}

Read GPS sensor

Now we can read the GSP information and log the time, position, altitude, speed and heading to the SD card in JSOn format:


void readGps() {
while (SerialGPS.available()) {
gps.encode(SerialGPS.read());
}
}

String read() {
String date = "";
date += gps.date.year();
date += "-";
date += gps.date.month() < 10 ? "0" : "";
date += gps.date.month();
date += "-";
date += gps.date.day() < 10 ? "0" : "";
date += gps.date.day();
date += gps.time.hour() < 10 ? " 0" : " ";
date += gps.time.hour();
date += ":";
date += gps.time.minute() < 10 ? "0" : "";
date += gps.time.minute();
date += ":";
date += gps.time.second() < 10 ? "0" : "";
date += gps.time.second();

String payload = "{";
payload += "\"topic\":\"";
payload += preferences.getString("topic");
payload += "\"";
payload += ", \"spacetime\": [";
payload += gps.location.lat();
payload += ", ";
payload += gps.location.lng();
payload += ", ";
payload += gps.altitude.meters();
payload += ", \"";
payload += date;
payload += "\"]";
payload += ", \"value\": {";
payload += "\"speed\": ";
payload += gps.speed.mps();
payload += ", \"course\": ";
payload += gps.course.deg();
payload += "}}";
return payload;
}

void log(String payload) {
File dataFile = SD.open("/cache.txt", FILE_APPEND);
if (dataFile) {
dataFile.println(payload);
} else {
Serial.println("error opening cache.txt");
}
dataFile.close();
File logFile = SD.open("/log.txt", FILE_APPEND);
if (logFile) {
logFile.println(payload);
} else {
Serial.println("error opening log.txt");
}
logFile.close();
Serial.print("Log: ");
Serial.println(payload);
}

HTTP server

Next we run a local HTTP server so that you can connect to the onboard ESP wifi network 3D-T Logger with password 23571113. Open a web browser and connect to the IP adress listed in the monitor (http://192.168.1.4). The ESP should display a web page to enter the credentials for the local Wi-Fi network and the server. The information entered in the form is stored in the ESP32 preferences. This is a persistent key value map that persists through restarts and flashing new code.


void process_parameters(String currentLine) {
Serial.print("Creditials:");
Serial.println(currentLine);
currentLine = urldecode(currentLine);
const int index = currentLine.indexOf("wifi_ssid");
if (index > 0) {
String query = currentLine.substring(index + 10);
String wifi_ssid = query.substring(0, query.indexOf("&"));
query = query.substring(query.indexOf("&") + 15);
String wifi_password = query.substring(0, query.indexOf("&"));
query = query.substring(query.indexOf("&") + 12);
String server_url = query.substring(0, query.indexOf("&"));
query = query.substring(query.indexOf("&") + 17);
String server_username = query.substring(0, query.indexOf("&"));
query = query.substring(query.indexOf("&") + 17);
String server_password = query.substring(0, query.indexOf("&"));
query = query.substring(query.indexOf("=") + 1);
String topic = query.substring(0, query.indexOf(" "));
Serial.println(wifi_ssid);
Serial.println(wifi_password);
Serial.println(server_url);
Serial.println(server_username);
Serial.println(server_password);
Serial.println(topic);
preferences.putString("wifi_ssid", wifi_ssid);
preferences.putString("wifi_password", wifi_password);
preferences.putString("server_url", server_url);
preferences.putString("server_username", server_username);
preferences.putString("server_password", server_password);
preferences.putString("topic", topic);
}
}
void serve_clients() {
WiFiClient client = server.available();
if (client) {
Serial.println("New Client.");
String currentLine = "";
while (client.connected()) {
if (client.available()) {
char c = client.read();
Serial.write(c);
header += c;
if (c == '\n') {
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if (currentLine.length() == 0) {
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();

client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
client.println("<body><div align=\"center\">");
client.println("<h1>3D-T Logger</h1>");
client.println("<p>Enter the network name (SSID) and password of the Wi-Fi router:</p>");
client.println("<form>");
client.println("<table border=0 >");
client.print("<tr><td><label>SSID</label></td><td><input type=\"text\" id=\"ssid\" name=\"wifi_ssid\" value=\"");
client.print(preferences.getString("wifi_ssid", ""));
client.println("\"></td></tr>");
client.println("<tr><td><label>Password</label></td><td><input type=\"password\" id=\"password\" name=\"wifi_password\" value=\"");
client.print(preferences.getString("wifi_password", ""));
client.println("\"></td></tr>");
client.println("</table><br><br>");
client.println("<p>Enter the information of the server for posting the data:</p>");
client.println("<table border=0 >");
client.println("<tr><td><label>Server</label></td><td><input type=\"text\" id=\"server\" name=\"server_url\" value=\"");
client.print(preferences.getString("server_url", ""));
client.println("\"></td></tr>");
client.println("<tr><td><label>Username</label></td><td><input type=\"username\" id=\"username\" name=\"server_username\" value=\"");
client.print(preferences.getString("server_username", ""));
client.println("\"></td></tr>");
client.println("<tr><td><label>Password</label></td><td><input type=\"password\" id=\"password\" name=\"server_password\" value=\"");
client.print(preferences.getString("server_password", ""));
client.println("\"></td></tr>");
client.println("<tr><td><label>Topic</label></td><td><input type=\"text\" id=\"topic\" name=\"topic\" value=\"");
client.print(preferences.getString("topic", ""));
client.println("\"></td></tr>");
client.println("<tr><td></td><td><input type=\"submit\" ></td></tr>");
client.println("</table>");
client.println("</form><br><br>");
if (WiFi.status() != WL_CONNECTED) {
client.println("Not connected");
} else {
client.print("Connected to ");
client.println(preferences.getString("wifi_ssid"));
}
client.println("</div></body></html>");
client.println();
break;
} else {
if (currentLine.startsWith("GET")) {
process_parameters(currentLine);
}
currentLine = "";
}
} else if (c != '\r') {
currentLine += c;
}
}
}
header = "";
client.stop();
Serial.println("Client disconnected.");
Serial.println("");
}
}

Wi-Fi client

When the local Wi-Fi ssid and password are entered the connection to the local Wi-Fi network is established. The IP adress of the ESP in the local network is printed on the monitor.

void wifi() {
String ssid = preferences.getString("wifi_ssid");
String password = preferences.getString("wifi_password");
Serial.print("Try wifi");
Serial.print(ssid);
Serial.println(password);
if (ssid == "" || password == "") {
return;
}
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.begin(ssid.c_str(), password.c_str());

for (int i = 0; i < 5; i++) {
if (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(1000);
}
}
if (WiFi.status() != WL_CONNECTED) {
Serial.println("Connection failed");
} else {
Serial.print("Connected with IP address ");
Serial.println(WiFi.localIP());
}
}

Post to server

Once the Wi-Fi connection is established the authentication token can be requested from the server using the username and password entered in the web page. The response is parsed and the token is stored.


void get_token() {
if (preferences.getString("server_url", "") == "") {
return;
}
HTTPClient http;
String url = "http://" + preferences.getString("server_url") + "/login";
String body = "{\"userName\":\"" + preferences.getString("server_username") + "\",\"password\":\"" +
preferences.getString("server_password") + "\"}";
Serial.print("Get token from " + url);
Serial.print("Body " + body);
http.begin(url);
http.addHeader("Content-Type", "application/json");
int httpResponseCode = http.POST(body);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println(httpResponseCode);
Serial.println(response);
DynamicJsonDocument doc(1024);
deserializeJson(doc, response);
const char* auth = doc["Authorization"];
token = auth;
Serial.print("Token ");
Serial.println(token);
} else {
Serial.print("Error on sending PUT Request: ");
Serial.println(httpResponseCode);
}
http.end();
}

If a Wi-Fi connection is available the information from the SD card is synchronized with the server via a HTTP post request. The token is stored in the header and the JSON payload in the body

int post(String body) {
int result = -1;
if (preferences.getString("server_url", "") == "") {
return result;
}
HTTPClient http;
String url = "http://" + preferences.getString("server_url") + "/states";
Serial.print("Post: " + url);
Serial.print("Body: " + body);
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.addHeader("Authorization", "Bearer " + token);
int httpResponseCode = http.POST(body);
if (httpResponseCode > 0) {
String response = http.getString();
Serial.println(response);
result = 1;
} else {
Serial.print("Error on sending POST Request: ");
Serial.println(httpResponseCode);
String response = http.getString();
Serial.println(response);
}
http.end();
return result;
}

void sync() {
File dataFile = SD.open("/cache.txt", FILE_READ);
String line = "";
int index = 0;
while (dataFile.available()) {
char c = dataFile.read();
if (c != '\n' && c != '\r') {
line += c;
index++;
} else {
if (index > 0) {
Serial.print("Publish ");
delay(200);
if (post(line) != -1) {
Serial.print("Posted ");
Serial.println(line);
} else {
return;
}
}
index = 0;
}
}
dataFile.close();
bool removed = SD.remove("/cache.txt");
Serial.println("Cleared cache");
}

Main loop

The event loop listens for incomming connections on the local web server and tries to setup a Wi-Fi connection for synchronizing the data. In this example the GPS is read every 100 milliseconds and the data is stored every 10 seconds.

void loop() {
if (WiFi.status() != WL_CONNECTED) {
wifi();
} else if (token.length() == 0) {
get_token();
}
serve_clients();
readGps();
if (counter > 100) {
String payload = read();
log(payload);
if (token != "" && WiFi) {
sync();
}
counter = 0;
}
delay(100);
++counter;
}

Flash

Connect the board with a USB cable and click on the PlatformIO: Upload icon in the bottom bar. This will compile and flash the code and starts a serial monitor. Connect your Wi-Fi to the Accesspoint 3D-T Logger and enter the password.

You can now configure the connection to your Wi-Fi router and enter the credentials of the server that the data should be posted to.