floranet/lora/mac.py
class MACDataDownlinkMessage(MACDataMessage):
    """A LoRa MAC Data Uplink Message.
    
    LoRa uplink data messages carry a PHY payload
    consiting of a single octet header (mhdr),
    a MAC payload (macpayload) and a 4-octet message
    integrity code (mic).
    
    Attributes:
        confirmed (bool): True if Confirmed Data Down
        devaddr (int): Device address (DevAddr)
        key (int): Encryption key (NwkSkey or AppSKey)
        
    """
    def __init__(self, devaddr, key, fcnt, adrenable, fopts,
                 fport, frmpayload, acknowledge=False):
        """MACDataDownlinkMessage initialisation method.
        
        """
        self.devaddr = devaddr
        self.key = key
        self.mhdr = MACHeader(UN_DATA_DOWN, LORAWAN_R1)
        ack = 1 if acknowledge is True else 0
        adr = 1 if adrenable is True else 0
        foptslen = len(fopts)
        fhdr = FrameHeader(devaddr, adr, 0, ack, foptslen, fcnt,
                           fopts, fpending=0, fdir='down')
        self.payload = MACPayload(fhdr, fport, frmpayload)
        self.mic = None
        
    def encode(self):
        """Create a binary representation of MACMessage object.
        
        Returns:
            String of packed data.
        
        """
        # Calculate the MIC.
        # The MIC is calculated as cmac = aes128_cmac(NwkSKey, B0 | msg)
        # MIC = cmac[0:3]
        # msg is defined as: MHDR | FHDR | FPort | FRMPayload
        # B0 is defined as:
        # 1 byte 0x49 | 4 bytes 0x00 | 1 byte dir=0 for uplink, 1 for downlink
        # 4 bytes devaddr | 4 bytes fcntup or fcntdown
        # 1 byte 0x00 | 1 bytes len
        msg = self.mhdr.encode() + self.payload.encode()
        B0 = struct.pack('<BLBLLBB', int('0x49', 16), 0, 1,
                         self.devaddr, self.payload.fhdr.fcnt, 0, len(msg))
        data = B0 + msg
        # Create the MIC over the entire message
        self.mic = aesEncrypt(intPackBytes(self.key, 16), data,
                              mode='CMAC')[0:4]
        msg += self.mic
        return msg

    def encrypt(self, key):
        super(MACDataDownlinkMessage, self).encrypt(key, dir=1)
floranet/util.py
def devaddrString(devaddr):
    """Convert a 32 bit Lora DevAddr to string hex representation.
    
    Args:
        devaddr (int): a 6 byte Lora DevAddr.
    
    Returns:
        The hex string representation.
    """
    return intHexString(devaddr, 4)
floranet/models/config.py
class Config(Model):
    """NetServer Configuration model
    
    Model representing the netserver configuration.
    
    Attributes:
        name(str): Server name
        listen (str): Interfaces to listen on
        port (int): LoRaWAN port
        webport (int): Web server port
        apitoken (str): REST API authentication token
        freqband (str): Frequency band
        netid (int): Network ID
        duplicateperiod (int): Period (seconds) used to check for duplicate messages
        fcrelaxed (bool): Relaxed frame count mode
        otaastart (int): OTAA range start address
        otaaend (int): OTAA range end address
        macqueuing (bool): Queue downlink MAC commands 
        macqueuelimit (int): Time (seconds) that MAC Commands can remain in the queue
        adrenable (bool): Adapative data rate enable
        adrmargin (int): SNR margin added to the calculation of adaptive data rate steps
        adrcycletime (int): Period (seconds) for ADR control cycle
        adrmessagetime (int): Minimum inter-ADR message time (seconds)
    """
    
    TABLENAME = 'config'
    
    @classmethod
    @inlineCallbacks
    def loadFactoryDefaults(self):
        """Populate NetServer configuration defaults."""
        
        # Server configuration
        c = Config()
        c.defaults()
        
        yield c.save()
        returnValue(c)

    def check(self):
        """Validate the system configuration object.
            
        Returns:
            valid (bool), message(dict): (True, empty) on success,
            (False, error message dict) otherwise.
        """
        messages = {}

        if self.name == '':
            messages['listen'] = 'Invalid name'
            
        if self.listen != '' and not (validIPv4Address(self.listen) or
                validIPv6Address(self.listen)):
            messages['listen'] = 'Invalid IP address'
            
        if self.port < 1 or self.port > 65535:
            messages['port'] = 'Invalid server port'

        if self.webport < 1 or self.webport > 65535:
            messages['webport'] = 'Invalid web port'

        if self.webport < 1 or self.webport > 65535:
            messages['webport'] = 'Invalid web port'
            
        if self.freqband not in LoraBand.BANDS:
            messages['freqband'] = 'Invalid frequency band'
            
        if self.netid < 1 or self.netid > int('0xFFFFFF', 16):
            messages['netid'] = 'Invalid network ID'

        if self.duplicateperiod < 1 or self.duplicateperiod > 60:
            messages['netid'] = 'Invalid duplicate period'
        
        if self.otaastart < 1 or self.otaastart > int('0xFFFFFFFF', 16):
            messages['otaastart'] = 'Invalid OTAA start address'
        elif self.otaaend < 1 or self.otaaend > int('0xFFFFFFFF', 16) \
             or self.otaaend <= self.otaastart:
            messages['otaastart'] = 'Invalid OTAA end address'
        
        if self.macqueuelimit < 60 or self.macqueuelimit > 86400:
            messages['macqueuelimit'] = 'Invalid MAC queueing limit'

        if self.adrmargin < 0.0:
            messages['adrmargin'] = 'Invalid ADR SNR margin'
        
        if self.adrcycletime < 60:
            messages['adrcycletime'] = 'Invalid ADR cycle time'
            
        if self.adrmessagetime < 1:
            messages['adrmessagetime'] = 'Invalid ADR message time'
 
        valid = not any(messages)
        return valid, messages
            
    def defaults(self):
        """Populate server configuration defaults
        
        """
        self.name = 'floranet'
        self.listen = '0.0.0.0'
        self.port = 1700
        self.webport = 8000
        self.apitoken = 'IMxHj@wfNkym@*+V85Rs^G<QXMD~p[eaX3S=_D8f7{z0q{GN'
        self.freqband = 'US915'
        self.netid = int('0x010203', 16)
        self.duplicateperiod = 10
        self.fcrelaxed = True        
        self.otaastart = int('0x06000001', 16)
        self.otaaend = int('0x060FFFFF', 16)
        self.macqueueing = True
        self.macqueuelimit = 120
        self.adrenable = True
        self.adrmargin = 0.0
        self.adrcycletime = 9000
        self.adrmessagetime = 10
    
    def valid(self):
        pass
