Win $20,000. Help build the future of education. Answer the call. Learn more

Archived | Integrating LPWAN networking and edge computing into your IoT solutions

Archived content

Archive date: 2021-06-09

This content is no longer being updated or maintained. The content is provided “as is.” Given the rapid evolution of technology, some content, steps, or illustrations may have changed.

Many large-scale IoT solutions require the use of long-range wireless networking to communicate among the sensors, gateways, the cloud, and any IoT apps that make up the IoT solution. These large-scale IoT solutions take advantage of edge computing (or fog computing, as some people call it).

In this article, you learn how to:

  • Combine NodeMCU boards with DHT sensors
  • Implement LPWAN (low power, wide area networking) networking (specifically, the LoRa protocol)
  • Use an IoT platform to monitor temperature and humidity sensors

The exact application is to monitor barns with hay bales for the risk of fire. Hay is extremely important to agriculture as animal feed. However, hay storage can be dangerous. If hay is not kept at a very low humidity, it becomes a breeding ground for bacteria. Living things, such as bacteria, produce heat. When hay gets hot enough, it combusts.

What you’ll need to build your application


In this article, you use the Incremental Build Model. To begin with, you create a solution for short-range access, based on the assumption that you have a wifi access point that is within range of the hay barn. Once you have that working, you add long-range networking, adding in LPWAN-enabled devices to communicate between the hay barn and the farmhouse. In both cases, the sensors are NodeMCU devices (DHT11 sensors that are attached to NodeMCU boards) that send information over wifi or LPWAN to the IBM Watson IoT Platform.

The first architecture allows you to test the reporting functionality without worrying about LPWAN. Then, in the second architecture, you focus on sending the readings a long distance.

Figure 1 shows the short-range architecture. The sensors (made out of a NodeMCU controller and a DHT11 sensor component) communicate with an access point in the farmhouse by using wifi. This access point is connected to the internet and routes packets back and forth. The sensors use HTTP to send the sensor readings (temperature and humidity) to the Watson IoT Platform. You can then use Watson IoT Platform to analyze these sensor readings. By using Watson IoT Platform, you can identify when the humidity is too high and needs to be reduced to avoid bacteria growth or when the temperature is too high and there is a risk of the hay catching on fire.

Figure 1: The short-range architecture that uses wifi

Short-range architecture

Figure 2 shows how to reach much longer ranges. The key is the addition of the two ESP32 controllers (shown in purple).

The barn ESP32 acts as a gateway for the sensor readings. It is the wifi access point that is used by the sensors, and it receives readings from the sensors by using HTTP over wifi. It then transmits those readings by using a LPWAN networking protocol called LoRa.

This transmission is received by the farmhouse ESP32, which might be a mile away. The farmhouse ESP32 then uses the farmhouse wifi access point to connect with the Watson IoT Platform and send the sensor reading, by using its HTTP API.

Figure 2: The long-range architecture

Long range architecture

The source code

I follow an iterative development model. At the end of this development process, we need three programs:

  • A Lua program for a NodeMCU board to run the sensor
  • A C program for the barn ESP32 to send sensor readings by using LoRa
  • A C program for the farmhouse ESP32 to receive sensor readings from LoRa and send them to the IBM Watson IoT Platform.

Each of these programs is developed in stages in this tutorial.

NodeMCU Lua sensor program

This program has three stages with source code available in my Hay_Bale_Fire GitHub repo:

  1. The 01_readDHT11.lua program reads the sensor and writes the result to the serial port. This simple program allows you to verify that you connected the circuit correctly.
  2. The 02_Update_IoT.lua program uses HTTP to write sensor readings to the IBM Watson IoT Platform. If you are using the short-range architecture, use this version of the Lua sensor program.
  3. The 06_Final_Sensor.lua program uses HTTP to connect to the barn ESP32. If you are using the long-range architecture, use this version of the Lua sensor program.

Barn ESP32 C program

This program also has three stages with source code in my Hay_Bale_Fire GitHub repo:

  1. The 03_ESP32_Blink_LED.ino program just turns an LED on and off. Its purpose is to verify that the board and development environment work as expected.
  2. The 04_LoRa_Send.ino program sends LoRa packets. You can use this program, and the 05_LoRa_Receive.ino program, to verify that LoRa works with the antennas that you connect at the distance you need.
  3. The 07_Final_Barn_ESP32.ino program is the full program that receives readings from the NodeMCU sensor and sends them to the farmhouse ESP32.

Farmhouse ESP32 C program

