Esempio n. 1
0
    def _processLinkCheckReq(self, device, command, request, lsnr):
        """Process a link check request
        
        Args:
            device (Device): Sending device
            command (LinkCheckReq): LinkCheckReq object
        """
        # We assume 'margin' corresponds to the
        # absolute value of LNSR, as an integer.
        # Set to zero if negative.
        margin = max(0, round(lsnr))
        # If we are processing the first request,
        # gateway count must be one, we guess.
        gwcnt = 1

        # Create the LinkCheckAns response and encode. Set fcntdown
        command = LinkCheckAns(margin=margin, gwcnt=gwcnt)

        # Queue the command if required
        if self.config.macqueueing:
            self._queueMACCommand(device.deveui, command)
            return

        frmpayload = command.encode()
        fcntdown = device.fcntdown + 1

        # Create the downlink message. Set fport=0,
        # encrypt with NwkSKey and encode
        message = MACDataDownlinkMessage(device.devaddr,
                                         device.nwkskey,
                                         fcntdown,
                                         self.config.adrenable,
                                         '',
                                         0,
                                         frmpayload,
                                         acknowledge=True)
        message.encrypt(device.nwkskey)
        data = message.encode()

        gateway = self.lora.gateway(device.gw_addr)
        if gateway is None:
            log.info(
                "Could not find gateway for gateway {gw_addr} for device "
                "{devaddr}",
                gw_addr=device.gw_addr,
                devaddr=devaddrString(device.devaddr))
            return

        # Create GatewayMessage and Txpk objects, send immediately
        request = GatewayMessage(version=1,
                                 token=0,
                                 remote=(gateway.host, gateway.port))
        device.rx = self.band.rxparams((device.tx_chan, device.tx_datr))
        txpk = self._txpkResponse(device, data, gateway, immediate=True)

        # Update the device fcntdown
        device.update(fcntdown=fcntdown)

        # Send the RX2 window message
        self.lora.sendPullResponse(request, txpk[2])
Esempio n. 2
0
    def start(self, netserver):
        """Load all application interfaces and start them.
        
        Args:
            netserver (NetServer): The network server
        """

        self.netserver = netserver

        # Get all concrete application interface objects
        appinterfaces = yield AppInterface.all()
        for appinterface in appinterfaces:
            # Get the interface, set the appinterface
            interface = yield appinterface.interfaces.get()
            if interface:
                interface.appinterface = appinterface
                self.interfaces.append(interface)

        # Start the interfaces
        for interface in self.interfaces:
            log.info("Starting application interface id {id}: {name}",
                     id=interface.appinterface.id,
                     name=interface.name)
            interface.start(self.netserver)
            if not interface.started:
                log.error("Could not start application interface "
                          "id {id}",
                          id=interface.appinterface.id)
Esempio n. 3
0
    def _sendJoinResponse(self, request, rxpk, gateway, app, device):
        """Send a join response message
        
        Called if a join response message is to be sent.
        
        Args:
            request: request (GatewayMessage): Received gateway message object
            app (Application): The requested application object
            device (Device): The requesting device object
        """
        # Get receive window parameters and
        # set dlsettings field
        device.rx = self.band.rxparams((device.tx_chan, device.tx_datr),
                                       join=True)
        dlsettings = 0 | self.band.rx1droffset << 4 | device.rx[2]['index']

        # Create the Join Response message
        log.info("Sending join response for devaddr {devaddr}",
                 devaddr=devaddrString(device.devaddr))
        response = JoinAcceptMessage(app.appkey, app.appnonce,
                                     self.config.netid, device.devaddr,
                                     dlsettings, device.rx[1]['delay'])
        data = response.encode()
        txpk = self._txpkResponse(device, data, gateway, rxpk.tmst)

        # Send the RX1 window messages
        self.lora.sendPullResponse(request, txpk[1])
        # Send the RX2 window message
        self.lora.sendPullResponse(request, txpk[2])