floranet/models/application.py
class Application(Model):
    """LoRa application class
    
    Model representing a LoRa application.
    
    Attributes:
        name (str): a user friendly name for the application
        domain (str): optional customer domain string
        appeui (int): global application ID (IEEE EUI64)
        appnonce (int): A unique ID provided by the network server
        appkey (int): AES-128 application secret key
        fport (int): Port field used for this application
    """
    
    TABLENAME = 'applications'
    BELONGSTO = [{'name': 'appinterface', 'class_name': 'AppInterface'}]
    HASMANY = [{'name': 'properties', 'class_name': 'AppProperty'}]
        
    @inlineCallbacks
    def valid(self):
        """Validate an application object.
            
        Returns:
            valid (bool), message(dict): (True, empty) on success,
            (False, error message dict) otherwise.
        """
        messages = {}

        # Check for unique appkeys
        duplicate = yield Application.exists(where=['appkey = ? AND appeui != ?',
                                                    self.appkey, self.appeui])
        if duplicate:
            messages['appkey'] = "Duplicate application key exists: appkey " \
                "must be unique."
        
        # Check the app interface exists
        if self.appinterface_id:
            exists = yield AppInterface.exists(where=['id = ?', self.appinterface_id])
            if not exists:
                messages['appinterface_id'] = "Application interface {} does not " \
                "exist.".format(self.appinterface_id)

        valid = not any(messages)
        returnValue((valid, messages))
floranet/lora/crypto.py
def aesEncrypt(key, data, mode=None):
    """AES encryption function
    
    Args:
        key (str): packed 128 bit key
        data (str): packed plain text data
        mode (str): Optional mode specification (CMAC)
        
    Returns:
        Packed encrypted data string
    """
    if mode == 'CMAC':
        # Create AES cipher using key argument, and encrypt data
        cipher = python_AES.new(key, python_AES.MODE_CMAC)
    elif mode == None:
        cipher = python_AES.new(key)
    return cipher.encrypt(data)
floranet/lora/wan.py
class Txpk(object):
    """A Gateway Txpk (downstream) JSON object.
    
    The root JSON object shall contain zero or more txpk
    objects. See Gateway to Server Interface Definition
    Section 6.2.4.
    
    Attributes:
        imme (bool): If true, the gateway is commanded to
                     transmit the frame immediately 
        tmst (int): If "imme" is not true and "tmst" is present,
                    the gateway is commanded to transmit the frame
                    when its internal timestamp counter equals the
                    value of "tmst".
        time (str): UTC time. The precision is one microsecond. The
                    format is ISO 8601 compact format. If "imme" is
                    false or not present and "tmst" is not present,
                    the gateway is commanded to transmit the frame at
                    this time.
        freq (float): The centre frequency on when the frame is to
                    be transmitted in units of MHz.
        rfch (int): The antenna on which the gateway is commanded
                    to transmit the frame.
        powe (int): The output power which what the gateway is
                    commanded to transmit the frame.
        modu (str): Modulation technique - "LORA" or "FSK".
        datr (str): Datarate identifier. For Lora, comprised of
                    "SFnBWm where n is the spreading factor and
                    m is the frame's bandwidth in kHz.
        codr (str): ECC code rate as "k/n" where k is carried
                    bits and n is total bits received.
        ipol (bool): If true, commands gateway to invert the
                    polarity of the transmitted bits. LoRa Server sets
                    value to true when "modu" equals "LORA", otherwise
                    the value is omitted.
        size (int): Number of octets in the received frame.
        data (str): Frame payload encoded in Base64. Padding characters
                    shall not be not added
        ncrc (bool): If not false, disable physical layer CRC generation
                    by the transmitter.
    """
    
    def __init__(self, imme=None, tmst=None, time=None, freq=None,
                 rfch=None, powe=None, modu=None, datr=None, codr=None,
                 ipol=None, size=None, data=None, ncrc=None):
        """Txpk initialisation method.
        
        """
        self.imme = imme
        self.tmst = tmst 
        self.time = time
        self.freq = freq
        self.rfch = rfch
        self.powe = powe
        self.modu = modu
        self.datr = datr
        self.codr = codr
        self.ipol = ipol  
        self.size = size
        self.data = data
        self.ncrc = ncrc
        self.keys = ['imme', 'tmst', 'time', 'freq', 'rfch',
                    'powe', 'modu', 'datr', 'codr', 'ipol',
                    'size', 'data', 'ncrc']
        # Base64 encode data, no padding
        if self.data is not None:
            self.size = len(self.data)
            self.data = base64.b64encode(self.data)
            # Remove padding
            if self.data[-2:] == '==':
                self.data = self.data[:-2]
            elif self.data[-1:] == '=':
                self.data = self.data[:-1]
        else:
            self.size = 0
    
    def encode(self):
        """Create a JSON string from Txpk object
        
        """
        # Create dict from attributes. Maintain added order
        jd = {'txpk': OrderedDict()}
        for key in self.keys:
            val = getattr(self, key)
            if val is not None:
                jd['txpk'][key] = val
        return json.dumps(jd, separators=(',', ':'))
