Arduino Long Range Communication Tutorial – LoRaLib Library

Click here to read LoRenz Shield >

loralib

To control the LoRenz shield we built in Arduino Long Range Communication Tutorial LoRenz shield, I developed LoRaLib – an open source Arduino library for SX1278 chip. This library was designed from scratch with a single thing in mind: easy to use API, so even beginners can start experimenting with LoRa communication. The goal of this library was to make long range communication just as easy as communicating over serial port.

Software

  • Arduino IDE
  • LoRaLib Arduino library (available on GitHub)

LoRaLib library

The SX1278 has many different settings to allow the user to fully customize the range, data rate, and power consumption, but the most important are the following three:

  • Bandwidth. The SX1278 allows to set bandwidth from 7.8 kHz to 500 kHz. The higher the bandwidth value, the faster the data transfer. However, this comes at a cost of lower total sensitivity, and therefore lower maximum range.
  • Spreading Factor. In LoRa modulation, each bit of information is represented by multiple chirps. Spreading factor is a measure of how many chirps are there per bit of data. The SX1278 supports 7 different settings, the higher the spreading factor, the slower the data transmission and higher the range.
  • Coding Rate. To improve the stability of transmission, SX1278 can perform additional error checking. The measure of this error checking is called coding rate and there are four possible values. At the lowest 4/5 coding rate, the transmission is a bit less stable, and a bit faster. At the highest coding rate 4/8, the link is much more reliable, but at the expense of slower data transmission rate.

In the default setting, the library uses 500 kHz bandwidth, coding rate 4/5 and spreading factor 12. These settings offer a reasonable compromise between range, stability, and data rate. Of course, the settings can be changed at any time by one simple function.

The library has a built-in packet class and an addressing system. The address is 8 bytes long, giving us a maximum of 18 quintillion (1.8 × 10^19) possible addresses. This amount is truly absurd. In comparison, NASA estimates the number of stars in our galaxy to be a “mere” 400 billion (4 × 10^11). Each packet consists of a source address, a destination address, and a payload of up to 240 bytes. Of course, the library provides several methods to read and write the packet data.

Let’s take a look at how easy it is to use this library. Suppose we have 2 LoRenz shields with SX1278 modules. They are a few hundred meters apart, so we can use the default settings. First, we have to include the library header file. Then, we create one instance of the LoRa class with default settings, and one instance of the packet class with the destination address and the message. The source address is generated automatically by the library and written into Arduino EEPROM. To check everything was saved correctly, we read the packet information and print it to the serial port. Next, we just have to call the tx() function and after a while … done! Our packet was successfully transmitted in a single command!

// include the library
#include <LoRaLib.h>

// create instance of LoRa class with default settings
LoRa lora;

// create instance of packet class
// destination: "20:05:55:FE:E1:92:8B:95"
// data:        "Hello World !"
packet pack("20:05:55:FE:E1:92:8B:95", "Hello World!");

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

  // initialize the LoRa module with default settings
  lora.init();

  // create a string to store the packet information
  char str[24];

  // print the source of the packet
  pack.getSourceStr(str);
  Serial.println(str);

  // print the destination of the packet
  pack.getDestinationStr(str);
  Serial.println(str);

  // print the length of the packet
  Serial.println(pack.length);
  
  // print the data of the packet
  Serial.println(pack.data);
}

void loop() {
  Serial.print("Sending packet ");

  // start transmitting the packet
  uint8_t state = lora.tx(pack);
  
  if(state == 0) {
    // if the function returned 0, a packet was successfully transmitted
    Serial.println(" success!");
    
  } else if(state == 1) {
    // if the function returned 1, the packet was longer than 256 bytes
    Serial.println(" too long!");
    
  }

  // wait a second before transmitting again
  delay(1000);
}

Of course, now we need a second Arduino with LoRenz shield to receive that packet. Everything is set up just as before, only this time we call the rx() function and then print the received packet. This function will wait for a packet and if none arrives in a certain timespan, the function will timeout so that you code doesn’t hang entirely. The library even performs check to see if the packet was damaged during the transfer and if so, discards it.