Esempio n. 4
0
    def _processADRRequests(self):
        """Updates devices with target data rate, and sends ADR requests.
        
        This method is called every adrcycletime seconds as a looping task.
        
        """
        # If we are running, return
        if self.adrprocessing is True:
            returnValue(None)

        self.adrprocessing = True

        devices = yield Device.all()
        sendtime = time.time()

        for device in devices:
            # Check this device is enabled
            if not device.enabled:
                continue

            # Check ADR is enabled
            if not device.adr:
                continue

            # Set the target data rate
            target = device.getADRDatarate(self.band, self.config.adrmargin)

            # If no target, or the current data rate is the target, continue
            if target is None:
                continue

            # Set the device adr_datr
            yield device.update(adr_datr=target)

            # Only send a request if we need to change
            if device.tx_datr == device.adr_datr:
                continue

            # If we are queueing commands, create the command and add to the queue.
            # Replace any existing requests.
            if self.config.macqueueing:
                log.info("Queuing ADR MAC Command")
                command = self._createLinkADRRequest(device)
                self._dequeueMACCommand(device.deveui, command)
                self._queueMACCommand(device.deveui, command)
                continue

            # Check we have reached the next scheduled ADR message time
            scheduled = sendtime + self.config.adrmessagetime
            current = time.time()
            if current < scheduled:
                yield txsleep(scheduled - current)

            # Refresh and send the LinkADRRequest
            sendtime = time.time()
            yield self._sendLinkADRRequest(device)

        self.adrprocessing = False
Esempio n. 5
0
    def _pollInboundMessages(self):
        """Poll Azure IOT hub for inbound messages and forward
        them to the Network Server"""
        
        # If we are running, return
        if self.polling is True:
            returnValue(None)
        
        log.info("Azure IoT HTTPS interface '{name}' commencing "
                 "polling loop", name=self.name)
        self.polling = True

        # Get the applications associated with this interface. 
        apps = yield Application.find(where=['appinterface_id = ?', self.appinterface.id]) 
        if apps is None:
            self.polling = False
            returnValue(None)
            
        # Loop through the applications 
        for app in apps:
            
            # Poll all devices associated with this app
            devices = yield Device.find(where=['appeui = ?', app.appeui])   
            if devices is None:
                returnValue(None)
                
            for device in devices:
                # Use the device appname property for the Azure devid,
                # if it exists. Otherwise, use the device name property
                devid = device.appname if device.appname else device.name
                
                # Form the url, headers and parameters
                url = 'https://{}/devices/{}/messages/devicebound'.format(
                    self.iothost, devid)
                resuri = '{}/devices/{}'.format(self.iothost, devid)
                headers = {'Authorization': self._iotHubSasToken(resuri)}
                params = {'api-version': self.API_VERSION}
                
                # Make the request, catch any exceptions
                try:
                    r = requests.get(url, headers=headers,
                              params=params, timeout=self.TIMEOUT)
                except requests.exceptions.RequestException:
                    log.debug("Application interface {name} could not poll "
                          "Azure IOT Hub {host} for device ID {device}",
                          name=self.name, host=self.iothost, device=devid)
                    continue
                
                # Response code 204 indicates there is no data to be sent.
                if r.status_code == 204:
                    continue
                # Response code 200 means we have data to send to the device
                elif r.status_code == 200:
                    appdata = r.content
                    self.netserver.inboundAppMessage(device.devaddr, appdata)

        self.polling = False
Esempio n. 6
0
 def _processLinkADRAns(self, device, command):
     """Process a link ADR answer
     
     Returns three ACKS: power_ack, datarate_ack, channelmask_ack
     
     Args:
         device (Device): Sending device
         command (LinkADRAns): LinkADRAns object
     """
     # Not much to do here - we will know if the device had changed datarate via
     # the rxpk field.
     log.info("Received LinkADRAns from device {devaddr}",
              devaddr=devaddrString(device.devaddr))