floranet/models/device.py
class Device(Model):
    """LoRa device class model.
        
    Attributes:
        name (str): Device name
        devclass (str): Device class
        devaddr (int): 32 bit end device address (DevAddr)
        deveui (int): Global end-device ID (IEEE EUI64)
        otaa (bool): Over the air activated flag
        appeui (int): Application unique identifer (AppEUI)
        nwkskey (int): 128 bit network session key (NwkSKey)
        appskey (int): 128 bit application session key (AppSKey)
        tx_chan (int): Transmit channel number
        tx_datr (str): Transmit data rate
        adr (bool): Device ADR control
        adr_datr (str): Target ADR data rate
        gw_addr (str): The gateway IP address
        fcntup (int): Uplink frame  counter received from the device
        fcntdown (int): Downlink frame counter sent to to the device
        fcnterror (bool): Frame count error flag
        snr_average (float): Calculated ADR average SNR
        snr (list): SNR readings array
        devnonce (list): Devnonce values from join requests
        created (str): Timestamp when the device object is created
        updated (str): Timestamp when the device object is updated
    """
    
    TABLENAME = 'devices'
    
    @inlineCallbacks
    def valid(self, server):
        """Validate a device object.
        
        Args:
            server (NetServer): Network server object
            
        Returns:
            valid (bool), message(dict): (True, empty) on success,
            (False, error message dict) otherwise.
        """
        messages = {}
        
        # Check we have a valid class
        if not self.devclass in {'a', 'c'}:
            messages['class'] = "Invalid device class"
        
        # For ABP device, check for unique devaddr and keys, and devaddr
        # validity
        if not self.otaa:
            check = {'devaddr': self.devaddr, 'nwkskey': self.nwkskey,
                     'appskey': self.appskey}
            for attr,v in check.items():
                exists = yield Device.exists(where=[attr + ' = ? AND deveui != ?',
                                                    v, self.deveui])
                if exists:
                    messages[attr] = "Device {} {} ".format(attr, v) + \
                                     "currently exists. Must be unique."

            # Check devaddr is correctly defined within the network range
            if not 'devaddr' in messages:
                if not server.checkDevaddr(self.devaddr):
                    messages['devaddr'] = "Device devaddr " + \
                            "{} ".format(devaddrString(self.devaddr)) + \
                            "is not within the configured network address range"

            # Check devaddr is not within the OTAA range
            if not 'devaddr' in messages:
                if self.devaddr >= server.config.otaastart and \
                 self.devaddr <= server.config.otaaend:
                    messages['devaddr'] = "Device devaddr " \
                            "{} ".format(devaddrString(self.devaddr)) + \
                             "is within configured OTAA address range"

        valid = not any(messages)
        returnValue((valid, messages))
    
    def isClassA(self):
        """Check if device is class A"""
        return self.devclass == 'a'
    
    def isClassB(self):
        """Check if device is class B"""
        return self.devclass == 'b'

    def isClassC(self):
        """Check if device is class C"""
        return self.devclass == 'c'
    
    def checkDevNonce(self, message):
        """Check the devnonce is not being repeated
        
        Args:
            message (JoinRequestMessage): Join request message
            
        Returns True if message is valid, otherwise False
        """
        if self.devnonce is None:
            self.devnonce = []
        
        # If the  devnonce has been seen previously, return False
        if message.devnonce in self.devnonce:
            return False
        # If we have exceeded the history length, pop the oldest devnonce
        if len(self.devnonce) >= 20:
            self.devnonce.pop(0)
        self.devnonce.append(message.devnonce)
        return True
            
    def checkFrameCount(self, fcntup, maxfcntgap, relaxed):
        """Sync fcntup counter with received value
        
        The value received must have incremented compared to current
        counter value and must be less than the gap value specified
        by MAX_FCNT_GAP after considering rollovers. Otherwise,
        too many frames have been lost.
        
        Args:
            fcntup (int): Received fcntup value
            maxfcntgap (int): MAX_FCNT_GAP, band specific
            relaxed (bool): frame count relaxed flag
            
        Returns:
            True if fcntup is within the limit, otherwise False.
        
        """
        # Relxed mode. If fcntup <=1 then set fcntdown to zero
        # and the device fcntup to match.
        if relaxed and fcntup <= 1:
            self.fcntdown = 0
            self.fcntup = fcntup
            self.fcnterror = False
        elif fcntup > (self.fcntup + maxfcntgap):
            self.fcnterror = True
        elif fcntup < self.fcntup and (65535 - self.fcntup + fcntup) > maxfcntgap:
            self.fcnterror = True
        else:
            self.fcntup = fcntup
            self.fcnterror = False
        
        return not self.fcnterror

    def updateSNR(self, lsnr):
        """Update Device SNR measures
        
        Updates the most recent received device SNR measure.
        We keep 11 samples for each device, and use the
        snr_pointer attribute to maintain the current sample
        index.
        
        Args:
            lsnr (float): Latest link SNR measure.
        
        """
        # Check we have a SNR measure
        if lsnr is None:
            return
        
        # Check if this is the first SNR reading
        if self.snr is None:
            self.snr = []
            
        # Update the current SNR reading
        if len(self.snr) == 11:
            self.snr.pop(0)
        self.snr.append(lsnr)
        
        # Update the average SNR, ensure we have at least 6 readings
        if len(self.snr) >= 6:
            self.snr_average = sum(self.snr[-6:])/6.0

    def getADRDatarate(self, band, margin):
        """Determine the optimal datarate that will achieve the
        objective margin in the given band.
        
        We assume each increase in datarate step (e.g. DR0 to DR1) requires
        an additional 3dB in SNR.
        
        Args:
            band (Band): Band in use
            margin (float): Target margin in dB
        
        Returns:
            Optimal datarate as a string on success, otherwise None.
        """
        if not hasattr(self, 'snr_average') or self.snr_average is None:
            return None
        
        # Target thresholds that the average must exceed. Note range(0,4)
        # refers to the first four (upstream) indices of the band.datarate
        # list. These are DR0, DR1, DR2, DR3
        thresholds = [float(i) * 3.0 + margin for i in range(0,4)]

        # If we have an average SNR less than the lowest threshold,
        # return the lowest DR
        if self.snr_average < thresholds[0]:
            return band.datarate[0]
        
        # Find the index of lowest threshold that the SNR average just exceeds
        i = [n for n,v in enumerate(thresholds) if self.snr_average >= v][-1]
        return band.datarate[i]
