Welcome to the TUM-GIS Sensor Nodes documentation

This repo contains documentation, Arduino sketches, and images of our sensor nodes and the sensor services we used.

_images/nodes.png

Contact and contribution

We are happy for any kind of comments, questions, corrections, and own contributions. Please visit the Github Repo of this documentation to report a correction, bug, or question or contribute with a pull request.

Contents

Solar powered Seeeduino

This sensor node is made to showcase a use-case of LoRaWAN sensor node powered using a solar panel. For achieving this a Seeeduino LoRaWAN microcontroller was used along with a solar panel connected using a solar shield. To show a generic use-case we have used a temperature and humidity sensor in this case, but it can be easily replaced with some other sensor as well. The entire setup was carefully placed in the ABS Waterproof case which is an easy to install water-proof and dust-proof case for an indoor or outdoor sensor installations. However, this case has no provision for the ventilation unlike the TFA case and so the readings obtained by the sensor may not accurately represent the outdoor weather conditions. In this example, we measure parameters such as temperature, humidity, and battery voltage.

_images/setup2.jpg

Sensor node in ABS Waterproof case.

Wiring setup

First of all, the solar panel is connected with the SOLAR pin and a battery is connected with a BAT pin on the solar charger shield as shown in the figure below. A DHT-22 Sensor is connected to A2 pin on the Seeeduino board using a connector cable and then the solar charger shield prepared in the previous step is mounted on the board.

_images/shield_wiring.jpg

Solar shield connections with the solar panel and a battery.

Apart from this, to measure the voltage of Lipo Battery we need to connect the VBAT pin to Analog pin A0, so that we can read the data from A0 pin. To achieve this, we need to Short R7 using a 0ohm resistor as shown in the figure here.

_images/short.jpg

Short R7 using a 0 ohm resistor for battery voltage measurement.

Final hardware setup looked as following:

_images/hardware2.png

Final hardware wiring setup.

Once all these connection were made, the board is connected with a computer using a USB cable. Further, steps of software part needs to be followed.

Software

To create this node, we use Arduino IDE for setting up the Seeeduino LoRaWAN device. First, install the Seeeduino LoRaWAN board to your Arduino IDE and select the correct port. Then following libraries needs to be installed before compiling the code:

  • Wire.h to communicate with I2C devices.
  • DHT.h for reading DHT-22 sensor.
  • RTCZero.h for controlling internal clock for time.
  • CayenneLPP.h for Cayenne Protocol.

Apart from this LoRaWan.h library is also used but it is bundled within Seeeduino Board and is not required to be separately installed.

Now download and run the Arduino Sketch for Solar powered Seeeduino sensor node file in the Arduino IDE. This code was created by merging the example code of both the sensors and the ttn-otaa example from the lmic library. Some required changes were made while merging the example codes. The user should change the network session key, app session key and device address in the code before compiling. These keys can be obtained from the TTN, SWM or other service providers.

Modify the keys in highlighted lines.
1
2
3
4
5
6
7
8
9
    // The EUIs and the AppKey must be given in big-endian format, i.e. the
    // most-significant-byte comes first (as displayed in the TTN console).
    // For TTN issued AppEUIs the first bytes should be 0x70, 0xB3, 0xD5.
 
    // void setId(char *DevAddr, char *DevEUI, char *AppEUI);
    lora.setId(NULL, "00942FBXXXXXXXXX", "70B3D57XXXXXXXXX");   
 
    // setKey(char *NwkSKey, char *AppSKey, char *AppKey);
    lora.setKey(NULL, NULL, "CB89A0AA43F6C5XXXXXXXXXXXXXXXXXX");

Following is the example code that can be used to measure the battery voltage of the Seeed solar charger shield:

Code for measuring the battery voltage
1
2
3
4
5
6
7
8
9
    BatteryValue = analogRead(analogInPin);
    // Calculate the battery voltage value
    outputValue = (float(BatteryValue)*5)/1023*2;
    // print the results to the serial monitor:
    SerialUSB.print("Analog value = " );
    SerialUSB.print(BatteryValue);
    SerialUSB.print("\t voltage = ");
    SerialUSB.println(outputValue);
    SerialUSB.println("V \n");

Services

This node is connected using the TheThingsNetwork service. Further, a node-red work bench is used to forward this collected data from the TTN platform to the OGC Sensor Things API configured on the FROST Server. The node-red workbench that was used for forwarding the data is available at Node red flow for Solar powered Seeeduino sensor node for Solar powered Seeeduino. To use this node-red-workbench go to the node-red platform https://iot.gis.bgu.tum.de:1885/, login with the credentials, go to the options and select Import>Clipboard. Select the downloaded .json file with the given option and click on import. Make necessary changes and deploy the flow.

Datastreams setup for this sensor node on the FROST server can be seen at: http://iot.gis.bgu.tum.de:8081/FROST-Server-gi3/v1.0/Things(19)/Datastreams

The node-red workbench for this sensor node could be found at: https://iot.gis.bgu.tum.de:1885/#flow/58838bc1.4ce6a4

The GRAFANA dash-board for visualizing the collected data is available at: https://iot.gis.bgu.tum.de:3050/d/TfCVFRNWz/solar-powered-seeeduino-with-dht22?orgId=1&refresh=10s

Code files

Arduino Sketch for Solar powered Seeeduino sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
#include <DHT.h>
#include <RTCZero.h>
#include <LoRaWan.h>
#include <Wire.h>
#include <CayenneLPP.h>
 
// Keep the following line, if the board is a Seeeduino LoRaWAN with GPS,
// otherwise comment the line out
 
// #define HAS_GPS 1

const int analogInPin = A0;
#define DHTPIN A2   
#define DHTTYPE DHT22 

DHT dht(DHTPIN, DHTTYPE);

int BatteryValue = 0;        
float outputValue = 0;

RTCZero rtc;
char buffer[256];                       // buffer for text messages received from the LoRaWAN module for display
 
CayenneLPP lpp(51);
 
void setup(void)
{
    digitalWrite(38, HIGH);             // Provide power to the 4 Grove connectors of the board
     
    for(int i = 0; i < 26; i ++)        // Set all pins to HIGH to save power (reduces the
    {                                   // current drawn during deep sleep by around 0.7mA).
        if (i!=13) {                    // Don't switch on the onboard user LED (pin 13).
          pinMode(i, OUTPUT);
          digitalWrite(i, HIGH);
        }
    }   
     
    delay(5000);                        // Wait 5 secs after reset/booting to give time for potential upload
    dht.begin();                                    // of a new sketch (sketches cannot be uploaded when in sleep mode)
    SerialUSB.begin(115200);            // Initialize USB/serial connection
    delay(500);
    // while(!SerialUSB);
    SerialUSB.println("Seeeduino LoRaWAN board started!");
 
    // nrgSave.begin(WAKE_RTC_ALARM);
    // rtc.begin(TIME_H24);
 
#ifdef HAS_GPS
    Serial.begin(9600);                 // Initialize serial connection to the GPS module
    delay(500);
    Serial.write("$PMTK161,0*28\r\n");  // Switch GPS module to standby mode as we don't use it in this sketch
#endif
     
    lora.init();                        // Initialize the LoRaWAN module
    
    memset(buffer, 0, 256);             // clear text buffer
    lora.getVersion(buffer, 256, 1);   
    memset(buffer, 0, 256);             // We call getVersion() two times, because after a reset the LoRaWAN module can be
    lora.getVersion(buffer, 256, 1);    // in sleep mode and then the first call only wakes it up and will not be performed.
    SerialUSB.print(buffer);
     
    memset(buffer, 0, 256);
    lora.getId(buffer, 256, 1);
    SerialUSB.print(buffer);
 
    // The following three constants (AppEUI, DevEUI, AppKey) must be changed
    // for every new sensor node. We are using the LoRaWAN OTAA mode (over the
    // air activation). Each sensor node must be manually registered in the
    // TTN console at https://console.thethingsnetwork.org before it can be
    // started. In the TTN console create a new device with the DevEUI also
    // being automatically generated. After the registration of the device the
    // three values can be copied from the TTN console. A detailed explanation
    // of these steps is given in
    // https://learn.adafruit.com/the-things-network-for-feather?view=all
 
    // The EUIs and the AppKey must be given in big-endian format, i.e. the
    // most-significant-byte comes first (as displayed in the TTN console).
    // For TTN issued AppEUIs the first bytes should be 0x70, 0xB3, 0xD5.
 
    // void setId(char *DevAddr, char *DevEUI, char *AppEUI);
    lora.setId(NULL, "00942FBXXXXXXXXX", "70B3D57XXXXXXXXX");   
 
    // setKey(char *NwkSKey, char *AppSKey, char *AppKey);
    lora.setKey(NULL, NULL, "CB89A0AA43F6C5XXXXXXXXXXXXXXXXXX");
     
    lora.setDeciveMode(LWOTAA);           // select OTAA join mode (note that setDeciveMode is not a typo; it is misspelled in the library)
    // lora.setDataRate(DR5, EU868);         // SF7, 125 kbps (highest data rate)
    lora.setDataRate(DR3, EU868);         // SF9, 125 kbps (medium data rate and range)
    // lora.setDataRate(DR0, EU868);         // SF12, 125 kbps (lowest data rate, highest max. distance)
 
    // lora.setAdaptiveDataRate(false);   
    lora.setAdaptiveDataRate(true);       // automatically adapt the data rate
     
    lora.setChannel(0, 868.1);
    lora.setChannel(1, 868.3);
    lora.setChannel(2, 868.5);
    lora.setChannel(3, 867.1);
    lora.setChannel(4, 867.3);
    lora.setChannel(5, 867.5);
    lora.setChannel(6, 867.7);
    lora.setChannel(7, 867.9);
 
    // The following two commands can be left commented out;
    // TTN works with the default values. (It also works when
    // uncommenting the commands, though.)
    // lora.setReceiceWindowFirst(0, 868.1);
    // lora.setReceiceWindowSecond(869.525, DR0);
    
    lora.setDutyCycle(false);             // for debugging purposes only - should normally be activated
    lora.setJoinDutyCycle(false);         // for debugging purposes only - should normally be activated
     
    lora.setPower(14);                    // LoRa transceiver power (14 is the maximum for the 868 MHz band)
     
    // while(!lora.setOTAAJoin(JOIN));
    while(!lora.setOTAAJoin(JOIN,20));    // wait until the node has successfully joined TTN
 
    lora.setPort(33);                     // all data packets are sent to LoRaWAN port 33
}
 
void loop(void)
{  
    bool result = false;
    float temp_hum_val[2] = {0};
    float temperature, humidity;
    // Reading temperature or humidity takes about 250 milliseconds!
    // Sensor readings may also be up to 2 seconds 'old' (its a very slow sensor)
    
    
    if(!dht.readTempAndHumidity(temp_hum_val)){
        SerialUSB.print("Humidity: "); 
        SerialUSB.print(humidity = temp_hum_val[0]);
        SerialUSB.print(" %\t");
        SerialUSB.print("Temperature: "); 
        SerialUSB.print(temperature = temp_hum_val[1]);
        SerialUSB.println(" *C");
    }
    else{
       SerialUSB.println("Failed to get temprature and humidity value.");
    }
       
    BatteryValue = analogRead(analogInPin);
    // Calculate the battery voltage value
    outputValue = (float(BatteryValue)*5)/1023*2;
    // print the results to the serial monitor:
    SerialUSB.print("Analog value = " );
    SerialUSB.print(BatteryValue);
    SerialUSB.print("\t voltage = ");
    SerialUSB.println(outputValue);
    SerialUSB.println("V \n");
     
    SerialUSB.println("-- LOOP");
    lpp.reset();
    lpp.addTemperature(1, temperature);
    lpp.addRelativeHumidity(2, humidity);
    lpp.addAnalogInput(3, outputValue);
        
    result = lora.transferPacket(lpp.getBuffer(), lpp.getSize(), 5);   // send the data packet (n byts) with a default timeout of 5 secs

    if(result)
    {
        short length;
        short rssi;
         
        memset(buffer, 0, 256);
        length = lora.receivePacket(buffer, 256, &rssi);
         
        if(length)
        {
            SerialUSB.print("Length is: ");
            SerialUSB.println(length);
            SerialUSB.print("RSSI is: ");
            SerialUSB.println(rssi);
            SerialUSB.print("Data is: ");
            for(unsigned char i = 0; i < length; i ++)
            {
                SerialUSB.print("0x");
                SerialUSB.print(buffer[i], HEX);
                SerialUSB.print(" ");
            }
            SerialUSB.println();
        }
    }
     
    lora.setDeviceLowPower();     // bring the LoRaWAN module to sleep mode
    doSleep((5*60-8)*1000);       // deep sleep for 292 secs (+ 3 secs transmission time + 5 secs timeout = 300 secs period)
    lora.setPort(33);             // send some command to wake up the LoRaWAN module again
}
 
// The following function implements deep sleep waiting. When being called the
// CPU goes into deep sleep mode (for power saving). It is woken up again by
// the CPU-internal real time clock (RTC) after the configured time.
//
// A similar function would also be available in the standard "ArduinoLowPower" library.
// However, in order to be able to use that library with the Seeeduino LoRaWAN board,
// four files in the package "Seeed SAMD boards by Seeed Studio Version 1.3.0" that is
// installed using the Arduino IDE board manager need to be patched. The reason is that
// Seeed Studio have not updated their files to a recent Arduino SAMD version yet
// and the official "ArduinoLowPower" library provided by the Arduino foundation is
// referring to some missing functions. For further information see here:
// https://forum.arduino.cc/index.php?topic=603900.0 and here:
// https://github.com/arduino/ArduinoCore-samd/commit/b9ac48c782ca4b82ffd7e65bf2c956152386d82b
 
