Skip to Content

LoRaWAN with Thinknode G1 Gateway

LoRaWAN with Thinknode G1 Gateway

In this post I will show you how to send data using LoRaWAN with the Thinknode G1 Gateway. LoRa (Long Range) is a wireless method to send small amounts of data over much longer distances (several kilometers) that can be achieved with WiFi or Bluetooth, for instance.

LoRaWAN is a transmission protocol that uses LoRa. It allows to build complex IoT systems with secure communication between devices and cloud applications. If you want to monitor sensors over the internet, LoRaWAN is what you need.

The End Devices (sensors) send their data to a Gateway, which forwards them to a Network Server. An Application Server can then access the Network Server to process the data, e.g. displaying a graph with temperature data.

LoRaWAN Architecture
LoRaWAN Architecture (source)

Note that the Sensors transmit data to the Gateway via LoRa, while the Gateway communicates with the Network Server via a Wi-Fi, Ethernet or Cellular connection. The Gateway is essentially a bridge between LoRa and the internet.

In the following sections we will use an ESP32 with an SX1276 LoRa Module to transmit environmental data such as temperature and humidity measured by a BME280 via LoRaWAN over a Thinknode G1 Gateway.

We will connect this Gateway to a Network Server on the The Things Network, where you then can monitor the environmental data or access it from an Internet Application for further processing.

Let’s get started!

Required Parts

I used an ESP32 Lite as microprocessor for the LoRa node but any other ESP32 will work just fine as well. If you want to use an Arduino or other microprocessor, you will need a board that runs on 3.3V.

As for the SX1276 LoRa transceiver module, watch out what version you buy! Depending on the country the permitted frequencies are different. It is 868MHz in Europe, 915MHz in North America and 433MHz in Asia.

The module description either lists the frequency or has a number such as 868 or 915 in the name. I listed a module with the 868MHz, since I am in Europe. But you can get this module also for the 915MHz band.

Similarly, make sure that the LoRaWAN Gateway you are using can operate in the required frequency band. The ThinkNode G1 Gateway listed below supports the 868MHz and the 915MHz frequency.

For the environmental sensor, I picked the BME280, which can measure temperature, humidity, air pressure and altitude. Make sure to buy the 3.3V version, since we will connect it to the ESP32.

ThinkNode G1 LoRaWAN Gateway

Lora 868/915M Module SX1276

ESP32 lite Lolin32

ESP32 lite

USB data cable

USB Data Cable

BME280

BME280 Sensor

Dupont wire set

Dupont Wire Set

Half_breadboard56a

Breadboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

What is LoRaWAN?

LoRaWAN stands for Long Range Wide Area Network. It is a wireless protocol designed specifically for the Internet of Things (IoT), allowing devices to communicate over long distances — we’re talking several kilometers, even in urban areas — using very little power.

Unlike WiFi or Bluetooth, which are great for short-range, high-bandwidth communication, LoRaWAN is all about range and efficiency. A sensor typically only send small data packets every few minutes or hours but can run on a small battery for years.

It’s ideal for battery-powered devices that send little information such as temperature sensors, GPS trackers, or soil moisture monitors across distances that Wi-Fi or Bluetooth cannot reach. On the other hand, you can’t send images or video over LoRa.

Let’s say you have a temperature sensor in a remote location. There’s no WiFi but you still want to monitor temperature every 15 minutes. What do you do?

You attach a temperature sensor to an ESP32 that is connected to a LoRa module (like the SX1276). It sends the data wirelessly via LoRa to a LoRaWAN gateway, in our case a Thinknode G1. This gateway can listen to signals from multiple sensors kilometers away and forwards their data to the network server. The gateway is essentially a bridge to the web.

You can run your own network server or you can use a pubic one. Here we will use the The Things Network (TTN), a free and open-source LoRaWAN infrastructure. From TTN, the data can be visualized in a dashboard, pushed to a cloud service, or used to trigger other actions—like sending alerts or turning on a fan. And since the Network Server runs in the Cloud, your data can then be processed, visualized, or acted upon from anywhere in the world.

In the next sections we setup the Gateway, connect it to The Things Network and build the LoRa end node with an ESP32 as microcontroller, the SX1276 as LoRa transceiver and the BME280 as environmental sensor.

The ThinkNode G1 Gateway

ThinkNode G1 by Elecrow is a LoRaWAN indoor gateway designed to connect to various network servers. This gateway supports multiple connection methods for configuration such as WiFi, Bluetooth, and Ethernet. It supports 8-channel transmission, and utilizes LoRa wireless technology to achieve long-distance data transmission.