floranet/lora/wan.py
class LoraWAN(protocol.DatagramProtocol):
    """LoRaWAN Gateway to Server Interface.
    
    Attributes:
        server (NetServer): The Lora Network Server
        port (Port): Twsited TCP port
        gateways (list): List of configured Gateways 
    
    """
        
    def __init__(self, server):
        """Initialize a LoRaWAN gateway network interface.
        
        Args:
            server: FloraNetServer object.        
        """
        self.server = server
        self.port = None
        self.gateways = []
    
    @inlineCallbacks  
    def start(self):
        """Start the gateway network interface"""
        self.gateways = yield Gateway.all()
        self.port = reactor.listenUDP(self.server.config.port, self,
                          interface=self.server.config.listen)
        returnValue(True)
    
    @inlineCallbacks
    def restart(self):
        """Restart the gateway network interface"""
        yield self.port.stopListening()
        self.port = reactor.listenUDP(self.server.config.port, self,
                          interface=self.server.config.listen)
        
    def addGateway(self, gateway):
        """Add a gateway to the active list
        
        Args:
            gateway (Gateway): The gateway to add
        
        """
        self.gateways.append(gateway)
        log.info("Added gateway {host} to the active list", host=gateway.host)

    def updateGateway(self, host, gateway):
        """Update a gateway
        
        Args:
            host (str): The host id to update
            gateway (Gateway): The updated gateway        
        """
        index = next((i for i,g in enumerate(self.gateways)
                      if g.host == host), None)
        if index is not None:
            for a in {'host', 'name', 'enabled', 'power'}:
                setattr(self.gateways[index], a, getattr(gateway, a))
            log.info("Updated gateway {host}", host=gateway.host)

    def deleteGateway(self, gateway):
        """Delete a gateway from the active list
        
        Args:
            gateway (Gateway): The gateway to delete
        
        """
        index = next((i for i,g in enumerate(self.gateways)
                      if g.host == gateway.host), None)
        if index is not None:
            del(self.gateways[index])
            log.info("Removed gateway {host} from the active list", host=gateway.host)

    def gateway(self, host):
        """Get the enabled gateway for host address
        
        Args:
            host (str): The host address
        
        Returns:
            Gateway object if found abd enabled, None otherwise.
        """
        gateway = next((g for g in self.gateways if g.host == host), None)
        return gateway
    
    def datagramReceived(self, data, (host, port)):
        """Handle an inbound LoraWAN datagram.
        
        Called from the Twisted reactor on receipt of an inbound
        UDP datagram. This method verified the gateway and
        dispatches the inbound GWMP types PULL_DATA and PUSH_DATA.
        
        Args:
            data (str): UDP packet data.
            (host, port) (tuple): Gateway IP address and port.
        
        """
        log.debug("Received {data} from {host}:{port}", data=repr(data),
                 host=host, port=port)
        gateway = self.gateway(host)
        if gateway is None:
            log.error("Gateway message from unknown gateway {host}", host=host)
            return
        if not gateway.enabled:
            log.error("Gateway message from disabled gateway {host}", host=host)
            return
        try:
            message = GatewayMessage.decode(data, (host, port))
        except (UnsupportedMethod, DecodeError) as e:
            if isinstance(e, UnsupportedMethod):
                log.error("Gateway message unsupported method error "
                        "{errstr}", errstr=str(e))
            elif isinstance(e, DecodeError):
                log.error("Gateway message decode error "
                        "{errstr}", errstr=str(e))
            return
        gateway.eui = message.gatewayEUI
        if message.id == PULL_DATA:
            log.debug("Received PULL_DATA from %s:%d" % (host, port))
            gateway.port = port
            self._acknowledgePullData(message)
        elif message.id == PUSH_DATA:
            log.debug("Received PUSH_DATA from %s:%d" % (host, port))
            self._acknowledgePushData(message)
            self.server.processPushDataMessage(message, gateway)
        elif message.id == TX_ACK:
            # TODO: Version 2 only
            pass
    
    def sendPullResponse(self, request, txpk):
        """"Send a PULL_RESP message to a gateway.
        
        The PULL_RESP message transports its payload, a JSON object,
        from the LoRa network server to the LoRa gateway. The length
        of a PULL_RESP message shall not exceed 1000 octets.
        
        Args:
            request (GatewayMessage): The decoded Pull Request
            txpk (Txpk): The txpk to be transported
        """
        # Create a new PULL_RESP message. We must send to the
        # gateway's PULL_DATA port.
        host = request.remote[0]
        gateway = self.gateway(host)
        if gateway is None:
            log.error("Pull Reponse - no known gateway for {host}",
                      host=host)
            return
        if gateway.port == None:
            log.error("Pull Reponse - no known port for gateway {host}",
                      host=host)
            return
        remote = (host, gateway.port)
        m = GatewayMessage(version=request.version, token=request.token,
                    identifier=PULL_RESP, gatewayEUI=gateway.eui,
                    remote=remote, ptype='txpk', txpk=txpk)
        log.debug("Sending PULL_RESP message to %s:%d" % remote)
        self._sendMessage(m)
    
    def _acknowledgePullData(self, request):
        """Acknowledge a PULL_DATA message from a gateway.
        
        The PULL_ACK message is used by the network server to
        acknowledge receipt of a PULL_DATA message.
        
        Args:
            request (GatewayMessage): The decoded PULL_DATA message.
        """
        # Create a new PULL_ACK message
        m = GatewayMessage(version=request.version, token=request.token,
                    identifier=PULL_ACK, gatewayEUI=request.gatewayEUI,
                    remote=request.remote)
        log.debug("Sending PULL_ACK message to %s:%d" % m.remote)
        self._sendMessage(m)
    
    def _acknowledgePushData(self, request):
        """Acknowledge a PUSH_DATA message from a gateway.
        
        The PUSH_ACK message is used by the network server to
        acknowledge receipt of a PUSH_DATA message.
        
        Args:
            request (GatewayMessage): The decoded PUSH_DATA message.
        """
        # Create a new PUSH_ACK message
        m = GatewayMessage(version=request.version, token=request.token,
                    identifier=PUSH_ACK, gatewayEUI=request.gatewayEUI,
                    remote=request.remote)
        log.debug("Sending PUSH_ACK message to %s:%d" % m.remote)
        self._sendMessage(m)
        
    def _sendMessage(self, message):
        """Encode and send a GWMP message
        
        Args:
            message (GatewayMessage): Outbound gateway message
        """
        # Encode and send
        packet = message.encode()
        (host, port) = message.remote
        log.debug("Sending {packet} to {host}:{port}", packet=repr(packet),
                 host=host, port=port)
        self.transport.write(packet, message.remote)