This program has two stages with source code in my Hay_Bale_Fire GitHub repo. For the initial “sanity check,” you can use the same 03_ESP32_Blink_LED.ino program that you use for the barn ESP32.

  1. The 05_LoRa_Receive.ino program receives packets and writes them to the serial port. It also flashes the LED for half a second for each packet, so you can take the ESP32 board with a USB battery and see at what range it stops receiving those packets.
  2. The 08_Final_Farmhouse_ESP32.ino program is the full program that receives readings from the barn ESP32 through LoRa and sends them through the internet to the IBM Watson IoT Platform.

Configure short-range architecture with just wifi networking

We start with the simpler architecture, with the sensors by using an internet access point to report their results.

Configure the sensors

  1. Follow the steps to configure NodeMCU as explained in section 1 of this article. There are two differences:

    • In Step 2, in addition to the firmware modules specified there, select DHT and encoder. If you are using an older NodeMCU board, choose instead of the default.
    • In Step 4, download the floating-point version. In this article, we deal with continuous values, so it is best to have more than just integers.
  2. Click Chip ID , and then paste the value into a text window. You will need this value later.
  3. Install the DHT11 sensor on the breadboard. Looking in page 3 (counting the cover) of the data sheet for the sensor, you see that pin #1 is the power supply, pin #2 the data, and pin #4 the ground. On page 2 of the data sheet, you see that when you look on the side with the holes, the pins are numbered left to right, so the right-most goes to the power supply, and so on. Connect the wires appropriately, as shown in NodeMCU board connected to a DHT11 sensor. I decided to use D1 to communicate with the DHT11 module. You can use other pins, but not D0.
NodeMCU board connected to a DHT11 sensor


  1. Run the 01_readDHT11.lua program to see the relative humidity and temperature where you are located.

Let’s walk through the 01_readDHT11.lua program.

The library function that we use is dht.read11. It returns multiple values, which Lua supports. The first value is the status. When you use floating point firmware, the temperature and humidity are provided in the second and third values respectively.

status, temp, humi = dht.read11(1)

If the status is dht.ERROR_TIMEOUT (-2), there is probably a connection error.

if status == dht.ERROR_TIMEOUT then
   print("DHT timed out, is it connected correctly?")

The other two values are dht.ERROR_ CHECKSUM (-1) and dht. OK (0). I don’t know why it happens, but I often get the checksum error with perfectly valid values – so I assume it cannot be relied upon.

 if status == dht.ERROR_CHECKSUM then
     print("Checksum error, results probably valid")

The Lua string.format function uses the same format as C’s printf. This code shows the format for a floating point number.

print(string.format("Relative humidify:%2.1f%%", humi));

The DHT11 is a Chinese product, so it uses the metric system. However, we are using an article that is written in the US and uses Fahrenheit. So we make this conversion to have both values.

   print(string.format("Temp: %2.1fC = %3.1fF", temp, temp*9/5+32))

Configure the Internet of Things Platform service

The Internet of Things Platform service is going to be the hub of the IoT application.

Create the IoT service in IBM Cloud

  1. Log in to your IBM Cloud account.
  2. Search for Internet of Things Platform and click the Internet of Things Platform (not the starter).
  3. Name the service Hay Bale Monitor and click Create.
  4. Click Launch. The IBM Watson IoT Platform dashboard opens.

Add your device types and devices

  1. In the Watson IoT Platform dashboard, hover over the left sidebar, and click DEVICES. (it may be already selected).
  2. Click the Device Types tab.
  3. Click Add Device Type.
  4. Name the new device type Hay-Sensor. The default type, Device, is correct.
  5. Click Next , and then click Finish.
  6. Click Register Devices to register your sensor.
  7. In the Select Existing Device Type field, choose the device type Hay-Sensor.
  8. Type the chip ID of the NodeMCU, which you got when you set up the hardware earlier, as the device ID, and then click Next.
  9. Enter a descriptive location, such as On my desk, and click Next.
  10. Click Next again, and then click Finish.
  11. Make a note of the authentication token from the Device Credentials section. You need this value later to send messages from your sensor to the Watson IoT Platform.


Configure the security for the IoT application

  1. In the Watson IoT Platform dashboard, hover over the left sidebar, and click SECURITY.
  2. Click the pencil icon for Connection Security.
  3. Change the security level of the default scope to TLS Optional. In our use case, we do not anticipate anybody will try to produce fake results.
  4. Click OK.

Send sensor data to the IoT Platform

Now that you configured Watson IoT Platform, the next step is to send the sensor data – the humidity and temperature readings – to the platform.

  1. Edit the 02_Update_IoT.lua program and update it with your wifi and Watson IoT Platform settings.
  2. Return to the Esplorer user interface, and enter the 02_Update_IoT.lua program on your device to start sending sensor readings to the Watson IoT Platform.
  3. Run the program.
  4. Wait a minute for the board to send its sensor data to the Watson IoT Platform.
  5. In the Watson IoT Platform dashboard, from the left sidebar, click DEVICES, and then click the Browse tab.
  6. Click your device ID, and then click State. The results should be similar to Sensor results in the Watson IoT Platform dashboard.
