Heltec Wireless Stick Lite – sending data to LoRaWAN – part 1

Heltec Wireless Stick is ESP32 module with LoRaWAN capabilities. Heltec provides own LoRaWAN stack, but since ESP32 and SX1262 (LoRa radio chip) are well used in many open source projects, so You can use it with free software.

First, disclaimer. For me LoRa is interesting topic, but due to limited time I don’t have much experience with them. I’ll try to explain this topic according to my knowledge, but my understanding may be wrong, so if You spot some errors, let me know (using comments).

First. LoRa and LoRaWAN. LoRa is radio communication protocol providing communication between two nodes. It provides LOng RAnge, low power low data rate :) LoRaWAN is something bigger.

LoRa is proprietary and there are many LoRaWAN networks worldwide. Some of them are private, some of them are publicly available. All my experiences are with The Things Network (TTN) and information in this article is related to TTN LoRaWAN network.

LoRa basically is communication between two devices. In LoRaWAN some of them become gateways, other are end devices. Gateway has access to Internet and forwards incoming packets to LoRaWAN backend.

TTN is a public and open network, anyone can send messages through it. There is Fair Use Policy in place, to prevent network congestion. Devices on free, public TTN has to keep daily radio time (when sending messages) under 30 s (total uplink air time per 24h), please read details on TTN page. Depending on payload size and how far end device is from gateway, device can send messages with interval from few minutes (single bytes, close to gateway) to few messages during a day (bigger payload or far away from gateway).

To use LoRaWAN Your devices has to be in range of gateway. How to check if there is any gateway in Your area? Check TTN mapper to see if there is any near of You.

If You are not lucky to have gateway near and You are serious to get into this topic, maybe You can start own gateway. You need some hardware and internet connection. We have Heltec HT-M01S LoRaWAN gateway which can be used with TTN. It has indoor case, but has outdoor antenna on short cable so maybe You can get it outside to get better range.

OK, so let’s start.

Register in TTN

In TTN network are many devices. How it is organized that messages from devices are routed to correct places? Devices in TTN are grouped into applications. So, first You need create an application. Then, You create devices. Starting from that You can see incoming packets on TTN console.

Just in…

First, go to TTN website and find SignUp button. Select plan matching Your needs. I assume You will use Community Edition/Individual which is free plan. After You have created account, log in, and click on Your user name in upper right corner. Go to Console. Currently TTN has three clusters Europe, North America and Australia. Select closest to You and when redirected select Applications in menu. Click Add application button. Fill required data (application id).

New TTN application

After creation, You will be redirect to application config page:

We need to create our first device so go to End devices and click Register end device.

Select device from database. HelTec Automation/Wireless Stick Lite class A, OTAA and select proper region. If You bought Wireless Stick Lite from us – then it will EU_863_870, as on screen.

Generate keys – remember these should be confidential, since will be used to connect to network as this device. And now – I’m not an LoRaWAN expert. As far as I understand documentation from TTN for device not preprogrammed You may use JoinEUI all zeros.

End device ID has to be unique across single application. It is human readable ID for this device. You can reuse IDs in different applications.

Now, to sum it up, for OTAA activation You will need:

  • JoiunEUI changes its name to AppEUI ( just zeros)
  • DevEUI
  • AppKey

Later copy them to code which be run on Wireless Stick. To copy it form console interface – note that when you hover with mouse to that field, you will get also icon to change display format to be more compatible with C/C++. It will be handy when You will be pasting it to the code.

Basic connectivity – example code

OK, we have new device configured, now we need to build one. As I said I will use Heltec Wireless Stick Lite. It is quite small (58×23 mm) board, with ESP32 on board and LoRa radio. I was testing it with SX126x-Arduino library and had no problems with starting sending data.

As for Nov 2024 Arduino IDE 2 definitions of Heltec board does not allow to compile version 2.x this library – it defines REGION_XXXXX. Version 2 of library require have not set any of those defines. Well, it is all open source, so if You want to, You can dive into boards.txt of ESP32 package find Wireless Stick Lite definitions and add new menu entry for non existing region, which will disable check in library code.

But, as I wrote many times here, I prefer PlatformIO to develop any code on ESP/Arduino and here You can use 2.x library w/o problems. OK, there is one quirk for (as for Nov 2024) – there is no board definition for Wireless Stick Lite v3, only for v2. The v2 version have used different ESP32 processor, so this definition can not be used (compiled binary wont work on that platform and even won’t upload to board). So just use board = heltec_wifi_lora_32_V3 (Heltec WiFi LoRa 32 V3) in platformio.ini and it will work.

I will provide You with sample code, but it is basically LoRaWAN example from library (for now, since in next parts there will be more changes). I have just dumped code for other processors, making it just for ESP32.

Now a bit of explanation what is in code.

First pin definitions to get radio communication:

// ESP32 - SX126x pin configuration
int PIN_LORA_RESET = 12;	 // LORA RESET
int PIN_LORA_NSS = 8;	 // LORA SPI CS
int PIN_LORA_SCLK = 9;	 // LORA SPI CLK
int PIN_LORA_MISO = 11;	 // LORA SPI MISO
int PIN_LORA_DIO_1 = 14; // LORA DIO_1
int PIN_LORA_BUSY = 13;	 // LORA SPI BUSY
int PIN_LORA_MOSI = 10;	 // LORA SPI MOSI
int RADIO_TXEN = -1;	 // LORA ANTENNA TX ENABLE
int RADIO_RXEN = -1;	 // LORA ANTENNA RX ENABLE