ThinkNode G1 LoRaWAN Gateway (source)

The gateway is equipped with an SX1302 LoRa concentrator and two SX1250 chips, offering 10 programmable parallel demodulation paths. It supports global ISM frequency bands, with a frequency range from 815MHz to 960MHz.

Technical Specification

The following table from the Elecrow product page lists the technical details of the Thinknode G1 Gateway:

ProcessorCPU/SoCMT7628N(MIPS24KEc@580MHz CPU)
System Memory128 MB DDR2
Storage32MB Nor Flash
Configuration softwareWEB
Communication
Wi-FiSupports IEEE 802.11 b/g/n wireless standards, built-in antenna
WiredSupport IEEE 802.3, IEEE 802.3u wired standards, RJ45 (10M / 100M)
Bluetooth-CompatibleBluetooth-compatible dual-mode (BR/EDR+BLE) 5.0 BLE, built-in ceramic antenna
LoRaWANBaseband chipSX1302, using LR1302 baseband module
Channel8 Channels
Node protocolSupports Class A/Class B/Class C
Frequency bandEU868/US915
Sensitivity-125dBm @125KHz/SF7-139dBm @125KHz/SF12
Transmit powerMaximum 26 dBm
Antenna1. Rubber stick antenna (matching), 3dBi gain, impedance 50 ohm;2. External antenna with base (optional), 3dBi gain, impedance 50 ohm;
Reserve InterfaceDC SocketPower input, DC 12V – 2A
EthernetRJ45 (10M / 100 M)
LoRa Antenna SocketRP-SMA female socket
GPS Antenna SocketRP-SMA female socket
Type-C InterfaceUsed for background control, connecting to the background panel configuration information or burning firmware, debugging
Micro SD Card SlotYES
Nano SIM Card SlotYES
Reserve Indicator lightDevice status lightYES
4GYES(Need to customize)
WLANYES
LoRaYES
PWRYES
ButtonReset Button
Size140*140*38mm
Case MaterialABS+PC(Case)、PC frosted (light guide)
Power Input12V-2A
Operating Temperature-20℃~55℃
Storage Temperature-30℃~70℃

For additional information see the ThinkNode G1 Datasheet.

Indicator Lights

The Thinknode G1 has five indicator lights. The four at the front indicate if the gateway has Power, and is connected to LoRa, WLAN or LTE. The photo below shows the top of the G1 with the four indicator lights at the front of the housing:

Indicator lights of the Thinknode G1
Indicator lights of the ThinkNode G1 (source)

The big, y-shaped LED indicator at the top can light in different colors and blink rates. The table below describes the meaning of the different signals:

LED Indicator Description for Thinknode G1
LED Indicator Description for ThinkNode G1 (source)

Connectors

On the back of the Thinknode G1 you will find the connectors for the Power Supply, Ethernet (ETH), USB-C, the LoRa Antenna, slots for a Micro SD and a Nano SIM card:

Connectors of the Thinknode G1
Connectors of the ThinkNode G1 (source)

Also on the back is a button for rebooting or entering configuration mode. The following table describes how to switch between modes:

Button Description for Thinknode G1
Button Description for ThinkNode G1 (source)

Setting up Hardware

First connect the antenna and then the power supply. The power LED should turn green and the top indicator will be red until the gateway is properly configured.

Hardware setup for ThinkNode G1 (source)

For configuration, you can connect to the gateway via an Ethernet cable (ETH) or WIFI. The User Manual of the ThinkNode G1 describes both methods. In the next section I show you how I configured the software of the ThinkNode G1 using the WIFI connection..

Software Configuration

To configure via Wi-Fi, press and hold the Button on the back of the ThinkNode G1 until the LED indicator on the top turns blue (that will take about 5 seconds):

Login to UI

Then scan your Wi-Fi network for a new Access Point named ThinkNode-G1_XXXXXX and connect to it:

Next open your browser, and go to IP address 192.168.1.1. You should see a login screen, where you enter “root” as Username and also as Password:

Connect to Wi-Fi

To connect the Gateway to your Wi-Fi network go to Network -> Wireless

and click on Scan to scan for your Wi-Fi network:

In the list click on your Wi-Fi network and in the follow-up dialog enter the password for it to access it.

Configure LoRaWAN

Next we need to configure the LoRa interface and Frequency. Go to LoRaWAN -> LoRa Gateway