Esempio n. 7
0
    def __init__(self, config):
        """NetServer initialisation method.
        
        Args:
            database (Database): Database configuration object
        
        """
        log.info("Initialising the server")
        self.message_cache = []
        self.task = {}
        self.commands = []
        self.adrprocessing = False

        self.config = config
        self.otarange = set(
            xrange(self.config.otaastart, self.config.otaaend + 1))
        self.band = eval(self.config.freqband)()
Esempio n. 8
0
    def _sendLinkADRRequest(self, device, command):
        """Send a Link ADR Request message
        
        Called if an ADR change is required for this device.
        
        Args:
            device: (Device): Target device
            command (LinkADRReq): Link ADR Request object
        """
        frmpayload = command.encode()

        # Create the downlink message. Increment fcntdown, set fport=0,
        # encrypt with NwkSKey and encode
        fcntdown = device.fcntdown + 1
        log.info("Sending ADR Request to devaddr {devaddr}",
                 devaddr=devaddrString(device.devaddr))
        message = MACDataDownlinkMessage(device.devaddr, device.nwkskey,
                                         fcntdown, self.config.adrenable, '',
                                         0, frmpayload)
        message.encrypt(device.nwkskey)
        data = message.encode()

        gateway = self.lora.gateway(device.gw_addr)
        if gateway is None:
            log.info(
                "Could not find gateway for gateway {gw_addr} for device "
                "{devaddr}",
                gw_addr=device.gw_addr,
                devaddr=devaddrString(device.devaddr))
            returnValue(None)

        # Create GatewayMessage and Txpk objects, send immediately
        request = GatewayMessage(version=1,
                                 token=0,
                                 remote=(gateway.host, gateway.port))
        device.rx = self.band.rxparams((device.tx_chan, device.tx_datr))
        txpk = self._txpkResponse(device, data, gateway, immediate=True)

        # Update the device fcntdown
        device.update(fcntdown=fcntdown)

        # Send the RX2 window message
        self.lora.sendPullResponse(request, txpk[2])
Esempio n. 9
0
    def _processJoinRequest(self, message, app, device):
        """Process an OTA Join Request message from a LoraWAN device
        
        This method checks the message devnonce and integrity code (MIC).
        If the devnonce has not been seen before, and the MIC is valid,
        we have a valid join request, and we can create the session
        keys and assign an OTA device address.
        
        Args:
            message (JoinRequestMessage): The join request message object
            app (Application): The requested application object
            device (Device): The requesting device object
            
        Returns:
            True on success, False otherwise.
        """
        # Perform devnonce check
        if not device.checkDevNonce(message):
            log.info(
                "Join request message from {deveui} failed message "
                "devnonce check.",
                deveui=euiString(message.deveui))
            returnValue(False)

        # Perform message integrity check.
        if not message.checkMIC(app.appkey):
            log.info(
                "Message from {deveui} failed message "
                "integrity check.",
                deveui=euiString(message.deveui))
            returnValue(False)

        # Assign DevEUI, NwkSkey and AppSKey.
        device.appeui = app.appeui
        device.nwkskey = self._createSessionKey(1, app, message)
        device.appskey = self._createSessionKey(2, app, message)

        # If required, obtain a OTA devaddr for the device
        if device.devaddr is None:
            device.devaddr = yield self._getFreeOTAAddress()

        returnValue(device.devaddr is not None)
