Main Content

Collect Agricultural Data over The Things Network

This example shows how to set up data collection from three sensors connected to a microprocessor board with a LoRa® radio.

This configuration allows the creation of a network of distributed sensors over a large area. The sensors send data to The Things Network, which is then forwarded to ThingSpeak™ for analysis and visualization. In the example, you build a prototype device, connect to The Things Network, and integrate data collection with ThingSpeak. The device shown here collects temperature, soil moisture, and GPS data.

Overview

The example consists of three major steps. The Things Network integration is further divided into several substeps. The most involved step is the building the device. To complete the example, you need a ThingSpeak account and an account at The Things Network. On ThingSpeak, you create a new channel. On the Things Network, you create an application and register a device. Then you create a payload decoder and add the integration that forwards the data to ThingSpeak.

1) Set up a ThingSpeak Channel to Collect Data

2) Set up The Things Network

  • Create Application

  • Register Device

  • Create Payload Format

  • Add Integration

3) Create Device

  • Hardware Used to Create Sensor Node

  • Schematic and Connections

4) Program Device

  • Programming Setup

  • Code

Set up a ThingSpeak Channel to Collect Data

1) Create a ThingSpeak channel, as shown in Collect Data in a New Channel. Record the write API key and the channel ID for your new channel.

2) Navigate to the Channel Settings page. Set the field labels as follows.

  • Field 1— Counter

  • Field 2— Soil Moisture

  • Field 3— Temperature F

3) Click Save Channel at the bottom to save your settings.

Configure The Things Network Application

Create an account on The Things Network, and then log in to The Things Network Console.

Create Application

1) Select Applications.

2) Select Add Application.

3) Create an Application ID, then add a Description. Select the Handler registration based on your location.

Register a Device

1) Click the Devices tab and register a device. For more information, see Device Registration.

2) Create a device ID. Enter the Device EUI if your device has one. If not, select the button on the left of the Device EUI field to generate the EUI automatically.

3) Cick Register. The browser returns you to the Overview tab.

4) Select the Settings tab.

5) In the settings, select ABP for the activation method. To aid debugging, you can optionally disable the Frame Counter Checks at the bottom of the page.

6) Record the Device Address, Network Session Key, and App Session Key. This information is necessary in your device code.

Create Payload Formatter

The payload formatter uses the bytes sent to the application from the gateway to assemble a message. In this example, the desired payload message is a JSON-encoded object that is sent to ThingSpeak.

1) Return to the application view using the navigation menu at the top. Then click the Payload Formats tab.

2) In the decoder interface, create the code to convert the bytes sent from your device into a JSON object to write to ThingSpeak. The conditional code for lat and lon handles the possibility of positive or negative values.

function Decoder(b, port) {
  
 var counter = b[0] << 8) | b[1];
 var moisture = b[2] | b[3] << 8;
 var temp= ( b[4] | b[5] << 8 )/100;
 var lat = ( b[6] | b[7] << 8 | b[8] << 16 | (b[8] & 0x80 ? 0xFF << 24 : 0)) / 10000;
 var lon = ( b[9] | b[10] << 8 | b[11] << 16 | (b[11] & 0x80 ? 0xFF << 24 : 0)) / 10000;

  return {
    field1: counter,
    field2: moisture,
    field3: temp,
    latitude: lat,
    longitude: lon
  }
}

Add Integration

To forward data to ThingSpeak, you must have an application on the Things Network with a registered device and a payload formatter. Create a ThingSpeak integration to forward the data.

1) Log in to your The Things Network Console.

2) Select Applications and select the application you want to forward data to ThingSpeak from.

3) Click the Integrations tab.

4) Select ThingSpeak.

5) In the Process ID field, name your integration.

6) In the Authorization field, enter the write API key for the channel that you want to store your data in. The API key is available from the API keys tab of your ThingSpeak channel.

7) In the Channel ID field, enter the channel ID for the ThingSpeak channel you want to forward data into. The channel ID is available on your ThingSpeak channel’s page.

Create Device

Hardware Used to Create Sensor Node

You can use various LoRa devices that support LoRaWan protocols for connecting to The Things Network. This example demonstrates the procedure using the following hardware setup.

Schematic and Connections

Connect the sensors as shown in the schematic. The photograph shows a possible configuration of the sensors in a project box. In this configuration, the temperature sensor inside the box might not exactly reflect the outside temperature. You need to add an antenna to the LoRa radio.

1) Connect the power and ground connections for the GPS and temperature sensors. Do not connect the power to the moisture sensor.

2) Connect the soil moisture sensor output to the analog input at A0.

3) Set up the system so that the moisture sensor power turns off when not in use. The moisture sensor power pin is connected to the GPIO pin 11 on the feather M0. Turning the power off when not in use extends the sensor lifetime.

4) Connect the DH- 22 sensor data pin to PA-15 on the Feather M0, which is pin 5 in the Arduino sketch.

5) For the GPS board, connect TX to RX on the Feather M0 and RX to TX.

