예제 #1
0
파일: test_packet.py 프로젝트: iotile/baBLE
def test_serialize():
    """ Test to serialize a packet (getting raw bytes from packet object). """
    packet = Packet(payload_type=Payload.Payload.ReadCentral,
                    controller_id=0,
                    connection_handle=0x0040,
                    attribute_handle=0x0003)

    assert packet.serialize() is not None
예제 #2
0
def write_without_response(self, controller_id, connection_handle, attribute_handle, value, timeout):

    @asyncio.coroutine
    def on_ack_received(packet, future):
        self.logger.debug("WriteWithoutResponse ack received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            future.set_result(True)
        else:
            error = BaBLEException(packet, "Failed to send WriteWithoutResponse packet",
                                   connection_handle=connection_handle,
                                   attribute_handle=attribute_handle)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(
        WriteWithoutResponseCentral,
        controller_id=controller_id,
        connection_handle=connection_handle,
        attribute_handle=attribute_handle,
        value=bytes(value)
    )

    self.register_callback(request_packet.packet_uuid, callback=on_ack_received, params={'future': future})

    self.send_packet(request_packet)

    try:
        result = yield from asyncio.wait_for(future, timeout=timeout)
        self.logger.debug("Write without response command sent")
        return result
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        raise RuntimeError("WriteWithoutResponse timed out")
예제 #3
0
파일: test_packet.py 프로젝트: iotile/baBLE
def test_build():
    """ Test building a packet. """
    packet = Packet.build(payload_module=ReadCentral,
                          controller_id=0,
                          connection_handle=0x0040,
                          attribute_handle=0x0003)

    assert isinstance(packet, Packet)
    assert packet.controller_id == 0
    assert packet.payload_type == Payload.Payload.ReadCentral
    assert packet.get('connection_handle') == 0x0040
    assert packet.get('attribute_handle') == 0x0003

    # Try to get a parameter that does not exist in the protocol
    with pytest.raises(KeyError):
        Packet.build(payload_module=ReadCentral, does_not_exist="test")
예제 #4
0
def list_controllers(self, timeout):

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("List of controllers received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            controllers = packet.get(
                name='controllers',
                format_function=lambda raw_ctrls: [Controller.from_flatbuffers(raw_ctrl) for raw_ctrl in raw_ctrls]
            )

            future.set_result(controllers)
        else:
            future.set_exception(BaBLEException(packet, "Failed to list controllers"))

    future = asyncio.Future()
    request_packet = Packet.build(GetControllersList)

    self.register_callback(
        request_packet.packet_uuid,
        callback=on_response_received,
        params={'future': future}
    )

    self.send_packet(request_packet)

    self.logger.debug("Waiting for list of controllers...")
    try:
        controllers = yield from asyncio.wait_for(future, timeout=timeout)
        return controllers
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        raise RuntimeError("List controllers timed out")
예제 #5
0
    def write(self, value):
        magic_code = value[:2]
        length = value[2:4]
        payload = value[4:]
        packet = Packet.extract(payload)

        self.input_buffer.append(packet)
        if packet.payload_type in self.callbacks:
            self.callbacks[packet.payload_type]()
예제 #6
0
파일: test_packet.py 프로젝트: iotile/baBLE
def test_extract():
    """ Test extract packet information from raw bytes. """
    # These raw bytes are from the ReadCentral packet built in test_build()
    raw_payload = bytearray(
        b'\x10\x00\x00\x00\x0c\x00\x10\x00\x0c\x00\x09\x00\x04\x00\x0a\x00\x0c\x00\x00\x00\x1c\x00'
        b'\x00\x00\x00\x0b\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x08\x00'
        b'\x06\x00\x04\x00\x08\x00\x00\x00\x03\x00\x40\x00')
    packet = Packet.extract(raw_payload)

    assert isinstance(packet, Packet)
    assert packet.controller_id == 0
    assert packet.payload_type == Payload.Payload.ReadCentral
    assert packet.get('connection_handle') == 0x0040
    assert packet.get('attribute_handle') == 0x0003

    # Test with a wrong payload bytes (random)
    raw_payload2 = bytearray(b'\x10\x00\x00\x00\x0c\x00')
    with pytest.raises(Exception):
        Packet.extract(raw_payload2)
예제 #7
0
def read(self, controller_id, connection_handle, attribute_handle, on_read,
         timeout):

    if isinstance(on_read, (tuple, list)):
        on_read_cb = on_read[0]
        on_read_params = on_read[1:]
    else:
        on_read_cb = on_read
        on_read_params = []

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("Read response received with status={}".format(
            packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            data = packet.get_dict([
                'controller_id', 'connection_handle', 'attribute_handle',
                ('value', bytes)
            ])

            on_read_cb(True, data, None, *on_read_params)

            future.set_result(data)
        else:
            error = BaBLEException(packet,
                                   "Failed to read value",
                                   connection_handle=connection_handle,
                                   attribute_handle=attribute_handle)
            on_read_cb(False, None, error, *on_read_params)

            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(ReadCentral,
                                  controller_id=controller_id,
                                  connection_handle=connection_handle,
                                  attribute_handle=attribute_handle)

    self.register_callback(request_packet.packet_uuid,
                           callback=on_response_received,
                           params={'future': future})

    self.send_packet(request_packet)

    self.logger.debug("Reading...")
    try:
        result = yield From(asyncio.wait_for(future, timeout=timeout))
        raise asyncio.Return(result)
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        on_read_cb(False, None, "Read timed out", *on_read_params)
        raise RuntimeError("Read timed out")
예제 #8
0
def probe_characteristics(self, controller_id, connection_handle, start_handle,
                          end_handle, on_characteristics_probed, timeout):

    if isinstance(on_characteristics_probed, (tuple, list)):
        on_characteristics_probed_cb = on_characteristics_probed[0]
        on_characteristics_probed_params = on_characteristics_probed[1:]
    else:
        on_characteristics_probed_cb = on_characteristics_probed
        on_characteristics_probed_params = []

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug(
            "Probe characteristics response received with status={}".format(
                packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            characteristics = packet.get_dict([
                'controller_id', 'connection_handle',
                ('characteristics', lambda chars:
                 [Characteristic.from_flatbuffers(char) for char in chars])
            ])
            on_characteristics_probed_cb(True, characteristics, None,
                                         *on_characteristics_probed_params)
            future.set_result(characteristics)
        else:
            error = BaBLEException(packet,
                                   "Failed to probe characteristics",
                                   connection_handle=connection_handle)
            on_characteristics_probed_cb(False, None, error,
                                         *on_characteristics_probed_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(ProbeCharacteristics,
                                  controller_id=controller_id,
                                  connection_handle=connection_handle,
                                  start_handle=start_handle,
                                  end_handle=end_handle)

    self.register_callback(request_packet.packet_uuid,
                           callback=on_response_received,
                           params={'future': future})

    self.send_packet(request_packet)

    self.logger.debug("Waiting for characteristics...")
    try:
        characteristics = yield From(asyncio.wait_for(future, timeout=timeout))
        raise asyncio.Return(characteristics)
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        raise RuntimeError("Probe characteristics timed out")
예제 #9
0
파일: test_packet.py 프로젝트: iotile/baBLE
def test_get():
    """ Test to get packet parameters. """
    packet = Packet(payload_type=Payload.Payload.ReadCentral,
                    controller_id=0,
                    connection_handle=0x0040,
                    attribute_handle=0x0003)

    # Test to get a single parameter
    assert packet.get('connection_handle') == 0x0040

    # Test to get a dictionary with multiple parameters
    expected_dict = {'connection_handle': 0x0040, 'attribute_handle': 0x0003}
    assert packet.get_dict(['connection_handle',
                            'attribute_handle']) == expected_dict

    # Test to get a dictionary with format function
    expected_dict['attribute_handle'] = 'test'
    assert packet.get_dict(
        ['connection_handle',
         ('attribute_handle', lambda value: 'test')]) == expected_dict
예제 #10
0
def test_send_packet(bridge_subprocess):
    """ Test sending a packet from CommandsManager. """
    command_manager = CommandsManager(bridge_subprocess)
    packet = Packet.build(StartScan, controller_id=0)

    command_manager.send_packet(packet)

    assert len(bridge_subprocess.stdin.input_buffer) == 1

    sent_packet = bridge_subprocess.stdin.input_buffer[0]
    assert sent_packet.payload_type == Payload.Payload.StartScan
    assert sent_packet.controller_id == 0
예제 #11
0
 def _generate_error(controller_id,
                     message,
                     native_class='HCI',
                     native_status=0x0c,
                     status=StatusCode.StatusCode.Denied,
                     **kwargs):
     return BaBLEException(packet=Packet(
         payload_type=Payload.Payload.BaBLEError,
         controller_id=controller_id,
         native_class=native_class,
         native_status=native_status,
         status=status,
         message=message),
                           **kwargs)
예제 #12
0
def test_handle(bridge_subprocess):
    """ Test if a packet is correctly handled if it is expected in CommandsManager. """
    def on_response_received(packet, test):
        assert packet.payload_type == Payload.Payload.StartScan
        assert test is True

    command_manager = CommandsManager(bridge_subprocess)

    # Create a request packet and register the response callback
    request_packet = Packet.build(StartScan, controller_id=0)
    command_manager.register_callback(request_packet.packet_uuid,
                                      callback=on_response_received,
                                      params={'test': True})

    # If wrong response packet is received, should not handle it
    wrong_response_packet = Packet.build(StartScan, controller_id=0)
    assert command_manager.handle(wrong_response_packet,
                                  lambda cb: None) is False

    # If expected response packet received, should be handled
    good_response_packet = Packet.build(StartScan, controller_id=0)
    good_response_packet.packet_uuid.set(uuid=request_packet.packet_uuid.uuid)
    assert command_manager.handle(good_response_packet,
                                  lambda cb: None) is True
예제 #13
0
def set_gatt_table(self, controller_id, services, characteristics, on_set, timeout):

    if isinstance(on_set, (tuple, list)):
        on_set_cb = on_set[0]
        on_set_params = on_set[1:]
    else:
        on_set_cb = on_set
        on_set_params = []

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("GATT table set with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            data = packet.get_dict(['controller_id'])

            on_set_cb(True, data, None, *on_set_params)
            future.set_result(data)
        else:
            error = BaBLEException(packet, "Failed to set GATT table")
            on_set_cb(False, None, error, *on_set_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(
        SetGATTTable,
        controller_id=controller_id,
        services=services,
        characteristics=characteristics
    )

    self.register_callback(
        request_packet.packet_uuid,
        callback=on_response_received,
        params={'future': future}
    )

    self.send_packet(request_packet)

    self.logger.debug("Waiting for setting GATT table response...")
    try:
        result = yield from asyncio.wait_for(future, timeout=timeout)
        return result
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        raise RuntimeError("Set GATT table timed out")
예제 #14
0
def cancel_connection(self, controller_id, on_connection_cancelled, timeout):

    if isinstance(on_connection_cancelled, (tuple, list)):
        on_connection_cancelled_cb = on_connection_cancelled[0]
        on_connection_cancelled_params = on_connection_cancelled[1:]
    else:
        on_connection_cancelled_cb = on_connection_cancelled
        on_connection_cancelled_params = []

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug(
            "Cancel connection response received with status={}".format(
                packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            on_connection_cancelled_cb(True, None, None,
                                       *on_connection_cancelled_params)
            future.set_result(True)
        else:
            error = BaBLEException(packet, "Failed to cancel connection")
            on_connection_cancelled_cb(False, None, error,
                                       *on_connection_cancelled_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(CancelConnection,
                                  controller_id=controller_id)

    self.register_callback(request_packet.packet_uuid,
                           callback=on_response_received,
                           params={'future': future})

    self.send_packet(request_packet)

    self.logger.debug("Waiting for connection to cancel...")
    try:
        result = yield From(asyncio.wait_for(future, timeout=timeout))
        raise asyncio.Return(result)
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        on_connection_cancelled_cb(False, None, "Cancel connection timed out",
                                   *on_connection_cancelled_params)
        raise RuntimeError("Cancel connection timed out")
예제 #15
0
def stop_scan(self, controller_id, on_scan_stopped, timeout):

    if isinstance(on_scan_stopped, (tuple, list)):
        on_scan_stopped_cb = on_scan_stopped[0]
        on_scan_stopped_params = on_scan_stopped[1:]
    else:
        on_scan_stopped_cb = on_scan_stopped
        on_scan_stopped_params = []

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("Stop scan response received with status={}".format(
            packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            self.remove_callback(
                PacketUuid(payload_type=Payload.DeviceFound,
                           controller_id=controller_id))
            on_scan_stopped_cb(True, packet.get_dict(['controller_id']), None,
                               *on_scan_stopped_params)
            future.set_result(True)
        else:
            error = BaBLEException(packet, "Failed to stop scan")
            on_scan_stopped_cb(False, None, error, *on_scan_stopped_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(StopScan, controller_id=controller_id)

    self.register_callback(request_packet.packet_uuid,
                           callback=on_response_received,
                           params={'future': future})

    self.send_packet(request_packet)

    self.logger.debug("Waiting for scan to stop...")
    try:
        result = yield From(asyncio.wait_for(future, timeout=timeout))
        raise asyncio.Return(result)
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        on_scan_stopped_cb(False, None, "Stop scan timed out",
                           *on_scan_stopped_params)
        raise RuntimeError("Stop scan timed out")
예제 #16
0
def notify(self, controller_id, connection_handle, attribute_handle, value,
           timeout):

    # TODO: use characteristic instead of attribute_handle

    @asyncio.coroutine
    def on_ack_received(packet, future):
        self.logger.debug(
            "EmitNotification ack received with status={}".format(
                packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            future.set_result(True)
        else:
            error = BaBLEException(packet,
                                   "Failed to send EmitNotification packet",
                                   connection_handle=connection_handle,
                                   attribute_handle=attribute_handle)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(EmitNotification,
                                  controller_id=controller_id,
                                  connection_handle=connection_handle,
                                  attribute_handle=attribute_handle,
                                  value=bytes(value))

    self.register_callback(request_packet.packet_uuid,
                           callback=on_ack_received,
                           params={'future': future})

    self.send_packet(request_packet)

    try:
        result = yield From(asyncio.wait_for(future, timeout=timeout))
        self.logger.debug("Notification sent")
        raise asyncio.Return(result)
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        raise RuntimeError("Notification timed out")
예제 #17
0
    def run(self):
        while True:
            header = bytearray()
            while len(header) < 4:
                header += self.file.read(1)

            if header[:2] != MAGIC_CODE:
                self.logger.error("Wrong magic code received: %s", header[:2])
                continue

            if sys.byteorder == 'little':  # depends on ENDIANNESS
                length = (header[3] << 8) | header[2]
            else:
                length = (header[2] << 8) | header[3]

            payload = bytearray()
            while len(payload) < length:
                payload += self.file.read(1)

            packet = Packet.extract(payload)
            if packet.payload_type == Payload.Exit:
                break

            self.on_receive(packet)
예제 #18
0
def disconnect(self, controller_id, connection_handle, on_disconnected, timeout):

    disconnected_event_uuid = PacketUuid(
        payload_type=Payload.DeviceDisconnected,
        controller_id=controller_id,
        connection_handle=connection_handle
    )

    if isinstance(on_disconnected, (tuple, list)):
        on_disconnected_cb = on_disconnected[0]
        on_disconnected_params = on_disconnected[1:]
    else:
        on_disconnected_cb = on_disconnected
        on_disconnected_params = []

    @asyncio.coroutine
    def on_device_disconnected(packet, future):
        self.logger.debug("Device disconnected event received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            data = packet.get_dict([
                'controller_id',
                'connection_handle',
                'reason',
                'code'
            ])

            self.remove_callback(
                PacketUuid(controller_id=controller_id, connection_handle=connection_handle),
                match_connection_only=True
            )
            on_disconnected_cb(True, data, None, *on_disconnected_params)
            future.set_result(data)
        else:
            error = BaBLEException(packet, "Failed to disconnect")
            on_disconnected_cb(False, None, error, *on_disconnected_params)
            future.set_exception(error)

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("Disconnect response received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code != StatusCode.Success:
            self.remove_callback(disconnected_event_uuid)

            error = BaBLEException(packet, "Failed to disconnect")
            on_disconnected_cb(False, None, error, *on_disconnected_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(Disconnect, controller_id=controller_id, connection_handle=connection_handle)

    self.register_callback(request_packet.packet_uuid, callback=on_response_received, params={'future': future})
    self.register_callback(
        disconnected_event_uuid,
        callback=on_device_disconnected,
        params={'future': future},
        replace=True
    )

    self.send_packet(request_packet)

    self.logger.debug("Disconnecting...")
    try:
        result = yield from asyncio.wait_for(future, timeout=timeout)
        return result
    except asyncio.TimeoutError:
        self.remove_callback(disconnected_event_uuid)
        on_disconnected_cb(False, None, "Disconnection timed out", *on_disconnected_params)
        raise RuntimeError("Disconnection timed out")
예제 #19
0
def set_advertising(self, controller_id, enabled, uuids, name, company_id, advertising_data, scan_response, on_set,
                    timeout):

    if isinstance(on_set, (tuple, list)):
        on_set_cb = on_set[0]
        on_set_params = on_set[1:]
    else:
        on_set_cb = on_set
        on_set_params = []

    if uuids is not None:
        if not isinstance(uuids, (tuple, list)):
            uuids = [uuids]

        for i, uuid in enumerate(uuids):
            if isinstance(uuid, UUID):
                uuids[i] = uuid_to_string(uuid)
            elif isinstance(uuid, string_types):
                uuids[i] = switch_endianness_string(uuid)
            else:
                raise ValueError("UUID must be either a uuid.UUID object or a string")

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("Set advertising response received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            data = packet.get_dict([
                'controller_id',
                'state'
            ])

            on_set_cb(True, data, None, *on_set_params)
            future.set_result(data)
        else:
            error = BaBLEException(packet, "Failed to set advertising")
            on_set_cb(False, None, error, *on_set_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(
        SetAdvertising,
        controller_id=controller_id,
        state=enabled,
        uuids=uuids,
        name=name,
        company_id=company_id,
        adv_manufacturer_data=advertising_data,
        scan_manufacturer_data=scan_response
    )

    self.register_callback(
        request_packet.packet_uuid,
        callback=on_response_received,
        params={'future': future}
    )

    self.send_packet(request_packet)

    self.logger.debug("Waiting for setting advertising response...")
    try:
        result = yield from asyncio.wait_for(future, timeout=timeout)
        return result
    except asyncio.TimeoutError:
        self.remove_callback(request_packet.packet_uuid)
        raise RuntimeError("Set advertising timed out")
예제 #20
0
def start_scan(self, controller_id, active_scan, on_device_found, on_scan_started, timeout):

    if isinstance(on_device_found, (tuple, list)):
        on_device_found_cb = on_device_found[0]
        on_device_found_params = on_device_found[1:]
    else:
        on_device_found_cb = on_device_found
        on_device_found_params = []

    if isinstance(on_scan_started, (tuple, list)):
        on_scan_started_cb = on_scan_started[0]
        on_scan_started_params = on_scan_started[1:]
    else:
        on_scan_started_cb = on_scan_started
        on_scan_started_params = []

    @asyncio.coroutine
    def on_device_found_event(packet):
        result = packet.get_dict([
            'controller_id',
            'type',
            ('address', lambda value: value.decode()),
            ('address_type', lambda value: 'public' if value == 0 else 'random'),
            'rssi',
            ('uuid', lambda value: string_to_uuid(value, input_byteorder='little')),
            'company_id',
            ('device_name', lambda value: value.decode()),
            ('manufacturer_data', bytes)
        ])

        on_device_found_cb(True, result, None, *on_device_found_params)

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("Start scan response received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            on_scan_started_cb(True, packet.get_dict(['controller_id']), None, *on_scan_started_params)
            future.set_result(True)
        else:
            self.remove_callback(PacketUuid(payload_type=Payload.DeviceFound, controller_id=controller_id))
            error = BaBLEException(packet, "Failed to start scan")
            on_scan_started_cb(False, None, error, *on_scan_started_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(StartScan, controller_id=controller_id, active_scan=active_scan)

    self.register_callback(
        PacketUuid(payload_type=Payload.DeviceFound, controller_id=controller_id),
        callback=on_device_found_event
    )
    self.register_callback(
        request_packet.packet_uuid,
        callback=on_response_received,
        params={'future': future}
    )

    self.send_packet(request_packet)

    self.logger.debug("Waiting for scan to start...")
    try:
        result = yield from asyncio.wait_for(future, timeout=timeout)
        return result
    except asyncio.TimeoutError:
        self.remove_callback([
            request_packet.packet_uuid,
            PacketUuid(payload_type=Payload.DeviceFound, controller_id=controller_id)
        ])
        on_scan_started_cb(False, None, "Start scan timed out", *on_scan_started_params)
        raise RuntimeError("Start scan timed out")
예제 #21
0
def connect(self, controller_id, address, address_type, connection_interval, on_connected_with_info, on_disconnected,
            timeout):

    if not isinstance(connection_interval, (tuple, list)) \
       or len(connection_interval) != 2 \
       or not all(isinstance(v, (int, float)) for v in connection_interval):
        raise ValueError("connection_interval must be a 2-number tuple or list ([min, max])")

    connected_event_uuid = PacketUuid(
        payload_type=Payload.DeviceConnected,
        controller_id=controller_id,
        address=address
    )

    if isinstance(on_connected_with_info, (tuple, list)):
        on_connected_cb = on_connected_with_info[0]
        on_connected_params = on_connected_with_info[1:]
    else:
        on_connected_cb = on_connected_with_info
        on_connected_params = []

    if isinstance(on_disconnected, (tuple, list)):
        on_disconnected_cb = on_disconnected[0]
        on_disconnected_params = on_disconnected[1:]
    else:
        on_disconnected_cb = on_disconnected
        on_disconnected_params = []

    @asyncio.coroutine
    def on_unexpected_disconnection(packet):
        self.logger.info("Unexpected disconnection event received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        data = packet.get_dict([
            'controller_id',
            'connection_handle',
            'reason',
            'code'
        ])
        self.remove_callback(
            PacketUuid(controller_id=controller_id, connection_handle=data['connection_handle']),
            match_connection_only=True
        )
        on_disconnected_cb(True, data, None, *on_disconnected_params)

    @asyncio.coroutine
    def on_connected(packet, future):
        self.logger.debug("Device connected event received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code == StatusCode.Success:
            device = packet.get_dict([
                'controller_id',
                'connection_handle',
                'address',
                ('address_type', lambda value: 'public' if value == 0 else 'random')
            ])

            self.register_callback(
                PacketUuid(payload_type=Payload.DeviceDisconnected,
                           controller_id=controller_id,
                           connection_handle=device['connection_handle']),
                callback=on_unexpected_disconnection
            )

            on_connected_cb(True, device, None, *on_connected_params)
            future.set_result(device)
        else:
            error = BaBLEException(packet, "Failed to connect", address=address)
            on_connected_cb(False, None, error, *on_connected_params)
            future.set_exception(error)
            return

    @asyncio.coroutine
    def on_response_received(packet, future):
        self.logger.debug("Connect response received with status={}".format(packet.status))
        self.remove_callback(packet.packet_uuid)

        if packet.status_code != StatusCode.Success:
            self.remove_callback(connected_event_uuid)
            error = BaBLEException(packet, "Failed to connect", address=address)
            on_connected_cb(False, None, error, *on_connected_params)
            future.set_exception(error)

    future = asyncio.Future()
    request_packet = Packet.build(
        Connect,
        controller_id=controller_id,
        address=address,
        address_type=0 if address_type == 'public' else 1,
        connection_interval_min=connection_interval[0],
        connection_interval_max=connection_interval[1]
    )

    self.register_callback(request_packet.packet_uuid, callback=on_response_received, params={'future': future})
    self.register_callback(connected_event_uuid, callback=on_connected, params={'future': future})

    self.send_packet(request_packet)

    self.logger.debug("Connecting...")
    try:
        result = yield from asyncio.wait_for(future, timeout=timeout)
        return result
    except asyncio.TimeoutError:
        self.remove_callback(connected_event_uuid)
        on_connected_cb(False, None, "Connection timed out", *on_connected_params)
        raise RuntimeError("Connection timed out")