LoRaWAN gateways up and running in Trondheim

lora-logo-transp-400x231

For a few months we have had two lora gateways up and running in Trondheim. One on the roof of olavskvartalet. Its gateway_ID is AA555A0008060353.

Another one is located on the roof of samfundet. Its gateway_ID is AA555A0008060252.
They are both forwarding data packets to The Things Network.

LoRaWAN is a Low Power Wide Area Network (LPWAN) specification intended for wireless battery operated Things. The intention is to provide infrastructure for the internet of things. More and more devices will be connected to the internet.

The gateways are publicly accessible. This means they will accept all packets and ship them off to The Things Network. To grab the data you use the http api.

To understand the data flow and architecture, visit the The Things Network wiki.

Two NTNU students are already using our lora network to measure co2 emissions in Elgeseter gate.

Feel free to contact us at hei@tradlosetrondheim.no if you find this interesting!

Let’s setup MultiConnect Conduit

multiconnect-conduit

The following guide assumes you have basic computer and basic linux knowledge. Hardware required:

  • MTCDT-210L-US-EU-GB Multitech mLinux Conduit (non-cellular)
  • MTAC-LORA-868 accessory card

For console access I plugged into the USB device port behind the device.
To access the device I use minicom(1):

minicom -D /dev/ttyACM0 -b 9600

This gives me the following:

            _     _                  
  _ __ ___ | |   (_)_ __  _   ___  __
 | '_ ` _ \| |   | | '_ \| | | \ \/ /
 | | | | | | |___| | | | | |_| |>  < 
 |_| |_| |_|_____|_|_| |_|\__,_/_/\_\
                                     
MultiTech Systems mLinux GNU/Linux
mLinux 3.1.0 mtcdt /dev/ttyGS0

mtcdt login: 

The username and password is root and root. That should be changed immediately.
I use openssl to produce a password with sufficient entropy:

openssl rand -base64 16

A new root password is set with passwd(1).

The mLinux distribution is based upon busybox, which is a minimal linux environment.

The distro version can be shown with:

cat /etc/mlinux-version

For me it displayed:

cat /etc/mlinux-version
mLinux 3.1.0
Built from branch: (detachedfrom4fd069b)
Revision: 4fd069b97a2354995920b52f731d661f0bacb39a

Network

mLinux has a static IP 192.168.2.1 as defined in /etc/network/interfaces.
A DHCP client is not enabled.

Instead of setting a static IP adress, use DHCP instead. Add the following to /etc/network/interfaces:

auto eth0             
iface eth0 inet dhcp

For changes to take effect issue:

ifdown eth0 && ifup eth0

This gives output:

udhcpc (v1.22.1) started
Sending discover...
Sending discover...
Sending select for 10.9.12.234...
Lease of 10.9.12.234 obtained, lease time 36000
/etc/udhcpc.d/50default: Adding DNS 10.9.0.11
/etc/udhcpc.d/50default: Adding DNS 10.9.0.10

Udhcpc is a very small DHCP client program geared towards embedded systems. If you have control over the DHCP server you can consistently offer a same ip by configuring the MAC address of the device.

Date and time

Set correct timezone:

ln -fs /usr/share/zoneinfo/Europe/Oslo /etc/localtime

Set date and time:

date "2016-03-07 11:17:40"

Update the hardware clock:

hwclock -u -w

Configuring packet forwarder

The lora network server is used if you want to run your own cloud. Since we are using the things network we only really need the packet forwarder to be running.

People have reported that the poly pkt forwarder works better than the basic pkt forwarder. Fetch the poly-packet-forwarder_2.1-r1. Install with:

opkg install poly-packet-forwarder_2.1-r1_arm926ejste.ipk

Change /etc/init.d/lora-network-server so that the variable pkt_fwd
is assigned pkt_fwd=/opt/lora/poly_pkt_fwd

Inside /var/config/lora/global_conf.json you have to add some changes. Here are a few of mine:

"gateway_ID": "008000000000A447",
...
"servers":
        [ { "server_address": "croft.thethings.girovito.nl",
            "serv_port_up": 1700,
            "serv_port_down": 1700,
            "serv_enabled": true }
        ],
...
"fake_gps": true,
 "ref_latitude": 63.43,
 "ref_longitude": 10.40,
...
/* Email of gateway operator, max 40 chars*/
"contact_email": "drift@tradlosetrondheim.no", 
/* Public description of this device, max 64 chars */
"description": "This is wireless trondheims multiconnect conduit number 1" 

Restart network server:

/etc/init.d/lora-network-server restart

