Transmitting GPS coordinates with LoRa

Hello everyone,

I am working on a project at the moment that requires GPS coordinates to be sent to a base station. I am not worried about sending correction data at the moment, I just want to get the first step up and running first.

The problem I have is that I am new to LoRa and cannot figure out what I need to add to get the signal sent and received correctly. The two boards I am trying to use are the SAMD21 Pro RF and the Ublox ZED-F9P.

I can get the LoRa's to communicate until i try adding in the sensor data. Any help is highly appreciated!

Here is the code I have tried for the GPS and the transmitter

#include <RH_RF95.h>
#include <SPI.h> //RadioHead Library
#include <Wire.h> //Needed for I2C to GPS
#include "SparkFun_Ublox_Arduino_Library.h" //http://librarymanager/All#SparkFun_Ublox_GPS
SFE_UBLOX_GPS myGPS;

RH_RF95 rf95(12, 6);

//int LED = 13; //Lora Status LED
long lastTime = 0; //Simple local timer. Limits amount if I2C traffic to Ublox module.
float frequency = 921.2;

void setup()
{
  SerialUSB.begin(115200);
  //while (!Serial); //Wait for user to open terminal
  Wire.begin();
  SerialUSB.println("RFM Server");

  if (rf95.init() == false){ //Initialize the radio
    SerialUSB.println("Radio Init Failed - Freezing");
    while (1);
  }
  else{
  // An LED indicator to let us know radio initialization has completed.
    SerialUSB.println("Receiver up!");
    //digitalWrite(LED, HIGH);
    delay(500);
    //digitalWrite(LED, LOW);
    delay(500);
  }
  
  if (myGPS.begin() == false) //Connect to the Ublox module using Wire port
  {
    SerialUSB.println(F("Ublox GPS not detected at default I2C address. Please check wiring. Freezing."));
    while (1);
  }

  rf95.setFrequency(frequency);
  //rf95.setTxPower(14, false); //This will be used to set the power of the radio, set it between 5 to 23 dBm
  //myGPS.enableDebugging(Serial);

  myGPS.setI2COutput(COM_TYPE_UBX); //Set the I2C port to output UBX only (turn off NMEA noise)
  myGPS.setNavigationFrequency(20); //Set output to 20 times a second

  byte rate = myGPS.getNavigationFrequency(); //Get the update rate of this module
  SerialUSB.print("Current update rate:");
  SerialUSB.println(rate);
  
  myGPS.saveConfiguration(); //Save the current settings to flash and BBR

}

void loop()
{
  //Query module only every second. Doing it more often will just cause I2C traffic.
  //The module only responds when a new position is available
  if (millis() - lastTime > 0)
  {
    lastTime = millis(); //Update the timer
    SerialUSB.print("HP Lat: ");
    int32_t latitude = myGPS.getHighResLatitude();
    SerialUSB.print(latitude);
    delay(250);
    
    SerialUSB.print(", HP Lon: ");
    int32_t longitude = myGPS.getHighResLongitude();
    SerialUSB.print(longitude);
    delay(250);
    
    SerialUSB.print(", HP Alt: ");
    int32_t altitude = myGPS.getAltitude();
    SerialUSB.print(altitude/1000);
    delay(250);
    
    //SerialUSB.print(", Accuracy: ");
    //uint32_t accuracy = myGPS.getHorizontalAccuracy();
    //SerialUSB.println(accuracy);

    //SerialUSB.print(", 3D Accuracy: ");
    //uint32_t DAccuracy = myGPS.getPositionAccuracy();
    //SerialUSB.print(DAccuracy);
    //SerialUSB.print(" mm");
    //delay(500);

    
    SerialUSB.println("Sending message");
    int32_t toSend = latitude;
    int32_t lat[2] = {toSend, (toSend >> 32)};
    rf95.waitPacketSent();
    SerialUSB.println("Sent message");


    delay(250);
  
  }

}

And here is the code for the receiver.

#include <SPI.h> //Radio Head Library:
#include <RH_RF95.h> 
RH_RF95 rf95(12, 6);
int LED = 13; //Status LED is on pin 13

int packetCounter = 0; //Counts the number of packets sent
long timeSinceLastPacket = 0; //Tracks the time stamp of last packet received
float frequency = 921.2; //Broadcast frequency