In the following dialog select WIFI as the Lora Interface, and EU868 as Frequency Plan for Europe. In North America you need to select US915. Set the Lora Mode to Packet Forwarder:

(Alternatively, If you want to configure the gateway as Basis Station have a look at the the Webtutorial).

Keep the other parameter as they are but make sure the Server Address is set to eu1.cloud.thethings.network (if you are living in Europe). We will need it to connect the gateway to The Things Network.

Finally press Save & Apply, which will restart the Gateway and if you are lucky that’s it.

However, I had issues and had to enter the DNS Server as well, otherwise my G1 would not connect to The Things Network.

DNS Server

To add or edit the DNS Server go to Network -> Interfaces and press the Edit button for the LAN:

In the dialog click on the Advanced Settings tab and edit the Use custom DNS servers fields:

I added 8.8.8.8 (Google) and 1.1.1.1 (Cloudflare) as DNS servers and finally my gateway connected to The Things Network.

Connecting Gateway to The Things Network

The Things Network (TTN) offers you a public Network Server that allows you to send sensor data from a LoRa device, e.g. temperature, to a website, where you can look at the data or process them with a Web Application.

The first step is to connect our ThinkNode G1 Gateway to the TTN. Go to the following URl: https://eu1.cloud.thethings.network/console/, click on the blue Add button and select Add new gateway:

Enter your Gateway EUI, which is printed on the back of the ThinkNode G1 Gateway or can be found on the Status page of the configuration UI for the gateway. After entering the EUI press Confirm:

This will extend the dialog and you can then enter a unique Gateway ID, e.g. the EUI of the gateway with the prefix ‘eui’ and a Gateway name, I picked Maet-ThinkNode-G1 for this test. You also must select a frequency plan that matches the frequency of your gateway (Europe 863-870 Mhz) for Europe, for instance:

Keep everything else as it is and press Register gateway. If everything works correctly you will see a status page that shows a green Gateway status indicating that the Gateway is connected:

If you get that far, congratulations. The worst should be over ; )

Create an Application

The Things Network (TTN) organizes End Devices (sensors, actuators) in so called “Applications”. Their role is to decode data and optionally forward them to external servers via Webhooks or MQTT.

Applications, Gateways and End Devices interact as follows: The End Device sends data (uplink) via LoRa. Next the Gateway receives the data and forwards it to TTN. Finally the TTN Network Serverroutes the data to a specific Application using the device’s DevEUI.

[End Device] ←→ [Gateway] ←→ [TTN Network Server] ←→ [Application]

So, before we can receive and monitor sensor data on TTN, we need to create an Applications. For that go to https://eu1.cloud.thethings.network/console/applications/add, which will open the following dialog:

There enter an Application ID, e.g. env-sensor-network and an Application name, e.g. Environmental Sensors as shown above.

Register End Device

Next we are going to add and register our End Device. On the page for your application (https://eu1.cloud.thethings.network/console/applications/env-sensor-network) you should see a box with a blue button labelled “+ Register end device”:

Press this button and you get the following dialog:

Enter the data for your End Device, most importantly the Frequency Plan, the LoRaWAN version and the JoinEUI. The JoinEUI (formerly called AppEUI) is a number with the following format: 70B3D57EDxxxxxxx. Replace the ‘xxxxxxx’ with a hex number to create a unique JoinEUI for your End Device, e.g. 70B3D57ED0000001.

Once done click the Confirm button on the right, which will extend the dialog and allows you to generate a DevEUI and a AppKey. Finally, you need to give your End Device a unique ID:

Press Register end device and then you should see the info for the device you created:

The most important pieces of information there are the AppEUI, DevEUI and AppKey. You will need these credentials in the code for the End Device. It enables the End Device to authenticate itself at The Things Network.

Building a LoRaWAN End Device with SX1276 and ESP32

You can buy many pre-build LoRaWAN devices with all kinds of sensors and functions but they are expensive. For an overview see the Device Repository for LoRaWAN of The Things Network. In this tutorial we are going to build our own end device, which isn’t difficult and much cheaper ; )

We will use an ESP32 as microcontroller and a SX1276 transceiver module to transmit data via LoRa. If you need more background information on the SX1276 see the Long range communication with LoRa SX1276 and ESP32 tutorial. In the following, I keep it short and just show you how to connect the SX1276 to the ESP32. Here is the connection table:

SX1276ESP32
MOSI23
MSIO19
SCK18
RST17
NSS5
DIO04
DIO116
GNDGND
VCC3.3V

