Heltec Wireless Stick v3 – part 3 – mapping LoRaWAN network range

Now lets do some experiments with real range of TTN network. First one reminder – TTN is free to use, but has restrictions on usage – 30 seconds of airtime per device per 24h. When doing range testing it is tempting to send a lot packets to test signal strength and coverage. But even if You are still in allowed 30s per 24h of airtime TTN network server can start ignoring messages from Your device if You send them too often (like every 30 s). Keep that in mind and do Your tests not flooding TTN network with messages.

First we need to know where we are with our device. Some GPS module will be required. I have several VK2828U7G5LF. I prefer them a bit over NEO6MV2, not only because I have few of them in a drawer. I like that they are in one piece – antenna is mounted on back of module. I have one piece less to worry about that can move. This is my preference, since I usually do short lived projects with GPS. Place all elements in box or use zip ties to fasten it to the some protoboard. For our current project both GPS modules will work perfectly fine.

To read GPS data stream in Arduino world every one uses TinyGPS library. I connect it to Serial1 on ESP32 (don’t want to use SoftwareSerial if not has to). Since we will read only data stream we need connect just 3 cables to GPS module – power, GND and RX from Serial1 (GPIO18 on ESP32-S3 used in Wireless Stick Lite v3) with TX from module. VK2828U7G5LF has this disadvantage over NEO that has small connector, not in raster 2.54 mm. I overcome this I cut cable in half, soldering wires to regular male pin header and protect wires from breaking with hot glue. When glue is still hot I wrap it with strip of paper and squeeze – that way glue gets on all wires, connector is thinner and, you won’t believe – looks a bit better. At least in my opinion :) That way I can attach regular jumper wires to GPS module.

Working with GPS is to read all data incoming on Serial1, pass it to the GPS object. When module is giving NMEA messages with location we can check that via calling gps.location.isValid() – we are ready to test coverage!

    while (Serial1.available() > 0) {
        gps.encode(Serial1.read());
    }

This code in loop will process all messages from GPS module.

Cayenne LPP

But how to send location via TTN? In part 1 I wrote that we should use some binary formats to get shortest messages. You may be tempted to use own format, but not always it is needed. In most cases available libraries will solve your problem. Write own only if really needed. In messaging world of IoT, at least in open source probably Cayenne LPP will be your first choice.

Cayenne LPP is binary format for sending most types of data, versatile and using low overhead. You can read more about it on GitHub repo with documentation (LoRa specific part). To create LPP messages I will use CayenneLPP from ElectronicCats.

In this setup each message may contain different set of data. For example you may send temperature and humidity in one message and in other temperature, humidity and air pressure. To distinguish parameters each one has own channel number assigned. What channel number use for each data is up to You. If You chose to send temperature in channel 1 then pressure may be in channel 2. That way receiving side will know what parameter is present in message.

To prepare messages first we have prepare LPP object, with buffer for data:

CayenneLPP lpp(15);

We will send only GPS coordinates which take 9 bytes (size of different payload You can check in GitHub repo with docs linked earlier), so 15 bytes for buffer is more than enough.

Now in function send_lora_frame we check if we have valid position available and if true prepare message.

    lpp.reset();
    if( gps.location.isValid()) {
        lpp.addGPS(1, gps.location.lat(), gps.location.lng(), gps.altitude.meters());
    } else return;
    if (lpp.getSize() > LORAWAN_APP_DATA_BUFF_SIZE) {
        Serial.println("Too big LPP buffer!!!, not sending!");
        return;
    }
    memcpy(m_lora_app_data.buffer, lpp.getBuffer(), lpp.getSize());
    m_lora_app_data.buffsize = lpp.getSize();

lpp.reset clears all data in buffer. If we receive proper location from GPS (gps.location.isValid() is true) then we add GPS data to buffer with addGPS. First argument (1) is channel number. Then latitude, longitude and altitude as provided by GPS module. All GPS related work is done by TinyGPS mentioned earlier – you need feed all data from GPS module to gps object, as was shown in snippet from loop.

In case no GPS location we return (else return;) since we don’t want to send empty message.

All types of data You can send via Electronic Cats Cayenne LPP implementation are collected here. Each addXXXX function returns cursor position in buffer after adding data or 0 if data don’t fit into buffer created when defining lpp variable. TBH I should check if addGPS doesn’t returned 0, but since we send only one measurement (location) of size 9 bytes and we have created 15 bytes buffer I have skipped that.

Next we check if LPP message fits in buffer for LoRaWAN message. Again, this seems not needed (LORWAN_APP_DATA_BUFFER_SIZE I defined as 64 bytes) but in next step code will be using memcpy. Every time you do operations directly writing to memory do size checks otherwise you may introduce serious bugs to your code.

Now, we are ready to copy all LPP message to LoRaWAN message buffer: memcpy(m_lora_app_data.buffer, lpp.getBuffer(), lpp.getSize());

