Skip to content
This repository has been archived by the owner on Aug 13, 2021. It is now read-only.

Commit

Permalink
Merge pull request #11 from BNNorman/master
Browse files Browse the repository at this point in the history
Added support for downlink messages
  • Loading branch information
pjb304 authored May 21, 2021
2 parents 55c3c26 + d6a5152 commit 3fccec8
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 17 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,38 @@ See: https://www.lora-alliance.org/portals/0/specs/LoRaWAN%20Specification%201R0
* Raspberry Pi
* SD card
* LoRa/GPS HAT
* Raspberry Pi power supply

## Installation
1. Install Raspbian on the Raspberry Pi
2. Enable SPI using raspi-config
3. Enable Serial using raspi-config (no login shell)
1. check your serial ports 'ls /dev/serial*'
2. check the serial port is receiving GPS data with 'cat /dev/serialx' where x is your port number.
4. Install the required packages `sudo apt install device-tree-compiler git python3-crypto python3-nmea2 python3-rpi.gpio python3-serial python3-spidev python3-configobj`
5. Download the git repo `git clone https://github.com/computenodes/LoRaWAN.git`
1. make a copy of dragingo.ini.default `cp dragino.ini.default dragino.ini`
2. make sure your dragino.ini is set to use the serial port from step 3
6. Enable additional CS lines (See section below for explanation)
1. Change into the overlay directory `cd dragino/overlay`
2. Compile the overlay `dtc -@ -I dts -O dtb -o spi-gpio-cs.dtbo spi-gpio-cs-overlay.dts`. This might generate a couple of warnings, but seems to work ok
3. Copy the output file to the required folder `sudo cp spi-gpio-cs.dtbo /boot/overlays/`
4. Enable the overlay at next reboot `echo "dtoverlay=spi-gpio-cs" | sudo tee -a /boot/config.txt`
5. Reboot the Pi `sudo reboot`
6. Check that the new cs lines are enabled `ls /dev/spidev0.*` should output `/dev/spidev0.0 /dev/spidev0.1 /dev/spidev0.2`. In which case the required SPI CS line now exists
7. Create a new device in The Things Network console and copy the details into the config file
7. Create a new device in The Things Network console and copy the details into the config file `dragino.ini`
8. Run the test programm `./test.py` and the device should transmit on the things network using OTAA authentication
9. run './test_downlink.py' to check downlink messages are received (after scheduling one in the TTN console)

## Additional Chip Select Details
For some reason the Dragino board does not use one of the standard chip select lines for the SPI communication. This can be overcome by using a device tree overlay to configure addtional SPI CS lines. I am not a device tree expert so I adapted the example given at https://www.raspberrypi.org/forums/viewtopic.php?f=63&t=157994 to provide the code needed for this to work.

## Downlink messages

See https://www.thethingsnetwork.org/docs/lorawan/classes/ for a complete description of LoRaWAN device classes.

Briefly, for class A device, downlink messages will only be sent after an uplink message. This is generally the type of device most people will be using as it consumes the least power on, for example, Arduino sensor devices. However, a Raspberry Pi + dragino HAT is constantly powered so when it isn't transmitting it can be always listening.

## TODO
* Make code more readable and easier to use (From upstream)
* Add GPS code into dragino class
* Use .ini files for config not .py
* investigate device tree compilation warnings
* Test recieving of messages
79 changes: 69 additions & 10 deletions dragino/dragino.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,28 @@ def __init__(
timeout=self.config.gps_serial_timeout)
self.gps_serial.flush()

# for downlink messages
self.downlinkCallback=None

def setDownlinkCallback(self,func=None):
"""
Configure the callback function which will receive
two parameters: decodedPayload and mtype.
decodedPayload will be a bytearray.
mtype will be MHDR.UNCONF_DATA_DOWN or MHDR.CONF_DATA_DOWN.
See test_downlink.py for usage.
func: function to call when a downlink message is received
"""
if hasattr(func,'__call__'):
self.logger.info("Setting downlinkCallback to %s",func)
self.downlinkCallback=func
else:
self.logger.info("downlinkCallback is not callable")


def _choose_freq(self, join=False):
if join:
available = JOIN_FREQS
Expand Down Expand Up @@ -119,15 +141,39 @@ def on_rx_done(self):
"""
Callback on RX complete, signalled by I/O
"""
self.logger.debug("Recieved message")
self.clear_irq_flags(RxDone=1)
payload = self.read_payload(nocheck=True)
lorawan = lorawan_msg([], self.appkey)
lorawan.read(payload)
lorawan.get_payload()
# print(lorawan.get_mhdr().get_mversion())
if lorawan.get_mhdr().get_mtype() == MHDR.JOIN_ACCEPT:
self.logger.debug("Join resp")
self.logger.debug("Recieved message")