Make sure that you connect VCC to the 3.3V output pin of your ESP32. Apart from the SPI interface (MOSI, MISO, SCK, RST, NSS) the digital IO connections on pins DIO0 and DIO1 are also essential. The picture below shows the complete wiring diagram:

Connecting SX1276 to ESP32
Connecting SX1276 to ESP32

As mentioned before, you must use an SX1276 transceiver module that operates on the LoRa frequency that is permitted in your country. In the wiring diagram above you can see that the SX1276 is marked as 868M, meaning that it uses the 868MHz band for Europe. For North America, you will need a 915MHz module.

The frequency of the SX1276, the Gateway and the settings on The Things Network all must match! In my case they are all set to 868MHz.

Sending Test Data via LoRaWAN

Now let’s write the code to send some test data from our End Device (ESP32 + SX1276) via LoRaWAN to the TTN.

But first you will need to install a library for LoRaWAN and we are going to use the MCCI LoRaWAN LMIC library. Just open the LIBRARY MANAGER, search for “mcci lorawan lmic library” and press INSTALL. The picture below shows a successful installation:


Installing MCCI LoRaWAN LMIC library
Installing MCCI LoRaWAN LMIC library

Note LoRaWAN defines three device classes: Class A, Class B, and Class C, each offering different trade-offs between power consumption and downlink availability. Class A is the default and most energy-efficient mode, where a device can only receive downlink messages in two short windows after it transmits an uplink. This class is ideal for battery-powered sensors and is fully supported by the LMIC library.

Class B adds scheduled receive windows using periodic beacons sent by the gateway, allowing more predictable downlink access at the cost of higher power consumption. Class C devices keep their receive windows open nearly all the time, enabling low-latency downlink communication but requiring constant power (e.g., mains-powered devices). However, the LMIC library supports only Class A.

Library Configuration

Next we need to configure the LMIC library for the LoRa frequency, version and chip we are using. The LMIC library contains a file lmic_project_config.h that you typically will find under the following Windows path:

...\OneDrive\Documents\Arduino\libraries\MCCI_LoRaWAN_LMIC_library\project_config

Open this file and edit its contents as shown below:

#define CFG_eu868 1
//#define CFG_us915 1
//#define CFG_au915 1
//#define CFG_as923 1
//#define CFG_kr920 1
//#define CFG_in866 1

#define CFG_sx1276_radio 1
//#define CFG_sx1261_radio 1
//#define CFG_sx1262_radio 1

//#define ARDUINO_heltec_wifi_lora_32_V3
//#define LMIC_USE_INTERRUPTS

#define LMIC_LORAWAN_SPEC_VERSION    LMIC_LORAWAN_SPEC_VERSION_1_0_3

Again, if you are not living in Europe you will need to pick the LoRa frequency for your country (Europe -> CFG_eu868, North America -> CFG_us915, …). Make sure to comment out all other frequencies.

We are using the SX1276 chip, so we also define CFG_sx1276_radio 1. Should you use a different LoRa chip, you have to change this here as well.

Code for sending data with LoRaWAN

The following code sends every minute a counter value as a string, e.g. “Counter = 5” from our End Device (ESP32 + SX1276) via the ThinkNode Gateway to TTN, where we can inspect it. More about that later. For now just have a quick look at the code before we discuss its details:

#include <lmic.h>
#include <hal/hal.h>


static const u1_t PROGMEM APPEUI[8] = { 0x01, 0x00, 0x00, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 };
void os_getArtEui(u1_t* buf) {
  memcpy_P(buf, APPEUI, 8);
}

static const u1_t PROGMEM DEVEUI[8] = { 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28};
void os_getDevEui(u1_t* buf) {
  memcpy_P(buf, DEVEUI, 8);
}

static const u1_t PROGMEM APPKEY[16] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
                                         0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };
void os_getDevKey(u1_t* buf) {
  memcpy_P(buf, APPKEY, 16);
}

static osjob_t sendjob;

const unsigned TX_INTERVAL = 60;  // Send every minute

const lmic_pinmap lmic_pins = {
  .nss = 5,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 17,
  .dio = { 4, 16, LMIC_UNUSED_PIN }
};