Next, few forward declarations:

static void lorawan_has_joined_handler(void);
static void lorawan_rx_handler(lmh_app_data_t *app_data);
static void lorawan_confirm_class_handler(DeviceClass_t Class);
static void lorawan_join_failed_handler(void);
static void send_lora_frame(void);
static uint32_t timers_init(void);

Those functions has to be defined later and all are required by library. lorawan_has_joined_handler is called when device has successfully connected to LoRaWAN network. lorawan_rx_handler is being called by library when some data from network has been received (aka downlink message).

As You may remember when registering device You have selected Class A device. There are three classes: A, B and C. Device can change its class and if it is happening library will call this function. Basically class describes how easy is to access your device. Class A means device is offline and only when has data to send to network then radio will operate. As You probably guess this mode is meant for sensor nodes sending some measurements to cloud. We will stick to this class and will work always that way.

lorawan_join_failed_handler is called when joining was not successful. Why it may happen? Wrong keys or just no gateway in range…

send_lora_frame is function which is sending data to network. OK, this function (and timers_init) are not directly required by library, but are required by how example code was written. It assumes that node will send data to LoRaWAN periodically. send_lora_frame is function doing that. In timers_init there is periodical timer started and it will call send_lora_frame. So, You may write your code differently especially when you want to send data not periodically but after some event. For example – node measuring rainfall is good example.

uint8_t nodeDeviceEUI[8] = {0x70, 0xB3, 0xD5, 0x7E, 0xD0, 0x06, 0xB8, 0xB9};
uint8_t nodeAppEUI[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
uint8_t nodeAppKey[16] = {0xF1, 0x37, 0x4B, 0xE1, 0x3B, 0x99, 0xC5, 0xCE, 0x7D, 0x1C, 0x16, 0x82, 0xEF, 0xEC, 0xF1, 0xAC};

These 3 variables will be used when joining to LoRaWAN network. Just copy values from your application console. This is only place you need to customize code from my example. Now upload it to Heltec Wireles Stick Lite (pio run -t upload) and watch Your console. You should see something like that:

Yay, we are online!

OK. Now dive to our code sending real data. send_lora_frame function:

    if (lmh_join_status_get() != LMH_SET)
    {
        //Not joined, try again later
        Serial.println("Did not join network, skip sending frame");
        return;
    }

We are not online? We are not sending.

    uint32_t i = 0;
    m_lora_app_data.port = LORAWAN_APP_PORT;
    m_lora_app_data.buffer[i++] = 'H';
    m_lora_app_data.buffer[i++] = 'i';
    m_lora_app_data.buffer[i++] = '!';
    m_lora_app_data.buffsize = i;

    lmh_error_status error = lmh_send(&m_lora_app_data, LMH_UNCONFIRMED_MSG);
    Serial.printf("lmh_send result %d\n", error);

We will send short message. LoRaWAN is not for text and I guess in most cases You will use Cayenne LPP (this will be covered in next part) as payload format. I use here simple ‘Hi!‘ to show you some features of TTN Console.

So, hit upload and watch TTN console. After time defined in LORAWAN_APP_TX_DUTYCYCLE You should see first messages. Click on it to expand details.

Single message as seen in console
Details of message

Yay, we have first message, but when You look in details, where are Your data? data entry in JSON have more info about sending device and payaload itself – frm_payload field have data sent by device. But why it is some cryptic characters, not ‘Hi!’?

Transmission in LoRaWAN is encrypted, this is first reason. Second reason is that TTN does not know what Your data is representing, what format is being used, so it displays ‘as is‘. Since TTN knows keys used to cipher messages nothing more is needed in that regard. We need to tell something about data format. This is done by Payload formatters – you can define one global for whole application, then it will be used to decode data from all devices in this application or you can define different formatters for each device. Formatter concept is the same in both cases, so I will show example on application level.

As You can see, if You will use CaeyenneLPP to send data TTN will know how to decode message. Since we have custom format, select JavaScript formatter. JS used by formatters is ES5.1, no network access, no require, max size 40 kB. If You need to know more, read documentation on TTN.

We can add simple function to decode string:

  let str = input.bytes
    .map((byte) => {
        return String.fromCharCode(byte);
    })
    .join("");

And return data:

  return {
    data: {
      bytes: input.bytes,
      str: str
    },
    warnings: [],
    errors: []
  };
}

TTN console expect this code be wrapped into decodeUplink function and input is function argument where attribute bytes is array with all bytes of payload (already deciphered). So whole custom uplink formatter will be as follow:


function decodeUplink(input) {
  let str = input.bytes
    .map((byte) => {
        return String.fromCharCode(byte);
    })
    .join("");

  return {
    data: {
      bytes: input.bytes,
      str: str
    },
    warnings: [],
    errors: []
  };
}

And now, after message has been received:

Brief view of message in console
Details of message as shown by console

What next?

OK, so You should be able to create own code and send data to TTN. Now, code can be downloaded from here.

As You probably saw, device joins to network each time it starts. It is not best practice to operate in that way. We should store session keys after successful join, and next time when Wireless Stick boots – use them and not sending new join request.

This can help also when Your device is on the edge of range – if response with session keys does not return to board, then it thinks that join has failed. If Your device can store keys after join, you can overcome that getting closer to gateway, join it and then it should work better. We will try to do in next part.

This example sends data to TTN, You can browse packets in TTN console but it is not very useful. So in next part we will see how TTN integrates with third party services (yours!) to send data collected from end devices.