My attempt at RF95 (LoRa)


#1

Having been a fan of RM95 radios for a while now, like a lot of folks I wanted to integrate them into the Photon world to gain access to easy Internet access. Several folks on this community board have posted their work so I thought I would post mine. My setup is currently:

  1. A Moteino board with RF95 LoRa radio acting as the server
  2. A Moteino board with RF95 LoRa radio acting as client #1
  3. A Photon board with RF95 LoRa radio acting as client #2

Every second, each client sends a packet of fictitious weather data (temp, humidity, wind speed and direction) to the server. To make the prototyping quicker, I use random numbers for temp, humidity and wind. Client 1 sends “N” for wind direction and client 2 sends “E”.

Each time the server receives a packet it subtracts a random value from each numeric field and switches the wind direction (N->S, E->W) and returns the packet. I track RSSI on the server along with time in ms for packet transfer.

I used the standard Particle web IDE and included the RadioHead libraries necessary to make everything work. I’m currently using the RHDatagram manager and will next try the RHReliableDatagram manager and then the RHMesh manager. These are the include files required for RHDatagram:

  • RadioHead.h
  • RH_RF95.cpp
  • RH_RF95.h
  • RHDatagram.cpp
  • RHDatagram.h
  • RHGenericDriver.cpp
  • RHGenericDriver.h
  • RHGenericSPI.cpp
  • RHGenericSPI.h
  • RHHardwareSPI.cpp
  • RHHardwareSPI.h
  • RHReliableDatagram.cpp
  • RHReliableDatagram.h
  • RHSPIDriver.cpp
  • RHSPIDriver.h

You don’t have to change anything in the RadioHead code…just import. Here is the client code running on the Photon:

//
// Last update: 03/25/18
//
/****************************************************/
/*  Includes                                        */
/****************************************************/
#include <Particle.h>
#include <RHReliableDatagram.h>
#include <RH_RF95.h>

/****************************************************/
/*  Defines                                         */
/****************************************************/
#define MY_ADDRESS      3
#define SERVER_ADDRESS  2
#define TXPWR           5

/****************************************************/
/*  Packet structure                                */
/****************************************************/
#pragma pack(push, 2)
typedef struct WEATHERDATA {
  float temp;
  float hum;
  float windSpeed;
  char windDir[2];
};
#pragma pack(pop)

/****************************************************/
/*  Globals                                         */
/****************************************************/
WEATHERDATA data;
const float FREQ = 915.0;
RH_RF95 rf95;
RHDatagram manager(rf95, MY_ADDRESS);
ApplicationWatchdog wd(60000, System.reset);            // Watchdog timer, will reset system after 60 seconds of inactivity

/****************************************************/
/*  setup                                           */
/****************************************************/
void setup(void) 
{
  Serial.begin(115200);
  setupRadio();
}

/****************************************************/
/*  loop                                            */
/****************************************************/
void loop(void)
{
  unsigned long startMilli,
                stopMilli;
               
  data.temp = random(50,95);
  data.hum = random(25,75);
  data.windSpeed = random(15,30);
  strcpy(data.windDir, "E");

  Serial.println("=======> Sending temp = " + String(data.temp,1));
  Serial.println("=======> Sending hum = " + String(data.hum,1));
  Serial.println("=======> Sending wind speed = " + String(data.windSpeed,0));
  Serial.println("=======> Sending wind direction = " + String(data.windDir));

  startMilli = millis();
  if (!manager.sendto((uint8_t *) &data, sizeof(data), SERVER_ADDRESS))
    Serial.print("Transmit failed");
  rf95.waitPacketSent(100);                  // wait 100 mSec max for packet to be sent
  stopMilli = millis();
  Serial.print("Transmission time (mSec) = ");
  Serial.println(stopMilli-startMilli);
  Serial.println();

  startMilli = millis();
  if (rf95.waitAvailableTimeout(100))        // wait 100 mSec max for response
  { 
    uint8_t bufLen = sizeof(data);
    if (manager.recvfrom((uint8_t *) &data, &bufLen))
    {
      stopMilli = millis();
      Serial.print("Response time (mSec) = ");
      Serial.println(stopMilli-startMilli);      
      if (bufLen == sizeof(data))
      {
        Serial.println("<======= Received temp = " + String(data.temp,1));
        Serial.println("<======= Received hum = " + String(data.hum,1));
        Serial.println("<======= Received wind speed = " + String(data.windSpeed,0));
        Serial.println("<======= Received wind direction = " + String(data.windDir));
      }
      else
        Serial.println("Incorrect response size");
    }
  }
  else
    Serial.println("Timed out waiting for response");
  Serial.println();
  delay(1000);
}

