mirror of
https://github.com/google/bumble.git
synced 2026-05-09 04:08:02 +00:00
Merge/rebase
This commit is contained in:
@@ -24,6 +24,10 @@ from bumble.company_ids import COMPANY_IDENTIFIERS
|
|||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import name_or_number
|
from bumble.core import name_or_number
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
|
HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND,
|
||||||
|
HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND,
|
||||||
|
HCI_Read_Local_Extended_Features_Command,
|
||||||
|
HCI_Read_Local_Supported_Features_Command,
|
||||||
map_null_terminated_utf8_string,
|
map_null_terminated_utf8_string,
|
||||||
HCI_SUCCESS,
|
HCI_SUCCESS,
|
||||||
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
||||||
@@ -58,6 +62,36 @@ def command_succeeded(response):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def get_common_info(host):
|
||||||
|
if host.supports_command(HCI_READ_LOCAL_SUPPORTED_FEATURES_COMMAND):
|
||||||
|
response = await host.send_command(HCI_Read_Local_Supported_Features_Command())
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
print()
|
||||||
|
print(color('LMP Features:', 'yellow'))
|
||||||
|
# TODO: support printing discrete enum values
|
||||||
|
print(' ', response.return_parameters.lmp_features.hex())
|
||||||
|
|
||||||
|
if host.supports_command(HCI_READ_LOCAL_EXTENDED_FEATURES_COMMAND):
|
||||||
|
response = await host.send_command(
|
||||||
|
HCI_Read_Local_Extended_Features_Command(page_number=0)
|
||||||
|
)
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
if response.return_parameters.max_page_number > 0:
|
||||||
|
print()
|
||||||
|
print(color('Extended LMP Features:', 'yellow'))
|
||||||
|
|
||||||
|
for page in range(1, response.return_parameters.max_page_number + 1):
|
||||||
|
response = await host.send_command(
|
||||||
|
HCI_Read_Local_Extended_Features_Command(page_number=page)
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
|
# TODO: support printing discrete enum values
|
||||||
|
print(f' Page {page}:')
|
||||||
|
print(' ', response.return_parameters.extended_lmp_features.hex())
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def get_classic_info(host):
|
async def get_classic_info(host):
|
||||||
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
if host.supports_command(HCI_READ_BD_ADDR_COMMAND):
|
||||||
@@ -162,6 +196,9 @@ async def async_main(transport):
|
|||||||
)
|
)
|
||||||
print(color(' LMP Subversion:', 'green'), host.local_version.lmp_subversion)
|
print(color(' LMP Subversion:', 'green'), host.local_version.lmp_subversion)
|
||||||
|
|
||||||
|
# Get the common info
|
||||||
|
await get_common_info(host)
|
||||||
|
|
||||||
# Get the Classic info
|
# Get the Classic info
|
||||||
await get_classic_info(host)
|
await get_classic_info(host)
|
||||||
|
|
||||||
|
|||||||
@@ -17,31 +17,25 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from bumble.controller import Controller
|
import click
|
||||||
|
|
||||||
|
from bumble.controller import Controller, Options
|
||||||
from bumble.link import LocalLink
|
from bumble.link import LocalLink
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
async def async_main():
|
async def async_main(extended_advertising, transport_names):
|
||||||
if len(sys.argv) != 3:
|
|
||||||
print(
|
|
||||||
'Usage: controllers.py <hci-transport-1> <hci-transport-2> '
|
|
||||||
'[<hci-transport-3> ...]'
|
|
||||||
)
|
|
||||||
print('example: python controllers.py pty:ble1 pty:ble2')
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create a local link to attach the controllers to
|
# Create a local link to attach the controllers to
|
||||||
link = LocalLink()
|
link = LocalLink()
|
||||||
|
|
||||||
# Create a transport and controller for all requested names
|
# Create a transport and controller for all requested names
|
||||||
transports = []
|
transports = []
|
||||||
controllers = []
|
controllers = []
|
||||||
for index, transport_name in enumerate(sys.argv[1:]):
|
options = Options(extended_advertising=extended_advertising)
|
||||||
|
for index, transport_name in enumerate(transport_names):
|
||||||
transport = await open_transport_or_link(transport_name)
|
transport = await open_transport_or_link(transport_name)
|
||||||
transports.append(transport)
|
transports.append(transport)
|
||||||
controller = Controller(
|
controller = Controller(
|
||||||
@@ -49,6 +43,7 @@ async def async_main():
|
|||||||
host_source=transport.source,
|
host_source=transport.source,
|
||||||
host_sink=transport.sink,
|
host_sink=transport.sink,
|
||||||
link=link,
|
link=link,
|
||||||
|
options=options,
|
||||||
)
|
)
|
||||||
controllers.append(controller)
|
controllers.append(controller)
|
||||||
|
|
||||||
@@ -61,9 +56,14 @@ async def async_main():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def main():
|
@click.command()
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
@click.option(
|
||||||
asyncio.run(async_main())
|
'--extended-advertising', is_flag=True, help="Enable extended advertising"
|
||||||
|
)
|
||||||
|
@click.argument('transports', nargs=-1, required=True)
|
||||||
|
def main(extended_advertising, transports):
|
||||||
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'WARNING').upper())
|
||||||
|
asyncio.run(async_main(extended_advertising, transports))
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ class Relay:
|
|||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
def main():
|
async def async_main():
|
||||||
# Check the Python version
|
# Check the Python version
|
||||||
if sys.version_info < (3, 6, 1):
|
if sys.version_info < (3, 6, 1):
|
||||||
print('ERROR: Python 3.6.1 or higher is required')
|
print('ERROR: Python 3.6.1 or higher is required')
|
||||||
@@ -280,8 +280,13 @@ def main():
|
|||||||
|
|
||||||
# Start a relay
|
# Start a relay
|
||||||
relay = Relay(args.port)
|
relay = Relay(args.port)
|
||||||
asyncio.get_event_loop().run_until_complete(relay.start())
|
async with relay.start():
|
||||||
asyncio.get_event_loop().run_forever()
|
await asyncio.Future()
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
def main():
|
||||||
|
asyncio.run(async_main())
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------------
|
# ----------------------------------------------------------------------------
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3266,7 +3266,9 @@ class HCI_Read_Local_Supported_Commands_Command(HCI_Command):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command()
|
@HCI_Command.command(
|
||||||
|
return_parameters_fields=[('status', STATUS_SPEC), ('lmp_features', 8)]
|
||||||
|
)
|
||||||
class HCI_Read_Local_Supported_Features_Command(HCI_Command):
|
class HCI_Read_Local_Supported_Features_Command(HCI_Command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ 7.4.3 Read Local Supported Features Command
|
See Bluetooth spec @ 7.4.3 Read Local Supported Features Command
|
||||||
@@ -3279,7 +3281,7 @@ class HCI_Read_Local_Supported_Features_Command(HCI_Command):
|
|||||||
return_parameters_fields=[
|
return_parameters_fields=[
|
||||||
('status', STATUS_SPEC),
|
('status', STATUS_SPEC),
|
||||||
('page_number', 1),
|
('page_number', 1),
|
||||||
('maximum_page_number', 1),
|
('max_page_number', 1),
|
||||||
('extended_lmp_features', 8),
|
('extended_lmp_features', 8),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -3448,7 +3450,9 @@ class HCI_LE_Set_Advertising_Parameters_Command(HCI_Command):
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command()
|
@HCI_Command.command(
|
||||||
|
return_parameters_fields=[('status', STATUS_SPEC), ('tx_power_level', 1)]
|
||||||
|
)
|
||||||
class HCI_LE_Read_Advertising_Physical_Channel_Tx_Power_Command(HCI_Command):
|
class HCI_LE_Read_Advertising_Physical_Channel_Tx_Power_Command(HCI_Command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec @ 7.8.6 LE Read Advertising Physical Channel Tx Power Command
|
See Bluetooth spec @ 7.8.6 LE Read Advertising Physical Channel Tx Power Command
|
||||||
@@ -5829,7 +5833,7 @@ class HCI_Inquiry_Result_With_RSSI_Event(HCI_Event):
|
|||||||
('status', STATUS_SPEC),
|
('status', STATUS_SPEC),
|
||||||
('connection_handle', 2),
|
('connection_handle', 2),
|
||||||
('page_number', 1),
|
('page_number', 1),
|
||||||
('maximum_page_number', 1),
|
('max_page_number', 1),
|
||||||
('extended_lmp_features', 8),
|
('extended_lmp_features', 8),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -428,8 +428,8 @@ class Host(AbortableEventEmitter):
|
|||||||
and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets
|
and self.acl_packets_in_flight < self.hc_total_num_le_acl_data_packets
|
||||||
):
|
):
|
||||||
packet = self.acl_packet_queue.pop()
|
packet = self.acl_packet_queue.pop()
|
||||||
self.send_hci_packet(packet)
|
|
||||||
self.acl_packets_in_flight += 1
|
self.acl_packets_in_flight += 1
|
||||||
|
self.send_hci_packet(packet)
|
||||||
|
|
||||||
def supports_command(self, command):
|
def supports_command(self, command):
|
||||||
# Find the support flag position for this command
|
# Find the support flag position for this command
|
||||||
@@ -568,7 +568,7 @@ class Host(AbortableEventEmitter):
|
|||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
color(
|
color(
|
||||||
'!!! {total_packets} completed but only '
|
f'!!! {total_packets} completed but only '
|
||||||
f'{self.acl_packets_in_flight} in flight'
|
f'{self.acl_packets_in_flight} in flight'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,11 +95,21 @@ class LocalLink:
|
|||||||
def on_address_changed(self, controller):
|
def on_address_changed(self, controller):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def send_advertising_data(self, sender_address, data):
|
def send_advertising_data(self, sender_address, data, scan_response):
|
||||||
# Send the advertising data to all controllers, except the sender
|
# Send the advertising data to all controllers, except the sender
|
||||||
for controller in self.controllers:
|
for controller in self.controllers:
|
||||||
if controller.random_address != sender_address:
|
if controller.random_address != sender_address:
|
||||||
controller.on_link_advertising_data(sender_address, data)
|
controller.on_link_advertising_data(sender_address, data, scan_response)
|
||||||
|
|
||||||
|
def send_extended_advertising_data(
|
||||||
|
self, sender_address, event_type, data, scan_response
|
||||||
|
):
|
||||||
|
# Send the advertising data to all controllers, except the sender
|
||||||
|
for controller in self.controllers:
|
||||||
|
if controller.random_address != sender_address:
|
||||||
|
controller.on_link_extended_advertising_data(
|
||||||
|
sender_address, event_type, data, scan_response
|
||||||
|
)
|
||||||
|
|
||||||
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
def send_acl_data(self, sender_controller, destination_address, transport, data):
|
||||||
# Send the data to the first controller with a matching address
|
# Send the data to the first controller with a matching address
|
||||||
@@ -151,30 +161,34 @@ class LocalLink:
|
|||||||
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
asyncio.get_running_loop().call_soon(self.on_connection_complete)
|
||||||
|
|
||||||
def on_disconnection_complete(
|
def on_disconnection_complete(
|
||||||
self, central_address, peripheral_address, disconnect_command
|
self, initiator_address, peer_address, disconnect_command
|
||||||
):
|
):
|
||||||
# Find the controller that initiated the disconnection
|
# Find the controller that initiated the disconnection
|
||||||
if not (central_controller := self.find_controller(central_address)):
|
if not (initiator_controller := self.find_controller(initiator_address)):
|
||||||
logger.warning('!!! Initiating controller not found')
|
logger.warning('!!! Initiating controller not found')
|
||||||
return
|
return
|
||||||
|
|
||||||
# Disconnect from the first controller with a matching address
|
# Disconnect from the first controller with a matching address
|
||||||
if peripheral_controller := self.find_controller(peripheral_address):
|
if peer_controller := self.find_controller(peer_address):
|
||||||
peripheral_controller.on_link_central_disconnected(
|
peer_controller.on_link_peer_disconnected(
|
||||||
central_address, disconnect_command.reason
|
initiator_address, disconnect_command.reason
|
||||||
)
|
)
|
||||||
|
|
||||||
central_controller.on_link_peripheral_disconnection_complete(
|
initiator_controller.on_link_initiated_disconnection_complete(
|
||||||
disconnect_command, HCI_SUCCESS
|
disconnect_command, HCI_SUCCESS
|
||||||
)
|
)
|
||||||
|
|
||||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
def disconnect(self, initiator_address, peer_address, disconnect_command):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'$$$ DISCONNECTION {central_address} -> '
|
f'$$$ DISCONNECTION {initiator_address} -> '
|
||||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
f'{peer_address}: reason = {disconnect_command.reason}'
|
||||||
|
)
|
||||||
|
asyncio.get_running_loop().call_soon(
|
||||||
|
self.on_disconnection_complete,
|
||||||
|
initiator_address,
|
||||||
|
peer_address,
|
||||||
|
disconnect_command,
|
||||||
)
|
)
|
||||||
args = [central_address, peripheral_address, disconnect_command]
|
|
||||||
asyncio.get_running_loop().call_soon(self.on_disconnection_complete, *args)
|
|
||||||
|
|
||||||
# pylint: disable=too-many-arguments
|
# pylint: disable=too-many-arguments
|
||||||
def on_connection_encrypted(
|
def on_connection_encrypted(
|
||||||
@@ -360,11 +374,11 @@ class RemoteLink:
|
|||||||
|
|
||||||
async def on_left_received(self, address):
|
async def on_left_received(self, address):
|
||||||
if address in self.central_connections:
|
if address in self.central_connections:
|
||||||
self.controller.on_link_peripheral_disconnected(Address(address))
|
self.controller.on_link_connection_lost(Address(address))
|
||||||
self.central_connections.remove(address)
|
self.central_connections.remove(address)
|
||||||
|
|
||||||
if address in self.peripheral_connections:
|
if address in self.peripheral_connections:
|
||||||
self.controller.on_link_central_disconnected(
|
self.controller.on_link_peer_disconnected(
|
||||||
address, HCI_CONNECTION_TIMEOUT_ERROR
|
address, HCI_CONNECTION_TIMEOUT_ERROR
|
||||||
)
|
)
|
||||||
self.peripheral_connections.remove(address)
|
self.peripheral_connections.remove(address)
|
||||||
@@ -384,7 +398,7 @@ class RemoteLink:
|
|||||||
async def on_advertisement_message_received(self, sender, advertisement):
|
async def on_advertisement_message_received(self, sender, advertisement):
|
||||||
try:
|
try:
|
||||||
self.controller.on_link_advertising_data(
|
self.controller.on_link_advertising_data(
|
||||||
Address(sender), bytes.fromhex(advertisement)
|
Address(sender), bytes.fromhex(advertisement), b''
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception('exception')
|
logger.exception('exception')
|
||||||
@@ -424,7 +438,7 @@ class RemoteLink:
|
|||||||
# Notify the controller
|
# Notify the controller
|
||||||
params = parse_parameters(message)
|
params = parse_parameters(message)
|
||||||
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
reason = int(params.get('reason', str(HCI_CONNECTION_TIMEOUT_ERROR)))
|
||||||
self.controller.on_link_central_disconnected(Address(sender), reason)
|
self.controller.on_link_peer_disconnected(Address(sender), reason)
|
||||||
|
|
||||||
# Forget the connection
|
# Forget the connection
|
||||||
if sender in self.peripheral_connections:
|
if sender in self.peripheral_connections:
|
||||||
@@ -471,7 +485,7 @@ class RemoteLink:
|
|||||||
async def send_advertising_data_to_relay(self, data):
|
async def send_advertising_data_to_relay(self, data):
|
||||||
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
await self.send_targeted_message('*', f'advertisement:{data.hex()}')
|
||||||
|
|
||||||
def send_advertising_data(self, _, data):
|
def send_advertising_data(self, _, data, scan_response):
|
||||||
self.execute(partial(self.send_advertising_data_to_relay, data))
|
self.execute(partial(self.send_advertising_data_to_relay, data))
|
||||||
|
|
||||||
async def send_acl_data_to_relay(self, peer_address, data):
|
async def send_acl_data_to_relay(self, peer_address, data):
|
||||||
|
|||||||
138
tests/controller_test.py
Normal file
138
tests/controller_test.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Copyright 2023 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Imports
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
from typing import List, Optional
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from bumble.device import Connection, Device
|
||||||
|
from bumble.host import Host
|
||||||
|
from bumble.link import LocalLink
|
||||||
|
from bumble.controller import Controller
|
||||||
|
from bumble.hci import (
|
||||||
|
Address,
|
||||||
|
HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR,
|
||||||
|
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR,
|
||||||
|
)
|
||||||
|
from bumble.transport import AsyncPipeSink
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class TwoDevices:
|
||||||
|
connections: List[Optional[Connection]]
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.connections = [None, None]
|
||||||
|
|
||||||
|
self.link = LocalLink()
|
||||||
|
self.controllers = [
|
||||||
|
Controller('C1', link=self.link),
|
||||||
|
Controller('C2', link=self.link),
|
||||||
|
]
|
||||||
|
self.devices = [
|
||||||
|
Device(
|
||||||
|
address=Address('F0:F1:F2:F3:F4:F5'),
|
||||||
|
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
|
||||||
|
),
|
||||||
|
Device(
|
||||||
|
address=Address('F5:F4:F3:F2:F1:F0'),
|
||||||
|
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
self.paired = [None, None]
|
||||||
|
|
||||||
|
def on_connection(self, which, connection):
|
||||||
|
self.connections[which] = connection
|
||||||
|
connection.on(
|
||||||
|
'disconnection', lambda reason: self.on_disconnection(which, reason)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_disconnection(self, which, _):
|
||||||
|
self.connections[which] = None
|
||||||
|
|
||||||
|
async def setup(self):
|
||||||
|
self.devices[0].on(
|
||||||
|
'connection', lambda connection: self.on_connection(0, connection)
|
||||||
|
)
|
||||||
|
self.devices[1].on(
|
||||||
|
'connection', lambda connection: self.on_connection(1, connection)
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.devices[0].power_on()
|
||||||
|
await self.devices[1].power_on()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_self_connection():
|
||||||
|
two_devices = TwoDevices()
|
||||||
|
await two_devices.setup()
|
||||||
|
|
||||||
|
await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
||||||
|
|
||||||
|
assert two_devices.connections[0] is not None
|
||||||
|
assert two_devices.connections[1] is not None
|
||||||
|
|
||||||
|
mock0 = MagicMock()
|
||||||
|
mock1 = MagicMock()
|
||||||
|
two_devices.connections[0].once('disconnection', mock0)
|
||||||
|
two_devices.connections[1].once('disconnection', mock1)
|
||||||
|
await two_devices.connections[0].disconnect(
|
||||||
|
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||||
|
)
|
||||||
|
mock0.assert_called_once_with(HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR)
|
||||||
|
mock1.assert_called_once_with(
|
||||||
|
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
assert two_devices.connections[0] is None
|
||||||
|
assert two_devices.connections[1] is None
|
||||||
|
|
||||||
|
await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
||||||
|
|
||||||
|
assert two_devices.connections[0] is not None
|
||||||
|
assert two_devices.connections[1] is not None
|
||||||
|
|
||||||
|
mock0 = MagicMock()
|
||||||
|
mock1 = MagicMock()
|
||||||
|
two_devices.connections[0].once('disconnection', mock0)
|
||||||
|
two_devices.connections[1].once('disconnection', mock1)
|
||||||
|
await two_devices.connections[1].disconnect(
|
||||||
|
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||||
|
)
|
||||||
|
mock1.assert_called_once_with(HCI_CONNECTION_TERMINATED_BY_LOCAL_HOST_ERROR)
|
||||||
|
mock0.assert_called_once_with(
|
||||||
|
HCI_REMOTE_DEVICE_TERMINATED_CONNECTION_DUE_TO_LOW_RESOURCES_ERROR
|
||||||
|
)
|
||||||
|
|
||||||
|
assert two_devices.connections[0] is None
|
||||||
|
assert two_devices.connections[1] is None
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
async def run_test_controller():
|
||||||
|
await test_self_connection()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
|
asyncio.run(run_test_controller())
|
||||||
Reference in New Issue
Block a user