void setup()
{
  pinMode(LED, OUTPUT);

  SerialUSB.begin(9600);
//  while(!SerialUSB); 
  SerialUSB.println("RFM Client!"); 

  //Initialize the Radio.
  if (rf95.init() == false){
    SerialUSB.println("Radio Init Failed - Freezing");
    while (1);
  }
  else{
    //An LED inidicator to let us know radio initialization has completed. 
    SerialUSB.println("Transmitter up!"); 
    digitalWrite(LED, HIGH);
    delay(500);
    digitalWrite(LED, LOW);
    delay(500);
  }

  // Set frequency
  rf95.setFrequency(frequency);
  rf95.setTxPower(14, false);
}


void loop()
{
  //SerialUSB.println("Sending message");

  //Send a message to the other radio
  //uint8_t toSend[] = "Hello there!";
  //sprintf(toSend, "Hi, my counter is: %d", packetCounter++);
  //rf95.send(toSend, sizeof(toSend));
  //rf95.waitPacketSent();

  // Now wait for a reply
  byte buf[RH_RF95_MAX_MESSAGE_LEN];
  byte len = sizeof(buf);

  if (rf95.waitAvailableTimeout(2000)) {
    // Should be a reply message for us now
    if (rf95.recv(buf, &len)) {
      SerialUSB.print("Got reply: ");
      SerialUSB.println((char*)buf);
      //SerialUSB.print(" RSSI: ");
      //SerialUSB.print(rf95.lastRssi(), DEC);
    }
    else {
      SerialUSB.println("Receive failed");
    }
  }
  else {
    SerialUSB.println("No reply, is the receiver running?");
  }
  delay(500);
  
}

have you checked examples for using RH_RF95 ?

In the transmitter you do

    int32_t lat[2] = {toSend, (toSend >> 32)};
    rf95.waitPacketSent();
    SerialUSB.println("Sent message");

but where is the

  rf95.send(toSend, sizeof(toSend));

that would actually send the data (before the waitPacketSent() call)

Also, if toSend is stored on 32 bytes, shifting it right by 32 position will give you 0

    int32_t toSend = latitude;
    int32_t lat[2] = {toSend, (toSend >> 32)};

your test on millis() is totally useless, esp. given all the delay() in the loop.

Thank you so much for your response. I have spent the past week giving myself a crash course in C++, as I am not super familiar with the language. Thank you for informing me about the rf95.send line, I did not see it in the example I was using. I have switched up my transmitter to send this instead,

SerialUSB.println("Sending message");
    //add 200 
    const int8_t toSendlat = latitude;
    const int8_t toSendlon = longitude;
    const int8_t toSendalt = altitude;
    uint8_t sendarr [3] = {0};
    sendarr[0] = (uint8_t) toSendlat;
    sendarr[1] = (uint8_t) toSendlon;
    sendarr[2] = (uint8_t) toSendalt;
    rf95.send(sendarr, sizeof(sendarr)); 
    rf95.waitPacketSent();
    SerialUSB.println("Sent message");

I know it is not the prettiest thing, but it works. However, the receiver does not process this and prints out lines of garbage.

What I receive

Got message: fDc 
Got message: lG⸮ 
Got message: ⸮C⸮ 
Got message: ⸮A⸮ 

I am not sure how to get the data processed correctly through the receiver, and have been unable to find any examples.

This is how the receiver is currently set up.

void loop()
{
  if (rf95.available()){
    // Should be a message for us now
    uint8_t buf[RH_RF95_MAX_MESSAGE_LEN];
    uint8_t len = sizeof(buf);

    if (rf95.recv(buf, &len)){
      digitalWrite(LED, HIGH); //Turn on status LED
      timeSinceLastPacket = millis(); //Timestamp this packet
      SerialUSB.print("Got message: ");
      SerialUSB.print((char*)buf);
      //SerialUSB.print(" RSSI: ");
      //SerialUSB.print(rf95.lastRssi(), DEC);
      SerialUSB.println();

    }
    else
      SerialUSB.println("Recieve failed");
  }
  //Turn off status LED if we haven't received a packet after 1s
  if(millis() - timeSinceLastPacket > 1000){
    digitalWrite(LED, LOW); //Turn off status LED
    timeSinceLastPacket = millis(); //Don't write LED but every 1s
  }
}

Thank you again for your response, I really appreciate it.

I can point you to working GPS tracker and receiver code that uses a different LoRa library if your interested.

The receiver has the option of fitting a display and its own GPS so it can calculate distance and direction to the remote transmitter.