void onEvent(ev_t ev) {
  Serial.printf("%d: ", os_getTime());
  switch (ev) {
    case EV_JOINING:
      Serial.println("EV_JOINING");
      break;
    case EV_JOINED:
      Serial.println("EV_JOINED");
      LMIC_setLinkCheckMode(0);
      break;
    case EV_JOIN_FAILED:
      Serial.println("EV_JOIN_FAILED");
      break;
    case EV_TXCOMPLETE:
      Serial.println("EV_TXCOMPLETE");
      if (LMIC.dataLen) {
        Serial.printf("> Downlink %d bytes\n", LMIC.dataLen);
        Serial.print("> Data: ");
        for (int i = 0; i < LMIC.dataLen; i++) {
          Serial.printf("%02X ", LMIC.frame[LMIC.dataBeg + i]);
        }
        Serial.println();
      }
      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      break;
    case EV_TXSTART:
      Serial.println(F("EV_TXSTART"));
      break;
    case EV_TXCANCELED:
      Serial.println("EV_TXCANCELED");
      break;
    case EV_JOIN_TXCOMPLETE:
      Serial.println("EV_JOIN_TXCOMPLETE: no JoinAccept");
      break;
    default:
      Serial.print("Unknown event: ");
      Serial.println((unsigned)ev);
      break;
  }
}

void do_send(osjob_t* j) {
  static uint16_t counter = 0;
  static char mydata[25];

  if (LMIC.opmode & OP_TXRXPEND) {
    Serial.println(F("OP_TXRXPEND, not sending"));
  } else {
    snprintf(mydata, sizeof(mydata), "Counter = %u", counter);
    LMIC_setTxData2(1, (uint8_t*)mydata, strlen(mydata), 0);
    Serial.printf("> Data: %s\n", mydata);
    counter++;
  }
}

void setup() {
  Serial.begin(115200);
  os_init_ex(&lmic_pins);
  LMIC_reset();
  do_send(&sendjob);
}

void loop() {
  os_runloop_once();
}

Libraries

The code begins by including the required LMIC libraries:

#include <lmic.h>
#include <hal/hal.h>

These libraries give us everything we need to talk to the SX1276 chip and manage LoRaWAN functionality. The lmic.h header handles protocol logic, while hal/hal.h connects LMIC with the hardware (in this case, our ESP32 and SX1276).

Authentication

LoRaWAN authentication begins with three critical keys: APPEUI, DEVEUI, and APPKEY. These are device credentials used to join a LoRaWAN network using OTAA (Over-The-Air Activation). You entered/generated these keys when you created the End Device in TTN and you can see them in the Device Information:

However, the LMIC library expects the bytes for APPEUI and DEVEUI in LSB-first (Least Significant Byte first) order, which means you have to reverse the bytes for APPEUI and DEVEUI in the code. For instance, if the AppEUI in the Device Info on TTN is 70 B3 D5 7E D0 00 00 01 as shown above, in the code it becomes:

APPEUI[8] = { 0x01, 0x00, 0x00, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 };

The same is valid for DEVUI but not for the APPKEY – it remains in the same byte order!

Key Constants

The code creates constant arrays for these authentication keys in the correct byte order and then calls a function that copies the key into a buffer that LMIC uses during the join process. These functions ensure that your ESP32 can securely authenticate with your LoRaWAN network server, such as The Things Network.

Below the constant for the APPEUI with the “copy” function os_getArtEui():

static const u1_t PROGMEM APPEUI[8] = { 0x01, 0x00, 0x00, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 };
void os_getArtEui(u1_t* buf) {
  memcpy_P(buf, APPEUI, 8);
}

The PROGMEM directive stores the data in flash memory to save RAM. Similar logic applies to the device EUI and application key. Just remember that APPEUI and DEVEUI are in reversed byte order but not APPKEY:

static const u1_t PROGMEM DEVEUI[8] = { 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28};
void os_getDevEui(u1_t* buf) {
  memcpy_P(buf, DEVEUI, 8);
}

static const u1_t PROGMEM APPKEY[16] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
                                         0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };
void os_getDevKey(u1_t* buf) {
  memcpy_P(buf, APPKEY, 16);
}

Obviously, you will have to use the APPEUI, DEVEUI and APPKEY values for your TTN Application and End Device!

Constants

Next, the code defines a global job object and a transmission interval.

static osjob_t sendjob;
const unsigned TX_INTERVAL = 60;  // Send every minute

The sendjob object holds the task scheduled for later execution, while TX_INTERVAL sets how often you want to transmit data—in this case, once every 60 seconds.

Pin Mapping

Now we define the pin mapping between the ESP32 and the SX1276 LoRa module:

const lmic_pinmap lmic_pins = {
  .nss = 5,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 17,
  .dio = { 4, 16, LMIC_UNUSED_PIN }
};