Data is placed in buffer message can be sent. But to test coverage You can not relay on periodic message sending like in previous two parts. This is perfect case for asynchronous sending. So we will use button. When pressed node will send its location to TTN. This time, instead of doing button reading from a scratch I have decided to use Bounce2 library.

Bounce2::Button button = Bounce2::Button();
void setup()
{
    //Button
    button.attach(4, INPUT_PULLDOWN);
    button.interval(5);
    button.setPressedState(HIGH);
}

Have chose GPIO4 (no particular reason for it) for button. Connected it to 3V3 and GPIO4 and in setup configure button object – attach inform it that we use GPIO4 and that should use internal pull-down mode on GPIO. interval set timeout for 5 milliseconds. During that time after change was detected next changes of state will be ignored. That prevents occurrence of bouncing (thus name of library) – random button state changes caused by mechanical properties of tact switch contacts. setPressedState inform library what state of button should be considered pressed.

Now, all you need to call periodically button.update(); in loop to use library functions to get current state of button. How to send data on button press?

In loop just check for pressed button and send data:

    if (button.pressed()) {
        if (millis()-lastSend > 10000) {
            send_lora_frame();
            lastSend = millis();
        } else {
            Serial.println("Too frequent uplink! Wait a bit longer...");
        }
    }

lastSend is variable keeping time of last packet sent. If from previous event passed less than 10000 ms (10 s) do not send another one. This is prevent you from flooding TTN with messages. But if you send packets even every 10s TTN network server soon will be ignoring your packets. Read about regional rules and stick to them.

Now log in to TTN console and go to settings of your application. You have drop Java Script payload formatter and change it to CayenneLPP for uplink:

Now send packet. If received by TTN you will get something similar to that:

Device id is redacted

Since TTN console know that you send data in Cayenne LPP format it can decode message and know it is location data.

But what next?

TTN Mapper

In first part I have sent You to TTN Mapper website to check coverage. Now we are sending messages to TTN, they are received and decoded in console. But this is not very useful. TTN, after receiving a message from your application can send all data to external application. These are so called integrations and now we will use predefined integration for TTN mapper. Select Integrations in application console and select Webhooks. Click Add webhook and serach for TTN mapper webhook. Select it and fill required fields:

Webhook and your email address are required. You may provide experiment name – if you do you will be able later to filter entries by its name. If left empty – you will be able to filter results by gateway or by device id.

Create webhook and you are ready to do some tests. Go to Advanced maps provide your device id and date ranges click and view map. Points will show (after refreshing) few seconds after you send location data, so you can see your tests almost in real-time. Again – remember NOT TO FLOOD network with messages!

And, results?

Now see some map from TTN Mapper. Dark blue is weakest signal, red – strongest.

A few words on results. Gateway is mounted on top of the roof but close to the ridge, on westren side. Ridge is oriented N-S so maybe it have a bit impact on range (for signals from east it may be a obstacle). We will see that in a moment, longest range is achieved when there is a chance for line-of-sight. Thus, on the street stretching straight to the east you can get transmission, but if you get into the side street and gateway is obscured by building or trees – no transmission.

I spend some time doing range tests, I will try get more data points in future maybe this info here will be updated. But even today I can show you that there some simple ways to improve range.

These tests on map above were done with default spring antenna included with Heltec Wirless Stick Lite. The simplest way to improve range is to use better antenna, duh. So here are some results with 5 dBi 868 MHz antenna and 50 cm pigtail SMA to connect it to Stick. Results?

OK, lets see it as animation

Click to see GIF animation

For me use of better antenna gave almost two times better range. I plan to test how it behave on side streets I was writing before, if there is a chance to get some transmission, but it takes time to do tests.

What about spreading factor?

All results presented here are on default setting for library. What does that mean? LoRa can get much better range but it is always at a cost. Basically You can send data much slower and get better results. But slower means a longer time on the air.

If you want to get know much more about LoRa I can recommend this article on Medium explaining how LoRa radio works. Data in LoRa is being represented as frequency change. How fast frequency rises or lowers is called spreading factor. So, for lower SF change is faster, data can be transferred in shorter time compared to higher SF, when change is slower. But slower change allow for better range.

So what difference in time? By default LoRaWAN library uses SF9 and all results here were with this setting. In TTN application console when you expand details on received uplink message you can find consumed_airtime parameter. For messages in code used (9 bytes of payload) airtime for SF9 was 0.2s. When changed to SF12 (a highest possible SF, probably with much better range) airtime became 1.45s – 7.25 times longer airtime!

As always – designing is always about compromises. I plan to do more tests with range achieved on different settings/different antennas but this takes a lot of time and I hope extend this series of post when I get more data.

Finally, code for download

OK, so last thing – code used in example – download PlatformIO project here.