On the sender side, no need for intermediary constants, theoretically you could just do

    uint8_t sendarr [3];
    sendarr[0] = latitude;
    sendarr[1] = longitude;
    sendarr[2] = altitude;

but there is a problem with this approach: a uint8_t is only one byte (0 to 255) and if you look at the type of data you have for the latitude, longitude, altitude --> they come from this:

int32_t latitude = myGPS.getHighResLatitude();
int32_t longitude = myGPS.getHighResLongitude();
int32_t altitude = myGPS.getAltitude();

so they are stored each in 4 bytes (a signed integer). This means you need 12 bytes to send the 3 of them.

In order to be efficient, ideally instead of storing the lat,long,alt in separate variables and then have to build an array to sending them out, best is to group them from the start into a structure. Then you will be able to directly send the structure. That could look like this:

struct dataInTransit_t {
  int32_t latitude;   // in ms
  int32_t longitude;  // in ms
  int32_t altitude;   // in m
} ;

this code declares a new type called dataInTransit_t in which we group 3 int32_t together and give them convenient names. As you want to send that over you also need to ensure the compiler is not doing funny optimization (should not with 4 bytes elements but to be safe or if you decide to transmit other stuff) and so we can give the structure some optional compilation attributes that we want all theses bytes to be as packed as possible together in memory. This is done with __attribute__ ((packed)) in the structure declaration

So that would look like this, and here is a quick example how to use it:

// declaring a new type "dataInTransit_t" as a structure with the info we want to send
struct __attribute__ ((packed)) dataInTransit_t {
  int32_t latitude;   // in ms
  int32_t longitude;  // in ms
  int32_t altitude;   // in m
} ;

// declaring a variable of this type
dataInTransit_t myGPSdata;

void setup() {
  Serial.begin(115200); // open serial monitor at 115200 bauds

  // initializing the structure wilt our data
  myGPSdata.latitude = 129168000;      // of course could be a call to myGPS.getHighResLatitude(); 
  myGPSdata.longitude = 275454360;   // myGPS.getHighResLatitude()
  myGPSdata.altitude = 8611;              // myGPS.getAltitude()

  // using the structure
  Serial.println(F("K2 coordinates:"));
  Serial.print(F("- Latitude  = ")); Serial.println(myGPSdata.latitude);
  Serial.print(F("- Longitude = ")); Serial.println(myGPSdata.longitude);
  Serial.print(F("- Altitude  = ")); Serial.println(myGPSdata.altitude);
}

void loop() {}

Once you have this structure, then that's what you can send directly:

rf95.send(myGPSdata, sizeof(myGPSdata)); 
rf95.waitPacketSent();

On the receiving side, today you are getting 3 bytes with

rf95.recv(buf, &len)

Those are the 3 bytes you had put in the buffer on the emitter side and you can't print an array withSerialUSB.print((char*)buf);That only works if buffer was really a null terminated char array (a cString) - which is not what you have there.

What you want to do is receive the data into the same dataInTransit_t structure which will then give you convenient access to the right attributes through their names. So you would do

 if (rf95.available()) {     // is there a message for us ?
    dataInTransit_t myGPSdata;
    uint8_t len = sizeof(myGPSdata);
    // get the message
    if (rf95.recv((uint8_t*) myGPSdata, &len)) {
      SerialUSB.print("Got message: ");
      SerialUSB.println(F("K2 coordinates:"));
      SerialUSB.print(F("- Latitude  = ")); SerialUSB.println(myGPSdata.latitude);
      SerialUSB.print(F("- Longitude = ")); SerialUSB.println(myGPSdata.longitude);
      SerialUSB.print(F("- Altitude  = ")); SerialUSB.println(myGPSdata.altitude);
    } else  SerialUSB.println("Recieve failed");
  }

that will only work if the emitter and receiver are both using the same endianness. Your arduino is little-endian (least significant byte comes first in memory). If you have an arduino on the other side you are fine, if not you need to check the endianness of the receiver as you might have to reverse the bytes in memory before being able to actually decode what you got.

for example if you try to send in hexadecimal: 0xABCDEF98, your arduino is organizing this in memory as 98, EF, CD, AB. so this is how the receiving buffer will see them. But if this is a big-endian architecture, it would expect the data to be represented as AB, CD, EF, 98

--> you'll need to reverse the bytes' order so that it sees that as the original int32

hope this helps