/****************************************************/
/*  setupRadio                                      */
/****************************************************/
void setupRadio(void)
{
  if (manager.init())
  {
    if (!rf95.setFrequency(FREQ))
      Serial.println("Unable to set RF95 frequency");
    if (!rf95.setModemConfig(RH_RF95::Bw500Cr45Sf128))
      Serial.println("Invalid setModemConfig() option");
    rf95.setTxPower(TXPWR);
    Serial.println("RF95 radio initialized.");
  }
  else
    Serial.println("RF95 radio initialization failed.");
  Serial.print("RF95 max message length = ");
  Serial.println(rf95.maxMessageLength());
}

LoRa vs Xbee vs Xenon
#2

Thank you for sharing and your hard work!


#3

Thanks for sharing this code!


#4

@KyleG, @rwb, you’re welcome. Just trying to pay it forward for the help I’ve received over the years.

Will update when I get the mesh testing completed. One more thing I wanted to pass along are some of the other available parameters available when configuring the radio:

Here are other enum’s when using rf95.setModemConfig():

Bw125Cr45Sf128    Bw = 125 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Default medium range.
Bw500Cr45Sf128    Bw = 500 kHz, Cr = 4/5, Sf = 128chips/symbol, CRC on. Fast+short range.
Bw31_25Cr48Sf512  Bw = 31.25 kHz, Cr = 4/8, Sf = 512chips/symbol, CRC on. Slow+long range.
Bw125Cr48Sf4096   Bw = 125 kHz, Cr = 4/8, Sf = 4096chips/symbol, CRC on. Slow+long range.

If you want to manually configure the radio, use rf95.setModemRegisters(&modem_config);

RH_RF95::ModemConfig modem_config = {
  0x78, // Reg 0x1D: BW=125kHz, Coding=4/8, Header=explicit
  0xc4, // Reg 0x1E: Spread=4096chips/symbol, CRC=enable
  0x0c  // Reg 0x26: LowDataRate=On, Agc=On
};

You can pick a lot of different parameters for testing. I would also suggest getting the LoRa calculator from SemTech. It allows you to do a lot of testing within the app.


#5

I got the RF95 radios working awhile back with the help of another guy on here but I never figured out how to receive a payload from an RF95 radio connected to a Photon or Electron and then save it as a variable that I could then push out to the web via a Particle Publish.

I could receive the payload on the RF95 + Photon and serial print it but wasn’t sure how to take that received payload, identify it, and then save it as a particular variable.

I’m assuming that’s what you did in your code but I have not dove into it yet due to lack of free time.

Also the Modem config settings are critical for using the ReliableDatagram code from my testing. I could not get the Reliable Datagram code to work past a few hundred feet until I changed the Modem Config settings and then I could get just about the same 1 mile range as when not using the Reliable Datagram code.

Please do share the Modem Config settings you find to work best for you.


#6

@RWB, the code I provided above is for a Photon acting as a client in a LoRa environment. There is an RF95 radio (HopeRF) connected to a Photon. Acting as another client is a Moteino (Arduino clone) with an RF95 radio built in to a single board. There is a second Moteino with a LoRa RF95 radio acting as the server. Since I was too lazy to connect real temp/hum sensors to the two clients, I just used random numbers. However, it does show you how to send data from the LoRa Photon to a LoRa Moteino (Atmel 328p). I had to do the 2-byte packing but I also had to do the same when using the Photon with an RFM69 radio connected to other Moteinos running an RFM69 radio.

On my real weather project, I have multiple sensors sending data over an RFM69 radio, sending data to my Internet gateway which is a Photon with an RFM69 radio. So, the Photon/RFM69 node receives a tremendous amount of data and also sends out selected data to other RFM69-based nodes for display. Once you get past the packing issue, it really isn’t any different than sending any data structure over a radio.


#7

I modified the earlier code from the Photon-RF95 client to add a real AM2302 (DHT22) so now I’m sending real temp/humidity to the server. Using the Adafruit_DHT (0.0.2) library from the web IDE.

