Helium-based DIY GPS vehicle tracker with RYS8839, RYLR993, and ESP32

I made a GPS tracker so I can test out the availability of the Helium network in my local area.
Nov 10, 2023 — 7 mins read — Projects

Helium-based DIY GPS vehicle tracker with RYS8839, RYLR993, and ESP32

I was recently introduced to the Helium network by a friend who discovered that it is locally available and people already installed antennas to provide coverage in my town.

This made me curious, so, I decided that I would make a project with one of the modules that Reyax has sent me for LoRa communication, that can also work with Helium, the RYLR993. This is the same module where I was able to make a communication at a 20km distance.

The device is controlled by an ESP32 development board that communicates with the RYLR993 LoRa Module and the RYS8839 GPS module via UART. The GPS module talks at a baud rate of 115200 so for it I used one of the 3 hardware serial ports available on the ESP32, while the LoRa module and its baud rate of 9600, is interacted with using software serial with the EspSoftwareSerial library.

In the video below, I have a full description of the device, the principle of operation, and a testing demo.


Using the regular serial communication on the ESP32, I made a relay where I could choose to send whatever command to any of the modules by first prefixing the data with the right string. This made it extremely easy for me during the testing phase because I was able to quickly re-configure both modules without connecting them with a separate USB to serial adapter.

Once the device powers up, a command is sent to the RYLR993 module to join the predefined Helium network. This join request, produces one of two events: "+EVT:JOINED" or "+EVT:JOIN FAILED". When the device fails to join, the join request is retried but once it succeeds, we then send the right sequence of commands to the GPS module so it starts with positioning.

The GPS module usually acquires the position fix in a few seconds and the position is sent to the ESP32 module every 3 seconds. The ESP32 module then checks if that position is more than 10 meters away from its previous position, and if it is, then the position is sent via the Helium network, to a Google Sheets file to be logged.

The Google Sheet file can be converted to a KML file so the movement path of the device can then be plotted on a map.

Example of my testing run with the GPS tracker

Example of my testing run with the GPS tracker

To reduce the amount of data transmitted through LoRa, I used the CayenneLPP protocol with its corresponding Arduino library to encode the GPS position and then, inside the Helium console, I made a function to decode that payload into the right values to be sent to the Google Sheet.

This function for decoding the CayenneLPP GPS bytes is available below, but you will need to replace the fields IDs with the ones from your Google Sheet integration.

function Decoder(bytes, port) { 
 latitude = (bytes[2]<<24>>8 | bytes[3]<<8 | bytes[4])/10000;
 longitude = (bytes[5]<<24>>8 | bytes[6]<<8 | bytes[7])/10000;
 altitude=((bytes[8]<<16 | bytes[9]<<8 | bytes[10])/100);
 var decodedPayload = {
  "latitude": latitude,
  "longitude": longitude,
  "altitude": altitude
 };

 return Serialize(decodedPayload)
}

// Generated: do not touch unless your Google Form fields have changed
var field_mapping = {
 "latitude": "entry.1150941718",
 "longitude": "entry.1756715096",
 "altitude": "entry.1315656095"
};
// End Generated

function Serialize(payload) {
 var str = [];
 for (var key in payload) {
  if (payload.hasOwnProperty(key)) {
   var name = encodeURIComponent(field_mapping[key]);
   var value = encodeURIComponent(payload[key]);
   str.push(name + "=" + value);
  }
 }
 return str.join("&");
}
// DO NOT REMOVE: Google Form Function

 The full code for the ESP32 is available below.

You can support me and the channel by buying from the links below at no additional cost to you!

#include <HardwareSerial.h>
#include <SoftwareSerial.h>
#include <TinyGPSPlus.h>
#include <CayenneLPP.h>

#define RX_lora 15
#define TX_lora 4
#define RX_gps 23
#define TX_gps 22

EspSoftwareSerial::UART lora_serial;
HardwareSerial gps_serial(2);

TinyGPSPlus gps;
CayenneLPP lpp(51);

double last_lat = 0;
double last_lng = 0;

bool helium_joined = false;

void setup()
{
  Serial.begin(115200);

  lora_serial.begin(9600, SWSERIAL_8N1, RX_lora, TX_lora, false);
  gps_serial.begin(115200, SERIAL_8N1, RX_gps, TX_gps);

  //wait a bit for serail to stabilize
  delay(5000);
  
  //try join Helium
  sendLoraCommand("AT+JOIN=1");
  delay(100);
  checkForLoRaData();
  
  //set GPS at idle
  sendGPSCommand("@GSTP"); //set idle
}

