A LoRa packet forwarder running on the host of a LoRa Picocell Gateway that forwards RF packets receive by the concentrator to a server through a IP/UDP link, and emits RF packets that are sent by the server.

Files at this revision

API Documentation at this revision

Comitter:
dgabino
Date:
Wed Apr 11 14:47:16 2018 +0000
Commit message:
Initial commit

Changed in this revision

LICENSE Show annotated file Show diff for this revision Revisions of this file
Makefile Show annotated file Show diff for this revision Revisions of this file
PROTOCOL.TXT Show annotated file Show diff for this revision Revisions of this file
VERSION Show annotated file Show diff for this revision Revisions of this file
daemon/run_pkt_fwd.sh Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/Makefile Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/cfg/global_conf_PicoV1p0_EU.json Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/cfg/global_conf_PicoV1p0_US.json Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/global_conf.json Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/inc/base64.h Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/inc/jitqueue.h Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/inc/parson.h Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/inc/timersync.h Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/inc/trace.h Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/readme.md Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/src/base64.c Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/src/jitqueue.c Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/src/lora_pkt_fwd.c Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/src/parson.c Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/src/timersync.c Show annotated file Show diff for this revision Revisions of this file
lora_pkt_fwd/update_gwid.sh Show annotated file Show diff for this revision Revisions of this file
readme.md Show annotated file Show diff for this revision Revisions of this file
util_ack/Makefile Show annotated file Show diff for this revision Revisions of this file
util_ack/readme.md Show annotated file Show diff for this revision Revisions of this file
util_ack/src/util_ack.c Show annotated file Show diff for this revision Revisions of this file
util_sink/Makefile Show annotated file Show diff for this revision Revisions of this file
util_sink/readme.md Show annotated file Show diff for this revision Revisions of this file
util_sink/src/util_sink.c Show annotated file Show diff for this revision Revisions of this file
util_tx_test/Makefile Show annotated file Show diff for this revision Revisions of this file
util_tx_test/inc/base64.h Show annotated file Show diff for this revision Revisions of this file
util_tx_test/readme.md Show annotated file Show diff for this revision Revisions of this file
util_tx_test/src/base64.c Show annotated file Show diff for this revision Revisions of this file
util_tx_test/src/util_tx_test.c Show annotated file Show diff for this revision Revisions of this file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,49 @@
+Copyright (C) 2013, SEMTECH S.A.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+	* Redistributions of source code must retain the above copyright
+	  notice, this list of conditions and the following disclaimer.
+	* Redistributions in binary form must reproduce the above copyright
+	  notice, this list of conditions and the following disclaimer in the
+	  documentation and/or other materials provided with the distribution.
+	* Neither the name of the Semtech corporation nor the
+	  names of its contributors may be used to endorse or promote products
+	  derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL SEMTECH S.A. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+--- For the parson library ---
+
+Parson ( http://kgabis.github.com/parson/ )
+Copyright (C) 2012 Krzysztof Gabis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,22 @@
+### Environment constants 
+
+LGW_PATH ?= ../../picoGW_hal/libloragw
+ARCH ?=
+CROSS_COMPILE ?=
+export
+
+### general build targets
+
+all:
+	$(MAKE) all -e -C lora_pkt_fwd
+	$(MAKE) all -e -C util_ack
+	$(MAKE) all -e -C util_sink
+	$(MAKE) all -e -C util_tx_test
+
+clean:
+	$(MAKE) clean -e -C lora_pkt_fwd
+	$(MAKE) clean -e -C util_ack
+	$(MAKE) clean -e -C util_sink
+	$(MAKE) clean -e -C util_tx_test
+
+### EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PROTOCOL.TXT	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,456 @@
+	  ______                              _
+	 / _____)             _              | |    
+	( (____  _____ ____ _| |_ _____  ____| |__  
+	 \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
+	 _____) ) ____| | | || |_| ____( (___| | | |
+	(______/|_____)_|_|_| \__)_____)\____)_| |_|
+	  (C)2013 Semtech-Cycleo
+
+Basic communication protocol between Lora gateway and server
+=============================================================
+
+
+1. Introduction
+----------------
+
+The protocol between the gateway and the server is purposefully very basic and 
+for demonstration purpose only, or for use on private and reliable networks.
+
+There is no authentication of the gateway or the server, and the acknowledges 
+are only used for network quality assessment, not to correct UDP datagrams 
+losses (no retries).
+
+
+2. System schematic and definitions
+------------------------------------
+
+	 ((( Y )))
+	     |
+	     |
+	+ - -|- - - - - - - - - - - - - +        xxxxxxxxxxxx          +--------+
+	| +--+-----------+     +------+ |       xx x  x     xxx        |        |
+	| |              |     |      | |      xx  Internet  xx        |        |
+	| | Concentrator |<--->| Host |<-------xx     or    xx-------->|        |
+	| |              | SPI |      | |      xx  Intranet  xx        | Server |
+	| +--------------+     +------+ |       xxxx   x   xxxx        |        |
+	|    ^                     ^    |           xxxxxxxx           |        |
+	|    | PPS +-------+ NMEA  |    |                              |        |
+	|    +-----|  GPS  |-------+    |                              +--------+
+	|          | (opt) |            |
+	|          +-------+            |
+	|                               |
+	|             Gateway           |
+	+- - - - - - - - - - - - - - - -+
+
+__Concentrator__: radio RX/TX board, based on Semtech multichannel modems 
+(SX130x), transceivers (SX135x) and/or low-power stand-alone modems (SX127x). 
+
+__Host__: embedded computer on which the packet forwarder is run. Drives the 
+concentrator through a SPI link.
+
+__GPS__: GNSS (GPS, Galileo, GLONASS, etc) receiver with a "1 Pulse Per Second"
+output and a serial link to the host to send NMEA frames containing time and
+geographical coordinates data. Optional.
+
+__Gateway__: a device composed of at least one radio concentrator, a host, some 
+network connection to the internet or a private network (Ethernet, 3G, Wifi, 
+microwave link), and optionally a GPS receiver for synchronization. 
+
+__Server__: an abstract computer that will process the RF packets received and 
+forwarded by the gateway, and issue RF packets in response that the gateway 
+will have to emit.
+
+It is assumed that the gateway can be behind a NAT or a firewall stopping any 
+incoming connection.
+It is assumed that the server has an static IP address (or an address solvable 
+through a DNS service) and is able to receive incoming connections on a 
+specific port.
+
+
+3. Upstream protocol
+---------------------
+
+### 3.1. Sequence diagram ###
+
+	+---------+                                                    +---------+
+	| Gateway |                                                    | Server  |
+	+---------+                                                    +---------+
+	     | -----------------------------------\                         |
+	     |-| When 1-N RF packets are received |                         |
+	     | ------------------------------------                         |
+	     |                                                              |
+	     | PUSH_DATA (token X, GW MAC, JSON payload)                    |
+	     |------------------------------------------------------------->|
+	     |                                                              |
+	     |                                           PUSH_ACK (token X) |
+	     |<-------------------------------------------------------------|
+	     |                              ------------------------------\ |
+	     |                              | process packets *after* ack |-|
+	     |                              ------------------------------- |
+	     |                                                              |
+
+### 3.2. PUSH_DATA packet ###
+
+That packet type is used by the gateway mainly to forward the RF packets 
+received, and associated metadata, to the server.
+
+ Bytes  | Function
+:------:|---------------------------------------------------------------------
+ 0      | protocol version = 2
+ 1-2    | random token
+ 3      | PUSH_DATA identifier 0x00
+ 4-11   | Gateway unique identifier (MAC address)
+ 12-end | JSON object, starting with {, ending with }, see section 4
+
+### 3.3. PUSH_ACK packet ###
+
+That packet type is used by the server to acknowledge immediately all the 
+PUSH_DATA packets received.
+
+ Bytes  | Function
+:------:|---------------------------------------------------------------------
+ 0      | protocol version = 2
+ 1-2    | same token as the PUSH_DATA packet to acknowledge
+ 3      | PUSH_ACK identifier 0x01
+
+
+4. Upstream JSON data structure
+--------------------------------
+
+The root object can contain an array named "rxpk":
+
+``` json
+{
+	"rxpk":[ {...}, ...]
+}
+```
+
+That array contains at least one JSON object, each object contain a RF packet 
+and associated metadata with the following fields:
+
+ Name |  Type  | Function
+:----:|:------:|--------------------------------------------------------------
+ time | string | UTC time of pkt RX, us precision, ISO 8601 'compact' format
+ tmst | number | Internal timestamp of "RX finished" event (32b unsigned)
+ freq | number | RX central frequency in MHz (unsigned float, Hz precision)
+ chan | number | Concentrator "IF" channel used for RX (unsigned integer)
+ rfch | number | Concentrator "RF chain" used for RX (unsigned integer)
+ stat | number | CRC status: 1 = OK, -1 = fail, 0 = no CRC
+ modu | string | Modulation identifier "LORA" or "FSK"
+ datr | string | LoRa datarate identifier (eg. SF12BW500)
+ datr | number | FSK datarate (unsigned, in bits per second)
+ codr | string | LoRa ECC coding rate identifier
+ rssi | number | RSSI in dBm (signed integer, 1 dB precision)
+ lsnr | number | Lora SNR ratio in dB (signed float, 0.1 dB precision)
+ size | number | RF packet payload size in bytes (unsigned integer)
+ data | string | Base64 encoded RF packet payload, padded
+
+Example (white-spaces, indentation and newlines added for readability):
+
+``` json
+{"rxpk":[
+	{
+		"time":"2013-03-31T16:21:17.528002Z",
+		"tmst":3512348611,
+		"chan":2,
+		"rfch":0,
+		"freq":866.349812,
+		"stat":1,
+		"modu":"LORA",
+		"datr":"SF7BW125",
+		"codr":"4/6",
+		"rssi":-35,
+		"lsnr":5.1,
+		"size":32,
+		"data":"-DS4CGaDCdG+48eJNM3Vai-zDpsR71Pn9CPA9uCON84"
+	},{
+		"time":"2013-03-31T16:21:17.530974Z",
+		"tmst":3512348514,
+		"chan":9,
+		"rfch":1,
+		"freq":869.1,
+		"stat":1,
+		"modu":"FSK",
+		"datr":50000,
+		"rssi":-75,
+		"size":16,
+		"data":"VEVTVF9QQUNLRVRfMTIzNA=="
+	},{
+		"time":"2013-03-31T16:21:17.532038Z",
+		"tmst":3316387610,
+		"chan":0,
+		"rfch":0,
+		"freq":863.00981,
+		"stat":1,
+		"modu":"LORA",
+		"datr":"SF10BW125",
+		"codr":"4/7",
+		"rssi":-38,
+		"lsnr":5.5,
+		"size":32,
+		"data":"ysgRl452xNLep9S1NTIg2lomKDxUgn3DJ7DE+b00Ass"
+	}
+]}
+```
+
+The root object can also contain an object named "stat" :
+
+``` json
+{
+	"rxpk":[ {...}, ...],
+	"stat":{...}
+}
+```
+
+It is possible for a packet to contain no "rxpk" array but a "stat" object.
+
+``` json
+{
+	"stat":{...}
+}
+```
+
+That object contains the status of the gateway, with the following fields:
+
+ Name |  Type  | Function
+:----:|:------:|--------------------------------------------------------------
+ time | string | UTC 'system' time of the gateway, ISO 8601 'expanded' format
+ lati | number | GPS latitude of the gateway in degree (float, N is +)
+ long | number | GPS latitude of the gateway in degree (float, E is +)
+ alti | number | GPS altitude of the gateway in meter RX (integer)
+ rxnb | number | Number of radio packets received (unsigned integer)
+ rxok | number | Number of radio packets received with a valid PHY CRC
+ rxfw | number | Number of radio packets forwarded (unsigned integer)
+ ackr | number | Percentage of upstream datagrams that were acknowledged
+ dwnb | number | Number of downlink datagrams received (unsigned integer)
+ txnb | number | Number of packets emitted (unsigned integer)
+
+Example (white-spaces, indentation and newlines added for readability):
+
+``` json
+{"stat":{
+	"time":"2014-01-12 08:59:28 GMT",
+	"lati":46.24000,
+	"long":3.25230,
+	"alti":145,
+	"rxnb":2,
+	"rxok":2,
+	"rxfw":2,
+	"ackr":100.0,
+	"dwnb":2,
+	"txnb":2
+}}
+```
+
+
+5. Downstream protocol
+-----------------------
+
+### 5.1. Sequence diagram ###
+
+	+---------+                                                    +---------+
+	| Gateway |                                                    | Server  |
+	+---------+                                                    +---------+
+	     | -----------------------------------\                         |
+	     |-| Every N seconds (keepalive time) |                         |
+	     | ------------------------------------                         |
+	     |                                                              |
+	     | PULL_DATA (token Y, MAC@)                                    |
+	     |------------------------------------------------------------->|
+	     |                                                              |
+	     |                                           PULL_ACK (token Y) |
+	     |<-------------------------------------------------------------|
+	     |                                                              |
+
+	+---------+                                                    +---------+
+	| Gateway |                                                    | Server  |
+	+---------+                                                    +---------+
+	     |      ------------------------------------------------------\ |
+	     |      | Anytime after first PULL_DATA for each packet to TX |-|
+	     |      ------------------------------------------------------- |
+	     |                                                              |
+	     |                            PULL_RESP (token Z, JSON payload) |
+	     |<-------------------------------------------------------------|
+	     |                                                              |
+	     | TX_ACK (token Z, JSON payload)                               |
+	     |------------------------------------------------------------->|
+
+### 5.2. PULL_DATA packet ###
+
+That packet type is used by the gateway to poll data from the server.
+
+This data exchange is initialized by the gateway because it might be 
+impossible for the server to send packets to the gateway if the gateway is 
+behind a NAT.
+
+When the gateway initialize the exchange, the network route towards the 
+server will open and will allow for packets to flow both directions.
+The gateway must periodically send PULL_DATA packets to be sure the network 
+route stays open for the server to be used at any time.
+
+ Bytes  | Function
+:------:|---------------------------------------------------------------------
+ 0      | protocol version = 2
+ 1-2    | random token
+ 3      | PULL_DATA identifier 0x02
+ 4-11   | Gateway unique identifier (MAC address)
+
+### 5.3. PULL_ACK packet ###
+
+That packet type is used by the server to confirm that the network route is 
+open and that the server can send PULL_RESP packets at any time.
+
+ Bytes  | Function
+:------:|---------------------------------------------------------------------
+ 0      | protocol version = 2
+ 1-2    | same token as the PULL_DATA packet to acknowledge
+ 3      | PULL_ACK identifier 0x04
+
+### 5.4. PULL_RESP packet ###
+
+That packet type is used by the server to send RF packets and associated 
+metadata that will have to be emitted by the gateway.
+
+ Bytes  | Function
+:------:|---------------------------------------------------------------------
+ 0      | protocol version = 2
+ 1-2    | random token
+ 3      | PULL_RESP identifier 0x03
+ 4-end  | JSON object, starting with {, ending with }, see section 6
+
+### 5.5. TX_ACK packet ###
+
+That packet type is used by the gateway to send a feedback to the server
+to inform if a downlink request has been accepted or rejected by the gateway.
+The datagram may optionnaly contain a JSON string to give more details on
+acknoledge. If no JSON is present (empty string), this means than no error
+occured.
+
+ Bytes  | Function
+:------:|---------------------------------------------------------------------
+ 0      | protocol version = 2
+ 1-2    | same token as the PULL_RESP packet to acknowledge
+ 3      | TX_ACK identifier 0x05
+ 4-11   | Gateway unique identifier (MAC address)
+ 12-end | [optional] JSON object, starting with {, ending with }, see section 6
+
+6. Downstream JSON data structure
+----------------------------------
+
+The root object of PULL_RESP packet must contain an object named "txpk":
+
+``` json
+{
+	"txpk": {...}
+}
+```
+
+That object contain a RF packet to be emitted and associated metadata with the following fields: 
+
+ Name |  Type  | Function
+:----:|:------:|--------------------------------------------------------------
+ imme | bool   | Send packet immediately (will ignore tmst & time)
+ tmst | number | Send packet on a certain timestamp value (will ignore time)
+ time | string | Send packet at a certain time (GPS synchronization required)
+ freq | number | TX central frequency in MHz (unsigned float, Hz precision)
+ rfch | number | Concentrator "RF chain" used for TX (unsigned integer)
+ powe | number | TX output power in dBm (unsigned integer, dBm precision)
+ modu | string | Modulation identifier "LORA" or "FSK"
+ datr | string | LoRa datarate identifier (eg. SF12BW500)
+ datr | number | FSK datarate (unsigned, in bits per second)
+ codr | string | LoRa ECC coding rate identifier
+ fdev | number | FSK frequency deviation (unsigned integer, in Hz) 
+ ipol | bool   | Lora modulation polarization inversion
+ prea | number | RF preamble size (unsigned integer)
+ size | number | RF packet payload size in bytes (unsigned integer)
+ data | string | Base64 encoded RF packet payload, padding optional
+ ncrc | bool   | If true, disable the CRC of the physical layer (optional)
+
+Most fields are optional.
+If a field is omitted, default parameters will be used.
+
+Examples (white-spaces, indentation and newlines added for readability):
+
+``` json
+{"txpk":{
+	"imme":true,
+	"freq":864.123456,
+	"rfch":0,
+	"powe":14,
+	"modu":"LORA",
+	"datr":"SF11BW125",
+	"codr":"4/6",
+	"ipol":false,
+	"size":32,
+	"data":"H3P3N2i9qc4yt7rK7ldqoeCVJGBybzPY5h1Dd7P7p8v"
+}}
+```
+
+``` json
+{"txpk":{
+	"imme":true,
+	"freq":861.3,
+	"rfch":0,
+	"powe":12,
+	"modu":"FSK",
+	"datr":50000,
+	"fdev":3000,
+	"size":32,
+	"data":"H3P3N2i9qc4yt7rK7ldqoeCVJGBybzPY5h1Dd7P7p8v"
+}}
+```
+
+The root object of TX_ACK packet must contain an object named "txpk_ack":
+
+``` json
+{
+	"txpk_ack": {...}
+}
+```
+
+That object contain status information concerning the associated PULL_RESP packet.
+
+ Name |  Type  | Function
+:----:|:------:|------------------------------------------------------------------------------
+error | string | Indication about success or type of failure that occured for downlink request.
+
+The possible values of "error" field are:
+
+ Value             | Definition
+:-----------------:|---------------------------------------------------------------------
+ NONE              | Packet has been programmed for downlink
+ TOO_LATE          | Rejected because it was already too late to program this packet for downlink
+ TOO_EARLY         | Rejected because downlink packet timestamp is too much in advance
+ COLLISION_PACKET  | Rejected because there was already a packet programmed in requested timeframe
+ COLLISION_BEACON  | Rejected because there was already a beacon planned in requested timeframe
+ TX_FREQ           | Rejected because requested frequency is not supported by TX RF chain
+ TX_POWER          | Rejected because requested power is not supported by gateway
+ GPS_UNLOCKED      | Rejected because GPS is unlocked, so GPS timestamp cannot be used
+
+Examples (white-spaces, indentation and newlines added for readability):
+
+``` json
+{"txpk_ack":{
+	"error":"COLLISION_PACKET"
+}}
+```
+
+7. Revisions
+-------------
+
+### v1.3 ###
+
+* Added downlink feedback from gateway to server (PULL_RESP -> TX_ACK)
+
+### v1.2 ###
+
+* Added value of FSK bitrate for upstream.
+* Added parameters for FSK bitrate and frequency deviation for downstream.
+
+### v1.1 ###
+
+* Added syntax for status report JSON object on upstream.
+
+### v1.0 ###
+
+* Initial version.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/VERSION	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,1 @@
+0.1.0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/daemon/run_pkt_fwd.sh	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,74 @@
+#!/bin/bash
+
+usage() {
+    echo missing working root directory path
+	echo usage: $0 [start/restart/check/stop] PATH
+    echo example: $0 check /home/pi/lora-net/
+    exit
+}
+
+#
+# Check input parameters
+#
+if [ -z "$2" ]; then 
+    usage
+fi
+
+#
+# Global variables
+#
+DIR=$2
+
+#
+# Functions
+#
+start() {
+    echo "Start packet forwarder..."
+    cd $DIR/lora_gateway
+    ./reset_lgw.sh start
+    cd $DIR/packet_forwarder/lora_pkt_fwd
+    ./lora_pkt_fwd
+}
+
+stop() {
+    echo "Stop packet forwarder"
+    sudo killall lora_pkt_fwd 
+}
+
+check() {
+    ps -ef | grep -v grep | grep -w 'lora_pkt_fwd' > /dev/null
+    result=$?
+    if [ "${result}" -eq "0" ] ; then
+         echo "`date`: lora_pkt_fwd is already running"
+         exit 0
+    fi
+    start
+}
+
+#
+# Main
+#
+
+case "$1" in
+    start)
+        start
+        ;;
+    stop)
+        stop
+        ;;
+    restart)
+        stop
+        start
+        ;;
+    check)
+        check
+        ;;
+    *)
+        usage
+        exit 1
+        ;;
+esac
+
+exit 0
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/Makefile	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,66 @@
+### Application-specific constants
+
+APP_NAME := lora_pkt_fwd
+
+### Environment constants 
+
+LGW_PATH ?= ../../lora_gateway/libloragw
+ARCH ?=
+CROSS_COMPILE ?=
+
+OBJDIR = obj
+INCLUDES = $(wildcard inc/*.h)
+
+### External constant definitions
+# must get library build option to know if mpsse must be linked or not
+
+include $(LGW_PATH)/library.cfg
+RELEASE_VERSION := `cat ../VERSION`
+
+### Constant symbols
+
+CC := $(CROSS_COMPILE)gcc
+AR := $(CROSS_COMPILE)ar
+
+CFLAGS := -O2 -Wall -Wextra -std=c99 -Iinc -I.
+VFLAG := -D VERSION_STRING="\"$(RELEASE_VERSION)\""
+
+### Constants for Lora concentrator HAL library
+# List the library sub-modules that are used by the application
+
+LGW_INC =
+ifneq ($(wildcard $(LGW_PATH)/inc/config.h),)
+  # only for HAL version 1.3 and beyond
+  LGW_INC += $(LGW_PATH)/inc/config.h
+endif
+LGW_INC += $(LGW_PATH)/inc/loragw_hal.h
+
+### Linking options
+
+LIBS := -lloragw -lrt -lpthread -lm
+
+### General build targets
+
+all: $(APP_NAME)
+
+clean:
+	rm -f $(OBJDIR)/*.o
+	rm -f $(APP_NAME)
+
+### Sub-modules compilation
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(OBJDIR)/%.o: src/%.c $(INCLUDES) | $(OBJDIR)
+	$(CC) -c $(CFLAGS) -I$(LGW_PATH)/inc $< -o $@
+
+### Main program compilation and assembly
+
+$(OBJDIR)/$(APP_NAME).o: src/$(APP_NAME).c $(LGW_INC) $(INCLUDES) | $(OBJDIR)
+	$(CC) -c $(CFLAGS) $(VFLAG) -I$(LGW_PATH)/inc $< -o $@
+
+$(APP_NAME): $(OBJDIR)/$(APP_NAME).o $(LGW_PATH)/libloragw.a $(OBJDIR)/parson.o $(OBJDIR)/base64.o $(OBJDIR)/jitqueue.o $(OBJDIR)/timersync.o
+	$(CC) -L$(LGW_PATH) $< $(OBJDIR)/parson.o $(OBJDIR)/base64.o $(OBJDIR)/jitqueue.o $(OBJDIR)/timersync.o -o $@ $(LIBS)
+
+### EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/cfg/global_conf_PicoV1p0_EU.json	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,216 @@
+{
+    "SX1301_conf": {
+        "lorawan_public": true,
+        "clksrc": 1, /* radio_1 provides clock to concentrator */
+        "antenna_gain": 0, /* antenna gain, in dBi */
+        "radio_0": {
+            "enable": true,
+            "type": "SX1257",
+            "freq": 867500000,
+            "rssi_offset": -164.0,
+            "tx_enable": true,
+            "tx_freq_min": 863000000,
+            "tx_freq_max": 870000000
+        },
+        "radio_1": {
+            "enable": true,
+            "type": "SX1257",
+            "freq": 868500000,
+            "rssi_offset": -164.0,
+            "tx_enable": false
+        },
+        "chan_multiSF_0": {
+            /* Lora MAC channel, 125kHz, all SF, 868.1 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -400000
+        },
+        "chan_multiSF_1": {
+            /* Lora MAC channel, 125kHz, all SF, 868.3 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -200000
+        },
+        "chan_multiSF_2": {
+            /* Lora MAC channel, 125kHz, all SF, 868.5 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": 0
+        },
+        "chan_multiSF_3": {
+            /* Lora MAC channel, 125kHz, all SF, 867.1 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": -400000
+        },
+        "chan_multiSF_4": {
+            /* Lora MAC channel, 125kHz, all SF, 867.3 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": -200000
+        },
+        "chan_multiSF_5": {
+            /* Lora MAC channel, 125kHz, all SF, 867.5 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 0
+        },
+        "chan_multiSF_6": {
+            /* Lora MAC channel, 125kHz, all SF, 867.7 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 200000
+        },
+        "chan_multiSF_7": {
+            /* Lora MAC channel, 125kHz, all SF, 867.9 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 400000
+        },
+        "chan_Lora_std": {
+            /* Lora MAC channel, 250kHz, SF7, 868.3 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -200000,
+            "bandwidth": 250000,
+            "spread_factor": 7
+        },
+        "chan_FSK": {
+            /* FSK 50kbps channel, 868.8 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": 300000,
+            "bandwidth": 125000,
+            "datarate": 50000
+        },
+		"tx_lut_0": {
+			/* TX gain table, index 0 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_1": {
+			/* TX gain table, index 1 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_2": {
+			/* TX gain table, index 2 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_3": {
+			/* TX gain table, index 3 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_4": {
+			/* TX gain table, index 4 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_5": {
+			/* TX gain table, index 5 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_6": {
+			/* TX gain table, index 6 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_7": {
+			/* TX gain table, index 7 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 11,
+			"dig_gain": 3
+		},
+		"tx_lut_8": {
+			/* TX gain table, index 8 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 13,
+			"dig_gain": 2
+		},
+		"tx_lut_9": {
+			/* TX gain table, index 9 */
+			"pa_gain": 0,
+			"mix_gain": 8,
+			"rf_power": 14,
+			"dig_gain": 3
+		},
+		"tx_lut_10": {
+			/* TX gain table, index 10 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 15,
+			"dig_gain": 2
+		},
+		"tx_lut_11": {
+			/* TX gain table, index 11 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 16,
+			"dig_gain": 1
+		},
+		"tx_lut_12": {
+			/* TX gain table, index 12 */
+			"pa_gain": 0,
+			"mix_gain": 9,
+			"rf_power": 17,
+			"dig_gain": 3
+		},
+		"tx_lut_13": {
+			/* TX gain table, index 13 */
+			"pa_gain": 0,
+			"mix_gain": 10,
+			"rf_power": 18,
+			"dig_gain": 3
+		},
+		"tx_lut_14": {
+			/* TX gain table, index 14 */
+			"pa_gain": 0,
+			"mix_gain": 11,
+			"rf_power": 19,
+			"dig_gain": 3
+		},
+		"tx_lut_15": {
+			/* TX gain table, index 15 */
+			"pa_gain": 0,
+			"mix_gain": 12,
+			"rf_power": 20,
+			"dig_gain": 3
+		}		
+    },
+
+    "gateway_conf": {
+        "gateway_ID": "AA555A0000240409",
+        /* change with default server address/ports, or overwrite in local_conf.json */
+        "server_address": "localhost",
+        "serv_port_up": 1680,
+        "serv_port_down": 1680,
+        /* adjust the following parameters for your network */
+        "keepalive_interval": 10,
+        "stat_interval": 30,
+        "push_timeout_ms": 100,
+        /* forward only valid packets */
+        "forward_crc_valid": true,
+        "forward_crc_error": false,
+        "forward_crc_disabled": false
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/cfg/global_conf_PicoV1p0_US.json	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,216 @@
+{
+    "SX1301_conf": {
+        "lorawan_public": true,
+        "clksrc": 1, /* radio_1 provides clock to concentrator */
+        "antenna_gain": 0, /* antenna gain, in dBi */
+        "radio_0": {
+            "enable": true,
+            "type": "SX1257",
+            "freq": 902700000,
+            "rssi_offset": -164.0,
+            "tx_enable": true,
+            "tx_freq_min": 902000000,
+            "tx_freq_max": 928000000
+        },
+        "radio_1": {
+            "enable": true,
+            "type": "SX1257",
+            "freq": 903400000,
+            "rssi_offset": -164.0,
+            "tx_enable": false
+        },
+        "chan_multiSF_0": {
+            /* Lora MAC channel, 125kHz, all SF, 902.3 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": -400000
+        },
+        "chan_multiSF_1": {
+            /* Lora MAC channel, 125kHz, all SF, 902.5 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": -200000
+        },
+        "chan_multiSF_2": {
+            /* Lora MAC channel, 125kHz, all SF, 902.7 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 0
+        },
+        "chan_multiSF_3": {
+            /* Lora MAC channel, 125kHz, all SF, 902.9 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 200000
+        },
+        "chan_multiSF_4": {
+            /* Lora MAC channel, 125kHz, all SF, 903.1 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -300000
+        },
+        "chan_multiSF_5": {
+            /* Lora MAC channel, 125kHz, all SF, 903.3 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -100000
+        },
+        "chan_multiSF_6": {
+            /* Lora MAC channel, 125kHz, all SF, 903.5 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": 100000
+        },
+        "chan_multiSF_7": {
+            /* Lora MAC channel, 125kHz, all SF, 903.7 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": 300000
+        },
+        "chan_Lora_std": {
+            /* Lora MAC channel, 500kHz, SF8, 903.0 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 300000,
+            "bandwidth": 500000,
+            "spread_factor": 8
+        },
+        "chan_FSK": {
+            /* FSK 100kbps channel, 903.0 MHz */
+            "enable": false,
+            "radio": 0,
+            "if": 300000,
+            "bandwidth": 250000,
+            "datarate": 100000
+        },
+		"tx_lut_0": {
+			/* TX gain table, index 0 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_1": {
+			/* TX gain table, index 1 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_2": {
+			/* TX gain table, index 2 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_3": {
+			/* TX gain table, index 3 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_4": {
+			/* TX gain table, index 4 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_5": {
+			/* TX gain table, index 5 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_6": {
+			/* TX gain table, index 6 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 11,
+			"dig_gain": 3
+		},
+		"tx_lut_7": {
+			/* TX gain table, index 7 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 13,
+			"dig_gain": 2
+		},
+		"tx_lut_8": {
+			/* TX gain table, index 8 */
+			"pa_gain": 0,
+			"mix_gain": 8,
+			"rf_power": 14,
+			"dig_gain": 3
+		},
+		"tx_lut_9": {
+			/* TX gain table, index 9 */
+			"pa_gain": 0,
+			"mix_gain": 10,
+			"rf_power": 15,
+			"dig_gain": 2
+		},
+		"tx_lut_10": {
+			/* TX gain table, index 10 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 16,
+			"dig_gain": 1
+		},
+		"tx_lut_11": {
+			/* TX gain table, index 11 */
+			"pa_gain": 0,
+			"mix_gain": 9,
+			"rf_power": 17,
+			"dig_gain": 3
+		},
+		"tx_lut_12": {
+			/* TX gain table, index 12 */
+			"pa_gain": 0,
+			"mix_gain": 10,
+			"rf_power": 18,
+			"dig_gain": 3
+		},
+		"tx_lut_13": {
+			/* TX gain table, index 13 */
+			"pa_gain": 0,
+			"mix_gain": 11,
+			"rf_power": 19,
+			"dig_gain": 3
+		},
+		"tx_lut_14": {
+			/* TX gain table, index 14 */
+			"pa_gain": 0,
+			"mix_gain": 12,
+			"rf_power": 20,
+			"dig_gain": 3
+		},
+		"tx_lut_15": {
+			/* TX gain table, index 15 */
+			"pa_gain": 0,
+			"mix_gain": 13,
+			"rf_power": 21,
+			"dig_gain": 2
+		}		
+    },
+
+    "gateway_conf": {
+        "gateway_ID": "AA555A0000240409",
+        /* change with default server address/ports, or overwrite in local_conf.json */
+        "server_address": "localhost",
+        "serv_port_up": 1680,
+        "serv_port_down": 1680,
+        /* adjust the following parameters for your network */
+        "keepalive_interval": 10,
+        "stat_interval": 30,
+        "push_timeout_ms": 100,
+        /* forward only valid packets */
+        "forward_crc_valid": true,
+        "forward_crc_error": false,
+        "forward_crc_disabled": false
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/global_conf.json	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,216 @@
+{
+    "SX1301_conf": {
+        "lorawan_public": true,
+        "clksrc": 1, /* radio_1 provides clock to concentrator */
+        "antenna_gain": 0, /* antenna gain, in dBi */
+        "radio_0": {
+            "enable": true,
+            "type": "SX1257",
+            "freq": 867500000,
+            "rssi_offset": -164.0,
+            "tx_enable": true,
+            "tx_freq_min": 863000000,
+            "tx_freq_max": 870000000
+        },
+        "radio_1": {
+            "enable": true,
+            "type": "SX1257",
+            "freq": 868500000,
+            "rssi_offset": -164.0,
+            "tx_enable": false
+        },
+        "chan_multiSF_0": {
+            /* Lora MAC channel, 125kHz, all SF, 868.1 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -400000
+        },
+        "chan_multiSF_1": {
+            /* Lora MAC channel, 125kHz, all SF, 868.3 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -200000
+        },
+        "chan_multiSF_2": {
+            /* Lora MAC channel, 125kHz, all SF, 868.5 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": 0
+        },
+        "chan_multiSF_3": {
+            /* Lora MAC channel, 125kHz, all SF, 867.1 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": -400000
+        },
+        "chan_multiSF_4": {
+            /* Lora MAC channel, 125kHz, all SF, 867.3 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": -200000
+        },
+        "chan_multiSF_5": {
+            /* Lora MAC channel, 125kHz, all SF, 867.5 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 0
+        },
+        "chan_multiSF_6": {
+            /* Lora MAC channel, 125kHz, all SF, 867.7 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 200000
+        },
+        "chan_multiSF_7": {
+            /* Lora MAC channel, 125kHz, all SF, 867.9 MHz */
+            "enable": true,
+            "radio": 0,
+            "if": 400000
+        },
+        "chan_Lora_std": {
+            /* Lora MAC channel, 250kHz, SF7, 868.3 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": -200000,
+            "bandwidth": 250000,
+            "spread_factor": 7
+        },
+        "chan_FSK": {
+            /* FSK 50kbps channel, 868.8 MHz */
+            "enable": true,
+            "radio": 1,
+            "if": 300000,
+            "bandwidth": 125000,
+            "datarate": 50000
+        },
+		"tx_lut_0": {
+			/* TX gain table, index 0 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_1": {
+			/* TX gain table, index 1 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_2": {
+			/* TX gain table, index 2 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_3": {
+			/* TX gain table, index 3 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_4": {
+			/* TX gain table, index 4 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_5": {
+			/* TX gain table, index 5 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_6": {
+			/* TX gain table, index 6 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 9,
+			"dig_gain": 3
+		},
+		"tx_lut_7": {
+			/* TX gain table, index 7 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 11,
+			"dig_gain": 3
+		},
+		"tx_lut_8": {
+			/* TX gain table, index 8 */
+			"pa_gain": 0,
+			"mix_gain": 5,
+			"rf_power": 13,
+			"dig_gain": 2
+		},
+		"tx_lut_9": {
+			/* TX gain table, index 9 */
+			"pa_gain": 0,
+			"mix_gain": 8,
+			"rf_power": 14,
+			"dig_gain": 3
+		},
+		"tx_lut_10": {
+			/* TX gain table, index 10 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 15,
+			"dig_gain": 2
+		},
+		"tx_lut_11": {
+			/* TX gain table, index 11 */
+			"pa_gain": 0,
+			"mix_gain": 6,
+			"rf_power": 16,
+			"dig_gain": 1
+		},
+		"tx_lut_12": {
+			/* TX gain table, index 12 */
+			"pa_gain": 0,
+			"mix_gain": 9,
+			"rf_power": 17,
+			"dig_gain": 3
+		},
+		"tx_lut_13": {
+			/* TX gain table, index 13 */
+			"pa_gain": 0,
+			"mix_gain": 10,
+			"rf_power": 18,
+			"dig_gain": 3
+		},
+		"tx_lut_14": {
+			/* TX gain table, index 14 */
+			"pa_gain": 0,
+			"mix_gain": 11,
+			"rf_power": 19,
+			"dig_gain": 3
+		},
+		"tx_lut_15": {
+			/* TX gain table, index 15 */
+			"pa_gain": 0,
+			"mix_gain": 12,
+			"rf_power": 20,
+			"dig_gain": 3
+		}		
+    },
+
+    "gateway_conf": {
+        "gateway_ID": "AA555A0000240409",
+        /* change with default server address/ports, or overwrite in local_conf.json */
+        "server_address": "localhost",
+        "serv_port_up": 1680,
+        "serv_port_down": 1680,
+        /* adjust the following parameters for your network */
+        "keepalive_interval": 10,
+        "stat_interval": 30,
+        "push_timeout_ms": 100,
+        /* forward only valid packets */
+        "forward_crc_valid": true,
+        "forward_crc_error": false,
+        "forward_crc_disabled": false
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/inc/base64.h	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,62 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Base64 encoding & decoding library
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+#ifndef _BASE64_H
+#define _BASE64_H
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <stdint.h>        /* C99 types */
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS PROTOTYPES ------------------------------------------ */
+
+/**
+@brief Encode binary data in Base64 string (no padding)
+@param in pointer to a table of binary data
+@param size number of bytes to be encoded to base64
+@param out pointer to a string where the function will output encoded data
+@param max_len max length of the out string (including null char)
+@return >=0 length of the resulting string (w/o null char), -1 for error
+*/
+int bin_to_b64_nopad(const uint8_t * in, int size, char * out, int max_len);
+
+/**
+@brief Decode Base64 string to binary data (no padding)
+@param in string containing only base64 valid characters
+@param size number of characters to be decoded from base64 (w/o null char)
+@param out pointer to a data buffer where the function will output decoded data
+@param out_max_len usable size of the output data buffer
+@return >=0 number of bytes written to the data buffer, -1 for error
+*/
+int b64_to_bin_nopad(const char * in, int size, uint8_t * out, int max_len);
+
+/* === derivative functions === */
+
+/**
+@brief Encode binary data in Base64 string (with added padding)
+*/
+int bin_to_b64(const uint8_t * in, int size, char * out, int max_len);
+
+/**
+@brief Decode Base64 string to binary data (remove padding if necessary)
+*/
+int b64_to_bin(const char * in, int size, uint8_t * out, int max_len);
+
+#endif
+
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/inc/jitqueue.h	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,155 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    LoRa concentrator : Just In Time TX scheduling queue
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Michael Coracin
+*/
+
+
+#ifndef _LORA_PKTFWD_JIT_H
+#define _LORA_PKTFWD_JIT_H
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <stdint.h>     /* C99 types */
+#include <stdbool.h>    /* bool type */
+#include <sys/time.h>   /* timeval */
+
+#include "loragw_hal.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC CONSTANTS ----------------------------------------------------- */
+
+#define JIT_QUEUE_MAX           32  /* Maximum number of packets to be stored in JiT queue */
+#define JIT_NUM_BEACON_IN_QUEUE 3   /* Number of beacons to be loaded in JiT queue at any time */
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC TYPES --------------------------------------------------------- */
+
+enum jit_pkt_type_e {
+    JIT_PKT_TYPE_DOWNLINK_CLASS_A,
+    JIT_PKT_TYPE_DOWNLINK_CLASS_B,
+    JIT_PKT_TYPE_DOWNLINK_CLASS_C,
+    JIT_PKT_TYPE_BEACON
+};
+
+enum jit_error_e {
+    JIT_ERROR_OK,           /* Packet ok to be sent */
+    JIT_ERROR_TOO_LATE,     /* Too late to send this packet */
+    JIT_ERROR_TOO_EARLY,    /* Too early to queue this packet */
+    JIT_ERROR_FULL,         /* Downlink queue is full */
+    JIT_ERROR_EMPTY,        /* Downlink queue is empty */
+    JIT_ERROR_COLLISION_PACKET, /* A packet is already enqueued for this timeframe */
+    JIT_ERROR_COLLISION_BEACON, /* A beacon is planned for this timeframe */
+    JIT_ERROR_TX_FREQ,      /* The required frequency for downlink is not supported */
+    JIT_ERROR_TX_POWER,     /* The required power for downlink is not supported */
+    JIT_ERROR_GPS_UNLOCKED, /* GPS timestamp could not be used as GPS is unlocked */
+    JIT_ERROR_INVALID       /* Packet is invalid */
+};
+
+struct jit_node_s {
+    /* API fields */
+    struct lgw_pkt_tx_s pkt;        /* TX packet */
+    enum jit_pkt_type_e pkt_type;   /* Packet type: Downlink, Beacon... */
+
+    /* Internal fields */
+    uint32_t pre_delay;             /* Amount of time before packet timestamp to be reserved */
+    uint32_t post_delay;            /* Amount of time after packet timestamp to be reserved (time on air) */
+};
+
+struct jit_queue_s {
+    uint8_t num_pkt;                /* Total number of packets in the queue (downlinks, beacons...) */
+    uint8_t num_beacon;             /* Number of beacons in the queue */
+    struct jit_node_s nodes[JIT_QUEUE_MAX]; /* Nodes/packets array in the queue */
+};
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS PROTOTYPES ------------------------------------------ */
+
+/**
+@brief Check if a JiT queue is full.
+
+@param queue[in] Just in Time queue to be checked.
+@return true if queue is full, false otherwise.
+*/
+bool jit_queue_is_full(struct jit_queue_s *queue);
+
+/**
+@brief Check if a JiT queue is empty.
+
+@param queue[in] Just in Time queue to be checked.
+@return true if queue is empty, false otherwise.
+*/
+bool jit_queue_is_empty(struct jit_queue_s *queue);
+
+/**
+@brief Initialize a Just in Time queue.
+
+@param queue[in] Just in Time queue to be initialized. Memory should have been allocated already.
+
+This function is used to reset every elements in the allocated queue.
+*/
+void jit_queue_init(struct jit_queue_s *queue);
+
+/**
+@brief Add a packet in a Just-in-Time queue
+
+@param queue[in/out] Just in Time queue in which the packet should be inserted
+@param time[in] Current concentrator time
+@param packet[in] Packet to be queued in JiT queue
+@param pkt_type[in] Type of packet to be queued: Downlink, Beacon
+@return success if the function was able to queue the packet
+
+This function is typically used when a packet is received from server for downlink.
+It will check if packet can be queued, with several criterias. Once the packet is queued, it has to be
+sent over the air. So all checks should happen before the packet being actually in the queue.
+*/
+enum jit_error_e jit_enqueue(struct jit_queue_s *queue, struct timeval *time, struct lgw_pkt_tx_s *packet, enum jit_pkt_type_e pkt_type);
+
+/**
+@brief Dequeue a packet from a Just-in-Time queue
+
+@param queue[in/out] Just in Time queue from which the packet should be removed
+@param index[in] in the queue where to get the packet to be removed
+@param packet[out] that was at index
+@param pkt_type[out] Type of packet dequeued: Downlink, Beacon
+@return success if the function was able to dequeue the packet
+
+This function is typically used when a packet is about to be placed on concentrator buffer for TX.
+The index is generally got using the jit_peek function.
+*/
+enum jit_error_e jit_dequeue(struct jit_queue_s *queue, int index, struct lgw_pkt_tx_s *packet, enum jit_pkt_type_e *pkt_type);
+
+/**
+@brief Check if there is a packet soon to be sent from the JiT queue.
+
+@param queue[in] Just in Time queue to parse for peeking a packet
+@param time[in] Current concentrator time
+@param pkt_idx[out] Packet index which is soon to be dequeued.
+@return success if the function was able to parse the queue. pkt_idx is set to -1 if no packet found.
+
+This function is typically used to check in JiT queue if there is a packet soon to be sent.
+It search the packet with the highest priority in queue, and check if its timestamp is near
+enough the current concentrator time.
+*/
+enum jit_error_e jit_peek(struct jit_queue_s *queue, struct timeval *time, int *pkt_idx);
+
+/**
+@brief Debug function to print the queue's content on console
+
+@param queue[in] Just in Time queue to be displayed
+@param show_all[in] Indicates if empty nodes have to be displayed or not
+*/
+void jit_print_queue(struct jit_queue_s *queue, bool show_all, int debug_level);
+
+#endif
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/inc/parson.h	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,222 @@
+/*
+ Parson ( http://kgabis.github.com/parson/ )
+ Copyright (c) 2012 - 2016 Krzysztof Gabis
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+
+#ifndef parson_parson_h
+#define parson_parson_h
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include <stddef.h>   /* size_t */
+
+/* Types and enums */
+typedef struct json_object_t JSON_Object;
+typedef struct json_array_t  JSON_Array;
+typedef struct json_value_t  JSON_Value;
+
+enum json_value_type {
+    JSONError   = -1,
+    JSONNull    = 1,
+    JSONString  = 2,
+    JSONNumber  = 3,
+    JSONObject  = 4,
+    JSONArray   = 5,
+    JSONBoolean = 6
+};
+typedef int JSON_Value_Type;
+
+enum json_result_t {
+    JSONSuccess = 0,
+    JSONFailure = -1
+};
+typedef int JSON_Status;
+
+typedef void * (*JSON_Malloc_Function)(size_t);
+typedef void   (*JSON_Free_Function)(void *);
+
+/* Call only once, before calling any other function from parson API. If not called, malloc and free
+   from stdlib will be used for all allocations */
+void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun);
+
+/* Parses first JSON value in a file, returns NULL in case of error */
+JSON_Value * json_parse_file(const char *filename);
+
+/* Parses first JSON value in a file and ignores comments (/ * * / and //),
+   returns NULL in case of error */
+JSON_Value * json_parse_file_with_comments(const char *filename);
+
+/*  Parses first JSON value in a string, returns NULL in case of error */
+JSON_Value * json_parse_string(const char *string);
+
+/*  Parses first JSON value in a string and ignores comments (/ * * / and //),
+    returns NULL in case of error */
+JSON_Value * json_parse_string_with_comments(const char *string);
+
+/* Serialization */
+size_t      json_serialization_size(const JSON_Value *value); /* returns 0 on fail */
+JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes);
+JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename);
+char *      json_serialize_to_string(const JSON_Value *value);
+
+/* Pretty serialization */
+size_t      json_serialization_size_pretty(const JSON_Value *value); /* returns 0 on fail */
+JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes);
+JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename);
+char *      json_serialize_to_string_pretty(const JSON_Value *value);
+
+void        json_free_serialized_string(char *string); /* frees string from json_serialize_to_string and json_serialize_to_string_pretty */
+
+/* Comparing */
+int  json_value_equals(const JSON_Value *a, const JSON_Value *b);
+
+/* Validation
+   This is *NOT* JSON Schema. It validates json by checking if object have identically
+   named fields with matching types.
+   For example schema {"name":"", "age":0} will validate
+   {"name":"Joe", "age":25} and {"name":"Joe", "age":25, "gender":"m"},
+   but not {"name":"Joe"} or {"name":"Joe", "age":"Cucumber"}.
+   In case of arrays, only first value in schema is checked against all values in tested array.
+   Empty objects ({}) validate all objects, empty arrays ([]) validate all arrays,
+   null validates values of every type.
+ */
+JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value);
+
+/*
+ * JSON Object
+ */
+JSON_Value  * json_object_get_value  (const JSON_Object *object, const char *name);
+const char  * json_object_get_string (const JSON_Object *object, const char *name);
+JSON_Object * json_object_get_object (const JSON_Object *object, const char *name);
+JSON_Array  * json_object_get_array  (const JSON_Object *object, const char *name);
+double        json_object_get_number (const JSON_Object *object, const char *name); /* returns 0 on fail */
+int           json_object_get_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */
+
+/* dotget functions enable addressing values with dot notation in nested objects,
+ just like in structs or c++/java/c# objects (e.g. objectA.objectB.value).
+ Because valid names in JSON can contain dots, some values may be inaccessible
+ this way. */
+JSON_Value  * json_object_dotget_value  (const JSON_Object *object, const char *name);
+const char  * json_object_dotget_string (const JSON_Object *object, const char *name);
+JSON_Object * json_object_dotget_object (const JSON_Object *object, const char *name);
+JSON_Array  * json_object_dotget_array  (const JSON_Object *object, const char *name);
+double        json_object_dotget_number (const JSON_Object *object, const char *name); /* returns 0 on fail */
+int           json_object_dotget_boolean(const JSON_Object *object, const char *name); /* returns -1 on fail */
+
+/* Functions to get available names */
+size_t        json_object_get_count(const JSON_Object *object);
+const char  * json_object_get_name (const JSON_Object *object, size_t index);
+
+/* Creates new name-value pair or frees and replaces old value with a new one.
+ * json_object_set_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value);
+JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string);
+JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number);
+JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean);
+JSON_Status json_object_set_null(JSON_Object *object, const char *name);
+
+/* Works like dotget functions, but creates whole hierarchy if necessary.
+ * json_object_dotset_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value);
+JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string);
+JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number);
+JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean);
+JSON_Status json_object_dotset_null(JSON_Object *object, const char *name);
+
+/* Frees and removes name-value pair */
+JSON_Status json_object_remove(JSON_Object *object, const char *name);
+
+/* Works like dotget function, but removes name-value pair only on exact match. */
+JSON_Status json_object_dotremove(JSON_Object *object, const char *key);
+
+/* Removes all name-value pairs in object */
+JSON_Status json_object_clear(JSON_Object *object);
+
+/*
+ *JSON Array
+ */
+JSON_Value  * json_array_get_value  (const JSON_Array *array, size_t index);
+const char  * json_array_get_string (const JSON_Array *array, size_t index);
+JSON_Object * json_array_get_object (const JSON_Array *array, size_t index);
+JSON_Array  * json_array_get_array  (const JSON_Array *array, size_t index);
+double        json_array_get_number (const JSON_Array *array, size_t index); /* returns 0 on fail */
+int           json_array_get_boolean(const JSON_Array *array, size_t index); /* returns -1 on fail */
+size_t        json_array_get_count  (const JSON_Array *array);
+
+/* Frees and removes value at given index, does nothing and returns JSONFailure if index doesn't exist.
+ * Order of values in array may change during execution.  */
+JSON_Status json_array_remove(JSON_Array *array, size_t i);
+
+/* Frees and removes from array value at given index and replaces it with given one.
+ * Does nothing and returns JSONFailure if index doesn't exist.
+ * json_array_replace_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_array_replace_value(JSON_Array *array, size_t i, JSON_Value *value);
+JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string);
+JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number);
+JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean);
+JSON_Status json_array_replace_null(JSON_Array *array, size_t i);
+
+/* Frees and removes all values from array */
+JSON_Status json_array_clear(JSON_Array *array);
+
+/* Appends new value at the end of array.
+ * json_array_append_value does not copy passed value so it shouldn't be freed afterwards. */
+JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value);
+JSON_Status json_array_append_string(JSON_Array *array, const char *string);
+JSON_Status json_array_append_number(JSON_Array *array, double number);
+JSON_Status json_array_append_boolean(JSON_Array *array, int boolean);
+JSON_Status json_array_append_null(JSON_Array *array);
+
+/*
+ *JSON Value
+ */
+JSON_Value * json_value_init_object (void);
+JSON_Value * json_value_init_array  (void);
+JSON_Value * json_value_init_string (const char *string); /* copies passed string */
+JSON_Value * json_value_init_number (double number);
+JSON_Value * json_value_init_boolean(int boolean);
+JSON_Value * json_value_init_null   (void);
+JSON_Value * json_value_deep_copy   (const JSON_Value *value);
+void         json_value_free        (JSON_Value *value);
+
+JSON_Value_Type json_value_get_type   (const JSON_Value *value);
+JSON_Object *   json_value_get_object (const JSON_Value *value);
+JSON_Array  *   json_value_get_array  (const JSON_Value *value);
+const char  *   json_value_get_string (const JSON_Value *value);
+double          json_value_get_number (const JSON_Value *value);
+int             json_value_get_boolean(const JSON_Value *value);
+
+/* Same as above, but shorter */
+JSON_Value_Type json_type   (const JSON_Value *value);
+JSON_Object *   json_object (const JSON_Value *value);
+JSON_Array  *   json_array  (const JSON_Value *value);
+const char  *   json_string (const JSON_Value *value);
+double          json_number (const JSON_Value *value);
+int             json_boolean(const JSON_Value *value);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/inc/timersync.h	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,32 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    LoRa concentrator : Just In Time TX scheduling queue
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Michael Coracin
+*/
+
+
+#ifndef _LORA_PKTFWD_TIMERSYNC_H
+#define _LORA_PKTFWD_TIMERSYNC_H
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <sys/time.h>    /* timeval */
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS PROTOTYPES ------------------------------------------ */
+
+int get_concentrator_time(struct timeval *concent_time, struct timeval unix_time);
+
+void thread_timersync(void);
+
+#endif
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/inc/trace.h	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,37 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    LoRa concentrator : Packet Forwarder trace helpers
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Michael Coracin
+*/
+
+
+#ifndef _LORA_PKTFWD_TRACE_H
+#define _LORA_PKTFWD_TRACE_H
+
+#define DEBUG_PKT_FWD   0
+#define DEBUG_JIT       0
+#define DEBUG_JIT_ERROR 1
+#define DEBUG_TIMERSYNC 0
+#define DEBUG_BEACON    0
+#define DEBUG_LOG       1
+
+#define MSG(args...) printf(args) /* message that is destined to the user */
+#define MSG_DEBUG(FLAG, fmt, ...)                                                                         \
+            do  {                                                                                         \
+                if (FLAG)                                                                                 \
+                    fprintf(stdout, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__); \
+            } while (0)
+
+
+
+#endif
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/readme.md	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,328 @@
+	 / _____)             _              | |    
+	( (____  _____ ____ _| |_ _____  ____| |__  
+	 \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
+	 _____) ) ____| | | || |_| ____( (___| | | |
+	(______/|_____)_|_|_| \__)_____)\____)_| |_|
+	  (C)2013 Semtech-Cycleo
+
+Lora Gateway packet forwarder
+=============================
+
+1. Introduction
+----------------
+
+The packet forwarder is a program running on the host of a Lora gateway that
+forwards RF packets receive by the concentrator to a server through a IP/UDP
+link, and emits RF packets that are sent by the server. It can also emit a
+network-wide GPS-synchronous beacon signal used for coordinating all nodes of
+the network.
+
+To learn more about the network protocol between the gateway and the server, 
+please read the PROTOCOL.TXT document.
+
+2. System schematic and definitions
+------------------------------------
+
+	((( Y )))
+	    |
+	    |
+	+- -|- - - - - - - - - - - - -+        xxxxxxxxxxxx          +--------+
+	|+--+-----------+     +------+|       xx x  x     xxx        |        |
+	||              |     |      ||      xx  Internet  xx        |        |
+	|| Concentrator |<----+ Host |<------xx     or    xx-------->|        |
+	||              | SPI |      ||      xx  Intranet  xx        | Server |
+	|+--------------+     +------+|       xxxx   x   xxxx        |        |
+	|   ^                    ^    |           xxxxxxxx           |        |
+	|   | PPS  +-----+  NMEA |    |                              |        |
+	|   +------| GPS |-------+    |                              +--------+
+	|          +-----+            |
+	|                             |
+	|            Gateway          |
+	+- - - - - - - - - - - - - - -+
+
+Concentrator: radio RX/TX board, based on Semtech multichannel modems (SX130x), 
+transceivers (SX135x) and/or low-power stand-alone modems (SX127x).
+
+Host: embedded computer on which the packet forwarder is run. Drives the 
+concentrator through a SPI link.
+
+Gateway: a device composed of at least one radio concentrator, a host, some 
+network connection to the internet or a private network (Ethernet, 3G, Wifi, 
+microwave link), and optionally a GPS receiver for synchronization. 
+
+Server: an abstract computer that will process the RF packets received and 
+forwarded by the gateway, and issue RF packets in response that the gateway 
+will have to emit.
+
+
+3. Dependencies
+----------------
+
+This program uses the Parson library (http://kgabis.github.com/parson/) by
+Krzysztof Gabis for JSON parsing.
+Many thanks to him for that very practical and well written library.
+
+This program is statically linked with the libloragw Lora concentrator library.
+It was tested with v1.3.0 of the library but should work with any later 
+version provided the API is v1 or a later backward-compatible API.
+Data structures of the received packets are accessed by name (ie. not at a
+binary level) so new functionalities can be added to the API without affecting
+that program at all.
+
+This program follows the v1.3 version of the gateway-to-server protocol.
+
+The last dependency is the hardware concentrator (based on FPGA or SX130x 
+chips) that must be matched with the proper version of the HAL.
+
+4. Usage
+---------
+
+* Pick the global_conf.json file from cfg/ directory that fit with your
+platform, region and feature need.
+* Update the JSON configuration (global and local) files, as explained below.
+* For IoT Starter Kit only, run:
+    ./reset_lgw.sh stop
+    ./reset_lgw.sh start
+* Run:
+    ./update_gwid.sh local_conf.json    (OPTIONAL)
+    ./lora_pkt_fwd
+
+To stop the application, press Ctrl+C.
+Unless it is manually stopped or encounter a critical error, the program will 
+run forever.
+
+There are no command line launch options.
+
+The way the program takes configuration files into account is the following:
+ * if there is a debug_conf.json parse it, others are ignored
+ * if there is a global_conf.json parse it, look for the next file
+ * if there is a local_conf.json parse it
+If some parameters are defined in both global and local configuration files, 
+the local definition overwrites the global definition. 
+
+The global configuration file should be exactly the same throughout your 
+network, contain all global parameters (parameters for "sensor" radio 
+channels) and preferably default "safe" values for parameters that are 
+specific for each gateway (eg. specify a default MAC address).
+
+As some of the parameters (like 'rssi_offset', 'tx_lut_*') are board dependant,
+several flavours of the global_conf.json file are provided in the cfg/
+directory.
+* global_conf.json.PCB_E286.EU868.*: to be used for Semtech reference design
+  board with PCB name PCB_E286 (also called Gateway Board v1.0 (no FPGA)).
+  Configured for Europe 868MHz channels.
+* global_conf.json.PCB_E336.EU868.*:to be used for Semtech reference design
+  board with PCB name PCB_E336 (also called Gateway Board v1.5 (with FPGA)).
+  Configured for Europe 868MHz channels.
+* global_conf.json.US902.*: to be used for Semtech reference design v1.0 or
+  v1.5. (No calibration done for RSSI offset and TX gains yet).
+  Configured for US 902MHz channels.
+
+Beside board related flavours, there are "features" flavours named "basic",
+"gps", "beacon".
+* global_conf.json.*.basic: to be used for basic packet forwarder usage, with
+no GPS.
+* global_conf.json.*.gps: to be used when the platform has a GPS receiver.
+* global_conf.json.*.beacon: to be used when the platform has a GPS receiver
+and we want the packet forwarder to emit beacons for synchronized networks.
+
+Rename the one you need to global_conf.json before launching the packet
+forwarder.
+
+The local configuration file should contain parameters that are specific to 
+each gateway (eg. MAC address, frequency for backhaul radio channels).
+
+In each configuration file, the program looks for a JSON object named 
+"SX1301_conf" that should contain the parameters for the Lora concentrator 
+board (RF channels definition, modem parameters, etc) and another JSON object 
+called "gateway_conf" that should contain the gateway parameters (gateway MAC 
+address, IP address of the server, keep-alive time, etc).
+
+To learn more about the JSON configuration format, read the provided JSON 
+files and the libloragw API documentation.
+
+Every X seconds (parameter settable in the configuration files) the program 
+display statistics on the RF packets received and sent, and the network 
+datagrams received and sent.
+The program also send some statistics to the server in JSON format.
+
+5. "Just-In-Time" downlink scheduling
+-------------------------------------
+
+The LoRa concentrator can have only one TX packet programmed for departure at a
+time. The actual departure of a downlink packet will be done based on its
+timestamp, when the concentrator internal counter reaches timestamp’s value.
+The departure of a beacon will be done based on a GPS PPS event.
+It may happen that, due to network variable latency, the gateway receives one
+or many downlink packets from the server while a TX is already programmed in the
+concentrator. The packet forwarder has to store and order incoming downlink
+packets (in a queue), so that they can all be programmed in the concentrator at
+the proper time and sent over the air.
+Possible failures that may occur and that have to be reported to the server are:
+- It is too early or too late to send a given packet
+- A packet collides with another packet already queued
+- A packet collides with a beacon
+- TX RF parameters (frequency, power) are not supported by gateway
+- Gateway’s GPS is unlocked, so cannot process Class B downlink
+It is called "Just-in-Time" (JiT) scheduling, because the packet forwarder will
+program a downlink or a beacon packet in the concentrator just before it has to
+be sent over the air.
+Another benefit of JiT is to optimize the gateway downlink capacity by avoiding
+to keep the concentrator TX buffer busy for too long.
+
+In order to achieve "Just-in-Time" scheduling, the following software elements
+have been added:
+- A JiT queue, with associated enqueue/peek/dequeue functions and packet
+acceptance criterias. It is where downlink packets are stored, waiting to be
+sent.
+- A JiT thread, which regularly checks if there is a packet in the JiT queue
+ready to be programmed in the concentrator, based on current concentrator
+internal time.
+- A Timer synchronization thread to keep the concentrator clock and Unix clock
+synchronized so that host processor can determine if a packet with a given
+timestamp can be programmed in the concentrator or not.
+
+5.1. Concentrator vs Unix time synchronization
+
+In order for the host to know if an incoming downlink packet can or cannot be
+queued in JiT queue for later transmission, it has to check if the timestamp of
+the packet designates a time later than the current concentrator counter or if
+it is already too late to be passed to the concentrator.
+In order to get current concentrator time, we can use the lgw_get_trigcnt() HAL
+function. The problem is that the sample register used to read this value can be
+configured in 2 different ways:
+    - Real time mode: when GPS is disabled, the value read in sample register is
+      the actual concentrator counter value.
+    - PPS mode: when GPS is enabled, the value read in sample register is the
+      value that the concentrator counter had when last GPS’s PPS occurred. So
+      this changes every second only.
+As in our case GPS is enabled (LGW_GPS_EN==1), we need to have a way to get the
+actual concentrator current time, at any time.
+For this, a new thread has been added to the packet forwarder (thread_timersync)
+which will regularly:
+    - Disable GPS mode of SX1301 counter sampler
+    - Get current Unix time
+    - Get current SX1301 counter
+    - Compute the offset between Unix and SX1301 clocks and store it
+    - Re-enable GPS mode of SX1301 counter sampler
+Then a new function has been added to estimate the current concentrator counter
+at any time based on the current Unix time and offset computed by the timersync
+thread.
+
+In addition to this, the Concentrator vs Unix time synchronization is used by
+the JiT thread to determine if a packet in the JiT queue has to be sent to the
+concentrator for transmission.
+So basically it is used for queueing and dequeuing packets to/from the JiT queue.
+
+5.2. Concentrator vs GPS time synchronization
+
+There are 2 cases for which we need to convert a GPS time to concentrator
+counter:
+    - Class B downlink: when the “time” field of JSON “txpk” is filled instead
+      of the “tmst” field, we need to be able to determine if the packet can be
+      queued in JiT queue or not, based on its corresponding concentrator
+      counter value.
+      Note: even if a Class-B downlink is given with a GPS timestamp, the
+      concentrator TX mode is configured as “TIMESTAMP”, and not “ON_GPS”. So
+      at the end, it is the counter value which will be used for transmission.
+    - Beacons: beacons transmission time is based on GPS clock, and the
+      concentrator TX mode is configured as “ON_GPS” for accurate beacon
+      transmission on GPS PPS event. In this case, the concentrator does not
+      need the packet counter to be set. But, as the JiT thread decides if a
+      packet has to be peeked or not, based on its concentrator counter, we need
+      to have the beacon packet counter set (see next chapter for more details
+      on JiT scheduling).
+We also need to convert a SX1301 counter value to GPS UTC time when we receive
+an uplink, in order to fill the “time” field of JSON “rxpk” structure.
+
+5.3. TX scheduling
+
+The JiT queue implemented is a static array of nodes, where each node contains:
+    - the downlink packet, with its type (beacon, downlink class A, B or C)
+    - a “pre delay” which depends on packet type (BEACON_GUARD, TX_START_DELAY…)
+    - a “post delay” which depends on packet type (“time on air” of this packet
+      computed based on its size, datarate and coderate, or BEACON_RESERVED)
+
+Several functions are implemented to manipulate this queue or get info from it:
+    - init: initialize array with default values
+    - is full / is empty: gives queue status
+    - enqueue: checks if the given packet can be queued or not, based on several
+      criteria’s
+    - peek: checks if the queue contains a packet that must be passed
+      immediately to the concentrator for transmission and returns corresponding
+      index if any.
+    - dequeue: actually removes from the queue the packet at index given by peek
+      function
+
+The queue is always kept sorted on ascending timestamp order.
+
+The JiT thread will regularly check in the JiT queue if there is a packet to be
+sent soon.  If a packet is matching, it is dequeued and programmed in the
+concentrator TX buffer.
+
+5.4. Fine tuning parameters
+
+There are few parameters of the JiT queue which could be tweaked to adapt to
+different system constraints.
+
+    - inc/jitqueue.h:
+        JIT_QUEUE_MAX: The maximum number of nodes in the queue.
+    - src/jitqueue.c:
+        TX_JIT_DELAY: The number of milliseconds a packet is programmed in the
+                      concentrator TX buffer before its actual departure time.
+        TX_MARGIN_DELAY: Packet collision check margin
+
+6. License
+-----------
+
+Copyright (C) 2013, SEMTECH S.A.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+* Neither the name of the Semtech corporation nor the
+  names of its contributors may be used to endorse or promote products
+  derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL SEMTECH S.A. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+6. License for Parson library
+------------------------------
+
+Parson ( http://kgabis.github.com/parson/ )
+Copyright (C) 2012 Krzysztof Gabis
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+*EOF*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/src/base64.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,308 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Base64 encoding & decoding library
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "base64.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define ARRAY_SIZE(a)       (sizeof(a) / sizeof((a)[0]))
+#define CRIT(a)             fprintf(stderr, "\nCRITICAL file:%s line:%d msg:%s\n", __FILE__, __LINE__, a);exit(EXIT_FAILURE)
+
+//#define DEBUG(args...)    fprintf(stderr,"debug: " args) /* diagnostic message that is destined to the user */
+#define DEBUG(args...)
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS ---------------------------------------------------- */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MODULE-WIDE VARIABLES ---------------------------------------- */
+
+static char code_62 = '+';    /* RFC 1421 standard character for code 62 */
+static char code_63 = '/';    /* RFC 1421 standard character for code 63 */
+static char code_pad = '=';    /* RFC 1421 padding character if padding */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DECLARATION ---------------------------------------- */
+
+/**
+@brief Convert a code in the range 0-63 to an ASCII character
+*/
+char code_to_char(uint8_t x);
+
+/**
+@brief Convert an ASCII character to a code in the range 0-63
+*/
+uint8_t char_to_code(char x);
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DEFINITION ----------------------------------------- */
+
+char code_to_char(uint8_t x) {
+    if (x <= 25) {
+        return 'A' + x;
+    } else if ((x >= 26) && (x <= 51)) {
+        return 'a' + (x - 26);
+    } else if ((x >= 52) && (x <= 61)) {
+        return '0' + (x - 52);
+    } else if (x == 62) {
+        return code_62;
+    } else if (x == 63) {
+        return code_63;
+    } else {
+        DEBUG("ERROR: %i IS OUT OF RANGE 0-63 FOR BASE64 ENCODING\n", x);
+        exit(EXIT_FAILURE);
+    } //TODO: improve error management
+}
+
+uint8_t char_to_code(char x) {
+    if ((x >= 'A') && (x <= 'Z')) {
+        return (uint8_t)x - (uint8_t)'A';
+    } else if ((x >= 'a') && (x <= 'z')) {
+        return (uint8_t)x - (uint8_t)'a' + 26;
+    } else if ((x >= '0') && (x <= '9')) {
+        return (uint8_t)x - (uint8_t)'0' + 52;
+    } else if (x == code_62) {
+        return 62;
+    } else if (x == code_63) {
+        return 63;
+    } else {
+        DEBUG("ERROR: %c (0x%x) IS INVALID CHARACTER FOR BASE64 DECODING\n", x, x);
+        exit(EXIT_FAILURE);
+    } //TODO: improve error management
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS DEFINITION ------------------------------------------ */
+
+int bin_to_b64_nopad(const uint8_t * in, int size, char * out, int max_len) {
+    int i;
+    int result_len; /* size of the result */
+    int full_blocks; /* number of 3 unsigned chars / 4 characters blocks */
+    int last_bytes; /* number of unsigned chars <3 in the last block */
+    int last_chars; /* number of characters <4 in the last block */
+    uint32_t b;
+
+    /* check input values */
+    if ((out == NULL) || (in == NULL)) {
+        DEBUG("ERROR: NULL POINTER AS OUTPUT IN BIN_TO_B64\n");
+        return -1;
+    }
+    if (size == 0) {
+        *out = 0; /* null string */
+        return 0;
+    }
+
+    /* calculate the number of base64 'blocks' */
+    full_blocks = size / 3;
+    last_bytes = size % 3;
+    switch (last_bytes) {
+        case 0: /* no byte left to encode */
+            last_chars = 0;
+            break;
+        case 1: /* 1 byte left to encode -> +2 chars */
+            last_chars = 2;
+            break;
+        case 2: /* 2 bytes left to encode -> +3 chars */
+            last_chars = 3;
+            break;
+        default:
+            CRIT("switch default that should not be possible");
+    }
+
+    /* check if output buffer is big enough */
+    result_len = (4 * full_blocks) + last_chars;
+    if (max_len < (result_len + 1)) { /* 1 char added for string terminator */
+        DEBUG("ERROR: OUTPUT BUFFER TOO SMALL IN BIN_TO_B64\n");
+        return -1;
+    }
+
+    /* process all the full blocks */
+    for (i = 0; i < full_blocks; ++i) {
+        b  = (0xFF & in[3 * i]    ) << 16;
+        b |= (0xFF & in[3 * i + 1]) << 8;
+        b |=  0xFF & in[3 * i + 2];
+        out[4 * i + 0] = code_to_char((b >> 18) & 0x3F);
+        out[4 * i + 1] = code_to_char((b >> 12) & 0x3F);
+        out[4 * i + 2] = code_to_char((b >> 6 ) & 0x3F);
+        out[4 * i + 3] = code_to_char( b        & 0x3F);
+    }
+
+    /* process the last 'partial' block and terminate string */
+    i = full_blocks;
+    if (last_chars == 0) {
+        out[4 * i] =  0; /* null character to terminate string */
+    } else if (last_chars == 2) {
+        b  = (0xFF & in[3 * i]    ) << 16;
+        out[4 * i + 0] = code_to_char((b >> 18) & 0x3F);
+        out[4 * i + 1] = code_to_char((b >> 12) & 0x3F);
+        out[4 * i + 2] =  0; /* null character to terminate string */
+    } else if (last_chars == 3) {
+        b  = (0xFF & in[3 * i]    ) << 16;
+        b |= (0xFF & in[3 * i + 1]) << 8;
+        out[4 * i + 0] = code_to_char((b >> 18) & 0x3F);
+        out[4 * i + 1] = code_to_char((b >> 12) & 0x3F);
+        out[4 * i + 2] = code_to_char((b >> 6 ) & 0x3F);
+        out[4 * i + 3] = 0; /* null character to terminate string */
+    }
+
+    return result_len;
+}
+
+int b64_to_bin_nopad(const char * in, int size, uint8_t * out, int max_len) {
+    int i;
+    int result_len; /* size of the result */
+    int full_blocks; /* number of 3 unsigned chars / 4 characters blocks */
+    int last_chars; /* number of characters <4 in the last block */
+    int last_bytes; /* number of unsigned chars <3 in the last block */
+    uint32_t b;
+    ;
+
+    /* check input values */
+    if ((out == NULL) || (in == NULL)) {
+        DEBUG("ERROR: NULL POINTER AS OUTPUT OR INPUT IN B64_TO_BIN\n");
+        return -1;
+    }
+    if (size == 0) {
+        return 0;
+    }
+
+    /* calculate the number of base64 'blocks' */
+    full_blocks = size / 4;
+    last_chars = size % 4;
+    switch (last_chars) {
+        case 0: /* no char left to decode */
+            last_bytes = 0;
+            break;
+        case 1: /* only 1 char left is an error */
+            DEBUG("ERROR: ONLY ONE CHAR LEFT IN B64_TO_BIN\n");
+            return -1;
+        case 2: /* 2 chars left to decode -> +1 byte */
+            last_bytes = 1;
+            break;
+        case 3: /* 3 chars left to decode -> +2 bytes */
+            last_bytes = 2;
+            break;
+        default:
+            CRIT("switch default that should not be possible");
+    }
+
+    /* check if output buffer is big enough */
+    result_len = (3 * full_blocks) + last_bytes;
+    if (max_len < result_len) {
+        DEBUG("ERROR: OUTPUT BUFFER TOO SMALL IN B64_TO_BIN\n");
+        return -1;
+    }
+
+    /* process all the full blocks */
+    for (i = 0; i < full_blocks; ++i) {
+        b  = (0x3F & char_to_code(in[4 * i]    )) << 18;
+        b |= (0x3F & char_to_code(in[4 * i + 1])) << 12;
+        b |= (0x3F & char_to_code(in[4 * i + 2])) << 6;
+        b |=  0x3F & char_to_code(in[4 * i + 3]);
+        out[3 * i + 0] = (b >> 16) & 0xFF;
+        out[3 * i + 1] = (b >> 8 ) & 0xFF;
+        out[3 * i + 2] =  b        & 0xFF;
+    }
+
+    /* process the last 'partial' block */
+    i = full_blocks;
+    if (last_bytes == 1) {
+        b  = (0x3F & char_to_code(in[4 * i]    )) << 18;
+        b |= (0x3F & char_to_code(in[4 * i + 1])) << 12;
+        out[3 * i + 0] = (b >> 16) & 0xFF;
+        if (((b >> 12) & 0x0F) != 0) {
+            DEBUG("WARNING: last character contains unusable bits\n");
+        }
+    } else if (last_bytes == 2) {
+        b  = (0x3F & char_to_code(in[4 * i]    )) << 18;
+        b |= (0x3F & char_to_code(in[4 * i + 1])) << 12;
+        b |= (0x3F & char_to_code(in[4 * i + 2])) << 6;
+        out[3 * i + 0] = (b >> 16) & 0xFF;
+        out[3 * i + 1] = (b >> 8 ) & 0xFF;
+        if (((b >> 6) & 0x03) != 0) {
+            DEBUG("WARNING: last character contains unusable bits\n");
+        }
+    }
+
+    return result_len;
+}
+
+int bin_to_b64(const uint8_t * in, int size, char * out, int max_len) {
+    int ret;
+
+    ret = bin_to_b64_nopad(in, size, out, max_len);
+
+    if (ret == -1) {
+        return -1;
+    }
+    switch (ret % 4) {
+        case 0: /* nothing to do */
+            return ret;
+        case 1:
+            DEBUG("ERROR: INVALID UNPADDED BASE64 STRING\n");
+            return -1;
+        case 2: /* 2 chars in last block, must add 2 padding char */
+            if (max_len >= (ret + 2 + 1)) {
+                out[ret] = code_pad;
+                out[ret + 1] = code_pad;
+                out[ret + 2] = 0;
+                return ret + 2;
+            } else {
+                DEBUG("ERROR: not enough room to add padding in bin_to_b64\n");
+                return -1;
+            }
+        case 3: /* 3 chars in last block, must add 1 padding char */
+            if (max_len >= (ret + 1 + 1)) {
+                out[ret] = code_pad;
+                out[ret + 1] = 0;
+                return ret + 1;
+            } else {
+                DEBUG("ERROR: not enough room to add padding in bin_to_b64\n");
+                return -1;
+            }
+        default:
+            CRIT("switch default that should not be possible");
+    }
+}
+
+int b64_to_bin(const char * in, int size, uint8_t * out, int max_len) {
+    if (in == NULL) {
+        DEBUG("ERROR: NULL POINTER AS OUTPUT OR INPUT IN B64_TO_BIN\n");
+        return -1;
+    }
+    if ((size % 4 == 0) && (size >= 4)) { /* potentially padded Base64 */
+        if (in[size - 2] == code_pad) { /* 2 padding char to ignore */
+            return b64_to_bin_nopad(in, size - 2, out, max_len);
+        } else if (in[size - 1] == code_pad) { /* 1 padding char to ignore */
+            return b64_to_bin_nopad(in, size - 1, out, max_len);
+        } else { /* no padding to ignore */
+            return b64_to_bin_nopad(in, size, out, max_len);
+        }
+    } else { /* treat as unpadded Base64 */
+        return b64_to_bin_nopad(in, size, out, max_len);
+    }
+}
+
+
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/src/jitqueue.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,465 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    LoRa concentrator : Just In Time TX scheduling queue
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Michael Coracin
+*/
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#define _GNU_SOURCE     /* needed for qsort_r to be defined */
+#include <stdlib.h>     /* qsort_r */
+#include <stdio.h>      /* printf, fprintf, snprintf, fopen, fputs */
+#include <string.h>     /* memset, memcpy */
+#include <pthread.h>
+#include <assert.h>
+#include <math.h>
+
+#include "trace.h"
+#include "jitqueue.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS & TYPES -------------------------------------------- */
+#define TX_START_DELAY          1500    /* microseconds */
+/* TODO: get this value from HAL? */
+#define TX_MARGIN_DELAY         1000    /* Packet overlap margin in microseconds */
+/* TODO: How much margin should we take? */
+#define TX_JIT_DELAY            50000   /* Pre-delay to program packet for TX in microseconds */
+#define TX_MAX_ADVANCE_DELAY    ((JIT_NUM_BEACON_IN_QUEUE + 1) * 128 * 1E6) /* Maximum advance delay accepted for a TX packet, compared to current time */
+
+#define BEACON_GUARD            3000000 /* Interval where no ping slot can be placed,
+                                            to ensure beacon can be sent */
+#define BEACON_RESERVED         2120000 /* Time on air of the beacon, with some margin */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE VARIABLES (GLOBAL) ------------------------------------------- */
+static pthread_mutex_t mx_jit_queue = PTHREAD_MUTEX_INITIALIZER; /* control access to JIT queue */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DEFINITION ----------------------------------------- */
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS DEFINITION ----------------------------------------- */
+
+bool jit_queue_is_full(struct jit_queue_s *queue) {
+    bool result;
+
+    pthread_mutex_lock(&mx_jit_queue);
+
+    result = (queue->num_pkt == JIT_QUEUE_MAX) ? true : false;
+
+    pthread_mutex_unlock(&mx_jit_queue);
+
+    return result;
+}
+
+bool jit_queue_is_empty(struct jit_queue_s *queue) {
+    bool result;
+
+    pthread_mutex_lock(&mx_jit_queue);
+
+    result = (queue->num_pkt == 0) ? true : false;
+
+    pthread_mutex_unlock(&mx_jit_queue);
+
+    return result;
+}
+
+void jit_queue_init(struct jit_queue_s *queue) {
+    int i;
+
+    pthread_mutex_lock(&mx_jit_queue);
+
+    memset(queue, 0, sizeof(*queue));
+    for (i = 0; i < JIT_QUEUE_MAX; i++) {
+        queue->nodes[i].pre_delay = 0;
+        queue->nodes[i].post_delay = 0;
+    }
+
+    pthread_mutex_unlock(&mx_jit_queue);
+}
+
+int compare(const void *a, const void *b, void *arg) {
+    struct jit_node_s *p = (struct jit_node_s *)a;
+    struct jit_node_s *q = (struct jit_node_s *)b;
+    int *counter = (int *)arg;
+    int p_count, q_count;
+
+    p_count = p->pkt.count_us;
+    q_count = q->pkt.count_us;
+
+    if (p_count > q_count) {
+        *counter = *counter + 1;
+    }
+
+    return p_count - q_count;
+}
+
+void jit_sort_queue(struct jit_queue_s *queue) {
+    int counter = 0;
+
+    if (queue->num_pkt == 0) {
+        return;
+    }
+
+    MSG_DEBUG(DEBUG_JIT, "sorting queue in ascending order packet timestamp - queue size:%u\n", queue->num_pkt);
+    qsort_r(queue->nodes, queue->num_pkt, sizeof(queue->nodes[0]), compare, &counter);
+    MSG_DEBUG(DEBUG_JIT, "sorting queue done - swapped:%d\n", counter);
+}
+
+bool jit_collision_test(uint32_t p1_count_us, uint32_t p1_pre_delay, uint32_t p1_post_delay, uint32_t p2_count_us, uint32_t p2_pre_delay, uint32_t p2_post_delay) {
+    if (((p1_count_us - p2_count_us) <= (p1_pre_delay + p2_post_delay + TX_MARGIN_DELAY)) ||
+            ((p2_count_us - p1_count_us) <= (p2_pre_delay + p1_post_delay + TX_MARGIN_DELAY))) {
+        return true;
+    } else {
+        return false;
+    }
+}
+
+enum jit_error_e jit_enqueue(struct jit_queue_s *queue, struct timeval *time, struct lgw_pkt_tx_s *packet, enum jit_pkt_type_e pkt_type) {
+    int i = 0;
+    uint32_t time_us = time->tv_sec * 1000000UL + time->tv_usec; /* convert time in µs */
+    uint32_t packet_post_delay = 0;
+    uint32_t packet_pre_delay = 0;
+    uint32_t target_pre_delay = 0;
+    enum jit_error_e err_collision;
+    uint32_t asap_count_us;
+
+    MSG_DEBUG(DEBUG_JIT, "Current concentrator time is %u, pkt_type=%d\n", time_us, pkt_type);
+
+    if (packet == NULL) {
+        MSG_DEBUG(DEBUG_JIT_ERROR, "ERROR: invalid parameter\n");
+        return JIT_ERROR_INVALID;
+    }
+
+    if (jit_queue_is_full(queue)) {
+        MSG_DEBUG(DEBUG_JIT_ERROR, "ERROR: cannot enqueue packet, JIT queue is full\n");
+        return JIT_ERROR_FULL;
+    }
+
+    /* Compute packet pre/post delays depending on packet's type */
+    switch (pkt_type) {
+        case JIT_PKT_TYPE_DOWNLINK_CLASS_A:
+        case JIT_PKT_TYPE_DOWNLINK_CLASS_B:
+        case JIT_PKT_TYPE_DOWNLINK_CLASS_C:
+            packet_pre_delay = TX_START_DELAY + TX_JIT_DELAY;
+            packet_post_delay = lgw_time_on_air(packet) * 1000UL; /* in us */
+            break;
+        case JIT_PKT_TYPE_BEACON:
+            /* As defined in LoRaWAN spec */
+            packet_pre_delay = TX_START_DELAY + BEACON_GUARD + TX_JIT_DELAY;
+            packet_post_delay = BEACON_RESERVED;
+            break;
+        default:
+            break;
+    }
+
+    pthread_mutex_lock(&mx_jit_queue);
+
+    /* An immediate downlink becomes a timestamped downlink "ASAP" */
+    /* Set the packet count_us to the first available slot */
+    if (pkt_type == JIT_PKT_TYPE_DOWNLINK_CLASS_C) {
+        /* change tx_mode to timestamped */
+        packet->tx_mode = TIMESTAMPED;
+
+        /* Search for the ASAP timestamp to be given to the packet */
+        asap_count_us = time_us + 1E6; /* TODO: Take 1 second margin, to be refined */
+        if (queue->num_pkt == 0) {
+            /* If the jit queue is empty, we can insert this packet */
+            MSG_DEBUG(DEBUG_JIT, "DEBUG: insert IMMEDIATE downlink, first in JiT queue (count_us=%u)\n", asap_count_us);
+        } else {
+            /* Else we can try to insert it:
+                - ASAP meaning NOW + MARGIN
+                - at the last index of the queue
+                - between 2 downlinks in the queue
+            */
+
+            /* First, try if the ASAP time collides with an already enqueued downlink */
+            for (i = 0; i < queue->num_pkt; i++) {
+                if (jit_collision_test(asap_count_us, packet_pre_delay, packet_post_delay, queue->nodes[i].pkt.count_us, queue->nodes[i].pre_delay, queue->nodes[i].post_delay) == true) {
+                    MSG_DEBUG(DEBUG_JIT, "DEBUG: cannot insert IMMEDIATE downlink at count_us=%u, collides with %u (index=%d)\n", asap_count_us, queue->nodes[i].pkt.count_us, i);
+                    break;
+                }
+            }
+            if (i == queue->num_pkt) {
+                /* No collision with ASAP time, we can insert it */
+                MSG_DEBUG(DEBUG_JIT, "DEBUG: insert IMMEDIATE downlink ASAP at %u (no collision)\n", asap_count_us);
+            } else {
+                /* Search for the best slot then */
+                for (i = 0; i < queue->num_pkt; i++) {
+                    asap_count_us = queue->nodes[i].pkt.count_us + queue->nodes[i].post_delay + packet_pre_delay + TX_JIT_DELAY + TX_MARGIN_DELAY;
+                    if (i == (queue->num_pkt - 1)) {
+                        /* Last packet index, we can insert after this one */
+                        MSG_DEBUG(DEBUG_JIT, "DEBUG: insert IMMEDIATE downlink, last in JiT queue (count_us=%u)\n", asap_count_us);
+                    } else {
+                        /* Check if packet can be inserted between this index and the next one */
+                        MSG_DEBUG(DEBUG_JIT, "DEBUG: try to insert IMMEDIATE downlink (count_us=%u) between index %d and index %d?\n", asap_count_us, i, i + 1);
+                        if (jit_collision_test(asap_count_us, packet_pre_delay, packet_post_delay, queue->nodes[i + 1].pkt.count_us, queue->nodes[i + 1].pre_delay, queue->nodes[i + 1].post_delay) == true) {
+                            MSG_DEBUG(DEBUG_JIT, "DEBUG: failed to insert IMMEDIATE downlink (count_us=%u), continue...\n", asap_count_us);
+                            continue;
+                        } else {
+                            MSG_DEBUG(DEBUG_JIT, "DEBUG: insert IMMEDIATE downlink (count_us=%u)\n", asap_count_us);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+        /* Set packet with ASAP timestamp */
+        packet->count_us = asap_count_us;
+    }
+
+    /* Check criteria_1: is it already too late to send this packet ?
+     *  The packet should arrive at least at (tmst - TX_START_DELAY) to be programmed into concentrator
+     *  Note: - Also add some margin, to be checked how much is needed, if needed
+     *        - Valid for both Downlinks and Beacon packets
+     *
+     *  Warning: unsigned arithmetic (handle roll-over)
+     *      t_packet < t_current + TX_START_DELAY + MARGIN
+     */
+    if ((packet->count_us - time_us) <= (TX_START_DELAY + TX_MARGIN_DELAY + TX_JIT_DELAY)) {
+        MSG_DEBUG(DEBUG_JIT_ERROR, "ERROR: Packet REJECTED, already too late to send it (current=%u, packet=%u, type=%d)\n", time_us, packet->count_us, pkt_type);
+        pthread_mutex_unlock(&mx_jit_queue);
+        return JIT_ERROR_TOO_LATE;
+    }
+
+    /* Check criteria_2: Does packet timestamp seem plausible compared to current time
+     *  We do not expect the server to program a downlink too early compared to current time
+     *  Class A: downlink has to be sent in a 1s or 2s time window after RX
+     *  Class B: downlink has to occur in a 128s time window
+     *  Class C: no check needed, departure time has been calculated previously
+     *  So let's define a safe delay above which we can say that the packet is out of bound: TX_MAX_ADVANCE_DELAY
+     *  Note: - Valid for Downlinks only, not for Beacon packets
+     *
+     *  Warning: unsigned arithmetic (handle roll-over)
+                t_packet > t_current + TX_MAX_ADVANCE_DELAY
+     */
+    if ((pkt_type == JIT_PKT_TYPE_DOWNLINK_CLASS_A) || (pkt_type == JIT_PKT_TYPE_DOWNLINK_CLASS_B)) {
+        if ((packet->count_us - time_us) > TX_MAX_ADVANCE_DELAY) {
+            MSG_DEBUG(DEBUG_JIT_ERROR, "ERROR: Packet REJECTED, timestamp seems wrong, too much in advance (current=%u, packet=%u, type=%d)\n", time_us, packet->count_us, pkt_type);
+            pthread_mutex_unlock(&mx_jit_queue);
+            return JIT_ERROR_TOO_EARLY;
+        }
+    }
+
+    /* Check criteria_3: does this new packet overlap with a packet already enqueued ?
+     *  Note: - need to take into account packet's pre_delay and post_delay of each packet
+     *        - Valid for both Downlinks and beacon packets
+     *        - Beacon guard can be ignored if we try to queue a Class A downlink
+     */
+    for (i = 0; i < queue->num_pkt; i++) {
+        /* We ignore Beacon Guard for Class A/C downlinks */
+        if (((pkt_type == JIT_PKT_TYPE_DOWNLINK_CLASS_A) || (pkt_type == JIT_PKT_TYPE_DOWNLINK_CLASS_C)) && (queue->nodes[i].pkt_type == JIT_PKT_TYPE_BEACON)) {
+            target_pre_delay = TX_START_DELAY;
+        } else {
+            target_pre_delay = queue->nodes[i].pre_delay;
+        }
+
+        /* Check if there is a collision
+         *  Warning: unsigned arithmetic (handle roll-over)
+         *      t_packet_new - pre_delay_packet_new < t_packet_prev + post_delay_packet_prev (OVERLAP on post delay)
+         *      t_packet_new + post_delay_packet_new > t_packet_prev - pre_delay_packet_prev (OVERLAP on pre delay)
+         */
+        if (jit_collision_test(packet->count_us, packet_pre_delay, packet_post_delay, queue->nodes[i].pkt.count_us, target_pre_delay, queue->nodes[i].post_delay) == true) {
+            switch (queue->nodes[i].pkt_type) {
+                case JIT_PKT_TYPE_DOWNLINK_CLASS_A:
+                case JIT_PKT_TYPE_DOWNLINK_CLASS_B:
+                case JIT_PKT_TYPE_DOWNLINK_CLASS_C:
+                    MSG_DEBUG(DEBUG_JIT_ERROR, "ERROR: Packet (type=%d) REJECTED, collision with packet already programmed at %u (%u)\n", pkt_type, queue->nodes[i].pkt.count_us, packet->count_us);
+                    err_collision = JIT_ERROR_COLLISION_PACKET;
+                    break;
+                case JIT_PKT_TYPE_BEACON:
+                    if (pkt_type != JIT_PKT_TYPE_BEACON) {
+                        /* do not overload logs for beacon/beacon collision, as it is expected to happen with beacon pre-scheduling algorith used */
+                        MSG_DEBUG(DEBUG_JIT_ERROR, "ERROR: Packet (type=%d) REJECTED, collision with beacon already programmed at %u (%u)\n", pkt_type, queue->nodes[i].pkt.count_us, packet->count_us);
+                    }
+                    err_collision = JIT_ERROR_COLLISION_BEACON;
+                    break;
+                default:
+                    MSG("ERROR: Unknown packet type, should not occur, BUG?\n");
+                    assert(0);
+                    break;
+            }
+            pthread_mutex_unlock(&mx_jit_queue);
+            return err_collision;
+        }
+    }
+
+    /* Finally enqueue it */
+    /* Insert packet at the end of the queue */
+    memcpy(&(queue->nodes[queue->num_pkt].pkt), packet, sizeof(struct lgw_pkt_tx_s));
+    queue->nodes[queue->num_pkt].pre_delay = packet_pre_delay;
+    queue->nodes[queue->num_pkt].post_delay = packet_post_delay;
+    queue->nodes[queue->num_pkt].pkt_type = pkt_type;
+    if (pkt_type == JIT_PKT_TYPE_BEACON) {
+        queue->num_beacon++;
+    }
+    queue->num_pkt++;
+    /* Sort the queue in ascending order of packet timestamp */
+    jit_sort_queue(queue);
+
+    /* Done */
+    pthread_mutex_unlock(&mx_jit_queue);
+
+    jit_print_queue(queue, false, DEBUG_JIT);
+
+    MSG_DEBUG(DEBUG_JIT, "enqueued packet with count_us=%u (size=%u bytes, toa=%u us, type=%u)\n", packet->count_us, packet->size, packet_post_delay, pkt_type);
+
+    return JIT_ERROR_OK;
+}
+
+enum jit_error_e jit_dequeue(struct jit_queue_s *queue, int index, struct lgw_pkt_tx_s *packet, enum jit_pkt_type_e *pkt_type) {
+    if (packet == NULL) {
+        MSG("ERROR: invalid parameter\n");
+        return JIT_ERROR_INVALID;
+    }
+
+    if ((index < 0) || (index >= JIT_QUEUE_MAX)) {
+        MSG("ERROR: invalid parameter\n");
+        return JIT_ERROR_INVALID;
+    }
+
+    if (jit_queue_is_empty(queue)) {
+        MSG("ERROR: cannot dequeue packet, JIT queue is empty\n");
+        return JIT_ERROR_EMPTY;
+    }
+
+    pthread_mutex_lock(&mx_jit_queue);
+
+    /* Dequeue requested packet */
+    memcpy(packet, &(queue->nodes[index].pkt), sizeof(struct lgw_pkt_tx_s));
+    queue->num_pkt--;
+    *pkt_type = queue->nodes[index].pkt_type;
+    if (*pkt_type == JIT_PKT_TYPE_BEACON) {
+        queue->num_beacon--;
+        MSG_DEBUG(DEBUG_BEACON, "--- Beacon dequeued ---\n");
+    }
+
+    /* Replace dequeued packet with last packet of the queue */
+    memcpy(&(queue->nodes[index]), &(queue->nodes[queue->num_pkt]), sizeof(struct jit_node_s));
+    memset(&(queue->nodes[queue->num_pkt]), 0, sizeof(struct jit_node_s));
+
+    /* Sort queue in ascending order of packet timestamp */
+    jit_sort_queue(queue);
+
+    /* Done */
+    pthread_mutex_unlock(&mx_jit_queue);
+
+    jit_print_queue(queue, false, DEBUG_JIT);
+
+    MSG_DEBUG(DEBUG_JIT, "dequeued packet with count_us=%u from index %d\n", packet->count_us, index);
+
+    return JIT_ERROR_OK;
+}
+
+enum jit_error_e jit_peek(struct jit_queue_s *queue, struct timeval *time, int *pkt_idx) {
+    /* Return index of node containing a packet inline with given time */
+    int i = 0;
+    int idx_highest_priority = -1;
+    uint32_t time_us;
+
+    if ((time == NULL) || (pkt_idx == NULL)) {
+        MSG("ERROR: invalid parameter\n");
+        return JIT_ERROR_INVALID;
+    }
+
+    if (jit_queue_is_empty(queue)) {
+        return JIT_ERROR_EMPTY;
+    }
+
+    time_us = time->tv_sec * 1000000UL + time->tv_usec;
+
+    pthread_mutex_lock(&mx_jit_queue);
+
+    /* Search for highest priority packet to be sent */
+    for (i = 0; i < queue->num_pkt; i++) {
+        /* First check if that packet is outdated:
+         *  If a packet seems too much in advance, and was not rejected at enqueue time,
+         *  it means that we missed it for peeking, we need to drop it
+         *
+         *  Warning: unsigned arithmetic
+         *      t_packet > t_current + TX_MAX_ADVANCE_DELAY
+         */
+        if ((queue->nodes[i].pkt.count_us - time_us) >= TX_MAX_ADVANCE_DELAY) {
+            /* We drop the packet to avoid lock-up */
+            queue->num_pkt--;
+            if (queue->nodes[i].pkt_type == JIT_PKT_TYPE_BEACON) {
+                queue->num_beacon--;
+                MSG("WARNING: --- Beacon dropped (current_time=%u, packet_time=%u) ---\n", time_us, queue->nodes[i].pkt.count_us);
+            } else {
+                MSG("WARNING: --- Packet dropped (current_time=%u, packet_time=%u) ---\n", time_us, queue->nodes[i].pkt.count_us);
+            }
+
+            /* Replace dropped packet with last packet of the queue */
+            memcpy(&(queue->nodes[i]), &(queue->nodes[queue->num_pkt]), sizeof(struct jit_node_s));
+            memset(&(queue->nodes[queue->num_pkt]), 0, sizeof(struct jit_node_s));
+
+            /* Sort queue in ascending order of packet timestamp */
+            jit_sort_queue(queue);
+
+            /* restart loop  after purge to find packet to be sent */
+            i = 0;
+            continue;
+        }
+
+        /* Then look for highest priority packet to be sent:
+         *  Warning: unsigned arithmetic (handle roll-over)
+         *      t_packet < t_highest
+         */
+        if ((idx_highest_priority == -1) || (((queue->nodes[i].pkt.count_us - time_us) < (queue->nodes[idx_highest_priority].pkt.count_us - time_us)))) {
+            idx_highest_priority = i;
+        }
+    }
+
+    /* Peek criteria 1: look for a packet to be sent in next TX_JIT_DELAY ms timeframe
+     *  Warning: unsigned arithmetic (handle roll-over)
+     *      t_packet < t_current + TX_JIT_DELAY
+     */
+    if ((queue->nodes[idx_highest_priority].pkt.count_us - time_us) < TX_JIT_DELAY) {
+        *pkt_idx = idx_highest_priority;
+        MSG_DEBUG(DEBUG_JIT, "peek packet with count_us=%u at index %d\n",
+                  queue->nodes[idx_highest_priority].pkt.count_us, idx_highest_priority);
+    } else {
+        *pkt_idx = -1;
+    }
+
+    pthread_mutex_unlock(&mx_jit_queue);
+
+    return JIT_ERROR_OK;
+}
+
+void jit_print_queue(struct jit_queue_s *queue, bool show_all, int debug_level) {
+    int i = 0;
+    int loop_end;
+
+    if (jit_queue_is_empty(queue)) {
+        MSG_DEBUG(debug_level, "INFO: [jit] queue is empty\n");
+    } else {
+        pthread_mutex_lock(&mx_jit_queue);
+
+        MSG_DEBUG(debug_level, "INFO: [jit] queue contains %d packets:\n", queue->num_pkt);
+        MSG_DEBUG(debug_level, "INFO: [jit] queue contains %d beacons:\n", queue->num_beacon);
+        loop_end = (show_all == true) ? JIT_QUEUE_MAX : queue->num_pkt;
+        for (i = 0; i < loop_end; i++) {
+            MSG_DEBUG(debug_level, " - node[%d]: count_us=%u - type=%d\n",
+                      i,
+                      queue->nodes[i].pkt.count_us,
+                      queue->nodes[i].pkt_type);
+        }
+
+        pthread_mutex_unlock(&mx_jit_queue);
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/src/lora_pkt_fwd.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,2139 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Configure Lora concentrator and forward packets to a server
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+*/
+
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+/* fix an issue between POSIX and C99 */
+#if __STDC_VERSION__ >= 199901L
+#define _XOPEN_SOURCE 600
+#else
+#define _XOPEN_SOURCE 500
+#endif
+
+#include <stdint.h>         /* C99 types */
+#include <stdbool.h>        /* bool type */
+#include <stdio.h>          /* printf, fprintf, snprintf, fopen, fputs */
+
+#include <string.h>         /* memset */
+#include <signal.h>         /* sigaction */
+#include <time.h>           /* time, clock_gettime, strftime, gmtime */
+#include <sys/time.h>       /* timeval */
+#include <unistd.h>         /* getopt, access */
+#include <stdlib.h>         /* atoi, exit */
+#include <errno.h>          /* error messages */
+#include <math.h>           /* modf */
+#include <assert.h>
+
+#include <sys/socket.h>     /* socket specific definitions */
+#include <netinet/in.h>     /* INET constants and stuff */
+#include <arpa/inet.h>      /* IP address conversion stuff */
+#include <netdb.h>          /* gai_strerror */
+
+#include <pthread.h>
+
+#include "trace.h"
+#include "jitqueue.h"
+#include "timersync.h"
+#include "parson.h"
+#include "base64.h"
+#include "loragw_hal.h"
+#include "loragw_aux.h"
+#include "loragw_reg.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define ARRAY_SIZE(a)   (sizeof(a) / sizeof((a)[0]))
+#define STRINGIFY(x)    #x
+#define STR(x)          STRINGIFY(x)
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS ---------------------------------------------------- */
+
+#ifndef VERSION_STRING
+#define VERSION_STRING "undefined"
+#endif
+
+#define DEFAULT_SERVER      127.0.0.1   /* hostname also supported */
+#define DEFAULT_PORT_UP     1780
+#define DEFAULT_PORT_DW     1782
+#define DEFAULT_KEEPALIVE   5           /* default time interval for downstream keep-alive packet */
+#define DEFAULT_STAT        30          /* default time interval for statistics */
+#define PUSH_TIMEOUT_MS     100
+#define PULL_TIMEOUT_MS     200
+#define FETCH_SLEEP_MS      50          /* nb of ms waited when a fetch return no packets */
+
+#define PROTOCOL_VERSION    2           /* v1.3 */
+
+#define PKT_PUSH_DATA   0
+#define PKT_PUSH_ACK    1
+#define PKT_PULL_DATA   2
+#define PKT_PULL_RESP   3
+#define PKT_PULL_ACK    4
+#define PKT_TX_ACK      5
+
+#define NB_PKT_MAX      15 /* max number of packets per fetch/send cycle */
+
+#define MIN_LORA_PREAMB 6 /* minimum Lora preamble length for this application */
+#define STD_LORA_PREAMB 8
+#define MIN_FSK_PREAMB  3 /* minimum FSK preamble length for this application */
+#define STD_FSK_PREAMB  5
+
+#define STATUS_SIZE     200
+#define TX_BUFF_SIZE    ((540 * NB_PKT_MAX) + 30 + STATUS_SIZE)
+
+#define COM_PATH_DEFAULT "/dev/ttyACM0"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE TYPES -------------------------------------------------------- */
+
+/**
+@struct coord_s
+@brief Geodesic coordinates
+*/
+struct coord_s {
+    double  lat;    /*!> latitude [-90,90] (North +, South -) */
+    double  lon;    /*!> longitude [-180,180] (East +, West -)*/
+    short   alt;    /*!> altitude in meters (WGS 84 geoid ref.) */
+};
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE VARIABLES (GLOBAL) ------------------------------------------- */
+
+/* signal handling variables */
+volatile bool exit_sig = false; /* 1 -> application terminates cleanly (shut down hardware, close open files, etc) */
+volatile bool quit_sig = false; /* 1 -> application terminates without shutting down the hardware */
+
+/* packets filtering configuration variables */
+static bool fwd_valid_pkt = true; /* packets with PAYLOAD CRC OK are forwarded */
+static bool fwd_error_pkt = false; /* packets with PAYLOAD CRC ERROR are NOT forwarded */
+static bool fwd_nocrc_pkt = false; /* packets with NO PAYLOAD CRC are NOT forwarded */
+
+/* network configuration variables */
+static uint64_t lgwm = 0; /* Lora gateway MAC address */
+static char serv_addr[64] = STR(DEFAULT_SERVER); /* address of the server (host name or IPv4/IPv6) */
+static char serv_port_up[8] = STR(DEFAULT_PORT_UP); /* server port for upstream traffic */
+static char serv_port_down[8] = STR(DEFAULT_PORT_DW); /* server port for downstream traffic */
+static int keepalive_time = DEFAULT_KEEPALIVE; /* send a PULL_DATA request every X seconds, negative = disabled */
+
+/* statistics collection configuration variables */
+static unsigned stat_interval = DEFAULT_STAT; /* time interval (in sec) at which statistics are collected and displayed */
+
+/* gateway <-> MAC protocol variables */
+static uint32_t net_mac_h; /* Most Significant Nibble, network order */
+static uint32_t net_mac_l; /* Least Significant Nibble, network order */
+
+/* network sockets */
+static int sock_up; /* socket for upstream traffic */
+static int sock_down; /* socket for downstream traffic */
+
+/* network protocol variables */
+static struct timeval push_timeout_half = {0, (PUSH_TIMEOUT_MS * 500)}; /* cut in half, critical for throughput */
+static struct timeval pull_timeout = {0, (PULL_TIMEOUT_MS * 1000)}; /* non critical for throughput */
+
+/* hardware access control and correction */
+pthread_mutex_t mx_concent = PTHREAD_MUTEX_INITIALIZER; /* control access to the concentrator */
+
+/* Reference coordinates, for broadcasting (beacon) */
+static struct coord_s reference_coord;
+
+/* Enable faking the GPS coordinates of the gateway */
+static bool gps_fake_enable; /* enable the feature */
+
+/* measurements to establish statistics */
+static pthread_mutex_t mx_meas_up = PTHREAD_MUTEX_INITIALIZER; /* control access to the upstream measurements */
+static uint32_t meas_nb_rx_rcv = 0; /* count packets received */
+static uint32_t meas_nb_rx_ok = 0; /* count packets received with PAYLOAD CRC OK */
+static uint32_t meas_nb_rx_bad = 0; /* count packets received with PAYLOAD CRC ERROR */
+static uint32_t meas_nb_rx_nocrc = 0; /* count packets received with NO PAYLOAD CRC */
+static uint32_t meas_up_pkt_fwd = 0; /* number of radio packet forwarded to the server */
+static uint32_t meas_up_network_byte = 0; /* sum of UDP bytes sent for upstream traffic */
+static uint32_t meas_up_payload_byte = 0; /* sum of radio payload bytes sent for upstream traffic */
+static uint32_t meas_up_dgram_sent = 0; /* number of datagrams sent for upstream traffic */
+static uint32_t meas_up_ack_rcv = 0; /* number of datagrams acknowledged for upstream traffic */
+
+static pthread_mutex_t mx_meas_dw = PTHREAD_MUTEX_INITIALIZER; /* control access to the downstream measurements */
+static uint32_t meas_dw_pull_sent = 0; /* number of PULL requests sent for downstream traffic */
+static uint32_t meas_dw_ack_rcv = 0; /* number of PULL requests acknowledged for downstream traffic */
+static uint32_t meas_dw_dgram_rcv = 0; /* count PULL response packets received for downstream traffic */
+static uint32_t meas_dw_network_byte = 0; /* sum of UDP bytes sent for upstream traffic */
+static uint32_t meas_dw_payload_byte = 0; /* sum of radio payload bytes sent for upstream traffic */
+static uint32_t meas_nb_tx_ok = 0; /* count packets emitted successfully */
+static uint32_t meas_nb_tx_fail = 0; /* count packets were TX failed for other reasons */
+static uint32_t meas_nb_tx_requested = 0; /* count TX request from server (downlinks) */
+static uint32_t meas_nb_tx_rejected_collision_packet = 0; /* count packets were TX request were rejected due to collision with another packet already programmed */
+static uint32_t meas_nb_tx_rejected_collision_beacon = 0; /* count packets were TX request were rejected due to collision with a beacon already programmed */
+static uint32_t meas_nb_tx_rejected_too_late = 0; /* count packets were TX request were rejected because it is too late to program it */
+static uint32_t meas_nb_tx_rejected_too_early = 0; /* count packets were TX request were rejected because timestamp is too much in advance */
+
+static pthread_mutex_t mx_stat_rep = PTHREAD_MUTEX_INITIALIZER; /* control access to the status report */
+static bool report_ready = false; /* true when there is a new report to send to the server */
+static char status_report[STATUS_SIZE]; /* status report as a JSON object */
+
+/* auto-quit function */
+static uint32_t autoquit_threshold = 0; /* enable auto-quit after a number of non-acknowledged PULL_DATA (0 = disabled)*/
+
+/* Just In Time TX scheduling */
+static struct jit_queue_s jit_queue;
+
+/* Gateway specificities */
+static int8_t antenna_gain = 0;
+
+/* TX capabilities */
+static struct lgw_tx_gain_lut_s txlut; /* TX gain table */
+static uint32_t tx_freq_min[LGW_RF_CHAIN_NB]; /* lowest frequency supported by TX chain */
+static uint32_t tx_freq_max[LGW_RF_CHAIN_NB]; /* highest frequency supported by TX chain */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DECLARATION ---------------------------------------- */
+
+static void sig_handler(int sigio);
+
+static int parse_SX1301_configuration(const char * conf_file);
+
+static int parse_gateway_configuration(const char * conf_file);
+
+static double difftimespec(struct timespec end, struct timespec beginning);
+
+/* threads */
+void thread_up(void);
+void thread_down(void);
+void thread_jit(void);
+void thread_timersync(void);
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DEFINITION ----------------------------------------- */
+
+static void exit_cleanup(void) {
+    MSG("INFO: Stopping concentrator\n");
+    lgw_stop();
+}
+
+static void sig_handler(int sigio) {
+    if (sigio == SIGQUIT) {
+        quit_sig = true;
+    } else if ((sigio == SIGINT) || (sigio == SIGTERM)) {
+        exit_sig = true;
+    }
+    return;
+}
+
+static void usage(void) {
+    MSG("~~~ Software versions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
+    MSG(" Packet Forwarder: Version: " VERSION_STRING "\n");
+    MSG(" HAL library: %s\n", lgw_version_info());
+    MSG("~~~ Available options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
+    MSG(" -h  print this help\n");
+    MSG(" -d <path> COM device to be used to access the concentrator board\n");
+    MSG("            => default path: " COM_PATH_DEFAULT "\n");
+    MSG("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
+}
+
+static int parse_SX1301_configuration(const char * conf_file) {
+    int i;
+    char param_name[32]; /* used to generate variable parameter names */
+    const char *str; /* used to store string value from JSON object */
+    const char conf_obj_name[] = "SX1301_conf";
+    JSON_Value *root_val = NULL;
+    JSON_Object *conf_obj = NULL;
+    JSON_Value *val = NULL;
+    struct lgw_conf_board_s boardconf;
+    struct lgw_conf_rxrf_s rfconf;
+    struct lgw_conf_rxif_s ifconf;
+    uint32_t sf, bw, fdev;
+
+    /* try to parse JSON */
+    root_val = json_parse_file_with_comments(conf_file);
+    if (root_val == NULL) {
+        MSG("ERROR: %s is not a valid JSON file\n", conf_file);
+        exit(EXIT_FAILURE);
+    }
+
+    /* point to the gateway configuration object */
+    conf_obj = json_object_get_object(json_value_get_object(root_val), conf_obj_name);
+    if (conf_obj == NULL) {
+        MSG("INFO: %s does not contain a JSON object named %s\n", conf_file, conf_obj_name);
+        return -1;
+    } else {
+        MSG("INFO: %s does contain a JSON object named %s, parsing SX1301 parameters\n", conf_file, conf_obj_name);
+    }
+
+    /* set board configuration */
+    memset(&boardconf, 0, sizeof boardconf); /* initialize configuration structure */
+    val = json_object_get_value(conf_obj, "lorawan_public"); /* fetch value (if possible) */
+    if (json_value_get_type(val) == JSONBoolean) {
+        boardconf.lorawan_public = (bool)json_value_get_boolean(val);
+    } else {
+        MSG("WARNING: Data type for lorawan_public seems wrong, please check\n");
+        boardconf.lorawan_public = false;
+    }
+    val = json_object_get_value(conf_obj, "clksrc"); /* fetch value (if possible) */
+    if (json_value_get_type(val) == JSONNumber) {
+        boardconf.clksrc = (uint8_t)json_value_get_number(val);
+    } else {
+        MSG("WARNING: Data type for clksrc seems wrong, please check\n");
+        boardconf.clksrc = 0;
+    }
+    MSG("INFO: lorawan_public %d, clksrc %d\n", boardconf.lorawan_public, boardconf.clksrc);
+    /* all parameters parsed, submitting configuration to the HAL */
+    if (lgw_board_setconf(boardconf) != LGW_HAL_SUCCESS) {
+        MSG("ERROR: Failed to configure board\n");
+        return -1;
+    }
+
+    /* set antenna gain configuration */
+    val = json_object_get_value(conf_obj, "antenna_gain"); /* fetch value (if possible) */
+    if (val != NULL) {
+        if (json_value_get_type(val) == JSONNumber) {
+            antenna_gain = (int8_t)json_value_get_number(val);
+        } else {
+            MSG("WARNING: Data type for antenna_gain seems wrong, please check\n");
+            antenna_gain = 0;
+        }
+    }
+    MSG("INFO: antenna_gain %d dBi\n", antenna_gain);
+
+    /* set configuration for tx gains */
+    memset(&txlut, 0, sizeof txlut); /* initialize configuration structure */
+    for (i = 0; i < TX_GAIN_LUT_SIZE_MAX; i++) {
+        snprintf(param_name, sizeof param_name, "tx_lut_%i", i); /* compose parameter path inside JSON structure */
+        val = json_object_get_value(conf_obj, param_name); /* fetch value (if possible) */
+        if (json_value_get_type(val) != JSONObject) {
+            MSG("INFO: no configuration for tx gain lut %i\n", i);
+            continue;
+        }
+        txlut.size++; /* update TX LUT size based on JSON object found in configuration file */
+        /* there is an object to configure that TX gain index, let's parse it */
+        snprintf(param_name, sizeof param_name, "tx_lut_%i.pa_gain", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONNumber) {
+            txlut.lut[i].pa_gain = (uint8_t)json_value_get_number(val);
+        } else {
+            MSG("WARNING: Data type for %s[%d] seems wrong, please check\n", param_name, i);
+            txlut.lut[i].pa_gain = 0;
+        }
+        snprintf(param_name, sizeof param_name, "tx_lut_%i.dac_gain", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONNumber) {
+            txlut.lut[i].dac_gain = (uint8_t)json_value_get_number(val);
+        } else {
+            txlut.lut[i].dac_gain = 3; /* This is the only dac_gain supported for now */
+        }
+        snprintf(param_name, sizeof param_name, "tx_lut_%i.dig_gain", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONNumber) {
+            txlut.lut[i].dig_gain = (uint8_t)json_value_get_number(val);
+        } else {
+            MSG("WARNING: Data type for %s[%d] seems wrong, please check\n", param_name, i);
+            txlut.lut[i].dig_gain = 0;
+        }
+        snprintf(param_name, sizeof param_name, "tx_lut_%i.mix_gain", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONNumber) {
+            txlut.lut[i].mix_gain = (uint8_t)json_value_get_number(val);
+        } else {
+            MSG("WARNING: Data type for %s[%d] seems wrong, please check\n", param_name, i);
+            txlut.lut[i].mix_gain = 0;
+        }
+        snprintf(param_name, sizeof param_name, "tx_lut_%i.rf_power", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONNumber) {
+            txlut.lut[i].rf_power = (int8_t)json_value_get_number(val);
+        } else {
+            MSG("WARNING: Data type for %s[%d] seems wrong, please check\n", param_name, i);
+            txlut.lut[i].rf_power = 0;
+        }
+    }
+    /* all parameters parsed, submitting configuration to the HAL */
+    if (txlut.size > 0) {
+        MSG("INFO: Configuring TX LUT with %u indexes\n", txlut.size);
+        if (lgw_txgain_setconf(&txlut) != LGW_HAL_SUCCESS) {
+            MSG("ERROR: Failed to configure concentrator TX Gain LUT\n");
+            return -1;
+        }
+    } else {
+        MSG("WARNING: No TX gain LUT defined\n");
+    }
+
+    /* set configuration for RF chains */
+    for (i = 0; i < LGW_RF_CHAIN_NB; ++i) {
+        memset(&rfconf, 0, sizeof rfconf); /* initialize configuration structure */
+        snprintf(param_name, sizeof param_name, "radio_%i", i); /* compose parameter path inside JSON structure */
+        val = json_object_get_value(conf_obj, param_name); /* fetch value (if possible) */
+        if (json_value_get_type(val) != JSONObject) {
+            MSG("INFO: no configuration for radio %i\n", i);
+            continue;
+        }
+        /* there is an object to configure that radio, let's parse it */
+        snprintf(param_name, sizeof param_name, "radio_%i.enable", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONBoolean) {
+            rfconf.enable = (bool)json_value_get_boolean(val);
+        } else {
+            rfconf.enable = false;
+        }
+        if (rfconf.enable == false) { /* radio disabled, nothing else to parse */
+            MSG("INFO: radio %i disabled\n", i);
+        } else  { /* radio enabled, will parse the other parameters */
+            snprintf(param_name, sizeof param_name, "radio_%i.freq", i);
+            rfconf.freq_hz = (uint32_t)json_object_dotget_number(conf_obj, param_name);
+            snprintf(param_name, sizeof param_name, "radio_%i.rssi_offset", i);
+            rfconf.rssi_offset = (float)json_object_dotget_number(conf_obj, param_name);
+            snprintf(param_name, sizeof param_name, "radio_%i.type", i);
+            str = json_object_dotget_string(conf_obj, param_name);
+            if (!strncmp(str, "SX1255", 6)) {
+                rfconf.type = LGW_RADIO_TYPE_SX1255;
+            } else if (!strncmp(str, "SX1257", 6)) {
+                rfconf.type = LGW_RADIO_TYPE_SX1257;
+            } else {
+                MSG("WARNING: invalid radio type: %s (should be SX1255 or SX1257)\n", str);
+            }
+            snprintf(param_name, sizeof param_name, "radio_%i.tx_enable", i);
+            val = json_object_dotget_value(conf_obj, param_name);
+            if (json_value_get_type(val) == JSONBoolean) {
+                rfconf.tx_enable = (bool)json_value_get_boolean(val);
+                if (rfconf.tx_enable == true) {
+                    /* tx is enabled on this rf chain, we need its frequency range */
+                    snprintf(param_name, sizeof param_name, "radio_%i.tx_freq_min", i);
+                    tx_freq_min[i] = (uint32_t)json_object_dotget_number(conf_obj, param_name);
+                    snprintf(param_name, sizeof param_name, "radio_%i.tx_freq_max", i);
+                    tx_freq_max[i] = (uint32_t)json_object_dotget_number(conf_obj, param_name);
+                    if ((tx_freq_min[i] == 0) || (tx_freq_max[i] == 0)) {
+                        MSG("WARNING: no frequency range specified for TX rf chain %d\n", i);
+                    }
+                }
+            } else {
+                rfconf.tx_enable = false;
+            }
+            MSG("INFO: radio %i enabled (type %s), center frequency %u, RSSI offset %f, tx enabled %d\n", i, str, rfconf.freq_hz, rfconf.rssi_offset, rfconf.tx_enable);
+        }
+        /* all parameters parsed, submitting configuration to the HAL */
+        if (lgw_rxrf_setconf(i, rfconf) != LGW_HAL_SUCCESS) {
+            MSG("ERROR: invalid configuration for radio %i\n", i);
+            return -1;
+        }
+    }
+
+    /* set configuration for Lora multi-SF channels (bandwidth cannot be set) */
+    for (i = 0; i < LGW_MULTI_NB; ++i) {
+        memset(&ifconf, 0, sizeof ifconf); /* initialize configuration structure */
+        snprintf(param_name, sizeof param_name, "chan_multiSF_%i", i); /* compose parameter path inside JSON structure */
+        val = json_object_get_value(conf_obj, param_name); /* fetch value (if possible) */
+        if (json_value_get_type(val) != JSONObject) {
+            MSG("INFO: no configuration for Lora multi-SF channel %i\n", i);
+            continue;
+        }
+        /* there is an object to configure that Lora multi-SF channel, let's parse it */
+        snprintf(param_name, sizeof param_name, "chan_multiSF_%i.enable", i);
+        val = json_object_dotget_value(conf_obj, param_name);
+        if (json_value_get_type(val) == JSONBoolean) {
+            ifconf.enable = (bool)json_value_get_boolean(val);
+        } else {
+            ifconf.enable = false;
+        }
+        if (ifconf.enable == false) { /* Lora multi-SF channel disabled, nothing else to parse */
+            MSG("INFO: Lora multi-SF channel %i disabled\n", i);
+        } else  { /* Lora multi-SF channel enabled, will parse the other parameters */
+            snprintf(param_name, sizeof param_name, "chan_multiSF_%i.radio", i);
+            ifconf.rf_chain = (uint32_t)json_object_dotget_number(conf_obj, param_name);
+            snprintf(param_name, sizeof param_name, "chan_multiSF_%i.if", i);
+            ifconf.freq_hz = (int32_t)json_object_dotget_number(conf_obj, param_name);
+            // TODO: handle individual SF enabling and disabling (spread_factor)
+            MSG("INFO: Lora multi-SF channel %i>  radio %i, IF %i Hz, 125 kHz bw, SF 7 to 12\n", i, ifconf.rf_chain, ifconf.freq_hz);
+        }
+        /* all parameters parsed, submitting configuration to the HAL */
+        if (lgw_rxif_setconf(i, ifconf) != LGW_HAL_SUCCESS) {
+            MSG("ERROR: invalid configuration for Lora multi-SF channel %i\n", i);
+            return -1;
+        }
+    }
+
+    /* set configuration for Lora standard channel */
+    memset(&ifconf, 0, sizeof ifconf); /* initialize configuration structure */
+    val = json_object_get_value(conf_obj, "chan_Lora_std"); /* fetch value (if possible) */
+    if (json_value_get_type(val) != JSONObject) {
+        MSG("INFO: no configuration for Lora standard channel\n");
+    } else {
+        val = json_object_dotget_value(conf_obj, "chan_Lora_std.enable");
+        if (json_value_get_type(val) == JSONBoolean) {
+            ifconf.enable = (bool)json_value_get_boolean(val);
+        } else {
+            ifconf.enable = false;
+        }
+        if (ifconf.enable == false) {
+            MSG("INFO: Lora standard channel %i disabled\n", i);
+        } else  {
+            ifconf.rf_chain = (uint32_t)json_object_dotget_number(conf_obj, "chan_Lora_std.radio");
+            ifconf.freq_hz = (int32_t)json_object_dotget_number(conf_obj, "chan_Lora_std.if");
+            bw = (uint32_t)json_object_dotget_number(conf_obj, "chan_Lora_std.bandwidth");
+            switch(bw) {
+                case 500000:
+                    ifconf.bandwidth = BW_500KHZ;
+                    break;
+                case 250000:
+                    ifconf.bandwidth = BW_250KHZ;
+                    break;
+                case 125000:
+                    ifconf.bandwidth = BW_125KHZ;
+                    break;
+                default:
+                    ifconf.bandwidth = BW_UNDEFINED;
+            }
+            sf = (uint32_t)json_object_dotget_number(conf_obj, "chan_Lora_std.spread_factor");
+            switch(sf) {
+                case  7:
+                    ifconf.datarate = DR_LORA_SF7;
+                    break;
+                case  8:
+                    ifconf.datarate = DR_LORA_SF8;
+                    break;
+                case  9:
+                    ifconf.datarate = DR_LORA_SF9;
+                    break;
+                case 10:
+                    ifconf.datarate = DR_LORA_SF10;
+                    break;
+                case 11:
+                    ifconf.datarate = DR_LORA_SF11;
+                    break;
+                case 12:
+                    ifconf.datarate = DR_LORA_SF12;
+                    break;
+                default:
+                    ifconf.datarate = DR_UNDEFINED;
+            }
+            MSG("INFO: Lora std channel> radio %i, IF %i Hz, %u Hz bw, SF %u\n", ifconf.rf_chain, ifconf.freq_hz, bw, sf);
+        }
+        if (lgw_rxif_setconf(8, ifconf) != LGW_HAL_SUCCESS) {
+            MSG("ERROR: invalid configuration for Lora standard channel\n");
+            return -1;
+        }
+    }
+
+    /* set configuration for FSK channel */
+    memset(&ifconf, 0, sizeof ifconf); /* initialize configuration structure */
+    val = json_object_get_value(conf_obj, "chan_FSK"); /* fetch value (if possible) */
+    if (json_value_get_type(val) != JSONObject) {
+        MSG("INFO: no configuration for FSK channel\n");
+    } else {
+        val = json_object_dotget_value(conf_obj, "chan_FSK.enable");
+        if (json_value_get_type(val) == JSONBoolean) {
+            ifconf.enable = (bool)json_value_get_boolean(val);
+        } else {
+            ifconf.enable = false;
+        }
+        if (ifconf.enable == false) {
+            MSG("INFO: FSK channel %i disabled\n", i);
+        } else  {
+            ifconf.rf_chain = (uint32_t)json_object_dotget_number(conf_obj, "chan_FSK.radio");
+            ifconf.freq_hz = (int32_t)json_object_dotget_number(conf_obj, "chan_FSK.if");
+            bw = (uint32_t)json_object_dotget_number(conf_obj, "chan_FSK.bandwidth");
+            fdev = (uint32_t)json_object_dotget_number(conf_obj, "chan_FSK.freq_deviation");
+            ifconf.datarate = (uint32_t)json_object_dotget_number(conf_obj, "chan_FSK.datarate");
+
+            /* if chan_FSK.bandwidth is set, it has priority over chan_FSK.freq_deviation */
+            if ((bw == 0) && (fdev != 0)) {
+                bw = 2 * fdev + ifconf.datarate;
+            }
+            if      (bw == 0) {
+                ifconf.bandwidth = BW_UNDEFINED;
+            } else if (bw <= 7800) {
+                ifconf.bandwidth = BW_7K8HZ;
+            } else if (bw <= 15600) {
+                ifconf.bandwidth = BW_15K6HZ;
+            } else if (bw <= 31200) {
+                ifconf.bandwidth = BW_31K2HZ;
+            } else if (bw <= 62500) {
+                ifconf.bandwidth = BW_62K5HZ;
+            } else if (bw <= 125000) {
+                ifconf.bandwidth = BW_125KHZ;
+            } else if (bw <= 250000) {
+                ifconf.bandwidth = BW_250KHZ;
+            } else if (bw <= 500000) {
+                ifconf.bandwidth = BW_500KHZ;
+            } else {
+                ifconf.bandwidth = BW_UNDEFINED;
+            }
+
+            MSG("INFO: FSK channel> radio %i, IF %i Hz, %u Hz bw, %u bps datarate\n", ifconf.rf_chain, ifconf.freq_hz, bw, ifconf.datarate);
+        }
+        if (lgw_rxif_setconf(9, ifconf) != LGW_HAL_SUCCESS) {
+            MSG("ERROR: invalid configuration for FSK channel\n");
+            return -1;
+        }
+    }
+    json_value_free(root_val);
+
+    return 0;
+}
+
+static int parse_gateway_configuration(const char * conf_file) {
+    const char conf_obj_name[] = "gateway_conf";
+    JSON_Value *root_val;
+    JSON_Object *conf_obj = NULL;
+    JSON_Value *val = NULL; /* needed to detect the absence of some fields */
+    const char *str; /* pointer to sub-strings in the JSON data */
+    unsigned long long ull = 0;
+
+    /* try to parse JSON */
+    root_val = json_parse_file_with_comments(conf_file);
+    if (root_val == NULL) {
+        MSG("ERROR: %s is not a valid JSON file\n", conf_file);
+        exit(EXIT_FAILURE);
+    }
+
+    /* point to the gateway configuration object */
+    conf_obj = json_object_get_object(json_value_get_object(root_val), conf_obj_name);
+    if (conf_obj == NULL) {
+        MSG("INFO: %s does not contain a JSON object named %s\n", conf_file, conf_obj_name);
+        return -1;
+    } else {
+        MSG("INFO: %s does contain a JSON object named %s, parsing gateway parameters\n", conf_file, conf_obj_name);
+    }
+
+    /* gateway unique identifier (aka MAC address) (optional) */
+    str = json_object_get_string(conf_obj, "gateway_ID");
+    if (str != NULL) {
+        sscanf(str, "%llx", &ull);
+        lgwm = ull;
+        MSG("INFO: gateway MAC address is configured to %016llX\n", ull);
+    }
+
+    /* server hostname or IP address (optional) */
+    str = json_object_get_string(conf_obj, "server_address");
+    if (str != NULL) {
+        strncpy(serv_addr, str, sizeof serv_addr);
+        MSG("INFO: server hostname or IP address is configured to \"%s\"\n", serv_addr);
+    }
+
+    /* get up and down ports (optional) */
+    val = json_object_get_value(conf_obj, "serv_port_up");
+    if (val != NULL) {
+        snprintf(serv_port_up, sizeof serv_port_up, "%u", (uint16_t)json_value_get_number(val));
+        MSG("INFO: upstream port is configured to \"%s\"\n", serv_port_up);
+    }
+    val = json_object_get_value(conf_obj, "serv_port_down");
+    if (val != NULL) {
+        snprintf(serv_port_down, sizeof serv_port_down, "%u", (uint16_t)json_value_get_number(val));
+        MSG("INFO: downstream port is configured to \"%s\"\n", serv_port_down);
+    }
+
+    /* get keep-alive interval (in seconds) for downstream (optional) */
+    val = json_object_get_value(conf_obj, "keepalive_interval");
+    if (val != NULL) {
+        keepalive_time = (int)json_value_get_number(val);
+        MSG("INFO: downstream keep-alive interval is configured to %u seconds\n", keepalive_time);
+    }
+
+    /* get interval (in seconds) for statistics display (optional) */
+    val = json_object_get_value(conf_obj, "stat_interval");
+    if (val != NULL) {
+        stat_interval = (unsigned)json_value_get_number(val);
+        MSG("INFO: statistics display interval is configured to %u seconds\n", stat_interval);
+    }
+
+    /* get time-out value (in ms) for upstream datagrams (optional) */
+    val = json_object_get_value(conf_obj, "push_timeout_ms");
+    if (val != NULL) {
+        push_timeout_half.tv_usec = 500 * (long int)json_value_get_number(val);
+        MSG("INFO: upstream PUSH_DATA time-out is configured to %u ms\n", (unsigned)(push_timeout_half.tv_usec / 500));
+    }
+
+    /* packet filtering parameters */
+    val = json_object_get_value(conf_obj, "forward_crc_valid");
+    if (json_value_get_type(val) == JSONBoolean) {
+        fwd_valid_pkt = (bool)json_value_get_boolean(val);
+    }
+    MSG("INFO: packets received with a valid CRC will%s be forwarded\n", (fwd_valid_pkt ? "" : " NOT"));
+    val = json_object_get_value(conf_obj, "forward_crc_error");
+    if (json_value_get_type(val) == JSONBoolean) {
+        fwd_error_pkt = (bool)json_value_get_boolean(val);
+    }
+    MSG("INFO: packets received with a CRC error will%s be forwarded\n", (fwd_error_pkt ? "" : " NOT"));
+    val = json_object_get_value(conf_obj, "forward_crc_disabled");
+    if (json_value_get_type(val) == JSONBoolean) {
+        fwd_nocrc_pkt = (bool)json_value_get_boolean(val);
+    }
+    MSG("INFO: packets received with no CRC will%s be forwarded\n", (fwd_nocrc_pkt ? "" : " NOT"));
+
+    /* get reference coordinates */
+    val = json_object_get_value(conf_obj, "ref_latitude");
+    if (val != NULL) {
+        reference_coord.lat = (double)json_value_get_number(val);
+        MSG("INFO: Reference latitude is configured to %f deg\n", reference_coord.lat);
+    }
+    val = json_object_get_value(conf_obj, "ref_longitude");
+    if (val != NULL) {
+        reference_coord.lon = (double)json_value_get_number(val);
+        MSG("INFO: Reference longitude is configured to %f deg\n", reference_coord.lon);
+    }
+    val = json_object_get_value(conf_obj, "ref_altitude");
+    if (val != NULL) {
+        reference_coord.alt = (short)json_value_get_number(val);
+        MSG("INFO: Reference altitude is configured to %i meters\n", reference_coord.alt);
+    }
+
+    /* Gateway GPS coordinates hardcoding (aka. faking) option */
+    val = json_object_get_value(conf_obj, "fake_gps");
+    if (json_value_get_type(val) == JSONBoolean) {
+        gps_fake_enable = (bool)json_value_get_boolean(val);
+        if (gps_fake_enable == true) {
+            MSG("INFO: fake GPS is enabled\n");
+        } else {
+            MSG("INFO: fake GPS is disabled\n");
+        }
+    }
+
+    /* Auto-quit threshold (optional) */
+    val = json_object_get_value(conf_obj, "autoquit_threshold");
+    if (val != NULL) {
+        autoquit_threshold = (uint32_t)json_value_get_number(val);
+        MSG("INFO: Auto-quit after %u non-acknowledged PULL_DATA\n", autoquit_threshold);
+    }
+
+    /* free JSON parsing data structure */
+    json_value_free(root_val);
+    return 0;
+}
+
+static double difftimespec(struct timespec end, struct timespec beginning) {
+    double x;
+
+    x = 1E-9 * (double)(end.tv_nsec - beginning.tv_nsec);
+    x += (double)(end.tv_sec - beginning.tv_sec);
+
+    return x;
+}
+
+static int send_tx_ack(uint8_t token_h, uint8_t token_l, enum jit_error_e error) {
+    uint8_t buff_ack[64]; /* buffer to give feedback to server */
+    int buff_index;
+
+    /* reset buffer */
+    memset(&buff_ack, 0, sizeof buff_ack);
+
+    /* Prepare downlink feedback to be sent to server */
+    buff_ack[0] = PROTOCOL_VERSION;
+    buff_ack[1] = token_h;
+    buff_ack[2] = token_l;
+    buff_ack[3] = PKT_TX_ACK;
+    *(uint32_t *)(buff_ack + 4) = net_mac_h;
+    *(uint32_t *)(buff_ack + 8) = net_mac_l;
+    buff_index = 12; /* 12-byte header */
+
+    /* Put no JSON string if there is nothing to report */
+    if (error != JIT_ERROR_OK) {
+        /* start of JSON structure */
+        memcpy((void *)(buff_ack + buff_index), (void *)"{\"txpk_ack\":{", 13);
+        buff_index += 13;
+        /* set downlink error status in JSON structure */
+        memcpy((void *)(buff_ack + buff_index), (void *)"\"error\":", 8);
+        buff_index += 8;
+        switch (error) {
+            case JIT_ERROR_FULL:
+            case JIT_ERROR_COLLISION_PACKET:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"COLLISION_PACKET\"", 18);
+                buff_index += 18;
+                /* update stats */
+                pthread_mutex_lock(&mx_meas_dw);
+                meas_nb_tx_rejected_collision_packet += 1;
+                pthread_mutex_unlock(&mx_meas_dw);
+                break;
+            case JIT_ERROR_TOO_LATE:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"TOO_LATE\"", 10);
+                buff_index += 10;
+                /* update stats */
+                pthread_mutex_lock(&mx_meas_dw);
+                meas_nb_tx_rejected_too_late += 1;
+                pthread_mutex_unlock(&mx_meas_dw);
+                break;
+            case JIT_ERROR_TOO_EARLY:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"TOO_EARLY\"", 11);
+                buff_index += 11;
+                /* update stats */
+                pthread_mutex_lock(&mx_meas_dw);
+                meas_nb_tx_rejected_too_early += 1;
+                pthread_mutex_unlock(&mx_meas_dw);
+                break;
+            case JIT_ERROR_COLLISION_BEACON:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"COLLISION_BEACON\"", 18);
+                buff_index += 18;
+                /* update stats */
+                pthread_mutex_lock(&mx_meas_dw);
+                meas_nb_tx_rejected_collision_beacon += 1;
+                pthread_mutex_unlock(&mx_meas_dw);
+                break;
+            case JIT_ERROR_TX_FREQ:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"TX_FREQ\"", 9);
+                buff_index += 9;
+                break;
+            case JIT_ERROR_TX_POWER:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"TX_POWER\"", 10);
+                buff_index += 10;
+                break;
+            case JIT_ERROR_GPS_UNLOCKED:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"GPS_UNLOCKED\"", 14);
+                buff_index += 14;
+                break;
+            default:
+                memcpy((void *)(buff_ack + buff_index), (void *)"\"UNKNOWN\"", 9);
+                buff_index += 9;
+                break;
+        }
+        /* end of JSON structure */
+        memcpy((void *)(buff_ack + buff_index), (void *)"}}", 2);
+        buff_index += 2;
+    }
+
+    buff_ack[buff_index] = 0; /* add string terminator, for safety */
+
+    /* send datagram to server */
+    return send(sock_down, (void *)buff_ack, buff_index, 0);
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- MAIN FUNCTION -------------------------------------------------------- */
+
+int main(int argc, char **argv) {
+    struct sigaction sigact; /* SIGQUIT&SIGINT&SIGTERM signal handling */
+    int i; /* loop variable and temporary variable for return value */
+    int x;
+
+    /* COM interfaces */
+    const char com_path_default[] = COM_PATH_DEFAULT;
+    const char *com_path = com_path_default;
+
+    /* configuration file related */
+    char *global_cfg_path = "global_conf.json"; /* contain global (typ. network-wide) configuration */
+    char *local_cfg_path = "local_conf.json"; /* contain node specific configuration, overwrite global parameters for parameters that are defined in both */
+    char *debug_cfg_path = "debug_conf.json"; /* if present, all other configuration files are ignored */
+
+    /* threads */
+    pthread_t thrid_up;
+    pthread_t thrid_down;
+    pthread_t thrid_jit;
+    pthread_t thrid_timersync;
+
+    /* network socket creation */
+    struct addrinfo hints;
+    struct addrinfo *result; /* store result of getaddrinfo */
+    struct addrinfo *q; /* pointer to move into *result data */
+    char host_name[64];
+    char port_name[64];
+
+    /* variables to get local copies of measurements */
+    uint32_t cp_nb_rx_rcv;
+    uint32_t cp_nb_rx_ok;
+    uint32_t cp_nb_rx_bad;
+    uint32_t cp_nb_rx_nocrc;
+    uint32_t cp_up_pkt_fwd;
+    uint32_t cp_up_network_byte;
+    uint32_t cp_up_payload_byte;
+    uint32_t cp_up_dgram_sent;
+    uint32_t cp_up_ack_rcv;
+    uint32_t cp_dw_pull_sent;
+    uint32_t cp_dw_ack_rcv;
+    uint32_t cp_dw_dgram_rcv;
+    uint32_t cp_dw_network_byte;
+    uint32_t cp_dw_payload_byte;
+    uint32_t cp_nb_tx_ok;
+    uint32_t cp_nb_tx_fail;
+    uint32_t cp_nb_tx_requested = 0;
+    uint32_t cp_nb_tx_rejected_collision_packet = 0;
+    uint32_t cp_nb_tx_rejected_collision_beacon = 0;
+    uint32_t cp_nb_tx_rejected_too_late = 0;
+    uint32_t cp_nb_tx_rejected_too_early = 0;
+
+    /* GPS coordinates variables */
+    struct coord_s cp_gps_coord = {0.0, 0.0, 0};
+
+    /* statistics variable */
+    time_t t;
+    char stat_timestamp[24];
+    float rx_ok_ratio;
+    float rx_bad_ratio;
+    float rx_nocrc_ratio;
+    float up_ack_ratio;
+    float dw_ack_ratio;
+
+    /* Parse command line options */
+    while((i = getopt(argc, argv, "hd:")) != -1) {
+        switch(i) {
+        case 'h':
+            usage();
+            return EXIT_SUCCESS;
+
+        case 'd':
+            if (optarg != NULL) {
+                com_path = optarg;
+            }
+            break;
+
+        default:
+            MSG("ERROR: argument parsing options, use -h option for help\n");
+            usage();
+            return EXIT_FAILURE;
+        }
+    }
+
+    /* display version informations */
+    MSG("*** Packet Forwarder for Lora PicoCell Gateway ***\nVersion: " VERSION_STRING "\n");
+    MSG("*** Lora concentrator HAL library version info ***\n%s\n", lgw_version_info());
+
+    /* register function to be called for exit cleanups */
+    atexit(exit_cleanup);
+
+    /* configure signal handling */
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigact.sa_handler = sig_handler;
+    sigaction(SIGQUIT, &sigact, NULL); /* Ctrl-\ */
+    sigaction(SIGINT, &sigact, NULL); /* Ctrl-C */
+    sigaction(SIGTERM, &sigact, NULL); /* default "kill" command */
+
+    /* Open communication bridge */
+    x = lgw_connect(com_path);
+    if (x == LGW_REG_ERROR) {
+        MSG("ERROR: FAIL TO CONNECT BOARD ON %s\n", com_path);
+        exit(EXIT_FAILURE);
+    }
+
+    MSG("*** MCU FW version for LoRa PicoCell Gateway ***\nVersion: 0x%08X\n***\n", lgw_mcu_version_info());
+
+    /* display host endianness */
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+    MSG("INFO: Little endian host\n");
+#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+    MSG("INFO: Big endian host\n");
+#else
+    MSG("INFO: Host endianness unknown\n");
+#endif
+
+    /* load configuration files */
+    if (access(debug_cfg_path, R_OK) == 0) { /* if there is a debug conf, parse only the debug conf */
+        MSG("INFO: found debug configuration file %s, parsing it\n", debug_cfg_path);
+        MSG("INFO: other configuration files will be ignored\n");
+        x = parse_SX1301_configuration(debug_cfg_path);
+        if (x != 0) {
+            exit(EXIT_FAILURE);
+        }
+        x = parse_gateway_configuration(debug_cfg_path);
+        if (x != 0) {
+            exit(EXIT_FAILURE);
+        }
+    } else if (access(global_cfg_path, R_OK) == 0) { /* if there is a global conf, parse it and then try to parse local conf  */
+        MSG("INFO: found global configuration file %s, parsing it\n", global_cfg_path);
+        x = parse_SX1301_configuration(global_cfg_path);
+        if (x != 0) {
+            exit(EXIT_FAILURE);
+        }
+        x = parse_gateway_configuration(global_cfg_path);
+        if (x != 0) {
+            exit(EXIT_FAILURE);
+        }
+        if (access(local_cfg_path, R_OK) == 0) {
+            MSG("INFO: found local configuration file %s, parsing it\n", local_cfg_path);
+            MSG("INFO: redefined parameters will overwrite global parameters\n");
+            parse_SX1301_configuration(local_cfg_path);
+            parse_gateway_configuration(local_cfg_path);
+        }
+    } else if (access(local_cfg_path, R_OK) == 0) { /* if there is only a local conf, parse it and that's all */
+        MSG("INFO: found local configuration file %s, parsing it\n", local_cfg_path);
+        x = parse_SX1301_configuration(local_cfg_path);
+        if (x != 0) {
+            exit(EXIT_FAILURE);
+        }
+        x = parse_gateway_configuration(local_cfg_path);
+        if (x != 0) {
+            exit(EXIT_FAILURE);
+        }
+    } else {
+        MSG("ERROR: [main] failed to find any configuration file named %s, %s OR %s\n", global_cfg_path, local_cfg_path, debug_cfg_path);
+        exit(EXIT_FAILURE);
+    }
+
+    /* get timezone info */
+    tzset();
+
+    /* sanity check on configuration variables */
+    // TODO
+
+    /* process some of the configuration variables */
+    net_mac_h = htonl((uint32_t)(0xFFFFFFFF & (lgwm >> 32)));
+    net_mac_l = htonl((uint32_t)(0xFFFFFFFF &  lgwm  ));
+
+    /* prepare hints to open network sockets */
+    memset(&hints, 0, sizeof hints);
+    hints.ai_family = AF_INET; /* WA: Forcing IPv4 as AF_UNSPEC makes connection on localhost to fail */
+    hints.ai_socktype = SOCK_DGRAM;
+
+    /* look for server address w/ upstream port */
+    i = getaddrinfo(serv_addr, serv_port_up, &hints, &result);
+    if (i != 0) {
+        MSG("ERROR: [up] getaddrinfo on address %s (PORT %s) returned %s\n", serv_addr, serv_port_up, gai_strerror(i));
+        exit(EXIT_FAILURE);
+    }
+
+    /* try to open socket for upstream traffic */
+    for (q = result; q != NULL; q = q->ai_next) {
+        sock_up = socket(q->ai_family, q->ai_socktype, q->ai_protocol);
+        if (sock_up == -1) {
+            continue;    /* try next field */
+        } else {
+            break;    /* success, get out of loop */
+        }
+    }
+    if (q == NULL) {
+        MSG("ERROR: [up] failed to open socket to any of server %s addresses (port %s)\n", serv_addr, serv_port_up);
+        i = 1;
+        for (q = result; q != NULL; q = q->ai_next) {
+            getnameinfo(q->ai_addr, q->ai_addrlen, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+            MSG("INFO: [up] result %i host:%s service:%s\n", i, host_name, port_name);
+            ++i;
+        }
+        exit(EXIT_FAILURE);
+    }
+
+    /* connect so we can send/receive packet with the server only */
+    i = connect(sock_up, q->ai_addr, q->ai_addrlen);
+    if (i != 0) {
+        MSG("ERROR: [up] connect returned %s\n", strerror(errno));
+        exit(EXIT_FAILURE);
+    }
+    freeaddrinfo(result);
+
+    /* look for server address w/ downstream port */
+    i = getaddrinfo(serv_addr, serv_port_down, &hints, &result);
+    if (i != 0) {
+        MSG("ERROR: [down] getaddrinfo on address %s (port %s) returned %s\n", serv_addr, serv_port_up, gai_strerror(i));
+        exit(EXIT_FAILURE);
+    }
+
+    /* try to open socket for downstream traffic */
+    for (q = result; q != NULL; q = q->ai_next) {
+        sock_down = socket(q->ai_family, q->ai_socktype, q->ai_protocol);
+        if (sock_down == -1) {
+            continue;    /* try next field */
+        } else {
+            break;    /* success, get out of loop */
+        }
+    }
+    if (q == NULL) {
+        MSG("ERROR: [down] failed to open socket to any of server %s addresses (port %s)\n", serv_addr, serv_port_up);
+        i = 1;
+        for (q = result; q != NULL; q = q->ai_next) {
+            getnameinfo(q->ai_addr, q->ai_addrlen, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+            MSG("INFO: [down] result %i host:%s service:%s\n", i, host_name, port_name);
+            ++i;
+        }
+        exit(EXIT_FAILURE);
+    }
+
+    /* connect so we can send/receive packet with the server only */
+    i = connect(sock_down, q->ai_addr, q->ai_addrlen);
+    if (i != 0) {
+        MSG("ERROR: [down] connect returned %s\n", strerror(errno));
+        exit(EXIT_FAILURE);
+    }
+    freeaddrinfo(result);
+
+    /* starting the concentrator */
+    i = lgw_start();
+    if (i == LGW_HAL_SUCCESS) {
+        MSG("INFO: [main] concentrator started, packet can now be received\n");
+    } else {
+        MSG("ERROR: [main] failed to start the concentrator\n");
+        exit(EXIT_FAILURE);
+    }
+
+    /* spawn threads to manage upstream and downstream */
+    i = pthread_create( &thrid_up, NULL, (void * (*)(void *))thread_up, NULL);
+    if (i != 0) {
+        MSG("ERROR: [main] impossible to create upstream thread\n");
+        exit(EXIT_FAILURE);
+    }
+    i = pthread_create( &thrid_down, NULL, (void * (*)(void *))thread_down, NULL);
+    if (i != 0) {
+        MSG("ERROR: [main] impossible to create downstream thread\n");
+        exit(EXIT_FAILURE);
+    }
+    i = pthread_create( &thrid_jit, NULL, (void * (*)(void *))thread_jit, NULL);
+    if (i != 0) {
+        MSG("ERROR: [main] impossible to create JIT thread\n");
+        exit(EXIT_FAILURE);
+    }
+    i = pthread_create( &thrid_timersync, NULL, (void * (*)(void *))thread_timersync, NULL);
+    if (i != 0) {
+        MSG("ERROR: [main] impossible to create Timer Sync thread\n");
+        exit(EXIT_FAILURE);
+    }
+
+    /* main loop task : statistics collection */
+    while (!exit_sig && !quit_sig) {
+        /* wait for next reporting interval */
+        wait_ms(1000 * stat_interval);
+
+        /* get timestamp for statistics */
+        t = time(NULL);
+        strftime(stat_timestamp, sizeof stat_timestamp, "%F %T %Z", gmtime(&t));
+
+        /* access upstream statistics, copy and reset them */
+        pthread_mutex_lock(&mx_meas_up);
+        cp_nb_rx_rcv       = meas_nb_rx_rcv;
+        cp_nb_rx_ok        = meas_nb_rx_ok;
+        cp_nb_rx_bad       = meas_nb_rx_bad;
+        cp_nb_rx_nocrc     = meas_nb_rx_nocrc;
+        cp_up_pkt_fwd      = meas_up_pkt_fwd;
+        cp_up_network_byte = meas_up_network_byte;
+        cp_up_payload_byte = meas_up_payload_byte;
+        cp_up_dgram_sent   = meas_up_dgram_sent;
+        cp_up_ack_rcv      = meas_up_ack_rcv;
+        meas_nb_rx_rcv = 0;
+        meas_nb_rx_ok = 0;
+        meas_nb_rx_bad = 0;
+        meas_nb_rx_nocrc = 0;
+        meas_up_pkt_fwd = 0;
+        meas_up_network_byte = 0;
+        meas_up_payload_byte = 0;
+        meas_up_dgram_sent = 0;
+        meas_up_ack_rcv = 0;
+        pthread_mutex_unlock(&mx_meas_up);
+        if (cp_nb_rx_rcv > 0) {
+            rx_ok_ratio = (float)cp_nb_rx_ok / (float)cp_nb_rx_rcv;
+            rx_bad_ratio = (float)cp_nb_rx_bad / (float)cp_nb_rx_rcv;
+            rx_nocrc_ratio = (float)cp_nb_rx_nocrc / (float)cp_nb_rx_rcv;
+        } else {
+            rx_ok_ratio = 0.0;
+            rx_bad_ratio = 0.0;
+            rx_nocrc_ratio = 0.0;
+        }
+        if (cp_up_dgram_sent > 0) {
+            up_ack_ratio = (float)cp_up_ack_rcv / (float)cp_up_dgram_sent;
+        } else {
+            up_ack_ratio = 0.0;
+        }
+
+        /* access downstream statistics, copy and reset them */
+        pthread_mutex_lock(&mx_meas_dw);
+        cp_dw_pull_sent    =  meas_dw_pull_sent;
+        cp_dw_ack_rcv      =  meas_dw_ack_rcv;
+        cp_dw_dgram_rcv    =  meas_dw_dgram_rcv;
+        cp_dw_network_byte =  meas_dw_network_byte;
+        cp_dw_payload_byte =  meas_dw_payload_byte;
+        cp_nb_tx_ok        =  meas_nb_tx_ok;
+        cp_nb_tx_fail      =  meas_nb_tx_fail;
+        cp_nb_tx_requested                 +=  meas_nb_tx_requested;
+        cp_nb_tx_rejected_collision_packet +=  meas_nb_tx_rejected_collision_packet;
+        cp_nb_tx_rejected_collision_beacon +=  meas_nb_tx_rejected_collision_beacon;
+        cp_nb_tx_rejected_too_late         +=  meas_nb_tx_rejected_too_late;
+        cp_nb_tx_rejected_too_early        +=  meas_nb_tx_rejected_too_early;
+        meas_dw_pull_sent = 0;
+        meas_dw_ack_rcv = 0;
+        meas_dw_dgram_rcv = 0;
+        meas_dw_network_byte = 0;
+        meas_dw_payload_byte = 0;
+        meas_nb_tx_ok = 0;
+        meas_nb_tx_fail = 0;
+        meas_nb_tx_requested = 0;
+        meas_nb_tx_rejected_collision_packet = 0;
+        meas_nb_tx_rejected_collision_beacon = 0;
+        meas_nb_tx_rejected_too_late = 0;
+        meas_nb_tx_rejected_too_early = 0;
+        pthread_mutex_unlock(&mx_meas_dw);
+        if (cp_dw_pull_sent > 0) {
+            dw_ack_ratio = (float)cp_dw_ack_rcv / (float)cp_dw_pull_sent;
+        } else {
+            dw_ack_ratio = 0.0;
+        }
+
+        /* overwrite with reference coordinates if function is enabled */
+        if (gps_fake_enable == true) {
+            cp_gps_coord = reference_coord;
+        }
+
+        /* display a report */
+        printf("\n##### %s #####\n", stat_timestamp);
+        printf("### [UPSTREAM] ###\n");
+        printf("# RF packets received by concentrator: %u\n", cp_nb_rx_rcv);
+        printf("# CRC_OK: %.2f%%, CRC_FAIL: %.2f%%, NO_CRC: %.2f%%\n", 100.0 * rx_ok_ratio, 100.0 * rx_bad_ratio, 100.0 * rx_nocrc_ratio);
+        printf("# RF packets forwarded: %u (%u bytes)\n", cp_up_pkt_fwd, cp_up_payload_byte);
+        printf("# PUSH_DATA datagrams sent: %u (%u bytes)\n", cp_up_dgram_sent, cp_up_network_byte);
+        printf("# PUSH_DATA acknowledged: %.2f%%\n", 100.0 * up_ack_ratio);
+        printf("### [DOWNSTREAM] ###\n");
+        printf("# PULL_DATA sent: %u (%.2f%% acknowledged)\n", cp_dw_pull_sent, 100.0 * dw_ack_ratio);
+        printf("# PULL_RESP(onse) datagrams received: %u (%u bytes)\n", cp_dw_dgram_rcv, cp_dw_network_byte);
+        printf("# RF packets sent to concentrator: %u (%u bytes)\n", (cp_nb_tx_ok + cp_nb_tx_fail), cp_dw_payload_byte);
+        printf("# TX errors: %u\n", cp_nb_tx_fail);
+        if (cp_nb_tx_requested != 0 ) {
+            printf("# TX rejected (collision packet): %.2f%% (req:%u, rej:%u)\n", 100.0 * cp_nb_tx_rejected_collision_packet / cp_nb_tx_requested, cp_nb_tx_requested, cp_nb_tx_rejected_collision_packet);
+            printf("# TX rejected (collision beacon): %.2f%% (req:%u, rej:%u)\n", 100.0 * cp_nb_tx_rejected_collision_beacon / cp_nb_tx_requested, cp_nb_tx_requested, cp_nb_tx_rejected_collision_beacon);
+            printf("# TX rejected (too late): %.2f%% (req:%u, rej:%u)\n", 100.0 * cp_nb_tx_rejected_too_late / cp_nb_tx_requested, cp_nb_tx_requested, cp_nb_tx_rejected_too_late);
+            printf("# TX rejected (too early): %.2f%% (req:%u, rej:%u)\n", 100.0 * cp_nb_tx_rejected_too_early / cp_nb_tx_requested, cp_nb_tx_requested, cp_nb_tx_rejected_too_early);
+        }
+        printf("### [JIT] ###\n");
+        jit_print_queue (&jit_queue, false, DEBUG_LOG);
+        printf("### [GPS] ###\n");
+        if (gps_fake_enable == true) {
+            printf("# GPS *FAKE* coordinates: latitude %.5f, longitude %.5f, altitude %i m\n", cp_gps_coord.lat, cp_gps_coord.lon, cp_gps_coord.alt);
+        } else {
+            printf("# GPS sync is disabled\n");
+        }
+        printf("##### END #####\n");
+
+        /* generate a JSON report (will be sent to server by upstream thread) */
+        pthread_mutex_lock(&mx_stat_rep);
+        snprintf(status_report, STATUS_SIZE, "\"stat\":{\"time\":\"%s\",\"rxnb\":%u,\"rxok\":%u,\"rxfw\":%u,\"ackr\":%.1f,\"dwnb\":%u,\"txnb\":%u}", stat_timestamp, cp_nb_rx_rcv, cp_nb_rx_ok, cp_up_pkt_fwd, 100.0 * up_ack_ratio, cp_dw_dgram_rcv, cp_nb_tx_ok);
+        report_ready = true;
+        pthread_mutex_unlock(&mx_stat_rep);
+    }
+
+    /* wait for upstream thread to finish (1 fetch cycle max) */
+    pthread_join(thrid_up, NULL);
+    pthread_cancel(thrid_down); /* don't wait for downstream thread */
+    pthread_cancel(thrid_jit); /* don't wait for jit thread */
+    pthread_cancel(thrid_timersync); /* don't wait for timer sync thread */
+
+    /* if an exit signal was received, try to quit properly */
+    if (exit_sig) {
+        /* shut down network sockets */
+        shutdown(sock_up, SHUT_RDWR);
+        shutdown(sock_down, SHUT_RDWR);
+        /* stop the hardware */
+        i = lgw_stop();
+        if (i == LGW_HAL_SUCCESS) {
+            MSG("INFO: concentrator stopped successfully\n");
+        } else {
+            MSG("WARNING: failed to stop concentrator successfully\n");
+        }
+    }
+
+    MSG("INFO: Exiting packet forwarder program\n");
+    exit(EXIT_SUCCESS);
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- THREAD 1: RECEIVING PACKETS AND FORWARDING THEM ---------------------- */
+
+void thread_up(void) {
+    int i, j; /* loop variables */
+    unsigned pkt_in_dgram; /* nb on Lora packet in the current datagram */
+
+    /* allocate memory for packet fetching and processing */
+    struct lgw_pkt_rx_s rxpkt[NB_PKT_MAX]; /* array containing inbound packets + metadata */
+    struct lgw_pkt_rx_s *p; /* pointer on a RX packet */
+    int nb_pkt;
+
+    /* data buffers */
+    uint8_t buff_up[TX_BUFF_SIZE]; /* buffer to compose the upstream packet */
+    int buff_index;
+    uint8_t buff_ack[32]; /* buffer to receive acknowledges */
+
+    /* protocol variables */
+    uint8_t token_h = 0; /* random token for acknowledgement matching */
+    uint8_t token_l = 0; /* random token for acknowledgement matching */
+    static uint16_t token = 0;
+    /* ping measurement variables */
+    struct timespec send_time;
+    struct timespec recv_time;
+
+    /* report management variable */
+    bool send_report = false;
+
+    /* mote info variables */
+    uint32_t mote_addr = 0;
+    uint16_t mote_fcnt = 0;
+
+    /* set upstream socket RX timeout */
+    i = setsockopt(sock_up, SOL_SOCKET, SO_RCVTIMEO, (void *)&push_timeout_half, sizeof push_timeout_half);
+    if (i != 0) {
+        MSG("ERROR: [up] setsockopt returned %s\n", strerror(errno));
+        exit(EXIT_FAILURE);
+    }
+
+    /* pre-fill the data buffer with fixed fields */
+    buff_up[0] = PROTOCOL_VERSION;
+    buff_up[3] = PKT_PUSH_DATA;
+    *(uint32_t *)(buff_up + 4) = net_mac_h;
+    *(uint32_t *)(buff_up + 8) = net_mac_l;
+
+    while (!exit_sig && !quit_sig) {
+
+        /* fetch packets */
+        pthread_mutex_lock(&mx_concent);
+        nb_pkt = lgw_receive(NB_PKT_MAX, rxpkt);
+        pthread_mutex_unlock(&mx_concent);
+        if (nb_pkt == LGW_HAL_ERROR) {
+            MSG("ERROR: [up] failed packet fetch, exiting\n");
+            exit(EXIT_FAILURE);
+        }
+
+        /* check if there are status report to send */
+        send_report = report_ready; /* copy the variable so it doesn't change mid-function */
+        /* no mutex, we're only reading */
+
+        /* wait a short time if no packets, nor status report */
+        if ((nb_pkt == 0) && (send_report == false)) {
+            wait_ms(FETCH_SLEEP_MS);
+            continue;
+        }
+
+        /* start composing datagram with the header */
+        token_h = (uint8_t)(token >> 8); // (uint8_t)rand(); /* random token */
+        token_l =  (uint8_t)(token & 0x00FF); //(uint8_t)rand(); /* random token */
+
+        buff_up[1] = token_h;
+        buff_up[2] = token_l;
+        token++;
+        buff_index = 12; /* 12-byte header */
+
+        /* start of JSON structure */
+        memcpy((void *)(buff_up + buff_index), (void *)"{\"rxpk\":[", 9);
+        buff_index += 9;
+
+        /* serialize Lora packets metadata and payload */
+        pkt_in_dgram = 0;
+        for (i = 0; i < nb_pkt; ++i) {
+            p = &rxpkt[i];
+
+            /* Get mote information from current packet (addr, fcnt) */
+            /* FHDR - DevAddr */
+            mote_addr  = p->payload[1];
+            mote_addr |= p->payload[2] << 8;
+            mote_addr |= p->payload[3] << 16;
+            mote_addr |= p->payload[4] << 24;
+            /* FHDR - FCnt */
+            mote_fcnt  = p->payload[6];
+            mote_fcnt |= p->payload[7] << 8;
+
+            /* basic packet filtering */
+            pthread_mutex_lock(&mx_meas_up);
+            meas_nb_rx_rcv += 1;
+            switch(p->status) {
+                case STAT_CRC_OK:
+                    meas_nb_rx_ok += 1;
+                    printf( "\nINFO: Received pkt from mote: %08X (fcnt=%u)\n", mote_addr, mote_fcnt );
+                    if (!fwd_valid_pkt) {
+                        pthread_mutex_unlock(&mx_meas_up);
+                        continue; /* skip that packet */
+                    }
+                    break;
+                case STAT_CRC_BAD:
+                    meas_nb_rx_bad += 1;
+                    if (!fwd_error_pkt) {
+                        pthread_mutex_unlock(&mx_meas_up);
+                        continue; /* skip that packet */
+                    }
+                    break;
+                case STAT_NO_CRC:
+                    meas_nb_rx_nocrc += 1;
+                    if (!fwd_nocrc_pkt) {
+                        pthread_mutex_unlock(&mx_meas_up);
+                        continue; /* skip that packet */
+                    }
+                    break;
+                default:
+                    MSG("WARNING: [up] received packet with unknown status %u (size %u, modulation %u, BW %u, DR %u, RSSI %.1f)\n", p->status, p->size, p->modulation, p->bandwidth, p->datarate, p->rssi);
+                    pthread_mutex_unlock(&mx_meas_up);
+                    continue; /* skip that packet */
+                    // exit(EXIT_FAILURE);
+            }
+            meas_up_pkt_fwd += 1;
+            meas_up_payload_byte += p->size;
+            pthread_mutex_unlock(&mx_meas_up);
+
+            /* Start of packet, add inter-packet separator if necessary */
+            if (pkt_in_dgram == 0) {
+                buff_up[buff_index] = '{';
+                ++buff_index;
+            } else {
+                buff_up[buff_index] = ',';
+                buff_up[buff_index + 1] = '{';
+                buff_index += 2;
+            }
+
+            /* RAW timestamp, 8-17 useful chars */
+            j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE - buff_index, "\"tmst\":%u", p->count_us);
+            if (j > 0) {
+                buff_index += j;
+            } else {
+                MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
+                exit(EXIT_FAILURE);
+            }
+
+            /* Packet concentrator channel, RF chain & RX frequency, 34-36 useful chars */
+            j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE - buff_index, ",\"chan\":%1u,\"rfch\":%1u,\"freq\":%.6lf", p->if_chain, p->rf_chain, ((double)p->freq_hz / 1e6));
+            if (j > 0) {
+                buff_index += j;
+            } else {
+                MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
+                exit(EXIT_FAILURE);
+            }
+
+            /* Packet status, 9-10 useful chars */
+            switch (p->status) {
+                case STAT_CRC_OK:
+                    memcpy((void *)(buff_up + buff_index), (void *)",\"stat\":1", 9);
+                    buff_index += 9;
+                    break;
+                case STAT_CRC_BAD:
+                    memcpy((void *)(buff_up + buff_index), (void *)",\"stat\":-1", 10);
+                    buff_index += 10;
+                    break;
+                case STAT_NO_CRC:
+                    memcpy((void *)(buff_up + buff_index), (void *)",\"stat\":0", 9);
+                    buff_index += 9;
+                    break;
+                default:
+                    MSG("ERROR: [up] received packet with unknown status\n");
+                    memcpy((void *)(buff_up + buff_index), (void *)",\"stat\":?", 9);
+                    buff_index += 9;
+                    exit(EXIT_FAILURE);
+            }
+
+            /* Packet modulation, 13-14 useful chars */
+            if (p->modulation == MOD_LORA) {
+                memcpy((void *)(buff_up + buff_index), (void *)",\"modu\":\"LORA\"", 14);
+                buff_index += 14;
+
+                /* Lora datarate & bandwidth, 16-19 useful chars */
+                switch (p->datarate) {
+                    case DR_LORA_SF7:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF7", 12);
+                        buff_index += 12;
+                        break;
+                    case DR_LORA_SF8:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF8", 12);
+                        buff_index += 12;
+                        break;
+                    case DR_LORA_SF9:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF9", 12);
+                        buff_index += 12;
+                        break;
+                    case DR_LORA_SF10:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF10", 13);
+                        buff_index += 13;
+                        break;
+                    case DR_LORA_SF11:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF11", 13);
+                        buff_index += 13;
+                        break;
+                    case DR_LORA_SF12:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF12", 13);
+                        buff_index += 13;
+                        break;
+                    default:
+                        MSG("ERROR: [up] lora packet with unknown datarate\n");
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"datr\":\"SF?", 12);
+                        buff_index += 12;
+                        exit(EXIT_FAILURE);
+                }
+                switch (p->bandwidth) {
+                    case BW_125KHZ:
+                        memcpy((void *)(buff_up + buff_index), (void *)"BW125\"", 6);
+                        buff_index += 6;
+                        break;
+                    case BW_250KHZ:
+                        memcpy((void *)(buff_up + buff_index), (void *)"BW250\"", 6);
+                        buff_index += 6;
+                        break;
+                    case BW_500KHZ:
+                        memcpy((void *)(buff_up + buff_index), (void *)"BW500\"", 6);
+                        buff_index += 6;
+                        break;
+                    default:
+                        MSG("ERROR: [up] lora packet with unknown bandwidth\n");
+                        memcpy((void *)(buff_up + buff_index), (void *)"BW?\"", 4);
+                        buff_index += 4;
+                        exit(EXIT_FAILURE);
+                }
+
+                /* Packet ECC coding rate, 11-13 useful chars */
+                switch (p->coderate) {
+                    case CR_LORA_4_5:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"codr\":\"4/5\"", 13);
+                        buff_index += 13;
+                        break;
+                    case CR_LORA_4_6:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"codr\":\"4/6\"", 13);
+                        buff_index += 13;
+                        break;
+                    case CR_LORA_4_7:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"codr\":\"4/7\"", 13);
+                        buff_index += 13;
+                        break;
+                    case CR_LORA_4_8:
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"codr\":\"4/8\"", 13);
+                        buff_index += 13;
+                        break;
+                    case 0: /* treat the CR0 case (mostly false sync) */
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"codr\":\"OFF\"", 13);
+                        buff_index += 13;
+                        break;
+                    default:
+                        MSG("ERROR: [up] lora packet with unknown coderate\n");
+                        memcpy((void *)(buff_up + buff_index), (void *)",\"codr\":\"?\"", 11);
+                        buff_index += 11;
+                        exit(EXIT_FAILURE);
+                }
+
+                /* Lora SNR, 11-13 useful chars */
+                j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE - buff_index, ",\"lsnr\":%.1f", p->snr);
+                if (j > 0) {
+                    buff_index += j;
+                } else {
+                    MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
+                    exit(EXIT_FAILURE);
+                }
+            } else if (p->modulation == MOD_FSK) {
+                memcpy((void *)(buff_up + buff_index), (void *)",\"modu\":\"FSK\"", 13);
+                buff_index += 13;
+
+                /* FSK datarate, 11-14 useful chars */
+                j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE - buff_index, ",\"datr\":%u", p->datarate);
+                if (j > 0) {
+                    buff_index += j;
+                } else {
+                    MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
+                    exit(EXIT_FAILURE);
+                }
+            } else {
+                MSG("ERROR: [up] received packet with unknown modulation\n");
+                exit(EXIT_FAILURE);
+            }
+
+            /* Packet RSSI, payload size, 18-23 useful chars */
+            j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE - buff_index, ",\"rssi\":%.0f,\"size\":%u", p->rssi, p->size);
+            if (j > 0) {
+                buff_index += j;
+            } else {
+                MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 4));
+                exit(EXIT_FAILURE);
+            }
+
+            /* Packet base64-encoded payload, 14-350 useful chars */
+            memcpy((void *)(buff_up + buff_index), (void *)",\"data\":\"", 9);
+            buff_index += 9;
+            j = bin_to_b64(p->payload, p->size, (char *)(buff_up + buff_index), 341); /* 255 bytes = 340 chars in b64 + null char */
+            if (j >= 0) {
+                buff_index += j;
+            } else {
+                MSG("ERROR: [up] bin_to_b64 failed line %u\n", (__LINE__ - 5));
+                exit(EXIT_FAILURE);
+            }
+            buff_up[buff_index] = '"';
+            ++buff_index;
+
+            /* End of packet serialization */
+            buff_up[buff_index] = '}';
+            ++buff_index;
+            ++pkt_in_dgram;
+        }
+
+        /* restart fetch sequence without sending empty JSON if all packets have been filtered out */
+        if (pkt_in_dgram == 0) {
+            if (send_report == true) {
+                /* need to clean up the beginning of the payload */
+                buff_index -= 8; /* removes "rxpk":[ */
+            } else {
+                /* all packet have been filtered out and no report, restart loop */
+                continue;
+            }
+        } else {
+            /* end of packet array */
+            buff_up[buff_index] = ']';
+            ++buff_index;
+            /* add separator if needed */
+            if (send_report == true) {
+                buff_up[buff_index] = ',';
+                ++buff_index;
+            }
+        }
+
+        /* add status report if a new one is available */
+        if (send_report == true) {
+            pthread_mutex_lock(&mx_stat_rep);
+            report_ready = false;
+            j = snprintf((char *)(buff_up + buff_index), TX_BUFF_SIZE - buff_index, "%s", status_report);
+            pthread_mutex_unlock(&mx_stat_rep);
+            if (j > 0) {
+                buff_index += j;
+            } else {
+                MSG("ERROR: [up] snprintf failed line %u\n", (__LINE__ - 5));
+                exit(EXIT_FAILURE);
+            }
+        }
+
+        /* end of JSON datagram payload */
+        buff_up[buff_index] = '}';
+        ++buff_index;
+        buff_up[buff_index] = 0; /* add string terminator, for safety */
+
+        printf("\nJSON up: %s\n", (char *)(buff_up + 12)); /* DEBUG: display JSON payload */
+
+        /* send datagram to server */
+        send(sock_up, (void *)buff_up, buff_index, 0);
+        clock_gettime(CLOCK_MONOTONIC, &send_time);
+        pthread_mutex_lock(&mx_meas_up);
+        meas_up_dgram_sent += 1;
+        meas_up_network_byte += buff_index;
+
+        /* wait for acknowledge (in 2 times, to catch extra packets) */
+        for (i = 0; i < 2; ++i) {
+            j = recv(sock_up, (void *)buff_ack, sizeof buff_ack, 0);
+            clock_gettime(CLOCK_MONOTONIC, &recv_time);
+            if (j == -1) {
+                if (errno == EAGAIN) { /* timeout */
+                    continue;
+                } else { /* server connection error */
+                    break;
+                }
+            } else if ((j < 4) || (buff_ack[0] != PROTOCOL_VERSION) || (buff_ack[3] != PKT_PUSH_ACK)) {
+                //MSG("WARNING: [up] ignored invalid non-ACL packet\n");
+                continue;
+            } else if ((buff_ack[1] != token_h) || (buff_ack[2] != token_l)) {
+                //MSG("WARNING: [up] ignored out-of sync ACK packet\n");
+                continue;
+            } else {
+                MSG("INFO: [up] PUSH_ACK received in %i ms\n", (int)(1000 * difftimespec(recv_time, send_time)));
+                meas_up_ack_rcv += 1;
+                break;
+            }
+        }
+        pthread_mutex_unlock(&mx_meas_up);
+    }
+    MSG("\nINFO: End of upstream thread\n");
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- THREAD 2: POLLING SERVER AND ENQUEUING PACKETS IN JIT QUEUE ---------- */
+
+void thread_down(void) {
+    int i; /* loop variables */
+
+    /* configuration and metadata for an outbound packet */
+    struct lgw_pkt_tx_s txpkt;
+    bool sent_immediate = false; /* option to sent the packet immediately */
+
+    /* local timekeeping variables */
+    struct timespec send_time; /* time of the pull request */
+    struct timespec recv_time; /* time of return from recv socket call */
+
+    /* data buffers */
+    uint8_t buff_down[1000]; /* buffer to receive downstream packets */
+    uint8_t buff_req[12]; /* buffer to compose pull requests */
+    int msg_len;
+
+    /* protocol variables */
+    uint8_t token_h; /* random token for acknowledgement matching */
+    uint8_t token_l; /* random token for acknowledgement matching */
+    bool req_ack = false; /* keep track of whether PULL_DATA was acknowledged or not */
+
+    /* JSON parsing variables */
+    JSON_Value *root_val = NULL;
+    JSON_Object *txpk_obj = NULL;
+    JSON_Value *val = NULL; /* needed to detect the absence of some fields */
+    const char *str; /* pointer to sub-strings in the JSON data */
+    short x0, x1;
+
+    /* auto-quit variable */
+    uint32_t autoquit_cnt = 0; /* count the number of PULL_DATA sent since the latest PULL_ACK */
+
+    /* Just In Time downlink */
+    struct timeval current_unix_time;
+    struct timeval current_concentrator_time;
+    enum jit_error_e jit_result = JIT_ERROR_OK;
+    enum jit_pkt_type_e downlink_type;
+
+    /* set downstream socket RX timeout */
+    i = setsockopt(sock_down, SOL_SOCKET, SO_RCVTIMEO, (void *)&pull_timeout, sizeof pull_timeout);
+    if (i != 0) {
+        MSG("ERROR: [down] setsockopt returned %s\n", strerror(errno));
+        exit(EXIT_FAILURE);
+    }
+
+    /* pre-fill the pull request buffer with fixed fields */
+    buff_req[0] = PROTOCOL_VERSION;
+    buff_req[3] = PKT_PULL_DATA;
+    *(uint32_t *)(buff_req + 4) = net_mac_h;
+    *(uint32_t *)(buff_req + 8) = net_mac_l;
+
+    /* JIT queue initialization */
+    jit_queue_init(&jit_queue);
+
+    while (!exit_sig && !quit_sig) {
+
+        /* auto-quit if the threshold is crossed */
+        if ((autoquit_threshold > 0) && (autoquit_cnt >= autoquit_threshold)) {
+            exit_sig = true;
+            MSG("INFO: [down] the last %u PULL_DATA were not ACKed, exiting application\n", autoquit_threshold);
+            break;
+        }
+
+        /* generate random token for request */
+        token_h = (uint8_t)rand(); /* random token */
+        token_l = (uint8_t)rand(); /* random token */
+        buff_req[1] = token_h;
+        buff_req[2] = token_l;
+
+        /* send PULL request and record time */
+        send(sock_down, (void *)buff_req, sizeof buff_req, 0);
+        clock_gettime(CLOCK_MONOTONIC, &send_time);
+        pthread_mutex_lock(&mx_meas_dw);
+        meas_dw_pull_sent += 1;
+        pthread_mutex_unlock(&mx_meas_dw);
+        req_ack = false;
+        autoquit_cnt++;
+
+        /* listen to packets and process them until a new PULL request must be sent */
+        recv_time = send_time;
+        while ((int)difftimespec(recv_time, send_time) < keepalive_time) {
+
+            /* try to receive a datagram */
+            msg_len = recv(sock_down, (void *)buff_down, (sizeof buff_down) - 1, 0);
+            clock_gettime(CLOCK_MONOTONIC, &recv_time);
+
+            /* if no network message was received, got back to listening sock_down socket */
+            if (msg_len == -1) {
+                //MSG("WARNING: [down] recv returned %s\n", strerror(errno)); /* too verbose */
+                continue;
+            }
+
+            /* if the datagram does not respect protocol, just ignore it */
+            if ((msg_len < 4) || (buff_down[0] != PROTOCOL_VERSION) || ((buff_down[3] != PKT_PULL_RESP) && (buff_down[3] != PKT_PULL_ACK))) {
+                MSG("WARNING: [down] ignoring invalid packet len=%d, protocol_version=%d, id=%d\n",
+                    msg_len, buff_down[0], buff_down[3]);
+                continue;
+            }
+
+            /* if the datagram is an ACK, check token */
+            if (buff_down[3] == PKT_PULL_ACK) {
+                if ((buff_down[1] == token_h) && (buff_down[2] == token_l)) {
+                    if (req_ack) {
+                        MSG("INFO: [down] duplicate ACK received :)\n");
+                    } else { /* if that packet was not already acknowledged */
+                        req_ack = true;
+                        autoquit_cnt = 0;
+                        pthread_mutex_lock(&mx_meas_dw);
+                        meas_dw_ack_rcv += 1;
+                        pthread_mutex_unlock(&mx_meas_dw);
+                        MSG("INFO: [down] PULL_ACK received in %i ms\n", (int)(1000 * difftimespec(recv_time, send_time)));
+                    }
+                } else { /* out-of-sync token */
+                    MSG("INFO: [down] received out-of-sync ACK\n");
+                }
+                continue;
+            }
+
+            /* the datagram is a PULL_RESP */
+            buff_down[msg_len] = 0; /* add string terminator, just to be safe */
+            MSG("INFO: [down] PULL_RESP received  - token[%d:%d] :)\n", buff_down[1], buff_down[2]); /* very verbose */
+            printf("\nJSON down: %s\n", (char *)(buff_down + 4)); /* DEBUG: display JSON payload */
+
+            /* initialize TX struct and try to parse JSON */
+            memset(&txpkt, 0, sizeof txpkt);
+            root_val = json_parse_string_with_comments((const char *)(buff_down + 4)); /* JSON offset */
+            if (root_val == NULL) {
+                MSG("WARNING: [down] invalid JSON, TX aborted\n");
+                continue;
+            }
+
+            /* look for JSON sub-object 'txpk' */
+            txpk_obj = json_object_get_object(json_value_get_object(root_val), "txpk");
+            if (txpk_obj == NULL) {
+                MSG("WARNING: [down] no \"txpk\" object in JSON, TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+
+            /* Parse "immediate" tag, or target timestamp */
+            i = json_object_get_boolean(txpk_obj, "imme"); /* can be 1 if true, 0 if false, or -1 if not a JSON boolean */
+            if (i == 1) {
+                /* TX procedure: send immediately */
+                sent_immediate = true;
+                downlink_type = JIT_PKT_TYPE_DOWNLINK_CLASS_C;
+                MSG("INFO: [down] a packet will be sent in \"immediate\" mode\n");
+            } else {
+                sent_immediate = false;
+                val = json_object_get_value(txpk_obj, "tmst");
+                if (val != NULL) {
+                    /* TX procedure: send on timestamp value */
+                    txpkt.count_us = (uint32_t)json_value_get_number(val);
+
+                    /* Concentrator timestamp is given, we consider it is a Class A downlink */
+                    downlink_type = JIT_PKT_TYPE_DOWNLINK_CLASS_A;
+                } else {
+                    /* TX procedure: send on UTC time (converted to timestamp value) */
+                    str = json_object_get_string(txpk_obj, "time");
+                    if (str == NULL) {
+                        MSG("WARNING: [down] no mandatory \"txpk.tmst\" or \"txpk.time\" objects in JSON, TX aborted\n");
+                        json_value_free(root_val);
+                        continue;
+                    }
+                    MSG("WARNING: [down] GPS disabled, impossible to send packet on specific UTC time, TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+            }
+
+            /* Parse "No CRC" flag (optional field) */
+            val = json_object_get_value(txpk_obj, "ncrc");
+            if (val != NULL) {
+                txpkt.no_crc = (bool)json_value_get_boolean(val);
+            }
+
+            /* parse target frequency (mandatory) */
+            val = json_object_get_value(txpk_obj, "freq");
+            if (val == NULL) {
+                MSG("WARNING: [down] no mandatory \"txpk.freq\" object in JSON, TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+            txpkt.freq_hz = (uint32_t)((double)(1.0e6) * json_value_get_number(val));
+
+            /* parse RF chain used for TX (mandatory) */
+            val = json_object_get_value(txpk_obj, "rfch");
+            if (val == NULL) {
+                MSG("WARNING: [down] no mandatory \"txpk.rfch\" object in JSON, TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+            txpkt.rf_chain = (uint8_t)json_value_get_number(val);
+
+            /* parse TX power (optional field) */
+            val = json_object_get_value(txpk_obj, "powe");
+            if (val != NULL) {
+                txpkt.rf_power = (int8_t)json_value_get_number(val) - antenna_gain;
+            }
+
+            /* Parse modulation (mandatory) */
+            str = json_object_get_string(txpk_obj, "modu");
+            if (str == NULL) {
+                MSG("WARNING: [down] no mandatory \"txpk.modu\" object in JSON, TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+            if (strcmp(str, "LORA") == 0) {
+                /* Lora modulation */
+                txpkt.modulation = MOD_LORA;
+
+                /* Parse Lora spreading-factor and modulation bandwidth (mandatory) */
+                str = json_object_get_string(txpk_obj, "datr");
+                if (str == NULL) {
+                    MSG("WARNING: [down] no mandatory \"txpk.datr\" object in JSON, TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+                i = sscanf(str, "SF%2hdBW%3hd", &x0, &x1);
+                if (i != 2) {
+                    MSG("WARNING: [down] format error in \"txpk.datr\", TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+                switch (x0) {
+                    case  7:
+                        txpkt.datarate = DR_LORA_SF7;
+                        break;
+                    case  8:
+                        txpkt.datarate = DR_LORA_SF8;
+                        break;
+                    case  9:
+                        txpkt.datarate = DR_LORA_SF9;
+                        break;
+                    case 10:
+                        txpkt.datarate = DR_LORA_SF10;
+                        break;
+                    case 11:
+                        txpkt.datarate = DR_LORA_SF11;
+                        break;
+                    case 12:
+                        txpkt.datarate = DR_LORA_SF12;
+                        break;
+                    default:
+                        MSG("WARNING: [down] format error in \"txpk.datr\", invalid SF, TX aborted\n");
+                        json_value_free(root_val);
+                        continue;
+                }
+                switch (x1) {
+                    case 125:
+                        txpkt.bandwidth = BW_125KHZ;
+                        break;
+                    case 250:
+                        txpkt.bandwidth = BW_250KHZ;
+                        break;
+                    case 500:
+                        txpkt.bandwidth = BW_500KHZ;
+                        break;
+                    default:
+                        MSG("WARNING: [down] format error in \"txpk.datr\", invalid BW, TX aborted\n");
+                        json_value_free(root_val);
+                        continue;
+                }
+
+                /* Parse ECC coding rate (optional field) */
+                str = json_object_get_string(txpk_obj, "codr");
+                if (str == NULL) {
+                    MSG("WARNING: [down] no mandatory \"txpk.codr\" object in json, TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+                if      (strcmp(str, "4/5") == 0) {
+                    txpkt.coderate = CR_LORA_4_5;
+                } else if (strcmp(str, "4/6") == 0) {
+                    txpkt.coderate = CR_LORA_4_6;
+                } else if (strcmp(str, "2/3") == 0) {
+                    txpkt.coderate = CR_LORA_4_6;
+                } else if (strcmp(str, "4/7") == 0) {
+                    txpkt.coderate = CR_LORA_4_7;
+                } else if (strcmp(str, "4/8") == 0) {
+                    txpkt.coderate = CR_LORA_4_8;
+                } else if (strcmp(str, "1/2") == 0) {
+                    txpkt.coderate = CR_LORA_4_8;
+                } else {
+                    MSG("WARNING: [down] format error in \"txpk.codr\", TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+
+                /* Parse signal polarity switch (optional field) */
+                val = json_object_get_value(txpk_obj, "ipol");
+                if (val != NULL) {
+                    txpkt.invert_pol = (bool)json_value_get_boolean(val);
+                }
+
+                /* parse Lora preamble length (optional field, optimum min value enforced) */
+                val = json_object_get_value(txpk_obj, "prea");
+                if (val != NULL) {
+                    i = (int)json_value_get_number(val);
+                    if (i >= MIN_LORA_PREAMB) {
+                        txpkt.preamble = (uint16_t)i;
+                    } else {
+                        txpkt.preamble = (uint16_t)MIN_LORA_PREAMB;
+                    }
+                } else {
+                    txpkt.preamble = (uint16_t)STD_LORA_PREAMB;
+                }
+
+            } else if (strcmp(str, "FSK") == 0) {
+                /* FSK modulation */
+                txpkt.modulation = MOD_FSK;
+
+                /* parse FSK bitrate (mandatory) */
+                val = json_object_get_value(txpk_obj, "datr");
+                if (val == NULL) {
+                    MSG("WARNING: [down] no mandatory \"txpk.datr\" object in JSON, TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+                txpkt.datarate = (uint32_t)(json_value_get_number(val));
+
+                /* parse frequency deviation (mandatory) */
+                val = json_object_get_value(txpk_obj, "fdev");
+                if (val == NULL) {
+                    MSG("WARNING: [down] no mandatory \"txpk.fdev\" object in JSON, TX aborted\n");
+                    json_value_free(root_val);
+                    continue;
+                }
+                txpkt.f_dev = (uint8_t)(json_value_get_number(val) / 1000.0); /* JSON value in Hz, txpkt.f_dev in kHz */
+
+                /* parse FSK preamble length (optional field, optimum min value enforced) */
+                val = json_object_get_value(txpk_obj, "prea");
+                if (val != NULL) {
+                    i = (int)json_value_get_number(val);
+                    if (i >= MIN_FSK_PREAMB) {
+                        txpkt.preamble = (uint16_t)i;
+                    } else {
+                        txpkt.preamble = (uint16_t)MIN_FSK_PREAMB;
+                    }
+                } else {
+                    txpkt.preamble = (uint16_t)STD_FSK_PREAMB;
+                }
+
+            } else {
+                MSG("WARNING: [down] invalid modulation in \"txpk.modu\", TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+
+            /* Parse payload length (mandatory) */
+            val = json_object_get_value(txpk_obj, "size");
+            if (val == NULL) {
+                MSG("WARNING: [down] no mandatory \"txpk.size\" object in JSON, TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+            txpkt.size = (uint16_t)json_value_get_number(val);
+
+            /* Parse payload data (mandatory) */
+            str = json_object_get_string(txpk_obj, "data");
+            if (str == NULL) {
+                MSG("WARNING: [down] no mandatory \"txpk.data\" object in JSON, TX aborted\n");
+                json_value_free(root_val);
+                continue;
+            }
+            i = b64_to_bin(str, strlen(str), txpkt.payload, sizeof txpkt.payload);
+            if (i != txpkt.size) {
+                MSG("WARNING: [down] mismatch between .size and .data size once converter to binary\n");
+            }
+
+            /* free the JSON parse tree from memory */
+            json_value_free(root_val);
+
+            /* select TX mode */
+            if (sent_immediate) {
+                txpkt.tx_mode = IMMEDIATE;
+            } else {
+                txpkt.tx_mode = TIMESTAMPED;
+            }
+
+            /* record measurement data */
+            pthread_mutex_lock(&mx_meas_dw);
+            meas_dw_dgram_rcv += 1; /* count only datagrams with no JSON errors */
+            meas_dw_network_byte += msg_len; /* meas_dw_network_byte */
+            meas_dw_payload_byte += txpkt.size;
+            pthread_mutex_unlock(&mx_meas_dw);
+
+            /* check TX parameter before trying to queue packet */
+            jit_result = JIT_ERROR_OK;
+            if ((txpkt.freq_hz < tx_freq_min[txpkt.rf_chain]) || (txpkt.freq_hz > tx_freq_max[txpkt.rf_chain])) {
+                jit_result = JIT_ERROR_TX_FREQ;
+                MSG("ERROR: Packet REJECTED, unsupported frequency - %u (min:%u,max:%u)\n", txpkt.freq_hz, tx_freq_min[txpkt.rf_chain], tx_freq_max[txpkt.rf_chain]);
+            }
+            if (jit_result == JIT_ERROR_OK) {
+                for (i = 0; i < txlut.size; i++) {
+                    if (txlut.lut[i].rf_power == txpkt.rf_power) {
+                        /* this RF power is supported, we can continue */
+                        break;
+                    }
+                }
+                if (i == txlut.size) {
+                    /* this RF power is not supported */
+                    jit_result = JIT_ERROR_TX_POWER;
+                    MSG("ERROR: Packet REJECTED, unsupported RF power for TX - %d\n", txpkt.rf_power);
+                }
+            }
+
+            /* insert packet to be sent into JIT queue */
+            if (jit_result == JIT_ERROR_OK) {
+                gettimeofday(&current_unix_time, NULL);
+                get_concentrator_time(&current_concentrator_time, current_unix_time);
+                jit_result = jit_enqueue(&jit_queue, &current_concentrator_time, &txpkt, downlink_type);
+                if (jit_result != JIT_ERROR_OK) {
+                    printf("ERROR: Packet REJECTED (jit error=%d)\n", jit_result);
+                }
+                pthread_mutex_lock(&mx_meas_dw);
+                meas_nb_tx_requested += 1;
+                pthread_mutex_unlock(&mx_meas_dw);
+            }
+
+            /* Send acknoledge datagram to server */
+            send_tx_ack(buff_down[1], buff_down[2], jit_result);
+        }
+    }
+    MSG("\nINFO: End of downstream thread\n");
+}
+
+void print_tx_status(uint8_t tx_status) {
+    switch (tx_status) {
+        case TX_OFF:
+            MSG("INFO: [jit] lgw_status returned TX_OFF\n");
+            break;
+        case TX_FREE:
+            MSG("INFO: [jit] lgw_status returned TX_FREE\n");
+            break;
+        case TX_EMITTING:
+            MSG("INFO: [jit] lgw_status returned TX_EMITTING\n");
+            break;
+        case TX_SCHEDULED:
+            MSG("INFO: [jit] lgw_status returned TX_SCHEDULED\n");
+            break;
+        default:
+            MSG("INFO: [jit] lgw_status returned UNKNOWN (%d)\n", tx_status);
+            break;
+    }
+}
+
+
+/* -------------------------------------------------------------------------- */
+/* --- THREAD 3: CHECKING PACKETS TO BE SENT FROM JIT QUEUE AND SEND THEM --- */
+
+void thread_jit(void) {
+    int result = LGW_HAL_SUCCESS;
+    struct lgw_pkt_tx_s pkt;
+    int pkt_index = -1;
+    struct timeval current_unix_time;
+    struct timeval current_concentrator_time;
+    enum jit_error_e jit_result;
+    enum jit_pkt_type_e pkt_type;
+    uint8_t tx_status;
+
+    while (!exit_sig && !quit_sig) {
+        wait_ms(10);
+
+        /* transfer data and metadata to the concentrator, and schedule TX */
+        gettimeofday(&current_unix_time, NULL);
+        get_concentrator_time(&current_concentrator_time, current_unix_time);
+        jit_result = jit_peek(&jit_queue, &current_concentrator_time, &pkt_index);
+        if (jit_result == JIT_ERROR_OK) {
+            if (pkt_index > -1) {
+                jit_result = jit_dequeue(&jit_queue, pkt_index, &pkt, &pkt_type);
+                if (jit_result == JIT_ERROR_OK) {
+
+                    /* check if concentrator is free for sending new packet */
+                    result = lgw_status(TX_STATUS, &tx_status);
+                    if (result == LGW_HAL_ERROR) {
+                        MSG("WARNING: [jit] lgw_status failed\n");
+                    } else {
+                        if (tx_status == TX_EMITTING) {
+                            MSG("WARNING: concentrator is currently busy\n");
+                            print_tx_status(tx_status);
+                            // continue;
+                        } else if (tx_status == TX_SCHEDULED) {
+                            MSG("WARNING: a downlink was already scheduled, overwritting it...\n");
+                            print_tx_status(tx_status);
+                        } else {
+                            /* Nothing to do */
+                        }
+                    }
+
+                    /* send packet to concentrator */
+                    pthread_mutex_lock(&mx_concent); /* may have to wait for a fetch to finish */
+                    result = lgw_send(pkt);
+                    pthread_mutex_unlock(&mx_concent); /* free concentrator ASAP */
+                    if (result == LGW_HAL_ERROR) {
+                        pthread_mutex_lock(&mx_meas_dw);
+                        meas_nb_tx_fail += 1;
+                        pthread_mutex_unlock(&mx_meas_dw);
+                        MSG("WARNING: [jit] lgw_send failed\n");
+                        continue;
+                    } else {
+                        pthread_mutex_lock(&mx_meas_dw);
+                        meas_nb_tx_ok += 1;
+                        pthread_mutex_unlock(&mx_meas_dw);
+                        MSG_DEBUG(DEBUG_PKT_FWD, "lgw_send done: count_us=%u\n", pkt.count_us);
+                    }
+                } else {
+                    MSG("ERROR: jit_dequeue failed with %d\n", jit_result);
+                }
+            }
+        } else if (jit_result == JIT_ERROR_EMPTY) {
+            /* Do nothing, it can happen */
+        } else {
+            MSG("ERROR: jit_peek failed with %d\n", jit_result);
+        }
+    }
+}
+
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/src/parson.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,1765 @@
+/*
+ Parson ( http://kgabis.github.com/parson/ )
+ Copyright (c) 2012 - 2016 Krzysztof Gabis
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+#ifdef _MSC_VER
+#define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#include "parson.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <math.h>
+
+#define STARTING_CAPACITY         15
+#define ARRAY_MAX_CAPACITY    122880 /* 15*(2^13) */
+#define OBJECT_MAX_CAPACITY      960 /* 15*(2^6)  */
+#define MAX_NESTING               19
+#define DOUBLE_SERIALIZATION_FORMAT "%f"
+
+#define SIZEOF_TOKEN(a)       (sizeof(a) - 1)
+#define SKIP_CHAR(str)        ((*str)++)
+#define SKIP_WHITESPACES(str) while (isspace(**str)) { SKIP_CHAR(str); }
+#define MAX(a, b)             ((a) > (b) ? (a) : (b))
+
+#undef malloc
+#undef free
+
+static JSON_Malloc_Function parson_malloc = malloc;
+static JSON_Free_Function parson_free = free;
+
+#define IS_CONT(b) (((unsigned char)(b) & 0xC0) == 0x80) /* is utf-8 continuation byte */
+
+/* Type definitions */
+typedef union json_value_value {
+    char        *string;
+    double       number;
+    JSON_Object *object;
+    JSON_Array  *array;
+    int          boolean;
+    int          null;
+} JSON_Value_Value;
+
+struct json_value_t {
+    JSON_Value_Type     type;
+    JSON_Value_Value    value;
+};
+
+struct json_object_t {
+    char       **names;
+    JSON_Value **values;
+    size_t       count;
+    size_t       capacity;
+};
+
+struct json_array_t {
+    JSON_Value **items;
+    size_t       count;
+    size_t       capacity;
+};
+
+/* Various */
+static char * read_file(const char *filename);
+static void   remove_comments(char *string, const char *start_token, const char *end_token);
+static char * parson_strndup(const char *string, size_t n);
+static char * parson_strdup(const char *string);
+static int    is_utf16_hex(const unsigned char *string);
+static int    num_bytes_in_utf8_sequence(unsigned char c);
+static int    verify_utf8_sequence(const unsigned char *string, int *len);
+static int    is_valid_utf8(const char *string, size_t string_len);
+static int    is_decimal(const char *string, size_t length);
+
+/* JSON Object */
+static JSON_Object * json_object_init(void);
+static JSON_Status   json_object_add(JSON_Object *object, const char *name, JSON_Value *value);
+static JSON_Status   json_object_resize(JSON_Object *object, size_t new_capacity);
+static JSON_Value  * json_object_nget_value(const JSON_Object *object, const char *name, size_t n);
+static void          json_object_free(JSON_Object *object);
+
+/* JSON Array */
+static JSON_Array * json_array_init(void);
+static JSON_Status  json_array_add(JSON_Array *array, JSON_Value *value);
+static JSON_Status  json_array_resize(JSON_Array *array, size_t new_capacity);
+static void         json_array_free(JSON_Array *array);
+
+/* JSON Value */
+static JSON_Value * json_value_init_string_no_copy(char *string);
+
+/* Parser */
+static void         skip_quotes(const char **string);
+static int          parse_utf_16(const char **unprocessed, char **processed);
+static char *       process_string(const char *input, size_t len);
+static char *       get_quoted_string(const char **string);
+static JSON_Value * parse_object_value(const char **string, size_t nesting);
+static JSON_Value * parse_array_value(const char **string, size_t nesting);
+static JSON_Value * parse_string_value(const char **string);
+static JSON_Value * parse_boolean_value(const char **string);
+static JSON_Value * parse_number_value(const char **string);
+static JSON_Value * parse_null_value(const char **string);
+static JSON_Value * parse_value(const char **string, size_t nesting);
+
+/* Serialization */
+static int    json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, int is_pretty, char *num_buf);
+static int    json_serialize_string(const char *string, char *buf);
+static int    append_indent(char *buf, int level);
+static int    append_string(char *buf, const char *string);
+
+/* Various */
+static char * parson_strndup(const char *string, size_t n) {
+    char *output_string = (char*)parson_malloc(n + 1);
+    if (!output_string)
+        return NULL;
+    output_string[n] = '\0';
+    strncpy(output_string, string, n);
+    return output_string;
+}
+
+static char * parson_strdup(const char *string) {
+    return parson_strndup(string, strlen(string));
+}
+
+static int is_utf16_hex(const unsigned char *s) {
+    return isxdigit(s[0]) && isxdigit(s[1]) && isxdigit(s[2]) && isxdigit(s[3]);
+}
+
+static int num_bytes_in_utf8_sequence(unsigned char c) {
+    if (c == 0xC0 || c == 0xC1 || c > 0xF4 || IS_CONT(c)) {
+        return 0;
+    } else if ((c & 0x80) == 0) {    /* 0xxxxxxx */
+        return 1;
+    } else if ((c & 0xE0) == 0xC0) { /* 110xxxxx */
+        return 2;
+    } else if ((c & 0xF0) == 0xE0) { /* 1110xxxx */
+        return 3;
+    } else if ((c & 0xF8) == 0xF0) { /* 11110xxx */
+        return 4;
+    }
+    return 0; /* won't happen */
+}
+
+static int verify_utf8_sequence(const unsigned char *string, int *len) {
+    unsigned int cp = 0;
+    *len = num_bytes_in_utf8_sequence(string[0]);
+
+    if (*len == 1) {
+        cp = string[0];
+    } else if (*len == 2 && IS_CONT(string[1])) {
+        cp = string[0] & 0x1F;
+        cp = (cp << 6) | (string[1] & 0x3F);
+    } else if (*len == 3 && IS_CONT(string[1]) && IS_CONT(string[2])) {
+        cp = ((unsigned char)string[0]) & 0xF;
+        cp = (cp << 6) | (string[1] & 0x3F);
+        cp = (cp << 6) | (string[2] & 0x3F);
+    } else if (*len == 4 && IS_CONT(string[1]) && IS_CONT(string[2]) && IS_CONT(string[3])) {
+        cp = string[0] & 0x7;
+        cp = (cp << 6) | (string[1] & 0x3F);
+        cp = (cp << 6) | (string[2] & 0x3F);
+        cp = (cp << 6) | (string[3] & 0x3F);
+    } else {
+        return 0;
+    }
+
+    /* overlong encodings */
+    if ((cp < 0x80    && *len > 1) ||
+        (cp < 0x800   && *len > 2) ||
+        (cp < 0x10000 && *len > 3)) {
+        return 0;
+    }
+
+    /* invalid unicode */
+    if (cp > 0x10FFFF) {
+        return 0;
+    }
+
+    /* surrogate halves */
+    if (cp >= 0xD800 && cp <= 0xDFFF) {
+        return 0;
+    }
+
+    return 1;
+}
+
+static int is_valid_utf8(const char *string, size_t string_len) {
+    int len = 0;
+    const char *string_end =  string + string_len;
+    while (string < string_end) {
+        if (!verify_utf8_sequence((const unsigned char*)string, &len)) {
+            return 0;
+        }
+        string += len;
+    }
+    return 1;
+}
+
+static int is_decimal(const char *string, size_t length) {
+    if (length > 1 && string[0] == '0' && string[1] != '.')
+        return 0;
+    if (length > 2 && !strncmp(string, "-0", 2) && string[2] != '.')
+        return 0;
+    while (length--)
+        if (strchr("xX", string[length]))
+            return 0;
+    return 1;
+}
+
+static char * read_file(const char * filename) {
+    FILE *fp = fopen(filename, "r");
+    size_t file_size;
+    long pos;
+    char *file_contents;
+    if (!fp)
+        return NULL;
+    fseek(fp, 0L, SEEK_END);
+    pos = ftell(fp);
+    if (pos < 0) {
+        fclose(fp);
+        return NULL;
+    }
+    file_size = pos;
+    rewind(fp);
+    file_contents = (char*)parson_malloc(sizeof(char) * (file_size + 1));
+    if (!file_contents) {
+        fclose(fp);
+        return NULL;
+    }
+    if (fread(file_contents, file_size, 1, fp) < 1) {
+        if (ferror(fp)) {
+            fclose(fp);
+            parson_free(file_contents);
+            return NULL;
+        }
+    }
+    fclose(fp);
+    file_contents[file_size] = '\0';
+    return file_contents;
+}
+
+static void remove_comments(char *string, const char *start_token, const char *end_token) {
+    int in_string = 0, escaped = 0;
+    size_t i;
+    char *ptr = NULL, current_char;
+    size_t start_token_len = strlen(start_token);
+    size_t end_token_len = strlen(end_token);
+    if (start_token_len == 0 || end_token_len == 0)
+        return;
+    while ((current_char = *string) != '\0') {
+        if (current_char == '\\' && !escaped) {
+            escaped = 1;
+            string++;
+            continue;
+        } else if (current_char == '\"' && !escaped) {
+            in_string = !in_string;
+        } else if (!in_string && strncmp(string, start_token, start_token_len) == 0) {
+            for(i = 0; i < start_token_len; i++)
+                string[i] = ' ';
+            string = string + start_token_len;
+            ptr = strstr(string, end_token);
+            if (!ptr)
+                return;
+            for (i = 0; i < (ptr - string) + end_token_len; i++)
+                string[i] = ' ';
+              string = ptr + end_token_len - 1;
+        }
+        escaped = 0;
+        string++;
+    }
+}
+
+/* JSON Object */
+static JSON_Object * json_object_init(void) {
+    JSON_Object *new_obj = (JSON_Object*)parson_malloc(sizeof(JSON_Object));
+    if (!new_obj)
+        return NULL;
+    new_obj->names = (char**)NULL;
+    new_obj->values = (JSON_Value**)NULL;
+    new_obj->capacity = 0;
+    new_obj->count = 0;
+    return new_obj;
+}
+
+static JSON_Status json_object_add(JSON_Object *object, const char *name, JSON_Value *value) {
+    size_t index = 0;
+    if (object == NULL || name == NULL || value == NULL) {
+        return JSONFailure;
+    }
+    if (object->count >= object->capacity) {
+        size_t new_capacity = MAX(object->capacity * 2, STARTING_CAPACITY);
+        if (new_capacity > OBJECT_MAX_CAPACITY)
+            return JSONFailure;
+        if (json_object_resize(object, new_capacity) == JSONFailure)
+            return JSONFailure;
+    }
+    if (json_object_get_value(object, name) != NULL)
+        return JSONFailure;
+    index = object->count;
+    object->names[index] = parson_strdup(name);
+    if (object->names[index] == NULL)
+        return JSONFailure;
+    object->values[index] = value;
+    object->count++;
+    return JSONSuccess;
+}
+
+static JSON_Status json_object_resize(JSON_Object *object, size_t new_capacity) {
+    char **temp_names = NULL;
+    JSON_Value **temp_values = NULL;
+
+    if ((object->names == NULL && object->values != NULL) ||
+        (object->names != NULL && object->values == NULL) ||
+        new_capacity == 0) {
+            return JSONFailure; /* Shouldn't happen */
+    }
+
+    temp_names = (char**)parson_malloc(new_capacity * sizeof(char*));
+    if (temp_names == NULL)
+        return JSONFailure;
+
+    temp_values = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*));
+    if (temp_values == NULL) {
+        parson_free(temp_names);
+        return JSONFailure;
+    }
+
+    if (object->names != NULL && object->values != NULL && object->count > 0) {
+        memcpy(temp_names, object->names, object->count * sizeof(char*));
+        memcpy(temp_values, object->values, object->count * sizeof(JSON_Value*));
+    }
+    parson_free(object->names);
+    parson_free(object->values);
+    object->names = temp_names;
+    object->values = temp_values;
+    object->capacity = new_capacity;
+    return JSONSuccess;
+}
+
+static JSON_Value * json_object_nget_value(const JSON_Object *object, const char *name, size_t n) {
+    size_t i, name_length;
+    for (i = 0; i < json_object_get_count(object); i++) {
+        name_length = strlen(object->names[i]);
+        if (name_length != n)
+            continue;
+        if (strncmp(object->names[i], name, n) == 0)
+            return object->values[i];
+    }
+    return NULL;
+}
+
+static void json_object_free(JSON_Object *object) {
+    while(object->count--) {
+        parson_free(object->names[object->count]);
+        json_value_free(object->values[object->count]);
+    }
+    parson_free(object->names);
+    parson_free(object->values);
+    parson_free(object);
+}
+
+/* JSON Array */
+static JSON_Array * json_array_init(void) {
+    JSON_Array *new_array = (JSON_Array*)parson_malloc(sizeof(JSON_Array));
+    if (!new_array)
+        return NULL;
+    new_array->items = (JSON_Value**)NULL;
+    new_array->capacity = 0;
+    new_array->count = 0;
+    return new_array;
+}
+
+static JSON_Status json_array_add(JSON_Array *array, JSON_Value *value) {
+    if (array->count >= array->capacity) {
+        size_t new_capacity = MAX(array->capacity * 2, STARTING_CAPACITY);
+        if (new_capacity > ARRAY_MAX_CAPACITY)
+            return JSONFailure;
+        if (json_array_resize(array, new_capacity) == JSONFailure)
+            return JSONFailure;
+    }
+    array->items[array->count] = value;
+    array->count++;
+    return JSONSuccess;
+}
+
+static JSON_Status json_array_resize(JSON_Array *array, size_t new_capacity) {
+    JSON_Value **new_items = NULL;
+    if (new_capacity == 0) {
+        return JSONFailure;
+    }
+    new_items = (JSON_Value**)parson_malloc(new_capacity * sizeof(JSON_Value*));
+    if (new_items == NULL) {
+        return JSONFailure;
+    }
+    if (array->items != NULL && array->count > 0) {
+        memcpy(new_items, array->items, array->count * sizeof(JSON_Value*));
+    }
+    parson_free(array->items);
+    array->items = new_items;
+    array->capacity = new_capacity;
+    return JSONSuccess;
+}
+
+static void json_array_free(JSON_Array *array) {
+    while (array->count--)
+        json_value_free(array->items[array->count]);
+    parson_free(array->items);
+    parson_free(array);
+}
+
+/* JSON Value */
+static JSON_Value * json_value_init_string_no_copy(char *string) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value)
+        return NULL;
+    new_value->type = JSONString;
+    new_value->value.string = string;
+    return new_value;
+}
+
+/* Parser */
+static void skip_quotes(const char **string) {
+    SKIP_CHAR(string);
+    while (**string != '\"') {
+        if (**string == '\0')
+            return;
+        if (**string == '\\') {
+            SKIP_CHAR(string);
+            if (**string == '\0')
+                return;
+        }
+        SKIP_CHAR(string);
+    }
+    SKIP_CHAR(string);
+}
+
+static int parse_utf_16(const char **unprocessed, char **processed) {
+    unsigned int cp, lead, trail;
+    char *processed_ptr = *processed;
+    const char *unprocessed_ptr = *unprocessed;
+    unprocessed_ptr++; /* skips u */
+    if (!is_utf16_hex((const unsigned char*)unprocessed_ptr) || sscanf(unprocessed_ptr, "%4x", &cp) == EOF)
+            return JSONFailure;
+    if (cp < 0x80) {
+        *processed_ptr = cp; /* 0xxxxxxx */
+    } else if (cp < 0x800) {
+        *processed_ptr++ = ((cp >> 6) & 0x1F) | 0xC0; /* 110xxxxx */
+        *processed_ptr   = ((cp     ) & 0x3F) | 0x80; /* 10xxxxxx */
+    } else if (cp < 0xD800 || cp > 0xDFFF) {
+        *processed_ptr++ = ((cp >> 12) & 0x0F) | 0xE0; /* 1110xxxx */
+        *processed_ptr++ = ((cp >> 6)  & 0x3F) | 0x80; /* 10xxxxxx */
+        *processed_ptr   = ((cp     )  & 0x3F) | 0x80; /* 10xxxxxx */
+    } else if (cp >= 0xD800 && cp <= 0xDBFF) { /* lead surrogate (0xD800..0xDBFF) */
+        lead = cp;
+        unprocessed_ptr += 4; /* should always be within the buffer, otherwise previous sscanf would fail */
+        if (*unprocessed_ptr++ != '\\' || *unprocessed_ptr++ != 'u' || /* starts with \u? */
+            !is_utf16_hex((const unsigned char*)unprocessed_ptr)          ||
+            sscanf(unprocessed_ptr, "%4x", &trail) == EOF           ||
+            trail < 0xDC00 || trail > 0xDFFF) { /* valid trail surrogate? (0xDC00..0xDFFF) */
+                return JSONFailure;
+        }
+        cp = ((((lead-0xD800)&0x3FF)<<10)|((trail-0xDC00)&0x3FF))+0x010000;
+        *processed_ptr++ = (((cp >> 18) & 0x07) | 0xF0); /* 11110xxx */
+        *processed_ptr++ = (((cp >> 12) & 0x3F) | 0x80); /* 10xxxxxx */
+        *processed_ptr++ = (((cp >> 6)  & 0x3F) | 0x80); /* 10xxxxxx */
+        *processed_ptr   = (((cp     )  & 0x3F) | 0x80); /* 10xxxxxx */
+    } else { /* trail surrogate before lead surrogate */
+        return JSONFailure;
+    }
+    unprocessed_ptr += 3;
+    *processed = processed_ptr;
+    *unprocessed = unprocessed_ptr;
+    return JSONSuccess;
+}
+
+
+/* Copies and processes passed string up to supplied length.
+Example: "\u006Corem ipsum" -> lorem ipsum */
+static char* process_string(const char *input, size_t len) {
+    const char *input_ptr = input;
+    size_t initial_size = (len + 1) * sizeof(char);
+    size_t final_size = 0;
+    char *output = (char*)parson_malloc(initial_size);
+    char *output_ptr = output;
+    char *resized_output = NULL;
+    while ((*input_ptr != '\0') && (size_t)(input_ptr - input) < len) {
+        if (*input_ptr == '\\') {
+            input_ptr++;
+            switch (*input_ptr) {
+                case '\"': *output_ptr = '\"'; break;
+                case '\\': *output_ptr = '\\'; break;
+                case '/':  *output_ptr = '/';  break;
+                case 'b':  *output_ptr = '\b'; break;
+                case 'f':  *output_ptr = '\f'; break;
+                case 'n':  *output_ptr = '\n'; break;
+                case 'r':  *output_ptr = '\r'; break;
+                case 't':  *output_ptr = '\t'; break;
+                case 'u':
+                    if (parse_utf_16(&input_ptr, &output_ptr) == JSONFailure)
+                        goto error;
+                    break;
+                default:
+                    goto error;
+            }
+        } else if ((unsigned char)*input_ptr < 0x20) {
+            goto error; /* 0x00-0x19 are invalid characters for json string (http://www.ietf.org/rfc/rfc4627.txt) */
+        } else {
+            *output_ptr = *input_ptr;
+        }
+        output_ptr++;
+        input_ptr++;
+    }
+    *output_ptr = '\0';
+    /* resize to new length */
+    final_size = (size_t)(output_ptr-output) + 1;
+    resized_output = (char*)parson_malloc(final_size);
+    if (resized_output == NULL)
+        goto error;
+    memcpy(resized_output, output, final_size);
+    parson_free(output);
+    return resized_output;
+error:
+    parson_free(output);
+    return NULL;
+}
+
+/* Return processed contents of a string between quotes and
+   skips passed argument to a matching quote. */
+static char * get_quoted_string(const char **string) {
+    const char *string_start = *string;
+    size_t string_len = 0;
+    skip_quotes(string);
+    if (**string == '\0')
+        return NULL;
+    string_len = *string - string_start - 2; /* length without quotes */
+    return process_string(string_start + 1, string_len);
+}
+
+static JSON_Value * parse_value(const char **string, size_t nesting) {
+    if (nesting > MAX_NESTING)
+        return NULL;
+    SKIP_WHITESPACES(string);
+    switch (**string) {
+        case '{':
+            return parse_object_value(string, nesting + 1);
+        case '[':
+            return parse_array_value(string, nesting + 1);
+        case '\"':
+            return parse_string_value(string);
+        case 'f': case 't':
+            return parse_boolean_value(string);
+        case '-':
+        case '0': case '1': case '2': case '3': case '4':
+        case '5': case '6': case '7': case '8': case '9':
+            return parse_number_value(string);
+        case 'n':
+            return parse_null_value(string);
+        default:
+            return NULL;
+    }
+}
+
+static JSON_Value * parse_object_value(const char **string, size_t nesting) {
+    JSON_Value *output_value = json_value_init_object(), *new_value = NULL;
+    JSON_Object *output_object = json_value_get_object(output_value);
+    char *new_key = NULL;
+    if (output_value == NULL)
+        return NULL;
+    SKIP_CHAR(string);
+    SKIP_WHITESPACES(string);
+    if (**string == '}') { /* empty object */
+        SKIP_CHAR(string);
+        return output_value;
+    }
+    while (**string != '\0') {
+        new_key = get_quoted_string(string);
+        SKIP_WHITESPACES(string);
+        if (new_key == NULL || **string != ':') {
+            json_value_free(output_value);
+            return NULL;
+        }
+        SKIP_CHAR(string);
+        new_value = parse_value(string, nesting);
+        if (new_value == NULL) {
+            parson_free(new_key);
+            json_value_free(output_value);
+            return NULL;
+        }
+        if(json_object_add(output_object, new_key, new_value) == JSONFailure) {
+            parson_free(new_key);
+            parson_free(new_value);
+            json_value_free(output_value);
+            return NULL;
+        }
+        parson_free(new_key);
+        SKIP_WHITESPACES(string);
+        if (**string != ',')
+            break;
+        SKIP_CHAR(string);
+        SKIP_WHITESPACES(string);
+    }
+    SKIP_WHITESPACES(string);
+    if (**string != '}' || /* Trim object after parsing is over */
+        json_object_resize(output_object, json_object_get_count(output_object)) == JSONFailure) {
+            json_value_free(output_value);
+            return NULL;
+    }
+    SKIP_CHAR(string);
+    return output_value;
+}
+
+static JSON_Value * parse_array_value(const char **string, size_t nesting) {
+    JSON_Value *output_value = json_value_init_array(), *new_array_value = NULL;
+    JSON_Array *output_array = json_value_get_array(output_value);
+    if (!output_value)
+        return NULL;
+    SKIP_CHAR(string);
+    SKIP_WHITESPACES(string);
+    if (**string == ']') { /* empty array */
+        SKIP_CHAR(string);
+        return output_value;
+    }
+    while (**string != '\0') {
+        new_array_value = parse_value(string, nesting);
+        if (!new_array_value) {
+            json_value_free(output_value);
+            return NULL;
+        }
+        if(json_array_add(output_array, new_array_value) == JSONFailure) {
+            parson_free(new_array_value);
+            json_value_free(output_value);
+            return NULL;
+        }
+        SKIP_WHITESPACES(string);
+        if (**string != ',')
+            break;
+        SKIP_CHAR(string);
+        SKIP_WHITESPACES(string);
+    }
+    SKIP_WHITESPACES(string);
+    if (**string != ']' || /* Trim array after parsing is over */
+        json_array_resize(output_array, json_array_get_count(output_array)) == JSONFailure) {
+            json_value_free(output_value);
+            return NULL;
+    }
+    SKIP_CHAR(string);
+    return output_value;
+}
+
+static JSON_Value * parse_string_value(const char **string) {
+    JSON_Value *value = NULL;
+    char *new_string = get_quoted_string(string);
+    if (new_string == NULL)
+        return NULL;
+    value = json_value_init_string_no_copy(new_string);
+    if (value == NULL) {
+        parson_free(new_string);
+        return NULL;
+    }
+    return value;
+}
+
+static JSON_Value * parse_boolean_value(const char **string) {
+    size_t true_token_size = SIZEOF_TOKEN("true");
+    size_t false_token_size = SIZEOF_TOKEN("false");
+    if (strncmp("true", *string, true_token_size) == 0) {
+        *string += true_token_size;
+        return json_value_init_boolean(1);
+    } else if (strncmp("false", *string, false_token_size) == 0) {
+        *string += false_token_size;
+        return json_value_init_boolean(0);
+    }
+    return NULL;
+}
+
+static JSON_Value * parse_number_value(const char **string) {
+    char *end;
+    double number = strtod(*string, &end);
+    JSON_Value *output_value;
+    if (is_decimal(*string, end - *string)) {
+        *string = end;
+        output_value = json_value_init_number(number);
+    } else {
+        output_value = NULL;
+    }
+    return output_value;
+}
+
+static JSON_Value * parse_null_value(const char **string) {
+    size_t token_size = SIZEOF_TOKEN("null");
+    if (strncmp("null", *string, token_size) == 0) {
+        *string += token_size;
+        return json_value_init_null();
+    }
+    return NULL;
+}
+
+/* Serialization */
+#define APPEND_STRING(str) do { written = append_string(buf, (str)); \
+                                if (written < 0) { return -1; } \
+                                if (buf != NULL) { buf += written; } \
+                                written_total += written; } while(0)
+
+#define APPEND_INDENT(level) do { written = append_indent(buf, (level)); \
+                                  if (written < 0) { return -1; } \
+                                  if (buf != NULL) { buf += written; } \
+                                  written_total += written; } while(0)
+
+static int json_serialize_to_buffer_r(const JSON_Value *value, char *buf, int level, int is_pretty, char *num_buf)
+{
+    const char *key = NULL, *string = NULL;
+    JSON_Value *temp_value = NULL;
+    JSON_Array *array = NULL;
+    JSON_Object *object = NULL;
+    size_t i = 0, count = 0;
+    double num = 0.0;
+    int written = -1, written_total = 0;
+
+    switch (json_value_get_type(value)) {
+        case JSONArray:
+            array = json_value_get_array(value);
+            count = json_array_get_count(array);
+            APPEND_STRING("[");
+            if (count > 0 && is_pretty)
+                APPEND_STRING("\n");
+            for (i = 0; i < count; i++) {
+                if (is_pretty)
+                    APPEND_INDENT(level+1);
+                temp_value = json_array_get_value(array, i);
+                written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf);
+                if (written < 0)
+                    return -1;
+                if (buf != NULL)
+                    buf += written;
+                written_total += written;
+                if (i < (count - 1))
+                    APPEND_STRING(",");
+                if (is_pretty)
+                    APPEND_STRING("\n");
+            }
+            if (count > 0 && is_pretty)
+                APPEND_INDENT(level);
+            APPEND_STRING("]");
+            return written_total;
+        case JSONObject:
+            object = json_value_get_object(value);
+            count  = json_object_get_count(object);
+            APPEND_STRING("{");
+            if (count > 0 && is_pretty)
+                APPEND_STRING("\n");
+            for (i = 0; i < count; i++) {
+                key = json_object_get_name(object, i);
+                if (is_pretty)
+                    APPEND_INDENT(level+1);
+                written = json_serialize_string(key, buf);
+                if (written < 0)
+                    return -1;
+                if (buf != NULL)
+                    buf += written;
+                written_total += written;
+                APPEND_STRING(":");
+                if (is_pretty)
+                    APPEND_STRING(" ");
+                temp_value = json_object_get_value(object, key);
+                written = json_serialize_to_buffer_r(temp_value, buf, level+1, is_pretty, num_buf);
+                if (written < 0)
+                    return -1;
+                if (buf != NULL)
+                    buf += written;
+                written_total += written;
+                if (i < (count - 1))
+                    APPEND_STRING(",");
+                if (is_pretty)
+                    APPEND_STRING("\n");
+            }
+            if (count > 0 && is_pretty)
+                APPEND_INDENT(level);
+            APPEND_STRING("}");
+            return written_total;
+        case JSONString:
+            string = json_value_get_string(value);
+            written = json_serialize_string(string, buf);
+            if (written < 0)
+                return -1;
+            if (buf != NULL)
+                buf += written;
+            written_total += written;
+            return written_total;
+        case JSONBoolean:
+            if (json_value_get_boolean(value))
+                APPEND_STRING("true");
+            else
+                APPEND_STRING("false");
+            return written_total;
+        case JSONNumber:
+            num = json_value_get_number(value);
+            if (buf != NULL)
+                num_buf = buf;
+            if (num == ((double)(int)num)) /*  check if num is integer */
+                written = sprintf(num_buf, "%d", (int)num);
+            else
+                written = sprintf(num_buf, DOUBLE_SERIALIZATION_FORMAT, num);
+            if (written < 0)
+                return -1;
+            if (buf != NULL)
+                buf += written;
+            written_total += written;
+            return written_total;
+        case JSONNull:
+            APPEND_STRING("null");
+            return written_total;
+        case JSONError:
+            return -1;
+        default:
+            return -1;
+    }
+}
+
+static int json_serialize_string(const char *string, char *buf) {
+    size_t i = 0, len = strlen(string);
+    char c = '\0';
+    int written = -1, written_total = 0;
+    APPEND_STRING("\"");
+    for (i = 0; i < len; i++) {
+        c = string[i];
+        switch (c) {
+            case '\"': APPEND_STRING("\\\""); break;
+            case '\\': APPEND_STRING("\\\\"); break;
+            case '/':  APPEND_STRING("\\/"); break; /* to make json embeddable in xml\/html */
+            case '\b': APPEND_STRING("\\b"); break;
+            case '\f': APPEND_STRING("\\f"); break;
+            case '\n': APPEND_STRING("\\n"); break;
+            case '\r': APPEND_STRING("\\r"); break;
+            case '\t': APPEND_STRING("\\t"); break;
+            default:
+                if (buf != NULL) {
+                    buf[0] = c;
+                    buf += 1;
+                }
+                written_total += 1;
+                break;
+        }
+    }
+    APPEND_STRING("\"");
+    return written_total;
+}
+
+static int append_indent(char *buf, int level) {
+    int i;
+    int written = -1, written_total = 0;
+    for (i = 0; i < level; i++) {
+        APPEND_STRING("    ");
+    }
+    return written_total;
+}
+
+static int append_string(char *buf, const char *string) {
+    if (buf == NULL) {
+        return (int)strlen(string);
+    }
+    return sprintf(buf, "%s", string);
+}
+
+#undef APPEND_STRING
+#undef APPEND_INDENT
+
+/* Parser API */
+JSON_Value * json_parse_file(const char *filename) {
+    char *file_contents = read_file(filename);
+    JSON_Value *output_value = NULL;
+    if (file_contents == NULL)
+        return NULL;
+    output_value = json_parse_string(file_contents);
+    parson_free(file_contents);
+    return output_value;
+}
+
+JSON_Value * json_parse_file_with_comments(const char *filename) {
+    char *file_contents = read_file(filename);
+    JSON_Value *output_value = NULL;
+    if (file_contents == NULL)
+        return NULL;
+    output_value = json_parse_string_with_comments(file_contents);
+    parson_free(file_contents);
+    return output_value;
+}
+
+JSON_Value * json_parse_string(const char *string) {
+    if (string == NULL)
+        return NULL;
+    SKIP_WHITESPACES(&string);
+    if (*string != '{' && *string != '[')
+        return NULL;
+    return parse_value((const char**)&string, 0);
+}
+
+JSON_Value * json_parse_string_with_comments(const char *string) {
+    JSON_Value *result = NULL;
+    char *string_mutable_copy = NULL, *string_mutable_copy_ptr = NULL;
+    string_mutable_copy = parson_strdup(string);
+    if (string_mutable_copy == NULL)
+        return NULL;
+    remove_comments(string_mutable_copy, "/*", "*/");
+    remove_comments(string_mutable_copy, "//", "\n");
+    string_mutable_copy_ptr = string_mutable_copy;
+    SKIP_WHITESPACES(&string_mutable_copy_ptr);
+    if (*string_mutable_copy_ptr != '{' && *string_mutable_copy_ptr != '[') {
+        parson_free(string_mutable_copy);
+        return NULL;
+    }
+    result = parse_value((const char**)&string_mutable_copy_ptr, 0);
+    parson_free(string_mutable_copy);
+    return result;
+}
+
+
+/* JSON Object API */
+
+JSON_Value * json_object_get_value(const JSON_Object *object, const char *name) {
+    if (object == NULL || name == NULL)
+        return NULL;
+    return json_object_nget_value(object, name, strlen(name));
+}
+
+const char * json_object_get_string(const JSON_Object *object, const char *name) {
+    return json_value_get_string(json_object_get_value(object, name));
+}
+
+double json_object_get_number(const JSON_Object *object, const char *name) {
+    return json_value_get_number(json_object_get_value(object, name));
+}
+
+JSON_Object * json_object_get_object(const JSON_Object *object, const char *name) {
+    return json_value_get_object(json_object_get_value(object, name));
+}
+
+JSON_Array * json_object_get_array(const JSON_Object *object, const char *name) {
+    return json_value_get_array(json_object_get_value(object, name));
+}
+
+int json_object_get_boolean(const JSON_Object *object, const char *name) {
+    return json_value_get_boolean(json_object_get_value(object, name));
+}
+
+JSON_Value * json_object_dotget_value(const JSON_Object *object, const char *name) {
+    const char *dot_position = strchr(name, '.');
+    if (!dot_position)
+        return json_object_get_value(object, name);
+    object = json_value_get_object(json_object_nget_value(object, name, dot_position - name));
+    return json_object_dotget_value(object, dot_position + 1);
+}
+
+const char * json_object_dotget_string(const JSON_Object *object, const char *name) {
+    return json_value_get_string(json_object_dotget_value(object, name));
+}
+
+double json_object_dotget_number(const JSON_Object *object, const char *name) {
+    return json_value_get_number(json_object_dotget_value(object, name));
+}
+
+JSON_Object * json_object_dotget_object(const JSON_Object *object, const char *name) {
+    return json_value_get_object(json_object_dotget_value(object, name));
+}
+
+JSON_Array * json_object_dotget_array(const JSON_Object *object, const char *name) {
+    return json_value_get_array(json_object_dotget_value(object, name));
+}
+
+int json_object_dotget_boolean(const JSON_Object *object, const char *name) {
+    return json_value_get_boolean(json_object_dotget_value(object, name));
+}
+
+size_t json_object_get_count(const JSON_Object *object) {
+    return object ? object->count : 0;
+}
+
+const char * json_object_get_name(const JSON_Object *object, size_t index) {
+    if (index >= json_object_get_count(object))
+        return NULL;
+    return object->names[index];
+}
+
+/* JSON Array API */
+JSON_Value * json_array_get_value(const JSON_Array *array, size_t index) {
+    if (index >= json_array_get_count(array))
+        return NULL;
+    return array->items[index];
+}
+
+const char * json_array_get_string(const JSON_Array *array, size_t index) {
+    return json_value_get_string(json_array_get_value(array, index));
+}
+
+double json_array_get_number(const JSON_Array *array, size_t index) {
+    return json_value_get_number(json_array_get_value(array, index));
+}
+
+JSON_Object * json_array_get_object(const JSON_Array *array, size_t index) {
+    return json_value_get_object(json_array_get_value(array, index));
+}
+
+JSON_Array * json_array_get_array(const JSON_Array *array, size_t index) {
+    return json_value_get_array(json_array_get_value(array, index));
+}
+
+int json_array_get_boolean(const JSON_Array *array, size_t index) {
+    return json_value_get_boolean(json_array_get_value(array, index));
+}
+
+size_t json_array_get_count(const JSON_Array *array) {
+    return array ? array->count : 0;
+}
+
+/* JSON Value API */
+JSON_Value_Type json_value_get_type(const JSON_Value *value) {
+    return value ? value->type : JSONError;
+}
+
+JSON_Object * json_value_get_object(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONObject ? value->value.object : NULL;
+}
+
+JSON_Array * json_value_get_array(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONArray ? value->value.array : NULL;
+}
+
+const char * json_value_get_string(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONString ? value->value.string : NULL;
+}
+
+double json_value_get_number(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONNumber ? value->value.number : 0;
+}
+
+int json_value_get_boolean(const JSON_Value *value) {
+    return json_value_get_type(value) == JSONBoolean ? value->value.boolean : -1;
+}
+
+void json_value_free(JSON_Value *value) {
+    switch (json_value_get_type(value)) {
+        case JSONObject:
+            json_object_free(value->value.object);
+            break;
+        case JSONString:
+            if (value->value.string) { parson_free(value->value.string); }
+            break;
+        case JSONArray:
+            json_array_free(value->value.array);
+            break;
+        default:
+            break;
+    }
+    parson_free(value);
+}
+
+JSON_Value * json_value_init_object(void) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value)
+        return NULL;
+    new_value->type = JSONObject;
+    new_value->value.object = json_object_init();
+    if (!new_value->value.object) {
+        parson_free(new_value);
+        return NULL;
+    }
+    return new_value;
+}
+
+JSON_Value * json_value_init_array(void) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value)
+        return NULL;
+    new_value->type = JSONArray;
+    new_value->value.array = json_array_init();
+    if (!new_value->value.array) {
+        parson_free(new_value);
+        return NULL;
+    }
+    return new_value;
+}
+
+JSON_Value * json_value_init_string(const char *string) {
+    char *copy = NULL;
+    JSON_Value *value;
+    size_t string_len = 0;
+    if (string == NULL)
+        return NULL;
+    string_len = strlen(string);
+    if (!is_valid_utf8(string, string_len))
+        return NULL;
+    copy = parson_strndup(string, string_len);
+    if (copy == NULL)
+        return NULL;
+    value = json_value_init_string_no_copy(copy);
+    if (value == NULL)
+        parson_free(copy);
+    return value;
+}
+
+JSON_Value * json_value_init_number(double number) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value)
+        return NULL;
+    new_value->type = JSONNumber;
+    new_value->value.number = number;
+    return new_value;
+}
+
+JSON_Value * json_value_init_boolean(int boolean) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value)
+        return NULL;
+    new_value->type = JSONBoolean;
+    new_value->value.boolean = boolean ? 1 : 0;
+    return new_value;
+}
+
+JSON_Value * json_value_init_null(void) {
+    JSON_Value *new_value = (JSON_Value*)parson_malloc(sizeof(JSON_Value));
+    if (!new_value)
+        return NULL;
+    new_value->type = JSONNull;
+    return new_value;
+}
+
+JSON_Value * json_value_deep_copy(const JSON_Value *value) {
+    size_t i = 0;
+    JSON_Value *return_value = NULL, *temp_value_copy = NULL, *temp_value = NULL;
+    const char *temp_string = NULL, *temp_key = NULL;
+    char *temp_string_copy = NULL;
+    JSON_Array *temp_array = NULL, *temp_array_copy = NULL;
+    JSON_Object *temp_object = NULL, *temp_object_copy = NULL;
+
+    switch (json_value_get_type(value)) {
+        case JSONArray:
+            temp_array = json_value_get_array(value);
+            return_value = json_value_init_array();
+            if (return_value == NULL)
+                return NULL;
+            temp_array_copy = json_value_get_array(return_value);
+            for (i = 0; i < json_array_get_count(temp_array); i++) {
+                temp_value = json_array_get_value(temp_array, i);
+                temp_value_copy = json_value_deep_copy(temp_value);
+                if (temp_value_copy == NULL) {
+                    json_value_free(return_value);
+                    return NULL;
+                }
+                if (json_array_add(temp_array_copy, temp_value_copy) == JSONFailure) {
+                    json_value_free(return_value);
+                    json_value_free(temp_value_copy);
+                    return NULL;
+                }
+            }
+            return return_value;
+        case JSONObject:
+            temp_object = json_value_get_object(value);
+            return_value = json_value_init_object();
+            if (return_value == NULL)
+                return NULL;
+            temp_object_copy = json_value_get_object(return_value);
+            for (i = 0; i < json_object_get_count(temp_object); i++) {
+                temp_key = json_object_get_name(temp_object, i);
+                temp_value = json_object_get_value(temp_object, temp_key);
+                temp_value_copy = json_value_deep_copy(temp_value);
+                if (temp_value_copy == NULL) {
+                    json_value_free(return_value);
+                    return NULL;
+                }
+                if (json_object_add(temp_object_copy, temp_key, temp_value_copy) == JSONFailure) {
+                    json_value_free(return_value);
+                    json_value_free(temp_value_copy);
+                    return NULL;
+                }
+            }
+            return return_value;
+        case JSONBoolean:
+            return json_value_init_boolean(json_value_get_boolean(value));
+        case JSONNumber:
+            return json_value_init_number(json_value_get_number(value));
+        case JSONString:
+            temp_string = json_value_get_string(value);
+            temp_string_copy = parson_strdup(temp_string);
+            if (temp_string_copy == NULL)
+                return NULL;
+            return_value = json_value_init_string_no_copy(temp_string_copy);
+            if (return_value == NULL)
+                parson_free(temp_string_copy);
+            return return_value;
+        case JSONNull:
+            return json_value_init_null();
+        case JSONError:
+            return NULL;
+        default:
+            return NULL;
+    }
+}
+
+size_t json_serialization_size(const JSON_Value *value) {
+    char num_buf[1100]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */
+    int res = json_serialize_to_buffer_r(value, NULL, 0, 0, num_buf);
+    return res < 0 ? 0 : (size_t)(res + 1);
+}
+
+JSON_Status json_serialize_to_buffer(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) {
+    int written = -1;
+    size_t needed_size_in_bytes = json_serialization_size(value);
+    if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes) {
+        return JSONFailure;
+    }
+    written = json_serialize_to_buffer_r(value, buf, 0, 0, NULL);
+    if (written < 0)
+        return JSONFailure;
+    return JSONSuccess;
+}
+
+JSON_Status json_serialize_to_file(const JSON_Value *value, const char *filename) {
+    JSON_Status return_code = JSONSuccess;
+    FILE *fp = NULL;
+    char *serialized_string = json_serialize_to_string(value);
+    if (serialized_string == NULL) {
+        return JSONFailure;
+    }
+    fp = fopen (filename, "w");
+    if (fp != NULL) {
+        if (fputs (serialized_string, fp) == EOF) {
+            return_code = JSONFailure;
+        }
+        if (fclose (fp) == EOF) {
+            return_code = JSONFailure;
+        }
+    }
+    json_free_serialized_string(serialized_string);
+    return return_code;
+}
+
+char * json_serialize_to_string(const JSON_Value *value) {
+    JSON_Status serialization_result = JSONFailure;
+    size_t buf_size_bytes = json_serialization_size(value);
+    char *buf = NULL;
+    if (buf_size_bytes == 0) {
+        return NULL;
+    }
+    buf = (char*)parson_malloc(buf_size_bytes);
+    if (buf == NULL)
+        return NULL;
+    serialization_result = json_serialize_to_buffer(value, buf, buf_size_bytes);
+    if (serialization_result == JSONFailure) {
+        json_free_serialized_string(buf);
+        return NULL;
+    }
+    return buf;
+}
+
+size_t json_serialization_size_pretty(const JSON_Value *value) {
+    char num_buf[1100]; /* recursively allocating buffer on stack is a bad idea, so let's do it only once */
+    int res = json_serialize_to_buffer_r(value, NULL, 0, 1, num_buf);
+    return res < 0 ? 0 : (size_t)(res + 1);
+}
+
+JSON_Status json_serialize_to_buffer_pretty(const JSON_Value *value, char *buf, size_t buf_size_in_bytes) {
+    int written = -1;
+    size_t needed_size_in_bytes = json_serialization_size_pretty(value);
+    if (needed_size_in_bytes == 0 || buf_size_in_bytes < needed_size_in_bytes)
+        return JSONFailure;
+    written = json_serialize_to_buffer_r(value, buf, 0, 1, NULL);
+    if (written < 0)
+        return JSONFailure;
+    return JSONSuccess;
+}
+
+JSON_Status json_serialize_to_file_pretty(const JSON_Value *value, const char *filename) {
+    JSON_Status return_code = JSONSuccess;
+    FILE *fp = NULL;
+    char *serialized_string = json_serialize_to_string_pretty(value);
+    if (serialized_string == NULL) {
+        return JSONFailure;
+    }
+    fp = fopen (filename, "w");
+    if (fp != NULL) {
+        if (fputs (serialized_string, fp) == EOF) {
+            return_code = JSONFailure;
+        }
+        if (fclose (fp) == EOF) {
+            return_code = JSONFailure;
+        }
+    }
+    json_free_serialized_string(serialized_string);
+    return return_code;
+}
+
+char * json_serialize_to_string_pretty(const JSON_Value *value) {
+    JSON_Status serialization_result = JSONFailure;
+    size_t buf_size_bytes = json_serialization_size_pretty(value);
+    char *buf = NULL;
+    if (buf_size_bytes == 0) {
+        return NULL;
+    }
+    buf = (char*)parson_malloc(buf_size_bytes);
+    if (buf == NULL)
+        return NULL;
+    serialization_result = json_serialize_to_buffer_pretty(value, buf, buf_size_bytes);
+    if (serialization_result == JSONFailure) {
+        json_free_serialized_string(buf);
+        return NULL;
+    }
+    return buf;
+}
+
+void json_free_serialized_string(char *string) {
+    parson_free(string);
+}
+
+JSON_Status json_array_remove(JSON_Array *array, size_t ix) {
+    JSON_Value *temp_value = NULL;
+    size_t last_element_ix = 0;
+    if (array == NULL || ix >= json_array_get_count(array)) {
+        return JSONFailure;
+    }
+    last_element_ix = json_array_get_count(array) - 1;
+    json_value_free(json_array_get_value(array, ix));
+    if (ix != last_element_ix) { /* Replace value with one from the end of array */
+        temp_value = json_array_get_value(array, last_element_ix);
+        if (temp_value == NULL) {
+            return JSONFailure;
+        }
+        array->items[ix] = temp_value;
+    }
+    array->count -= 1;
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_value(JSON_Array *array, size_t ix, JSON_Value *value) {
+    if (array == NULL || value == NULL || ix >= json_array_get_count(array)) {
+        return JSONFailure;
+    }
+    json_value_free(json_array_get_value(array, ix));
+    array->items[ix] = value;
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_string(JSON_Array *array, size_t i, const char* string) {
+    JSON_Value *value = json_value_init_string(string);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_replace_value(array, i, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_number(JSON_Array *array, size_t i, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_replace_value(array, i, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_boolean(JSON_Array *array, size_t i, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_replace_value(array, i, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_replace_null(JSON_Array *array, size_t i) {
+    JSON_Value *value = json_value_init_null();
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_replace_value(array, i, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_clear(JSON_Array *array) {
+    size_t i = 0;
+    if (array == NULL)
+        return JSONFailure;
+    for (i = 0; i < json_array_get_count(array); i++) {
+        json_value_free(json_array_get_value(array, i));
+    }
+    array->count = 0;
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_value(JSON_Array *array, JSON_Value *value) {
+    if (array == NULL || value == NULL)
+        return JSONFailure;
+    return json_array_add(array, value);
+}
+
+JSON_Status json_array_append_string(JSON_Array *array, const char *string) {
+    JSON_Value *value = json_value_init_string(string);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_append_value(array, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_number(JSON_Array *array, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_append_value(array, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_boolean(JSON_Array *array, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_append_value(array, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_array_append_null(JSON_Array *array) {
+    JSON_Value *value = json_value_init_null();
+    if (value == NULL)
+        return JSONFailure;
+    if (json_array_append_value(array, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_set_value(JSON_Object *object, const char *name, JSON_Value *value) {
+    size_t i = 0;
+    JSON_Value *old_value;
+    if (object == NULL || name == NULL || value == NULL)
+        return JSONFailure;
+    old_value = json_object_get_value(object, name);
+    if (old_value != NULL) { /* free and overwrite old value */
+        json_value_free(old_value);
+        for (i = 0; i < json_object_get_count(object); i++) {
+            if (strcmp(object->names[i], name) == 0) {
+                object->values[i] = value;
+                return JSONSuccess;
+            }
+        }
+    }
+    /* add new key value pair */
+    return json_object_add(object, name, value);
+}
+
+JSON_Status json_object_set_string(JSON_Object *object, const char *name, const char *string) {
+    return json_object_set_value(object, name, json_value_init_string(string));
+}
+
+JSON_Status json_object_set_number(JSON_Object *object, const char *name, double number) {
+    return json_object_set_value(object, name, json_value_init_number(number));
+}
+
+JSON_Status json_object_set_boolean(JSON_Object *object, const char *name, int boolean) {
+    return json_object_set_value(object, name, json_value_init_boolean(boolean));
+}
+
+JSON_Status json_object_set_null(JSON_Object *object, const char *name) {
+    return json_object_set_value(object, name, json_value_init_null());
+}
+
+JSON_Status json_object_dotset_value(JSON_Object *object, const char *name, JSON_Value *value) {
+    const char *dot_pos = NULL;
+    char *current_name = NULL;
+    JSON_Object *temp_obj = NULL;
+    JSON_Value *new_value = NULL;
+    if (value == NULL || name == NULL || value == NULL)
+        return JSONFailure;
+    dot_pos = strchr(name, '.');
+    if (dot_pos == NULL) {
+        return json_object_set_value(object, name, value);
+    } else {
+        current_name = parson_strndup(name, dot_pos - name);
+        temp_obj = json_object_get_object(object, current_name);
+        if (temp_obj == NULL) {
+            new_value = json_value_init_object();
+            if (new_value == NULL) {
+                parson_free(current_name);
+                return JSONFailure;
+            }
+            if (json_object_add(object, current_name, new_value) == JSONFailure) {
+                json_value_free(new_value);
+                parson_free(current_name);
+                return JSONFailure;
+            }
+            temp_obj = json_object_get_object(object, current_name);
+        }
+        parson_free(current_name);
+        return json_object_dotset_value(temp_obj, dot_pos + 1, value);
+    }
+}
+
+JSON_Status json_object_dotset_string(JSON_Object *object, const char *name, const char *string) {
+    JSON_Value *value = json_value_init_string(string);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_object_dotset_value(object, name, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_number(JSON_Object *object, const char *name, double number) {
+    JSON_Value *value = json_value_init_number(number);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_object_dotset_value(object, name, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_boolean(JSON_Object *object, const char *name, int boolean) {
+    JSON_Value *value = json_value_init_boolean(boolean);
+    if (value == NULL)
+        return JSONFailure;
+    if (json_object_dotset_value(object, name, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_dotset_null(JSON_Object *object, const char *name) {
+    JSON_Value *value = json_value_init_null();
+    if (value == NULL)
+        return JSONFailure;
+    if (json_object_dotset_value(object, name, value) == JSONFailure) {
+        json_value_free(value);
+        return JSONFailure;
+    }
+    return JSONSuccess;
+}
+
+JSON_Status json_object_remove(JSON_Object *object, const char *name) {
+    size_t i = 0, last_item_index = 0;
+    if (object == NULL || json_object_get_value(object, name) == NULL)
+        return JSONFailure;
+    last_item_index = json_object_get_count(object) - 1;
+    for (i = 0; i < json_object_get_count(object); i++) {
+        if (strcmp(object->names[i], name) == 0) {
+            parson_free(object->names[i]);
+            json_value_free(object->values[i]);
+            if (i != last_item_index) { /* Replace key value pair with one from the end */
+                object->names[i] = object->names[last_item_index];
+                object->values[i] = object->values[last_item_index];
+            }
+            object->count -= 1;
+            return JSONSuccess;
+        }
+    }
+    return JSONFailure; /* No execution path should end here */
+}
+
+JSON_Status json_object_dotremove(JSON_Object *object, const char *name) {
+    const char *dot_pos = strchr(name, '.');
+    char *current_name = NULL;
+    JSON_Object *temp_obj = NULL;
+    if (dot_pos == NULL) {
+        return json_object_remove(object, name);
+    } else {
+        current_name = parson_strndup(name, dot_pos - name);
+        temp_obj = json_object_get_object(object, current_name);
+        if (temp_obj == NULL) {
+            parson_free(current_name);
+            return JSONFailure;
+        }
+        parson_free(current_name);
+        return json_object_dotremove(temp_obj, dot_pos + 1);
+    }
+}
+
+JSON_Status json_object_clear(JSON_Object *object) {
+    size_t i = 0;
+    if (object == NULL) {
+        return JSONFailure;
+    }
+    for (i = 0; i < json_object_get_count(object); i++) {
+        parson_free(object->names[i]);
+        json_value_free(object->values[i]);
+    }
+    object->count = 0;
+    return JSONSuccess;
+}
+
+JSON_Status json_validate(const JSON_Value *schema, const JSON_Value *value) {
+    JSON_Value *temp_schema_value = NULL, *temp_value = NULL;
+    JSON_Array *schema_array = NULL, *value_array = NULL;
+    JSON_Object *schema_object = NULL, *value_object = NULL;
+    JSON_Value_Type schema_type = JSONError, value_type = JSONError;
+    const char *key = NULL;
+    size_t i = 0, count = 0;
+    if (schema == NULL || value == NULL)
+        return JSONFailure;
+    schema_type = json_value_get_type(schema);
+    value_type = json_value_get_type(value);
+    if (schema_type != value_type && schema_type != JSONNull) /* null represents all values */
+        return JSONFailure;
+    switch (schema_type) {
+        case JSONArray:
+            schema_array = json_value_get_array(schema);
+            value_array = json_value_get_array(value);
+            count = json_array_get_count(schema_array);
+            if (count == 0)
+                return JSONSuccess; /* Empty array allows all types */
+            /* Get first value from array, rest is ignored */
+            temp_schema_value = json_array_get_value(schema_array, 0);
+            for (i = 0; i < json_array_get_count(value_array); i++) {
+                temp_value = json_array_get_value(value_array, i);
+                if (json_validate(temp_schema_value, temp_value) == 0) {
+                    return JSONFailure;
+                }
+            }
+            return JSONSuccess;
+        case JSONObject:
+            schema_object = json_value_get_object(schema);
+            value_object = json_value_get_object(value);
+            count = json_object_get_count(schema_object);
+            if (count == 0)
+                return JSONSuccess; /* Empty object allows all objects */
+            else if (json_object_get_count(value_object) < count)
+                return JSONFailure; /* Tested object mustn't have less name-value pairs than schema */
+            for (i = 0; i < count; i++) {
+                key = json_object_get_name(schema_object, i);
+                temp_schema_value = json_object_get_value(schema_object, key);
+                temp_value = json_object_get_value(value_object, key);
+                if (temp_value == NULL)
+                    return JSONFailure;
+                if (json_validate(temp_schema_value, temp_value) == JSONFailure)
+                    return JSONFailure;
+            }
+            return JSONSuccess;
+        case JSONString: case JSONNumber: case JSONBoolean: case JSONNull:
+            return JSONSuccess; /* equality already tested before switch */
+        case JSONError: default:
+            return JSONFailure;
+    }
+}
+
+JSON_Status json_value_equals(const JSON_Value *a, const JSON_Value *b) {
+    JSON_Object *a_object = NULL, *b_object = NULL;
+    JSON_Array *a_array = NULL, *b_array = NULL;
+    const char *a_string = NULL, *b_string = NULL;
+    const char *key = NULL;
+    size_t a_count = 0, b_count = 0, i = 0;
+    JSON_Value_Type a_type, b_type;
+    a_type = json_value_get_type(a);
+    b_type = json_value_get_type(b);
+    if (a_type != b_type) {
+        return 0;
+    }
+    switch (a_type) {
+        case JSONArray:
+            a_array = json_value_get_array(a);
+            b_array = json_value_get_array(b);
+            a_count = json_array_get_count(a_array);
+            b_count = json_array_get_count(b_array);
+            if (a_count != b_count) {
+                return 0;
+            }
+            for (i = 0; i < a_count; i++) {
+                if (!json_value_equals(json_array_get_value(a_array, i),
+                                       json_array_get_value(b_array, i))) {
+                    return 0;
+                }
+            }
+            return 1;
+        case JSONObject:
+            a_object = json_value_get_object(a);
+            b_object = json_value_get_object(b);
+            a_count = json_object_get_count(a_object);
+            b_count = json_object_get_count(b_object);
+            if (a_count != b_count) {
+                return 0;
+            }
+            for (i = 0; i < a_count; i++) {
+                key = json_object_get_name(a_object, i);
+                if (!json_value_equals(json_object_get_value(a_object, key),
+                                       json_object_get_value(b_object, key))) {
+                    return 0;
+                }
+            }
+            return 1;
+        case JSONString:
+            a_string = json_value_get_string(a);
+            b_string = json_value_get_string(b);
+            return strcmp(a_string, b_string) == 0;
+        case JSONBoolean:
+            return json_value_get_boolean(a) == json_value_get_boolean(b);
+        case JSONNumber:
+            return fabs(json_value_get_number(a) - json_value_get_number(b)) < 0.000001; /* EPSILON */
+        case JSONError:
+            return 1;
+        case JSONNull:
+            return 1;
+        default:
+            return 1;
+    }
+}
+
+JSON_Value_Type json_type(const JSON_Value *value) {
+    return json_value_get_type(value);
+}
+
+JSON_Object * json_object (const JSON_Value *value) {
+    return json_value_get_object(value);
+}
+
+JSON_Array * json_array  (const JSON_Value *value) {
+    return json_value_get_array(value);
+}
+
+const char * json_string (const JSON_Value *value) {
+    return json_value_get_string(value);
+}
+
+double json_number (const JSON_Value *value) {
+    return json_value_get_number(value);
+}
+
+int json_boolean(const JSON_Value *value) {
+    return json_value_get_boolean(value);
+}
+
+void json_set_allocation_functions(JSON_Malloc_Function malloc_fun, JSON_Free_Function free_fun) {
+    parson_malloc = malloc_fun;
+    parson_free = free_fun;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/src/timersync.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,133 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    LoRa concentrator : Timer synchronization
+        Provides synchronization between unix, concentrator and gps clocks
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Michael Coracin
+*/
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <stdio.h>        /* printf, fprintf, snprintf, fopen, fputs */
+#include <stdint.h>        /* C99 types */
+#include <pthread.h>
+
+#include "trace.h"
+#include "timersync.h"
+#include "loragw_hal.h"
+#include "loragw_reg.h"
+#include "loragw_aux.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS & TYPES -------------------------------------------- */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define timersub(a, b, result)                                                \
+  do {                                                                        \
+    (result)->tv_sec = (a)->tv_sec - (b)->tv_sec;                             \
+    (result)->tv_usec = (a)->tv_usec - (b)->tv_usec;                          \
+    if ((result)->tv_usec < 0) {                                              \
+      --(result)->tv_sec;                                                     \
+      (result)->tv_usec += 1000000;                                           \
+    }                                                                         \
+  } while (0)
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE VARIABLES (GLOBAL) ------------------------------------------- */
+
+static pthread_mutex_t mx_timersync = PTHREAD_MUTEX_INITIALIZER; /* control access to timer sync offsets */
+static struct timeval offset_unix_concent = {0, 0}; /* timer offset between unix host and concentrator */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE SHARED VARIABLES (GLOBAL) ------------------------------------ */
+extern bool exit_sig;
+extern bool quit_sig;
+extern pthread_mutex_t mx_concent;
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS DEFINITION ------------------------------------------ */
+
+int get_concentrator_time(struct timeval *concent_time, struct timeval unix_time) {
+    struct timeval local_timeval;
+
+    if (concent_time == NULL) {
+        MSG("ERROR: %s invalid parameter\n", __FUNCTION__);
+        return -1;
+    }
+
+    pthread_mutex_lock(&mx_timersync); /* protect global variable access */
+    timersub(&unix_time, &offset_unix_concent, &local_timeval);
+    pthread_mutex_unlock(&mx_timersync);
+
+    /* TODO: handle sx1301 coutner wrap-up !! */
+    concent_time->tv_sec = local_timeval.tv_sec;
+    concent_time->tv_usec = local_timeval.tv_usec;
+
+    MSG_DEBUG(DEBUG_TIMERSYNC, " --> TIME: unix current time is   %ld,%ld\n", unix_time.tv_sec, unix_time.tv_usec);
+    MSG_DEBUG(DEBUG_TIMERSYNC, "           offset is              %ld,%ld\n", offset_unix_concent.tv_sec, offset_unix_concent.tv_usec);
+    MSG_DEBUG(DEBUG_TIMERSYNC, "           sx1301 current time is %ld,%ld\n", local_timeval.tv_sec, local_timeval.tv_usec);
+
+    return 0;
+}
+
+/* ---------------------------------------------------------------------------------------------- */
+/* --- THREAD 6: REGULARLAY MONITOR THE OFFSET BETWEEN UNIX CLOCK AND CONCENTRATOR CLOCK -------- */
+
+void thread_timersync(void) {
+    struct timeval unix_timeval;
+    struct timeval concentrator_timeval;
+    uint32_t sx1301_timecount = 0;
+    struct timeval offset_previous = {0, 0};
+    struct timeval offset_drift = {0, 0}; /* delta between current and previous offset */
+
+    while (!exit_sig && !quit_sig) {
+        /* Get current unix time */
+        gettimeofday(&unix_timeval, NULL);
+
+        /* Get current concentrator counter value (1MHz) */
+        lgw_get_trigcnt(&sx1301_timecount);
+
+        concentrator_timeval.tv_sec = sx1301_timecount / 1000000UL;
+        concentrator_timeval.tv_usec = sx1301_timecount - (concentrator_timeval.tv_sec * 1000000UL);
+
+        /* Compute offset between unix and concentrator timers, with microsecond precision */
+        offset_previous.tv_sec = offset_unix_concent.tv_sec;
+        offset_previous.tv_usec = offset_unix_concent.tv_usec;
+
+        pthread_mutex_lock(&mx_timersync); /* protect global variable access */
+        timersub(&unix_timeval, &concentrator_timeval, &offset_unix_concent);
+        pthread_mutex_unlock(&mx_timersync);
+
+        timersub(&offset_unix_concent, &offset_previous, &offset_drift);
+
+        MSG_DEBUG(DEBUG_TIMERSYNC, "  sx1301    = %u (µs) - timeval (%ld,%ld)\n",
+                  sx1301_timecount,
+                  concentrator_timeval.tv_sec,
+                  concentrator_timeval.tv_usec);
+        MSG_DEBUG(DEBUG_TIMERSYNC, "  unix_timeval = %ld,%ld\n", unix_timeval.tv_sec, unix_timeval.tv_usec);
+
+        MSG("INFO: host/sx1301 time offset=(%lds:%ldµs) - drift=%ldµs\n",
+            offset_unix_concent.tv_sec,
+            offset_unix_concent.tv_usec,
+            offset_drift.tv_sec * 1000000UL + offset_drift.tv_usec);
+
+        /* delay next sync */
+        /* If we consider a crystal oscillator precision of about 20ppm worst case, and a clock
+            running at 1MHz, this would mean 1µs drift every 50000µs (10000000/20).
+            As here the time precision is not critical, we should be able to cope with at least 1ms drift,
+            which should occur after 50s (50000µs * 1000).
+            Let's set the thread sleep to 1 minute for now */
+        wait_ms(60000);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lora_pkt_fwd/update_gwid.sh	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,35 @@
+#!/bin/sh
+
+# This script is a helper to update the Gateway_ID field of given
+# JSON configuration file, as a EUI-64 address generated from the 64-bits
+# STM32 unique ID of the PicoCell Gateway board.
+#
+# Usage examples:
+#       ./update_gwid.sh ./local_conf.json
+
+iot_sk_update_gwid() {
+    # get gateway ID from STM32 unique ID to generate an EUI-64 address
+    GWID=$(./util_chip_id)
+
+    # replace the default gateway ID by actual GWID, in given JSON configuration file
+    sed -i 's/\(^\s*"gateway_ID":\s*"\).\{16\}"\s*\(,\?\).*$/\1'${GWID}'"\2/' $1
+
+    echo "Gateway_ID set to "$GWID" in file "$1
+}
+
+if [ $# -ne 1 ]
+then
+    echo "Usage: $0 [filename]"
+    echo "  filename: Path to JSON file containing Gateway_ID for packet forwarder"
+    exit 1
+fi
+
+# Check if util_chip_id is there, necessary to get STM32 unique ID
+if ! [ -x "$(command -v ./util_chip_id)" ]; then
+  echo 'Error: ./util_chip_id not found.' >&2
+  exit 1
+fi
+
+iot_sk_update_gwid $1
+
+exit 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/readme.md	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,116 @@
+	 / _____)             _              | |    
+	( (____  _____ ____ _| |_ _____  ____| |__  
+	 \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
+	 _____) ) ____| | | || |_| ____( (___| | | |
+	(______/|_____)_|_|_| \__)_____)\____)_| |_|
+	  (C)2013 Semtech-Cycleo
+
+Lora network packet forwarder project
+======================================
+
+## 1. Core program: lora_pkt_fwd
+--------------------------------
+
+The packet forwarder is a program running on the host of a LoRa Picocell gateway
+that forwards RF packets receive by the concentrator to a server through a IP/UDP
+link, and emits RF packets that are sent by the server.
+
+	((( Y )))
+	    |
+	    |
+	+- -|- - - - - - - - - - - - -+        xxxxxxxxxxxx          +--------+
+	|+--+-----------+     +------+|       xx x  x     xxx        |        |
+	||              |     |      ||      xx  Internet  xx        |        |
+	|| Concentrator |<----+ Host |<------xx     or    xx-------->|        |
+	||              | USB |      ||      xx  Intranet  xx        | Server |
+	|+--------------+     +------+|       xxxx   x   xxxx        |        |
+	|                             |           xxxxxxxx           |        |
+	|                             |                              |        |
+	|            Gateway          |                              +--------+
+	+- - - - - - - - - - - - - - -+
+
+Uplink: radio packets received by the gateway, with metadata added by the
+gateway, forwarded to the server. Might also include gateway status.
+
+Downlink: packets generated by the server, with additional metadata, to be
+transmitted by the gateway on the radio channel. Might also include
+configuration data for the gateway.
+
+## 2. Helper programs
+---------------------
+
+Those programs are included in the project to provide examples on how to 
+communicate with the packet forwarder, and to help the system builder use it 
+without having to implement a full Lora network server.
+
+### 2.1. util_sink ###
+
+The packet sink is a simple helper program listening on a single port for UDP 
+datagrams, and displaying a message each time one is received. The content of 
+the datagram itself is ignored.
+
+### 2.2. util_ack ###
+
+The packet acknowledger is a simple helper program listening on a single UDP 
+port and responding to PUSH_DATA datagrams with PUSH_ACK, and to PULL_DATA 
+datagrams with PULL_ACK.
+
+### 2.3. util_tx_test ###
+
+The network packet sender is a simple helper program used to send packets 
+through the gateway-to-server downlink route.
+
+## 3. Helper scripts
+--------------------
+
+### 3.1. packet_forwarder/lora_pkt_fwd/update_gwid.sh
+
+This script allows automatic update of Gateway_ID with unique MCU ID, in
+packet forwarder JSON configuration file.
+
+note: the util_chip_id procided with picoGW_hal repository needs to be compiled
+and located in the same directory as this script.
+
+## 4. User Guide
+----------------
+
+[A detailed PicoCell GW user guide is available here](http://www.semtech.com/images/datasheet/picocell_gateway_user_guide.pdf)
+
+## 5. Changelog
+---------------
+
+### v0.1.0 - 2017-05-03 ###
+
+* Updated lora_pkt_fwd for lgw_connect API change. A new command line option
+is available to specify the COM device to be used to access the PicoCell board.
+
+### v0.0.1 - 2017-04-07 ###
+
+* Initial release of the packet forwarder , protocol specifications and helper
+programs for Picocell Gateway, based on the packet forwarder of the Gateway
+v1.0/v1.5.
+
+## 6. Legal notice
+------------------
+
+The information presented in this project documentation does not form part of 
+any quotation or contract, is believed to be accurate and reliable and may be 
+changed without notice. No liability will be accepted by the publisher for any 
+consequence of its use. Publication thereof does not convey nor imply any 
+license under patent or other industrial or intellectual property rights. 
+Semtech assumes no responsibility or liability whatsoever for any failure or 
+unexpected operation resulting from misuse, neglect improper installation, 
+repair or improper handling or unusual physical or electrical stress 
+including, but not limited to, exposure to parameters beyond the specified 
+maximum ratings or operation outside the specified range. 
+
+SEMTECH PRODUCTS ARE NOT DESIGNED, INTENDED, AUTHORIZED OR WARRANTED TO BE 
+SUITABLE FOR USE IN LIFE-SUPPORT APPLICATIONS, DEVICES OR SYSTEMS OR OTHER 
+CRITICAL APPLICATIONS. INCLUSION OF SEMTECH PRODUCTS IN SUCH APPLICATIONS IS 
+UNDERSTOOD TO BE UNDERTAKEN SOLELY AT THE CUSTOMER’S OWN RISK. Should a 
+customer purchase or use Semtech products for any such unauthorized 
+application, the customer shall indemnify and hold Semtech and its officers, 
+employees, subsidiaries, affiliates, and distributors harmless against all 
+claims, costs damages and attorney fees which could arise.
+
+*EOF*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_ack/Makefile	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,33 @@
+### Application-specific constants
+
+APP_NAME := util_ack
+
+### Constant symbols
+
+CC := $(CROSS_COMPILE)gcc
+AR := $(CROSS_COMPILE)ar
+
+CFLAGS := -O2 -Wall -Wextra -std=c99 -Iinc -I.
+
+OBJDIR = obj
+
+### General build targets
+
+all: $(APP_NAME)
+
+clean:
+	rm -f $(OBJDIR)/*.o
+	rm -f $(APP_NAME)
+
+### Main program compilation and assembly
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(OBJDIR)/%.o: src/%.c | $(OBJDIR)
+	$(CC) -c $(CFLAGS) $< -o $@
+
+$(APP_NAME): $(OBJDIR)/$(APP_NAME).o
+	$(CC) $< -o $@
+
+### EOF
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_ack/readme.md	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,65 @@
+	 / _____)             _              | |    
+	( (____  _____ ____ _| |_ _____  ____| |__  
+	 \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
+	 _____) ) ____| | | || |_| ____( (___| | | |
+	(______/|_____)_|_|_| \__)_____)\____)_| |_|
+	  (C)2013 Semtech-Cycleo
+
+Utility: packet acknowledger
+=============================
+
+1. Introduction
+----------------
+
+The packet acknowledger is a simple helper program listening on a single UDP 
+port and responding to PUSH_DATA datagrams with PUSH_ACK, and to PULL_DATA 
+datagrams with PULL_ACK.
+
+Informations about the datagrams received and the answers send are display on 
+screen to help communication debugging.
+
+Packets not following the protocol detailed in the PROTOCOL.TXT document in the
+basic_pkt_fwt directory are ignored.
+
+2. Dependencies
+----------------
+
+This program follows the v1.1 version of the gateway-to-server protocol.
+
+3. Usage
+---------
+
+Start the program with the port number as first and only argument.
+
+To stop the application, press Ctrl+C.
+
+4. License
+-----------
+
+Copyright (C) 2013, SEMTECH S.A.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+* Neither the name of the Semtech corporation nor the
+  names of its contributors may be used to endorse or promote products
+  derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL SEMTECH S.A. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*EOF*
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_ack/src/util_ack.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,192 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Network sink, receives UDP packets and sends an acknowledge
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+/* fix an issue between POSIX and C99 */
+#if __STDC_VERSION__ >= 199901L
+#define _XOPEN_SOURCE 600
+#else
+#define _XOPEN_SOURCE 500
+#endif
+
+#include <stdint.h>     /* C99 types */
+#include <stdio.h>      /* printf, fprintf, sprintf, fopen, fputs */
+#include <unistd.h>     /* usleep */
+
+#include <string.h>     /* memset */
+#include <time.h>       /* time, clock_gettime, strftime, gmtime, clock_nanosleep*/
+#include <stdlib.h>     /* atoi, exit */
+#include <errno.h>      /* error messages */
+
+#include <sys/socket.h> /* socket specific definitions */
+#include <netinet/in.h> /* INET constants and stuff */
+#include <arpa/inet.h>  /* IP address conversion stuff */
+#include <netdb.h>      /* gai_strerror */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define ARRAY_SIZE(a)   (sizeof(a) / sizeof((a)[0]))
+#define STRINGIFY(x)    #x
+#define STR(x)          STRINGIFY(x)
+#define MSG(args...)    fprintf(stderr, args) /* message that is destined to the user */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS ---------------------------------------------------- */
+
+#define PROTOCOL_VERSION 2
+
+#define PKT_PUSH_DATA    0
+#define PKT_PUSH_ACK     1
+#define PKT_PULL_DATA    2
+#define PKT_PULL_RESP    3
+#define PKT_PULL_ACK     4
+
+/* -------------------------------------------------------------------------- */
+/* --- MAIN FUNCTION -------------------------------------------------------- */
+
+int main(int argc, char **argv) {
+    int i; /* loop variable and temporary variable for return value */
+
+    /* server socket creation */
+    int sock; /* socket file descriptor */
+    struct addrinfo hints;
+    struct addrinfo *result; /* store result of getaddrinfo */
+    struct addrinfo *q; /* pointer to move into *result data */
+    char host_name[64];
+    char port_name[64];
+
+    /* variables for receiving and sending packets */
+    struct sockaddr_storage dist_addr;
+    socklen_t addr_len = sizeof dist_addr;
+    uint8_t databuf[4096];
+    int byte_nb;
+
+    /* variables for protocol management */
+    uint32_t raw_mac_h; /* Most Significant Nibble, network order */
+    uint32_t raw_mac_l; /* Least Significant Nibble, network order */
+    uint64_t gw_mac; /* MAC address of the client (gateway) */
+    uint8_t ack_command;
+
+    /* check if port number was passed as parameter */
+    if (argc != 2) {
+        MSG("Usage: util_ack <port number>\n");
+        exit(EXIT_FAILURE);
+    }
+
+    /* prepare hints to open network sockets */
+    memset(&hints, 0, sizeof hints);
+    hints.ai_family = AF_UNSPEC; /* should handle IP v4 or v6 automatically */
+    hints.ai_socktype = SOCK_DGRAM;
+    hints.ai_flags = AI_PASSIVE; /* will assign local IP automatically */
+
+    /* look for address */
+    i = getaddrinfo(NULL, argv[1], &hints, &result);
+    if (i != 0) {
+        MSG("ERROR: getaddrinfo returned %s\n", gai_strerror(i));
+        exit(EXIT_FAILURE);
+    }
+
+    /* try to open socket and bind it */
+    for (q = result; q != NULL; q = q->ai_next) {
+        sock = socket(q->ai_family, q->ai_socktype, q->ai_protocol);
+        if (sock == -1) {
+            continue; /* socket failed, try next field */
+        } else {
+            i = bind(sock, q->ai_addr, q->ai_addrlen);
+            if (i == -1) {
+                shutdown(sock, SHUT_RDWR);
+                continue; /* bind failed, try next field */
+            } else {
+                break; /* success, get out of loop */
+            }
+        }
+    }
+    if (q == NULL) {
+        MSG("ERROR: failed to open socket or to bind to it\n");
+        i = 1;
+        for (q = result; q != NULL; q = q->ai_next) {
+            getnameinfo(q->ai_addr, q->ai_addrlen, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+            MSG("INFO: result %i host:%s service:%s\n", i, host_name, port_name);
+            ++i;
+        }
+        exit(EXIT_FAILURE);
+    }
+    MSG("INFO: util_ack listening on port %s\n", argv[1]);
+    freeaddrinfo(result);
+
+    while (1) {
+        /* wait to receive a packet */
+        byte_nb = recvfrom(sock, databuf, sizeof databuf, 0, (struct sockaddr *)&dist_addr, &addr_len);
+        if (byte_nb == -1) {
+            MSG("ERROR: recvfrom returned %s \n", strerror(errno));
+            exit(EXIT_FAILURE);
+        }
+
+        /* display info about the sender */
+        i = getnameinfo((struct sockaddr *)&dist_addr, addr_len, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+        if (i == -1) {
+            MSG("ERROR: getnameinfo returned %s \n", gai_strerror(i));
+            exit(EXIT_FAILURE);
+        }
+        printf(" -> pkt in , host %s (port %s), %i bytes", host_name, port_name, byte_nb);
+
+        /* check and parse the payload */
+        if (byte_nb < 12) { /* not enough bytes for packet from gateway */
+            printf(" (too short for GW <-> MAC protocol)\n");
+            continue;
+        }
+        /* don't touch the token in position 1-2, it will be sent back "as is" for acknowledgement */
+        if (databuf[0] != PROTOCOL_VERSION) { /* check protocol version number */
+            printf(", invalid version %u\n", databuf[0]);
+            continue;
+        }
+        raw_mac_h = *((uint32_t *)(databuf + 4));
+        raw_mac_l = *((uint32_t *)(databuf + 8));
+        gw_mac = ((uint64_t)ntohl(raw_mac_h) << 32) + (uint64_t)ntohl(raw_mac_l);
+
+        /* interpret gateway command */
+        switch (databuf[3]) {
+            case PKT_PUSH_DATA:
+                printf(", PUSH_DATA from gateway 0x%08X%08X\n", (uint32_t)(gw_mac >> 32), (uint32_t)(gw_mac & 0xFFFFFFFF));
+                ack_command = PKT_PUSH_ACK;
+                printf("<-  pkt out, PUSH_ACK for host %s (port %s)", host_name, port_name);
+                break;
+            case PKT_PULL_DATA:
+                printf(", PULL_DATA from gateway 0x%08X%08X\n", (uint32_t)(gw_mac >> 32), (uint32_t)(gw_mac & 0xFFFFFFFF));
+                ack_command = PKT_PULL_ACK;
+                printf("<-  pkt out, PULL_ACK for host %s (port %s)", host_name, port_name);
+                break;
+            default:
+                printf(", unexpected command %u\n", databuf[3]);
+                continue;
+        }
+
+        /* add some artificial latency */
+        usleep(30000); /* 30 ms */
+
+        /* send acknowledge and check return value */
+        databuf[3] = ack_command;
+        byte_nb = sendto(sock, (void *)databuf, 4, 0, (struct sockaddr *)&dist_addr, addr_len);
+        if (byte_nb == -1) {
+            printf(", send error:%s\n", strerror(errno));
+        } else {
+            printf(", %i bytes sent\n", byte_nb);
+        }
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_sink/Makefile	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,33 @@
+### Application-specific constants
+
+APP_NAME := util_sink
+
+### Constant symbols
+
+CC := $(CROSS_COMPILE)gcc
+AR := $(CROSS_COMPILE)ar
+
+CFLAGS := -O2 -Wall -Wextra -std=c99 -Iinc -I.
+
+OBJDIR = obj
+
+### General build targets
+
+all: $(APP_NAME)
+
+clean:
+	rm -f $(OBJDIR)/*.o
+	rm -f $(APP_NAME)
+
+### Main program compilation and assembly
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(OBJDIR)/%.o: src/%.c | $(OBJDIR)
+	$(CC) -c $(CFLAGS) $< -o $@
+
+$(APP_NAME): $(OBJDIR)/$(APP_NAME).o
+	$(CC) $< -o $@
+
+### EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_sink/readme.md	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,62 @@
+	 / _____)             _              | |    
+	( (____  _____ ____ _| |_ _____  ____| |__  
+	 \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
+	 _____) ) ____| | | || |_| ____( (___| | | |
+	(______/|_____)_|_|_| \__)_____)\____)_| |_|
+	  (C)2013 Semtech-Cycleo
+
+Utility: packet sink
+=====================
+
+1. Introduction
+----------------
+
+The packet sink is a simple helper program listening on a single port for UDP 
+datagrams, and displaying a message each time one is received. The content of 
+the datagram itself is ignored.
+
+This allow to test another software (locally or on another computer) that 
+sends UDP datagrams without having ICMP 'port closed' errors each time.
+
+2. Dependencies
+----------------
+
+None.
+
+3. Usage
+---------
+
+Start the program with the port number as first and only argument.
+
+To stop the application, press Ctrl+C.
+
+4. License
+-----------
+
+Copyright (C) 2013, SEMTECH S.A.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+* Neither the name of the Semtech corporation nor the
+  names of its contributors may be used to endorse or promote products
+  derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL SEMTECH S.A. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*EOF*
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_sink/src/util_sink.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,124 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Network sink, receives UDP packets on certain ports and discards them
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+/* fix an issue between POSIX and C99 */
+#if __STDC_VERSION__ >= 199901L
+#define _XOPEN_SOURCE 600
+#else
+#define _XOPEN_SOURCE 500
+#endif
+
+#include <stdint.h>     /* C99 types */
+#include <stdio.h>      /* printf, fprintf, sprintf, fopen, fputs */
+
+#include <string.h>     /* memset */
+#include <time.h>       /* time, clock_gettime, strftime, gmtime, clock_nanosleep*/
+#include <stdlib.h>     /* atoi, exit */
+#include <errno.h>      /* error messages */
+
+#include <sys/socket.h> /* socket specific definitions */
+#include <netinet/in.h> /* INET constants and stuff */
+#include <arpa/inet.h>  /* IP address conversion stuff */
+#include <netdb.h>      /* gai_strerror */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define ARRAY_SIZE(a)   (sizeof(a) / sizeof((a)[0]))
+#define STRINGIFY(x)    #x
+#define STR(x)          STRINGIFY(x)
+#define MSG(args...)    fprintf(stderr, args) /* message that is destined to the user */
+
+/* -------------------------------------------------------------------------- */
+/* --- MAIN FUNCTION -------------------------------------------------------- */
+
+int main(int argc, char **argv) {
+    int i; /* loop variable and temporary variable for return value */
+
+    /* server socket creation */
+    int sock; /* socket file descriptor */
+    struct addrinfo hints;
+    struct addrinfo *result; /* store result of getaddrinfo */
+    struct addrinfo *q; /* pointer to move into *result data */
+    char host_name[64];
+    char port_name[64];
+
+    /* variables for receiving packets */
+    struct sockaddr_storage dist_addr;
+    socklen_t addr_len = sizeof dist_addr;
+    uint8_t databuf[4096];
+    int byte_nb;
+
+    /* check if port number was passed as parameter */
+    if (argc != 2) {
+        MSG("Usage: util_sink <port number>\n");
+        exit(EXIT_FAILURE);
+    }
+
+    /* prepare hints to open network sockets */
+    memset(&hints, 0, sizeof hints);
+    hints.ai_family = AF_UNSPEC; /* should handle IP v4 or v6 automatically */
+    hints.ai_socktype = SOCK_DGRAM;
+    hints.ai_flags = AI_PASSIVE; /* will assign local IP automatically */
+
+    /* look for address */
+    i = getaddrinfo(NULL, argv[1], &hints, &result);
+    if (i != 0) {
+        MSG("ERROR: getaddrinfo returned %s\n", gai_strerror(i));
+        exit(EXIT_FAILURE);
+    }
+
+    /* try to open socket and bind it */
+    for (q = result; q != NULL; q = q->ai_next) {
+        sock = socket(q->ai_family, q->ai_socktype, q->ai_protocol);
+        if (sock == -1) {
+            continue; /* socket failed, try next field */
+        } else {
+            i = bind(sock, q->ai_addr, q->ai_addrlen);
+            if (i == -1) {
+                shutdown(sock, SHUT_RDWR);
+                continue; /* bind failed, try next field */
+            } else {
+                break; /* success, get out of loop */
+            }
+        }
+    }
+    if (q == NULL) {
+        MSG("ERROR: failed to open socket or to bind to it\n");
+        i = 1;
+        for (q = result; q != NULL; q = q->ai_next) {
+            getnameinfo(q->ai_addr, q->ai_addrlen, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+            MSG("result %i host:%s service:%s\n", i, host_name, port_name);
+            ++i;
+        }
+        exit(EXIT_FAILURE);
+    }
+    MSG("INFO: util_sink listening on port %s\n", argv[1]);
+    freeaddrinfo(result);
+
+    while (1) {
+        byte_nb = recvfrom(sock, databuf, sizeof databuf, 0, (struct sockaddr *)&dist_addr, &addr_len);
+        if (byte_nb == -1) {
+            MSG("ERROR: recvfrom returned %s \n", strerror(errno));
+            exit(EXIT_FAILURE);
+        }
+        getnameinfo((struct sockaddr *)&dist_addr, addr_len, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+        printf("Got packet from host %s port %s, %i bytes long\n", host_name, port_name, byte_nb);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_tx_test/Makefile	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,36 @@
+### Application-specific constants
+
+APP_NAME := util_tx_test
+
+### Constant symbols
+
+CC := $(CROSS_COMPILE)gcc
+AR := $(CROSS_COMPILE)ar
+
+CFLAGS := -O2 -Wall -Wextra -std=c99 -Iinc -I.
+
+OBJDIR = obj
+INCLUDES = $(wildcard inc/*.h)
+
+### General build targets
+
+all: $(APP_NAME)
+
+clean:
+	rm -f $(OBJDIR)/*.o
+	rm -f $(APP_NAME)
+
+### Sub-modules compilation
+
+$(OBJDIR):
+	mkdir -p $(OBJDIR)
+
+$(OBJDIR)/%.o: src/%.c $(INCLUDES) | $(OBJDIR)
+	$(CC) -c $(CFLAGS) $< -o $@
+
+### Main program assembly
+
+$(APP_NAME): $(OBJDIR)/$(APP_NAME).o  $(OBJDIR)/base64.o
+	$(CC) $< $(OBJDIR)/base64.o -o $@
+
+### EOF
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_tx_test/inc/base64.h	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,62 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Base64 encoding & decoding library
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+#ifndef _BASE64_H
+#define _BASE64_H
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <stdint.h>        /* C99 types */
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS PROTOTYPES ------------------------------------------ */
+
+/**
+@brief Encode binary data in Base64 string (no padding)
+@param in pointer to a table of binary data
+@param size number of bytes to be encoded to base64
+@param out pointer to a string where the function will output encoded data
+@param max_len max length of the out string (including null char)
+@return >=0 length of the resulting string (w/o null char), -1 for error
+*/
+int bin_to_b64_nopad(const uint8_t * in, int size, char * out, int max_len);
+
+/**
+@brief Decode Base64 string to binary data (no padding)
+@param in string containing only base64 valid characters
+@param size number of characters to be decoded from base64 (w/o null char)
+@param out pointer to a data buffer where the function will output decoded data
+@param out_max_len usable size of the output data buffer
+@return >=0 number of bytes written to the data buffer, -1 for error
+*/
+int b64_to_bin_nopad(const char * in, int size, uint8_t * out, int max_len);
+
+/* === derivative functions === */
+
+/**
+@brief Encode binary data in Base64 string (with added padding)
+*/
+int bin_to_b64(const uint8_t * in, int size, char * out, int max_len);
+
+/**
+@brief Decode Base64 string to binary data (remove padding if necessary)
+*/
+int b64_to_bin(const char * in, int size, uint8_t * out, int max_len);
+
+#endif
+
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_tx_test/readme.md	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,75 @@
+	 / _____)             _              | |    
+	( (____  _____ ____ _| |_ _____  ____| |__  
+	 \____ \| ___ |    (_   _) ___ |/ ___)  _ \ 
+	 _____) ) ____| | | || |_| ____( (___| | | |
+	(______/|_____)_|_|_| \__)_____)\____)_| |_|
+	  (C)2013 Semtech-Cycleo
+
+Utility: network packet sender
+===============================
+
+1. Introduction
+----------------
+
+The network packet sender is a simple helper program used to send packets 
+through the gateway-to-server downlink route.
+
+The program start by waiting for a gateway to send it a PULL_DATA datagram.
+After that, it will send back to the gateway a specified amount of PULL_RESP 
+datagrams, each containing a packet to be sent immediately and a variable 
+payload.
+
+2. Dependencies
+----------------
+
+This program follows the v1.1 version of the gateway-to-server protocol.
+
+3. Usage
+---------
+
+The application runs until the specified number of packets have been sent.
+Press Ctrl+C to stop the application before that.
+
+Use the -h option to get help and details about available options.
+
+The packets are [9-n] bytes long, and have following payload content:
++----------+---------------+---------------+---------------+---------------+---+---+---+---+---+---+---+---+
+|    Id    | PktCnt[31:24] | PktCnt[23:16] | PktCnt[15:8]  | PktCnt[7:0]   | P | E | R |FCS| 0 | 1 |...| n |
++----------+---------------+---------------+---------------+---------------+---+---+---+---+---+---+---+---+
+
+Id            : User defined ID to differentiate sender at receiver side. (8 bits)
+PktCnt        : Packet counter incremented at each transmission. (32 bits)
+‘P’, ‘E’, ‘R’ : ASCII values for characters 'P', 'E' and 'R'.
+FCS           : Checksum: 8-bits sum of Id, PktCnt[31 :24] , PktCnt[23 :16] , PktCnt[15 :8] , PktCnt[7:0], ‘P’,’E’,’R’
+0,1, ..., n   : Padding bytes up until user specified payload length.
+
+4. License
+-----------
+
+Copyright (C) 2013, SEMTECH S.A.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+* Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+* Neither the name of the Semtech corporation nor the
+  names of its contributors may be used to endorse or promote products
+  derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL SEMTECH S.A. BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+*EOF*
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_tx_test/src/base64.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,308 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Base64 encoding & decoding library
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "base64.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define ARRAY_SIZE(a)       (sizeof(a) / sizeof((a)[0]))
+#define CRIT(a)             fprintf(stderr, "\nCRITICAL file:%s line:%d msg:%s\n", __FILE__, __LINE__, a);exit(EXIT_FAILURE)
+
+//#define DEBUG(args...)    fprintf(stderr,"debug: " args) /* diagnostic message that is destined to the user */
+#define DEBUG(args...)
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS ---------------------------------------------------- */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MODULE-WIDE VARIABLES ---------------------------------------- */
+
+static char code_62 = '+';    /* RFC 1421 standard character for code 62 */
+static char code_63 = '/';    /* RFC 1421 standard character for code 63 */
+static char code_pad = '=';    /* RFC 1421 padding character if padding */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DECLARATION ---------------------------------------- */
+
+/**
+@brief Convert a code in the range 0-63 to an ASCII character
+*/
+char code_to_char(uint8_t x);
+
+/**
+@brief Convert an ASCII character to a code in the range 0-63
+*/
+uint8_t char_to_code(char x);
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DEFINITION ----------------------------------------- */
+
+char code_to_char(uint8_t x) {
+    if (x <= 25) {
+        return 'A' + x;
+    } else if ((x >= 26) && (x <= 51)) {
+        return 'a' + (x - 26);
+    } else if ((x >= 52) && (x <= 61)) {
+        return '0' + (x - 52);
+    } else if (x == 62) {
+        return code_62;
+    } else if (x == 63) {
+        return code_63;
+    } else {
+        DEBUG("ERROR: %i IS OUT OF RANGE 0-63 FOR BASE64 ENCODING\n", x);
+        exit(EXIT_FAILURE);
+    } //TODO: improve error management
+}
+
+uint8_t char_to_code(char x) {
+    if ((x >= 'A') && (x <= 'Z')) {
+        return (uint8_t)x - (uint8_t)'A';
+    } else if ((x >= 'a') && (x <= 'z')) {
+        return (uint8_t)x - (uint8_t)'a' + 26;
+    } else if ((x >= '0') && (x <= '9')) {
+        return (uint8_t)x - (uint8_t)'0' + 52;
+    } else if (x == code_62) {
+        return 62;
+    } else if (x == code_63) {
+        return 63;
+    } else {
+        DEBUG("ERROR: %c (0x%x) IS INVALID CHARACTER FOR BASE64 DECODING\n", x, x);
+        exit(EXIT_FAILURE);
+    } //TODO: improve error management
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- PUBLIC FUNCTIONS DEFINITION ------------------------------------------ */
+
+int bin_to_b64_nopad(const uint8_t * in, int size, char * out, int max_len) {
+    int i;
+    int result_len; /* size of the result */
+    int full_blocks; /* number of 3 unsigned chars / 4 characters blocks */
+    int last_bytes; /* number of unsigned chars <3 in the last block */
+    int last_chars; /* number of characters <4 in the last block */
+    uint32_t b;
+
+    /* check input values */
+    if ((out == NULL) || (in == NULL)) {
+        DEBUG("ERROR: NULL POINTER AS OUTPUT IN BIN_TO_B64\n");
+        return -1;
+    }
+    if (size == 0) {
+        *out = 0; /* null string */
+        return 0;
+    }
+
+    /* calculate the number of base64 'blocks' */
+    full_blocks = size / 3;
+    last_bytes = size % 3;
+    switch (last_bytes) {
+        case 0: /* no byte left to encode */
+            last_chars = 0;
+            break;
+        case 1: /* 1 byte left to encode -> +2 chars */
+            last_chars = 2;
+            break;
+        case 2: /* 2 bytes left to encode -> +3 chars */
+            last_chars = 3;
+            break;
+        default:
+            CRIT("switch default that should not be possible");
+    }
+
+    /* check if output buffer is big enough */
+    result_len = (4 * full_blocks) + last_chars;
+    if (max_len < (result_len + 1)) { /* 1 char added for string terminator */
+        DEBUG("ERROR: OUTPUT BUFFER TOO SMALL IN BIN_TO_B64\n");
+        return -1;
+    }
+
+    /* process all the full blocks */
+    for (i = 0; i < full_blocks; ++i) {
+        b  = (0xFF & in[3 * i]    ) << 16;
+        b |= (0xFF & in[3 * i + 1]) << 8;
+        b |=  0xFF & in[3 * i + 2];
+        out[4 * i + 0] = code_to_char((b >> 18) & 0x3F);
+        out[4 * i + 1] = code_to_char((b >> 12) & 0x3F);
+        out[4 * i + 2] = code_to_char((b >> 6 ) & 0x3F);
+        out[4 * i + 3] = code_to_char( b        & 0x3F);
+    }
+
+    /* process the last 'partial' block and terminate string */
+    i = full_blocks;
+    if (last_chars == 0) {
+        out[4 * i] =  0; /* null character to terminate string */
+    } else if (last_chars == 2) {
+        b  = (0xFF & in[3 * i]    ) << 16;
+        out[4 * i + 0] = code_to_char((b >> 18) & 0x3F);
+        out[4 * i + 1] = code_to_char((b >> 12) & 0x3F);
+        out[4 * i + 2] =  0; /* null character to terminate string */
+    } else if (last_chars == 3) {
+        b  = (0xFF & in[3 * i]    ) << 16;
+        b |= (0xFF & in[3 * i + 1]) << 8;
+        out[4 * i + 0] = code_to_char((b >> 18) & 0x3F);
+        out[4 * i + 1] = code_to_char((b >> 12) & 0x3F);
+        out[4 * i + 2] = code_to_char((b >> 6 ) & 0x3F);
+        out[4 * i + 3] = 0; /* null character to terminate string */
+    }
+
+    return result_len;
+}
+
+int b64_to_bin_nopad(const char * in, int size, uint8_t * out, int max_len) {
+    int i;
+    int result_len; /* size of the result */
+    int full_blocks; /* number of 3 unsigned chars / 4 characters blocks */
+    int last_chars; /* number of characters <4 in the last block */
+    int last_bytes; /* number of unsigned chars <3 in the last block */
+    uint32_t b;
+    ;
+
+    /* check input values */
+    if ((out == NULL) || (in == NULL)) {
+        DEBUG("ERROR: NULL POINTER AS OUTPUT OR INPUT IN B64_TO_BIN\n");
+        return -1;
+    }
+    if (size == 0) {
+        return 0;
+    }
+
+    /* calculate the number of base64 'blocks' */
+    full_blocks = size / 4;
+    last_chars = size % 4;
+    switch (last_chars) {
+        case 0: /* no char left to decode */
+            last_bytes = 0;
+            break;
+        case 1: /* only 1 char left is an error */
+            DEBUG("ERROR: ONLY ONE CHAR LEFT IN B64_TO_BIN\n");
+            return -1;
+        case 2: /* 2 chars left to decode -> +1 byte */
+            last_bytes = 1;
+            break;
+        case 3: /* 3 chars left to decode -> +2 bytes */
+            last_bytes = 2;
+            break;
+        default:
+            CRIT("switch default that should not be possible");
+    }
+
+    /* check if output buffer is big enough */
+    result_len = (3 * full_blocks) + last_bytes;
+    if (max_len < result_len) {
+        DEBUG("ERROR: OUTPUT BUFFER TOO SMALL IN B64_TO_BIN\n");
+        return -1;
+    }
+
+    /* process all the full blocks */
+    for (i = 0; i < full_blocks; ++i) {
+        b  = (0x3F & char_to_code(in[4 * i]    )) << 18;
+        b |= (0x3F & char_to_code(in[4 * i + 1])) << 12;
+        b |= (0x3F & char_to_code(in[4 * i + 2])) << 6;
+        b |=  0x3F & char_to_code(in[4 * i + 3]);
+        out[3 * i + 0] = (b >> 16) & 0xFF;
+        out[3 * i + 1] = (b >> 8 ) & 0xFF;
+        out[3 * i + 2] =  b        & 0xFF;
+    }
+
+    /* process the last 'partial' block */
+    i = full_blocks;
+    if (last_bytes == 1) {
+        b  = (0x3F & char_to_code(in[4 * i]    )) << 18;
+        b |= (0x3F & char_to_code(in[4 * i + 1])) << 12;
+        out[3 * i + 0] = (b >> 16) & 0xFF;
+        if (((b >> 12) & 0x0F) != 0) {
+            DEBUG("WARNING: last character contains unusable bits\n");
+        }
+    } else if (last_bytes == 2) {
+        b  = (0x3F & char_to_code(in[4 * i]    )) << 18;
+        b |= (0x3F & char_to_code(in[4 * i + 1])) << 12;
+        b |= (0x3F & char_to_code(in[4 * i + 2])) << 6;
+        out[3 * i + 0] = (b >> 16) & 0xFF;
+        out[3 * i + 1] = (b >> 8 ) & 0xFF;
+        if (((b >> 6) & 0x03) != 0) {
+            DEBUG("WARNING: last character contains unusable bits\n");
+        }
+    }
+
+    return result_len;
+}
+
+int bin_to_b64(const uint8_t * in, int size, char * out, int max_len) {
+    int ret;
+
+    ret = bin_to_b64_nopad(in, size, out, max_len);
+
+    if (ret == -1) {
+        return -1;
+    }
+    switch (ret % 4) {
+        case 0: /* nothing to do */
+            return ret;
+        case 1:
+            DEBUG("ERROR: INVALID UNPADDED BASE64 STRING\n");
+            return -1;
+        case 2: /* 2 chars in last block, must add 2 padding char */
+            if (max_len >= (ret + 2 + 1)) {
+                out[ret] = code_pad;
+                out[ret + 1] = code_pad;
+                out[ret + 2] = 0;
+                return ret + 2;
+            } else {
+                DEBUG("ERROR: not enough room to add padding in bin_to_b64\n");
+                return -1;
+            }
+        case 3: /* 3 chars in last block, must add 1 padding char */
+            if (max_len >= (ret + 1 + 1)) {
+                out[ret] = code_pad;
+                out[ret + 1] = 0;
+                return ret + 1;
+            } else {
+                DEBUG("ERROR: not enough room to add padding in bin_to_b64\n");
+                return -1;
+            }
+        default:
+            CRIT("switch default that should not be possible");
+    }
+}
+
+int b64_to_bin(const char * in, int size, uint8_t * out, int max_len) {
+    if (in == NULL) {
+        DEBUG("ERROR: NULL POINTER AS OUTPUT OR INPUT IN B64_TO_BIN\n");
+        return -1;
+    }
+    if ((size % 4 == 0) && (size >= 4)) { /* potentially padded Base64 */
+        if (in[size - 2] == code_pad) { /* 2 padding char to ignore */
+            return b64_to_bin_nopad(in, size - 2, out, max_len);
+        } else if (in[size - 1] == code_pad) { /* 1 padding char to ignore */
+            return b64_to_bin_nopad(in, size - 1, out, max_len);
+        } else { /* no padding to ignore */
+            return b64_to_bin_nopad(in, size, out, max_len);
+        }
+    } else { /* treat as unpadded Base64 */
+        return b64_to_bin_nopad(in, size, out, max_len);
+    }
+}
+
+
+/* --- EOF ------------------------------------------------------------------ */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util_tx_test/src/util_tx_test.c	Wed Apr 11 14:47:16 2018 +0000
@@ -0,0 +1,502 @@
+/*
+ / _____)             _              | |
+( (____  _____ ____ _| |_ _____  ____| |__
+ \____ \| ___ |    (_   _) ___ |/ ___)  _ \
+ _____) ) ____| | | || |_| ____( (___| | | |
+(______/|_____)_|_|_| \__)_____)\____)_| |_|
+  (C)2013 Semtech-Cycleo
+
+Description:
+    Ask a gateway to emit packets using GW <-> server protocol
+
+License: Revised BSD License, see LICENSE.TXT file include in the project
+Maintainer: Sylvain Miermont
+*/
+
+
+/* -------------------------------------------------------------------------- */
+/* --- DEPENDANCIES --------------------------------------------------------- */
+
+/* fix an issue between POSIX and C99 */
+#if __STDC_VERSION__ >= 199901L
+#define _XOPEN_SOURCE 600
+#else
+#define _XOPEN_SOURCE 500
+#endif
+
+#include <stdint.h>     /* C99 types */
+#include <stdbool.h>    /* bool type */
+#include <stdio.h>      /* printf fprintf sprintf fopen fputs */
+#include <unistd.h>     /* getopt access usleep */
+
+#include <string.h>     /* memset */
+#include <signal.h>     /* sigaction */
+#include <stdlib.h>     /* exit codes */
+#include <errno.h>      /* error messages */
+
+#include <sys/socket.h> /* socket specific definitions */
+#include <netinet/in.h> /* INET constants and stuff */
+#include <arpa/inet.h>  /* IP address conversion stuff */
+#include <netdb.h>      /* gai_strerror */
+
+#include "base64.h"
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE MACROS ------------------------------------------------------- */
+
+#define ARRAY_SIZE(a)   (sizeof(a) / sizeof((a)[0]))
+#define MSG(args...)    fprintf(stderr, args) /* message that is destined to the user */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE CONSTANTS ---------------------------------------------------- */
+
+#define PROTOCOL_VERSION 2
+
+#define PKT_PUSH_DATA   0
+#define PKT_PUSH_ACK    1
+#define PKT_PULL_DATA   2
+#define PKT_PULL_RESP   3
+#define PKT_PULL_ACK    4
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE VARIABLES (GLOBAL) ------------------------------------------- */
+
+/* signal handling variables */
+struct sigaction sigact; /* SIGQUIT&SIGINT&SIGTERM signal handling */
+static int exit_sig = 0; /* 1 -> application terminates cleanly (shut down hardware, close open files, etc) */
+static int quit_sig = 0; /* 1 -> application terminates without shutting down the hardware */
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DECLARATION ---------------------------------------- */
+
+static void sig_handler(int sigio);
+
+void usage (void);
+
+/* -------------------------------------------------------------------------- */
+/* --- PRIVATE FUNCTIONS DEFINITION ----------------------------------------- */
+
+static void sig_handler(int sigio) {
+    if (sigio == SIGQUIT) {
+        quit_sig = 1;;
+    } else if ((sigio == SIGINT) || (sigio == SIGTERM)) {
+        exit_sig = 1;
+    }
+}
+
+/* describe command line options */
+void usage(void) {
+    MSG("Usage: util_tx_test {options}\n");
+    MSG("Available options:\n");
+    MSG(" -h print this help\n");
+    MSG(" -n <int or service> port number for gateway link\n");
+    MSG(" -f <float> target frequency in MHz\n");
+    MSG(" -m <str> Modulation type ['LORA, 'FSK']\n");
+    MSG(" -s <int> Spreading Factor [7:12]\n");
+    MSG(" -b <int> Modulation bandwidth in kHz [125,250,500]\n");
+    MSG(" -d <uint> FSK frequency deviation in kHz [1:250]\n");
+    MSG(" -r <float> FSK bitrate in kbps [0.5:250]\n");
+    MSG(" -p <int> RF power (dBm)\n");
+    MSG(" -z <uint> Payload size in bytes [9:255]\n");
+    MSG(" -t <int> pause between packets (ms)\n");
+    MSG(" -x <int> numbers of times the sequence is repeated\n");
+    MSG(" -v <uint> test ID, inserted in payload for PER test [0:255]\n");
+    MSG(" -i send packet using inverted modulation polarity \n");
+}
+
+/* -------------------------------------------------------------------------- */
+/* --- MAIN FUNCTION -------------------------------------------------------- */
+
+int main(int argc, char **argv) {
+    int i, j, x;
+    unsigned int xu;
+    char arg_s[64];
+
+    /* application parameters */
+    char mod[64] = "LORA"; /* LoRa modulation by default */
+    float f_target = 866.0; /* target frequency */
+    int sf = 10; /* SF10 by default */
+    int bw = 125; /* 125kHz bandwidth by default */
+    int pow = 14; /* 14 dBm by default */
+    int delay = 1000; /* 1 second between packets by default */
+    int repeat = 1; /* sweep only once by default */
+    bool invert = false;
+    float br_kbps = 50; /* 50 kbps by default */
+    uint8_t fdev_khz = 25; /* 25 khz by default */
+
+    /* packet payload variables */
+    int payload_size = 9; /* minimum size for PER frame */
+    uint8_t payload_bin[255];
+    char payload_b64[341];
+    int payload_index;
+
+    /* PER payload variables */
+    uint8_t id = 0;
+
+    /* server socket creation */
+    int sock; /* socket file descriptor */
+    struct addrinfo hints;
+    struct addrinfo *result; /* store result of getaddrinfo */
+    struct addrinfo *q; /* pointer to move into *result data */
+    char serv_port[8] = "1680";
+    char host_name[64];
+    char port_name[64];
+
+    /* variables for receiving and sending packets */
+    struct sockaddr_storage dist_addr;
+    socklen_t addr_len = sizeof dist_addr;
+    uint8_t databuf[500];
+    int buff_index;
+    int byte_nb;
+
+    /* variables for gateway identification */
+    uint32_t raw_mac_h; /* Most Significant Nibble, network order */
+    uint32_t raw_mac_l; /* Least Significant Nibble, network order */
+    uint64_t gw_mac; /* MAC address of the client (gateway) */
+
+    /* prepare hints to open network sockets */
+    memset(&hints, 0, sizeof hints);
+    hints.ai_family = AF_UNSPEC; /* should handle IP v4 or v6 automatically */
+    hints.ai_socktype = SOCK_DGRAM;
+    hints.ai_flags = AI_PASSIVE; /* will assign local IP automatically */
+
+    /* parse command line options */
+    while ((i = getopt (argc, argv, "hn:f:m:s:b:d:r:p:z:t:x:v:i")) != -1) {
+        switch (i) {
+            case 'h':
+                usage();
+                return EXIT_FAILURE;
+                break;
+
+            case 'n': /* -n <int or service> port number for gateway link */
+                strncpy(serv_port, optarg, sizeof serv_port);
+                break;
+
+            case 'f': /* -f <float> target frequency in MHz */
+                i = sscanf(optarg, "%f", &f_target);
+                if ((i != 1) || (f_target < 30.0) || (f_target > 3000.0)) {
+                    MSG("ERROR: invalid TX frequency\n");
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'm': /* -m <str> Modulation type */
+                i = sscanf(optarg, "%63s", arg_s);
+                if ((i != 1) || ((strcmp(arg_s, "LORA") != 0) && (strcmp(arg_s, "FSK")))) {
+                    MSG("ERROR: invalid modulation type\n");
+                    usage();
+                    return EXIT_FAILURE;
+                } else {
+                    sprintf(mod, "%s", arg_s);
+                }
+                break;
+
+            case 's': /* -s <int> Spreading Factor */
+                i = sscanf(optarg, "%i", &sf);
+                if ((i != 1) || (sf < 7) || (sf > 12)) {
+                    MSG("ERROR: invalid spreading factor\n");
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'b': /* -b <int> Modulation bandwidth in kHz */
+                i = sscanf(optarg, "%i", &bw);
+                if ((i != 1) || ((bw != 125) && (bw != 250) && (bw != 500))) {
+                    MSG("ERROR: invalid LORA bandwidth\n");
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'd': /* -d <uint> FSK frequency deviation */
+                i = sscanf(optarg, "%u", &xu);
+                if ((i != 1) || (xu < 1) || (xu > 250)) {
+                    MSG("ERROR: invalid FSK frequency deviation\n");
+                    usage();
+                    return EXIT_FAILURE;
+                } else {
+                    fdev_khz = (uint8_t)xu;
+                }
+                break;
+
+            case 'r': /* -q <float> FSK bitrate */
+                i = sscanf(optarg, "%f", &br_kbps);
+                if ((i != 1) || (br_kbps < 0.5) || (br_kbps > 250)) {
+                    MSG("ERROR: invalid FSK bitrate\n");
+                    usage();
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'p': /* -p <int> RF power */
+                i = sscanf(optarg, "%i", &pow);
+                if ((i != 1) || (pow < 0) || (pow > 30)) {
+                    MSG("ERROR: invalid RF power\n");
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'z': /* -z <uint> Payload size */
+                i = sscanf(optarg, "%i", &payload_size);
+                if ((i != 1) || (payload_size < 9) || (payload_size > 255)) {
+                    MSG("ERROR: invalid payload size\n");
+                    usage();
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 't': /* -t <int> pause between RF packets (ms) */
+                i = sscanf(optarg, "%i", &delay);
+                if ((i != 1) || (delay < 0)) {
+                    MSG("ERROR: invalid time between RF packets\n");
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'x': /* -x <int> numbers of times the sequence is repeated */
+                i = sscanf(optarg, "%d", &repeat);
+                if ((i != 1) || (repeat < 1)) {
+                    MSG("ERROR: invalid number of repeats\n");
+                    return EXIT_FAILURE;
+                }
+                break;
+
+            case 'v': /* -v <uint> test Id */
+                i = sscanf(optarg, "%u", &xu);
+                if ((i != 1) || ((xu < 1) || (xu > 255))) {
+                    MSG("ERROR: invalid Id\n");
+                    return EXIT_FAILURE;
+                } else {
+                    id = (uint8_t)xu;
+                }
+                break;
+
+            case 'i': /* -i send packet using inverted modulation polarity */
+                invert = true;
+                break;
+
+            default:
+                MSG("ERROR: argument parsing failure, use -h option for help\n");
+                usage();
+                return EXIT_FAILURE;
+        }
+    }
+
+    /* compose local address (auto-complete a structure for socket) */
+    i = getaddrinfo(NULL, serv_port, &hints, &result);
+    if (i != 0) {
+        MSG("ERROR: getaddrinfo returned %s\n", gai_strerror(i));
+        exit(EXIT_FAILURE);
+    }
+
+    /* try to open socket and bind to it */
+    for (q = result; q != NULL; q = q->ai_next) {
+        sock = socket(q->ai_family, q->ai_socktype, q->ai_protocol);
+        if (sock == -1) {
+            continue; /* socket failed, try next field */
+        } else {
+            i = bind(sock, q->ai_addr, q->ai_addrlen);
+            if (i == -1) {
+                shutdown(sock, SHUT_RDWR);
+                continue; /* bind failed, try next field */
+            } else {
+                break; /* success, get out of loop */
+            }
+        }
+    }
+    if (q == NULL) {
+        MSG("ERROR: failed to open socket or to bind to it\n");
+        exit(EXIT_FAILURE);
+    }
+    freeaddrinfo(result);
+
+    /* configure signal handling */
+    sigemptyset(&sigact.sa_mask);
+    sigact.sa_flags = 0;
+    sigact.sa_handler = sig_handler;
+    sigaction(SIGQUIT, &sigact, NULL);
+    sigaction(SIGINT, &sigact, NULL);
+    sigaction(SIGTERM, &sigact, NULL);
+
+    /* display setup summary */
+    if (strcmp(mod, "FSK") == 0) {
+        MSG("INFO: %i FSK pkts @%f MHz (FDev %u kHz, Bitrate %.2f kbps, %dB payload) %i dBm, %i ms between each\n", repeat, f_target, fdev_khz, br_kbps, payload_size, pow, delay);
+    } else {
+        MSG("INFO: %i LoRa pkts @%f MHz (BW %d kHz, SF%i, %dB payload) %i dBm, %i ms between each\n", repeat, f_target, bw, sf, payload_size, pow, delay);
+    }
+
+    /* wait to receive a PULL_DATA request packet */
+    MSG("INFO: waiting to receive a PULL_DATA request on port %s\n", serv_port);
+    while (1) {
+        byte_nb = recvfrom(sock, databuf, sizeof databuf, 0, (struct sockaddr *)&dist_addr, &addr_len);
+        if ((quit_sig == 1) || (exit_sig == 1)) {
+            exit(EXIT_SUCCESS);
+        } else if (byte_nb < 0) {
+            MSG("WARNING: recvfrom returned an error\n");
+        } else if ((byte_nb < 12) || (databuf[0] != PROTOCOL_VERSION) || (databuf[3] != PKT_PULL_DATA)) {
+            MSG("INFO: packet received, not PULL_DATA request\n");
+        } else {
+            break; /* success! */
+        }
+    }
+
+    /* retrieve gateway MAC from the request */
+    raw_mac_h = *((uint32_t *)(databuf + 4));
+    raw_mac_l = *((uint32_t *)(databuf + 8));
+    gw_mac = ((uint64_t)ntohl(raw_mac_h) << 32) + (uint64_t)ntohl(raw_mac_l);
+
+    /* display info about the sender */
+    i = getnameinfo((struct sockaddr *)&dist_addr, addr_len, host_name, sizeof host_name, port_name, sizeof port_name, NI_NUMERICHOST);
+    if (i == -1) {
+        MSG("ERROR: getnameinfo returned %s \n", gai_strerror(i));
+        exit(EXIT_FAILURE);
+    }
+    MSG("INFO: PULL_DATA request received from gateway 0x%08X%08X (host %s, port %s)\n", (uint32_t)(gw_mac >> 32), (uint32_t)(gw_mac & 0xFFFFFFFF), host_name, port_name);
+
+    /* PKT_PULL_RESP datagrams header */
+    databuf[0] = PROTOCOL_VERSION;
+    databuf[1] = 0; /* no token */
+    databuf[2] = 0; /* no token */
+    databuf[3] = PKT_PULL_RESP;
+    buff_index = 4;
+
+    /* start of JSON structure */
+    memcpy((void *)(databuf + buff_index), (void *)"{\"txpk\":{\"imme\":true", 20);
+    buff_index += 20;
+
+    /* TX frequency */
+    i = snprintf((char *)(databuf + buff_index), 20, ",\"freq\":%.6f", f_target);
+    if ((i >= 0) && (i < 20)) {
+        buff_index += i;
+    } else {
+        MSG("ERROR: snprintf failed line %d\n", (__LINE__ - 4));
+        exit(EXIT_FAILURE);
+    }
+
+    /* RF channel */
+    memcpy((void *)(databuf + buff_index), (void *)",\"rfch\":0", 9);
+    buff_index += 9;
+
+    /* TX power */
+    i = snprintf((char *)(databuf + buff_index), 12, ",\"powe\":%i", pow);
+    if ((i >= 0) && (i < 12)) {
+        buff_index += i;
+    } else {
+        MSG("ERROR: snprintf failed line %d\n", (__LINE__ - 4));
+        exit(EXIT_FAILURE);
+    }
+
+    /* modulation type and parameters */
+    if (strcmp(mod, "FSK") == 0) {
+        i = snprintf((char *)(databuf + buff_index), 50, ",\"modu\":\"FSK\",\"datr\":%u,\"fdev\":%u", (unsigned int)(br_kbps * 1e3), (unsigned int)(fdev_khz * 1e3));
+        if ((i >= 0) && (i < 50)) {
+            buff_index += i;
+        } else {
+            MSG("ERROR: snprintf failed line %d\n", (__LINE__ - 4));
+            exit(EXIT_FAILURE);
+        }
+    } else {
+        i = snprintf((char *)(databuf + buff_index), 50, ",\"modu\":\"LORA\",\"datr\":\"SF%iBW%i\",\"codr\":\"4/6\"", sf, bw);
+        if ((i >= 0) && (i < 50)) {
+            buff_index += i;
+        } else {
+            MSG("ERROR: snprintf failed line %d\n", (__LINE__ - 4));
+            exit(EXIT_FAILURE);
+        }
+    }
+
+    /* signal polarity */
+    if (invert) {
+        memcpy((void *)(databuf + buff_index), (void *)",\"ipol\":true", 12);
+        buff_index += 12;
+    } else {
+        memcpy((void *)(databuf + buff_index), (void *)",\"ipol\":false", 13);
+        buff_index += 13;
+    }
+
+    /* Preamble size */
+    if (strcmp(mod, "LORA") == 0) {
+        memcpy((void *)(databuf + buff_index), (void *)",\"prea\":8", 9);
+        buff_index += 9;
+    }
+
+    /* payload size */
+    i = snprintf((char *)(databuf + buff_index), 12, ",\"size\":%i", payload_size);
+    if ((i >= 0) && (i < 12)) {
+        buff_index += i;
+    } else {
+        MSG("ERROR: snprintf failed line %d\n", (__LINE__ - 4));
+        exit(EXIT_FAILURE);
+    }
+
+    /* payload JSON object */
+    memcpy((void *)(databuf + buff_index), (void *)",\"data\":\"", 9);
+    buff_index += 9;
+    payload_index = buff_index; /* keep the value where the payload content start */
+
+    /* payload place-holder & end of JSON structure */
+    x = bin_to_b64(payload_bin, payload_size, payload_b64, sizeof payload_b64); /* dummy conversion to get exact size */
+    if (x >= 0) {
+        buff_index += x;
+    } else {
+        MSG("ERROR: bin_to_b64 failed line %d\n", (__LINE__ - 4));
+        exit(EXIT_FAILURE);
+    }
+
+    /* Close JSON structure */
+    memcpy((void *)(databuf + buff_index), (void *)"\"}}", 3);
+    buff_index += 3; /* ends up being the total length of payload */
+
+    /* main loop */
+    for (i = 0; i < repeat; ++i) {
+        /* fill payload */
+        payload_bin[0] = id;
+        payload_bin[1] = (uint8_t)(i >> 24);
+        payload_bin[2] = (uint8_t)(i >> 16);
+        payload_bin[3] = (uint8_t)(i >> 8);
+        payload_bin[4] = (uint8_t)(i);
+        payload_bin[5] = 'P';
+        payload_bin[6] = 'E';
+        payload_bin[7] = 'R';
+        payload_bin[8] = (uint8_t)(payload_bin[0] + payload_bin[1] + payload_bin[2] + payload_bin[3] + payload_bin[4] + payload_bin[5] + payload_bin[6] + payload_bin[7]);
+        for (j = 0; j < (payload_size - 9); j++) {
+            payload_bin[9 + j] = j;
+        }
+
+#if 0
+        for (j = 0; j < payload_size; j++ ) {
+            printf("0x%02X ", payload_bin[j]);
+        }
+        printf("\n");
+#endif
+
+        /* encode the payload in Base64 */
+        x = bin_to_b64(payload_bin, payload_size, payload_b64, sizeof payload_b64);
+        if (x >= 0) {
+            memcpy((void *)(databuf + payload_index), (void *)payload_b64, x);
+        } else {
+            MSG("ERROR: bin_to_b64 failed line %d\n", (__LINE__ - 4));
+            exit(EXIT_FAILURE);
+        }
+
+        /* send packet to the gateway */
+        byte_nb = sendto(sock, (void *)databuf, buff_index, 0, (struct sockaddr *)&dist_addr, addr_len);
+        if (byte_nb == -1) {
+            MSG("WARNING: sendto returned an error %s\n", strerror(errno));
+        } else {
+            MSG("INFO: packet #%i sent successfully\n", i);
+        }
+
+        /* wait inter-packet delay */
+        usleep(delay * 1000);
+
+        /* exit loop on user signals */
+        if ((quit_sig == 1) || (exit_sig == 1)) {
+            break;
+        }
+    }
+
+
+    exit(EXIT_SUCCESS);
+}
+
+/* --- EOF ------------------------------------------------------------------ */