Initial commit. Code from Bnorman
This commit is contained in:
commit
7b3854e6ca
11 changed files with 775 additions and 0 deletions
151
.gitignore
vendored
Normal file
151
.gitignore
vendored
Normal file
|
@ -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
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# lora_single_chan_gateway
|
||||||
|
|
||||||
|
Base on: https://github.com/tftelkamp/single_chan_pkt_fwd
|
57
decode_msg.py
Executable file
57
decode_msg.py
Executable file
|
@ -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))
|
BIN
gateway_packages.db
Normal file
BIN
gateway_packages.db
Normal file
Binary file not shown.
0
lora_single_chan_gateway/__init__.py
Normal file
0
lora_single_chan_gateway/__init__.py
Normal file
270
lora_single_chan_gateway/board_config.py
Normal file
270
lora_single_chan_gateway/board_config.py
Normal file
|
@ -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
|
113
lora_single_chan_gateway/gateway.py
Executable file
113
lora_single_chan_gateway/gateway.py
Executable file
|
@ -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)
|
36
lora_single_chan_gateway/package.py
Normal file
36
lora_single_chan_gateway/package.py
Normal file
|
@ -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:"
|
78
plot_db.py
Executable file
78
plot_db.py
Executable file
|
@ -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()
|
36
print_db.py
Executable file
36
print_db.py
Executable file
|
@ -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)
|
31
send_msg.py
Executable file
31
send_msg.py
Executable file
|
@ -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)
|
Reference in a new issue