Sensor results in the Watson IoT Platform dashboard


Let’s walk through the Update_IoT Lua program.

The NodeMCU uses HTTPS to send the information to the Watson IoT Platform.

The configuration values are at the top of the program to make them easy to change. The wifi configuration is in lines 5-9:

-- WiFi configuration
wifiConf = {
    ssid = "Ori",
    passwd = "<<<redacted>>>"

It is followed by configuration values for the Watson IoT Platform credentials. To get the OrgID, click Settings on the left sidebar. The token ID is the token ID you got when you registered the device.

-- IoT Platform coniguration
iotPlatformCred = {
    orgID = "<<<redacted>>>",
    devType = "Hay-Sensor",
    devID = node.chipid(),
    authMethod = "use-token-auth",
    authToken = "<<<redacted>>>"

From these configuration values, we create the values we use in the HTTPS request later. First, the URL.

hostname = string.format("",
url = string.format("https://%s:8883/api/v0002/device/types/%s/devices/%d/events/sensorReading",
   hostname, iotPlatformCred.devType, iotPlatformCred.devID)

And then the HTTP headers. We need two headers:

  • Content-Type, the data format (application/json for the JSON format).
  • Authorization, which we use to authenticate ourselves to the server. It uses HTTP basic authentication.

  httpHeaders =
      "Content-Type: application/json\r\n" ..
      "Authorization: Basic " ..
          encoder.toBase64(iotPlatformCred.authMethod .. ":" ..
                           iotPlatformCred.authToken) .. "\r\n"

This code is where we actually connect to wifi as a station (as opposed to an access point). You can read more about this process in the NodeMCU documentation.

  -- Actually connect
    ssid = wifiConf.ssid,
    pwd = wifiConf.passwd,

This function sends a string message to the Watson IoT Platform.

  function httpSend(jsonMsg)

To send the message, the program uses the method.The first three parameters are self-explanatory. The final parameter is a callback function that tells us whether the request was successful or not., httpHeaders, jsonMsg,

This callback function just prints the code and response. We could create more sophisticated error handling, but considering we send a reading every minute, and the article suggests taking them twice a day, it isn’t a major problem if a few readings get lost.

        function(code, data)
            print(code, data)
        end  -- end of callback function
    )        -- end of call
end          -- end of function httpSend()

This function creates the JSON, and then sends it. For debugging purposes, it also sends the result to standard output (the UART used to communicate with the computer over USB).

function sendResult(temp, humidity)
    print(string.format("Temp: %2.1fC, humidity %2.1f%%",
        temp, humidity))

The JSON standard requires field names to be enclosed in quotation marks. We could use \" for the quotation marks, but we can also use [[ and ]] as our string delimiters. Such strings may also encompass multiple lines.

You can read more about Lua string literals here.

    jsonMsg = [[{"temp": ]] .. temp ..
        [[, "humidity": ]] .. humidity .. [[}


The readSensor function reads the sensor and sends the result (if the reading is successful).

function readSensor()
    status, temp, humidity = dht.read11(dht11pin)

    if (status ~= dht.ERROR_TIMEOUT) then
        sendResult(temp, humidity)

To run the same function repeatedly, we use NodeMCU’s tmr (timer) module.

sensorTimer = tmr.create()
sensorTimer:register(updateFreq * 1000, tmr.ALARM_AUTO,
    function() readSensor() end)

Configure the long-range architecture with LoRa networking

The design that we have so far works great on a desk in an office. However, in the real world, very few hay barns have a wifi access point. The solution is to use the LoRa protocol. Using the components in this tutorial, I got a range of about 200 meters (an eighth of a mile). You can get a much higher range if you use directional antennas.

The NodeMCU board that we’ve been using does not have a LoRa device on it. This setup leaves us with two possibilities:

  • Get an independent device that can transmit and receive LoRa packets and integrate it into the NodeMCU both in terms of electronics (communication between the board and the device) and software (write the device driver to send and receive messages).
  • Get a more expensive development board that already has LoRa capabilities integrated into it.

In the interest of simplicity, I chose the second solution. The MakerFocus ESP32 development board costs around $25. For the same price, you can get six NodeMCU boads. However, in production, this system won’t need as many LoRa boards. You’ll need just one per barn and one where there is a wifi connection to the internet. But in each barn you should have multiple sensors. Ideally, you would put several sensors between the hay bales where moisture typically collects.

This board is manufactured by Heltec. You can learn how to program it in this blog post.

Configure the gateways

The ESP32 development boards will be configured as gateways. They are not full-fledged gateways that can pass all traffic, but they are very specialized ones that just transfer sensor readings.

One big difference between the NodeMCU board and this ESP32 board is the electrical connectors. Instead of pins to snap into a breadboard, it has holes to which you can solder wires, or use the included pin headers. I prefer to avoid soldering, and luckily in this project soldering is not required. The ESP32 boards only function as gateways between wireless networks.

After you get the board, you need to connect the antenna.

  1. Screw the antenna’s SMA connector into one end of the wire.
  2. Push the connector at the other side of the wire into the MMCX connector on the board. This connector does not screw.
  3. Connect micro-USB to the PC, as you did with NodeMCU.

Unfortunately, the Lua ecosystem isn’t as well developed on ESP32 as on ESP2866 (where NodeMCU is a mature, dependable platform). The ESP32 board requires us to use C instead.

  1. Download and install the device driver for the CP2102 USB to serial chip.
  2. Download the Arduino IDE. If you use Windows 10, you can install it from the Microsoft Store.
  3. Install the ESP32 package. Remember to run get.exe. Preferably, run it from the command line so you can see whether it fails.
  4. Click Tools > Board, scroll down and select Heltec_WIFI_LoRa_32.

Before we get to real programming, we should do a sanity check by running the IoT equivalent of “Hello, world” – a blinking LED.

  1. Open the Arduino IDE and create a new “sketch” (basically, a program). Copy the 03_ESP32_Blink_LED.ino program into it.
  2. Click the upload icon (the green left facing arrow).
  3. After the sketch is done compiling, the white LED next to the screen should be on for a second and then off for half a second, and then the cycle repeats. You can see the location of this LED in Blinking the built-in LED.
Blinking the built-in LED


Let’s step through the 03_ESP32_Blink_LED.ino program.

The first line defines a constant integer variable called builtInLED, and assigns it the value 25. To know that the pin to use is 25, I looked at the board’s pinout diagram. In that diagram, you can see that GPIO25 is the same connection as the LED. In contrast to Lua programs, in C you must declare the type of each variable.

const int builtInLED = 25;

Arduino C programs have a very strict structure. There is one function, setup, that is called when the device is activated (or after a reset). Another function, loop, is called repeatedly while the device is running. Neither function returns a value, which is the reason they are preceded by the void keyword.

void setup() {
  // put your setup code here, to run once:

The only thing that we need to set up is that the LED pin is output.

    pinMode(builtInLED, OUTPUT);


In the main loop we turn on the LED, wait a second, turn it off, and then wait for half a second. Because this code is the main loop, it is repeated until the device is turned off.

void loop() {
  // put your main code here, to run repeatedly:

  // On for a second
  digitalWrite(builtInLED, HIGH);

  // Off for half a second
  digitalWrite(builtInLED, LOW);

Configure the LoRa connectivity

Now that we know we can program the ESP32 boards, it is time to connect them using the LoRa protocol.

Install the LoRa library

Arduino does not come with a library to connect to the LoRa chip on our ESP32 board, so we need to install one.

  1. Click Sketch > Include Library > Manage Libraries.
  2. Select the LoRa by Sandeep Mistry library. Click Install , and then click Close. You can read more about this library in its README. You can also read the API documentation.
  3. Click Close.

This library supports only the LoRa chip (in our case, the SX1276), not the entire LoRaWAN protocol. However, for our application, LoRaWAN is too heavy. We just need to forward a short sensor data message from each sensor node to the Watson IoT Platform every minute. And even the “every minute” requirement is negotiable, but I only chose that rate for ease of development. A packet every ten or twenty minutes might do just as well since temperature and humidity don’t change that quickly.

Develop the sender and receiver programs

The C code for the sender program (04_LoRa_Send.ino) runs on the ESP32 board that would be located in a hay barn. The C code for the receiver program (05_LoRa_Receive.ino) runs on the ESP32 board that is located in a farmhouse with a wifi connection.

For development purposes, you can have both boards connected to the same computer if you remember to click Tools > Port and select the correct serial port for each one before you upload the program to it. The port is the same value in both windows of the Arduino IDE, so if you are working on both at the same time you’ll need fix it before each upload.

With the code running, you should see a short flash (a tenth of a second) on the sender board every few seconds. This flash means that a packet is sent. On the receiver side, you’ll see a longer flash (half a second) every time a packet is received. To see more information, go to the Arduino IDE, select the port of the receiver and click Tools > Serial Monitor. The result is similar to Serial monitor of the receiver (probably with lower serial numbers).

Serial monitor of the receiver


In the serial monitor you can see the time that passed since the previous packet and two radio parameters: the received signal strength indicator (RSSI) and the signal to noise ratio. Packets have a serial number, so you can see when one is missing. The red frame shows one such example.

Some parts of this C program are identical to those parts of the blinking LED program above. Let’s step through the new parts to the sender program (04_LoRa_Send.ino).

The first few lines declare the libraries that we use, by using the #include directive. In this case we need two libraries: the SPI library (because we use the Serial Peripheral Interface bus to communicate with the LoRa device) and the LoRa library itself.

#include <SPI.h>
#include <LoRa.h>

C does not have convenient hash tables in the same way that modern programming languages do. It was originally designed in the 1970s when memory was expensive. Instead, it lets you define structures with fields. When the program is compiled, those fields are converted into offsets from the start of the structure so there is no need to store field names.

In this case, the structure contains the numbers of the GPIO (general purpose input/output) pins used to communicate with the peripheral device. Technically speaking, SPI requires only four pins, but there is often also a pin to reset the peripheral and another for the device to send an interrupt to request attention.

typedef struct spi_pins {

The first four pins are those pins that are used by SPI itself. The first three are the synchronization clock, communication from the CPU (the ESP32) to the peripheral device (SX1276, the LoRa device), and communication back from the peripheral to the CPU.

int sck;
int miso;
int mosi;

The fourth pin is client select. The purpose of this pin is to be able to communicate with additional SPI devices at the additional cost of just one pin per device and without requiring the CPU to have more than a single SPI circuit.

int ss;

The following two field definitions are not actually part of the SPI standard, but appear very often for SPI devices. The first allows the CPU to reset the device. The second stands for interrupt request, and is used by the device to ask the CPU to stop whatever it is doing and deal with a request from the device.

  int rst;
  int irq;

The next lines define the pins that are connected to the SX1276 in our board. If you have a different board, you need to look at its pinout to see where it has such connections.

const spi_pins LORA_SPI_PINS =
    {.sck=5, .miso=19, .mosi=27, .ss=18, .rst=14, .irq=26};

const int MHz = 1000*1000;

void setup() {

These lines tell the SPI and LoRa libraries which pins to use.

  SPI.begin(LORA_SPI_PINS.sck, LORA_SPI_PINS.miso,
  LoRa.setPins(, LORA_SPI_PINS.rst, LORA_SPI_PINS.irq);

Now, start the LoRa network. You need to provide the frequency in Hertz (cycles per second). The frequencies you’re allowed to use vary from country to country. If this setup fails, there is nothing the device can do, so just enter an endless loop.

// US Frequency, use 866*MHz in Europe
if (!LoRa.begin(915*MHz))
  while (1)

The network configuration:

  // Network configuration. Use the slowest speed and
  // highest redundancy. This gives us the maximum possible
  // range.

  // The sync word determines which frequencies will be used
  // when. If it is a value that isn't in common use (the
  // common values are 0x12 and 0x34), it reduces the chance
  // of interference.

void loop() {

Variables that are defined inside a function are local variables, accessible only inside the function. One of these variables is a static variable. Static variables are only initialized once and keep their value between invocations of the function. This feature is useful for a counter that gives packets serial numbers.

int sendSuccess;
static int counter = 0;

Begin the packet. You can write to the packet by using printf, with a format string and parameters to be placed inside it.

LoRa.printf("Hello #%d", counter++);

The LoRa.endPacket function sends the packet, and its return value tells you whether it was successful or not.

sendSuccess = LoRa.endPacket();

We display a short pulse of light (100 milliseconds) if the packet was sent successfully. Whether it was sent successfully or not, wait a second before starting again.

  if (sendSuccess) {
    // Short pulse to show the message was sent
    digitalWrite(builtInLED, HIGH);
    digitalWrite(builtInLED, LOW);
  } else

Now let’s step through the new parts to the receiver program (05_LoRa_Receive.ino).

void setup() {

The receiver program sends out diagnostics on the serial port, so it needs to initialize it. The value is the bits per second of the serial port.

// Initialize the serial device, wait until it is available

Until the serial port is initialized, don’t do anything.

while (!Serial)

Send a message once the serial port is initialized.

  Serial.println("LoRa Receiver");


void loop() {
  int packetSize;

To measure the time between consecutive packets, we use the millis() function. This function provides the number of milliseconds since the last reset.

static unsigned long lastPacketTime = millis();

This function is an array of unsigned characters (that is, bytes). It is one more than the maximum packet length we support because strings in C are supposed to end with a zero. By adding a character at the end of the string, we can ensure it ends with a zero.

unsigned char packet[maxPacketLength+1];
int i;

The loop function is called again as soon as it ends. So, when there are no packets, we will be polling at a very high frequency, which might confuse the SX1276 chip. By waiting a 20th of a second, we make things a lot more stable.


The LoRa.parsePacket function polls to see whether a packet is available, and if so returns its size in bytes.

packetSize = LoRa.parsePacket();
if (packetSize) {   // If there's a packet

You can also use printf to write to the serial port. The divider is 1000.0 because if you divide an integer by another integer in C the result is an integer, with the fractional part dropped. In this case, we want a more accurate result than a whole number of seconds.

Serial.printf("Received packet (after %4.2f seconds): ",

lastPacketTime = millis();

When reading the packet, make sure not to exceed the allocated length. C does not handle this issue for you. The packet variable is a 256-byte array, but if we tried a command such as packet[300]=0; the C compiler would cheerfully obey and overwrite whatever information is 300 bytes after the beginning of the packet variable. There is a whole class of security vulnerabilities that are caused by this issue.

   // Read packet
   for(i=0; i<maxPacketLength && LoRa.available(); i++)
     packet[i] =;

   // Make sure the packet is zero terminated
   packet[i] = 0;

When you specify in a format string %s, it means a zero terminated string. Therefore, it is important to make sure that the packet has a zero at the end.

    // Print packet and data
    Serial.printf("%s, with RSSI %d and S/N ratio %4.2f\n",
      packet, LoRa.packetRssi(), LoRa.packetSnr());


And there you have it, long-range wireless communications that works in a range that is measured in miles rather than feet.

Sending sensor readings to the IoT Platform

Now that we have all the components, it is time to connect the system end-to-end. The data flow of the long-range architecture shows the data flow:

  1. The barn ESP32 is a wifi access point. The sensors use wifi to connect to it, and tunnel over HTTP to send the actual data, within the URL of the page they are supposedly retrieving.
  2. The barn ESP32 uses LoRa to send a packet with the sensor data to the farmhouse ESP32.
  3. The farmhouse ESP32 uses wifi as a station (a normal device, as opposed to an access point) to access the internet.
  4. There is a connection between the access point in the farmhouse and the internet, and between the internet and the IBM Watson IoT Platform. We use this pre-existing connection.

The data flow in steps 3 and 4 is implemented with the IBM Watson IoT Platform API over HTTP.

The data flow of the long-range architecture


Sending sensor data from the NodeMCU to the barn ESP32

In each barn, the barn ESP32 functions as a wifi access point with the SSID barn-net. In the interest of saving bandwidth, we do not send the complete JSON at this point – that would require us to transmit the field names in addition to their content. Instead, NodeMCU provides the information as an HTTP file path. The IP for the access point is, and the URL NodeMCU attempts to retrieve is< _chip ID_ >/< _temperature_ >/< _humidity_ >.

In theory, this kind of use case calls for using a UDP protocol because there is no need for retransmits. But, I chose to use HTTP regardless because it simplifies debugging. If there is a problem, it would be trivial to connect to barn-net with a smartphone and go to a test URL, such as, to see what happens.

The Final Sensor Lua program (06_Final_Sensor.lua) is very similar to the short-range configuration Lua program. The only difference is that instead of sending JSON directly to the IBM Watson IoT Platform, it uses HTTP to connect to the barn ESP32. To do so, it uses the http.get function. The program also prints to the serial port for debugging purposes.

function requestUrl(path)
  http.get("" .. path, nil,
  function(code, data)
    print("Got response to " .. path .. ":" .. data)

Configuring the barn ESP32

The barn ESP32 has three tasks:

  1. Provide a wifi access point to the NodeMCU sensors.
  2. Function as an HTTP server to receive sensor readings from the NodeMCU servers.
  3. Send the sensor readings by using LoRa to the farmhouse ESP32.

The full source code for the C program that controls the barn ESP32 (07_Final_Barn_ESP32.ino) is based on the sender C code you saw earlier.

Configuring the barn ESP32 as a wifi access point

The access point code is mostly inside the setup function. It is self-explanatory for the most part. To see the meaning of the various parameters, you can look in the header file for the wifi library for ESP32 when you use Arduino. Espressif has made it available on GitHub.

#include "WiFi.h"


void setup() {


  // Be an access point

  // SSID of barn-net, accept up to fifteen different stations
  WiFi.softAP("barn-net", NULL, 1, 0, 15);

  // Access point IP, default gateway IP, and net mask
  WiFi.softAPConfig(IPAddress(10,0,0,1), IPAddress(10,0,0,1), IPAddress(255,0,0,0));


Configuring the barn ESP32 as an HTTP server to receive sensor data

The library that we are using does not have an HTTP server. However, it has a TCP server (see the sample code).

We create the HTTP server in the constructor for the server object (this library is actually in C++, a superset of C with object-oriented programming). It listens, as you expect, on TCP port 80. Then, in the setup function, we start the server.

// The HTTP server for messages
WiFiServer server(80);

void setup() {


  // Begin the HTTP server

This function reads a word (characters until space) and returns it as a String.

// Read a word from the client connection
String readWord(WiFiClient clientConn) {

The character read is initialized as an exclamation point so it won’t be a space (which would stop the word reading immediately).

char ch = '!';
String retVal = "";

Keep reading as long as:

  1. The client stays connected.
  2. There are characters available to read.
  3. The last character read is not a space.

This check will fail if the HTTP message is very long. In such a case, when the data in the first packet ends no more data will be immediately available so the message will be truncated. However, we do not encounter this issue because the maximum expected message length is very short. Here’s a worst-case estimate of its length in characters:

HTTP message element Character length
HTTP Verb 4 (GET plus a space)
Slashes 3
Chip ID 7
Temperature in Celsius 3 (for temperature under -9, at 100-degrees Celsius water boils and our sensors has long since ceased to function)
Relative humidity 3
Total 20

HTTP runs over TCP, which itself runs over IP. It’s doubtful any will ever use TCP options with this system, but even if they did the longest TCP header is just 60 bytes. Before that comes the IP header, which at worst is 36 bytes. Taken together, it will be a maximum of 116 bytes. The maximum size of a wifi packet is 2304 bytes, enough for the 116 bytes we need.

while (clientConn.connected() && clientConn.available() && ch != ' ') {

Read a character, and unless it is a space add it to the return value.

    ch =;
    if (ch != ' ')
      retVal += ch;

  return retVal;

We do not need to parse the message but instead just send it to the farmhouse ESP32. However, if the message is an error we don’t want to send it. The easiest way to check whether the message is valid is to parse it using the sscanf function.

// Check if the reading you got is legitimate or an error
boolean checkReading(String reading) {
  int a, b, c;
  int results;

The sscanf function expects to receive a C style string, which is a pointer to characters (also known as char * or char[]). However, String is a C++ style string. The .c_str method converts between the two. Functions in the scanf family expect to get as parameters pointers to the variables they are to fill with values. In C, & <variable> means the address at which <variable> is stored.

// A legitimate reading is three decimal numbers, each preceded by a slash.
results = sscanf(reading.c_str(), "/%d/%d/%d", &a, &b, &c);

The return value is the number of variables filled with data. If everything is successful, there will be three of them. So, the reading is valid if the result is three.

  return results == 3;

This function handles an HTTP client connection, which it receives as a parameter.

void handleHTTP(WiFiClient clientConn) {
  String path;

  Serial.println("Got a client connection");

Read, and throw away, the first word, which is the HTTP verb (GET, POST, and so on).


Read the second word, which is the path. This word is what we really want.

path = readWord(clientConn);

Check whether this value is valid. Report back in case it is a user on a browser who is debugging a problem.

if (checkReading(path))

If we close the connection immediately, the response might not get to the client, so we wait a second.


If we tried to send the LoRa message before we closed the client connection, it would time out. The sensors wouldn’t care, but it would make troubleshooting with a browser less reliable.

  if (checkReading(path))


void loop() {

In the main loop, use server.available to check whether there is a client connection available. If there is one, have handleHTTP handle it. If not, continue (to the next iteration of the loop, so we continue checking endlessly).

  WiFiClient clientConn = server.available();

  if (clientConn)

Sending sensor data from the barn ESP32 to the farmhouse ESP32

The code to send the LoRa message is very similar to the LoRa sender code documented earlier in this article. The only real difference is that the setup and message sending are now in their own functions for the sake of clarity.

Configuring the farmhouse ESP32

The farmhouse ESP32 program (08_Final_Farmhouse_ESP32.ino) uses the wifi in the farmhouse as a station (a normal device as opposed to an access point). It receives LoRa messages from the barn ESP32, parses them, converts them to JSON, and sends them to the IBM IoT Platform.

Configuring the farmhouse ESP32 as a wifi device

To connect to a wifi access point requires only one function call, WiFi.begin. However, it might take some time before the connection happens. There’s no point doing anything else until it does.

WiFi.begin(ssid.c_str(), password.c_str());
while (WiFi.status() != WL_CONNECTED) {

Configuring the farmhouse ESP32 to receive LoRa messages

This process is nearly identical to what we did before in 05_LoRa_Receive.ino. There are only two differences:

  1. After we receive the message, we parse it and then send it out.

    // Parse the packet and send it onward.
    sscanf(packet, "/%d/%d/%d", &chipID, &temp, &humidity);
    sendMessage(chipID, temp, humidity);
  2. We use the millis function to figure how long it takes to process each message and report the information on the serial port. This step is important because the processing we use here is blocking. If we receive two LoRa messages during this time, one is likely to be dropped.

    void processLoRaPacket(int packetSize) {
      int handlingTime = millis();
      Serial.printf("It took %d [msec] to relay this message\n", millis()-handlingTime);

Sending events on to the IoT Platform

To send the HTTP message, we need two strings: the URL and the JSON message. Both must be created at run time. The URL encodes the chipID for the NodeMCU, which is different between different sensors. The JSON includes the temperature and humidity, which vary.

To create these strings, we use the snprintf function. It uses the same format string as printf, but writes to a string rather than standard output (or, for Serial.printf, the device’s serial port). There is a simpler function that does that, sprintf, but it is dangerous because it might overflow the buffer size. When using strings in C, it is necessary to be very careful to prevent buffer overflows – especially when dealing with information that might come from an insecure source, such as LoRa.

We can concatenate String objects by using plus, but that does not work for char arrays, the original object used for strings in C (String is part of C++, and not supported by all the functions).

const String hostname = orgID + "";

// The URL is a format string because the sensor's chip ID varies
const String urlFormatString = "https://" + hostname +


  snprintf(url, maxPacketLength, urlFormatString.c_str(), chipID);
  snprintf(msg, maxPacketLength, "{\"temp\": %d, \"humidity\": %d}", temp, humidity);

Using this information, we can POST a new message. To use the IBM IoT Platform’s API we need the content type to be application/json, use the user name use-token-auth, and use the authentication token as the password.


  Serial.printf("URL: %s\n", url);

  http.addHeader("Content-Type", "application/json");
  http.setAuthorization("use-token-auth", authToken);
  int httpCode = http.POST(msg);

Configuring Watson IoT Platform to receive the sensor data

The only change that is needed in the IBM IoT Platform is to make sure all the devices are registered and that they have the same authentication token – the same one that is in the farmhouse ESP32 program.

You specify the authentication token when you add a device in the IBM Watson IoT Platform’s dashboard, as you can see in Specifying an authentication token in device creation on the IBM Watson IoT Platform.

Specifying an authentication token in device creation on the IBM Watson IoT Platform


From prototype to production

The system documented in this article is a rough prototype. Here are some of the features you might want to add before you go into production:

  1. When you install your sensors in a production environment, such as in an actual hay barn, you’ll need to consider a couple of environmental issues:

    • To measure humidity, the DHT11 must be exposed to the air. But to keep the sensors from breaking, they should be in a container or case of some kind. (If you have access to a 3D printer, you could create your own IoT device case, as I describe in this article.) It is recommended to also monitor temperature inside or between hay bales, where hot pockets can develop. For that purpose, you’d need sturdier construction, for example a PVC pipe.
      • Some hay barns have electrical connections to enable ventilation. If that is the case for the one you’re monitoring, you can use the same electricity to power the barn ESP32 and those sensors that are not buried between hay bales. Where you don’t have an electrical connection, use a rechargeable USB battery. Most hay bale fires occur in the first six weeks after baling, so if the sensors run out of battery after more than six weeks it is not a problem. You can leave them in place until you need to use the hay and recharge the battery before the next baling.
  2. When developing the system, it is useful to get a new reading every minute for debugging. However, in real life, temperature and humidity don’t change that quickly. You can save on bandwidth and battery power by taking a measurement every thirty minutes instead. Change line 3 in this file.
  3. If there are multiple sensors and they are activated at the same time (for example, because they are connected to the same power supply), there might be collisions that would lead to lost packets. Normally such packets will be buffered and retransmitted, but we are working with systems with very low RAM for buffers. The LoRa segment does not have retransmits at all.

    To reduce the likelihood of a collision, add a random number to the wait. In the same file, change line 42 to:

        sensorTimer:register((updateFreq + math.random(120))*1000, tmr.ALARM_AUTO, readSensor)
  4. Automatic device registration. When the farmhouse ESP32 gets a 403 HTTP response code (here, line 105), you can register the new sensor automatically by using the API.
  5. Obviously, you want to do more than just collect the sensor data readings. You’ll want to use the information in the Watson IoT Platform. Store it in a database, but then issue alerts when the humidity and temperature are too high. Also, specify a lower priority alert when there are no new readings from a sensor, which suggests that the battery might need to be replaced.


You should now be able to apply these principles to run other types of IoT sensors, as in the short-range architecture. You should also be able to run sensors at a distance from an internet access point, by using LoRa to transfer the sensor data. In Part 2 of this series, you build on what you have learned here by using simple edge analytics to activate safety equipment without relying on the Internet.