forked from auracaster/bumble_mirror
Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd930e3bde | |||
| 9af426db45 | |||
| 4286b2ab59 | |||
| 3442358dea | |||
| bf3e05ef91 | |||
| 5351ab8a42 | |||
| 49b2c13e69 | |||
| 2c2f512180 | |||
| 859aea5a99 | |||
| 962737a97b | |||
| 85496aaff5 | |||
| a95e601a5c | |||
| df218b5370 | |||
| 0f737244b5 | |||
| a258ba383a | |||
| c53e1d2480 | |||
| 620c135ac4 | |||
| fca73a49a3 | |||
| cf70db84a1 | |||
| 7731c41f80 | |||
| 278341cbc0 | |||
| fb49a87494 | |||
| eba82b9d9a | |||
| 677fc77d3c | |||
| e026de295f | |||
| 52c15705e9 | |||
| 45ca0ef071 | |||
| e0af954baa | |||
| 044597de66 | |||
| fb68fa6a33 | |||
| b6fe7460ac | |||
| 5c59b6ca6d | |||
| dcd66743f6 | |||
| 423a5a95d8 | |||
| 6f1f185642 | |||
| 8e881fdb18 | |||
| 4907022398 | |||
| e93f71c035 | |||
| 94ff80563b | |||
| 552deab8a7 | |||
| a72beb1b06 | |||
| 7e62d4a81a | |||
| a50181e6b8 | |||
| 9e1358536b | |||
| 21d8a0d577 | |||
| a8e61673d0 | |||
| bd25cf27df | |||
| fdf2da7023 | |||
| dfb6734324 | |||
| 51ae6a5969 | |||
| 4fc13585cc | |||
| c5e5397ed8 | |||
| 4c6320f98a | |||
| cc0d56ad14 | |||
| 0019fa8e79 | |||
| 7ae1bf8959 | |||
| 9541cb6db0 | |||
| 1cd13dfc19 | |||
| d4346c3c9b | |||
| afe8765508 | |||
| 41d1772cb5 | |||
| 6e9078d60e | |||
| d5c7d0db57 | |||
| b70ebdef73 | |||
| 3af027e234 | |||
| 6e719ca9fd | |||
| 1a580d1c1e | |||
| aee7348687 | |||
| 864889ccab | |||
| fda00dcb28 | |||
| 77e5618ce7 | |||
| 6fa857ad13 | |||
| bc29f327ef | |||
| 1894b96de4 | |||
| 33ae047765 | |||
| 1efa2e9d44 | |||
| c34c5fdf17 |
@@ -200,3 +200,22 @@
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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.
|
||||||
|
|||||||
+1209
File diff suppressed because it is too large
Load Diff
+219
-17
@@ -24,9 +24,12 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
|
import humanize
|
||||||
|
from typing import Optional, Union
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from prettytable import PrettyTable
|
||||||
|
|
||||||
from prompt_toolkit import Application
|
from prompt_toolkit import Application
|
||||||
from prompt_toolkit.history import FileHistory
|
from prompt_toolkit.history import FileHistory
|
||||||
@@ -58,6 +61,7 @@ from bumble.device import ConnectionParametersPreferences, Device, Connection, P
|
|||||||
from bumble.utils import AsyncRunner
|
from bumble.utils import AsyncRunner
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
from bumble.gatt import Characteristic, Service, CharacteristicDeclaration, Descriptor
|
||||||
|
from bumble.gatt_client import CharacteristicProxy
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
HCI_Constant,
|
HCI_Constant,
|
||||||
HCI_LE_1M_PHY,
|
HCI_LE_1M_PHY,
|
||||||
@@ -119,9 +123,12 @@ def parse_phys(phys):
|
|||||||
# Console App
|
# Console App
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ConsoleApp:
|
class ConsoleApp:
|
||||||
|
connected_peer: Optional[Peer]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.known_addresses = set()
|
self.known_addresses = set()
|
||||||
self.known_attributes = []
|
self.known_remote_attributes = []
|
||||||
|
self.known_local_attributes = []
|
||||||
self.device = None
|
self.device = None
|
||||||
self.connected_peer = None
|
self.connected_peer = None
|
||||||
self.top_tab = 'device'
|
self.top_tab = 'device'
|
||||||
@@ -158,6 +165,8 @@ class ConsoleApp:
|
|||||||
'device': None,
|
'device': None,
|
||||||
'local-services': None,
|
'local-services': None,
|
||||||
'remote-services': None,
|
'remote-services': None,
|
||||||
|
'local-values': None,
|
||||||
|
'remote-values': None,
|
||||||
},
|
},
|
||||||
'filter': {
|
'filter': {
|
||||||
'address': None,
|
'address': None,
|
||||||
@@ -168,10 +177,11 @@ class ConsoleApp:
|
|||||||
'disconnect': None,
|
'disconnect': None,
|
||||||
'discover': {'services': None, 'attributes': None},
|
'discover': {'services': None, 'attributes': None},
|
||||||
'request-mtu': None,
|
'request-mtu': None,
|
||||||
'read': LiveCompleter(self.known_attributes),
|
'read': LiveCompleter(self.known_remote_attributes),
|
||||||
'write': LiveCompleter(self.known_attributes),
|
'write': LiveCompleter(self.known_remote_attributes),
|
||||||
'subscribe': LiveCompleter(self.known_attributes),
|
'local-write': LiveCompleter(self.known_local_attributes),
|
||||||
'unsubscribe': LiveCompleter(self.known_attributes),
|
'subscribe': LiveCompleter(self.known_remote_attributes),
|
||||||
|
'unsubscribe': LiveCompleter(self.known_remote_attributes),
|
||||||
'set-phy': {'1m': None, '2m': None, 'coded': None},
|
'set-phy': {'1m': None, '2m': None, 'coded': None},
|
||||||
'set-default-phy': None,
|
'set-default-phy': None,
|
||||||
'quit': None,
|
'quit': None,
|
||||||
@@ -203,6 +213,8 @@ class ConsoleApp:
|
|||||||
self.log_text = FormattedTextControl(
|
self.log_text = FormattedTextControl(
|
||||||
get_cursor_position=lambda: Point(0, max(0, len(self.log_lines) - 1))
|
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_height = Dimension(min=7, weight=4)
|
||||||
self.log_max_lines = 100
|
self.log_max_lines = 100
|
||||||
self.log_lines = []
|
self.log_lines = []
|
||||||
@@ -218,9 +230,17 @@ class ConsoleApp:
|
|||||||
filter=Condition(lambda: self.top_tab == 'local-services'),
|
filter=Condition(lambda: self.top_tab == 'local-services'),
|
||||||
),
|
),
|
||||||
ConditionalContainer(
|
ConditionalContainer(
|
||||||
Frame(Window(self.remote_services_text), title='Remove Services'),
|
Frame(Window(self.local_values_text), title='Local Values'),
|
||||||
|
filter=Condition(lambda: self.top_tab == 'local-values'),
|
||||||
|
),
|
||||||
|
ConditionalContainer(
|
||||||
|
Frame(Window(self.remote_services_text), title='Remote Services'),
|
||||||
filter=Condition(lambda: self.top_tab == '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(
|
ConditionalContainer(
|
||||||
Frame(Window(self.log_text, height=self.log_height), title='Log'),
|
Frame(Window(self.log_text, height=self.log_height), title='Log'),
|
||||||
filter=Condition(lambda: self.top_tab == 'log'),
|
filter=Condition(lambda: self.top_tab == 'log'),
|
||||||
@@ -362,17 +382,19 @@ class ConsoleApp:
|
|||||||
|
|
||||||
def show_remote_services(self, services):
|
def show_remote_services(self, services):
|
||||||
lines = []
|
lines = []
|
||||||
del self.known_attributes[:]
|
del self.known_remote_attributes[:]
|
||||||
for service in services:
|
for service in services:
|
||||||
lines.append(("ansicyan", f"{service}\n"))
|
lines.append(("ansicyan", f"{service}\n"))
|
||||||
|
|
||||||
for characteristic in service.characteristics:
|
for characteristic in service.characteristics:
|
||||||
lines.append(('ansimagenta', f' {characteristic} + \n'))
|
lines.append(('ansimagenta', f' {characteristic} + \n'))
|
||||||
self.known_attributes.append(
|
self.known_remote_attributes.append(
|
||||||
f'{service.uuid.to_hex_str()}.{characteristic.uuid.to_hex_str()}'
|
f'{service.uuid.to_hex_str()}.{characteristic.uuid.to_hex_str()}'
|
||||||
)
|
)
|
||||||
self.known_attributes.append(f'*.{characteristic.uuid.to_hex_str()}')
|
self.known_remote_attributes.append(
|
||||||
self.known_attributes.append(f'#{characteristic.handle:X}')
|
f'*.{characteristic.uuid.to_hex_str()}'
|
||||||
|
)
|
||||||
|
self.known_remote_attributes.append(f'#{characteristic.handle:X}')
|
||||||
for descriptor in characteristic.descriptors:
|
for descriptor in characteristic.descriptors:
|
||||||
lines.append(("ansigreen", f" {descriptor}\n"))
|
lines.append(("ansigreen", f" {descriptor}\n"))
|
||||||
|
|
||||||
@@ -381,12 +403,31 @@ class ConsoleApp:
|
|||||||
|
|
||||||
def show_local_services(self, attributes):
|
def show_local_services(self, attributes):
|
||||||
lines = []
|
lines = []
|
||||||
|
del self.known_local_attributes[:]
|
||||||
for attribute in attributes:
|
for attribute in attributes:
|
||||||
if isinstance(attribute, Service):
|
if isinstance(attribute, Service):
|
||||||
|
# Save the most recent service for use later
|
||||||
|
service = attribute
|
||||||
lines.append(("ansicyan", f"{attribute}\n"))
|
lines.append(("ansicyan", f"{attribute}\n"))
|
||||||
elif isinstance(attribute, (Characteristic, CharacteristicDeclaration)):
|
elif isinstance(attribute, Characteristic):
|
||||||
|
# CharacteristicDeclaration includes all info from Characteristic
|
||||||
|
# no need to print it twice
|
||||||
|
continue
|
||||||
|
elif isinstance(attribute, CharacteristicDeclaration):
|
||||||
|
# Save the most recent characteristic declaration for use later
|
||||||
|
characteristic_declaration = attribute
|
||||||
|
self.known_local_attributes.append(
|
||||||
|
f'{service.uuid.to_hex_str()}.{attribute.characteristic.uuid.to_hex_str()}'
|
||||||
|
)
|
||||||
|
self.known_local_attributes.append(
|
||||||
|
f'#{attribute.characteristic.handle:X}'
|
||||||
|
)
|
||||||
lines.append(("ansimagenta", f" {attribute}\n"))
|
lines.append(("ansimagenta", f" {attribute}\n"))
|
||||||
elif isinstance(attribute, Descriptor):
|
elif isinstance(attribute, Descriptor):
|
||||||
|
self.known_local_attributes.append(
|
||||||
|
f'{service.uuid.to_hex_str()}.{characteristic_declaration.characteristic.uuid.to_hex_str()}.{attribute.type.to_hex_str()}'
|
||||||
|
)
|
||||||
|
self.known_local_attributes.append(f'#{attribute.handle:X}')
|
||||||
lines.append(("ansigreen", f" {attribute}\n"))
|
lines.append(("ansigreen", f" {attribute}\n"))
|
||||||
else:
|
else:
|
||||||
lines.append(("ansiyellow", f"{attribute}\n"))
|
lines.append(("ansiyellow", f"{attribute}\n"))
|
||||||
@@ -490,7 +531,9 @@ class ConsoleApp:
|
|||||||
|
|
||||||
self.show_attributes(attributes)
|
self.show_attributes(attributes)
|
||||||
|
|
||||||
def find_characteristic(self, param):
|
def find_remote_characteristic(self, param) -> Optional[CharacteristicProxy]:
|
||||||
|
if not self.connected_peer:
|
||||||
|
return None
|
||||||
parts = param.split('.')
|
parts = param.split('.')
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
service_uuid = UUID(parts[0]) if parts[0] != '*' else None
|
||||||
@@ -510,6 +553,38 @@ class ConsoleApp:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def find_local_attribute(
|
||||||
|
self, param
|
||||||
|
) -> Optional[Union[Characteristic, Descriptor]]:
|
||||||
|
parts = param.split('.')
|
||||||
|
if len(parts) == 3:
|
||||||
|
service_uuid = UUID(parts[0])
|
||||||
|
characteristic_uuid = UUID(parts[1])
|
||||||
|
descriptor_uuid = UUID(parts[2])
|
||||||
|
return self.device.gatt_server.get_descriptor_attribute(
|
||||||
|
service_uuid, characteristic_uuid, descriptor_uuid
|
||||||
|
)
|
||||||
|
if len(parts) == 2:
|
||||||
|
service_uuid = UUID(parts[0])
|
||||||
|
characteristic_uuid = UUID(parts[1])
|
||||||
|
characteristic_attributes = (
|
||||||
|
self.device.gatt_server.get_characteristic_attributes(
|
||||||
|
service_uuid, characteristic_uuid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if characteristic_attributes:
|
||||||
|
return characteristic_attributes[1]
|
||||||
|
return None
|
||||||
|
elif len(parts) == 1:
|
||||||
|
if parts[0].startswith('#'):
|
||||||
|
attribute_handle = int(f'{parts[0][1:]}', 16)
|
||||||
|
attribute = self.device.gatt_server.get_attribute(attribute_handle)
|
||||||
|
if isinstance(attribute, (Characteristic, Descriptor)):
|
||||||
|
return attribute
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def rssi_monitor_loop(self):
|
async def rssi_monitor_loop(self):
|
||||||
while True:
|
while True:
|
||||||
if self.monitor_rssi and self.connected_peer:
|
if self.monitor_rssi and self.connected_peer:
|
||||||
@@ -668,10 +743,109 @@ class ConsoleApp:
|
|||||||
'device',
|
'device',
|
||||||
'local-services',
|
'local-services',
|
||||||
'remote-services',
|
'remote-services',
|
||||||
|
'local-values',
|
||||||
|
'remote-values',
|
||||||
}:
|
}:
|
||||||
self.top_tab = params[0]
|
self.top_tab = params[0]
|
||||||
self.ui.invalidate()
|
self.ui.invalidate()
|
||||||
|
|
||||||
|
while self.top_tab == 'local-values':
|
||||||
|
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"]
|
||||||
|
|
||||||
|
# if there's no connections, add a column just for value
|
||||||
|
if not self.device.connections:
|
||||||
|
field_names.append("Value")
|
||||||
|
|
||||||
|
# if there are connections, add a column for each connection's value
|
||||||
|
for connection in self.device.connections.values():
|
||||||
|
field_names.append(f"Connection {connection.handle}")
|
||||||
|
|
||||||
|
for attribute in self.device.gatt_server.attributes:
|
||||||
|
if isinstance(attribute, Characteristic):
|
||||||
|
service = self.device.gatt_server.get_attribute_group(
|
||||||
|
attribute.handle, Service
|
||||||
|
)
|
||||||
|
if not service:
|
||||||
|
continue
|
||||||
|
values = [
|
||||||
|
attribute.read_value(connection)
|
||||||
|
for connection in self.device.connections.values()
|
||||||
|
]
|
||||||
|
if not values:
|
||||||
|
values = [attribute.read_value(None)]
|
||||||
|
prettytable.add_row([f"{service.uuid}", attribute.uuid, ""] + values)
|
||||||
|
|
||||||
|
elif isinstance(attribute, Descriptor):
|
||||||
|
service = self.device.gatt_server.get_attribute_group(
|
||||||
|
attribute.handle, Service
|
||||||
|
)
|
||||||
|
if not service:
|
||||||
|
continue
|
||||||
|
characteristic = self.device.gatt_server.get_attribute_group(
|
||||||
|
attribute.handle, Characteristic
|
||||||
|
)
|
||||||
|
if not characteristic:
|
||||||
|
continue
|
||||||
|
values = [
|
||||||
|
attribute.read_value(connection)
|
||||||
|
for connection in self.device.connections.values()
|
||||||
|
]
|
||||||
|
if not values:
|
||||||
|
values = [attribute.read_value(None)]
|
||||||
|
|
||||||
|
# TODO: future optimization: convert CCCD value to human readable string
|
||||||
|
|
||||||
|
prettytable.add_row(
|
||||||
|
[service.uuid, characteristic.uuid, attribute.type] + values
|
||||||
|
)
|
||||||
|
|
||||||
|
prettytable.field_names = field_names
|
||||||
|
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, _):
|
async def do_get_phy(self, _):
|
||||||
if not self.connected_peer:
|
if not self.connected_peer:
|
||||||
self.show_error('not connected')
|
self.show_error('not connected')
|
||||||
@@ -714,7 +888,7 @@ class ConsoleApp:
|
|||||||
self.show_error('not connected')
|
self.show_error('not connected')
|
||||||
return
|
return
|
||||||
|
|
||||||
characteristic = self.find_characteristic(params[0])
|
characteristic = self.find_remote_characteristic(params[0])
|
||||||
if characteristic is None:
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
return
|
return
|
||||||
@@ -739,15 +913,43 @@ class ConsoleApp:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
value = str.encode(params[1]) # must be a string
|
value = str.encode(params[1]) # must be a string
|
||||||
|
|
||||||
characteristic = self.find_characteristic(params[0])
|
characteristic = self.find_remote_characteristic(params[0])
|
||||||
if characteristic is None:
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
return
|
return
|
||||||
|
|
||||||
# use write with response if supported
|
# use write with response if supported
|
||||||
with_response = characteristic.properties & Characteristic.WRITE
|
with_response = characteristic.properties & Characteristic.Properties.WRITE
|
||||||
await characteristic.write_value(value, with_response=with_response)
|
await characteristic.write_value(value, with_response=with_response)
|
||||||
|
|
||||||
|
async def do_local_write(self, params):
|
||||||
|
if len(params) != 2:
|
||||||
|
self.show_error(
|
||||||
|
'invalid syntax', 'expected local-write <attribute> <value>'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if params[1].upper().startswith("0X"):
|
||||||
|
value = bytes.fromhex(params[1][2:]) # parse as hex string
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = int(params[1]).to_bytes(2, "little") # try as 2 byte integer
|
||||||
|
except ValueError:
|
||||||
|
value = str.encode(params[1]) # must be a string
|
||||||
|
|
||||||
|
attribute = self.find_local_attribute(params[0])
|
||||||
|
if not attribute:
|
||||||
|
self.show_error('invalid syntax', 'unable to find attribute')
|
||||||
|
return
|
||||||
|
|
||||||
|
# send data to any subscribers
|
||||||
|
if isinstance(attribute, Characteristic):
|
||||||
|
attribute.write_value(None, value)
|
||||||
|
if attribute.has_properties(Characteristic.NOTIFY):
|
||||||
|
await self.device.gatt_server.notify_subscribers(attribute)
|
||||||
|
if attribute.has_properties(Characteristic.INDICATE):
|
||||||
|
await self.device.gatt_server.indicate_subscribers(attribute)
|
||||||
|
|
||||||
async def do_subscribe(self, params):
|
async def do_subscribe(self, params):
|
||||||
if not self.connected_peer:
|
if not self.connected_peer:
|
||||||
self.show_error('not connected')
|
self.show_error('not connected')
|
||||||
@@ -757,7 +959,7 @@ class ConsoleApp:
|
|||||||
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
||||||
return
|
return
|
||||||
|
|
||||||
characteristic = self.find_characteristic(params[0])
|
characteristic = self.find_remote_characteristic(params[0])
|
||||||
if characteristic is None:
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
return
|
return
|
||||||
@@ -777,7 +979,7 @@ class ConsoleApp:
|
|||||||
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
self.show_error('invalid syntax', 'expected subscribe <attribute>')
|
||||||
return
|
return
|
||||||
|
|
||||||
characteristic = self.find_characteristic(params[0])
|
characteristic = self.find_remote_characteristic(params[0])
|
||||||
if characteristic is None:
|
if characteristic is None:
|
||||||
self.show_error('no such characteristic')
|
self.show_error('no such characteristic')
|
||||||
return
|
return
|
||||||
|
|||||||
+16
-5
@@ -30,6 +30,8 @@ from bumble.hci import (
|
|||||||
HCI_VERSION_NAMES,
|
HCI_VERSION_NAMES,
|
||||||
LMP_VERSION_NAMES,
|
LMP_VERSION_NAMES,
|
||||||
HCI_Command,
|
HCI_Command,
|
||||||
|
HCI_Command_Complete_Event,
|
||||||
|
HCI_Command_Status_Event,
|
||||||
HCI_READ_BD_ADDR_COMMAND,
|
HCI_READ_BD_ADDR_COMMAND,
|
||||||
HCI_Read_BD_ADDR_Command,
|
HCI_Read_BD_ADDR_Command,
|
||||||
HCI_READ_LOCAL_NAME_COMMAND,
|
HCI_READ_LOCAL_NAME_COMMAND,
|
||||||
@@ -45,11 +47,20 @@ from bumble.host import Host
|
|||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def command_succeeded(response):
|
||||||
|
if isinstance(response, HCI_Command_Status_Event):
|
||||||
|
return response.status == HCI_SUCCESS
|
||||||
|
if isinstance(response, HCI_Command_Complete_Event):
|
||||||
|
return response.return_parameters.status == HCI_SUCCESS
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
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):
|
||||||
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
response = await host.send_command(HCI_Read_BD_ADDR_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
color('Classic Address:', 'yellow'), response.return_parameters.bd_addr
|
||||||
@@ -57,7 +68,7 @@ async def get_classic_info(host):
|
|||||||
|
|
||||||
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
if host.supports_command(HCI_READ_LOCAL_NAME_COMMAND):
|
||||||
response = await host.send_command(HCI_Read_Local_Name_Command())
|
response = await host.send_command(HCI_Read_Local_Name_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print()
|
print()
|
||||||
print(
|
print(
|
||||||
color('Local Name:', 'yellow'),
|
color('Local Name:', 'yellow'),
|
||||||
@@ -73,7 +84,7 @@ async def get_le_info(host):
|
|||||||
response = await host.send_command(
|
response = await host.send_command(
|
||||||
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
HCI_LE_Read_Number_Of_Supported_Advertising_Sets_Command()
|
||||||
)
|
)
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
color('LE Number Of Supported Advertising Sets:', 'yellow'),
|
||||||
response.return_parameters.num_supported_advertising_sets,
|
response.return_parameters.num_supported_advertising_sets,
|
||||||
@@ -84,7 +95,7 @@ async def get_le_info(host):
|
|||||||
response = await host.send_command(
|
response = await host.send_command(
|
||||||
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
HCI_LE_Read_Maximum_Advertising_Data_Length_Command()
|
||||||
)
|
)
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('LE Maximum Advertising Data Length:', 'yellow'),
|
color('LE Maximum Advertising Data Length:', 'yellow'),
|
||||||
response.return_parameters.max_advertising_data_length,
|
response.return_parameters.max_advertising_data_length,
|
||||||
@@ -93,7 +104,7 @@ async def get_le_info(host):
|
|||||||
|
|
||||||
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
if host.supports_command(HCI_LE_READ_MAXIMUM_DATA_LENGTH_COMMAND):
|
||||||
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
response = await host.send_command(HCI_LE_Read_Maximum_Data_Length_Command())
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if command_succeeded(response):
|
||||||
print(
|
print(
|
||||||
color('Maximum Data Length:', 'yellow'),
|
color('Maximum Data Length:', 'yellow'),
|
||||||
(
|
(
|
||||||
|
|||||||
+3
-4
@@ -230,13 +230,13 @@ class GattlinkNodeBridge(GattlinkL2capEndpoint, Device.Listener):
|
|||||||
)
|
)
|
||||||
self.tx_characteristic = Characteristic(
|
self.tx_characteristic = Characteristic(
|
||||||
GG_GATTLINK_TX_CHARACTERISTIC_UUID,
|
GG_GATTLINK_TX_CHARACTERISTIC_UUID,
|
||||||
Characteristic.NOTIFY,
|
Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
)
|
)
|
||||||
self.tx_characteristic.on('subscription', self.on_tx_subscription)
|
self.tx_characteristic.on('subscription', self.on_tx_subscription)
|
||||||
self.psm_characteristic = Characteristic(
|
self.psm_characteristic = Characteristic(
|
||||||
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID,
|
GG_GATTLINK_L2CAP_CHANNEL_PSM_CHARACTERISTIC_UUID,
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([psm, 0]),
|
bytes([psm, 0]),
|
||||||
)
|
)
|
||||||
@@ -339,8 +339,7 @@ async def run(
|
|||||||
|
|
||||||
# Create a UDP to TX bridge (receive from TX, send to UDP)
|
# Create a UDP to TX bridge (receive from TX, send to UDP)
|
||||||
bridge.tx_socket, _ = await loop.create_datagram_endpoint(
|
bridge.tx_socket, _ = await loop.create_datagram_endpoint(
|
||||||
# pylint: disable-next=unnecessary-lambda
|
asyncio.DatagramProtocol,
|
||||||
lambda: asyncio.DatagramProtocol(),
|
|
||||||
remote_addr=(send_host, send_port),
|
remote_addr=(send_host, send_port),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+21
-4
@@ -24,7 +24,7 @@ from prompt_toolkit.shortcuts import PromptSession
|
|||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.transport import open_transport_or_link
|
from bumble.transport import open_transport_or_link
|
||||||
from bumble.smp import PairingDelegate, PairingConfig
|
from bumble.pairing import PairingDelegate, PairingConfig
|
||||||
from bumble.smp import error_name as smp_error_name
|
from bumble.smp import error_name as smp_error_name
|
||||||
from bumble.keys import JsonKeyStore
|
from bumble.keys import JsonKeyStore
|
||||||
from bumble.core import ProtocolError
|
from bumble.core import ProtocolError
|
||||||
@@ -264,6 +264,7 @@ async def pair(
|
|||||||
sc,
|
sc,
|
||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
|
ctkd,
|
||||||
io,
|
io,
|
||||||
prompt,
|
prompt,
|
||||||
request,
|
request,
|
||||||
@@ -302,7 +303,8 @@ async def pair(
|
|||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(
|
CharacteristicValue(
|
||||||
read=read_with_error, write=write_with_error
|
read=read_with_error, write=write_with_error
|
||||||
@@ -316,6 +318,7 @@ async def pair(
|
|||||||
if mode == 'classic':
|
if mode == 'classic':
|
||||||
device.classic_enabled = True
|
device.classic_enabled = True
|
||||||
device.le_enabled = False
|
device.le_enabled = False
|
||||||
|
device.classic_smp_enabled = ctkd
|
||||||
|
|
||||||
# Get things going
|
# Get things going
|
||||||
await device.power_on()
|
await device.power_on()
|
||||||
@@ -342,8 +345,13 @@ async def pair(
|
|||||||
print(color(f'Pairing failed: {error}', 'red'))
|
print(color(f'Pairing failed: {error}', 'red'))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# Advertise so that peers can find us and connect
|
if mode == 'le':
|
||||||
await device.start_advertising(auto_restart=True)
|
# Advertise so that peers can find us and connect
|
||||||
|
await device.start_advertising(auto_restart=True)
|
||||||
|
else:
|
||||||
|
# Become discoverable and connectable
|
||||||
|
await device.set_discoverable(True)
|
||||||
|
await device.set_connectable(True)
|
||||||
|
|
||||||
# Run until the user asks to exit
|
# Run until the user asks to exit
|
||||||
await Waiter.instance.wait_until_terminated()
|
await Waiter.instance.wait_until_terminated()
|
||||||
@@ -378,6 +386,13 @@ class LogHandler(logging.Handler):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'--bond', type=bool, default=True, help='Enable bonding', show_default=True
|
'--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(
|
@click.option(
|
||||||
'--io',
|
'--io',
|
||||||
type=click.Choice(
|
type=click.Choice(
|
||||||
@@ -404,6 +419,7 @@ def main(
|
|||||||
sc,
|
sc,
|
||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
|
ctkd,
|
||||||
io,
|
io,
|
||||||
prompt,
|
prompt,
|
||||||
request,
|
request,
|
||||||
@@ -426,6 +442,7 @@ def main(
|
|||||||
sc,
|
sc,
|
||||||
mitm,
|
mitm,
|
||||||
bond,
|
bond,
|
||||||
|
ctkd,
|
||||||
io,
|
io,
|
||||||
prompt,
|
prompt,
|
||||||
request,
|
request,
|
||||||
|
|||||||
+40
-7
@@ -23,12 +23,13 @@
|
|||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import functools
|
||||||
import struct
|
import struct
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
from typing import Dict, Type, TYPE_CHECKING
|
from typing import Dict, Type, TYPE_CHECKING
|
||||||
|
|
||||||
from bumble.core import UUID, name_or_number
|
from bumble.core import UUID, name_or_number, get_dict_key_by_value, ProtocolError
|
||||||
from bumble.hci import HCI_Object, key_with_value
|
from bumble.hci import HCI_Object, key_with_value, HCI_Constant
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -184,13 +185,18 @@ UUID_2_FIELD_SPEC = lambda x, y: UUID.parse_uuid_2(x, y) # noqa: E731
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Exceptions
|
# Exceptions
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class ATT_Error(Exception):
|
class ATT_Error(ProtocolError):
|
||||||
def __init__(self, error_code, att_handle=0x0000):
|
def __init__(self, error_code, att_handle=0x0000, message=''):
|
||||||
self.error_code = error_code
|
super().__init__(
|
||||||
|
error_code,
|
||||||
|
error_namespace='att',
|
||||||
|
error_name=ATT_PDU.error_name(error_code),
|
||||||
|
)
|
||||||
self.att_handle = att_handle
|
self.att_handle = att_handle
|
||||||
|
self.message = message
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f'ATT_Error({ATT_PDU.error_name(self.error_code)})'
|
return f'ATT_Error(error={self.error_name}, handle={self.att_handle:04X}): {self.message}'
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -725,11 +731,38 @@ class Attribute(EventEmitter):
|
|||||||
READ_REQUIRES_AUTHORIZATION = 0x40
|
READ_REQUIRES_AUTHORIZATION = 0x40
|
||||||
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
WRITE_REQUIRES_AUTHORIZATION = 0x80
|
||||||
|
|
||||||
|
PERMISSION_NAMES = {
|
||||||
|
READABLE: 'READABLE',
|
||||||
|
WRITEABLE: 'WRITEABLE',
|
||||||
|
READ_REQUIRES_ENCRYPTION: 'READ_REQUIRES_ENCRYPTION',
|
||||||
|
WRITE_REQUIRES_ENCRYPTION: 'WRITE_REQUIRES_ENCRYPTION',
|
||||||
|
READ_REQUIRES_AUTHENTICATION: 'READ_REQUIRES_AUTHENTICATION',
|
||||||
|
WRITE_REQUIRES_AUTHENTICATION: 'WRITE_REQUIRES_AUTHENTICATION',
|
||||||
|
READ_REQUIRES_AUTHORIZATION: 'READ_REQUIRES_AUTHORIZATION',
|
||||||
|
WRITE_REQUIRES_AUTHORIZATION: 'WRITE_REQUIRES_AUTHORIZATION',
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def string_to_permissions(permissions_str: str):
|
||||||
|
try:
|
||||||
|
return functools.reduce(
|
||||||
|
lambda x, y: x | get_dict_key_by_value(Attribute.PERMISSION_NAMES, y),
|
||||||
|
permissions_str.split(","),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
except TypeError as exc:
|
||||||
|
raise TypeError(
|
||||||
|
f"Attribute::permissions error:\nExpected a string containing any of the keys, separated by commas: {','.join(Attribute.PERMISSION_NAMES.values())}\nGot: {permissions_str}"
|
||||||
|
) from exc
|
||||||
|
|
||||||
def __init__(self, attribute_type, permissions, value=b''):
|
def __init__(self, attribute_type, permissions, value=b''):
|
||||||
EventEmitter.__init__(self)
|
EventEmitter.__init__(self)
|
||||||
self.handle = 0
|
self.handle = 0
|
||||||
self.end_group_handle = 0
|
self.end_group_handle = 0
|
||||||
self.permissions = permissions
|
if isinstance(permissions, str):
|
||||||
|
self.permissions = self.string_to_permissions(permissions)
|
||||||
|
else:
|
||||||
|
self.permissions = permissions
|
||||||
|
|
||||||
# Convert the type to a UUID object if it isn't already
|
# Convert the type to a UUID object if it isn't already
|
||||||
if isinstance(attribute_type, str):
|
if isinstance(attribute_type, str):
|
||||||
|
|||||||
+236
-32
@@ -21,7 +21,12 @@ import itertools
|
|||||||
import random
|
import random
|
||||||
import struct
|
import struct
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.core import BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE
|
from bumble.core import (
|
||||||
|
BT_CENTRAL_ROLE,
|
||||||
|
BT_PERIPHERAL_ROLE,
|
||||||
|
BT_LE_TRANSPORT,
|
||||||
|
BT_BR_EDR_TRANSPORT,
|
||||||
|
)
|
||||||
|
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
HCI_ACL_DATA_PACKET,
|
HCI_ACL_DATA_PACKET,
|
||||||
@@ -29,17 +34,21 @@ from bumble.hci import (
|
|||||||
HCI_COMMAND_PACKET,
|
HCI_COMMAND_PACKET,
|
||||||
HCI_COMMAND_STATUS_PENDING,
|
HCI_COMMAND_STATUS_PENDING,
|
||||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||||
|
HCI_CONTROLLER_BUSY_ERROR,
|
||||||
HCI_EVENT_PACKET,
|
HCI_EVENT_PACKET,
|
||||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||||
HCI_LE_1M_PHY,
|
HCI_LE_1M_PHY,
|
||||||
HCI_SUCCESS,
|
HCI_SUCCESS,
|
||||||
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
HCI_UNKNOWN_HCI_COMMAND_ERROR,
|
||||||
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||||
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
HCI_VERSION_BLUETOOTH_CORE_5_0,
|
||||||
Address,
|
Address,
|
||||||
HCI_AclDataPacket,
|
HCI_AclDataPacket,
|
||||||
HCI_AclDataPacketAssembler,
|
HCI_AclDataPacketAssembler,
|
||||||
HCI_Command_Complete_Event,
|
HCI_Command_Complete_Event,
|
||||||
HCI_Command_Status_Event,
|
HCI_Command_Status_Event,
|
||||||
|
HCI_Connection_Complete_Event,
|
||||||
|
HCI_Connection_Request_Event,
|
||||||
HCI_Disconnection_Complete_Event,
|
HCI_Disconnection_Complete_Event,
|
||||||
HCI_Encryption_Change_Event,
|
HCI_Encryption_Change_Event,
|
||||||
HCI_LE_Advertising_Report_Event,
|
HCI_LE_Advertising_Report_Event,
|
||||||
@@ -47,7 +56,9 @@ from bumble.hci import (
|
|||||||
HCI_LE_Read_Remote_Features_Complete_Event,
|
HCI_LE_Read_Remote_Features_Complete_Event,
|
||||||
HCI_Number_Of_Completed_Packets_Event,
|
HCI_Number_Of_Completed_Packets_Event,
|
||||||
HCI_Packet,
|
HCI_Packet,
|
||||||
|
HCI_Role_Change_Event,
|
||||||
)
|
)
|
||||||
|
from typing import Optional, Union, Dict
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -65,13 +76,14 @@ class DataObject:
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Connection:
|
class Connection:
|
||||||
def __init__(self, controller, handle, role, peer_address, link):
|
def __init__(self, controller, handle, role, peer_address, link, transport):
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.role = role
|
self.role = role
|
||||||
self.peer_address = peer_address
|
self.peer_address = peer_address
|
||||||
self.link = link
|
self.link = link
|
||||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||||
|
self.transport = transport
|
||||||
|
|
||||||
def on_hci_acl_data_packet(self, packet):
|
def on_hci_acl_data_packet(self, packet):
|
||||||
self.assembler.feed_packet(packet)
|
self.assembler.feed_packet(packet)
|
||||||
@@ -82,23 +94,33 @@ class Connection:
|
|||||||
def on_acl_pdu(self, data):
|
def on_acl_pdu(self, data):
|
||||||
if self.link:
|
if self.link:
|
||||||
self.link.send_acl_data(
|
self.link.send_acl_data(
|
||||||
self.controller.random_address, self.peer_address, data
|
self.controller, self.peer_address, self.transport, data
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Controller:
|
class Controller:
|
||||||
def __init__(self, name, host_source=None, host_sink=None, link=None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
host_source=None,
|
||||||
|
host_sink=None,
|
||||||
|
link=None,
|
||||||
|
public_address: Optional[Union[bytes, str, Address]] = None,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.hci_sink = None
|
self.hci_sink = None
|
||||||
self.link = link
|
self.link = link
|
||||||
|
|
||||||
self.central_connections = (
|
self.central_connections: Dict[
|
||||||
{}
|
Address, Connection
|
||||||
) # Connections where this controller is the central
|
] = {} # Connections where this controller is the central
|
||||||
self.peripheral_connections = (
|
self.peripheral_connections: Dict[
|
||||||
{}
|
Address, Connection
|
||||||
) # Connections where this controller is the peripheral
|
] = {} # Connections where this controller is the peripheral
|
||||||
|
self.classic_connections: Dict[
|
||||||
|
Address, Connection
|
||||||
|
] = {} # Connections in BR/EDR
|
||||||
|
|
||||||
self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0
|
self.hci_version = HCI_VERSION_BLUETOOTH_CORE_5_0
|
||||||
self.hci_revision = 0
|
self.hci_revision = 0
|
||||||
@@ -148,7 +170,14 @@ class Controller:
|
|||||||
self.advertising_timer_handle = None
|
self.advertising_timer_handle = None
|
||||||
|
|
||||||
self._random_address = Address('00:00:00:00:00:00')
|
self._random_address = Address('00:00:00:00:00:00')
|
||||||
self._public_address = None
|
if isinstance(public_address, Address):
|
||||||
|
self._public_address = public_address
|
||||||
|
elif public_address is not None:
|
||||||
|
self._public_address = Address(
|
||||||
|
public_address, Address.PUBLIC_DEVICE_ADDRESS
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._public_address = Address('00:00:00:00:00:00')
|
||||||
|
|
||||||
# Set the source and sink interfaces
|
# Set the source and sink interfaces
|
||||||
if host_source:
|
if host_source:
|
||||||
@@ -271,7 +300,9 @@ class Controller:
|
|||||||
handle = 0
|
handle = 0
|
||||||
max_handle = 0
|
max_handle = 0
|
||||||
for connection in itertools.chain(
|
for connection in itertools.chain(
|
||||||
self.central_connections.values(), self.peripheral_connections.values()
|
self.central_connections.values(),
|
||||||
|
self.peripheral_connections.values(),
|
||||||
|
self.classic_connections.values(),
|
||||||
):
|
):
|
||||||
max_handle = max(max_handle, connection.handle)
|
max_handle = max(max_handle, connection.handle)
|
||||||
if connection.handle == handle:
|
if connection.handle == handle:
|
||||||
@@ -279,14 +310,19 @@ class Controller:
|
|||||||
handle = max_handle + 1
|
handle = max_handle + 1
|
||||||
return handle
|
return handle
|
||||||
|
|
||||||
def find_connection_by_address(self, address):
|
def find_le_connection_by_address(self, address):
|
||||||
return self.central_connections.get(address) or self.peripheral_connections.get(
|
return self.central_connections.get(address) or self.peripheral_connections.get(
|
||||||
address
|
address
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def find_classic_connection_by_address(self, address):
|
||||||
|
return self.classic_connections.get(address)
|
||||||
|
|
||||||
def find_connection_by_handle(self, handle):
|
def find_connection_by_handle(self, handle):
|
||||||
for connection in itertools.chain(
|
for connection in itertools.chain(
|
||||||
self.central_connections.values(), self.peripheral_connections.values()
|
self.central_connections.values(),
|
||||||
|
self.peripheral_connections.values(),
|
||||||
|
self.classic_connections.values(),
|
||||||
):
|
):
|
||||||
if connection.handle == handle:
|
if connection.handle == handle:
|
||||||
return connection
|
return connection
|
||||||
@@ -298,6 +334,12 @@ class Controller:
|
|||||||
return connection
|
return connection
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def find_classic_connection_by_handle(self, handle):
|
||||||
|
for connection in self.classic_connections.values():
|
||||||
|
if connection.handle == handle:
|
||||||
|
return connection
|
||||||
|
return None
|
||||||
|
|
||||||
def on_link_central_connected(self, central_address):
|
def on_link_central_connected(self, central_address):
|
||||||
'''
|
'''
|
||||||
Called when an incoming connection occurs from a central on the link
|
Called when an incoming connection occurs from a central on the link
|
||||||
@@ -310,7 +352,12 @@ class Controller:
|
|||||||
if connection is None:
|
if connection is None:
|
||||||
connection_handle = self.allocate_connection_handle()
|
connection_handle = self.allocate_connection_handle()
|
||||||
connection = Connection(
|
connection = Connection(
|
||||||
self, connection_handle, BT_PERIPHERAL_ROLE, peer_address, self.link
|
self,
|
||||||
|
connection_handle,
|
||||||
|
BT_PERIPHERAL_ROLE,
|
||||||
|
peer_address,
|
||||||
|
self.link,
|
||||||
|
BT_LE_TRANSPORT,
|
||||||
)
|
)
|
||||||
self.peripheral_connections[peer_address] = connection
|
self.peripheral_connections[peer_address] = connection
|
||||||
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
|
logger.debug(f'New PERIPHERAL connection handle: 0x{connection_handle:04X}')
|
||||||
@@ -364,7 +411,12 @@ class Controller:
|
|||||||
if connection is None:
|
if connection is None:
|
||||||
connection_handle = self.allocate_connection_handle()
|
connection_handle = self.allocate_connection_handle()
|
||||||
connection = Connection(
|
connection = Connection(
|
||||||
self, connection_handle, BT_CENTRAL_ROLE, peer_address, self.link
|
self,
|
||||||
|
connection_handle,
|
||||||
|
BT_CENTRAL_ROLE,
|
||||||
|
peer_address,
|
||||||
|
self.link,
|
||||||
|
BT_LE_TRANSPORT,
|
||||||
)
|
)
|
||||||
self.central_connections[peer_address] = connection
|
self.central_connections[peer_address] = connection
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -432,16 +484,19 @@ class Controller:
|
|||||||
|
|
||||||
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
|
def on_link_encrypted(self, peer_address, _rand, _ediv, _ltk):
|
||||||
# For now, just setup the encryption without asking the host
|
# For now, just setup the encryption without asking the host
|
||||||
if connection := self.find_connection_by_address(peer_address):
|
if connection := self.find_le_connection_by_address(peer_address):
|
||||||
self.send_hci_packet(
|
self.send_hci_packet(
|
||||||
HCI_Encryption_Change_Event(
|
HCI_Encryption_Change_Event(
|
||||||
status=0, connection_handle=connection.handle, encryption_enabled=1
|
status=0, connection_handle=connection.handle, encryption_enabled=1
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_link_acl_data(self, sender_address, data):
|
def on_link_acl_data(self, sender_address, transport, data):
|
||||||
# Look for the connection to which this data belongs
|
# Look for the connection to which this data belongs
|
||||||
connection = self.find_connection_by_address(sender_address)
|
if transport == BT_LE_TRANSPORT:
|
||||||
|
connection = self.find_le_connection_by_address(sender_address)
|
||||||
|
else:
|
||||||
|
connection = self.find_classic_connection_by_address(sender_address)
|
||||||
if connection is None:
|
if connection is None:
|
||||||
logger.warning(f'!!! no connection for {sender_address}')
|
logger.warning(f'!!! no connection for {sender_address}')
|
||||||
return
|
return
|
||||||
@@ -478,6 +533,87 @@ class Controller:
|
|||||||
)
|
)
|
||||||
self.send_hci_packet(HCI_LE_Advertising_Report_Event([report]))
|
self.send_hci_packet(HCI_LE_Advertising_Report_Event([report]))
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
# Classic link connections
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
def on_classic_connection_request(self, peer_address, link_type):
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Connection_Request_Event(
|
||||||
|
bd_addr=peer_address,
|
||||||
|
class_of_device=0,
|
||||||
|
link_type=link_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_classic_connection_complete(self, peer_address, status):
|
||||||
|
if status == HCI_SUCCESS:
|
||||||
|
# Allocate (or reuse) a connection handle
|
||||||
|
peer_address = peer_address
|
||||||
|
connection = self.classic_connections.get(peer_address)
|
||||||
|
if connection is None:
|
||||||
|
connection_handle = self.allocate_connection_handle()
|
||||||
|
connection = Connection(
|
||||||
|
controller=self,
|
||||||
|
handle=connection_handle,
|
||||||
|
# Role doesn't matter in Classic because they are managed by HCI_Role_Change and HCI_Role_Discovery
|
||||||
|
role=BT_CENTRAL_ROLE,
|
||||||
|
peer_address=peer_address,
|
||||||
|
link=self.link,
|
||||||
|
transport=BT_BR_EDR_TRANSPORT,
|
||||||
|
)
|
||||||
|
self.classic_connections[peer_address] = connection
|
||||||
|
logger.debug(
|
||||||
|
f'New CLASSIC connection handle: 0x{connection_handle:04X}'
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
connection_handle = connection.handle
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Connection_Complete_Event(
|
||||||
|
status=status,
|
||||||
|
connection_handle=connection_handle,
|
||||||
|
bd_addr=peer_address,
|
||||||
|
encryption_enabled=False,
|
||||||
|
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
connection = None
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Connection_Complete_Event(
|
||||||
|
status=status,
|
||||||
|
connection_handle=0,
|
||||||
|
bd_addr=peer_address,
|
||||||
|
encryption_enabled=False,
|
||||||
|
link_type=HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_classic_disconnected(self, peer_address, reason):
|
||||||
|
# Send a disconnection complete event
|
||||||
|
if connection := self.classic_connections.get(peer_address):
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Disconnection_Complete_Event(
|
||||||
|
status=HCI_SUCCESS,
|
||||||
|
connection_handle=connection.handle,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Remove the connection
|
||||||
|
del self.classic_connections[peer_address]
|
||||||
|
else:
|
||||||
|
logger.warning(f'!!! No classic connection found for {peer_address}')
|
||||||
|
|
||||||
|
def on_classic_role_change(self, peer_address, new_role):
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Role_Change_Event(
|
||||||
|
status=HCI_SUCCESS,
|
||||||
|
bd_addr=peer_address,
|
||||||
|
new_role=new_role,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
# Advertising support
|
# Advertising support
|
||||||
############################################################
|
############################################################
|
||||||
@@ -521,7 +657,31 @@ class Controller:
|
|||||||
See Bluetooth spec Vol 2, Part E - 7.1.5 Create Connection command
|
See Bluetooth spec Vol 2, Part E - 7.1.5 Create Connection command
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# TODO: classic mode not supported yet
|
if self.link is None:
|
||||||
|
return
|
||||||
|
logger.debug(f'Connection request to {command.bd_addr}')
|
||||||
|
|
||||||
|
# Check that we don't already have a pending connection
|
||||||
|
if self.link.get_pending_connection():
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Command_Status_Event(
|
||||||
|
status=HCI_CONTROLLER_BUSY_ERROR,
|
||||||
|
num_hci_command_packets=1,
|
||||||
|
command_opcode=command.op_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.link.classic_connect(self, command.bd_addr)
|
||||||
|
|
||||||
|
# Say that the connection is pending
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Command_Status_Event(
|
||||||
|
status=HCI_COMMAND_STATUS_PENDING,
|
||||||
|
num_hci_command_packets=1,
|
||||||
|
command_opcode=command.op_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def on_hci_disconnect_command(self, command):
|
def on_hci_disconnect_command(self, command):
|
||||||
'''
|
'''
|
||||||
@@ -537,19 +697,57 @@ class Controller:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Notify the link of the disconnection
|
# Notify the link of the disconnection
|
||||||
if not (
|
handle = command.connection_handle
|
||||||
connection := self.find_central_connection_by_handle(
|
if connection := self.find_central_connection_by_handle(handle):
|
||||||
command.connection_handle
|
if self.link:
|
||||||
)
|
self.link.disconnect(
|
||||||
):
|
self.random_address, connection.peer_address, command
|
||||||
logger.warning('connection not found')
|
)
|
||||||
return
|
else:
|
||||||
|
# Remove the connection
|
||||||
|
del self.central_connections[connection.peer_address]
|
||||||
|
elif connection := self.find_classic_connection_by_handle(handle):
|
||||||
|
if self.link:
|
||||||
|
self.link.classic_disconnect(
|
||||||
|
self,
|
||||||
|
connection.peer_address,
|
||||||
|
HCI_REMOTE_USER_TERMINATED_CONNECTION_ERROR,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Remove the connection
|
||||||
|
del self.classic_connections[connection.peer_address]
|
||||||
|
|
||||||
if self.link:
|
def on_hci_accept_connection_request_command(self, command):
|
||||||
self.link.disconnect(self.random_address, connection.peer_address, command)
|
'''
|
||||||
else:
|
See Bluetooth spec Vol 2, Part E - 7.1.8 Accept Connection Request command
|
||||||
# Remove the connection
|
'''
|
||||||
del self.central_connections[connection.peer_address]
|
|
||||||
|
if self.link is None:
|
||||||
|
return
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Command_Status_Event(
|
||||||
|
status=HCI_SUCCESS,
|
||||||
|
num_hci_command_packets=1,
|
||||||
|
command_opcode=command.op_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.link.classic_accept_connection(self, command.bd_addr, command.role)
|
||||||
|
|
||||||
|
def on_hci_switch_role_command(self, command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 2, Part E - 7.2.8 Switch Role command
|
||||||
|
'''
|
||||||
|
|
||||||
|
if self.link is None:
|
||||||
|
return
|
||||||
|
self.send_hci_packet(
|
||||||
|
HCI_Command_Status_Event(
|
||||||
|
status=HCI_SUCCESS,
|
||||||
|
num_hci_command_packets=1,
|
||||||
|
command_opcode=command.op_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.link.classic_switch_role(self, command.bd_addr, command.role)
|
||||||
|
|
||||||
def on_hci_set_event_mask_command(self, command):
|
def on_hci_set_event_mask_command(self, command):
|
||||||
'''
|
'''
|
||||||
@@ -627,6 +825,12 @@ class Controller:
|
|||||||
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
ret = HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR
|
||||||
return bytes([ret])
|
return bytes([ret])
|
||||||
|
|
||||||
|
def on_hci_write_extended_inquiry_response_command(self, _command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||||
|
'''
|
||||||
|
return bytes([HCI_SUCCESS])
|
||||||
|
|
||||||
def on_hci_write_simple_pairing_mode_command(self, _command):
|
def on_hci_write_simple_pairing_mode_command(self, _command):
|
||||||
'''
|
'''
|
||||||
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
See Bluetooth spec Vol 2, Part E - 7.3.59 Write Simple Pairing Mode Command
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Constants
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# fmt: off
|
||||||
|
|
||||||
|
WL = [-60, -30, 58, 172, 334, 538, 1198, 3042]
|
||||||
|
RL42 = [0, 7, 6, 5, 4, 3, 2, 1, 7, 6, 5, 4, 3, 2, 1, 0]
|
||||||
|
ILB = [
|
||||||
|
2048,
|
||||||
|
2093,
|
||||||
|
2139,
|
||||||
|
2186,
|
||||||
|
2233,
|
||||||
|
2282,
|
||||||
|
2332,
|
||||||
|
2383,
|
||||||
|
2435,
|
||||||
|
2489,
|
||||||
|
2543,
|
||||||
|
2599,
|
||||||
|
2656,
|
||||||
|
2714,
|
||||||
|
2774,
|
||||||
|
2834,
|
||||||
|
2896,
|
||||||
|
2960,
|
||||||
|
3025,
|
||||||
|
3091,
|
||||||
|
3158,
|
||||||
|
3228,
|
||||||
|
3298,
|
||||||
|
3371,
|
||||||
|
3444,
|
||||||
|
3520,
|
||||||
|
3597,
|
||||||
|
3676,
|
||||||
|
3756,
|
||||||
|
3838,
|
||||||
|
3922,
|
||||||
|
4008,
|
||||||
|
]
|
||||||
|
WH = [0, -214, 798]
|
||||||
|
RH2 = [2, 1, 2, 1]
|
||||||
|
# Values in QM2/QM4/QM6 left shift three bits than original g722 specification.
|
||||||
|
QM2 = [-7408, -1616, 7408, 1616]
|
||||||
|
QM4 = [
|
||||||
|
0,
|
||||||
|
-20456,
|
||||||
|
-12896,
|
||||||
|
-8968,
|
||||||
|
-6288,
|
||||||
|
-4240,
|
||||||
|
-2584,
|
||||||
|
-1200,
|
||||||
|
20456,
|
||||||
|
12896,
|
||||||
|
8968,
|
||||||
|
6288,
|
||||||
|
4240,
|
||||||
|
2584,
|
||||||
|
1200,
|
||||||
|
0,
|
||||||
|
]
|
||||||
|
QM6 = [
|
||||||
|
-136,
|
||||||
|
-136,
|
||||||
|
-136,
|
||||||
|
-136,
|
||||||
|
-24808,
|
||||||
|
-21904,
|
||||||
|
-19008,
|
||||||
|
-16704,
|
||||||
|
-14984,
|
||||||
|
-13512,
|
||||||
|
-12280,
|
||||||
|
-11192,
|
||||||
|
-10232,
|
||||||
|
-9360,
|
||||||
|
-8576,
|
||||||
|
-7856,
|
||||||
|
-7192,
|
||||||
|
-6576,
|
||||||
|
-6000,
|
||||||
|
-5456,
|
||||||
|
-4944,
|
||||||
|
-4464,
|
||||||
|
-4008,
|
||||||
|
-3576,
|
||||||
|
-3168,
|
||||||
|
-2776,
|
||||||
|
-2400,
|
||||||
|
-2032,
|
||||||
|
-1688,
|
||||||
|
-1360,
|
||||||
|
-1040,
|
||||||
|
-728,
|
||||||
|
24808,
|
||||||
|
21904,
|
||||||
|
19008,
|
||||||
|
16704,
|
||||||
|
14984,
|
||||||
|
13512,
|
||||||
|
12280,
|
||||||
|
11192,
|
||||||
|
10232,
|
||||||
|
9360,
|
||||||
|
8576,
|
||||||
|
7856,
|
||||||
|
7192,
|
||||||
|
6576,
|
||||||
|
6000,
|
||||||
|
5456,
|
||||||
|
4944,
|
||||||
|
4464,
|
||||||
|
4008,
|
||||||
|
3576,
|
||||||
|
3168,
|
||||||
|
2776,
|
||||||
|
2400,
|
||||||
|
2032,
|
||||||
|
1688,
|
||||||
|
1360,
|
||||||
|
1040,
|
||||||
|
728,
|
||||||
|
432,
|
||||||
|
136,
|
||||||
|
-432,
|
||||||
|
-136,
|
||||||
|
]
|
||||||
|
QMF_COEFFS = [3, -11, 12, 32, -210, 951, 3876, -805, 362, -156, 53, -11]
|
||||||
|
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Classes
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class G722Decoder(object):
|
||||||
|
"""G.722 decoder with bitrate 64kbit/s.
|
||||||
|
|
||||||
|
For the Blocks in the sub-band decoders, please refer to the G.722
|
||||||
|
specification for the required information. G722 specification:
|
||||||
|
https://www.itu.int/rec/T-REC-G.722-201209-I
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._x = [0] * 24
|
||||||
|
self._band = [Band(), Band()]
|
||||||
|
# The initial value in BLOCK 3L
|
||||||
|
self._band[0].det = 32
|
||||||
|
# The initial value in BLOCK 3H
|
||||||
|
self._band[1].det = 8
|
||||||
|
|
||||||
|
def decode_frame(self, encoded_data) -> bytearray:
|
||||||
|
result_array = bytearray(len(encoded_data) * 4)
|
||||||
|
self.g722_decode(result_array, encoded_data)
|
||||||
|
return result_array
|
||||||
|
|
||||||
|
def g722_decode(self, result_array, encoded_data) -> int:
|
||||||
|
"""Decode the data frame using g722 decoder."""
|
||||||
|
result_length = 0
|
||||||
|
|
||||||
|
for code in encoded_data:
|
||||||
|
higher_bits = (code >> 6) & 0x03
|
||||||
|
lower_bits = code & 0x3F
|
||||||
|
|
||||||
|
rlow = self.lower_sub_band_decoder(lower_bits)
|
||||||
|
rhigh = self.higher_sub_band_decoder(higher_bits)
|
||||||
|
|
||||||
|
# Apply the receive QMF
|
||||||
|
self._x[:22] = self._x[2:]
|
||||||
|
self._x[22] = rlow + rhigh
|
||||||
|
self._x[23] = rlow - rhigh
|
||||||
|
|
||||||
|
xout2 = sum(self._x[2 * i] * QMF_COEFFS[i] for i in range(12))
|
||||||
|
xout1 = sum(self._x[2 * i + 1] * QMF_COEFFS[11 - i] for i in range(12))
|
||||||
|
|
||||||
|
result_length = self.update_decoded_result(
|
||||||
|
xout1, result_length, result_array
|
||||||
|
)
|
||||||
|
result_length = self.update_decoded_result(
|
||||||
|
xout2, result_length, result_array
|
||||||
|
)
|
||||||
|
|
||||||
|
return result_length
|
||||||
|
|
||||||
|
def update_decoded_result(self, xout, byte_length, byte_array) -> int:
|
||||||
|
result = (int)(xout >> 11)
|
||||||
|
bytes_result = result.to_bytes(2, 'little', signed=True)
|
||||||
|
byte_array[byte_length] = bytes_result[0]
|
||||||
|
byte_array[byte_length + 1] = bytes_result[1]
|
||||||
|
return byte_length + 2
|
||||||
|
|
||||||
|
def lower_sub_band_decoder(self, lower_bits) -> int:
|
||||||
|
"""Lower sub-band decoder for last six bits."""
|
||||||
|
|
||||||
|
# Block 5L
|
||||||
|
# INVQBL
|
||||||
|
wd1 = lower_bits
|
||||||
|
wd2 = QM6[wd1]
|
||||||
|
wd1 >>= 2
|
||||||
|
wd2 = (self._band[0].det * wd2) >> 15
|
||||||
|
# RECONS
|
||||||
|
rlow = self._band[0].s + wd2
|
||||||
|
|
||||||
|
# Block 6L
|
||||||
|
# LIMIT
|
||||||
|
if rlow > 16383:
|
||||||
|
rlow = 16383
|
||||||
|
elif rlow < -16384:
|
||||||
|
rlow = -16384
|
||||||
|
|
||||||
|
# Block 2L
|
||||||
|
# INVQAL
|
||||||
|
wd2 = QM4[wd1]
|
||||||
|
dlowt = (self._band[0].det * wd2) >> 15
|
||||||
|
|
||||||
|
# Block 3L
|
||||||
|
# LOGSCL
|
||||||
|
wd2 = RL42[wd1]
|
||||||
|
wd1 = (self._band[0].nb * 127) >> 7
|
||||||
|
wd1 += WL[wd2]
|
||||||
|
|
||||||
|
if wd1 < 0:
|
||||||
|
wd1 = 0
|
||||||
|
elif wd1 > 18432:
|
||||||
|
wd1 = 18432
|
||||||
|
|
||||||
|
self._band[0].nb = wd1
|
||||||
|
|
||||||
|
# SCALEL
|
||||||
|
wd1 = (self._band[0].nb >> 6) & 31
|
||||||
|
wd2 = 8 - (self._band[0].nb >> 11)
|
||||||
|
|
||||||
|
if wd2 < 0:
|
||||||
|
wd3 = ILB[wd1] << -wd2
|
||||||
|
else:
|
||||||
|
wd3 = ILB[wd1] >> wd2
|
||||||
|
|
||||||
|
self._band[0].det = wd3 << 2
|
||||||
|
|
||||||
|
# Block 4L
|
||||||
|
self._band[0].block4(dlowt)
|
||||||
|
|
||||||
|
return rlow
|
||||||
|
|
||||||
|
def higher_sub_band_decoder(self, higher_bits) -> int:
|
||||||
|
"""Higher sub-band decoder for first two bits."""
|
||||||
|
|
||||||
|
# Block 2H
|
||||||
|
# INVQAH
|
||||||
|
wd2 = QM2[higher_bits]
|
||||||
|
dhigh = (self._band[1].det * wd2) >> 15
|
||||||
|
|
||||||
|
# Block 5H
|
||||||
|
# RECONS
|
||||||
|
rhigh = dhigh + self._band[1].s
|
||||||
|
|
||||||
|
# Block 6H
|
||||||
|
# LIMIT
|
||||||
|
if rhigh > 16383:
|
||||||
|
rhigh = 16383
|
||||||
|
elif rhigh < -16384:
|
||||||
|
rhigh = -16384
|
||||||
|
|
||||||
|
# Block 3H
|
||||||
|
# LOGSCH
|
||||||
|
wd2 = RH2[higher_bits]
|
||||||
|
wd1 = (self._band[1].nb * 127) >> 7
|
||||||
|
wd1 += WH[wd2]
|
||||||
|
|
||||||
|
if wd1 < 0:
|
||||||
|
wd1 = 0
|
||||||
|
elif wd1 > 22528:
|
||||||
|
wd1 = 22528
|
||||||
|
self._band[1].nb = wd1
|
||||||
|
|
||||||
|
# SCALEH
|
||||||
|
wd1 = (self._band[1].nb >> 6) & 31
|
||||||
|
wd2 = 10 - (self._band[1].nb >> 11)
|
||||||
|
|
||||||
|
if wd2 < 0:
|
||||||
|
wd3 = ILB[wd1] << -wd2
|
||||||
|
else:
|
||||||
|
wd3 = ILB[wd1] >> wd2
|
||||||
|
self._band[1].det = wd3 << 2
|
||||||
|
|
||||||
|
# Block 4H
|
||||||
|
self._band[1].block4(dhigh)
|
||||||
|
|
||||||
|
return rhigh
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class Band(object):
|
||||||
|
"""Structure for G722 decode proccessing."""
|
||||||
|
|
||||||
|
s: int = 0
|
||||||
|
nb: int = 0
|
||||||
|
det: int = 0
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._sp = 0
|
||||||
|
self._sz = 0
|
||||||
|
self._r = [0] * 3
|
||||||
|
self._a = [0] * 3
|
||||||
|
self._ap = [0] * 3
|
||||||
|
self._p = [0] * 3
|
||||||
|
self._d = [0] * 7
|
||||||
|
self._b = [0] * 7
|
||||||
|
self._bp = [0] * 7
|
||||||
|
self._sg = [0] * 7
|
||||||
|
|
||||||
|
def saturate(self, amp: int) -> int:
|
||||||
|
if amp > 32767:
|
||||||
|
return 32767
|
||||||
|
elif amp < -32768:
|
||||||
|
return -32768
|
||||||
|
else:
|
||||||
|
return amp
|
||||||
|
|
||||||
|
def block4(self, d: int) -> None:
|
||||||
|
"""Block4 for both lower and higher sub-band decoder."""
|
||||||
|
wd1 = 0
|
||||||
|
wd2 = 0
|
||||||
|
wd3 = 0
|
||||||
|
|
||||||
|
# RECONS
|
||||||
|
self._d[0] = d
|
||||||
|
self._r[0] = self.saturate(self.s + d)
|
||||||
|
|
||||||
|
# PARREC
|
||||||
|
self._p[0] = self.saturate(self._sz + d)
|
||||||
|
|
||||||
|
# UPPOL2
|
||||||
|
for i in range(3):
|
||||||
|
self._sg[i] = (self._p[i]) >> 15
|
||||||
|
wd1 = self.saturate((self._a[1]) << 2)
|
||||||
|
wd2 = -wd1 if self._sg[0] == self._sg[1] else wd1
|
||||||
|
|
||||||
|
if wd2 > 32767:
|
||||||
|
wd2 = 32767
|
||||||
|
|
||||||
|
wd3 = 128 if self._sg[0] == self._sg[2] else -128
|
||||||
|
wd3 += wd2 >> 7
|
||||||
|
wd3 += (self._a[2] * 32512) >> 15
|
||||||
|
|
||||||
|
if wd3 > 12288:
|
||||||
|
wd3 = 12288
|
||||||
|
elif wd3 < -12288:
|
||||||
|
wd3 = -12288
|
||||||
|
self._ap[2] = wd3
|
||||||
|
|
||||||
|
# UPPOL1
|
||||||
|
self._sg[0] = (self._p[0]) >> 15
|
||||||
|
self._sg[1] = (self._p[1]) >> 15
|
||||||
|
wd1 = 192 if self._sg[0] == self._sg[1] else -192
|
||||||
|
wd2 = (self._a[1] * 32640) >> 15
|
||||||
|
|
||||||
|
self._ap[1] = self.saturate(wd1 + wd2)
|
||||||
|
wd3 = self.saturate(15360 - self._ap[2])
|
||||||
|
|
||||||
|
if self._ap[1] > wd3:
|
||||||
|
self._ap[1] = wd3
|
||||||
|
elif self._ap[1] < -wd3:
|
||||||
|
self._ap[1] = -wd3
|
||||||
|
|
||||||
|
# UPZERO
|
||||||
|
wd1 = 0 if d == 0 else 128
|
||||||
|
self._sg[0] = d >> 15
|
||||||
|
for i in range(1, 7):
|
||||||
|
self._sg[i] = (self._d[i]) >> 15
|
||||||
|
wd2 = wd1 if self._sg[i] == self._sg[0] else -wd1
|
||||||
|
wd3 = (self._b[i] * 32640) >> 15
|
||||||
|
self._bp[i] = self.saturate(wd2 + wd3)
|
||||||
|
|
||||||
|
# DELAYA
|
||||||
|
for i in range(6, 0, -1):
|
||||||
|
self._d[i] = self._d[i - 1]
|
||||||
|
self._b[i] = self._bp[i]
|
||||||
|
|
||||||
|
for i in range(2, 0, -1):
|
||||||
|
self._r[i] = self._r[i - 1]
|
||||||
|
self._p[i] = self._p[i - 1]
|
||||||
|
self._a[i] = self._ap[i]
|
||||||
|
|
||||||
|
# FILTEP
|
||||||
|
self._sp = 0
|
||||||
|
for i in range(1, 3):
|
||||||
|
wd1 = self.saturate(self._r[i] + self._r[i])
|
||||||
|
self._sp += (self._a[i] * wd1) >> 15
|
||||||
|
self._sp = self.saturate(self._sp)
|
||||||
|
|
||||||
|
# FILTEZ
|
||||||
|
self._sz = 0
|
||||||
|
for i in range(6, 0, -1):
|
||||||
|
wd1 = self.saturate(self._d[i] + self._d[i])
|
||||||
|
self._sz += (self._b[i] * wd1) >> 15
|
||||||
|
self._sz = self.saturate(self._sz)
|
||||||
|
|
||||||
|
# PREDIC
|
||||||
|
self.s = self.saturate(self._sp + self._sz)
|
||||||
+254
-100
@@ -29,11 +29,13 @@ from .colors import color
|
|||||||
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
from .att import ATT_CID, ATT_DEFAULT_MTU, ATT_PDU
|
||||||
from .gatt import Characteristic, Descriptor, Service
|
from .gatt import Characteristic, Descriptor, Service
|
||||||
from .hci import (
|
from .hci import (
|
||||||
|
HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||||
|
HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||||
HCI_CENTRAL_ROLE,
|
HCI_CENTRAL_ROLE,
|
||||||
HCI_COMMAND_STATUS_PENDING,
|
HCI_COMMAND_STATUS_PENDING,
|
||||||
HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
|
HCI_CONNECTION_REJECTED_DUE_TO_LIMITED_RESOURCES_ERROR,
|
||||||
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
|
||||||
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
HCI_EXTENDED_INQUIRY_MODE,
|
HCI_EXTENDED_INQUIRY_MODE,
|
||||||
HCI_GENERAL_INQUIRY_LAP,
|
HCI_GENERAL_INQUIRY_LAP,
|
||||||
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
HCI_INVALID_HCI_COMMAND_PARAMETERS_ERROR,
|
||||||
@@ -50,6 +52,7 @@ from .hci import (
|
|||||||
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
HCI_LE_EXTENDED_CREATE_CONNECTION_COMMAND,
|
||||||
HCI_LE_RAND_COMMAND,
|
HCI_LE_RAND_COMMAND,
|
||||||
HCI_LE_READ_PHY_COMMAND,
|
HCI_LE_READ_PHY_COMMAND,
|
||||||
|
HCI_LE_SET_PHY_COMMAND,
|
||||||
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_NOT_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_NOT_REQUIRED_NO_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
HCI_MITM_REQUIRED_GENERAL_BONDING_AUTHENTICATION_REQUIREMENTS,
|
||||||
@@ -94,10 +97,13 @@ from .hci import (
|
|||||||
HCI_LE_Set_Scan_Enable_Command,
|
HCI_LE_Set_Scan_Enable_Command,
|
||||||
HCI_LE_Set_Scan_Parameters_Command,
|
HCI_LE_Set_Scan_Parameters_Command,
|
||||||
HCI_LE_Set_Scan_Response_Data_Command,
|
HCI_LE_Set_Scan_Response_Data_Command,
|
||||||
|
HCI_PIN_Code_Request_Reply_Command,
|
||||||
|
HCI_PIN_Code_Request_Negative_Reply_Command,
|
||||||
HCI_Read_BD_ADDR_Command,
|
HCI_Read_BD_ADDR_Command,
|
||||||
HCI_Read_RSSI_Command,
|
HCI_Read_RSSI_Command,
|
||||||
HCI_Reject_Connection_Request_Command,
|
HCI_Reject_Connection_Request_Command,
|
||||||
HCI_Remote_Name_Request_Command,
|
HCI_Remote_Name_Request_Command,
|
||||||
|
HCI_Switch_Role_Command,
|
||||||
HCI_Set_Connection_Encryption_Command,
|
HCI_Set_Connection_Encryption_Command,
|
||||||
HCI_StatusError,
|
HCI_StatusError,
|
||||||
HCI_User_Confirmation_Request_Negative_Reply_Command,
|
HCI_User_Confirmation_Request_Negative_Reply_Command,
|
||||||
@@ -137,6 +143,7 @@ from .keys import (
|
|||||||
KeyStore,
|
KeyStore,
|
||||||
PairingKeys,
|
PairingKeys,
|
||||||
)
|
)
|
||||||
|
from .pairing import PairingConfig
|
||||||
from . import gatt_client
|
from . import gatt_client
|
||||||
from . import gatt_server
|
from . import gatt_server
|
||||||
from . import smp
|
from . import smp
|
||||||
@@ -194,6 +201,7 @@ DEVICE_DEFAULT_L2CAP_COC_MAX_CREDITS = l2cap.L2CAP_LE_CREDIT_BASED_CONN
|
|||||||
# Classes
|
# Classes
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Advertisement:
|
class Advertisement:
|
||||||
address: Address
|
address: Address
|
||||||
@@ -310,6 +318,9 @@ class AdvertisementDataAccumulator:
|
|||||||
|
|
||||||
def update(self, report):
|
def update(self, report):
|
||||||
advertisement = Advertisement.from_advertising_report(report)
|
advertisement = Advertisement.from_advertising_report(report)
|
||||||
|
if advertisement is None:
|
||||||
|
return None
|
||||||
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
if advertisement.is_scan_response:
|
if advertisement.is_scan_response:
|
||||||
@@ -522,6 +533,9 @@ class Connection(CompositeEventEmitter):
|
|||||||
authenticated: bool
|
authenticated: bool
|
||||||
sc: bool
|
sc: bool
|
||||||
link_key_type: int
|
link_key_type: int
|
||||||
|
gatt_client: gatt_client.Client
|
||||||
|
pairing_peer_io_capability: Optional[int]
|
||||||
|
pairing_peer_authentication_requirements: Optional[int]
|
||||||
|
|
||||||
@composite_listener
|
@composite_listener
|
||||||
class Listener:
|
class Listener:
|
||||||
@@ -585,10 +599,12 @@ class Connection(CompositeEventEmitter):
|
|||||||
self.gatt_server = (
|
self.gatt_server = (
|
||||||
device.gatt_server
|
device.gatt_server
|
||||||
) # By default, use the device's shared server
|
) # By default, use the device's shared server
|
||||||
|
self.pairing_peer_io_capability = None
|
||||||
|
self.pairing_peer_authentication_requirements = None
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@classmethod
|
@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
|
Instantiate an incomplete connection (ie. one waiting for a HCI Connection
|
||||||
Complete event).
|
Complete event).
|
||||||
@@ -601,26 +617,30 @@ class Connection(CompositeEventEmitter):
|
|||||||
device.public_address,
|
device.public_address,
|
||||||
peer_address,
|
peer_address,
|
||||||
None,
|
None,
|
||||||
None,
|
role,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
def complete(self, handle, peer_resolvable_address, role, parameters):
|
def complete(self, handle, parameters):
|
||||||
"""
|
"""
|
||||||
Finish an incomplete connection upon completion.
|
Finish an incomplete connection upon completion.
|
||||||
"""
|
"""
|
||||||
assert self.handle is None
|
assert self.handle is None
|
||||||
assert self.transport == BT_BR_EDR_TRANSPORT
|
assert self.transport == BT_BR_EDR_TRANSPORT
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.peer_resolvable_address = peer_resolvable_address
|
|
||||||
self.role = role
|
|
||||||
self.parameters = parameters
|
self.parameters = parameters
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def role_name(self):
|
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
|
@property
|
||||||
def is_encrypted(self):
|
def is_encrypted(self):
|
||||||
@@ -628,7 +648,7 @@ class Connection(CompositeEventEmitter):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_incomplete(self) -> bool:
|
def is_incomplete(self) -> bool:
|
||||||
return self.handle == None
|
return self.handle is None
|
||||||
|
|
||||||
def send_l2cap_pdu(self, cid, pdu):
|
def send_l2cap_pdu(self, cid, pdu):
|
||||||
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
self.device.send_l2cap_pdu(self.handle, cid, pdu)
|
||||||
@@ -663,6 +683,9 @@ class Connection(CompositeEventEmitter):
|
|||||||
async def encrypt(self, enable: bool = True) -> None:
|
async def encrypt(self, enable: bool = True) -> None:
|
||||||
return await self.device.encrypt(self, enable)
|
return await self.device.encrypt(self, enable)
|
||||||
|
|
||||||
|
async def switch_role(self, role: int) -> None:
|
||||||
|
return await self.device.switch_role(self, role)
|
||||||
|
|
||||||
async def sustain(self, timeout=None):
|
async def sustain(self, timeout=None):
|
||||||
"""Idles the current task waiting for a disconnect or timeout"""
|
"""Idles the current task waiting for a disconnect or timeout"""
|
||||||
|
|
||||||
@@ -738,9 +761,11 @@ class DeviceConfiguration:
|
|||||||
self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
self.advertising_interval_max = DEVICE_DEFAULT_ADVERTISING_INTERVAL
|
||||||
self.le_enabled = True
|
self.le_enabled = True
|
||||||
# LE host enable 2nd parameter
|
# 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_sc_enabled = True
|
||||||
self.classic_ssp_enabled = True
|
self.classic_ssp_enabled = True
|
||||||
|
self.classic_smp_enabled = True
|
||||||
self.classic_accept_any = True
|
self.classic_accept_any = True
|
||||||
self.connectable = True
|
self.connectable = True
|
||||||
self.discoverable = True
|
self.discoverable = True
|
||||||
@@ -768,12 +793,16 @@ class DeviceConfiguration:
|
|||||||
self.le_simultaneous_enabled = config.get(
|
self.le_simultaneous_enabled = config.get(
|
||||||
'le_simultaneous_enabled', self.le_simultaneous_enabled
|
'le_simultaneous_enabled', self.le_simultaneous_enabled
|
||||||
)
|
)
|
||||||
|
self.classic_enabled = config.get('classic_enabled', self.classic_enabled)
|
||||||
self.classic_sc_enabled = config.get(
|
self.classic_sc_enabled = config.get(
|
||||||
'classic_sc_enabled', self.classic_sc_enabled
|
'classic_sc_enabled', self.classic_sc_enabled
|
||||||
)
|
)
|
||||||
self.classic_ssp_enabled = config.get(
|
self.classic_ssp_enabled = config.get(
|
||||||
'classic_ssp_enabled', self.classic_ssp_enabled
|
'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(
|
self.classic_accept_any = config.get(
|
||||||
'classic_accept_any', self.classic_accept_any
|
'classic_accept_any', self.classic_accept_any
|
||||||
)
|
)
|
||||||
@@ -864,7 +893,7 @@ device_host_event_handlers: list[str] = []
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Device(CompositeEventEmitter):
|
class Device(CompositeEventEmitter):
|
||||||
# incomplete list of fields.
|
# Incomplete list of fields.
|
||||||
random_address: Address
|
random_address: Address
|
||||||
public_address: Address
|
public_address: Address
|
||||||
classic_enabled: bool
|
classic_enabled: bool
|
||||||
@@ -879,6 +908,7 @@ class Device(CompositeEventEmitter):
|
|||||||
Address, List[asyncio.Future[Union[Connection, Tuple[Address, int, int]]]]
|
Address, List[asyncio.Future[Union[Connection, Tuple[Address, int, int]]]]
|
||||||
]
|
]
|
||||||
advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
|
advertisement_accumulators: Dict[Address, AdvertisementDataAccumulator]
|
||||||
|
config: DeviceConfiguration
|
||||||
|
|
||||||
@composite_listener
|
@composite_listener
|
||||||
class Listener:
|
class Listener:
|
||||||
@@ -966,9 +996,10 @@ class Device(CompositeEventEmitter):
|
|||||||
self.connect_own_address_type = None
|
self.connect_own_address_type = None
|
||||||
|
|
||||||
# Use the initial config or a default
|
# Use the initial config or a default
|
||||||
|
config = config or DeviceConfiguration()
|
||||||
|
self.config = config
|
||||||
|
|
||||||
self.public_address = Address('00:00:00:00:00:00')
|
self.public_address = Address('00:00:00:00:00:00')
|
||||||
if config is None:
|
|
||||||
config = DeviceConfiguration()
|
|
||||||
self.name = config.name
|
self.name = config.name
|
||||||
self.random_address = config.address
|
self.random_address = config.address
|
||||||
self.class_of_device = config.class_of_device
|
self.class_of_device = config.class_of_device
|
||||||
@@ -976,12 +1007,14 @@ class Device(CompositeEventEmitter):
|
|||||||
self.advertising_data = config.advertising_data
|
self.advertising_data = config.advertising_data
|
||||||
self.advertising_interval_min = config.advertising_interval_min
|
self.advertising_interval_min = config.advertising_interval_min
|
||||||
self.advertising_interval_max = config.advertising_interval_max
|
self.advertising_interval_max = config.advertising_interval_max
|
||||||
self.keystore = KeyStore.create_for_device(config)
|
self.keystore = None
|
||||||
self.irk = config.irk
|
self.irk = config.irk
|
||||||
self.le_enabled = config.le_enabled
|
self.le_enabled = config.le_enabled
|
||||||
|
self.classic_enabled = config.classic_enabled
|
||||||
self.le_simultaneous_enabled = config.le_simultaneous_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_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.discoverable = config.discoverable
|
||||||
self.connectable = config.connectable
|
self.connectable = config.connectable
|
||||||
self.classic_accept_any = config.classic_accept_any
|
self.classic_accept_any = config.classic_accept_any
|
||||||
@@ -991,15 +1024,22 @@ class Device(CompositeEventEmitter):
|
|||||||
for characteristic in service.get("characteristics", []):
|
for characteristic in service.get("characteristics", []):
|
||||||
descriptors = []
|
descriptors = []
|
||||||
for descriptor in characteristic.get("descriptors", []):
|
for descriptor in characteristic.get("descriptors", []):
|
||||||
|
# Leave this check until 5/25/2023
|
||||||
|
if descriptor.get("permission", False):
|
||||||
|
raise Exception(
|
||||||
|
"Error parsing Device Config's GATT Services. The key 'permission' must be renamed to 'permissions'"
|
||||||
|
)
|
||||||
new_descriptor = Descriptor(
|
new_descriptor = Descriptor(
|
||||||
attribute_type=descriptor["descriptor_type"],
|
attribute_type=descriptor["descriptor_type"],
|
||||||
permissions=descriptor["permission"],
|
permissions=descriptor["permissions"],
|
||||||
)
|
)
|
||||||
descriptors.append(new_descriptor)
|
descriptors.append(new_descriptor)
|
||||||
new_characteristic = Characteristic(
|
new_characteristic = Characteristic(
|
||||||
uuid=characteristic["uuid"],
|
uuid=characteristic["uuid"],
|
||||||
properties=characteristic["properties"],
|
properties=Characteristic.Properties.from_string(
|
||||||
permissions=int(characteristic["permissions"], 0),
|
characteristic["properties"]
|
||||||
|
),
|
||||||
|
permissions=characteristic["permissions"],
|
||||||
descriptors=descriptors,
|
descriptors=descriptors,
|
||||||
)
|
)
|
||||||
characteristics.append(new_characteristic)
|
characteristics.append(new_characteristic)
|
||||||
@@ -1017,12 +1057,12 @@ class Device(CompositeEventEmitter):
|
|||||||
self.random_address = address
|
self.random_address = address
|
||||||
|
|
||||||
# Setup SMP
|
# Setup SMP
|
||||||
self.smp_manager = smp.Manager(self)
|
self.smp_manager = smp.Manager(
|
||||||
self.l2cap_channel_manager.register_fixed_channel(smp.SMP_CID, self.on_smp_pdu)
|
self, pairing_config_factory=lambda connection: PairingConfig()
|
||||||
self.l2cap_channel_manager.register_fixed_channel(
|
|
||||||
smp.SMP_BR_CID, self.on_smp_pdu
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.l2cap_channel_manager.register_fixed_channel(smp.SMP_CID, self.on_smp_pdu)
|
||||||
|
|
||||||
# Register the SDP server with the L2CAP Channel Manager
|
# Register the SDP server with the L2CAP Channel Manager
|
||||||
self.sdp_server.register(self.l2cap_channel_manager)
|
self.sdp_server.register(self.l2cap_channel_manager)
|
||||||
|
|
||||||
@@ -1145,6 +1185,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# Reset the controller
|
# Reset the controller
|
||||||
await self.host.reset()
|
await self.host.reset()
|
||||||
|
|
||||||
|
# Try to get the public address from the controller
|
||||||
response = await self.send_command(HCI_Read_BD_ADDR_Command()) # type: ignore[call-arg]
|
response = await self.send_command(HCI_Read_BD_ADDR_Command()) # type: ignore[call-arg]
|
||||||
if response.return_parameters.status == HCI_SUCCESS:
|
if response.return_parameters.status == HCI_SUCCESS:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -1152,6 +1193,17 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
self.public_address = response.return_parameters.bd_addr
|
self.public_address = response.return_parameters.bd_addr
|
||||||
|
|
||||||
|
# Instantiate the Key Store (we do this here rather than at __init__ time
|
||||||
|
# because some Key Store implementations use the public address as a namespace)
|
||||||
|
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):
|
if self.host.supports_command(HCI_WRITE_LE_HOST_SUPPORT_COMMAND):
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_Write_LE_Host_Support_Command(
|
HCI_Write_LE_Host_Support_Command(
|
||||||
@@ -1199,7 +1251,7 @@ class Device(CompositeEventEmitter):
|
|||||||
await self.send_command(HCI_LE_Clear_Resolving_List_Command()) # type: ignore[call-arg]
|
await self.send_command(HCI_LE_Clear_Resolving_List_Command()) # type: ignore[call-arg]
|
||||||
|
|
||||||
resolving_keys = await self.keystore.get_resolving_keys()
|
resolving_keys = await self.keystore.get_resolving_keys()
|
||||||
for (irk, address) in resolving_keys:
|
for irk, address in resolving_keys:
|
||||||
await self.send_command(
|
await self.send_command(
|
||||||
HCI_LE_Add_Device_To_Resolving_List_Command(
|
HCI_LE_Add_Device_To_Resolving_List_Command(
|
||||||
peer_identity_address_type=address.address_type,
|
peer_identity_address_type=address.address_type,
|
||||||
@@ -1242,6 +1294,11 @@ class Device(CompositeEventEmitter):
|
|||||||
# Done
|
# Done
|
||||||
self.powered_on = True
|
self.powered_on = True
|
||||||
|
|
||||||
|
async def power_off(self) -> None:
|
||||||
|
if self.powered_on:
|
||||||
|
await self.host.flush()
|
||||||
|
self.powered_on = False
|
||||||
|
|
||||||
def supports_le_feature(self, feature):
|
def supports_le_feature(self, feature):
|
||||||
return self.host.supports_le_feature(feature)
|
return self.host.supports_le_feature(feature)
|
||||||
|
|
||||||
@@ -1575,7 +1632,7 @@ class Device(CompositeEventEmitter):
|
|||||||
pending connection.
|
pending connection.
|
||||||
|
|
||||||
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
|
connection_parameters_preferences: (BLE only, ignored for BR/EDR)
|
||||||
* None: use all PHYs with default parameters
|
* None: use the 1M PHY with default parameters
|
||||||
* map: each entry has a PHY as key and a ConnectionParametersPreferences
|
* map: each entry has a PHY as key and a ConnectionParametersPreferences
|
||||||
object as value
|
object as value
|
||||||
|
|
||||||
@@ -1644,9 +1701,7 @@ class Device(CompositeEventEmitter):
|
|||||||
if connection_parameters_preferences is None:
|
if connection_parameters_preferences is None:
|
||||||
if connection_parameters_preferences is None:
|
if connection_parameters_preferences is None:
|
||||||
connection_parameters_preferences = {
|
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
|
self.connect_own_address_type = own_address_type
|
||||||
@@ -1666,7 +1721,7 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not phys:
|
if not phys:
|
||||||
raise ValueError('least one supported PHY needed')
|
raise ValueError('at least one supported PHY needed')
|
||||||
|
|
||||||
phy_count = len(phys)
|
phy_count = len(phys)
|
||||||
initiating_phys = phy_list_to_bits(phys)
|
initiating_phys = phy_list_to_bits(phys)
|
||||||
@@ -1768,7 +1823,7 @@ class Device(CompositeEventEmitter):
|
|||||||
else:
|
else:
|
||||||
# Save pending connection
|
# Save pending connection
|
||||||
self.pending_connections[peer_address] = Connection.incomplete(
|
self.pending_connections[peer_address] = Connection.incomplete(
|
||||||
self, peer_address
|
self, peer_address, BT_CENTRAL_ROLE
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: allow passing other settings
|
# TODO: allow passing other settings
|
||||||
@@ -1807,7 +1862,7 @@ class Device(CompositeEventEmitter):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.abort_on('flush', pending_connection)
|
return await self.abort_on('flush', pending_connection)
|
||||||
except ConnectionError as error:
|
except core.ConnectionError as error:
|
||||||
raise core.TimeoutError() from error
|
raise core.TimeoutError() from error
|
||||||
finally:
|
finally:
|
||||||
self.remove_listener('connection', on_connection)
|
self.remove_listener('connection', on_connection)
|
||||||
@@ -1905,9 +1960,12 @@ class Device(CompositeEventEmitter):
|
|||||||
self.on('connection', on_connection)
|
self.on('connection', on_connection)
|
||||||
self.on('connection_failure', on_connection_failure)
|
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.pending_connections[peer_address] = Connection.incomplete(
|
||||||
self, peer_address
|
self, peer_address, BT_PERIPHERAL_ROLE
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -2041,21 +2099,31 @@ class Device(CompositeEventEmitter):
|
|||||||
async def set_connection_phy(
|
async def set_connection_phy(
|
||||||
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
self, connection, tx_phys=None, rx_phys=None, phy_options=None
|
||||||
):
|
):
|
||||||
|
if not self.host.supports_command(HCI_LE_SET_PHY_COMMAND):
|
||||||
|
logger.warning('ignoring request, command not supported')
|
||||||
|
return
|
||||||
|
|
||||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||||
(1 if rx_phys is None else 0) << 1
|
(1 if rx_phys is None else 0) << 1
|
||||||
)
|
)
|
||||||
|
|
||||||
return await self.send_command(
|
result = await self.send_command(
|
||||||
HCI_LE_Set_PHY_Command(
|
HCI_LE_Set_PHY_Command(
|
||||||
connection_handle=connection.handle,
|
connection_handle=connection.handle,
|
||||||
all_phys=all_phys_bits,
|
all_phys=all_phys_bits,
|
||||||
tx_phys=phy_list_to_bits(tx_phys),
|
tx_phys=phy_list_to_bits(tx_phys),
|
||||||
rx_phys=phy_list_to_bits(rx_phys),
|
rx_phys=phy_list_to_bits(rx_phys),
|
||||||
phy_options=0 if phy_options is None else int(phy_options),
|
phy_options=0 if phy_options is None else int(phy_options),
|
||||||
),
|
)
|
||||||
check_result=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
|
logger.warning(
|
||||||
|
'HCI_LE_Set_PHY_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
|
)
|
||||||
|
raise HCI_StatusError(result)
|
||||||
|
|
||||||
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
async def set_default_phy(self, tx_phys=None, rx_phys=None):
|
||||||
all_phys_bits = (1 if tx_phys is None else 0) | (
|
all_phys_bits = (1 if tx_phys is None else 0) | (
|
||||||
(1 if rx_phys is None else 0) << 1
|
(1 if rx_phys is None else 0) << 1
|
||||||
@@ -2170,6 +2238,10 @@ class Device(CompositeEventEmitter):
|
|||||||
keys = await self.keystore.get(str(address))
|
keys = await self.keystore.get(str(address))
|
||||||
if keys is not None:
|
if keys is not None:
|
||||||
logger.debug('found keys in the key store')
|
logger.debug('found keys in the key store')
|
||||||
|
if keys.link_key is None:
|
||||||
|
logger.warning('no link key')
|
||||||
|
return None
|
||||||
|
|
||||||
return keys.link_key.value
|
return keys.link_key.value
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@@ -2289,6 +2361,34 @@ class Device(CompositeEventEmitter):
|
|||||||
'connection_encryption_failure', on_encryption_failure
|
'connection_encryption_failure', on_encryption_failure
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
async def switch_role(self, connection: Connection, role: int):
|
||||||
|
pending_role_change = asyncio.get_running_loop().create_future()
|
||||||
|
|
||||||
|
def on_role_change(new_role):
|
||||||
|
pending_role_change.set_result(new_role)
|
||||||
|
|
||||||
|
def on_role_change_failure(error_code):
|
||||||
|
pending_role_change.set_exception(HCI_Error(error_code))
|
||||||
|
|
||||||
|
connection.on('role_change', on_role_change)
|
||||||
|
connection.on('role_change_failure', on_role_change_failure)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self.send_command(
|
||||||
|
HCI_Switch_Role_Command(bd_addr=connection.peer_address, role=role) # type: ignore[call-arg]
|
||||||
|
)
|
||||||
|
if result.status != HCI_COMMAND_STATUS_PENDING:
|
||||||
|
logger.warning(
|
||||||
|
'HCI_Switch_Role_Command failed: '
|
||||||
|
f'{HCI_Constant.error_name(result.status)}'
|
||||||
|
)
|
||||||
|
raise HCI_StatusError(result)
|
||||||
|
await connection.abort_on('disconnection', pending_role_change)
|
||||||
|
finally:
|
||||||
|
connection.remove_listener('role_change', on_role_change)
|
||||||
|
connection.remove_listener('role_change_failure', on_role_change_failure)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
async def request_remote_name(self, remote: Union[Address, Connection]) -> str:
|
async def request_remote_name(self, remote: Union[Address, Connection]) -> str:
|
||||||
# Set up event handlers
|
# Set up event handlers
|
||||||
@@ -2346,8 +2446,14 @@ class Device(CompositeEventEmitter):
|
|||||||
def on_link_key(self, bd_addr, link_key, key_type):
|
def on_link_key(self, bd_addr, link_key, key_type):
|
||||||
# Store the keys in the key store
|
# Store the keys in the key store
|
||||||
if self.keystore:
|
if self.keystore:
|
||||||
|
authenticated = key_type in (
|
||||||
|
HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_192_TYPE,
|
||||||
|
HCI_AUTHENTICATED_COMBINATION_KEY_GENERATED_FROM_P_256_TYPE,
|
||||||
|
)
|
||||||
pairing_keys = PairingKeys()
|
pairing_keys = PairingKeys()
|
||||||
pairing_keys.link_key = PairingKeys.Key(value=link_key)
|
pairing_keys.link_key = PairingKeys.Key(
|
||||||
|
value=link_key, authenticated=authenticated
|
||||||
|
)
|
||||||
|
|
||||||
async def store_keys():
|
async def store_keys():
|
||||||
try:
|
try:
|
||||||
@@ -2391,25 +2497,24 @@ class Device(CompositeEventEmitter):
|
|||||||
connection_handle,
|
connection_handle,
|
||||||
transport,
|
transport,
|
||||||
peer_address,
|
peer_address,
|
||||||
peer_resolvable_address,
|
|
||||||
role,
|
role,
|
||||||
connection_parameters,
|
connection_parameters,
|
||||||
):
|
):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Connection: [0x{connection_handle:04X}] '
|
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:
|
if connection_handle in self.connections:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'new connection reuses the same handle as a previous connection'
|
'new connection reuses the same handle as a previous connection'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
peer_resolvable_address = None
|
||||||
|
|
||||||
if transport == BT_BR_EDR_TRANSPORT:
|
if transport == BT_BR_EDR_TRANSPORT:
|
||||||
# Create a new connection
|
# Create a new connection
|
||||||
connection = self.pending_connections.pop(peer_address)
|
connection = self.pending_connections.pop(peer_address)
|
||||||
connection.complete(
|
connection.complete(connection_handle, connection_parameters)
|
||||||
connection_handle, peer_resolvable_address, role, connection_parameters
|
|
||||||
)
|
|
||||||
self.connections[connection_handle] = connection
|
self.connections[connection_handle] = connection
|
||||||
|
|
||||||
# Emit an event to notify listeners of the new connection
|
# Emit an event to notify listeners of the new connection
|
||||||
@@ -2494,7 +2599,7 @@ class Device(CompositeEventEmitter):
|
|||||||
self.advertising = False
|
self.advertising = False
|
||||||
|
|
||||||
# Notify listeners
|
# Notify listeners
|
||||||
error = ConnectionError(
|
error = core.ConnectionError(
|
||||||
error_code,
|
error_code,
|
||||||
transport,
|
transport,
|
||||||
peer_address,
|
peer_address,
|
||||||
@@ -2521,7 +2626,9 @@ class Device(CompositeEventEmitter):
|
|||||||
# device configuration is set to accept any incoming connection
|
# device configuration is set to accept any incoming connection
|
||||||
elif self.classic_accept_any:
|
elif self.classic_accept_any:
|
||||||
# Save pending connection
|
# 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(
|
self.host.send_command_sync(
|
||||||
HCI_Accept_Connection_Request_Command(
|
HCI_Accept_Connection_Request_Command(
|
||||||
@@ -2567,7 +2674,7 @@ class Device(CompositeEventEmitter):
|
|||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_disconnection_failure(self, connection, error_code):
|
def on_disconnection_failure(self, connection, error_code):
|
||||||
logger.debug(f'*** Disconnection failed: {error_code}')
|
logger.debug(f'*** Disconnection failed: {error_code}')
|
||||||
error = ConnectionError(
|
error = core.ConnectionError(
|
||||||
error_code,
|
error_code,
|
||||||
connection.transport,
|
connection.transport,
|
||||||
connection.peer_address,
|
connection.peer_address,
|
||||||
@@ -2612,7 +2719,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# On Secure Simple Pairing complete, in case:
|
# On Secure Simple Pairing complete, in case:
|
||||||
# - Connection isn't already authenticated
|
# - Connection isn't already authenticated
|
||||||
# - AND we are not the initiator of the authentication
|
# - AND we are not the initiator of the authentication
|
||||||
# We must trigger authentication to known if we are truly authenticated
|
# We must trigger authentication to know if we are truly authenticated
|
||||||
if not connection.authenticating and not connection.authenticated:
|
if not connection.authenticating and not connection.authenticated:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'*** Trigger Connection Authentication: [0x{connection.handle:04X}] '
|
f'*** Trigger Connection Authentication: [0x{connection.handle:04X}] '
|
||||||
@@ -2627,22 +2734,6 @@ class Device(CompositeEventEmitter):
|
|||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
|
||||||
# Map the SMP IO capability to a Classic IO capability
|
|
||||||
# pylint: disable=line-too-long
|
|
||||||
io_capability = {
|
|
||||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
|
||||||
smp.SMP_DISPLAY_YES_NO_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
|
||||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
|
||||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
|
||||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
|
||||||
}.get(pairing_config.delegate.io_capability)
|
|
||||||
|
|
||||||
if io_capability is None:
|
|
||||||
logger.warning(
|
|
||||||
f'cannot map IO capability ({pairing_config.delegate.io_capability}'
|
|
||||||
)
|
|
||||||
io_capability = HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
|
||||||
|
|
||||||
# Compute the authentication requirements
|
# Compute the authentication requirements
|
||||||
authentication_requirements = (
|
authentication_requirements = (
|
||||||
# No Bonding
|
# No Bonding
|
||||||
@@ -2661,53 +2752,50 @@ class Device(CompositeEventEmitter):
|
|||||||
self.host.send_command_sync(
|
self.host.send_command_sync(
|
||||||
HCI_IO_Capability_Request_Reply_Command(
|
HCI_IO_Capability_Request_Reply_Command(
|
||||||
bd_addr=connection.peer_address,
|
bd_addr=connection.peer_address,
|
||||||
io_capability=io_capability,
|
io_capability=pairing_config.delegate.classic_io_capability,
|
||||||
oob_data_present=0x00, # Not present
|
oob_data_present=0x00, # Not present
|
||||||
authentication_requirements=authentication_requirements,
|
authentication_requirements=authentication_requirements,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_authentication_io_capability_response(
|
||||||
|
self, connection, io_capability, authentication_requirements
|
||||||
|
):
|
||||||
|
connection.peer_pairing_io_capability = io_capability
|
||||||
|
connection.peer_pairing_authentication_requirements = (
|
||||||
|
authentication_requirements
|
||||||
|
)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_address
|
@with_connection_from_address
|
||||||
def on_authentication_user_confirmation_request(self, connection, code):
|
def on_authentication_user_confirmation_request(self, connection, code):
|
||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
io_capability = pairing_config.delegate.classic_io_capability
|
||||||
can_compare = pairing_config.delegate.io_capability not in (
|
|
||||||
smp.SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
|
||||||
smp.SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Respond
|
# Respond
|
||||||
if can_compare:
|
if io_capability == HCI_DISPLAY_YES_NO_IO_CAPABILITY:
|
||||||
|
if connection.peer_pairing_io_capability in (
|
||||||
async def compare_numbers():
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
numbers_match = await connection.abort_on(
|
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
'disconnection',
|
):
|
||||||
pairing_config.delegate.compare_numbers(code, digits=6),
|
# Display the code and ask the user to compare
|
||||||
)
|
async def prompt():
|
||||||
if numbers_match:
|
return (
|
||||||
await self.host.send_command(
|
await pairing_config.delegate.compare_numbers(code, digits=6),
|
||||||
HCI_User_Confirmation_Request_Reply_Command(
|
|
||||||
bd_addr=connection.peer_address
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
await self.host.send_command(
|
|
||||||
HCI_User_Confirmation_Request_Negative_Reply_Command(
|
|
||||||
bd_addr=connection.peer_address
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.create_task(compare_numbers())
|
else:
|
||||||
else:
|
# Ask the user to confirm the pairing, without showing a code
|
||||||
|
async def prompt():
|
||||||
|
return await pairing_config.delegate.confirm()
|
||||||
|
|
||||||
async def confirm():
|
async def confirm():
|
||||||
confirm = await connection.abort_on(
|
if await prompt():
|
||||||
'disconnection', pairing_config.delegate.confirm()
|
|
||||||
)
|
|
||||||
if confirm:
|
|
||||||
await self.host.send_command(
|
await self.host.send_command(
|
||||||
HCI_User_Confirmation_Request_Reply_Command(
|
HCI_User_Confirmation_Request_Reply_Command(
|
||||||
bd_addr=connection.peer_address
|
bd_addr=connection.peer_address
|
||||||
@@ -2720,7 +2808,17 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
asyncio.create_task(confirm())
|
AsyncRunner.spawn(connection.abort_on('disconnection', confirm()))
|
||||||
|
return
|
||||||
|
|
||||||
|
if io_capability == HCI_DISPLAY_ONLY_IO_CAPABILITY:
|
||||||
|
# Display the code to the user
|
||||||
|
AsyncRunner.spawn(pairing_config.delegate.display_number(code, 6))
|
||||||
|
|
||||||
|
# Automatic confirmation
|
||||||
|
self.host.send_command_sync(
|
||||||
|
HCI_User_Confirmation_Request_Reply_Command(bd_addr=connection.peer_address)
|
||||||
|
)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@@ -2728,15 +2826,11 @@ class Device(CompositeEventEmitter):
|
|||||||
def on_authentication_user_passkey_request(self, connection):
|
def on_authentication_user_passkey_request(self, connection):
|
||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
io_capability = pairing_config.delegate.classic_io_capability
|
||||||
can_input = pairing_config.delegate.io_capability in (
|
|
||||||
smp.SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
|
||||||
smp.SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Respond
|
# Respond
|
||||||
if can_input:
|
if io_capability == HCI_KEYBOARD_ONLY_IO_CAPABILITY:
|
||||||
|
# Ask the user to input a number
|
||||||
async def get_number():
|
async def get_number():
|
||||||
number = await connection.abort_on(
|
number = await connection.abort_on(
|
||||||
'disconnection', pairing_config.delegate.get_number()
|
'disconnection', pairing_config.delegate.get_number()
|
||||||
@@ -2762,6 +2856,50 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_pin_code_request(self, connection):
|
||||||
|
# Classic legacy pairing
|
||||||
|
# Ask what the pairing config should be for this connection
|
||||||
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
io_capability = pairing_config.delegate.classic_io_capability
|
||||||
|
|
||||||
|
# Respond
|
||||||
|
if io_capability == HCI_KEYBOARD_ONLY_IO_CAPABILITY:
|
||||||
|
# Ask the user to enter a string
|
||||||
|
async def get_pin_code():
|
||||||
|
pin_code = await connection.abort_on(
|
||||||
|
'disconnection', pairing_config.delegate.get_string(16)
|
||||||
|
)
|
||||||
|
|
||||||
|
if pin_code is not None:
|
||||||
|
pin_code = bytes(pin_code, encoding='utf-8')
|
||||||
|
pin_code_len = len(pin_code)
|
||||||
|
assert 0 < pin_code_len <= 16, "pin_code should be 1-16 bytes"
|
||||||
|
await self.host.send_command(
|
||||||
|
HCI_PIN_Code_Request_Reply_Command(
|
||||||
|
bd_addr=connection.peer_address,
|
||||||
|
pin_code_length=pin_code_len,
|
||||||
|
pin_code=pin_code,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logger.debug("delegate.get_string() returned None")
|
||||||
|
await self.host.send_command(
|
||||||
|
HCI_PIN_Code_Request_Negative_Reply_Command(
|
||||||
|
bd_addr=connection.peer_address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(get_pin_code())
|
||||||
|
else:
|
||||||
|
self.host.send_command_sync(
|
||||||
|
HCI_PIN_Code_Request_Negative_Reply_Command(
|
||||||
|
bd_addr=connection.peer_address
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@with_connection_from_address
|
@with_connection_from_address
|
||||||
@@ -2769,6 +2907,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# Ask what the pairing config should be for this connection
|
# Ask what the pairing config should be for this connection
|
||||||
pairing_config = self.pairing_config_factory(connection)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
|
|
||||||
|
# Show the passkey to the user
|
||||||
connection.abort_on(
|
connection.abort_on(
|
||||||
'disconnection', pairing_config.delegate.display_number(passkey)
|
'disconnection', pairing_config.delegate.display_number(passkey)
|
||||||
)
|
)
|
||||||
@@ -2776,7 +2915,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@try_with_connection_from_address
|
@try_with_connection_from_address
|
||||||
def on_remote_name(self, connection, address, remote_name):
|
def on_remote_name(self, connection: Connection, address, remote_name):
|
||||||
# Try to decode the name
|
# Try to decode the name
|
||||||
try:
|
try:
|
||||||
remote_name = remote_name.decode('utf-8')
|
remote_name = remote_name.decode('utf-8')
|
||||||
@@ -2794,7 +2933,7 @@ class Device(CompositeEventEmitter):
|
|||||||
# [Classic only]
|
# [Classic only]
|
||||||
@host_event_handler
|
@host_event_handler
|
||||||
@try_with_connection_from_address
|
@try_with_connection_from_address
|
||||||
def on_remote_name_failure(self, connection, address, error):
|
def on_remote_name_failure(self, connection: Connection, address, error):
|
||||||
if connection:
|
if connection:
|
||||||
connection.emit('remote_name_failure', error)
|
connection.emit('remote_name_failure', error)
|
||||||
self.emit('remote_name_failure', address, error)
|
self.emit('remote_name_failure', address, error)
|
||||||
@@ -2905,6 +3044,21 @@ class Device(CompositeEventEmitter):
|
|||||||
)
|
)
|
||||||
connection.emit('connection_data_length_change')
|
connection.emit('connection_data_length_change')
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@with_connection_from_address
|
||||||
|
def on_role_change(self, connection, new_role):
|
||||||
|
connection.role = new_role
|
||||||
|
connection.emit('role_change', new_role)
|
||||||
|
|
||||||
|
# [Classic only]
|
||||||
|
@host_event_handler
|
||||||
|
@try_with_connection_from_address
|
||||||
|
def on_role_change_failure(self, connection, address, error):
|
||||||
|
if connection:
|
||||||
|
connection.emit('role_change_failure', error)
|
||||||
|
self.emit('role_change_failure', address, error)
|
||||||
|
|
||||||
@with_connection_from_handle
|
@with_connection_from_handle
|
||||||
def on_pairing_start(self, connection):
|
def on_pairing_start(self, connection):
|
||||||
connection.emit('pairing_start')
|
connection.emit('pairing_start')
|
||||||
|
|||||||
+2
-2
@@ -41,14 +41,14 @@ class GenericAccessService(Service):
|
|||||||
def __init__(self, device_name, appearance=(0, 0)):
|
def __init__(self, device_name, appearance=(0, 0)):
|
||||||
device_name_characteristic = Characteristic(
|
device_name_characteristic = Characteristic(
|
||||||
GATT_DEVICE_NAME_CHARACTERISTIC,
|
GATT_DEVICE_NAME_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
device_name.encode('utf-8')[:248],
|
device_name.encode('utf-8')[:248],
|
||||||
)
|
)
|
||||||
|
|
||||||
appearance_characteristic = Characteristic(
|
appearance_characteristic = Characteristic(
|
||||||
GATT_APPEARANCE_CHARACTERISTIC,
|
GATT_APPEARANCE_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
struct.pack('<H', (appearance[0] << 6) | appearance[1]),
|
struct.pack('<H', (appearance[0] << 6) | appearance[1]),
|
||||||
)
|
)
|
||||||
|
|||||||
+57
-47
@@ -28,7 +28,7 @@ import enum
|
|||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
from typing import Optional, Sequence
|
from typing import Optional, Sequence, List
|
||||||
|
|
||||||
from .colors import color
|
from .colors import color
|
||||||
from .core import UUID, get_dict_key_by_value
|
from .core import UUID, get_dict_key_by_value
|
||||||
@@ -259,63 +259,68 @@ class Characteristic(Attribute):
|
|||||||
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
See Vol 3, Part G - 3.3 CHARACTERISTIC DEFINITION
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Property flags
|
uuid: UUID
|
||||||
BROADCAST = 0x01
|
properties: Characteristic.Properties
|
||||||
READ = 0x02
|
|
||||||
WRITE_WITHOUT_RESPONSE = 0x04
|
|
||||||
WRITE = 0x08
|
|
||||||
NOTIFY = 0x10
|
|
||||||
INDICATE = 0x20
|
|
||||||
AUTHENTICATED_SIGNED_WRITES = 0x40
|
|
||||||
EXTENDED_PROPERTIES = 0x80
|
|
||||||
|
|
||||||
PROPERTY_NAMES = {
|
class Properties(enum.IntFlag):
|
||||||
BROADCAST: 'BROADCAST',
|
"""Property flags"""
|
||||||
READ: 'READ',
|
|
||||||
WRITE_WITHOUT_RESPONSE: 'WRITE_WITHOUT_RESPONSE',
|
|
||||||
WRITE: 'WRITE',
|
|
||||||
NOTIFY: 'NOTIFY',
|
|
||||||
INDICATE: 'INDICATE',
|
|
||||||
AUTHENTICATED_SIGNED_WRITES: 'AUTHENTICATED_SIGNED_WRITES',
|
|
||||||
EXTENDED_PROPERTIES: 'EXTENDED_PROPERTIES',
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
BROADCAST = 0x01
|
||||||
def property_name(property_int):
|
READ = 0x02
|
||||||
return Characteristic.PROPERTY_NAMES.get(property_int, '')
|
WRITE_WITHOUT_RESPONSE = 0x04
|
||||||
|
WRITE = 0x08
|
||||||
|
NOTIFY = 0x10
|
||||||
|
INDICATE = 0x20
|
||||||
|
AUTHENTICATED_SIGNED_WRITES = 0x40
|
||||||
|
EXTENDED_PROPERTIES = 0x80
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def properties_as_string(properties):
|
def from_string(properties_str: str) -> Characteristic.Properties:
|
||||||
return ','.join(
|
property_names: List[str] = []
|
||||||
[
|
for property in Characteristic.Properties:
|
||||||
Characteristic.property_name(p)
|
if property.name is None:
|
||||||
for p in Characteristic.PROPERTY_NAMES
|
raise TypeError()
|
||||||
if properties & p
|
property_names.append(property.name)
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
def string_to_property(property_string) -> Characteristic.Properties:
|
||||||
def string_to_properties(properties_str: str):
|
for property in zip(Characteristic.Properties, property_names):
|
||||||
return functools.reduce(
|
if property_string == property[1]:
|
||||||
lambda x, y: x | get_dict_key_by_value(Characteristic.PROPERTY_NAMES, y),
|
return property[0]
|
||||||
properties_str.split(","),
|
raise TypeError(f"Unable to convert {property_string} to Property")
|
||||||
0,
|
|
||||||
)
|
try:
|
||||||
|
return functools.reduce(
|
||||||
|
lambda x, y: x | string_to_property(y),
|
||||||
|
properties_str.split(","),
|
||||||
|
Characteristic.Properties(0),
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
raise TypeError(
|
||||||
|
f"Characteristic.Properties::from_string() error:\nExpected a string containing any of the keys, separated by commas: {','.join(property_names)}\nGot: {properties_str}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# For backwards compatibility these are defined here
|
||||||
|
# For new code, please use Characteristic.Properties.X
|
||||||
|
BROADCAST = Properties.BROADCAST
|
||||||
|
READ = Properties.READ
|
||||||
|
WRITE_WITHOUT_RESPONSE = Properties.WRITE_WITHOUT_RESPONSE
|
||||||
|
WRITE = Properties.WRITE
|
||||||
|
NOTIFY = Properties.NOTIFY
|
||||||
|
INDICATE = Properties.INDICATE
|
||||||
|
AUTHENTICATED_SIGNED_WRITES = Properties.AUTHENTICATED_SIGNED_WRITES
|
||||||
|
EXTENDED_PROPERTIES = Properties.EXTENDED_PROPERTIES
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
uuid,
|
uuid,
|
||||||
properties,
|
properties: Characteristic.Properties,
|
||||||
permissions,
|
permissions,
|
||||||
value=b'',
|
value=b'',
|
||||||
descriptors: Sequence[Descriptor] = (),
|
descriptors: Sequence[Descriptor] = (),
|
||||||
):
|
):
|
||||||
super().__init__(uuid, permissions, value)
|
super().__init__(uuid, permissions, value)
|
||||||
self.uuid = self.type
|
self.uuid = self.type
|
||||||
if isinstance(properties, str):
|
self.properties = properties
|
||||||
self.properties = Characteristic.string_to_properties(properties)
|
|
||||||
else:
|
|
||||||
self.properties = properties
|
|
||||||
self.descriptors = descriptors
|
self.descriptors = descriptors
|
||||||
|
|
||||||
def get_descriptor(self, descriptor_type):
|
def get_descriptor(self, descriptor_type):
|
||||||
@@ -325,12 +330,15 @@ class Characteristic(Attribute):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def has_properties(self, properties: Characteristic.Properties) -> bool:
|
||||||
|
return self.properties & properties == properties
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return (
|
return (
|
||||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||||
f'end=0x{self.end_group_handle:04X}, '
|
f'end=0x{self.end_group_handle:04X}, '
|
||||||
f'uuid={self.uuid}, '
|
f'uuid={self.uuid}, '
|
||||||
f'properties={Characteristic.properties_as_string(self.properties)})'
|
f'{self.properties!s})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -340,6 +348,8 @@ class CharacteristicDeclaration(Attribute):
|
|||||||
See Vol 3, Part G - 3.3.1 CHARACTERISTIC DECLARATION
|
See Vol 3, Part G - 3.3.1 CHARACTERISTIC DECLARATION
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
characteristic: Characteristic
|
||||||
|
|
||||||
def __init__(self, characteristic, value_handle):
|
def __init__(self, characteristic, value_handle):
|
||||||
declaration_bytes = (
|
declaration_bytes = (
|
||||||
struct.pack('<BH', characteristic.properties, value_handle)
|
struct.pack('<BH', characteristic.properties, value_handle)
|
||||||
@@ -355,8 +365,8 @@ class CharacteristicDeclaration(Attribute):
|
|||||||
return (
|
return (
|
||||||
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
f'CharacteristicDeclaration(handle=0x{self.handle:04X}, '
|
||||||
f'value_handle=0x{self.value_handle:04X}, '
|
f'value_handle=0x{self.value_handle:04X}, '
|
||||||
f'uuid={self.characteristic.uuid}, properties='
|
f'uuid={self.characteristic.uuid}, '
|
||||||
f'{Characteristic.properties_as_string(self.characteristic.properties)})'
|
f'{self.characteristic.properties!s})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+95
-20
@@ -23,9 +23,12 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import List, Optional, Dict, Tuple, Callable, Union, Any
|
||||||
|
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
@@ -50,6 +53,7 @@ from .att import (
|
|||||||
ATT_Read_Request,
|
ATT_Read_Request,
|
||||||
ATT_Write_Command,
|
ATT_Write_Command,
|
||||||
ATT_Write_Request,
|
ATT_Write_Request,
|
||||||
|
ATT_Error,
|
||||||
)
|
)
|
||||||
from . import core
|
from . import core
|
||||||
from .core import UUID, InvalidStateError, ProtocolError
|
from .core import UUID, InvalidStateError, ProtocolError
|
||||||
@@ -73,6 +77,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# Proxies
|
# Proxies
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class AttributeProxy(EventEmitter):
|
class AttributeProxy(EventEmitter):
|
||||||
|
client: Client
|
||||||
|
|
||||||
def __init__(self, client, handle, end_group_handle, attribute_type):
|
def __init__(self, client, handle, end_group_handle, attribute_type):
|
||||||
EventEmitter.__init__(self)
|
EventEmitter.__init__(self)
|
||||||
self.client = client
|
self.client = client
|
||||||
@@ -101,6 +107,9 @@ class AttributeProxy(EventEmitter):
|
|||||||
|
|
||||||
|
|
||||||
class ServiceProxy(AttributeProxy):
|
class ServiceProxy(AttributeProxy):
|
||||||
|
uuid: UUID
|
||||||
|
characteristics: List[CharacteristicProxy]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_client(service_class, client, service_uuid):
|
def from_client(service_class, client, service_uuid):
|
||||||
# The service and its characteristics are considered to have already been
|
# The service and its characteristics are considered to have already been
|
||||||
@@ -130,10 +139,21 @@ class ServiceProxy(AttributeProxy):
|
|||||||
|
|
||||||
|
|
||||||
class CharacteristicProxy(AttributeProxy):
|
class CharacteristicProxy(AttributeProxy):
|
||||||
def __init__(self, client, handle, end_group_handle, uuid, properties):
|
properties: Characteristic.Properties
|
||||||
|
descriptors: List[DescriptorProxy]
|
||||||
|
subscribers: Dict[Any, Callable]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
client,
|
||||||
|
handle,
|
||||||
|
end_group_handle,
|
||||||
|
uuid,
|
||||||
|
properties: int,
|
||||||
|
):
|
||||||
super().__init__(client, handle, end_group_handle, uuid)
|
super().__init__(client, handle, end_group_handle, uuid)
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.properties = properties
|
self.properties = Characteristic.Properties(properties)
|
||||||
self.descriptors = []
|
self.descriptors = []
|
||||||
self.descriptors_discovered = False
|
self.descriptors_discovered = False
|
||||||
self.subscribers = {} # Map from subscriber to proxy subscriber
|
self.subscribers = {} # Map from subscriber to proxy subscriber
|
||||||
@@ -148,7 +168,9 @@ class CharacteristicProxy(AttributeProxy):
|
|||||||
async def discover_descriptors(self):
|
async def discover_descriptors(self):
|
||||||
return await self.client.discover_descriptors(self)
|
return await self.client.discover_descriptors(self)
|
||||||
|
|
||||||
async def subscribe(self, subscriber=None, prefer_notify=True):
|
async def subscribe(
|
||||||
|
self, subscriber: Optional[Callable] = None, prefer_notify=True
|
||||||
|
):
|
||||||
if subscriber is not None:
|
if subscriber is not None:
|
||||||
if subscriber in self.subscribers:
|
if subscriber in self.subscribers:
|
||||||
# We already have a proxy subscriber
|
# We already have a proxy subscriber
|
||||||
@@ -175,7 +197,7 @@ class CharacteristicProxy(AttributeProxy):
|
|||||||
return (
|
return (
|
||||||
f'Characteristic(handle=0x{self.handle:04X}, '
|
f'Characteristic(handle=0x{self.handle:04X}, '
|
||||||
f'uuid={self.uuid}, '
|
f'uuid={self.uuid}, '
|
||||||
f'properties={Characteristic.properties_as_string(self.properties)})'
|
f'{self.properties!s})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -201,6 +223,9 @@ class ProfileServiceProxy:
|
|||||||
# GATT Client
|
# GATT Client
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Client:
|
class Client:
|
||||||
|
services: List[ServiceProxy]
|
||||||
|
cached_values: Dict[int, Tuple[datetime, bytes]]
|
||||||
|
|
||||||
def __init__(self, connection):
|
def __init__(self, connection):
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
self.mtu_exchange_done = False
|
self.mtu_exchange_done = False
|
||||||
@@ -212,6 +237,7 @@ class Client:
|
|||||||
) # Notification subscribers, by attribute handle
|
) # Notification subscribers, by attribute handle
|
||||||
self.indication_subscribers = {} # Indication subscribers, by attribute handle
|
self.indication_subscribers = {} # Indication subscribers, by attribute handle
|
||||||
self.services = []
|
self.services = []
|
||||||
|
self.cached_values = {}
|
||||||
|
|
||||||
def send_gatt_pdu(self, pdu):
|
def send_gatt_pdu(self, pdu):
|
||||||
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
self.connection.send_l2cap_pdu(ATT_CID, pdu)
|
||||||
@@ -296,6 +322,35 @@ class Client:
|
|||||||
if c.uuid == uuid
|
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):
|
def on_service_discovered(self, service):
|
||||||
'''Add a service to the service list if it wasn't already there'''
|
'''Add a service to the service list if it wasn't already there'''
|
||||||
already_known = False
|
already_known = False
|
||||||
@@ -306,7 +361,7 @@ class Client:
|
|||||||
if not already_known:
|
if not already_known:
|
||||||
self.services.append(service)
|
self.services.append(service)
|
||||||
|
|
||||||
async def discover_services(self, uuids=None):
|
async def discover_services(self, uuids=None) -> List[ServiceProxy]:
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
See Vol 3, Part G - 4.4.1 Discover All Primary Services
|
||||||
'''
|
'''
|
||||||
@@ -332,8 +387,10 @@ class Client:
|
|||||||
'!!! unexpected error while discovering services: '
|
'!!! unexpected error while discovering services: '
|
||||||
f'{HCI_Constant.error_name(response.error_code)}'
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
raise ATT_Error(
|
||||||
return
|
error_code=response.error_code,
|
||||||
|
message='Unexpected error while discovering services',
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
for (
|
for (
|
||||||
@@ -349,7 +406,7 @@ class Client:
|
|||||||
logger.warning(
|
logger.warning(
|
||||||
f'bogus handle values: {attribute_handle} {end_group_handle}'
|
f'bogus handle values: {attribute_handle} {end_group_handle}'
|
||||||
)
|
)
|
||||||
return
|
return []
|
||||||
|
|
||||||
# Create a service proxy for this service
|
# Create a service proxy for this service
|
||||||
service = ServiceProxy(
|
service = ServiceProxy(
|
||||||
@@ -452,7 +509,9 @@ class Client:
|
|||||||
# TODO
|
# TODO
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def discover_characteristics(self, uuids, service):
|
async def discover_characteristics(
|
||||||
|
self, uuids, service: Optional[ServiceProxy]
|
||||||
|
) -> List[CharacteristicProxy]:
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
See Vol 3, Part G - 4.6.1 Discover All Characteristics of a Service and 4.6.2
|
||||||
Discover Characteristics by UUID
|
Discover Characteristics by UUID
|
||||||
@@ -465,12 +524,12 @@ class Client:
|
|||||||
services = [service] if service else self.services
|
services = [service] if service else self.services
|
||||||
|
|
||||||
# Perform characteristic discovery for each service
|
# Perform characteristic discovery for each service
|
||||||
discovered_characteristics = []
|
discovered_characteristics: List[CharacteristicProxy] = []
|
||||||
for service in services:
|
for service in services:
|
||||||
starting_handle = service.handle
|
starting_handle = service.handle
|
||||||
ending_handle = service.end_group_handle
|
ending_handle = service.end_group_handle
|
||||||
|
|
||||||
characteristics = []
|
characteristics: List[CharacteristicProxy] = []
|
||||||
while starting_handle <= ending_handle:
|
while starting_handle <= ending_handle:
|
||||||
response = await self.send_request(
|
response = await self.send_request(
|
||||||
ATT_Read_By_Type_Request(
|
ATT_Read_By_Type_Request(
|
||||||
@@ -491,8 +550,10 @@ class Client:
|
|||||||
'!!! unexpected error while discovering characteristics: '
|
'!!! unexpected error while discovering characteristics: '
|
||||||
f'{HCI_Constant.error_name(response.error_code)}'
|
f'{HCI_Constant.error_name(response.error_code)}'
|
||||||
)
|
)
|
||||||
# TODO raise appropriate exception
|
raise ATT_Error(
|
||||||
return
|
error_code=response.error_code,
|
||||||
|
message='Unexpected error while discovering characteristics',
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Stop if for some reason the list was empty
|
# Stop if for some reason the list was empty
|
||||||
@@ -535,8 +596,11 @@ class Client:
|
|||||||
return discovered_characteristics
|
return discovered_characteristics
|
||||||
|
|
||||||
async def discover_descriptors(
|
async def discover_descriptors(
|
||||||
self, characteristic=None, start_handle=None, end_handle=None
|
self,
|
||||||
):
|
characteristic: Optional[CharacteristicProxy] = None,
|
||||||
|
start_handle=None,
|
||||||
|
end_handle=None,
|
||||||
|
) -> List[DescriptorProxy]:
|
||||||
'''
|
'''
|
||||||
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
|
See Vol 3, Part G - 4.7.1 Discover All Characteristic Descriptors
|
||||||
'''
|
'''
|
||||||
@@ -549,7 +613,7 @@ class Client:
|
|||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
descriptors = []
|
descriptors: List[DescriptorProxy] = []
|
||||||
while starting_handle <= ending_handle:
|
while starting_handle <= ending_handle:
|
||||||
response = await self.send_request(
|
response = await self.send_request(
|
||||||
ATT_Find_Information_Request(
|
ATT_Find_Information_Request(
|
||||||
@@ -656,8 +720,8 @@ class Client:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if (
|
if (
|
||||||
characteristic.properties & Characteristic.NOTIFY
|
characteristic.properties & Characteristic.Properties.NOTIFY
|
||||||
and characteristic.properties & Characteristic.INDICATE
|
and characteristic.properties & Characteristic.Properties.INDICATE
|
||||||
):
|
):
|
||||||
if prefer_notify:
|
if prefer_notify:
|
||||||
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
||||||
@@ -665,10 +729,10 @@ class Client:
|
|||||||
else:
|
else:
|
||||||
bits = ClientCharacteristicConfigurationBits.INDICATION
|
bits = ClientCharacteristicConfigurationBits.INDICATION
|
||||||
subscribers = self.indication_subscribers
|
subscribers = self.indication_subscribers
|
||||||
elif characteristic.properties & Characteristic.NOTIFY:
|
elif characteristic.properties & Characteristic.Properties.NOTIFY:
|
||||||
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
bits = ClientCharacteristicConfigurationBits.NOTIFICATION
|
||||||
subscribers = self.notification_subscribers
|
subscribers = self.notification_subscribers
|
||||||
elif characteristic.properties & Characteristic.INDICATE:
|
elif characteristic.properties & Characteristic.Properties.INDICATE:
|
||||||
bits = ClientCharacteristicConfigurationBits.INDICATION
|
bits = ClientCharacteristicConfigurationBits.INDICATION
|
||||||
subscribers = self.indication_subscribers
|
subscribers = self.indication_subscribers
|
||||||
else:
|
else:
|
||||||
@@ -778,6 +842,7 @@ class Client:
|
|||||||
|
|
||||||
offset += len(part)
|
offset += len(part)
|
||||||
|
|
||||||
|
self.cache_value(attribute_handle, attribute_value)
|
||||||
# Return the value as bytes
|
# Return the value as bytes
|
||||||
return attribute_value
|
return attribute_value
|
||||||
|
|
||||||
@@ -912,6 +977,8 @@ class Client:
|
|||||||
)
|
)
|
||||||
if not subscribers:
|
if not subscribers:
|
||||||
logger.warning('!!! received notification with no subscriber')
|
logger.warning('!!! received notification with no subscriber')
|
||||||
|
|
||||||
|
self.cache_value(notification.attribute_handle, notification.attribute_value)
|
||||||
for subscriber in subscribers:
|
for subscriber in subscribers:
|
||||||
if callable(subscriber):
|
if callable(subscriber):
|
||||||
subscriber(notification.attribute_value)
|
subscriber(notification.attribute_value)
|
||||||
@@ -923,6 +990,8 @@ class Client:
|
|||||||
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
|
subscribers = self.indication_subscribers.get(indication.attribute_handle, [])
|
||||||
if not subscribers:
|
if not subscribers:
|
||||||
logger.warning('!!! received indication with no subscriber')
|
logger.warning('!!! received indication with no subscriber')
|
||||||
|
|
||||||
|
self.cache_value(indication.attribute_handle, indication.attribute_value)
|
||||||
for subscriber in subscribers:
|
for subscriber in subscribers:
|
||||||
if callable(subscriber):
|
if callable(subscriber):
|
||||||
subscriber(indication.attribute_value)
|
subscriber(indication.attribute_value)
|
||||||
@@ -931,3 +1000,9 @@ class Client:
|
|||||||
|
|
||||||
# Confirm that we received the indication
|
# Confirm that we received the indication
|
||||||
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
self.send_confirmation(ATT_Handle_Value_Confirmation())
|
||||||
|
|
||||||
|
def cache_value(self, attribute_handle: int, value: bytes):
|
||||||
|
self.cached_values[attribute_handle] = (
|
||||||
|
datetime.now(),
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
|||||||
+21
-3
@@ -27,7 +27,7 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import struct
|
import struct
|
||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional, TypeVar, Type
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .colors import color
|
from .colors import color
|
||||||
@@ -135,6 +135,21 @@ class Server(EventEmitter):
|
|||||||
return attribute
|
return attribute
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
AttributeGroupType = TypeVar('AttributeGroupType', Service, Characteristic)
|
||||||
|
|
||||||
|
def get_attribute_group(
|
||||||
|
self, handle: int, group_type: Type[AttributeGroupType]
|
||||||
|
) -> Optional[AttributeGroupType]:
|
||||||
|
return next(
|
||||||
|
(
|
||||||
|
attribute
|
||||||
|
for attribute in self.attributes
|
||||||
|
if isinstance(attribute, group_type)
|
||||||
|
and attribute.handle <= handle <= attribute.end_group_handle
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
def get_service_attribute(self, service_uuid: UUID) -> Optional[Service]:
|
def get_service_attribute(self, service_uuid: UUID) -> Optional[Service]:
|
||||||
return next(
|
return next(
|
||||||
(
|
(
|
||||||
@@ -228,7 +243,10 @@ class Server(EventEmitter):
|
|||||||
# unless there is one already
|
# unless there is one already
|
||||||
if (
|
if (
|
||||||
characteristic.properties
|
characteristic.properties
|
||||||
& (Characteristic.NOTIFY | Characteristic.INDICATE)
|
& (
|
||||||
|
Characteristic.Properties.NOTIFY
|
||||||
|
| Characteristic.Properties.INDICATE
|
||||||
|
)
|
||||||
and characteristic.get_descriptor(
|
and characteristic.get_descriptor(
|
||||||
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
|
GATT_CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR
|
||||||
)
|
)
|
||||||
@@ -691,7 +709,7 @@ class Server(EventEmitter):
|
|||||||
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
length=entry_size, attribute_data_list=b''.join(attribute_data_list)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logging.warning(f"not found {request}")
|
logging.debug(f"not found {request}")
|
||||||
|
|
||||||
self.send_response(connection, response)
|
self.send_response(connection, response)
|
||||||
|
|
||||||
|
|||||||
+19
-1
@@ -1491,7 +1491,7 @@ class HCI_Object:
|
|||||||
elif field_type == -2:
|
elif field_type == -2:
|
||||||
# 16-bit signed
|
# 16-bit signed
|
||||||
field_value = struct.unpack_from('<h', data, offset)[0]
|
field_value = struct.unpack_from('<h', data, offset)[0]
|
||||||
offset += 1
|
offset += 2
|
||||||
elif field_type == 3:
|
elif field_type == 3:
|
||||||
# 24-bit unsigned
|
# 24-bit unsigned
|
||||||
padded = data[offset : offset + 3] + bytes([0])
|
padded = data[offset : offset + 3] + bytes([0])
|
||||||
@@ -2097,6 +2097,24 @@ class HCI_Link_Key_Request_Negative_Reply_Command(HCI_Command):
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@HCI_Command.command(
|
||||||
|
fields=[
|
||||||
|
('bd_addr', Address.parse_address),
|
||||||
|
('pin_code_length', 1),
|
||||||
|
('pin_code', 16),
|
||||||
|
],
|
||||||
|
return_parameters_fields=[
|
||||||
|
('status', STATUS_SPEC),
|
||||||
|
('bd_addr', Address.parse_address),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
class HCI_PIN_Code_Request_Reply_Command(HCI_Command):
|
||||||
|
'''
|
||||||
|
See Bluetooth spec @ 7.1.12 PIN Code Request Reply Command
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@HCI_Command.command(
|
@HCI_Command.command(
|
||||||
fields=[('bd_addr', Address.parse_address)],
|
fields=[('bd_addr', Address.parse_address)],
|
||||||
|
|||||||
+36
-20
@@ -24,7 +24,10 @@ from bumble.colors import color
|
|||||||
from bumble.l2cap import L2CAP_PDU
|
from bumble.l2cap import L2CAP_PDU
|
||||||
from bumble.snoop import Snooper
|
from bumble.snoop import Snooper
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from .hci import (
|
from .hci import (
|
||||||
|
Address,
|
||||||
HCI_ACL_DATA_PACKET,
|
HCI_ACL_DATA_PACKET,
|
||||||
HCI_COMMAND_COMPLETE_EVENT,
|
HCI_COMMAND_COMPLETE_EVENT,
|
||||||
HCI_COMMAND_PACKET,
|
HCI_COMMAND_PACKET,
|
||||||
@@ -53,7 +56,6 @@ from .hci import (
|
|||||||
HCI_LE_Write_Suggested_Default_Data_Length_Command,
|
HCI_LE_Write_Suggested_Default_Data_Length_Command,
|
||||||
HCI_Link_Key_Request_Negative_Reply_Command,
|
HCI_Link_Key_Request_Negative_Reply_Command,
|
||||||
HCI_Link_Key_Request_Reply_Command,
|
HCI_Link_Key_Request_Reply_Command,
|
||||||
HCI_PIN_Code_Request_Negative_Reply_Command,
|
|
||||||
HCI_Packet,
|
HCI_Packet,
|
||||||
HCI_Read_Buffer_Size_Command,
|
HCI_Read_Buffer_Size_Command,
|
||||||
HCI_Read_Local_Supported_Commands_Command,
|
HCI_Read_Local_Supported_Commands_Command,
|
||||||
@@ -92,10 +94,9 @@ HOST_HC_TOTAL_NUM_ACL_DATA_PACKETS = 1
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Connection:
|
class Connection:
|
||||||
def __init__(self, host, handle, role, peer_address, transport):
|
def __init__(self, host, handle, peer_address, transport):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.handle = handle
|
self.handle = handle
|
||||||
self.role = role
|
|
||||||
self.peer_address = peer_address
|
self.peer_address = peer_address
|
||||||
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
self.assembler = HCI_AclDataPacketAssembler(self.on_acl_pdu)
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
@@ -142,6 +143,24 @@ class Host(AbortableEventEmitter):
|
|||||||
if controller_sink:
|
if controller_sink:
|
||||||
self.set_packet_sink(controller_sink)
|
self.set_packet_sink(controller_sink)
|
||||||
|
|
||||||
|
def find_connection_by_bd_addr(
|
||||||
|
self,
|
||||||
|
bd_addr: Address,
|
||||||
|
transport: Optional[int] = None,
|
||||||
|
check_address_type: bool = False,
|
||||||
|
) -> Optional[Connection]:
|
||||||
|
for connection in self.connections.values():
|
||||||
|
if connection.peer_address.to_bytes() == bd_addr.to_bytes():
|
||||||
|
if (
|
||||||
|
check_address_type
|
||||||
|
and connection.peer_address.address_type != bd_addr.address_type
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if transport is None or connection.transport == transport:
|
||||||
|
return connection
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
async def flush(self) -> None:
|
async def flush(self) -> None:
|
||||||
# Make sure no command is pending
|
# Make sure no command is pending
|
||||||
await self.command_semaphore.acquire()
|
await self.command_semaphore.acquire()
|
||||||
@@ -376,8 +395,8 @@ class Host(AbortableEventEmitter):
|
|||||||
|
|
||||||
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
|
||||||
for (octet, flags) in enumerate(HCI_SUPPORTED_COMMANDS_FLAGS):
|
for octet, flags in enumerate(HCI_SUPPORTED_COMMANDS_FLAGS):
|
||||||
for (flag_position, value) in enumerate(flags):
|
for flag_position, value in enumerate(flags):
|
||||||
if value == command:
|
if value == command:
|
||||||
# Check if the flag is set
|
# Check if the flag is set
|
||||||
if octet < len(self.local_supported_commands) and flag_position < 8:
|
if octet < len(self.local_supported_commands) and flag_position < 8:
|
||||||
@@ -390,7 +409,7 @@ class Host(AbortableEventEmitter):
|
|||||||
@property
|
@property
|
||||||
def supported_commands(self):
|
def supported_commands(self):
|
||||||
commands = []
|
commands = []
|
||||||
for (octet, flags) in enumerate(self.local_supported_commands):
|
for octet, flags in enumerate(self.local_supported_commands):
|
||||||
if octet < len(HCI_SUPPORTED_COMMANDS_FLAGS):
|
if octet < len(HCI_SUPPORTED_COMMANDS_FLAGS):
|
||||||
for flag in range(8):
|
for flag in range(8):
|
||||||
if flags & (1 << flag) != 0:
|
if flags & (1 << flag) != 0:
|
||||||
@@ -514,7 +533,7 @@ class Host(AbortableEventEmitter):
|
|||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
# Create/update the connection
|
# Create/update the connection
|
||||||
logger.debug(
|
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)}'
|
f'{event.peer_address} as {HCI_Constant.role_name(event.role)}'
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -523,7 +542,6 @@ class Host(AbortableEventEmitter):
|
|||||||
connection = Connection(
|
connection = Connection(
|
||||||
self,
|
self,
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
event.role,
|
|
||||||
event.peer_address,
|
event.peer_address,
|
||||||
BT_LE_TRANSPORT,
|
BT_LE_TRANSPORT,
|
||||||
)
|
)
|
||||||
@@ -540,7 +558,6 @@ class Host(AbortableEventEmitter):
|
|||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
BT_LE_TRANSPORT,
|
BT_LE_TRANSPORT,
|
||||||
event.peer_address,
|
event.peer_address,
|
||||||
None,
|
|
||||||
event.role,
|
event.role,
|
||||||
connection_parameters,
|
connection_parameters,
|
||||||
)
|
)
|
||||||
@@ -569,7 +586,6 @@ class Host(AbortableEventEmitter):
|
|||||||
connection = Connection(
|
connection = Connection(
|
||||||
self,
|
self,
|
||||||
event.connection_handle,
|
event.connection_handle,
|
||||||
BT_CENTRAL_ROLE,
|
|
||||||
event.bd_addr,
|
event.bd_addr,
|
||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
)
|
)
|
||||||
@@ -582,7 +598,6 @@ class Host(AbortableEventEmitter):
|
|||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
event.bd_addr,
|
event.bd_addr,
|
||||||
None,
|
None,
|
||||||
BT_CENTRAL_ROLE,
|
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -602,8 +617,7 @@ class Host(AbortableEventEmitter):
|
|||||||
if event.status == HCI_SUCCESS:
|
if event.status == HCI_SUCCESS:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
|
f'### DISCONNECTION: [0x{event.connection_handle:04X}] '
|
||||||
f'{connection.peer_address} as '
|
f'{connection.peer_address} '
|
||||||
f'{HCI_Constant.role_name(connection.role)}, '
|
|
||||||
f'reason={event.reason}'
|
f'reason={event.reason}'
|
||||||
)
|
)
|
||||||
del self.connections[event.connection_handle]
|
del self.connections[event.connection_handle]
|
||||||
@@ -719,12 +733,13 @@ class Host(AbortableEventEmitter):
|
|||||||
f'role change for {event.bd_addr}: '
|
f'role change for {event.bd_addr}: '
|
||||||
f'{HCI_Constant.role_name(event.new_role)}'
|
f'{HCI_Constant.role_name(event.new_role)}'
|
||||||
)
|
)
|
||||||
# TODO: lookup the connection and update the role
|
self.emit('role_change', event.bd_addr, event.new_role)
|
||||||
else:
|
else:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'role change for {event.bd_addr} failed: '
|
f'role change for {event.bd_addr} failed: '
|
||||||
f'{HCI_Constant.error_name(event.status)}'
|
f'{HCI_Constant.error_name(event.status)}'
|
||||||
)
|
)
|
||||||
|
self.emit('role_change_failure', event.bd_addr, event.status)
|
||||||
|
|
||||||
def on_hci_le_data_length_change_event(self, event):
|
def on_hci_le_data_length_change_event(self, event):
|
||||||
self.emit(
|
self.emit(
|
||||||
@@ -794,11 +809,7 @@ class Host(AbortableEventEmitter):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_hci_pin_code_request_event(self, event):
|
def on_hci_pin_code_request_event(self, event):
|
||||||
# For now, just refuse all requests
|
self.emit('pin_code_request', event.bd_addr)
|
||||||
# TODO: delegate the decision
|
|
||||||
self.send_command_sync(
|
|
||||||
HCI_PIN_Code_Request_Negative_Reply_Command(bd_addr=event.bd_addr)
|
|
||||||
)
|
|
||||||
|
|
||||||
def on_hci_link_key_request_event(self, event):
|
def on_hci_link_key_request_event(self, event):
|
||||||
async def send_link_key():
|
async def send_link_key():
|
||||||
@@ -828,7 +839,12 @@ class Host(AbortableEventEmitter):
|
|||||||
self.emit('authentication_io_capability_request', event.bd_addr)
|
self.emit('authentication_io_capability_request', event.bd_addr)
|
||||||
|
|
||||||
def on_hci_io_capability_response_event(self, event):
|
def on_hci_io_capability_response_event(self, event):
|
||||||
pass
|
self.emit(
|
||||||
|
'authentication_io_capability_response',
|
||||||
|
event.bd_addr,
|
||||||
|
event.io_capability,
|
||||||
|
event.authentication_requirements,
|
||||||
|
)
|
||||||
|
|
||||||
def on_hci_user_confirmation_request_event(self, event):
|
def on_hci_user_confirmation_request_event(self, event):
|
||||||
self.emit(
|
self.emit(
|
||||||
|
|||||||
+27
-11
@@ -20,15 +20,19 @@
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from typing import Optional
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
from .colors import color
|
from .colors import color
|
||||||
from .hci import Address
|
from .hci import Address
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .device import Device
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -173,13 +177,13 @@ class KeyStore:
|
|||||||
separator = '\n'
|
separator = '\n'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_for_device(device_config):
|
def create_for_device(device: Device) -> Optional[KeyStore]:
|
||||||
if device_config.keystore is None:
|
if device.config.keystore is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
keystore_type = device_config.keystore.split(':', 1)[0]
|
keystore_type = device.config.keystore.split(':', 1)[0]
|
||||||
if keystore_type == 'JsonKeyStore':
|
if keystore_type == 'JsonKeyStore':
|
||||||
return JsonKeyStore.from_device_config(device_config)
|
return JsonKeyStore.from_device(device)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -204,7 +208,9 @@ class JsonKeyStore(KeyStore):
|
|||||||
self.directory_name = os.path.join(
|
self.directory_name = os.path.join(
|
||||||
appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), self.KEYS_DIR
|
appdirs.user_data_dir(self.APP_NAME, self.APP_AUTHOR), self.KEYS_DIR
|
||||||
)
|
)
|
||||||
json_filename = f'{self.namespace}.json'.lower().replace(':', '-')
|
json_filename = (
|
||||||
|
f'{self.namespace}.json'.lower().replace(':', '-').replace('/p', '-p')
|
||||||
|
)
|
||||||
self.filename = os.path.join(self.directory_name, json_filename)
|
self.filename = os.path.join(self.directory_name, json_filename)
|
||||||
else:
|
else:
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
@@ -213,9 +219,19 @@ class JsonKeyStore(KeyStore):
|
|||||||
logger.debug(f'JSON keystore: {self.filename}')
|
logger.debug(f'JSON keystore: {self.filename}')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_device_config(device_config):
|
def from_device(device: Device) -> Optional[JsonKeyStore]:
|
||||||
params = device_config.keystore.split(':', 1)[1:]
|
if not device.config.keystore:
|
||||||
namespace = str(device_config.address)
|
return None
|
||||||
|
|
||||||
|
params = device.config.keystore.split(':', 1)[1:]
|
||||||
|
|
||||||
|
# Use a namespace based on the device address
|
||||||
|
if device.public_address not in (Address.ANY, Address.ANY_RANDOM):
|
||||||
|
namespace = str(device.public_address)
|
||||||
|
elif device.random_address != Address.ANY_RANDOM:
|
||||||
|
namespace = str(device.random_address)
|
||||||
|
else:
|
||||||
|
namespace = JsonKeyStore.DEFAULT_NAMESPACE
|
||||||
if params:
|
if params:
|
||||||
filename = params[0]
|
filename = params[0]
|
||||||
else:
|
else:
|
||||||
@@ -241,7 +257,7 @@ class JsonKeyStore(KeyStore):
|
|||||||
json.dump(db, output, sort_keys=True, indent=4)
|
json.dump(db, output, sort_keys=True, indent=4)
|
||||||
|
|
||||||
# Atomically replace the previous file
|
# Atomically replace the previous file
|
||||||
os.rename(temp_filename, self.filename)
|
os.replace(temp_filename, self.filename)
|
||||||
|
|
||||||
async def delete(self, name: str) -> None:
|
async def delete(self, name: str) -> None:
|
||||||
db = await self.load()
|
db = await self.load()
|
||||||
@@ -257,7 +273,7 @@ class JsonKeyStore(KeyStore):
|
|||||||
db = await self.load()
|
db = await self.load()
|
||||||
|
|
||||||
namespace = db.setdefault(self.namespace, {})
|
namespace = db.setdefault(self.namespace, {})
|
||||||
namespace[name] = keys.to_dict()
|
namespace.setdefault(name, {}).update(keys.to_dict())
|
||||||
|
|
||||||
await self.save(db)
|
await self.save(db)
|
||||||
|
|
||||||
|
|||||||
@@ -796,6 +796,11 @@ class Channel(EventEmitter):
|
|||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
if self.state == self.OPEN:
|
||||||
|
self.change_state(self.CLOSED)
|
||||||
|
self.emit('close')
|
||||||
|
|
||||||
def send_configure_request(self):
|
def send_configure_request(self):
|
||||||
options = L2CAP_Control_Frame.encode_configuration_options(
|
options = L2CAP_Control_Frame.encode_configuration_options(
|
||||||
[
|
[
|
||||||
@@ -1105,6 +1110,10 @@ class LeConnectionOrientedChannel(EventEmitter):
|
|||||||
self.disconnection_result = asyncio.get_running_loop().create_future()
|
self.disconnection_result = asyncio.get_running_loop().create_future()
|
||||||
return await self.disconnection_result
|
return await self.disconnection_result
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
if self.state == self.CONNECTED:
|
||||||
|
self.change_state(self.DISCONNECTED)
|
||||||
|
|
||||||
def on_pdu(self, pdu):
|
def on_pdu(self, pdu):
|
||||||
if self.sink is None:
|
if self.sink is None:
|
||||||
logger.warning('received pdu without a sink')
|
logger.warning('received pdu without a sink')
|
||||||
@@ -1492,8 +1501,12 @@ class ChannelManager:
|
|||||||
def on_disconnection(self, connection_handle, _reason):
|
def on_disconnection(self, connection_handle, _reason):
|
||||||
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
logger.debug(f'disconnection from {connection_handle}, cleaning up channels')
|
||||||
if connection_handle in self.channels:
|
if connection_handle in self.channels:
|
||||||
|
for _, channel in self.channels[connection_handle].items():
|
||||||
|
channel.abort()
|
||||||
del self.channels[connection_handle]
|
del self.channels[connection_handle]
|
||||||
if connection_handle in self.le_coc_channels:
|
if connection_handle in self.le_coc_channels:
|
||||||
|
for _, channel in self.le_coc_channels[connection_handle].items():
|
||||||
|
channel.abort()
|
||||||
del self.le_coc_channels[connection_handle]
|
del self.le_coc_channels[connection_handle]
|
||||||
if connection_handle in self.identifiers:
|
if connection_handle in self.identifiers:
|
||||||
del self.identifiers[connection_handle]
|
del self.identifiers[connection_handle]
|
||||||
|
|||||||
+118
-6
@@ -19,12 +19,15 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
from bumble.core import BT_PERIPHERAL_ROLE, BT_BR_EDR_TRANSPORT, BT_LE_TRANSPORT
|
||||||
from bumble.colors import color
|
from bumble.colors import color
|
||||||
from bumble.hci import (
|
from bumble.hci import (
|
||||||
Address,
|
Address,
|
||||||
HCI_SUCCESS,
|
HCI_SUCCESS,
|
||||||
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
HCI_CONNECTION_ACCEPT_TIMEOUT_ERROR,
|
||||||
HCI_CONNECTION_TIMEOUT_ERROR,
|
HCI_CONNECTION_TIMEOUT_ERROR,
|
||||||
|
HCI_PAGE_TIMEOUT_ERROR,
|
||||||
|
HCI_Connection_Complete_Event,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -57,6 +60,11 @@ class LocalLink:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.controllers = set()
|
self.controllers = set()
|
||||||
self.pending_connection = None
|
self.pending_connection = None
|
||||||
|
self.pending_classic_connection = None
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
# Common utils
|
||||||
|
############################################################
|
||||||
|
|
||||||
def add_controller(self, controller):
|
def add_controller(self, controller):
|
||||||
logger.debug(f'new controller: {controller}')
|
logger.debug(f'new controller: {controller}')
|
||||||
@@ -71,22 +79,39 @@ class LocalLink:
|
|||||||
return controller
|
return controller
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def on_address_changed(self, controller):
|
def find_classic_controller(self, address):
|
||||||
pass
|
for controller in self.controllers:
|
||||||
|
if controller.public_address == address:
|
||||||
|
return controller
|
||||||
|
return None
|
||||||
|
|
||||||
def get_pending_connection(self):
|
def get_pending_connection(self):
|
||||||
return self.pending_connection
|
return self.pending_connection
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
# LE handlers
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
def on_address_changed(self, controller):
|
||||||
|
pass
|
||||||
|
|
||||||
def send_advertising_data(self, sender_address, data):
|
def send_advertising_data(self, sender_address, data):
|
||||||
# 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)
|
||||||
|
|
||||||
def send_acl_data(self, sender_address, destination_address, 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
|
||||||
if controller := self.find_controller(destination_address):
|
if transport == BT_LE_TRANSPORT:
|
||||||
controller.on_link_acl_data(sender_address, data)
|
destination_controller = self.find_controller(destination_address)
|
||||||
|
source_address = sender_controller.random_address
|
||||||
|
elif transport == BT_BR_EDR_TRANSPORT:
|
||||||
|
destination_controller = self.find_classic_controller(destination_address)
|
||||||
|
source_address = sender_controller.public_address
|
||||||
|
|
||||||
|
if destination_controller is not None:
|
||||||
|
destination_controller.on_link_acl_data(source_address, transport, data)
|
||||||
|
|
||||||
def on_connection_complete(self):
|
def on_connection_complete(self):
|
||||||
# Check that we expect this call
|
# Check that we expect this call
|
||||||
@@ -163,6 +188,89 @@ class LocalLink:
|
|||||||
if peripheral_controller := self.find_controller(peripheral_address):
|
if peripheral_controller := self.find_controller(peripheral_address):
|
||||||
peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk)
|
peripheral_controller.on_link_encrypted(central_address, rand, ediv, ltk)
|
||||||
|
|
||||||
|
############################################################
|
||||||
|
# Classic handlers
|
||||||
|
############################################################
|
||||||
|
|
||||||
|
def classic_connect(self, initiator_controller, responder_address):
|
||||||
|
logger.debug(
|
||||||
|
f'[Classic] {initiator_controller.public_address} connects to {responder_address}'
|
||||||
|
)
|
||||||
|
responder_controller = self.find_classic_controller(responder_address)
|
||||||
|
if responder_controller is None:
|
||||||
|
initiator_controller.on_classic_connection_complete(
|
||||||
|
responder_address, HCI_PAGE_TIMEOUT_ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
self.pending_classic_connection = (initiator_controller, responder_controller)
|
||||||
|
|
||||||
|
responder_controller.on_classic_connection_request(
|
||||||
|
initiator_controller.public_address,
|
||||||
|
HCI_Connection_Complete_Event.ACL_LINK_TYPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
def classic_accept_connection(
|
||||||
|
self, responder_controller, initiator_address, responder_role
|
||||||
|
):
|
||||||
|
logger.debug(
|
||||||
|
f'[Classic] {responder_controller.public_address} accepts to connect {initiator_address}'
|
||||||
|
)
|
||||||
|
initiator_controller = self.find_classic_controller(initiator_address)
|
||||||
|
if initiator_controller is None:
|
||||||
|
responder_controller.on_classic_connection_complete(
|
||||||
|
responder_controller.public_address, HCI_PAGE_TIMEOUT_ERROR
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
async def task():
|
||||||
|
if responder_role != BT_PERIPHERAL_ROLE:
|
||||||
|
initiator_controller.on_classic_role_change(
|
||||||
|
responder_controller.public_address, int(not (responder_role))
|
||||||
|
)
|
||||||
|
initiator_controller.on_classic_connection_complete(
|
||||||
|
responder_controller.public_address, HCI_SUCCESS
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(task())
|
||||||
|
responder_controller.on_classic_role_change(
|
||||||
|
initiator_controller.public_address, responder_role
|
||||||
|
)
|
||||||
|
responder_controller.on_classic_connection_complete(
|
||||||
|
initiator_controller.public_address, HCI_SUCCESS
|
||||||
|
)
|
||||||
|
self.pending_classic_connection = None
|
||||||
|
|
||||||
|
def classic_disconnect(self, initiator_controller, responder_address, reason):
|
||||||
|
logger.debug(
|
||||||
|
f'[Classic] {initiator_controller.public_address} disconnects {responder_address}'
|
||||||
|
)
|
||||||
|
responder_controller = self.find_classic_controller(responder_address)
|
||||||
|
|
||||||
|
async def task():
|
||||||
|
initiator_controller.on_classic_disconnected(responder_address, reason)
|
||||||
|
|
||||||
|
asyncio.create_task(task())
|
||||||
|
responder_controller.on_classic_disconnected(
|
||||||
|
initiator_controller.public_address, reason
|
||||||
|
)
|
||||||
|
|
||||||
|
def classic_switch_role(
|
||||||
|
self, initiator_controller, responder_address, initiator_new_role
|
||||||
|
):
|
||||||
|
responder_controller = self.find_classic_controller(responder_address)
|
||||||
|
if responder_controller is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
async def task():
|
||||||
|
initiator_controller.on_classic_role_change(
|
||||||
|
responder_address, initiator_new_role
|
||||||
|
)
|
||||||
|
|
||||||
|
asyncio.create_task(task())
|
||||||
|
responder_controller.on_classic_role_change(
|
||||||
|
initiator_controller.public_address, int(not (initiator_new_role))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class RemoteLink:
|
class RemoteLink:
|
||||||
@@ -200,6 +308,9 @@ class RemoteLink:
|
|||||||
def get_pending_connection(self):
|
def get_pending_connection(self):
|
||||||
return self.pending_connection
|
return self.pending_connection
|
||||||
|
|
||||||
|
def get_pending_classic_connection(self):
|
||||||
|
return self.pending_classic_connection
|
||||||
|
|
||||||
async def wait_until_connected(self):
|
async def wait_until_connected(self):
|
||||||
await self.websocket
|
await self.websocket
|
||||||
|
|
||||||
@@ -366,7 +477,8 @@ class RemoteLink:
|
|||||||
async def send_acl_data_to_relay(self, peer_address, data):
|
async def send_acl_data_to_relay(self, peer_address, data):
|
||||||
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
await self.send_targeted_message(peer_address, f'acl:{data.hex()}')
|
||||||
|
|
||||||
def send_acl_data(self, _, peer_address, data):
|
def send_acl_data(self, _, peer_address, _transport, data):
|
||||||
|
# TODO: handle different transport
|
||||||
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
self.execute(partial(self.send_acl_data_to_relay, peer_address, data))
|
||||||
|
|
||||||
async def send_connection_request_to_relay(self, peer_address):
|
async def send_connection_request_to_relay(self, peer_address):
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# Copyright 2021-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 enum
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
|
||||||
|
from .hci import (
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
|
)
|
||||||
|
from .smp import (
|
||||||
|
SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
|
SMP_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
|
SMP_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
|
SMP_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
SMP_KEYBOARD_DISPLAY_IO_CAPABILITY,
|
||||||
|
SMP_ENC_KEY_DISTRIBUTION_FLAG,
|
||||||
|
SMP_ID_KEY_DISTRIBUTION_FLAG,
|
||||||
|
SMP_SIGN_KEY_DISTRIBUTION_FLAG,
|
||||||
|
SMP_LINK_KEY_DISTRIBUTION_FLAG,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class PairingDelegate:
|
||||||
|
"""Abstract base class for Pairing Delegates."""
|
||||||
|
|
||||||
|
# I/O Capabilities.
|
||||||
|
# These are defined abstractly, and can be mapped to specific Classic pairing
|
||||||
|
# and/or SMP constants.
|
||||||
|
class IoCapability(enum.IntEnum):
|
||||||
|
NO_OUTPUT_NO_INPUT = SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||||
|
KEYBOARD_INPUT_ONLY = SMP_KEYBOARD_ONLY_IO_CAPABILITY
|
||||||
|
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY
|
||||||
|
DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
||||||
|
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
||||||
|
|
||||||
|
# Direct names for backward compatibility.
|
||||||
|
NO_OUTPUT_NO_INPUT = IoCapability.NO_OUTPUT_NO_INPUT
|
||||||
|
KEYBOARD_INPUT_ONLY = IoCapability.KEYBOARD_INPUT_ONLY
|
||||||
|
DISPLAY_OUTPUT_ONLY = IoCapability.DISPLAY_OUTPUT_ONLY
|
||||||
|
DISPLAY_OUTPUT_AND_YES_NO_INPUT = IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT
|
||||||
|
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
||||||
|
|
||||||
|
# Key Distribution [LE only]
|
||||||
|
class KeyDistribution(enum.IntFlag):
|
||||||
|
DISTRIBUTE_ENCRYPTION_KEY = SMP_ENC_KEY_DISTRIBUTION_FLAG
|
||||||
|
DISTRIBUTE_IDENTITY_KEY = SMP_ID_KEY_DISTRIBUTION_FLAG
|
||||||
|
DISTRIBUTE_SIGNING_KEY = SMP_SIGN_KEY_DISTRIBUTION_FLAG
|
||||||
|
DISTRIBUTE_LINK_KEY = SMP_LINK_KEY_DISTRIBUTION_FLAG
|
||||||
|
|
||||||
|
DEFAULT_KEY_DISTRIBUTION: int = (
|
||||||
|
SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG
|
||||||
|
)
|
||||||
|
|
||||||
|
# Default mapping from abstract to Classic I/O capabilities.
|
||||||
|
# Subclasses may override this if they prefer a different mapping.
|
||||||
|
CLASSIC_IO_CAPABILITIES_MAP = {
|
||||||
|
NO_OUTPUT_NO_INPUT: HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
|
KEYBOARD_INPUT_ONLY: HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
|
DISPLAY_OUTPUT_ONLY: HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
|
DISPLAY_OUTPUT_AND_YES_NO_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT: HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
}
|
||||||
|
|
||||||
|
io_capability: IoCapability
|
||||||
|
local_initiator_key_distribution: KeyDistribution
|
||||||
|
local_responder_key_distribution: KeyDistribution
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
io_capability=NO_OUTPUT_NO_INPUT,
|
||||||
|
local_initiator_key_distribution=DEFAULT_KEY_DISTRIBUTION,
|
||||||
|
local_responder_key_distribution=DEFAULT_KEY_DISTRIBUTION,
|
||||||
|
) -> None:
|
||||||
|
self.io_capability = io_capability
|
||||||
|
self.local_initiator_key_distribution = local_initiator_key_distribution
|
||||||
|
self.local_responder_key_distribution = local_responder_key_distribution
|
||||||
|
|
||||||
|
@property
|
||||||
|
def classic_io_capability(self) -> int:
|
||||||
|
"""Map the abstract I/O capability to a Classic constant."""
|
||||||
|
|
||||||
|
# pylint: disable=line-too-long
|
||||||
|
return self.CLASSIC_IO_CAPABILITIES_MAP.get(
|
||||||
|
self.io_capability, HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def smp_io_capability(self) -> int:
|
||||||
|
"""Map the abstract I/O capability to an SMP constant."""
|
||||||
|
|
||||||
|
# This is just a 1-1 direct mapping
|
||||||
|
return self.io_capability
|
||||||
|
|
||||||
|
async def accept(self) -> bool:
|
||||||
|
"""Accept or reject a Pairing request."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def confirm(self) -> bool:
|
||||||
|
"""Respond yes or no to a Pairing confirmation question."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
# pylint: disable-next=unused-argument
|
||||||
|
async def compare_numbers(self, number: int, digits: int) -> bool:
|
||||||
|
"""Compare two numbers."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def get_number(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Return an optional number as an answer to a passkey request.
|
||||||
|
Returning `None` will result in a negative reply.
|
||||||
|
"""
|
||||||
|
return 0
|
||||||
|
|
||||||
|
async def get_string(self, max_length) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Return a string whose utf-8 encoding is up to max_length bytes.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
# pylint: disable-next=unused-argument
|
||||||
|
async def display_number(self, number: int, digits: int) -> None:
|
||||||
|
"""Display a number."""
|
||||||
|
|
||||||
|
# [LE only]
|
||||||
|
async def key_distribution_response(
|
||||||
|
self, peer_initiator_key_distribution: int, peer_responder_key_distribution: int
|
||||||
|
) -> Tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Return the key distribution response in an SMP protocol context.
|
||||||
|
|
||||||
|
NOTE: since it is only used by the SMP protocol, this method's input and output
|
||||||
|
are directly as integers, using the SMP constants, rather than the abstract
|
||||||
|
KeyDistribution enums.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
int(
|
||||||
|
peer_initiator_key_distribution & self.local_initiator_key_distribution
|
||||||
|
),
|
||||||
|
int(
|
||||||
|
peer_responder_key_distribution & self.local_responder_key_distribution
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
class PairingConfig:
|
||||||
|
"""Configuration for the Pairing protocol."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
sc: bool = True,
|
||||||
|
mitm: bool = True,
|
||||||
|
bonding: bool = True,
|
||||||
|
delegate: Optional[PairingDelegate] = None,
|
||||||
|
) -> None:
|
||||||
|
self.sc = sc
|
||||||
|
self.mitm = mitm
|
||||||
|
self.bonding = bonding
|
||||||
|
self.delegate = delegate or PairingDelegate()
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return (
|
||||||
|
f'PairingConfig(sc={self.sc}, '
|
||||||
|
f'mitm={self.mitm}, bonding={self.bonding}, '
|
||||||
|
f'delegate[{self.delegate.io_capability}])'
|
||||||
|
)
|
||||||
@@ -20,6 +20,7 @@ import struct
|
|||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List
|
||||||
from ..core import AdvertisingData
|
from ..core import AdvertisingData
|
||||||
|
from ..device import Device, Connection
|
||||||
from ..gatt import (
|
from ..gatt import (
|
||||||
GATT_ASHA_SERVICE,
|
GATT_ASHA_SERVICE,
|
||||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||||
@@ -31,7 +32,7 @@ from ..gatt import (
|
|||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
)
|
)
|
||||||
from ..device import Device
|
from ..utils import AsyncRunner
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -55,16 +56,16 @@ class AshaService(TemplateService):
|
|||||||
self.hisyncid = hisyncid
|
self.hisyncid = hisyncid
|
||||||
self.capability = capability # Device Capabilities [Left, Monaural]
|
self.capability = capability # Device Capabilities [Left, Monaural]
|
||||||
self.device = device
|
self.device = device
|
||||||
self.emitted_data_name = 'ASHA_data_' + str(self.capability)
|
|
||||||
self.audio_out_data = b''
|
self.audio_out_data = b''
|
||||||
self.psm = psm # a non-zero psm is mainly for testing purpose
|
self.psm = psm # a non-zero psm is mainly for testing purpose
|
||||||
|
|
||||||
# Handler for volume control
|
# Handler for volume control
|
||||||
def on_volume_write(_connection, value):
|
def on_volume_write(connection, value):
|
||||||
logger.info(f'--- VOLUME Write:{value[0]}')
|
logger.info(f'--- VOLUME Write:{value[0]}')
|
||||||
|
self.emit('volume', connection, value[0])
|
||||||
|
|
||||||
# Handler for audio control commands
|
# Handler for audio control commands
|
||||||
def on_audio_control_point_write(_connection, value):
|
def on_audio_control_point_write(connection: Connection, value):
|
||||||
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
logger.info(f'--- AUDIO CONTROL POINT Write:{value.hex()}')
|
||||||
opcode = value[0]
|
opcode = value[0]
|
||||||
if opcode == AshaService.OPCODE_START:
|
if opcode == AshaService.OPCODE_START:
|
||||||
@@ -76,18 +77,33 @@ class AshaService(TemplateService):
|
|||||||
f'volume={value[3]}, '
|
f'volume={value[3]}, '
|
||||||
f'otherstate={value[4]}'
|
f'otherstate={value[4]}'
|
||||||
)
|
)
|
||||||
|
self.emit(
|
||||||
|
'start',
|
||||||
|
connection,
|
||||||
|
{
|
||||||
|
'codec': value[1],
|
||||||
|
'audiotype': value[2],
|
||||||
|
'volume': value[3],
|
||||||
|
'otherstate': value[4],
|
||||||
|
},
|
||||||
|
)
|
||||||
elif opcode == AshaService.OPCODE_STOP:
|
elif opcode == AshaService.OPCODE_STOP:
|
||||||
logger.info('### STOP')
|
logger.info('### STOP')
|
||||||
|
self.emit('stop', connection)
|
||||||
elif opcode == AshaService.OPCODE_STATUS:
|
elif opcode == AshaService.OPCODE_STATUS:
|
||||||
logger.info(f'### STATUS: connected={value[1]}')
|
logger.info(f'### STATUS: connected={value[1]}')
|
||||||
|
|
||||||
# TODO Respond with a status
|
# OPCODE_STATUS does not need audio status point update
|
||||||
# asyncio.create_task(device.notify_subscribers(audio_status_characteristic,
|
if opcode != AshaService.OPCODE_STATUS:
|
||||||
# force=True))
|
AsyncRunner.spawn(
|
||||||
|
device.notify_subscribers(
|
||||||
|
self.audio_status_characteristic, force=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.read_only_properties_characteristic = Characteristic(
|
self.read_only_properties_characteristic = Characteristic(
|
||||||
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
GATT_ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes(
|
bytes(
|
||||||
[
|
[
|
||||||
@@ -104,19 +120,20 @@ class AshaService(TemplateService):
|
|||||||
|
|
||||||
self.audio_control_point_characteristic = Characteristic(
|
self.audio_control_point_characteristic = Characteristic(
|
||||||
GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
GATT_ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
||||||
Characteristic.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE,
|
Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||||
Characteristic.WRITEABLE,
|
Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(write=on_audio_control_point_write),
|
CharacteristicValue(write=on_audio_control_point_write),
|
||||||
)
|
)
|
||||||
self.audio_status_characteristic = Characteristic(
|
self.audio_status_characteristic = Characteristic(
|
||||||
GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
GATT_ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([0]),
|
bytes([0]),
|
||||||
)
|
)
|
||||||
self.volume_characteristic = Characteristic(
|
self.volume_characteristic = Characteristic(
|
||||||
GATT_ASHA_VOLUME_CHARACTERISTIC,
|
GATT_ASHA_VOLUME_CHARACTERISTIC,
|
||||||
Characteristic.WRITE_WITHOUT_RESPONSE,
|
Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||||
Characteristic.WRITEABLE,
|
Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(write=on_volume_write),
|
CharacteristicValue(write=on_volume_write),
|
||||||
)
|
)
|
||||||
@@ -126,7 +143,7 @@ class AshaService(TemplateService):
|
|||||||
def on_data(data):
|
def on_data(data):
|
||||||
logging.debug(f'<<< data received:{data}')
|
logging.debug(f'<<< data received:{data}')
|
||||||
|
|
||||||
self.emit(self.emitted_data_name, data)
|
self.emit('data', channel.connection, data)
|
||||||
self.audio_out_data += data
|
self.audio_out_data += data
|
||||||
|
|
||||||
channel.sink = on_data
|
channel.sink = on_data
|
||||||
@@ -135,7 +152,7 @@ class AshaService(TemplateService):
|
|||||||
self.psm = self.device.register_l2cap_channel_server(self.psm, on_coc, 8)
|
self.psm = self.device.register_l2cap_channel_server(self.psm, on_coc, 8)
|
||||||
self.le_psm_out_characteristic = Characteristic(
|
self.le_psm_out_characteristic = Characteristic(
|
||||||
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC,
|
GATT_ASHA_LE_PSM_OUT_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
struct.pack('<H', self.psm),
|
struct.pack('<H', self.psm),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class BatteryService(TemplateService):
|
|||||||
self.battery_level_characteristic = PackedCharacteristicAdapter(
|
self.battery_level_characteristic = PackedCharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
CharacteristicValue(read=read_battery_level),
|
CharacteristicValue(read=read_battery_level),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ class DeviceInformationService(TemplateService):
|
|||||||
# TODO: pnp_id
|
# TODO: pnp_id
|
||||||
):
|
):
|
||||||
characteristics = [
|
characteristics = [
|
||||||
Characteristic(uuid, Characteristic.READ, Characteristic.READABLE, field)
|
Characteristic(
|
||||||
|
uuid, Characteristic.Properties.READ, Characteristic.READABLE, field
|
||||||
|
)
|
||||||
for (field, uuid) in (
|
for (field, uuid) in (
|
||||||
(manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
(manufacturer_name, GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC),
|
||||||
(model_number, GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
|
(model_number, GATT_MODEL_NUMBER_STRING_CHARACTERISTIC),
|
||||||
@@ -79,7 +81,7 @@ class DeviceInformationService(TemplateService):
|
|||||||
characteristics.append(
|
characteristics.append(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_SYSTEM_ID_CHARACTERISTIC,
|
GATT_SYSTEM_ID_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
self.pack_system_id(*system_id),
|
self.pack_system_id(*system_id),
|
||||||
)
|
)
|
||||||
@@ -89,7 +91,7 @@ class DeviceInformationService(TemplateService):
|
|||||||
characteristics.append(
|
characteristics.append(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
GATT_REGULATORY_CERTIFICATION_DATA_LIST_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
ieee_regulatory_certification_data_list,
|
ieee_regulatory_certification_data_list,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class HeartRateService(TemplateService):
|
|||||||
self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
|
self.heart_rate_measurement_characteristic = DelegatedCharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
GATT_HEART_RATE_MEASUREMENT_CHARACTERISTIC,
|
||||||
Characteristic.NOTIFY,
|
Characteristic.Properties.NOTIFY,
|
||||||
0,
|
0,
|
||||||
CharacteristicValue(read=read_heart_rate_measurement),
|
CharacteristicValue(read=read_heart_rate_measurement),
|
||||||
),
|
),
|
||||||
@@ -164,7 +164,7 @@ class HeartRateService(TemplateService):
|
|||||||
if body_sensor_location is not None:
|
if body_sensor_location is not None:
|
||||||
self.body_sensor_location_characteristic = Characteristic(
|
self.body_sensor_location_characteristic = Characteristic(
|
||||||
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
GATT_BODY_SENSOR_LOCATION_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([int(body_sensor_location)]),
|
bytes([int(body_sensor_location)]),
|
||||||
)
|
)
|
||||||
@@ -182,7 +182,7 @@ class HeartRateService(TemplateService):
|
|||||||
self.heart_rate_control_point_characteristic = PackedCharacteristicAdapter(
|
self.heart_rate_control_point_characteristic = PackedCharacteristicAdapter(
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
GATT_HEART_RATE_CONTROL_POINT_CHARACTERISTIC,
|
||||||
Characteristic.WRITE,
|
Characteristic.Properties.WRITE,
|
||||||
Characteristic.WRITEABLE,
|
Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(write=write_heart_rate_control_point_value),
|
CharacteristicValue(write=write_heart_rate_control_point_value),
|
||||||
),
|
),
|
||||||
|
|||||||
+21
-11
@@ -439,7 +439,7 @@ class DLC(EventEmitter):
|
|||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f'<<< Credits [{self.dlci}]: '
|
f'<<< Credits [{self.dlci}]: '
|
||||||
f'received {credits}, total={self.tx_credits}'
|
f'received {received_credits}, total={self.tx_credits}'
|
||||||
)
|
)
|
||||||
data = data[1:]
|
data = data[1:]
|
||||||
|
|
||||||
@@ -852,17 +852,27 @@ class Server(EventEmitter):
|
|||||||
# Register ourselves with the L2CAP channel manager
|
# Register ourselves with the L2CAP channel manager
|
||||||
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
device.register_l2cap_server(RFCOMM_PSM, self.on_connection)
|
||||||
|
|
||||||
def listen(self, acceptor):
|
def listen(self, acceptor, channel=0):
|
||||||
# Find a free channel number
|
if channel:
|
||||||
for channel in range(
|
if channel in self.acceptors:
|
||||||
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START, RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1
|
# Busy
|
||||||
):
|
return 0
|
||||||
if channel not in self.acceptors:
|
else:
|
||||||
self.acceptors[channel] = acceptor
|
# Find a free channel number
|
||||||
return channel
|
for candidate in range(
|
||||||
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_START,
|
||||||
|
RFCOMM_DYNAMIC_CHANNEL_NUMBER_END + 1,
|
||||||
|
):
|
||||||
|
if candidate not in self.acceptors:
|
||||||
|
channel = candidate
|
||||||
|
break
|
||||||
|
|
||||||
# All channels used...
|
if channel == 0:
|
||||||
return 0
|
# All channels used...
|
||||||
|
return 0
|
||||||
|
|
||||||
|
self.acceptors[channel] = acceptor
|
||||||
|
return channel
|
||||||
|
|
||||||
def on_connection(self, l2cap_channel):
|
def on_connection(self, l2cap_channel):
|
||||||
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
logger.debug(f'+++ new L2CAP connection: {l2cap_channel}')
|
||||||
|
|||||||
+21
-78
@@ -31,7 +31,16 @@ from typing import Dict, Optional, Type
|
|||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
from .colors import color
|
from .colors import color
|
||||||
from .hci import Address, HCI_LE_Enable_Encryption_Command, HCI_Object, key_with_value
|
from .hci import (
|
||||||
|
HCI_DISPLAY_ONLY_IO_CAPABILITY,
|
||||||
|
HCI_DISPLAY_YES_NO_IO_CAPABILITY,
|
||||||
|
HCI_KEYBOARD_ONLY_IO_CAPABILITY,
|
||||||
|
HCI_NO_INPUT_NO_OUTPUT_IO_CAPABILITY,
|
||||||
|
Address,
|
||||||
|
HCI_LE_Enable_Encryption_Command,
|
||||||
|
HCI_Object,
|
||||||
|
key_with_value,
|
||||||
|
)
|
||||||
from .core import (
|
from .core import (
|
||||||
BT_BR_EDR_TRANSPORT,
|
BT_BR_EDR_TRANSPORT,
|
||||||
BT_CENTRAL_ROLE,
|
BT_CENTRAL_ROLE,
|
||||||
@@ -476,7 +485,7 @@ class AddressResolver:
|
|||||||
address_bytes = bytes(address)
|
address_bytes = bytes(address)
|
||||||
hash_part = address_bytes[0:3]
|
hash_part = address_bytes[0:3]
|
||||||
prand = address_bytes[3:6]
|
prand = address_bytes[3:6]
|
||||||
for (irk, resolved_address) in self.resolving_keys:
|
for irk, resolved_address in self.resolving_keys:
|
||||||
local_hash = crypto.ah(irk, prand)
|
local_hash = crypto.ah(irk, prand)
|
||||||
if local_hash == hash_part:
|
if local_hash == hash_part:
|
||||||
# Match!
|
# Match!
|
||||||
@@ -491,76 +500,6 @@ class AddressResolver:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
class PairingDelegate:
|
|
||||||
NO_OUTPUT_NO_INPUT = SMP_NO_INPUT_NO_OUTPUT_IO_CAPABILITY
|
|
||||||
KEYBOARD_INPUT_ONLY = SMP_KEYBOARD_ONLY_IO_CAPABILITY
|
|
||||||
DISPLAY_OUTPUT_ONLY = SMP_DISPLAY_ONLY_IO_CAPABILITY
|
|
||||||
DISPLAY_OUTPUT_AND_YES_NO_INPUT = SMP_DISPLAY_YES_NO_IO_CAPABILITY
|
|
||||||
DISPLAY_OUTPUT_AND_KEYBOARD_INPUT = SMP_KEYBOARD_DISPLAY_IO_CAPABILITY
|
|
||||||
DEFAULT_KEY_DISTRIBUTION: int = (
|
|
||||||
SMP_ENC_KEY_DISTRIBUTION_FLAG | SMP_ID_KEY_DISTRIBUTION_FLAG
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
io_capability: int = NO_OUTPUT_NO_INPUT,
|
|
||||||
local_initiator_key_distribution: int = DEFAULT_KEY_DISTRIBUTION,
|
|
||||||
local_responder_key_distribution: int = DEFAULT_KEY_DISTRIBUTION,
|
|
||||||
) -> None:
|
|
||||||
self.io_capability = io_capability
|
|
||||||
self.local_initiator_key_distribution = local_initiator_key_distribution
|
|
||||||
self.local_responder_key_distribution = local_responder_key_distribution
|
|
||||||
|
|
||||||
async def accept(self) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def confirm(self) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# pylint: disable-next=unused-argument
|
|
||||||
async def compare_numbers(self, number: int, digits: int) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def get_number(self) -> int:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# pylint: disable-next=unused-argument
|
|
||||||
async def display_number(self, number: int, digits: int) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
async def key_distribution_response(
|
|
||||||
self, peer_initiator_key_distribution, peer_responder_key_distribution
|
|
||||||
):
|
|
||||||
return (
|
|
||||||
(peer_initiator_key_distribution & self.local_initiator_key_distribution),
|
|
||||||
(peer_responder_key_distribution & self.local_responder_key_distribution),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
class PairingConfig:
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
sc: bool = True,
|
|
||||||
mitm: bool = True,
|
|
||||||
bonding: bool = True,
|
|
||||||
delegate: Optional[PairingDelegate] = None,
|
|
||||||
) -> None:
|
|
||||||
self.sc = sc
|
|
||||||
self.mitm = mitm
|
|
||||||
self.bonding = bonding
|
|
||||||
self.delegate = delegate or PairingDelegate()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
io_capability_str = SMP_Command.io_capability_name(self.delegate.io_capability)
|
|
||||||
return (
|
|
||||||
f'PairingConfig(sc={self.sc}, '
|
|
||||||
f'mitm={self.mitm}, bonding={self.bonding}, '
|
|
||||||
f'delegate[{io_capability_str}])'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class Session:
|
class Session:
|
||||||
# Pairing methods
|
# Pairing methods
|
||||||
@@ -635,7 +574,7 @@ class Session:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, manager, connection, pairing_config):
|
def __init__(self, manager, connection, pairing_config, is_initiator):
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
self.preq = None
|
self.preq = None
|
||||||
@@ -674,7 +613,7 @@ class Session:
|
|||||||
self.ctkd_task = None
|
self.ctkd_task = None
|
||||||
|
|
||||||
# Decide if we're the initiator or the responder
|
# Decide if we're the initiator or the responder
|
||||||
self.is_initiator = connection.role == BT_CENTRAL_ROLE
|
self.is_initiator = is_initiator
|
||||||
self.is_responder = not self.is_initiator
|
self.is_responder = not self.is_initiator
|
||||||
|
|
||||||
# Listen for connection events
|
# Listen for connection events
|
||||||
@@ -1652,12 +1591,12 @@ class Manager(EventEmitter):
|
|||||||
Implements the Initiator and Responder roles of the Security Manager Protocol
|
Implements the Initiator and Responder roles of the Security Manager Protocol
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, device):
|
def __init__(self, device, pairing_config_factory):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.device = device
|
self.device = device
|
||||||
self.sessions = {}
|
self.sessions = {}
|
||||||
self._ecc_key = None
|
self._ecc_key = None
|
||||||
self.pairing_config_factory = lambda connection: PairingConfig()
|
self.pairing_config_factory = pairing_config_factory
|
||||||
|
|
||||||
def send_command(self, connection, command):
|
def send_command(self, connection, command):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@@ -1670,6 +1609,8 @@ class Manager(EventEmitter):
|
|||||||
def on_smp_pdu(self, connection, pdu):
|
def on_smp_pdu(self, connection, pdu):
|
||||||
# Look for a session with this connection, and create one if none exists
|
# Look for a session with this connection, and create one if none exists
|
||||||
if not (session := self.sessions.get(connection.handle)):
|
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)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
if pairing_config is None:
|
if pairing_config is None:
|
||||||
# Pairing disabled
|
# Pairing disabled
|
||||||
@@ -1678,7 +1619,7 @@ class Manager(EventEmitter):
|
|||||||
SMP_Pairing_Failed_Command(reason=SMP_PAIRING_NOT_SUPPORTED_ERROR),
|
SMP_Pairing_Failed_Command(reason=SMP_PAIRING_NOT_SUPPORTED_ERROR),
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
session = Session(self, connection, pairing_config)
|
session = Session(self, connection, pairing_config, is_initiator=False)
|
||||||
self.sessions[connection.handle] = session
|
self.sessions[connection.handle] = session
|
||||||
|
|
||||||
# Parse the L2CAP payload into an SMP Command object
|
# Parse the L2CAP payload into an SMP Command object
|
||||||
@@ -1699,10 +1640,12 @@ class Manager(EventEmitter):
|
|||||||
|
|
||||||
async def pair(self, connection):
|
async def pair(self, connection):
|
||||||
# TODO: check if there's already a session for this 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)
|
pairing_config = self.pairing_config_factory(connection)
|
||||||
if pairing_config is None:
|
if pairing_config is None:
|
||||||
raise ValueError('pairing config must not be None when initiating')
|
raise ValueError('pairing config must not be None when initiating')
|
||||||
session = Session(self, connection, pairing_config)
|
session = Session(self, connection, pairing_config, is_initiator=True)
|
||||||
self.sessions[connection.handle] = session
|
self.sessions[connection.handle] = session
|
||||||
return await session.pair()
|
return await session.pair()
|
||||||
|
|
||||||
|
|||||||
+17
-1
@@ -20,7 +20,7 @@ import logging
|
|||||||
import traceback
|
import traceback
|
||||||
import collections
|
import collections
|
||||||
import sys
|
import sys
|
||||||
from typing import Awaitable, TypeVar
|
from typing import Awaitable, Set, TypeVar
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pyee import EventEmitter
|
from pyee import EventEmitter
|
||||||
|
|
||||||
@@ -157,6 +157,9 @@ class AsyncRunner:
|
|||||||
# Shared default queue
|
# Shared default queue
|
||||||
default_queue = WorkQueue()
|
default_queue = WorkQueue()
|
||||||
|
|
||||||
|
# Shared set of running tasks
|
||||||
|
running_tasks: Set[Awaitable] = set()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def run_in_task(queue=None):
|
def run_in_task(queue=None):
|
||||||
"""
|
"""
|
||||||
@@ -187,6 +190,19 @@ class AsyncRunner:
|
|||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def spawn(coroutine):
|
||||||
|
"""
|
||||||
|
Spawn a task to run a coroutine in a "fire and forget" mode.
|
||||||
|
|
||||||
|
Using this method instead of just calling `asyncio.create_task(coroutine)`
|
||||||
|
is necessary when you don't keep a reference to the task, because `asyncio`
|
||||||
|
only keeps weak references to alive tasks.
|
||||||
|
"""
|
||||||
|
task = asyncio.create_task(coroutine)
|
||||||
|
AsyncRunner.running_tasks.add(task)
|
||||||
|
task.add_done_callback(AsyncRunner.running_tasks.remove)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
class FlowControlAsyncPipe:
|
class FlowControlAsyncPipe:
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ nav:
|
|||||||
- Apps & Tools:
|
- Apps & Tools:
|
||||||
- Overview: apps_and_tools/index.md
|
- Overview: apps_and_tools/index.md
|
||||||
- Console: apps_and_tools/console.md
|
- Console: apps_and_tools/console.md
|
||||||
- Link Relay: apps_and_tools/link_relay.md
|
- Bench: apps_and_tools/bench.md
|
||||||
- HCI Bridge: apps_and_tools/hci_bridge.md
|
- HCI Bridge: apps_and_tools/hci_bridge.md
|
||||||
- Golden Gate Bridge: apps_and_tools/gg_bridge.md
|
- Golden Gate Bridge: apps_and_tools/gg_bridge.md
|
||||||
- Show: apps_and_tools/show.md
|
- Show: apps_and_tools/show.md
|
||||||
@@ -51,6 +51,7 @@ nav:
|
|||||||
- Pair: apps_and_tools/pair.md
|
- Pair: apps_and_tools/pair.md
|
||||||
- Unbond: apps_and_tools/unbond.md
|
- Unbond: apps_and_tools/unbond.md
|
||||||
- USB Probe: apps_and_tools/usb_probe.md
|
- USB Probe: apps_and_tools/usb_probe.md
|
||||||
|
- Link Relay: apps_and_tools/link_relay.md
|
||||||
- Hardware:
|
- Hardware:
|
||||||
- Overview: hardware/index.md
|
- Overview: hardware/index.md
|
||||||
- Platforms:
|
- Platforms:
|
||||||
@@ -62,7 +63,7 @@ nav:
|
|||||||
- Examples:
|
- Examples:
|
||||||
- Overview: examples/index.md
|
- Overview: examples/index.md
|
||||||
|
|
||||||
copyright: Copyright 2021-2022 Google LLC
|
copyright: Copyright 2021-2023 Google LLC
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: 'material'
|
name: 'material'
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
BENCH TOOL
|
||||||
|
==========
|
||||||
|
|
||||||
|
The "bench" tool implements a number of different ways of measuring the
|
||||||
|
throughput and/or latency between two devices.
|
||||||
|
|
||||||
|
# General Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage: bench.py [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--device-config FILENAME Device configuration file
|
||||||
|
--role [sender|receiver|ping|pong]
|
||||||
|
--mode [gatt-client|gatt-server|l2cap-client|l2cap-server|rfcomm-client|rfcomm-server]
|
||||||
|
--att-mtu MTU GATT MTU (gatt-client mode) [23<=x<=517]
|
||||||
|
-s, --packet-size SIZE Packet size (server role) [8<=x<=4096]
|
||||||
|
-c, --packet-count COUNT Packet count (server role)
|
||||||
|
-sd, --start-delay SECONDS Start delay (server role)
|
||||||
|
--help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
central Run as a central (initiates the connection)
|
||||||
|
peripheral Run as a peripheral (waits for a connection)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options for the ``central`` Command
|
||||||
|
```
|
||||||
|
Usage: bumble-bench central [OPTIONS] TRANSPORT
|
||||||
|
|
||||||
|
Run as a central (initiates the connection)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--peripheral ADDRESS_OR_NAME Address or name to connect to
|
||||||
|
--connection-interval, --ci CONNECTION_INTERVAL
|
||||||
|
Connection interval (in ms)
|
||||||
|
--phy [1m|2m|coded] PHY to use
|
||||||
|
--help Show this message and exit.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
To test once device against another, one of the two devices must be running
|
||||||
|
the ``peripheral`` command and the other the ``central`` command. The device
|
||||||
|
running the ``peripheral`` command will accept connections from the device
|
||||||
|
running the ``central`` command.
|
||||||
|
When using Bluetooth LE (all modes except for ``rfcomm-server`` and ``rfcomm-client``utils),
|
||||||
|
the default addresses configured in the tool should be sufficient. But when using
|
||||||
|
Bluetooth Classic, the address of the Peripheral must be specified on the Central
|
||||||
|
using the ``--peripheral`` option. The address will be printed by the Peripheral when
|
||||||
|
it starts.
|
||||||
|
|
||||||
|
Independently of whether the device is the Central or Peripheral, each device selects a
|
||||||
|
``mode`` and and ``role`` to run as. The ``mode`` and ``role`` of the Central and Peripheral
|
||||||
|
must be compatible.
|
||||||
|
|
||||||
|
Device 1 mode | Device 2 mode
|
||||||
|
------------------|------------------
|
||||||
|
``gatt-client`` | ``gatt-server``
|
||||||
|
``l2cap-client`` | ``l2cap-server``
|
||||||
|
``rfcomm-client`` | ``rfcomm-server``
|
||||||
|
|
||||||
|
Device 1 role | Device 2 role
|
||||||
|
--------------|--------------
|
||||||
|
``sender`` | ``receiver``
|
||||||
|
``ping`` | ``pong``
|
||||||
|
|
||||||
|
|
||||||
|
# Examples
|
||||||
|
|
||||||
|
In the following examples, we have two USB Bluetooth controllers, one on `usb:0` and
|
||||||
|
the other on `usb:1`, and two consoles/terminals. We will run a command in each.
|
||||||
|
|
||||||
|
!!! example "GATT Throughput"
|
||||||
|
Using the default mode and role for the Central and Peripheral.
|
||||||
|
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
In this default configuration, the Central runs a Sender, as a GATT client,
|
||||||
|
connecting to the Peripheral running a Receiver, as a GATT server.
|
||||||
|
|
||||||
|
!!! example "L2CAP Throughput"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "RFComm Throughput"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode rfcomm-server peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: the BT address of the Peripheral will be printed out, use it with the
|
||||||
|
``--peripheral`` option for the Central.
|
||||||
|
|
||||||
|
In this example, we use a larger packet size and packet count than the default.
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode rfcomm-client --packet-size 2000 --packet-count 100 central --peripheral 00:16:A4:5A:40:F2 usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Ping/Pong Latency"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --role pong peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --role ping central usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed modes with GATT and custom connection interval"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode gatt-client peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode gatt-server central --ci 10 usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed modes with L2CAP and custom PHY"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server central --phy 2m usb:1
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! example "Reversed roles with L2CAP"
|
||||||
|
In the first console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-client --role sender peripheral usb:0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the second console/terminal:
|
||||||
|
```
|
||||||
|
$ bumble-bench --mode l2cap-server --role receiver central usb:1
|
||||||
|
```
|
||||||
@@ -5,6 +5,7 @@ Included in the project are a few apps and tools, built on top of the core libra
|
|||||||
These include:
|
These include:
|
||||||
|
|
||||||
* [Console](console.md) - an interactive text-based console
|
* [Console](console.md) - an interactive text-based console
|
||||||
|
* [Bench](bench.md) - Speed and Latency benchmarking between two devices (LE and Classic)
|
||||||
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
* [Pair](pair.md) - Pair/bond two devices (LE and Classic)
|
||||||
* [Unbond](unbond.md) - Remove a previously established bond
|
* [Unbond](unbond.md) - Remove a previously established bond
|
||||||
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
* [HCI Bridge](hci_bridge.md) - a HCI transport bridge to connect two HCI transports and filter/snoop the HCI packets
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ The project initially only supported BLE (Bluetooth Low Energy), but support for
|
|||||||
eventually added. Support for BLE is therefore currently somewhat more advanced than for Classic.
|
eventually added. Support for BLE is therefore currently somewhat more advanced than for Classic.
|
||||||
|
|
||||||
!!! warning
|
!!! warning
|
||||||
This project is still very much experimental and in an alpha state where a lot of things are still missing or broken, and what's there changes frequently.
|
This project is still in an early state of development where some things are still missing or broken, and what's implemented may change and evolve frequently.
|
||||||
Also, there are still a few hardcoded values/parameters in some of the examples and apps which need to be changed (those will eventually be command line arguments, as appropriate)
|
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Bumble Hands-Free",
|
"name": "Bumble Hands-Free",
|
||||||
"class_of_device": 2360324
|
"class_of_device": 2360324,
|
||||||
|
"keystore": "JsonKeyStore",
|
||||||
|
"le_enabled": false
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-9
@@ -209,7 +209,7 @@ async def keyboard_host(device, peer_address):
|
|||||||
return
|
return
|
||||||
for i, characteristic in enumerate(report_characteristics):
|
for i, characteristic in enumerate(report_characteristics):
|
||||||
print(color('REPORT:', 'yellow'), characteristic)
|
print(color('REPORT:', 'yellow'), characteristic)
|
||||||
if characteristic.properties & Characteristic.NOTIFY:
|
if characteristic.properties & Characteristic.Properties.NOTIFY:
|
||||||
await peer.discover_descriptors(characteristic)
|
await peer.discover_descriptors(characteristic)
|
||||||
report_reference_descriptor = characteristic.get_descriptor(
|
report_reference_descriptor = characteristic.get_descriptor(
|
||||||
GATT_REPORT_REFERENCE_DESCRIPTOR
|
GATT_REPORT_REFERENCE_DESCRIPTOR
|
||||||
@@ -241,7 +241,9 @@ async def keyboard_device(device, command):
|
|||||||
# Create an 'input report' characteristic to send keyboard reports to the host
|
# Create an 'input report' characteristic to send keyboard reports to the host
|
||||||
input_report_characteristic = Characteristic(
|
input_report_characteristic = Characteristic(
|
||||||
GATT_REPORT_CHARACTERISTIC,
|
GATT_REPORT_CHARACTERISTIC,
|
||||||
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([0, 0, 0, 0, 0, 0, 0, 0]),
|
bytes([0, 0, 0, 0, 0, 0, 0, 0]),
|
||||||
[
|
[
|
||||||
@@ -256,8 +258,8 @@ async def keyboard_device(device, command):
|
|||||||
# Create an 'output report' characteristic to receive keyboard reports from the host
|
# Create an 'output report' characteristic to receive keyboard reports from the host
|
||||||
output_report_characteristic = Characteristic(
|
output_report_characteristic = Characteristic(
|
||||||
GATT_REPORT_CHARACTERISTIC,
|
GATT_REPORT_CHARACTERISTIC,
|
||||||
Characteristic.READ
|
Characteristic.Properties.READ
|
||||||
| Characteristic.WRITE
|
| Characteristic.Properties.WRITE
|
||||||
| Characteristic.WRITE_WITHOUT_RESPONSE,
|
| Characteristic.WRITE_WITHOUT_RESPONSE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([0]),
|
bytes([0]),
|
||||||
@@ -278,7 +280,7 @@ async def keyboard_device(device, command):
|
|||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
'Bumble',
|
'Bumble',
|
||||||
)
|
)
|
||||||
@@ -289,13 +291,13 @@ async def keyboard_device(device, command):
|
|||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_PROTOCOL_MODE_CHARACTERISTIC,
|
GATT_PROTOCOL_MODE_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([HID_REPORT_PROTOCOL]),
|
bytes([HID_REPORT_PROTOCOL]),
|
||||||
),
|
),
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_HID_INFORMATION_CHARACTERISTIC,
|
GATT_HID_INFORMATION_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
# bcdHID=1.1, bCountryCode=0x00,
|
# bcdHID=1.1, bCountryCode=0x00,
|
||||||
# Flags=RemoteWake|NormallyConnectable
|
# Flags=RemoteWake|NormallyConnectable
|
||||||
@@ -309,7 +311,7 @@ async def keyboard_device(device, command):
|
|||||||
),
|
),
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_REPORT_MAP_CHARACTERISTIC,
|
GATT_REPORT_MAP_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
HID_KEYBOARD_REPORT_MAP,
|
HID_KEYBOARD_REPORT_MAP,
|
||||||
),
|
),
|
||||||
@@ -322,7 +324,7 @@ async def keyboard_device(device, command):
|
|||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([100]),
|
bytes([100]),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ async def main():
|
|||||||
# Add the ASHA service to the GATT server
|
# Add the ASHA service to the GATT server
|
||||||
read_only_properties_characteristic = Characteristic(
|
read_only_properties_characteristic = Characteristic(
|
||||||
ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
ASHA_READ_ONLY_PROPERTIES_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes(
|
bytes(
|
||||||
[
|
[
|
||||||
@@ -127,13 +127,13 @@ async def main():
|
|||||||
)
|
)
|
||||||
audio_control_point_characteristic = Characteristic(
|
audio_control_point_characteristic = Characteristic(
|
||||||
ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
ASHA_AUDIO_CONTROL_POINT_CHARACTERISTIC,
|
||||||
Characteristic.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE,
|
Characteristic.Properties.WRITE | Characteristic.WRITE_WITHOUT_RESPONSE,
|
||||||
Characteristic.WRITEABLE,
|
Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(write=on_audio_control_point_write),
|
CharacteristicValue(write=on_audio_control_point_write),
|
||||||
)
|
)
|
||||||
audio_status_characteristic = Characteristic(
|
audio_status_characteristic = Characteristic(
|
||||||
ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
ASHA_AUDIO_STATUS_CHARACTERISTIC,
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([0]),
|
bytes([0]),
|
||||||
)
|
)
|
||||||
@@ -145,7 +145,7 @@ async def main():
|
|||||||
)
|
)
|
||||||
le_psm_out_characteristic = Characteristic(
|
le_psm_out_characteristic = Characteristic(
|
||||||
ASHA_LE_PSM_OUT_CHARACTERISTIC,
|
ASHA_LE_PSM_OUT_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
struct.pack('<H', psm),
|
struct.pack('<H', psm),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ async def main():
|
|||||||
)
|
)
|
||||||
manufacturer_name_characteristic = Characteristic(
|
manufacturer_name_characteristic = Characteristic(
|
||||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
"Fitbit",
|
"Fitbit",
|
||||||
[descriptor],
|
[descriptor],
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ async def main():
|
|||||||
)
|
)
|
||||||
manufacturer_name_characteristic = Characteristic(
|
manufacturer_name_characteristic = Characteristic(
|
||||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
"Fitbit",
|
"Fitbit",
|
||||||
[descriptor],
|
[descriptor],
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ async def main():
|
|||||||
)
|
)
|
||||||
manufacturer_name_characteristic = Characteristic(
|
manufacturer_name_characteristic = Characteristic(
|
||||||
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
GATT_MANUFACTURER_NAME_STRING_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
'Fitbit',
|
'Fitbit',
|
||||||
[descriptor],
|
[descriptor],
|
||||||
@@ -109,13 +109,13 @@ async def main():
|
|||||||
[
|
[
|
||||||
Characteristic(
|
Characteristic(
|
||||||
'D901B45B-4916-412E-ACCA-376ECB603B2C',
|
'D901B45B-4916-412E-ACCA-376ECB603B2C',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(read=my_custom_read, write=my_custom_write),
|
CharacteristicValue(read=my_custom_read, write=my_custom_write),
|
||||||
),
|
),
|
||||||
Characteristic(
|
Characteristic(
|
||||||
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
'552957FB-CF1F-4A31-9535-E78847E1A714',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(
|
CharacteristicValue(
|
||||||
read=my_custom_read_with_error, write=my_custom_write_with_error
|
read=my_custom_read_with_error, write=my_custom_write_with_error
|
||||||
@@ -123,7 +123,7 @@ async def main():
|
|||||||
),
|
),
|
||||||
Characteristic(
|
Characteristic(
|
||||||
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
|
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
'hello',
|
'hello',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -74,19 +74,21 @@ async def main():
|
|||||||
# Add a few entries to the device's GATT server
|
# Add a few entries to the device's GATT server
|
||||||
characteristic1 = Characteristic(
|
characteristic1 = Characteristic(
|
||||||
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
|
'486F64C6-4B5F-4B3B-8AFF-EDE134A8446A',
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([0x40]),
|
bytes([0x40]),
|
||||||
)
|
)
|
||||||
characteristic2 = Characteristic(
|
characteristic2 = Characteristic(
|
||||||
'8EBDEBAE-0017-418E-8D3B-3A3809492165',
|
'8EBDEBAE-0017-418E-8D3B-3A3809492165',
|
||||||
Characteristic.READ | Characteristic.INDICATE,
|
Characteristic.Properties.READ | Characteristic.Properties.INDICATE,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([0x41]),
|
bytes([0x41]),
|
||||||
)
|
)
|
||||||
characteristic3 = Characteristic(
|
characteristic3 = Characteristic(
|
||||||
'8EBDEBAE-0017-418E-8D3B-3A3809492165',
|
'8EBDEBAE-0017-418E-8D3B-3A3809492165',
|
||||||
Characteristic.READ | Characteristic.NOTIFY | Characteristic.INDICATE,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.NOTIFY
|
||||||
|
| Characteristic.Properties.INDICATE,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([0x42]),
|
bytes([0x42]),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ install_requires =
|
|||||||
pyserial >= 3.5; platform_system!='Emscripten'
|
pyserial >= 3.5; platform_system!='Emscripten'
|
||||||
pyusb >= 1.2; platform_system!='Emscripten'
|
pyusb >= 1.2; platform_system!='Emscripten'
|
||||||
websockets >= 8.1; platform_system!='Emscripten'
|
websockets >= 8.1; platform_system!='Emscripten'
|
||||||
|
prettytable >= 3.6.0
|
||||||
|
humanize >= 4.6.0
|
||||||
|
|
||||||
[options.entry_points]
|
[options.entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
@@ -57,6 +59,7 @@ console_scripts =
|
|||||||
bumble-unbond = bumble.apps.unbond:main
|
bumble-unbond = bumble.apps.unbond:main
|
||||||
bumble-usb-probe = bumble.apps.usb_probe:main
|
bumble-usb-probe = bumble.apps.usb_probe:main
|
||||||
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
bumble-link-relay = bumble.apps.link_relay.link_relay:main
|
||||||
|
bumble-bench = bumble.apps.bench:main
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
* = py.typed, *.pyi
|
* = py.typed, *.pyi
|
||||||
|
|||||||
+32
-8
@@ -21,6 +21,7 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
|
from bumble.core import BT_BR_EDR_TRANSPORT
|
||||||
from bumble.link import LocalLink
|
from bumble.link import LocalLink
|
||||||
from bumble.device import Device
|
from bumble.device import Device
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
@@ -58,18 +59,19 @@ class TwoDevices:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.connections = [None, None]
|
self.connections = [None, None]
|
||||||
|
|
||||||
|
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
|
||||||
self.link = LocalLink()
|
self.link = LocalLink()
|
||||||
self.controllers = [
|
self.controllers = [
|
||||||
Controller('C1', link=self.link),
|
Controller('C1', link=self.link, public_address=addresses[0]),
|
||||||
Controller('C2', link=self.link),
|
Controller('C2', link=self.link, public_address=addresses[1]),
|
||||||
]
|
]
|
||||||
self.devices = [
|
self.devices = [
|
||||||
Device(
|
Device(
|
||||||
address='F0:F1:F2:F3:F4:F5',
|
address=addresses[0],
|
||||||
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
|
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
|
||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
address='F5:F4:F3:F2:F1:F0',
|
address=addresses[1],
|
||||||
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
|
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -79,6 +81,9 @@ class TwoDevices:
|
|||||||
def on_connection(self, which, connection):
|
def on_connection(self, which, connection):
|
||||||
self.connections[which] = connection
|
self.connections[which] = connection
|
||||||
|
|
||||||
|
def on_paired(self, which, keys):
|
||||||
|
self.paired[which] = keys
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -94,12 +99,21 @@ async def test_self_connection():
|
|||||||
'connection', lambda connection: two_devices.on_connection(1, connection)
|
'connection', lambda connection: two_devices.on_connection(1, connection)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Enable Classic connections
|
||||||
|
two_devices.devices[0].classic_enabled = True
|
||||||
|
two_devices.devices[1].classic_enabled = True
|
||||||
|
|
||||||
# Start
|
# Start
|
||||||
await two_devices.devices[0].power_on()
|
await two_devices.devices[0].power_on()
|
||||||
await two_devices.devices[1].power_on()
|
await two_devices.devices[1].power_on()
|
||||||
|
|
||||||
# Connect the two devices
|
# Connect the two devices
|
||||||
await two_devices.devices[0].connect(two_devices.devices[1].random_address)
|
await asyncio.gather(
|
||||||
|
two_devices.devices[0].connect(
|
||||||
|
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||||
|
),
|
||||||
|
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||||
|
)
|
||||||
|
|
||||||
# Check the post conditions
|
# Check the post conditions
|
||||||
assert two_devices.connections[0] is not None
|
assert two_devices.connections[0] is not None
|
||||||
@@ -152,6 +166,9 @@ def sink_codec_capabilities():
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_source_sink_1():
|
async def test_source_sink_1():
|
||||||
two_devices = TwoDevices()
|
two_devices = TwoDevices()
|
||||||
|
# Enable Classic connections
|
||||||
|
two_devices.devices[0].classic_enabled = True
|
||||||
|
two_devices.devices[1].classic_enabled = True
|
||||||
await two_devices.devices[0].power_on()
|
await two_devices.devices[0].power_on()
|
||||||
await two_devices.devices[1].power_on()
|
await two_devices.devices[1].power_on()
|
||||||
|
|
||||||
@@ -171,9 +188,16 @@ async def test_source_sink_1():
|
|||||||
listener = Listener(Listener.create_registrar(two_devices.devices[1]))
|
listener = Listener(Listener.create_registrar(two_devices.devices[1]))
|
||||||
listener.on('connection', on_avdtp_connection)
|
listener.on('connection', on_avdtp_connection)
|
||||||
|
|
||||||
connection = await two_devices.devices[0].connect(
|
async def make_connection():
|
||||||
two_devices.devices[1].random_address
|
connections = await asyncio.gather(
|
||||||
)
|
two_devices.devices[0].connect(
|
||||||
|
two_devices.devices[1].public_address, BT_BR_EDR_TRANSPORT
|
||||||
|
),
|
||||||
|
two_devices.devices[1].accept(two_devices.devices[0].public_address),
|
||||||
|
)
|
||||||
|
return connections[0]
|
||||||
|
|
||||||
|
connection = await make_connection()
|
||||||
client = await Protocol.connect(connection)
|
client = await Protocol.connect(connection)
|
||||||
endpoints = await client.discover_remote_endpoints()
|
endpoints = await client.discover_remote_endpoints()
|
||||||
assert len(endpoints) == 1
|
assert len(endpoints) == 1
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 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 hashlib
|
||||||
|
import os
|
||||||
|
from bumble.decoder import G722Decoder
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_decode_file():
|
||||||
|
decoder = G722Decoder()
|
||||||
|
output_bytes = bytearray()
|
||||||
|
|
||||||
|
with open(
|
||||||
|
os.path.join(os.path.dirname(__file__), 'g722_sample.g722'), 'rb'
|
||||||
|
) as file:
|
||||||
|
file_content = file.read()
|
||||||
|
frame_length = 80
|
||||||
|
data_length = int(len(file_content) / frame_length)
|
||||||
|
|
||||||
|
for i in range(0, data_length):
|
||||||
|
decoded_data = decoder.decode_frame(
|
||||||
|
file_content[i * frame_length : i * frame_length + frame_length]
|
||||||
|
)
|
||||||
|
output_bytes.extend(decoded_data)
|
||||||
|
|
||||||
|
result = hashlib.md5(output_bytes).hexdigest()
|
||||||
|
assert result == 'b58e0cdd012d12f5633fc796c3b0fbd4'
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test_decode_file()
|
||||||
File diff suppressed because one or more lines are too long
+188
-30
@@ -23,6 +23,7 @@ import pytest
|
|||||||
|
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
from bumble.gatt_client import CharacteristicProxy
|
from bumble.gatt_client import CharacteristicProxy
|
||||||
|
from bumble.gatt_server import Server
|
||||||
from bumble.link import LocalLink
|
from bumble.link import LocalLink
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
@@ -37,10 +38,12 @@ from bumble.gatt import (
|
|||||||
Service,
|
Service,
|
||||||
Characteristic,
|
Characteristic,
|
||||||
CharacteristicValue,
|
CharacteristicValue,
|
||||||
|
Descriptor,
|
||||||
)
|
)
|
||||||
from bumble.transport import AsyncPipeSink
|
from bumble.transport import AsyncPipeSink
|
||||||
from bumble.core import UUID
|
from bumble.core import UUID
|
||||||
from bumble.att import (
|
from bumble.att import (
|
||||||
|
Attribute,
|
||||||
ATT_EXCHANGE_MTU_REQUEST,
|
ATT_EXCHANGE_MTU_REQUEST,
|
||||||
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
ATT_ATTRIBUTE_NOT_FOUND_ERROR,
|
||||||
ATT_PDU,
|
ATT_PDU,
|
||||||
@@ -112,7 +115,7 @@ async def test_characteristic_encoding():
|
|||||||
|
|
||||||
c = Foo(
|
c = Foo(
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
123,
|
123,
|
||||||
)
|
)
|
||||||
@@ -141,7 +144,9 @@ async def test_characteristic_encoding():
|
|||||||
|
|
||||||
characteristic = Characteristic(
|
characteristic = Characteristic(
|
||||||
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([123]),
|
bytes([123]),
|
||||||
)
|
)
|
||||||
@@ -237,7 +242,9 @@ async def test_attribute_getters():
|
|||||||
characteristic_uuid = UUID('FDB159DB-036C-49E3-B3DB-6325AC750806')
|
characteristic_uuid = UUID('FDB159DB-036C-49E3-B3DB-6325AC750806')
|
||||||
characteristic = Characteristic(
|
characteristic = Characteristic(
|
||||||
characteristic_uuid,
|
characteristic_uuid,
|
||||||
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([123]),
|
bytes([123]),
|
||||||
)
|
)
|
||||||
@@ -282,7 +289,7 @@ def test_CharacteristicAdapter():
|
|||||||
v = bytes([1, 2, 3])
|
v = bytes([1, 2, 3])
|
||||||
c = Characteristic(
|
c = Characteristic(
|
||||||
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
GATT_BATTERY_LEVEL_CHARACTERISTIC,
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
v,
|
v,
|
||||||
)
|
)
|
||||||
@@ -418,7 +425,7 @@ async def test_read_write():
|
|||||||
|
|
||||||
characteristic1 = Characteristic(
|
characteristic1 = Characteristic(
|
||||||
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -435,7 +442,7 @@ async def test_read_write():
|
|||||||
|
|
||||||
characteristic2 = Characteristic(
|
characteristic2 = Characteristic(
|
||||||
'66DE9057-C848-4ACA-B993-D675644EBB85',
|
'66DE9057-C848-4ACA-B993-D675644EBB85',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
CharacteristicValue(
|
CharacteristicValue(
|
||||||
read=on_characteristic2_read, write=on_characteristic2_write
|
read=on_characteristic2_read, write=on_characteristic2_write
|
||||||
@@ -498,7 +505,7 @@ async def test_read_write2():
|
|||||||
v = bytes([0x11, 0x22, 0x33, 0x44])
|
v = bytes([0x11, 0x22, 0x33, 0x44])
|
||||||
characteristic1 = Characteristic(
|
characteristic1 = Characteristic(
|
||||||
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
value=v,
|
value=v,
|
||||||
)
|
)
|
||||||
@@ -542,7 +549,7 @@ async def test_subscribe_notify():
|
|||||||
|
|
||||||
characteristic1 = Characteristic(
|
characteristic1 = Characteristic(
|
||||||
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
Characteristic.READ | Characteristic.NOTIFY,
|
Characteristic.Properties.READ | Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([1, 2, 3]),
|
bytes([1, 2, 3]),
|
||||||
)
|
)
|
||||||
@@ -558,7 +565,7 @@ async def test_subscribe_notify():
|
|||||||
|
|
||||||
characteristic2 = Characteristic(
|
characteristic2 = Characteristic(
|
||||||
'66DE9057-C848-4ACA-B993-D675644EBB85',
|
'66DE9057-C848-4ACA-B993-D675644EBB85',
|
||||||
Characteristic.READ | Characteristic.INDICATE,
|
Characteristic.Properties.READ | Characteristic.Properties.INDICATE,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([4, 5, 6]),
|
bytes([4, 5, 6]),
|
||||||
)
|
)
|
||||||
@@ -574,7 +581,9 @@ async def test_subscribe_notify():
|
|||||||
|
|
||||||
characteristic3 = Characteristic(
|
characteristic3 = Characteristic(
|
||||||
'AB5E639C-40C1-4238-B9CB-AF41F8B806E4',
|
'AB5E639C-40C1-4238-B9CB-AF41F8B806E4',
|
||||||
Characteristic.READ | Characteristic.NOTIFY | Characteristic.INDICATE,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.NOTIFY
|
||||||
|
| Characteristic.Properties.INDICATE,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([7, 8, 9]),
|
bytes([7, 8, 9]),
|
||||||
)
|
)
|
||||||
@@ -794,32 +803,46 @@ async def test_mtu_exchange():
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_char_property_to_string():
|
def test_char_property_to_string():
|
||||||
# single
|
# single
|
||||||
assert Characteristic.property_name(0x01) == "BROADCAST"
|
assert str(Characteristic.Properties(0x01)) == "Properties.BROADCAST"
|
||||||
assert Characteristic.property_name(Characteristic.BROADCAST) == "BROADCAST"
|
assert str(Characteristic.Properties.BROADCAST) == "Properties.BROADCAST"
|
||||||
|
|
||||||
# double
|
# double
|
||||||
assert Characteristic.properties_as_string(0x03) == "BROADCAST,READ"
|
assert str(Characteristic.Properties(0x03)) == "Properties.READ|BROADCAST"
|
||||||
assert (
|
assert (
|
||||||
Characteristic.properties_as_string(
|
str(Characteristic.Properties.BROADCAST | Characteristic.Properties.READ)
|
||||||
Characteristic.BROADCAST | Characteristic.READ
|
== "Properties.READ|BROADCAST"
|
||||||
)
|
|
||||||
== "BROADCAST,READ"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
def test_char_property_string_to_type():
|
def test_characteristic_property_from_string():
|
||||||
# single
|
# single
|
||||||
assert Characteristic.string_to_properties("BROADCAST") == Characteristic.BROADCAST
|
assert (
|
||||||
|
Characteristic.Properties.from_string("BROADCAST")
|
||||||
|
== Characteristic.Properties.BROADCAST
|
||||||
|
)
|
||||||
|
|
||||||
# double
|
# double
|
||||||
assert (
|
assert (
|
||||||
Characteristic.string_to_properties("BROADCAST,READ")
|
Characteristic.Properties.from_string("BROADCAST,READ")
|
||||||
== Characteristic.BROADCAST | Characteristic.READ
|
== Characteristic.Properties.BROADCAST | Characteristic.Properties.READ
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
Characteristic.string_to_properties("READ,BROADCAST")
|
Characteristic.Properties.from_string("READ,BROADCAST")
|
||||||
== Characteristic.BROADCAST | Characteristic.READ
|
== Characteristic.Properties.BROADCAST | Characteristic.Properties.READ
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_characteristic_property_from_string_assert():
|
||||||
|
with pytest.raises(TypeError) as e_info:
|
||||||
|
Characteristic.Properties.from_string("BROADCAST,HELLO")
|
||||||
|
|
||||||
|
assert (
|
||||||
|
str(e_info.value)
|
||||||
|
== """Characteristic.Properties::from_string() error:
|
||||||
|
Expected a string containing any of the keys, separated by commas: BROADCAST,READ,WRITE_WITHOUT_RESPONSE,WRITE,NOTIFY,INDICATE,AUTHENTICATED_SIGNED_WRITES,EXTENDED_PROPERTIES
|
||||||
|
Got: BROADCAST,HELLO"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -830,7 +853,9 @@ async def test_server_string():
|
|||||||
|
|
||||||
characteristic = Characteristic(
|
characteristic = Characteristic(
|
||||||
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.NOTIFY,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([123]),
|
bytes([123]),
|
||||||
)
|
)
|
||||||
@@ -841,13 +866,13 @@ async def test_server_string():
|
|||||||
assert (
|
assert (
|
||||||
str(server.gatt_server)
|
str(server.gatt_server)
|
||||||
== """Service(handle=0x0001, end=0x0005, uuid=UUID-16:1800 (Generic Access))
|
== """Service(handle=0x0001, end=0x0005, uuid=UUID-16:1800 (Generic Access))
|
||||||
CharacteristicDeclaration(handle=0x0002, value_handle=0x0003, uuid=UUID-16:2A00 (Device Name), properties=READ)
|
CharacteristicDeclaration(handle=0x0002, value_handle=0x0003, uuid=UUID-16:2A00 (Device Name), Properties.READ)
|
||||||
Characteristic(handle=0x0003, end=0x0003, uuid=UUID-16:2A00 (Device Name), properties=READ)
|
Characteristic(handle=0x0003, end=0x0003, uuid=UUID-16:2A00 (Device Name), Properties.READ)
|
||||||
CharacteristicDeclaration(handle=0x0004, value_handle=0x0005, uuid=UUID-16:2A01 (Appearance), properties=READ)
|
CharacteristicDeclaration(handle=0x0004, value_handle=0x0005, uuid=UUID-16:2A01 (Appearance), Properties.READ)
|
||||||
Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), properties=READ)
|
Characteristic(handle=0x0005, end=0x0005, uuid=UUID-16:2A01 (Appearance), Properties.READ)
|
||||||
Service(handle=0x0006, end=0x0009, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
|
Service(handle=0x0006, end=0x0009, uuid=3A657F47-D34F-46B3-B1EC-698E29B6B829)
|
||||||
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, properties=READ,WRITE,NOTIFY)
|
CharacteristicDeclaration(handle=0x0007, value_handle=0x0008, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, Properties.NOTIFY|WRITE|READ)
|
||||||
Characteristic(handle=0x0008, end=0x0009, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, properties=READ,WRITE,NOTIFY)
|
Characteristic(handle=0x0008, end=0x0009, uuid=FDB159DB-036C-49E3-B3DB-6325AC750806, Properties.NOTIFY|WRITE|READ)
|
||||||
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)"""
|
Descriptor(handle=0x0009, type=UUID-16:2902 (Client Characteristic Configuration), value=0000)"""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -861,6 +886,139 @@ async def async_main():
|
|||||||
await test_mtu_exchange()
|
await test_mtu_exchange()
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_attribute_string_to_permissions():
|
||||||
|
assert Attribute.string_to_permissions('READABLE') == 1
|
||||||
|
assert Attribute.string_to_permissions('WRITEABLE') == 2
|
||||||
|
assert Attribute.string_to_permissions('READABLE,WRITEABLE') == 3
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_characteristic_permissions():
|
||||||
|
characteristic = Characteristic(
|
||||||
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.NOTIFY,
|
||||||
|
'READABLE,WRITEABLE',
|
||||||
|
)
|
||||||
|
assert characteristic.permissions == 3
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_characteristic_has_properties():
|
||||||
|
characteristic = Characteristic(
|
||||||
|
'FDB159DB-036C-49E3-B3DB-6325AC750806',
|
||||||
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.NOTIFY,
|
||||||
|
'READABLE,WRITEABLE',
|
||||||
|
)
|
||||||
|
assert characteristic.has_properties(Characteristic.Properties.READ)
|
||||||
|
assert characteristic.has_properties(
|
||||||
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE
|
||||||
|
)
|
||||||
|
assert not characteristic.has_properties(
|
||||||
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE
|
||||||
|
| Characteristic.Properties.INDICATE
|
||||||
|
)
|
||||||
|
assert not characteristic.has_properties(Characteristic.Properties.INDICATE)
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_descriptor_permissions():
|
||||||
|
descriptor = Descriptor('2902', 'READABLE,WRITEABLE')
|
||||||
|
assert descriptor.permissions == 3
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_get_attribute_group():
|
||||||
|
device = Device()
|
||||||
|
|
||||||
|
# add some services / characteristics to the gatt server
|
||||||
|
characteristic1 = Characteristic(
|
||||||
|
'1111',
|
||||||
|
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
|
||||||
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
|
bytes([123]),
|
||||||
|
)
|
||||||
|
characteristic2 = Characteristic(
|
||||||
|
'2222',
|
||||||
|
Characteristic.READ | Characteristic.WRITE | Characteristic.NOTIFY,
|
||||||
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
|
bytes([123]),
|
||||||
|
)
|
||||||
|
services = [Service('1212', [characteristic1]), Service('3233', [characteristic2])]
|
||||||
|
device.gatt_server.add_services(services)
|
||||||
|
|
||||||
|
# get the handles from gatt server
|
||||||
|
characteristic_attributes1 = device.gatt_server.get_characteristic_attributes(
|
||||||
|
UUID('1212'), UUID('1111')
|
||||||
|
)
|
||||||
|
assert characteristic_attributes1 is not None
|
||||||
|
characteristic_attributes2 = device.gatt_server.get_characteristic_attributes(
|
||||||
|
UUID('3233'), UUID('2222')
|
||||||
|
)
|
||||||
|
assert characteristic_attributes2 is not None
|
||||||
|
descriptor1 = device.gatt_server.get_descriptor_attribute(
|
||||||
|
UUID('1212'), UUID('1111'), UUID('2902')
|
||||||
|
)
|
||||||
|
assert descriptor1 is not None
|
||||||
|
descriptor2 = device.gatt_server.get_descriptor_attribute(
|
||||||
|
UUID('3233'), UUID('2222'), UUID('2902')
|
||||||
|
)
|
||||||
|
assert descriptor2 is not None
|
||||||
|
|
||||||
|
# confirm the handles map back to the service
|
||||||
|
assert (
|
||||||
|
UUID('1212')
|
||||||
|
== device.gatt_server.get_attribute_group(
|
||||||
|
characteristic_attributes1[0].handle, Service
|
||||||
|
).uuid
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
UUID('1212')
|
||||||
|
== device.gatt_server.get_attribute_group(
|
||||||
|
characteristic_attributes1[1].handle, Service
|
||||||
|
).uuid
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
UUID('1212')
|
||||||
|
== device.gatt_server.get_attribute_group(descriptor1.handle, Service).uuid
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
UUID('3233')
|
||||||
|
== device.gatt_server.get_attribute_group(
|
||||||
|
characteristic_attributes2[0].handle, Service
|
||||||
|
).uuid
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
UUID('3233')
|
||||||
|
== device.gatt_server.get_attribute_group(
|
||||||
|
characteristic_attributes2[1].handle, Service
|
||||||
|
).uuid
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
UUID('3233')
|
||||||
|
== device.gatt_server.get_attribute_group(descriptor2.handle, Service).uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
# confirm the handles map back to the characteristic
|
||||||
|
assert (
|
||||||
|
UUID('1111')
|
||||||
|
== device.gatt_server.get_attribute_group(
|
||||||
|
descriptor1.handle, Characteristic
|
||||||
|
).uuid
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
UUID('2222')
|
||||||
|
== device.gatt_server.get_attribute_group(
|
||||||
|
descriptor2.handle, Characteristic
|
||||||
|
).uuid
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
logging.basicConfig(level=os.environ.get('BUMBLE_LOGLEVEL', 'INFO').upper())
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ from bumble.hci import (
|
|||||||
HCI_LE_Set_Scan_Parameters_Command,
|
HCI_LE_Set_Scan_Parameters_Command,
|
||||||
HCI_Number_Of_Completed_Packets_Event,
|
HCI_Number_Of_Completed_Packets_Event,
|
||||||
HCI_Packet,
|
HCI_Packet,
|
||||||
|
HCI_PIN_Code_Request_Reply_Command,
|
||||||
HCI_Read_Local_Supported_Commands_Command,
|
HCI_Read_Local_Supported_Commands_Command,
|
||||||
HCI_Read_Local_Supported_Features_Command,
|
HCI_Read_Local_Supported_Features_Command,
|
||||||
HCI_Read_Local_Version_Information_Command,
|
HCI_Read_Local_Version_Information_Command,
|
||||||
@@ -213,6 +214,23 @@ def test_HCI_Command():
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
def test_HCI_PIN_Code_Request_Reply_Command():
|
||||||
|
pin_code = b'1234'
|
||||||
|
pin_code_length = len(pin_code)
|
||||||
|
# here to make the test pass, we need to
|
||||||
|
# pad pin_code, as HCI_Object.format_fields
|
||||||
|
# does not do it for us
|
||||||
|
padded_pin_code = pin_code + bytes(16 - pin_code_length)
|
||||||
|
command = HCI_PIN_Code_Request_Reply_Command(
|
||||||
|
bd_addr=Address(
|
||||||
|
'00:11:22:33:44:55', address_type=Address.PUBLIC_DEVICE_ADDRESS
|
||||||
|
),
|
||||||
|
pin_code_length=pin_code_length,
|
||||||
|
pin_code=padded_pin_code,
|
||||||
|
)
|
||||||
|
basic_check(command)
|
||||||
|
|
||||||
|
|
||||||
def test_HCI_Reset_Command():
|
def test_HCI_Reset_Command():
|
||||||
command = HCI_Reset_Command()
|
command = HCI_Reset_Command()
|
||||||
basic_check(command)
|
basic_check(command)
|
||||||
@@ -440,6 +458,7 @@ def run_test_events():
|
|||||||
def run_test_commands():
|
def run_test_commands():
|
||||||
test_HCI_Command()
|
test_HCI_Command()
|
||||||
test_HCI_Reset_Command()
|
test_HCI_Reset_Command()
|
||||||
|
test_HCI_PIN_Code_Request_Reply_Command()
|
||||||
test_HCI_Read_Local_Version_Information_Command()
|
test_HCI_Read_Local_Version_Information_Command()
|
||||||
test_HCI_Read_Local_Supported_Commands_Command()
|
test_HCI_Read_Local_Supported_Commands_Command()
|
||||||
test_HCI_Read_Local_Supported_Features_Command()
|
test_HCI_Read_Local_Supported_Features_Command()
|
||||||
|
|||||||
+85
-22
@@ -22,14 +22,14 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bumble.controller import Controller
|
from bumble.controller import Controller
|
||||||
|
from bumble.core import BT_BR_EDR_TRANSPORT, BT_PERIPHERAL_ROLE, BT_CENTRAL_ROLE
|
||||||
from bumble.link import LocalLink
|
from bumble.link import LocalLink
|
||||||
from bumble.device import Device, Peer
|
from bumble.device import Device, Peer
|
||||||
from bumble.host import Host
|
from bumble.host import Host
|
||||||
from bumble.gatt import Service, Characteristic
|
from bumble.gatt import Service, Characteristic
|
||||||
from bumble.transport import AsyncPipeSink
|
from bumble.transport import AsyncPipeSink
|
||||||
|
from bumble.pairing import PairingConfig, PairingDelegate
|
||||||
from bumble.smp import (
|
from bumble.smp import (
|
||||||
PairingConfig,
|
|
||||||
PairingDelegate,
|
|
||||||
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
SMP_PAIRING_NOT_SUPPORTED_ERROR,
|
||||||
SMP_CONFIRM_VALUE_FAILED_ERROR,
|
SMP_CONFIRM_VALUE_FAILED_ERROR,
|
||||||
)
|
)
|
||||||
@@ -47,18 +47,19 @@ class TwoDevices:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.connections = [None, None]
|
self.connections = [None, None]
|
||||||
|
|
||||||
|
addresses = ['F0:F1:F2:F3:F4:F5', 'F5:F4:F3:F2:F1:F0']
|
||||||
self.link = LocalLink()
|
self.link = LocalLink()
|
||||||
self.controllers = [
|
self.controllers = [
|
||||||
Controller('C1', link=self.link),
|
Controller('C1', link=self.link, public_address=addresses[0]),
|
||||||
Controller('C2', link=self.link),
|
Controller('C2', link=self.link, public_address=addresses[1]),
|
||||||
]
|
]
|
||||||
self.devices = [
|
self.devices = [
|
||||||
Device(
|
Device(
|
||||||
address='F0:F1:F2:F3:F4:F5',
|
address=addresses[0],
|
||||||
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
|
host=Host(self.controllers[0], AsyncPipeSink(self.controllers[0])),
|
||||||
),
|
),
|
||||||
Device(
|
Device(
|
||||||
address='F5:F4:F3:F2:F1:F0',
|
address=addresses[1],
|
||||||
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
|
host=Host(self.controllers[1], AsyncPipeSink(self.controllers[1])),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@@ -98,6 +99,60 @@ async def test_self_connection():
|
|||||||
assert two_devices.connections[1] is not None
|
assert two_devices.connections[1] is not None
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'responder_role,',
|
||||||
|
(BT_CENTRAL_ROLE, BT_PERIPHERAL_ROLE),
|
||||||
|
)
|
||||||
|
async def test_self_classic_connection(responder_role):
|
||||||
|
# Create two devices, each with a controller, attached to the same link
|
||||||
|
two_devices = TwoDevices()
|
||||||
|
|
||||||
|
# Attach listeners
|
||||||
|
two_devices.devices[0].on(
|
||||||
|
'connection', lambda connection: two_devices.on_connection(0, connection)
|
||||||
|
)
|
||||||
|
two_devices.devices[1].on(
|
||||||
|
'connection', lambda connection: two_devices.on_connection(1, connection)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Enable Classic connections
|
||||||
|
two_devices.devices[0].classic_enabled = True
|
||||||
|
two_devices.devices[1].classic_enabled = True
|
||||||
|
|
||||||
|
# Start
|
||||||
|
await two_devices.devices[0].power_on()
|
||||||
|
await two_devices.devices[1].power_on()
|
||||||
|
|
||||||
|
# Connect the two devices
|
||||||
|
await asyncio.gather(
|
||||||
|
two_devices.devices[0].connect(
|
||||||
|
two_devices.devices[1].public_address, transport=BT_BR_EDR_TRANSPORT
|
||||||
|
),
|
||||||
|
two_devices.devices[1].accept(
|
||||||
|
two_devices.devices[0].public_address, responder_role
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check the post conditions
|
||||||
|
assert two_devices.connections[0] is not None
|
||||||
|
assert two_devices.connections[1] is not None
|
||||||
|
|
||||||
|
# Check the role
|
||||||
|
assert two_devices.connections[0].role != responder_role
|
||||||
|
assert two_devices.connections[1].role == responder_role
|
||||||
|
|
||||||
|
# Role switch
|
||||||
|
await two_devices.connections[0].switch_role(responder_role)
|
||||||
|
|
||||||
|
# Check the role
|
||||||
|
assert two_devices.connections[0].role == responder_role
|
||||||
|
assert two_devices.connections[1].role != responder_role
|
||||||
|
|
||||||
|
await two_devices.connections[0].disconnect()
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_self_gatt():
|
async def test_self_gatt():
|
||||||
@@ -107,25 +162,28 @@ async def test_self_gatt():
|
|||||||
# Add some GATT characteristics to device 1
|
# Add some GATT characteristics to device 1
|
||||||
c1 = Characteristic(
|
c1 = Characteristic(
|
||||||
'3A143AD7-D4A7-436B-97D6-5B62C315E833',
|
'3A143AD7-D4A7-436B-97D6-5B62C315E833',
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([1, 2, 3]),
|
bytes([1, 2, 3]),
|
||||||
)
|
)
|
||||||
c2 = Characteristic(
|
c2 = Characteristic(
|
||||||
'9557CCE2-DB37-46EB-94C4-50AE5B9CB0F8',
|
'9557CCE2-DB37-46EB-94C4-50AE5B9CB0F8',
|
||||||
Characteristic.READ | Characteristic.WRITE,
|
Characteristic.Properties.READ | Characteristic.Properties.WRITE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([4, 5, 6]),
|
bytes([4, 5, 6]),
|
||||||
)
|
)
|
||||||
c3 = Characteristic(
|
c3 = Characteristic(
|
||||||
'84FC1A2E-C52D-4A2D-B8C3-8855BAB86638',
|
'84FC1A2E-C52D-4A2D-B8C3-8855BAB86638',
|
||||||
Characteristic.READ | Characteristic.WRITE_WITHOUT_RESPONSE,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.WRITE_WITHOUT_RESPONSE,
|
||||||
Characteristic.READABLE | Characteristic.WRITEABLE,
|
Characteristic.READABLE | Characteristic.WRITEABLE,
|
||||||
bytes([7, 8, 9]),
|
bytes([7, 8, 9]),
|
||||||
)
|
)
|
||||||
c4 = Characteristic(
|
c4 = Characteristic(
|
||||||
'84FC1A2E-C52D-4A2D-B8C3-8855BAB86638',
|
'84FC1A2E-C52D-4A2D-B8C3-8855BAB86638',
|
||||||
Characteristic.READ | Characteristic.NOTIFY | Characteristic.INDICATE,
|
Characteristic.Properties.READ
|
||||||
|
| Characteristic.Properties.NOTIFY
|
||||||
|
| Characteristic.Properties.INDICATE,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([1, 1, 1]),
|
bytes([1, 1, 1]),
|
||||||
)
|
)
|
||||||
@@ -178,7 +236,7 @@ async def test_self_gatt_long_read():
|
|||||||
characteristics = [
|
characteristics = [
|
||||||
Characteristic(
|
Characteristic(
|
||||||
f'3A143AD7-D4A7-436B-97D6-5B62C315{i:04X}',
|
f'3A143AD7-D4A7-436B-97D6-5B62C315{i:04X}',
|
||||||
Characteristic.READ,
|
Characteristic.Properties.READ,
|
||||||
Characteristic.READABLE,
|
Characteristic.READABLE,
|
||||||
bytes([x & 255 for x in range(i)]),
|
bytes([x & 255 for x in range(i)]),
|
||||||
)
|
)
|
||||||
@@ -203,7 +261,7 @@ async def test_self_gatt_long_read():
|
|||||||
found_service = result[0]
|
found_service = result[0]
|
||||||
found_characteristics = await found_service.discover_characteristics()
|
found_characteristics = await found_service.discover_characteristics()
|
||||||
assert len(found_characteristics) == 513
|
assert len(found_characteristics) == 513
|
||||||
for (i, characteristic) in enumerate(found_characteristics):
|
for i, characteristic in enumerate(found_characteristics):
|
||||||
value = await characteristic.read_value()
|
value = await characteristic.read_value()
|
||||||
assert value == characteristics[i].value
|
assert value == characteristics[i].value
|
||||||
|
|
||||||
@@ -258,11 +316,11 @@ async def _test_self_smp_with_configs(pairing_config1, pairing_config2):
|
|||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
IO_CAP = [
|
IO_CAP = [
|
||||||
PairingDelegate.NO_OUTPUT_NO_INPUT,
|
PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT,
|
||||||
PairingDelegate.KEYBOARD_INPUT_ONLY,
|
PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY,
|
||||||
PairingDelegate.DISPLAY_OUTPUT_ONLY,
|
PairingDelegate.IoCapability.DISPLAY_OUTPUT_ONLY,
|
||||||
PairingDelegate.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_YES_NO_INPUT,
|
||||||
PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
|
PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT,
|
||||||
]
|
]
|
||||||
SC = [False, True]
|
SC = [False, True]
|
||||||
MITM = [False, True]
|
MITM = [False, True]
|
||||||
@@ -276,7 +334,10 @@ KEY_DIST = range(16)
|
|||||||
itertools.chain(
|
itertools.chain(
|
||||||
itertools.product([IO_CAP], SC, MITM, [15]),
|
itertools.product([IO_CAP], SC, MITM, [15]),
|
||||||
itertools.product(
|
itertools.product(
|
||||||
[[PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT]], SC, MITM, KEY_DIST
|
[[PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT]],
|
||||||
|
SC,
|
||||||
|
MITM,
|
||||||
|
KEY_DIST,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -319,7 +380,7 @@ async def test_self_smp(io_caps, sc, mitm, key_dist):
|
|||||||
else:
|
else:
|
||||||
if (
|
if (
|
||||||
self.peer_delegate.io_capability
|
self.peer_delegate.io_capability
|
||||||
== PairingDelegate.KEYBOARD_INPUT_ONLY
|
== PairingDelegate.IoCapability.KEYBOARD_INPUT_ONLY
|
||||||
):
|
):
|
||||||
peer_number = 6789
|
peer_number = 6789
|
||||||
else:
|
else:
|
||||||
@@ -362,7 +423,7 @@ async def test_self_smp(io_caps, sc, mitm, key_dist):
|
|||||||
async def test_self_smp_reject():
|
async def test_self_smp_reject():
|
||||||
class RejectingDelegate(PairingDelegate):
|
class RejectingDelegate(PairingDelegate):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(PairingDelegate.NO_OUTPUT_NO_INPUT)
|
super().__init__(PairingDelegate.IoCapability.NO_OUTPUT_NO_INPUT)
|
||||||
|
|
||||||
async def accept(self):
|
async def accept(self):
|
||||||
return False
|
return False
|
||||||
@@ -383,12 +444,14 @@ async def test_self_smp_reject():
|
|||||||
async def test_self_smp_wrong_pin():
|
async def test_self_smp_wrong_pin():
|
||||||
class WrongPinDelegate(PairingDelegate):
|
class WrongPinDelegate(PairingDelegate):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(PairingDelegate.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT)
|
super().__init__(
|
||||||
|
PairingDelegate.IoCapability.DISPLAY_OUTPUT_AND_KEYBOARD_INPUT
|
||||||
|
)
|
||||||
|
|
||||||
async def compare_numbers(self, number, digits):
|
async def compare_numbers(self, number, digits):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
wrong_pin_pairing_config = PairingConfig(delegate=WrongPinDelegate())
|
wrong_pin_pairing_config = PairingConfig(mitm=True, delegate=WrongPinDelegate())
|
||||||
paired = False
|
paired = False
|
||||||
try:
|
try:
|
||||||
await _test_self_smp_with_configs(
|
await _test_self_smp_with_configs(
|
||||||
|
|||||||
Reference in New Issue
Block a user