//
// Last update: 03/27/18
//
/****************************************************/
/*  Includes                                        */
/****************************************************/
#include <Particle.h>
#include <RHDatagram.h>
#include <RH_RF95.h>
#include <Adafruit_DHT.h>

/****************************************************/
/*  Defines                                         */
/****************************************************/
#define MY_ADDRESS      3
#define SERVER_ADDRESS  2
#define TXPWR           5
#define DHTPIN          D3
#define DHTTYPE         DHT22

/****************************************************/
/*  Packet structure                                */
/****************************************************/
#pragma pack(push, 2)
typedef struct WEATHERDATA {
    float temp;
    float hum;
    float windSpeed;
    char windDir[2];
};
#pragma pack(pop)

/****************************************************/
/*  Globals                                         */
/****************************************************/
WEATHERDATA data;
DHT dht(DHTPIN, DHTTYPE);
RH_RF95 rf95;
const float FREQ = 915.0;
RHDatagram manager(rf95, MY_ADDRESS);
ApplicationWatchdog wd(60000, System.reset);            // Watchdog timer, will reset system after 60 seconds of inactivity

/****************************************************/
/*  setup                                           */
/****************************************************/
void setup(void) 
{
    Serial.begin(115200); 
    pinMode(DHTPIN, INPUT);
    setupRadio();
    dht.begin();
}

/****************************************************/
/*  loop                                            */
/****************************************************/
void loop(void)
{
    float f = dht.getTempFarenheit();
    float h = dht.getHumidity();
  
    if (isnan(f) || isnan(h)) 
    {
        Serial.println("Failed to read from DHT sensor!");
        f = 0;
        h = 0;
    }
    else
    {
        data.temp = f;
        data.hum = h;
    }
    data.windSpeed = random(15,30);
    strcpy(data.windDir, "E");

    Serial.println("=======> Sending temp = " + String(data.temp,1));
    Serial.println("=======> Sending hum = " + String(data.hum,1));
    Serial.println("=======> Sending wind speed = " + String(data.windSpeed,0));
    Serial.println("=======> Sending wind direction = " + String(data.windDir));
    Serial.println();

    // Send data to server
    if (!manager.sendto((uint8_t *) &data, sizeof(data), SERVER_ADDRESS))
        Serial.print("Transmit failed");
    rf95.waitPacketSent(250);                  // wait 100 mSec max for packet to be sent
 
    // Receive response from server
    if (rf95.waitAvailableTimeout(250))        // wait 100 mSec max for response
    {  
        uint8_t bufLen = sizeof(data);
        if (manager.recvfrom((uint8_t *) &data, &bufLen))
        {
            if (bufLen == sizeof(data))
            {
                Serial.println("<======= Received temp = " + String(data.temp,1));
                Serial.println("<======= Received hum = " + String(data.hum,1));
                Serial.println("<======= Received wind speed = " + String(data.windSpeed,0));
                Serial.println("<======= Received wind direction = " + String(data.windDir));
                Serial.println();
            }
            else
                Serial.println("Incorrect response size");
        }
    }
    else
    {
        Serial.println("Timed out waiting for response");
        Serial.println();
    }
    delay(2000);
}

/****************************************************/
/*  setupRadio                                      */
/****************************************************/
void setupRadio(void)
{
    if (manager.init())
        {
            if (!rf95.setFrequency(FREQ))
                Serial.println("Unable to set RF95 frequency");
            if (!rf95.setModemConfig(RH_RF95::Bw500Cr45Sf128))
                Serial.println("Invalid setModemConfig() option");
            rf95.setTxPower(TXPWR);
            Serial.println("RF95 radio initialized.");
        }
    else
        Serial.println("RF95 radio initialization failed.");
    Serial.print("RF95 max message length = ");
    Serial.println(rf95.maxMessageLength());
}

#8

Having used the RFM69 radios for several years now, I’ve always liked that they support encryption. Well, I was reading the fine print this morning on the RadioHead libraries and found out that the RF95 radios also support a 16-byte key just like the RFM69! I had to download and install another 10 libraries but I finally got the new encrypted version to compile. It apparently only supports a simpler datagram style packet but I wanted to at least play with it. I now have to add these new libraries to the Moteino LoRa setup I’m using for the client. I will report back when I’m able to transfer encrypted messages.