Tail log for debug info:

tail -f /var/log/lora-pkt-fwd.log

My gateway is now available at The Things Network.

Data fetching and visualization using d3.js

complex-664440_1280

Data fetching is the process of grabbing data from a data source. In our lora network, packets are shipped off to The Things Network API.

On the web, javascript is what we got for doing computation in a browser. Ajax is a set of techniques used on the client-side to create asynchronous web applications.

ALSO READ: Temperature monitoring with lora

To fetch resources asynchronously, utilize the XMLHttpRequest object:

var trtHttp = (function (my) {
    var http = new XMLHttpRequest();

    my.getJson = function (url, cb) {
        http.onreadystatechange = function () {
            if (http.readyState === XMLHttpRequest.DONE) {
                if (http.status === 200) {
                    cb(JSON.parse(http.responseText));
                }
            }
        };

        http.open("GET", url, true);
        http.send();
    }

    return my;
}(trtHttp || {}));

The wrapping of getJson inside trtHttp is a javascript way of namespacing functions.
With this pattern, member functions of trtHttp can be spread out in different files.

Fetch data from our temperature sensor:

var url = 'http://thethingsnetwork.org/api/v0/nodes/02031003/?limit=200&format=json';

trtHttp.getJson(url, function (data) {
    console.log(data);
});

If you don’t feel the need for going this lowlevel, you can use jQuery:

$.getJSON(url, function (data) {
    console.log(data);
});

This achieve the same thing.

ALSO READ: Sending text with lorawan towards the things network

Data visualization

D3.js is a javaScript library for manipulating documents based on data. Let’s create a bar chart from a dataset containing integers between 0 and 10.

barchart-integers-2

var data = [1,2,3,4,5,6,7,8,9]

var margin = {
    bottom: 10,
    top: 10,
    left: 50,
    right: 20
};

var width = 500;
var height = 300;

var chartWidth = width - margin.left - margin.right;
var chartHeight = height - margin.top - margin.bottom;

var yScale = d3.scale.linear()
    .domain([0, 10])
    .range([chartHeight, margin.bottom + margin.top])

var yAxis = d3.svg.axis()
    .scale(yScale)
    .orient('left');

var svg = d3.select('body')
    .append('svg')
    .attr('width', width)
    .attr('height', height)

var chart = svg.append('g')
    .attr('width', chartWidth)
    .attr('height', chartHeight)
    .attr('transform', "translate(" + margin.left + ",0)")

svg.append('g')
    .attr('transform', "translate(30,0)")
    .attr("class","axis")
    .call(yAxis)

chart.selectAll('rect')
    .data(data)
    .enter()
    .append('rect')
    .attr('fill', function (d) {
        return "rgb(0, 0, " + (d * 30) + ")";
    })

    .attr('x', function (d, i) {
        return i * chartWidth / data.length
    })

    .attr('y', function (d) {
        return yScale(d);
    })

    .attr('width', chartWidth / data.length-1)

    .attr('height', function (d) {
        return yScale(0) - yScale(d);
    })

To fit our data of integers onto an svg we need to convert the integer range [0, 10] to [0, 300]. This is needed because otherwise the bars would be very small since the height of the svg is 300 pixels.
The d3.scale function performs the conversion. We need only provide it with an input domain and an output range.

To use the data from our temperature node, we simply wrap the code inside a json data fetch block:

trtHttp.getJson(url, function (data) {

    var data = data.map(function (item) {
        return item.data_json;
    })

    [...]
})

The temperature node location is in our office right now, so temperature values vary between 22 and 23 celcius.
The graph is not particurlarly exciting. Here it is:

barchart-temperatures

Temperature monitoring with lora

IMG_20151216_134116This is how I programmed the NUCLEO-L152RE microcontroller and Grove – Temperature&Humidity Sensor Pro to monitor temperature and humidity. The temperature readings are shipped off to the things network via our LoRaWAN network.

The purpose of this is to help others get started using the LoRaWAN technology to build IOT applications.

ALSO READ: Sending text with lorawan towards the things network

Temp_humi_pro
Grove – Temperature and Humidity Sensor Pro

To read temperature data from the Grove sensor I imported the DHT C++ library. Next I added an include for the header file

#include "DHT.h"

The temperature is read with


DHT sensor(A1, AM2302); // AM2302 is the product name of the sensor
int err = sensor.readData();

float getTemperature() {

    int err = 1;
  
    while(err != 0) {
        wait(2.0f); // Wait 2 seconds
        err = sensor.readData();
    }
    
    return sensor.ReadTemperature(CELCIUS);
}

