commit 7b3854e6ca2d65502a7c6e7dc0689ae84bfe049e
Author: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date:   Thu Nov 15 18:21:45 2018 +0000

    Initial commit. Code from Bnorman

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6d910af
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,151 @@
+
+# Created by https://www.gitignore.io/api/python,git
+# Edit at https://www.gitignore.io/?templates=python,git
+
+### Git ###
+# Created by git for backups. To disable backups in Git:
+# $ git config --global mergetool.keepBackup false
+*.orig
+
+# Created by git when using merge tools for conflicts
+*.BACKUP.*
+*.BASE.*
+*.LOCAL.*
+*.REMOTE.*
+*_BACKUP_*.txt
+*_BASE_*.txt
+*_LOCAL_*.txt
+*_REMOTE_*.txt
+
+### Python ###
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+### Python Patch ###
+.venv/
+
+### Python.VirtualEnv Stack ###
+# Virtualenv
+# http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
+[Bb]in
+[Ii]nclude
+[Ll]ib
+[Ll]ib64
+[Ll]ocal
+[Ss]cripts
+pyvenv.cfg
+pip-selfcheck.json
+
+# End of https://www.gitignore.io/api/python,git
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..b9c126e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# lora_single_chan_gateway
+
+Base on: https://github.com/tftelkamp/single_chan_pkt_fwd
diff --git a/decode_msg.py b/decode_msg.py
new file mode 100755
index 0000000..c935bb5
--- /dev/null
+++ b/decode_msg.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+
+import struct
+
+MSG_TYPES = {0x01: 'PING',
+             0x02: 'PONG',
+             0x10: 'Weather Report',
+             0x11: 'Weather Report Request'}
+
+PAYLOAD_TYPES = {0x20: 'Outside Temperature',
+                 0x21: 'Outside Humidity',
+                 0x22: 'Outside Pressure',
+                 0x50: 'Inside Temperature',
+                 0x51: 'Inside Humidity',
+                 0x52: 'Inside Pressure',
+                 0x80: 'Battery Voltage',
+                 0x81: 'Battery Current',
+                 0x90: 'Solar Cell Voltage',
+                 0x91: 'Solar Cell Current'}
+
+def decode_msg(msg):
+    binary = bytes.fromhex(msg)
+
+    if len(binary) < 5:
+        return {}
+
+    decoded = {
+        'to': binary[0],
+        'from': binary[1],
+        'id': binary[2],
+        'flags': binary[3],
+        'type': binary[4],
+        'values': []
+    }
+
+    for i in range(5, len(binary), 5):
+        decoded['values'].append({'id': binary[i],
+                                  'val': struct.unpack('f', binary[i+1:][:4])[0]})
+    return decoded
+
+
+def pretty_print_header(decoded):
+    type_msg = MSG_TYPES.get(decoded['type'], "")
+    return '0x{from:02x} -> 0x{to:02x} (ID={id}, FLAGS=0x{flags:02x}) TYPE(0x{type:02x})="{type_msg}" '.format(**decoded, type_msg=type_msg)
+
+
+def pretty_print_payload(decoded):
+    values = []
+    for val in decoded['values']:
+        values.append('  (0x{id:02x}) {type:20s} = {val:15.4f}'.format(type=PAYLOAD_TYPES.get(val['id'], ''), **val))
+    return '\n'.join(values)
+
+if __name__ == "__main__":
+    decoded = decode_msg("ff100f0010203333c74122806ed247804c3751409000000000")
+
+    print("####", pretty_print_header(decoded))
+    print(pretty_print_payload(decoded))
diff --git a/gateway_packages.db b/gateway_packages.db
new file mode 100644
index 0000000..5b3e8b4
Binary files /dev/null and b/gateway_packages.db differ
diff --git a/lora_single_chan_gateway/__init__.py b/lora_single_chan_gateway/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lora_single_chan_gateway/board_config.py b/lora_single_chan_gateway/board_config.py
new file mode 100644
index 0000000..d96bc7d
--- /dev/null
+++ b/lora_single_chan_gateway/board_config.py
@@ -0,0 +1,270 @@
+
+import datetime
+import logging
+import time
+
+import RPi.GPIO as GPIO
+import spidev
+
+
+class LoraBoardDraguino():
+    def __init__(self, frequency, sf):
+        assert 7 <= sf <= 12
+
+        self.frequency = frequency
+        self.sf = sf
+
+        # pin are given in BCM schema
+        self._pin_ss = 25   # GPIO 6
+        self._pin_dio0 = 4  # GPIO 7
+        self._pin_rst = 17  # GPIO 0
+
+        self._spi_bus = 0
+        self._spi_cs = 0    # TODO: Draguino has it's own non-standard CS pin
+
+        self.spi = None
+        self._is_sx1272 = None  # is set in setup
+
+    def __enter__(self):
+        self.setup_device()
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        self.teardown_device()
+
+    def _init_pins(self):
+        GPIO.setmode(GPIO.BCM)
+
+        GPIO.setup(self._pin_ss, GPIO.OUT)
+        GPIO.setup(self._pin_dio0, GPIO.IN)
+        GPIO.setup(self._pin_rst, GPIO.OUT)
+
+        # save default values
+        #GPIO.output(self._pin_ss, 1)
+        #GPIO.output(self._pin_rst, 0)
+
+    def _init_spi(self):
+        self.spi = spidev.SpiDev()
+        self.spi.open(self._spi_bus, self._spi_cs)
+        self.spi.max_speed_hz = 5000000  # TODO: what value works good?
+
+    def setup_device(self):
+        self._init_pins()
+        self._init_spi()
+
+        GPIO.output(self._pin_rst, 1)
+        time.sleep(0.10)
+        GPIO.output(self._pin_rst, 0)
+        time.sleep(0.10)
+
+        version = self.read_register(SX127x.REG_VERSION)
+
+        if version == 0x22:
+            logging.info("SX1272 detected, starting...")
+            self._is_sx1272 = True
+        else:
+            # sx1276?
+            GPIO.output(self._pin_rst, 0)
+            time.sleep(0.10)
+            GPIO.output(self._pin_rst, 1)
+            time.sleep(0.10)
+
+            version = self.read_register(SX127x.REG_VERSION)
+
+            if version == 0x12:
+                logging.info("SX1276 detected, starting...")
+                self._is_sx1272 = False
+            else:
+                logging.critical("Unrecognized transceiver")
+                raise RuntimeError("Unrecognized transceiver")
+
+        self.write_register(SX127x.REG_OPMODE, SX127x.SX72_MODE_LONG_RANGE | SX127x.SX72_MODE_SLEEP)
+
+        frf = int((self.frequency << 19) / 32000000)
+        self.write_register(SX127x.REG_FRF_MSB, (frf >> 16) & 0xFF)
+        self.write_register(SX127x.REG_FRF_MID, (frf >> 8) & 0xFF)
+        self.write_register(SX127x.REG_FRF_LSB, frf & 0xFF)
+
+        #self.write_register(SX127x.REG_SYNC_WORD, 0x34);  # LoRaWAN public sync word
+
+        if self._is_sx1272:
+            if self.sf == 11 or self.sf == 12:
+                self.write_register(SX127x.REG_MODEM_CONFIG, 0x0B)
+            else:
+                self.write_register(SX127x.REG_MODEM_CONFIG, 0x0A)
+            self.write_register(SX127x.REG_MODEM_CONFIG2, (self.sf << 4) | 0x04)
+        else:
+            if self.sf == 11 or self.sf == 12:
+                self.write_register(SX127x.REG_MODEM_CONFIG3, 0x0C)
+            else:
+                self.write_register(SX127x.REG_MODEM_CONFIG3, 0x04)
+            self.write_register(SX127x.REG_MODEM_CONFIG, 0x72)  # 125kHz 4/5
+            self.write_register(SX127x.REG_MODEM_CONFIG2, (self.sf << 4) | 0x04)  # enable CTC
+
+        if self.sf == 10 or self.sf == 11 or self.sf == 12:
+            self.write_register(SX127x.REG_SYMB_TIMEOUT_LSB, 0x05)
+        else:
+            self.write_register(SX127x.REG_SYMB_TIMEOUT_LSB, 0x08)
+
+        self.write_register(SX127x.REG_MAX_PAYLOAD_LENGTH, 0x80)
+        self.write_register(SX127x.REG_PAYLOAD_LENGTH, 0x80)
+        #self.write_register(SX127x.REG_HOP_PERIOD, 0xFF)
+        self.write_register(SX127x.REG_FIFO_ADDR_PTR, self.read_register(SX127x.REG_FIFO_RX_BASE_AD))
+
+        self.write_register(SX127x.REG_PA_CONFIG, 0) # set to a very low value
+
+        # Set Continuous Receive Mode
+        self.write_register(SX127x.REG_LNA, SX127x.LNA_MAX_GAIN)
+        self.write_register(SX127x.REG_OPMODE, SX127x.SX72_MODE_LONG_RANGE | SX127x.SX72_MODE_RX_CONTINUOS)
+
+    def teardown_device(self):
+        logging.info("tear down transceiver...")
+        GPIO.cleanup()
+        self.spi.close()
+
+    def read_register(self, register):
+        GPIO.output(self._pin_ss, 0)
+
+        value = self.spi.xfer([register & 0x7F, 0])[1]
+
+        GPIO.output(self._pin_ss, 1)
+
+        return value
+
+    def write_register(self, register, value):
+        GPIO.output(self._pin_ss, 0)
+
+        self.spi.xfer([register | 0x80, value])
+
+        GPIO.output(self._pin_ss, 1)
+
+    def write_bulk_register(self, register, values):
+        GPIO.output(self._pin_ss, 0)
+
+        self.spi.xfer([register | 0x80] + list(values))
+
+        GPIO.output(self._pin_ss, 1)
+
+    def set_mode_rx(self):
+        self.write_register(SX127x.REG_OPMODE, SX127x.SX72_MODE_LONG_RANGE | SX127x.SX72_MODE_RX_CONTINUOS)
+
+    def receive_package(self):
+        self.write_register(SX127x.REG_IRQ_FLAGS, 0x40)  # clear rxDone
+
+        irqflags = self.read_register(SX127x.REG_IRQ_FLAGS)
+        if irqflags & 0x20:
+            logging.warning("CRC error")
+            self.write_register(SX127x.REG_IRQ_FLAGS, 0x20)
+            crc = False
+            #return {'datetime': datetime.datetime.now(),
+            #         'crc': False}  # TODO: still get payload?
+        else:
+            crc = True
+
+        current_addr = self.read_register(SX127x.REG_FIFO_RX_CURRENT_ADDR)
+        received_count = self.read_register(SX127x.REG_RX_NB_BYTES)
+
+        self.write_register(SX127x.REG_FIFO_ADDR_PTR, current_addr)
+
+        payload = bytearray()
+        for _ in range(received_count):
+            payload.append(self.read_register(SX127x.REG_FIFO))
+
+        return {'datetime': datetime.datetime.utcnow().replace(tzinfo=datetime.timezone.utc),
+                'crc': crc,
+                'pkt_snr': self.pkt_snr,
+                'pkt_rssi': self.pkt_rssi,
+                'rssi': self.rssi,
+                'payload': bytes(payload)}
+
+    def send_package(self, msg):
+        assert type(msg) == bytes
+        assert len(msg) <= 255
+
+        # TODO: wait until send (done at end)
+
+        self.write_register(SX127x.REG_OPMODE, SX127x.SX72_MODE_STANDBY)
+
+        self.write_register(SX127x.REG_FIFO_ADDR_PTR, self.read_register(SX127x.REG_FIFO_TX_BASE_AD))
+
+        self.write_register(SX127x.REG_PAYLOAD_LENGTH, len(msg))
+
+        self.write_bulk_register(SX127x.REG_FIFO, msg)
+
+        self.write_register(SX127x.REG_OPMODE, SX127x.SX72_MODE_TX)
+
+        self.wait_until_send()
+
+    def wait_until_send(self):
+        while self.read_register(SX127x.REG_IRQ_FLAGS) & 0x08 == 0:
+            pass  # busy waiting
+
+        self.write_register(SX127x.REG_IRQ_FLAGS, 0x08)  # Clear TX DONE
+
+    @property
+    def pkt_snr(self):
+        snr_value = self.read_register(SX127x.REG_PKT_SNR_VALUE)
+        if snr_value & 0x80:
+            snr_value = ((~snr_value + 1) & 0xFF) >> 2
+            return -snr_value
+        else:
+            return (snr_value & 0xFF) >> 2
+
+    @property
+    def pkt_rssi(self):
+        if self._is_sx1272:
+            rssicorr = 139
+        else:
+            rssicorr = 157
+        return self.read_register(0x1A) - rssicorr
+
+    @property
+    def rssi(self):
+        if self._is_sx1272:
+            rssicorr = 139
+        else:
+            rssicorr = 157
+        return self.read_register(0x1B) - rssicorr
+
+
+class SX127x:
+    REG_FIFO                    = 0x00
+    REG_FRF_MSB                 = 0x06
+    REG_FRF_MID                 = 0x07
+    REG_FRF_LSB                 = 0x08
+    REG_PA_CONFIG               = 0x09
+    REG_LNA                     = 0x0C
+    REG_FIFO_ADDR_PTR           = 0x0D
+    REG_FIFO_TX_BASE_AD         = 0x0E
+    REG_FIFO_RX_BASE_AD         = 0x0F
+    REG_RX_NB_BYTES             = 0x13
+    REG_OPMODE                  = 0x01
+    REG_FIFO_RX_CURRENT_ADDR    = 0x10
+    REG_IRQ_FLAGS               = 0x12
+    REG_DIO_MAPPING_1           = 0x40
+    REG_DIO_MAPPING_2           = 0x41
+    REG_MODEM_CONFIG            = 0x1D
+    REG_MODEM_CONFIG2           = 0x1E
+    REG_MODEM_CONFIG3           = 0x26
+    REG_SYMB_TIMEOUT_LSB        = 0x1F
+    REG_PKT_SNR_VALUE           = 0x19
+    REG_PAYLOAD_LENGTH          = 0x22
+    REG_IRQ_FLAGS_MASK          = 0x11
+    REG_MAX_PAYLOAD_LENGTH      = 0x23
+    REG_HOP_PERIOD              = 0x24
+    REG_SYNC_WORD               = 0x39
+    REG_VERSION                 = 0x42
+
+    SX72_MODE_LONG_RANGE        = 0x80
+
+    SX72_MODE_SLEEP             = 0x00
+    SX72_MODE_STANDBY           = 0x01
+    SX72_MODE_FSTX              = 0x02
+    SX72_MODE_TX                = 0x03
+    SX72_MODE_FSRX              = 0x04
+    SX72_MODE_RX_CONTINUOS      = 0x05
+    SX72_MODE_RX_SINGLE         = 0x06
+
+    LNA_MAX_GAIN                = 0x23
+    LNA_OFF_GAIN                = 0x00
+    LNA_LOW_GAIN                = 0x20
\ No newline at end of file
diff --git a/lora_single_chan_gateway/gateway.py b/lora_single_chan_gateway/gateway.py
new file mode 100755
index 0000000..24d304d
--- /dev/null
+++ b/lora_single_chan_gateway/gateway.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+
+import base64
+import json
+import logging
+from random import randint
+import socket
+import sqlite3
+
+from board_config import LoraBoardDraguino
+import RPi.GPIO as GPIO
+
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+GATEWAY_HOST = "router.eu.thethings.network"
+GATEWAY_PORT = 1700
+
+
+class LoRaPacketsDB(object):
+    def __init__(self):
+        self.conn = sqlite3.connect('gateway_packages.db')  # we only need the db at runtime
+        self.c = self.conn.cursor()
+
+        self._init_db()
+
+    def _init_db(self):
+        create_table = """CREATE TABLE IF NOT EXISTS `PACKETS` (
+            `ID` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
+            `DATETIME` TEXT NOT NULL,
+            `DATA` BLOB,
+            `FREQUENCY` NUMERIC,
+            `IS_CRC_OK` INTEGER,
+            `PKT_SNR` NUMERIC,
+            `PKT_RSSI` NUMERIC,
+            `RSSI` NUMERIC
+        )"""
+        self.conn.execute(create_table)
+
+    def log_pkg(self, board, packet):
+        query = """INSERT INTO `PACKETS` (
+            `DATETIME`,
+            `DATA`,
+            `FREQUENCY`,
+            `IS_CRC_OK`,
+            `PKT_SNR`,
+            `PKT_RSSI`,
+            `RSSI`
+        ) VALUES (?, ?, ?, ?, ?, ?, ?)
+        """
+        try:
+            self.c.execute(query, (payload['datetime'].isoformat(),
+                                   payload.get('payload'),
+                                   board.frequency,
+                                   payload.get('crc'),
+                                   payload.get('pkt_snr'),
+                                   payload.get('pkt_rssi'),
+                                   payload.get('rssi')))
+            self.conn.commit()
+        except sqlite3.IntegrityError as e:
+            logging.exception("SQL ERROR: ", e)
+
+
+def construct_semtec_udp(board, payload):
+    # https://github.com/Lora-net/packet_forwarder/blob/d0226eae6e7b6bbaec6117d0d2372bf17819c438/PROTOCOL.TXT#L99
+    frame = bytearray()
+    frame.append(2)  # Protocol version = 2
+    frame.extend([randint(0, 255), randint(0, 255)])  # Random numbers
+    frame.extend([0x80, 0xFA, 0x5C, 0xFF, 0xFF, 0x69, 0x33, 0xBB])  # TODO: construct from hardware
+
+    data = {
+        "rxpk":
+            {
+                "time": payload['datetime'].isoformat(),
+                "freq": board.frequency / 1000 / 1000,
+                "stat": 1 if payload['crc'] is True else -1,
+                "modu": "LORA",
+                "datr": "SF7BW125",  # TODO: configurable
+                "codr": "4/5",
+                "rssi": payload['pkt_rssi'],
+                "lsnr": payload['pkt_snr'],
+                "size": len(payload['payload']),
+                "data": base64.standard_b64encode(payload['payload']).decode("utf-8")
+            }
+    }
+
+    frame.extend(map(ord, json.dumps(data)))
+
+    return bytes(frame)
+
+
+if __name__ == "__main__":
+    # sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # UDP
+
+    db = LoRaPacketsDB()
+
+    with LoraBoardDraguino(433300000, 7) as board:
+        board.set_mode_rx()
+        logging.info("Listening at SF{} on {} MHz".format(board.sf, board.frequency/1000000))
+
+        while True:
+            if GPIO.input(board._pin_dio0) == 1:
+                payload = board.receive_package()
+
+                # semtec_udp = construct_semtec_udp(board, payload)
+                # sock.sendto(semtec_udp, (GATEWAY_HOST, GATEWAY_PORT))
+                # logging.info(semtec_udp)
+
+                # logging.info("Received: {}".format(payload))
+                logging.info("Received at {}: {} = {}".format(payload['datetime'].isoformat(), payload['payload'].hex(), payload['payload'][4:]))
+
+                db.log_pkg(board, payload)
diff --git a/lora_single_chan_gateway/package.py b/lora_single_chan_gateway/package.py
new file mode 100644
index 0000000..cb35b41
--- /dev/null
+++ b/lora_single_chan_gateway/package.py
@@ -0,0 +1,36 @@
+
+import datetime
+
+
+class ReceivedPackage(object):
+    def __init__(self, payload=None, frequency=None):
+        self.datetime = datetime.datetime.now()
+
+        self.modulation = None
+
+        self.frequency = frequency  # given in MHz
+
+        self.snr = None
+        self.rssi = None
+
+        self.payload = payload
+
+    def __str__(self):
+        ret_str = ""
+
+        ret_str += self.modulation + " "
+
+
+class LoraReceivedPackage(ReceivedPackage):
+    def __init__(self, payload=None, frequency=None, datarate = None, codingrate = None, crc = None):
+        super(LoraReceivedPackage).__init__(self, payload=payload, frequency=frequency)
+
+        self.modulation = "LORA"
+
+        self.datarate = datarate
+        self.codingrate = codingrate
+
+        self.crc = crc
+
+    def __str__(self):
+        return "LORA:"
\ No newline at end of file
diff --git a/plot_db.py b/plot_db.py
new file mode 100755
index 0000000..f5188f1
--- /dev/null
+++ b/plot_db.py
@@ -0,0 +1,78 @@
+#!/usr/bin/env python3
+
+import dateutil.parser
+import sqlite3
+
+import matplotlib.dates
+import matplotlib.pyplot
+import matplotlib.pyplot as plt
+
+from decode_msg import decode_msg
+
+
+if __name__ == "__main__":
+    conn = sqlite3.connect('gateway_packages.db')
+    c = conn.cursor()
+
+    c.execute('SELECT * FROM `PACKETS`')
+
+    dates = []
+    values_temp = []
+    values_press = []
+    values_bat_val = []
+    values_cell_val = []
+
+    for line in c.fetchall():
+        try:
+            line = list(line)
+
+            msg = {'id': line[0],
+                   'datetime': line[1],
+                   'data': bytes(line[2]).hex(),
+                   'frequency': line[3],
+                   'crc': line[4],
+                   'pkt_snr': line[5],
+                   'pkt_rssi': line[6],
+                   'rssi': line[7]}
+
+            if not msg['crc']:
+                continue
+
+            decoded = decode_msg(msg['data'])
+
+            # weather report
+            if decoded['type'] == 0x10:
+                datetime = dateutil.parser.parse(msg['datetime'])
+                temperatur = None
+                pressure = None
+                battery_voltage = None
+                solar_voltage = None
+                for val in decoded['values']:
+                    if val['id'] == 0x20:
+                        temperatur = val['val']
+                    elif val['id'] == 0x22:
+                        pressure = val['val']
+                    elif val['id'] == 0x80:
+                        battery_voltage = val['val']
+                    elif val['id'] == 0x90:
+                        solar_voltage = val['val']
+
+                dates.append(datetime)
+                values_temp.append(temperatur)
+                values_press.append(pressure)
+                values_bat_val.append(battery_voltage)
+                values_cell_val.append(solar_voltage)
+        except:
+            print("ERROR in: ", line)
+    print('plot!')
+
+    fig, ax1 = plt.subplots()
+
+    ax1.set_xlabel('time')
+    ax1.set_ylabel('voltage')
+
+    ax1.plot_date(dates, values_bat_val, linestyle='solid', marker='None', label='Battery Voltage')
+    ax1.plot_date(dates, values_cell_val, linestyle='solid', marker='None', label='Solar Cell Voltage')
+    ax1.plot_date(dates, values_press, linestyle='solid', marker='None', label='Temperatur')
+    plt.legend()
+    matplotlib.pyplot.show()
diff --git a/print_db.py b/print_db.py
new file mode 100755
index 0000000..3a07411
--- /dev/null
+++ b/print_db.py
@@ -0,0 +1,36 @@
+#!/usr/bin/env python3
+
+from decode_msg import decode_msg, pretty_print_header, pretty_print_payload, MSG_TYPES
+
+import sqlite3
+
+if __name__ == "__main__":
+    conn = sqlite3.connect('gateway_packages.db')
+    c = conn.cursor()
+
+    c.execute('SELECT * FROM `PACKETS`')
+
+    for line in c.fetchall():
+        try:
+            line = list(line)
+
+            msg = {'id': line[0],
+                   'datetime': line[1],
+                   'data': bytes(line[2]).hex(),
+                   'frequency': line[3],
+                   'crc': line[4],
+                   'pkt_snr': line[5],
+                   'pkt_rssi': line[6],
+                   'rssi': line[7]}
+
+            decoded = decode_msg(msg['data'])
+
+            color = '\033[92m' if msg['crc'] else '\033[91m'
+
+            print(color, '### {id:04d} ({datetime}): {header}\033[0m (PKT_SNR={pkt_snr}, PKT_RSSI={pkt_rssi}, RSSI={rssi}) '.format(**msg, header=pretty_print_header(decoded)))
+
+            # weather report
+            if decoded['type'] == 0x10:
+                print(pretty_print_payload(decoded))
+        except:
+            print("ERROR in: ", line)
diff --git a/send_msg.py b/send_msg.py
new file mode 100755
index 0000000..18421ce
--- /dev/null
+++ b/send_msg.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python3
+
+import argparse
+import logging
+
+from lora_single_chan_gateway.board_config import LoraBoardDraguino
+
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+
+    parser.add_argument('message', type=str, help='message to send')
+
+    # http://www.airspayce.com/mikem/arduino/RadioHead/classRH__RF95.html "packet format"
+    parser.add_argument('--from', type=int, default=0xFF)
+    parser.add_argument('--to', type=int, default=0xFF)
+    parser.add_argument('--id', type=int, default=0x00)
+    parser.add_argument('--flags', type=int, default=0x00)
+
+    args = parser.parse_args()
+
+    GATEWAY_HEADER = bytes([args.to, args.__dict__['from'], args.id, args.flags])
+
+    with LoraBoardDraguino(433300000, 7) as board:
+        msg = GATEWAY_HEADER + args.message.encode('utf-8')
+        print(msg)
+
+        board.send_package(msg)
\ No newline at end of file