try:

payload = self.read_payload(nocheck=True)

if payload is None:
self.logger.info("payload is None")
return

if not self.config.joined():
# not joined yet
self.logger.info("processing JOIN_ACCEPT payload")
lorawan = lorawan_msg([], self.appkey)
lorawan.read(payload)
decodedPayload=lorawan.get_payload()
else:
# joined
self.logger.info("processing payload after joined")
lorawan = lorawan_msg(self.network_key, self.apps_key)
lorawan.read(payload)
decodedPayload=lorawan.get_payload()

except Exception as e:
self.logger.exception("Exception %s",e)
return

mtype=lorawan.get_mhdr().get_mtype()
self.logger.debug("Processing message: MDHR version %s mtype %s payload %s ",lorawan.get_mhdr().get_mversion(), mtype,decodedPayload)

if mtype == MHDR.JOIN_ACCEPT:
self.logger.debug("Processing JOIN_ACCEPT")
#It's a response to a join request
lorawan.valid_mic()
self.device_addr = lorawan.get_devaddr()
Expand All @@ -138,9 +184,22 @@ def on_rx_done(self):
self.logger.info("APPS key: %s", self.apps_key)
self.frame_count = 1
self.config.save_credentials(
self.device_addr, self.network_key,
self.apps_key, self.frame_count)
self.device_addr, self.network_key,
self.apps_key, self.frame_count)
return

elif mtype==MHDR.UNCONF_DATA_DOWN or mtype==MHDR.CONF_DATA_DOWN:
self.logger.debug("Downlink data received")

if self.downlinkCallback is not None:
lorawan.valid_mic()
self.downlinkCallback(decodedPayload,mtype)
return
else:
self.logger.debug("Unexpected message type %s",mtype)



def on_tx_done(self):
"""
Callback on TX complete is signaled using I/O
Expand Down
11 changes: 8 additions & 3 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@

GPIO.setwarnings(False)

D = Dragino("dragino.ini", logging_level=logging.DEBUG)
# add logfile
logLevel=logging.DEBUG
logging.basicConfig(filename="test.log", format='%(asctime)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s', level=logLevel)


D = Dragino("dragino.ini", logging_level=logLevel)
D.join()
while not D.registered():
print("Waiting")
print("Waiting for JOIN ACCEPT")
sleep(2)
#sleep(10)
for i in range(0, 5):
D.send("Hello World")
print("Sent message")
print("Sent Hello World message")
sleep(1)
67 changes: 67 additions & 0 deletions test_downlink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""
downlink message testing
NOTE: you need to setup a queued downlink message in the TTN console before
running this test.
Downlink messages are only sent for class A devices after an uplink
message is received by TTN.
"""
import logging
from time import sleep,time
import RPi.GPIO as GPIO
from dragino import Dragino
from dragino.LoRaWAN.MHDR import *


GPIO.setwarnings(False)

logLevel=logging.DEBUG
logging.basicConfig(filename="test_downlink.log", format='%(asctime)s - %(funcName)s - %(lineno)d - %(levelname)s - %(message)s', level=logLevel)

logging.info("Starting session")

callbackReceived=False

def downlinkCallback(payload,mtype):
'''
Called by dragino.on_rx_done() when an UNCONF_DATA_DOWN or CONF_DATA_DOWN downlink message arrives.
Scheduling a CONF_DATA_DOW message requires an uplink response which
impacts on the fair use policy. Not recommended!
payload: bytearray
mtype: one of UNCONF_DATA_DOWN or CONF_DATA_DOWN
'''
global callbackReceived
callbackReceived = True
print("downlink message received")

if mtype==MHDR.UNCONF_DATA_DOWN:
print("Received UNCONF_DATA_DOWN payload:",payload)
else:
print("Received CONF_DATA_DOWN payload:",payload)


D = Dragino("dragino.ini", logging_level=logLevel)

D.setDownlinkCallback(downlinkCallback)

D.join()
while not D.registered():
print("Waiting for JOIN_ACCEPT")
sleep(2)


print("Sending a message to prompt for any scheduled downlinks.")
D.send("hello")

print("Waiting for callback message. Press CTRL-C to quit.")
try:
while not callbackReceived:
sleep(2)

except Exception as e:
print("Exception:",e)

print("test_downlink.py Finished")

0 comments on commit 3fccec8

Please sign in to comment.