The humidity sensor is accessed in a similar way: sensor.ReadHumidity()

I plugged the grove sensor into A0 of the grove shield. This turned out not to work because the Semtech lora transceiver uses this port for NRESET. I used A1 instead.

I’m occasionally getting

Err 6

from sensor.readData(), which is a checksum (CRC) error.
To get a temperature value without error, I read the sensor in a loop until there is no error.

After a few flashings of binary onto the microcontroller I got this error:


Jan 18 11:11:15 bergman kernel: VFS: busy inodes on changed media or resized disk sdb
cp: error writing ‘/media/d/NODE_L152RE/LoRaWAN_send_text_2_NUCLEO_L152RE.bin’: No space left on device
cp: failed to extend ‘/media/d/NODE_L152RE/LoRaWAN_send_text_2_NUCLEO_L152RE.bin’: No space left on device

I’m unable to repair the root cause of this. But replugging the device fixes the symptom.

To execute the temperature reading code each 60s I add this at the end of the onSendFrame function:

os_setTimedCallback(j, os_getTime() + sec2osticks(60), onSendFrame);

Live demo

temp_and_hum

See our live demo for real-time humidity and temperature readings! Also see our github repo for its code.

The things network API do not support TLS yet. I had to add an exception in our apache config to circumvent browser blocking of non-TLS content:

RewriteRule ^/demo/lora-temperature - [L]

The full code

#include "mbed.h"
#include "lmic.h"
#include "debug.h"
#include "Serial.h"

#include "DHT.h"

#define LORAWAN_NET_ID (uint32_t) 0x00000000
#define LORAWAN_DEV_ADDR (uint32_t) 0x02031003
#define LORAWAN_ADR_ON 1
#define LORAWAN_CONFIRMED_MSG_ON 1
#define LORAWAN_APP_PORT 3//15

DHT sensor(A1, AM2302);

Serial co2(D8, D2);

static uint8_t NwkSKey[] = {
    0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 
    0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
};

static uint8_t ArtSKey[] = {
    0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 
    0xAB, 0xF7, 0x15, 0x88, 0x09, 0xCF, 0x4F, 0x3C
};

osjob_t initjob;
osjob_t sendFrameJob;
u1_t n = 0;

void os_getArtEui (uint8_t *buf) {} // ignore
void os_getDevEui (uint8_t *buf) {} // ignore
void os_getDevKey (uint8_t *buf) {} // ignore

float getTemperature() {

    int err = 1;
  
    while(err != 0) {
        wait(2.0f);
        err = sensor.readData();
    }
    
    return sensor.ReadTemperature(CELCIUS);
}

float getHumidity() {
    return sensor.ReadHumidity();
}
    
void onSendFrame (osjob_t* j) {
 
    const unsigned char cmd_get_sensor[] = {
        0xff, 0x01, 0x86, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x79
    };

    for (int i = 0; i < 9; i++) {
        co2.putc(cmd_get_sensor[i]);
        wait(0.01);
    }
    
    char message[32];
    
    float temperature = getTemperature();
    printf("Temperature is %4.2f \r\n", temperature);
    
    float humidity = getHumidity();
    printf("Humidity is %4.2f \r\n", humidity);

    sprintf(message, "%4.2f %4.2f", temperature, humidity);
    
    int frameLength = strlen(message); // keep it < 32
    for (int i = 0; i < frameLength; i++) {
        LMIC.frame[i] = message[i];
    }
    int result = LMIC_setTxData2(LORAWAN_APP_PORT, LMIC.frame, 
        frameLength, LORAWAN_CONFIRMED_MSG_ON); // calls onEvent()
          
    os_setTimedCallback(j, os_getTime() + sec2osticks(60), onSendFrame);
}

void onInit (osjob_t* j) {
    LMIC_reset();
    LMIC_setAdrMode(LORAWAN_ADR_ON);
    LMIC_setDrTxpow(DR_SF12, 14);
    LMIC_setSession(LORAWAN_NET_ID, LORAWAN_DEV_ADDR, NwkSKey, ArtSKey);
 
    onSendFrame(NULL);
}

void onEvent (ev_t ev) { // called by lmic.cpp, see also oslmic.h
    debug_event(ev);
    if (ev == EV_TXCOMPLETE) {
        os_setCallback(&sendFrameJob, onSendFrame);
    }
}

int main (void) {
    debug_str("Entered main\r\n");
    os_init();
    os_setCallback(&initjob, onInit);
    os_runloop(); // blocking
}