class SEWIO: def __init__(self, dev=DEFAULT_IP, recvport=DEFAULT_UDP, recvip=DEFAULT_GW): ''' Instantiates the KillerBee class for the Sewio Sniffer. @type dev: String @param dev: IP address (ex 10.10.10.2) @type recvport: Integer @param recvport: UDP port to listen for sniffed packets on. @type recvip: String @param recvip: IP address of the host, where the sniffer will send sniffed packets to. @return: None @rtype: None ''' self._channel = None self._modulation = 0 #unknown, will be set by change channel currently self.handle = None self.dev = dev #TODO The receive port and receive IP address are currently not # obtained from or verified against the Sewio sniffer, nor are they # used to change the settings on the sniffer. self.udp_recv_port = recvport self.udp_recv_ip = recvip self.__revision_num = getFirmwareVersion(self.dev) if self.__revision_num not in TESTED_FW_VERS: print( "Warning: Firmware revision {0} reported by the sniffer is not currently supported. Errors may occur and dev_sewio.py may need updating." .format(self.__revision_num)) self.handle = socket(AF_INET, SOCK_DGRAM) self.handle.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.handle.bind((self.udp_recv_ip, self.udp_recv_port)) self.__stream_open = False self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): '''Actually close the receiving UDP socket.''' self.sniffer_off() # turn sniffer off if it's currently running self.handle.close() # socket.close() self.handle = None def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information appropriate for the client and firmware version. @rtype: None @return: None ''' self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.FREQ_900, True) return # KillerBee expects the driver to implement this function def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [ self.dev, "Sewio Open-Sniffer v{0}".format(self.__revision_num), getMacAddr(self.dev) ] def __make_rest_call(self, path, fetch=True): ''' Wrapper to the sniffer's RESTful services. Reports URL/HTTP errors as KBInterfaceErrors. @rtype: If fetch==True, returns a String of the page. Otherwise, it returns True if an HTTP 200 code was received. ''' try: html = urllib2.urlopen("http://{0}/{1}".format(self.dev, path)) if fetch: return html.read() else: return (html.getcode() == 200) except Exception as e: raise KBInterfaceError( "Unable to preform a call to {0}/{1} (error: {2}).".format( self.dev, path, e)) def __sniffer_status(self): ''' Because the firmware accepts only toggle commands for sniffer on/off, we need to check what state it's in before taking action. It's also useful to make sure our command worked. @rtype: Boolean ''' html = self.__make_rest_call('') # Yup, we're going to have to steal the status out of a JavaScript variable res = re.search(r'<!--#pindex-->([A-Z]+),', html) if res is None: raise KBInterfaceError( "Unable to parse the sniffer's current status.") # RUNNING means it's sniffing, STOPPED means it's not. return (res.group(1) == "RUNNING") def __sync_status(self): ''' This updates the standard self.__stream_open variable based on the status as reported from asking the remote sniffer. ''' self.__stream_open = self.__sniffer_status() def __sniffer_channel(self): ''' Because the firmware accepts only toggle commands for sniffer on/off, we need to check what state it's in before taking action. It's also useful to make sure our command worked. @rtype: Boolean ''' html = self.__make_rest_call('') # Yup, we're going to have to steal the channel number out of a JavaScript variable # var values = removeSSItag('<!--#pindex-->RUNNING,00:1a:b6:00:0a:a4,10.10.10.2,0,High,0x0000,OFF,0,0').split(","); res = re.search(r'<!--#pindex-->[A-Z]+,[0-9a-f:]+,[0-9.]+,([0-9]+),', html) if res is None: raise KBInterfaceError( "Unable to parse the sniffer's current channel.") return int(res.group(1)) # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None): ''' Turns the sniffer on such that pnext() will start returning observed data. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) # Because the Sewio just toggles, we have to only hit the page # if we need to go from off to on state. self.__sync_status() if self.__stream_open == False: if channel != None: self.set_channel(channel) if not self.__make_rest_call('status.cgi?p=2', fetch=False): raise KBInterfaceError( "Error instructing sniffer to start capture.") #This makes sure the change actually happened self.__sync_status() if not self.__stream_open: raise KBInterfaceError("Sniffer did not turn on capture.") # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off. @rtype: None ''' # Because the Sewio just toggles, we have to only hit the page # if we need to go from on to off state. self.__sync_status() if self.__stream_open == True: if not self.__make_rest_call('status.cgi?p=2', fetch=False): raise KBInterfaceError( "Error instructing sniffer to stop capture.") #This makes sure the change actually happened self.__sync_status() if self.__stream_open: raise KBInterfaceError("Sniffer did not turn off capture.") @staticmethod def __get_default_modulation(channel): ''' Return the Sewio-specific integer representing the modulation which should be choosen to be IEEE 802.15.4 complinating for a given channel number. Captured values from sniffing Sewio web interface, unsure why these are done as such. Available modulations are listed at: http://www.sewio.net/open-sniffer/develop/http-rest-interface/ @rtype: Integer, or None if unable to determine modulation ''' if channel >= 11 or channel <= 26: return '0' #O-QPSK 250 kb/s 2.4GHz elif channel >= 1 or channel <= 10: return 'c' #O-QPSK 250 kb/s 915MHz elif channel >= 128 or channel <= 131: return '1c' #O-QPSK 250 kb/s 760MHz elif channel == 0: return '0' #O-QPSK 100 kb/s 868MHz else: return None #Error status # KillerBee expects the driver to implement this function def set_channel(self, channel): ''' Sets the radio interface to the specified channel, and the matching modulation setting. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if self.capabilities.is_valid_channel(channel): # We only need to update our channel if it doesn't match the currently reported one. curChannel = self.__sniffer_channel() if channel != curChannel: self.modulation = self.__get_default_modulation(channel) print("Setting to channel {0}, modulation {1}.".format( channel, self.modulation)) # Examples captured in fw v0.5 sniffing: # channel 6, 250 compliant: http://10.10.10.2/settings.cgi?chn=6&modul=c&rxsens=0 # channel 12, 250 compliant: http://10.10.10.2/settings.cgi?chn=12&modul=0&rxsens=0 # chinese 0, 780 MHz, 250 compliant: http://10.10.10.2/settings.cgi?chn=128&modul=1c&rxsens=0 # chinese 3, 786 MHz, 250 compliant: http://10.10.10.2/settings.cgi?chn=131&modul=1c&rxsens=0 #rxsens 0 is normal, 3 is high sensitivity to receive at self.__make_rest_call( "settings.cgi?chn={0}&modul={1}&rxsens=3".format( channel, self.modulation), fetch=False) self._channel = self.__sniffer_channel() else: self._channel = curChannel else: raise Exception( 'Invalid channel number ({0}) was provided'.format(channel)) # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0): ''' Not implemented. ''' self.capabilities.require(KBCapabilities.INJECT) @staticmethod def __parse_zep_v2(data): ''' Parse the packet from the ZigBee encapsulation protocol version 2/3 and return the fields desired for usage by pnext(). There is support here for some oddities specific to the Sewio implementation of ZEP and the packet, such as CC24xx format FCS headers being expected. The ZEP protocol parsing is mainly based on Wireshark source at: http://anonsvn.wireshark.org/wireshark/trunk/epan/dissectors/packet-zep.c * ZEP v2 Header will have the following format (if type=1/Data): * |Preamble|Version| Type |Channel ID|Device ID|CRC/LQI Mode|LQI Val|NTP Timestamp|Sequence#|Reserved|Length| * |2 bytes |1 byte |1 byte| 1 byte | 2 bytes | 1 byte |1 byte | 8 bytes | 4 bytes |10 bytes|1 byte| * ZEP v2 Header will have the following format (if type=2/Ack): * |Preamble|Version| Type |Sequence#| * |2 bytes |1 byte |1 byte| 4 bytes | #define ZEP_PREAMBLE "EX" #define ZEP_V2_HEADER_LEN 32 #define ZEP_V2_ACK_LEN 8 #define ZEP_V2_TYPE_DATA 1 #define ZEP_V2_TYPE_ACK 2 #define ZEP_LENGTH_MASK 0x7F ''' # Unpack constant part of ZEPv2 (preamble, version, zeptype) = unpack('<HBB', data[:4]) if preamble != 22597 or version < 2: # 'EX'==22597, and v3 is compat with v2 (I think??) raise Exception( "Can not parse provided data as ZEP due to incorrect preamble or unsupported version." ) if zeptype == 1: #data (ch, devid, crcmode, lqival, ntpsec, ntpnsec, seqnum, length) = unpack(">BHBBIII10xB", data[4:32]) #print "Data ZEP:", ch, devid, crcmode, lqival, ntpsec, ntpnsec, seqnum, length #We could convert the NTP timestamp received to system time, but the # Sewio firmware uses "relative timestamping" where it begins at 0 each time # the sniffer is started. Thus, it isn't that useful to us, so we just add the # time the packet is received at the host instead. #print "\tConverted time:", ntp_to_system_time(ntpsec, ntpnsec) recdtime = datetime.utcnow() #The LQI comes in ZEP, but the RSSI comes in the first byte of the FCS, # if the FCS was correct. If the byte is 0xb1, Wireshark appears to do 0xb1-256 = -79 dBm. # It appears that if CRC/LQI Mode field == 1, then checksum was bad, so the RSSI isn't # available, as the CRC is left in the packet. If it == 0, then the first byte of FCS is the RSSI. # From Wireshark: #define IEEE802154_CC24xx_CRC_OK 0x8000 #define IEEE802154_CC24xx_RSSI 0x00FF frame = data[32:] # A length vs len(frame) check is not used here but is an # additional way to verify that all is good (length == len(frame)). if crcmode == 0: validcrc = ((ord(data[-1]) & 0x80) == 0x80) rssi = ord(data[-2]) # We have to trust the sniffer that the FCS was OK, so we compute # what a good FCS should be and patch it back into the packet. frame = frame[:-2] + makeFCS(frame[:-2]) else: validcrc = False rssi = None return (frame, ch, validcrc, rssi, lqival, recdtime) elif zeptype == 2: #ack frame = data[8:] (seqnum) = unpack(">I", data[4:8]) recdtime = datetime.utcnow() validcrc = (frame[-2:] == makeFCS(frame[:-2])) return (frame, None, validcrc, None, None, recdtime) return None # KillerBee expects the driver to implement this function def pnext(self, timeout=100): ''' Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() #start sniffing # Use socket timeouts to implement the timeout self.handle.settimeout(timeout / 1000000.0) # it takes seconds frame = None donetime = datetime.utcnow() + timedelta(microseconds=timeout) while True: try: data, addr = self.handle.recvfrom(1024) except error_timeout: return None # Ensure it's data coming from the right place, for now we just # check the sending IP address. Ex: addr = ('10.10.10.2', 17754) if addr[0] != self.dev: continue # Dissect the UDP packet (frame, ch, validcrc, rssi, lqival, recdtime) = self.__parse_zep_v2(data) print "Valid CRC", validcrc, "LQI", lqival, "RSSI", rssi if frame == None or (ch is not None and ch != self._channel): #TODO this maybe should be an error condition, instead of ignored? print("ZEP parsing issue (bytes length={0}, channel={1}).". format(len(frame) if frame is not None else None, ch)) continue break if frame is None: return None #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = { 0: frame, 1: validcrc, 2: rssi, 'bytes': frame, 'validcrc': validcrc, 'rssi': rssi, 'dbm': None, 'location': None, 'datetime': recdtime } if rssi is not None: # Per note from Sewino team regarding encoding of RSSI value as 2's complement dBm values if rssi > 127: result['dbm'] = rssi - 256 else: result['dbm'] = rssi return result def jammer_on(self, channel=None): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM) def jammer_off(self, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM)
class APIMOTE: def __init__(self, dev, revision=DEFAULT_REVISION): ''' Instantiates the KillerBee class for the ApiMote platform running GoodFET firmware. @type dev: String @param dev: Serial device identifier (ex /dev/ttyUSB0) @type revision: Integer @param revision: The revision number for the ApiMote, which is used by the called GoodFET libraries to properly communicate with and configure the hardware. @return: None @rtype: None ''' self._channel = None self.handle = None self.dev = dev self.__revision_num = revision # Set enviroment variables for GoodFET code to use os.environ["platform"] = "apimote%d".format(self.__revision_num) os.environ["board"] = "apimote%d".format(self.__revision_num) self.handle = GoodFETCCSPI() self.handle.serInit(port=self.dev) self.handle.setup() # TODO can we verify here the revision number that was sent is correct? self.__stream_open = False self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): self.handle.serClose() self.handle = None def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information appropriate for GoodFETCCSPI client and firmware. @rtype: None @return: None ''' self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) self.capabilities.setcapab(KBCapabilities.INJECT, True) self.capabilities.setcapab(KBCapabilities.PHYJAM_REFLEX, True) self.capabilities.setcapab(KBCapabilities.SET_SYNC, True) return # KillerBee expects the driver to implement this function def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "GoodFET Apimote v%d".format(self.__revision_num), ""] # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) self.handle.RF_promiscuity(1); self.handle.RF_autocrc(0); if channel != None: self.set_channel(channel) self.handle.CC_RFST_RX(); #print "Sniffer started (listening as %010x on %i MHz)" % (self.handle.RF_getsmac(), self.handle.RF_getfreq()/10**6); self.__stream_open = True # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' #TODO actually have firmware stop sending us packets! self.__stream_open = False # KillerBee expects the driver to implement this function def set_channel(self, channel): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 11 or channel <= 26: self._channel = channel self.handle.RF_setchan(channel) else: raise Exception('Invalid channel') # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None: self.set_channel(channel) self.handle.RF_autocrc(1) #let radio add the CRC for pnum in range(0, count): gfready = [ord(x) for x in packet] #convert packet string to GoodFET expected integer format gfready.insert(0, len(gfready)+2) #add a length that leaves room for CRC self.handle.RF_txpacket(gfready) time.sleep(1) # KillerBee expects the driver to implement this function def pnext(self, timeout=100): ''' Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() #start sniffing packet = None; start = datetime.utcnow() while (packet is None and (start + timedelta(microseconds=timeout) > datetime.utcnow())): packet = self.handle.RF_rxpacket() rssi = self.handle.RF_getrssi() #TODO calibrate if packet is None: return None frame = packet[1:] if frame[-2:] == makeFCS(frame[:-2]): validcrc = True else: validcrc = False #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi, 'location':None} result['dbm'] = rssi - 45 #TODO tune specifically to the Apimote platform (does ext antenna need to different?) result['datetime'] = datetime.utcnow() return result def ping(self, da, panid, sa, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' raise Exception('Not yet implemented') def jammer_on(self, channel=None): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM_REFLEX) self.handle.RF_promiscuity(1) self.handle.RF_autocrc(0) if channel != None: self.set_channel(channel) self.handle.CC_RFST_RX() self.handle.RF_carrier() #constant carrier wave jamming #self.handle.RF_reflexjam() #reflexive jamming (advanced) def set_sync(self, sync=0xA70F): '''Set the register controlling the 802.15.4 PHY sync byte.''' self.capabilities.require(KBCapabilities.SET_SYNC) if (sync >> 16) > 0: raise Exception("Sync word (%x) must be 2-bytes or less." % sync) return self.handle.poke(CC2420_REG_SYNC, sync) def jammer_off(self, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')
class CC253x: USB_DIR_OUT = 0x40 USB_DIR_IN = 0xC0 USB_POWER_ON = 0xC5 USB_POWER_STATUS = 0xC6 USB_XFER_START = 0xD0 USB_XFER_STOP = 0xD1 USB_XFER_CHAN = 0xD2 USB_CC2530_DATA_EP = 0x82 USB_CC2531_DATA_EP = 0x83 VARIANT_CC2530 = 0 VARIANT_CC2531 = 1 def __init__(self, dev, bus, variant): #TODO deprecate bus param, and dev becomes a usb.core.Device object, not a string in pyUSB 1.x use """ Instantiates the KillerBee class for Zigduino running GoodFET firmware. @type dev: String @param dev: PyUSB device @return: None @rtype: None """ if variant == CC253x.VARIANT_CC2530: self._data_ep = CC253x.USB_CC2530_DATA_EP else: self._data_ep = CC253x.USB_CC2531_DATA_EP self._channel = None self._page = 0 self.dev = dev self.__stream_open = False self.capabilities = KBCapabilities() self.__set_capabilities() # Set default configuration self.dev.set_configuration() # get name from USB descriptor self.name = usb.util.get_string(self.dev, self.dev.iProduct) # Get wMaxPacketSize from the data endpoint for cfg in self.dev: for intf in cfg: for ep in intf: if ep.bEndpointAddress == self._data_ep: self._maxPacketSize = ep.wMaxPacketSize def close(self): if self.__stream_open == True: self.sniffer_off() pass def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): """ Sets the capability information appropriate for CC253x. @rtype: None @return: None """ self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) # KillerBee expects the driver to implement this function def get_dev_info(self): """ Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. """ # TODO Determine if there is a way to get a unique ID from the device return [self.name, "CC253x", ""] # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None, page=0): """ Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None """ self.capabilities.require(KBCapabilities.SNIFF) if channel != None: self.set_channel(channel, page) # Enable power in 802.15.4 radio self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_POWER_ON, wIndex = 4) while True: # check if powered up power_status = self.dev.ctrl_transfer(CC253x.USB_DIR_IN, CC253x.USB_POWER_STATUS, data_or_wLength = 1) if power_status[0] == 4: break time.sleep(0.1) # Set the channel in the hardware now the radio is powered up self._do_set_channel() # Start capture self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_START) self.__stream_open = True # KillerBee expects the driver to implement this function def sniffer_off(self): """ Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None """ if self.__stream_open == True: # TODO Here, and in other places, add error handling for ctrl_transfer failure self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_STOP) self.__stream_open = False def _do_set_channel(self): # Internal function to unconditionally set the channel in the hardware # For some CC253x dongles, this can only be done when the radio is powered up. self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_CHAN, wIndex = 0, data_or_wLength = [self._channel]) self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_CHAN, wIndex = 1, data_or_wLength = [0x00]) # KillerBee expects the driver to implement this function def set_channel(self, channel, page=0): """ Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None """ self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 11 or channel <= 26: self._channel = channel if self.__stream_open == True: self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_STOP) self._do_set_channel() self.dev.ctrl_transfer(CC253x.USB_DIR_OUT, CC253x.USB_XFER_START) else: raise Exception('Invalid channel') if page: raise Exception('SubGHz not supported') # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0, page=0): """ Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None """ raise Exception('Not yet implemented') # KillerBee expects the driver to implement this function def pnext(self, timeout=100): """ Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] """ if self.__stream_open == False: self.sniffer_on() #start sniffing ret = None framedata = [] explen = 0 # expected remaining packet length while True: pdata = None try: pdata = self.dev.read(self._data_ep, self._maxPacketSize, timeout=timeout) except usb.core.USBError as e: if e.errno != 110: #Operation timed out print("Error args: {}".format(e.args)) raise e #TODO error handling enhancements for USB 1.0 else: return None # Accumulate in 'framedata' until we have an entire frame for byteval in pdata: framedata.append(struct.pack("B", byteval)) if len(pdata) < 64: if len(pdata) < 2: #print "ERROR: Very short frame" return None framelen = ord(framedata[1]) if len(framedata) - 3 != framelen: #print "ERROR: Bad frame length: expected {0}, got {1}".format(framelen, len(framedata)) return None if framedata[0] != '\x00': #print "Not a capture frame:", framedata return None payloadlen = ord(framedata[7]) # Includes TI format FCS payload = framedata[8:] if len(payload) != payloadlen: # TODO: Log "ERROR: Bad payload length" return None # See TI Smart RF User Guide for usage of 'CC24XX' format FCS fields # in last two bytes of framedata. Note that we remove these before return of the frame. # RSSI is signed value, offset by 73 (see CC2530 data sheet for offset) rssi = struct.unpack("b", framedata[-2])[0] - 73 fcsx = ord(framedata[-1]) # validcrc is the bit 7 in fcsx validcrc = (fcsx & 0x80) == 0x80 # correlation value is bits 0-6 in fcsx correlation = fcsx & 0x7f ret = {1:validcrc, 2:rssi, 'validcrc':validcrc, 'rssi':rssi, 'lqi':correlation, 'dbm':rssi,'datetime':datetime.utcnow()} # Convert the framedata to a string for the return value, and replace the TI FCS with a real FCS # if the radio told us that the FCS had passed validation. if validcrc: ret[0] = ''.join(payload[:-2]) + makeFCS(payload[:-2]) else: ret[0] = ''.join(payload) ret['bytes'] = ret[0] return ret def jammer_on(self, channel=None, page=0): """ Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None """ raise Exception('Not yet implemented') def set_sync(self, sync=0xA7): """ Set the register controlling the 802.15.4 PHY sync byte. """ raise Exception('Not yet implemented') def jammer_off(self, channel=None, page=0): """ Not yet implemented. @return: None @rtype: None """ raise Exception('Not yet implemented')
class SL_NODETEST: def __init__(self, serialpath): ''' Instantiates the KillerBee class for Silabs Node Test @param serialpath: /dev/ttyACM* type serial port identifier @return: None @rtype: None ''' self._channel = None self._page = 0 self.handle = None self.dev = serialpath self.date = None self.lon, self.lat, self.alt = (None, None, None) self.__stream_open = False self.handle = serial.Serial(port=self.dev, baudrate=115200, \ timeout=.1, bytesize=8, parity='N', stopbits=1, xonxoff=0) self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): ''' Closes the serial port. After closing, must reinitialize class again before use. @return: None @rtype: None ''' self.handle.close() self.handle = None # KillerBee implements these, maybe it shouldn't and instead leave it to the driver as needed. # TODO deprecate these functions in favor of self.capabilities class functions def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information for Silabs Node Test @rtype: None @return: None ''' #NOTE frequencies will actually depend on radio daughterboard on the Silabs dev kit self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.FREQ_915, True) self.capabilities.setcapab(KBCapabilities.FREQ_863, True) self.capabilities.setcapab(KBCapabilities.FREQ_868, True) self.capabilities.setcapab(KBCapabilities.FREQ_870, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) return def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "Silabs Node Test", ""] def __send_cmd(self, cmdstr, arg=None, confirm=True, send_return=True): self.handle.flush() #time.sleep(1.5) #this delay seems crucial if arg != None: cmdstr += ' ' + arg self.handle.write(cmdstr) if send_return: self.handle.write('\r') #print "Sent %s" % (cmdstr) if confirm: ret = False for x in range(10): d = self.handle.readline().strip() #print 'got', d if d == '>': ret = True break else: ret = True if not ret: raise Exception('Could not send command %s' % cmdstr) def __dissect_pkt(self, packet): ''' internal routine to deconstruct serial text received from device packet will be in the format: "{{num} {oflo} {seq} {per} {err} {lqi} {rssi}{ed} {gain} {status} {time} {fp}{length}{payload}}" @type packet: String @param: packet: The raw packet to dissect @rtype: List @return: Returns None if packet is not in correct format. When a packet is correct, a list is returned, in the form [ String: Frame | Bool: Valid CRC | Int: Unscaled RSSI ] ''' data = packet[1:].replace('{', ' ').replace('}', ' ').split() # should be 12 fields + payload length + payload if not data or not len(data[13:]) == int(data[12], 16): print "Error parsing stream received from device (payload size error):", packet return None # payload is in the form e.g. "0x03 0x08 0xA3 0xFF 0xFF 0xFF 0xFF 0x07" so we need to convert to a string frame = '' for x in data[13:]: try: frame += chr(int(x, 16)) except: print "Error parsing stream received from device (invalid payload):", packet return None # Parse other useful fields try: rssi = int(data[6]) # sniffer doesn't give us the CRC so we must add it, but must be correct or we would not have received it validcrc = True frame += makeFCS(frame) except: print "Error parsing stream received from device (invalid rssi or FCS build error):", packet return None return [frame, validcrc, rssi] # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None, page=0): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) if channel != None or page: self.set_channel(channel, page) if not self.__stream_open: # make sure we get hex dump of RX packet self.__send_cmd("showPayload", "1") # start sniffer self.__send_cmd("rx", confirm=False) for x in range(10): d = self.handle.readline() #print 'got', d if '{payload}' in d: self.mode = MODE_SNIFF self.__stream_open = True break if not self.__stream_open: raise Exception('Could not start sniffer') # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' self.__send_cmd("e", send_return=False, confirm=False) for x in range(5): d = self.handle.readline().strip() #print 'got', d if "test end" in d: self.mode = MODE_NONE self.__stream_open = False break if self.__stream_open: raise Exception('Could not stop sniffer') # KillerBee expects the driver to implement this function def set_channel(self, channel, page=0): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) #subghz channels must be calculated to include page (3 LSB page + 5 LSB channel) # e.g. page 28 channel 15 == '100' + '01111' = '10001111' = 0x8F self._channel = channel if page: channel = ((page << 5) & 0xff) + (channel & 0x1f) self._page = page #TODO actually check that it responds correctly to the request self.__send_cmd("setchannel", "%02x" % channel) # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0, page=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None or page: self.set_channel(channel, page) # Append two bytes to be replaced with FCS by firmware. packet = ''.join([packet, "\x00\x00"]) raise Exception('Not yet implemented') # KillerBee expects the driver to implement this function #TODO I suspect that if you don't call this often enough while getting frames, the serial buffer may overflow. def pnext(self, timeout=100): ''' Returns packet data as a string, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None if timeout expires and no packet received or packet is corrupt. When a packet is received, a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if not self.__stream_open: self.sniffer_on() #start sniffing self.handle.timeout = timeout # Allow pySerial to handle timeout packet = self.handle.readline().strip() if packet == '': return None # Sense timeout case and return data = self.__dissect_pkt(packet) if not data: return None frame = data[0] validcrc = data[1] rssi = data[2] #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = { 0: frame, 1: validcrc, 2: rssi, 'bytes': frame, 'validcrc': validcrc, 'rssi': rssi } result[ 'dbm'] = None #TODO calculate dBm antenna signal based on RSSI formula result['datetime'] = datetime.utcnow( ) # TODO - see what time field in sniff is actually telling us result['location'] = (self.lon, self.lat, self.alt) return result def ping(self, da, panid, sa, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' raise Exception('Not yet implemented') def jammer_on(self, channel=None, page=0): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM) if channel != None or page: self.set_channel(channel, page) #TODO implement raise Exception('Not yet implemented') def jammer_off(self, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')
class SL_BEEHIVE: def __init__(self, serialpath): ''' Instantiates the KillerBee class for Silabs BEEHIVE @param serialpath: /dev/ttyACM* type serial port identifier @return: None @rtype: None ''' self._channel = None self._page = 0 self.handle = None self.dev = serialpath self.date = None self.lon, self.lat, self.alt = (None, None, None) self.__stream_open = False self.handle = serial.Serial(port=self.dev, baudrate=115200, \ timeout=.02, bytesize=8, parity='N', stopbits=1, xonxoff=0) self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): ''' Closes the serial port. After closing, must reinitialize class again before use. @return: None @rtype: None ''' self.handle.close() self.handle = None # KillerBee implements these, maybe it shouldn't and instead leave it to the driver as needed. # TODO deprecate these functions in favor of self.capabilities class functions def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information for Silabs Node Test @rtype: None @return: None ''' #NOTE frequencies will actually depend on radio daughterboard on the Silabs dev kit self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.FREQ_915, True) self.capabilities.setcapab(KBCapabilities.FREQ_863, True) self.capabilities.setcapab(KBCapabilities.FREQ_868, True) self.capabilities.setcapab(KBCapabilities.FREQ_870, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.INJECT, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) return def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "BeeHive SG", ""] def __send_cmd(self, cmdstr, arg=None, confirm= True, send_return= True, extra_delay= 0, initial_read= 3): # read any cruft #time.sleep(0.1) for x in range(initial_read): self.handle.readline() # some commands require us to be in idle, so do it always self.handle.write("rx 0\r") time.sleep(0.02) for x in range(3): self.handle.readline() if arg != None: cmdstr += ' ' + arg self.handle.write(cmdstr) if send_return: self.handle.write('\r') #time.sleep(0.1) #print "Sent %s" % (cmdstr) time.sleep(extra_delay) if confirm: ret= False for x in range(100): d= self.handle.readline().strip() #print 'got', d if d[-1:] == '>': ret= True break else: ret= True if not ret: raise Exception('Could not send command %s' % cmdstr) def __dissect_pkt(self, packet): ''' internal routine to deconstruct serial text received from device packet will be in the format: "{{(rxPacket)}{len:11}{timeUs:994524212}{crc:Pass}{rssi:-44}{lqi:210}{phy:0}{isAck:False}{syncWordId:0}{antenna:0}{payload: 0x0a 0x03 0x08 0xba 0xff 0xff }" ''' try: data = packet.replace('}','').split(':')[10].split() except: return None, None, None if not data: return None, None, None # payload is in the form e.g. "0x03 0x08 0xA3 0xFF 0xFF 0xFF 0xFF 0x07" so we need to convert to a string out = '' # first byte is length if len(data) != int(data[0], 16) + 1: return None, None, None for x in data[1:]: try: out += chr(int(x, 16)) except: return None, None, None try: crc = packet.replace('}','').split(':')[4].split('{')[0] == 'Pass' rssi = packet.replace('}','').split(':')[4].split('{')[0] except: return None, None, None return rssi, out, crc # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None, page=0): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) if channel != None or page: self.set_channel(channel, page) if not self.__stream_open: # make sure we get all packets self.__init_radio() # start sniffer self.__send_cmd("rx", "1", confirm= False) for x in range(5): d = self.handle.readline() #print 'got', d if 'Rx:Enabled' in d: self.mode = MODE_SNIFF self.__stream_open = True if not self.__stream_open: raise Exception('Could not start sniffer') # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' # reset timeout as sniffer has made it long self.handle.timeout= 0.2 self.__send_cmd("rx", "0", confirm= False, initial_read= 0) for x in range(3): d= self.handle.readline().strip() if "Rx:Disabled" in d: self.mode = MODE_NONE self.__stream_open = False self.handle.readline() if self.__stream_open: raise Exception('Could not stop sniffer') def __init_radio(self): self.__send_cmd("reset", extra_delay = 0.6) self.__send_cmd("rx", "0") self.set_channel(self._channel, self._page) self.__send_cmd("BeeHiveMode", "1") self.__send_cmd("BeeHiveOptions", "1 1 1 1") # KillerBee expects the driver to implement this function def set_channel(self, channel, page=0): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) #subghz channels must be calculated to include page (3 LSB page + 5 LSB channel) # e.g. page 28 channel 15 == '100' + '01111' = '10001111' = 0x8F self._channel = channel self._page = page if page: channel = ((page << 5) & 0xff) + (channel & 0x1f) if page == 31: self.__send_cmd("915MHz", extra_delay= 0.3) else: self.__send_cmd("863MHz", extra_delay= 0.3) else: self.__send_cmd("2p4GHz") #TODO actually check that it responds correctly to the request self.__send_cmd("setchannel", "%d" % channel) # KillerBee expects the driver to implement this function # note that BEEHIVE never generates FCS - you must provide it as part of the packet! def inject(self, packet, channel=None, count=1, delay=0, page=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, with FCS. @type channel: Integer @param channel: Sets the channel, optional @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 127: raise Exception('Packet too long') if channel != None or page: self.set_channel(channel, page) # self.__send_cmd("setTxLength", "%d" % len(packet)) maxp = 118 # we can send max 256 bytes over the wire so we must split large packets due to hex doubling the size if len(packet) > maxp: tosend = maxp else: tosend = len(packet) self.__send_cmd("setTxPayload", "00 %02x%s" % ((len(packet)), packet[:tosend].encode('hex'))) if len(packet) > maxp: self.__send_cmd("setTxPayload", "%d %s" % (tosend + 1, packet[tosend:].encode('hex'))) #print 'sending', len(packet), 'bytes:', packet.encode('hex') for pnum in range(0, count): self.__send_cmd("tx", "1", confirm= False) time.sleep(delay) # KillerBee expects the driver to implement this function #TODO I suspect that if you don't call this often enough while getting frames, the serial buffer may overflow. def pnext(self, timeout=1): ''' Returns packet data as a string, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if not self.__stream_open: self.sniffer_on() #start sniffing self.handle.timeout=timeout # Allow pySerial to handle timeout packet = self.handle.readline().strip() #print 'reading' if packet == '': return None # Sense timeout case and return #print packet rssi, frame, validcrc = self.__dissect_pkt(packet) if not frame: print "Error parsing stream received from device:", packet # Parse received data as <rssi>!<time>!<packtlen>!<frame> try: rssi = int(rssi) except: print "Error parsing stream received from device:", packet return None #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi} result['dbm'] = None #TODO calculate dBm antenna signal based on RSSI formula result['datetime'] = datetime.utcnow() # TODO - see what time field in sniff is actually telling us result['location'] = (self.lon, self.lat, self.alt) return result def ping(self, da, panid, sa, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' raise Exception('Not yet implemented') def jammer_on(self, channel=None, page=0): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM) if channel != None or page: self.set_channel(channel, page) #TODO implement raise Exception('Not yet implemented') def jammer_off(self, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')
class FREAKDUINO: def __init__(self, serialpath): ''' Instantiates the KillerBee class for our sketch running on ChibiArduino on Freakduino hardware. @param serialpath: /dev/ttyUSB* type serial port identifier @return: None @rtype: None ''' self._channel = None self._page = 0 self.handle = None self.dev = serialpath self.date = None self.lon, self.lat, self.alt = (None, None, None) self.handle = serial.Serial(port=self.dev, baudrate=57600, \ timeout=1, bytesize=8, parity='N', stopbits=1, xonxoff=0) self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): ''' Closes the serial port. After closing, must reinitialize class again before use. @return: None @rtype: None ''' self.handle.close() self.handle = None # KillerBee implements these, maybe it shouldn't and instead leave it to the driver as needed. # TODO deprecate these functions in favor of self.capabilities class functions def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information for Freakdruino device based on the currently loaded sketch. @rtype: None @return: None ''' #TODO have it set based on what the sketch says it can support self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) return def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "Dartmouth Freakduino", ""] def __send_cmd(self, cmdstr, arg=None): self.handle.flush() time.sleep(1.5) #this delay seems crucial self.handle.write("S") self.handle.write(cmdstr) if arg != None: self.handle.write(arg) self.handle.write('\r') #print "Sent S%s" % (cmdstr) self.handle.flush() # Deprecated due to unreliability def __serial_cmd(self, cmdstr, arg=None): ''' Sends a command over the self.conn serial connection. Ex: If provided cmdstr = "C!N" it will send "SC!N", telling the device to turn on sniffing ("N"), and it expects to receive a confirmation back "&C!N" to confirm success. ''' print "Flushing out of buffer:", self.handle.inWaiting() self.handle.flushInput() if len(cmdstr) > 3: raise Exception("Command string is less than minimum length (S%s)." % cmdstr) self.__send_cmd(cmdstr, arg) # TODO ugly and unreliable: # This should just wait for a & and then parse things after it, # however it seems sometimes you have to resend the command or something. print "Line:", self.handle.readline(eol='&') counter = 0 char = self.handle.read() while (char != '&'): print self.handle.inWaiting(), "Waiting...", char time.sleep(0.01) if (counter > 8): self.__send_cmd(cmdstr, arg) counter = 0 print "Resend Response Line:", self.handle.readline(eol='&') else: counter += 1 char = self.handle.read() response = '' for i in range(3): response += self.handle.read() if response == cmdstr[:3]: print "Got a response:", response, "matches", cmdstr return True else: print "Invalid response:", response, cmdstr[:3] return False # Send the command for the Dartmouth-mod Freakduino to dump data logged in EEPROM def eeprom_dump(self): self.__send_cmd("C!D") # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None, page=0): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) if channel != None: self.set_channel(channel, page) #TODO implement mode change to start sniffer sending packets to us self.__send_cmd("C!N") self.mode = MODE_SNIFF self.__stream_open = True # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' self.__send_cmd("C!F") self.mode = MODE_NONE self.__stream_open = False # KillerBee expects the driver to implement this function def set_channel(self, channel, page=0): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 10 or channel <= 26: self._channel = channel #TODO actually check that it responds correctly to the request self.__send_cmd("C!C %d" % channel) else: raise Exception('Invalid channel') if page: raise Exception('SubGHz not supported') # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0, page=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None: self.set_channel(channel, page) # Append two bytes to be replaced with FCS by firmware. packet = ''.join([packet, "\x00\x00"]) for pnum in range(0, count): raise Exception('Not yet implemented') # Format for packet is opcode CMD_INJECT_FRAME, one-byte length, packet data #TODO RZ_USB_COMMAND_EP, struct.pack("B", RZ_CMD_INJECT_FRAME) + struct.pack("B", len(packet)) + packet) time.sleep(delay) # KillerBee expects the driver to implement this function #TODO I suspect that if you don't call this often enough while getting frames, the serial buffer may overflow. def pnext(self, timeout=100): ''' Returns packet data as a string, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() #start sniffing return self.pnext_rec(timeout) # Bulk of pnext implementation, but does not ensure the sniffer is on first, thus usable for EEPROM reading def pnext_rec(self, timeout=100): pdata = '' ldata = '' self.handle.timeout=timeout # Allow pySerial to handle timeout startChar = self.handle.read() if startChar == None: return None # Sense timeout case and return # Listens for serial message of general format R!<data>; if startChar == "R": # Get packet data if self.handle.read() == "!": x = self.handle.read() while (x != ";"): pdata += x x = self.handle.read() if startChar == "L": # Get location data if self.handle.read() == "!": x = self.handle.read() while (x != ";"): ldata += x x = self.handle.read() if startChar == "[": # Sense when done reading from EEPROM if self.handle.read(40) == "{[ DONE READING BACK ALL LOGGED DATA ]}]": raise StopIteration("All Data Read") # If location received, update our local variables: if ldata != None and ldata != '': self.processLocationUpdate(ldata) if pdata == None or pdata == '': return None # Parse received data as <rssi>!<time>!<packtlen>!<frame> data = pdata.split("!", 3) try: rssi = ord(data[0]) frame = data[3] if frame[-2:] == makeFCS(frame[:-2]): validcrc = True else: validcrc = False except: print "Error parsing stream received from device:", pdata, data return None #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi} result['dbm'] = None #TODO calculate dBm antenna signal based on RSSI formula result['datetime'] = self.getCaptureDateTime(data) result['location'] = (self.lon, self.lat, self.alt) return result def getCaptureDateTime(self, data): try: timestr = "%08d" % (struct.unpack('L', data[1])[0]) #in format hhmmsscc time = dttime(int(timestr[:2]), int(timestr[2:4]), int(timestr[4:6]), int(timestr[6:])) except: print "Issue with time format:", timestr, data time = None if self.date == None: self.date = date.utcnow().date() if time == None or time == dttime.min: time = (datetime.utcnow()).time() #TODO address timezones by going to UTC everywhere return datetime.combine(self.date, time) def processLocationUpdate(self, ldata): ''' Take a location string passed from the device and update the driver's internal state of last received location. Format of ldata: longlatialtidate ''' self.lon = struct.unpack('l', ldata[0:4])[0] self.lat = struct.unpack('l', ldata[4:8])[0] self.alt = struct.unpack('l', ldata[8:12])[0] date = str(struct.unpack('L', ldata[12:16])[0]) self.date = datetime.date(date[-2:], date[-4:-2], date[:-4]) #TODO parse data formats (lon=-7228745 lat=4370648 alt=3800 age=63 date=70111 time=312530) print self.lon, self.lat, self.alt, self.date def ping(self, da, panid, sa, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' raise Exception('Not yet implemented') def jammer_on(self, channel=None, page=0): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM) if self.__cmdmode != RZ_CMD_MODE_AC: self._set_mode(RZ_CMD_MODE_AC) if channel != None: self.set_channel(channel, page) #TODO implement raise Exception('Not yet implemented') def jammer_off(self, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')
class FREAKDUINO: def __init__(self, serialpath): ''' Instantiates the KillerBee class for our sketch running on ChibiArduino on Freakduino hardware. @param serialpath: /dev/ttyUSB* type serial port identifier @return: None @rtype: None ''' self._channel = None self.handle = None self.dev = serialpath self.date = None self.lon, self.lat, self.alt = (None, None, None) self.handle = serial.Serial(port=self.dev, baudrate=57600, \ timeout=1, bytesize=8, parity='N', stopbits=1, xonxoff=0) self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): ''' Closes the serial port. After closing, must reinitialize class again before use. @return: None @rtype: None ''' self.handle.close() self.handle = None # KillerBee implements these, maybe it shouldn't and instead leave it to the driver as needed. # TODO deprecate these functions in favor of self.capabilities class functions def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information for Freakdruino device based on the currently loaded sketch. @rtype: None @return: None ''' #TODO have it set based on what the sketch says it can support self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) return def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "Dartmouth Freakduino", ""] def __send_cmd(self, cmdstr, arg=None): self.handle.flush() time.sleep(1.5) #this delay seems crucial self.handle.write("S") self.handle.write(cmdstr) if arg != None: self.handle.write(arg) self.handle.write('\r') #print "Sent S%s" % (cmdstr) self.handle.flush() # Deprecated due to unreliability def __serial_cmd(self, cmdstr, arg=None): ''' Sends a command over the self.conn serial connection. Ex: If provided cmdstr = "C!N" it will send "SC!N", telling the device to turn on sniffing ("N"), and it expects to receive a confirmation back "&C!N" to confirm success. ''' print "Flushing out of buffer:", self.handle.inWaiting() self.handle.flushInput() if len(cmdstr) > 3: raise Exception("Command string is less than minimum length (S%s)." % cmdstr) self.__send_cmd(cmdstr, arg) # TODO ugly and unreliable: # This should just wait for a & and then parse things after it, # however it seems sometimes you have to resend the command or something. print "Line:", self.handle.readline(eol='&') counter = 0 char = self.handle.read() while (char != '&'): print self.handle.inWaiting(), "Waiting...", char time.sleep(0.01) if (counter > 8): self.__send_cmd(cmdstr, arg) counter = 0 print "Resend Response Line:", self.handle.readline(eol='&') else: counter += 1 char = self.handle.read() response = '' for i in range(3): response += self.handle.read() if response == cmdstr[:3]: print "Got a response:", response, "matches", cmdstr return True else: print "Invalid response:", response, cmdstr[:3] return False # Send the command for the Dartmouth-mod Freakduino to dump data logged in EEPROM def eeprom_dump(self): self.__send_cmd("C!D") # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) if channel != None: self.set_channel(channel) #TODO implement mode change to start sniffer sending packets to us self.__send_cmd("C!N") self.mode = MODE_SNIFF self.__stream_open = True # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' self.__send_cmd("C!F") self.mode = MODE_NONE self.__stream_open = False # KillerBee expects the driver to implement this function def set_channel(self, channel): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 10 or channel <= 26: self._channel = channel #TODO actually check that it responds correctly to the request self.__send_cmd("C!C %d" % channel) else: raise Exception('Invalid channel') # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None: self.set_channel(channel) # Append two bytes to be replaced with FCS by firmware. packet = ''.join([packet, "\x00\x00"]) for pnum in range(0, count): raise Exception('Not yet implemented') # Format for packet is opcode CMD_INJECT_FRAME, one-byte length, packet data #TODO RZ_USB_COMMAND_EP, struct.pack("B", RZ_CMD_INJECT_FRAME) + struct.pack("B", len(packet)) + packet) time.sleep(delay) # KillerBee expects the driver to implement this function #TODO I suspect that if you don't call this often enough while getting frames, the serial buffer may overflow. def pnext(self, timeout=100): ''' Returns packet data as a string, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() #start sniffing return self.pnext_rec(timeout) # Bulk of pnext implementation, but does not ensure the sniffer is on first, thus usable for EEPROM reading def pnext_rec(self, timeout=100): pdata = '' ldata = '' self.handle.timeout=timeout # Allow pySerial to handle timeout startChar = self.handle.read() if startChar == None: return None # Sense timeout case and return # Listens for serial message of general format R!<data>; if startChar == "R": # Get packet data if self.handle.read() == "!": x = self.handle.read() while (x != ";"): pdata += x x = self.handle.read() if startChar == "L": # Get location data if self.handle.read() == "!": x = self.handle.read() while (x != ";"): ldata += x x = self.handle.read() if startChar == "[": # Sense when done reading from EEPROM if self.handle.read(40) == "{[ DONE READING BACK ALL LOGGED DATA ]}]": raise StopIteration("All Data Read") # If location received, update our local variables: if ldata != None and ldata != '': self.processLocationUpdate(ldata) if pdata == None or pdata == '': return None # Parse received data as <rssi>!<time>!<packtlen>!<frame> data = pdata.split("!", 3) try: rssi = ord(data[0]) frame = data[3] if frame[-2:] == makeFCS(frame[:-2]): validcrc = True else: validcrc = False except: print "Error parsing stream received from device:", pdata, data return None #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi} result['dbm'] = None #TODO calculate dBm antenna signal based on RSSI formula result['datetime'] = self.getCaptureDateTime(data) result['location'] = (self.lon, self.lat, self.alt) return result def getCaptureDateTime(self, data): try: timestr = "%08d" % (struct.unpack('L', data[1])[0]) #in format hhmmsscc time = dttime(int(timestr[:2]), int(timestr[2:4]), int(timestr[4:6]), int(timestr[6:])) except: print "Issue with time format:", timestr, data time = None if self.date == None: self.date = date.today() if time == None or time == dttime.min: time = (datetime.now()).time() #TODO address timezones by going to UTC everywhere return datetime.combine(self.date, time) def processLocationUpdate(self, ldata): ''' Take a location string passed from the device and update the driver's internal state of last received location. Format of ldata: longlatialtidate ''' self.lon = struct.unpack('l', ldata[0:4])[0] self.lat = struct.unpack('l', ldata[4:8])[0] self.alt = struct.unpack('l', ldata[8:12])[0] date = str(struct.unpack('L', ldata[12:16])[0]) self.date = datetime.date(date[-2:], date[-4:-2], date[:-4]) #TODO parse data formats (lon=-7228745 lat=4370648 alt=3800 age=63 date=70111 time=312530) print self.lon, self.lat, self.alt, self.date def ping(self, da, panid, sa, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' raise Exception('Not yet implemented') def jammer_on(self, channel=None): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM) if self.__cmdmode != RZ_CMD_MODE_AC: self._set_mode(RZ_CMD_MODE_AC) if channel != None: self.set_channel(channel) #TODO implement raise Exception('Not yet implemented') def jammer_off(self, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')
class SEWIO: def __init__(self, dev=DEFAULT_IP, recvport=DEFAULT_UDP, recvip=DEFAULT_GW): ''' Instantiates the KillerBee class for the Sewio Sniffer. @type dev: String @param dev: IP address (ex 10.10.10.2) @type recvport: Integer @param recvport: UDP port to listen for sniffed packets on. @type recvip: String @param recvip: IP address of the host, where the sniffer will send sniffed packets to. @return: None @rtype: None ''' self._channel = None self._modulation = 0 #unknown, will be set by change channel currently self.handle = None self.dev = dev #TODO The receive port and receive IP address are currently not # obtained from or verified against the Sewio sniffer, nor are they # used to change the settings on the sniffer. self.udp_recv_port = recvport self.udp_recv_ip = recvip self.__revision_num = getFirmwareVersion(self.dev) if self.__revision_num not in TESTED_FW_VERS: print("Warning: Firmware revision {0} reported by the sniffer is not currently supported. Errors may occur and dev_sewio.py may need updating.".format(self.__revision_num)) self.handle = socket(AF_INET, SOCK_DGRAM) self.handle.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) self.handle.bind((self.udp_recv_ip, self.udp_recv_port)) self.__stream_open = False self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): '''Actually close the receiving UDP socket.''' self.sniffer_off() # turn sniffer off if it's currently running self.handle.close() # socket.close() self.handle = None def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information appropriate for the client and firmware version. @rtype: None @return: None ''' self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.FREQ_900, True) self.capabilities.setcapab(KBCapabilities.INJECT, True) #Added by Soteria return # KillerBee expects the driver to implement this function def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "Sewio Open-Sniffer v{0}".format(self.__revision_num), getMacAddr(self.dev)] def __make_rest_call(self, path, fetch=True): ''' Wrapper to the sniffer's RESTful services. Reports URL/HTTP errors as KBInterfaceErrors. @rtype: If fetch==True, returns a String of the page. Otherwise, it returns True if an HTTP 200 code was received. ''' try: html = urllib2.urlopen("http://{0}/{1}".format(self.dev, path)) if fetch: return html.read() else: return (html.getcode() == 200) except Exception as e: raise KBInterfaceError("Unable to preform a call to {0}/{1} (error: {2}).".format(self.dev, path, e)) def __sniffer_status(self): ''' Because the firmware accepts only toggle commands for sniffer on/off, we need to check what state it's in before taking action. It's also useful to make sure our command worked. @rtype: Boolean ''' html = self.__make_rest_call('') # Yup, we're going to have to steal the status out of a JavaScript variable res = re.search(r'<!--#pindex-->([A-Z]+),', html) if res is None: raise KBInterfaceError("Unable to parse the sniffer's current status.") # RUNNING means it's sniffing, STOPPED means it's not. return (res.group(1) == "RUNNING") def __sync_status(self): ''' This updates the standard self.__stream_open variable based on the status as reported from asking the remote sniffer. ''' self.__stream_open = self.__sniffer_status() def __sniffer_channel(self): ''' Because the firmware accepts only toggle commands for sniffer on/off, we need to check what state it's in before taking action. It's also useful to make sure our command worked. @rtype: Boolean ''' html = self.__make_rest_call('') # Yup, we're going to have to steal the channel number out of a JavaScript variable # var values = removeSSItag('<!--#pindex-->RUNNING,00:1a:b6:00:0a:a4,10.10.10.2,0,High,0x0000,OFF,0,0').split(","); res = re.search(r'<!--#pindex-->[A-Z]+,[0-9a-f:]+,[0-9.]+,([0-9]+),', html) if res is None: raise KBInterfaceError("Unable to parse the sniffer's current channel.") return int(res.group(1)) # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None): ''' Turns the sniffer on such that pnext() will start returning observed data. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) # Because the Sewio just toggles, we have to only hit the page # if we need to go from off to on state. self.__sync_status() if self.__stream_open == False: if channel != None: self.set_channel(channel) if not self.__make_rest_call('status.cgi?p=2', fetch=False): raise KBInterfaceError("Error instructing sniffer to start capture.") #This makes sure the change actually happened self.__sync_status() if not self.__stream_open: raise KBInterfaceError("Sniffer did not turn on capture.") # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off. @rtype: None ''' # Because the Sewio just toggles, we have to only hit the page # if we need to go from on to off state. self.__sync_status() if self.__stream_open == True: if not self.__make_rest_call('status.cgi?p=2', fetch=False): raise KBInterfaceError("Error instructing sniffer to stop capture.") #This makes sure the change actually happened self.__sync_status() if self.__stream_open: raise KBInterfaceError("Sniffer did not turn off capture.") @staticmethod def __get_default_modulation(channel): ''' Return the Sewio-specific integer representing the modulation which should be choosen to be IEEE 802.15.4 complinating for a given channel number. Captured values from sniffing Sewio web interface, unsure why these are done as such. Available modulations are listed at: http://www.sewio.net/open-sniffer/develop/http-rest-interface/ @rtype: Integer, or None if unable to determine modulation ''' if channel >= 11 or channel <= 26: return '0' #O-QPSK 250 kb/s 2.4GHz elif channel >= 1 or channel <= 10: return 'c' #O-QPSK 250 kb/s 915MHz elif channel >= 128 or channel <= 131: return '1c' #O-QPSK 250 kb/s 760MHz elif channel == 0: return '0' #O-QPSK 100 kb/s 868MHz else: return None #Error status # KillerBee expects the driver to implement this function def set_channel(self, channel): ''' Sets the radio interface to the specified channel, and the matching modulation setting. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if self.capabilities.is_valid_channel(channel): # We only need to update our channel if it doesn't match the currently reported one. curChannel = self.__sniffer_channel() if channel != curChannel: self.modulation = self.__get_default_modulation(channel) print("Setting to channel {0}, modulation {1}.".format(channel, self.modulation)) # Examples captured in fw v0.5 sniffing: # channel 6, 250 compliant: http://10.10.10.2/settings.cgi?chn=6&modul=c&rxsens=0 # channel 12, 250 compliant: http://10.10.10.2/settings.cgi?chn=12&modul=0&rxsens=0 # chinese 0, 780 MHz, 250 compliant: http://10.10.10.2/settings.cgi?chn=128&modul=1c&rxsens=0 # chinese 3, 786 MHz, 250 compliant: http://10.10.10.2/settings.cgi?chn=131&modul=1c&rxsens=0 #rxsens 0 is normal, 3 is high sensitivity to receive at self.__make_rest_call("settings.cgi?chn={0}&modul={1}&rxsens=3".format(channel, self.modulation), fetch=False) self._channel = self.__sniffer_channel() else: self._channel = curChannel else: raise Exception('Invalid channel number ({0}) was provided'.format(channel)) # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0): ''' Implemented by soteria :-) ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None: self.set_channel(channel) self.modulation = self.__get_default_modulation(channel) # Transmitted power level. # Default value: 0 (3.0 dBm) txlevel = 0 # Sniff after injection RxEnable = 1 RepeatNumber = 1 timeSpace = 1 autoCorrect = 1 correctPacket = packet.encode("hex") # import PrintHelper # correctPacket += PrintHelper.STATIC_DATA.LOW_DIM_VALUE packetLength = len(correctPacket)/ 2 print "Packet length = " + str(packetLength) # Turn on sniffer (so we can continue to sniff after the injection) self.sniffer_on(channel=channel) for pnum in range(0, count): #Inject the packet formatedcall = "inject.cgi?chn={0}&modul={1}&txlevel={2}&rxen={3}&nrepeat={4}&tspace={5}&autocrc={6}&spayload={7}&len={8}".format(channel,self.modulation,txlevel,RxEnable,RepeatNumber,timeSpace,autoCorrect,correctPacket,packetLength) print formatedcall self.__make_rest_call(formatedcall, fetch=True) time.sleep(delay) @staticmethod def __parse_zep_v2(data): ''' Parse the packet from the ZigBee encapsulation protocol version 2/3 and return the fields desired for usage by pnext(). There is support here for some oddities specific to the Sewio implementation of ZEP and the packet, such as CC24xx format FCS headers being expected. The ZEP protocol parsing is mainly based on Wireshark source at: http://anonsvn.wireshark.org/wireshark/trunk/epan/dissectors/packet-zep.c * ZEP v2 Header will have the following format (if type=1/Data): * |Preamble|Version| Type |Channel ID|Device ID|CRC/LQI Mode|LQI Val|NTP Timestamp|Sequence#|Reserved|Length| * |2 bytes |1 byte |1 byte| 1 byte | 2 bytes | 1 byte |1 byte | 8 bytes | 4 bytes |10 bytes|1 byte| * ZEP v2 Header will have the following format (if type=2/Ack): * |Preamble|Version| Type |Sequence#| * |2 bytes |1 byte |1 byte| 4 bytes | #define ZEP_PREAMBLE "EX" #define ZEP_V2_HEADER_LEN 32 #define ZEP_V2_ACK_LEN 8 #define ZEP_V2_TYPE_DATA 1 #define ZEP_V2_TYPE_ACK 2 #define ZEP_LENGTH_MASK 0x7F ''' # Unpack constant part of ZEPv2 (preamble, version, zeptype) = unpack('<HBB', data[:4]) if preamble != 22597 or version < 2: # 'EX'==22597, and v3 is compat with v2 (I think??) raise Exception("Can not parse provided data as ZEP due to incorrect preamble or unsupported version.") if zeptype == 1: #data (ch, devid, crcmode, lqival, ntpsec, ntpnsec, seqnum, length) = unpack(">BHBBIII10xB", data[4:32]) #print "Data ZEP:", ch, devid, crcmode, lqival, ntpsec, ntpnsec, seqnum, length #We could convert the NTP timestamp received to system time, but the # Sewio firmware uses "relative timestamping" where it begins at 0 each time # the sniffer is started. Thus, it isn't that useful to us, so we just add the # time the packet is received at the host instead. #print "\tConverted time:", ntp_to_system_time(ntpsec, ntpnsec) recdtime = datetime.utcnow() #The LQI comes in ZEP, but the RSSI comes in the first byte of the FCS, # if the FCS was correct. If the byte is 0xb1, Wireshark appears to do 0xb1-256 = -79 dBm. # It appears that if CRC/LQI Mode field == 1, then checksum was bad, so the RSSI isn't # available, as the CRC is left in the packet. If it == 0, then the first byte of FCS is the RSSI. # From Wireshark: #define IEEE802154_CC24xx_CRC_OK 0x8000 #define IEEE802154_CC24xx_RSSI 0x00FF frame = data[32:] # A length vs len(frame) check is not used here but is an # additional way to verify that all is good (length == len(frame)). if crcmode == 0: validcrc = ((ord(data[-1]) & 0x80) == 0x80) rssi = ord(data[-2]) # We have to trust the sniffer that the FCS was OK, so we compute # what a good FCS should be and patch it back into the packet. frame = frame[:-2] + makeFCS(frame[:-2]) else: validcrc = False rssi = None return (frame, ch, validcrc, rssi, lqival, recdtime) elif zeptype == 2: #ack frame = data[8:] (seqnum) = unpack(">I", data[4:8]) recdtime = datetime.utcnow() validcrc = (frame[-2:] == makeFCS(frame[:-2])) return (frame, None, validcrc, None, None, recdtime) return None # KillerBee expects the driver to implement this function def pnext(self, timeout=100): ''' Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() #start sniffing # Use socket timeouts to implement the timeout self.handle.settimeout(timeout / 1000000.0) # it takes seconds frame = None donetime = datetime.utcnow() + timedelta(microseconds=timeout) while True: try: data, addr = self.handle.recvfrom(1024) except error_timeout: return None # Ensure it's data coming from the right place, for now we just # check the sending IP address. Ex: addr = ('10.10.10.2', 17754) if addr[0] != self.dev: continue # Dissect the UDP packet (frame, ch, validcrc, rssi, lqival, recdtime) = self.__parse_zep_v2(data) print "Valid CRC", validcrc, "LQI", lqival, "RSSI", rssi if frame == None or (ch is not None and ch != self._channel): #TODO this maybe should be an error condition, instead of ignored? print("ZEP parsing issue (bytes length={0}, channel={1}).".format(len(frame) if frame is not None else None, ch)) continue break if frame is None: return None #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi, 'dbm':None, 'location':None, 'datetime':recdtime} if rssi is not None: # Per note from Sewino team regarding encoding of RSSI value as 2's complement dBm values if rssi > 127: result['dbm'] = rssi - 256 else: result['dbm'] = rssi return result def jammer_on(self, channel=None): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM) def jammer_off(self, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM)
class APIMOTE: def __init__(self, dev, revision=DEFAULT_REVISION): ''' Instantiates the KillerBee class for the ApiMote platform running GoodFET firmware. @type dev: String @param dev: Serial device identifier (ex /dev/ttyUSB0) @type revision: Integer @param revision: The revision number for the ApiMote, which is used by the called GoodFET libraries to properly communicate with and configure the hardware. @return: None @rtype: None ''' self._channel = None self._page = 0 self.handle = None self.dev = dev self.__revision_num = revision # Set enviroment variables for GoodFET code to use os.environ["platform"] = "apimote%d".format(self.__revision_num) os.environ["board"] = "apimote%d".format(self.__revision_num) self.handle = GoodFETCCSPI() self.handle.serInit(port=self.dev) self.handle.setup() # TODO can we verify here the revision number that was sent is correct? self.__stream_open = False self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): self.handle.serClose() self.handle = None def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information appropriate for GoodFETCCSPI client and firmware. @rtype: None @return: None ''' self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) self.capabilities.setcapab(KBCapabilities.INJECT, True) self.capabilities.setcapab(KBCapabilities.PHYJAM_REFLEX, True) self.capabilities.setcapab(KBCapabilities.SET_SYNC, True) return # KillerBee expects the driver to implement this function def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "GoodFET Apimote v%d".format(self.__revision_num), ""] # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None, page=0): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) self.handle.RF_promiscuity(1) self.handle.RF_autocrc(0) if channel != None: self.set_channel(channel, page) self.handle.CC_RFST_RX() #print "Sniffer started (listening as %010x on %i MHz)" % (self.handle.RF_getsmac(), self.handle.RF_getfreq()/10**6); self.__stream_open = True # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' #TODO actually have firmware stop sending us packets! self.__stream_open = False # KillerBee expects the driver to implement this function def set_channel(self, channel, page=0): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 11 or channel <= 26: self._channel = channel self.handle.RF_setchan(channel) else: raise Exception('Invalid channel') if page: raise Exception('SubGHz not supported') # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0, page=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None: self.set_channel(channel, page) self.handle.RF_autocrc(1) #let radio add the CRC for pnum in range(0, count): gfready = [ord(x) for x in packet] #convert packet string to GoodFET expected integer format gfready.insert(0, len(gfready)+2) #add a length that leaves room for CRC self.handle.RF_txpacket(gfready) time.sleep(1) # KillerBee expects the driver to implement this function def pnext(self, timeout=100): ''' Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() packet = None start = datetime.utcnow() while (packet is None and (start + timedelta(microseconds=timeout) > datetime.utcnow())): packet = self.handle.RF_rxpacket() rssi = self.handle.RF_getrssi() #TODO calibrate if packet is None: return None frame = packet[1:] validcrc = False if frame[-2:] == makeFCS(frame[:-2]): validcrc = True #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = {0:frame, 1:validcrc, 2:rssi, 'bytes':frame, 'validcrc':validcrc, 'rssi':rssi, 'location':None} result['dbm'] = rssi - 45 #TODO tune specifically to the Apimote platform (does ext antenna need to different?) result['datetime'] = datetime.utcnow() return result def ping(self, da, panid, sa, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' raise Exception('Not yet implemented') def jammer_on(self, channel=None, page=0): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @type page: Integer @param page: Sets the subghz page, not supported on this device @rtype: None ''' self.capabilities.require(KBCapabilities.PHYJAM_REFLEX) self.handle.RF_promiscuity(1) self.handle.RF_autocrc(0) if channel != None: self.set_channel(channel, page) self.handle.CC_RFST_RX() self.handle.RF_carrier() #constant carrier wave jamming #self.handle.RF_reflexjam() #reflexive jamming (advanced) def set_sync(self, sync=0xA70F): '''Set the register controlling the 802.15.4 PHY sync byte.''' self.capabilities.require(KBCapabilities.SET_SYNC) if (sync >> 16) > 0: raise Exception("Sync word (%x) must be 2-bytes or less." % sync) return self.handle.poke(CC2420_REG_SYNC, sync) def jammer_off(self, channel=None, page=0): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')
class ZIGDUINO: def __init__(self, dev): ''' Instantiates the KillerBee class for Zigduino running GoodFET firmware. @type dev: String @param dev: Serial device identifier (ex /dev/ttyUSB0) @return: None @rtype: None ''' self._channel = None self.handle = None self.dev = dev self.handle = GoodFETatmel128rfa1() self.handle.serInit(port=self.dev) self.handle.setup() self.__stream_open = False self.capabilities = KBCapabilities() self.__set_capabilities() def close(self): self.handle.serClose() self.handle = None def check_capability(self, capab): return self.capabilities.check(capab) def get_capabilities(self): return self.capabilities.getlist() def __set_capabilities(self): ''' Sets the capability information appropriate for GoodFETAVR client and firmware. @rtype: None @return: None ''' self.capabilities.setcapab(KBCapabilities.FREQ_2400, True) self.capabilities.setcapab(KBCapabilities.SNIFF, True) self.capabilities.setcapab(KBCapabilities.SETCHAN, True) self.capabilities.setcapab(KBCapabilities.INJECT, True) #self.capabilities.setcapab(KBCapabilities.PHYJAM_REFLEX, True) self.capabilities.setcapab(KBCapabilities.SET_SYNC, True) # KillerBee expects the driver to implement this function def get_dev_info(self): ''' Returns device information in a list identifying the device. @rtype: List @return: List of 3 strings identifying device. ''' return [self.dev, "Zigduino", ""] # KillerBee expects the driver to implement this function def sniffer_on(self, channel=None): ''' Turns the sniffer on such that pnext() will start returning observed data. Will set the command mode to Air Capture if it is not already set. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SNIFF) if channel != None: self.set_channel(channel) #print "Sniffer started (listening as %010x on %i MHz)" % (self.handle.RF_getsmac(), self.handle.RF_getfreq()/10**6); self.__stream_open = True # KillerBee expects the driver to implement this function def sniffer_off(self): ''' Turns the sniffer off, freeing the hardware for other functions. It is not necessary to call this function before closing the interface with close(). @rtype: None ''' #TODO actually have firmware stop sending us packets! self.__stream_open = False # KillerBee expects the driver to implement this function def set_channel(self, channel): ''' Sets the radio interface to the specifid channel (limited to 2.4 GHz channels 11-26) @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' self.capabilities.require(KBCapabilities.SETCHAN) if channel >= 11 or channel <= 26: self._channel = channel self.handle.RF_setchannel(channel) else: raise Exception('Invalid channel') # KillerBee expects the driver to implement this function def inject(self, packet, channel=None, count=1, delay=0): ''' Injects the specified packet contents. @type packet: String @param packet: Packet contents to transmit, without FCS. @type channel: Integer @param channel: Sets the channel, optional @type count: Integer @param count: Transmits a specified number of frames, def=1 @type delay: Float @param delay: Delay between each frame, def=1 @rtype: None ''' self.capabilities.require(KBCapabilities.INJECT) if len(packet) < 1: raise Exception('Empty packet') if len(packet) > 125: # 127 - 2 to accommodate FCS raise Exception('Packet too long') if channel != None: self.set_channel(channel) self.handle.RF_autocrc(1) #let the radio add the CRC for pnum in range(0, count): gfready = [ ord(x) for x in packet ] #convert packet string to GoodFET expected integer format gfready.insert(0, len(gfready) + 2) #add a length that leaves room for CRC self.handle.RF_txpacket(gfready) time.sleep(delay) # KillerBee expects the driver to implement this function def pnext(self, timeout=100): ''' Returns a dictionary containing packet data, else None. @type timeout: Integer @param timeout: Timeout to wait for packet reception in usec @rtype: List @return: Returns None is timeout expires and no packet received. When a packet is received, a dictionary is returned with the keys bytes (string of packet bytes), validcrc (boolean if a vaid CRC), rssi (unscaled RSSI), and location (may be set to None). For backwards compatibility, keys for 0,1,2 are provided such that it can be treated as if a list is returned, in the form [ String: packet contents | Bool: Valid CRC | Int: Unscaled RSSI ] ''' if self.__stream_open == False: self.sniffer_on() #start sniffing packet = None start = datetime.utcnow() while (packet is None and (start + timedelta(microseconds=timeout) > datetime.utcnow())): packet = self.handle.RF_rxpacket() if packet is None: return None rssi = self.handle.RF_getrssi() #TODO calibrate frame = packet if frame[-2:] == makeFCS(frame[:-2]): validcrc = True else: validcrc = False #Return in a nicer dictionary format, so we don't have to reference by number indicies. #Note that 0,1,2 indicies inserted twice for backwards compatibility. result = { 0: frame, 1: validcrc, 2: rssi, 'bytes': frame, 'validcrc': validcrc, 'rssi': rssi, 'location': None } result[ 'dbm'] = rssi - 45 #TODO tune specifically to the Tmote platform (does ext antenna need to different?) result['datetime'] = datetime.utcnow() return result def jammer_on(self, channel=None): ''' Not yet implemented. @type channel: Integer @param channel: Sets the channel, optional @rtype: None ''' raise Exception('Not yet implemented') def set_sync(self, sync=0xA7): '''Set the register controlling the 802.15.4 PHY sync byte.''' self.capabilities.require(KBCapabilities.SET_SYNC) if (sync >> 8) > 0: raise Exception("Sync word (%x) must be 1 byte." % sync) if (sync & 0x0F) == 0: raise Exception( "Least-significant nybble in sync (%x) cannot be 0." % sync) return self.handle.poke(ATMEL_REG_SYNC, sync) def jammer_off(self, channel=None): ''' Not yet implemented. @return: None @rtype: None ''' #TODO implement raise Exception('Not yet implemented')