floranet/web/webserver.py
class WebServer(object):
    """Webserver class.
        
    Attributes:
        app (Flask): Flask app instance
        site (Site): Twisted web site
        port (Port): Twisted UDP port
        api (RestApi): Rest API instance
        login (LoginManager): Flask login manager
    """
    def __init__(self, server):
        """Initialize the web server.
        
        Args:
            server: NetServer object      
        """
        self.server = server
        self.port = None
        
        # Create Flask app and configure
        self.app = Flask(__name__)
        self.app.config['ERROR_404_HELP'] = False

        # Create REST API instance
        self.restapi = RestApi(self.app, self.server)

        # Create LoginManager instance and configure
        self.login = LoginManager()
        self.login.init_app(self.app)
        self.login.request_loader(self.load_user)
        
    def start(self):
        """Start the Web Server """
        self.site = Site(WSGIResource(reactor, reactor.getThreadPool(), self.app))
        self.port = reactor.listenTCP(self.server.config.webport, self.site)

    @inlineCallbacks
    def restart(self):
        """Restart the web server"""
        yield self.port.stopListening()
        self.port = reactor.listenTCP(self.server.config.webport, self.site)
        
    def load_user(self, request):
        """Flask login request_loader callback.
        
        The expected behavior is to return a User instance if
        the provided credentials are valid, and return None
        otherwise.
        
        Args:
            request (request): Flask request object
            
        Returns:
            User object on success, otherwise None
        """
        # Get the token as Authorisation header or JSON parameter
        token = request.headers.get('Authorization')
        if token is None:
            data = request.get_json()
            if data is None:
                return None
            token = data['token'] if 'token' in data else None

        # Verify the token
        if token is not None:
            if token.encode('utf8') == self.server.config.apitoken:
                return User('api', None)

        return None
floranet/log.py
class Log(Logger):
    def __init__(self):
    def start(self, console, logfile, debug):
floranet/util.py
def euiString(eui):
    """Convert a Lora EUI to string hex representation.
    
    Args:
        eui (int): an 8 byte Lora EUI.
    
    Returns:
        The hex string representation.
    """
    return intHexString(eui, 8)
floranet/lora/bands.py
class AU915(US915):
    """Australian 915-928 ISM Band
    
    Subclass of US915 band. Same parameters apply, with the exception
    of upstream channels, which are upshifted to start at 915.2 MHz
    (channels 0 to 63) and 915.9 (channels 64 to 71).
    
    """
    def __init__(self):
        super(AU915, self).__init__()
        self.upstream = []
        for i in range(0, 64):
            self.upstream.append((9152 + 2.0 * i)/10)
        for i in range(0, 8):
            self.upstream.append((9159 + 16.0 * i)/10)
floranet/lora/bands.py
class US915(LoraBand):
    """US 902-928 ISM Band
    
    upstream (list): 72 upstream (from device) channels:
                    64 channels (0 to 63) utilizing LoRa 125 kHz BW
                    starting at 902.3 MHz and incrementing
                    linearly by 200 kHz to 914.9 MHz.
                    8 channels (64 to 71) utilizing LoRa 500 kHz BW
                    starting at 903.0 MHz and incrementing linearly
                    by 1.6 MHz to 914.2 MHz. Units of MHz
    downstream (list): 8 channels numbered 0 to 7 utilizing
                    LoRa 500 kHz BW starting at 923.3 MHz and incrementing
                    linearly by 600 kHz to 927.5 MHz. Units of MHz
    datarate (dict): Data rate configuration as per Table 18 of the
                    LoRa specification
    datarate_rev (dict): Reverse lookup for datarate.
    maxpayload (dict): Maximim payload size, indexed by datarate as per
                    Table 20 of the LoRa specification
    rx1dr (dict): Dictionary of lists to lookup the RX1 window data rate by
                    transmit data rate and Rx1DROffset parameters. Lookup by
                    rx1dr[txdatarate][rx1droffset]
    rx1droffset (int): RX1 default DR offset
    receive_delay (dict): First and second window window receive delays
    join_accept_delay (dict): First and second window join accept delay
    max_fcnt_gap  (int): Maximum tolerable frame count gap.
    maxmac (dict): List of maximum MACpayload sizes (in bytes) for each
                    datarate.
    maxapp (dict): List of maximum application sizes (in bytes) for each
                    datarate.
                    
                    
    """

    def __init__(self):
        """Initialize a US915 band object."""

        # Upstream channels in MHz
        self.upstream = []
        for i in range(0, 64):
            self.upstream.append((9033 + 2.0 * i)/10)
        for i in range(0, 8):
            self.upstream.append((9030 + 16.0 * (i - 64))/10)
        # Downstream channels in MHz
        self.downstream = []
        for i in range(0, 8):
            self.downstream.append((9233 + 6.0 * i)/10)
        self.datarate = {
            0:  'SF10BW125',
            1:  'SF9BW125',
            2:  'SF8BW125',
            3:  'SF7BW125',
            4:  'SF8BW500',
            8:  'SF12BW500',
            9:  'SF11BW500',
            10: 'SF10BW500',
            11: 'SF9BW500',
            12: 'SF8BW500',
            13: 'SF7BW500'
        }
        self.datarate_rev = {v:k for k, v in self.datarate.items()}
        self.txpower = {0: 30, 1: 28, 2: 26, 3: 24, 4: 22, 5: 20,
                        6: 18, 7: 16, 8: 14, 9: 12, 10: 10 }
        self.rx1dr = {
                    0: [10,  9,   8,   8],
                    1: [11,  10,  9,   8],
                    2: [12,  11,  10,  9],
                    3: [13,  12,  11,  10],
                    4: [13,  13,  12,  11],
                    8: [8,   8,   8,   8],
                    9: [9,   8,   8,   8],
                   10: [10,  9,   8,   8],
                   11: [11,  10,  9,   8],
                   12: [11,  11,  10,  9], 
                   13: [13,  12,  11,  9]  }
        self.rx1droffset = 0
        self.receive_delay = {1: 1, 2: 2}
        self.join_accept_delay = {1: 5, 2: 6}
        self.max_fcnt_gap = 16384
        self.maxpayloadlen = {
            0: 19,
            1: 61,
            2: 137,
            3: 250,
            4: 250,
            8: 61,
            9: 137,
            10: 250,
            11: 250,
            12: 250,
            13: 250 }
        self.maxappdatalen = {
            0: 11,
            1: 53,
            2: 129,
            3: 242,
            4: 242,
            8: 53,
            9: 129,
            10: 242,
            11: 242,
            12: 242,
            13: 242 }
