Compare commits

..

2 Commits

Author SHA1 Message Date
Gilles Boccon-Gibod 2c2f512180 add comment to explain the initial role choice 2023-04-07 12:19:28 -07:00
Gilles Boccon-Gibod 859aea5a99 fix role state for classic connections 2023-04-07 10:24:26 -07:00
12 changed files with 73 additions and 167 deletions
-19
View File
@@ -200,22 +200,3 @@
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.
---
Files: bumble/colors.py
Copyright (c) 2012 Giorgos Verigakis <verigak@gmail.com>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+2 -48
View File
@@ -24,7 +24,6 @@ import logging
import os
import random
import re
import humanize
from typing import Optional, Union
from collections import OrderedDict
@@ -166,7 +165,6 @@ class ConsoleApp:
'local-services': None,
'remote-services': None,
'local-values': None,
'remote-values': None,
},
'filter': {
'address': None,
@@ -214,7 +212,6 @@ class ConsoleApp:
get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1))
)
self.local_values_text = FormattedTextControl()
self.remote_values_text = FormattedTextControl()
self.log_height = Dimension(min=7, weight=4)
self.log_max_lines = 100
self.log_lines = []
@@ -237,10 +234,6 @@ class ConsoleApp:
Frame(Window(self.remote_services_text), title='Remote Services'),
filter=Condition(lambda: self.top_tab == 'remote-services'),
),
ConditionalContainer(
Frame(Window(self.remote_values_text), title='Remote Values'),
filter=Condition(lambda: self.top_tab == 'remote-values'),
),
ConditionalContainer(
Frame(Window(self.log_text, height=self.log_height), title='Log'),
filter=Condition(lambda: self.top_tab == 'log'),
@@ -744,7 +737,6 @@ class ConsoleApp:
'local-services',
'remote-services',
'local-values',
'remote-values',
}:
self.top_tab = params[0]
self.ui.invalidate()
@@ -753,10 +745,6 @@ class ConsoleApp:
await self.do_show_local_values()
await asyncio.sleep(1)
while self.top_tab == 'remote-values':
await self.do_show_remote_values()
await asyncio.sleep(1)
async def do_show_local_values(self):
prettytable = PrettyTable()
field_names = ["Service", "Characteristic", "Descriptor"]
@@ -812,40 +800,6 @@ class ConsoleApp:
self.local_values_text.text = prettytable.get_string()
self.ui.invalidate()
async def do_show_remote_values(self):
prettytable = PrettyTable(
field_names=[
"Connection",
"Service",
"Characteristic",
"Descriptor",
"Time",
"Value",
]
)
for connection in self.device.connections.values():
for handle, (time, value) in connection.gatt_client.cached_values.items():
row = [connection.handle]
attribute = connection.gatt_client.get_attributes(handle)
if not attribute:
continue
if len(attribute) == 3:
row.extend(
[attribute[0].uuid, attribute[1].uuid, attribute[2].type]
)
elif len(attribute) == 2:
row.extend([attribute[0].uuid, attribute[1].uuid, ""])
elif len(attribute) == 1:
row.extend([attribute[0].uuid, "", ""])
else:
continue
row.extend([humanize.naturaltime(time), value])
prettytable.add_row(row)
self.remote_values_text.text = prettytable.get_string()
self.ui.invalidate()
async def do_get_phy(self, _):
if not self.connected_peer:
self.show_error('not connected')
@@ -945,9 +899,9 @@ class ConsoleApp:
# send data to any subscribers
if isinstance(attribute, Characteristic):
attribute.write_value(None, value)
if attribute.has_properties(Characteristic.NOTIFY):
if attribute.has_properties([Characteristic.NOTIFY]):
await self.device.gatt_server.notify_subscribers(attribute)
if attribute.has_properties(Characteristic.INDICATE):
if attribute.has_properties([Characteristic.INDICATE]):
await self.device.gatt_server.indicate_subscribers(attribute)
async def do_subscribe(self, params):
+11
View File
@@ -264,6 +264,7 @@ async def pair(
sc,
mitm,
bond,
ctkd,
io,
prompt,
request,
@@ -317,6 +318,7 @@ async def pair(
if mode == 'classic':
device.classic_enabled = True
device.le_enabled = False
device.classic_smp_enabled = ctkd
# Get things going
await device.power_on()
@@ -379,6 +381,13 @@ class LogHandler(logging.Handler):
@click.option(
'--bond', type=bool, default=True, help='Enable bonding', show_default=True
)
@click.option(
'--ctkd',
type=bool,
default=True,
help='Enable CTKD',
show_default=True,
)
@click.option(
'--io',
type=click.Choice(
@@ -405,6 +414,7 @@ def main(
sc,
mitm,
bond,
ctkd,
io,
prompt,
request,
@@ -427,6 +437,7 @@ def main(
sc,
mitm,
bond,
ctkd,
io,
prompt,
request,
+44 -26
View File
@@ -529,7 +529,6 @@ class Connection(CompositeEventEmitter):
authenticated: bool
sc: bool
link_key_type: int
gatt_client: gatt_client.Client
@composite_listener
class Listener:
@@ -596,7 +595,7 @@ class Connection(CompositeEventEmitter):
# [Classic only]
@classmethod
def incomplete(cls, device, peer_address):
def incomplete(cls, device, peer_address, role):
"""
Instantiate an incomplete connection (ie. one waiting for a HCI Connection
Complete event).
@@ -609,28 +608,30 @@ class Connection(CompositeEventEmitter):
device.public_address,
peer_address,
None,
None,
role,
None,
None,
)
# [Classic only]
def complete(self, handle, peer_resolvable_address, role, parameters):
def complete(self, handle, parameters):
"""
Finish an incomplete connection upon completion.
"""
assert self.handle is None
assert self.transport == BT_BR_EDR_TRANSPORT
self.handle = handle
self.peer_resolvable_address = peer_resolvable_address
# Quirk: role might be known before complete
if self.role is None:
self.role = role
self.parameters = parameters
@property
def role_name(self):
return 'CENTRAL' if self.role == BT_CENTRAL_ROLE else 'PERIPHERAL'
if self.role is None:
return 'NOT-SET'
if self.role == BT_CENTRAL_ROLE:
return 'CENTRAL'
if self.role == BT_PERIPHERAL_ROLE:
return 'PERIPHERAL'
return f'UNKNOWN[{self.role}]'
@property
def is_encrypted(self):
@@ -638,7 +639,7 @@ class Connection(CompositeEventEmitter):
@property
def is_incomplete(self) -> bool:
return self.handle == None
return self.handle is None
def send_l2cap_pdu(self, cid, pdu):
self.device.send_l2cap_pdu(self.handle, cid, pdu)
@@ -751,10 +752,11 @@ class DeviceConfiguration:
self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL
self.le_enabled = True
# LE host enable 2nd parameter
self.le_simultaneous_enabled = True
self.le_simultaneous_enabled = False
self.classic_enabled = False
self.classic_sc_enabled = True
self.classic_ssp_enabled = True
self.classic_smp_enabled = True
self.classic_accept_any = True
self.connectable = True
self.discoverable = True
@@ -789,6 +791,9 @@ class DeviceConfiguration:
self.classic_ssp_enabled = config.get(
'classic_ssp_enabled', self.classic_ssp_enabled
)
self.classic_smp_enabled = config.get(
'classic_smp_enabled', self.classic_smp_enabled
)
self.classic_accept_any = config.get(
'classic_accept_any', self.classic_accept_any
)
@@ -998,8 +1003,9 @@ class Device(CompositeEventEmitter):
self.le_enabled = config.le_enabled
self.classic_enabled = config.classic_enabled
self.le_simultaneous_enabled = config.le_simultaneous_enabled
self.classic_ssp_enabled = config.classic_ssp_enabled
self.classic_sc_enabled = config.classic_sc_enabled
self.classic_ssp_enabled = config.classic_ssp_enabled
self.classic_smp_enabled = config.classic_smp_enabled
self.discoverable = config.discoverable
self.connectable = config.connectable
self.classic_accept_any = config.classic_accept_any
@@ -1044,9 +1050,6 @@ class Device(CompositeEventEmitter):
# Setup SMP
self.smp_manager = smp.Manager(self)
self.l2cap_channel_manager.register_fixed_channel(smp.SMP_CID, self.on_smp_pdu)
self.l2cap_channel_manager.register_fixed_channel(
smp.SMP_BR_CID, self.on_smp_pdu
)
# Register the SDP server with the L2CAP Channel Manager
self.sdp_server.register(self.l2cap_channel_manager)
@@ -1183,6 +1186,12 @@ class Device(CompositeEventEmitter):
if self.keystore is None:
self.keystore = KeyStore.create_for_device(self)
# Finish setting up SMP based on post-init configurable options
if self.classic_smp_enabled:
self.l2cap_channel_manager.register_fixed_channel(
smp.SMP_BR_CID, self.on_smp_pdu
)
if self.host.supports_command(HCI_WRITE_LE_HOST_SUPPORT_COMMAND):
await self.send_command(
HCI_Write_LE_Host_Support_Command(
@@ -1611,7 +1620,7 @@ class Device(CompositeEventEmitter):
pending connection.
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
* None: use the 1M PHY with default parameters
* None: use all PHYs with default parameters
* map: each entry has a PHY as key and a ConnectionParametersPreferences
object as value
@@ -1680,7 +1689,9 @@ class Device(CompositeEventEmitter):
if connection_parameters_preferences is None:
if connection_parameters_preferences is None:
connection_parameters_preferences = {
HCI_LE_1M_PHY: ConnectionParametersPreferences.default
HCI_LE_1M_PHY: ConnectionParametersPreferences.default,
HCI_LE_2M_PHY: ConnectionParametersPreferences.default,
HCI_LE_CODED_PHY: ConnectionParametersPreferences.default,
}
self.connect_own_address_type = own_address_type
@@ -1802,7 +1813,7 @@ class Device(CompositeEventEmitter):
else:
# Save pending connection
self.pending_connections[peer_address] = Connection.incomplete(
self, peer_address
self, peer_address, BT_CENTRAL_ROLE
)
# TODO: allow passing other settings
@@ -1939,9 +1950,12 @@ class Device(CompositeEventEmitter):
self.on('connection', on_connection)
self.on('connection_failure', on_connection_failure)
# Save pending connection
# Save pending connection, with the Peripheral role.
# Even if we requested a role switch in the HCI_Accept_Connection_Request
# command, this connection is still considered Peripheral until an eventual
# role change event.
self.pending_connections[peer_address] = Connection.incomplete(
self, peer_address
self, peer_address, BT_PERIPHERAL_ROLE
)
try:
@@ -2214,6 +2228,9 @@ class Device(CompositeEventEmitter):
keys = await self.keystore.get(str(address))
if keys is not None:
logger.debug('found keys in the key store')
if keys.link_key is None:
logger.debug('no link key')
return None
return keys.link_key.value
# [Classic only]
@@ -2463,25 +2480,24 @@ class Device(CompositeEventEmitter):
connection_handle,
transport,
peer_address,
peer_resolvable_address,
role,
connection_parameters,
):
logger.debug(
f'*** Connection: [0x{connection_handle:04X}] '
f'{peer_address} as {HCI_Constant.role_name(role)}'
f'{peer_address} {"" if role is None else HCI_Constant.role_name(role)}'
)
if connection_handle in self.connections:
logger.warning(
'new connection reuses the same handle as a previous connection'
)
peer_resolvable_address = None
if transport == BT_BR_EDR_TRANSPORT:
# Create a new connection
connection = self.pending_connections.pop(peer_address)
connection.complete(
connection_handle, peer_resolvable_address, role, connection_parameters
)
connection.complete(connection_handle, connection_parameters)
self.connections[connection_handle] = connection
# Emit an event to notify listeners of the new connection
@@ -2593,7 +2609,9 @@ class Device(CompositeEventEmitter):
# device configuration is set to accept any incoming connection
elif self.classic_accept_any:
# Save pending connection
self.pending_connections[bd_addr] = Connection.incomplete(self, bd_addr)
self.pending_connections[bd_addr] = Connection.incomplete(
self, bd_addr, BT_PERIPHERAL_ROLE
)
self.host.send_command_sync(
HCI_Accept_Connection_Request_Command(
+2 -47
View File
@@ -27,8 +27,7 @@ from __future__ import annotations
import asyncio
import logging
import struct
from datetime import datetime
from typing import List, Optional, Dict, Tuple, Callable, Union, Any
from typing import List, Optional, Dict, Any, Callable
from pyee import EventEmitter
@@ -168,9 +167,7 @@ class CharacteristicProxy(AttributeProxy):
async def discover_descriptors(self):
return await self.client.discover_descriptors(self)
async def subscribe(
self, subscriber: Optional[Callable] = None, prefer_notify=True
):
async def subscribe(self, subscriber=None, prefer_notify=True):
if subscriber is not None:
if subscriber in self.subscribers:
# We already have a proxy subscriber
@@ -224,7 +221,6 @@ class ProfileServiceProxy:
# -----------------------------------------------------------------------------
class Client:
services: List[ServiceProxy]
cached_values: Dict[int, Tuple[datetime, bytes]]
def __init__(self, connection):
self.connection = connection
@@ -237,7 +233,6 @@ class Client:
) # Notification subscribers, by attribute handle
self.indication_subscribers = {} # Indication subscribers, by attribute handle
self.services = []
self.cached_values = {}
def send_gatt_pdu(self, pdu):
self.connection.send_l2cap_pdu(ATT_CID, pdu)
@@ -322,35 +317,6 @@ class Client:
if c.uuid == uuid
]
def get_attribute_grouping(
self, attribute_handle: int
) -> Optional[
Union[
ServiceProxy,
Tuple[ServiceProxy, CharacteristicProxy],
Tuple[ServiceProxy, CharacteristicProxy, DescriptorProxy],
]
]:
"""
Get the attribute(s) associated with an attribute handle
"""
for service in self.services:
if service.handle == attribute_handle:
return service
if service.handle <= attribute_handle <= service.end_group_handle:
for characteristic in service.characteristics:
if characteristic.handle == attribute_handle:
return (service, characteristic)
if (
characteristic.handle
<= attribute_handle
<= characteristic.end_group_handle
):
for descriptor in characteristic.descriptors:
if descriptor.handle == attribute_handle:
return (service, characteristic, descriptor)
return None
def on_service_discovered(self, service):
'''Add a service to the service list if it wasn't already there'''
already_known = False
@@ -842,7 +808,6 @@ class Client:
offset += len(part)
self.cache_value(attribute_handle, attribute_value)
# Return the value as bytes
return attribute_value
@@ -977,8 +942,6 @@ class Client:
)
if not subscribers:
logger.warning('!!! received notification with no subscriber')
self.cache_value(notification.attribute_handle, notification.attribute_value)
for subscriber in subscribers:
if callable(subscriber):
subscriber(notification.attribute_value)
@@ -990,8 +953,6 @@ class Client:
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
if not subscribers:
logger.warning('!!! received indication with no subscriber')
self.cache_value(indication.attribute_handle, indication.attribute_value)
for subscriber in subscribers:
if callable(subscriber):
subscriber(indication.attribute_value)
@@ -1000,9 +961,3 @@ class Client:
# Confirm that we received the indication
self.send_confirmation(ATT_Handle_Value_Confirmation())
def cache_value(self, attribute_handle: int, value: bytes):
self.cached_values[attribute_handle] = (
datetime.now(),
value,
)
+3 -13
View File
@@ -94,10 +94,9 @@ HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
# -----------------------------------------------------------------------------
class Connection:
def __init__(self, host, handle, role, peer_address, transport):
def __init__(self, host, handle, peer_address, transport):
self.host = host
self.handle = handle
self.role = role
self.peer_address = peer_address
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
self.transport = transport
@@ -534,7 +533,7 @@ class Host(AbortableEventEmitter):
if event.status == HCI_SUCCESS:
# Create/update the connection
logger.debug(
f'### CONNECTION: [0x{event.connection_handle:04X}] '
f'### LE CONNECTION: [0x{event.connection_handle:04X}] '
f'{event.peer_address} as {HCI_Constant.role_name(event.role)}'
)
@@ -543,7 +542,6 @@ class Host(AbortableEventEmitter):
connection = Connection(
self,
event.connection_handle,
event.role,
event.peer_address,
BT_LE_TRANSPORT,
)
@@ -560,7 +558,6 @@ class Host(AbortableEventEmitter):
event.connection_handle,
BT_LE_TRANSPORT,
event.peer_address,
None,
event.role,
connection_parameters,
)
@@ -589,7 +586,6 @@ class Host(AbortableEventEmitter):
connection = Connection(
self,
event.connection_handle,
BT_CENTRAL_ROLE,
event.bd_addr,
BT_BR_EDR_TRANSPORT,
)
@@ -602,7 +598,6 @@ class Host(AbortableEventEmitter):
BT_BR_EDR_TRANSPORT,
event.bd_addr,
None,
BT_CENTRAL_ROLE,
None,
)
else:
@@ -622,8 +617,7 @@ class Host(AbortableEventEmitter):
if event.status == HCI_SUCCESS:
logger.debug(
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
f'{connection.peer_address} as '
f'{HCI_Constant.role_name(connection.role)}, '
f'{connection.peer_address} '
f'reason={event.reason}'
)
del self.connections[event.connection_handle]
@@ -739,10 +733,6 @@ class Host(AbortableEventEmitter):
f'role change for {event.bd_addr}: '
f'{HCI_Constant.role_name(event.new_role)}'
)
if connection := self.find_connection_by_bd_addr(
event.bd_addr, BT_BR_EDR_TRANSPORT
):
connection.role = event.new_role
self.emit('role_change', event.bd_addr, event.new_role)
else:
logger.debug(
+1 -1
View File
@@ -273,7 +273,7 @@ class JsonKeyStore(KeyStore):
db = await self.load()
namespace = db.setdefault(self.namespace, {})
namespace[name] = keys.to_dict()
namespace.setdefault(name, {}).update(keys.to_dict())
await self.save(db)
+1 -1
View File
@@ -439,7 +439,7 @@ class DLC(EventEmitter):
logger.debug(
f'<<< Credits [{self.dlci}]: '
f'received {credits}, total={self.tx_credits}'
f'received {received_credits}, total={self.tx_credits}'
)
data = data[1:]
+5 -9
View File
@@ -553,7 +553,7 @@ class PairingConfig:
def __init__(
self,
sc: bool = True,
mitm: bool = True,
mitm: bool = False,
bonding: bool = True,
delegate: Optional[PairingDelegate] = None,
) -> None:
@@ -645,7 +645,7 @@ class Session:
},
}
def __init__(self, manager, connection, pairing_config, is_initiator):
def __init__(self, manager, connection, pairing_config):
self.manager = manager
self.connection = connection
self.preq = None
@@ -684,7 +684,7 @@ class Session:
self.ctkd_task = None
# Decide if we're the initiator or the responder
self.is_initiator = is_initiator
self.is_initiator = connection.role == BT_CENTRAL_ROLE
self.is_responder = not self.is_initiator
# Listen for connection events
@@ -1680,8 +1680,6 @@ class Manager(EventEmitter):
def on_smp_pdu(self, connection, pdu):
# Look for a session with this connection, and create one if none exists
if not (session := self.sessions.get(connection.handle)):
if connection.role == BT_CENTRAL_ROLE:
logger.warning('Remote starts pairing as Peripheral!')
pairing_config = self.pairing_config_factory(connection)
if pairing_config is None:
# Pairing disabled
@@ -1690,7 +1688,7 @@ class Manager(EventEmitter):
SMP_Pairing_Failed_Command(reason=SMP_PAIRING_NOT_SUPPORTED_ERROR),
)
return
session = Session(self, connection, pairing_config, is_initiator=False)
session = Session(self, connection, pairing_config)
self.sessions[connection.handle] = session
# Parse the L2CAP payload into an SMP Command object
@@ -1711,12 +1709,10 @@ class Manager(EventEmitter):
async def pair(self, connection):
# TODO: check if there's already a session for this connection
if connection.role != BT_CENTRAL_ROLE:
logger.warning('Start pairing as Peripheral!')
pairing_config = self.pairing_config_factory(connection)
if pairing_config is None:
raise ValueError('pairing config must not be None when initiating')
session = Session(self, connection, pairing_config, is_initiator=True)
session = Session(self, connection, pairing_config)
self.sessions[connection.handle] = session
return await session.pair()
+3 -1
View File
@@ -1,4 +1,6 @@
{
"name": "Bumble Hands-Free",
"class_of_device": 2360324
"class_of_device": 2360324,
"keystore": "JsonKeyStore",
"le_enabled": false
}
-1
View File
@@ -44,7 +44,6 @@ install_requires =
pyusb >= 1.2; platform_system!='Emscripten'
websockets >= 8.1; platform_system!='Emscripten'
prettytable >= 3.6.0
humanize >= 4.6.0
[options.entry_points]
console_scripts =
+1 -1
View File
@@ -447,7 +447,7 @@ async def test_self_smp_wrong_pin():
async def compare_numbers(self, number, digits):
return False
wrong_pin_pairing_config = PairingConfig(delegate=WrongPinDelegate())
wrong_pin_pairing_config = PairingConfig(mitm=True, delegate=WrongPinDelegate())
paired = False
try:
await _test_self_smp_with_configs(