computenodes / dragino

LoRaWAN implementation in python
GNU Affero General Public License v3.0
27 stars 11 forks source link

Downlink support #10

Closed theopieri closed 3 years ago

theopieri commented 3 years ago

Hi, Can this code support downlink? If yes can you provide me more details... I can uplink data with otaa.

BNNorman commented 3 years ago

As it stands no but the changes to make it work are relatively simple. I have this working for a project I worked on last year. Firstly you need to add a new function to register the callback function. Secondly, you need to make changes to the on_rx_done() function in dragino.py.

step 1 - add a variable to hold the callback function address in class Dragino __init__()

self.msgCallback=None

step 2 - add a new method to class Dragino

 def setMsgCallback(self,callback):
        self.msgCallback=callback

step 3 modify class Dragino on_rx_done() - this is my changed on_rx_done(). Note this change only looks for unconfirmed downlink messages i.e. TTN isn't expecting a reply.

def on_rx_done(self):
        """
            Callback on RX complete, signalled by I/O
        """
        self.clear_irq_flags(RxDone=1)
        self.logger.debug("on_rx_done() Received downlink message")

        payload = self.read_payload(nocheck=True)
        if payload is None:
            self.logger("on_rx_done() Received message but payload is None")
            return  

        self.logger.debug("on_rx_done() payload="+"".join(format(x, '02x') for x in bytes(payload)))

        try:

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

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

        self.logger.debug("on_rx_done() mversion=%s", lorawan.get_mhdr().get_mversion())

        mtype=lorawan.get_mhdr().get_mtype()

        if mtype == MHDR.JOIN_ACCEPT:
            self.logger.info("on_rx_done() Processing JOIN_ACCEPT")
            #It's a response to a join request
            lorawan.valid_mic()
            self.device_addr = lorawan.get_devaddr()
            self.logger.debug("on_rx_done() Device: %s", self.device_addr)
            self.network_key = lorawan.derive_nwskey(self.devnonce)
            self.logger.debug("on_rx_done() Network key: %s", self.network_key)
            self.apps_key = lorawan.derive_appskey(self.devnonce)
            self.logger.debug("on_rx_done() APPS key: %s", self.apps_key)
            return

        elif mtype == MHDR.UNCONF_DATA_DOWN:
            self.logger.info("on_rx_done() processing UNCONF_DATA_DOWN")
            try:
                # keys obtained at OTAA join?
                lorawan = lorawan_msg(self.network_key, self.apps_key)
                lorawan.read(payload)
                decodedPayload = lorawan.get_payload()

                # note downlink messages are hex bytes in the TTN console
                self.logger.debug("on_rx_done() UNCONF_DATA_DOWN decodedPayload %s",decodedPayload)
                if self.msgCallback is not None:
                    self.logger.info("on_rx_done() starting callback to downlink manager")
                    self.msgCallback(decodedPayload)
                else:
                    self.logger.error("on_rx_done() no callback configured")
                return
            except Exception as e:
                self.logger.exception("on_rx_done() UNCONF_DATA_DOWN %s",e)
                return

        elif mtype == MHDR.CONF_DATA_DOWN :
            self.logger.warning("on_rx_done() CONF_DATA_DOWN - not supported")

            #if self.msgCallback is not None:
            #    self.logger.info("on_rx_done() starting callback to downlink manager")
            #    self.msgCallback(payload)
        else:
            self.logger.info("on_rx_done() callback ignored mtype=%s",mtype)

This works for me on an older version (last year) of this code. I haven't, but need to, modified the latest version

Be aware that queued downlink messages might only sent be after TTN sees an uplink message - depends which LoraWAN class you use. In my use case the uplink message interval was about 6 minutes so that would be the rate that downlinks could be sent.

Note, also, that the get_gps() method is not blocking (albeit for a configurable wait period). But that's another story.

pjb304 commented 3 years ago

I'm afraid I don't have time to add that into the main code, but if either of you would like to do so and issue a pull request I'll happily review.

BNNorman commented 3 years ago

I'll be updating my modified copy of your code in the near future, with your latest version. I've never done a pull request before so not sure how to go about that other than to attach the changed file (dragino.py) for you to look at.

pjb304 commented 3 years ago

Thank you. There's plenty of documentation about how to do a pull request. The short version is you create your own fork of the code make the changes to it, and then use the pull request tab on this project to create a request to merge the changes from your version into this version.

BNNorman commented 3 years ago

Ok, I'll try doing it that way.

BNNorman commented 3 years ago

Just looking at adding something to readme.md - the todo list includes adding GPS support but that is already included. Does readme.md need to be updated?

pjb304 commented 3 years ago

Almost certainly!

BNNorman commented 3 years ago

I have made the required changes but have yet to test on my Dragino - busy busy , like you, with more critical stuff. But I'll get there.

pjb304 commented 3 years ago

Thanks for working on this :)

BNNorman commented 3 years ago

Having some problems with receiving downlinks (Other than JOIN_ACCEPT).

Each time an uplink is sent the dragino.py code chooses a different frequency (it stayed with one frequency before IIRC). If TTN does not send the downlink on the current frequency then the downlink message is lost. The question is "How long to wait for a possible downlink message before we can transmit again, on a randomly selected frequency". When I get my test code sorted out I'll try to put a number on that but I suspect there will be a lot of lost downlink messages and they are thwarting my testing.

One other issue I had was shutting down my dragino overnight - It would not talk to TTN the next day. This, I think, has something to do with credential caching and (maybe) aging thereof (but not sure). I removed the appskey/devaddr and nwkskey which had been appended to the dragino.ini file and finally got the dragino and TTN talking to each other again. Personally I would prefer the caching to go in a seperate file and maybe add a timestamp to check it's staleness. If stale then issue a rejoin before a send. Possibly that could be controlled by the code using dragino.py but would need a good explanation/example for people to follow.

Back to the grindstone...

pjb304 commented 3 years ago

The LoRaWAN specs include the details of how long a device should wait for messages after transmit (class A), and to listen all the time (class C), I've not looked into the channel assignment for downlink - I think it works differently to uplink.

I believe that once cached OTAA credentials ahould not expire and so can be re-used for as long as needed, although I don't have time to find the reference for that now. In my testing with v3 the OTAA has seemed more solid than ABP which would often just get dropped.

BNNorman commented 3 years ago

Yes of course. I forgot about rx1 and rx2 though rx2 is a different frequency to rx1 iirc.

BNNorman commented 3 years ago

It's working now. A bit of tidying up and I'll be ready to push/pull.

pjb304 commented 3 years ago

The original request for downlink support is has been implemented by #11 and is now in release v0.0.06 (https://github.com/computenodes/dragino/releases/tag/v0.0.6)

I've found blog posts from TTN from a few years ago saying that OTAA sessions will don't expire https://www.thethingsnetwork.org/forum/t/lorawan-session-termination-expiration/3556 The most recent documentation I've found https://www.thethingsindustries.com/docs/devices/abp-vs-otaa/ says that the OTAA credentials can be cached and then used at a later date.

The best practice would be to occasionally send uplinks with Acks and after X messages where you don't get an Ack back perform a rejoin, but that's a subject for another issue/discussion.

As the requested down link functionality is now in the code I'll close this one.