floranet/lora/bands.py
class EU868(LoraBand):
    """ EUROPEAN 863-870 ISM Band 
        based on LoRoWAN_Regional_Parameters_v1_0
    """
    def __init__(self):
        super(EU868, self).__init__()
        self.upstream = [ 868.10 , 868.30 , 868.50 , 867.1 , 867.3 , 867.5 , 867.7 , 867.9 , 868.8 ]
        self.downstream = self.upstream
        self.datarate = {
            0: 'SF12BW125',
            1: 'SF11BW125',
            2: 'SF10BW125',
            3: 'SF9BW125',
            4: 'SF8BW125',
            5: 'SF7BW125',
            6: 'SF7BW250'
            #7: FSK ?
        }
        self.datarate_rev = {v:k for k, v in self.datarate.items()}
        self.txpower = { 0:20 , 1:14 , 2:11 , 3:8 , 4:5 , 5:2 }
        self.rx1dr = {
            0: [ 0 , 0 , 0 , 0 , 0 , 0 ],
            1: [ 1 , 0 , 0 , 0 , 0 , 0 ],
            2: [ 2 , 1 , 0 , 0 , 0 , 0 ],
            3: [ 3 , 2 , 1 , 0 , 0 , 0 ],
            4: [ 4 , 3 , 2 , 1 , 0 , 0 ],
            5: [ 5 , 4 , 3 , 2 , 1 , 0 ],
            6: [ 6 , 5 , 4 , 3 , 2 , 1 ],
            7: [ 7 , 6 , 5 , 4 , 3 , 2 ],
        }
        self.rx1droffset = 0
        self.receive_delay = {1: 1, 2: 2}
        self.join_accept_delay = {1: 5, 2: 6}
        self.max_fcnt_gap = 16384
        self.maxpayloadlen = {
            0: 59,
            1: 59,
            2: 59,
            3:123,
            4:250,
            5:250,
            6:250,
            7:250,
        }
        self.maxappdatalen = {
            0: 51,
            1: 51,
            2: 51,
            3:115,
            4:242,
            5:242,
            6:242,
            7:242
        }

    def _rx2receive(self):
        """Get second receive window parameters
        RX2 (second receive window) settings uses a fixed data
        rate and frequency. Default parameters are 869.525Mhz / DR0
        Returns:
            A dict of RX2 frequency, datarate string, datarate index
        """
        rxindex = 0
        return {'freq': 869.525, 'datr': self.datarate[rxindex],
                'index': rxindex}
floranet/lora/mac.py
class MACMessage(object):
    """A LoRa MAC message.
    
    """        
    @classmethod
    def decode(cls, data):
        """Decode the message type.
        
        Args:
            data (str): UDP packet data.
        
        Returns:
            MACJoinMessage or MACDataMessage on success, None otherwise.
            
        """
        # Message (PHYPayload) must be at least 1 byte
        if len(data) < 1:
            raise DecodeError()
        # Decode the MAC Header
        mhdr = MACHeader.decode(data[0])
        # Decode the Message
        if mhdr.mtype == JOIN_REQUEST:
            return JoinRequestMessage.decode(mhdr, data)
        elif mhdr.mtype == UN_DATA_UP or mhdr.mtype == CO_DATA_UP:
            return MACDataUplinkMessage.decode(mhdr, data)
        else:
            return None

    def isJoinRequest(self):
        """Check if message is a Join Request.
        
        Returns:
            True on match, otherwise False.
        
        """
        return self.mhdr.mtype == JOIN_REQUEST
    
    def isMACCommand(self):
        """Check if message is a MAC Command.
        
        Returns:
            True on match, otherwise False.
        
        """
        return self.payload.fport == 0

    def hasMACCommands(self):
        """Check if the message has piggybacked MAC commands.
        
        Returns:
            True on match, otherwise False.
        """
        return hasattr(self, 'commands') and len(self.commands) > 0
    
    def isUnconfirmedDataUp(self):
        """Check if message is Unconfirmed Data Up.
        
        Returns:
            True on match, otherwise False.
        
        """
        return self.mhdr.mtype == UN_DATA_UP
    
    def isConfirmedDataUp(self):
        """Check if message is Confirmed Data Up.
        
        Returns:
            True on match, otherwise False.
        
        """
        return self.mhdr.mtype == CO_DATA_UP
floranet/lora/mac.py
class LinkADRReq(MACCommand):
    """With the LinkADRReq command, the network server requests an
    end-device to perform a rate adaptation.
    
    Attributes:
        length (int): Frame length
        cid (int): Command identifier
        datarate (int): a 4-bit integer representing the device target data
                      rate, region specific
        txpower (int): a 4-bit integer that defines device transmit power,
                      region specific
        chmask (int): a 16-bit unsigned integer that encodes the channels
                      usable for uplink access - bit 0 is the LSB.
        chmaskcntl (int): a 3-bit integer that controls the interpretation
                      of the ChMask bit mask.
        nbrep (int): a 4-bit integer defining the number of repetitions
                      for each uplink message.
    """
    length = 5
    
    def __init__(self, datarate, txpower, chmask, chmaskcntl, nbrep):
        self.cid = LINKADRREQ
        self.datarate = datarate
        self.txpower = txpower
        self.chmask = chmask
        self.chmaskcntl = chmaskcntl
        self.nbrep = nbrep
    
    def encode(self):
        """Create a binary representation of LinkADRReq object.
        
        Returns:
            String of packed data.
        
        """
        datarate_txpower = 0 | (self.datarate << 4) | self.txpower
        redundancy = 0 | (self.chmaskcntl << 4) | self.nbrep
        data = struct.pack('<BBHB', self.cid, datarate_txpower, self.chmask, redundancy)
        return data
floranet/util.py
def intUnpackBytes(data, endian='big'):
    """Convert an packed binary string representation to an integer.
    
    Args:
        data (str): packed binary data
        endian (str): endian type: 'big' or 'little'
    
    Returns:
        An integer.
    """
    if isinstance(data, str):
        data = bytearray(data)
    if endian == 'big':
        data = reversed(data)
    num = 0
    for offset, byte in enumerate(data):
        num += byte << (offset * 8)
    return num
floranet/util.py
def txsleep(secs):
    """Simulate a reactor sleep
    
    Args:
        secs (float): time to sleep
    """
    d = Deferred()
    reactor.callLater(secs, d.callback, None)
    return d
