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])
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])
def test_devaddrString(self): eui = int('0x06100000', 16) expected = "06:10:00:00" result = util.devaddrString(eui) self.assertEqual(expected, result)
def test_devaddrString(self): """Test devaddrString""" expected = "0610.0000" eui = int('0x06100000', 16) result = util.devaddrString(eui) self.assertEqual(expected, result)
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 show(ctx): """show the system configuration. Args: ctx (Context): Click context """ # Form the url and payload server = ctx.obj['server'] payload = {'token': ctx.obj['token']} url = 'http://{}/api/v{}/system'.format(server, str(version)) # Make the request data = restRequest(server, url, 'get', payload, 200) if data is None: return # Single application c = data indent = ' ' * 10 click.echo('System: {} at {}'.format(c['name'], server)) click.echo('{}Network interface: {}'.format(indent, c['listen'])) click.echo('{}LoraWAN port: {}'.format(indent, c['port'])) click.echo('{}Web server port: {}'.format(indent, c['webport'])) click.echo('{}Frequency band: {}'.format(indent, c['freqband'])) click.echo('{}Network ID: 0x{}'.format(indent, intHexString(c['netid'], 3, sep=2))) click.echo('{}OTAA Address Range: 0x{} - 0x{}'.format( indent, devaddrString(c['otaastart']), devaddrString(c['otaaend']))) t = 'Yes' if c['adrenable'] else 'No' click.echo('{}ADR enabled: {}'.format(indent, t)) if c['adrenable']: click.echo('{}ADR margin: {} dB'.format(indent, c['adrmargin'])) click.echo('{}ADR cycle time: {} s'.format(indent, c['adrcycletime'])) click.echo('{}ADR message time: {} s'.format(indent, c['adrmessagetime'])) t = 'Yes' if c['fcrelaxed'] else 'No' click.echo('{}Relaxed frame count: {}'.format(indent, t)) t = 'Yes' if c['macqueueing'] else 'No' click.echo('{}MAC queueing: {}'.format(indent, t)) if c['macqueueing']: click.echo('{}MAC queue limit: {} s'.format(indent, c['macqueuelimit'])) return
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])
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))
def show(ctx, deveui): """show a device, or all devices. Args: ctx (Context): Click context deveui (str): Device EUI string """ if '.' in deveui: deveui = str(hexStringInt(str(deveui))) # Form the url and payload server = ctx.obj['server'] payload = {'token': ctx.obj['token']} url = 'http://{}/api/v{}'.format(server, version) url += '/devices' if deveui == 'all' else '/device/{}'.format(deveui) # Make the request data = restRequest(server, url, 'get', payload, 200) if data is None: return # Single device if deveui != 'all': d = data indent = ' ' * 10 enable = 'enabled' if d['enabled'] else 'disabled' drate = d['tx_datr'] if d['tx_datr'] else 'N/A' nwkid = hex(d['devaddr'] >> 25) snrav = '{0:.2f} dBm'.format(d['snr_average']) if d['snr_average'] else 'N/A' appname = d['appname'] if d['appname'] else 'N/A' lat = '{0:.4f}'.format(d['latitude']) if d['latitude'] else 'N/A' lon = '{0:.4f}'.format(d['longitude']) if d['longitude'] else 'N/A' activ = 'Over the air (OTAA)' if d['otaa'] else 'Personalization (ABP)' click.echo('Device EUI: ' + euiString(d['deveui'])) click.echo(indent + 'device address ' + devaddrString(d['devaddr']) + \ ' nwkID ' + nwkid + ' ' + enable) click.echo(indent + 'name: ' + d['name']) click.echo(indent + 'class: ' + d['devclass'].upper()) click.echo(indent + 'application EUI: ' + euiString(d['appeui'])) click.echo(indent + 'activation: ' + activ) click.echo(indent + 'appname: ' + appname) click.echo(indent + 'latitude: ' + lat) click.echo(indent + 'longitude: ' + lon) if not d['otaa']: click.echo(indent + 'appskey: ' + intHexString(d['appskey'], 16)) click.echo(indent + 'nwkskey: ' + intHexString(d['nwkskey'], 16)) click.echo(indent + 'data rate: ' + drate) click.echo(indent + 'average SNR: ' + snrav) return # All devices click.echo('{:15}'.format('Device') + \ '{:24}'.format('DeviceEUI') + \ '{:12}'.format('DevAddr') + \ '{:9}'.format('Enabled') + \ '{:5}'.format('Act') + \ '{:12}'.format('Average-SNR')) for i,d in data.iteritems(): enable = 'Yes' if d['enabled'] else 'No' active = 'OTA' if d['otaa'] else 'ABP' snravg = '{0:.2f} dBm'.format(d['snr_average']) if d['snr_average'] else 'N/A' click.echo('{:14.14}'.format(d['name']) + ' ' + \ '{:23}'.format(euiString(d['deveui'])) + ' ' + \ '{:12}'.format(devaddrString(d['devaddr'])) + \ '{:9}'.format(enable) + \ '{:5}'.format(active) + \ '{:12}'.format(snravg))
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])
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)