class TestParseZigBeeIOData(unittest.TestCase): """ Test parsing ZigBee specific IO data """ def setUp(self): self.zigbee = ZigBee(None) def test_parse_dio_adc(self): data = b"\x01\x08\x00\x0e\x08\x00\x00\x00\x02P\x02\x06" expected_results = [{"dio-11": True, "adc-1": 0, "adc-2": 592, "adc-3": 518}] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_dio_adc_supply_voltage_not_clamped(self): """ When bit 7 on the ADC mask is set, the supply voltage is included in the ADC I/O sample section. This sample may exceed 10 bits of precision, even though all other ADC channels are limited to a range of 0-1.2v with 10 bits of precision. I assume that a voltage divider and the firmware is used internally to compute the actual Vcc voltage. Therefore, the I/O sampling routine must not clamp this ADC channel to 10 bits of precision. """ data = b"\x01\x00\x00\x80\x0D\x18" expected_results = [{"adc-7": 0xD18}] # import pdb # pdb.set_trace() results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results)
class TestZigBee(unittest.TestCase): """ Tests ZigBee-specific features """ def setUp(self): self.zigbee = ZigBee(None) def test_null_terminated_field(self): """ Packets with null-terminated fields should be properly parsed """ expected_data = '\x01\x02\x03\x04' terminator = '\x00' node_identifier = '\x95' + '\x00' * 21 + expected_data + terminator + '\x00' * 8 data = self.zigbee._split_response(node_identifier) self.assertEqual(data['node_id'], expected_data) def test_split_node_identification_identifier(self): data = '\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaa\x20\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'node_id_indicator', 'sender_addr_long': '\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'sender_addr': '\x7d\x84', 'options': '\x02', 'source_addr': '\x7d\x84', 'source_addr_long': '\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'node_id': ' ', 'parent_source_addr': '\xff\xfe', 'device_type': '\x01', 'source_event': '\x01', 'digi_profile_id': '\xc1\x05', 'manufacturer_id': '\x10\x1e', } self.assertEqual(info, expected_info) def test_split_node_identification_identifier2(self): data = '\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaaCoordinator\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'node_id_indicator', 'sender_addr_long': '\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'sender_addr': '\x7d\x84', 'options': '\x02', 'source_addr': '\x7d\x84', 'source_addr_long': '\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'node_id': 'Coordinator', 'parent_source_addr': '\xff\xfe', 'device_type': '\x01', 'source_event': '\x01', 'digi_profile_id': '\xc1\x05', 'manufacturer_id': '\x10\x1e', } self.assertEqual(info, expected_info)
class TestParseZigBeeIOData(unittest.TestCase): """ Test parsing ZigBee specific IO data """ def setUp(self): self.zigbee = ZigBee(None) def test_parse_dio_adc(self): data = b'\x01\x08\x00\x0e\x08\x00\x00\x00\x02P\x02\x06' expected_results = [{'dio-11': True, 'adc-1': 0, 'adc-2': 592, 'adc-3': 518}] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_samples_ticket_44(self): """ This example is from ticket 44 on Google Code: https://code.google.com/p/python-xbee/issues/detail?id=44 The author claims the given data is generated by an Xbee Pro 900HP module, but I could only find a definition for packet with a response type of 0x92 in the XBee ZB specification. """ data = (b'\x01' + # Number of samples b'\x10\x00' + # Digital I/O mask (CD/DIO12 enabled) b'\x0E' + # ADC 1,2,3 enabled b'\x10\x00' + # DIO12 is high b'\x03\xA4' + # ADC1 = 932 b'\x01\x31' + # ADC2 = 305 b'\x03\x31') # ADC3 = 817 expected_results = [{'dio-12': True, 'adc-1': 932, 'adc-2': 305, 'adc-3': 817}] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_dio_adc_supply_voltage_not_clamped(self): """ When bit 7 on the ADC mask is set, the supply voltage is included in the ADC I/O sample section. This sample may exceed 10 bits of precision, even though all other ADC channels are limited to a range of 0-1.2v with 10 bits of precision. I assume that a voltage divider and the firmware is used internally to compute the actual Vcc voltage. Therefore, the I/O sampling routine must not clamp this ADC channel to 10 bits of precision. """ data = b'\x01\x00\x00\x80\x0D\x18' expected_results = [{'adc-7':0xD18}] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results)
def test_send(self): """ Test send() with AT command. """ device = Serial() xbee = ZigBee(device) xbee.send('at', command='MY') result = device.get_data_written() expected = b'~\x00\x04\x08\x01MYP' self.assertEqual(result, expected)
def reconnect_xbee(self): #search for available ports port_to_connect = '' while port_to_connect == '': #detect platform and format port names if _platform.startswith('win'): ports = ['COM%s' % (i + 1) for i in range(256)] elif _platform.startswith('linux'): # this excludes your current terminal "/dev/tty" ports = glob.glob('/dev/ttyUSB*') else: raise EnvironmentError('Unsupported platform: ' + _platform) ports_avail = [] #loop through all possible ports and try to connect for port in ports: try: s = serial.Serial(port) s.close() ports_avail.append(port) except (OSError, serial.SerialException): pass if len(ports_avail) ==1: port_to_connect = ports_avail[0] elif len(ports_avail)==0: #No Serial port found, continue looping. print( "No serial port detected. Trying again...") time.sleep(1) elif len(ports_avail)>1: #Multiple serial ports detected. Get user input to decide which one to connect to #com_input = raw_input("Multiple serial ports available. Which serial port do you want? \n"+str(self.ports_avail)+":").upper(); if self.default_serial == None: raise EnvironmentError('Incorrect command line parameters. If there are multiple serial devices, indicate what port you want to be used using --serialport') elif self.default_serial.upper() in ports_avail: port_to_connect = self.default_serial.upper() else: raise EnvironmentError('Incorrect command line parameters. Serial port is not known as a valid port. Valid ports are:'+ str(ports_avail)) #connect to xbee or uart ser = serial.Serial(port_to_connect, 115200) if self.uart_connection: self.xbee = UARTConnection(ser) else: self.xbee = ZigBee(ser) print('xbee connected to port ' + port_to_connect) return self
def reconnect_xbee(self): #search for available ports port_to_connect = '' while port_to_connect == '': #detect platform and format port names if _platform.startswith('win'): ports = ['COM%s' % (i + 1) for i in range(256)] elif _platform.startswith('linux'): # this excludes your current terminal "/dev/tty" ports = glob.glob('/dev/ttyUSB*') else: raise EnvironmentError('Unsupported platform: ' + _platform) ports_avail = [] #loop through all possible ports and try to connect for port in ports: try: s = serial.Serial(port) s.close() ports_avail.append(port) except (OSError, serial.SerialException): pass if len(ports_avail) ==1: port_to_connect = ports_avail[0] elif len(ports_avail)==0: #No Serial port found, continue looping. print( "No serial port detected. Trying again...") time.sleep(1) elif len(ports_avail)>1: #Multiple serial ports detected. Get user input to decide which one to connect to #com_input = raw_input("Multiple serial ports available. Which serial port do you want? \n"+str(self.ports_avail)+":").upper(); if self.default_serial == None: raise EnvironmentError('Incorrect command line parameters. If there are multiple serial devices, indicate what port you want to be used using --serialport') elif self.default_serial.upper() in ports_avail: port_to_connect = self.default_serial.upper() else: raise EnvironmentError('Incorrect command line parameters. Serial port is not known as a valid port. Valid ports are:'+ str(ports_avail)) #connect to xbee self.xbee = ZigBee(serial.Serial(port_to_connect, 115200)) print('xbee connected to port ' + port_to_connect) return self
def __enter__(self): if _platform == "linux" or _platform == "linux2": self.ser = serial.Serial('/dev/ttyUSB0', 38400) elif _platform == "win32": self.ser = serial.Serial('COM5', 38400) self.xbee = ZigBee(self.ser) print 'xbee created/initialized' return self
class TestParseZigBeeIOData(unittest.TestCase): """ Test parsing ZigBee specific IO data """ def setUp(self): self.zigbee = ZigBee(None) def test_parse_dio_adc(self): data = b'\x01\x08\x00\x0e\x08\x00\x00\x00\x02P\x02\x06' expected_results = [{ 'dio-11': True, 'adc-1': 0, 'adc-2': 592, 'adc-3': 518 }] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_dio_adc_supply_voltage_not_clamped(self): """ When bit 7 on the ADC mask is set, the supply voltage is included in the ADC I/O sample section. This sample may exceed 10 bits of precision, even though all other ADC channels are limited to a range of 0-1.2v with 10 bits of precision. I assume that a voltage divider and the firmware is used internally to compute the actual Vcc voltage. Therefore, the I/O sampling routine must not clamp this ADC channel to 10 bits of precision. """ data = b'\x01\x00\x00\x80\x0D\x18' expected_results = [{'adc-7': 0xD18}] #import pdb #pdb.set_trace() results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results)
class TestParseZigBeeIOData(unittest.TestCase): """ Test parsing ZigBee specific IO data """ def setUp(self): self.zigbee = ZigBee(None) def test_parse_dio_adc(self): data = b'\x01\x08\x00\x0e\x08\x00\x00\x00\x02P\x02\x06' expected_results = [{'dio-11': True, 'adc-1': 0, 'adc-2': 592, 'adc-3': 518}] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results)
class TestParseZigBeeIOData(unittest.TestCase): """ Test parsing ZigBee specific IO data """ def setUp(self): self.zigbee = ZigBee(None) def test_parse_dio_adc(self): data = '\x01\x08\x00\x0e\x08\x00\x00\x00\x02P\x02\x06' expected_results = [{ 'dio-11': True, 'adc-1': 0, 'adc-2': 592, 'adc-3': 518 }] results = self.zigbee._parse_samples(data) self.assertEqual(results, expected_results)
class Receiver: def __init__(self, db_type): self.data_shape = struct.Struct( ''.join(map(lambda x: x[0], db_type))) self.data_size = self.data_shape.size self.expected_packets = self.data_size / MAX_PACKET_SIZE + 1 self.source_addr = None self.source_addr_long = None self.outbound = [] def async_tx(self, command): """Eventually send a command """ self.outbound.append(command) def __enter__(self): if _platform == "linux" or _platform == "linux2": self.ser = serial.Serial('/dev/ttyUSB0', 38400) elif _platform == "win32": self.ser = serial.Serial('COM5', 38400) self.xbee = ZigBee(self.ser) print 'xbee created/initialized' return self def data_lines(self): while True: payload = '' for x in xrange(self.expected_packets): packet = self.xbee.wait_read_frame() while packet.get('id', None) == 'tx_status': print('got tx_status frame') packet = self.xbee.wait_read_frame() self.source_addr_long = packet.get( 'source_addr_long', self.source_addr_long) self.source_addr = packet.get( 'source_addr', self.source_addr) payload += packet['rf_data'] payload += packet['rssi'] if x < self.expected_packets - 1 and len(payload) < 100: break; # let our data be processed yield self.data_shape.unpack(payload) # flush the command queue to the xbee for cmd in self.outbound: self.xbee.tx(dest_addr_long=self.source_addr_long, dest_addr=self.source_addr, data=cmd) print("command {}".format(' '.join("0x{:02x}".format(i) for i in cmd))) print "sent a command" self.outbound = [] #self.xbee.tx(dest_addr_long=source_addr_long, # dest_addr=source_addr, data=b'Hello world') def __exit__(self, type, value, traceback): self.xbee = None self.ser.close() if isinstance(value, serial.SerialException): print traceback return True
class TestZigBee(unittest.TestCase): """ Tests ZigBee-specific features """ def setUp(self): self.zigbee = ZigBee(None) def test_null_terminated_field(self): """ Packets with null-terminated fields should be properly parsed """ expected_data = b'\x01\x02\x03\x04' terminator = b'\x00' node_identifier = b'\x95' + b'\x00' * 21 + expected_data + terminator + b'\x00' * 8 data = self.zigbee._split_response(node_identifier) self.assertEqual(data['node_id'], expected_data) def test_split_node_identification_identifier(self): data = b'\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaa\x20\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'node_id_indicator', 'sender_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'sender_addr': b'\x7d\x84', 'options': b'\x02', 'source_addr': b'\x7d\x84', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'node_id': b' ', 'parent_source_addr': b'\xff\xfe', 'device_type': b'\x01', 'source_event': b'\x01', 'digi_profile_id': b'\xc1\x05', 'manufacturer_id': b'\x10\x1e', } self.assertEqual(info, expected_info) def test_split_node_identification_identifier2(self): data = b'\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaaCoordinator\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'node_id_indicator', 'sender_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'sender_addr': b'\x7d\x84', 'options': b'\x02', 'source_addr': b'\x7d\x84', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'node_id': b'Coordinator', 'parent_source_addr': b'\xff\xfe', 'device_type': b'\x01', 'source_event': b'\x01', 'digi_profile_id': b'\xc1\x05', 'manufacturer_id': b'\x10\x1e', } self.assertEqual(info, expected_info) def test_is_remote_at_response_parameter_parsed_as_io_samples(self): """ A remote AT command of IS, to take a sample immediately and respond with the results, must be appropriately parsed for IO data. """ data = b'\x97A\x00\x13\xa2\x00@oG\xe4v\x1aIS\x00\x01\x1c\xc0\x06\x18\x00\x02\x8c\x03\x96' info = self.zigbee._split_response(data) expected_info = { 'id': 'remote_at_response', 'frame_id': b'A', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'IS', 'status': b'\x00', 'parameter': [{ 'dio-10': False, 'adc-2': 918, 'dio-6': False, 'dio-11': True, 'adc-1': 652 }] } self.assertEqual(info, expected_info) def test_lowercase_is_remote_at_response_parameter_parsed_as_io_samples( self): """ A remote AT command of lowercase is, to take a sample immediately and respond with the results, must be appropriately parsed for IO data. """ data = b'\x97A\x00\x13\xa2\x00@oG\xe4v\x1ais\x00\x01\x1c\xc0\x06\x18\x00\x02\x8c\x03\x96' info = self.zigbee._split_response(data) expected_info = { 'id': 'remote_at_response', 'frame_id': b'A', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'is', 'status': b'\x00', 'parameter': [{ 'dio-10': False, 'adc-2': 918, 'dio-6': False, 'dio-11': True, 'adc-1': 652 }] } self.assertEqual(info, expected_info) def test_parsing_may_encounter_field_which_does_not_exist(self): """ Some fields are optional and may not exist; parsing should not crash if/when they are not available. """ data = b'\x97A\x00\x13\xa2\x00@oG\xe4v\x1aIS\x01' info = self.zigbee._split_response(data) expected_info = { 'id': 'remote_at_response', 'frame_id': b'A', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'IS', 'status': b'\x01', } self.assertEqual(info, expected_info) def test_nd_at_response_parameter_parsed(self): """ An at_response for an ND command must be parsed. """ data = b'\x88AND\x00v\x1a\x00\x13\xa2\x00@oG\xe4ENDPOINT-1\x00\xff\xfe\x01\x00\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'at_response', 'frame_id': b'A', 'command': b'ND', 'status': b'\x00', 'parameter': { 'source_addr': b'\x76\x1a', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x6f\x47\xe4', 'node_identifier': b'ENDPOINT-1', 'parent_address': b'\xff\xfe', 'device_type': b'\x01', 'status': b'\x00', 'profile_id': b'\xc1\x05', 'manufacturer': b'\x10\x1e', } } self.assertEqual(info, expected_info) def test_lowercase_nd_at_response_parameter_parsed(self): """ An at_response for a lowercase nd command must be parsed. """ data = b'\x88And\x00v\x1a\x00\x13\xa2\x00@oG\xe4ENDPOINT-1\x00\xff\xfe\x01\x00\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'at_response', 'frame_id': b'A', 'command': b'nd', 'status': b'\x00', 'parameter': { 'source_addr': b'\x76\x1a', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x6f\x47\xe4', 'node_identifier': b'ENDPOINT-1', 'parent_address': b'\xff\xfe', 'device_type': b'\x01', 'status': b'\x00', 'profile_id': b'\xc1\x05', 'manufacturer': b'\x10\x1e', } } self.assertEqual(info, expected_info)
def setUp(self): self.zigbee = ZigBee(None)
logging.info('Reading nodes configuration "%s"', nodefilename) nodeconfig = configparser.ConfigParser() try: nodeconfig.read(nodefilename) except: logging.critical('Could not read the node configuration file "%s".', nodefilename) raise # Open serial port with serial.Serial(args.port, args.rate) as ser: # Create an xbee ZigBee communication object dispatch = Dispatch(ser) logging.debug('Creating xbee object.') xbee = ZigBee(ser, callback=dispatch.dispatch) try: if args.command == 'listen': listen(xbee, dispatch, config, nodeconfig) elif args.command == 'configure': config_client(xbee, dispatch, config, nodeconfig) else: logging.critical('Unknown command "%s", terminating.', args.command) finally: # halt() must be called before closing the serial port in order to ensure proper thread shutdown logging.info('Halting xbee.') xbee.halt() ser.close() logging.info('Closed serial port.')
class TestZigBee(unittest.TestCase): """ Tests ZigBee-specific features """ def setUp(self): self.zigbee = ZigBee(None) def test_null_terminated_field(self): """ Packets with null-terminated fields should be properly parsed """ expected_data = b"\x01\x02\x03\x04" terminator = b"\x00" node_identifier = b"\x95" + b"\x00" * 21 + expected_data + terminator + b"\x00" * 8 data = self.zigbee._split_response(node_identifier) self.assertEqual(data["node_id"], expected_data) def test_split_node_identification_identifier(self): data = b"\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaa\x20\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e" info = self.zigbee._split_response(data) expected_info = { "id": "node_id_indicator", "sender_addr_long": b"\x00\x13\xa2\x00\x40\x52\x2b\xaa", "sender_addr": b"\x7d\x84", "options": b"\x02", "source_addr": b"\x7d\x84", "source_addr_long": b"\x00\x13\xa2\x00\x40\x52\x2b\xaa", "node_id": b" ", "parent_source_addr": b"\xff\xfe", "device_type": b"\x01", "source_event": b"\x01", "digi_profile_id": b"\xc1\x05", "manufacturer_id": b"\x10\x1e", } self.assertEqual(info, expected_info) def test_split_node_identification_identifier2(self): data = b"\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaaCoordinator\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e" info = self.zigbee._split_response(data) expected_info = { "id": "node_id_indicator", "sender_addr_long": b"\x00\x13\xa2\x00\x40\x52\x2b\xaa", "sender_addr": b"\x7d\x84", "options": b"\x02", "source_addr": b"\x7d\x84", "source_addr_long": b"\x00\x13\xa2\x00\x40\x52\x2b\xaa", "node_id": b"Coordinator", "parent_source_addr": b"\xff\xfe", "device_type": b"\x01", "source_event": b"\x01", "digi_profile_id": b"\xc1\x05", "manufacturer_id": b"\x10\x1e", } self.assertEqual(info, expected_info) def test_is_remote_at_response_parameter_parsed_as_io_samples(self): """ A remote AT command of IS, to take a sample immediately and respond with the results, must be appropriately parsed for IO data. """ data = b"\x97A\x00\x13\xa2\x00@oG\xe4v\x1aIS\x00\x01\x1c\xc0\x06\x18\x00\x02\x8c\x03\x96" info = self.zigbee._split_response(data) expected_info = { "id": "remote_at_response", "frame_id": b"A", "source_addr_long": b"\x00\x13\xa2\x00@oG\xe4", "source_addr": b"v\x1a", "command": b"IS", "status": b"\x00", "parameter": [{"dio-10": False, "adc-2": 918, "dio-6": False, "dio-11": True, "adc-1": 652}], } self.assertEqual(info, expected_info) def test_lowercase_is_remote_at_response_parameter_parsed_as_io_samples(self): """ A remote AT command of lowercase is, to take a sample immediately and respond with the results, must be appropriately parsed for IO data. """ data = b"\x97A\x00\x13\xa2\x00@oG\xe4v\x1ais\x00\x01\x1c\xc0\x06\x18\x00\x02\x8c\x03\x96" info = self.zigbee._split_response(data) expected_info = { "id": "remote_at_response", "frame_id": b"A", "source_addr_long": b"\x00\x13\xa2\x00@oG\xe4", "source_addr": b"v\x1a", "command": b"is", "status": b"\x00", "parameter": [{"dio-10": False, "adc-2": 918, "dio-6": False, "dio-11": True, "adc-1": 652}], } self.assertEqual(info, expected_info) def test_parsing_may_encounter_field_which_does_not_exist(self): """ Some fields are optional and may not exist; parsing should not crash if/when they are not available. """ data = b"\x97A\x00\x13\xa2\x00@oG\xe4v\x1aIS\x01" info = self.zigbee._split_response(data) expected_info = { "id": "remote_at_response", "frame_id": b"A", "source_addr_long": b"\x00\x13\xa2\x00@oG\xe4", "source_addr": b"v\x1a", "command": b"IS", "status": b"\x01", } self.assertEqual(info, expected_info) def test_nd_at_response_parameter_parsed(self): """ An at_response for an ND command must be parsed. """ data = b"\x88AND\x00v\x1a\x00\x13\xa2\x00@oG\xe4ENDPOINT-1\x00\xff\xfe\x01\x00\xc1\x05\x10\x1e" info = self.zigbee._split_response(data) expected_info = { "id": "at_response", "frame_id": b"A", "command": b"ND", "status": b"\x00", "parameter": { "source_addr": b"\x76\x1a", "source_addr_long": b"\x00\x13\xa2\x00\x40\x6f\x47\xe4", "node_identifier": b"ENDPOINT-1", "parent_address": b"\xff\xfe", "device_type": b"\x01", "status": b"\x00", "profile_id": b"\xc1\x05", "manufacturer": b"\x10\x1e", }, } self.assertEqual(info, expected_info) def test_lowercase_nd_at_response_parameter_parsed(self): """ An at_response for a lowercase nd command must be parsed. """ data = b"\x88And\x00v\x1a\x00\x13\xa2\x00@oG\xe4ENDPOINT-1\x00\xff\xfe\x01\x00\xc1\x05\x10\x1e" info = self.zigbee._split_response(data) expected_info = { "id": "at_response", "frame_id": b"A", "command": b"nd", "status": b"\x00", "parameter": { "source_addr": b"\x76\x1a", "source_addr_long": b"\x00\x13\xa2\x00\x40\x6f\x47\xe4", "node_identifier": b"ENDPOINT-1", "parent_address": b"\xff\xfe", "device_type": b"\x01", "status": b"\x00", "profile_id": b"\xc1\x05", "manufacturer": b"\x10\x1e", }, } self.assertEqual(info, expected_info)
class TestZigBee(unittest.TestCase): """ Tests ZigBee-specific features """ def setUp(self): self.zigbee = ZigBee(None) def test_null_terminated_field(self): """ Packets with null-terminated fields should be properly parsed """ expected_data = b'\x01\x02\x03\x04' terminator = b'\x00' node_identifier = b'\x95' + b'\x00' * 21 + expected_data + terminator + b'\x00' * 8 data = self.zigbee._split_response(node_identifier) self.assertEqual(data['node_id'], expected_data) def test_split_node_identification_identifier(self): data = b'\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaa\x20\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'node_id_indicator', 'sender_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'sender_addr': b'\x7d\x84', 'options': b'\x02', 'source_addr': b'\x7d\x84', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'node_id': b' ', 'parent_source_addr': b'\xff\xfe', 'device_type': b'\x01', 'source_event': b'\x01', 'digi_profile_id': b'\xc1\x05', 'manufacturer_id': b'\x10\x1e', } self.assertEqual(info, expected_info) def test_split_node_identification_identifier2(self): data = b'\x95\x00\x13\xa2\x00\x40\x52\x2b\xaa\x7d\x84\x02\x7d\x84\x00\x13\xa2\x00\x40\x52\x2b\xaaCoordinator\x00\xff\xfe\x01\x01\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'node_id_indicator', 'sender_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'sender_addr': b'\x7d\x84', 'options': b'\x02', 'source_addr': b'\x7d\x84', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x52\x2b\xaa', 'node_id': b'Coordinator', 'parent_source_addr': b'\xff\xfe', 'device_type': b'\x01', 'source_event': b'\x01', 'digi_profile_id': b'\xc1\x05', 'manufacturer_id': b'\x10\x1e', } self.assertEqual(info, expected_info) def test_is_remote_at_response_parameter_parsed_as_io_samples(self): """ A remote AT command of IS, to take a sample immediately and respond with the results, must be appropriately parsed for IO data. """ data = b'\x97A\x00\x13\xa2\x00@oG\xe4v\x1aIS\x00\x01\x1c\xc0\x06\x18\x00\x02\x8c\x03\x96' info = self.zigbee._split_response(data) expected_info = { 'id': 'remote_at_response', 'frame_id': b'A', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'IS', 'status': b'\x00', 'parameter': [{'adc-1': 652, 'adc-2': 918, 'dio-10': False, 'dio-11': True, 'dio-12': True, 'dio-6': False, 'dio-7': False }] } self.assertEqual(info, expected_info) def test_lowercase_is_remote_at_response_parameter_parsed_as_io_samples(self): """ A remote AT command of lowercase is, to take a sample immediately and respond with the results, must be appropriately parsed for IO data. """ data = b'\x97A\x00\x13\xa2\x00@oG\xe4v\x1ais\x00\x01\x1c\xc0\x06\x18\x00\x02\x8c\x03\x96' info = self.zigbee._split_response(data) expected_info = { 'id': 'remote_at_response', 'frame_id': b'A', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'is', 'status': b'\x00', 'parameter': [{'adc-1': 652, 'adc-2': 918, 'dio-10': False, 'dio-11': True, 'dio-12': True, 'dio-6': False, 'dio-7': False }] } self.assertEqual(info, expected_info) def test_parsing_may_encounter_field_which_does_not_exist(self): """ Some fields are optional and may not exist; parsing should not crash if/when they are not available. """ data = b'\x97A\x00\x13\xa2\x00@oG\xe4v\x1aIS\x01' info = self.zigbee._split_response(data) expected_info = { 'id': 'remote_at_response', 'frame_id': b'A', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'IS', 'status': b'\x01', } self.assertEqual(info, expected_info) def test_nd_at_response_parameter_parsed(self): """ An at_response for an ND command must be parsed. """ data = b'\x88AND\x00v\x1a\x00\x13\xa2\x00@oG\xe4ENDPOINT-1\x00\xff\xfe\x01\x00\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'at_response', 'frame_id': b'A', 'command': b'ND', 'status': b'\x00', 'parameter': {'source_addr': b'\x76\x1a', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x6f\x47\xe4', 'node_identifier': b'ENDPOINT-1', 'parent_address': b'\xff\xfe', 'device_type': b'\x01', 'status': b'\x00', 'profile_id': b'\xc1\x05', 'manufacturer': b'\x10\x1e', } } self.assertEqual(info, expected_info) def test_lowercase_nd_at_response_parameter_parsed(self): """ An at_response for a lowercase nd command must be parsed. """ data = b'\x88And\x00v\x1a\x00\x13\xa2\x00@oG\xe4ENDPOINT-1\x00\xff\xfe\x01\x00\xc1\x05\x10\x1e' info = self.zigbee._split_response(data) expected_info = { 'id': 'at_response', 'frame_id': b'A', 'command': b'nd', 'status': b'\x00', 'parameter': {'source_addr': b'\x76\x1a', 'source_addr_long': b'\x00\x13\xa2\x00\x40\x6f\x47\xe4', 'node_identifier': b'ENDPOINT-1', 'parent_address': b'\xff\xfe', 'device_type': b'\x01', 'status': b'\x00', 'profile_id': b'\xc1\x05', 'manufacturer': b'\x10\x1e', } } self.assertEqual(info, expected_info)
nodefilename = config['DEFAULT']['nodefile'] logging.info('Reading nodes configuration "%s"', nodefilename) nodeconfig = configparser.ConfigParser() try: nodeconfig.read(nodefilename) except: logging.critical('Could not read the node configuration file "%s".', nodefilename) raise # Open serial port with serial.Serial(args.port,args.rate) as ser: # Create an xbee ZigBee communication object dispatch = Dispatch(ser) logging.debug('Creating xbee object.') xbee = ZigBee(ser,callback=dispatch.dispatch) try: if args.command == 'listen': listen(xbee,dispatch,config,nodeconfig) elif args.command == 'configure': config_client(xbee,dispatch,config,nodeconfig) else: logging.critical('Unknown command "%s", terminating.',args.command) finally: # halt() must be called before closing the serial port in order to ensure proper thread shutdown logging.info('Halting xbee.') xbee.halt() ser.close() logging.info('Closed serial port.')
class Receiver: def __init__(self, db_type, serialport): self.data_shape = {key:struct.Struct( ''.join(map(lambda x: x[0], packet))) for key, packet in db_type.iteritems()} #Print out packet size for key, packet in self.data_shape.iteritems(): print("Packet Size {}: {}".format(key,packet.size)) #Check if all packets have the same size self.data_size = self.data_shape[self.data_shape.keys()[0]].size data_mismatch = False for i in xrange(1,len(self.data_shape)): if (self.data_shape[i].size != self.data_size): print("Data Packets are not the same in size: " + str(self.data_size) + " " + str(self.data_shape[i].size)) data_mismatch = True if data_mismatch: raise ValueError("Data packet size mismatch") self.expected_packets = self.data_size / MAX_PACKET_SIZE + 1 self.source_addr = None self.source_addr_long = None self.packet_type = None self.outbound = [] self.rssi = -100 self.stored_data = [tuple([None])]*len(self.data_shape.keys()) self.default_serial = serialport def async_tx(self, command): """Eventually send a command """ self.outbound.append(command) def reconnect_xbee(self): #search for available ports port_to_connect = '' while port_to_connect == '': #detect platform and format port names if _platform.startswith('win'): ports = ['COM%s' % (i + 1) for i in range(256)] elif _platform.startswith('linux'): # this excludes your current terminal "/dev/tty" ports = glob.glob('/dev/ttyUSB*') else: raise EnvironmentError('Unsupported platform: ' + _platform) ports_avail = [] #loop through all possible ports and try to connect for port in ports: try: s = serial.Serial(port) s.close() ports_avail.append(port) except (OSError, serial.SerialException): pass if len(ports_avail) ==1: port_to_connect = ports_avail[0] elif len(ports_avail)==0: #No Serial port found, continue looping. print( "No serial port detected. Trying again...") time.sleep(1) elif len(ports_avail)>1: #Multiple serial ports detected. Get user input to decide which one to connect to #com_input = raw_input("Multiple serial ports available. Which serial port do you want? \n"+str(self.ports_avail)+":").upper(); if self.default_serial == None: raise EnvironmentError('Incorrect command line parameters. If there are multiple serial devices, indicate what port you want to be used using --serialport') elif self.default_serial.upper() in ports_avail: port_to_connect = self.default_serial.upper() else: raise EnvironmentError('Incorrect command line parameters. Serial port is not known as a valid port. Valid ports are:'+ str(ports_avail)) #connect to xbee self.xbee = ZigBee(serial.Serial(port_to_connect, 115200)) print('xbee connected to port ' + port_to_connect) return self def __enter__(self): return self.reconnect_xbee() def data_lines(self): #counter to limit extra packets sent packetCnt=0 while True: try: payload = '' yield_data = () for x in xrange(self.expected_packets): packet = self.xbee.wait_read_frame() #limit packets by only sending decibel strength only when if statement is true if(packetCnt>=10): self.xbee.at(command="DB") packetCnt=0 packetCnt=packetCnt+1 while packet.get('id', None) != 'rx': #Checks for tx_response if packet.get('id', None) == 'tx_status': print('got tx_status frame') #Checks for command response and signal strength elif packet.get('id', None) == 'at_response': if packet.get('command', None) == 'DB': self.rssi = ord(packet.get('parameter',self.rssi)) packet = self.xbee.wait_read_frame() self.source_addr_long = packet.get( 'source_addr_long', self.source_addr_long) self.source_addr = packet.get( 'source_addr', self.source_addr) payload += packet['rf_data'] # Read first two bytes, to determine packet type packet_type = struct.unpack("h", payload[:2])[0] # Unpack Struct according to ID, and update global parameters for data_type, data_shape in self.data_shape.iteritems(): if (packet_type == data_type): self.stored_data[data_type] = data_shape.unpack(payload[2:]) else: self.stored_data[data_type] = tuple([None] * len([i for i in data_shape.format if i != 'x'])) yield_data = tuple([i for j in self.stored_data for i in j]) #Add RSSI to each packet yield_data += tuple([self.rssi]) # let our data be processed - unpacks an array of tuples into one single tuple yield yield_data # flush the command queue to the xbee for cmd in self.outbound: self.xbee.tx(dest_addr_long=self.source_addr_long, dest_addr=self.source_addr, data=cmd) print("command {}".format(' '.join("0x{:02x}".format(i) for i in cmd))) print("sent a command") self.outbound = [] except (OSError, serial.SerialException, IOError): #catch exception if xbee is unplugged, and try to reconnect print("Xbee disconnected!") self.reconnect_xbee() def __exit__(self, type, value, traceback): self.xbee = None try: self.ser.close() except(AttributeError): #If the program exits before ser is initiallized, ser.close() will throw an AttributeError, which is caught and ignored pass if isinstance(value, serial.SerialException): print(traceback) return True
class Receiver: def __init__(self, db_type, serialport, uart_connection=False): self.data_shape = {key:struct.Struct( ''.join(map(lambda x: x[0], packet))) for key, packet in db_type.iteritems()} #Print out packet size for key, packet in self.data_shape.iteritems(): print("Packet Size {}: {}".format(key,packet.size)) #number of packet frames we're expecting to receive. Only valid if we're sending over 100 bytes of data self.expected_packets = 1 #assume we will only send 1 packet down for now self.source_addr = None self.source_addr_long = None self.packet_type = None self.outbound = [] self.rssi = 255 self.stored_data = [tuple([None])]*len(self.data_shape.keys()) self.default_serial = serialport self.uart_connection = uart_connection def async_tx(self, command): """Eventually send a command """ self.outbound.append(command) def reconnect_xbee(self): #search for available ports port_to_connect = '' while port_to_connect == '': #detect platform and format port names if _platform.startswith('win'): ports = ['COM%s' % (i + 1) for i in range(256)] elif _platform.startswith('linux'): # this excludes your current terminal "/dev/tty" ports = glob.glob('/dev/ttyUSB*') else: raise EnvironmentError('Unsupported platform: ' + _platform) ports_avail = [] #loop through all possible ports and try to connect for port in ports: try: s = serial.Serial(port) s.close() ports_avail.append(port) except (OSError, serial.SerialException): pass if len(ports_avail) ==1: port_to_connect = ports_avail[0] elif len(ports_avail)==0: #No Serial port found, continue looping. print( "No serial port detected. Trying again...") time.sleep(1) elif len(ports_avail)>1: #Multiple serial ports detected. Get user input to decide which one to connect to #com_input = raw_input("Multiple serial ports available. Which serial port do you want? \n"+str(self.ports_avail)+":").upper(); if self.default_serial == None: raise EnvironmentError('Incorrect command line parameters. If there are multiple serial devices, indicate what port you want to be used using --serialport') elif self.default_serial.upper() in ports_avail: port_to_connect = self.default_serial.upper() else: raise EnvironmentError('Incorrect command line parameters. Serial port is not known as a valid port. Valid ports are:'+ str(ports_avail)) #connect to xbee or uart ser = serial.Serial(port_to_connect, 115200) if self.uart_connection: self.xbee = UARTConnection(ser) else: self.xbee = ZigBee(ser) print('xbee connected to port ' + port_to_connect) return self def __enter__(self): return self.reconnect_xbee() def data_lines(self): #counter to limit extra packets sent packetCnt=0 while True: try: payload = '' yield_data = () for x in xrange(self.expected_packets): packet = self.xbee.wait_read_frame() #limit packets by only sending decibel strength only when if statement is true if(packetCnt>=10): self.xbee.at(command="DB") packetCnt=0 packetCnt=packetCnt+1 while packet.get('id', None) != 'rx': #Checks for tx_response if packet.get('id', None) == 'tx_status': print('got tx_status frame') #Checks for command response and signal strength elif packet.get('id', None) == 'at_response': if packet.get('command', None) == 'DB': self.rssi = ord(packet.get('parameter',chr(self.rssi))) packet = self.xbee.wait_read_frame() self.source_addr_long = packet.get( 'source_addr_long', self.source_addr_long) self.source_addr = packet.get( 'source_addr', self.source_addr) payload += packet['rf_data'] # Read first two bytes, to determine packet type packet_type = struct.unpack("h", payload[:2])[0] # Unpack Struct according to ID, and update global parameters for data_type, data_shape in self.data_shape.iteritems(): if (packet_type == data_type): self.stored_data[data_type] = data_shape.unpack(payload[2:]) else: self.stored_data[data_type] = tuple([None] * len([i for i in data_shape.format if i != 'x'])) yield_data = tuple([i for j in self.stored_data for i in j]) #Add RSSI to each packet yield_data += tuple([self.rssi]) # let our data be processed - unpacks an array of tuples into one single tuple yield yield_data # flush the command queue to the xbee for cmd in self.outbound: self.xbee.tx(dest_addr_long=self.source_addr_long, dest_addr=self.source_addr, data=cmd) print("command {}".format(' '.join("0x{:02x}".format(i) for i in cmd))) print("sent a command") self.outbound = [] except (OSError, serial.SerialException, IOError): #catch exception if xbee is unplugged, and try to reconnect print("Xbee disconnected!") self.reconnect_xbee() def __exit__(self, type, value, traceback): self.xbee = None try: self.ser.close() except(AttributeError): #If the program exits before ser is initiallized, ser.close() will throw an AttributeError, which is caught and ignored pass if isinstance(value, serial.SerialException): print(traceback) return True