6) Enable the LoRa radio by connecting PA20 (pin 29, GPIO 6) on the Feather M0 to ground.

7) Create a power switch by connecting a switch from the En pin to ground.

Program Device

Programming Setup

1) Download the latest Arduino IDE.

2) Download the Adafruit GPS library or add the Adafruit_GPS library in the library manager. Select Sketch > Include Library > Manage Libraries. Search Adafruit_GPS to add it to your installed libraries.

3) Download the LoraWan-in-C library for the Arduino environment or add the lmic and hal/hal libraries in the library manager. Select Sketch > Include Library > Manage Libraries. Search lmic and select MCCI LoRaWAN LMIC library to add it to your installed libraries. Also add the MCCI Arduino LoRaWan Library to the library manager.

4) Create the application. Open a new window in the Arduino IDE and save the file. Add the code provided in the Code section.

Code

1) Start by including the appropriate libraries and initializing the variables.

#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
#include "DHT.h"
#include <Adafruit_GPS.h>

#define DHTPIN 5
#define GPSSerial Serial1
#define SOIL_PIN 14
#define SOIL_POWER_PIN 11
#define GPSECHO false   // Set to 'true' if you want to debug and listen to the raw GPS sentences
#define PAYLOAD_SIZE 13 // Number of bytes sent plus 2


// LoRaWAN NwkSKey, network session key
static const PROGMEM u1_t NWKSKEY[16] = {0x98, 0xEB, 0x1A, 0xC5, 0xF9, 0x20, 0x15, 0xCD, 0x12, 0xE5, 0x72, 0xFF, 0xCD, 0xE2, 0x94, 0x46};
// LoRaWAN AppSKey, application session key
static const u1_t PROGMEM APPSKEY[16] = {0x50, 0x28, 0x4D, 0xAE, 0xEA, 0x41, 0x53, 0x7E, 0xCA, 0x70, 0xD2, 0x26, 0xCC, 0x14, 0x66, 0x19};
// LoRaWAN end-device address (DevAddr)
static const u4_t DEVADDR = 0x26021115;

// Callbacks are only used in over-the-air activation. Leave these variables empty unless you use over the air activation.
void os_getArtEui(u1_t *buf) {}
void os_getDevEui(u1_t *buf) {}
void os_getDevKey(u1_t *buf) {}

// Payload to send to TTN gateway
static uint8_t payload[PAYLOAD_SIZE];
static osjob_t sendjob;

// Schedule TX at least this many seconds
const unsigned TX_INTERVAL = 60; //was 30

// Pin mapping for Adafruit Feather M0 LoRa
const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {3, 6, LMIC_UNUSED_PIN},
    .rxtx_rx_active = 0,
    .rssi_cal = 8, // LBT cal for the Adafruit Feather M0 LoRa, in dB.
    .spi_freq = 8000000,
};

Adafruit_GPS GPS(&GPSSerial); // Connect to the GPS on the hardware port.

DHT dht(DHTPIN, DHT22);  // Connect to the temperature sensor.
uint16_t counter = 0;
int32_t myLatitude = -12345; // Initialize for testing before GPS finds a lock.
int32_t myLongitude = 54321; // Initialize for testing.
int myMoisture = 0; // 10 bit ADC value.
float temperatureF = 1111; 

2) Use the setup function to start the temperature sensor, the GPS, and the LoRa radio.

void setup()
{
    Serial.begin(115200);
    dht.begin();
    Serial.println("Start");
    // Set the power pin for the moisture sensor
    pinMode(SOIL_POWER_PIN,OUTPUT);
    digitalWrite(SOIL_POWER_PIN, LOW);

    GPS.begin(9600); // 9600 NMEA is the default baud rate.
    GPS.sendCommand(PMTK_SET_NMEA_OUTPUT_RMCGGA);
    GPS.sendCommand(PMTK_SET_NMEA_UPDATE_1HZ); // Set a 1 Hz update rate.

    delay(1000); // Wait for GPS to initialize.

    // Ask for firmware version
    GPSSerial.println(PMTK_Q_RELEASE);
    // Initialize the LMIC.
    os_init();
    // Reset the MAC state. Resetting discards the session and pending data transfers. 
    LMIC_reset();

    // Set static session parameters. 
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession(0x13, DEVADDR, nwkskey, appskey);

    LMIC_selectSubBand(1);
    // Only use the correct The Things Network channels, disable the others.
    for (int c = 0; c < 72; c++)
    {
        if ((c < 8) || (c > 15))
        {
            LMIC_disableChannel(c);
        }
    }
    
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);

    // Disable link check validation
    LMIC_setLinkCheckMode(0);

    // TTN uses SF9 for its RX2 window.
    LMIC.dn2Dr = DR_SF9;

    // Set data rate and transmit power for uplink (note: txpow seems to be ignored by the library)
    LMIC_setDrTxpow(DR_SF7, 14);

    // Start job.
    processJob(&sendjob);
}

3) Use the loop function to initiate the LoRa process and parse the GPS data.