void doSleep(uint32_t millis) {
    if (!rtc.isConfigured()) {    // if called for the first time,
        rtc.begin(false);         // then initialize the real time clock (RTC)
    }
 
    uint32_t now = rtc.getEpoch();
    rtc.setAlarmEpoch(now + millis/1000);
    rtc.enableAlarm(rtc.MATCH_HHMMSS);
 
    rtc.standbyMode();            // bring CPU into deep sleep mode (until woken up by the RTC)
}
Node red flow for Solar powered Seeeduino sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
[
    {
        "id": "58838bc1.4ce6a4",
        "type": "tab",
        "label": "Device1",
        "disabled": false,
        "info": ""
    },
    {
        "id": "daeb7602.698d18",
        "type": "switch",
        "z": "58838bc1.4ce6a4",
        "name": "Separate",
        "property": "key",
        "propertyType": "msg",
        "rules": [
            {
                "t": "cont",
                "v": "temperature",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "humidity",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "analog",
                "vt": "str"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 4,
        "x": 220,
        "y": 180,
        "wires": [
            [
                "a3a522a5.a81a9"
            ],
            [
                "367717e8.191318"
            ],
            [
                "466fd2c5.586efc"
            ],
            []
        ]
    },
    {
        "id": "e2798231.c9314",
        "type": "split",
        "z": "58838bc1.4ce6a4",
        "name": "",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "key",
        "x": 90,
        "y": 180,
        "wires": [
            [
                "daeb7602.698d18"
            ]
        ]
    },
    {
        "id": "5c3e3ed9.0b4dd",
        "type": "debug",
        "z": "58838bc1.4ce6a4",
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 810,
        "y": 180,
        "wires": []
    },
    {
        "id": "367717e8.191318",
        "type": "function",
        "z": "58838bc1.4ce6a4",
        "name": "Humidity",
        "func": "var humValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": humValue, \"Datastream\": {\"@iot.id\": 102}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 440,
        "y": 200,
        "wires": [
            [
                "c777922b.84784"
            ]
        ]
    },
    {
        "id": "c777922b.84784",
        "type": "http request",
        "z": "58838bc1.4ce6a4",
        "name": "POST Observation",
        "method": "POST",
        "ret": "obj",
        "paytoqs": false,
        "url": "http://iot.gis.bgu.tum.de:8081/FROST-Server-gi3/v1.0/Observations",
        "tls": "",
        "proxy": "",
        "authType": "basic",
        "x": 630,
        "y": 180,
        "wires": [
            [
                "5c3e3ed9.0b4dd"
            ]
        ]
    },
    {
        "id": "a3a522a5.a81a9",
        "type": "function",
        "z": "58838bc1.4ce6a4",
        "name": "Temperature",
        "func": "var tempValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": tempValue, \"Datastream\": {\"@iot.id\": 101}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 450,
        "y": 160,
        "wires": [
            [
                "c777922b.84784"
            ]
        ]
    },
    {
        "id": "41ae6239.73f9bc",
        "type": "ttn uplink",
        "z": "58838bc1.4ce6a4",
        "name": "TTN Input",
        "app": "58ceff1f.8576a",
        "dev_id": "tum-gis-device1",
        "field": "",
        "x": 80,
        "y": 60,
        "wires": [
            [
                "491bb4da.0eb58c"
            ]
        ]
    },
    {
        "id": "491bb4da.0eb58c",
        "type": "cayennelpp-decoder",
        "z": "58838bc1.4ce6a4",
        "name": "",
        "x": 280,
        "y": 60,
        "wires": [
            [
                "e2798231.c9314",
                "f2d3534b.0f44f"
            ]
        ]
    },
    {
        "id": "466fd2c5.586efc",
        "type": "function",
        "z": "58838bc1.4ce6a4",
        "name": "Battery Voltage",
        "func": "var batteryvolt = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": batteryvolt, \"Datastream\": {\"@iot.id\": 104}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 440,
        "y": 240,
        "wires": [
            [
                "c777922b.84784"
            ]
        ]
    },
    {
        "id": "f2d3534b.0f44f",
        "type": "debug",
        "z": "58838bc1.4ce6a4",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 490,
        "y": 60,
        "wires": []
    },
    {
        "id": "58ceff1f.8576a",
        "type": "ttn app",
        "z": "",
        "appId": "gis-tum-sensors",
        "accessKey": "ttn-account-ACCESSKEY_HERE",
        "discovery": "discovery.thethingsnetwork.org:1900"
    }
]

Indoor Ambient Monitoring

This sensor node is made to showcase a use-case of LoRaWAN for indoor ambience monitoring. For achieving this a multitude of sensors were used which can monitor the quality of the ambience. In this example we measure parameters such as temperature, humidity, air pressure, air quality, CO2, loudness, gas, PM2.5, and light.

_images/hardware-setup1.jpg

Hardware setup.

Wiring setup

First of all, the grove base shield was connected over the Seeeduino LoRaWAN board. The board was set at the 5V mode. Then, the sensor connections were made using the connector cables as following:

  • Loudness Sensor – Analog Pin A0
  • PM 2.5 Sensor – I2C pin
  • Digital Light Sensor – I2C pin
  • BME680 Sensor – I2C pin
  • MHZ19B CO2 Sensor – Digital Pin D4
  • Air Quality Sensor - A2

Apart from this, there is no need of any other wiring in this case.

_images/hardware1.png

Hardware connections.

Once all these connection were made, the board is connected with a computer using a USB cable. Further, steps of software part needs to be followed.

Software

To create this node, we use Arduino IDE for setting up the Seeeduino LoRaWAN device. First, install the Seeeduino LoRaWAN board board to your Arduino IDE and select the correct port. Then following libraries needs to be installed before compiling the code:

Apart from this LoRaWan.h library is also used but it is bundled within Seeeduino Board and is not required to be separately installed.

Now download and run the Arduino Sketch for Indoor Ambient Monitoring sensor node file in the Arduino IDE. This code was created by merging the example code of each of these attached sensor and the ttn-otaa example from the Seeeduino board. Some required changes were made while merging the example codes. For example, as there are multiple sensors each needs to be defined with a unique name. So, here HM330X was named as sensor while AirqualitySensor as sensors.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
AirQualitySensor sensors(A2);

SoftwareSerial ss(4,5);
MHZ19 mhz(&ss);

#define BME_SCK 13
#define BME_MISO 12
#define BME_MOSI 11
#define BME_CS 10

#define IIC_ADDR  uint8_t(0x76)
Seeed_BME680 bme680(IIC_ADDR);

int loudness,a;

HM330X sensor;
u8 buf[30];

The user should change the network session key, app session key and device address in the code before compiling. These keys can be obtained from the TTN, SWM or any other service providers.

Modify the keys in highlighted lines
1
2
3
4
5
6
7
8
9
    // The EUIs and the AppKey must be given in big-endian format, i.e. the
    // most-significant-byte comes first (as displayed in the TTN console).
    // For TTN issued AppEUIs the first bytes should be 0x70, 0xB3, 0xD5.
 
    // void setId(char *DevAddr, char *DevEUI, char *AppEUI);
    lora.setId(NULL, "00942FBXXXXXXXXX", "70B3D57XXXXXXXXX");   
 
    // setKey(char *NwkSKey, char *AppSKey, char *AppKey);
    lora.setKey(NULL, NULL, "CB89A0AA43F6C5XXXXXXXXXXXXXXXXXX");

Services

This node is connected using the TheThingsNetwork service. Further, a node-red work bench is used to forward this collected data from the TTN platform to the OGC Sensor Things API configured on the FROST Server. The node-red workbench that was used for forwarding the data is available at Node red flow for Indoor Ambient Monitoring sensor node. To use this node-red-workbench go to the node-red platform https://iot.gis.bgu.tum.de:1885/, login with the credentials, go to the options and select Import>Clipboard. Select the downloaded .json file with the given option and click on import. Make necessary changes and deploy the flow.

Datastreams setup for this sensor node on the FROST server can be seen at: http://iot.gis.bgu.tum.de:8081/FROST-Server-gi3/v1.0/Things(21)/Datastreams

The node-red workbench for this sensor node could be found at: https://iot.gis.bgu.tum.de:1885/#flow/7d5c6b14.d2af94

The GRAFANA dash-board for visualizing the collected data is available at: https://iot.gis.bgu.tum.de:3050/d/jDJ1li1Wz/indoor-ambient-monitoring-with-seeeduino-lorawan-and-sensors?orgId=1

Code files

Arduino Sketch for Indoor Ambient Monitoring sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#include <Wire.h>
#include <Digital_Light_TSL2561.h>
#include "Air_Quality_Sensor.h"
#include "seeed_bme680.h"
#include "Seeed_HM330X.h"
#include <SoftwareSerial.h>
#include <MHZ19.h>
#include <RTCZero.h>
#include <LoRaWan.h>
#include <CayenneLPP.h>

RTCZero rtc;
char buffer[256];                       // buffer for text messages received from the LoRaWAN module for display
 
CayenneLPP lpp(51);

AirQualitySensor sensors(A2);

SoftwareSerial ss(4,5);
MHZ19 mhz(&ss);

#define BME_SCK 13
#define BME_MISO 12
#define BME_MOSI 11
#define BME_CS 10

#define IIC_ADDR  uint8_t(0x76)
Seeed_BME680 bme680(IIC_ADDR);

int loudness,a;

HM330X sensor;
u8 buf[30];

const char *str[]={"sensor num: ","PM1.0 concentration(CF=1,Standard particulate matter,unit:ug/m3): ",
                    "PM2.5 concentration(CF=1,Standard particulate matter,unit:ug/m3): ",
                    "PM10 concentration(CF=1,Standard particulate matter,unit:ug/m3): ",
                    "PM1.0 concentration(Atmospheric environment,unit:ug/m3): ",
                    "PM2.5 concentration(Atmospheric environment,unit:ug/m3): ",
                    "PM10 concentration(Atmospheric environment,unit:ug/m3): ",
                    };

err_t print_result(const char* str,u16 value)
{
    if(NULL==str)
        return ERROR_PARAM;
    SerialUSB.print(str);
    SerialUSB.println(value);
    return NO_ERROR;
}

/*parse buf with 29 u8-data*/
err_t parse_result(u8 *data)
{
    u16 value=0;
    err_t NO_ERROR;
    if(NULL==data)
        return ERROR_PARAM;
    for(int i=1;i<8;i++)
    {
         value = (u16)data[i*2]<<8|data[i*2+1];
         print_result(str[i-1],value);
         if(i==6)
           {   a=value;
               SerialUSB.println(a);
           }
    }
}

err_t parse_result_value(u8 *data)
{
    if(NULL==data)
        return ERROR_PARAM;
    for(int i=0;i<28;i++)
    {
        SerialUSB.print(data[i],HEX);
        SerialUSB.print("  ");
        if((0==(i)%5)||(0==i))
        {
            SerialUSB.println(" ");
        }
    }
    u8 sum=0;
    for(int i=0;i<28;i++)
    {
        sum+=data[i];
    }
    if(sum!=data[28])
    {
        SerialUSB.println("wrong checkSum!!!!");
    }
    SerialUSB.println(" ");
    SerialUSB.println(" ");
    return NO_ERROR;
}


void setup()
{ 
  Wire.begin();
  
  for(int i = 0; i < 26; i ++)        // Set all pins to HIGH to save power (reduces the
    {                                   // current drawn during deep sleep by around 0.7mA).
        if (i!=13) {                    // Don't switch on the onboard user LED (pin 13).
          pinMode(i, OUTPUT);
          digitalWrite(i, HIGH);
        }
    }   
     
  delay(5000); 
  
  SerialUSB.begin(115200);
      delay(100);
  SerialUSB.println("SerialUSB start");

  lora.init();                        // Initialize the LoRaWAN module
    
    memset(buffer, 0, 256);             // clear text buffer
    lora.getVersion(buffer, 256, 1);   
    memset(buffer, 0, 256);             // We call getVersion() two times, because after a reset the LoRaWAN module can be
    lora.getVersion(buffer, 256, 1);    // in sleep mode and then the first call only wakes it up and will not be performed.
    SerialUSB.print(buffer);
     
    memset(buffer, 0, 256);
    lora.getId(buffer, 256, 1);
    SerialUSB.print(buffer);
 
    // The following three constants (AppEUI, DevEUI, AppKey) must be changed
    // for every new sensor node. We are using the LoRaWAN OTAA mode (over the
    // air activation). Each sensor node must be manually registered in the
    // TTN console at https://console.thethingsnetwork.org before it can be
    // started. In the TTN console create a new device with the DevEUI also
    // being automatically generated. After the registration of the device the
    // three values can be copied from the TTN console. A detailed explanation
    // of these steps is given in
    // https://learn.adafruit.com/the-things-network-for-feather?view=all
 
    // The EUIs and the AppKey must be given in big-endian format, i.e. the
    // most-significant-byte comes first (as displayed in the TTN console).
    // For TTN issued AppEUIs the first bytes should be 0x70, 0xB3, 0xD5.
 
    // void setId(char *DevAddr, char *DevEUI, char *AppEUI);
    lora.setId(NULL, "00942FBXXXXXXXXX", "70B3D57XXXXXXXXX");   
 
    // setKey(char *NwkSKey, char *AppSKey, char *AppKey);
    lora.setKey(NULL, NULL, "CB89A0AA43F6C5XXXXXXXXXXXXXXXXXX");
     
    lora.setDeciveMode(LWOTAA);           // select OTAA join mode (note that setDeciveMode is not a typo; it is misspelled in the library)
    // lora.setDataRate(DR5, EU868);         // SF7, 125 kbps (highest data rate)
    lora.setDataRate(DR3, EU868);         // SF9, 125 kbps (medium data rate and range)
    // lora.setDataRate(DR0, EU868);         // SF12, 125 kbps (lowest data rate, highest max. distance)
 
    // lora.setAdaptiveDataRate(false);   
    lora.setAdaptiveDataRate(true);       // automatically adapt the data rate
     
    lora.setChannel(0, 868.1);
    lora.setChannel(1, 868.3);
    lora.setChannel(2, 868.5);
    lora.setChannel(3, 867.1);
    lora.setChannel(4, 867.3);
    lora.setChannel(5, 867.5);
    lora.setChannel(6, 867.7);
    lora.setChannel(7, 867.9);
 
    // The following two commands can be left commented out;
    // TTN works with the default values. (It also works when
    // uncommenting the commands, though.)
    // lora.setReceiceWindowFirst(0, 868.1);
    // lora.setReceiceWindowSecond(869.525, DR0);
    
    lora.setDutyCycle(false);             // for debugging purposes only - should normally be activated
    lora.setJoinDutyCycle(false);         // for debugging purposes only - should normally be activated
     
    lora.setPower(14);                    // LoRa transceiver power (14 is the maximum for the 868 MHz band)
     
    // while(!lora.setOTAAJoin(JOIN));
    while(!lora.setOTAAJoin(JOIN,20));    // wait until the node has successfully joined TTN
 
    lora.setPort(33);   
    
    if(sensor.init())
    {
        SerialUSB.println("HM330X init failed!!!");
        while(1);
    }

  if (sensors.init()) {
    SerialUSB.println("Sensor ready.");
  }
  else {
    SerialUSB.println("Sensor ERROR!");
  }
  
  TSL2561.init();

    while (!bme680.init()) 
  {
    SerialUSB.println("bme680 init failed ! can't find device!");
  delay(10000);
  }

  ss.begin(9600);

}

void loop()
{
  bool result = false;
  float temperature,humidity,pressure,airquality,light,gas,CO2;
    
  loudness = analogRead(0);
  SerialUSB.print("The Loudness Sensor value is: ");
  SerialUSB.println(loudness);
  SerialUSB.println();
  delay(3000);
    
  int quality = sensors.slope();

  SerialUSB.print("Air Quality Sensor value is: ");
  SerialUSB.println(airquality=sensors.getValue());
  
  if (quality == AirQualitySensor::FORCE_SIGNAL) {
    SerialUSB.println("High pollution! Force signal active.");
  }
  else if (quality == AirQualitySensor::HIGH_POLLUTION) {
    SerialUSB.println("High pollution!");
  }
  else if (quality == AirQualitySensor::LOW_POLLUTION) {
    SerialUSB.println("Low pollution!");
  }
  else if (quality == AirQualitySensor::FRESH_AIR) {
    SerialUSB.println("Fresh air.");
  }
  SerialUSB.println();
  delay(3000);
  
  SerialUSB.print("The Light Sensor value is: ");
  SerialUSB.println(light=TSL2561.readVisibleLux());
  SerialUSB.println();
  delay(3000);
    
    if(sensor.read_sensor_value(buf,29))
    {
        SerialUSB.println("HM330X read result failed!!!");
    }
    parse_result_value(buf);
    parse_result(buf);
    SerialUSB.println(" ");
    delay(3000);

    if (bme680.read_sensor_data()) 
  {
    SerialUSB.println("Failed to perform reading :(");
    return;
  }
  SerialUSB.print("temperature ===>> ");
  SerialUSB.print(temperature = bme680.sensor_result_value.temperature);
  SerialUSB.println(" C");

  SerialUSB.print("pressure ===>> ");
  SerialUSB.print(pressure = bme680.sensor_result_value.pressure/ 1000.0);
  SerialUSB.println(" KPa");

  SerialUSB.print("humidity ===>> ");
  SerialUSB.print(humidity = bme680.sensor_result_value.humidity);
  SerialUSB.println(" %");

  SerialUSB.print("gas ===>> ");
  SerialUSB.print(gas = bme680.sensor_result_value.gas/ 1000.0);
  SerialUSB.println(" Kohms");

  SerialUSB.println();
  
  delay(3000);

  MHZ19_RESULT response = mhz.retrieveData();
  if (response == MHZ19_RESULT_OK)
  {
    SerialUSB.print(F("CO2: "));
    SerialUSB.println(CO2=mhz.getCO2());
    SerialUSB.print(F("Min CO2: "));
    SerialUSB.println(mhz.getMinCO2());
    SerialUSB.print(F("Temperature: "));
    SerialUSB.println(mhz.getTemperature());
    SerialUSB.print(F("Accuracy: "));
    SerialUSB.println(mhz.getAccuracy());
    SerialUSB.println();
  }
  else
  {
    SerialUSB.print(F("Error, code: "));
    SerialUSB.println(response);
  }
  
    lpp.reset();
    lpp.addTemperature(1, temperature);
    lpp.addRelativeHumidity(2, humidity);
    lpp.addAnalogInput(3, airquality);
    lpp.addLuminosity(4, light);
    lpp.addBarometricPressure(5, pressure);
    lpp.addLuminosity(6, CO2);
    lpp.addAnalogInput(7, gas);
    lpp.addLuminosity(8, loudness);
    lpp.addLuminosity(9, a);   
    result = lora.transferPacket(lpp.getBuffer(), lpp.getSize(), 5);   // send the data packet (n byts) with a default timeout of 5 secs

    if(result)
    {
        short length;
        short rssi;
         
        memset(buffer, 0, 256);
        length = lora.receivePacket(buffer, 256, &rssi);
         
        if(length)
        {
            SerialUSB.print("Length is: ");
            SerialUSB.println(length);
            SerialUSB.print("RSSI is: ");
            SerialUSB.println(rssi);
            SerialUSB.print("Data is: ");
            for(unsigned char i = 0; i < length; i ++)
            {
                SerialUSB.print("0x");
                SerialUSB.print(buffer[i], HEX);
                SerialUSB.print(" ");
            }
            SerialUSB.println();
        }
    }
     
    lora.setDeviceLowPower();     // bring the LoRaWAN module to sleep mode
    doSleep((5*60-8)*1000);       // deep sleep for 292 secs (+ 3 secs transmission time + 5 secs timeout = 300 secs period)
    lora.setPort(33);    

}

void doSleep(uint32_t millis) {
    if (!rtc.isConfigured()) {    // if called for the first time,
        rtc.begin(false);         // then initialize the real time clock (RTC)
    }
 
    uint32_t now = rtc.getEpoch();
    rtc.setAlarmEpoch(now + millis/1000);
    rtc.enableAlarm(rtc.MATCH_HHMMSS);
 
    rtc.standbyMode();            // bring CPU into deep sleep mode (until woken up by the RTC)
}
Node red flow for Indoor Ambient Monitoring sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
[
    {
        "id": "7d5c6b14.d2af94",
        "type": "tab",
        "label": "Device3",
        "disabled": false,
        "info": ""
    },
    {
        "id": "4d581d8.f14c0e4",
        "type": "switch",
        "z": "7d5c6b14.d2af94",
        "name": "Separate",
        "property": "key",
        "propertyType": "msg",
        "rules": [
            {
                "t": "cont",
                "v": "temperature_1",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "humidity",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "analog_in_3",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "luminosity_4",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "barometric",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "luminosity_6",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "analog_in_7",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "luminosity_8",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "luminosity_9",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 9,
        "x": 220,
        "y": 180,
        "wires": [
            [
                "92425d9f.bca98"
            ],
            [
                "9b919750.d6fff8"
            ],
            [
                "620b1ab2.a69224"
            ],
            [
                "eee677bf.fe16d8"
            ],
            [
                "4b424590.139b4c"
            ],
            [
                "bc3f8433.7b89f8"
            ],
            [
                "13968bca.c2cd34"
            ],
            [
                "c7fcb372.4b2df"
            ],
            [
                "b52fa683.7c5fb8"
            ]
        ]
    },
    {
        "id": "9010a80d.dfcdb8",
        "type": "split",
        "z": "7d5c6b14.d2af94",
        "name": "",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "key",
        "x": 90,
        "y": 180,
        "wires": [
            [
                "4d581d8.f14c0e4"
            ]
        ]
    },
    {
        "id": "a3b86e6f.56637",
        "type": "debug",
        "z": "7d5c6b14.d2af94",
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 870,
        "y": 240,
        "wires": []
    },
    {
        "id": "9b919750.d6fff8",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Humidity",
        "func": "var humValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": humValue, \"Datastream\": {\"@iot.id\": 113}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 440,
        "y": 200,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "c67491fc.f4755",
        "type": "http request",
        "z": "7d5c6b14.d2af94",
        "name": "POST Observation",
        "method": "POST",
        "ret": "obj",
        "paytoqs": false,
        "url": "http://iot.gis.bgu.tum.de:8081/FROST-Server-gi3/v1.0/Observations",
        "tls": "",
        "proxy": "",
        "authType": "basic",
        "x": 690,
        "y": 240,
        "wires": [
            [
                "a3b86e6f.56637"
            ]
        ]
    },
    {
        "id": "92425d9f.bca98",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Temperature",
        "func": "var tempValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": tempValue, \"Datastream\": {\"@iot.id\": 112}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 450,
        "y": 160,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "a16b3beb.1a5028",
        "type": "debug",
        "z": "7d5c6b14.d2af94",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 490,
        "y": 60,
        "wires": []
    },
    {
        "id": "681442b7.1266ac",
        "type": "ttn uplink",
        "z": "7d5c6b14.d2af94",
        "name": "TTN Input",
        "app": "58ceff1f.8576a",
        "dev_id": "tum-gis-device3",
        "field": "",
        "x": 80,
        "y": 60,
        "wires": [
            [
                "531b0b35.751284"
            ]
        ]
    },
    {
        "id": "531b0b35.751284",
        "type": "cayennelpp-decoder",
        "z": "7d5c6b14.d2af94",
        "name": "",
        "x": 260,
        "y": 60,
        "wires": [
            [
                "9010a80d.dfcdb8",
                "a16b3beb.1a5028"
            ]
        ]
    },
    {
        "id": "620b1ab2.a69224",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Air Quality",
        "func": "var quality = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": quality, \"Datastream\": {\"@iot.id\": 119}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 450,
        "y": 240,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "eee677bf.fe16d8",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Light",
        "func": "var light = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": light, \"Datastream\": {\"@iot.id\": 117}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 430,
        "y": 280,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "4b424590.139b4c",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Barometric Pressure",
        "func": "var pressure = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": pressure, \"Datastream\": {\"@iot.id\": 114}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 480,
        "y": 320,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "bc3f8433.7b89f8",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "co2",
        "func": "var co2 = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": co2, \"Datastream\": {\"@iot.id\": 118}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 430,
        "y": 360,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "13968bca.c2cd34",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Gas",
        "func": "var gas = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": gas, \"Datastream\": {\"@iot.id\": 121}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 430,
        "y": 400,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "c7fcb372.4b2df",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Loudness",
        "func": "var loudness = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": loudness, \"Datastream\": {\"@iot.id\": 120}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 440,
        "y": 440,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "b52fa683.7c5fb8",
        "type": "function",
        "z": "7d5c6b14.d2af94",
        "name": "Dust-PM2.5",
        "func": "var Dust = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": Dust, \"Datastream\": {\"@iot.id\": 128}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 450,
        "y": 480,
        "wires": [
            [
                "c67491fc.f4755"
            ]
        ]
    },
    {
        "id": "58ceff1f.8576a",
        "type": "ttn app",
        "z": "",
        "appId": "gis-tum-sensors",
        "accessKey": "ttn-account-ACCESSKEY_HERE",
        "discovery": "discovery.thethingsnetwork.org:1900"
    }
]

Feather M0 LoRa in TFA Housing

This sensor node is made to showcase a use-case of LoRaWAN technology for outdoor weather monitoring. For achieving this a Feather M0 LoRa module was used with temperature and pressure sensor. The entire setup was carefully placed in the TFA Housing which is an all-weather protective cover for outdoor transmitters. In this example we measure parameters such as temperature, humidity, altitude, and air pressure.

_images/setup1.jpg

Sensor node in TFA Housing.

Hardware

To build this sensor node we have used following hardware components:

_images/setup-insideview.jpg

Inside view of Sensor node in TFA Housing

Also, as the final hardware setup with antenna couldn’t completely fit into the casing, a small hole was made at the bottom of the casing to allow the remaining portion of antenna to stay outside.

_images/setup-bottom.jpg

Bottom view of Sensor node in TFA Housing

Wiring setup

First of all, the Feather M0 LoRa board was prepared by soldering the board with the provided grid of pins. Then the board is connected with the sensors using a breadboard. The sensor connections were made using the connector cables as following:

DHT-22 Sensor connections:
  • Feather 3V to DHT22 pin 1
  • Feather GND to DHT22 pin 4
  • Feather pin 12 to DHT22 pin 2
  • Resistor between DHT pin 1 and DHT pin 2
_images/feather_wiring_hero.png

Wiring with DHT-22 Sensor

Grove-Barometer Sensor connections:
  • Feather SCL to Barometer Sensor pin 1 (yellow)
  • Feather SDA to Barometer Sensor pin 2 (white)
  • Feather 3V to Barometer Sensor pin 3 (red)
  • Feather GND to Barometer Sensor pin 4 (black)

Apart from this, Feather pin 6 should be permanently wired with Feather pin io1 as shown in the figure above.

To ensure the durable connections, smaller jumper wires were used on the breadboard instead of longer connecting cables. Sensors and cables were also supported with an insulating duct tape.

Final hardware setup looked as following:

_images/hardware.png

Final hardware wiring

Once all these connection were made, the board is connected with a computer using a USB cable. Further, steps of software part needs to be followed.

Software

To create this node, we use Arduino IDE for setting up the Feather M0 LoRa module. First, install the Feather M0 LoRa board to your Arduino IDE and select the correct port. Then following libraries needs to be installed before compiling the code:

  • lmic.h for implementing LoRaWAN on Arduino hardware.
  • hal/hal.h bundled with lmic library.
  • Adafruit_SleepyDog.h for controlling low power sleep mode.
  • Wire.h to communicate with I2C devices.
  • BMP085.h for Barometer sensor.
  • DHT.h for reading DHT-22 sensor.
  • CayenneLPP.h for Cayenne Protocol.

Apart from this, SPI.h library is also used for communicating with serial peripheral interface but it is already inbuilt in Arduino IDE and is not required to be separately installed.

Now download and run the Arduino Sketch for Outdoor Weather Monitoring sensor node file in the Arduino IDE. This code was created by merging the example code of both the sensors and the ttn-otaa example from the lmic library. Some required changes were made while merging the example codes. The user should change the network session key, app session key and device address in the code before compiling. These keys can be obtained from the TTN, SWM or other service providers.

Modify the keys in highlighted lines.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// LoRaWAN NwkSKey, network session key
// This should be in big-endian (aka msb).
static const PROGMEM u1_t NWKSKEY[16] = {NETWORK_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN AppSKey, application session key
// This should also be in big-endian (aka msb).
static const u1_t PROGMEM APPSKEY[16] = {APPLICATION_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed, so this should be in big-endian (aka msb) too.
static const u4_t DEVADDR = 0x260XXXXX   ; // <-- Change this address for every node!

The pin mapping configured in the code should also be verified for the board that is being used. Current pin mapping is set as per the Feather M0 LoRa board.

Set the correct pin mapping for the board that is used.
1
2
3
4
5
6
// Pin mapping
const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {3, 6, LMIC_UNUSED_PIN},

Following is the example code that can be used to measure the battery voltage of the Feather M0 LoRa board:

Code for measuring the battery voltage
1
2
3
4
5
6
7
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2;    // we divided by 2, so multiply back
            measuredvbat *= 3.3;  // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1024; // convert to voltage

            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Battery Voltage: ");

Services

This node is connected using the TheThingsNetwork service. Further, a node-red work bench is used to forward this collected data from the TTN platform to the OGC Sensor Things API configured on the FROST Server. The node-red workbench that was used for forwarding the data is available at Node red flow for Outdoor Weather Monitoring sensor node. To use this node-red-workbench go to the node-red platform https://iot.gis.bgu.tum.de:1885/, login with the credentials, go to the options and select Import>Clipboard. Select the downloaded .json file with the given option and click on import. Make necessary changes and deploy the flow.

Datastreams setup for this sensor node on the FROST server can be seen at: http://iot.gis.bgu.tum.de:8081/FROST-Server-gi3/v1.0/Things(20)/Datastreams

The node-red workbench for this sensor node could be found at: https://iot.gis.bgu.tum.de:1885/#flow/f6f7a740.c6b338

The GRAFANA dash-board for visualizing the collected data is available at: https://iot.gis.bgu.tum.de:3050/d/sMJ3jAAWz/featherm0lora-in-tfa-housing?orgId=1

Code files

Arduino Sketch for Outdoor Weather Monitoring sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
 /*******************************************************************************
 * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
 *
 *
 *
 *
 * Permission is hereby granted, free of charge, to anyone
 * obtaining a copy of this document and accompanying files,
 * to do whatever they want with them without any restriction,
 * including, but not limited to, copying, modification and redistribution.
 * NO WARRANTY OF ANY KIND IS PROVIDED.
 *
 * This example sends a valid LoRaWAN packet with payload "Hello,
 * world!", using frequency and encryption settings matching those of
 * the The Things Network.
 *
 * This uses ABP (Activation-by-personalisation), where a DevAddr and
 * Session keys are preconfigured (unlike OTAA, where a DevEUI and
 * application key is configured, while the DevAddr and session keys are
 * assigned/generated in the over-the-air-activation procedure).
 *
 * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
 * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
 * violated by this sketch when left running for longer)!
 *
 * To use this sketch, first register your application and device with
 * the things network, to set or generate a DevAddr, NwkSKey and
 * AppSKey. Each device should have their own unique values for these
 * fields.
 *
 * Do not forget to define the radio type correctly in config.h.
 *
 *******************************************************************************/
// #define SERIALDEBUG
 
#ifdef SERIALDEBUG
  #define SERIALDEBUG_PRINT(...) Serial.print(__VA_ARGS__)
  #define SERIALDEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#else
  #define SERIALDEBUG_PRINT(...)
  #define SERIALDEBUG_PRINTLN(...)
#endif
 
 
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
#include <Adafruit_SleepyDog.h>
#include <DHT.h>
#include <CayenneLPP.h>
#include "BMP085.h"
#include <Wire.h>

CayenneLPP lpp(51);
 
#define DHTPIN            12        // Pin which is connected to the DHT sensor.
#define DHTTYPE           DHT22     // DHT 22 (AM2302)
 
// DHT_Unified dht(DHTPIN, DHTTYPE);
DHT dht(DHTPIN, DHTTYPE);
 
#define VBATPIN A7

float temperature2;
float pressure;
float atm;
float altitude;
BMP085 myBarometer;
 
// LoRaWAN NwkSKey, network session key
// This should be in big-endian (aka msb).
static const PROGMEM u1_t NWKSKEY[16] = {NETWORK_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN AppSKey, application session key
// This should also be in big-endian (aka msb).
static const u1_t PROGMEM APPSKEY[16] = {APPLICATION_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed, so this should be in big-endian (aka msb) too.
static const u4_t DEVADDR = 0x260XXXXX   ; // <-- Change this address for every node!
 
// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }
 
static osjob_t sendjob;
 
// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL = 1;       // seconds transmit cycle plus ...
const unsigned SLEEP_TIME = 60*9+55;  // seconds sleep time plus ...
const unsigned MEASURE_TIME = 2;      // seconds measuring time should lead to ...
                                      // 5 minute(s) total cycle time
 
// Pin mapping
const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {3, 6, LMIC_UNUSED_PIN},
};
 
 
void onEvent (ev_t ev) {
//    Serial.print(os_getTime());
//    Serial.print(": ");
    SERIALDEBUG_PRINT(os_getTime());
    SERIALDEBUG_PRINT(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
            SERIALDEBUG_PRINTLN(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            SERIALDEBUG_PRINTLN(F("EV_JOINING"));
            break;
        case EV_JOINED:
            SERIALDEBUG_PRINTLN(F("EV_JOINED"));
            break;
        case EV_RFU1:
            SERIALDEBUG_PRINTLN(F("EV_RFU1"));
            break;
        case EV_JOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
            SERIALDEBUG_PRINTLN(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
            if (LMIC.txrxFlags & TXRX_ACK)
              SERIALDEBUG_PRINTLN(F("Received ack"));
            if (LMIC.dataLen) {
              SERIALDEBUG_PRINT(F("Received "));
              SERIALDEBUG_PRINT(LMIC.dataLen);
              SERIALDEBUG_PRINTLN(F(" bytes of payload"));
            }
            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
             
            SERIALDEBUG_PRINTLN("going to sleep now ... ");
            // lmic library sleeps automatically after transmission has been completed
            for(int i= 0; i < SLEEP_TIME / 16; i++) {
              Watchdog.sleep(16000); // maximum seems to be 16 seconds
              SERIALDEBUG_PRINT('.');
            }
            if (SLEEP_TIME % 16) {
              Watchdog.sleep((SLEEP_TIME % 16)*1000);
              SERIALDEBUG_PRINT('*');             
            }
            SERIALDEBUG_PRINTLN("... woke up again");
             
            break;
        case EV_LOST_TSYNC:
            SERIALDEBUG_PRINTLN(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            SERIALDEBUG_PRINTLN(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            SERIALDEBUG_PRINTLN(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            SERIALDEBUG_PRINTLN(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            SERIALDEBUG_PRINTLN(F("EV_LINK_ALIVE"));
            break;
         default:
            SERIALDEBUG_PRINTLN(F("Unknown event"));
            break;
    }
}
 
void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) {
        SERIALDEBUG_PRINTLN(F("OP_TXRXPEND, not sending"));
    } else {
        // Prepare upstream data transmission at the next possible time.
 
        float temperature, humidity, measuredvbat;
        int16_t int16_temperature, int16_humidity, int16_vbat;
     
        // Start a measurement to update the sensor's internal temperature & humidity reading
        SERIALDEBUG_PRINTLN("Start measurement...");
        temperature = dht.readTemperature();
        // delay(2000);
        Watchdog.sleep(2000);
        // Now read the recently measured temperature (2 secs ago) as Celsius (the default)
        temperature = dht.readTemperature();
        // Read the recently measured humidity (2 secs ago)
        humidity = dht.readHumidity();
        SERIALDEBUG_PRINTLN("... finished!");
     
        // Check if any reads failed and exit early (to try again).
        if (isnan(humidity) || isnan(temperature)) {
            SERIALDEBUG_PRINTLN("Failed to read from DHT sensor!");
            for (int i=0; i<5; i++) {
              digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH                   
              delay(150);
              digitalWrite(LED_BUILTIN, LOW);    // turn the LED on by making the voltage HIGH                   
              delay(150);
            }
            // ok, then wait for another period and try it again
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
        } else {
            SERIALDEBUG_PRINT("Humidity: ");
            SERIALDEBUG_PRINT(humidity);
            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Temperature: ");
            SERIALDEBUG_PRINT(temperature);
            SERIALDEBUG_PRINT(" *C ");
 
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2;    // we divided by 2, so multiply back
            measuredvbat *= 3.3;  // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1024; // convert to voltage

            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINTLN(measuredvbat);

            temperature2 = myBarometer.bmp085GetTemperature(myBarometer.bmp085ReadUT()); //Get the temperature, bmp085ReadUT MUST be called first
            pressure = myBarometer.bmp085GetPressure(myBarometer.bmp085ReadUP());//Get the temperature
   
           /*
            To specify a more accurate altitude, enter the correct mean sea level
            pressure level.  For example, if the current pressure level is 1019.00 hPa
            enter 101900 since we include two decimal places in the integer value。
           */
           altitude = myBarometer.calcAltitude(pressure); 
           
           atm = pressure / 101325;
        
           lpp.reset();
           lpp.addTemperature(1, temperature);
           lpp.addRelativeHumidity(2, humidity);
           lpp.addAnalogInput(3, measuredvbat);
           lpp.addTemperature(4, temperature2);
           lpp.addBarometricPressure(5, pressure/100);
           lpp.addAnalogInput(6, atm);
           lpp.addAnalogInput(7, altitude);
            
//            LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
 
            // send the 6 bytes payload to LoRaWAN port 7
            LMIC_setTxData2(7, lpp.getBuffer(), lpp.getSize(), 0);
            SERIALDEBUG_PRINTLN(F("Packet queued")); 
            digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH
        }
         
        // LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
        // Serial.println(F("Packet queued"));
    }
    // Next TX is scheduled after TX_COMPLETE event.
}
 
void setup() {
    delay(5000);
 
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
 
#ifdef SERIALDEBUG
    Serial.begin(9600);
    // while (!Serial);
#endif
 
    dht.begin();
    myBarometer.init(); 
    SERIALDEBUG_PRINTLN(F("Starting"));
 
    #ifdef VCC_ENABLE
    // For Pinoccio Scout boards
    pinMode(VCC_ENABLE, OUTPUT);
    digitalWrite(VCC_ENABLE, HIGH);
    delay(1000);
    #endif
 
    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
 
    // Set static session parameters. Instead of dynamically establishing a session
    // by joining the network, precomputed session parameters are be provided.
    #ifdef PROGMEM
    // On AVR, these values are stored in flash and only copied to RAM
    // once. Copy them to a temporary buffer here, LMIC_setSession will
    // copy them into a buffer of its own again.
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
    #else
    // If not running an AVR with PROGMEM, just use the arrays directly
    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
    #endif
 
    #if defined(CFG_eu868)
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.
    // NA-US channels 0-71 are configured automatically
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.
    #elif defined(CFG_us915)
    // NA-US channels 0-71 are configured automatically
    // but only one group of 8 should (a subband) should be active
    // TTN recommends the second sub band, 1 in a zero based count.
    // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
    LMIC_selectSubBand(1);
    #endif
 
    // 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
    do_send(&sendjob);
}
 
void loop() {
    os_runloop_once();
}
Node red flow for Outdoor Weather Monitoring sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
[
    {
        "id": "f6f7a740.c6b338",
        "type": "tab",
        "label": "Device 2",
        "disabled": false,
        "info": ""
    },
    {
        "id": "fafe9ad3.9659e8",
        "type": "switch",
        "z": "f6f7a740.c6b338",
        "name": "Separate",
        "property": "key",
        "propertyType": "msg",
        "rules": [
            {
                "t": "cont",
                "v": "temperature_1",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "humidity",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "analog_in_3",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "temperature_4",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "barometric",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "analog_in_6",
                "vt": "str"
            },
            {
                "t": "cont",
                "v": "analog_in_7",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 7,
        "x": 220,
        "y": 180,
        "wires": [
            [
                "492a1844.49a228"
            ],
            [
                "b5be1839.3121a8"
            ],
            [
                "d7e35050.187eb"
            ],
            [
                "c5363ad1.5d3418"
            ],
            [
                "ee2891fa.0dbbe"
            ],
            [
                "71354cb4.e6af04"
            ],
            [
                "d48c0c97.4eb08"
            ]
        ]
    },
    {
        "id": "ccb2fb81.aacd58",
        "type": "split",
        "z": "f6f7a740.c6b338",
        "name": "",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "key",
        "x": 90,
        "y": 180,
        "wires": [
            [
                "fafe9ad3.9659e8"
            ]
        ]
    },
    {
        "id": "657fd8a7.01c5e8",
        "type": "debug",
        "z": "f6f7a740.c6b338",
        "name": "",
        "active": false,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "x": 870,
        "y": 240,
        "wires": []
    },
    {
        "id": "b5be1839.3121a8",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Humidity",
        "func": "var humValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": humValue, \"Datastream\": {\"@iot.id\": 106}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 440,
        "y": 200,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "dd5d521b.5c984",
        "type": "http request",
        "z": "f6f7a740.c6b338",
        "name": "POST Observation",
        "method": "POST",
        "ret": "obj",
        "paytoqs": false,
        "url": "http://iot.gis.bgu.tum.de:8081/FROST-Server-gi3/v1.0/Observations",
        "tls": "",
        "proxy": "",
        "authType": "basic",
        "x": 690,
        "y": 240,
        "wires": [
            [
                "657fd8a7.01c5e8"
            ]
        ]
    },
    {
        "id": "492a1844.49a228",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Temperature",
        "func": "var tempValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": tempValue, \"Datastream\": {\"@iot.id\": 105}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 450,
        "y": 160,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "739d03d0.606a6c",
        "type": "debug",
        "z": "f6f7a740.c6b338",
        "name": "",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "x": 490,
        "y": 60,
        "wires": []
    },
    {
        "id": "cb8ef1e2.a85f6",
        "type": "ttn uplink",
        "z": "f6f7a740.c6b338",
        "name": "TTN Input",
        "app": "58ceff1f.8576a",
        "dev_id": "tum-gis-device2",
        "field": "",
        "x": 80,
        "y": 60,
        "wires": [
            [
                "aae507e3.771c18"
            ]
        ]
    },
    {
        "id": "aae507e3.771c18",
        "type": "cayennelpp-decoder",
        "z": "f6f7a740.c6b338",
        "name": "",
        "x": 260,
        "y": 60,
        "wires": [
            [
                "ccb2fb81.aacd58",
                "739d03d0.606a6c"
            ]
        ]
    },
    {
        "id": "d7e35050.187eb",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Battery Voltage",
        "func": "var Batteryvolt = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": Batteryvolt, \"Datastream\": {\"@iot.id\": 107}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 460,
        "y": 240,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "c5363ad1.5d3418",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Temperature2",
        "func": "var tempValue = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": tempValue, \"Datastream\": {\"@iot.id\": 108}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 460,
        "y": 280,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "ee2891fa.0dbbe",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Barometric Pressure",
        "func": "var pressure = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": pressure, \"Datastream\": {\"@iot.id\": 109}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 480,
        "y": 320,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "71354cb4.e6af04",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Pressure atm",
        "func": "var atm = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": atm, \"Datastream\": {\"@iot.id\": 110}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 450,
        "y": 360,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "d48c0c97.4eb08",
        "type": "function",
        "z": "f6f7a740.c6b338",
        "name": "Altitude",
        "func": "var altitude = msg.payload.valueOf();\nvar newMessage =  { payload: {  \"result\": altitude, \"Datastream\": {\"@iot.id\": 111}} };\nnewMessage.headers = {\"Content-type\" : \"application/json\"}\nreturn newMessage;",
        "outputs": 1,
        "noerr": 0,
        "x": 440,
        "y": 400,
        "wires": [
            [
                "dd5d521b.5c984"
            ]
        ]
    },
    {
        "id": "58ceff1f.8576a",
        "type": "ttn app",
        "z": "",
        "appId": "gis-tum-sensors",
        "accessKey": "ttn-account-ACCESSKEY_HERE",
        "discovery": "discovery.thethingsnetwork.org:1900"
    }
]

Adafruit 32u4 LoRa

This tutorial is made to showcase the use of Adafruit 32u4 board to create a LoRaWAN enabled sensor node. In the following example, a temperature and humidity sensor was used with the Adafruit 32u4 board.

Hardware

To build this sensor node we have used following hardware components:

Microcontroller

The Adafruit Feather 32u4 LoRa module is operated by the 8bit ATmega32u4 microcontroller running at 8MHz. It has 32 KB flash memory (to store the program code) and 2 KB of RAM (to store variables, status information, and buffers). The operating voltage of the board is 3.3V (this is important when attaching sensors and other peripherals; they also must operate on 3.3V). The board offers 20 general purpose digital input/output pins (20 GPIOs) with 10 analog input pins (with 12bit analog digital converters (ADC)), one serial port (programmable Universal Asynchronous Receiver and Transmitter, UART), one I2C port, one SPI port, one USB port. The board comes with an embedded Lithium polymer battery management chip and status indicator led, which allows to directly connect a 3.7V LiPo rechargeable battery that will be automatically recharged when the board is powered over its USB connector. The Adafruit Feather 32u4 LoRa board is available in German shops from around 37 € to 45 €.

The LoRa transmitter and receiver is encapsulated within an RFM95 module from the company HopeRF. This module uses the LoRa chip SX1276 from the company Semtech and is dedicated to the 868 MHz frequency band. The RFM95 module is connected via SPI interface to the microcontroller. Most of the required connections of the LoRa transceiver pins with the microcontroller are already built-in on the Adafruit Feather 32u4 LoRa board. However, Digital Pin 6 of the microcontroller must be connected to DIO1 of the LoRa transceiver module in addition using a simple wire. Since the module only implements the LoRa physical layer, the LoRaWAN protocol stack must be implemented in software on the microcontroller. We are using the Arduino library LMIC for that purpose (see below). The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C.

Sensor

We have attached a DHT22 sensor to the microcontroller board, which measures air temperature and humidity. The minimal time interval between two measurements is 2 seconds. All data transfers between the DHT22 and the microcontroller use a single digital line. The sensor data pin is attached to a GPIO pin (here: Digital Pin 5) of the microcontroller. In addition, a so-called pull-up resistor of 4.7k to 10k Ohm must be connected between the data line and VCC (+3.3V). The DHT22 datasheet provides more technical details about the DHT22 Sensor. A tutorial on how to use the DHT22 sensor with Arduino microcontrollers is provided here. The sensor is available in German shops for around 4 € to 10 €.

_images/setup.png

The Adafruit Feather 32u4 RFM95 LoRa with attached antenna (top), a 1000 mAh lithium polymer (LiPo) battery (bottom), and an attached DHT22 temperature / humidity sensor (white box on the left)

For more details on the wiring connections, follow this tutorial. Once all these connection are made, the board is connected with a computer using a USB cable. Further, steps of software part needs to be followed. But, before that we need to register a new device on the service that we are using.

Software

The sensor node has been programmed using the Arduino IDE. Please note, that in the Arduino framework a program is called a ‘Sketch’.

After the sketch has successfully established a connection to The Things Network it reports the air temperature, humidity, and the voltage of a (possibly) attached LiPo battery every 5 minutes. All three values are being encoded in two byte integer values each (in most significant byte order) and then sent as a 6 bytes data packet to the respective TTN application using LoRaWAN port 7. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement; this must be considered in the Payload Decoding Function used in The Things Network (see here).

In between two sensor readings the microcontroller is going into deep sleep mode to save battery power. With a 1000 mAh LiPo battery and the current version of the sketch the system can run for at least 5 months. (Further optimizations would be possible, for example, not switching on the LED on the microcontroller board during LoRa data transmissions.)

The employed RFM95 LoRa module does not provide built-in support of the LoRaWAN protocol. Thus, it has to be implemented on the ATmega32u4 microcontroller. We use the IBM LMIC (LoraMAC-in-C) library for Arduino. Since the ATmega32u4 microcontroller only has 32 KB of flash memory and the LMIC library is taking most of it, there is only very limited code space left for the application dealing with the sensors (about 2 KB). Nevertheless, this is sufficient to query some sensors like in our example the DHT22.

Now download and run the Arduino Sketch for Adafruit32u4 LoRa sensor node file in the Arduino IDE. This code was created by merging the example code of both the sensors and the ttn-otaa example from the lmic library. Some required changes were made while merging the example codes. The user should change the network session key, app session key and device address in the code before compiling. These keys can be obtained from the TTN account as shown in the services section.

Modify the keys in highlighted lines.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// LoRaWAN NwkSKey, network session key
// This should be in big-endian (aka msb).
static const PROGMEM u1_t NWKSKEY[16] = {NETWORK_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN AppSKey, application session key
// This should also be in big-endian (aka msb).
static const u1_t PROGMEM APPSKEY[16] = {APPLICATION_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed, so this should be in big-endian (aka msb) too.
static const u4_t DEVADDR = 0x260XXXXX   ; // <-- Change this address for every node!

Following is the example code that can be used to measure the battery voltage of the sensor node:

Code for measuring the battery voltage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2;    // we divided by 2, so multiply back
            measuredvbat *= 3.3;   // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1023;  // convert to voltage
            int16_vbat = round(measuredvbat * 100);
            mydata[4] = (byte) (int16_vbat >> 8);
            mydata[5] = (byte) (int16_vbat & 0x00FF);
            SERIALDEBUG_PRINT(" \t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINT(measuredvbat);
            SERIALDEBUG_PRINTLN(" V");

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The sketch given above connects the sensor node with The Things Network (TTN) using the Activation-by-Personalisation (ABP) mode. In this mode, the required keys for data encryption and session management are created manually using the TTN console window and must be pasted into the source code of the sketch below. In order to get this running, you will need to create a new device in the TTN console window. This assumes that you already have a TTN user account (which needs to be created otherwise). In the settings menu of the newly created device the ABP mode must be selected and the settings must be saved. Then copy the DevAddr, the NwkSKey, and the AppSKey from the TTN console web page of the newly registered device and paste them into the proper places in the sketch above. Please make sure that you choose for each of the three keys the correct byte ordering (MSB for all three keys). A detailed explanation of these steps is given here. Then the sketch can be compiled and uploaded to the Adafruit Feather 32u4 LoRa microcontroller.

Important hint: everytime the sensor node is reset or being started again, make sure to reset the frame counter of the registered sensor in the TTN console web page of the registered device. The reason is that in LoRaWAN all transmitted data packets have a frame counter, which is incremented after each data frame being sent. This way a LoRaWAN application can avoid receiving and using the same packet again (replay attack). When TTN receives a data packet, it checks if the frame number is higher than the last one received before. If not, the received packet is considered to be old or a replay attack and is discarded. When the sensor node is reset or being started again, its frame counter is also reset to 0, hence, the TTN application assumes that all new packages are old, because their frame counter is lower than the last frame received (before the reset). A manual frame counter reset is only necessary when registering the node using ABP mode. In OTAA mode the frame counter is automatically reset in the sensor node and the TTN network server.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (Payload Decoder Function). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak ). Such a forwarding can be configured in the TTN console in the “Integrations” tab. TTN payload decoder for Adafruit32u4 LoRa sensor node given here checks if a packet was received on LoRaWAN port 7 and then assumes that it consists of the 6 bytes encoded as described above. It creates the three Javascript objects ‘temperature’, ‘humidity’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==7) {…} statement by adding else if (port==8) {…} else if (port==9) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this page. Here is a screenshot showing the webclient:

_images/webclient.png

Web client for data visualization

Code files

Arduino Sketch for Adafruit32u4 LoRa sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
/*******************************************************************************
 * Arduino Sketch for a LoRaWAN sensor node that is registered with
 * 'The Things Network' (TTN) www.thethingsnetwork.org
 *
 * Author:  Thomas H. Kolbe, thomas.kolbe@tum.de
 * Version: 1.0
 * Last update: 2018-05-21
 *
 * The sensor node is based on the Adafruit Feather 32u4 LoRa microcontroller
 * board https://learn.adafruit.com/adafruit-feather-32u4-radio-with-lora-radio-module/
 * The sensor node uses a DHT22 sensor measuring air temperature and humidity.
 * Also the voltage of an attached LiPo battery is monitored and sent as
 * an observation. All three values are encoded as 2 byte integer values each.
 * Hence, the total message payload is 6 bytes. Before the values are converted
 * to integers they are multiplied by 100 to preserve 2 digits after the decimal
 * point. Thus, the received values must be divided by 100 to obtain the measured
 * values. The payload is sent every 300s to LoRaWAN port 7. The following
 * Javascript function can be used as a payload decoding function in TTN:
 *
 * function Decoder(bytes, port) {
 *   // Decode an uplink message from a buffer
 *   // (array) of bytes to an object of fields.
 *   if (port==7) {
 *     var decoded = {
 *       "temperature": (bytes[0] << 8 | bytes[1]) / 100.0,
 *       "humidity": (bytes[2] << 8 | bytes[3]) / 100.0,
 *       "vbattery": (bytes[4] << 8 | bytes[5]) / 100.0
 *     };
 *   } else {
 *     var decoded = null;
 *   }
 *   return decoded;
 * }
 *
 * In between two data transmissions the microcontroller board can go
 * into sleep mode to reduce energy consumption for extended operation
 * time when running on battery. Usage of the sleep mode must be
 * explicitly configured below.
 *
 * Important hint: everytime the sensor node is reset or being started again,
 * make sure to reset the frame counter of the registered sensor in the
 * TTN console at https://console.thethingsnetwork.org. The reason is that
 * in LoRaWAN all packets that are transmitted have a frame counter, which
 * is incremented after each data frame being sent. This way a LoRaWAN application
 * can avoid receiving and using the same packet again (replay attack). When
 * TTN receives a data packet, it checks if the frame number is higher than
 * the last one received before. If not, the received packet is considered
 * to be old or a replay attack and is discarded. When the sensor node is
 * reset or being started again, its frame counter is also reset to 0, hence,
 * the TTN application assumes that all new packages are old, because their
 * frame counter is lower than the last frame received (before the reset).
 *
 * Note, that the DHT22 data pin must be connected to Digital Pin 5 of the
 * microcontroller board. A resistor of 4.7k - 10k Ohm must be connected to
 * the data pin and VCC (+3.3V). Digital Pin 6 must be connected to IO1 of the
 * LoRa transceiver module using a simple wire.
 *
 * The code is based on the Open Source library LMIC implementing the LoRaWAN
 * protocol stack on top of a given LoRa transceiver module (here: RFM95 from
 * HopeRF, which uses the Semtech SX1276 LoRa chip). The library is originally
 * being developed by IBM and has been ported to the Arduino platform. See
 * notes below from the original developers.
 *
 *******************************************************************************
 * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
 *
 * Permission is hereby granted, free of charge, to anyone
 * obtaining a copy of this document and accompanying files,
 * to do whatever they want with them without any restriction,
 * including, but not limited to, copying, modification and redistribution.
 * NO WARRANTY OF ANY KIND IS PROVIDED.
 *
 * This uses ABP (Activation-by-personalisation), where a DevAddr and
 * Session keys are preconfigured (unlike OTAA, where a DevEUI and
 * application key is configured, while the DevAddr and session keys are
 * assigned/generated in the over-the-air-activation procedure).
 *
 * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
 * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
 * violated by this sketch when left running for longer)!
 *
 * To use this sketch, first register your application and device with
 * the things network, to set or generate a DevAddr, NwkSKey and
 * AppSKey. Each device should have their own unique values for these
 * fields.
 *
 * Do not forget to define the radio type correctly in config.h.
 *
 *******************************************************************************/
 
// If the following line is uncommented, messages are being printed out to the
// serial connection for debugging purposes. When using the Arduino Integrated
// Development Environment (Arduino IDE), these messages are displayed in the
// Serial Monitor selecting the proper port and a baudrate of 115200.
 
//#define SERIALDEBUG
 
#ifdef SERIALDEBUG
  #define SERIALDEBUG_PRINT(...) Serial.print(__VA_ARGS__)
  #define SERIALDEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#else
  #define SERIALDEBUG_PRINT(...)
  #define SERIALDEBUG_PRINTLN(...)
#endif
 
// If the following line is uncommented, the sensor node goes into sleep mode
// in between two data transmissions. Also the 2secs time between the
// initialization of the DHT22 sensor and the reading of the observations
// is spent in sleep mode.
// Note, that on the Adafruit Feather 32u4 LoRa board the Serial connection
// gets lost as soon as the board goes into sleep mode, and it will not be
// established again. Thus, the definition of SERIALDEBUG should be commented
// out above when using sleep mode.
 
#define SLEEPMODE
 
#ifdef SLEEPMODE
#include <Adafruit_SleepyDog.h>
#endif
 
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
 
#include <DHT.h>
 
#define DHTPIN            5       // Arduino Digital Pin which is connected to the DHT sensor.
#define DHTTYPE           DHT22   // DHT 22 (AM2302)
 
DHT dht(DHTPIN, DHTTYPE);         // create the sensor object
 
#define VBATPIN A9                // battery voltage is measured from Analog Input A9
 
// The following three constants (NwkSKey, AppSKey, DevAddr) must be changed
// for every new sensor node. We are using the LoRaWAN ABP mode (activation by
// personalisation) which means that each sensor node must be manually registered
// in the TTN console at https://console.thethingsnetwork.org before it can be
// started. In the TTN console create a new device and choose ABP mode in the
// settings of the newly created device. Then, let TTN generate the NwkSKey and
// and the AppSKey and copy them (together with the device address) from the webpage
// and paste them below.
 
// LoRaWAN NwkSKey, network session key
// This should be in big-endian (aka msb).
static const PROGMEM u1_t NWKSKEY[16] = {NETWORK_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN AppSKey, application session key
// This should also be in big-endian (aka msb).
static const u1_t PROGMEM APPSKEY[16] = {APPLICATION_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed, so this should be in big-endian (aka msb) too.
static const u4_t DEVADDR = 0x260XXXXX   ; // <-- Change this address for every node!

// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }
 
// The following array of bytes is a placeholder to contain the message payload
// which is transmitted to the LoRaWAN gateway. We are currently only using 6 bytes.
// Please make sure to extend the size of the array, if more sensors should be
// attached to the sensor node and the message payload becomes larger than 10 bytes.
static uint8_t mydata[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA}; 
 
static osjob_t sendjob;
 
// Schedule transmission every TX_INTERVAL seconds (might become longer due to duty
// cycle limitations). The total interval time is 2secs for the measurement
// plus 3secs for the LoRaWAN packet transmission plus TX_INTERVAL_AFTER_SLEEP seconds
// plus SLEEP_TIME seconds (microcontroller in sleep mode)
const unsigned TX_INTERVAL = 300;       // overall cycle time (send one set of observations every 5mins)
const unsigned TX_WAIT_AFTER_SLEEP = 1; // seconds to wait after return from sleep mode before the next transmit is scheduled
const unsigned TX_TIME = 3;             // rough estimate of transmission time of a single packet
const unsigned MEASURE_TIME = 2;        // seconds measuring time
const unsigned SLEEP_TIME = TX_INTERVAL - TX_WAIT_AFTER_SLEEP - TX_TIME - MEASURE_TIME;
const unsigned WAIT_TIME = TX_INTERVAL - TX_TIME - MEASURE_TIME;
 
// Pin mapping
const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {7, 6, LMIC_UNUSED_PIN},
};
 
void onEvent (ev_t ev) {
    SERIALDEBUG_PRINT(os_getTime());
    SERIALDEBUG_PRINT(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
            SERIALDEBUG_PRINTLN(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            SERIALDEBUG_PRINTLN(F("EV_JOINING"));
            break;
        case EV_JOINED:
            SERIALDEBUG_PRINTLN(F("EV_JOINED"));
            break;
        case EV_RFU1:
            SERIALDEBUG_PRINTLN(F("EV_RFU1"));
            break;
        case EV_JOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
            SERIALDEBUG_PRINTLN(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
            if (LMIC.txrxFlags & TXRX_ACK)
              SERIALDEBUG_PRINTLN(F("Received ack"));
            if (LMIC.dataLen) {
#ifdef SERIALDEBUG
              SERIALDEBUG_PRINT(F("Received "));
              SERIALDEBUG_PRINT(LMIC.dataLen);
              SERIALDEBUG_PRINT(F(" bytes of payload: 0x"));
              for (int i=0; i<LMIC.dataLen; i++) {
                if (LMIC.frame[LMIC.dataBeg + i] < 0x10) {
                  SERIALDEBUG_PRINT(F("0"));
                }
                SERIALDEBUG_PRINT(LMIC.frame[LMIC.dataBeg + i], HEX);
              }
              SERIALDEBUG_PRINTLN();
#endif
              // add your code to handle a received downlink data packet here
            }
 
#ifdef SLEEPMODE           
            // Schedule next transmission in 1 second after the board returns from sleep mode
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_WAIT_AFTER_SLEEP), do_send);
             
            SERIALDEBUG_PRINTLN("going to sleep now ... ");
            // lmic library sleeps automatically after transmission has been completed
            for(int i= 0; i < SLEEP_TIME / 8; i++) {
              Watchdog.sleep(8000); // maximum seems to be 8 seconds
              SERIALDEBUG_PRINT('.');
            }
            if (SLEEP_TIME % 8) {
              Watchdog.sleep((SLEEP_TIME % 8)*1000);
              SERIALDEBUG_PRINT('*');             
            }
            SERIALDEBUG_PRINTLN("... woke up again");
#else
            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(WAIT_TIME), do_send);
#endif          
            break;
        case EV_LOST_TSYNC:
            SERIALDEBUG_PRINTLN(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            SERIALDEBUG_PRINTLN(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            SERIALDEBUG_PRINTLN(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            SERIALDEBUG_PRINTLN(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            SERIALDEBUG_PRINTLN(F("EV_LINK_ALIVE"));
            break;
         default:
            SERIALDEBUG_PRINTLN(F("Unknown event"));
            break;
    }
}
 
void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) {
        SERIALDEBUG_PRINTLN(F("OP_TXRXPEND, not sending"));
    } else {
        // Prepare upstream data transmission at the next possible time.
 
        float temperature, humidity, measuredvbat;
        int16_t int16_temperature, int16_humidity, int16_vbat;
     
        // Start a measurement to update the sensor's internal temperature & humidity reading.
        // Note, that when fetching measurements from a DHT22 sensor, the reported
        // values belong to the measurement BEFORE the current measurement.
        // Therefore, in order to get current observations, we first perform a new measurement
        // and wait 2 secs (which is the minimum time between two sensor observations for
        // the DHT22) and then directly retrieve the observations again.
        temperature = dht.readTemperature();
#ifdef SLEEPMODE
        Watchdog.sleep(2000);
#else
        delay(2000);
#endif       
        // Now read the recently measured temperature (2 secs ago) as Celsius (the default)
        temperature = dht.readTemperature();
        // Read the recently measured humidity (2 secs ago)
        humidity = dht.readHumidity();
     
        // Check if any reads failed and exit early (to try again).
        if (isnan(humidity) || isnan(temperature)) {
            SERIALDEBUG_PRINTLN("Failed to read from DHT sensor!");
            // blink the LED five times to indicate that the sensor values could not be read
            for (int i=0; i<5; i++) {
              digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH                   
              delay(150);
              digitalWrite(LED_BUILTIN, LOW);    // turn the LED on by making the voltage HIGH                   
              delay(150);
            }
            // ok, then wait for another period and try it again
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
        } else {
            SERIALDEBUG_PRINT("Humidity: ");
            SERIALDEBUG_PRINT(humidity);
            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Temperature: ");
            SERIALDEBUG_PRINT(temperature);
            SERIALDEBUG_PRINT(" °C ");
 
            int16_temperature = 100*temperature;
            int16_humidity = 100*humidity;
            mydata[0] = (byte) (int16_temperature >> 8);
            mydata[1] = (byte) (int16_temperature & 0x00FF);
            mydata[2] = (byte) (int16_humidity >> 8);
            mydata[3] = (byte) (int16_humidity & 0x00FF);
 
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2;    // we divided by 2, so multiply back
            measuredvbat *= 3.3;   // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1023;  // convert to voltage
            int16_vbat = round(measuredvbat * 100);
            mydata[4] = (byte) (int16_vbat >> 8);
            mydata[5] = (byte) (int16_vbat & 0x00FF);
            SERIALDEBUG_PRINT(" \t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINT(measuredvbat);
            SERIALDEBUG_PRINTLN(" V");
            
            // Send the 6 bytes payload to LoRaWAN port 7 and do not request an acknowledgement.
            // The following call does not directly sends the data, but puts a "send job"
            // in the job queue. This job eventually is performed in the call "os_runloop_once();"
            // issued repeatedly in the "loop()" method below. After the transmission is
            // complete, the EV_TXCOMPLETE event is signaled, which is handled in the
            // event handler method "onEvent (ev_t ev)" above. In the EV_TXCOMPLETE branch
            // then a new call to the "do_send(osjob_t* j)" method is being prepared for
            // delayed execution with a waiting time of TX_INTERVAL seconds.
            LMIC_setTxData2(7, mydata, 6, 0);
            SERIALDEBUG_PRINTLN(F("Packet queued")); 
            digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH
             
            // Next TX is scheduled after TX_COMPLETE event.
        }
    }
}
 
void setup() {
    delay(5000);                    // give enough time to open serial monitor (if needed)
 
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
 
#ifdef SERIALDEBUG
    Serial.begin(115200);
    // while (!Serial);
#endif
 
    dht.begin();                    // initialize DHT22 sensor
     
    SERIALDEBUG_PRINTLN(F("Starting"));
 
    #ifdef VCC_ENABLE
    // For Pinoccio Scout boards
    pinMode(VCC_ENABLE, OUTPUT);
    digitalWrite(VCC_ENABLE, HIGH);
    delay(1000);
    #endif
 
    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
 
    // Set static session parameters. Instead of dynamically establishing a session
    // by joining the network, precomputed session parameters are be provided.
    #ifdef PROGMEM
    // On AVR, these values are stored in flash and only copied to RAM
    // once. Copy them to a temporary buffer here, LMIC_setSession will
    // copy them into a buffer of its own again.
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
    #else
    // If not running an AVR with PROGMEM, just use the arrays directly
    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
    #endif
 
    #if defined(CFG_eu868)
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.
    // NA-US channels 0-71 are configured automatically
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.
    #elif defined(CFG_us915)
    // NA-US channels 0-71 are configured automatically
    // but only one group of 8 should (a subband) should be active
    // TTN recommends the second sub band, 1 in a zero based count.
    // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
    LMIC_selectSubBand(1);
    #endif
 
    // 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. This will initiate the repetitive sending of data packets,
    // because after each data transmission, a delayed call to "do_send()"
    // is being scheduled again.
    do_send(&sendjob);
}
 
void loop() {
    os_runloop_once();
}
TTN payload decoder for Adafruit32u4 LoRa sensor node
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Decoder (bytes, port) {
  var result = {};
  var transformers = {};
   
  if (port==7) {
     transformers = {
      'temperature': function transform (bytes) {
          value=bytes[0]*256 + bytes[1];
          if (value>=32768) value=value-65536;
          return value/100.0;
        },
      'humidity': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
      'vbattery': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
    }
   
    result['temperature'] = {
      value: transformers['temperature'](bytes.slice(0, 2)),
      uom: 'Celsius',
    }
   
    result['humidity'] = {
      value: transformers['humidity'](bytes.slice(2, 4)),
      uom: 'Percent',
    }
   
    result['vbattery'] = {
      value: transformers['vbattery'](bytes.slice(4, 6)),
      uom: 'Volt',
    }
  }
   
  return result;
}

Adafruit 32u4 LoRa with Display

This tutorial is made to showcase the use of Adafruit 32u4 board to create a LoRaWAN enabled sensor node with a display and a case. In the following example, a temperature and humidity sensor was used with the Adafruit 32u4 board to create this tutorial.

Hardware

To build this sensor node we have used following hardware components:

Microcontroller

The Adafruit Feather 32u4 LoRa module is operated by the 8bit ATmega32u4 microcontroller running at 8MHz. It has 32 KB flash memory (to store the program code) and 2 KB of RAM (to store variables, status information, and buffers). The operating voltage of the board is 3.3V (this is important when attaching sensors and other peripherals; they also must operate on 3.3V). The board offers 20 general purpose digital input/output pins (20 GPIOs) with 10 analog input pins (with 12bit analog digital converters (ADC)), one serial port (programmable Universal Asynchronous Receiver and Transmitter, UART), one I2C port, one SPI port, one USB port. The board comes with an embedded Lithium polymer battery management chip and status indicator led, which allows to directly connect a 3.7V LiPo rechargeable battery that will be automatically recharged when the board is powered over its USB connector. The Adafruit Feather 32u4 LoRa board is available in German shops from around 37 € to 45 €.

The LoRa transmitter and receiver is encapsulated within an RFM95 module from the company HopeRF. This module uses the LoRa chip SX1276 from the company Semtech and is dedicated to the 868 MHz frequency band. The RFM95 module is connected via SPI interface to the microcontroller. Most of the required connections of the LoRa transceiver pins with the microcontroller are already built-in on the Adafruit Feather 32u4 LoRa board. However, Digital Pin 6 of the microcontroller must be connected to DIO1 of the LoRa transceiver module in addition using a simple wire. Since the module only implements the LoRa physical layer, the LoRaWAN protocol stack must be implemented in software on the microcontroller. We are using the Arduino library LMIC for that purpose (see below). The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C.

Sensor

We have attached a DHT22 sensor to the microcontroller board, which measures air temperature and humidity. The minimal time interval between two measurements is 2 seconds. All data transfers between the DHT22 and the microcontroller use a single digital line. The sensor data pin is attached to a GPIO pin (here: Digital Pin 6) of the microcontroller. In addition, a so-called pull-up resistor of 4.7k to 10k Ohm must be connected between the data line and VCC (+3.3V). The DHT22 datasheet provides more technical details about the DHT22 Sensor. A tutorial on how to use the DHT22 sensor with Arduino microcontrollers is provided here. The sensor is available in German shops for around 4 € to 10 €.

_images/setup.jpg

The Adafruit Feather 32u4 RFM95 LoRa installed in a 3D printed case. On top of the microcontroller board an Adafruit Display Wing with a 4 digit 14 segments LED display is attached. Right of the display the DHT22 temperature / humidity sensor is mounted. In the rear part of the case a 2000 mAh polymer (LiPo) battery is installed. On the right side the antenna is visible.

Display / Beeper

On top of the microcontroller board we have attached an Adafruit Display Wing with a 4 digit 14 segments LED display. It can show 0-4 numbers or letters (upper case and lower case). The display controller is using the I2C protocol and the I2C pins SDA and SCL are directly connected to the Adafruit Feather via the Wing connectors. The Wing is using the default I2C address (0x70). Also a 3.3V beeper is installed that is used to indicate that a new message was received and is now being displayed. The ‘+’ pin of the beeper has to be connected to Digital Pin 12 and the ‘-‘ pin to GND. The display and the beeper can be used to notify a user with (very) short messages. The reason why we have included this is mostly to experiment with and to demonstrate the downlink capabilities of LoRaWAN. When a downlink message has been queued it will be transmitted to the node right after it has transmitted the next data packet (uplink data). Hence, it depends on the transmission time period how long it can take unless the node receives and displays a downlink message.

Case

The case was 3D printed using the design files provided by Adafruit. The case consists of three parts. Part 1 is the main enclosure (it does not have a switch holder or tabs, the design file is feather-case.stl). Part 2 is the battery holder (with a slide switch holder, the design file is feather-bat-switch.stl). Part 3 is the case topper (with a cutout for the Adafruit Feather Wing, the design file is feather-top-wing.stl). All design files can be downloaded from Thingiverse.

We have ordered the three parts from an online 3D printing service. The quality of the delivered parts was generally ok, but not good enough for snapping the three parts firmly together. It is not clear yet whether this is a problem of the design files or of the printing service. We used two rubber bands In order to fix the three parts together.

Once all these connection are made, the board is connected with a computer using a USB cable. Further, steps of software part needs to be followed. But, before that we need to register a new device on the service that we are using.

Software

The sensor node has been programmed using the Arduino IDE. Please note, that in the Arduino framework a program is called a ‘Sketch’.

After the sketch has successfully established a connection to The Things Network it reports the air temperature, humidity, and the voltage of a (possibly) attached LiPo battery every 5 minutes. All three values are being encoded in two byte integer values each (in most significant byte order) and then sent as a 6 bytes data packet to the respective TTN application using LoRaWAN port 7. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement; this must be considered in the Payload Decoding Function used in The Things Network (see here).

In between two sensor readings the microcontroller is going into deep sleep mode to save battery power. We still have to run some tests to find out for how long the system can run using the 2000 mAh LiPo battery and the current version of the sketch. Showing a received message on the display draws a considerable amount of power and will shorten battery life significantly. Hence, when running on battery it is recommended to clear a displayed message soon by sending a simple space character (0x20). (Further optimizations would be possible, for example, not switching on the LED on the microcontroller board during LoRa data transmissions.)

The employed RFM95 LoRa module does not provide built-in support of the LoRaWAN protocol. Thus, it has to be implemented on the ATmega32u4 microcontroller. We use the IBM LMIC (LoraMAC-in-C) library for Arduino. Since the ATmega32u4 microcontroller only has 32 KB of flash memory and the LMIC library is taking most of it, there is only very limited code space left for the application dealing with the sensors (about 2 KB). Nevertheless, this is sufficient to query some sensors like in our example the DHT22.

Now download and run the Arduino Sketch for Adafruit32u4 LoRa with display sensor node file in the Arduino IDE. This code was created by merging the example code of both the sensors and the ttn-otaa example from the lmic library. Some required changes were made while merging the example codes. The user should change the network session key, app session key and device address in the code before compiling. These keys can be obtained from the TTN account as shown in the services section.

Modify the keys in highlighted lines.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// This EUI must be in little-endian format, so least-significant-byte
// first. When copying an EUI from ttnctl output, this means to reverse
// the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
static const u1_t PROGMEM APPEUI[8]={ 0x55, 0xC1, 0x00, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };
void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}
 
// This should also be in little endian format, see above.
static const u1_t PROGMEM DEVEUI[8]={ 0xF6, 0xE2, 0x10, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };
void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}
 
// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
// The key shown here is the semtech default key.
static const u1_t PROGMEM APPKEY[16] = { 0xC2, 0x21, 0x2E, 0x7A, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };
void os_getDevKey (u1_t* buf) {  memcpy_P(buf, APPKEY, 16);}

Following is the example code that can be used to measure the battery voltage of the sensor node:

Code for measuring the battery voltage
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2.0;      // we divided by 2, so multiply back
            measuredvbat *= 3.3;      // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1023.0;   // convert to voltage
            int16_vbat = round(measuredvbat * 100.0);
            mydata[4] = (byte) (int16_vbat >> 8);
            mydata[5] = (byte) (int16_vbat & 0x00FF);
            SERIALDEBUG_PRINT(" \t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINT(measuredvbat);
            SERIALDEBUG_PRINTLN(" V");

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The sketch given above connects the sensor node with The Things Network (TTN) using the Over-the-Air-Activation (OTAA) mode. In this mode, we use the three keys AppEUI, DevEUI, AppKey. The DevEUI should be delivered with the sensor node by the manufacturer, the other two keys are created using the TTN console. Each sensor node must be manually registered in the TTN console before it can be started. This assumes that you already have a TTN user account (which needs to be created otherwise). In the TTN console create a new device and enter the DevEUI number that was shipped with the Adafruit Feather LoRa board. Note that the shipped number only consists of 6 bytes while LoRaWAN requires an 8 bytes DevEUI. We simply add 0x00 0x00 in the middle of the 6 bytes provided. If you have lost the provided DevEUI you can also let the TTN console create a new one. After the registration of the device the respective keys (AppEUI, DevEUI, AppKey) can be copied from the TTN console and must be pasted into the the proper places in the source code of the sketch above. Please make sure that you choose for each of the three keys the correct byte ordering (DevEUI, AppEUI in LSB; AppKey in MSB). A detailed explanation of these steps is given here. Then the sketch can be compiled and uploaded to the Adafruit Feather 32u4 LoRa microcontroller. Note that the three constants (AppEUI, DevEUI, AppKey) must be changed in the source code for every new sensor node.

Using the OTAA mode has the advantage over the ABP (activation by personalization) mode that during connection the session keys are newly created which improves security. Another advantage is that the packet counter is automatically reset to 0 both in the node and in the TTN application.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (Payload Decoder Function). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak). Such a forwarding can be configured in the TTN console in the “Integrations” tab. TTN payload decoder for Adafruit32u4 LoRa with display sensor node given here checks if a packet was received on LoRaWAN port 7 and then assumes that it consists of the 6 bytes encoded as described above. It creates the three Javascript objects ‘temperature’, ‘humidity’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==7) {…} statement by adding else if (port==8) {…} else if (port==9) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this page. Here is a screenshot showing the webclient:

_images/webclient1.png

Web client for data visualization

Code files

Arduino Sketch for Adafruit32u4 LoRa with display sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
/*******************************************************************************
 * Arduino Sketch for a LoRaWAN sensor node that is registered with
 * 'The Things Network' (TTN) www.thethingsnetwork.org
 *
 * Author:  Thomas H. Kolbe, thomas.kolbe@tum.de
 * Version: 1.0.0
 * Last update: 2018-12-09
 *
 * The sensor node is based on the Adafruit Feather LoRa microcontroller board
 * with either the AVR ATmega32u4 or the ATSAMD21G18 ARM Cortex M0 microcontroller.
 * See https://learn.adafruit.com/adafruit-feather-32u4-radio-with-lora-radio-module/
 * or https://learn.adafruit.com/adafruit-feather-m0-radio-with-lora-radio-module/
 * The sensor node uses a DHT22 sensor measuring air temperature and humidity.
 * Also the voltage of an attached LiPo battery is monitored and sent as
 * an observation. All three values are encoded as 2 byte integer values each.
 * Hence, the total message payload is 6 bytes. Before the values are converted
 * to integers they are multiplied by 100 to preserve 2 digits after the decimal
 * point. Thus, the received values must be divided by 100 to obtain the measured
 * values. The payload is sent every 300s to LoRaWAN port 7. The following
 * Javascript function can be used as a payload decoding function in TTN:
 *
 * function Decoder(bytes, port) {
 *   // Decode an uplink message from a buffer
 *   // (array) of bytes to an object of fields.
 *   if (port==7) {
 *     var decoded = {
 *       "temperature": (bytes[0] << 8 | bytes[1]) / 100.0,
 *       "humidity": (bytes[2] << 8 | bytes[3]) / 100.0,
 *       "vbattery": (bytes[4] << 8 | bytes[5]) / 100.0
 *     };
 *   } else {
 *     var decoded = null;
 *   }
 *   return decoded;
 * }
 *
 * In between two data transmissions the microcontroller board can go
 * into sleep mode to reduce energy consumption for extended operation
 * time when running on battery. Usage of the sleep mode must be
 * explicitly configured below.
 *
 * Note, that the DHT22 data pin must be connected to Digital Pin 6 of the
 * microcontroller board (for the Feather 32u4) or Digital Pin 12 (for
 * the Feather M0). A resistor of 4.7k - 10k Ohm must be connected to
 * the data pin and VCC (+3.3V).
 *
 * Digital Pin 5 (for the Feather 32u4) must be connected to DIO1 of the
 * LoRa transceiver module using a simple wire.
 *
 * For this node we also attach an Adafruit Feather Wing with a four digit
 * 14-segments LED display. The display controller is using I2C and the
 * I2C pins SDA and SCL are directly connected to the Adafruit Feather
 * via the Wing connectors. The wing is using the default I2C address
 * (0x70). Any LoRaWAN downlink message sent to this node is shown on
 * the display (only the first 4 characters). We treat each byte of the
 * received payload as a character in ASCII code. Besides numbers and
 * letters in upper and lower case also some special characters are
 * supported. For further details on the Feather Display Wing see here:
 * https://learn.adafruit.com/14-segment-alpha-numeric-led-featherwing
 *
 * In order to notify persons standing nearby that a new text was received
 * we let the node beep a couple of times. Therefore, Digital Pin 12 (for
 * the Feather 32u4) should be connected to the '+' port of a 3.3V buzzer
 * module. The '-' port of the buzzer must be connected to GND.
 * If a payload containing just a single space (character code 0x20) is
 * received, the display will be blanked without emitting beeps.
 *
 * Note that if the LED display shows some text this will draw a
 * significant amount of power. This will certainly reduce the operational
 * duration when running on battery.
 *
 * The code is based on the Open Source library LMIC implementing the LoRaWAN
 * protocol stack on top of a given LoRa transceiver module (here: RFM95 from
 * HopeRF, which uses the Semtech SX1276 LoRa chip). The library is originally
 * being developed by IBM and has been ported to the Arduino platform. See
 * notes below from the original developers.
 *
 *******************************************************************************
 * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
 *
 * Permission is hereby granted, free of charge, to anyone
 * obtaining a copy of this document and accompanying files,
 * to do whatever they want with them without any restriction,
 * including, but not limited to, copying, modification and redistribution.
 * NO WARRANTY OF ANY KIND IS PROVIDED.
 *
 * This uses OTAA (Over-the-air activation), where a DevEUI and
 * application key is configured, which are used in an over-the-air
 * activation procedure where a DevAddr and session keys are
 * assigned/generated for use with all further communication.
 *
 * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
 * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
 * violated by this sketch when left running for longer)!
 *
 * To use this sketch, first register your application and device with
 * the things network, to set or generate an AppEUI, DevEUI and AppKey.
 * Multiple devices can use the same AppEUI, but each device has its own
 * DevEUI and AppKey.
 *
 * Do not forget to define the radio type correctly in config.h.
 *
 *******************************************************************************/
 
// If the following line is uncommented, messages are being printed out to the
// serial connection for debugging purposes. When using the Arduino Integrated
// Development Environment (Arduino IDE), these messages are displayed in the
// Serial Monitor selecting the proper port and a baudrate of 115200.
 
// #define SERIALDEBUG
 
#ifdef SERIALDEBUG
  #define SERIALDEBUG_PRINT(...) Serial.print(__VA_ARGS__)
  #define SERIALDEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#else
  #define SERIALDEBUG_PRINT(...)
  #define SERIALDEBUG_PRINTLN(...)
#endif
 
// If the following line is uncommented, the sensor node goes into sleep mode
// in between two data transmissions. Also the 2secs time between the
// initialization of the DHT22 sensor and the reading of the observations
// is spent in sleep mode.
// Note, that on the Adafruit Feather 32u4 LoRa board the Serial connection
// gets lost as soon as the board goes into sleep mode, and it will not be
// established again. Thus, the definition of SERIALDEBUG should be commented
// out above when using sleep mode with this board.
 
#define SLEEPMODE
 
#ifdef SLEEPMODE
  #include <Adafruit_SleepyDog.h>
#endif
 
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
 
#include <util/atomic.h>
#include <avr/power.h>
 
#include <DHT.h>
 
#ifdef __AVR_ATmega32U4__
  #define DHTPIN            6     // Arduino Digital Pin which is connected to the DHT sensor for Feather 32u4.
#endif
#ifdef ARDUINO_SAMD_FEATHER_M0
  #define DHTPIN            12    // Arduino Digital Pin which is connected to the DHT sensor for Feather M0.
#endif
#define DHTTYPE          DHT22    // DHT 22 (AM2302)
 
DHT dht(DHTPIN, DHTTYPE);         // create the sensor object
 
 
#ifdef __AVR_ATmega32U4__
   #define VBATPIN A9             // battery voltage is measured from Analog Input A9 for Feather 32u4
#endif
#ifdef ARDUINO_SAMD_FEATHER_M0
   #define VBATPIN A7             // battery voltage is measured from Analog Input A7 for Feather M0
#endif
 
#ifdef __AVR_ATmega32U4__
  extern volatile unsigned long timer0_overflow_count;
#endif
 
#include <Wire.h>
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"
 
Adafruit_AlphaNum4 alpha4 = Adafruit_AlphaNum4();
 
#define BUZZERPIN 12              // Arduino Digital Pin which is connected to the buzzer module
 
 
// The following three constants (AppEUI, DevEUI, AppKey) must be changed
// for every new sensor node. We are using the LoRaWAN OTAA mode (over the
// air activation). Each sensor node must be manually registered in the
// TTN console at https://console.thethingsnetwork.org before it can be
// started. In the TTN console create a new device and enter the DevEUI
// number that was shipped with the Adafruit Feather LoRa board. Note that
// the shipped number only consists of 6 bytes while LoRaWAN requires
// an 8 bytes DevEUI. We simply add 0x00 0x00 in the middle of the 6 bytes
// provided. If you have lost the provided DevEUI you can also let the
// TTN console create a new one. After the registration of the device the
// three values can be copied from the TTN console. A detailed explanation
// of these steps is given in
// https://learn.adafruit.com/the-things-network-for-feather?view=all
 
// This EUI must be in little-endian format, so least-significant-byte
// first. When copying an EUI from ttnctl output, this means to reverse
// the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
static const u1_t PROGMEM APPEUI[8]={ 0x55, 0xC1, 0x00, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };
void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}
 
// This should also be in little endian format, see above.
static const u1_t PROGMEM DEVEUI[8]={ 0xF6, 0xE2, 0x10, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };
void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}
 
// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
// The key shown here is the semtech default key.
static const u1_t PROGMEM APPKEY[16] = { 0xC2, 0x21, 0x2E, 0x7A, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX };
void os_getDevKey (u1_t* buf) {  memcpy_P(buf, APPKEY, 16);}
 
 
// The following array of bytes is a placeholder to contain the message payload
// which is transmitted to the LoRaWAN gateway. We are currently only using 6 bytes.
// Please make sure to extend the size of the array, if more sensors should be
// attached to the sensor node and the message payload becomes larger than 10 bytes.
static uint8_t mydata[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA}; 
 
static osjob_t sendjob;
 
// Schedule transmission every TX_INTERVAL seconds (might become longer due to duty
// cycle limitations). The total interval time is 2secs for the measurement
// plus 3secs for the LoRaWAN packet transmission plus SLEEP_TIME seconds
// plus SLEEP_TIME seconds (microcontroller in sleep mode)
const unsigned int TX_INTERVAL = 300;       // overall cycle time (send one set of observations every 5mins)
const unsigned int TX_TIME = 22;            // rough estimate of transmission time of a single packet
const unsigned int MEASURE_TIME = 2;        // seconds measuring time
const unsigned int SLEEP_TIME = TX_INTERVAL - TX_TIME - MEASURE_TIME;
const unsigned int WAIT_TIME = TX_INTERVAL - TX_TIME - MEASURE_TIME;
 
// Pin mapping of the LoRa transceiver. Please make sure that DIO1 is connected
// to Arduino Digital Pin 6 using an external wire. DIO2 is left unconnected
// (it is only required, if FSK modulation instead of LoRa would be used).
#ifdef __AVR_ATmega32U4__
  const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {7, 5, LMIC_UNUSED_PIN},     // in the Feather 32u4 DIO0 is connected to Arduino Digital Pin 7
  };
#endif
#ifdef ARDUINO_SAMD_FEATHER_M0
  const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {3, 6, LMIC_UNUSED_PIN},     // in the Feather M0 DIO0 is connected to Arduino Digital Pin 3
  };
#endif
 
void onEvent (ev_t ev) {
    SERIALDEBUG_PRINT(os_getTime());
    SERIALDEBUG_PRINT(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
            SERIALDEBUG_PRINTLN(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            SERIALDEBUG_PRINTLN(F("EV_JOINING"));
            break;
        case EV_JOINED:
            SERIALDEBUG_PRINTLN(F("EV_JOINED"));
 
            // Disable link check validation (automatically enabled
            // during join, but not supported by TTN at this time).
//            LMIC_setLinkCheckMode(0);
            break;
        case EV_RFU1:
            SERIALDEBUG_PRINTLN(F("EV_RFU1"));
            break;
        case EV_JOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
            SERIALDEBUG_PRINTLN(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
            if (LMIC.txrxFlags & TXRX_ACK)
              SERIALDEBUG_PRINTLN(F("Received ack"));
            if (LMIC.dataLen) {
#ifdef SERIALDEBUG
              SERIALDEBUG_PRINT(F("Received "));
              SERIALDEBUG_PRINT(LMIC.dataLen);
              SERIALDEBUG_PRINT(F(" bytes of payload: 0x"));
              for (int i=0; i<LMIC.dataLen; i++) {
                if (LMIC.frame[LMIC.dataBeg + i] < 0x10) {
                  SERIALDEBUG_PRINT(F("0"));
                }
                SERIALDEBUG_PRINT(LMIC.frame[LMIC.dataBeg + i], HEX);
              }
              SERIALDEBUG_PRINTLN();
#endif
              // add your code to handle a received downlink data packet here                 
              alpha4.clear();
              for (int i=0; i<LMIC.dataLen && i<4; i++) {
                  alpha4.writeDigitAscii(i, LMIC.frame[LMIC.dataBeg + i]);
              }
              alpha4.writeDisplay();
              if (!(LMIC.frame[LMIC.dataBeg]==' ' && LMIC.dataLen==1))
                  messagebeep();
            }
 
#ifdef SLEEPMODE           
            // Schedule next transmission in 1ms second after the board returns from sleep mode
            os_setTimedCallback(&sendjob, os_getTime()+ms2osticks(1), do_send);
             
            SERIALDEBUG_PRINTLN("going to sleep now ... ");
            // lmic library sleeps automatically after transmission has been completed
 
            doSleep((uint32_t)SLEEP_TIME*1000);
/*           
            int sleepcycles = (int)SLEEP_TIME / 8;
            int restsleep = (int)SLEEP_TIME % 8;
            for(int i=0; i < sleepcycles; i++) {
              Watchdog.sleep(8000); // maximum seems to be 8 seconds
              SERIALDEBUG_PRINT('.');
            }
            if (restsleep) {
              Watchdog.sleep(restsleep*1000);
              SERIALDEBUG_PRINT('*');             
            }
            SERIALDEBUG_PRINTLN("... woke up again");
 
#ifdef __AVR_ATmega32U4__
            // The following statement is required to prevent that LMIC spends another
            // couple of seconds busy waiting for some RX packets. This is only required
            // when using SLEEPMODE, because during sleep mode the Arduino timer variables
            // are not being incremented and LMIC job scheduling is based on this. 
//            timer0_overflow_count += 3E6;
             
            ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
                extern volatile unsigned long timer0_millis;
                extern volatile unsigned long timer0_overflow_count;
                timer0_millis += SLEEP_TIME*1000;
                // timer0 uses a /64 prescaler and overflows every 256 timer ticks
                timer0_overflow_count += microsecondsToClockCycles((uint32_t)SLEEP_TIME * 1000000) / (64 * 256);
            }
#endif
*/
            // We need to reset the duty cycle limits within the LMIC library.
            // The reason is that in sleep mode the Arduino system timers millis and micros
            // do not get incremented. However, LMIC monitors the adherence to the
            // LoRaWAN duty cycle limitations using the system timers millis and micros.
            // Since LMIC does not know that we have slept for a long time and duty
            // cycle requirements in fact are met, we must reset the respective LMIC timers
            // in order to prevent the library to wait for some extra time (which would
            // not use sleep mode and, thus, would waste battery energy).
            LMIC.bands[BAND_MILLI].avail = os_getTime();
            LMIC.bands[BAND_CENTI].avail = os_getTime();
            LMIC.bands[BAND_DECI].avail = os_getTime();
#else
            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(WAIT_TIME), do_send);
#endif          
            break;
        case EV_LOST_TSYNC:
            SERIALDEBUG_PRINTLN(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            SERIALDEBUG_PRINTLN(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            SERIALDEBUG_PRINTLN(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            SERIALDEBUG_PRINTLN(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            SERIALDEBUG_PRINTLN(F("EV_LINK_ALIVE"));
            break;
         default:
            SERIALDEBUG_PRINTLN(F("Unknown event"));
            break;
    }
}
 
void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) {
        SERIALDEBUG_PRINTLN(F("OP_TXRXPEND, not sending"));
    } else {
        // Prepare upstream data transmission at the next possible time.
 
        float temperature, humidity, measuredvbat;
        int16_t int16_temperature, int16_humidity, int16_vbat;
     
        // Start a measurement to update the sensor's internal temperature & humidity reading.
        // Note, that when fetching measurements from a DHT22 sensor, the reported
        // values belong to the measurement BEFORE the current measurement.
        // Therefore, in order to get current observations, we first perform a new measurement
        // and wait 2 secs (which is the minimum time between two sensor observations for
        // the DHT22) and then directly retrieve the observations again.
         
        temperature = dht.readTemperature();
#ifdef SLEEPMODE
//        Watchdog.sleep(MEASURE_TIME * 1000UL);
        doSleep(MEASURE_TIME * 1000UL);
#else
        delay(MEASURE_TIME * 1000UL);
#endif
         
        // Now read the recently measured temperature (2 secs ago) as Celsius (the default)
        temperature = dht.readTemperature();
 
        // Read the recently measured humidity (2 secs ago)
        humidity = dht.readHumidity();
     
        // Check if any reads failed and exit early (to try again).
        if (isnan(humidity) || isnan(temperature)) {
            SERIALDEBUG_PRINTLN("Failed to read from DHT sensor!");
            // blink the LED five times to indicate that the sensor values could not be read
            for (int i=0; i<5; i++) {
              digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH                   
              delay(150);
              digitalWrite(LED_BUILTIN, LOW);    // turn the LED on by making the voltage HIGH                   
              delay(150);
            }
            // ok, then wait for another period and try it again
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
        } else {
            SERIALDEBUG_PRINT("Humidity: ");
            SERIALDEBUG_PRINT(humidity);
            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Temperature: ");
            SERIALDEBUG_PRINT(temperature);
            SERIALDEBUG_PRINT(" °C ");
 
            int16_temperature = round(100.0*temperature);
            int16_humidity = round(100.0*humidity);
            mydata[0] = (byte) (int16_temperature >> 8);
            mydata[1] = (byte) (int16_temperature & 0x00FF);
            mydata[2] = (byte) (int16_humidity >> 8);
            mydata[3] = (byte) (int16_humidity & 0x00FF);
 
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2.0;      // we divided by 2, so multiply back
            measuredvbat *= 3.3;      // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1023.0;   // convert to voltage
            int16_vbat = round(measuredvbat * 100.0);
            mydata[4] = (byte) (int16_vbat >> 8);
            mydata[5] = (byte) (int16_vbat & 0x00FF);
            SERIALDEBUG_PRINT(" \t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINT(measuredvbat);
            SERIALDEBUG_PRINTLN(" V");
            
            // Send the 6 bytes payload to LoRaWAN port 7 and do not request an acknowledgement.
            // The following call does not directly sends the data, but puts a "send job"
            // in the job queue. This job eventually is performed in the call "os_runloop_once();"
            // issued repeatedly in the "loop()" method below. After the transmission is
            // complete, the EV_TXCOMPLETE event is signaled, which is handled in the
            // event handler method "onEvent (ev_t ev)" above. In the EV_TXCOMPLETE branch
            // then a new call to the "do_send(osjob_t* j)" method is being prepared for
            // delayed execution with a waiting time of TX_INTERVAL seconds.
            LMIC_setTxData2(7, mydata, 6, 0);
            SERIALDEBUG_PRINTLN(F("Packet queued")); 
            digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH
             
            // Next TX is scheduled after TX_COMPLETE event.
        }
    }
}
 
void doSleep(uint32_t time) {
  ADCSRA &= ~(1 << ADEN);
  power_adc_disable();
 
  while (time > 0) {
    uint16_t slept;
    if (time < 8000)
      slept = Watchdog.sleep(time);
    else
      slept = Watchdog.sleep(8000);
 
    // Update the millis() and micros() counters, so duty cycle
    // calculations remain correct. This is a hack, fiddling with
    // Arduino's internal variables, which is needed until
    // https://github.com/arduino/Arduino/issues/5087 is fixed.
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
      extern volatile unsigned long timer0_millis;
      extern volatile unsigned long timer0_overflow_count;
      timer0_millis += slept;
      // timer0 uses a /64 prescaler and overflows every 256 timer ticks
      timer0_overflow_count += microsecondsToClockCycles((uint32_t)slept * 1000) / (64 * 256);
    }
 
    if (slept >= time)
      break;
    time -= slept;
  }
 
  power_adc_enable();
  ADCSRA |= (1 << ADEN);
}
 
void beep(bool longbeep) {
    digitalWrite(BUZZERPIN, HIGH);   // turn the BUZZER off by making the voltage LOW
    if (longbeep)
      delay(250);
    else
      delay(100);
    digitalWrite(BUZZERPIN, LOW);   // turn the BUZZER off by making the voltage LOW
    delay(100);
}
 
void messagebeep() {
    beep(false);
    beep(true);
    beep(false);
    beep(false);
    delay(200);
    beep(false);
    beep(true);
    beep(false);
    beep(false);
}
 
void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
 
    pinMode(BUZZERPIN, OUTPUT);
    digitalWrite(BUZZERPIN, LOW);   // turn the BUZZER off by making the voltage LOW
     
    alpha4.begin(0x70);             // pass in the I2C address of the display
    alpha4.clear();
    alpha4.writeDisplay();
    alpha4.writeDigitAscii(0, 'T');
    alpha4.writeDigitAscii(1, 'e');
    alpha4.writeDigitAscii(2, 's');
    alpha4.writeDigitAscii(3, 't');
    alpha4.writeDisplay();
 
    messagebeep();
 
    delay(10000);                    // give enough time to open serial monitor (if needed) or to start uploading of a new sketch
 
    alpha4.clear();
    alpha4.writeDisplay();
 
#ifdef SERIALDEBUG
    Serial.begin(115200);
    // while (!Serial);
#endif
 
    dht.begin();                    // initialize DHT22 sensor
     
    SERIALDEBUG_PRINTLN(F("Starting"));
 
    #ifdef VCC_ENABLE
    // For Pinoccio Scout boards
    pinMode(VCC_ENABLE, OUTPUT);
    digitalWrite(VCC_ENABLE, HIGH);
    delay(1000);
    #endif
 
    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
 
    #if defined(CFG_eu868)
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.
    // NA-US channels 0-71 are configured automatically
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.
    #elif defined(CFG_us915)
    // NA-US channels 0-71 are configured automatically
    // but only one group of 8 should (a subband) should be active
    // TTN recommends the second sub band, 1 in a zero based count.
    // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
    LMIC_selectSubBand(1);
    #endif
 
    // 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_SF9,14);
 
    // Start job. This will initiate the repetitive sending of data packets,
    // because after each data transmission, a delayed call to "do_send()"
    // is being scheduled again.
    do_send(&sendjob);
 
    // The following settings should further reduce energy consumption. I have not 
    // tested them yet, they are taken from a post in the TTN forum. See
    // https://www.thethingsnetwork.org/forum/t/adafruit-lora-feather-gateway/2440/50
    /*
    power_adc_disable();
    power_usart0_disable();
    power_twi_disable();
    power_timer1_disable();
    power_timer2_disable();
    power_timer3_disable();
    power_usart1_disable();
    power_usb_disable();
    USBCON |= (1 << FRZCLK);
    PLLCSR &= ~(1 << PLLE);
    USBCON &= ~(1 << USBE );
    clock_prescale_set(clock_div_2);
    */
}
 
void loop() {
    os_runloop_once();
}
TTN payload decoder for Adafruit32u4 LoRa with display sensor node
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Decoder (bytes, port) {
  var result = {};
  var transformers = {};
   
  if (port==7) {
     transformers = {
      'temperature': function transform (bytes) {
          value=bytes[0]*256 + bytes[1];
          if (value>=32768) value=value-65536;
          return value/100.0;
        },
      'humidity': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
      'vbattery': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
    }
   
    result['temperature'] = {
      value: transformers['temperature'](bytes.slice(0, 2)),
      uom: 'Celsius',
    }
   
    result['humidity'] = {
      value: transformers['humidity'](bytes.slice(2, 4)),
      uom: 'Percent',
    }
   
    result['vbattery'] = {
      value: transformers['vbattery'](bytes.slice(4, 6)),
      uom: 'Volt',
    }
  }
   
  return result;
}

Adafruit M0 LoRa

Hardware

Microcontroller
_images/feather_board.jpg

Feather M0 with RFM95 LoRa Radio - 900 MHz - RadioFruit from Adafruit. Feather M0 LoRa tutorial with explanations, datasheets, and examples.

The Adafruit Feather M0 LoRa board is operated by the 32bit ATSAMD21G18 ARM Cortex M0 microcontroller running at 48MHz. It has 256 KB flash memory (to store the program code) and 32 KB of RAM (to store variables, status information, and buffers). The operating voltage of the board is 3.3V (this is important when attaching sensors and other peripherals; they also must operate on 3.3V). The board offers 20 general purpose digital input/output pins (20 GPIOs) with 10 analog input pins (with 12bit analog digital converters (ADC)), one analog output pin, one serial port (programmable Universal Asynchronous Receiver and Transmitter, UART), one I2C port, one SPI port, one USB port. The board comes with an embedded Lithium polymer battery management chip and status indicator led, which allows to directly connect a 3.7V LiPo rechargeable battery that will be automatically recharged when the board is powered over its USB connector. The Adafruit Feather M0 LoRa board is available in German shops from around 37 € to 45 €.

_images/hardware.jpg

The Adafruit Feather M0 RFM95 LoRa with attached antenna placed onto a prototyping breadboard. (On this photo the DHT22 sensor and the LiPo battery are missing; we will upload a new photo in the future)

The LoRa transmitter and receiver is encapsulated within an RFM95 module from the company HopeRF. This module uses the LoRa chip SX1276 from the company Semtech and is dedicated to the 868 MHz frequency band. The RFM95 module is connected via SPI interface to the microcontroller. Most of the required connections of the LoRa transceiver pins with the microcontroller are already built-in on the Adafruit Feather M0 LoRa board. However, Digital Pin 6 of the microcontroller must be connected to DIO1 of the LoRa transceiver module in addition using a simple wire. Since the module only implements the LoRa physical layer, the LoRaWAN protocol stack must be implemented in software on the microcontroller. We are using the Arduino library LMIC for that purpose (see below). The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C.

Sensor

We have attached a DHT22 sensor to the microcontroller board, which measures air temperature and humidity. The minimal time interval between two measurements is 2 seconds. All data transfers between the DHT22 and the microcontroller use a single digital line. The sensor data pin is attached to a GPIO pin (here: Digital Pin 12) of the microcontroller. In addition, a so-called pull-up resistor of 4.7k to 10k Ohm must be connected between the data line and VCC (+3.3V). The DHT22 datasheet can be accessed here. A tutorial on how to use the DHT22 sensor with Arduino microcontrollers is provided on this page. The sensor is available in German shops for around 4 € to 10 €.

Software

The sensor node has been programmed using the Arduino IDE. Please note, that in the Arduino framework a program is called a ‘Sketch’.

Now download and run the Arduino Sketch for Adafruit M0 LoRa sensor node file in the Arduino IDE. After the sketch has successfully established a connection to The Things Network it reports the air temperature, humidity, and the voltage of a (possibly) attached LiPo battery every 5 minutes. All three values are being encoded in two byte integer values each and then sent as a 6 bytes data packet to the respective TTN application using LoRaWAN port 7. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement; this must be considered in the Payload Decoding Function used in The Things Network (see below).

In between two sensor readings the microcontroller is going into deep sleep mode to save battery power. With a 2000 mAh LiPo battery and the current version of the sketch the system can run for at least 3 months. (Further optimizations would be possible, for example, not switching on the LED on the microcontroller board during LoRa data transmissions.)

The employed RFM95 LoRa module does not provide built-in support of the LoRaWAN protocol. Thus, it has to be implemented on the ARM Cortex M0 microcontroller. We use the IBM LMIC (LoraMAC-in-C) library for Arduino, which can be downloaded from this repository. The ARM Cortex M0 microcontroller has 256 KB of flash memory, which is plenty enough for the LMIC library, the code dealing with the sensors, and even some sophisticated analysis tasks (if required). The source code is given in the following listing:

Note, that the source code is very similar to the source code for the Adafruit Feather 32u4 LoRa board given on the Wiki page LoRaWAN Node - Adafruit 32u4 LoRa. The source code for the Adafruit Feather 32u4 LoRa board has also more detailed comments. It is planned to merge them into a single source code that can be used and compiled for both types of microcontrollers (ATmega32u4 and ARM Cortex M0). The merged source code is already available from LoRaWAN Node - Adafruit 32u4 LoRa, but was not tested with the M0 microcontroller board yet.

Note also, that there is an open issue regarding the deep sleep mode on the ARM Cortex M0 microcontroller in the source code above. During deep sleep mode the (software) timers of the LMIC library are not incremented and after wake-up the library does not recognize that enough time has passed to allow sending another data packet. This built-in mechanism of the LMIC library should ensure that the sensor node does not exceed the maximum duty cycle for LoRaWAN of 1%. This somehow also affects the waiting time for a possible downlink data packet coming from the gateway. As a consequence, the sensor node is not only active for around 2.5 seconds (0.5 seconds to submit the most recent datapacket to the gateway (uplink) and 2 seconds to wait for possible downlink data packets), but sometimes for about 5-6 seconds before it goes back into deep sleep mode (this can be seen from the duration the red LED is activated on the board). These extra seconds awake (with the LED and the LoRa transceiver module switched on) reduce battery lifetime significantly. The ATmega32u4 microcontroller does not have these problems and can go faster back to deep sleep mode. As a result the Adafruit Feather 32u4 LoRa board can run with a 1000 mAh LiPo battery for 5 months and the Adafruit Feather M0 LoRa board with a 2000 mAh LiPo battery for only 3 months.

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The sketch given above connects the sensor node with The Things Network (TTN) using the Activation-by-Personalisation (ABP) mode. In this mode, the required keys for data encryption and session management are created manually using the TTN console window and must be pasted into the source code of the sketch provided in software section . In order to get this running, you will need to create a new device <https://www.thethingsnetwork.org/docs/devices/registration.html>`_ in the TTN console window. This assumes that you already have a TTN user account (which needs to be created otherwise). In the settings menu of the newly created device the ABP mode must be selected and the settings must be saved. Then copy the DevAddr, the NwkSKey, and the AppSKey from the TTN console web page of the newly registered device and paste them into the proper places in the sketch above. Please make sure that you choose for each of the three keys the correct byte ordering (MSB for all three keys). A detailed explanation of these steps is given here. Then the sketch can be compiled and uploaded to the Adafruit Feather M0 LoRa microcontroller.

Important hint: everytime the sensor node is reset or being started again, make sure to reset the frame counter of the registered sensor in the TTN console web page of the registered device. The reason is that in LoRaWAN all transmitted data packets have a frame counter, which is incremented after each data frame being sent. This way a LoRaWAN application can avoid receiving and using the same packet again (replay attack). When TTN receives a data packet, it checks if the frame number is higher than the last one received before. If not, the received packet is considered to be old or a replay attack and is discarded. When the sensor node is reset or being started again, its frame counter is also reset to 0, hence, the TTN application assumes that all new packages are old, because their frame counter is lower than the last frame received (before the reset). A manual frame counter reset is only necessary when registering the node using ABP mode. In OTAA mode the frame counter is automatically reset in the sensor node and the TTN network server.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (Payload Decoder Function). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak ). Such a forwarding can be configured in the TTN console in the “Integrations” tab. TTN payload decoder for Adafruit M0 LoRa sensor node given here checks if a packet was received on LoRaWAN port 7 and then assumes that it consists of the 6 bytes encoded as described above. It creates the three Javascript objects ‘temperature’, ‘humidity’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==7) {…} statement by adding else if (port==8) {…} else if (port==9) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this page. Here is a screenshot showing the webclient: (Note that the sensor node was wrongly registered with TTN using the name adafruit-feather-32u4-lora - it should have been adafruit-feather-m0-lora. Hence, while the legend says it is a 32u4 microcontroller in fact it is the M0)

_images/webclient2.png

Web client for data visualization

Code files

Arduino Sketch for Adafruit M0 LoRa sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/*******************************************************************************
 * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
 *
 * Permission is hereby granted, free of charge, to anyone
 * obtaining a copy of this document and accompanying files,
 * to do whatever they want with them without any restriction,
 * including, but not limited to, copying, modification and redistribution.
 * NO WARRANTY OF ANY KIND IS PROVIDED.
 *
 * This example sends a valid LoRaWAN packet with payload "Hello,
 * world!", using frequency and encryption settings matching those of
 * the The Things Network.
 *
 * This uses ABP (Activation-by-personalisation), where a DevAddr and
 * Session keys are preconfigured (unlike OTAA, where a DevEUI and
 * application key is configured, while the DevAddr and session keys are
 * assigned/generated in the over-the-air-activation procedure).
 *
 * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
 * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
 * violated by this sketch when left running for longer)!
 *
 * To use this sketch, first register your application and device with
 * the things network, to set or generate a DevAddr, NwkSKey and
 * AppSKey. Each device should have their own unique values for these
 * fields.
 *
 * Do not forget to define the radio type correctly in config.h.
 *
 *******************************************************************************/
 
// #define SERIALDEBUG
 
#ifdef SERIALDEBUG
  #define SERIALDEBUG_PRINT(...) Serial.print(__VA_ARGS__)
  #define SERIALDEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#else
  #define SERIALDEBUG_PRINT(...)
  #define SERIALDEBUG_PRINTLN(...)
#endif
 
 
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
 
#include <Adafruit_SleepyDog.h>
 
// #include <Adafruit_Sensor.h>
#include <DHT.h>
// #include <DHT_U.h>
 
#define DHTPIN            12        // Pin which is connected to the DHT sensor.
#define DHTTYPE           DHT22     // DHT 22 (AM2302)
 
// DHT_Unified dht(DHTPIN, DHTTYPE);
DHT dht(DHTPIN, DHTTYPE);
 
#define VBATPIN A7
 
// LoRaWAN NwkSKey, network session key
// This should be in big-endian (aka msb).
static const PROGMEM u1_t NWKSKEY[16] = {NETWORK_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN AppSKey, application session key
// This should also be in big-endian (aka msb).
static const u1_t PROGMEM APPSKEY[16] = {APPLICATION_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed, so this should be in big-endian (aka msb) too.
static const u4_t DEVADDR = 0x260XXXXX   ; // <-- Change this address for every node!
 
// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }
 
static uint8_t mydata[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0xA};
static osjob_t sendjob;
 
// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL = 1;       // seconds transmit cycle plus ...
const unsigned SLEEP_TIME = 60*4+55;  // seconds sleep time plus ...
const unsigned MEASURE_TIME = 2;      // seconds measuring time should lead to ...
                                      // 5 minute(s) total cycle time
 
// Pin mapping
const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {3, 6, LMIC_UNUSED_PIN},
};
 
 
void onEvent (ev_t ev) {
//    Serial.print(os_getTime());
//    Serial.print(": ");
    SERIALDEBUG_PRINT(os_getTime());
    SERIALDEBUG_PRINT(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
            SERIALDEBUG_PRINTLN(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            SERIALDEBUG_PRINTLN(F("EV_JOINING"));
            break;
        case EV_JOINED:
            SERIALDEBUG_PRINTLN(F("EV_JOINED"));
            break;
        case EV_RFU1:
            SERIALDEBUG_PRINTLN(F("EV_RFU1"));
            break;
        case EV_JOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
            SERIALDEBUG_PRINTLN(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
            if (LMIC.txrxFlags & TXRX_ACK)
              SERIALDEBUG_PRINTLN(F("Received ack"));
            if (LMIC.dataLen) {
              SERIALDEBUG_PRINT(F("Received "));
              SERIALDEBUG_PRINT(LMIC.dataLen);
              SERIALDEBUG_PRINTLN(F(" bytes of payload"));
            }
            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
             
            SERIALDEBUG_PRINTLN("going to sleep now ... ");
            // lmic library sleeps automatically after transmission has been completed
            for(int i= 0; i < SLEEP_TIME / 16; i++) {
              Watchdog.sleep(16000); // maximum seems to be 16 seconds
              SERIALDEBUG_PRINT('.');
            }
            if (SLEEP_TIME % 16) {
              Watchdog.sleep((SLEEP_TIME % 16)*1000);
              SERIALDEBUG_PRINT('*');             
            }
            SERIALDEBUG_PRINTLN("... woke up again");
             
            break;
        case EV_LOST_TSYNC:
            SERIALDEBUG_PRINTLN(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            SERIALDEBUG_PRINTLN(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            SERIALDEBUG_PRINTLN(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            SERIALDEBUG_PRINTLN(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            SERIALDEBUG_PRINTLN(F("EV_LINK_ALIVE"));
            break;
         default:
            SERIALDEBUG_PRINTLN(F("Unknown event"));
            break;
    }
}
 
void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) {
        SERIALDEBUG_PRINTLN(F("OP_TXRXPEND, not sending"));
    } else {
        // Prepare upstream data transmission at the next possible time.
 
        float temperature, humidity, measuredvbat;
        int16_t int16_temperature, int16_humidity, int16_vbat;
     
        // Start a measurement to update the sensor's internal temperature & humidity reading
        SERIALDEBUG_PRINTLN("Start measurement...");
        temperature = dht.readTemperature();
        // delay(2000);
        Watchdog.sleep(2000);
        // Now read the recently measured temperature (2 secs ago) as Celsius (the default)
        temperature = dht.readTemperature();
        // Read the recently measured humidity (2 secs ago)
        humidity = dht.readHumidity();
        SERIALDEBUG_PRINTLN("... finished!");
     
        // Check if any reads failed and exit early (to try again).
        if (isnan(humidity) || isnan(temperature)) {
            SERIALDEBUG_PRINTLN("Failed to read from DHT sensor!");
            for (int i=0; i<5; i++) {
              digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH                   
              delay(150);
              digitalWrite(LED_BUILTIN, LOW);    // turn the LED on by making the voltage HIGH                   
              delay(150);
            }
            // ok, then wait for another period and try it again
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
        } else {
            SERIALDEBUG_PRINT("Humidity: ");
            SERIALDEBUG_PRINT(humidity);
            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Temperature: ");
            SERIALDEBUG_PRINT(temperature);
            SERIALDEBUG_PRINT(" *C ");
 
            int16_temperature = 100*temperature;
            int16_humidity = 100*humidity;
            mydata[0] = (byte) (int16_temperature >> 8);
            mydata[1] = (byte) (int16_temperature & 0x00FF);
            mydata[2] = (byte) (int16_humidity >> 8);
            mydata[3] = (byte) (int16_humidity & 0x00FF);
 
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2;    // we divided by 2, so multiply back
            measuredvbat *= 3.3;   // Multiply by 3.3V, our reference voltage
            measuredvbat /= 1023;  // convert to voltage
            int16_vbat = round(measuredvbat * 100);
            mydata[4] = (byte) (int16_vbat >> 8);
            mydata[5] = (byte) (int16_vbat & 0x00FF);
            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINTLN(measuredvbat);
             
//            LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
 
            // send the 6 bytes payload to LoRaWAN port 7
            LMIC_setTxData2(7, mydata, 6, 0);
            SERIALDEBUG_PRINTLN(F("Packet queued")); 
            digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH
        }
         
        // LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0);
        // Serial.println(F("Packet queued"));
    }
    // Next TX is scheduled after TX_COMPLETE event.
}
 
void setup() {
    delay(5000);
 
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
 
#ifdef SERIALDEBUG
    Serial.begin(115200);
    // while (!Serial);
#endif
 
    dht.begin();
     
    SERIALDEBUG_PRINTLN(F("Starting"));
 
    #ifdef VCC_ENABLE
    // For Pinoccio Scout boards
    pinMode(VCC_ENABLE, OUTPUT);
    digitalWrite(VCC_ENABLE, HIGH);
    delay(1000);
    #endif
 
    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
 
    // Set static session parameters. Instead of dynamically establishing a session
    // by joining the network, precomputed session parameters are be provided.
    #ifdef PROGMEM
    // On AVR, these values are stored in flash and only copied to RAM
    // once. Copy them to a temporary buffer here, LMIC_setSession will
    // copy them into a buffer of its own again.
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
    #else
    // If not running an AVR with PROGMEM, just use the arrays directly
    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
    #endif
 
    #if defined(CFG_eu868)
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.
    // NA-US channels 0-71 are configured automatically
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.
    #elif defined(CFG_us915)
    // NA-US channels 0-71 are configured automatically
    // but only one group of 8 should (a subband) should be active
    // TTN recommends the second sub band, 1 in a zero based count.
    // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
    LMIC_selectSubBand(1);
    #endif
 
    // 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
    do_send(&sendjob);
}
 
void loop() {
    os_runloop_once();
}
TTN payload decoder for Adafruit M0 LoRa sensor node
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Decoder (bytes, port) {
  var result = {};
  var transformers = {};
   
  if (port==7) {
     transformers = {
      'temperature': function transform (bytes) {
          value=bytes[0]*256 + bytes[1];
          if (value>=32768) value=value-65536;
          return value/100.0;
        },
      'humidity': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
      'vbattery': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
    }
   
    result['temperature'] = {
      value: transformers['temperature'](bytes.slice(0, 2)),
      uom: 'Celsius',
    }
   
    result['humidity'] = {
      value: transformers['humidity'](bytes.slice(2, 4)),
      uom: 'Percent',
    }
   
    result['vbattery'] = {
      value: transformers['vbattery'](bytes.slice(4, 6)),
      uom: 'Volt',
    }
  }
   
  return result;
}

Dragino LoRa Arduino Shield

This tutorial is made to showcase the use of Dragino LoRa Arduino board to create a LoRaWAN enabled sensor node. In the following example, a temperature and humidity sensor was used with the Dragino LoRa board.

Hardware

Microcontroller
_images/GPS_Shield_with_Lora_BEE.jpg

LoRa/GPS Shield from Dragino. LoRa/GPS Shield Wiki with explanations, datasheets, and examples.

The employed microcontroller board is an Arduino Uno R3 variant (i.e. it is a cheap clone of the Arduino Uno R3). It is operated by the 8bit ATmega328 microcontroller running at 16MHz. It has 32 KB flash memory (to store the program code), 1 KB EEPROM (to store configuration data), and 2 KB of RAM (to store variables, status information, and buffers). The operating voltage of the board is 5V (this is important when attaching sensors and other peripherals; they also must operate on 5V). The board offers 20 general purpose digital input/output pins (20 GPIOs) of which 6 can be used as analog input pins (with 10bit analog digital converters (ADC)) and 6 as PWM outputs, one serial port (programmable Universal Asynchronous Receiver and Transmitter, UART), one I2C port, one SPI port, one USB port (which is attached to a USB/Serial converter that is connected to the hardware serial port). Arduino Uno R3 compatible boards are available in German shops from around 5 € to 10 €. The original Arduino Uno R3 board costs around 22 €.

The Dragino LoRa/GPS Shield runs on 5V and is directly attached to the connectors of the Arduino Uno R3 microcontroller board. It comes with a built-in LoRa transmitter and receiver chip SX1276 from the company Semtech that is dedicated to the 868 MHz frequency band. The SX1276 module is connected via SPI interface to the microcontroller. For that purpose, Lora CLK, Lora D0, and Lora DI must be jumpered to SCK, MISO, and MOSI respectively (on the left side of the Dragino shield when looking on the top side of the shield with the Antenna connectors showing to the right). Lora DIO1 and Lora DIO2 must be jumpered to Arduino Digital Pin 6 and Pin 7 respectively. Since the module only implements the LoRa physical layer, the LoRaWAN protocol stack must be implemented in software on the microcontroller. We are using the Arduino library LMIC for that purpose (see below). The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C.

The board also contains a Quectel L80 GPS module (based on the MTK MT3339 GPS receiver) with a built-in antenna. According to the Dragino Wiki “this GPS module can calculate and predict orbits automatically using the ephemeris data (up to 3 days) stored in internal flash memory, so the shield can fix position quickly even at indoor signal levels with low power consumption”. The GPS module has a serial UART interface that can be connected in different ways to the Arduino microcontroller. The default data transmission rate is 9600 baud, the default position reporting rate is 1s (1 Hz). The module is capable to report up to 10 positions per second (10 Hz). Supported protocols are NMEA 0183 and MediaTek PMTK. Note that the ATmega328 microcontroller has only one hardware serial UART interface and this is already connected via a USB/Serial converter to the USB port of the Arduino board. In order to attach the serial interface of the GPS module to the microcontroller two general purpose IO lines (GPIOs) are being used and the serial protocol is implemented in software. The GPS_RXD pin on the Dragino Shield must be connected to Arduino Digital Pin 4 and the GPS_TXD pin to Digital Pin 3 using two wires. No jumpers must be present for GPS_RXD and GPS_TXD (besides the two wires mentioned above to Digital Pins 4 and 3). The Dragino LoRa/GPS Shield is available in German shops for around 34 € to 40 €.

Since the Arduino Uno R3 board normally has to be powered externally via the USB port or the power connector, we have added the Solar Charger Shield V2.2 from the company Seeedstudio. This shield is directly attached to the connectors of the Arduino Uno R3 microcontroller board and sits in-between the Arduino board (bottom) and the LoRa/GPS Shield (top). A lithium polymer LiPo battery with 3.7V can be attached to the shield. The 3.7V of the battery is transformed to 5V as required by the Arduino microcontroller board. The battery is automatically recharged when the Arduino board is powered externally (over USB or the power connector). Also a photovoltaic panel with 4.8-6V can be attached to the shield to recharge the battery. The Solar Charger Shield V2.2 can report the current battery voltage level. For that purpose we had to solder a bridge on the shield at the connector marked as ‘R7’. Over a voltage divider the battery anode is connected to Analog Pin A0 and can be queried using the built-in analog/digital converter. The Solar Charger Shield V2.2 is available in German shops for around 12 € to 18 €.

Sensor

We have attached a DHT22 sensor to the microcontroller board, which measures air temperature and humidity. The minimal time interval between two measurements is 2 seconds. All data transfers between the DHT22 and the microcontroller use a single digital line. The sensor data pin is attached to a GPIO pin (here: Digital Pin 5) of the microcontroller. In addition, a so-called pull-up resistor of 4.7k to 10k Ohm must be connected between the data line and VCC (+3.3V). The DHT22 datasheet provides more technical details about the DHT22 Sensor. A tutorial on how to use the DHT22 sensor with Arduino microcontrollers is provided here. The sensor is available in German shops for around 4 € to 10 €.

_images/Dragino_LoRa_GPS_Shield_with_DHT22.jpg

The Arduino Uno R3 (bottom) with attached Solar Charger Shield and a 2000 mAh lithium polymer LiPo battery (middle), the Dragino LoRa/GPS Shield with attached antenna (top), and an attached DHT22 temperature / humidity sensor (white box on the left).

Software

The sensor node has been programmed using the Arduino IDE. Please note, that in the Arduino framework a program is called a ‘Sketch’.

After the sketch has successfully established a connection to The Things Network it reports the air temperature, humidity, and the voltage of a (possibly) attached LiPo battery every 5 minutes. All three values are being encoded in two byte integer values each (in most significant byte order) and then sent as a 6 bytes data packet to the respective TTN application using LoRaWAN port 7. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement; this must be considered in the Payload Decoding Function used in The Things Network (see this section).

The next eight bytes contain two 32 bit integer values (MSB) for latitude and longitude . In order to a) provide enough precision and b) avoid negative values, the original angles (given as decimal fractions) are first added with an offset (90.0 degrees for the latitude and 180.0 degrees for the longitude) and then multiplied by 1,000,000. These transformations have to be reverted in the Payload Decoding Function. The next two bytes represent a 16 bit integer value for the altitude (MSB). The next byte contains the current number of satellites seen by the GPS receiver. Note that only when this number is greater or equal to 4 the provided GPS position is a current one. Finally, the last two bytes contain a 16 bit integer value (MSB) for the battery voltage in centivolts (this value will be divided by 100 in the Payload Decoding Function to provide volts). The entire data packet is sent to the respective TTN application using LoRaWAN port 9. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol.

Currently we are not making use of the sleep mode, because we have to find out how to deal with the GPS receiver in conjunction with deep sleep mode. This means that the board is constantly drawing a significant amount of power reducing battery life considerably. Using the current sketch the sensor node can operate roughly 6 hours on battery power before it has to be recharged. Besides software improvements there are also other possibilities to reduce power consumption: the Arduino board and the Dragino LoRa/GPS Shield have power LEDs which are constantly lit during operation. Furthermore, the Dragino LoRa/GPS Shield has an indicator LED that blinks when the GPS module is successfully receiving position fixes. These LEDs could be desoldered to reduce the energy consumption of the sensor node.

The employed SX1276 LoRa module on the Dragino LoRa/GPS shield does not provide built-in support of the LoRaWAN protocol. Thus, it has to be implemented on the ATmega328 microcontroller. We use the IBM LMIC (LoraMAC-in-C) library for Arduino, which can be downloaded from this repository. Since the ATmega328 microcontroller only has 32 KB of flash memory and the LMIC library is taking most of it, there is only very limited code space left for the application dealing with the sensors (about 2 KB). Nevertheless, this is sufficient to query some sensors like in our example the DHT22 and to decode the GPS data. The source code is given in the following section: Arduino Sketch for Dragino LoRa sensor node

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The sketch given above connects the sensor node with The Things Network (TTN) using the Activation-by-Personalisation (ABP) mode. In this mode, the required keys for data encryption and session management are created manually using the TTN console window and must be pasted into the source code of the sketch below. In order to get this running, you will need to create a new device in the TTN console window. This assumes that you already have a TTN user account (which needs to be created otherwise). In the settings menu of the newly created device the ABP mode must be selected and the settings must be saved. Then copy the DevAddr, the NwkSKey, and in the AppSKey from the TTN console web page of the newly registered device and paste them into the proper places in the sketch above. Please make sure that you choose for each of the three keys the correct byte ordering (MSB for all three keys). A detailed explanation of these steps is given here. Then the sketch can be compiled and uploaded to the Arduino Uno R3 microcontroller.

Important hint: everytime the sensor node is reset or being started again, make sure to reset the frame counter of the registered sensor in the TTN console web page of the registered device. The reason is that in LoRaWAN all transmitted data packets have a frame counter, which is incremented after each data frame being sent. This way a LoRaWAN application can avoid receiving and using the same packet again (replay attack). When TTN receives a data packet, it checks if the frame number is higher than the last one received before. If not, the received packet is considered to be old or a replay attack and is discarded. When the sensor node is reset or being started again, its frame counter is also reset to 0, hence, the TTN application assumes that all new packages are old, because their frame counter is lower than the last frame received (before the reset). A manual frame counter reset is only necessary when registering the node using ABP mode. In OTAA mode the frame counter is automatically reset in the sensor node and the TTN network server.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (TTN payload decoder for Dragino LoRa sensor node). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak ). Such a forwarding can be configured in the TTN console in the “Integrations” tab. The Payload Decoder Function given below checks if a packet was received on LoRaWAN port 9 and then assumes that it consists of the 17 bytes encoded as described above. It creates the seven Javascript objects ‘temperature’, ‘humidity’, ‘lat’, ‘lon’, ‘altitude’, ‘sat’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==9) {…} statement by adding else if (port==7) {…} else if (port==8) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this page. Here is a screenshot showing the webclient:

_images/SOS-Webclient-dragino-shield-with-gps.png

Web client for data visualization

Code files

Arduino Sketch for Dragino LoRa sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
/*******************************************************************************
 * Arduino Sketch for a LoRaWAN sensor node that is registered with
 * 'The Things Network' (TTN) www.thethingsnetwork.org
 *
 * Author:  Thomas H. Kolbe, thomas.kolbe@tum.de
 * Version: 0.4
 * Last update: 2018-11-28
 *
 * The sensor node is based on the Arduino Uno3 microcontroller board
 * and the Dragino Lora Shield with GPS receiver. Also a Seeedstudio
 * Solar Charger Shield V2.2 is connected to provide a battery power
 * supply with the possibility to use a small PV panel for recharging.
 * See https://wiki.dragino.com/index.php?title=Lora/GPS_Shield
 * and http://wiki.seeedstudio.com/Solar_Charger_Shield_V2.2/
 *
 * The sensor node uses a DHT22 sensor measuring air temperature and humidity.
 * The GPS receiver of the Dragino Lora Shield is used to locate the node.
 * The voltage of an attached LiPo battery is monitored and sent as an
 * additional observation.
 *
 * All three values are encoded as 2 byte integer values each.
 * Hence, the total message payload is 6 bytes. Before the values are converted
 * to integers they are multiplied by 100 to preserve 2 digits after the decimal
 * point. Thus, the received values must be divided by 100 to obtain the measured
 * values. The payload is sent every 60s to LoRaWAN port 9. The following
 * Javascript function can be used as a payload decoding function in TTN:
 *
 * function Decoder(bytes, port) {
 *   // Decode an uplink message from a buffer
 *   // (array) of bytes to an object of fields.
 *   if (port==7) {
 *     var decoded = {
 *       "temperature": (bytes[0] << 8 | bytes[1]) / 100.0,
 *       "humidity": (bytes[2] << 8 | bytes[3]) / 100.0,
 *       "vbattery": (bytes[4] << 8 | bytes[5]) / 100.0
 *     };
 *   } else {
 *     var decoded = null;
 *   }
 *   return decoded;
 * }
 *
 * In between two data transmissions the microcontroller board can go
 * into sleep mode to reduce energy consumption for extended operation
 * time when running on battery. Usage of the sleep mode must be
 * explicitly configured below.
 *
 * Important hint: everytime the sensor node is reset or being started again,
 * make sure to reset the frame counter of the registered sensor in the
 * TTN console at https://console.thethingsnetwork.org. The reason is that
 * in LoRaWAN all transmitted packets have a frame counter, which is
 * incremented after each data frame being sent. This way a LoRaWAN application
 * can avoid receiving and using the same packet again (replay attack). When
 * TTN receives a data packet, it checks if the frame number is higher than
 * the last one received before. If not, the received packet is considered
 * to be old or a replay attack and is discarded. When the sensor node is
 * reset or being started again, its frame counter is also reset to 0, hence,
 * the TTN application assumes that all new packages are old, because their
 * frame counter is lower than the last frame received (before the reset).
 *
 * Note, that the DHT22 data pin must be connected to Digital Pin 5 of the
 * Arduino board. A resistor of 4.7k - 10k Ohm must be connected to
 * the data pin and VCC (+5V). The GPS_RXD pin on the Dragiono Shield must
 * be connected to Arduino Digital Pin 4 and the GPS_TXD pin to Digital Pin 3.
 * Lora CLK, Lora D0, and Lora DI must be jumpered to SCK, MISO, and MOSI
 * respectively (on the left side of the Dragino shield when looking on the
 * top side of the shield with the Antenna connectors shwoing to the right).
 * Lora DIO1 and Lora DIO2 must be jumpered to Arduino Digital Pin 6 and
 * Pin 7 respectively. No jumpers must be present for GPS_RXD and GPS_TXD
 * (besides the two wires mentioned above to Digital Pins 4 and 3).
 *
 * The code is based on the Open Source library LMIC implementing the LoRaWAN
 * protocol stack on top of a given LoRa transceiver module (here: RFM95 from
 * HopeRF, which uses the Semtech SX1276 LoRa chip). The library is originally
 * being developed by IBM and has been ported to the Arduino platform. See
 * notes below from the original developers.
 *
 *******************************************************************************
 * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
 *
 * Permission is hereby granted, free of charge, to anyone
 * obtaining a copy of this document and accompanying files,
 * to do whatever they want with them without any restriction,
 * including, but not limited to, copying, modification and redistribution.
 * NO WARRANTY OF ANY KIND IS PROVIDED.
 *
 * This uses ABP (Activation-by-personalisation), where a DevAddr and
 * Session keys are preconfigured (unlike OTAA, where a DevEUI and
 * application key is configured, while the DevAddr and session keys are
 * assigned/generated in the over-the-air-activation procedure).
 *
 * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
 * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
 * violated by this sketch when left running for longer)!
 *
 * To use this sketch, first register your application and device with
 * the things network, to set or generate a DevAddr, NwkSKey and
 * AppSKey. Each device should have their own unique values for these
 * fields.
 *
 * Do not forget to define the radio type correctly in config.h.
 *
 *******************************************************************************/
 
// If the following line is uncommented, messages are being printed out to the
// serial connection for debugging purposes. When using the Arduino Integrated
// Development Environment (Arduino IDE), these messages are displayed in the
// Serial Monitor selecting the proper port and a baudrate of 115200.
 
// #define SERIALDEBUG
 
#ifdef SERIALDEBUG
  #define SERIALDEBUG_PRINT(...) Serial.print(__VA_ARGS__)
  #define SERIALDEBUG_PRINTLN(...) Serial.println(__VA_ARGS__)
#else
  #define SERIALDEBUG_PRINT(...)
  #define SERIALDEBUG_PRINTLN(...)
#endif
 
// If the following line is uncommented, the sensor node goes into sleep mode
// in between two data transmissions. Also the 2secs time between the
// initialization of the DHT22 sensor and the reading of the observations
// is spent in sleep mode.
// Note, that on the Adafruit Feather 32u4 LoRa board the Serial connection
// gets lost as soon as the board goes into sleep mode, and it will not be
// established again. Thus, the definition of SERIALDEBUG should be commented
// out above when using sleep mode with this board.
 
// #define SLEEPMODE
 
#ifdef SLEEPMODE
  #include <Adafruit_SleepyDog.h>
#endif
 
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
 
#include <DHT.h>
#define DHTPIN           5        // Arduino Digital Pin which is connected to the DHT sensor for Arduino.
#define DHTTYPE          DHT22    // DHT 22 (AM2302)
DHT dht(DHTPIN, DHTTYPE);         // create the sensor object
 
#include <TinyGPS.h>
TinyGPS gps;
bool newGPSdata = false;
 
#include <SoftwareSerial.h>
SoftwareSerial SWSerial(3, 4);
 
#define VBATPIN A0                // battery voltage is measured from Analog Input A0 for Seeed Solar Shield V2.2
 
// The following three constants (NwkSKey, AppSKey, DevAddr) must be changed
// for every new sensor node. We are using the LoRaWAN ABP mode (activation by
// personalisation) which means that each sensor node must be manually registered
// in the TTN console at https://console.thethingsnetwork.org before it can be
// started. In the TTN console create a new device and choose ABP mode in the
// settings of the newly created device. Then, let TTN generate the NwkSKey and
// and the AppSKey and copy them (together with the device address) from the webpage
// and paste them below.
 
// LoRaWAN NwkSKey, network session key
// This should be in big-endian (aka msb).
static const PROGMEM u1_t NWKSKEY[16] = {NETWORK_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN AppSKey, application session key
// This should also be in big-endian (aka msb).
static const u1_t PROGMEM APPSKEY[16] = {APPLICATION_SESSION_KEY_HERE_IN_MSB_FORMAT};

// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed, so this should be in big-endian (aka msb) too.
static const u4_t DEVADDR = 0x260XXXXX   ; // <-- Change this address for every node!
 
// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in config.h, otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }
 
// The following array of bytes is a placeholder to contain the message payload
// which is transmitted to the LoRaWAN gateway. We are currently only using 6 bytes.
// Please make sure to extend the size of the array, if more sensors should be
// attached to the sensor node and the message payload becomes larger than 10 bytes.
static uint8_t mydata[17] = {0, 1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xE, 0xF, 0x10}; 
 
static osjob_t sendjob;
 
// Schedule transmission every TX_INTERVAL seconds (might become longer due to duty
// cycle limitations). The total interval time is 2secs for the measurement
// plus 3secs for the LoRaWAN packet transmission plus TX_INTERVAL_AFTER_SLEEP seconds
// plus SLEEP_TIME seconds (microcontroller in sleep mode)
const unsigned TX_INTERVAL = 300;       // overall cycle time (send one set of observations every 5 mins)
// const unsigned TX_INTERVAL = 30;       // overall cycle time (send one set of observations every 30 secs)
const unsigned TX_TIME = 3;             // rough estimate of transmission time of a single packet
const unsigned MEASURE_TIME = 2;        // seconds measuring time
const unsigned SLEEP_TIME = TX_INTERVAL - TX_TIME - MEASURE_TIME;
const unsigned WAIT_TIME = TX_INTERVAL - TX_TIME - MEASURE_TIME;
 
// Pin mapping of the LoRa transceiver. Please make sure that DIO1 is connected
// to Arduino Digital Pin 6 using an external wire. DIO2 is left unconnected
// (it is only required, if FSK modulation instead of LoRa would be used).
const lmic_pinmap lmic_pins = {
    .nss = 10,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 9,
    .dio = {2, 6, 7},
};
 
void onEvent (ev_t ev) {
    SERIALDEBUG_PRINT(os_getTime());
    SERIALDEBUG_PRINT(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
            SERIALDEBUG_PRINTLN(F("EV_SCAN_TIMEOUT"));
            break;
        case EV_BEACON_FOUND:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_FOUND"));
            break;
        case EV_BEACON_MISSED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_MISSED"));
            break;
        case EV_BEACON_TRACKED:
            SERIALDEBUG_PRINTLN(F("EV_BEACON_TRACKED"));
            break;
        case EV_JOINING:
            SERIALDEBUG_PRINTLN(F("EV_JOINING"));
            break;
        case EV_JOINED:
            SERIALDEBUG_PRINTLN(F("EV_JOINED"));
            break;
        case EV_RFU1:
            SERIALDEBUG_PRINTLN(F("EV_RFU1"));
            break;
        case EV_JOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_JOIN_FAILED"));
            break;
        case EV_REJOIN_FAILED:
            SERIALDEBUG_PRINTLN(F("EV_REJOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
            SERIALDEBUG_PRINTLN(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
            if (LMIC.txrxFlags & TXRX_ACK)
              SERIALDEBUG_PRINTLN(F("Received ack"));
            if (LMIC.dataLen) {
#ifdef SERIALDEBUG
              SERIALDEBUG_PRINT(F("Received "));
              SERIALDEBUG_PRINT(LMIC.dataLen);
              SERIALDEBUG_PRINT(F(" bytes of payload: 0x"));
              for (int i=0; i<LMIC.dataLen; i++) {
                if (LMIC.frame[LMIC.dataBeg + i] < 0x10) {
                  SERIALDEBUG_PRINT(F("0"));
                }
                SERIALDEBUG_PRINT(LMIC.frame[LMIC.dataBeg + i], HEX);
              }
              SERIALDEBUG_PRINTLN();
#endif
              // add your code to handle a received downlink data packet here
            }
 
#ifdef SLEEPMODE           
            // Schedule next transmission in 1ms second after the board returns from sleep mode
            os_setTimedCallback(&sendjob, os_getTime()+ms2osticks(1), do_send);
             
            SERIALDEBUG_PRINTLN("going to sleep now ... ");
            // lmic library sleeps automatically after transmission has been completed
            for(int i= 0; i < SLEEP_TIME / 8; i++) {
              Watchdog.sleep(8000); // maximum seems to be 8 seconds
              SERIALDEBUG_PRINT('.');
            }
            if (SLEEP_TIME % 8) {
              Watchdog.sleep((SLEEP_TIME % 8)*1000);
              SERIALDEBUG_PRINT('*');             
            }
            SERIALDEBUG_PRINTLN("... woke up again");
 
            // We need to reset the duty cycle limits within the LMIC library.
            // The reason is that in sleep mode the Arduino system timers millis and micros
            // do not get incremented. However, LMIC monitors the adherence to the
            // LoRaWAN duty cycle limitations using the system timers millis and micros.
            // Since LMIC does not know that we have slept for a long time and duty
            // cycle requirements in fact are met, we must reset the respective LMIC timers
            // in order to prevent the library to wait for some extra time (which would
            // not use sleep mode and, thus, would waste battery energy).
            LMIC.bands[BAND_MILLI].avail = os_getTime();
            LMIC.bands[BAND_CENTI].avail = os_getTime();
            LMIC.bands[BAND_DECI].avail = os_getTime();
#else
            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(WAIT_TIME), do_send);
#endif          
            break;
        case EV_LOST_TSYNC:
            SERIALDEBUG_PRINTLN(F("EV_LOST_TSYNC"));
            break;
        case EV_RESET:
            SERIALDEBUG_PRINTLN(F("EV_RESET"));
            break;
        case EV_RXCOMPLETE:
            // data received in ping slot
            SERIALDEBUG_PRINTLN(F("EV_RXCOMPLETE"));
            break;
        case EV_LINK_DEAD:
            SERIALDEBUG_PRINTLN(F("EV_LINK_DEAD"));
            break;
        case EV_LINK_ALIVE:
            SERIALDEBUG_PRINTLN(F("EV_LINK_ALIVE"));
            break;
         default:
            SERIALDEBUG_PRINTLN(F("Unknown event"));
            break;
    }
}
 
void do_send(osjob_t* j){
    // Check if there is not a current TX/RX job running
    if (LMIC.opmode & OP_TXRXPEND) {
        SERIALDEBUG_PRINTLN(F("OP_TXRXPEND, not sending"));
    } else {
        // Prepare upstream data transmission at the next possible time.
 
        float temperature, humidity, measuredvbat, lat, lon, alt;
        int16_t int16_temperature, int16_humidity, int16_vbat, int16_alt;
        int32_t int32_lat, int32_lon;
        unsigned long age;
        byte sat=0;
     
        // Start a measurement to update the sensor's internal temperature & humidity reading.
        // Note, that when fetching measurements from a DHT22 sensor, the reported
        // values belong to the measurement BEFORE the current measurement.
        // Therefore, in order to get current observations, we first perform a new measurement
        // and wait 2 secs (which is the minimum time between two sensor observations for
        // the DHT22) and then directly retrieve the observations again.
        temperature = dht.readTemperature();
//        temperature = 23;
#ifdef SLEEPMODE
        Watchdog.sleep(2000);
#else
        delay(2000);
#endif       
        // Now read the recently measured temperature (2 secs ago) as Celsius (the default)
        temperature = dht.readTemperature();
//        temperature = 23;
        // Read the recently measured humidity (2 secs ago)
        humidity = dht.readHumidity();
//        humidity = 66;
     
        // Check if any reads failed and exit early (to try again).
        if (isnan(humidity) || isnan(temperature)) {
            SERIALDEBUG_PRINTLN("Failed to read from DHT sensor!");
            // blink the LED five times to indicate that the sensor values could not be read
            for (int i=0; i<5; i++) {
              digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH                   
              delay(150);
              digitalWrite(LED_BUILTIN, LOW);    // turn the LED on by making the voltage HIGH                   
              delay(150);
            }
            // ok, then wait for another period and try it again
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
        } else {
            SERIALDEBUG_PRINT("Humidity: ");
            SERIALDEBUG_PRINT(humidity);
            SERIALDEBUG_PRINT(" %\t");
            SERIALDEBUG_PRINT("Temperature: ");
            SERIALDEBUG_PRINT(temperature);
            SERIALDEBUG_PRINT(" °C ");
 
            int16_temperature = round(100.0*temperature);
            int16_humidity = round(100.0*humidity);
            mydata[0] = (byte) (int16_temperature >> 8);
            mydata[1] = (byte) (int16_temperature & 0x00FF);
            mydata[2] = (byte) (int16_humidity >> 8);
            mydata[3] = (byte) (int16_humidity & 0x00FF);
 
            if (newGPSdata) {
                gps.f_get_position(&lat, &lon, &age);
                int32_lat = round(1000000.0*(lat+90.0));
                int32_lon = round(1000000.0*(lon+180.0));
                alt = gps.f_altitude();
                int16_alt = round(alt);
                sat = gps.satellites();           
                mydata[4] = (byte) (int32_lat >> 24);
                mydata[5] = (byte) ((int32_lat >> 16) & 0x00FF);
                mydata[6] = (byte) ((int32_lat >> 8) & 0x0000FF);
                mydata[7] = (byte) (int32_lat & 0x000000FF);
                mydata[8] = (byte) (int32_lon >> 24);
                mydata[9] = (byte) ((int32_lon >> 16) & 0x00FF);
                mydata[10] = (byte) ((int32_lon >> 8) & 0x0000FF);
                mydata[11] = (byte) (int32_lon & 0x000000FF);
                mydata[12] = (byte) (int16_alt >> 8);
                mydata[13] = (byte) (int16_alt & 0x00FF);
                mydata[14] = sat;
            } else {
                mydata[14] = 0;
            }
 
#ifdef VBATPIN
            measuredvbat = analogRead(VBATPIN);
            measuredvbat *= 2.0;      // we divided by 2, so multiply back
            measuredvbat *= 5.0;      // Multiply by 5V, our reference voltage
            measuredvbat /= 1023.0;   // convert to voltage
#else
            measuredvbat = 0.0;
#endif                     
            int16_vbat = round(measuredvbat * 100.0);
            mydata[15] = (byte) (int16_vbat >> 8);
            mydata[16] = (byte) (int16_vbat & 0x00FF);
            SERIALDEBUG_PRINT(" \t");
            SERIALDEBUG_PRINT("Battery Voltage: ");
            SERIALDEBUG_PRINT(measuredvbat);
            SERIALDEBUG_PRINTLN(" V");
            
            // Send the 17 bytes payload to LoRaWAN port 9 and do not request an acknowledgement.
            // The following call does not directly sends the data, but puts a "send job"
            // in the job queue. This job eventually is performed in the call "os_runloop_once();"
            // issued repeatedly in the "loop()" method below. After the transmission is
            // complete, the EV_TXCOMPLETE event is signaled, which is handled in the
            // event handler method "onEvent (ev_t ev)" above. In the EV_TXCOMPLETE branch
            // then a new call to the "do_send(osjob_t* j)" method is being prepared for
            // delayed execution with a waiting time of TX_INTERVAL seconds.
            LMIC_setTxData2(9, mydata, 17, 0);
            SERIALDEBUG_PRINTLN(F("Packet queued"));
            newGPSdata=false; 
            digitalWrite(LED_BUILTIN, HIGH);    // turn the LED on by making the voltage HIGH
             
            // Next TX is scheduled after TX_COMPLETE event.
        }
    }
}
 
void setup() {
    delay(5000);                    // give enough time to open serial monitor (if needed)
 
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW); // turn the LED off by making the voltage LOW
 
#ifdef SERIALDEBUG
    Serial.begin(115200);
    // while (!Serial);
#endif
 
    dht.begin();                    // initialize DHT22 sensor
     
    SERIALDEBUG_PRINTLN(F("Starting"));
 
    #ifdef VCC_ENABLE
    // For Pinoccio Scout boards
    pinMode(VCC_ENABLE, OUTPUT);
    digitalWrite(VCC_ENABLE, HIGH);
    delay(1000);
    #endif
 
    SWSerial.begin(9600);
 
    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
 
    // Set static session parameters. Instead of dynamically establishing a session
    // by joining the network, precomputed session parameters are be provided.
    #ifdef PROGMEM
    // On AVR, these values are stored in flash and only copied to RAM
    // once. Copy them to a temporary buffer here, LMIC_setSession will
    // copy them into a buffer of its own again.
    uint8_t appskey[sizeof(APPSKEY)];
    uint8_t nwkskey[sizeof(NWKSKEY)];
    memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
    memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
    LMIC_setSession (0x1, DEVADDR, nwkskey, appskey);
    #else
    // If not running an AVR with PROGMEM, just use the arrays directly
    LMIC_setSession (0x1, DEVADDR, NWKSKEY, APPSKEY);
    #endif
 
    #if defined(CFG_eu868)
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.
    // NA-US channels 0-71 are configured automatically
    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF12, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(7, 867900000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(8, 868800000, DR_RANGE_MAP(DR_FSK,  DR_FSK),  BAND_MILLI);      // g2-band
    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.
    #elif defined(CFG_us915)
    // NA-US channels 0-71 are configured automatically
    // but only one group of 8 should (a subband) should be active
    // TTN recommends the second sub band, 1 in a zero based count.
    // https://github.com/TheThingsNetwork/gateway-conf/blob/master/US-global_conf.json
    LMIC_selectSubBand(1);
    #endif
 
    // 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);
    LMIC_setDrTxpow(DR_SF9,14);
 
    // Start job. This will initiate the repetitive sending of data packets,
    // because after each data transmission, a delayed call to "do_send()"
    // is being scheduled again.
    do_send(&sendjob);
}
 
void loop() {
/* 
    // read from port 1, send to port 0:
    if (Serial.available()) {
      int inByte = Serial.read();
      SWSerial.write(inByte);
    }
  
    // read from port 0, send to port 1:
    if (SWSerial.available()) {
      int inByte = SWSerial.read();
      Serial.write(inByte);
    }
*/
    unsigned long chars;
    unsigned short sentences, failed;
 
    // For one second we parse GPS data and report some key values
//    for (unsigned long start = millis(); millis() - start < 1000;)
//    {
      while (SWSerial.available())
      {
        char c = SWSerial.read();
        // Serial.write(c); // uncomment this line if you want to see the GPS data flowing
        if (gps.encode(c)) // Did a new valid sentence come in?
          newGPSdata = true;
      }
//    }
 
/*
    if (newGPSdata)
    {
      float flat, flon;
      unsigned long age;
      gps.f_get_position(&flat, &flon, &age);
      Serial.print("LAT=");
      Serial.print(flat == TinyGPS::GPS_INVALID_F_ANGLE ? 0.0 : flat, 6);
      Serial.print(" LON=");
      Serial.print(flon == TinyGPS::GPS_INVALID_F_ANGLE ? 0.0 : flon, 6);
      Serial.print(" ALT=");
      Serial.print(gps.f_altitude() == TinyGPS::GPS_INVALID_F_ALTITUDE ? 0.0 : gps.f_altitude(), 6);    
      Serial.print(" SAT=");
      Serial.print(gps.satellites() == TinyGPS::GPS_INVALID_SATELLITES ? 0 : gps.satellites());
      Serial.print(" PREC=");
      Serial.print(gps.hdop() == TinyGPS::GPS_INVALID_HDOP ? 0 : gps.hdop());
    }
 
    gps.stats(&chars, &sentences, &failed);
    Serial.print(" CHARS=");
    Serial.print(chars);
    Serial.print(" SENTENCES=");
    Serial.print(sentences);
    Serial.print(" CSUM ERR=");
    Serial.println(failed);
    if (chars == 0)
      Serial.println("** No characters received from GPS: check wiring **");
*/
    os_runloop_once();
}
TTN payload decoder for Dragino LoRa sensor node
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
function Decoder(bytes, port) {
  var result = {};
  var transformers = {};

  if (port == 9) {
    transformers = {
      temperature: function transform(bytes) {
        value = bytes[0] * 256 + bytes[1];
        if (value >= 32768) value = value - 65536;
        return value / 100.0;
      },
      humidity: function transform(bytes) {
        return (bytes[0] * 256 + bytes[1]) / 100.0;
      },
      lat: function transform(bytes) {
        return (
          (bytes[0] * 16777216 + bytes[1] * 65536 + bytes[2] * 256 + bytes[3]) /
            1000000.0 -
          90.0
        );
      },
      lon: function transform(bytes) {
        return (
          (bytes[0] * 16777216 + bytes[1] * 65536 + bytes[2] * 256 + bytes[3]) /
            1000000.0 -
          180.0
        );
      },
      altitude: function transform(bytes) {
        return bytes[0] * 256 + bytes[1];
      },
      sat: function transform(bytes) {
        return bytes[0];
      },
      vbattery: function transform(bytes) {
        return (bytes[0] * 256 + bytes[1]) / 100.0;
      }
    };

    result["temperature"] = {
      value: transformers["temperature"](bytes.slice(0, 2)),
      uom: "Celsius"
    };

    result["humidity"] = {
      value: transformers["humidity"](bytes.slice(2, 4)),
      uom: "Percent"
    };

    result["lat"] = {
      value: transformers["lat"](bytes.slice(4, 8)),
      uom: "Degree"
    };

    result["lon"] = {
      value: transformers["lon"](bytes.slice(8, 12)),
      uom: "Degree"
    };

    result["altitude"] = {
      value: transformers["altitude"](bytes.slice(12, 14)),
      uom: "Meter"
    };

    result["sat"] = {
      value: transformers["sat"](bytes.slice(14, 15)),
      uom: "Count"
    };

    result["vbattery"] = {
      value: transformers["vbattery"](bytes.slice(15, 17)),
      uom: "Volt"
    };

    return result;
  }
}

Pycom LoPy4

This tutorial is made to showcase the use of Pycom LoPy4 board to create a LoRaWAN enabled sensor node. In the following example, a temperature and humidity sensor was used with the Pycom LoPy4 board.

Hardware

Microcontroller
_images/Pycom_LoPy4.jpg

LoPy4 from Pycom. LoPy4 pinout, documentation, example code.

The Pycom LoPy4 is a microcontroller board offering many radio frequency (RF) connection options, namely LoRa (and LoRaWAN), SIGFOX, Bluetooth (Classic and Low Energy, BLE), and WiFi. In contrast to most other microcontroller boards the LoPy4 is programmed in MicroPython, which is a special subset of the Python 3 programming language and libraries for microcontrollers. The module is operated by the Espressif ESP32 microcontroller board, which contains a dual-core Xtensa 32bit LX6 processor running with up to 240MHz, 8 MB of flash memory (to store the program code and some files within a file system), and 520 KB of RAM (to store variables, status information, and buffers). The ESP32 module also has built-in WiFi and Bluetooth LE connectivity. In addition, the LoPy4 has 4 MB of PSRAM (pseudo static RAM) that is used as a memory extension for the ESP32. The operating voltage of the board is 3.3V (this is important when attaching sensors and other peripherals; they also must operate on 3.3V). The board offers 18 general purpose input/output pins (18 GPIOs), from which up to 12 can be used as analog input pins (with 12bit analog digital converters (ADC)) and two as analog output pins (8bit digital analog converter (DAC)). Most GPIO pins can be configured for specific hardware protocols. In total 3 serial ports (programmable Universal Asynchronous Receiver and Transmitter, UART), 2 I2C ports, 3 SPI ports, 1 CAN bus, 1 PWM channel, and an I2S port can be utilized. The board has a built-in RGB LED that can be programmed by the user. The LoPy4 is available from the manufacturer for around 35 €.

The LoPy4 needs to be operated on 3.5V – 5.5V put to the VIN pin. The onboard regulator brings it down to 3.3V. The 3.3V pin can only be used as an output. Do not feed 3.3V into this pin as this could damage the regulator. The board can be programmed over the serial interface, or via WiFi using a telnet or FTP connection. By default, the LoPy4 acts as a WiFi access point (SSID: lopy4–wlan–XXXX, Password: www.pycom.io) and a user can connect to the module after joining the WiFi network in order to upload user programs and data files.

A WiFi and Bluetooth antenna is mounted on the LoPy board, but also an external antenna can be connected via an SMA-type connector. The LoRa or SIGFOX antenna has to be connected via an SMA-type connector. The LoRa transmitter and receiver is encapsulated within a LoRa module. It uses the LoRa chip SX1276 from the company Semtech and can be configured to work either in the 433 MHz, 868 MHz, or 915 MHz frequency band. The LoRa module is connected via SPI interface to the microcontroller and all of the required connections of the LoRa transceiver pins with the microcontroller are already built-in on the LoPy4 board. Since the module only implements the LoRa physical layer, the LoRaWAN protocol stack is implemented in software on the microcontroller. The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C.

_images/Pycom_LoPy4_ExpansionBoard3.1_DHT22.jpg

The Pycom LoPy4 with the Expansion Board 3 (inside the blue case) and an externally attached DHT22 temperature and humidity sensor.

Expansion board 3.0

The LoPy4 can be attached to the Pycom Expansion Board 3.0. The board offers a USB port that is connected internally via a USB/Serial converter to the default serial port (UART) of the LoPy4. This allows to easily upload programs and data files to the LoPy4 from a developer computer over USB connection. The expansion board also comes with a connector for a 3.7V lithium polymer (LiPo) battery with an additional battery charger circuit. When the expansion board is connected via USB to a developer computer or to an USB charger, an attached battery will be automatically charged. The battery voltage can be monitored via the LoPy4 analog to digital converter (ADC). The board also comes with a user LED, a user switch, and a MicroSD card slot to read and write files from the LoPy4, for example, to log recorded data. The LoPy Epansion Board 3.0 is available from the manufacturer for 16 €.

Sensor

We have attached a Grove DHT22 sensor module to the expansion board, which measures air temperature and humidity. The minimal time interval between two measurements is 2 seconds. All data transfers between the DHT22 and the microcontroller use a single digital line. The sensor data pin is attached to a GPIO pin (here: GPIO22) of the expansion board. The 5V pin of the Grove module is connected to 3V3 of the expansion board, and the GND of the Grove module to GND of the expansion board. The DHT22 datasheet can be accessed here. The sensor is available in German shops for around 4 € to 10 €.

Software

The sensor node has been programmed in the MicroPython language. We use Microsoft’s Visual Studio Code platform with the Pymakr plugin to edit and upload the program. The Pymakr plugin is developed by Pycom and can be used with either Visual Studio Code or the Atom Text Editor. Both IDEs can be downloaded free of charge; Atom is also Open Source software. Note that MicroPython programs do not need to be compiled (like Java or C/C++ programs). The source code is interpreted by the Microcontroller instead.

The source code consists of the following two files. main.py and boot.py. They must be copied into the base folder on the LoPy4. We use a library for the DHT22 written by Erik de Lange. It can be downloaded from the following link but is also provided here dht22.py. The library has to be copied into the subdirectory “lib” on the LoPy4.

After the program has successfully established a connection to The Things Network it reports the air temperature, humidity, and the voltage of an attached LiPo battery every 5 minutes. Since we are running the device on an USB charger, the program does not check the battery level and the transferred value is always set to 0 V. All three values are being encoded in two byte integer values each (in most significant byte order) and then sent as a 6 bytes data packet to the respective TTN application using LoRaWAN port 7. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement ; this must be considered in the Payload Decoding Function used in The Things Network (see below).

The program as given above does not make use of the deep sleep mode or any other power saving method. In between two sensor readings the microcontroller is busy ‘doing nothing’ until the waiting time before the next measurement is over. When the LoPy4 should be operated on battery, the power saving modes of the LoPy4 should be investigated. Note that this will require to restructure the main.py program significantly.

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The program given above connects the sensor node with The Things Network (TTN) using the Over-the-Air-Activation (OTAA) mode. In this mode, we use the three keys AppEUI, DevEUI, AppKey. The DevEUI is pre-programmed into the LoPy4. In order to register the device with TTN, you first need to fetch the DevEUI from the LoPy4 board. This is explained in the LoPy4 documentation. Each sensor node must be manually registered in the TTN console before it can be started. This assumes that you already have a TTN user account and have created an application in the user account (both need to be created otherwise). In the TTN console create a new device using the DevEUI value that was previously determined. After the registration of the device the two generated keys (AppEUI, AppKey) can be copied from the TTN console and must be pasted into the the proper places in the source code of the program above. Please make sure that you choose for both keys the correct byte ordering (all are in MSB, i.e. in the same ordering as given in the TTN console). A detailed explanation of these steps is given here. Then the program can be uploaded to the LoPy4 microcontroller. Note that the two constants (AppEUI, AppKey) must be changed in the source code for every new sensor node (the DevEUI is different for each node anyway).

Using the OTAA mode has the advantage over the ABP (activation by personalization) mode that during connection the session keys are newly created which improves security. Another advantage is that the packet counter is automatically reset to 0 both in the node and in the TTN application.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (Payload Decoder Function). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak ). Such a forwarding can be configured in the TTN console in the “Integrations” tab. TTN payload decoder given here checks if a packet was received on LoRaWAN port 7 and then assumes that it consists of the 6 bytes encoded as described above. It creates the three Javascript objects ‘temperature’, ‘humidity’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==7) {…} statement by adding else if (port==8) {…} else if (port==9) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this page. Here is a screenshot showing the webclient:

_images/SOS-Webclient-lopy4-with-DHT22.png

Web client for data visualization

Code files

boot.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from network import LoRa
import socket
import time
import ubinascii
import pycom
from machine import Pin
import dht22

# Initialise LoRa in LORAWAN mode.
# Please pick the region that matches where you are using the device:
# Asia = LoRa.AS923
# Australia = LoRa.AU915
# Europe = LoRa.EU868
# United States = LoRa.US915
lora = LoRa(mode=LoRa.LORAWAN, region=LoRa.EU868)

# Create the OTAA authentication parameters:
# directly copy the values from the Things Network Console and replace the
# xxxx's and yyyy's by these values (do not prepend anything like '0x' or similar)
app_eui = ubinascii.unhexlify('xxxxxxxxxxxxxxxx')
app_key = ubinascii.unhexlify('yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy')

print("Initializing DHT22 Sensor... ",end='')
dht = dht22.device(Pin.exp_board.G22)
print("ready!\n")

pycom.heartbeat(False)
pycom.rgbled(0xFF0000)

# join a network using OTAA (Over the Air Activation)
lora.join(activation=LoRa.OTAA, auth=(app_eui, app_key), timeout=0)

# wait until the module has joined the network
while not lora.has_joined():
    time.sleep(2.5)
    print('Not yet joined...')

pycom.rgbled(0x00FF00)
time.sleep(1)

# create a LoRa socket
s = socket.socket(socket.AF_LORA, socket.SOCK_RAW)

# set the LoRaWAN data rate
s.setsockopt(socket.SOL_LORA, socket.SO_DR, 5)

# set the LoRaWAN port number for transmitted packets
s.bind(7)

pycom.heartbeat(True)
main.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
from network import LoRa
import socket
import time
import ubinascii
import pycom
from machine import Pin
import dht22

while (True):
    pycom.heartbeat(False)
    pycom.rgbled(0x800000)

    # start a new measurement (taking 2 seconds) to ensure that the next 
    # retrieval of readings has current values
    dht.trigger()
    time.sleep(0.2)

    # now start a 2nd measurements which - according to the DHT22 datasheet -
    # delivers the measured values from the previous reading
    hasreading=False
    numtrials=0

    while(not hasreading):
        hasreading=dht.trigger()
        numtrials=numtrials+1
        if hasreading:
            print("RH = {}%  T = {}C".format(dht.humidity, dht.temperature))
        else:
            print(dht.status)

    hum_msb=int(dht.humidity*100/256)
    hum_lsb=int(dht.humidity*100%256)

    tmp_int=int(dht.temperature*100)
    # if temperature value is negative, then represent it by its 2's complement (16 bit)
    if (tmp_int<0):
        tmp_int=65536+tmp_int

    tmp_msb=int(tmp_int/256)
    tmp_lsb=int(tmp_int%256)
    print("RH = {} {}  T = {} {}".format(hum_msb, hum_lsb, tmp_msb, tmp_lsb))
    pycom.rgbled(0x000040)

    # make the socket blocking
    # (waits for the data to be sent and for the 2 receive windows to expire)
    s.setblocking(True)

    # send some data
    s.send(bytes([tmp_msb, tmp_lsb, hum_msb, hum_lsb, 0, 0]))

    # make the socket non-blocking
    # (because if there's no data received it will block forever...)
    s.setblocking(False)

    # get any data received (if any...)
    data = s.recv(64)
    print(data)

    pycom.heartbeat(True)
    # wait for such a time period that we have one measurement every 300 seconds
    time.sleep(300-numtrials*4-3)
dht22.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73

# dht22.py
#
# Class file for accessing the DHT22 temperature and humidity sensor using a WiPy 2.0.
#
# 2018 - Erik de Lange

from machine import Pin

import pycom
import time

class device:

    def __init__(self, pin):
        self.temperature = None
        self.humidity = None
        self.status = "NoConversionStartedError"
        self.pin = Pin(pin, mode=Pin.OPEN_DRAIN)

    def trigger(self):
        self.pin(1)
        time.sleep(2)  # enforce two second read interval

        self.pin(0)  # send start signal (1ms low).
        time.sleep_ms(1)

        pulses = pycom.pulses_get(self.pin, 100)  # capture communication

        self.pin.init(Pin.OPEN_DRAIN)

        if len(pulses) != 82:  # 40 data bit plus one acknowledge expected
            self.status = "ReadError - received {} only pulses".format(len(pulses))
            return False

        bits = []

        for level, duration in pulses[1:]:
            if level == 1:
                bits.append(0 if duration < 50 else 1)  # convert to 0 or 1

        data = []

        for n in range(5):
            byte = 0
            for i in range(8):  # shift 8 bits into a byte
                byte <<= 1
                byte += bits[n * 8 + i]
            data.append(byte)

        int_rh, dec_rh, int_t, dec_t, csum = data

        if ((int_rh + dec_rh + int_t + dec_t) & 0xFF) != csum:
            self.status = "Checksum Error"
            return False

        self.humidity = ((int_rh * 256) + dec_rh) / 10
        self.temperature = (((int_t & 0x7F) * 256) + dec_t) / 10
        if (int_t & 0x80) > 0:
            self.temperature *= -1

        self.status = "OK"
        return True


if __name__ == "__main__":
    dht = device(Pin.exp_board.G22)

    for _ in range(5):
        if dht.trigger() == True:
            print("RH = {}%  T = {}C".format(dht.humidity, dht.temperature))
        else:
            print(dht.status)
TTN payload decoder
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function Decoder (bytes, port) {
  var result = {};
  var transformers = {};
   
  if (port==7) {
     transformers = {
      'temperature': function transform (bytes) {
          value=bytes[0]*256 + bytes[1];
          if (value>=32768) value=value-65536;
          return value/100.0;
        },
      'humidity': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
      'vbattery': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
    }
   
    result['temperature'] = {
      value: transformers['temperature'](bytes.slice(0, 2)),
      uom: 'Celsius',
    }
   
    result['humidity'] = {
      value: transformers['humidity'](bytes.slice(2, 4)),
      uom: 'Percent',
    }
   
    result['vbattery'] = {
      value: transformers['vbattery'](bytes.slice(4, 6)),
      uom: 'Volt',
    }
  }
   
  return result;
}

Seeeduino LoRaWAN

Hardware

Microcontroller

The Seeeduino LoRaWAN module is operated by the 32bit microcontroller ATSAMD21G18 (ARM® Cortex®-M0+) running at 48MHz. It has 256 KB flash memory (to store the program code) and 32 KB of RAM (to store variables, status information, and buffers). The operating voltage of the board is 3.3V (this is important when attaching sensors and other peripherals; they also must operate on 3.3V). The board offers 20 general purpose digital input/output pins (20 GPIOs), 6 analog input pins (with 12bit analog digital converters (ADC)), 1 analog output pin (with 10bit digital analog converter (DAC)), 2 serial ports (2 programmable Universal Asynchronous Receiver and Transmitters, UARTs). The board comes with an embedded lithium battery management chip and status indicator led, which allows to directly connect a 3.7V LiPo rechargeable battery that will be automatically recharged when the board is powered over its USB connector. The battery voltage level can be queried from analog input A4, the charging status (charging, full) from analog input A5. The Seeeduino LoRaWAN (without GPS module) is available in German shops for around 37 €.

The LoRa transmitter and receiver is encapsulated within an RHF76-052AM module from the Chinese company RisingHF. The RF module contains its own microcontroller, which implements the LoRaWAN protocol. The module is connected via the serial interface to the ATSAMD21G18 microcontroller and can be controlled by sending so-called ‘AT’ commands. The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C. The explanation of all supported commands as well as a number of examples on how to use the Seeeduino LoRaWAN are given on the Seeeduino LoRaWAN Wiki.

The board has 4 on-board Grove connectors. ‘Grove’ is a framework developed by the company Seeed Studio standardizing the connectors, operating voltages, and pin configurations for attaching peripherals like sensors, actuators, and displays to microcontrollers. Note that the Grove modules need to be able to operate (also) on 3.3V (instead of only with 5V), because the Seeeduino LoRaWAN board only provides 3.3V to the Grove connectors. Important hint: if you want to use the Grove ports, make sure to include the command “digitalWrite(38, HIGH)” in the setup() routine of your program. A low level on that pin deactivates the power supply of the four Grove ports.

The board has also the typical Arduino UNO connectors allowing to attach so-called Arduino shields (however, please note that the shields must be working with 3.3V; the normal operating voltage for the Arduino UNO microcontroller and its shields is 5V).

_images/Seeeduino_LoRaWAN_with_BME280.jpg

The Seeeduino LoRaWAN GPS microcontroller with a 6600 mAh lithium polymer (LiPo) battery (bottom), and an attached BME280 temperature / humidity / barometer sensor module.

Sensor

We attached a Bosch BME280 sensor module to the extension connectors of the microcontroller board using 5 wires. The employed BME280 sensor board is a cheap no-name product. VCC and GND are connected to 3.3V and GND of the microcontroller board respectively. SCL and SDA from the sensor board are connected to SCL and SDA of the microcontroller board. SDO from the sensor board is also connected to GND of the microcontroller; it selects 0x76 as the I2C device address (a high level, i.e. 3.3V, would set the device address to 0x77 - this is relevant, if two sensor modules should be operated on the same I2C bus). Note that there is also a Seeed Grove BME280 module available which alternatively can be used and connected to the first I2C Grove connector of the Seeeduino LoRaWAN board. The BME280 measures temperature in the range -40 - 85 ℃, with ±1.0°C accuracy; 0% - 100% relative humidity with ±3% accuracy; and atmospheric pressure in the range 300 - 1100 hPa (1 hPa= one hundred Pa) with ±1.0 hPa accuracy. It offers the two interface standards I2C and SPI (we are using I2C here and the default I2C address 0x76). The atmospheric pressure changes with altitude, hence, the BME280 can also be used to measure the approximate altitude of a place.

Software

The sensor node has been programmed using the Arduino IDE. Please note, that in the Arduino framework a program is called a ‘Sketch’.

In order to support the “Seeeduino LoRaWAN” board with the Arduino IDE, make sure to have installed the package “Seeed SAMD boards by Seeed Studio” in version 1.3.0 using the board manager in the Arduino IDE. This is also explained on a dedicated webpage from Seeed Studio. The sketch requires the software libraries “RTCZero”, “Arduino_BME280”, “Adafruit_Sensor”, “Wire”, and “LoRaWAN”. The first three have to be installed using the library manager of the Arduino IDE, the fourth library is already installed with the Arduino IDE and the latter library comes with the “Seeeduino LoRaWAN” board installation.

After the sketch has successfully established a connection to The Things Network it reports the air temperature, relative humidity, air pressure, altitude, and the voltage of a (possibly) attached LiPo battery every 5 minutes. All five values are being encoded in two byte integer values each and then sent as a 10 bytes data packet to the respective TTN application using LoRaWAN port 33. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage, pressure, altitude, and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement; this must be considered in the Payload Decoding Function used in The Things Network (see below).

In between two sensor readings the microcontroller, the LoRaWAN module, and the sensor module are going into deep sleep mode to save battery power. During LoRaWAN data transmission the device draws up to 65mA current. When in sleep mode the entire node only draws around 0.06 mA power. Hence, with a 6600 mAh 3.7V LiPo battery and the current version of the sketch the system should be able to run for many years before recharging (not taking into account the self-discharging rate of the battery).

The source code is provided in the following section Arduino Sketch for Seeeduino LoRaWAN sensor node

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The sketch given above connects the sensor node with The Things Network (TTN) using the Over-the-Air-Activation (OTAA) mode. In this mode, we use the three keys AppEUI, DevEUI, AppKey. The DevEUI should normally be delivered with the sensor node by the manufacturer. However, it seems that there is no explicit DevEUI provided with the Seeeduino LoRaWAN module. Therefore, it has to be generated automatically together with the other two keys using the TTN console. Each sensor node must be manually registered in the TTN console before it can be started. This assumes that you already have a TTN user account (which needs to be created otherwise). In the TTN console create a new device with also the DevEUI being automatically generated. After the registration of the device the respective keys (AppEUI, DevEUI, AppKey) can be copied from the TTN console and must be pasted into the the proper places in the source code of the sketch above. Please make sure that you choose for each of the three keys are in the correct byte ordering (all are in MSB, i.e. in the same ordering as given in the TTN console). A detailed explanation of these steps is given on this page. Then the sketch can be compiled and uploaded to the Seeeduino LoRaWAN microcontroller. Note that the three constants (AppEUI, DevEUI, AppKey) must be changed in the source code for every new sensor node.

Using the OTAA mode has the advantage over the ABP (activation by personalization) mode that during connection the session keys are newly created which improves security. Another advantage is that the packet counter is automatically reset to 0 both in the node and in the TTN application.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (Payload Decoder Function). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak ).

Such a forwarding can be configured in the TTN console in the “Integrations” tab. TTN payload decoder for Seeeduino LoRaWAN sensor node given here checks if a packet was received on LoRaWAN port 33 and then assumes that it consists of the 10 bytes encoded as described above. It creates the five Javascript objects ‘temperature’, ‘humidity’, ‘pressure’, ‘altitude’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==33) {…} statement by adding else if (port==7) {…} else if (port==8) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this webpage. Here is a screenshot showing the webclient:

_images/SOS-Webclient-seeeduino-with-bme280.png

Web client for data visualization

Code files

Arduino Sketch for Seeeduino LoRaWAN sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/*******************************************************************************
 * Arduino Sketch for a LoRaWAN sensor node that is registered with
 * 'The Things Network' (TTN) www.thethingsnetwork.org
 *
 * Filename: Seeeduino_LoRaWAN_GPS_BME280_OTAA_Sleep_Adafruit_V2.ino
 *
 * Author:  Thomas H. Kolbe, thomas.kolbe@tum.de
 * Version: 1.0.1
 * Last update: 2019-04-17
 *
 * This sketch works with a Seeeduino LoRaWAN microcontroller board (with or
 * without embedded GPS module). See http://wiki.seeedstudio.com/Seeeduino_LoRAWAN/
 * It requires a Seeed Grove BME280 air temperature, relative humidity,
 * and air pressure sensor module attached to the I2C Grove connector of
 * the microcontroller board. The current configuration assumes that
 * the BME280 is configured to I2C device address 0x76 (default).
 * The sketch makes a connection to The Things Network (TTN) using
 * LoRaWAN in OTAA mode. It then sends a data packet of 10 bytes to
 * LoRaWAN port 33 around every 5 minutes. The packet contains the
 * following 5 integer values (16 bit, most significant byte (MSB) first):
 *   1. temperature in Celsius (signed, multiplied by 100)
 *   2. relative humidity in percent (unsigned, multiplied by 100)
 *   3. air pressure in Pascal (unsigned, divided by 10)
 *   4. current altitude in Meters (unsigned, multiplied by 10)
 *   5. battery voltage in millivolt (unsigned)
 * These values have to be decoded by the LoRaWAN network controller
 * using a proper "payload decoder function" written in Javascript.
 *
 * Note that when the board is powered over the USB connector and
 * no battery is connected, the measured battery voltage is incorrect.
 *
 * If the board shall be running on a lithium polymer (LiPo) battery,
 * it is recommended to remove the green power LED from the board or
 * to cut the connection between the LED and the resistor lying above
 * of it as the LED constantly draws around 8mW of power. In order to
 * save energy the sketch puts the GPS module on the board to standby
 * mode right from the beginning. After each measurement and data transfer
 * the LoRaWAN module and the sensor is put to standby mode, too, and the
 * microcontroller goes into deep sleep mode. All components require
 * a total current of around 0.34mA during sleep mode and up to 65mA
 * during LoRa transmission for the board version with GPS. The board
 * version without GPS only requires 0.06mA during sleep mode. Since the
 * entire system is mostly sleeping, the GPS board should be running
 * around 2 years on a 6600mAh LiPo battery before recharging
 * (6600mAh / 0.34mA / 24 = 808 days). The non GPS board version should
 * even run for more than 10 years...
 *
 * This code is based on example code given on the Seeeduino LoRaWAN
 * wiki page. It utilizes the Open Source libraries "Adafruit_BME280"
 * and "Adafruit_Sensor" provided by the company Adafruit and the
 * library "LoRaWan.h" provided by Seeed Studio.
 *******************************************************************************/
 
#include <RTCZero.h>
#include <LoRaWan.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
 
// Keep the following line, if the board is a Seeeduino LoRaWAN with GPS,
// otherwise comment the line out
 
// #define HAS_GPS 1
 
#define BME280_ADDRESS       (0x76)   // I2C device address of the BME280 sensor
 
// The barometer of the BME280 can also be used to estimate the current
// altitude of the device, if the air pressure at sea level (NN) is known.
// The following value has to be set to the current air pressure at NN (in hPa)
// in order to give reasonable altitude estimations. Note that this value is
// slowly changing over time. For Munich the current value can be obtained
// from https://www.meteo.physik.uni-muenchen.de/mesomikro/stadt/messung.php
 
#define SEALEVELPRESSURE_HPA (1017.8) 
 
Adafruit_BME280 bme280;
 
RTCZero rtc;
 
unsigned char data[10];                 // buffer for the LoRaWAN data packet to be transferred
char buffer[256];                       // buffer for text messages received from the LoRaWAN module for display
 
 
void setup(void)
{
    digitalWrite(38, HIGH);             // Provide power to the 4 Grove connectors of the board
     
    for(int i = 0; i < 26; i ++)        // Set all pins to HIGH to save power (reduces the
    {                                   // current drawn during deep sleep by around 0.7mA).
        if (i!=13) {                    // Don't switch on the onboard user LED (pin 13).
          pinMode(i, OUTPUT);
          digitalWrite(i, HIGH);
        }
    }   
     
    delay(5000);                        // Wait 5 secs after reset/booting to give time for potential upload
                                        // of a new sketch (sketches cannot be uploaded when in sleep mode)
    SerialUSB.begin(115200);            // Initialize USB/serial connection
    delay(500);
    // while(!SerialUSB);
    SerialUSB.println("Seeeduino LoRaWAN board started!");
 
    if(!bme280.begin(BME280_ADDRESS)) { // Initialize the BME280 sensor module
      SerialUSB.println("BME280 device error!");
    }
 
    // Set the BME280 to a very low power operation mode (c.f. chapter 3.5
    // "Recommended modes of operation" in the BME280 datasheet. See
    // https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf );
    // proper values can only be queried every 60s
    bme280.setSampling(Adafruit_BME280::MODE_FORCED,
                    Adafruit_BME280::SAMPLING_X16,  // temperature
                    Adafruit_BME280::SAMPLING_X16,  // pressure
                    Adafruit_BME280::SAMPLING_X16,  // humidity
                    Adafruit_BME280::FILTER_OFF   );
 
    // nrgSave.begin(WAKE_RTC_ALARM);
    // rtc.begin(TIME_H24);
 
#ifdef HAS_GPS
    Serial.begin(9600);                 // Initialize serial connection to the GPS module
    delay(500);
    Serial.write("$PMTK161,0*28\r\n");  // Switch GPS module to standby mode as we don't use it in this sketch
#endif
     
    lora.init();                        // Initialize the LoRaWAN module
     
    memset(buffer, 0, 256);             // clear text buffer
    lora.getVersion(buffer, 256, 1);   
    memset(buffer, 0, 256);             // We call getVersion() two times, because after a reset the LoRaWAN module can be
    lora.getVersion(buffer, 256, 1);    // in sleep mode and then the first call only wakes it up and will not be performed.
    SerialUSB.print(buffer);
     
    memset(buffer, 0, 256);
    lora.getId(buffer, 256, 1);
    SerialUSB.print(buffer);
 
    // The following three constants (AppEUI, DevEUI, AppKey) must be changed
    // for every new sensor node. We are using the LoRaWAN OTAA mode (over the
    // air activation). Each sensor node must be manually registered in the
    // TTN console at https://console.thethingsnetwork.org before it can be
    // started. In the TTN console create a new device with the DevEUI also
    // being automatically generated. After the registration of the device the
    // three values can be copied from the TTN console. A detailed explanation
    // of these steps is given in
    // https://learn.adafruit.com/the-things-network-for-feather?view=all
 
    // The EUIs and the AppKey must be given in big-endian format, i.e. the
    // most-significant-byte comes first (as displayed in the TTN console).
    // For TTN issued AppEUIs the first bytes should be 0x70, 0xB3, 0xD5.
 
    // void setId(char *DevAddr, char *DevEUI, char *AppEUI);
    lora.setId(NULL, "xxxxxxxxxxxxxxxx", "yyyyyyyyyyyyyyyy");   
 
    // setKey(char *NwkSKey, char *AppSKey, char *AppKey);
    lora.setKey(NULL, NULL, "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
     
    lora.setDeciveMode(LWOTAA);           // select OTAA join mode (note that setDeciveMode is not a typo; it is misspelled in the library)
    // lora.setDataRate(DR5, EU868);         // SF7, 125 kbps (highest data rate)
    lora.setDataRate(DR3, EU868);         // SF9, 125 kbps (medium data rate and range)
    // lora.setDataRate(DR0, EU868);         // SF12, 125 kbps (lowest data rate, highest max. distance)
 
    // lora.setAdaptiveDataRate(false);   
    lora.setAdaptiveDataRate(true);       // automatically adapt the data rate
     
    lora.setChannel(0, 868.1);
    lora.setChannel(1, 868.3);
    lora.setChannel(2, 868.5);
    lora.setChannel(3, 867.1);
    lora.setChannel(4, 867.3);
    lora.setChannel(5, 867.5);
    lora.setChannel(6, 867.7);
    lora.setChannel(7, 867.9);
 
    // The following two commands can be left commented out;
    // TTN works with the default values. (It also works when
    // uncommenting the commands, though.)
    // lora.setReceiceWindowFirst(0, 868.1);
    // lora.setReceiceWindowSecond(869.525, DR0);
    
    lora.setDutyCycle(false);             // for debugging purposes only - should normally be activated
    lora.setJoinDutyCycle(false);         // for debugging purposes only - should normally be activated
     
    lora.setPower(14);                    // LoRa transceiver power (14 is the maximum for the 868 MHz band)
     
    // while(!lora.setOTAAJoin(JOIN));
    while(!lora.setOTAAJoin(JOIN,20));    // wait until the node has successfully joined TTN
 
    lora.setPort(33);                     // all data packets are sent to LoRaWAN port 33
}
 
void loop(void)
{  
    bool result = false;   
    float temperature, altitude, pressure, humidity;
    int16_t int16_temperature, int16_humidity, int16_pressure, int16_altitude, int16_vbat;
 
    bme280.takeForcedMeasurement();       // wake the sensor up for the next readings
 
    //get and print temperatures
    SerialUSB.print("Temp: ");
    SerialUSB.print(temperature = bme280.readTemperature());
    SerialUSB.print("C  ");
   
    //get and print atmospheric pressure data
    SerialUSB.print("Pressure: ");
    SerialUSB.print(pressure = bme280.readPressure());
    SerialUSB.print("Pa  ");
 
    //get and print altitude data
    SerialUSB.print("Altitude: ");
    SerialUSB.print(altitude = bme280.readAltitude(SEALEVELPRESSURE_HPA));
    SerialUSB.print("m  ");
 
    //get and print humidity data
    SerialUSB.print("Humidity: ");
    SerialUSB.print(humidity = bme280.readHumidity());
    SerialUSB.print("%  ");
     
    //get and print battery voltage
    SerialUSB.print("VBat: ");
    SerialUSB.print(int16_vbat=lora.getBatteryVoltage());
    SerialUSB.println("mV");
 
    int16_temperature = temperature*100.0;
    int16_humidity = humidity*100.0;
    int16_pressure = pressure/10.0;
    int16_altitude = altitude*10.0;
 
    data[0] = (byte) (int16_temperature >> 8);
    data[1] = (byte) (int16_temperature & 0x00FF);
    data[2] = (byte) (int16_humidity >> 8);
    data[3] = (byte) (int16_humidity & 0x00FF);
    data[4] = (byte) (int16_pressure >> 8);
    data[5] = (byte) (int16_pressure & 0x00FF);
    data[6] = (byte) (int16_altitude >> 8);
    data[7] = (byte) (int16_altitude & 0x00FF);
    data[8] = (byte) (int16_vbat >> 8);
    data[9] = (byte) (int16_vbat & 0x00FF);
     
    result = lora.transferPacket(data, 10, 5);   // send the data packet (10 bytes) with a default timeout of 5 secs
     
    if(result)
    {
        short length;
        short rssi;
         
        memset(buffer, 0, 256);
        length = lora.receivePacket(buffer, 256, &rssi);
         
        if(length)
        {
            SerialUSB.print("Length is: ");
            SerialUSB.println(length);
            SerialUSB.print("RSSI is: ");
            SerialUSB.println(rssi);
            SerialUSB.print("Data is: ");
            for(unsigned char i = 0; i < length; i ++)
            {
                SerialUSB.print("0x");
                SerialUSB.print(buffer[i], HEX);
                SerialUSB.print(" ");
            }
            SerialUSB.println();
        }
    }
     
    lora.setDeviceLowPower();     // bring the LoRaWAN module to sleep mode
    doSleep((5*60-8)*1000);       // deep sleep for 292 secs (+ 3 secs transmission time + 5 secs timeout = 300 secs period)
    lora.setPort(33);             // send some command to wake up the LoRaWAN module again
}
 
// The following function implements deep sleep waiting. When being called the
// CPU goes into deep sleep mode (for power saving). It is woken up again by
// the CPU-internal real time clock (RTC) after the configured time.
//
// A similar function would also be available in the standard "ArduinoLowPower" library.
// However, in order to be able to use that library with the Seeeduino LoRaWAN board,
// four files in the package "Seeed SAMD boards by Seeed Studio Version 1.3.0" that is
// installed using the Arduino IDE board manager need to be patched. The reason is that
// Seeed Studio have not updated their files to a recent Arduino SAMD version yet
// and the official "ArduinoLowPower" library provided by the Arduino foundation is
// referring to some missing functions. For further information see here:
// https://forum.arduino.cc/index.php?topic=603900.0 and here:
// https://github.com/arduino/ArduinoCore-samd/commit/b9ac48c782ca4b82ffd7e65bf2c956152386d82b
 
void doSleep(uint32_t millis) {
    if (!rtc.isConfigured()) {    // if called for the first time,
        rtc.begin(false);         // then initialize the real time clock (RTC)
    }
 
    uint32_t now = rtc.getEpoch();
    rtc.setAlarmEpoch(now + millis/1000);
    rtc.enableAlarm(rtc.MATCH_HHMMSS);
 
    rtc.standbyMode();            // bring CPU into deep sleep mode (until woken up by the RTC)
}
TTN payload decoder for Seeeduino LoRaWAN sensor node
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function Decoder (bytes, port) {
  var result = {};
  var transformers = {};
   
  if (port==33) {
    transformers = {
      'temperature': function transform (bytes) {
          value=bytes[0]*256 + bytes[1];
          if (value>=32768) value=value-65536;
          return value/100.0;
        },
      'humidity': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
      'pressure': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/10.0;
        },
      'altitude': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/10.0;
        },
      'vbattery': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/1000.0;
        }
    }
     
    result['temperature'] = {
      value: transformers['temperature'](bytes.slice(0, 2)),
      uom: 'Celsius',
    }
     
     result['humidity'] = {
      value: transformers['humidity'](bytes.slice(2, 4)),
      uom: 'Percent',
    }
   
    result['pressure'] = {
      value: transformers['pressure'](bytes.slice(4, 6)),
      uom: 'hPa',
    }
     
    result['altitude'] = {
      value: transformers['altitude'](bytes.slice(6, 8)),
      uom: 'Meter',
    } 
 
    result['vbattery'] = {
      value: transformers['vbattery'](bytes.slice(8, 10)),
      uom: 'Volt',
    }
  }
  
  return result;
}

Seeeduino LoRaWAN with GPS

Hardware

Micro-controller

The Seeeduino LoRaWAN module is operated by the 32bit microcontroller ATSAMD21G18 (ARM® Cortex®-M0+) running at 48MHz. It has 256 KB flash memory (to store the program code) and 32 KB of RAM (to store variables, status information, and buffers). The operating voltage of the board is 3.3V (this is important when attaching sensors and other peripherals; they also must operate on 3.3V). The board offers 20 general purpose digital input/output pins (20 GPIOs), 6 analog input pins (with 12bit analog digital converters (ADC)), 1 analog output pin (with 10bit digital analog converter (DAC)), 2 serial ports (2 programmable Universal Asynchronous Receiver and Transmitters, UARTs). The board comes with an embedded lithium battery management chip and status indicator led, which allows to directly connect a 3.7V LiPo rechargeable battery that will be automatically recharged when the board is powered over its USB connector. The battery voltage level can be queried from analog input A4, the charging status (charging, full) from analog input A5. There is an on-board L70 GPS receiver module from the company Quectel Wireless Solutions and a small chip antenna. The Seeeduino LoRaWAN GPS module is available in German shops from around 37 € to 45 €.

The LoRa transmitter and receiver is encapsulated within an RHF76-052AM module from the Chinese company RisingHF. The RF module contains its own microcontroller, which implements the LoRaWAN protocol. The module is connected via the serial interface to the ATSAMD21G18 microcontroller and can be controlled by sending so-called ‘AT’ commands. The implemented LoRaWAN functionality is compatible with LoRaWAN Class A/C. The explanation of all supported commands as well as a number of examples on how to use the Seeeduino LoRaWAN are given on the Seeeduino LoRaWAN Wiki.

The board has 4 on-board Grove connectors. ‘Grove’ is a framework developed by the company Seeed Studio standardizing the connectors, operating voltages, and pin configurations for attaching peripherals like sensors, actuators, and displays to microcontrollers. The board has also the typical Arduino UNO connectors allowing to attach so-called Arduino shields (however, please note that the shields must be working with 3.3V; the normal operating voltage for the Arduino UNO microcontroller and its shields is 5V).

_images/Seeeduino_LoRaWAN_with_GPS_and_BME280.JPG

The Seeeduino LoRaWAN GPS microcontroller with a 6600 mAh lithium polymer (LiPo) battery (bottom), and an attached BME280 temperature / humidity / barometer sensor module.

Sensor

We attached a Seeed BME280 Grove module with a Bosch BME280 sensor to the first I2C Grove connector of the Seeeduino LoRaWAN board. The BME280 measures temperature in the range -40 - 85 ℃, with ±1.0°C accuracy; 0%-100% relative humidity with ±3% accuracy; and atmospheric pressure in the range 300 - 1100 hPa (1 hPa= one hundred Pa) with ±1.0 hPa accuracy. It offers the two interface standards I2C and SPI (we are using I2C here and the default I2C address 0x76). The atmospheric pressure changes with altitude, hence, the BME280 can also be used to measure the approximate altitude of a place.

Software

The sensor node has been programmed using the Arduino IDE. Please note, that in the Arduino framework a program is called a ‘Sketch’.

In order to support the “Seeeduino LoRaWAN” board with the Arduino IDE, make sure to have installed the package “Seeed SAMD boards by Seeed Studio” in version 1.3.0 using the board manager in the Arduino IDE. This is also explained on a dedicated webpage from Seeed Studio. The sketch requires the software libraries “RTCZero”, “Arduino_BME280”, “Adafruit_Sensor”, “Wire”, and “LoRaWAN”. The first three have to be installed using the library manager of the Arduino IDE, the fourth library is already installed with the Arduino IDE and the latter library comes with the “Seeeduino LoRaWAN” board installation.

After the sketch has successfully established a connection to The Things Network it measures the air temperature, humidity, and pressure every 30 seconds. From the measured values also the altitude of the device (in meters above ground) is estimated. All four values are being encoded in two byte integer values each and then sent as an 8 bytes data packet to the respective TTN application using LoRaWAN port 8. Please note, that LoRaWAN messages can be addressed to ports 1-255 (port 0 is reserved); these ports are similar to port numbers 0-65535 when using the Internet TCP/IP protocol. Voltage, pressure, altitude, and humidity values are always greater or equal to 0, but the temperature value can also become negative. Negative values are represented as a two’s complement; this must be considered in the Payload Decoding Function used in The Things Network (see below).

In between two sensor readings the microcontroller, the LoRaWAN module, and the sensor module are going into deep sleep mode to save battery power. During LoRaWAN data transmission the device draws up to 65mA current. When in sleep mode the entire node only draws around 0.06 mA power. Hence, with a 6600 mAh 3.7V LiPo battery and the current version of the sketch the system should be able to run for many years before recharging (not taking into account the self-discharging rate of the battery).

The source code is provided in the following section Arduino Sketch for Seeeduino LoRaWAN with GPS sensor node

Services

The services used for this sensor-node are:

Registration of the sensor node with The Things Network (TTN)

The LoRaWAN protocol makes use of a number of different identifiers, addresses, keys, etc. These are required to unambiguously identify devices, applications, as well as to encrypt and decrypt messages. The names and meanings are nicely explained on a dedicated TTN web page.

The sketch given above connects the sensor node with The Things Network (TTN) using the Over-the-Air-Activation (OTAA) mode. In this mode, we use the three keys AppEUI, DevEUI, AppKey. The DevEUI should normally be delivered with the sensor node by the manufacturer. However, it seems that there is no explicit DevEUI provided with the Seeeduino LoRaWAN module. Therefore, it has to be generated automatically together with the other two keys using the TTN console. Each sensor node must be manually registered in the TTN console before it can be started. This assumes that you already have a TTN user account (which needs to be created otherwise). In the TTN console create a new device with also the DevEUI being automatically generated. After the registration of the device the respective keys (AppEUI, DevEUI, AppKey) can be copied from the TTN console and must be pasted into the the proper places in the source code of the sketch above. Please make sure that you choose for each of the three keys are in the correct byte ordering (all are in MSB, i.e. in the same ordering as given in the TTN console). A detailed explanation of these steps is given on this page. Then the sketch can be compiled and uploaded to the Seeeduino LoRaWAN microcontroller. Note that the three constants (AppEUI, DevEUI, AppKey) must be changed in the source code for every new sensor node.

Using the OTAA mode has the advantage over the ABP (activation by personalization) mode that during connection the session keys are newly created which improves security. Another advantage is that the packet counter is automatically reset to 0 both in the node and in the TTN application.

TTN Payload Decoding

Everytime a data packet is received by a TTN application a dedicated Javascript function is being called (Payload Decoder Function). This function can be used to decode the received byte string and to create proper Javascript objects or values that can directly be read by humans when looking at the incoming data packet. This is also useful to format the data in a specific way that can then be forwarded to an external application (e.g. a sensor data platform like MyDevices or Thingspeak ).

Such a forwarding can be configured in the TTN console in the “Integrations” tab. TTN payload decoder for Seeeduino LoRaWAN with GPS sensor node given here checks if a packet was received on LoRaWAN port 33 and then assumes that it consists of the 10 bytes encoded as described above. It creates the five Javascript objects ‘temperature’, ‘humidity’, ‘pressure’, ‘altitude’, and ‘vbattery’. Each object has two fields: ‘value’ holds the value and ‘uom’ gives the unit of measure. The source code can simply be copied and pasted into the ‘decoder’ tab in the TTN console after having selected the application. Choose the option ‘Custom’ in the ‘Payload Format’ field. Note that when you also want to handle other sensor nodes sending packets on different LoRaWAN ports, then the Payload Decoder Function can be extended after the end of the if (port==33) {…} statement by adding else if (port==7) {…} else if (port==8) {…} etc.

The Things Network - OGC SensorWeb Integration

The presented Payload Decoder Function works also with the TTN-OGC SWE Integration for the 52° North Sensor Observation Service (SOS). This software component can be downloaded from this repository. It connects a TTN application with a running transactional Sensor Observation Service 2.0.0 (SOS). Data packets received from TTN are imported into the SOS. The SOS persistently stores sensor data from an arbitrary number of sensor nodes and can be queried for the most recent as well as for historic sensor data readings. The 52° North SOS comes with its own REST API and a nice web client allowing to browse the stored sensor data in a convenient way.

We are running an instance of the 52° North SOS and the TTN-OGC SWE Integration. The web client for this LoRaWAN sensor node can be accessed on this webpage. Here is a screenshot showing the webclient:

_images/SOS-Webclient-seeeduino-with-bme2801.png

Web client for data visualization

Code files

Arduino Sketch for Seeeduino LoRaWAN with GPS sensor node
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
/*******************************************************************************
 * Arduino Sketch for a LoRaWAN sensor node that is registered with
 * 'The Things Network' (TTN) www.thethingsnetwork.org
 *
 * Filename: Seeeduino_LoRaWAN_GPS_BME280_OTAA_Sleep_Adafruit_V2.ino
 *
 * Author:  Thomas H. Kolbe, thomas.kolbe@tum.de
 * Version: 1.0.1
 * Last update: 2019-04-17
 *
 * This sketch works with a Seeeduino LoRaWAN microcontroller board (with or
 * without embedded GPS module). See http://wiki.seeedstudio.com/Seeeduino_LoRAWAN/
 * It requires a Seeed Grove BME280 air temperature, relative humidity,
 * and air pressure sensor module attached to the I2C Grove connector of
 * the microcontroller board. The current configuration assumes that
 * the BME280 is configured to I2C device address 0x76 (default).
 * The sketch makes a connection to The Things Network (TTN) using
 * LoRaWAN in OTAA mode. It then sends a data packet of 10 bytes to
 * LoRaWAN port 33 around every 5 minutes. The packet contains the
 * following 5 integer values (16 bit, most significant byte (MSB) first):
 *   1. temperature in Celsius (signed, multiplied by 100)
 *   2. relative humidity in percent (unsigned, multiplied by 100)
 *   3. air pressure in Pascal (unsigned, divided by 10)
 *   4. current altitude in Meters (unsigned, multiplied by 10)
 *   5. battery voltage in millivolt (unsigned)
 * These values have to be decoded by the LoRaWAN network controller
 * using a proper "payload decoder function" written in Javascript.
 *
 * Note that when the board is powered over the USB connector and
 * no battery is connected, the measured battery voltage is incorrect.
 *
 * If the board shall be running on a lithium polymer (LiPo) battery,
 * it is recommended to remove the green power LED from the board or
 * to cut the connection between the LED and the resistor lying above
 * of it as the LED constantly draws around 8mW of power. In order to
 * save energy the sketch puts the GPS module on the board to standby
 * mode right from the beginning. After each measurement and data transfer
 * the LoRaWAN module and the sensor is put to standby mode, too, and the
 * microcontroller goes into deep sleep mode. All components require
 * a total current of around 0.34mA during sleep mode and up to 65mA
 * during LoRa transmission for the board version with GPS. The board
 * version without GPS only requires 0.06mA during sleep mode. Since the
 * entire system is mostly sleeping, the GPS board should be running
 * around 2 years on a 6600mAh LiPo battery before recharging
 * (6600mAh / 0.34mA / 24 = 808 days). The non GPS board version should
 * even run for more than 10 years...
 *
 * This code is based on example code given on the Seeeduino LoRaWAN
 * wiki page. It utilizes the Open Source libraries "Adafruit_BME280"
 * and "Adafruit_Sensor" provided by the company Adafruit and the
 * library "LoRaWan.h" provided by Seeed Studio.
 *******************************************************************************/
 
#include <RTCZero.h>
#include <LoRaWan.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
 
// Keep the following line, if the board is a Seeeduino LoRaWAN with GPS,
// otherwise comment the line out
 
#define HAS_GPS 1
 
#define BME280_ADDRESS       (0x76)   // I2C device address of the BME280 sensor
 
// The barometer of the BME280 can also be used to estimate the current
// altitude of the device, if the air pressure at sea level (NN) is known.
// The following value has to be set to the current air pressure at NN (in hPa)
// in order to give reasonable altitude estimations. Note that this value is
// slowly changing over time. For Munich the current value can be obtained
// from https://www.meteo.physik.uni-muenchen.de/mesomikro/stadt/messung.php
 
#define SEALEVELPRESSURE_HPA (1017.8) 
 
Adafruit_BME280 bme280;
 
RTCZero rtc;
 
unsigned char data[10];                 // buffer for the LoRaWAN data packet to be transferred
char buffer[256];                       // buffer for text messages received from the LoRaWAN module for display
 
 
void setup(void)
{
    digitalWrite(38, HIGH);             // Provide power to the 4 Grove connectors of the board
     
    for(int i = 0; i < 26; i ++)        // Set all pins to HIGH to save power (reduces the
    {                                   // current drawn during deep sleep by around 0.7mA).
        if (i!=13) {                    // Don't switch on the onboard user LED (pin 13).
          pinMode(i, OUTPUT);
          digitalWrite(i, HIGH);
        }
    }   
     
    delay(5000);                        // Wait 5 secs after reset/booting to give time for potential upload
                                        // of a new sketch (sketches cannot be uploaded when in sleep mode)
    SerialUSB.begin(115200);            // Initialize USB/serial connection
    delay(500);
    // while(!SerialUSB);
    SerialUSB.println("Seeeduino LoRaWAN board started!");
 
    if(!bme280.begin(BME280_ADDRESS)) { // Initialize the BME280 sensor module
      SerialUSB.println("BME280 device error!");
    }
 
    // Set the BME280 to a very low power operation mode (c.f. chapter 3.5
    // "Recommended modes of operation" in the BME280 datasheet. See
    // https://cdn-shop.adafruit.com/datasheets/BST-BME280_DS001-10.pdf );
    // proper values can only be queried every 60s
    bme280.setSampling(Adafruit_BME280::MODE_FORCED,
                    Adafruit_BME280::SAMPLING_X16,  // temperature
                    Adafruit_BME280::SAMPLING_X16,  // pressure
                    Adafruit_BME280::SAMPLING_X16,  // humidity
                    Adafruit_BME280::FILTER_OFF   );
 
    // nrgSave.begin(WAKE_RTC_ALARM);
    // rtc.begin(TIME_H24);
 
#ifdef HAS_GPS
    Serial.begin(9600);                 // Initialize serial connection to the GPS module
    delay(500);
    Serial.write("$PMTK161,0*28\r\n");  // Switch GPS module to standby mode as we don't use it in this sketch
#endif
     
    lora.init();                        // Initialize the LoRaWAN module
     
    memset(buffer, 0, 256);             // clear text buffer
    lora.getVersion(buffer, 256, 1);   
    memset(buffer, 0, 256);             // We call getVersion() two times, because after a reset the LoRaWAN module can be
    lora.getVersion(buffer, 256, 1);    // in sleep mode and then the first call only wakes it up and will not be performed.
    SerialUSB.print(buffer);
     
    memset(buffer, 0, 256);
    lora.getId(buffer, 256, 1);
    SerialUSB.print(buffer);
 
    // The following three constants (AppEUI, DevEUI, AppKey) must be changed
    // for every new sensor node. We are using the LoRaWAN OTAA mode (over the
    // air activation). Each sensor node must be manually registered in the
    // TTN console at https://console.thethingsnetwork.org before it can be
    // started. In the TTN console create a new device with the DevEUI also
    // being automatically generated. After the registration of the device the
    // three values can be copied from the TTN console. A detailed explanation
    // of these steps is given in
    // https://learn.adafruit.com/the-things-network-for-feather?view=all
 
    // The EUIs and the AppKey must be given in big-endian format, i.e. the
    // most-significant-byte comes first (as displayed in the TTN console).
    // For TTN issued AppEUIs the first bytes should be 0x70, 0xB3, 0xD5.
 
    // void setId(char *DevAddr, char *DevEUI, char *AppEUI);
    lora.setId(NULL, "xxxxxxxxxxxxxxxx", "yyyyyyyyyyyyyyyy");   
 
    // setKey(char *NwkSKey, char *AppSKey, char *AppKey);
    lora.setKey(NULL, NULL, "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz");
     
    lora.setDeciveMode(LWOTAA);           // select OTAA join mode (note that setDeciveMode is not a typo; it is misspelled in the library)
    // lora.setDataRate(DR5, EU868);         // SF7, 125 kbps (highest data rate)
    lora.setDataRate(DR3, EU868);         // SF9, 125 kbps (medium data rate and range)
    // lora.setDataRate(DR0, EU868);         // SF12, 125 kbps (lowest data rate, highest max. distance)
 
    // lora.setAdaptiveDataRate(false);   
    lora.setAdaptiveDataRate(true);       // automatically adapt the data rate
     
    lora.setChannel(0, 868.1);
    lora.setChannel(1, 868.3);
    lora.setChannel(2, 868.5);
    lora.setChannel(3, 867.1);
    lora.setChannel(4, 867.3);
    lora.setChannel(5, 867.5);
    lora.setChannel(6, 867.7);
    lora.setChannel(7, 867.9);
 
    // The following two commands can be left commented out;
    // TTN works with the default values. (It also works when
    // uncommenting the commands, though.)
    // lora.setReceiceWindowFirst(0, 868.1);
    // lora.setReceiceWindowSecond(869.525, DR0);
    
    lora.setDutyCycle(false);             // for debugging purposes only - should normally be activated
    lora.setJoinDutyCycle(false);         // for debugging purposes only - should normally be activated
     
    lora.setPower(14);                    // LoRa transceiver power (14 is the maximum for the 868 MHz band)
     
    // while(!lora.setOTAAJoin(JOIN));
    while(!lora.setOTAAJoin(JOIN,20));    // wait until the node has successfully joined TTN
 
    lora.setPort(33);                     // all data packets are sent to LoRaWAN port 33
}
 
void loop(void)
{  
    bool result = false;   
    float temperature, altitude, pressure, humidity;
    int16_t int16_temperature, int16_humidity, int16_pressure, int16_altitude, int16_vbat;
 
    bme280.takeForcedMeasurement();       // wake the sensor up for the next readings
 
    //get and print temperatures
    SerialUSB.print("Temp: ");
    SerialUSB.print(temperature = bme280.readTemperature());
    SerialUSB.print("C  ");
   
    //get and print atmospheric pressure data
    SerialUSB.print("Pressure: ");
    SerialUSB.print(pressure = bme280.readPressure());
    SerialUSB.print("Pa  ");
 
    //get and print altitude data
    SerialUSB.print("Altitude: ");
    SerialUSB.print(altitude = bme280.readAltitude(SEALEVELPRESSURE_HPA));
    SerialUSB.print("m  ");
 
    //get and print humidity data
    SerialUSB.print("Humidity: ");
    SerialUSB.print(humidity = bme280.readHumidity());
    SerialUSB.print("%  ");
     
    //get and print battery voltage
    SerialUSB.print("VBat: ");
    SerialUSB.print(int16_vbat=lora.getBatteryVoltage());
    SerialUSB.println("mV");
 
    int16_temperature = temperature*100.0;
    int16_humidity = humidity*100.0;
    int16_pressure = pressure/10.0;
    int16_altitude = altitude*10.0;
 
    data[0] = (byte) (int16_temperature >> 8);
    data[1] = (byte) (int16_temperature & 0x00FF);
    data[2] = (byte) (int16_humidity >> 8);
    data[3] = (byte) (int16_humidity & 0x00FF);
    data[4] = (byte) (int16_pressure >> 8);
    data[5] = (byte) (int16_pressure & 0x00FF);
    data[6] = (byte) (int16_altitude >> 8);
    data[7] = (byte) (int16_altitude & 0x00FF);
    data[8] = (byte) (int16_vbat >> 8);
    data[9] = (byte) (int16_vbat & 0x00FF);
     
    result = lora.transferPacket(data, 10, 5);   // send the data packet (10 bytes) with a default timeout of 5 secs
     
    if(result)
    {
        short length;
        short rssi;
         
        memset(buffer, 0, 256);
        length = lora.receivePacket(buffer, 256, &rssi);
         
        if(length)
        {
            SerialUSB.print("Length is: ");
            SerialUSB.println(length);
            SerialUSB.print("RSSI is: ");
            SerialUSB.println(rssi);
            SerialUSB.print("Data is: ");
            for(unsigned char i = 0; i < length; i ++)
            {
                SerialUSB.print("0x");
                SerialUSB.print(buffer[i], HEX);
                SerialUSB.print(" ");
            }
            SerialUSB.println();
        }
    }
     
    lora.setDeviceLowPower();     // bring the LoRaWAN module to sleep mode
    doSleep((5*60-8)*1000);       // deep sleep for 292 secs (+ 3 secs transmission time + 5 secs timeout = 300 secs period)
    lora.setPort(33);             // send some command to wake up the LoRaWAN module again
}
 
// The following function implements deep sleep waiting. When being called the
// CPU goes into deep sleep mode (for power saving). It is woken up again by
// the CPU-internal real time clock (RTC) after the configured time.
//
// A similar function would also be available in the standard "ArduinoLowPower" library.
// However, in order to be able to use that library with the Seeeduino LoRaWAN board,
// four files in the package "Seeed SAMD boards by Seeed Studio Version 1.3.0" that is
// installed using the Arduino IDE board manager need to be patched. The reason is that
// Seeed Studio have not updated their files to a recent Arduino SAMD version yet
// and the official "ArduinoLowPower" library provided by the Arduino foundation is
// referring to some missing functions. For further information see here:
// https://forum.arduino.cc/index.php?topic=603900.0 and here:
// https://github.com/arduino/ArduinoCore-samd/commit/b9ac48c782ca4b82ffd7e65bf2c956152386d82b
 
void doSleep(uint32_t millis) {
    if (!rtc.isConfigured()) {    // if called for the first time,
        rtc.begin(false);         // then initialize the real time clock (RTC)
    }
 
    uint32_t now = rtc.getEpoch();
    rtc.setAlarmEpoch(now + millis/1000);
    rtc.enableAlarm(rtc.MATCH_HHMMSS);
 
    rtc.standbyMode();            // bring CPU into deep sleep mode (until woken up by the RTC)
}
TTN payload decoder for Seeeduino LoRaWAN with GPS sensor node
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
function Decoder (bytes, port) {
  var result = {};
  var transformers = {};
   
  if (port==33) {
    transformers = {
      'temperature': function transform (bytes) {
          value=bytes[0]*256 + bytes[1];
          if (value>=32768) value=value-65536;
          return value/100.0;
        },
      'humidity': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/100.0;
        },
      'pressure': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/10.0;
        },
      'altitude': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/10.0;
        },
      'vbattery': function transform (bytes) {
          return (bytes[0]*256 + bytes[1])/1000.0;
        }
    }
     
    result['temperature'] = {
      value: transformers['temperature'](bytes.slice(0, 2)),
      uom: 'Celsius',
    }
     
     result['humidity'] = {
      value: transformers['humidity'](bytes.slice(2, 4)),
      uom: 'Percent',
    }
   
    result['pressure'] = {
      value: transformers['pressure'](bytes.slice(4, 6)),
      uom: 'hPa',
    }
     
    result['altitude'] = {
      value: transformers['altitude'](bytes.slice(6, 8)),
      uom: 'Meter',
    } 
 
    result['vbattery'] = {
      value: transformers['vbattery'](bytes.slice(8, 10)),
      uom: 'Volt',
    }
  }
  
  return result;
}

All-on-one Rpi Node

This sensor node is made to showcase a use-case of Raspberry Pi for a complete all on one sensor node. For achieving this a DHT-22 sensor along with a digital light sensor was used to measure temperature, humidity, and light. The sensor readings were directly pushed to the FROST Server running on the Pi itself. As a result, the Pi act as an independent sensor system running on the WLAN/WiFi network.

_images/hardware-setup.jpg

Hardware setup.

Hardware

To realize the objective, following components were used:

Wiring setup

First of all, the grove base shield was connected over the Raspberry Pi board. Then, the sensor connections were made using the connector cables as following:

  • DHT22 Sensor – Digital pin D4
  • Digital Light Sensor – I2C pin-1

Apart from this, there is no need of any other wiring in this case.

Once all these connection were made, the board is remotely accessed with a computer using SSH/VNC mode. Further, steps of software part needs to be followed.

Software

Configuring eduroam

To enable the raspberry pi node to connect with the eduroam WiFi network, first of all download this T-Telesec Global Root Class 2 certificate in .pem format. Copy this certificate file in the /etc/ssl/certs/ folder of the Raspberry Pi.

Now, enter the following command:

sudo nano /etc/network/interfaces

Edit this file and enter the following lines in the file. If the file is already having these lines, keep it unchanged.

allow-hotplug wlan0
iface wlan0 inet manual
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

Save the file with Ctrl+X and then press Y. Now again enter following command:

sudo nano /etc/wpa_supplicant/wpa_supplicant.conf

In the editor mode, enter the following lines in this file. Modify the Identity and Password with the correct eduroam login credentials. Save the file with Ctrl+X and then press Y.

ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=DE

network={
  ssid="eduroam"
  proto=RSN
  key_mgmt=WPA-EAP
  eap=PEAP
  ca_cert="/etc/ssl/certs/T-TeleSec_GlobalRoot_Class_2.pem"
  identity="gxxxxxx@eduroam.mwn.de"
  password="XXXXXXXXXX"
  phase1="peaplabel=0"
  phase2="auth=MSCHAPV2"
  subject_match="radius.lrz.de"}

Finally, enter the following command to restart the wifi configuration. Reboot the RaspberryPi and it should have a running WiFi with eduroam.

sudo wpa_supplicant -i wlan0 -c /etc/wpa_supplicant/wpa_supplicant.conf
Installing docker

To install docker service on the raspberry pi follow the tutorial available here and perform till step 4-i. After this you will have a running docker service on your raspberrypi.

Installing Node-Red

To install a node-red on the raspberyy pi node follow the following tutorial or alternatively follow the commands given below:

sudo apt-get install build-essential
sudo apt-get update
sudo apt-get upgrade
bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)
sudo systemctl start nodered.service      #Autostart node-red at startup
sudo systemctl enable nodered.service      #Autostart node-red at startup

After this, you should have a running node-red service on port 1880 which can be accessed via http://localhost:1880

It is to be noted, that although we have installed a node-red service on our sensor-node, we aren’t using it for this example.

Installing FROST Server

To setup a FROST server follow this detailed guide available on its github repository. Basically there are five major stpes:

Step-by-step commands are also provided below for the reference:

apt-get install postgresql postgis pgadmin3
sudo apt-get update                                       # update package list
sudo apt install openjdk-11-jdk
sudo apt-get install tomcat8 tomcat8-docs tomcat8-admin   # install tomcat
sudo nano /etc/tomcat8/tomcat-users.xml
sudo nano /usr/share/tomcat8-admin/manager/WEB-INF/web.xml
export CATALINA_HOME=/usr/share/tomcat8
sudo service tomcat8 restart

After the above steps are completed, a SensorThings API service should be running at: http://localhost:8080/FROST-Service/v1.0

GrovePi+ and Sensors

To create this sensor node, we used Python for setting up the Raspberry Pi. First, install the Dexter Grove Pi plus library on the board. Now download and run the Python script for All-on-one Raspberry Pi sensor node file in the text editor. This code was created by merging the example code of each of these attached sensor with the python code for transmitting the data to the FROST server. Some required changes were made while merging the example codes, such as changing the pin number for the sensor. The code also requires following dependent libraries to run:

Download these two .pyc files in the same folder with the Python script for All-on-one Raspberry Pi sensor node code file. Create a sub-folder inside this main folder and rename it as “lib”. Move the Adafruit_I2C.pyc into that lib folder. Now, the code can be compiled and run successfully. To post the sensor data to the FROST sever, an http post request needs to be made from the python. For this we use requests library available in the python. The URL needs to be configured in the ‘URL’ variable. Each post request is made separately with the unique datastream id of that particular sensor. Modify the post requests depending on the sensor value and the datastream ids.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import requests, json
URL= "http://tumgispinode.duckdns.org:8080/FROST-Server/v1.0/Observations"
header = {"Content-type" : "application/json"}
				payload = {'result': temp, 'Datastream': {'@iot.id': 1}}
				r = requests.post(URL, data=json.dumps(payload))
				payload = {'result': humidity, 'Datastream': {'@iot.id': 2}}
				r = requests.post(URL, data=json.dumps(payload))
				payload = {'result': readVisibleLux(), 'Datastream': {'@iot.id': 3}}
				r = requests.post(URL, data=json.dumps(payload))
				print(r.text)

To execute the code file run the following command:

python all_on_one_rpi_node.py

The code for sensors need to be modified according to the sensors used. The code below shows the part of the code used here to read, store, and print the sensor values.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
blue = 0    # The Blue colored sensor.
white = 1   # The White colored sensor.

def main():
	init()
	while (True):
		try:
			# This example uses the blue colored sensor. 
			# The first parameter is the port, the second parameter is the type of sensor.
			[temp,humidity] = grovepi.dht(sensor,white)  
			if math.isnan(temp) == False and math.isnan(humidity) == False:
				print("temp = %.02f C humidity =%.02f%%"%(temp, humidity))
				print("Lux: %i [Vis+IR=%i, IR=%i @ Gain=%ix, Timing=%.1fms]" % (readVisibleLux(), channel0, channel1, gain_m, timing_ms))
				sleep(50)		
		except IOError:
			print ("Error")

Services

This node direclty pushes the sensor data to the OGC Sensor Things API configured on the FROST Server using WLAN or WiFi connection. To be able to access the device from the local LAN we use the DNS service from the DuckDNS.

DuckDNS

DuckDNS is a free dynamic DNS hosted on AWS. Anyone can create a free account on this platform and register an ipaddress with a free subdomain as xxxx.duckdns.org. For this example we registered our ipaddress as tumgispinode.duckdns.org. It is required to update the ip address on the website if the ip of the device has changed. This can be automated with a shell script on Raspberry Pi to check the ip and update it on the duckdns website if it has changed. This is the reference Shell script for updating IP address on duckdns platform used in this example. Modify the ECHO URL and the token no with your set-dns address and the authentication token no from the duckdns website.

In addition to that, a cron tab task needs to be setup for running this shell script every few minutes. Enter the crontab edit mode with

 crontab -e

Now add the following lines in the end to automatically run the shell script every five minutes and the always run the python code for sensor readings on the boot. Modify the path of the files according to your file location.

 */5 * * * * /home/pi/duckdns/duck.sh
 @reboot python /home/pi/GrovePi/Node/node.py
Grafana Dashboard

To visualize the collected sensor data we use dashboard service available from Grafana. To install Grafana using docker run:

$ docker run -d -p 3000:3000 grafana/grafana

To enable the Grafana service to be able to read the data from the OGC Sensor things API we need to use linksmart-sensorthings-datasource extension. There is another repository explaining this installation, alternatively you install it with following commands:

docker exec -it -u root grafana /bin/bash
apt-get update
./bin/grafana-cli plugins install linksmart-sensorthings-datasource

Datastreams setup for this sensor node on the FROST server can be seen at: http://tumgispinode.duckdns.org:8080/FROST-Server/v1.0/Datastreams

The GRAFANA dash-board for visualizing the collected data is available at: http://tumgispinode.duckdns.org:3000/d/NAn_6Jmgk/raspberry-pi-node?orgId=1

Code files

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
import grovepi
import math
from time import sleep
import smbus
import requests, json
from Adafruit_I2C import Adafruit_I2C
import RPi.GPIO as GPIO
from smbus import SMBus

URL= "http://tumgispinode.duckdns.org:8080/FROST-Server/v1.0/Observations"
header = {"Content-type" : "application/json"}

TSL2561_Control = 0x80
TSL2561_Timing = 0x81
TSL2561_Interrupt = 0x86
TSL2561_Channel0L = 0x8C
TSL2561_Channel0H = 0x8D
TSL2561_Channel1L = 0x8E
TSL2561_Channel1H = 0x8F

TSL2561_Address = 0x29 #device address

LUX_SCALE = 14 # scale by 2^14
RATIO_SCALE = 9 # scale ratio by 2^9
CH_SCALE = 10 # scale channel values by 2^10
CHSCALE_TINT0 = 0x7517 # 322/11 * 2^CH_SCALE
CHSCALE_TINT1 = 0x0fe7 # 322/81 * 2^CH_SCALE

K1T = 0x0040 # 0.125 * 2^RATIO_SCALE
B1T = 0x01f2 # 0.0304 * 2^LUX_SCALE
M1T = 0x01be # 0.0272 * 2^LUX_SCALE
K2T = 0x0080 # 0.250 * 2^RATIO_SCA
B2T = 0x0214 # 0.0325 * 2^LUX_SCALE
M2T = 0x02d1 # 0.0440 * 2^LUX_SCALE
K3T = 0x00c0 # 0.375 * 2^RATIO_SCALE
B3T = 0x023f # 0.0351 * 2^LUX_SCALE
M3T = 0x037b # 0.0544 * 2^LUX_SCALE
K4T = 0x0100 # 0.50 * 2^RATIO_SCALE
B4T = 0x0270 # 0.0381 * 2^LUX_SCALE
M4T = 0x03fe # 0.0624 * 2^LUX_SCALE
K5T = 0x0138 # 0.61 * 2^RATIO_SCALE
B5T = 0x016f # 0.0224 * 2^LUX_SCALE
M5T = 0x01fc # 0.0310 * 2^LUX_SCALE
K6T = 0x019a # 0.80 * 2^RATIO_SCALE
B6T = 0x00d2 # 0.0128 * 2^LUX_SCALE
M6T = 0x00fb # 0.0153 * 2^LUX_SCALE
K7T = 0x029a # 1.3 * 2^RATIO_SCALE
B7T = 0x0018 # 0.00146 * 2^LUX_SCALE
M7T = 0x0012 # 0.00112 * 2^LUX_SCALE
K8T = 0x029a # 1.3 * 2^RATIO_SCALE
B8T = 0x0000 # 0.000 * 2^LUX_SCALE
M8T = 0x0000 # 0.000 * 2^LUX_SCALE



K1C = 0x0043 # 0.130 * 2^RATIO_SCALE
B1C = 0x0204 # 0.0315 * 2^LUX_SCALE
M1C = 0x01ad # 0.0262 * 2^LUX_SCALE
K2C = 0x0085 # 0.260 * 2^RATIO_SCALE
B2C = 0x0228 # 0.0337 * 2^LUX_SCALE
M2C = 0x02c1 # 0.0430 * 2^LUX_SCALE
K3C = 0x00c8 # 0.390 * 2^RATIO_SCALE
B3C = 0x0253 # 0.0363 * 2^LUX_SCALE
M3C = 0x0363 # 0.0529 * 2^LUX_SCALE
K4C = 0x010a # 0.520 * 2^RATIO_SCALE
B4C = 0x0282 # 0.0392 * 2^LUX_SCALE
M4C = 0x03df # 0.0605 * 2^LUX_SCALE
K5C = 0x014d # 0.65 * 2^RATIO_SCALE
B5C = 0x0177 # 0.0229 * 2^LUX_SCALE
M5C = 0x01dd # 0.0291 * 2^LUX_SCALE
K6C = 0x019a # 0.80 * 2^RATIO_SCALE
B6C = 0x0101 # 0.0157 * 2^LUX_SCALE
M6C = 0x0127 # 0.0180 * 2^LUX_SCALE
K7C = 0x029a # 1.3 * 2^RATIO_SCALE
B7C = 0x0037 # 0.00338 * 2^LUX_SCALE
M7C = 0x002b # 0.00260 * 2^LUX_SCALE
K8C = 0x029a # 1.3 * 2^RATIO_SCALE
B8C = 0x0000 # 0.000 * 2^LUX_SCALE
M8C = 0x0000 # 0.000 * 2^LUX_SCALE

# bus parameters
rev = GPIO.RPI_REVISION
if rev == 2 or rev == 3:
	bus = smbus.SMBus(1)
else:
	bus = smbus.SMBus(0)
i2c = Adafruit_I2C(TSL2561_Address)

debug = False
cooldown_time = 0.005 # measured in seconds
packageType = 0 # 0=T package, 1=CS package
gain = 0        # current gain: 0=1x, 1=16x [dynamically selected]
gain_m = 1      # current gain, as multiplier
timing = 2      # current integration time: 0=13.7ms, 1=101ms, 2=402ms [dynamically selected]
timing_ms = 0   # current integration time, in ms
channel0 = 0    # raw current value of visible+ir sensor
channel1 = 0    # raw current value of ir sensor
schannel0 = 0   # normalized current value of visible+ir sensor
schannel1 = 0   # normalized current value of ir sensor


def readRegister(address):
	try:
		byteval = i2c.readU8(address)

		sleep(cooldown_time)
		if (debug):
			print("TSL2561.readRegister: returned 0x%02X from reg 0x%02X" % (byteval, address))
		return byteval
	except IOError:
		print("TSL2561.readRegister: error reading byte from reg 0x%02X" % address)
		return -1


def writeRegister(address, val):
	try:
		i2c.write8(address, val)

		sleep(cooldown_time)
		if (debug):
			print("TSL2561.writeRegister: wrote 0x%02X to reg 0x%02X" % (val, address))
	except IOError:

		sleep(cooldown_time)
		print("TSL2561.writeRegister: error writing byte to reg 0x%02X" % address)
		return -1

def powerUp():
	writeRegister(TSL2561_Control, 0x03)

def powerDown():
	writeRegister(TSL2561_Control, 0x00)

def setTintAndGain():
	global gain_m, timing_ms

	if gain == 0:
		gain_m = 1
	else:
		gain_m = 16

	if timing == 0:
		timing_ms = 13.7
	elif timing == 1:
		timing_ms = 101
	else:
		timing_ms = 402
	writeRegister(TSL2561_Timing, timing | gain << 4)

def readLux():
	sleep(float(timing_ms + 1) / 1000)

	ch0_low  = readRegister(TSL2561_Channel0L)
	ch0_high = readRegister(TSL2561_Channel0H)
	ch1_low  = readRegister(TSL2561_Channel1L)
	ch1_high = readRegister(TSL2561_Channel1H)

	global channel0, channel1
	channel0 = (ch0_high<<8) | ch0_low
	channel1 = (ch1_high<<8) | ch1_low

	sleep(cooldown_time)
	if debug:
		print("TSL2561.readVisibleLux: channel 0 = %i, channel 1 = %i [gain=%ix, timing=%ims]" % (channel0, channel1, gain_m, timing_ms))

def readVisibleLux():
	global timing, gain

	powerUp()
	readLux()

	if channel0 < 500 and timing == 0:
		timing = 1
		sleep(cooldown_time)
		if debug:
			print("TSL2561.readVisibleLux: too dark. Increasing integration time from 13.7ms to 101ms")
		setTintAndGain()
		readLux()

	if channel0 < 500 and timing == 1:
		timing = 2
		sleep(cooldown_time)
		if debug:
			print("TSL2561.readVisibleLux: too dark. Increasing integration time from 101ms to 402ms")
		setTintAndGain()
		readLux()

	if channel0 < 500 and timing == 2 and gain == 0:
		gain = 1
		sleep(cooldown_time)
		if debug:
			print("TSL2561.readVisibleLux: too dark. Setting high gain")
		setTintAndGain()
		readLux()

	if (channel0 > 20000 or channel1 > 20000) and timing == 2 and gain == 1:
		gain = 0
		sleep(cooldown_time)
		if debug:
			print("TSL2561.readVisibleLux: enough light. Setting low gain")
		setTintAndGain()
		readLux()

	if (channel0 > 20000 or channel1 > 20000) and timing == 2:
		timing = 1
		sleep(cooldown_time)
		if debug:
			print("TSL2561.readVisibleLux: enough light. Reducing integration time from 402ms to 101ms")
		setTintAndGain()
		readLux()

	if (channel0 > 10000 or channel1 > 10000) and timing == 1:
		timing = 0
		sleep(cooldown_time)
		if debug:
			print("TSL2561.readVisibleLux: enough light. Reducing integration time from 101ms to 13.7ms")
		setTintAndGain()
		readLux()

	powerDown()

	if (timing == 0 and (channel0 > 5000 or channel1 > 5000)) or (timing == 1 and (channel0 > 37000 or channel1 > 37000)) or (timing == 2 and (channel0 > 65000 or channel1 > 65000)):
		# overflow
		return -1

	return calculateLux(channel0, channel1)

def calculateLux(ch0, ch1):
	chScale = 0
	if timing == 0:   # 13.7 msec
		chScale = CHSCALE_TINT0
	elif timing == 1: # 101 msec
		chScale = CHSCALE_TINT1;
	else:           # assume no scaling
		chScale = (1 << CH_SCALE)

	if gain == 0:
		chScale = chScale << 4 # scale 1X to 16X

	# scale the channel values
	global schannel0, schannel1
	schannel0 = (ch0 * chScale) >> CH_SCALE
	schannel1 = (ch1 * chScale) >> CH_SCALE

	ratio = 0
	if schannel0 != 0:
		ratio = (schannel1 << (RATIO_SCALE+1)) / schannel0
	ratio = (ratio + 1) >> 1

	if packageType == 0: # T package
		if ((ratio >= 0) and (ratio <= K1T)):
			b=B1T; m=M1T;
		elif (ratio <= K2T):
			b=B2T; m=M2T;
		elif (ratio <= K3T):
			b=B3T; m=M3T;
		elif (ratio <= K4T):
			b=B4T; m=M4T;
		elif (ratio <= K5T):
			b=B5T; m=M5T;
		elif (ratio <= K6T):
			b=B6T; m=M6T;
		elif (ratio <= K7T):
			b=B7T; m=M7T;
		elif (ratio > K8T):
			b=B8T; m=M8T;
	elif packageType == 1: # CS package
		if ((ratio >= 0) and (ratio <= K1C)):
			b=B1C; m=M1C;
		elif (ratio <= K2C):
			b=B2C; m=M2C;
		elif (ratio <= K3C):
			b=B3C; m=M3C;
		elif (ratio <= K4C):
			b=B4C; m=M4C;
		elif (ratio <= K5C):
			b=B5C; m=M5C;
		elif (ratio <= K6C):
			b=B6C; m=M6C;
		elif (ratio <= K7C):
			b=B7C; m=M7C;

	temp = ((schannel0*b)-(schannel1*m))
	if temp < 0:
		temp = 0;
	temp += (1<<(LUX_SCALE-1))
	# strip off fractional portion
	lux = temp>>LUX_SCALE
	sleep(cooldown_time)
	if debug:
		print("TSL2561.calculateLux: %i" % lux)

	return lux

def init():
	powerUp()
	setTintAndGain()
	writeRegister(TSL2561_Interrupt, 0x00)
	powerDown()
# Connect the Grove Temperature & Humidity Sensor Pro to digital port D4
# This example uses the blue colored sensor.
# SIG,NC,VCC,GND
sensor = 4  # The Sensor goes on digital port 4.

# temp_humidity_sensor_type
# Grove Base Kit comes with the blue sensor.
blue = 0    # The Blue colored sensor.
white = 1   # The White colored sensor.

def main():
	init()
	while (True):
		try:
			# This example uses the blue colored sensor. 
			# The first parameter is the port, the second parameter is the type of sensor.
			[temp,humidity] = grovepi.dht(sensor,white)  
			if math.isnan(temp) == False and math.isnan(humidity) == False:
				print("temp = %.02f C humidity =%.02f%%"%(temp, humidity))
				print("Lux: %i [Vis+IR=%i, IR=%i @ Gain=%ix, Timing=%.1fms]" % (readVisibleLux(), channel0, channel1, gain_m, timing_ms))
				payload = {'result': temp, 'Datastream': {'@iot.id': 1}}
				r = requests.post(URL, data=json.dumps(payload))
				payload = {'result': humidity, 'Datastream': {'@iot.id': 2}}
				r = requests.post(URL, data=json.dumps(payload))
				payload = {'result': readVisibleLux(), 'Datastream': {'@iot.id': 3}}
				r = requests.post(URL, data=json.dumps(payload))
				print(r.text)
				sleep(50)		
		except IOError:
			print ("Error")
			
		

if __name__ == "__main__":
        main()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

getMyIP() {
    local _ip _myip _line _nl=$'\n'
    while IFS=$': \t' read -a _line ;do
        [ -z "${_line%inet}" ] &&
           _ip=${_line[${#_line[1]}>4?1:2]} &&
           [ "${_ip#127.0.0.1}" ] && _myip=$_ip
      done< <(LANG=C /sbin/ifconfig)
    printf ${1+-v} $1 "%s${_nl:0:$[${#1}>0?0:1]}" $_myip
}

echo url="https://www.duckdns.org/update?domains=tumgispinode.duckdns.org&token=XXXXXXXXXXXXXXXXXX&ip=$(getMyIP)&verbose=TRUE" | curl -k -o /home/pi/duckdns/duck.log -K -

Indices and tables