ESP32 LoRaWAN Node using Arduino

ESP32 LoRaWAN Node ModuleTo test out the recent addition in my lab that is the LoRa gateway, I needed one LoRa node also. LoRa modules are available to be used with Arduino as well as Raspberry Pi and are pretty cheap too. However, you can get complete LoRa module with ESP32 and OLED display at pretty low price from Banggood, and they fit the purpose. And by using these, you don’t need to jumble around jumpers. In this post, I have covered how to make a simple, cheap LoRa node that can send data to The Things Network(TTN). Before getting into set up regarding LoRa first thing that has to be done is add support for ESP32 module to your Arduino IDE. I have covered that in an earlier post and you can check that out here.  Now after that lets dive into setting up Arduino IDE for LoRa communication.

Getting Started

About the Module: The module I have used for testing is the ESP32 LoRa module by Heltec, and you can buy it from here. The module that I have got is meant for 868MHz and choose the module depending upon the frequency plan supported in your country for LoRa. This module comes with  SX1276 (Datasheet: LoRa chip, ESP32 chip,  0.96-inch blue OLED display, and CP2102 USB to UART converter. The ESP32 module comes with 32Mb of flash memory.

The pin diagram of the module is as follows; please keep in mind the pins those are connected to the onboard peripherals like the OLED display and the Lora module. Any external peripherals need to be connected accordingly. The used pins are marked with a red arrow in the pin diagram.


heltec ESP32 LoRa module

You can check about the module in detail here in the official site of the module. To connect the module to the PC, the required drivers for CP2102 need to be installed and can be downloaded from here.

Installing support for the Display: Now as you have a basic idea about the module, lets first install the library required for the onboard OLED module. The library that I have used is  u8g2 by Oliver. This can be installed merely by getting into the manage libraries section of the IDE and searching for u8g2.

U8g2: Library for monochrome displays, version 2

Installing Lora Support: Adding support for LoRaWAN in Arduino is pretty simple. LoRa is a physical layer communication protocol, which means it only takes care of physical layer communication it doesn’t do any job regarding network management and handshaking. To implement a network of such modules however needs require complex networking protocols. There where the LoRaWAN protocol comes into the picture and the complete MAC layer has to be written w.r.t to the microcontroller. The library I have used for the MAC layer of LoRaWAN for Arduino/ESP32 is arduino-lmic by Matthijs Kooijman. If you want to know in-depth about LoRaWAN, you can check out this article.

You can simply download the repository and add to Arduino IDE, and you are good to go.

The code I have provided send data to TTN on the press of a button connected to GPIO 13 (Arduino naming ) and flash a LED 2 times connected to GPIO 12. The text Data Sent is displayed on the OLED. If the TTN server sends back any scheduled data, the LED flashes for three times and data is printed to the serial console. Please check the video for details.

The important thing however in the code is to select the pins properly depending upon the ESP32 LoRa module you are using, and all the available have different pin configurations. So make sure to change accordingly. In my case it is,

// Pin mapping
const lmic_pinmap lmic_pins = {
  .nss = 18,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 14,
  .dio = {26, 33, 32}

Please check the pin diagram of respective modules. The next important thing in the sketch is to set the network keys which are obtained from TTN while registering the new node (can be obtained after the addition also, so no need to copy if not required). The keys are different and depend on the mode of activations used which are either ABP (Activation By Personalization) or OTAA (Over The Air Activation). In this sketch, I have used ABP method, and the required keys are Network Session Key, Application Session Key, and Device address.

To obtain the keys first, you need to register an application in TTN and to do so navigate to TTN console applications page but before that make sure you have an account. After that the node can be added to the application and keys can be obtained. Please check the video for details.  To read about ABP and OTAA activation methods and security measures in LoRaWAN, you can check this article.

One final thing is even if you do not have a LoRa gateway, make sure you have one around may be other users or any operator, and moreover, that is configured for TTN and your used frequency band. Otherwise, this will not work. Even if there is one gateway around it will work.



 * Original Code: 
 * Modified By: Bikash Narayan Panda ( 
 * ***/
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
#include <U8x8lib.h>

static const PROGMEM u1_t NWKSKEY[16] ={______ };
static const PROGMEM u1_t APPSKEY[16] ={______ };
static const u4_t DEVADDR = 0xxxxxx; // <-- Change this address for every node!

// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }

static uint8_t mydata[] = "Hi from WGLabz!";
static osjob_t sendjob;

// Pin mapping
const lmic_pinmap lmic_pins = {
  .nss = 18,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 14,
  .dio = {26, 33, 32}
//OLED Declaration 
U8X8_SSD1306_128X64_NONAME_SW_I2C u8x8(/* clock=*/ 15, /* data=*/ 4, /* reset=*/ 16);

//LED and Button Pins
int buttonPin = 13;
int ledPin=12;
int boardLED=25;
int lastState=0;

void onEvent (ev_t ev) {
    Serial.print(": ");
    switch(ev) {
        case EV_TXCOMPLETE:
           Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
           u8x8.drawString(0, 2, "Data Sent");
           u8x8.drawString(0, 4, "Button Released");
           if(LMIC.dataLen) {
               // data received in rx slot after tx
               Serial.print(F("Data Received: "));
               u8x8.drawString(0, 3, "Data Received: ");
               Serial.print(F(" bytes for downlink: 0x"));
               for (int i = 0; i < LMIC.dataLen; i++) {
                   if (LMIC.frame[LMIC.dataBeg + i] < 0x10) {
                   Serial.print(LMIC.frame[LMIC.dataBeg + i], HEX);
           // Schedule next transmission
           // os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
            Serial.println(F("Unknown event"));
            u8x8.drawString(0, 2, "Unknown event");

void ledFLash(int flashes){
    int lastStateLED=digitalRead(ledPin);
    for(int i=0;i<flashes;i++){
        digitalWrite(ledPin, HIGH);
        digitalWrite(ledPin, LOW);
void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    u8x8.drawString(0, 4, "Button Pressed");
    if (LMIC.opmode & OP_TXRXPEND) {
        Serial.println(F("OP_TXRXPEND, not sending"));
        u8x8.drawString(0, 1, "OP_TXRXPEND, not sending");
    } else {
        // Prepare upstream data transmission at the next possible time.
        LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
        Serial.println(F("Packet queued"));
        u8x8.drawString(0, 1, "Packet queued");
    // Next TX is scheduled after TX_COMPLETE event.

void setup() {
    //setup the display
    u8x8.drawString(0, 0, "WGLabz LoRa Test");


    //In/Out Pins
    pinMode(ledPin, OUTPUT);
    pinMode(buttonPin, INPUT_PULLUP);
    digitalWrite(buttonPin, HIGH);

    // LMIC init &RESET

    // Set static session parameters. Instead of dynamically establishing a session
    // by joining the network, precomputed session parameters are be provided.
    #ifdef PROGMEM
    // On AVR, these values are stored in flash and only copied to RAM
    // once. Copy them to a temporary buffer here, LMIC_setSession will
    // copy them into a buffer of its own again.
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
    // If not running an AVR with PROGMEM, just use the arrays directly 
    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);

    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band

    // Disable link check validation
    LMIC.dn2Dr = DR_SF9;
    // Set data rate and transmit power (note: txpow seems to be ignored by the library)

    // In my place we are not using 868 MHz

void loop() {

Related posts

Leave a Comment