This structure tells LMIC which GPIO pins connect to the SX1276’s control lines. For example, nss is the SPI chip-select pin, rst is Reset, and dio maps to the interrupt pins used by the radio.

onEvent

The onEvent() function handles various LoRaWAN events—everything from join requests to transmission completions.

void onEvent(ev_t ev) {
  Serial.printf("%d: ", os_getTime());
  switch (ev) {
    case EV_JOINING:
      Serial.println("EV_JOINING");
      break;
    case EV_JOINED:
      Serial.println("EV_JOINED");
      LMIC_setLinkCheckMode(0);
      break;
    ...
  }
}

This function helps you monitor what your device is doing. For instance, EV_JOINED means the device successfully connected to the network, and we turn off link check mode with LMIC_setLinkCheckMode(0) to save airtime. When EV_TXCOMPLETE triggers, the device has finished sending data, and we queue the next transmission:

    case EV_TXCOMPLETE:
      Serial.println("EV_TXCOMPLETE");
      if (LMIC.dataLen) {
        Serial.printf("> Downlink %d bytes\n", LMIC.dataLen);
        Serial.print("> Data: ");
        for (int i = 0; i < LMIC.dataLen; i++) {
          Serial.printf("%02X ", LMIC.frame[LMIC.dataBeg + i]);
        }
        Serial.println();
      }
      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      break;

The EV_TXCOMPLETE case also checks if the LoRa module received data (downlink). If there is a downlink (LMIC.dataLen > 0) the code prints the bytes of the data as hexadecimal numbers.

You can send data from TTN by clicking on the Messaging tab of your End Device:

do_send

The core transmission logic happens inside the do_send() function.

void do_send(osjob_t* j) {
  static uint16_t counter = 0;
  static char mydata[25];

  if (LMIC.opmode & OP_TXRXPEND) {
    Serial.println(F("OP_TXRXPEND, not sending"));
  } else {
    snprintf(mydata, sizeof(mydata), "Counter = %u", counter);
    LMIC_setTxData2(1, (uint8_t*)mydata, strlen(mydata), 0);
    Serial.printf("> Data: %s\n", mydata);
    counter++;
  }
}

This function builds a string like "Counter = 23" and queues it for transmission. It first checks if another transmission is still pending — LoRaWAN is strict about timing. If it’s clear, it sends the packet on port 1 with no confirmation (the last argument 0 means unconfirmed uplink).

setup

The setup() function kicks everything off:

void setup() {
  Serial.begin(115200);
  os_init_ex(&lmic_pins);
  LMIC_reset();
  do_send(&sendjob);
}

It initializes the serial port for debugging, sets up the LMIC stack with the correct pin mapping, resets LMIC’s internal state, and queues the first transmission.

loop

Finally, the loop() function keeps the LMIC runtime ticking.

void loop() {
  os_runloop_once();
}

Unlike traditional Arduino loops, LMIC requires this call inside loop() to keep the job scheduler and event system running properly.

Serial Monitor

If you upload the code and open the Serial Monitor you should see the following messages appearing:

Uplink Messages

With this setup, your ESP32 and SX1276 will send a counter string over LoRaWAN every minute. If you go to the TTN you can choose to look at the uplink data messages sent from our End Device. Select Applications and click on Live data:

On the right side you will then see the timestamped message sent from our ESP32:

Payload Formatter

However, it doesn’t show the actual data of the message, our counter value. This is because TTN does not know how to decode the data in the message. You will need to specify or implement a Payload Formatter to be able to see the data.

Open the Sidebar, select Application, click Payload formatters and select Uplink:

You can then select existing Payload Formatter or implement your own. We will implement our own and therefore select “Custom Javascript formatter” as Formatter type:

For “Formatter code” enter the following code, which simply converts the bytes we are sending back to characters:

function decodeUplink(input) {
  let str = "";
  for (let i = 0; i < input.bytes.length; i++) {
    str += String.fromCharCode(input.bytes[i]);
  }
  return { data: { message: str } };
}

If you now look at the uplink messages again you will find a field Payload at the end, which shows the string with the counter value the End Device sends:

In the next section we add an environmental sensor to the ESP32 and change the code to send temperature, humidity and air pressure data via LoRaWAN to TTN.

Sending Environmental Data via LoRaWAN

A classical application for LoRaWAN is to send environmental data such as temperature, humidity and air pressure from a LoRa End Device to TTN. In this section, we are going to add the BME280 sensor to our circuit and extend the code to send the data measured by the sensor.

If you want more information on the BME280 see the How To Use BME280 Pressure Sensor or the Temperature Plotter on e-Paper Display tutorials.