floranet/lora/mac.py
class MACCommand(object):
    """A MAC Command.
    
    LoRa MAC commands consist of a command identifier (CID) of
    1 octect followed by a possibly empty command-specific sequence
    of octets.
    
    """
    @classmethod
    def decode(cls, data):
        """Create a MACCommand object from binary representation.
        
        Args:
            data (str): FRMpayload, or fopts.
        
        Returns:
            MACCommand object on success, otherwise None.
            
        """
        if len(data) == 0:
            return None
        cid = struct.unpack('B', data[0])[0]
        if cid == LINKCHECKREQ:
            return LinkCheckReq.decode(data)
        elif cid == LINKADRANS:
            return LinkADRAns.decode(data)
        # TODO
        #elif cid == DUTYCYCLEANS:
        #    return DutyCycleReq.decode(data)
        #elif cid == RXPARAMSETUPANS:
        #    return RxParamSetupReq.decode(data)
        #elif cid == DEVSTATUSANS:
        #    return DevStatusReq.decode(data)
        #elif cid == NEWCHANNELANS:
        #    return NewChannelReq.decode(data)
        #elif cid == RXTIMINGSETUPANS:
        #    return RxTimingSetupReq.decode(data)
        else:
            return None
    
        
    def isLinkCheckReq(self):
        """Check if the message is a LinkCheckReq MAC Command.
        
        Returns:
            True on match, otherwise False.
        
        """
        return self.cid == LINKCHECKREQ
    
    def isLinkADRAns(self):
        """Check if the message is a LinkADRAns MAC Command.
        
        Returns:
            True on match, otherwise False.
        
        """
        return self.cid == LINKADRANS
floranet/util.py
def intPackBytes(n, length, endian='big'):
    """Convert an integer to a packed binary string representation.
    
    Args:
        n (int): Integer to convert
        length (int): converted string length
        endian (str): endian type: 'big' or 'little'
    
    Returns:
        A packed binary string.
    """
    if length == 0:
        return ''
    h = '%x' % n
    s = ('0'*(len(h) % 2) + h).zfill(length*2).decode('hex')
    if endian == 'big':
        return s
    else:
        return s[::-1]
floranet/lora/wan.py
class GatewayMessage(object):
    """A Gateway Message.
    
    Messages sent between the LoRa gateway and the LoRa network
    server. The gateway message protocol operates over UDP and
    occupies the data area of a UDP packet. See Gateway to Server
    Interface Definition.
    
    Attributes:
        version (int): Protocol version - 0x01 or 0x02
        token (str): Arbitrary tracking value set by the gateway.
        id (int): Identifier - see GWMP Identifiers above.
        gatewayEUI (str): Gateway device identifier.
        payload (str): GWMP payload.
        remote (tuple): Gateway IP address and port.
        ptype (str): JSON protocol top-level object type.

    """

    def __init__(self, version=1, token=0, identifier=None,
                 gatewayEUI=None, txpk=None, remote=None,
                 ptype=None):
        """GatewayMessage initialisation method.
        
        Args:
            version (int): GWMP version.
            token (str): Message token.
            id: GWMP identifier.
            gatewayEUI: gateway device identifier.
            payload: GWMP payload.
            ptype (str): payload type
            remote: (host, port)
            
        Raises:
            TypeError: If payload argument is set to None.
        
        """
        self.version = version
        self.token = token
        self.id = identifier
        self.gatewayEUI = gatewayEUI
        self.payload = ''
        self.ptype = ptype
        self.remote = remote
        
        self.rxpk = None
        self.txpk = txpk
        self.stat = None
    
    @classmethod
    def decode(cls, data, remote):
        """Create a Message object from binary representation.
        
        Args:
            data (str): UDP packet data.
            remote (tuple): Gateway address and port.
        
        Returns:
            GatewayMessage object on success.
            
        """
        # Check length
        if len(data) < 4:
            raise DecodeError("Message too short.")
        # Decode header
        (version, token, identifer) = struct.unpack('<BHB', data[:4])
        m = GatewayMessage(version=version, token=token, identifier=identifer)
        m.remote = remote
        # Test versions (1 or 2) and supported message types
        if ( m.version not in (1, 2) or 
             m.version == 1 and m.id not in (PUSH_DATA, PULL_DATA) or 
             m.version == 2 and m.id not in (PUSH_DATA, PULL_DATA, TX_ACK)
             ):
                raise UnsupportedMethod()

        # Decode gateway EUI and payload
        if m.id == PUSH_DATA:
            if len(data) < 12:
                raise DecodeError("PUSH_DATA message too short.")
            m.gatewayEUI = struct.unpack('<Q', data[4:12])[0]
            m.payload = data[12:]
        elif m.id == PULL_DATA:
            if len(data) < 12:
                raise DecodeError("PULL_DATA message too short.")
            m.gatewayEUI = struct.unpack('<Q', data[4:12])[0]
        elif m.id == TX_ACK:
            m.payload = data[4:]
            
        # Decode PUSH_DATA payload
        if m.id == PUSH_DATA:
            try:
                jdata = json.loads(m.payload)
            except ValueError:
                raise DecodeError("JSON payload decode error")
            m.ptype = jdata.keys()[0]
            # Rxpk payload - one or more.
            if  m.ptype == 'rxpk':
                m.rxpk = []
                for r in jdata['rxpk']:
                    rx = Rxpk.decode(r)
                    if rx is not None:
                        m.rxpk.append(rx)
                if not m.rxpk:
                    raise DecodeError("Rxpk payload decode error")
            # Stat payload
            elif m.ptype == 'stat':
                m.stat = Stat.decode(jdata)
                if m.stat is None:
                    raise DecodeError("Stat payload decode error")
            # Unknown payload type
            else:
                raise DecodeError("Unknown payload type")
        return m

    def encode(self):
        """Create a binary representation of message from Message object.
        
        Returns:
            String of packed data.
        
        """
        data = ''
        if self.id == PUSH_ACK:
            data = struct.pack('<BHB', self.version, self.token, self.id)
        elif self.id == PULL_ACK:
            data = struct.pack('<BHBQ', self.version, self.token, self.id,
                               self.gatewayEUI)
        elif self.id == PULL_RESP:
            if self.version == 1:
                self.token = 0
            self.payload = self.txpk.encode()
            data = struct.pack('<BHB', self.version, self.token, self.id) + \
                    self.payload
        return data