For the record, you have to add the following libraries:

  • Speck.h & cpp
  • BlockCipher.h & cpp
  • Crypto.h & cpp
  • RHEncryptedDriver.h & cpp
  • RotateUtil.h
  • EndianUtil.h

You have to uncomment the last line of RadioHead.h to enable encryption.


#9

Well folks…I have to admit that I failed to implement the encryption add-on for the RF95 radio listed above. Somewhere between encryption and decryption things didn’t quite work out. However, I did find a neat crypto replacement from the following Dragino Github repository:

https://github.com/dragino/Arduino-Profile-Examples/tree/master/libraries/Dragino/examples/LoRa/Concurrent/concurrent_gateway

I’m not a crypto guy and I’m sure the protection afforded by this encryption isn’t as strong as others, but seriously, how many people need hard core encryption? If you do, let me know what you’re using. Here is some more info on the method they use:

https://en.wikipedia.org/wiki/XXTEA

I ended up removing the crypto files listed above and just added the encrypt.h and encrypt.cpp files from the Dragino code.

If anyone gets the Speck code working, I’d love to see how you did it.


#10

For some reason, I thought the RFM95w Reliable Datagram was encrypted somehow but not sure why I thought that.

Have you done any range testing with the RFM95w radios? I got 1 mile with simple wire antennas in the city with mainly 1 and 2 story houses which is pretty impressive.


#11

As far as I know, the RF95 radio does not provide any form of native encryption capabilities. It was nice with the RFM69 since it has hardware AES. I just finished reading several Hope datasheets on the RF95 and the word encryption wasn’t mentioned anywhere.

I haven’t done any extensive testing, and the place where I did do testing was very heavily wooded but even then I was getting over 1/2 mile. Once I finish my initial installation and tests of the mesh capabilities I will probably do some range testing. One of my ultimate goals is to use the LoRa range capability in my drones to control various devices that will be payloads. Since most of the traffic will be very short in nature, I should get very respectable distances since trees and other ground-based items won’t be an issue.


#12

Well, I finished my testing with RHReliableDatagram. I performed all my testing with the default of 3 retries per packet with a 200 ms timeout.

Moving on to RHMesh.

After that, I will hopefully have some time for distance testing.


#13

Thanks Your sharing project.

In my project I have Gateway/server (based on Atmega1284p and RFM95W 868 Mhz) and 8 remote Nodes/clients (based on Atmega328 and RFM95W 868 Mhz). All Nodes are poweres from batteries. All Nodes sleeps 16s and then attepts to link with Gateway. If connetion is succesfull Node send its status (5 bytes) and recive from Gatewaycommand. And so all Nodes. All is nice when amount of Nodes is not more as 4 else some Nodes cannot connect to Gateway. I gess here are colligions when few Nodes are attemting connect to the Gateway simultaneuosly. In RadioHead library is supported CAD (Channel Active Detect), but I not found examples how can be applay this method in Gateway and Nodes…
What do You can advise me?

Thank You.


#14

@rwb I haven’t done any range testing myself, but if you check out the forums at lowpowerlabs.com you will find more than you can count.


#15

@Leonid49 Does every node have to send at exactly 16s intervals? You can try adding a random delay (1-50ms) which will keep nodes from getting “synched”. You also want to optimize the gateway code to receive and process each incoming packet. You might think of adding a circular queue for incoming packets. Another possibility is to add a “retry” function. Depending on your radio library of choice, most have some kind of retry built into the API calls.


#16

Thank You, Bryan.

<You can try adding a random delay (1-50ms) which will keep nodes from getting “synched”.>
I think it is a good idea. I try this today and let You know rezult.

In my opinion the best solution would be: the Gateway sequentially polls the nodes in turn. All Nodes should be constantly in Listen mode (let say 20ms Rxmode and 200ms Iddle mode). Tthe Gateway sends a request to the desired node with a long preamble (in this case, 20 + 200 = 220 ms) and the node address. All Nodes weik up (yes, this is not good) and analizes Address in receved packet. That Node which address is iquals to address in packet receves payload, execute received command and send to the Gateway its state.
Such aproach alows Gateway to poll nodes only if it is needed. But I haven’t tried this method and nothing found how this to do.
What You can say abaut this method? Did You tried it?

Thank You.


#17

I have never tried this method. Good luck and let me know how it works out for you.


#18

Thanks, Bryan.
Yes, I will try this and let You know rezult.