Esempio n. 10
0
    def start(self):
        """Start the netserver.
        
        Sets up scheduled tasks and start listening on the required
        interfaces.
        """
        log.info("Starting the server")

        # Setup scheduled tasks
        # 1. ADR Requests
        self.task['processADRRequests'] = task.LoopingCall(
            self._processADRRequests)
        if self.config.adrenable:
            self.task['processADRRequests'].start(self.config.adrcycletime)

        # 2. Message cache
        self.task['cleanMessageCache'] = task.LoopingCall(
            self._cleanMessageCache)
        self.task['cleanMessageCache'].start(
            max(10, self.config.duplicateperiod * 2))

        # 3. MAC Command queue
        self.task['manageMACCommandQueue'] = task.LoopingCall(
            self._manageMACCommandQueue)
        if self.config.macqueueing:
            self.task['manageMACCommandQueue'].start(
                self.config.macqueuelimit / 2)

        # Start the web server
        log.info("Starting the web server")
        self.webserver = WebServer(self)
        try:
            self.webserver.start()
        except CannotListenError:
            log.error("Error starting the web server: cannot listen.")
            reactor.stop()

        # Start server network interfaces:
        # LoRa gateway interface
        log.info("Starting the LoRaWAN interface")
        self.lora = LoraWAN(self)
        try:
            self.lora.start()
        except CannotListenError:
            log.error("Error opening LoRa interface UDP port {port}",
                      port=self.config.port)
            reactor.stop()

        # Application interfaces
        interfaceManager.start(self)
Esempio n. 11
0
    def inboundAppMessage(self, devaddr, appdata, acknowledge=False):
        """Sends inbound data from the application interface to the device
        
        Args:
            devaddr (int): 32 bit device address (DevAddr)
            appdata (str): packed application data
            acknowledge (bool): Acknowledged message
        """

        log.info("Inbound message to devaddr {devaddr}",
                 devaddr=devaddrString(devaddr))

        # Retrieve the active device
        device = yield self._getActiveDevice(devaddr)
        if device is None:
            log.error("Cannot send to unregistered device address {devaddr}",
                      devaddr=devaddrString(devaddr))
            returnValue(None)

        # Check the device is enabled
        if not device.enabled:
            log.error(
                "Inbound application message for disabled device "
                "{deveui}",
                deveui=euiString(device.deveui))
            returnValue(None)

        # Get the associated application
        app = yield Application.find(where=['appeui = ?', device.appeui],
                                     limit=1)
        if app is None:
            log.error(
                "Inbound application message for {deveui} - "
                "AppEUI {appeui} does not match any configured applications.",
                deveui=euiString(device.deveui),
                appeui=device.appeui)
            returnValue(None)

        # Find the gateway
        gateway = self.lora.gateway(device.gw_addr)
        if gateway is None:
            log.error(
                "Could not find gateway for inbound message to "
                "{devaddr}.",
                devaddr=devaddrString(device.devaddr))
            returnValue(None)

        # Increment fcntdown
        fcntdown = device.fcntdown + 1

        # Piggyback any queued MAC messages in fopts
        fopts = ''
        device.rx = self.band.rxparams((device.tx_chan, device.tx_datr),
                                       join=False)
        if self.config.macqueueing:
            # Get all of this device's queued commands: this returns a list of tuples (index, command)
            commands = [(i, c[2]) for i, c in enumerate(self.commands)
                        if device.deveui == c[1]]
            for (index, command) in commands:
                # Check if we can accommodate the command. If so, encode and remove from the queue
                if self.band.checkAppPayloadLen(device.rx[1]['datr'],
                                                len(fopts) + len(appdata)):
                    fopts += command.encode()
                    del self.commands[index]
                else:
                    break

        # Create the downlink message, encrypt with AppSKey and encode
        response = MACDataDownlinkMessage(device.devaddr,
                                          device.nwkskey,
                                          device.fcntdown,
                                          self.config.adrenable,
                                          fopts,
                                          int(app.fport),
                                          appdata,
                                          acknowledge=acknowledge)
        response.encrypt(device.appskey)
        data = response.encode()

        # Create Txpk objects
        txpk = self._txpkResponse(device,
                                  data,
                                  gateway,
                                  itmst=int(device.tmst),
                                  immediate=False)
        request = GatewayMessage(version=2,
                                 gatewayEUI=gateway.eui,
                                 remote=(gateway.host, gateway.port))

        # Save the frame count down
        device.update(fcntdown=fcntdown)

        # Send RX1 window message
        self.lora.sendPullResponse(request, txpk[1])
        # If Class A, send the RX2 window message
        self.lora.sendPullResponse(request, txpk[2])
