#! /usr/bin/python """ test_ieee.py By Paul Malmsten, 2010 pmalmsten@gmail.com Tests the XBee (IEEE 802.15.4) implementation class for XBee API compliance """ from xbee.tests.Fake import Serial from xbee.thread.ieee import XBee from xbee.frame import APIFrame from xbee.python2to3 import intToByte, stringToBytes import unittest import sys import traceback class InitXBee(unittest.TestCase): """ Base initalization class """ def setUp(self): """ Initialize XBee object """ self.xbee = XBee(None) class TestBuildCommand(InitXBee): """ _build_command should properly build a command packet """ def test_build_at_data_mismatch(self): """ if not enough or incorrect data is provided, an exception should be raised. """ try: self.xbee._build_command("at") except KeyError: # Test passes return # No exception? Fail. self.fail( "An exception was not raised with improper data supplied" ) def test_build_at_data_len_mismatch(self): """ if data of incorrect length is provided, an exception should be raised """ try: self.xbee._build_command("at", frame_id="AB", command="MY") except ValueError: # Test passes return # No exception? Fail. self.fail( "An exception was not raised with improper data length" ) def test_build_at(self): """ _build_command should build a valid at command packet which has no parameter data to be saved """ at_command = stringToBytes("MY") frame = intToByte(43) data = self.xbee._build_command( "at", frame_id=frame, command=at_command ) expected_data = b'\x08+MY' self.assertEqual(data, expected_data) def test_build_at_with_default(self): """ _build_command should build a valid at command packet which has no parameter data to be saved and no frame specified (the default value of \x00 should be used) """ at_command = stringToBytes("MY") data = self.xbee._build_command("at", command=at_command) expected_data = b'\x08\x00MY' self.assertEqual(data, expected_data) class TestSplitResponse(InitXBee): """ _split_response should properly split a response packet """ def test_unrecognized_response(self): """ if a response begins with an unrecognized id byte, _split_response should raise an exception """ data = b'\x23\x00\x00\x00' try: self.xbee._split_response(data) except KeyError: # Passes return # Test Fails self.fail() def test_transmit_packet_received(self): """ if a response begins with an ID that is unrecognized as a response ID but is a valid transmission ID, show a helpful error indicating that a device may be in command mode. """ from xbee.backend.base import CommandFrameException data = b'\x01\x00\x00\x00' try: self.xbee._split_response(data) except CommandFrameException: # Passes return # Test Fails self.fail() def test_bad_data_long(self): """ if a response doesn't match the specification's layout, _split_response should raise an exception """ # Over length data = b'\x8a\x00\x00\x00' self.assertRaises(ValueError, self.xbee._split_response, data) def test_bad_data_short(self): """ if a response doesn't match the specification's layout, _split_response should raise an exception """ # Under length data = b'\x8a' self.assertRaises(ValueError, self.xbee._split_response, data) def test_split_status_response(self): """ _split_response should properly split a status response packet """ data = b'\x8a\x01' info = self.xbee._split_response(data) expected_info = {'id': 'status', 'status': b'\x01'} self.assertEqual(info, expected_info) def test_split_short_at_response(self): """ _split_response should properly split an at_response packet which has no parameter data """ data = b'\x88DMY\x01' info = self.xbee._split_response(data) expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01'} self.assertEqual(info, expected_info) def test_split_at_resp_with_param(self): """ _split_response should properly split an at_response packet which has parameter data """ data = b'\x88DMY\x01ABCDEF' info = self.xbee._split_response(data) expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01', 'parameter': b'ABCDEF'} self.assertEqual(info, expected_info) def test_generalized_packet_parsing(self): """ _split_response should properly parse packets in a generalized manner when specified by the protocol definition. """ # Temporarily modify parsing rule (taking a backup of the original rule) parse_rule_orig = self.xbee.api_responses[b"\x88"]["parsing"] self.xbee.api_responses[b"\x88"]["parsing"] = \ [("parameter", lambda self, orig: b"GHIJKL")] data = b'\x88DMY\x01ABCDEF' info = self.xbee._split_response(data) expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01', 'parameter': b'GHIJKL'} # Restore parsing rule to original self.xbee.api_responses[b"\x88"]["parsing"] = parse_rule_orig self.assertEqual(info, expected_info) class TestParseIOData(InitXBee): """ XBee class should properly parse IO data received from an XBee device """ def test_parse_single_dio(self): """ _parse_samples should properly parse a packet containing a single sample of only digital io data """ # One sample, ADC disabled and DIO8 enabled, DIO 0-7 enabled header = b'\x01\x01\xFF' # First 7 bits ignored, DIO8 high, DIO 0-7 high sample = b'\x01\xFF' data = header + sample expected_results = [{'dio-0': True, 'dio-1': True, 'dio-2': True, 'dio-3': True, 'dio-4': True, 'dio-5': True, 'dio-6': True, 'dio-7': True, 'dio-8': True}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_single_dio_again(self): """ _parse_samples should properly parse a packet containing a single sample of only digital io data, which alternates between on and off """ # One sample, ADC disabled and DIO8 enabled, DIO 0-7 enabled header = b'\x01\x01\xFF' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating sample = b'\x00\xAA' data = header + sample expected_results = [{'dio-0': False, 'dio-1': True, 'dio-2': False, 'dio-3': True, 'dio-4': False, 'dio-5': True, 'dio-6': False, 'dio-7': True, 'dio-8': False}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_single_dio_subset(self): """ _parse_samples should properly parse a packet containing a single sample of only digital io data for only a subset of the available pins """ # One sample, ADC disabled # DIO 1,3,5,7 enabled header = b'\x01\x00\xAA' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating sample = b'\x00\xAA' data = header + sample expected_results = [{'dio-1': True, 'dio-3': True, 'dio-5': True, 'dio-7': True}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_single_dio_subset_again(self): """ _parse_samples should properly parse a packet containing a single sample of only digital io data for only a subset of the available pins """ # One sample, ADC disabled # DIO 0 enabled header = b'\x01\x00\x01' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating sample = b'\x00\xAA' data = header + sample expected_results = [{'dio-0': False}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_multiple_dio_subset(self): """ _parse_samples should properly parse a packet containing two samples of only digital io data for one dio line """ # Two samples, ADC disabled # DIO 0 enabled header = b'\x02\x00\x01' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating sample = b'\x00\xAA' + b'\x00\x01' data = header + sample expected_results = [{'dio-0': False}, {'dio-0': True}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_multiple_dio(self): """ _parse_samples should properly parse a packet containing three samples of only digital io data """ # Three samples, ADC disabled and DIO8 enabled, DIO 0-7 enabled header = b'\x03\x01\xFF' # First 7 bits ignored # First sample: all bits on # Second sample: alternating bits on # Third sample: all bits off sample = b'\x01\xFF' + b'\x00\xAA' + b'\x00\x00' data = header + sample expected_results = [{'dio-0': True, 'dio-1': True, 'dio-2': True, 'dio-3': True, 'dio-4': True, 'dio-5': True, 'dio-6': True, 'dio-7': True, 'dio-8': True}, {'dio-0': False, 'dio-1': True, 'dio-2': False, 'dio-3': True, 'dio-4': False, 'dio-5': True, 'dio-6': False, 'dio-7': True, 'dio-8': False}, {'dio-0': False, 'dio-1': False, 'dio-2': False, 'dio-3': False, 'dio-4': False, 'dio-5': False, 'dio-6': False, 'dio-7': False, 'dio-8': False}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_multiple_adc_subset(self): """ _parse_samples should parse a data packet containing multiple samples of adc data from multiple pins in the proper order """ # One sample, ADC 0,1 enabled # DIO disabled header = b'\x02\x06\x00' # No dio data # ADC0 value of 0 # ADC1 value of 255 # ADC0 value of 5 # ADC1 value of 7 sample = b'\x00\x00' + b'\x00\xFF' + b'\x00\x05' + b'\x00\x07' data = header + sample expected_results = [{'adc-0': 0, 'adc-1': 255}, {'adc-0': 5, 'adc-1': 7}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) def test_parse_single_dio_adc_subset(self): """ _parse_samples should properly parse a packet containing a single sample of digital and analog io data for only a subset of the available pins """ # One sample, ADC 0 enabled # DIO 1,3,5,7 enabled header = b'\x01\x02\xAA' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating # ADC0 value of 255 sample = b'\x00\xAA\x00\xFF' data = header + sample expected_results = [{'dio-1': True, 'dio-3': True, 'dio-5': True, 'dio-7': True, 'adc-0': 255}] results = self.xbee._parse_samples(data) self.assertEqual(results, expected_results) class TestWriteToDevice(unittest.TestCase): """ XBee class should properly write binary data in a valid API frame to a given serial device, including a valid command packet. """ def test_send_at_command(self): """ calling send should write a full API frame containing the API AT command packet to the serial device. """ serial_port = Serial() xbee = XBee(serial_port) # Send an AT command xbee.send('at', frame_id=stringToBytes('A'), command=stringToBytes('MY')) # Expect a full packet to be written to the device expected_data = b'\x7E\x00\x04\x08AMY\x10' result_data = serial_port.get_data_written() self.assertEqual(result_data, expected_data) def test_send_at_command_with_param(self): """ calling send should write a full API frame containing the API AT command packet to the serial device. """ serial_port = Serial() xbee = XBee(serial_port) # Send an AT command xbee.send( 'at', frame_id=stringToBytes('A'), command=stringToBytes('MY'), parameter=b'\x00\x00' ) # Expect a full packet to be written to the device result_data = serial_port.get_data_written() expected_data = b'\x7E\x00\x06\x08AMY\x00\x00\x10' self.assertEqual(result_data, expected_data) class TestSendShorthand(unittest.TestCase): """ Tests shorthand for sending commands to an XBee provided by XBee.__getattr__ """ def setUp(self): """ Prepare a fake device to read from """ self.ser = Serial() self.xbee = XBee(self.ser) def test_send_at_command(self): """ Send an AT command with a shorthand call """ # Send an AT command self.xbee.at(frame_id=stringToBytes('A'), command=stringToBytes('MY')) # Expect a full packet to be written to the device result_data = self.ser.get_data_written() expected_data = b'\x7E\x00\x04\x08AMY\x10' self.assertEqual(result_data, expected_data) def test_send_at_command_with_param(self): """ calling send should write a full API frame containing the API AT command packet to the serial device. """ # Send an AT command self.xbee.at(frame_id=stringToBytes('A'), command=stringToBytes('MY'), parameter=b'\x00\x00') # Expect a full packet to be written to the device result_data = self.ser.get_data_written() expected_data = b'\x7E\x00\x06\x08AMY\x00\x00\x10' self.assertEqual(result_data, expected_data) def test_send_tx_with_close_brace(self): """ Calling tx where the given data string includes a close brace '}' must write correctly. """ self.xbee.tx(dest_addr=b'\x01\x02', data=b'{test=1}') result_data = self.ser.get_data_written() expected_data = b'\x7E\x00\x0D\x01\x00\x01\x02\x00{test=1}\xD5' self.assertEqual(result_data, expected_data) def test_shorthand_disabled(self): """ When shorthand is disabled, any attempt at calling a non-existant attribute should raise AttributeError """ self.xbee = XBee(self.ser, shorthand=False) try: self.xbee.at except AttributeError: pass else: self.fail("Specified shorthand command should not exist") class TestReadFromDevice(unittest.TestCase): """ XBee class should properly read and parse binary data from a serial port device. """ def test_read_at(self): """ read and parse a parameterless AT command """ device = Serial() device.set_read_data(b'\x7E\x00\x05\x88DMY\x01\x8c') xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01'} self.assertEqual(info, expected_info) def test_read_at_params(self): """ read and parse an AT command with a parameter """ device = Serial() device.set_read_data(b'\x7E\x00\x08\x88DMY\x01\x00\x00\x00\x8c') xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01', 'parameter': b'\x00\x00\x00'} self.assertEqual(info, expected_info) def test_is_response_parsed_as_io(self): """ I/O data in a AT response for an IS command is parsed. """ # Build IO data # One sample, ADC 0 enabled # DIO 1,3,5,7 enabled header = b'\x01\x02\xAA' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating # ADC0 value of 255 sample = b'\x00\xAA\x00\xFF' data = header + sample device = Serial() device.set_read_data(APIFrame(data=b'\x88DIS\x00' + data).output()) xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'IS', 'status': b'\x00', 'parameter': [{'dio-1': True, 'dio-3': True, 'dio-5': True, 'dio-7': True, 'adc-0': 255}]} self.assertEqual(info, expected_info) def test_is_remote_response_parsed_as_io(self): """ I/O data in a Remote AT response for an IS command is parsed. """ # Build IO data # One sample, ADC 0 enabled # DIO 1,3,5,7 enabled header = b'\x01\x02\xAA' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating # ADC0 value of 255 sample = b'\x00\xAA\x00\xFF' data = header + sample device = Serial() device.set_read_data( APIFrame( data=b'\x97D\x00\x13\xa2\x00@oG\xe4v\x1aIS\x00' + data ).output() ) xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'remote_at_response', 'frame_id': b'D', 'source_addr_long': b'\x00\x13\xa2\x00@oG\xe4', 'source_addr': b'v\x1a', 'command': b'IS', 'status': b'\x00', 'parameter': [{'dio-1': True, 'dio-3': True, 'dio-5': True, 'dio-7': True, 'adc-0': 255}]} self.assertEqual(info, expected_info) def test_read_io_data(self): """ XBee class should properly read and parse incoming IO data """ # Build IO data # One sample, ADC 0 enabled # DIO 1,3,5,7 enabled header = b'\x01\x02\xAA' # First 7 bits ignored, DIO8 low, DIO 0-7 alternating # ADC0 value of 255 sample = b'\x00\xAA\x00\xFF' data = header + sample # Wrap data in frame # RX frame data rx_io_resp = b'\x83\x00\x01\x28\x00' device = Serial() device.set_read_data(b'\x7E\x00\x0C' + rx_io_resp + data + b'\xfd') xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'rx_io_data', 'source_addr': b'\x00\x01', 'rssi': b'\x28', 'options': b'\x00', 'samples': [{'dio-1': True, 'dio-3': True, 'dio-5': True, 'dio-7': True, 'adc-0': 255}] } self.assertEqual(info, expected_info) def test_read_empty_string(self): """ Reading an empty string must not cause a crash Occasionally, the serial port fails to read properly, and returns an empty string. In this event, we must not crash. """ class BadReadDevice(Serial): def __init__(self, bad_read_index, data): self.read_id = 0 self.bad_read_index = bad_read_index super(BadReadDevice, self).__init__() self.set_read_data(data) def inWaiting(self): return 1 def read(self, length=1): if self.read_id == self.bad_read_index: self.read_id += 1 return '' else: self.read_id += 1 return super(BadReadDevice, self).read() badDevice = BadReadDevice(1, b'\x7E\x00\x05\x88DMY\x01\x8c') xbee = XBee(badDevice) try: xbee.wait_read_frame() except Exception: exc_type, exc_value, exc_traceback = sys.exc_info() self.fail("".join(traceback.format_exception( exc_type, exc_value, exc_traceback ))) def test_read_at_params_in_escaped_mode(self): """ read and parse an AT command with a parameter in escaped API mode """ device = Serial() device.set_read_data(b'~\x00\t\x88DMY\x01}^}]}1}3m') xbee = XBee(device, escaped=True) info = xbee.wait_read_frame() expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01', 'parameter': b'\x7E\x7D\x11\x13'} self.assertEqual(info, expected_info) def test_empty_frame_ignored(self): """ If an empty frame is received from a device, it must be ignored. """ device = Serial() device.set_read_data(b'\x7E\x00\x00\xFF\x7E\x00\x05\x88DMY\x01\x8c') xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'at_response', 'frame_id': b'D', 'command': b'MY', 'status': b'\x01'} self.assertEqual(info, expected_info) def test_read_rx_with_close_brace(self): """ An rx data frame including a close brace must be read properly. """ device = Serial() device.set_read_data(APIFrame(b'\x81\x01\x02\x55\x00{test=1}').output()) xbee = XBee(device) info = xbee.wait_read_frame() expected_info = {'id': 'rx', 'source_addr': b'\x01\x02', 'rssi': b'\x55', 'options': b'\x00', 'rf_data': b'{test=1}'} self.assertEqual(info, expected_info) def test_read_rx_with_close_brace_escaped(self): """ An escaped rx data frame including a close brace must be read properly. """ device = Serial() device.set_read_data(APIFrame(b'\x81\x01\x02\x55\x00{test=1}', escaped=True).output()) xbee = XBee(device, escaped=True) info = xbee.wait_read_frame() expected_info = {'id': 'rx', 'source_addr': b'\x01\x02', 'rssi': b'\x55', 'options': b'\x00', 'rf_data': b'{test=1}'} self.assertEqual(info, expected_info) if __name__ == '__main__': unittest.main()