floranet/lora/mac.py
class LinkCheckAns(MACCommand):
    """Used by the network server to respond to a LinkCheckReq command
    
    Attributes:
        length (int): Frame length 
        cid (int): Command identifier
        margin (int): an 8-bit unsigned integer in the range of 0..254
                      indicating the link margin in dB of the last
                      successfully received LinkCheckReq command.
        gwcnt (int): the number of gateways that successfully received
                     the last LinkCheckReq command
        
    """
    length = 3
    
    def __init__(self, margin=0, gwcnt=1):
        self.cid = LINKCHECKANS
        self.margin = margin
        self.gwcnt = gwcnt
    
    def encode(self):
        """Create a binary representation of LinkCheckReq object.
        
        Returns:
            String of packed data.
        
        """
        data = struct.pack('BBB', self.cid, self.margin, self.gwcnt)
        return data
floranet/imanager.py
class InterfaceManager(object):
    def __init__(self):
    def start(self, netserver):
    def getInterface(self, appinterface_id):
    def getAllInterfaces(self):
    def checkInterface(self, appinterface_id):
    def createInterface(self, interface):
    def updateInterface(self, interface):
    def deleteInterface(self, interface):
floranet/lora/mac.py
class JoinAcceptMessage(MACMessage):
    """A LoRa Join Accept message.
    
    The join accept message contains an
    application nonce of 3 octets (appnonce),
    3 octet a network identifier (netid), a 4
    octet device address (devaddr), a 1 octet
    delay between tx and rx (rxdelay) and
    an optional list of channel frequencies
    (cflist).
    
    Attributes:
        mhdr (MACHeader): MAC header
        appkey (int): Application key
        appnonce (int): Application nonce
        netid (int): Network identifer
        devaddr (int): Device address
        dlsettings (int): DLsettings field
        rxdelay (int): Delay between tx and rx
        cflist (list): List of channel frequencies
        mic (int): Message integrity code
    """
        
    def __init__(self, appkey, appnonce, netid, devaddr, dlsettings,
                 rxdelay, cflist=[]):
        """JoinAcceptMessage initialisation method.
        
        """
        self.mhdr = MACHeader(JOIN_ACCEPT, LORAWAN_R1)
        self.appkey = appkey
        self.appnonce = appnonce
        self.netid = netid
        self.devaddr = devaddr
        self.dlsettings = dlsettings
        self.rxdelay = rxdelay
        self.cflist = cflist
        self.mic = None

    def encode(self):
        """Create a binary representation of JoinAcceptMessage object.
        
        Returns:
            Packed JoinAccept message.
        """
        # Encoding Join-accept:
        # MAC Header
        # 3 bytes appnonce
        # 3 bytes netid 
        # 4 bytes devaddr
        # 1 byte dlsettings
        # 1 byte rxdelay
        # Optional cflist
        
        # Create the message
        header = self.mhdr.encode()
        msg =  intPackBytes(self.appnonce, 3, endian='little') + \
               intPackBytes(self.netid, 3, endian='little') + \
               struct.pack('<L', self.devaddr) + \
               struct.pack('B', self.dlsettings) + \
               struct.pack('B', self.rxdelay)
        # CFList is not used in a Join Accept message for US/AU bands
        if self.cflist:
            pass
        # Create the MIC over the entire message
        self.mic = aesEncrypt(intPackBytes(self.appkey, 16), header + msg,
                              mode='CMAC')[0:4]
        msg += self.mic
        # Add the header and encrypt the message using AES-128 decrypt
        data = header + aesDecrypt(intPackBytes(self.appkey, 16), msg)
        return data


Question: import struct
import imp
import os
import time
from twisted.internet import reactor, task, protocol
from twisted.internet.error import CannotListenError
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.enterprise import adbapi
from twistar.registry import Registry
from floranet.models.config import Config
from floranet.models.device import Device
from floranet.models.application import Application
from floranet.imanager import interfaceManager
from floranet.lora.wan import LoraWAN, GatewayMessage, Txpk
from floranet.lora.mac import (MACMessage, MACDataDownlinkMessage, JoinAcceptMessage,
      MACCommand, LinkCheckAns, LinkADRReq)
from floranet.lora.bands import AU915, US915, EU868
from floranet.lora.crypto import aesEncrypt
from floranet.web.webserver import WebServer
from floranet.util import txsleep, euiString, devaddrString, intPackBytes, intUnpackBytes
from floranet.log import log
            returnValue(None)
        
        # Find the set difference between the two lists, return lowest free address
        diff = self.otarange.difference(devaddrs)
        returnValue(diff.pop())

    @inlineCallbacks
    def _getActiveDevice(self, devaddr):
        """Searches active devices for the given devaddr.
        
        Args:
            devaddr (int): A 32 bit end device network address (DevAddr).
        
        Returns:
            A device object if successful, None otherwise.
        """
        # Search active device for devaddr
        device = yield Device.find(where=['devaddr = ?', devaddr], limit=1)
        returnValue(device)
        
    def _checkDuplicateMessage(self, message):
        """Checks for duplicate gateway messages.
        
        We check for duplicate messages that may have been sent from
        different gateways that heard the same LoRa PHY payload. The
        period to check is defined by the config parameter duplicateperod.
        Filtering uses the arrival time and MIC as cache entries -
        duplicate frames will have the same MIC.

        Args:
            message (MACMessage): LoRa MAC message object
        
        Returns:
            True if a duplicate is found, otherwise False.
        """
        # Search the cache for matching MIC entries within
        # self.config.duplicateperiod. Each entry in the
        # cache list is a tuple (mic, timestamp)
        if self.config.duplicateperiod == 0:
            return False
        mark = time.time()
        duplicate = next((True for e in self.message_cache if
                      e[0] == message.mic and 
                      e[1] + self.config.duplicateperiod > mark
                      ), False)
        if not duplicate:
            self.message_cache.append((message.mic, mark))
        return duplicate
    
    def _cleanMessageCache(self):
        """Removes stale entries from the message cache.
        
        This method is periodically called to limit the growth of
        the message_cache list.
        """
        mark = time.time()
        self.message_cache = [x for x in self.message_cache if not
                              (x[1] + self.config.duplicateperiod) < mark]

    def _manageMACCommandQueue(self):
        """Removes expired MAC Commands from the queue.
        
        This method is periodically called to limit the queue size.
        """
        mark = time.time()
        self.commands = [x for x in self.commands if not
                              (x[0] + self.config.macqueuelimit) < mark]
        
    @inlineCallbacks
    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:

Answer:                 yield txsleep(scheduled - current)