Click here to read LoRenz Shield >
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.
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:
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.
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.
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!