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.
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!
- ESP32 Development Board - https://s.click.aliexpress.com/e/_oFmrM7t
- NodeMCU Development Board - https://s.click.aliexpress.com/e/_oEKPd4X
- D1 Mini ESP8266 - https://s.click.aliexpress.com/e/_mtJeFHI
- Mini PC for Home Assistant - https://s.click.aliexpress.com/e/_msrHCog
- LILIGO LoRa Development board - https://s.click.aliexpress.com/e/_DeXZ7Zz
- ESP32 LoRaWAN Gateway - https://s.click.aliexpress.com/e/_DCP4OYB
- TTGO LoRa32 - https://s.click.aliexpress.com/e/_DlAGfrp
- Soldering Station - https://s.click.aliexpress.com/e/_EzQ4Aab
- Screwdriver Set - https://s.click.aliexpress.com/e/_DFvd9Jd
- Repair Toolkit - https://s.click.aliexpress.com/e/_DBnunr5
- Soldering Mat - https://s.click.aliexpress.com/e/_DchreR1
- Multimeter - https://s.click.aliexpress.com/e/_DkGtZpt
- Mini Breadboards - https://s.click.aliexpress.com/e/_DE16dRN
#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());
}