// include the library
#include <LoRaLib.h>

// create instances of LoRa and packet classes with default settings
LoRa lora;
packet pack;

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

  // initialize the LoRa module with default settings
  lora.init();
}

void loop() {
  Serial.print("Waiting for incoming transmission ... ");
  
  // start receiving single packet
  uint8_t state = lora.rx(pack);

  if(state == 0) {
    // if the function returned 0, a packet was successfully received
    Serial.println("success!");

    // create a string to store the packet information
    char str[24];

    // print the source of the packet
    pack.getSourceStr(str);
    Serial.println(str);

    // print the destination of the packet
    pack.getDestinationStr(str);
    Serial.println(str);

    // print the length of the packet
    Serial.println(pack.length);

    // print the data of the packet
    Serial.println(pack.data);
    
  } else if(state == 1) {
    // if the function returned 1, no packet was received before timeout
    Serial.println("timeout!");
    
  } else if(state == 2) {
    // if the function returned 2, a packet was received, but is malformed
    Serial.println("CRC error!");
    
  }
  
}

Of course, this is just the most basic example. The library itself can do a lot more and is still under development. For more in-depth information about the library and all the different functions, please refer to my GitHub and the documentation hosted there.

 

Encryption on Arduino

Before we finish this short overview, I just wanted to add a little section concerning encryption on Arduino. I briefly mentioned this problem in my last article. Right now, all the data we are sending are unencrypted. This means that anyone with the same setup we have, with the same module and the same settings, will be able to intercept and read our messages. The attacker could even send his own and we won’t be able to tell the difference. That’s not exactly something that could be considered secure.

The simplest solution to this is to use some form of encryption. Specifically, I decided to use the Rijndael cipher. Never heard of it? That’s because the name is Dutch, which makes it impossible to both remember and pronounce. The cipher itself is actually pretty widespread, but under a much catchier name: AES. It’s a symmetrical cipher that offers excellent compromise between encryption speed and security. Plus, there are several AES libraries for Arduino readily available! The one we’ll be using in this project is AESLib by Davy Landman (available on GitHub).

As I mentioned above, AES is a symmetrical cipher – this means that it uses the same key for encrypting and decrypting messages. Right now, we only have two devices, so it’s pretty easy to just hard-code the key into the Arduino. Of course, if we wanted to dynamically add more devices and create some sort of wireless network, we would have to somehow implement secure key exchange, for example using Diffie-Hellman exchange. But we won’t delve too deep in this territory right now, we’ll just hard-code the key into our Arduino sketch.

How will this change the code from the previous chapter? Not much, to be honest, we just add the secret key and a command to encrypt or decrypt the data in our packet. Here’s the transmitter part, the encryption is done with the function aes128_enc_single().

// include the libraries
#include <LoRaLib.h>
#include <AESLib.h>

// create instance of LoRa class with default settings
LoRa lora;

// create instance of packet class
// destination: "20:05:55:FE:E1:92:8B:95"
// data:        "Hello World !"
packet pack("20:05:55:FE:E1:92:8B:95", "Hello World!   ");

// our secret 16-byte long key
uint8_t key[] = {0x2C, 0x66, 0x54, 0x94, 0xE3, 0xAE, 0xC7, 0x32,
                 0xC4, 0x66, 0xC8, 0xBE, 0xF3, 0x71, 0x22, 0x36};

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

  // initialize the LoRa module with default settings
  lora.init();

  // create strings to store the packet information
  char src[24];
  char dest[24];
  
  // print the source of the packet
  pack.getSourceStr(src);
  Serial.print("Source:\t\t\t");
  Serial.println(src);

  // print the destination of the packet
  pack.getDestinationStr(dest);
  Serial.print("Destination:\t\t");
  Serial.println(dest);

  // print the length of the packet
  Serial.print("Total # of bytes:\t");
  Serial.println(pack.length);

  // print the contents of unencrypted packet
  Serial.println("-------- Plain text ---------");
  Serial.println(pack.data);

  // encrypt the data
  aes128_enc_single(key, pack.data);

  // print the contents of encrypted packet
  Serial.println("--- Encrypted with AES128 ---");
  Serial.println(pack.data);
}

