forked from auracaster/bumble_mirror
Compare commits
2 Commits
v0.0.203
...
gbg/contro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07270240e3 | ||
|
|
d12b15b5d4 |
@@ -24,6 +24,10 @@ from bumble.company_ids import COMPANY_IDENTIFIERS
|
||||
from bumble.colors import color
|
||||
from bumble.core import name_or_number
|
||||
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,
|
||||
HCI_SUCCESS,
|
||||
HCI_LE_SUPPORTED_FEATURES_NAMES,
|
||||
@@ -58,6 +62,36 @@ def command_succeeded(response):
|
||||
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):
|
||||
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)
|
||||
|
||||
# Get the common info
|
||||
await get_common_info(host)
|
||||
|
||||
# Get the Classic info
|
||||
await get_classic_info(host)
|
||||
|
||||
|
||||
@@ -17,31 +17,25 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import os
|
||||
|
||||
from bumble.controller import Controller
|
||||
import click
|
||||
|
||||
from bumble.controller import Controller, Options
|
||||
from bumble.link import LocalLink
|
||||
from bumble.transport import open_transport_or_link
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
async def async_main():
|
||||
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
|
||||
|
||||
async def async_main(extended_advertising, transport_names):
|
||||
# Create a local link to attach the controllers to
|
||||
link = LocalLink()
|
||||
|
||||
# Create a transport and controller for all requested names
|
||||
transports = []
|
||||
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)
|
||||
transports.append(transport)
|
||||
controller = Controller(
|
||||
@@ -49,6 +43,7 @@ async def async_main():
|
||||
host_source=transport.source,
|
||||
host_sink=transport.sink,
|
||||
link=link,
|
||||
options=options,
|
||||
)
|
||||
controllers.append(controller)
|
||||
|
||||
@@ -61,9 +56,14 @@ async def async_main():
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
def main():
|
||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||
asyncio.run(async_main())
|
||||
@click.command()
|
||||
@click.option(
|
||||
'--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
|
||||
if sys.version_info < (3, 6, 1):
|
||||
print('ERROR: Python 3.6.1 or higher is required')
|
||||
@@ -280,8 +280,13 @@ def main():
|
||||
|
||||
# Start a relay
|
||||
relay = Relay(args.port)
|
||||
asyncio.get_event_loop().run_until_complete(relay.start())
|
||||
asyncio.get_event_loop().run_forever()
|
||||
async with relay.start():
|
||||
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):
|
||||
'''
|
||||
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=[
|
||||
('status', STATUS_SPEC),
|
||||
('page_number', 1),
|
||||
('maximum_page_number', 1),
|
||||
('max_page_number', 1),
|
||||
('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):
|
||||
'''
|
||||
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),
|
||||
('connection_handle', 2),
|
||||
('page_number', 1),
|
||||
('maximum_page_number', 1),
|
||||
('max_page_number', 1),
|
||||
('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
|
||||
):
|
||||
packet = self.acl_packet_queue.pop()
|
||||
self.send_hci_packet(packet)
|
||||
self.acl_packets_in_flight += 1
|
||||
self.send_hci_packet(packet)
|
||||
|
||||
def supports_command(self, command):
|
||||
# Find the support flag position for this command
|
||||
@@ -568,7 +568,7 @@ class Host(AbortableEventEmitter):
|
||||
else:
|
||||
logger.warning(
|
||||
color(
|
||||
'!!! {total_packets} completed but only '
|
||||
f'!!! {total_packets} completed but only '
|
||||
f'{self.acl_packets_in_flight} in flight'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -95,11 +95,21 @@ class LocalLink:
|
||||
def on_address_changed(self, controller):
|
||||
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
|
||||
for controller in self.controllers:
|
||||
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):
|
||||
# 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)
|
||||
|
||||
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
|
||||
if not (central_controller := self.find_controller(central_address)):
|
||||
if not (initiator_controller := self.find_controller(initiator_address)):
|
||||
logger.warning('!!! Initiating controller not found')
|
||||
return
|
||||
|
||||
# Disconnect from the first controller with a matching address
|
||||
if peripheral_controller := self.find_controller(peripheral_address):
|
||||
peripheral_controller.on_link_central_disconnected(
|
||||
central_address, disconnect_command.reason
|
||||
if peer_controller := self.find_controller(peer_address):
|
||||
peer_controller.on_link_peer_disconnected(
|
||||
initiator_address, disconnect_command.reason
|
||||
)
|
||||
|
||||
central_controller.on_link_peripheral_disconnection_complete(
|
||||
initiator_controller.on_link_initiated_disconnection_complete(
|
||||
disconnect_command, HCI_SUCCESS
|
||||
)
|
||||
|
||||
def disconnect(self, central_address, peripheral_address, disconnect_command):
|
||||
def disconnect(self, initiator_address, peer_address, disconnect_command):
|
||||
logger.debug(
|
||||
f'$$$ DISCONNECTION {central_address} -> '
|
||||
f'{peripheral_address}: reason = {disconnect_command.reason}'
|
||||
f'$$$ DISCONNECTION {initiator_address} -> '
|
||||
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
|
||||
def on_connection_encrypted(
|
||||
@@ -360,11 +374,11 @@ class RemoteLink:
|
||||
|
||||
async def on_left_received(self, address):
|
||||
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)
|
||||
|
||||
if address in self.peripheral_connections:
|
||||
self.controller.on_link_central_disconnected(
|
||||
self.controller.on_link_peer_disconnected(
|
||||
address, HCI_CONNECTION_TIMEOUT_ERROR
|
||||
)
|
||||
self.peripheral_connections.remove(address)
|
||||
@@ -384,7 +398,7 @@ class RemoteLink:
|
||||
async def on_advertisement_message_received(self, sender, advertisement):
|
||||
try:
|
||||
self.controller.on_link_advertising_data(
|
||||
Address(sender), bytes.fromhex(advertisement)
|
||||
Address(sender), bytes.fromhex(advertisement), b''
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('exception')
|
||||
@@ -424,7 +438,7 @@ class RemoteLink:
|
||||
# Notify the controller
|
||||
params = parse_parameters(message)
|
||||
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
|
||||
if sender in self.peripheral_connections:
|
||||
@@ -471,7 +485,7 @@ class RemoteLink:
|
||||
async def send_advertising_data_to_relay(self, data):
|
||||
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))
|
||||
|
||||
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