Esempio n. 12
0
    def processPushDataMessage(self, request, gateway):
        """Process a PUSH_DATA message from a LoraWAN gateway
        
        Args:
            request (GatewayMessage): the received gateway message object
            gateway (Gateway): the gateway that sent the message
        
        Returns:
            True on success, otherwise False
        """
        if not request.rxpk:
            request.rxpk = []
        for rxpk in request.rxpk:
            # Decode the MAC message
            message = MACMessage.decode(rxpk.data)
            if message is None:
                log.info(
                    "MAC message decode error for gateway {gateway}: message "
                    "timestamp {timestamp}",
                    gateway=gateway.host,
                    timestamp=str(rxpk.time))
                returnValue(False)

            # Check if thisis a duplicate message
            if self._checkDuplicateMessage(message):
                returnValue(False)

            # Join Request
            if message.isJoinRequest():
                # Get the application using appeui
                app = yield Application.find(
                    where=['appeui = ?', message.appeui], limit=1)
                #app = next((a for a in self.applications if
                #            a.appeui == message.appeui), None)
                if app is None:
                    log.info(
                        "Message from {deveui} - AppEUI {appeui} "
                        "does not match any configured applications.",
                        deveui=euiString(message.deveui),
                        appeui=message.appeui)
                    returnValue(False)

                # Find the Device
                device = yield Device.find(
                    where=['deveui = ?', message.deveui], limit=1)
                if device is None:
                    #log.info("Message from unregistered device {deveui}",
                    #     deveui=euiString(message.deveui))
                    #returnValue(False)
                    # TODO save device to database (cheng)
                    device = Device(deveui=message.deveui,
                                    name='smk_node',
                                    devclass='A',
                                    enabled=True,
                                    otaa=True,
                                    devaddr=None,
                                    devnonce=[],
                                    appeui=message.appeui,
                                    nwkskey='',
                                    appskey='',
                                    fcntup=0,
                                    fcntdown=0,
                                    fcnterror=False,
                                    snr=[],
                                    snr_average=0)
                    #device.save()
                else:
                    # Check the device is enabled
                    if not device.enabled:
                        log.info("Join request for disabled device {deveui}.",
                                 deveui=euiString(device.deveui))
                        returnValue(False)

                # Process join request
                joined = yield self._processJoinRequest(message, app, device)
                if joined:
                    # Update the ADR measures
                    if self.config.adrenable:
                        device.updateSNR(rxpk.lsnr)

                    yield device.update(tx_chan=rxpk.chan,
                                        tx_datr=rxpk.datr,
                                        devaddr=device.devaddr,
                                        nwkskey=device.nwkskey,
                                        appskey=device.appskey,
                                        time=rxpk.time,
                                        tmst=rxpk.tmst,
                                        gw_addr=gateway.host,
                                        fcntup=0,
                                        fcntdown=0,
                                        fcnterror=False,
                                        devnonce=device.devnonce,
                                        snr=device.snr,
                                        snr_average=device.snr_average)

                    log.info(
                        "Successful Join request from DevEUI {deveui} "
                        "for AppEUI {appeui} | Assigned address {devaddr}",
                        deveui=euiString(device.deveui),
                        appeui=euiString(app.appeui),
                        devaddr=devaddrString(device.devaddr))

                    # Send the join response
                    device.save()
                    self._sendJoinResponse(request, rxpk, gateway, app, device)
                    returnValue(True)
                else:
                    log.info(
                        "Could not process join request from device "
                        "{deveui}.",
                        deveui=euiString(device.deveui))
                    returnValue(False)

            # LoRa message. Check this is a registered device
            device = yield self._getActiveDevice(message.payload.fhdr.devaddr)
            if device is None:
                log.info(
                    "Message from device using unregistered address "
                    "{devaddr}",
                    devaddr=devaddrString(message.payload.fhdr.devaddr))
                returnValue(False)

            # Check the device is enabled
            if not device.enabled:
                log.info("Message from disabled device {devaddr}",
                         devaddr=devaddrString(message.payload.fhdr.devaddr))
                returnValue(False)

            # Check frame counter
            if not device.checkFrameCount(message.payload.fhdr.fcnt,
                                          self.band.max_fcnt_gap,
                                          self.config.fcrelaxed):
                log.info("Message from {devaddr} failed frame count check.",
                         devaddr=devaddrString(message.payload.fhdr.devaddr))
                log.debug(
                    "Received frame count {fcnt}, device frame count {dfcnt}",
                    fcnt=message.payload.fhdr.fcnt,
                    dfcnt=device.fcntup)
                yield device.update(fcntup=device.fcntup,
                                    fcntdown=device.fcntdown,
                                    fcnterror=device.fcnterror)
                returnValue(False)

            # Perform message integrity check.
            if not message.checkMIC(device.nwkskey):
                log.info(
                    "Message from {devaddr} failed message "
                    "integrity check.",
                    devaddr=devaddrString(message.payload.fhdr.devaddr))
                returnValue(False)

            # Update SNR reading and device
            device.updateSNR(rxpk.lsnr)
            yield device.update(tx_chan=rxpk.chan,
                                tx_datr=rxpk.datr,
                                fcntup=device.fcntup,
                                fcntdown=device.fcntdown,
                                fcnterror=device.fcnterror,
                                time=rxpk.time,
                                tmst=rxpk.tmst,
                                adr=bool(message.payload.fhdr.adr),
                                snr=device.snr,
                                snr_average=device.snr_average,
                                gw_addr=gateway.host)

            # Set the device rx window parameters
            device.rx = self.band.rxparams((device.tx_chan, device.tx_datr),
                                           join=False)

            # Process MAC Commands
            commands = []
            # Standalone MAC command
            if message.isMACCommand():
                message.decrypt(device.nwkskey)
                commands = [MACCommand.decode(message.payload.frmpayload)]
            # Contains piggybacked MAC command(s)
            elif message.hasMACCommands():
                commands = message.commands

            for command in commands:
                if command.isLinkCheckReq():
                    self._processLinkCheckReq(device, command, request,
                                              rxpk.lsnr)
                elif command.isLinkADRAns():
                    self._processLinkADRAns(device, command)
                # TODO: add other MAC commands

            # Process application data message
            if message.isUnconfirmedDataUp() or message.isConfirmedDataUp():
                # Find the app
                app = yield Application.find(
                    where=['appeui = ?', device.appeui], limit=1)
                if app is None:
                    log.info(
                        "Message from {devaddr} - AppEUI {appeui} "
                        "does not match any configured applications.",
                        devaddr=euiString(device.devaddr),
                        appeui=device.appeui)
                    returnValue(False)

                # Decrypt frmpayload
                message.decrypt(device.appskey)
                appdata = str(message.payload.frmpayload)
                port = message.payload.fport

                # Route the data to an application server via the configured interface
                log.info("Outbound message from devaddr {devaddr}",
                         devaddr=devaddrString(device.devaddr))
                interface = interfaceManager.getInterface(app.appinterface_id)
                if interface is None:
                    log.error(
                        "No outbound interface found for application "
                        "{app}",
                        app=app.name)
                elif not interface.started:
                    log.error(
                        "Outbound interface for application "
                        "{app} is not started",
                        app=app.name)
                else:
                    self._outboundAppMessage(interface, device, app, port,
                                             appdata)

                # Send an ACK if required
                if message.isConfirmedDataUp():
                    yield self.inboundAppMessage(device.devaddr,
                                                 '',
                                                 acknowledge=True)