void loop() {
  Serial.print("Sending packet ");

  // start transmitting the packet
  uint8_t state = lora.tx(pack);
  
  if(state == 0) {
    // if the function returned 0, a packet was successfully transmitted
    Serial.println(" success!");
    
  } else if(state == 1) {
    // if the function returned 1, the packet was longer than 256 bytes
    Serial.println(" too long!");
    
  }

  // wait a second before transmitting again
  delay(1000);
}

And here’s the receiver, we can now decrypt using the same key and function aes128_dec_single().

// include the libraries
#include <LoRaLib.h>
#include <AESLib.h>

// create instances of LoRa and packet classes with default settings
LoRa lora;
packet pack;

// our secret 16-byte long key
uint8_t key[] = {0x2C, 0x66, 0x54, 0x94, 0xE3, 0xAE, 0xC7, 0x32,
                 0xC4, 0x66, 0xC8, 0xBE, 0xF3, 0x71, 0x22, 0x36};

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

  // initialize the LoRa module with default settings
  lora.init();
}

void loop() {
  Serial.print("Waiting for incoming transmission ... ");
  
  // start receiving single packet
  uint8_t state = lora.rx(pack);

  if(state == 0) {
    // if the function returned 0, a packet was successfully received
    Serial.println("success!");

    // create strings to store the packet information
    char src[24];
    char dest[24];
    
    // print the source of the packet
    pack.getSourceStr(src);
    Serial.print("Source:\t\t\t");
    Serial.println(src);
  
    // print the destination of the packet
    pack.getDestinationStr(dest);
    Serial.print("Destination:\t\t");
    Serial.println(dest);
  
    // print the length of the packet
    Serial.print("Total # of bytes:\t");
    Serial.println(pack.length);

    // print the contents of encrypted packet
    Serial.print("Encrypted (AES128):\t");
    Serial.println(pack.data);
  
    // decrypt the data
    aes128_dec_single(key, pack.data);
  
    // print the contents of unencrypted packet
    Serial.print("Plain text:\t\t");
    Serial.println(pack.data);
    
  } else if(state == 1) {
    // if the function returned 1, no packet was received before timeout
    Serial.println("timeout!");
    
  } else if(state == 2) {
    // if the function returned 2, a packet was received, but is malformed
    Serial.println("CRC error!");
    
  }
  
}

Our messages are now secure, thanks to the secret key. If anyone is listening on our conversation, he won’t be able to see anything but the addresses and then 240 bytes of gibberish in each packet. Similarly, if the attacker tries to transmit his own message, we’ll immediately know, because it won’t be encrypted.

Encryption with AES is incredibly easy on Arduino, so I encourage to use it. It’s not just a good programming practice. You never know who and why might be eavesdropping on your seemingly innocent conversations.

 

Conclusion

This marks the end of our short tour through long range wireless communication on Arduino. Let me know if you make your own LoRenz shield and use it for some cool Arduino project! And if you have ideas for improvements for both the LoRenz shield and the LoRaLib library, GitHub is the best place to share them with me.

When I was testing this shield, I was able to reliably transmit packets over 500 m in a fairly unobstructed environment, and over 200 m in a thick forest, with 500 kHz bandwidth, spreading factor 12 and 4/8 coding rate. All of that using just a short 10 cm antenna and powering the transmitter with a cheap 9 V battery (the receiver was receiving power from USB port, and ultimately through the Arduino on-board regulator). This distance could likely be increased even more (by lowering the bandwidth), but that would come at a significant cost of transfer speed, which should be around 1 kbps with the above settings.

Nevertheless, for my future projects, these distances are more than enough. Follow DevicePlus on social media, so that you don’t miss out on anything!


 
Click here to read LoRenz Shield >

Jan Gromes
Jan Gromes
Jan is currently studying Electrical Engineering at Brno University of Technology. He has many years of experience building projects using Arduino and other microcontrollers. His special interest lies in mechanical design of robotic systems.

Check us out on Social Media