Connecting BME280 to ESP32

The BME280 sensor has an I2C interface and is easy to add to the circuit. The following table shows all the connections you have to make, including the existing ones for the SX1276:

SX1276BME280ESP32
MOSI23
MSIO19
SCK18
RST17
NSS5
DIO04
DIO116
SCL25
SDA33
GNDGNDGND
VCCVCC3.3V

And here the complete wiring between BME280, SX1276 and the ESP32:

Wiring of BME280, SX1276 and ESP32
Wiring of BME280, SX1276 and ESP32

Code for Sending Environmental Data via LoRaWAN

Next we are going to install two libraries: The Adafruit_BME280 to read data from the BME280:

Adafruit_BME280 library in Library Manager
Adafruit_BME280 library in Library Manager

and the CayenneLPP library, which formats sensor data for LoRaWAN transmissions:

CayenneLPP library in Library Manager
CayenneLPP library in Library Manager

The following code is a simple change and extension of the previous code. Instead of sending a counter, we are sending temperature, humidity and air pressure measurements from the BME280 sensor. Have a quick look and then we will discuss the differences to the previous code:

#include <lmic.h>
#include <hal/hal.h>
#include <CayenneLPP.h>
#include <Adafruit_BME280.h>


static const u1_t PROGMEM APPEUI[8] = { 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };
void os_getArtEui(u1_t* buf) {
  memcpy_P(buf, APPEUI, 8);
}

static const u1_t PROGMEM DEVEUI[8] = { 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28};
void os_getDevEui(u1_t* buf) {
  memcpy_P(buf, DEVEUI, 8);
}

static const u1_t PROGMEM APPKEY[16] = { 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
                                         0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18 };
void os_getDevKey(u1_t* buf) {
  memcpy_P(buf, APPKEY, 16);
}

static osjob_t sendjob;

const unsigned TX_INTERVAL = 60;  // Send every minute

const lmic_pinmap lmic_pins = {
  .nss = 5,
  .rxtx = LMIC_UNUSED_PIN,
  .rst = 17,
  .dio = { 4, 16, LMIC_UNUSED_PIN }
};

CayenneLPP lpp(16);
Adafruit_BME280 bme;

void initBMESensor() {
  Wire.begin(33, 25);  // Software I2C for BME280
  bme.begin(0x76, &Wire);
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1,  // temperature
                  Adafruit_BME280::SAMPLING_X1,  // pressure
                  Adafruit_BME280::SAMPLING_X1,  // humidity
                  Adafruit_BME280::FILTER_OFF);
}

void onEvent(ev_t ev) {
  Serial.printf("%d: ", os_getTime());
  switch (ev) {
    case EV_JOINING:
      Serial.println("EV_JOINING");
      break;
    case EV_JOINED:
      Serial.println("EV_JOINED");
      LMIC_setLinkCheckMode(0);
      break;
    case EV_JOIN_FAILED:
      Serial.println("EV_JOIN_FAILED");
      break;
    case EV_TXCOMPLETE:
      Serial.println("EV_TXCOMPLETE");
      if (LMIC.dataLen) {
        Serial.printf("> Downlink %d bytes\n", LMIC.dataLen);
        Serial.print("> Data: ");
        for (int i = 0; i < LMIC.dataLen; i++) {
          Serial.printf("%02X ", LMIC.frame[LMIC.dataBeg + i]);
        }
        Serial.println();
      }
      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      break;
    case EV_TXSTART:
      Serial.println(F("EV_TXSTART"));
      break;
    case EV_TXCANCELED:
      Serial.println("EV_TXCANCELED");
      break;
    case EV_JOIN_TXCOMPLETE:
      Serial.println("EV_JOIN_TXCOMPLETE: no JoinAccept");
      break;
    default:
      Serial.print("Unknown event: ");
      Serial.println((unsigned)ev);
      break;
  }
}

void do_send(osjob_t* j) {
  bme.takeForcedMeasurement();
  float temp = bme.readTemperature();
  float hum = bme.readHumidity();
  float pres = bme.readPressure() / 100.0;

  lpp.reset();
  lpp.addTemperature(1, temp);  // channel 1
  lpp.addRelativeHumidity(2, hum);   // channel 2
  lpp.addBarometricPressure(3, pres);   // channel 3

  if (LMIC.opmode & OP_TXRXPEND) {
    Serial.println(F("OP_TXRXPEND, not sending"));
  } else {
    LMIC_setTxData2(1, lpp.getBuffer(), lpp.getSize(), 0);
    Serial.printf("> Temperature: %.2f\n", temp);
    Serial.printf("> Humidity: %.2f\n", hum);
    Serial.printf("> Pressure: %.2f\n", pres);
  }
}