void loop() // Run over and over again
{
    os_runloop_once();

    char c = GPS.read();
    if (GPSECHO)
     {
        if (c){
            Serial.print(c);
              }
     }
    // If a sentence is received, parse it
    if (GPS.newNMEAreceived())
    {
        if (!GPS.parse(GPS.lastNMEA())) // Also sets the newNMEAreceived() flag to false
            return;                   
    }
}

4) The GetSensorData function turns on the power to the moisture sensor and reads its data, then turns the power off. It also reads the temperature sensor and checks for information from the GPS device. If there is a GPS fix, this function updates the position information.

void GetSensorData()
{
    digitalWrite(SOIL_POWER_PIN, HIGH);
    delay(1000);
    myMoisture = analogRead(SOIL_PIN);
    digitalWrite(SOIL_POWER_PIN, LOW);
    temperatureF = dht.readTemperature( true );
    Serial.println("moisture " + String( myMoisture ) + " temp " + String( temperatureF ));
     
    if (GPS.fix)
    {
        Serial.print( "Location: " );
        Serial.print( GPS.latitudeDegrees * 100, 4 );
        Serial.print( " break " );
        Serial.print( GPS.lat );
        Serial.print( ", " );
        Serial.print( GPS.longitudeDegrees * 100 , 4 );
        Serial.println( GPS.lon );
        myLatitude = GPS.latitudeDegrees * 10000;
        myLongitude = GPS.longitudeDegrees * 10000;
    }
}

5) Use the onEvent function to process events from the LoRa radio. The function updates the serial monitor, schedules the next transmission, and receives messages.

void onEvent(ev_t ev)
{
    Serial.print(os_getTime());
    Serial.print(": ");

    switch (ev)
    {
    case EV_SCAN_TIMEOUT:
        Serial.println(F("EV_SCAN_TIMEOUT"));
        break;
    case EV_BEACON_FOUND:
        Serial.println(F("EV_BEACON_FOUND"));
        break;
    case EV_BEACON_MISSED:
        Serial.println(F("EV_BEACON_MISSED"));
        break;
    case EV_BEACON_TRACKED:
        Serial.println(F("EV_BEACON_TRACKED"));
        break;
    case EV_JOINING:
        Serial.println(F("EV_JOINING"));
        break;
    case EV_JOINED:
        Serial.println(F("EV_JOINED"));
        break;
    case EV_JOIN_FAILED:
        Serial.println(F("EV_JOIN_FAILED"));
        break;
    case EV_REJOIN_FAILED:
        Serial.println(F("EV_REJOIN_FAILED"));
        break;
    case EV_TXCOMPLETE:
        Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
        if (LMIC.txrxFlags & TXRX_ACK)
            Serial.println(F("Received ack"));
        if (LMIC.dataLen)
        {
            Serial.println(F("Received "));
            Serial.println(LMIC.dataLen);
            Serial.println(F(" bytes of payload"));
        }
        // Schedule next transmission
        os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
        break;
    case EV_LOST_TSYNC:
        Serial.println(F("EV_LOST_TSYNC"));
        break;
    case EV_RESET:
        Serial.println(F("EV_RESET"));
        break;
    case EV_RXCOMPLETE:
        // data received in ping slot
        Serial.println(F("EV_RXCOMPLETE"));
        break;
    case EV_LINK_DEAD:
        Serial.println(F("EV_LINK_DEAD"));
        break;
    case EV_LINK_ALIVE:
        Serial.println(F("EV_LINK_ALIVE"));
        break;

    case EV_TXSTART:
        Serial.println(F("EV_TXSTART"));
        break;
    default:
        Serial.print(F("Unknown event: "));
        Serial.println((unsigned)ev);
        break;
    }
}

6) The processJob function converts sensor data to bits to be sent over the LoRa radio.

void processJob(osjob_t *j)
{
    getSensorData();
    
    if (LMIC.opmode & OP_TXRXPEND) // Check if there is a current TX/RX job running.
    {
        Serial.println(F("OP_TXRXPEND, not sending"));
    }
    else
    {
        payload[0] = byte(counter);
        payload[1] = counter >>8;

        payload[2] = byte(myMoisture);
        payload[3] = myMoisture >> 8;

        int shiftTemp = int(temperatureF * 100); // Convert temperature float to integer for sending and save two places.
        payload[4] = byte(shiftTemp);
        payload[5] = shiftTemp >> 8;

        payload[6] = byte(myLatitude);
        payload[7] = myLatitude >> 8;
        payload[8] = myLatitude >> 16;

        payload[9] = byte(myLongitude);
        payload[10] = myLongitude >> 8;
        payload[11] = myLongitude >> 16;

        LMIC_setTxData2(1, payload, sizeof(payload) - 1, 0); // Prepare upstream data transmission at the next possible time.

        counter++;
        Serial.println(String(counter));
    }
    // Next TX is scheduled after TX_COMPLETE event.

To learn how to create a detailed visualization dashboard in your ThingSpeak cannel, see Create Customized ThingSpeak Channel View.