void loop() {
  if (Serial.available()){
    String content = Serial.readString();
    content.trim();
    if(content.startsWith("lora:")) {
      Serial.println("Writing to Lora Module");
      sendLoraCommand(content.substring(5));
    } else if(content.startsWith("gps:")) {
      Serial.println("Writing to GPS Module");
      sendGPSCommand(content.substring(4));
    } else if(content == "current_gps") {
      Serial.println("Current position: " + String(gps.location.lat(), 6) + ", " + String(gps.location.lng(), 6));
      Serial.println("Distance: " + String(gps.distanceBetween(gps.location.lat(), gps.location.lng(), last_lat, last_lng)));
      Serial.println("Altitude: " + String(gps.altitude.meters()));
      Serial.println("Failed Checksum: " + String(gps.failedChecksum()));
      //send out current location
      setCayenneData();
    }
  }

  checkForLoRaData();

  String gps_incomming = "";
  while (gps_serial.available() > 0) {
    char c = (char)gps_serial.read();
    gps_incomming += c;
    if (gps.encode(c)) {
      handleLocation();
    }
  }
  // if(gps_incomming != "") {
  //   Serial.println(gps_incomming);
  // }
}

void checkForLoRaData() {
  if (lora_serial.available()) {
    String incomming = lora_serial.readString();
    incomming.trim();
    Serial.println("Received from Lora Module:");
    Serial.println(incomming);
    if(incomming.indexOf("+EVT:JOINED") >= 0) {
      Serial.println("Joined Helium!");
      helium_joined = true;
      //only init GPS after Helium is joined
      initGPS();
    } else if(incomming.indexOf("+EVT:JOIN FAILED") >= 0) {
      Serial.println("Failed to join Helium :) Retry!");
      sendLoraCommand("AT+JOIN=1");
      helium_joined = false;
    }
  }
}

void sendLoraCommand(String command) {
  command = command + "\r\n";
  char* buf = (char*) malloc(sizeof(char) * command.length() + 1);
  Serial.println(command);
  command.toCharArray(buf, command.length() + 1);
  lora_serial.write(buf);
  free(buf);
}

void sendLoraCayenne(uint8_t *data, uint8_t size) {
  String command = "AT+SEND=1:0:";
  for (unsigned char i = 0; i < size; i++)
  {
    if(data[i] < 0x10) {
      command += '0';
    }
    command += String(data[i], HEX);
  }
  command += "\r\n";
  Serial.println(command);
  //only send data if joined to Helium network
  if(helium_joined) {
    char* buf = (char*) malloc(sizeof(char) * command.length() + 1);
    command.toCharArray(buf, command.length() + 1);
    lora_serial.write(buf);
    free(buf);
  } else {
    Serial.println("Not sent, not joined!");
  }
}

void sendGPSCommand(String command) {
  command = command + "\r\n";
  char* buf = (char*) malloc(sizeof(char) * command.length() + 1);
  Serial.println(command);
  command.toCharArray(buf, command.length() + 1);
  gps_serial.write(buf);
  free(buf);
}

void initGPS() {
  Serial.println("Initializing GPS");

  sendGPSCommand("@GSTP"); //set idle
  delay(100);
  sendGPSCommand("@GNS 815"); //use all satelites
  delay(100);
  sendGPSCommand("@GPPS 1"); //output as soon as clock data is received
  delay(100);
  sendGPSCommand("@GSOP 1 3000 0"); //output data once every 3 seconds
  delay(100);
  sendGPSCommand("@GSR"); //hot start
  delay(100);
}

void handleLocation()
{
  if (gps.location.isValid())
  {
    if(last_lat == 0 || last_lng == 0) {
      //first position, set initial values
      last_lat = gps.location.lat();
      last_lng = gps.location.lng();
      Serial.println("Initial position: " + String(last_lat, 6) + ", " + String(last_lng, 6));
      setCayenneData();
    } else {
      //update last position if distance is greater than 10m
      if(gps.distanceBetween(gps.location.lat(), gps.location.lng(), last_lat, last_lng) > 10) {
        last_lat = gps.location.lat();
        last_lng = gps.location.lng();
        Serial.println("Updated position: " + String(last_lat, 6) + ", " + String(last_lng, 6));
        setCayenneData();
      }
    }
  }
}

void setCayenneData() {
  lpp.reset();
  lpp.addGPS(1, gps.location.lat(), gps.location.lng(), gps.altitude.meters());
  sendLoraCayenne(lpp.getBuffer(), lpp.getSize());
}



esp32 LoRa LoRaWAN GPS
Read next

Repairing a broken laptop charger cable with epoxy

Cables are usually made to be durable but if you don't treat them well, they often break. These breaks happen in places where there is repea...

You might also enojy this

LoRa Distance Testing with RYLR998 in the open field - Amazing results!

I'm planning to build a device that will make use of the LoRa communication protocol to transmit data over long distances and for that, I fi...