void setup() {
  Serial.begin(115200);
  initBMESensor();
  os_init_ex(&lmic_pins);
  LMIC_reset();
  do_send(&sendjob);
}

void loop() {
  os_runloop_once();
}

The code uses the same keys for APPEUI, DEVEUI and APPKEY as before. The main extension is a function the initializes the BME280 sensor and a change to the function that sends the data.

initBMESensor

The initBMESensor() function initializes the BME280 using software I2C. Instead of the ESP32’s default I2C pins, it specifies GPIO 33 for SDA and GPIO 25 for SCL:

void initBMESensor() {
  Wire.begin(33, 25);  // Software I2C for BME280
  bme.begin(0x76, &Wire);
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1,  // temperature
                  Adafruit_BME280::SAMPLING_X1,  // pressure
                  Adafruit_BME280::SAMPLING_X1,  // humidity
                  Adafruit_BME280::FILTER_OFF);
}

Note that I am using the I2C address 0x76, when configuring the sensor via bme.begin(0x76, &Wire). Your sensor may have a different I2C address. For instance, 0x77 is also common for the BME280.

The sensor is configured in “forced mode”, meaning it only measures when explicitly told to do so. This approach saves power. Sampling is set to the minimum (1x) for temperature, pressure, and humidity, and the onboard filter is disabled.

do_send

The do_send() function first gets temperature, humidity and air pressure measurements from the BME280 sensor

void do_send(osjob_t* j) {
  bme.takeForcedMeasurement();
  float temp = bme.readTemperature();
  float hum = bme.readHumidity();
  float pres = bme.readPressure() / 100.0;

It then formats the data for the LoRaWAN transmission by adding temperature, humidity and air pressure as different channels to the data packet:

  lpp.reset();
  lpp.addTemperature(1, temp);  // channel 1
  lpp.addRelativeHumidity(2, hum);   // channel 2
  lpp.addBarometricPressure(3, pres);   // channel 3

Finally, the data is sent as the usual packet of bytes. In addition the code prints the data to the Serial Monitor:

  if (LMIC.opmode & OP_TXRXPEND) {
    Serial.println(F("OP_TXRXPEND, not sending"));
  } else {
    LMIC_setTxData2(1, lpp.getBuffer(), lpp.getSize(), 0);
    Serial.printf("> Temperature: %.2f\n", temp);
    Serial.printf("> Humidity: %.2f\n", hum);
    Serial.printf("> Pressure: %.2f\n", pres);
  }
}

For more information have a look at the Send Environmental Data with LoRa tutorial that does something similar but uses raw LoRa instead of LoRaWAN.

Serial Monitor

If you upload and run the code you should see the following output on your Serial Monitor:

Payload Formatter

If you want to inspect the data on TTN you will need to specify a Payload Formatter as before. However, in this case it is simpler. We don’t need to write our own Custom Javascript Formatter but can use the CayenneLPP formatter:

In the Device Overview you can then see the nicely formatted environmental data sent in the three channels:

And that’s it. Now you know how to build your own LoRaWAN End Device to send environmental data over the ThinkNode G1 Gateway to TTN.

Conclusions

This article introduced the Thinknode G1 Gateway. A LoRaWAN gateway essentially allows you to send data from LoRa sensors to the internet. We built our on LoRa sensor with an ESP32, SX1276 and BME280 sensor and used it to send temperature, humidity and air pressure to The Things Network.

We barely scratched the surface of LoRaWAN and I encourage you to browse the information on www.thethingsnetwork.org/docs/lorawan to learn more. For more guidance on how to configure the ThinkNode G1 see the Manual and the Webtutorial.

If you just want to transmit data between two LoRa devices, LoRaWAN is likely overkill and you better off with raw LoRa. Have a look at the Long range communication with LoRa SX1276 and ESP32 and the Send Environmental Data with LoRa.

Finally, if you prefer an Arduino over the ESP32 you either need to use a logic level converter or an Arduino that operates on 3.3 Volts, since the SX1276 module does not work with 5V! Have a look at the Interface Arduino Uno with ST7735 TFT using Level Shifter tutorial for more information.

If you have any questions feel free to leave them in the comment section.

Happy Tinkering ; )

Links

Some links I found useful when writing this article: